From 259d4a3794933720c1206fc92d7e7e3c913cf0bc Mon Sep 17 00:00:00 2001 From: bridge Date: Fri, 21 Nov 2025 23:58:36 +0800 Subject: [PATCH] update pack --- debug_client.py | 13 --- requirements.txt | 1 - src/classes/animal.py | 19 ++-- src/classes/auxiliary.py | 25 +++-- src/classes/celestial_phenomenon.py | 22 ++--- src/classes/effect.py | 8 +- src/classes/item.py | 15 ++- src/classes/name.py | 17 ++-- src/classes/persona.py | 32 +++---- src/classes/plant.py | 19 ++-- src/classes/region.py | 37 +++---- src/classes/sect.py | 66 +++++++------ src/classes/technique.py | 30 +++--- src/classes/weapon.py | 31 +++--- src/run/create_map.py | 12 +-- src/server/main.py | 55 +++++++---- src/utils/df.py | 143 ++++++++++++++++++++++++---- static/config.yml | 2 +- tools/package/pack.ps1 | 9 +- 19 files changed, 316 insertions(+), 240 deletions(-) delete mode 100644 debug_client.py diff --git a/debug_client.py b/debug_client.py deleted file mode 100644 index 46e2e76..0000000 --- a/debug_client.py +++ /dev/null @@ -1,13 +0,0 @@ -import urllib.request -import json - -try: - with urllib.request.urlopen("http://localhost:8000/api/state") as response: - print(f"Status: {response.status}") - print(response.read().decode('utf-8')) -except urllib.error.HTTPError as e: - print(f"HTTP Error: {e.code}") - print(e.read().decode('utf-8')) -except Exception as e: - print(f"Error: {e}") - diff --git a/requirements.txt b/requirements.txt index 0d18c75..105ce85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ PyYAML>=6.0 litellm>=1.0.0 omegaconf>=2.3.0 json5>=0.9.0 -pandas>=2.0.0 fastapi>=0.100.0 uvicorn>=0.20.0 websockets>=11.0 diff --git a/src/classes/animal.py b/src/classes/animal.py index bf5c713..c3bde47 100644 --- a/src/classes/animal.py +++ b/src/classes/animal.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field from typing import Optional -from src.utils.df import game_configs +from src.utils.df import game_configs, get_str, get_int, get_list_int from src.utils.config import CONFIG from src.classes.item import Item, items_by_id from src.classes.cultivation import Realm @@ -50,19 +50,14 @@ def _load_animals() -> tuple[dict[int, Animal], dict[str, Animal]]: animals_by_name: dict[str, Animal] = {} animal_df = game_configs["animal"] - for _, row in animal_df.iterrows(): - # 处理item_ids - item_ids_list = [] - item_ids = row.get("item_ids") - if item_ids is not None and str(item_ids).strip() and str(item_ids) != 'nan': - for item_id_str in str(item_ids).split(CONFIG.df.ids_separator): - item_ids_list.append(int(float(item_id_str.strip()))) + for row in animal_df: + item_ids_list = get_list_int(row, "item_ids") animal = Animal( - id=int(row["id"]), - name=str(row["name"]), - desc=str(row["desc"]), - realm=Realm.from_id(int(row["stage_id"])), + id=get_int(row, "id"), + name=get_str(row, "name"), + desc=get_str(row, "desc"), + realm=Realm.from_id(get_int(row, "stage_id")), item_ids=item_ids_list ) animals_by_id[animal.id] = animal diff --git a/src/classes/auxiliary.py b/src/classes/auxiliary.py index c56d747..f796335 100644 --- a/src/classes/auxiliary.py +++ b/src/classes/auxiliary.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field from typing import Optional, Dict -from src.utils.df import game_configs +from src.utils.df import game_configs, get_str, get_int from src.classes.effect import load_effect_from_str from src.classes.equipment_grade import EquipmentGrade from src.classes.sect import Sect, sects_by_id @@ -55,19 +55,17 @@ def _load_auxiliaries() -> tuple[Dict[int, Auxiliary], Dict[str, Auxiliary], Dic if df is None: return auxiliaries_by_id, auxiliaries_by_name, auxiliaries_by_sect_id - for _, row in df.iterrows(): - raw_sect = row.get("sect_id") - sect_id: Optional[int] = None - if raw_sect is not None and str(raw_sect).strip() and str(raw_sect).strip() != "nan": - sect_id = int(float(raw_sect)) + for row in df: + sect_id = get_int(row, "sect_id", -1) + if sect_id == -1: + sect_id = None - raw_effects_val = row.get("effects", "") - effects = load_effect_from_str(raw_effects_val) + effects = load_effect_from_str(get_str(row, "effects")) - sect_obj: Optional[Sect] = sects_by_id.get(int(sect_id)) if sect_id is not None else None + sect_obj: Optional[Sect] = sects_by_id.get(sect_id) if sect_id is not None else None # 解析grade - grade_str = str(row.get("grade", "普通")) + grade_str = get_str(row, "grade", "普通") grade = EquipmentGrade.COMMON for g in EquipmentGrade: if g.value == grade_str: @@ -75,11 +73,11 @@ def _load_auxiliaries() -> tuple[Dict[int, Auxiliary], Dict[str, Auxiliary], Dic break a = Auxiliary( - id=int(row["id"]), - name=str(row["name"]), + id=get_int(row, "id"), + name=get_str(row, "name"), grade=grade, sect_id=sect_id, - desc=str(row.get("desc", "")), + desc=get_str(row, "desc"), effects=effects, sect=sect_obj, ) @@ -93,4 +91,3 @@ def _load_auxiliaries() -> tuple[Dict[int, Auxiliary], Dict[str, Auxiliary], Dic auxiliaries_by_id, auxiliaries_by_name, auxiliaries_by_sect_id = _load_auxiliaries() - diff --git a/src/classes/celestial_phenomenon.py b/src/classes/celestial_phenomenon.py index d6bdf61..19a7f9a 100644 --- a/src/classes/celestial_phenomenon.py +++ b/src/classes/celestial_phenomenon.py @@ -22,7 +22,7 @@ import random from dataclasses import dataclass from typing import Optional -from src.utils.df import game_configs +from src.utils.df import game_configs, get_str, get_int from src.classes.effect import load_effect_from_str from src.classes.rarity import Rarity, get_rarity_from_str @@ -69,26 +69,21 @@ def _load_celestial_phenomena() -> dict[int, CelestialPhenomenon]: return phenomena_by_id phenomenon_df = game_configs["celestial_phenomenon"] - for _, row in phenomenon_df.iterrows(): + for row in phenomenon_df: # 解析稀有度 - rarity_val = row.get("rarity", "N") - rarity_str = str(rarity_val).strip().upper() + rarity_str = get_str(row, "rarity", "N").upper() rarity = get_rarity_from_str(rarity_str) if rarity_str and rarity_str != "NAN" else get_rarity_from_str("N") # 解析effects - raw_effects_val = row.get("effects", "") - effects = load_effect_from_str(raw_effects_val) - - # 解析持续年限(默认5年) - duration_years = int(row.get("duration_years", 5)) + effects = load_effect_from_str(get_str(row, "effects")) phenomenon = CelestialPhenomenon( - id=int(row["id"]), - name=str(row["name"]), + id=get_int(row, "id"), + name=get_str(row, "name"), rarity=rarity, effects=effects, - desc=str(row["desc"]), - duration_years=duration_years, + desc=get_str(row, "desc"), + duration_years=get_int(row, "duration_years", 5), ) phenomena_by_id[phenomenon.id] = phenomenon @@ -113,4 +108,3 @@ def get_random_celestial_phenomenon() -> Optional[CelestialPhenomenon]: weights = [p.weight for p in phenomena] return random.choices(phenomena, weights=weights, k=1)[0] - diff --git a/src/classes/effect.py b/src/classes/effect.py index 97e895a..1296e78 100644 --- a/src/classes/effect.py +++ b/src/classes/effect.py @@ -188,21 +188,21 @@ def _merge_effects(base: dict[str, object], addition: dict[str, object]) -> dict def build_effects_map_from_df( - df, + data: list[dict[str, Any]], key_column: str, parse_key: Callable[[str], Any], effects_column: str = "effects", ) -> dict[Any, dict[str, object]]: """ - 将配表 DataFrame 构造成 {key -> effects} 的映射: + 将配表数据列表构造成 {key -> effects} 的映射: - key_column:用于定位键(字符串),通过 parse_key 解析为目标键(如 Enum) - effects_column:字符串列,使用 load_effect_from_str 解析 解析失败或空值的行将被忽略。 """ effects_map: dict[Any, dict[str, object]] = {} - if df is None: + if not data: return effects_map - for _, row in df.iterrows(): + for row in data: raw_key = str(row.get(key_column, "")).strip() if not raw_key or raw_key == "nan": continue diff --git a/src/classes/item.py b/src/classes/item.py index ff4e1e7..4762e41 100644 --- a/src/classes/item.py +++ b/src/classes/item.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from src.utils.df import game_configs +from src.utils.df import game_configs, get_str, get_int from src.classes.cultivation import Realm @dataclass @@ -31,12 +31,12 @@ def _load_items() -> tuple[dict[int, Item], dict[str, Item]]: items_by_name: dict[str, Item] = {} item_df = game_configs["item"] - for _, row in item_df.iterrows(): + for row in item_df: item = Item( - id=int(row["id"]), - name=str(row["name"]), - desc=str(row["desc"]), - realm=Realm.from_id(int(row["stage_id"])) + id=get_int(row, "id"), + name=get_str(row, "name"), + desc=get_str(row, "desc"), + realm=Realm.from_id(get_int(row, "stage_id")) ) items_by_id[item.id] = item items_by_name[item.name] = item @@ -45,6 +45,3 @@ def _load_items() -> tuple[dict[int, Item], dict[str, Item]]: # 从配表加载item数据 items_by_id, items_by_name = _load_items() - - - diff --git a/src/classes/name.py b/src/classes/name.py index f59f742..ead3b15 100644 --- a/src/classes/name.py +++ b/src/classes/name.py @@ -2,7 +2,7 @@ import random from typing import Optional from dataclasses import dataclass -from src.utils.df import game_configs +from src.utils.df import game_configs, get_str from src.classes.avatar import Gender @@ -43,9 +43,9 @@ class NameManager: """从CSV加载姓名数据""" # 加载姓氏 last_name_df = game_configs["last_name"] - for _, row in last_name_df.iterrows(): - name = str(row["last_name"]).strip() - sect = str(row.get("sect", "")).strip() if row.get("sect") and str(row.get("sect")) != "nan" else None + for row in last_name_df: + name = get_str(row, "last_name") + sect = get_str(row, "sect") if sect: if sect not in self.sect_last_names: @@ -56,11 +56,11 @@ class NameManager: # 加载名字 given_name_df = game_configs["given_name"] - for _, row in given_name_df.iterrows(): - name = str(row["given_name"]).strip() - gender_str = str(row["gender"]).strip() + for row in given_name_df: + name = get_str(row, "given_name") + gender_str = get_str(row, "gender") gender = Gender.MALE if gender_str == "男" else Gender.FEMALE - sect = str(row.get("sect", "")).strip() if row.get("sect") and str(row.get("sect")) != "nan" else None + sect = get_str(row, "sect") if sect: if sect not in self.sect_given_names: @@ -194,4 +194,3 @@ def get_random_name_with_surname( """ sect_name = sect.name if sect is not None else None return _name_manager.get_random_full_name_with_surname(gender, surname, sect_name) - diff --git a/src/classes/persona.py b/src/classes/persona.py index c097f86..8dd4af1 100644 --- a/src/classes/persona.py +++ b/src/classes/persona.py @@ -2,13 +2,11 @@ import random from dataclasses import dataclass from typing import List, Optional, TYPE_CHECKING -from src.utils.df import game_configs +from src.utils.df import game_configs, get_str, get_list_str, get_int from src.utils.config import CONFIG from src.classes.effect import load_effect_from_str from src.classes.rarity import Rarity, get_rarity_from_str -ids_separator = CONFIG.df.ids_separator - if TYPE_CHECKING: # 仅用于类型检查,避免运行时循环导入 from src.classes.avatar import Avatar @@ -49,27 +47,24 @@ def _load_personas() -> tuple[dict[int, Persona], dict[str, Persona]]: personas_by_name: dict[str, Persona] = {} persona_df = game_configs["persona"] - for _, row in persona_df.iterrows(): + for row in persona_df: # 解析exclusion_names字符串,转换为字符串列表 - exclusion_names_str = str(row["exclusion_names"]) if str(row["exclusion_names"]) != "nan" else "" - exclusion_names = [] - if exclusion_names_str: - exclusion_names = [x.strip() for x in exclusion_names_str.split(ids_separator) if x.strip()] + exclusion_names = get_list_str(row, "exclusion_names") + # 解析稀有度(缺失或为 NaN 时默认为 N) - rarity_val = row.get("rarity", "N") - rarity_str = str(rarity_val).strip().upper() + rarity_str = get_str(row, "rarity", "N").upper() rarity = get_rarity_from_str(rarity_str) if rarity_str and rarity_str != "NAN" else get_rarity_from_str("N") - # 条件:可为空 - condition_val = row.get("condition", "") - condition = "" if str(condition_val) == "nan" else str(condition_val).strip() + + # 条件 + condition = get_str(row, "condition") + # 解析effects - raw_effects_val = row.get("effects", "") - effects = load_effect_from_str(raw_effects_val) + effects = load_effect_from_str(get_str(row, "effects")) persona = Persona( - id=int(row["id"]), - name=str(row["name"]), - desc=str(row["desc"]), + id=get_int(row, "id"), + name=get_str(row, "name"), + desc=get_str(row, "desc"), exclusion_names=exclusion_names, rarity=rarity, condition=condition, @@ -138,4 +133,3 @@ def get_random_compatible_personas(num_personas: int = 2, avatar: Optional["Avat selected_ids.add(selected_persona.id) return selected_personas - diff --git a/src/classes/plant.py b/src/classes/plant.py index e403a9d..929a573 100644 --- a/src/classes/plant.py +++ b/src/classes/plant.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field from typing import Optional -from src.utils.df import game_configs +from src.utils.df import game_configs, get_str, get_int, get_list_int from src.utils.config import CONFIG from src.classes.item import Item, items_by_id from src.classes.cultivation import Realm @@ -50,19 +50,14 @@ def _load_plants() -> tuple[dict[int, Plant], dict[str, Plant]]: plants_by_name: dict[str, Plant] = {} plant_df = game_configs["plant"] - for _, row in plant_df.iterrows(): - # 处理item_ids - item_ids_list = [] - item_ids = row.get("item_ids") - if item_ids is not None and str(item_ids).strip() and str(item_ids) != 'nan': - for item_id_str in str(item_ids).split(CONFIG.df.ids_separator): - item_ids_list.append(int(float(item_id_str.strip()))) + for row in plant_df: + item_ids_list = get_list_int(row, "item_ids") plant = Plant( - id=int(row["id"]), - name=str(row["name"]), - desc=str(row["desc"]), - realm=Realm.from_id(int(row["stage_id"])), + id=get_int(row, "id"), + name=get_str(row, "name"), + desc=get_str(row, "desc"), + realm=Realm.from_id(get_int(row, "stage_id")), item_ids=item_ids_list ) plants_by_id[plant.id] = plant diff --git a/src/classes/region.py b/src/classes/region.py index d1665f5..175d062 100644 --- a/src/classes/region.py +++ b/src/classes/region.py @@ -3,7 +3,7 @@ from typing import Union, TypeVar, Type, Optional from enum import Enum from abc import ABC, abstractmethod -from src.utils.df import game_configs +from src.utils.df import game_configs, get_str, get_int, get_list_int from src.utils.config import CONFIG from src.classes.essence import EssenceType, Essence from src.classes.animal import Animal, animals_by_id @@ -453,39 +453,26 @@ def _load_regions(region_type: Type[T], config_name: str) -> tuple[dict[int, T], regions_by_name: dict[str, T] = {} region_df = game_configs[config_name] - for _, row in region_df.iterrows(): + for row in region_df: # 构建基础参数 base_params = { - "id": int(row["id"]), - "name": str(row["name"]), - "desc": str(row["desc"]), - "shape": Shape.from_str(str(row["shape"])), - "north_west_cor": str(row["north-west-cor"]), - "south_east_cor": str(row["south-east-cor"]) + "id": get_int(row, "id"), + "name": get_str(row, "name"), + "desc": get_str(row, "desc"), + "shape": Shape.from_str(get_str(row, "shape")), + "north_west_cor": get_str(row, "north-west-cor"), + "south_east_cor": get_str(row, "south-east-cor") } # 如果是修炼区域,添加额外参数 if region_type == CultivateRegion: - base_params["essence_type"] = EssenceType.from_str(str(row["root_type"])) - base_params["essence_density"] = int(row["root_density"]) + base_params["essence_type"] = EssenceType.from_str(get_str(row, "root_type")) + base_params["essence_density"] = get_int(row, "root_density") # 如果是普通区域,添加动植物ID参数 elif region_type == NormalRegion: - # 处理动物IDs - animal_ids_list = [] - animal_ids = row.get("animal_ids") - if animal_ids is not None and str(animal_ids).strip() and str(animal_ids) != 'nan': - for animal_id_str in str(animal_ids).split(CONFIG.df.ids_separator): - animal_ids_list.append(int(float(animal_id_str.strip()))) - base_params["animal_ids"] = animal_ids_list - - # 处理植物IDs - plant_ids_list = [] - plant_ids = row.get("plant_ids") - if plant_ids is not None and str(plant_ids).strip() and str(plant_ids) != 'nan': - for plant_id_str in str(plant_ids).split(CONFIG.df.ids_separator): - plant_ids_list.append(int(float(plant_id_str.strip()))) - base_params["plant_ids"] = plant_ids_list + base_params["animal_ids"] = get_list_int(row, "animal_ids") + base_params["plant_ids"] = get_list_int(row, "plant_ids") region = region_type(**base_params) regions_by_id[region.id] = region diff --git a/src/classes/sect.py b/src/classes/sect.py index 32b1e89..f3c50e9 100644 --- a/src/classes/sect.py +++ b/src/classes/sect.py @@ -3,7 +3,7 @@ from pathlib import Path import json from src.classes.alignment import Alignment -from src.utils.df import game_configs +from src.utils.df import game_configs, get_str, get_float, get_int from src.classes.effect import load_effect_from_str from src.utils.config import CONFIG @@ -68,6 +68,7 @@ class Sect: from src.classes.sect_ranks import SectRank, DEFAULT_RANK_NAMES # 优先使用自定义名称,否则使用默认名称 return self.rank_names.get(rank.value, DEFAULT_RANK_NAMES.get(rank, "弟子")) + def _split_names(value: object) -> list[str]: raw = "" if value is None or str(value) == "nan" else str(value) sep = CONFIG.df.ids_separator @@ -85,56 +86,59 @@ def _load_sects() -> tuple[dict[int, Sect], dict[str, Sect]]: sect_region_df = game_configs.get("sect_region") hq_by_sect_id: dict[int, tuple[str, str]] = {} if sect_region_df is not None: - for _, sr in sect_region_df.iterrows(): - sid_str = str(sr.get("sect_id", "")).strip() - # 跳过说明行或空值 - if not sid_str.isdigit(): + for sr in sect_region_df: + sid = get_int(sr, "sect_id", -1) + if sid == -1: continue - sid = int(sid_str) - hq_name = str(sr.get("headquarter_name", "")).strip() - hq_desc = str(sr.get("headquarter_desc", "")).strip() + hq_name = get_str(sr, "headquarter_name") + hq_desc = get_str(sr, "headquarter_desc") hq_by_sect_id[sid] = (hq_name, hq_desc) + # 可能不存在 technique 配表或未添加 sect 列,做容错 tech_df = game_configs.get("technique") assets_base = Path("assets/sects") - for _, row in df.iterrows(): - image_path = assets_base / f"{row['name']}.png" + + for row in df: + name = get_str(row, "name") + image_path = assets_base / f"{name}.png" # 收集该宗门下配置的功法名称 technique_names: list[str] = [] - if tech_df is not None and "sect" in tech_df.columns: + # 检查 tech_df 是否存在以及是否有数据 + if tech_df: + # 检查是否存在 sect 字段 (检查第一行或当前行) technique_names = [ - str(tname).strip() - for tname in tech_df.loc[tech_df["sect"] == row["name"], "name"].tolist() - if str(tname).strip() + get_str(t, "name") + for t in tech_df + if get_str(t, "sect") == name and get_str(t, "name") ] - # 读取权重(缺省/NaN 则为 1.0) - weight_val = row.get("weight", 1) - weight = float(str(weight_val)) if str(weight_val) != "nan" else 1.0 + weight = get_float(row, "weight", 1.0) - # 读取 effects(兼容 JSON/单引号字面量/空) - effects = load_effect_from_str(row.get("effects", "")) + # 读取 effects + effects = load_effect_from_str(get_str(row, "effects")) # 读取倾向兵器类型 - preferred_weapon_val = row.get("preferred_weapon", "") - preferred_weapon = str(preferred_weapon_val).strip() if str(preferred_weapon_val) != "nan" else "" + preferred_weapon = get_str(row, "preferred_weapon") # 从 sect_region.csv 中优先取驻地名称/描述;否则兼容旧列或退回宗门名 - csv_hq = hq_by_sect_id.get(int(row["id"])) + sid = get_int(row, "id") + csv_hq = hq_by_sect_id.get(sid) hq_name_from_csv = (csv_hq[0] if csv_hq else "").strip() if csv_hq else "" hq_desc_from_csv = (csv_hq[1] if csv_hq else "").strip() if csv_hq else "" + hq_name = hq_name_from_csv or get_str(row, "headquarter_name") or name + hq_desc = hq_desc_from_csv or get_str(row, "headquarter_desc") + sect = Sect( - id=int(row["id"]), - name=str(row["name"]), - desc=str(row["desc"]), - member_act_style=str(row["member_act_style"]), - alignment=Alignment.from_str(row["alignment"]), - # 驻地:优先 sect_region.csv;否则兼容旧列;最终回退宗门名 + id=sid, + name=name, + desc=get_str(row, "desc"), + member_act_style=get_str(row, "member_act_style"), + alignment=Alignment.from_str(get_str(row, "alignment")), headquarter=SectHeadQuarter( - name=(hq_name_from_csv or str(row.get("headquarter_name", "")).strip() or str(row["name"])) , - desc=(hq_desc_from_csv or str(row.get("headquarter_desc", ""))), + name=hq_name, + desc=hq_desc, image=image_path, ), technique_names=technique_names, @@ -189,4 +193,4 @@ def get_sect_info_with_rank(avatar: "Avatar", detailed: bool = False) -> str: return f"{sect_rank_str}{detail_part}" # 如果没有括号(理论上不应该出现),直接返回职位字符串 - return sect_rank_str \ No newline at end of file + return sect_rank_str diff --git a/src/classes/technique.py b/src/classes/technique.py index 079f448..8f81a38 100644 --- a/src/classes/technique.py +++ b/src/classes/technique.py @@ -7,7 +7,7 @@ import json from src.classes.effect import load_effect_from_str from src.classes.color import Color, TECHNIQUE_GRADE_COLORS -from src.utils.df import game_configs +from src.utils.df import game_configs, get_str, get_float, get_int from src.classes.alignment import Alignment from src.classes.root import Root, RootElement @@ -104,25 +104,25 @@ def loads() -> tuple[dict[int, Technique], dict[str, Technique]]: techniques_by_id: dict[int, Technique] = {} techniques_by_name: dict[str, Technique] = {} df = game_configs["technique"] - for _, row in df.iterrows(): - attr = TechniqueAttribute(str(row["technique_root"]).strip()) - name = str(row["name"]).strip() - grade = TechniqueGrade.from_str(row.get("grade", "下品")) - cond_val = row.get("condition", "") - condition = "" if str(cond_val) == "nan" else str(cond_val).strip() - weight_val = row.get("weight", 1) - weight = float(str(weight_val)) if str(weight_val) != "nan" else 1.0 - sect_val = row.get("sect", "") - sect = None if str(sect_val) == "nan" or str(sect_val).strip() == "" else str(sect_val).strip() - # 读取 effects(兼容 JSON/单引号字面量/空) - effects = load_effect_from_str(row.get("effects", "")) + for row in df: + attr = TechniqueAttribute(get_str(row, "technique_root")) + name = get_str(row, "name") + grade = TechniqueGrade.from_str(get_str(row, "grade", "下品")) + condition = get_str(row, "condition") + weight = get_float(row, "weight", 1.0) + + sect = get_str(row, "sect") + if not sect: + sect = None + + effects = load_effect_from_str(get_str(row, "effects")) t = Technique( - id=int(row["id"]), + id=get_int(row, "id"), name=name, attribute=attr, grade=grade, - desc=str(row.get("desc", "")), + desc=get_str(row, "desc"), weight=weight, condition=condition, sect=sect, diff --git a/src/classes/weapon.py b/src/classes/weapon.py index 65cf611..1b3631b 100644 --- a/src/classes/weapon.py +++ b/src/classes/weapon.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field from typing import Optional, Dict -from src.utils.df import game_configs +from src.utils.df import game_configs, get_str, get_int from src.classes.effect import load_effect_from_str from src.classes.equipment_grade import EquipmentGrade from src.classes.weapon_type import WeaponType @@ -65,19 +65,17 @@ def _load_weapons() -> tuple[Dict[int, Weapon], Dict[str, Weapon], Dict[int, Wea if df is None: return weapons_by_id, weapons_by_name, weapons_by_sect_id - for _, row in df.iterrows(): - raw_sect = row.get("sect_id") - sect_id: Optional[int] = None - if raw_sect is not None and str(raw_sect).strip() and str(raw_sect).strip() != "nan": - sect_id = int(float(raw_sect)) + for row in df: + sect_id = get_int(row, "sect_id", -1) + if sect_id == -1: + sect_id = None - raw_effects_val = row.get("effects", "") - effects = load_effect_from_str(raw_effects_val) + effects = load_effect_from_str(get_str(row, "effects")) - sect_obj: Optional[Sect] = sects_by_id.get(int(sect_id)) if sect_id is not None else None + sect_obj: Optional[Sect] = sects_by_id.get(sect_id) if sect_id is not None else None # 解析weapon_type - weapon_type_str = str(row.get("weapon_type", "")) + weapon_type_str = get_str(row, "weapon_type") weapon_type = None for wt in WeaponType: if wt.value == weapon_type_str: @@ -85,10 +83,12 @@ def _load_weapons() -> tuple[Dict[int, Weapon], Dict[str, Weapon], Dict[int, Wea break if weapon_type is None: - raise ValueError(f"武器 {row['name']} 的weapon_type '{weapon_type_str}' 无效,必须是有效的兵器类型") + # 如果找不到对应类型,可以决定是跳过还是抛错 + # 这里保持原有逻辑,如果是空或者非法,抛错提示配置问题 + raise ValueError(f"武器 {get_str(row, 'name')} 的weapon_type '{weapon_type_str}' 无效,必须是有效的兵器类型") # 解析grade - grade_str = str(row.get("grade", "普通")) + grade_str = get_str(row, "grade", "普通") grade = EquipmentGrade.COMMON for g in EquipmentGrade: if g.value == grade_str: @@ -96,12 +96,12 @@ def _load_weapons() -> tuple[Dict[int, Weapon], Dict[str, Weapon], Dict[int, Wea break w = Weapon( - id=int(row["id"]), - name=str(row["name"]), + id=get_int(row, "id"), + name=get_str(row, "name"), weapon_type=weapon_type, grade=grade, sect_id=sect_id, - desc=str(row.get("desc", "")), + desc=get_str(row, "desc"), effects=effects, sect=sect_obj, ) @@ -130,4 +130,3 @@ def get_treasure_weapon(weapon_type: WeaponType) -> Optional[Weapon]: if weapon.weapon_type == weapon_type and weapon.grade == EquipmentGrade.TREASURE: return weapon return None - diff --git a/src/run/create_map.py b/src/run/create_map.py index 6f7c2b2..0175e39 100644 --- a/src/run/create_map.py +++ b/src/run/create_map.py @@ -4,7 +4,7 @@ from src.classes.essence import Essence, EssenceType from src.classes.sect_region import SectRegion from src.classes.region import Shape from src.classes.sect import Sect -from src.utils.df import game_configs +from src.utils.df import game_configs, get_str, get_int BASE_W = 70 BASE_H = 50 @@ -73,12 +73,12 @@ def add_sect_headquarters(game_map: Map, enabled_sects: list[Sect]): # 从 sect_region.csv 读取(按 sect_id 对齐):sect_name、headquarter_name、headquarter_desc sect_region_df = game_configs["sect_region"] hq_by_id: dict[int, tuple[str, str, str]] = { - int(row["sect_id"]): ( - str(row["sect_name"]).strip(), - str(row["headquarter_name"]).strip(), - str(row["headquarter_desc"]).strip(), + get_int(row, "sect_id"): ( + get_str(row, "sect_name"), + get_str(row, "headquarter_name"), + get_str(row, "headquarter_desc"), ) - for _, row in sect_region_df.iterrows() + for row in sect_region_df } # 坐标字典按 sect.name 提供,转换为按 sect.id 对齐 id_to_coords: dict[int, tuple[tuple[int, int], tuple[int, int]]] = { diff --git a/src/server/main.py b/src/server/main.py index 50ef1a6..266d326 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -199,32 +199,33 @@ app.add_middleware( # 路径处理:兼容开发环境和 PyInstaller 打包环境 if getattr(sys, 'frozen', False): # PyInstaller 打包模式 - base_path = sys._MEIPASS - # 在 pack.ps1 中,我们把 web/dist 映射到了 web_dist - WEB_DIST_PATH = os.path.join(base_path, 'web_dist') - # assets 同理 - ASSETS_PATH = os.path.join(base_path, 'assets') + # 1. 获取 EXE 所在目录 (外部目录) + exe_dir = os.path.dirname(sys.executable) + + # 2. 寻找外部的 web_static + WEB_DIST_PATH = os.path.join(exe_dir, 'web_static') + + # 3. Assets 依然在 _internal 里 (因为我们在 pack.ps1 里用了 --add-data) + # 注意:ASSETS_PATH 仍然指向 _internal/assets + ASSETS_PATH = os.path.join(sys._MEIPASS, 'assets') else: # 开发模式 base_path = os.path.join(os.path.dirname(__file__), '..', '..') WEB_DIST_PATH = os.path.join(base_path, 'web', 'dist') ASSETS_PATH = os.path.join(base_path, 'assets') -# 1. 挂载游戏资源 (图片等) -if os.path.exists(ASSETS_PATH): - app.mount("/assets", StaticFiles(directory=ASSETS_PATH), name="assets") -else: - print(f"Warning: Assets path not found: {ASSETS_PATH}") +# 规范化路径 +WEB_DIST_PATH = os.path.abspath(WEB_DIST_PATH) +ASSETS_PATH = os.path.abspath(ASSETS_PATH) -# 2. 挂载前端静态页面 (Web Dist) -if os.path.exists(WEB_DIST_PATH): - print(f"Serving Web UI from: {WEB_DIST_PATH}") - app.mount("/", StaticFiles(directory=WEB_DIST_PATH, html=True), name="web_dist") -else: - print(f"Warning: Web dist path not found: {WEB_DIST_PATH}. Please run 'npm run build' in web directory.") - - @app.get("/") - def read_root(): +print(f"Runtime mode: {'Frozen/Packaged' if getattr(sys, 'frozen', False) else 'Development'}") +print(f"Assets path: {ASSETS_PATH}") +print(f"Web dist path: {WEB_DIST_PATH}") + +# (静态文件挂载已移动到文件末尾,以避免覆盖 API 路由) + +@app.get("/") +def read_root(): return {"status": "online", "app": "Cultivation World Simulator Backend (Headless / Dev Mode)"} @app.websocket("/ws") @@ -552,6 +553,22 @@ def api_load_game(req: LoadGameRequest): traceback.print_exc() raise HTTPException(status_code=500, detail=f"Load failed: {str(e)}") +# --- 静态文件挂载 (必须放在最后) --- + +# 1. 挂载游戏资源 (图片等) +if os.path.exists(ASSETS_PATH): + app.mount("/assets", StaticFiles(directory=ASSETS_PATH), name="assets") +else: + print(f"Warning: Assets path not found: {ASSETS_PATH}") + +# 2. 挂载前端静态页面 (Web Dist) +# 放在最后,因为 "/" 会匹配所有未定义的路由 +if os.path.exists(WEB_DIST_PATH): + print(f"Serving Web UI from: {WEB_DIST_PATH}") + app.mount("/", StaticFiles(directory=WEB_DIST_PATH, html=True), name="web_dist") +else: + print(f"Warning: Web dist path not found: {WEB_DIST_PATH}.") + def start(): """启动服务的入口函数""" # 改为 8002 端口 diff --git a/src/utils/df.py b/src/utils/df.py index edd5b87..d137d26 100644 --- a/src/utils/df.py +++ b/src/utils/df.py @@ -1,29 +1,134 @@ -import pandas as pd +import csv from pathlib import Path +from typing import Any, Dict, List, Optional from src.utils.config import CONFIG -def load_csv(path: Path) -> pd.DataFrame: - # 跳过第二行说明行,只读取标题行和实际数据行 - df = pd.read_csv(path, skiprows=[1]) - row_types = { - "id": int, - "name": str, - "description": str, - "desc": str, - "weight": float, - } - for column, dtype in row_types.items(): - if column in df.columns: - df[column] = df[column].astype(dtype) - return df +def load_csv(path: Path) -> List[Dict[str, Any]]: + data = [] + if not path.exists(): + return data -def load_game_configs() -> dict[str, pd.DataFrame]: + with open(path, "r", encoding="utf-8") as f: + lines = list(csv.reader(f)) + + if len(lines) < 1: + return data + + headers = [h.strip() for h in lines[0]] + # 去除BOM + if headers and headers[0].startswith('\ufeff'): + headers[0] = headers[0][1:] + + # 如果有第二行(说明行),则从第三行开始读取数据 + start_index = 2 if len(lines) > 1 else 1 + + # 预定义的类型转换规则 (部分核心字段) + type_converters = { + "id": int, + "weight": float, + "sect_id": int, + "stage_id": int, + "root_density": int, + "duration_years": int, + } + + for i in range(start_index, len(lines)): + row_values = lines[i] + if not row_values: + continue + + row_dict = {} + for h_idx, header in enumerate(headers): + if h_idx < len(row_values): + val = row_values[h_idx].strip() + + # 统一处理空值 + if not val or val.lower() == 'nan': + val = None + + # 类型转换 + elif header in type_converters: + try: + val = type_converters[header](val) + except (ValueError, TypeError): + pass # 转换失败保留原字符串 + + row_dict[header] = val + else: + row_dict[header] = None + + data.append(row_dict) + + return data + +def load_game_configs() -> dict[str, List[Dict[str, Any]]]: game_configs = {} for path in CONFIG.paths.game_configs.glob("*.csv"): - df = load_csv(path) - game_configs[path.stem] = df + data = load_csv(path) + game_configs[path.stem] = data return game_configs -game_configs = load_game_configs() \ No newline at end of file +game_configs = load_game_configs() + +# ============================================================================= +# 辅助函数:让业务层代码更简洁 +# ============================================================================= + +def get_str(row: Dict[str, Any], key: str, default: str = "") -> str: + val = row.get(key) + if val is None: + return default + return str(val).strip() + +def get_int(row: Dict[str, Any], key: str, default: int = 0) -> int: + val = row.get(key) + if val is None: + return default + try: + return int(float(val)) # 处理可能存在的浮点数字符串 "1.0" + except (ValueError, TypeError): + return default + +def get_float(row: Dict[str, Any], key: str, default: float = 0.0) -> float: + val = row.get(key) + if val is None: + return default + try: + return float(val) + except (ValueError, TypeError): + return default + +def get_bool(row: Dict[str, Any], key: str, default: bool = False) -> bool: + val = row.get(key) + if val is None: + return default + s = str(val).lower() + return s in ('true', '1', 'yes', 'y') + +def get_list_int(row: Dict[str, Any], key: str, separator: str = None) -> List[int]: + """解析整数列表,如 "1|2|3" """ + if separator is None: + separator = CONFIG.df.ids_separator + val = row.get(key) + if not val: + return [] + res = [] + for x in str(val).split(separator): + x = x.strip() + if x: + try: + res.append(int(float(x))) + except ValueError: + pass + return res + +def get_list_str(row: Dict[str, Any], key: str, separator: str = None) -> List[str]: + """解析字符串列表""" + if separator is None: + separator = CONFIG.df.ids_separator + val = row.get(key) + if not val: + return [] + return [x.strip() for x in str(val).split(separator) if x.strip()] diff --git a/static/config.yml b/static/config.yml index 0992042..f000acc 100644 --- a/static/config.yml +++ b/static/config.yml @@ -1,5 +1,5 @@ meta: - version: "1.0.3" + version: "1.0.5" llm: # 填入litellm支持的model name和key diff --git a/tools/package/pack.ps1 b/tools/package/pack.ps1 index 535581c..d8c8cd0 100644 --- a/tools/package/pack.ps1 +++ b/tools/package/pack.ps1 @@ -101,7 +101,7 @@ $argsList = @( # Data Files "--add-data", "${AssetsPath};assets", # Game Assets (Images) -> _internal/assets - "--add-data", "${WebDistDir};web_dist", # Web Frontend -> _internal/web_dist + # REMOVED: "--add-data", "${WebDistDir};web_dist", (We will copy this manually to outside) "--add-data", "${StaticPath};static", # Configs -> _internal/static (backup) # Excludes @@ -183,6 +183,13 @@ try { Write-Host "✓ Copied static to exe directory" -ForegroundColor Green } } + + # Copy Web Dist to exe directory (Manual copy instead of PyInstaller bundle) + if (Test-Path $WebDistDir) { + $DestWeb = Join-Path $ExeDir "web_static" + Copy-Item -Path $WebDistDir -Destination $DestWeb -Recurse -Force + Write-Host "✓ Copied web_dist to web_static in exe directory" -ForegroundColor Green + } } # Clean up build and spec directories (delete entire directories)