- 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>
198 lines
8.3 KiB
C#
198 lines
8.3 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|