From f5715cbcdf327704973812b3e2fcd41638de3b47 Mon Sep 17 00:00:00 2001 From: JA <1323160571@qq.com> Date: Thu, 3 Sep 2026 15:37:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wenzijiaoben/py/universal_tool.py | 211 ++++++++++++++++++++++++------ 1 file changed, 168 insertions(+), 43 deletions(-) diff --git a/wenzijiaoben/py/universal_tool.py b/wenzijiaoben/py/universal_tool.py index 5e68119..62b102d 100644 --- a/wenzijiaoben/py/universal_tool.py +++ b/wenzijiaoben/py/universal_tool.py @@ -84,10 +84,34 @@ class ProjectScanner: elif entry == "work": self.work_dir = p - # 2. 项目名 - if self.config_dir: - base = os.path.basename(self.config_dir) - self.project_name = base.replace("_config", "") + # 1b. 配置目录兜底:部分项目(如 HM6)把配置表放在 P-XXX_game/config + # 而非独立的 *_config 目录。优先顶层 config 目录,其次游戏目录下的 config 子目录, + # 取其中确实含 xlsx/csv 的那个作为 config_dir。 + if not self.config_dir: + candidates = [] + top_config = os.path.join(self.root, "config") + if os.path.isdir(top_config): + candidates.append(top_config) + if self.game_dir: + gc = os.path.join(self.game_dir, "config") + if os.path.isdir(gc): + candidates.append(gc) + for c in candidates: + try: + has_table = any(f.endswith((".xlsx", ".csv")) for f in os.listdir(c)) + except OSError: + has_table = False + if has_table: + self.config_dir = c + break + + # 2. 项目名:优先从 *_config 目录名提取;否则从 *_game 目录名提取;再否则用 config 目录名 + if self.config_dir and os.path.basename(self.config_dir).endswith("_config"): + self.project_name = os.path.basename(self.config_dir).replace("_config", "") + elif self.game_dir and os.path.basename(self.game_dir).endswith("_game"): + self.project_name = os.path.basename(self.game_dir).replace("_game", "") + elif self.config_dir: + self.project_name = os.path.basename(self.config_dir) # 3. 数据表目录(csv_dir): work/cvs 优先(若存在),否则用 config_dir 本身(含xlsx) # find_file 优先查找 .xlsx,其次 .csv,两种格式都直接读取 @@ -98,49 +122,79 @@ class ProjectScanner: if not self.csv_dir: self.csv_dir = self.config_dir - # 4. 搜索根目录:优先 Resources_moved,其次 Resources + # 4&5. 图标目录: 在 game_dir 下递归搜索 atlas_ui 目录, + # 在其中找 atlas_* 子目录(只保留含图片的),resources_dir 记为 atlas_ui 的父目录。 + # 直接按 atlas_ui 定位可兼容 P-L12(unity/Assets/...) 与 L17(Assets/...) 任意层级, + # 避免误匹配同名但错误的 Resources_moved 目录。 if self.game_dir: for root, dirs, _ in os.walk(self.game_dir): - for name in ["Resources_moved", "Resources"]: - if name in dirs: - self.resources_dir = os.path.join(root, name) - break - if self.resources_dir: - break + if os.path.basename(root) == "atlas_ui": + self.resources_dir = os.path.dirname(root) + for d in dirs: + if d.startswith("atlas_"): + dp = os.path.join(root, d) + if any(f.lower().endswith((".png", ".jpg", ".jpeg")) + for f in os.listdir(dp)): + self.icon_dirs[d] = dp + break # 找到第一个 atlas_ui 即止 - # 5. 图标目录 (递归搜索 atlas_* 子目录,只保留包含图片的叶子目录) - if self.resources_dir: - for root, dirs, files in os.walk(self.resources_dir): - for d in dirs: - if d.startswith("atlas_"): - dp = os.path.join(root, d) - # 检查是否包含图片文件 - has_img = any(f.lower().endswith((".png", ".jpg", ".jpeg")) for f in os.listdir(dp)) - if has_img: - self.icon_dirs[d] = dp - - # 6. Scripts 目录 + # 6. Scripts 目录: 递归收集所有名为 Scripts 的目录,挑选"真实游戏脚本"目录。 + # 规则:优先"直接位于 Assets 目录下"的 Scripts(Unity 游戏源码根目录约定, + # 如 unity/Assets/Scripts 或 Assets/Scripts),排除 TutorialInfo/示例等非源码目录; + # 在其中再优先含游戏常量文件(GameError/GameText/Error/Text/Event/Msg Const)者; + # 无 Assets/Scripts 时回退到全部候选中"含游戏常量"或".cs 最多"的目录。 + # 同时跳过 Library(Unity 包缓存, 文件巨多且非游戏源码),避免误选并加速。 if self.game_dir: - unity = os.path.join(self.game_dir, "unity") - assets = os.path.join(unity, "Assets") if os.path.isdir(unity) else None - if assets: - scripts = os.path.join(assets, "Scripts") - if os.path.isdir(scripts): - self.scripts_dir = scripts + all_scripts = [] + for root, dirs, _ in os.walk(self.game_dir): + if "Library" in dirs: + dirs.remove("Library") # 不进入 Unity 包缓存 + if os.path.basename(root) == "Scripts": + all_scripts.append(root) - # 7. GameTextConst.cs + # 直接位于 Assets 下的 Scripts 目录(游戏源码根) + assets_scripts = [p for p in all_scripts + if os.path.basename(os.path.dirname(p)) == "Assets"] + candidates = assets_scripts if assets_scripts else all_scripts + + def _has_game_const(p): + # 递归查找游戏常量文件(常量常嵌套在子目录, 不能只看直接子文件) + try: + for r, _, files in os.walk(p): + for f in files: + if f.endswith(".cs") and "Const" in f and any( + k in f for k in ("GameError", "GameText", "Error", "Text", "Event", "Msg") + ): + return True + except OSError: + pass + return False + + def _score(p): + return 1000 if _has_game_const(p) else 0 + + if candidates: + self.scripts_dir = max(candidates, key=_score) + + # 7. 系统提示常量文件: 收集所有 *Const*.cs, + # 优先 ErrorConst/GameErrorConst/GameTextConst/TextConst,否则取第一个。 if self.scripts_dir: + const_files = [] for root, _, files in os.walk(self.scripts_dir): for f in files: - if f.lower().endswith(".cs") and "const" in f.lower() and ("text" in f.lower() or "game" in f.lower()): - self.game_text_const = os.path.join(root, f) - return - # 宽松匹配:任意 *Const*.cs - for root, _, files in os.walk(self.scripts_dir): - for f in files: - if "Const" in f and f.endswith(".cs"): - self.game_text_const = os.path.join(root, f) - return + if f.endswith(".cs") and "Const" in f: + const_files.append(os.path.join(root, f)) + if const_files: + def _pick(sub): + for p in const_files: + if sub in os.path.basename(p): + return p + return None + self.game_text_const = ( + _pick("GameErrorConst") or _pick("ErrorConst") + or _pick("GameTextConst") or _pick("TextConst") + or const_files[0] + ) def find_file(self, name): """在 csv_dir 或 config_dir 中查找文件(优先 .xlsx,其次 .csv)""" @@ -253,7 +307,7 @@ def find_image(icon_name, icon_dirs, icon_key): # ================================================================ -# 四、GameTextConst.cs 自动解析 +# 四、系统提示常量 / 事件文本解析 # ================================================================ def parse_game_text_const(cs_path, scripts_dir): @@ -308,15 +362,85 @@ def parse_game_text_const(cs_path, scripts_dir): continue # 去重文本值 + # 优先保留"被其他源码引用"的常量;若引用扫描未命中(项目引用方式不同,如 L17 把错误码集中管理), + # 则回退为使用常量文件中的全部 public const string,避免整章变空。 seen = set() result = [] - for name, text in all_consts.items(): - if name in referenced and text not in seen: + src_items = ((n, t) for n, t in all_consts.items() if n in referenced) if referenced else all_consts.items() + for name, text in src_items: + if text not in seen: seen.add(text) result.append(text) return result +def parse_event_system_tips(scripts_dir): + """ + 扫描 scripts_dir 下所有 .cs 文件,提取通过 event_system_tip 事件触发显示的 + 系统提示文本 —— 即形如 + TriggerEventGlobal(GameEventConst.event_system_tip, "文本") + 的第二个字符串参数(真正的弹窗提示文案)。 + 跳过 // 注释行。返回去重后的文本列表。 + + 说明:不少项目(如 L17)并未把系统提示集中写在 GameTextConst/GameErrorConst 里, + 而是散落在成百上千个 .cs 文件中的 event_system_tip 事件调用里, + GameEventConst 本身只继承 FGameEventConst 定义事件名常量, + 因此必须扫描调用现场才能拿到真实文案。 + """ + if not scripts_dir or not os.path.isdir(scripts_dir): + return [] + + # event_system_tip 之后(允许跨空白/换行)紧跟逗号与字符串字面量 + pattern = re.compile(r'event_system_tip\b[^;{]*?,\s*"((?:[^"\\]|\\.)*)"', re.DOTALL) + + seen = set() + result = [] + for root, _, files in os.walk(scripts_dir): + for fname in files: + if not fname.endswith(".cs"): + continue + try: + with open(os.path.join(root, fname), "r", encoding="utf-8", errors="ignore") as f: + lines = f.readlines() + except Exception: + continue + # 先逐行剔除 // 注释,再拼接扫描,避免误抓注释里的写法 + clean = [] + for line in lines: + idx = line.find("//") + if idx != -1: + line = line[:idx] + clean.append(line) + src = "\n".join(clean) + for m in pattern.finditer(src): + text = m.group(1) + # 还原转义 + text = text.replace("\\n", "\n").replace("\\t", "\t").replace('\\"', '"') + if text and text not in seen: + seen.add(text) + result.append(text) + return result + + +def get_system_prompts(scanner): + """ + 系统提示统一数据源:合并两类来源并去重: + 1) GameErrorConst / GameTextConst 等常量文件中的 public const string 值 + (parse_game_text_const, 含回退到全部常量)。 + 2) 散落在各 .cs 文件中的 event_system_tip 事件触发文本 + (parse_event_system_tips, 真正弹窗提示文案)。 + """ + const_prompts = parse_game_text_const(scanner.game_text_const, scanner.scripts_dir) + event_tips = parse_event_system_tips(scanner.scripts_dir) + seen = set() + combined = [] + for t in const_prompts + event_tips: + if t and t not in seen: + seen.add(t) + combined.append(t) + return combined + + # ================================================================ # 五、文档格式化辅助 # ================================================================ @@ -613,7 +737,8 @@ def build_document(scanner, output_path): print(f" 数据目录: {data_dir} (xlsx {xlsx_n} 个 / csv {csv_n} 个)") # 预加载数据 - system_prompts = parse_game_text_const(scanner.game_text_const, scanner.scripts_dir) + # 系统提示 = GameErrorConst/GameTextConst 常量值 合并 event_system_tip 触发文本 + system_prompts = get_system_prompts(scanner) player_names = get_player_names(scanner) print(f" 系统提示: {len(system_prompts)}条")