Files
zeling_v2/Assets/_Game/Scripts/Editor/Modules/IdCodegenModule.cs
Joywayer 6eaa83dc71 feat: Round 48 narrative systems improvements
- QuestSO: Add ValidateBranchCycles() DFS detection for branches[].nextQuest loop
- QuestSO: Mark three legacy prerequisite fields with v2.0 removal warning in Tooltip
- IQuestManager: Add QuestLockReason enum + QuestLockInfo struct (strongly-typed lock info)
- IQuestManager: Add GetQuestLockInfo() method to interface; GetQuestLockReason() now delegates to it
- IQuestEventSource: Add OnQuestStateChanged(questId, oldState, newState) unified event
- QuestManager: Implement GetQuestLockInfo(); fire OnQuestStateChanged on all state transitions
- DialogueManager: Add one-frame yield in HandleChoices before ShowChoices (skip-debounce fix)
- DialogueManager: Increment _playbackId in ForceEnd() to invalidate residual choice callbacks
- DialogueSequenceSO: Add UNITY_EDITOR debug log in TryGetActiveVariant on variant match
- WorldStateRegistry: Add OnBatchStateChanged event + BatchMark() batch-write API
- DialogueModule: List badge shows warning indicator for unconditional-shadowing variants
- DialogueModule: BuildVariantsCard shows logic mode (AND/OR) alongside flag conditions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-25 00:05:15 +08:00

198 lines
8.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// DataHub ID 生成模块 —— 扫描 QuestSO / NpcSO / DialogueSequenceSO / EventChainSO 资产,
/// 自动生成 <c>Assets/_Game/Scripts/Core/GameIds.Generated.cs</c>
/// 提供编译期 ID 常量,消除代码中的魔法字符串。
/// </summary>
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<UnityEngine.Object> 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<QuestSO>()
.Where(q => !string.IsNullOrEmpty(q.questId))
.Select(q => q.questId)
.Distinct(StringComparer.Ordinal)
.OrderBy(s => s, StringComparer.Ordinal)
.ToList();
var npcs = AssetOperations.FindAll<NpcSO>()
.Where(n => !string.IsNullOrEmpty(n.npcId))
.Select(n => n.npcId)
.Distinct(StringComparer.Ordinal)
.OrderBy(s => s, StringComparer.Ordinal)
.ToList();
var dialogues = AssetOperations.FindAll<DialogueSequenceSO>()
.Where(d => !string.IsNullOrEmpty(d.sequenceId))
.Select(d => d.sequenceId)
.Distinct(StringComparer.Ordinal)
.OrderBy(s => s, StringComparer.Ordinal)
.ToList();
var chains = AssetOperations.FindAll<EventChainSO>()
.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<string> quests, List<string> npcs,
List<string> dialogues, List<string> chains)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated>");
sb.AppendLine("// 此文件由 DataHub > ID生成 模块自动生成,请勿手动编辑。");
sb.AppendLine("// 手动维护的 ID 常量请放在 GameIds.cs 中。");
sb.AppendLine("// </auto-generated>");
sb.AppendLine();
sb.AppendLine("namespace BaseGames.Core");
sb.AppendLine("{");
sb.AppendLine(" /// <summary>自动生成的游戏资产 ID 常量。每次执行 DataHub > ID生成 后刷新。</summary>");
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<string> ids, string stripPrefix)
{
sb.AppendLine($" /// <summary>来自 {docSource} 的 ID 常量。</summary>");
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();
}
/// <summary>
/// 将原始 ID 字符串转为合法的 C# 标识符。
/// 剥离常见前缀(如 "Quest_"),然后将剩余部分中的非字母数字字符替换为下划线,
/// 确保不以数字开头。
/// </summary>
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;
}
}
}