This commit is contained in:
JA
2026-09-03 15:37:54 +08:00
parent c18f9d5536
commit f5715cbcdf
+164 -39
View File
@@ -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
# 5. 图标目录 (递归搜索 atlas_* 子目录,只保留包含图片的叶子目录)
if self.resources_dir:
for root, dirs, files in os.walk(self.resources_dir):
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)
# 检查是否包含图片文件
has_img = any(f.lower().endswith((".png", ".jpg", ".jpeg")) for f in os.listdir(dp))
if has_img:
if any(f.lower().endswith((".png", ".jpg", ".jpeg"))
for f in os.listdir(dp)):
self.icon_dirs[d] = dp
break # 找到第一个 atlas_ui 即止
# 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)}条")