import argparse import json import os import re import sys from pathlib import Path sys.stdout.reconfigure(encoding='utf-8') sys.stderr.reconfigure(encoding='utf-8') PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent DEFAULT_CONFIG_DIR = PROJECT_ROOT / "Assets" / "Resources" / "Resources_moved" / "config" DEFAULT_OUTPUT = Path(__file__).resolve().parent / "ConfigLinkData.json" DEFAULT_TABLE_INFO = Path(__file__).resolve().parent / "table_info.json" def parse_args(): parser = argparse.ArgumentParser(description="ConfigLinkViewer 配置表联动关系自动生成") parser.add_argument("--config-dir", type=str, default=None, help="JSON 配置导出目录 (默认: Assets/Resources/Resources_moved/config)") parser.add_argument("--output", type=str, default=None, help="输出 ConfigLinkData.json 路径") parser.add_argument("--table-info", type=str, default=None, help="表信息配置 JSON 路径 (默认: table_info.json)") return parser.parse_args() def load_table_info(filepath): if filepath.exists(): with open(filepath, "r", encoding="utf-8") as f: data = json.load(f) entries = data.get("entries", []) return { entry["name"]: (entry.get("displayName", entry["name"]), entry.get("description", "")) for entry in entries } return {} TABLE_NAME_TO_CN = { "hero": "英雄", "enemy": "敌人", "equip": "装备", "prop": "道具", "skill": "技能", "buff": "增益效果", "attribute": "属性", "quest": "任务", "player": "玩家", "rune": "符文", "recharge": "充值", "checkpointreward": "关卡奖励", "activity": "活动", "map_act": "地图波次1", "map_act2": "地图波次2", "map_act3": "地图波次3", "map_act4": "地图波次4", "fight_sample": "主线关卡", "fight_arena": "竞技场战斗", "fight_fb1": "副本1", "fight_fb2": "副本2", "fight_fb3": "副本3", "fight_fb4": "副本4", "map": "地图", } def load_json_file(filepath): try: with open(filepath, "r", encoding="utf-8-sig") as f: return json.load(f) except UnicodeDecodeError: with open(filepath, "r", encoding="utf-8") as f: return json.load(f) except json.JSONDecodeError: return [] def extract_fields_from_json(filepath): data = load_json_file(filepath) if isinstance(data, list) and len(data) > 0: return list(data[0].keys()) elif isinstance(data, dict): return list(data.keys()) return [] def detect_reward_format(values): if not values: return "" sample = "" for v in values[:5]: if isinstance(v, str): sample = v break elif isinstance(v, list) and len(v) > 0: for item in v[:3]: if isinstance(item, str): sample = item break if sample: break if not sample: return "" if re.match(r"^item_\d+_\d+$", sample): return "item_id_num" if re.match(r"^hero_\d+$", sample): return "hero_id" if re.match(r"^rune_\d+_\d+$", sample): return "item_id_num" if re.match(r"^equip_\d+_\d+$", sample): return "item_id_num" if re.match(r"^Hero_\d+$", sample): return "item_id_num" if re.match(r"^\d+_\d+_\d+_\d+$", sample): return "id_lv_count_delay" if re.match(r"^\d+_\d+_\d+$", sample): return "id_lv_num" return "" def detect_consumes_format(values): if not values: return "" sample = "" for v in values[:5]: if isinstance(v, str): sample = v break elif isinstance(v, list) and len(v) > 0: for item in v[:3]: if isinstance(item, str): sample = item break if sample: break if not sample: return "" if re.match(r"^item_\d+_\d+$", sample): return "item_id_num" return "" def detect_monster_format(values): if not values: return "" sample = "" for v in values[:5]: if isinstance(v, str): sample = v break elif isinstance(v, list) and len(v) > 0: for item in v[:3]: if isinstance(item, str): sample = item break if sample: break if not sample: return "" if re.match(r"^\d+_\d+_\d+_\d+(\.\d+)?$", sample): return "id_lv_count_delay" if re.match(r"^\d+_\d+_\d+$", sample): return "id_lv_num" return "" def get_relation_for_target_id(table_name): mapping = { "herolevel": ("hero", "关联英雄ID"), "herostage": ("hero", "关联英雄ID"), "herolevelinternal": ("enemy", "关联敌人ID"), "enemylevel": ("enemy", "关联敌人ID"), "equiplevel": ("equip", "关联装备ID"), "equipstage": ("equip", "关联装备ID"), "playerlevel": ("player", "关联玩家ID"), "playerstage": ("player", "关联玩家ID"), "runelevel": ("rune", "关联符文ID"), "skilllevel": ("skill", "关联技能ID"), "buff": ("skill", "关联技能ID"), "map": ("map", "关联地图ID"), "map1": ("map", "关联地图ID"), "map2": ("map", "关联地图ID"), "map3": ("map", "关联地图ID"), "map_act": ("map", "关联地图ID"), "map_act2": ("map", "关联地图ID"), "map_act3": ("map", "关联地图ID"), "map_act4": ("map", "关联地图ID"), "test_map_act": ("map", "关联地图ID"), "trainbreak": ("prop", "关联训练突破ID"), "trainlevel": ("prop", "关联训练等级ID"), } if table_name in mapping: return mapping[table_name] return None def build_relations(table_name, fields, sample_values): relations = [] reward_fields = { "rewards", "rewards2", "pass_rewards", "activateRewards", "reward", "dailyQuestScoreRewards", "weeklyQuestScoreRewards", "achievementScoreRewards", "heroTujianRewards", } consume_fields = {"consumes", "lostConsumes"} monster_fields = {"monsters", "enemy_ids"} equip_fields = {"equips"} skill_fields = {"skill_ids", "skills"} hero_fields = {"hero_ids"} quest_fields = {"quest"} attr_fields = {"attr_id", "level_attr_id", "stage_attr_id"} shard_fields = {"shardItem"} compose_fields = {"compose"} next_fields = {"next"} recharge_fields = {"recharge_id"} product_fields = {"productId"} out_enemy_fields = {"out_enemy_config"} scene_fight_fields = {"fight_cf_name"} scene_map_fields = {"map_cf_name"} time_limit_fields = {"time_limit_boss"} for field in fields: if field == "target_id": result = get_relation_for_target_id(table_name) if result: target, desc = result relations.append({ "field": field, "target": target, "targetField": "id", "format": "", "description": desc, }) continue if field == "activityId": relations.append({ "field": field, "target": "activity", "targetField": "id", "format": "", "description": "所属活动ID", }) continue if field == "reward_id": relations.append({ "field": field, "target": "checkpointreward", "targetField": "id", "format": "", "description": "关卡奖励ID", }) continue if field in reward_fields: fmt = detect_reward_format(sample_values.get(field, [])) if not fmt: fmt = "item_id_num" desc_map = { "rewards": "奖励道具", "rewards2": "付费奖励道具", "pass_rewards": "首通奖励道具", "activateRewards": "激活奖励道具", "reward": "宝箱奖励道具", } desc = desc_map.get(field, f"{field}奖励道具") if table_name == "gacha" and field == "rewards": relations.append({ "field": field, "target": "hero", "targetField": "id", "format": "hero_id", "description": "召唤产出英雄(格式: hero_英雄ID)", }) else: relations.append({ "field": field, "target": "prop", "targetField": "id", "format": fmt, "description": f"{desc}(格式: {fmt})" if fmt else desc, }) continue if field in consume_fields: fmt = detect_consumes_format(sample_values.get(field, [])) desc_map = { "consumes": "消耗道具", "lostConsumes": "失败损失道具", } desc = desc_map.get(field, field) relations.append({ "field": field, "target": "prop", "targetField": "id", "format": fmt if fmt else "item_id_num", "description": f"{desc}(格式: item_id_num)" if fmt else f"{desc}(格式: item_id_num)", }) continue if field in monster_fields: fmt = detect_monster_format(sample_values.get(field, [])) desc_map = { "monsters": "怪物列表", "enemy_ids": "敌人列表", } desc = desc_map.get(field, field) format_desc = f"(格式: {fmt})" if fmt else "" relations.append({ "field": field, "target": "enemy", "targetField": "id", "format": fmt, "description": f"{desc}{format_desc}", }) continue if field in time_limit_fields: relations.append({ "field": field, "target": "enemy", "targetField": "id", "format": "id_lv_num_hp", "description": "限时Boss(格式: 敌人ID_等级_数量_血量百分比)", }) continue if field in equip_fields: relations.append({ "field": field, "target": "equip", "targetField": "id", "format": "", "description": "装备槽位装备ID列表", }) continue if field in skill_fields: relations.append({ "field": field, "target": "skill", "targetField": "id", "format": "", "description": "技能ID列表", }) continue if field in hero_fields: relations.append({ "field": field, "target": "hero", "targetField": "id", "format": "", "description": "关联英雄ID列表", }) continue if field in quest_fields: relations.append({ "field": field, "target": "quest", "targetField": "id", "format": "", "description": "关联任务ID列表", }) continue if field in attr_fields: relations.append({ "field": field, "target": "attribute", "targetField": "id", "format": "", "description": "关联属性ID", }) continue if field in shard_fields: relations.append({ "field": field, "target": "prop", "targetField": "id", "format": "", "description": "碎片道具ID", }) continue if field in compose_fields: relations.append({ "field": field, "target": "prop", "targetField": "id", "format": "", "description": "合成材料道具ID列表", }) continue if field in next_fields: relations.append({ "field": field, "target": table_name, "targetField": "id", "format": "", "description": "下一个ID(自引用)", }) continue if field in recharge_fields: relations.append({ "field": field, "target": "recharge", "targetField": "id", "format": "", "description": "关联充值档位ID", }) continue if field in product_fields: relations.append({ "field": field, "target": "prop", "targetField": "id", "format": "", "description": "商品道具ID", }) continue if field in out_enemy_fields: relations.append({ "field": field, "target": "map_act", "targetField": "id", "format": "", "description": "出怪波次配置表名", }) continue if field in scene_fight_fields: relations.append({ "field": field, "target": "fight_sample", "targetField": "id", "format": "", "description": "关联战斗配置表名", }) continue if field in scene_map_fields: relations.append({ "field": field, "target": "map", "targetField": "id", "format": "", "description": "关联地图配置表名", }) continue if re.match(r"^.+Id$", field) and field != "Id": prefix = field[:-2] for known_table in TABLE_NAME_TO_CN: if known_table.lower() == prefix.lower(): relations.append({ "field": field, "target": known_table, "targetField": "id", "format": "", "description": f"关联{TABLE_NAME_TO_CN[known_table]}ID", }) break return relations SUSPICIOUS_ID_PATTERNS = [ (re.compile(r"^.+Id$"), "可能引用其他表的ID字段(驼峰)"), (re.compile(r"^.+_id$"), "可能引用其他表的ID字段(下划线)"), (re.compile(r"^.+_ids$"), "可能引用其他表的ID列表(下划线)"), (re.compile(r"^.+_Ids$"), "可能引用其他表的ID列表(驼峰)"), ] def analyze_gaps(all_tables, table_fields_map): issues = [] ai_prompt_parts = [] tables_not_in_info = [t["name"] for t in all_tables if t["displayName"] == t["name"]] if tables_not_in_info: issues.append({ "type": "missing_display_name", "severity": "warning", "title": "以下表缺少中文名和描述", "items": tables_not_in_info, }) ai_prompt_parts.append( f"以下表缺少中文名和描述,请补全 displayName 和 description:\n" + "\n".join(f" - {t}" for t in tables_not_in_info) ) no_relation_tables = [t["name"] for t in all_tables if len(t["relations"]) == 0] if no_relation_tables: issues.append({ "type": "no_relations", "severity": "info", "title": "以下表没有任何引用关系(可能是叶子表或需人工检查)", "items": no_relation_tables, }) unrecognized_fields = [] for table_name, fields in table_fields_map.items(): table_entry = next((t for t in all_tables if t["name"] == table_name), None) if not table_entry: continue related_fields = {r["field"] for r in table_entry["relations"]} for field in fields: if field in related_fields: continue for pattern, desc in SUSPICIOUS_ID_PATTERNS: if pattern.match(field): unrecognized_fields.append(f" {table_name}.{field} - {desc}") break if unrecognized_fields: issues.append({ "type": "unrecognized_fields", "severity": "warning", "title": "以下字段可能是引用关系但脚本未能识别", "items": unrecognized_fields, }) ai_prompt_parts.append( "以下字段可能是引用关系但脚本未能识别,请分析并补全 relations:\n" + "\n".join(unrecognized_fields) ) tables_with_missing_fields = [] for table in all_tables: if table["displayName"] == table["name"] and len(table["relations"]) == 0: fields = table_fields_map.get(table["name"], []) if fields: tables_with_missing_fields.append( f" {table['name']} (字段: {', '.join(fields[:8])}{'...' if len(fields) > 8 else ''})" ) if tables_with_missing_fields: issues.append({ "type": "fully_missing", "severity": "error", "title": "以下表完全未配置(无中文名、无描述、无引用关系)", "items": tables_with_missing_fields, }) ai_prompt_parts.append( "以下表完全未配置,请补全 displayName、description 和 relations:\n" + "\n".join(tables_with_missing_fields) ) return issues, ai_prompt_parts def main(): args = parse_args() config_dir = Path(args.config_dir) if args.config_dir else DEFAULT_CONFIG_DIR output_file = Path(args.output) if args.output else DEFAULT_OUTPUT table_info_file = Path(args.table_info) if args.table_info else DEFAULT_TABLE_INFO table_info = load_table_info(table_info_file) all_tables = [] table_fields_map = {} json_files = sorted(config_dir.glob("*.json")) for filepath in json_files: table_name = filepath.stem fields = extract_fields_from_json(filepath) if not fields: continue table_fields_map[table_name] = fields sample_values = {} data = load_json_file(filepath) if isinstance(data, list) and len(data) > 0: first_row = data[0] for field in fields: if field in first_row: val = first_row[field] if isinstance(val, list): sample_values[field] = val else: sample_values[field] = [val] if val is not None else [] display_name, description = table_info.get(table_name, (table_name, "")) relations = build_relations(table_name, fields, sample_values) all_tables.append({ "name": table_name, "displayName": display_name, "description": description, "fileExtension": ".xlsx", "relations": relations, }) txt_files = sorted(config_dir.glob("*.txt")) for filepath in txt_files: table_name = filepath.stem if table_name in table_info: display_name, description = table_info[table_name] else: display_name, description = table_name, "" all_tables.append({ "name": table_name, "displayName": display_name, "description": description, "fileExtension": ".txt", "relations": [], }) output = {"tables": all_tables} output_file.parent.mkdir(parents=True, exist_ok=True) with open(output_file, "w", encoding="utf-8") as f: json.dump(output, f, ensure_ascii=False, indent=4) total_tables = len(all_tables) total_relations = sum(len(t['relations']) for t in all_tables) tables_with_rels = sum(1 for t in all_tables if t['relations']) print("=" * 60) print(" ConfigLinkData.json 生成完成") print("=" * 60) print(f" 输出文件: {output_file}") print(f" 总表数: {total_tables}") print(f" 有引用关系的表: {tables_with_rels}") print(f" 总引用关系数: {total_relations}") print() issues, ai_prompt_parts = analyze_gaps(all_tables, table_fields_map) if not issues: print(" 所有表均已完整配置,无需要补全的内容。") else: print("-" * 60) print(" 查漏补缺报告") print("-" * 60) for issue in issues: severity_icon = {"error": "[!]", "warning": "[*]", "info": "[i]"}.get(issue["severity"], "") print(f"\n {severity_icon} {issue['title']} ({len(issue['items'])}项)") for item in issue["items"]: print(f" {item}") if ai_prompt_parts: print() print("=" * 60) print(" AI 补全提示词(可复制发送给 AI)") print("=" * 60) print() print("我需要你帮助维护配置表联动查看器的配置文件。") print() print(f"当前目录:{output_file.parent}") print() print("请:") print("1. 读取 ConfigLinkData.json,分析所有表的配置情况") print("2. 找出缺少中文名称(displayName == name)或描述为空的表,补充中文名称和描述") print("3. 分析字段引用关系,为缺少联动关系的表添加 relations") print("4. 对于 target_id 字段,通常引用对应的基础表(如 herolevel.target_id -> hero)") print("5. 对于 xxxId/xxx_id 字段,根据前缀推断目标表(如 itemId -> prop)") print("6. 同时更新 table_info.json(中文名称)和 ConfigLinkData.json(完整配置)") print("7. 输出修改摘要") print() print("需要处理的内容:") print() for part in ai_prompt_parts: print(part) print() print("注意:") print("- displayName 应该是简洁的中文名称(如\"英雄\"、\"装备\")") print("- description 应该是清晰的功能描述(如\"英雄基础属性配置\")") print("- relations 字段格式:{\"field\": \"字段名\", \"target\": \"目标表名\", \"targetField\": \"id\", \"description\": \"关联描述\"}") print("- 保持 JSON 格式正确,不要添加多余逗号") print("- table_info.json 只需更新 displayName 和 description") print("- ConfigLinkData.json 需要更新 displayName、description 和 relations") print() if __name__ == "__main__": main()