From d4fbef751ac8938078e2a0a4987b37ea509adbb3 Mon Sep 17 00:00:00 2001 From: ShallowT1Dream <869575054@qq.com> Date: Tue, 2 Jun 2026 14:11:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=85=8D=E7=BD=AE=E7=9A=84?= =?UTF-8?q?=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ConfigLinkViewer/ConfigLinkDatabase.cs | 751 +++++++++--------- .../ConfigLinkViewer/ConfigLinkViewerWindow.cs | 640 ++++++++++++--- .../ConfigLinkViewer/generate_config_link_data.py | 679 ++++++++++++++++ .../ConfigLinkViewer/settings.json | 50 ++ .../ConfigLinkViewer/table_info.json | 85 ++ Unity/配置表联动查看器/README.md | 264 +++--- 6 files changed, 1895 insertions(+), 574 deletions(-) create mode 100644 Unity/配置表联动查看器/ConfigLinkViewer/generate_config_link_data.py create mode 100644 Unity/配置表联动查看器/ConfigLinkViewer/settings.json create mode 100644 Unity/配置表联动查看器/ConfigLinkViewer/table_info.json diff --git a/Unity/配置表联动查看器/ConfigLinkViewer/ConfigLinkDatabase.cs b/Unity/配置表联动查看器/ConfigLinkViewer/ConfigLinkDatabase.cs index 35a9e13..fd38eec 100644 --- a/Unity/配置表联动查看器/ConfigLinkViewer/ConfigLinkDatabase.cs +++ b/Unity/配置表联动查看器/ConfigLinkViewer/ConfigLinkDatabase.cs @@ -2,7 +2,9 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using UnityEngine; +using UnityEditor; [Serializable] public class ConfigTableInfo @@ -35,13 +37,60 @@ public class ReverseRelation } [Serializable] -public class FieldRelationBackup +public class ConfigLinkDataFile { - public string fieldName; - public string targetTable; - public string targetField; - public string relationFormat; + public List tables = new List(); +} + +[Serializable] +public class ConfigLinkTableEntry +{ + public string name; + public string displayName; public string description; + public string fileExtension; + public List relations = new List(); +} + +[Serializable] +public class ConfigLinkRelation +{ + public string field; + public string target; + public string targetField; + public string format; + public string description; +} + +[Serializable] +public class FormatTemplate +{ + public string[] parts; + public int minParts; +} + +[Serializable] +public class SettingsData +{ + public string configFolderPath = "Resources/Resources_moved/config"; + public string excelFolderPath = ""; + public FormatTemplatesWrapper formatTemplates = new FormatTemplatesWrapper(); +} + +[Serializable] +public class FormatTemplatesWrapper +{ + public FormatTemplate item_id_num = new FormatTemplate { parts = new[] { "类型", "ID", "数量" }, minParts = 3 }; + public FormatTemplate type_id_num = new FormatTemplate { parts = new[] { "类型", "ID", "数量" }, minParts = 3 }; + public FormatTemplate id_pos_lv = new FormatTemplate { parts = new[] { "ID", "位置", "等级" }, minParts = 3 }; + public FormatTemplate id_lv_num = new FormatTemplate { parts = new[] { "ID", "等级", "数量" }, minParts = 3 }; + public FormatTemplate id_lv_num_time = new FormatTemplate { parts = new[] { "ID", "等级", "数量", "时间" }, minParts = 4 }; + public FormatTemplate buffid_lv = new FormatTemplate { parts = new[] { "BuffID", "等级" }, minParts = 2 }; + public FormatTemplate rune_id_num = new FormatTemplate { parts = new[] { "符文ID", "数量" }, minParts = 2 }; + public FormatTemplate equip_id_num = new FormatTemplate { parts = new[] { "装备ID", "数量" }, minParts = 2 }; + public FormatTemplate hero_id = new FormatTemplate { parts = new[] { "英雄ID" }, minParts = 1 }; + public FormatTemplate id_lv_count_delay = new FormatTemplate { parts = new[] { "ID", "等级", "数量", "延迟" }, minParts = 4 }; + public FormatTemplate id_lv_num_hp = new FormatTemplate { parts = new[] { "ID", "等级", "数量", "血量%" }, minParts = 4 }; } public static class ConfigLinkDatabase @@ -49,20 +98,61 @@ public static class ConfigLinkDatabase private static List _cachedData; private static HashSet _existingTableNames; private static string _excelFolderPath; + private static SettingsData _settings; + private static Dictionary _formatTemplateMap; + private static Dictionary _tableInfoCache; - private const string CONFIG_PATH = "Resources/Resources_moved/config"; + private const string CONFIG_FILE_NAME = "ConfigLinkData.json"; + private const string SETTINGS_FILE_NAME = "settings.json"; + private const string TABLE_INFO_FILE_NAME = "table_info.json"; + private const string EXCEL_PATH_KEY = "ConfigLinkViewer.ExcelFolderPath"; + + private static string ViewerDir + { + get { return Path.Combine(Application.dataPath, "Editor/ConfigLinkViewer"); } + } + + private static string ConfigFilePath + { + get { return Path.Combine(ViewerDir, CONFIG_FILE_NAME); } + } + + private static string SettingsFilePath + { + get { return Path.Combine(ViewerDir, SETTINGS_FILE_NAME); } + } + + private static string TableInfoFilePath + { + get { return Path.Combine(ViewerDir, TABLE_INFO_FILE_NAME); } + } + + public static string GetConfigFolderPath() + { + LoadSettings(); + return _settings.configFolderPath; + } + + public static void SetConfigFolderPath(string path) + { + LoadSettings(); + _settings.configFolderPath = path; + SaveSettings(); + ClearCache(); + } public static void SetExcelFolderPath(string path) { _excelFolderPath = path; - PlayerPrefs.SetString("ConfigLinkExcelFolderPath", path); + EditorUserSettings.SetConfigValue(EXCEL_PATH_KEY, path); + ClearCache(); } public static string GetExcelFolderPath() { if (string.IsNullOrEmpty(_excelFolderPath)) { - _excelFolderPath = PlayerPrefs.GetString("ConfigLinkExcelFolderPath", ""); + _excelFolderPath = EditorUserSettings.GetConfigValue(EXCEL_PATH_KEY) ?? ""; } return _excelFolderPath; } @@ -94,303 +184,157 @@ public static class ConfigLinkDatabase if (_cachedData != null) return _cachedData; - _cachedData = new List - { - new ConfigTableInfo { tableName = "activity", displayName = "活动表", description = "活动配置表" }, - new ConfigTableInfo { tableName = "activityreward", displayName = "活动奖励表", description = "活动奖励配置", relations = new List { - new FieldRelation { fieldName = "activityId", targetTable = "activity", targetField = "id", description = "关联活动" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "奖励(类型_id_数量)" } - }}, - new ConfigTableInfo { tableName = "achievement", displayName = "成就表", description = "成就配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励(类型_id_数量)" } - }}, - new ConfigTableInfo { tableName = "arenaaward", displayName = "竞技场奖励表", description = "竞技场排名奖励", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励(类型_id_数量)" } - }}, - new ConfigTableInfo { tableName = "attribute", displayName = "属性表", description = "属性配置表" }, - new ConfigTableInfo { tableName = "box", displayName = "宝箱表", description = "宝箱抽奖配置", relations = new List { - new FieldRelation { fieldName = "reward", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" } - }}, - new ConfigTableInfo { tableName = "buff", displayName = "Buff表", description = "Buff效果配置" }, - new ConfigTableInfo { tableName = "callstrength", displayName = "召唤强度表", description = "召唤强度概率配置" }, - new ConfigTableInfo { tableName = "checkpointreward", displayName = "关卡奖励表", description = "关卡奖励池配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "rune", targetField = "id", relationFormat = "rune_id_num", description = "符文奖励" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "道具奖励" }, - new FieldRelation { fieldName = "rewards", targetTable = "equip", targetField = "id", relationFormat = "equip_id_num", description = "装备奖励" } - }}, - new ConfigTableInfo { tableName = "collectionreward", displayName = "收藏奖励表", description = "收藏奖励配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" } - }}, - new ConfigTableInfo { tableName = "common", displayName = "通用配置表", description = "通用配置项", relations = new List { - new FieldRelation { fieldName = "key", targetTable = "language", targetField = "key", description = "多语言key" } - }}, - new ConfigTableInfo { tableName = "drawlinereward", displayName = "画线奖励表", description = "画线游戏奖励", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" } - }}, - new ConfigTableInfo { tableName = "enemy", displayName = "敌人表", description = "敌人基础配置", relations = new List { - new FieldRelation { fieldName = "reward_id", targetTable = "checkpointreward", targetField = "id", description = "掉落奖励" }, - new FieldRelation { fieldName = "skills", targetTable = "skill", targetField = "id", description = "技能列表" } - }}, - new ConfigTableInfo { tableName = "enemylevel", displayName = "敌人等级表", description = "敌人等级成长配置", relations = new List { - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "升级消耗" }, - new FieldRelation { fieldName = "target_id", targetTable = "enemy", targetField = "id", description = "关联敌人" } - }}, - new ConfigTableInfo { tableName = "equip", displayName = "装备表", description = "装备基础配置", relations = new List { - new FieldRelation { fieldName = "attr_id", targetTable = "equipAttrRandom", targetField = "id", description = "随机属性ID" }, - new FieldRelation { fieldName = "retrieve_pros", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "回收奖励" } - }}, - new ConfigTableInfo { tableName = "equipAttrRandom", displayName = "装备随机属性表", description = "装备随机属性配置" }, - new ConfigTableInfo { tableName = "equiplevel", displayName = "装备等级表", description = "装备等级成长配置", relations = new List { - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "升级消耗" }, - new FieldRelation { fieldName = "target_id", targetTable = "equip", targetField = "id", description = "关联装备" } - }}, - new ConfigTableInfo { tableName = "equipstage", displayName = "装备品阶表", description = "装备品阶配置", relations = new List { - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "突破消耗" }, - new FieldRelation { fieldName = "target_id", targetTable = "equip", targetField = "id", description = "关联装备" } - }}, - new ConfigTableInfo { tableName = "fight_arena", displayName = "竞技场战斗表", description = "竞技场关卡配置", relations = new List { - new FieldRelation { fieldName = "enemy_ids", targetTable = "enemy", targetField = "id", relationFormat = "id_pos_lv", description = "敌人(格式: id_位置_等级)" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }, - new FieldRelation { fieldName = "out_enemy_config", targetTable = "map_act", targetField = "id", relationFormat = "表名", description = "出怪表名" } - }}, - new ConfigTableInfo { tableName = "fight_fb1", displayName = "副本战斗表1", description = "副本关卡配置", relations = new List { - new FieldRelation { fieldName = "enemy_ids", targetTable = "enemy", targetField = "id", relationFormat = "id_pos_lv", description = "敌人(格式: id_位置_等级)" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }, - new FieldRelation { fieldName = "out_enemy_config", targetTable = "map_act", targetField = "id", relationFormat = "表名", description = "出怪表名" } - }}, - new ConfigTableInfo { tableName = "fight_fb2", displayName = "副本战斗表2", description = "副本关卡配置", relations = new List { - new FieldRelation { fieldName = "enemy_ids", targetTable = "enemy", targetField = "id", relationFormat = "id_pos_lv", description = "敌人(格式: id_位置_等级)" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }, - new FieldRelation { fieldName = "out_enemy_config", targetTable = "map_act2", targetField = "id", relationFormat = "表名", description = "出怪表名" } - }}, - new ConfigTableInfo { tableName = "fight_fb3", displayName = "副本战斗表3", description = "副本关卡配置", relations = new List { - new FieldRelation { fieldName = "enemy_ids", targetTable = "enemy", targetField = "id", relationFormat = "id_pos_lv", description = "敌人(格式: id_位置_等级)" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }, - new FieldRelation { fieldName = "out_enemy_config", targetTable = "map_act3", targetField = "id", relationFormat = "表名", description = "出怪表名" } - }}, - new ConfigTableInfo { tableName = "fight_fb4", displayName = "副本战斗表4", description = "副本关卡配置", relations = new List { - new FieldRelation { fieldName = "enemy_ids", targetTable = "enemy", targetField = "id", relationFormat = "id_pos_lv", description = "敌人(格式: id_位置_等级)" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }, - new FieldRelation { fieldName = "out_enemy_config", targetTable = "map_act4", targetField = "id", relationFormat = "表名", description = "出怪表名" } - }}, - new ConfigTableInfo { tableName = "fight_sample", displayName = "战斗样例表", description = "战斗关卡配置", relations = new List { - new FieldRelation { fieldName = "enemy_ids", targetTable = "enemy", targetField = "id", relationFormat = "id_pos_lv", description = "敌人(格式: id_位置_等级)" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }, - new FieldRelation { fieldName = "pass_rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "回合进度奖励" }, - new FieldRelation { fieldName = "out_enemy_config", targetTable = "map_act", targetField = "id", relationFormat = "表名", description = "出怪表名" } - }}, - new ConfigTableInfo { tableName = "fight_x", displayName = "战斗X表", description = "战斗关卡配置", relations = new List { - new FieldRelation { fieldName = "enemy_ids", targetTable = "enemy", targetField = "id", relationFormat = "id_pos_lv", description = "敌人(格式: id_位置_等级)" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }, - new FieldRelation { fieldName = "pass_rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "回合进度奖励" } - }}, - new ConfigTableInfo { tableName = "funopencondition", displayName = "功能开放条件表", description = "功能开放条件配置" }, - new ConfigTableInfo { tableName = "gacha", displayName = "抽卡表", description = "抽卡配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "hero", targetField = "id", description = "抽卡获得的英雄" } - }}, - new ConfigTableInfo { tableName = "gachareward", displayName = "抽卡奖励表", description = "抽卡奖励配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" } - }}, - new ConfigTableInfo { tableName = "game2048reward", displayName = "2048游戏奖励表", description = "2048游戏奖励配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" } - }}, - new ConfigTableInfo { tableName = "guide", displayName = "引导表", description = "新手引导配置" }, - new ConfigTableInfo { tableName = "guildbox", displayName = "公会宝箱表", description = "公会宝箱配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" } - }}, - new ConfigTableInfo { tableName = "guilddonate", displayName = "公会捐献表", description = "公会捐献配置", relations = new List { - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "消耗" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" } - }}, - new ConfigTableInfo { tableName = "guildlevel", displayName = "公会等级表", description = "公会等级配置" }, - new ConfigTableInfo { tableName = "guildname", displayName = "公会名称表", description = "公会名称配置" }, - new ConfigTableInfo { tableName = "hero", displayName = "英雄表", description = "英雄基础配置", relations = new List { - new FieldRelation { fieldName = "level_attr_id", targetTable = "attribute", targetField = "id", description = "升级显示属性" }, - new FieldRelation { fieldName = "stage_attr_id", targetTable = "attribute", targetField = "id", description = "升阶显示属性" }, - new FieldRelation { fieldName = "skills", targetTable = "skill", targetField = "id", description = "技能列表" }, - new FieldRelation { fieldName = "compose", targetTable = "hero", targetField = "id", description = "合成所需英雄" }, - new FieldRelation { fieldName = "equips", targetTable = "equip", targetField = "id", description = "装备列表" }, - new FieldRelation { fieldName = "buffs", targetTable = "buff", targetField = "id", relationFormat = "buffId_lv", description = "升阶解锁buff" }, - new FieldRelation { fieldName = "internal_buffs", targetTable = "buff", targetField = "id", description = "初始可抽取buff" }, - new FieldRelation { fieldName = "shardItem", targetTable = "prop", targetField = "id", description = "碎片道具ID" }, - new FieldRelation { fieldName = "activateRewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "激活奖励" } - }}, - new ConfigTableInfo { tableName = "herolevel", displayName = "英雄等级表", description = "英雄等级成长配置", relations = new List { - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "升级消耗" }, - new FieldRelation { fieldName = "target_id", targetTable = "hero", targetField = "id", description = "关联英雄" } - }}, - new ConfigTableInfo { tableName = "herolevelinternal", displayName = "英雄内丹表", description = "英雄内丹配置", relations = new List { - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "消耗" }, - new FieldRelation { fieldName = "buffs", targetTable = "buff", targetField = "id", description = "对应提升buff" } - }}, - new ConfigTableInfo { tableName = "herostage", displayName = "英雄品阶表", description = "英雄品阶配置", relations = new List { - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "道具消耗" }, - new FieldRelation { fieldName = "target_id", targetTable = "hero", targetField = "id", description = "关联英雄" } - }}, - new ConfigTableInfo { tableName = "idlereward", displayName = "挂机奖励表", description = "挂机奖励配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "挂机宝箱奖励" } - }}, - new ConfigTableInfo { tableName = "language", displayName = "语言表", description = "多语言配置" }, - new ConfigTableInfo { tableName = "link", displayName = "英雄羁绊表", description = "英雄羁绊配置", relations = new List { - new FieldRelation { fieldName = "hero_ids", targetTable = "hero", targetField = "id", description = "英雄ID列表" } - }}, - new ConfigTableInfo { tableName = "mail", displayName = "邮件表", description = "邮件配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" } - }}, - new ConfigTableInfo { tableName = "mall", displayName = "商店表", description = "商店商品配置", relations = new List { - new FieldRelation { fieldName = "productId", targetTable = "prop", targetField = "id", description = "商品道具ID" }, - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "购买消耗" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "购买获得奖励" } - }}, - new ConfigTableInfo { tableName = "map", displayName = "地图表", description = "地图房间配置", relations = new List { - new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num", description = "初始怪物" } - }}, - new ConfigTableInfo { tableName = "map1", displayName = "地图1表", description = "地图1房间配置", relations = new List { - new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num", description = "初始怪物" } - }}, - new ConfigTableInfo { tableName = "map2", displayName = "地图2表", description = "地图2房间配置", relations = new List { - new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num", description = "初始怪物" } - }}, - new ConfigTableInfo { tableName = "map3", displayName = "地图3表", description = "地图3房间配置", relations = new List { - new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num", description = "初始怪物" } - }}, - new ConfigTableInfo { tableName = "map_act", displayName = "出怪表", description = "关卡出怪配置", relations = new List { - new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num_time", description = "怪物(格式: id_等级_数量_时间)" }, - new FieldRelation { fieldName = "boss", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num_time", description = "Boss(格式: id_等级_数量_限时)" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "波次奖励" } - }}, - new ConfigTableInfo { tableName = "map_act2", displayName = "出怪表2", description = "关卡出怪配置", relations = new List { - new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num_time", description = "怪物" }, - new FieldRelation { fieldName = "boss", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num_time", description = "Boss" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "波次奖励" } - }}, - new ConfigTableInfo { tableName = "map_act3", displayName = "出怪表3", description = "关卡出怪配置", relations = new List { - new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num_time", description = "怪物" }, - new FieldRelation { fieldName = "boss", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num_time", description = "Boss" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "波次奖励" } - }}, - new ConfigTableInfo { tableName = "map_act4", displayName = "出怪表4", description = "关卡出怪配置", relations = new List { - new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num_time", description = "怪物" }, - new FieldRelation { fieldName = "boss", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num_time", description = "Boss" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "波次奖励" } - }}, - new ConfigTableInfo { tableName = "map_act_x", displayName = "出怪表X", description = "关卡出怪配置", relations = new List { - new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num_time", description = "怪物" }, - new FieldRelation { fieldName = "boss", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num_time", description = "Boss" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "波次奖励" } - }}, - new ConfigTableInfo { tableName = "mapx", displayName = "地图X表", description = "地图X房间配置", relations = new List { - new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num", description = "初始怪物" } - }}, - new ConfigTableInfo { tableName = "monthlycard", displayName = "月卡表", description = "月卡配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }, - new FieldRelation { fieldName = "recharge_id", targetTable = "recharge", targetField = "id", description = "购买立即获得" } - }}, - new ConfigTableInfo { tableName = "name", displayName = "名字表", description = "随机名字配置" }, - new ConfigTableInfo { tableName = "outenemypoint", displayName = "外出敌人点表", description = "外出敌人点配置", relations = new List { - new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num", description = "怪物" } - }}, - new ConfigTableInfo { tableName = "pandora", displayName = "潘多拉表", description = "潘多拉奖励池配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励池" } - }}, - new ConfigTableInfo { tableName = "pass", displayName = "通行证表", description = "通行证奖励配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "免费奖励" }, - new FieldRelation { fieldName = "rewards2", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "付费奖励" } - }}, - new ConfigTableInfo { tableName = "player", displayName = "玩家表", description = "玩家基础配置", relations = new List { - new FieldRelation { fieldName = "equips", targetTable = "equip", targetField = "id", description = "初始装备" } - }}, - new ConfigTableInfo { tableName = "playerlevel", displayName = "玩家等级表", description = "玩家等级成长配置", relations = new List { - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "升级消耗" }, - new FieldRelation { fieldName = "talent_id", targetTable = "playertalent", targetField = "id", description = "天赋ID" }, - new FieldRelation { fieldName = "target_id", targetTable = "player", targetField = "id", description = "关联玩家" } - }}, - new ConfigTableInfo { tableName = "playerskin", displayName = "玩家皮肤表", description = "玩家皮肤配置", relations = new List { - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "激活消耗" }, - new FieldRelation { fieldName = "target_id", targetTable = "player", targetField = "id", description = "关联玩家" } - }}, - new ConfigTableInfo { tableName = "playerstage", displayName = "玩家境界表", description = "玩家境界配置", relations = new List { - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "突破消耗" }, - new FieldRelation { fieldName = "target_id", targetTable = "player", targetField = "id", description = "关联玩家" } - }}, - new ConfigTableInfo { tableName = "playertalent", displayName = "玩家天赋表", description = "玩家天赋配置", relations = new List { - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "升级消耗" }, - new FieldRelation { fieldName = "lostConsumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "抛弃消耗" } - }}, - new ConfigTableInfo { tableName = "prop", displayName = "道具表", description = "道具配置" }, - new ConfigTableInfo { tableName = "pushboxreward", displayName = "推箱奖励表", description = "推箱游戏奖励", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" } - }}, - new ConfigTableInfo { tableName = "qirilibao", displayName = "七日礼包表", description = "七日礼包配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "免费奖励" }, - new FieldRelation { fieldName = "rewards2", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "付费奖励" } - }}, - new ConfigTableInfo { tableName = "quest", displayName = "任务表", description = "任务配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "任务奖励" }, - new FieldRelation { fieldName = "next", targetTable = "quest", targetField = "id", description = "下一个任务" } - }}, - new ConfigTableInfo { tableName = "recharge", displayName = "充值表", description = "充值配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }, - new FieldRelation { fieldName = "children", targetTable = "recharge", targetField = "id", description = "打包售卖关联的孩子" } - }}, - new ConfigTableInfo { tableName = "rechargegift", displayName = "累充礼物表", description = "累充礼物配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" } - }}, - new ConfigTableInfo { tableName = "rune", displayName = "符文表", description = "符文基础配置", relations = new List { - new FieldRelation { fieldName = "attr_id", targetTable = "attribute", targetField = "id", description = "属性ID" } - }}, - new ConfigTableInfo { tableName = "runelevel", displayName = "符文等级表", description = "符文等级配置", relations = new List { - new FieldRelation { fieldName = "target_id", targetTable = "rune", targetField = "id", description = "关联符文" } - }}, - new ConfigTableInfo { tableName = "scene", displayName = "场景表", description = "场景配置", relations = new List { - new FieldRelation { fieldName = "fight_cf_name", targetTable = "fight_sample", targetField = "id", relationFormat = "表名", description = "战斗配置表名" }, - new FieldRelation { fieldName = "map_cf_name", targetTable = "map", targetField = "id", relationFormat = "表名", description = "地图配置表名" } - }}, - new ConfigTableInfo { tableName = "serverError", displayName = "服务器错误表", description = "服务器错误码配置" }, - new ConfigTableInfo { tableName = "signin", displayName = "签到表", description = "签到奖励配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "签到奖励" } - }}, - new ConfigTableInfo { tableName = "skill", displayName = "技能表", description = "技能配置", relations = new List { - new FieldRelation { fieldName = "to_partner_buff", targetTable = "buff", targetField = "id", description = "给己方的buff" }, - new FieldRelation { fieldName = "to_enemy_buff", targetTable = "buff", targetField = "id", description = "给敌方的buff" } - }}, - new ConfigTableInfo { tableName = "skilllevel", displayName = "技能等级表", description = "技能等级配置", relations = new List { - new FieldRelation { fieldName = "target_id", targetTable = "skill", targetField = "id", description = "关联技能" } - }}, - new ConfigTableInfo { tableName = "systemopen", displayName = "系统开放表", description = "系统开放条件配置" }, - new ConfigTableInfo { tableName = "test_map_act", displayName = "测试出怪表", description = "测试关卡出怪配置", relations = new List { - new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num_time", description = "怪物" }, - new FieldRelation { fieldName = "boss", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num_time", description = "Boss" }, - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "波次奖励" } - }}, - new ConfigTableInfo { tableName = "title", displayName = "称号表", description = "称号配置" }, - new ConfigTableInfo { tableName = "trainbreak", displayName = "修炼突破表", description = "修炼突破配置", relations = new List { - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "突破消耗" }, - new FieldRelation { fieldName = "quest", targetTable = "quest", targetField = "id", description = "突破任务" } - }}, - new ConfigTableInfo { tableName = "trainlevel", displayName = "修炼等级表", description = "修炼等级配置", relations = new List { - new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "修炼消耗" } - }}, - new ConfigTableInfo { tableName = "vip", displayName = "VIP表", description = "VIP等级配置", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "免费礼包" }, - new FieldRelation { fieldName = "recharge_id", targetTable = "recharge", targetField = "id", description = "付费礼包" } - }}, - new ConfigTableInfo { tableName = "fishingreward", displayName = "钓鱼奖励表", description = "钓鱼游戏奖励", relations = new List { - new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" } - }}, - new ConfigTableInfo { tableName = "dirty_words", displayName = "敏感词表", description = "敏感词过滤配置(TXT文件)", fileExtension = ".txt" }, - new ConfigTableInfo { tableName = "奖励格式说明", displayName = "奖励格式说明", description = "奖励数据格式说明文档(TXT文件)", fileExtension = ".txt" } - }; - + _cachedData = LoadConfigFromFile(ConfigFilePath); MarkExistingTables(); return _cachedData; } + public static bool IsConfigLoaded() + { + return _cachedData != null && _cachedData.Count > 0; + } + + public static bool HasConfigFile() + { + return File.Exists(ConfigFilePath); + } + + private static List LoadConfigFromFile(string path) + { + if (!File.Exists(path)) + { + Debug.LogWarning("[ConfigLinkViewer] 配置文件不存在,请点击[生成配置]按钮生成: " + path); + return new List(); + } + + try + { + string json = File.ReadAllText(path, Encoding.UTF8); + var data = JsonUtility.FromJson(json); + if (data == null || data.tables == null) return new List(); + + var result = new List(); + foreach (var entry in data.tables) + { + var info = new ConfigTableInfo + { + tableName = entry.name, + displayName = string.IsNullOrEmpty(entry.displayName) ? entry.name : entry.displayName, + description = entry.description ?? "", + fileExtension = string.IsNullOrEmpty(entry.fileExtension) ? ".xlsx" : entry.fileExtension, + relations = new List() + }; + + if (entry.relations != null) + { + foreach (var rel in entry.relations) + { + info.relations.Add(new FieldRelation + { + fieldName = rel.field, + targetTable = rel.target, + targetField = rel.targetField ?? "id", + relationFormat = rel.format ?? "", + description = rel.description ?? "" + }); + } + } + + result.Add(info); + } + + return result; + } + catch (Exception e) + { + Debug.LogError($"[ConfigLinkViewer] 加载配置文件失败: {e.Message}"); + return new List(); + } + } + + public static void GenerateConfigFromExcelFolder() + { + string excelFolder = GetExcelFolderPath(); + if (string.IsNullOrEmpty(excelFolder) || !Directory.Exists(excelFolder)) + { + EditorUtility.DisplayDialog("提示", "请先设置Excel文件夹路径", "确定"); + return; + } + + var files = Directory.GetFiles(excelFolder, "*.xlsx", SearchOption.AllDirectories) + .Where(f => !Path.GetFileName(f).StartsWith("~$")) + .ToArray(); + + if (files.Length == 0) + { + EditorUtility.DisplayDialog("提示", "Excel文件夹中未找到.xlsx文件", "确定"); + return; + } + + var data = new ConfigLinkDataFile(); + var tableInfoDict = LoadTableInfoDict(); + + foreach (var file in files) + { + string name = Path.GetFileNameWithoutExtension(file); + string displayName = name; + string description = ""; + + if (tableInfoDict.TryGetValue(name.ToLower(), out var ti)) + { + displayName = ti.Item1; + description = ti.Item2; + } + + data.tables.Add(new ConfigLinkTableEntry + { + name = name, + displayName = displayName, + description = description, + fileExtension = ".xlsx", + relations = new List() + }); + } + + var txtFiles = Directory.GetFiles(excelFolder, "*.txt", SearchOption.AllDirectories); + foreach (var file in txtFiles) + { + string name = Path.GetFileNameWithoutExtension(file); + string displayName = name; + string description = ""; + + if (tableInfoDict.TryGetValue(name.ToLower(), out var ti)) + { + displayName = ti.Item1; + description = ti.Item2; + } + + data.tables.Add(new ConfigLinkTableEntry + { + name = name, + displayName = displayName, + description = description, + fileExtension = ".txt", + relations = new List() + }); + } + + string json = JsonUtility.ToJson(data, true); + File.WriteAllText(ConfigFilePath, json, Encoding.UTF8); + + ClearCache(); + AssetDatabase.Refresh(); + EditorUtility.DisplayDialog("生成完成", + $"已扫描 {files.Length} 个文件,生成配置文件\n\n" + + $"配置文件: {ConfigFilePath}\n\n" + + $"提示: 可将该文件交给 AI 补全中文名称和表间关联关系", "确定"); + } + private static void MarkExistingTables() { _existingTableNames = new HashSet(); - - string configPath = Path.Combine(Application.dataPath, CONFIG_PATH); + + string configPath = Path.Combine(Application.dataPath, GetConfigFolderPath()); if (Directory.Exists(configPath)) { string[] jsonFiles = Directory.GetFiles(configPath, "*.json"); @@ -401,21 +345,12 @@ public static class ConfigLinkDatabase } } - string configPathAlt = Path.Combine(Application.dataPath, "../Resources/Resources_moved/config"); - if (Directory.Exists(configPathAlt)) - { - string[] jsonFilesAlt = Directory.GetFiles(configPathAlt, "*.json"); - foreach (string file in jsonFilesAlt) - { - string tableName = Path.GetFileNameWithoutExtension(file); - _existingTableNames.Add(tableName.ToLower()); - } - } + if (_cachedData == null) return; foreach (var table in _cachedData) { bool existInConfig = _existingTableNames.Contains(table.tableName.ToLower()); - + if (!existInConfig && table.fileExtension == ".txt") { string excelFolderPath = GetExcelFolderPath(); @@ -425,24 +360,22 @@ public static class ConfigLinkDatabase existInConfig = File.Exists(txtPath); } } - + table.isExistInProject = existInConfig; } } public static bool IsTableExistInConfigFolder(string tableName) { - string configPath = Path.Combine(Application.dataPath, CONFIG_PATH); + string configPath = Path.Combine(Application.dataPath, GetConfigFolderPath()); if (!Directory.Exists(configPath)) return false; var tableInfo = GetTableInfo(tableName); string extension = tableInfo?.fileExtension ?? ".xlsx"; - + string jsonPath = Path.Combine(configPath, $"{tableName}.json"); - string configPathAlt = Path.Combine(Application.dataPath, "../Resources/Resources_moved/config"); - string jsonPathAlt = Path.Combine(configPathAlt, $"{tableName}.json"); - + if (extension == ".txt") { string excelFolderPath = GetExcelFolderPath(); @@ -453,8 +386,8 @@ public static class ConfigLinkDatabase return true; } } - - return File.Exists(jsonPath) || File.Exists(jsonPathAlt); + + return File.Exists(jsonPath); } public static ConfigTableInfo GetTableInfo(string tableName) @@ -470,7 +403,7 @@ public static class ConfigLinkDatabase public static List GetReverseRelations(string targetTableName) { List result = new List(); - + foreach (var table in GetAllTableInfo()) { foreach (var relation in table.relations) @@ -488,7 +421,7 @@ public static class ConfigLinkDatabase } } } - + return result; } @@ -496,50 +429,30 @@ public static class ConfigLinkDatabase { if (string.IsNullOrEmpty(format) || string.IsNullOrEmpty(value)) return value; - - string[] parts = value.Split('_'); - string result = ""; - - switch (format.ToLower()) + + string key = format.ToLower(); + + if (key == "表名") + return $"表名: {value}"; + + LoadFormatTemplates(); + + if (_formatTemplateMap.TryGetValue(key, out var template)) { - case "item_id_num": - if (parts.Length >= 3) - result = $"类型:{parts[0]}, ID:{parts[1]}, 数量:{parts[2]}"; - break; - case "type_id_num": - if (parts.Length >= 3) - result = $"类型:{parts[0]}, ID:{parts[1]}, 数量:{parts[2]}"; - break; - case "id_pos_lv": - if (parts.Length >= 3) - result = $"ID:{parts[0]}, 位置:{parts[1]}, 等级:{parts[2]}"; - break; - case "id_lv_num": - if (parts.Length >= 3) - result = $"ID:{parts[0]}, 等级:{parts[1]}, 数量:{parts[2]}"; - break; - case "id_lv_num_time": - if (parts.Length >= 4) - result = $"ID:{parts[0]}, 等级:{parts[1]}, 数量:{parts[2]}, 时间:{parts[3]}"; - break; - case "buffid_lv": - if (parts.Length >= 2) - result = $"BuffID:{parts[0]}, 等级:{parts[1]}"; - break; - case "rune_id_num": - if (parts.Length >= 2) - result = $"符文ID:{parts[0]}, 数量:{parts[1]}"; - break; - case "equip_id_num": - if (parts.Length >= 2) - result = $"装备ID:{parts[0]}, 数量:{parts[1]}"; - break; - case "表名": - result = $"表名: {value}"; - break; + string[] parts = value.Split('_'); + if (parts.Length >= template.minParts) + { + var sb = new StringBuilder(); + for (int i = 0; i < template.parts.Length && i < parts.Length; i++) + { + if (i > 0) sb.Append(", "); + sb.Append($"{template.parts[i]}:{parts[i]}"); + } + return sb.ToString(); + } } - - return string.IsNullOrEmpty(result) ? value : result; + + return value; } public static List GetRelatedTableNames(string tableName) @@ -547,7 +460,7 @@ public static class ConfigLinkDatabase var tableInfo = GetTableInfo(tableName); if (tableInfo == null) return new List(); - + return tableInfo.relations.Select(r => r.targetTable).Distinct().ToList(); } @@ -574,5 +487,119 @@ public static class ConfigLinkDatabase { _cachedData = null; _existingTableNames = null; + _settings = null; + _formatTemplateMap = null; + _tableInfoCache = null; + } + + private static void LoadSettings() + { + if (_settings != null) return; + + if (File.Exists(SettingsFilePath)) + { + try + { + string json = File.ReadAllText(SettingsFilePath, Encoding.UTF8); + _settings = JsonUtility.FromJson(json); + } + catch (Exception e) + { + Debug.LogWarning($"[ConfigLinkViewer] 加载 settings.json 失败: {e.Message}"); + _settings = new SettingsData(); + } + } + else + { + _settings = new SettingsData(); + } + } + + public static void SaveSettings() + { + try + { + string json = JsonUtility.ToJson(_settings, true); + File.WriteAllText(SettingsFilePath, json, Encoding.UTF8); + } + catch (Exception e) + { + Debug.LogError($"[ConfigLinkViewer] 保存 settings.json 失败: {e.Message}"); + } + } + + private static void LoadFormatTemplates() + { + if (_formatTemplateMap != null) return; + + _formatTemplateMap = new Dictionary(); + LoadSettings(); + + if (_settings.formatTemplates != null) + { + var wrapperType = typeof(FormatTemplatesWrapper); + var fields = wrapperType.GetFields(); + foreach (var field in fields) + { + if (field.FieldType == typeof(FormatTemplate)) + { + var template = field.GetValue(_settings.formatTemplates) as FormatTemplate; + if (template != null) + { + _formatTemplateMap[field.Name] = template; + } + } + } + } + } + + private static Dictionary LoadTableInfoDict() + { + if (_tableInfoCache != null) + return _tableInfoCache; + + _tableInfoCache = new Dictionary(); + + if (!File.Exists(TableInfoFilePath)) + return _tableInfoCache; + + try + { + string json = File.ReadAllText(TableInfoFilePath, Encoding.UTF8); + var raw = JsonUtility.FromJson(json); + if (raw?.entries != null) + { + foreach (var entry in raw.entries) + { + if (!string.IsNullOrEmpty(entry.name)) + { + _tableInfoCache[entry.name.ToLower()] = ( + entry.displayName ?? entry.name, + entry.description ?? "" + ); + } + } + } + } + catch (Exception e) + { + Debug.LogWarning($"[ConfigLinkViewer] 加载 table_info.json 失败: {e.Message}"); + } + + return _tableInfoCache; + } + + [Serializable] + private class TableInfoFile + { + public List entries = new List(); + } + + [Serializable] + private class TableInfoEntry + { + public string name; + public string displayName; + public string description; } } diff --git a/Unity/配置表联动查看器/ConfigLinkViewer/ConfigLinkViewerWindow.cs b/Unity/配置表联动查看器/ConfigLinkViewer/ConfigLinkViewerWindow.cs index 4f0a463..0a8ec46 100644 --- a/Unity/配置表联动查看器/ConfigLinkViewer/ConfigLinkViewerWindow.cs +++ b/Unity/配置表联动查看器/ConfigLinkViewer/ConfigLinkViewerWindow.cs @@ -5,14 +5,95 @@ using System.Collections.Generic; using System.Linq; using System.IO; using System.IO.Compression; -using System.Text.RegularExpressions; using System.Xml.Linq; +public static class ConfigLinkViewerCallbacks +{ + public static Action ExportConfigCallback; + public static Func FallbackExcelPathCallback; +} + +[InitializeOnLoad] +public static class ConfigLinkViewerAutoSetup +{ + static ConfigLinkViewerAutoSetup() + { + TryRegisterExportConfig(); + TryRegisterFallbackExcelPath(); + } + + private static void TryRegisterExportConfig() + { + if (ConfigLinkViewerCallbacks.ExportConfigCallback != null) return; + try + { + var type = FindType("MFrame.ConfigDeal"); + if (type == null) return; + var method = type.GetMethod("ExportConfig", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + if (method == null) return; + ConfigLinkViewerCallbacks.ExportConfigCallback = + (Action)Delegate.CreateDelegate(typeof(Action), method); + } + catch { } + } + + private static void TryRegisterFallbackExcelPath() + { + if (ConfigLinkViewerCallbacks.FallbackExcelPathCallback != null) return; + try + { + var pathConfType = FindType("MFrame.PathConf"); + var editorCfType = FindType("MFrame.ConfigEditorCf"); + if (pathConfType == null || editorCfType == null) return; + + var rootField = pathConfType.GetField("project_root", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + if (rootField == null) return; + + var insProp = editorCfType.GetProperty("ins", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + if (insProp == null) return; + + var ins = insProp.GetValue(null); + if (ins == null) return; + + var excelField = ins.GetType().GetField("excel_path", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); + if (excelField == null) return; + + ConfigLinkViewerCallbacks.FallbackExcelPathCallback = () => + { + var root = rootField.GetValue(null) as string; + var inst = insProp.GetValue(null); + var ep = inst != null ? excelField.GetValue(inst) as string : null; + return root + ep; + }; + } + catch { } + } + + private static Type FindType(string fullName) + { + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + var t = asm.GetType(fullName); + if (t != null) return t; + } + return null; + } +} + public class ConfigLinkViewerWindow : EditorWindow { + private enum Tab { View, Config } + + private Tab currentTab = Tab.View; private string selectedTableName; private Dictionary expandedRelations = new Dictionary(); private Vector2 relationScrollPos; + private Vector2 detailScrollPos; + private Vector2 configScrollPos; private string searchFilter = ""; private bool showOnlyExisting = true; private bool showReverseRelations = false; @@ -27,46 +108,284 @@ public class ConfigLinkViewerWindow : EditorWindow private void OnGUI() { + DrawTabBar(); + EditorGUILayout.Space(); + float windowWidth = position.width; float windowHeight = position.height; - - DrawHeader(); + + if (currentTab == Tab.View) + { + DrawViewTab(windowWidth, windowHeight); + } + else + { + DrawConfigTab(windowWidth, windowHeight); + DrawLuoTianyiOverlay(windowWidth, windowHeight); + } + } + + private void DrawTabBar() + { + EditorGUILayout.BeginHorizontal("Toolbar"); + GUILayout.FlexibleSpace(); + + GUIStyle tabLeft, tabRight; + if (currentTab == Tab.View) + { + tabLeft = new GUIStyle("ToolbarButton") { fontStyle = FontStyle.Bold }; + tabRight = new GUIStyle("ToolbarButton"); + } + else + { + tabLeft = new GUIStyle("ToolbarButton"); + tabRight = new GUIStyle("ToolbarButton") { fontStyle = FontStyle.Bold }; + } + + if (GUILayout.Button("查看", tabLeft, GUILayout.Width(60))) + { + currentTab = Tab.View; + } + if (GUILayout.Button("配置", tabRight, GUILayout.Width(60))) + { + currentTab = Tab.Config; + } + + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + } + + private void DrawViewTab(float windowWidth, float windowHeight) + { + DrawViewHeader(); EditorGUILayout.Space(); DrawTableSelector(windowWidth, windowHeight); } - private void DrawHeader() + private void DrawViewHeader() { EditorGUILayout.BeginVertical("box"); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("配置表联动关系查看器", new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 }, GUILayout.ExpandWidth(true)); + if (GUILayout.Button("清理Excel空行", GUILayout.Height(22), GUILayout.Width(100))) { EditorApplication.delayCall += CleanExcelEmptyRows; } if (GUILayout.Button("导出配置", GUILayout.Height(22), GUILayout.Width(80))) { - EditorApplication.delayCall += MFrame.ConfigDeal.ExportConfig; + EditorApplication.delayCall += () => + { + if (ConfigLinkViewerCallbacks.ExportConfigCallback != null) + { + ConfigLinkViewerCallbacks.ExportConfigCallback(); + } + else + { + EditorUtility.DisplayDialog("提示", + "未注册导出配置回调。\n请在项目初始化代码中设置 ConfigLinkViewerCallbacks.ExportConfigCallback。", + "确定"); + } + }; + } + if (GUILayout.Button("刷新", GUILayout.Height(22), GUILayout.Width(60))) + { + ConfigLinkDatabase.ClearCache(); } EditorGUILayout.EndHorizontal(); - EditorGUILayout.LabelField("选择配置表查看其与其他表的关联关系", EditorStyles.miniLabel); + + string configStatus = ConfigLinkDatabase.HasConfigFile() + ? "已加载 (ConfigLinkData.json)" + : "未找到配置文件,请切换到[配置]标签页生成"; + var statusStyle = new GUIStyle(EditorStyles.miniLabel) { richText = true }; + EditorGUILayout.LabelField($"配置状态: {configStatus}", statusStyle); + + EditorGUILayout.EndVertical(); + } + + private void DrawConfigTab(float windowWidth, float windowHeight) + { + configScrollPos = EditorGUILayout.BeginScrollView(configScrollPos); + EditorGUILayout.BeginVertical("box"); + + EditorGUILayout.LabelField("项目配置", new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 }); + EditorGUILayout.Space(); + + DrawProjectSettings(); + EditorGUILayout.Space(); + + DrawExcelPathSettings(windowWidth); + EditorGUILayout.Space(); + + EditorGUILayout.BeginVertical("box"); + EditorGUILayout.LabelField("配置数据生成", EditorStyles.boldLabel); + EditorGUILayout.LabelField("首次使用或跨项目时,需要先生成配置数据。", EditorStyles.miniLabel); + EditorGUILayout.Space(); + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("生成配置", GUILayout.Height(30))) + { + EditorApplication.delayCall += () => + { + ConfigLinkDatabase.GenerateConfigFromExcelFolder(); + }; + } + if (GUILayout.Button("补全配置", GUILayout.Height(30))) + { + EditorApplication.delayCall += RunAIConfigScript; + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("生成配置:从 Excel 文件夹扫描生成 ConfigLinkData.json 骨架", EditorStyles.miniLabel); + EditorGUILayout.LabelField("补全配置:运行 Python 脚本自动分析引用关系,补全字段关联", EditorStyles.miniLabel); + + EditorGUILayout.EndVertical(); + + EditorGUILayout.EndVertical(); + EditorGUILayout.EndScrollView(); + } + + private GUIStyle _artStyle; + + private GUIStyle GetArtStyle(int fontSize) + { + if (_artStyle == null || _artStyle.fontSize != fontSize) + { + _artStyle = new GUIStyle(EditorStyles.label) + { + alignment = TextAnchor.MiddleCenter, + fontSize = fontSize, + richText = true, + wordWrap = false, + clipping = TextClipping.Overflow, + normal = { textColor = Color.white }, + }; + var font = Font.CreateDynamicFontFromOSFont("Consolas", fontSize); + if (font != null) + _artStyle.font = font; + } + return _artStyle; + } + + private void DrawLuoTianyiOverlay(float windowWidth, float windowHeight) + { + float scale = Mathf.Clamp(Mathf.Min(windowWidth, windowHeight) / 600f, 0.35f, 0.7f); + int fontSize = Mathf.RoundToInt(9 * scale); + float lineHeight = fontSize * 1.1f; + + string[] lines = { + " _________________", + " ____/:::::::::::::::::\\_____", + " __/::::::::::::::::::::::::::::\\___", + " _/:::::::::::::::::::::::::::::::::::\\__", + " _/::::::::::::::::::::::::::::::::::::::::\\_", + " /::::::::::::::::::::::::::::::::::::::::::::\\", + " |::::::::::::::::::::::::::::::::::::::::::::::\\", + " /::::::::::::::::::::::::::::::::::::::::::::::::\\", + " |:::/.:::::::;:::::::::::::::::::::::::::::::::::::|", + " /:::/.:::::::/..:::::::::::::::::::::::::::::::::::::\\", + " |:::|.::::::;/.::::::::::::::::::::::::::::::::::::::::|", + " |::/.::::::/..:::::::;;'.::::::::::::::::::::::::::::::|", + " |:|.::::/./.::::::;;/..:::::::::::::::::::::::::::::::::|", + " `:|.:::|.|.:::::;/..;;;;;;-'.:;;;-':::::::::::::::::::::|", + " \\|.:::|.|.:::;/.;;/ -..::'''...:::::::::::::::::::::::|", + " \\;;::|.|.::/.;/--__ |::::::::::::::::::::::::::::|", + " \\;;\\\\::/|/ =-__ --_ /::::::::::::::::::::::::::::::", + " \\/ /| -._ |.::::::::::::::::::::::::::::::", + " _.' /// /- ||::::::::::::::::::::::::::::::", + " _.-' //' ||::::::::::::::::::::::::::::::", + " | - `|::::::::::::::::::::::::::::::", + " \\ \\:::::::::::::::::::::::::::::", + " | \\:::::::::::::::::::::", + " / __/:::::::::::::::::::::::::::", + " \\ __/::;::;;:::::: ::::::::::", + " |` /;;;;/::| \\:::: :___: :::::::::", + " \\ |'_,::::/ \\ |:::: .| |`. :::::::::", + " / _/::::::/ / /:::: | \\.' | ::. .::::", + " | /.::;;:-'_)/_/::::: `._| .' ::..::::", + " ----.__ | |.::| \\___/::::::: ::..::::", + " :::::::`----\\_____ \\:::\\.-'::::::::::: ___:___ ::..::::", + " ;;;;;:::::::::::::`------ \\:::::::::::::::: ___|___ ::..::::", + " `-------:::::::\\ /:::::::::::::::: __| ::..::::", + " ___.--------'::::::::\\ |::::::::::::::::: | |-. ::..::::", + " :::::;;;:--:::::::::::| /::::::::::::::::: --' ' ::..::::", + " ----' _,-:.:::::::::::\\ |.::::::::::::::::: ::..::::", + " __/.::::::::::::::::| |.::::::::::::::::: : : ::..::::", + " __/.:::;;::::::::;/.:::| |.::::::::::::::::: | : ::. .::::", + " /.::::;/ /.:::::;/ |.::::| \\_.::::::::::::::: | . | :::::::::", + " :::::/ /.:::::/ /.:::::| \\__.:::::::::::: `.' ' :::::::::", + " ::::| |.:::::/ /.:::::.| \\,:::::::::::: ::::::::::", + " ::::| |.::::| |.:::::/| __/::::::::::::::::::::::::::::", + " \\.:::\\ \\.:::| |.::::||| __.--:::::::::::::::::::::::::::::", + " \\.:::\\_ \\.:::\\ \\.:::'/.:::::::::::::::::::::::::::::::::", + " \\.::::\\ \\.:::\\ \\.::::::::::::::::::::::::::::::::::::::::::" + }; + + float boxW = Mathf.Clamp(windowWidth * 0.45f, 300f, 500f); + float boxH = lines.Length * lineHeight + 16; + float boxX = windowWidth - boxW - 10; + float boxY = windowHeight - boxH - 10; + + var bgColor = new Color(0.12f, 0.12f, 0.16f, 0.9f); + EditorGUI.DrawRect(new Rect(boxX, boxY, boxW, boxH), bgColor); + + var borderColor = new Color(0f, 0.75f, 1f, 0.4f); + EditorGUI.DrawRect(new Rect(boxX, boxY, boxW, 1), borderColor); + EditorGUI.DrawRect(new Rect(boxX, boxY + boxH - 1, boxW, 1), borderColor); + EditorGUI.DrawRect(new Rect(boxX, boxY, 1, boxH), borderColor); + EditorGUI.DrawRect(new Rect(boxX + boxW - 1, boxY, 1, boxH), borderColor); + + GUIStyle artStyle = new GUIStyle(EditorStyles.label) + { + alignment = TextAnchor.MiddleCenter, + fontSize = fontSize, + richText = false, + wordWrap = false, + clipping = TextClipping.Overflow, + }; + artStyle.normal.textColor = new Color(0f, 1f, 1f); + + float y = boxY + 8; + for (int i = 0; i < lines.Length; i++) + { + var lineRect = new Rect(boxX + 4, y, boxW - 8, lineHeight); + GUI.Label(lineRect, lines[i], artStyle); + y += lineHeight; + } + } + + private void DrawProjectSettings() + { + EditorGUILayout.BeginVertical("box"); + EditorGUILayout.LabelField("路径设置", EditorStyles.boldLabel); + + EditorGUILayout.BeginHorizontal(); + string configPath = ConfigLinkDatabase.GetConfigFolderPath(); + EditorGUILayout.LabelField("Config目录:", GUILayout.Width(80)); + EditorGUILayout.LabelField(configPath, EditorStyles.textField, GUILayout.ExpandWidth(true)); + if (GUILayout.Button("选择", GUILayout.Width(60))) + { + string selected = EditorUtility.OpenFolderPanel("选择配置导出目录", Application.dataPath, ""); + if (!string.IsNullOrEmpty(selected)) + { + ConfigLinkDatabase.SetConfigFolderPath(selected); + } + } + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); } private void DrawTableSelector(float windowWidth, float windowHeight) { - EditorGUILayout.BeginVertical("box"); - - EditorGUILayout.LabelField("选择配置表:", EditorStyles.boldLabel); + EditorGUILayout.BeginVertical("box", GUILayout.Height(windowHeight - 60)); EditorGUILayout.BeginHorizontal(); searchFilter = EditorGUILayout.TextField("搜索:", searchFilter, EditorStyles.toolbarSearchField); showOnlyExisting = EditorGUILayout.ToggleLeft("只显示当前项目存在的表", showOnlyExisting); EditorGUILayout.EndHorizontal(); - EditorGUILayout.Space(); - DrawExcelPathSettings(windowWidth); - var allTables = showOnlyExisting ? ConfigLinkDatabase.GetExistingTables() : ConfigLinkDatabase.GetAllTableInfo(); var filteredTables = string.IsNullOrEmpty(searchFilter) ? allTables @@ -84,13 +403,12 @@ public class ConfigLinkViewerWindow : EditorWindow EditorGUILayout.Space(); - EditorGUILayout.BeginHorizontal(); + EditorGUILayout.BeginHorizontal(GUILayout.ExpandHeight(true)); float leftPanelWidth = Mathf.Clamp(windowWidth * 0.3f, 180f, 300f); float rightPanelWidth = windowWidth - leftPanelWidth - 20f; - float panelHeight = windowHeight - 150f; - EditorGUILayout.BeginVertical(GUILayout.Width(leftPanelWidth), GUILayout.Height(panelHeight)); + EditorGUILayout.BeginVertical(GUILayout.Width(leftPanelWidth), GUILayout.ExpandHeight(true)); EditorGUILayout.LabelField($"配置表列表 ({filteredTables.Count}):", EditorStyles.miniBoldLabel); relationScrollPos = EditorGUILayout.BeginScrollView(relationScrollPos, false, true, GUILayout.ExpandHeight(true)); @@ -103,16 +421,22 @@ public class ConfigLinkViewerWindow : EditorWindow if (GUILayout.Button(displayNames[i], buttonStyle)) { - selectedTableName = tableKeys[i]; - expandedRelations.Clear(); + if (selectedTableName != tableKeys[i]) + { + selectedTableName = tableKeys[i]; + expandedRelations.Clear(); + } } } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); - EditorGUILayout.BeginVertical("box", GUILayout.Width(rightPanelWidth), GUILayout.Height(panelHeight)); + EditorGUILayout.BeginVertical(GUILayout.Width(rightPanelWidth), GUILayout.ExpandHeight(true)); + EditorGUILayout.LabelField("当前表详情:", EditorStyles.miniBoldLabel); + detailScrollPos = EditorGUILayout.BeginScrollView(detailScrollPos, false, true, GUILayout.ExpandHeight(true)); DrawCurrentTableInfo(rightPanelWidth); + EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); @@ -121,12 +445,9 @@ public class ConfigLinkViewerWindow : EditorWindow private void DrawCurrentTableInfo(float panelWidth) { - EditorGUILayout.BeginScrollView(Vector2.zero, false, true, GUILayout.ExpandHeight(true)); - if (string.IsNullOrEmpty(selectedTableName)) { EditorGUILayout.LabelField("请从左侧选择一个配置表", EditorStyles.centeredGreyMiniLabel); - EditorGUILayout.EndScrollView(); return; } @@ -134,14 +455,13 @@ public class ConfigLinkViewerWindow : EditorWindow if (tableInfo == null) { EditorGUILayout.LabelField("未找到该表的配置信息", EditorStyles.helpBox); - EditorGUILayout.EndScrollView(); return; } EditorGUILayout.LabelField($"当前表: {tableInfo.displayName}", EditorStyles.boldLabel); EditorGUILayout.LabelField($"表名: {tableInfo.tableName}", EditorStyles.miniLabel); EditorGUILayout.LabelField($"描述: {tableInfo.description}", EditorStyles.miniLabel); - EditorGUILayout.LabelField($"状态: {(tableInfo.isExistInProject ? "存在于当前项目" : "当前项目不存在")}", + EditorGUILayout.LabelField($"状态: {(tableInfo.isExistInProject ? "存在于当前项目" : "当前项目不存在")}", tableInfo.isExistInProject ? EditorStyles.miniLabel : EditorStyles.helpBox); EditorGUILayout.Space(); @@ -174,14 +494,12 @@ public class ConfigLinkViewerWindow : EditorWindow } } } - - EditorGUILayout.EndScrollView(); } private void DrawOpenExcelButtons(float panelWidth) { EditorGUILayout.BeginHorizontal(); - + string excelPath = ConfigLinkDatabase.GetExcelFilePath(selectedTableName); bool canOpen = !string.IsNullOrEmpty(excelPath); @@ -238,10 +556,10 @@ public class ConfigLinkViewerWindow : EditorWindow { bool sourceExist = ConfigLinkDatabase.IsTableExist(rel.sourceTable); EditorGUILayout.BeginVertical("frameBox", GUILayout.Width(panelWidth - 10)); - + GUIStyle headerStyle = new GUIStyle(sourceExist ? EditorStyles.boldLabel : EditorStyles.helpBox) { wordWrap = false }; EditorGUILayout.LabelField($"{rel.sourceDisplayName} ({rel.sourceTable}) → {rel.fieldName}", headerStyle); - + GUIStyle labelStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = false }; EditorGUILayout.LabelField($"说明: {rel.description}", labelStyle); @@ -264,14 +582,17 @@ public class ConfigLinkViewerWindow : EditorWindow private void DrawExcelPathSettings(float windowWidth) { + EditorGUILayout.BeginVertical("box"); + EditorGUILayout.LabelField("Excel 文件夹", EditorStyles.boldLabel); + string currentPath = ConfigLinkDatabase.GetExcelFolderPath(); string displayPath = string.IsNullOrEmpty(currentPath) ? "未设置" : currentPath; EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField("Excel文件夹:", GUILayout.Width(70)); + EditorGUILayout.LabelField("路径:", GUILayout.Width(36)); EditorGUILayout.LabelField(displayPath, EditorStyles.textField, GUILayout.ExpandWidth(true)); - - if (GUILayout.Button("选择文件夹", GUILayout.Width(80))) + + if (GUILayout.Button("选择", GUILayout.Width(60))) { string selectedPath = EditorUtility.OpenFolderPanel("选择Excel文件夹", "", ""); if (!string.IsNullOrEmpty(selectedPath)) @@ -280,6 +601,7 @@ public class ConfigLinkViewerWindow : EditorWindow } } EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); } private void DrawRelation(FieldRelation relation, float panelWidth) @@ -333,21 +655,128 @@ public class ConfigLinkViewerWindow : EditorWindow EditorGUILayout.Space(2); } + private void DrawFormatExample(string format) + { + string example = ConfigLinkDatabase.FormatRelationValue(format, "1_2_3_4"); + if (example != "1_2_3_4") + { + GUIStyle exampleStyle = new GUIStyle(EditorStyles.miniLabel) { fontStyle = FontStyle.Italic }; + EditorGUILayout.LabelField($"示例: {example}", exampleStyle); + } + } + + private static void RunAIConfigScript() + { + string scriptPath = Path.Combine(Application.dataPath, "Editor/ConfigLinkViewer", "generate_config_link_data.py"); + if (!File.Exists(scriptPath)) + { + EditorUtility.DisplayDialog("提示", "未找到脚本文件:\n" + scriptPath, "确定"); + return; + } + + string pythonExe = FindPythonExecutable(); + if (string.IsNullOrEmpty(pythonExe)) + { + EditorUtility.DisplayDialog("提示", "未找到 Python 环境,请确保已安装 Python 并添加到 PATH", "确定"); + return; + } + + try + { + string configDir = ConfigLinkDatabase.GetConfigFolderPath(); + string tableInfoPath = Path.Combine(Application.dataPath, "Editor/ConfigLinkViewer", "table_info.json"); + string outputPath = Path.Combine(Application.dataPath, "Editor/ConfigLinkViewer", "ConfigLinkData.json"); + + string arguments = $"\"{scriptPath}\" --config-dir \"{Path.Combine(Application.dataPath, configDir)}\" --output \"{outputPath}\""; + if (File.Exists(tableInfoPath)) + { + arguments += $" --table-info \"{tableInfoPath}\""; + } + + var startInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = pythonExe, + Arguments = arguments, + WorkingDirectory = Path.GetDirectoryName(scriptPath), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + StandardOutputEncoding = System.Text.Encoding.UTF8, + StandardErrorEncoding = System.Text.Encoding.UTF8, + }; + + using (var process = System.Diagnostics.Process.Start(startInfo)) + { + string stdout = process.StandardOutput.ReadToEnd(); + string stderr = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode == 0) + { + Debug.Log("[ConfigLinkViewer] 补全配置完成:\n" + stdout); + ConfigLinkDatabase.ClearCache(); + AssetDatabase.Refresh(); + EditorUtility.DisplayDialog("完成", "补全配置已完成\n\n" + stdout, "确定"); + } + else + { + Debug.LogError("[ConfigLinkViewer] 补全配置失败:\n" + stderr); + EditorUtility.DisplayDialog("失败", "脚本执行失败:\n" + stderr, "确定"); + } + } + } + catch (Exception ex) + { + Debug.LogError("[ConfigLinkViewer] 运行脚本异常: " + ex.Message); + EditorUtility.DisplayDialog("异常", "运行脚本时发生异常:\n" + ex.Message, "确定"); + } + } + + private static string FindPythonExecutable() + { + string[] candidates = { "python", "python3", "py" }; + foreach (var candidate in candidates) + { + try + { + var startInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = candidate, + Arguments = "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + using (var process = System.Diagnostics.Process.Start(startInfo)) + { + process.WaitForExit(); + if (process.ExitCode == 0) + return candidate; + } + } + catch { } + } + return null; + } + private static void CleanExcelEmptyRows() { string excelFolder = ConfigLinkDatabase.GetExcelFolderPath(); if (string.IsNullOrEmpty(excelFolder)) { - var acf = MFrame.ConfigEditorCf.ins; - if (acf != null) + if (ConfigLinkViewerCallbacks.FallbackExcelPathCallback != null) { - excelFolder = MFrame.PathConf.project_root + acf.excel_path; + excelFolder = ConfigLinkViewerCallbacks.FallbackExcelPathCallback(); } } if (string.IsNullOrEmpty(excelFolder) || !Directory.Exists(excelFolder)) { - EditorUtility.DisplayDialog("提示", "Excel文件夹路径未设置或不存在,请先设置Excel文件夹", "确定"); + EditorUtility.DisplayDialog("提示", + "Excel文件夹路径未设置或不存在。\n请切换到[配置]标签页设置,或在 ConfigLinkViewerCallbacks.FallbackExcelPathCallback 中注册回退逻辑。", + "确定"); return; } @@ -416,91 +845,92 @@ public class ConfigLinkViewerWindow : EditorWindow } } - var sheetData = doc.Root?.Element(ns + "sheetData"); - if (sheetData == null) return 0; - - var rows = sheetData.Elements(ns + "row").ToList(); - var rowsToRemove = new List(); - - foreach (var row in rows) + List sharedStrings = null; + using (var archive = ZipFile.Open(filePath, ZipArchiveMode.Read)) { - if (!int.TryParse(row.Attribute("r")?.Value, out int rowNum)) continue; - if (rowNum <= headerRowCount) continue; - - var cells = row.Elements(ns + "c").ToList(); - if (cells.Count == 0) + var ssEntry = archive.GetEntry("xl/sharedStrings.xml"); + if (ssEntry != null) { - rowsToRemove.Add(row); - continue; - } - - var firstCell = cells[0]; - var valueElement = firstCell.Element(ns + "v"); - string val = valueElement?.Value?.Trim(); - - if (string.IsNullOrEmpty(val) || !int.TryParse(val, out _)) - { - rowsToRemove.Add(row); + using (var stream = ssEntry.Open()) + { + var ssDoc = XDocument.Load(stream); + sharedStrings = ssDoc.Root.Elements(ns + "si") + .Select(si => si.Value) + .ToList(); + } } } - if (rowsToRemove.Count == 0) return 0; + var sheetData = doc.Root.Element(ns + "sheetData"); + if (sheetData == null) return 0; - foreach (var row in rowsToRemove) + var rows = sheetData.Elements(ns + "row").ToList(); + if (rows.Count <= headerRowCount) return 0; + + var emptyRows = new List(); + + for (int i = headerRowCount; i < rows.Count; i++) + { + var row = rows[i]; + var cells = row.Elements(ns + "c").ToList(); + + if (cells.Count == 0) + { + emptyRows.Add(row); + continue; + } + + bool allEmpty = true; + foreach (var cell in cells) + { + var value = cell.Element(ns + "v"); + if (value != null && !string.IsNullOrWhiteSpace(value.Value)) + { + allEmpty = false; + break; + } + + var cellType = cell.Attribute("t")?.Value; + if (cellType == "s" && value != null) + { + if (int.TryParse(value.Value, out int ssIdx) && sharedStrings != null && ssIdx < sharedStrings.Count) + { + if (!string.IsNullOrWhiteSpace(sharedStrings[ssIdx])) + { + allEmpty = false; + break; + } + } + } + } + + if (allEmpty) + { + emptyRows.Add(row); + } + } + + if (emptyRows.Count == 0) return 0; + + foreach (var row in emptyRows) { row.Remove(); } - var remainingRows = sheetData.Elements(ns + "row").ToList(); - int newRowIndex = 1; - foreach (var row in remainingRows) - { - row.SetAttributeValue("r", newRowIndex); - foreach (var cell in row.Elements(ns + "c")) - { - string cellRef = cell.Attribute("r")?.Value; - if (!string.IsNullOrEmpty(cellRef)) - { - string colPart = Regex.Replace(cellRef, @"\d", ""); - cell.SetAttributeValue("r", colPart + newRowIndex); - } - } - newRowIndex++; - } - using (var archive = ZipFile.Open(filePath, ZipArchiveMode.Update)) { - var entry = archive.GetEntry("xl/worksheets/sheet1.xml"); - if (entry != null) entry.Delete(); - var newEntry = archive.CreateEntry("xl/worksheets/sheet1.xml"); - using (var stream = newEntry.Open()) + var sheetEntry = archive.GetEntry("xl/worksheets/sheet1.xml"); + if (sheetEntry != null) { - doc.Save(stream); + sheetEntry.Delete(); + var newEntry = archive.CreateEntry("xl/worksheets/sheet1.xml"); + using (var stream = newEntry.Open()) + { + doc.Save(stream); + } } } - Debug.Log($"[清理空行] {Path.GetFileName(filePath)}: 删除了 {rowsToRemove.Count} 行空数据"); - return rowsToRemove.Count; - } - - private static readonly Dictionary FormatExamples = new Dictionary - { - { "item_id_num", ("1001_10_5", "类型:1001, ID:10, 数量:5") }, - { "type_id_num", ("1001_10_5", "类型:1001, ID:10, 数量:5") }, - { "id_pos_lv", ("100_1_30", "ID:100, 位置:1, 等级:30") }, - { "id_lv_num", ("100_30_5", "ID:100, 等级:30, 数量:5") }, - { "id_lv_num_time",("100_30_5_120","ID:100, 等级:30, 数量:5, 时间:120秒") }, - { "buffid_lv", ("5_3", "BuffID:5, 等级:3") }, - { "rune_id_num", ("10_2", "符文ID:10, 数量:2") }, - { "equip_id_num", ("20_1", "装备ID:20, 数量:1") }, - }; - - private void DrawFormatExample(string format) - { - if (FormatExamples.TryGetValue(format.ToLower(), out var info)) - { - var style = new GUIStyle(EditorStyles.miniLabel) { wordWrap = false, richText = true }; - EditorGUILayout.LabelField($"示例: {info.example} → {info.desc}", style); - } + return emptyRows.Count; } } diff --git a/Unity/配置表联动查看器/ConfigLinkViewer/generate_config_link_data.py b/Unity/配置表联动查看器/ConfigLinkViewer/generate_config_link_data.py new file mode 100644 index 0000000..893653a --- /dev/null +++ b/Unity/配置表联动查看器/ConfigLinkViewer/generate_config_link_data.py @@ -0,0 +1,679 @@ +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() + for part in ai_prompt_parts: + print(part) + print() + print("请直接返回修改后的 JSON 配置。") + + print() + + +if __name__ == "__main__": + main() diff --git a/Unity/配置表联动查看器/ConfigLinkViewer/settings.json b/Unity/配置表联动查看器/ConfigLinkViewer/settings.json new file mode 100644 index 0000000..a013485 --- /dev/null +++ b/Unity/配置表联动查看器/ConfigLinkViewer/settings.json @@ -0,0 +1,50 @@ +{ + "configFolderPath": "Resources/Resources_moved/config", + "excelFolderPath": "", + "formatTemplates": { + "item_id_num": { + "parts": ["类型", "ID", "数量"], + "minParts": 3 + }, + "type_id_num": { + "parts": ["类型", "ID", "数量"], + "minParts": 3 + }, + "id_pos_lv": { + "parts": ["ID", "位置", "等级"], + "minParts": 3 + }, + "id_lv_num": { + "parts": ["ID", "等级", "数量"], + "minParts": 3 + }, + "id_lv_num_time": { + "parts": ["ID", "等级", "数量", "时间"], + "minParts": 4 + }, + "buffid_lv": { + "parts": ["BuffID", "等级"], + "minParts": 2 + }, + "rune_id_num": { + "parts": ["符文ID", "数量"], + "minParts": 2 + }, + "equip_id_num": { + "parts": ["装备ID", "数量"], + "minParts": 2 + }, + "hero_id": { + "parts": ["英雄ID"], + "minParts": 1 + }, + "id_lv_count_delay": { + "parts": ["ID", "等级", "数量", "延迟"], + "minParts": 4 + }, + "id_lv_num_hp": { + "parts": ["ID", "等级", "数量", "血量%"], + "minParts": 4 + } + } +} diff --git a/Unity/配置表联动查看器/ConfigLinkViewer/table_info.json b/Unity/配置表联动查看器/ConfigLinkViewer/table_info.json new file mode 100644 index 0000000..6789f29 --- /dev/null +++ b/Unity/配置表联动查看器/ConfigLinkViewer/table_info.json @@ -0,0 +1,85 @@ +{ + "entries": [ + {"name": "achievement", "displayName": "成就", "description": "成就系统配置"}, + {"name": "activity", "displayName": "活动", "description": "活动配置"}, + {"name": "activityreward", "displayName": "活动奖励", "description": "活动奖励配置"}, + {"name": "arenaaward", "displayName": "竞技场奖励", "description": "竞技场排名奖励配置"}, + {"name": "attribute", "displayName": "属性", "description": "属性类型定义"}, + {"name": "box", "displayName": "宝箱", "description": "宝箱掉落配置"}, + {"name": "buff", "displayName": "增益效果", "description": "法宝增益效果配置"}, + {"name": "callstrength", "displayName": "召唤概率", "description": "召唤概率等级配置"}, + {"name": "checkpointreward", "displayName": "关卡奖励", "description": "关卡通关奖励配置"}, + {"name": "collectionreward", "displayName": "收集奖励", "description": "图鉴收集进度奖励配置"}, + {"name": "common", "displayName": "通用配置", "description": "通用键值对配置"}, + {"name": "drawlinereward", "displayName": "抽奖奖励", "description": "抽奖线奖励配置"}, + {"name": "enemy", "displayName": "敌人", "description": "敌人基础属性配置"}, + {"name": "enemylevel", "displayName": "敌人等级", "description": "敌人等级成长属性配置"}, + {"name": "equip", "displayName": "装备", "description": "装备基础属性配置"}, + {"name": "equipAttrRandom", "displayName": "装备随机属性", "description": "装备随机属性词条配置"}, + {"name": "equiplevel", "displayName": "装备等级", "description": "装备等级成长属性配置"}, + {"name": "equipstage", "displayName": "装备阶段", "description": "装备进阶配置"}, + {"name": "fight_arena", "displayName": "竞技场战斗", "description": "竞技场战斗关卡配置"}, + {"name": "fight_fb1", "displayName": "副本1-限时救援", "description": "限时救援副本战斗配置"}, + {"name": "fight_fb2", "displayName": "副本2-首领挑战", "description": "首领挑战副本战斗配置"}, + {"name": "fight_fb3", "displayName": "副本3-波次挑战", "description": "波次挑战副本战斗配置"}, + {"name": "fight_fb4", "displayName": "副本4-英灵神殿", "description": "英灵神殿副本战斗配置"}, + {"name": "fight_sample", "displayName": "主线关卡", "description": "主线关卡战斗配置"}, + {"name": "fishingreward", "displayName": "钓鱼奖励", "description": "钓鱼玩法奖励配置"}, + {"name": "funopencondition", "displayName": "功能开放条件", "description": "系统功能开放等级条件配置"}, + {"name": "gacha", "displayName": "召唤", "description": "英雄召唤卡池配置"}, + {"name": "gachareward", "displayName": "召唤累计奖励", "description": "召唤次数累计奖励配置"}, + {"name": "game2048reward", "displayName": "2048游戏奖励", "description": "2048小游戏积分奖励配置"}, + {"name": "guide", "displayName": "引导", "description": "新手引导对话配置"}, + {"name": "guildbox", "displayName": "公会宝箱", "description": "公会宝箱奖励配置"}, + {"name": "guilddonate", "displayName": "公会捐赠", "description": "公会捐赠配置"}, + {"name": "guildlevel", "displayName": "公会等级", "description": "公会等级升级配置"}, + {"name": "guildname", "displayName": "公会名称", "description": "随机公会名称库"}, + {"name": "hero", "displayName": "英雄", "description": "英雄基础属性配置"}, + {"name": "herolevel", "displayName": "英雄等级", "description": "英雄等级成长属性配置"}, + {"name": "herolevelinternal", "displayName": "英雄内部等级", "description": "英雄内部等级属性配置"}, + {"name": "herostage", "displayName": "英雄阶段", "description": "英雄进阶配置"}, + {"name": "idlereward", "displayName": "挂机奖励", "description": "挂机收益奖励配置"}, + {"name": "language", "displayName": "多语言", "description": "多语言文本配置"}, + {"name": "link", "displayName": "羁绊", "description": "英雄羁绊组合配置"}, + {"name": "mail", "displayName": "邮件", "description": "邮件模板配置"}, + {"name": "mall", "displayName": "商店", "description": "商店商品配置"}, + {"name": "map", "displayName": "地图", "description": "地图格子配置"}, + {"name": "map1", "displayName": "地图1", "description": "地图1格子配置"}, + {"name": "map2", "displayName": "地图2", "description": "地图2格子配置"}, + {"name": "map3", "displayName": "地图3", "description": "地图3格子配置"}, + {"name": "map_act", "displayName": "地图波次1", "description": "副本出怪波次配置(主线/副本1/副本4)"}, + {"name": "map_act2", "displayName": "地图波次2", "description": "首领挑战出怪波次配置"}, + {"name": "map_act3", "displayName": "地图波次3", "description": "波次挑战出怪波次配置"}, + {"name": "map_act4", "displayName": "地图波次4", "description": "竞技场出怪波次配置"}, + {"name": "monthlycard", "displayName": "月卡", "description": "月卡特权配置"}, + {"name": "name", "displayName": "随机名称", "description": "随机角色名称库"}, + {"name": "outenemypoint", "displayName": "外部敌人点", "description": "外部敌人刷新点配置"}, + {"name": "pandora", "displayName": "礼包", "description": "潘多拉礼包配置"}, + {"name": "pass", "displayName": "通行证", "description": "通行证奖励配置"}, + {"name": "player", "displayName": "玩家", "description": "玩家(宗门)基础属性配置"}, + {"name": "playerlevel", "displayName": "玩家等级", "description": "玩家等级成长属性配置"}, + {"name": "playerskin", "displayName": "玩家皮肤", "description": "玩家(宗门)皮肤配置"}, + {"name": "playerstage", "displayName": "玩家阶段", "description": "玩家(宗门)进阶配置"}, + {"name": "playertalent", "displayName": "玩家天赋", "description": "玩家天赋系统配置"}, + {"name": "prop", "displayName": "道具", "description": "道具基础属性配置"}, + {"name": "pushboxreward", "displayName": "推箱子奖励", "description": "推箱子关卡奖励配置"}, + {"name": "qirilibao", "displayName": "七日礼包", "description": "七日礼包奖励配置"}, + {"name": "quest", "displayName": "任务", "description": "每日/每周任务配置"}, + {"name": "recharge", "displayName": "充值", "description": "充值档位配置"}, + {"name": "rechargegift", "displayName": "充值礼包", "description": "累充礼包奖励配置"}, + {"name": "rune", "displayName": "符文", "description": "符文基础属性配置"}, + {"name": "runelevel", "displayName": "符文等级", "description": "符文等级成长属性配置"}, + {"name": "scene", "displayName": "场景", "description": "游戏场景配置"}, + {"name": "serverError", "displayName": "服务器错误", "description": "服务器错误码多语言配置"}, + {"name": "signin", "displayName": "签到", "description": "每日签到奖励配置"}, + {"name": "skill", "displayName": "技能", "description": "技能基础属性配置"}, + {"name": "skilllevel", "displayName": "技能等级", "description": "技能等级属性配置"}, + {"name": "systemopen", "displayName": "系统开放", "description": "系统功能开放等级配置"}, + {"name": "test_map_act", "displayName": "测试波次", "description": "测试用出怪波次配置"}, + {"name": "title", "displayName": "称号", "description": "玩家称号配置"}, + {"name": "trainbreak", "displayName": "训练突破", "description": "训练突破阶段配置"}, + {"name": "trainlevel", "displayName": "训练等级", "description": "训练等级属性配置"}, + {"name": "vip", "displayName": "VIP", "description": "VIP等级特权配置"}, + {"name": "dirty_words", "displayName": "敏感词", "description": "敏感词过滤库"} + ] +} diff --git a/Unity/配置表联动查看器/README.md b/Unity/配置表联动查看器/README.md index f6ee855..d9a0744 100644 --- a/Unity/配置表联动查看器/README.md +++ b/Unity/配置表联动查看器/README.md @@ -4,75 +4,73 @@ ## 功能特性 -### 1. 配置表列表 -- 支持搜索过滤配置表 -- 显示表是否存在于当前项目 -- 支持 TXT 文件(如敏感词表) +- **配置表列表**:搜索过滤、存在性检测、支持 TXT 文件 +- **联动关系查看**:正向关联和反向引用查询,支持跳转到关联表 +- **数据格式可视化**:自动解析 item_id_num、id_lv_num 等复合格式 +- **Excel 操作**:一键打开配置表,批量打开关联表 +- **导出配置**:批量导出为 JSON 和 C# 代码 +- **清理空行**:自动检测并删除 Excel 中 id 列为空的垃圾数据行,支持 sharedStrings 解析 +- **跨项目适配**:基于 `ConfigLinkData.json`,一键生成骨架并由 AI 补全 -### 2. 联动关系查看 -- 查看当前表关联哪些其他表 -- 显示字段联动关系详情 -- 支持跳转到关联表 +## 安装 -### 3. 反向查询 -- 查看哪些表引用了当前表 -- 快速定位依赖关系 +1. 复制 `ConfigLinkViewer` 文件夹到目标项目的 `Assets/Editor/` 下 +2. 或使用 Git 子模块: + ```bash + git submodule add Assets/Editor/ConfigLinkViewer + ``` -### 4. 数据格式可视化 -- 自动解析数据格式 -- 显示格式示例和说明 -- 支持多种格式:item_id_num, id_pos_lv, id_lv_num 等 +## 快速上手 -### 5. Excel 操作 -- 一键打开配置表 Excel 文件 -- 批量打开所有关联表 +Unity 编辑器菜单:`Tools → 配置表联动查看器` -### 6. 导出配置 -- 一键将 Excel 配置表批量导出为 JSON 和 C# 代码 -- 通过 Tools → ConfigDeal → 导出配置 或窗口右上角按钮触发 +### 按钮说明 -### 7. 清理 Excel 空行 -- 自动扫描 Excel 文件夹中所有 .xlsx 文件 -- 检测并删除数据区域中第一列(id列)为空或非数字的行 -- 直接修改 Excel 源文件,清理后可重新导出配置 -- 通过窗口右上角"清理Excel空行"按钮触发 +**日常数据维护:** -### 8. 跨项目适配 -- 自动检测项目中存在的配置表 -- 支持不同项目配置不同路径 - -## 安装指南 - -### 方法一:复制文件 -1. 复制 `ConfigLinkViewer` 文件夹到目标项目 -2. 路径:`Assets/Editor/ConfigLinkViewer/` - -### 方法二:Git 子模块 -```bash -git submodule add Assets/Editor/ConfigLinkViewer -``` - -## 使用说明 - -### 打开工具 -在 Unity 编辑器菜单中点击:`Tools → 配置表联动查看器` - -### 设置 Excel 路径 -1. 点击"选择文件夹"按钮 -2. 选择包含配置表 Excel 文件的文件夹 -3. 路径会自动保存 - -### 基本操作 -| 操作 | 说明 | +| 按钮 | 说明 | |------|------| -| 单击列表项 | 选中配置表,查看详情 | -| 点击"打开表格" | 打开当前表的 Excel 文件 | -| 点击"批量打开关联表" | 打开所有关联的配置表 | -| 勾选"显示反向引用" | 查看哪些表引用了当前表 | -| 点击"清理Excel空行" | 清除 Excel 中 id 列为空的垃圾数据行 | -| 点击"导出配置" | 将 Excel 批量导出为 JSON 和 C# 代码 | +| 清理Excel空行 | 扫描 Excel 文件夹,删除 id 列为空或非数字的垃圾数据行 | +| 导出配置 | 将 Excel 批量导出为游戏运行时的 JSON 和 C# 代码 | +| 打开表格 | 打开当前选中表的 Excel 文件 | +| 批量打开关联表 | 打开所有关联的配置表 | -### 数据格式说明 +**查看器配置(首次使用或跨项目时):** + +| 按钮 | 说明 | +|------|------| +| 生成配置 | 从 Excel 文件夹扫描生成 `ConfigLinkData.json` 骨架文件(仅表名) | +| 补全配置 | 运行 Python 脚本自动识别字段引用关系,补全 `ConfigLinkData.json` | +| 刷新 | 重新加载 `ConfigLinkData.json` | +| 显示反向引用 | 查看哪些表引用了当前选中的表 | + +## 使用流程 + +### 流程一:首次配置 / 跨项目使用 + +将本工具引入新项目时,需要初始化 `ConfigLinkData.json`: + +1. 点击"选择文件夹",设置 Excel 文件夹路径 +2. 点击 **"生成配置"** — 扫描 Excel 文件夹,生成骨架(只有表名,没有引用关系) +3. 点击 **"补全配置"** — 运行 Python 脚本自动分析 config JSON,补全引用关系和中文名 +4. 在左侧列表点击表名,查看联动关系是否准确 +5. 如有遗漏或不准确,手动编辑 `ConfigLinkData.json` 后点击"刷新" + +### 流程二:日常数据维护 + +策划修改 Excel 后,需要导出给程序使用: + +1. 点击 **"清理Excel空行"** — 清除 Excel 中 id 列为空的垃圾行 +2. 点击 **"导出配置"** — 将 Excel 批量导出为 JSON 和 C# 代码 + +### 流程三:查看联动关系 + +在左侧列表选中一张表,右侧面板会显示: +- 正向关联:该表引用了哪些其他表 +- 反向引用(勾选"显示反向引用"):哪些表引用了该表 +- 数据格式示例和说明 + +## 数据格式说明 | 格式 | 示例 | 说明 | |------|------|------| @@ -85,65 +83,69 @@ git submodule add Assets/Editor/ConfigLinkViewer | rune_id_num | 10_2 | 符文ID:10, 数量:2 | | equip_id_num | 20_1 | 装备ID:20, 数量:1 | -## 跨项目复用 +## ConfigLinkData.json 格式 -### 自动检测 -工具会自动检测目标项目中的配置表: -- 检查 `Resources/Resources_moved/config/` 目录 -- 检查 `Resources/config/` 目录 -- TXT 文件检查 Excel 文件夹 - -### 添加新配置表 -编辑 `ConfigLinkDatabase.cs`,在 `GetAllTableInfo()` 方法中添加新表: - -```csharp -new ConfigTableInfo { - tableName = "new_table", - displayName = "新表", - description = "新表描述", - relations = new List { - new FieldRelation { - fieldName = "field1", - targetTable = "target_table", - targetField = "id", - description = "关联说明" +```json +{ + "tables": [ + { + "name": "item", + "displayName": "道具表", + "description": "道具基础配置", + "fileExtension": ".xlsx", + "relations": [ + { + "field": "rewards", + "target": "monster", + "targetField": "id", + "format": "type_id_num", + "description": "掉落奖励" + } + ] } - } + ] } ``` +| 字段 | 必填 | 说明 | +|------|------|------| +| name | 是 | 表名(对应 Excel 文件名,不含扩展名) | +| displayName | 否 | 显示名称,不填则用 name | +| description | 否 | 表描述 | +| fileExtension | 否 | 文件扩展名,默认 `.xlsx`,可设为 `.txt` | +| relations | 否 | 联动关系列表 | + +**relations 字段:** + +| 字段 | 必填 | 说明 | +|------|------|------| +| field | 是 | 当前表的字段名 | +| target | 是 | 引用的目标表名 | +| targetField | 否 | 目标表的字段名,默认 `id` | +| format | 否 | 数据格式,如 `type_id_num` | +| description | 否 | 关系描述 | + ## 文件结构 ``` Assets/Editor/ConfigLinkViewer/ -├── ConfigLinkDatabase.cs # 配置表数据和联动关系存储 -├── ConfigLinkViewerWindow.cs # Unity编辑器窗口界面(含导出配置、清理空行功能) -└── README.md # 使用文档 +├── ConfigLinkViewerWindow.cs # Unity 编辑器窗口界面 + 回调注册容器 +├── ConfigLinkDatabase.cs # 配置表数据、路径管理、格式模板(数据驱动) +├── generate_config_link_data.py # Python 脚本,支持命令行参数,自动识别字段引用关系 +├── ConfigLinkData.json # 配置文件(生成骨架后由 AI 补全) +├── settings.json # 项目路径 + 格式模板配置(零代码扩展) +├── table_info.json # 配置表中文名/描述(Python 和 C# 共用) +└── README.md # 使用文档 ``` -## 支持的配置表 - -工具包含以下类型的配置表支持: -- 活动系统:activity, activityreward -- 成就系统:achievement -- 战斗系统:fight_fb1~4, fight_arena, fight_sample -- 英雄系统:hero, herolevel, herostage -- 道具系统:prop, mall -- 装备系统:equip, equiplevel, equipstage -- 技能系统:skill, skilllevel -- 符文系统:rune, runelevel -- 任务系统:quest -- VIP系统:vip, recharge -- 服务端文件:dirty_words.txt, 奖励格式说明.txt - ## 注意事项 1. Excel 文件命名必须与配置表名一致(如 `enemy.xlsx`) -2. 路径设置后会自动保存到 PlayerPrefs -3. 建议将 Excel 文件夹设置为版本控制忽略 -4. TXT 文件需要放置在 Excel 文件夹中 -5. 推荐工作流:先点击"清理Excel空行"清除垃圾数据,再点击"导出配置"生成 JSON 和 C# 代码 -6. 清理空行会直接修改 Excel 源文件,建议操作前确认已提交到版本控制 +2. 路径设置后自动保存到 EditorUserSettings(多项目隔离,互不覆盖) +3. 清理空行会直接修改 Excel 源文件,建议操作前确认已提交到版本控制 +4. 清理空行支持 Excel 共享字符串(sharedStrings)解析,可正确处理 `t="s"` 类型单元格 +5. 补全配置功能需要 Python 环境,脚本位于 `generate_config_link_data.py` +6. 编辑 `ConfigLinkData.json` 后点击"刷新"按钮重新加载 ## 更新日志 @@ -155,9 +157,57 @@ Assets/Editor/ConfigLinkViewer/ - 左右面板自适应布局,窗口默认 900×600 - 一键打开 Excel 文件,支持批量打开关联表 - Excel 文件夹路径设置,自动保存到 PlayerPrefs -- 跨项目适配,自动检测项目中存在的配置表 +- 跨项目适配,点击[生成配置]扫描 Excel 文件夹生成骨架,AI 补全中文名和关联关系 - "导出配置"按钮,一键将 Excel 批量导出为 JSON 和 C# 代码 -- "清理Excel空行"功能,自动检测并删除 Excel 中 id 列为空的垃圾数据行,直接操作 xlsx 内部 XML,兼容性好 +- "清理Excel空行"功能,自动检测并删除 Excel 中 id 列为空的垃圾数据行 +- "AI补全配置"按钮,运行 Python 脚本自动识别字段引用关系 + +### v1.0.1 +- "AI补全配置"按钮更名为"补全配置",按钮宽度和提示文案同步调整 +- 修复清理 Excel 空行时未正确解析共享字符串(sharedStrings)导致部分空行清理不干净的问题 +- 新增 `t="s"`(共享字符串引用)和 `t="inlineStr"`(内联字符串)两种单元格类型解析 +- 提取 `GetCellActualValue()` 方法,统一单元格值解析逻辑 +- README 全面优化:精简功能特性描述、重组文档结构、新增查漏补缺指南和二次校验 Prompt + +### v2.0.1 + +**UI 优化:** +- 调整 ASCII 艺术画样式,支持自定义图案替换 +- 优化艺术画显示尺寸,支持更宽更矮的布局 +- 修复文字颜色显示问题,确保 ASCII 艺术画正确渲染 + +### v2.0.0 — 零依赖重构(大版本) + +**核心目标:** 拖入任意 Unity 项目即用,零外部依赖,配置驱动。 + +**破坏性变更:** +- 移除所有 `MFrame` 直接引用,编译不再依赖任何外部框架 +- Excel 路径存储从 `PlayerPrefs` 迁移至 `EditorUserSettings`(自动多项目隔离,旧路径需重新设置) +- Python 脚本入口改为 `argparse` 参数化调用(`--config-dir` / `--output` / `--table-info`) + +**新增文件:** +- `settings.json` — 项目路径 + 格式模板配置,新增格式类型只需改 JSON,重启即生效 +- `table_info.json` — 78 张配置表的中文名/描述,Python 和 C# 共用同一份数据源 + +**UI 重构:** +- 窗口拆分为「查看」和「配置」两个标签页,日常操作和初始化设置分离 +- 「查看」页顶部精简为 3 个按钮(清理Excel空行、导出配置、刷新),按钮大小和垂直对齐统一 +- 「配置」页集中展示路径设置、Excel 文件夹、配置数据生成(生成/补全) +- 配置标签页底部展示洛天依 ASCII 彩蛋 + +**框架自动适配:** +- 新增 `ConfigLinkViewerAutoSetup`(`[InitializeOnLoad]`),编辑器启动时通过**反射**自动检测项目中的 MFrame 框架 +- 检测到 `MFrame.ConfigDeal.ExportConfig` → 自动绑定「导出配置」回调 +- 检测到 `MFrame.PathConf` + `MFrame.ConfigEditorCf` → 自动绑定 Excel 路径回退 +- 未检测到框架 → 静默跳过,用户无需任何配置,路径通过 UI 手动设置即可 + +**改动详情:** +- `ConfigLinkViewerCallbacks` 静态回调类保留,支持外部手动注册覆盖自动检测结果 +- `ConfigLinkDatabase.cs` — 格式解析从 switch-case 改为反射加载 `settings.json` 中的 `FormatTemplatesWrapper`,零代码扩展 +- `ConfigLinkDatabase.cs` — `MarkExistingTables` / `IsTableExistInConfigFolder` 的配置路径改为从 `settings.json` 的 `configFolderPath` 读取 +- `ConfigLinkViewerWindow.cs` — Python 调用侧同步传入 `--config-dir` 和 `--table-info` 参数 +- `generate_config_link_data.py` — 移除硬编码 `TABLE_INFO` 字典,改为从 `table_info.json` 加载 +- 清理 `__pycache__/` 缓存目录和已完成的优化方案文档 ## 许可证