using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using BaseGames.Dialogue; using BaseGames.Quest; using BaseGames.EventChain; namespace BaseGames.Editor.Modules { /// /// DataHub ID 生成模块 —— 扫描 QuestSO / NpcSO / DialogueSequenceSO / EventChainSO 资产, /// 自动生成 Assets/_Game/Scripts/Core/GameIds.Generated.cs, /// 提供编译期 ID 常量,消除代码中的魔法字符串。 /// public class IdCodegenModule : IDataModule, IDataModuleOrdered { private const string OutputPath = "Assets/_Game/Scripts/Core/GameIds.Generated.cs"; public string ModuleId => "idcodegen"; public string DisplayName => "ID 生成"; public string IconName => "d_cs Script Icon"; public int DisplayOrder => 140; private Label _statusLabel; private string _lastResult; // ── IDataModule ─────────────────────────────────────────────────────── public void Initialize() { } public void BuildListPane(VisualElement container, Action onSelected) { var desc = new Label( "扫描项目中的 QuestSO / NpcSO / DialogueSequenceSO / EventChainSO 资产,\n" + $"生成 {OutputPath} 常量文件。\n\n" + "生成后在代码中通过 GameIdsGenerated.Quest.XXX 等访问。"); desc.style.whiteSpace = WhiteSpace.Normal; desc.style.marginBottom = 12; desc.style.paddingLeft = 8; desc.style.paddingRight = 8; desc.style.fontSize = 11; container.Add(desc); var btn = new Button(RunCodegen) { text = "⚡ 生成 GameIds.Generated.cs" }; btn.style.marginLeft = 8; btn.style.marginRight = 8; btn.style.height = 28; container.Add(btn); _statusLabel = new Label(_lastResult ?? ""); _statusLabel.style.whiteSpace = WhiteSpace.Normal; _statusLabel.style.marginTop = 10; _statusLabel.style.marginLeft = 8; _statusLabel.style.marginRight = 8; _statusLabel.style.fontSize = 11; container.Add(_statusLabel); } public void BuildDetailPane(VisualElement container, UnityEngine.Object selected) { // 无需详情面板 } public void OnActivated() { } // ── 代码生成 ────────────────────────────────────────────────────────── private void RunCodegen() { try { var quests = AssetOperations.FindAll() .Where(q => !string.IsNullOrEmpty(q.questId)) .Select(q => q.questId) .Distinct(StringComparer.Ordinal) .OrderBy(s => s, StringComparer.Ordinal) .ToList(); var npcs = AssetOperations.FindAll() .Where(n => !string.IsNullOrEmpty(n.npcId)) .Select(n => n.npcId) .Distinct(StringComparer.Ordinal) .OrderBy(s => s, StringComparer.Ordinal) .ToList(); var dialogues = AssetOperations.FindAll() .Where(d => !string.IsNullOrEmpty(d.sequenceId)) .Select(d => d.sequenceId) .Distinct(StringComparer.Ordinal) .OrderBy(s => s, StringComparer.Ordinal) .ToList(); var chains = AssetOperations.FindAll() .Where(c => !string.IsNullOrEmpty(c.chainId)) .Select(c => c.chainId) .Distinct(StringComparer.Ordinal) .OrderBy(s => s, StringComparer.Ordinal) .ToList(); string code = BuildSourceCode(quests, npcs, dialogues, chains); string fullPath = Path.GetFullPath(OutputPath); string dir = Path.GetDirectoryName(fullPath); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); File.WriteAllText(fullPath, code, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); AssetDatabase.ImportAsset(OutputPath, ImportAssetOptions.ForceUpdate); int total = quests.Count + npcs.Count + dialogues.Count + chains.Count; _lastResult = $"✅ 生成完成({total} 个常量:Quest×{quests.Count} Npc×{npcs.Count} " + $"Dialogue×{dialogues.Count} Chain×{chains.Count})"; } catch (Exception e) { _lastResult = $"❌ 生成失败:{e.Message}"; Debug.LogException(e); } if (_statusLabel != null) _statusLabel.text = _lastResult; } private static string BuildSourceCode(List quests, List npcs, List dialogues, List chains) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("// 此文件由 DataHub > ID生成 模块自动生成,请勿手动编辑。"); sb.AppendLine("// 手动维护的 ID 常量请放在 GameIds.cs 中。"); sb.AppendLine("// "); sb.AppendLine(); sb.AppendLine("namespace BaseGames.Core"); sb.AppendLine("{"); sb.AppendLine(" /// 自动生成的游戏资产 ID 常量。每次执行 DataHub > ID生成 后刷新。"); sb.AppendLine(" public static class GameIdsGenerated"); sb.AppendLine(" {"); AppendSection(sb, "Quest", "QuestSO.questId", quests, "Quest_"); AppendSection(sb, "Npc", "NpcSO.npcId", npcs, "NPC_"); AppendSection(sb, "Dialogue", "DialogueSequenceSO.sequenceId", dialogues, "DLG_"); AppendSection(sb, "Chain", "EventChainSO.chainId", chains, "Chain_"); sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static void AppendSection(StringBuilder sb, string className, string docSource, List ids, string stripPrefix) { sb.AppendLine($" /// 来自 {docSource} 的 ID 常量。"); sb.AppendLine($" public static class {className}"); sb.AppendLine(" {"); if (ids.Count == 0) sb.AppendLine(" // 项目中暂无此类资产"); else foreach (string id in ids) sb.AppendLine($" public const string {ToFieldName(id, stripPrefix)} = \"{id}\";"); sb.AppendLine(" }"); sb.AppendLine(); } /// /// 将原始 ID 字符串转为合法的 C# 标识符。 /// 剥离常见前缀(如 "Quest_"),然后将剩余部分中的非字母数字字符替换为下划线, /// 确保不以数字开头。 /// private static string ToFieldName(string rawId, string stripPrefix) { string name = rawId; if (!string.IsNullOrEmpty(stripPrefix) && name.StartsWith(stripPrefix, StringComparison.OrdinalIgnoreCase)) name = name.Substring(stripPrefix.Length); // 将非字母数字字符替换为下划线 name = Regex.Replace(name, @"[^A-Za-z0-9_]", "_"); // 不能以数字开头 if (name.Length > 0 && char.IsDigit(name[0])) name = "_" + name; if (string.IsNullOrEmpty(name)) name = "_"; return name; } } }