修改配置的插件

This commit is contained in:
ShallowT1Dream
2026-06-02 14:11:28 +08:00
parent 9a0faaccd7
commit d4fbef751a
6 changed files with 1895 additions and 574 deletions

View File

@@ -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<ConfigLinkTableEntry> tables = new List<ConfigLinkTableEntry>();
}
[Serializable]
public class ConfigLinkTableEntry
{
public string name;
public string displayName;
public string description;
public string fileExtension;
public List<ConfigLinkRelation> relations = new List<ConfigLinkRelation>();
}
[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<ConfigTableInfo> _cachedData;
private static HashSet<string> _existingTableNames;
private static string _excelFolderPath;
private static SettingsData _settings;
private static Dictionary<string, FormatTemplate> _formatTemplateMap;
private static Dictionary<string, (string displayName, string description)> _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<ConfigTableInfo>
{
new ConfigTableInfo { tableName = "activity", displayName = "活动表", description = "活动配置表" },
new ConfigTableInfo { tableName = "activityreward", displayName = "活动奖励表", description = "活动奖励配置", relations = new List<FieldRelation> {
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<FieldRelation> {
new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励(类型_id_数量)" }
}},
new ConfigTableInfo { tableName = "arenaaward", displayName = "竞技场奖励表", description = "竞技场排名奖励", relations = new List<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }
}},
new ConfigTableInfo { tableName = "common", displayName = "通用配置表", description = "通用配置项", relations = new List<FieldRelation> {
new FieldRelation { fieldName = "key", targetTable = "language", targetField = "key", description = "多语言key" }
}},
new ConfigTableInfo { tableName = "drawlinereward", displayName = "画线奖励表", description = "画线游戏奖励", relations = new List<FieldRelation> {
new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }
}},
new ConfigTableInfo { tableName = "enemy", displayName = "敌人表", description = "敌人基础配置", relations = new List<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
new FieldRelation { fieldName = "rewards", targetTable = "hero", targetField = "id", description = "抽卡获得的英雄" }
}},
new ConfigTableInfo { tableName = "gachareward", displayName = "抽卡奖励表", description = "抽卡奖励配置", relations = new List<FieldRelation> {
new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }
}},
new ConfigTableInfo { tableName = "game2048reward", displayName = "2048游戏奖励表", description = "2048游戏奖励配置", relations = new List<FieldRelation> {
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<FieldRelation> {
new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }
}},
new ConfigTableInfo { tableName = "guilddonate", displayName = "公会捐献表", description = "公会捐献配置", relations = new List<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
new FieldRelation { fieldName = "hero_ids", targetTable = "hero", targetField = "id", description = "英雄ID列表" }
}},
new ConfigTableInfo { tableName = "mail", displayName = "邮件表", description = "邮件配置", relations = new List<FieldRelation> {
new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }
}},
new ConfigTableInfo { tableName = "mall", displayName = "商店表", description = "商店商品配置", relations = new List<FieldRelation> {
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<FieldRelation> {
new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num", description = "初始怪物" }
}},
new ConfigTableInfo { tableName = "map1", displayName = "地图1表", description = "地图1房间配置", relations = new List<FieldRelation> {
new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num", description = "初始怪物" }
}},
new ConfigTableInfo { tableName = "map2", displayName = "地图2表", description = "地图2房间配置", relations = new List<FieldRelation> {
new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num", description = "初始怪物" }
}},
new ConfigTableInfo { tableName = "map3", displayName = "地图3表", description = "地图3房间配置", relations = new List<FieldRelation> {
new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num", description = "初始怪物" }
}},
new ConfigTableInfo { tableName = "map_act", displayName = "出怪表", description = "关卡出怪配置", relations = new List<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num", description = "初始怪物" }
}},
new ConfigTableInfo { tableName = "monthlycard", displayName = "月卡表", description = "月卡配置", relations = new List<FieldRelation> {
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<FieldRelation> {
new FieldRelation { fieldName = "monsters", targetTable = "enemy", targetField = "id", relationFormat = "id_lv_num", description = "怪物" }
}},
new ConfigTableInfo { tableName = "pandora", displayName = "潘多拉表", description = "潘多拉奖励池配置", relations = new List<FieldRelation> {
new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励池" }
}},
new ConfigTableInfo { tableName = "pass", displayName = "通行证表", description = "通行证奖励配置", relations = new List<FieldRelation> {
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<FieldRelation> {
new FieldRelation { fieldName = "equips", targetTable = "equip", targetField = "id", description = "初始装备" }
}},
new ConfigTableInfo { tableName = "playerlevel", displayName = "玩家等级表", description = "玩家等级成长配置", relations = new List<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }
}},
new ConfigTableInfo { tableName = "qirilibao", displayName = "七日礼包表", description = "七日礼包配置", relations = new List<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "奖励" }
}},
new ConfigTableInfo { tableName = "rune", displayName = "符文表", description = "符文基础配置", relations = new List<FieldRelation> {
new FieldRelation { fieldName = "attr_id", targetTable = "attribute", targetField = "id", description = "属性ID" }
}},
new ConfigTableInfo { tableName = "runelevel", displayName = "符文等级表", description = "符文等级配置", relations = new List<FieldRelation> {
new FieldRelation { fieldName = "target_id", targetTable = "rune", targetField = "id", description = "关联符文" }
}},
new ConfigTableInfo { tableName = "scene", displayName = "场景表", description = "场景配置", relations = new List<FieldRelation> {
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<FieldRelation> {
new FieldRelation { fieldName = "rewards", targetTable = "prop", targetField = "id", relationFormat = "type_id_num", description = "签到奖励" }
}},
new ConfigTableInfo { tableName = "skill", displayName = "技能表", description = "技能配置", relations = new List<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
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<FieldRelation> {
new FieldRelation { fieldName = "consumes", targetTable = "prop", targetField = "id", relationFormat = "item_id_num", description = "修炼消耗" }
}},
new ConfigTableInfo { tableName = "vip", displayName = "VIP表", description = "VIP等级配置", relations = new List<FieldRelation> {
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<FieldRelation> {
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<ConfigTableInfo> LoadConfigFromFile(string path)
{
if (!File.Exists(path))
{
Debug.LogWarning("[ConfigLinkViewer] 配置文件不存在,请点击[生成配置]按钮生成: " + path);
return new List<ConfigTableInfo>();
}
try
{
string json = File.ReadAllText(path, Encoding.UTF8);
var data = JsonUtility.FromJson<ConfigLinkDataFile>(json);
if (data == null || data.tables == null) return new List<ConfigTableInfo>();
var result = new List<ConfigTableInfo>();
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<FieldRelation>()
};
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<ConfigTableInfo>();
}
}
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<ConfigLinkRelation>()
});
}
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<ConfigLinkRelation>()
});
}
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>();
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,16 +345,7 @@ 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)
{
@@ -432,7 +367,7 @@ public static class ConfigLinkDatabase
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;
@@ -440,8 +375,6 @@ public static class ConfigLinkDatabase
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")
{
@@ -454,7 +387,7 @@ public static class ConfigLinkDatabase
}
}
return File.Exists(jsonPath) || File.Exists(jsonPathAlt);
return File.Exists(jsonPath);
}
public static ConfigTableInfo GetTableInfo(string tableName)
@@ -497,49 +430,29 @@ public static class ConfigLinkDatabase
if (string.IsNullOrEmpty(format) || string.IsNullOrEmpty(value))
return value;
string[] parts = value.Split('_');
string result = "";
string key = format.ToLower();
switch (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<string> GetRelatedTableNames(string tableName)
@@ -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<SettingsData>(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<string, FormatTemplate>();
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<string, (string, string)> LoadTableInfoDict()
{
if (_tableInfoCache != null)
return _tableInfoCache;
_tableInfoCache = new Dictionary<string, (string, string)>();
if (!File.Exists(TableInfoFilePath))
return _tableInfoCache;
try
{
string json = File.ReadAllText(TableInfoFilePath, Encoding.UTF8);
var raw = JsonUtility.FromJson<TableInfoFile>(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<TableInfoEntry> entries = new List<TableInfoEntry>();
}
[Serializable]
private class TableInfoEntry
{
public string name;
public string displayName;
public string description;
}
}

View File

@@ -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<string> 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<string, bool> expandedRelations = new Dictionary<string, bool>();
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()
? "<color=green>已加载 (ConfigLinkData.json)</color>"
: "<color=yellow>未找到配置文件,请切换到[配置]标签页生成</color>";
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));
@@ -102,17 +420,23 @@ public class ConfigLinkViewerWindow : EditorWindow
GUIStyle buttonStyle = isSelected ? "Button" : listButtonStyle;
if (GUILayout.Button(displayNames[i], buttonStyle))
{
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,7 +455,6 @@ public class ConfigLinkViewerWindow : EditorWindow
if (tableInfo == null)
{
EditorGUILayout.LabelField("未找到该表的配置信息", EditorStyles.helpBox);
EditorGUILayout.EndScrollView();
return;
}
@@ -174,8 +494,6 @@ public class ConfigLinkViewerWindow : EditorWindow
}
}
}
EditorGUILayout.EndScrollView();
}
private void DrawOpenExcelButtons(float panelWidth)
@@ -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");
List<string> sharedStrings = null;
using (var archive = ZipFile.Open(filePath, ZipArchiveMode.Read))
{
var ssEntry = archive.GetEntry("xl/sharedStrings.xml");
if (ssEntry != null)
{
using (var stream = ssEntry.Open())
{
var ssDoc = XDocument.Load(stream);
sharedStrings = ssDoc.Root.Elements(ns + "si")
.Select(si => si.Value)
.ToList();
}
}
}
var sheetData = doc.Root.Element(ns + "sheetData");
if (sheetData == null) return 0;
var rows = sheetData.Elements(ns + "row").ToList();
var rowsToRemove = new List<XElement>();
if (rows.Count <= headerRowCount) return 0;
foreach (var row in rows)
var emptyRows = new List<XElement>();
for (int i = headerRowCount; i < rows.Count; i++)
{
if (!int.TryParse(row.Attribute("r")?.Value, out int rowNum)) continue;
if (rowNum <= headerRowCount) continue;
var row = rows[i];
var cells = row.Elements(ns + "c").ToList();
if (cells.Count == 0)
{
rowsToRemove.Add(row);
emptyRows.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 _))
bool allEmpty = true;
foreach (var cell in cells)
{
rowsToRemove.Add(row);
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 (rowsToRemove.Count == 0) return 0;
if (allEmpty)
{
emptyRows.Add(row);
}
}
foreach (var row in rowsToRemove)
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 sheetEntry = archive.GetEntry("xl/worksheets/sheet1.xml");
if (sheetEntry != null)
{
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<string, (string example, string desc)> FormatExamples = new Dictionary<string, (string, string)>
{
{ "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;
}
}

View File

@@ -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()

View File

@@ -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
}
}
}

View File

@@ -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": "敏感词过滤库"}
]
}

View File

@@ -4,75 +4,73 @@
## 功能特性
### 1. 配置表列表
- 支持搜索过滤配置
- 显示表是否存在于当前项目
- 支持 TXT 文件(如敏感词表)
- **配置表列表**:搜索过滤、存在性检测、支持 TXT 文件
- **联动关系查看**:正向关联和反向引用查询,支持跳转到关联
- **数据格式可视化**:自动解析 item_id_num、id_lv_num 等复合格式
- **Excel 操作**:一键打开配置表,批量打开关联表
- **导出配置**:批量导出为 JSON 和 C# 代码
- **清理空行**:自动检测并删除 Excel 中 id 列为空的垃圾数据行,支持 sharedStrings 解析
- **跨项目适配**:基于 `ConfigLinkData.json`,一键生成骨架并由 AI 补全
### 2. 联动关系查看
- 查看当前表关联哪些其他表
- 显示字段联动关系详情
- 支持跳转到关联表
## 安装
### 3. 反向查询
- 查看哪些表引用了当前表
- 快速定位依赖关系
### 4. 数据格式可视化
- 自动解析数据格式
- 显示格式示例和说明
- 支持多种格式item_id_num, id_pos_lv, id_lv_num 等
### 5. Excel 操作
- 一键打开配置表 Excel 文件
- 批量打开所有关联表
### 6. 导出配置
- 一键将 Excel 配置表批量导出为 JSON 和 C# 代码
- 通过 Tools → ConfigDeal → 导出配置 或窗口右上角按钮触发
### 7. 清理 Excel 空行
- 自动扫描 Excel 文件夹中所有 .xlsx 文件
- 检测并删除数据区域中第一列id列为空或非数字的行
- 直接修改 Excel 源文件,清理后可重新导出配置
- 通过窗口右上角"清理Excel空行"按钮触发
### 8. 跨项目适配
- 自动检测项目中存在的配置表
- 支持不同项目配置不同路径
## 安装指南
### 方法一:复制文件
1. 复制 `ConfigLinkViewer` 文件夹到目标项目
2. 路径:`Assets/Editor/ConfigLinkViewer/`
### 方法二Git 子模块
1. 复制 `ConfigLinkViewer` 文件夹到目标项目的 `Assets/Editor/`
2. 或使用 Git 子模块:
```bash
git submodule add <repository-url> Assets/Editor/ConfigLinkViewer
```
## 使用说明
## 快速上手
### 打开工具
在 Unity 编辑器菜单中点击:`Tools → 配置表联动查看器`
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 <repository-url> 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<FieldRelation> {
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编辑器窗口界面含导出配置、清理空行功能
├── 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__/` 缓存目录和已完成的优化方案文档
## 许可证