Files
Planner_Tools/Unity/配置表联动查看器/ConfigLinkViewer/generate_config_link_data.py
ShallowT1Dream 3071ec4b8f 提交修改
2026-06-04 14:28:46 +08:00

699 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()