- 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>
327 lines
13 KiB
C#
327 lines
13 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
using UnityEngine.UIElements;
|
||
using BaseGames.Editor.Modules;
|
||
|
||
namespace BaseGames.Editor
|
||
{
|
||
/// <summary>
|
||
/// 数据管理总枢纽窗口(DataHub)。
|
||
/// 布局:导航侧边栏(120px) | TwoPaneSplitView → 列表区(220px) + 详情区(flex)。
|
||
/// 菜单:BaseGames / Data Hub (priority=50)
|
||
/// </summary>
|
||
public class DataHubWindow : EditorWindow
|
||
{
|
||
private const string UssPath = "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss";
|
||
private const string PrefKey = "DataHub.ActiveModuleId";
|
||
private const float NavWidth = 120f;
|
||
private const float ListWidth = 220f;
|
||
private const float MinWinWidth = 680f;
|
||
private const float MinWinHeight = 420f;
|
||
|
||
[MenuItem("BaseGames/Data Hub", priority = 50)]
|
||
public static void Open()
|
||
{
|
||
var wnd = GetWindow<DataHubWindow>();
|
||
wnd.titleContent = new GUIContent("Data Hub", EditorGUIUtility.IconContent("d_ScriptableObject Icon").image);
|
||
wnd.minSize = new Vector2(MinWinWidth, MinWinHeight);
|
||
}
|
||
|
||
// ── 状态 ─────────────────────────────────────────────────────────────
|
||
|
||
private readonly List<IDataModule> _modules = new();
|
||
private readonly HashSet<string> _initializedIds = new();
|
||
private IDataModule _activeModule;
|
||
|
||
private VisualElement _navSidebar;
|
||
// NavBar 搜索:按 DisplayName 过滤可见模块
|
||
private string _navFilter = "";
|
||
private readonly Dictionary<string, Button> _navButtons = new();
|
||
|
||
// 缓存:列表区和详情区引用(由 TwoPaneSplitView 子节点提供)
|
||
private VisualElement _listWrapper;
|
||
private VisualElement _detailWrapper;
|
||
|
||
// 当前选中资产
|
||
private UnityEngine.Object _selected;
|
||
|
||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||
|
||
public void CreateGUI()
|
||
{
|
||
// USS
|
||
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
||
if (uss != null) rootVisualElement.styleSheets.Add(uss);
|
||
|
||
// 注册模块
|
||
RegisterModules();
|
||
|
||
// 构建 UI
|
||
BuildLayout();
|
||
|
||
// 恢复上次激活的模块
|
||
string savedId = EditorPrefs.GetString(PrefKey, string.Empty);
|
||
var toActivate = _modules.Find(m => m.ModuleId == savedId) ?? _modules.FirstOrDefault();
|
||
if (toActivate != null) ActivateModule(toActivate);
|
||
}
|
||
|
||
// ── 模块注册 ──────────────────────────────────────────────────────────
|
||
|
||
private void RegisterModules()
|
||
{
|
||
_modules.Clear();
|
||
|
||
// 自动发现所有实现 IDataModule 的非抽象类型(排除接口/抽象类本身)
|
||
// TypeCache 仅在 UnityEditor 中可用,零运行时开销
|
||
var moduleTypes = UnityEditor.TypeCache.GetTypesDerivedFrom<IDataModule>();
|
||
var instances = new List<IDataModule>(moduleTypes.Count);
|
||
foreach (var t in moduleTypes)
|
||
{
|
||
if (t.IsAbstract || t.IsInterface) continue;
|
||
try { instances.Add((IDataModule)Activator.CreateInstance(t)); }
|
||
catch (Exception ex)
|
||
{
|
||
Debug.LogWarning($"[DataHubWindow] 无法实例化模块 {t.Name}: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// 按 DisplayOrder 升序排列(若模块实现了 IDataModuleOrdered 则使用其值,否则默认 0)
|
||
instances.Sort((a, b) =>
|
||
{
|
||
int oa = a is IDataModuleOrdered ao ? ao.DisplayOrder : 0;
|
||
int ob = b is IDataModuleOrdered bo ? bo.DisplayOrder : 0;
|
||
int c = oa.CompareTo(ob);
|
||
return c != 0 ? c : string.Compare(a.DisplayName, b.DisplayName, StringComparison.OrdinalIgnoreCase);
|
||
});
|
||
|
||
_modules.AddRange(instances);
|
||
}
|
||
|
||
// ── 布局 ─────────────────────────────────────────────────────────────
|
||
|
||
private void BuildLayout()
|
||
{
|
||
var root = rootVisualElement;
|
||
root.style.flexDirection = FlexDirection.Row;
|
||
root.style.flexGrow = 1;
|
||
|
||
// 导航侧边栏
|
||
_navSidebar = BuildNavSidebar();
|
||
root.Add(_navSidebar);
|
||
|
||
// 垂直分隔线
|
||
var divider = new VisualElement();
|
||
divider.style.width = 1;
|
||
divider.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.25f));
|
||
root.Add(divider);
|
||
|
||
// TwoPaneSplitView(列表 + 详情)
|
||
var split = new TwoPaneSplitView(0, ListWidth, TwoPaneSplitViewOrientation.Horizontal);
|
||
split.style.flexGrow = 1;
|
||
root.Add(split);
|
||
|
||
// 列表区容器
|
||
_listWrapper = new VisualElement();
|
||
_listWrapper.style.flexGrow = 1;
|
||
_listWrapper.style.overflow = Overflow.Hidden;
|
||
split.Add(_listWrapper);
|
||
|
||
// 详情区:外层 ScrollView 提供滚动视口,_detailWrapper 是内容容器(自然高度)
|
||
var detailScroll = new ScrollView(ScrollViewMode.Vertical);
|
||
detailScroll.style.flexGrow = 1;
|
||
detailScroll.style.overflow = Overflow.Hidden;
|
||
split.Add(detailScroll);
|
||
|
||
_detailWrapper = new VisualElement();
|
||
_detailWrapper.style.flexDirection = FlexDirection.Column;
|
||
_detailWrapper.style.paddingBottom = 16;
|
||
detailScroll.Add(_detailWrapper);
|
||
}
|
||
|
||
private VisualElement BuildNavSidebar()
|
||
{
|
||
var sidebar = new VisualElement();
|
||
sidebar.style.width = NavWidth;
|
||
sidebar.style.flexShrink = 0;
|
||
sidebar.style.flexDirection = FlexDirection.Column;
|
||
sidebar.style.paddingTop = 8;
|
||
|
||
// 标题
|
||
var title = new Label("DATA HUB");
|
||
title.style.fontSize = 10;
|
||
title.style.opacity = 0.5f;
|
||
title.style.paddingLeft = 10;
|
||
title.style.marginBottom = 4;
|
||
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||
sidebar.Add(title);
|
||
|
||
// 搜索框
|
||
var searchField = new TextField();
|
||
searchField.style.marginLeft = 6;
|
||
searchField.style.marginRight = 6;
|
||
searchField.style.marginBottom = 6;
|
||
searchField.tooltip = "按模块名称过滤";
|
||
// 自定义占位符效果
|
||
if (string.IsNullOrEmpty(searchField.value))
|
||
searchField.value = "";
|
||
searchField.RegisterValueChangedCallback(evt =>
|
||
{
|
||
_navFilter = evt.newValue ?? "";
|
||
ApplyNavFilter();
|
||
});
|
||
sidebar.Add(searchField);
|
||
|
||
_navButtons.Clear();
|
||
foreach (var module in _modules)
|
||
{
|
||
var btn = BuildNavItem(module);
|
||
_navButtons[module.ModuleId] = btn;
|
||
sidebar.Add(btn);
|
||
}
|
||
|
||
// 弹性填充
|
||
var spacer = new VisualElement();
|
||
spacer.style.flexGrow = 1;
|
||
sidebar.Add(spacer);
|
||
|
||
return sidebar;
|
||
}
|
||
|
||
private void ApplyNavFilter()
|
||
{
|
||
foreach (var module in _modules)
|
||
{
|
||
if (!_navButtons.TryGetValue(module.ModuleId, out var btn)) continue;
|
||
bool visible = string.IsNullOrEmpty(_navFilter) ||
|
||
module.DisplayName.IndexOf(_navFilter, System.StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||
module.ModuleId.IndexOf(_navFilter, System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||
btn.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
|
||
}
|
||
}
|
||
|
||
private Button BuildNavItem(IDataModule module)
|
||
{
|
||
var btn = new Button(() => ActivateModule(module));
|
||
btn.name = "nav-" + module.ModuleId;
|
||
btn.style.flexDirection = FlexDirection.Row;
|
||
btn.style.alignItems = Align.Center;
|
||
btn.style.paddingLeft = 10;
|
||
btn.style.paddingRight = 8;
|
||
btn.style.paddingTop = 8;
|
||
btn.style.paddingBottom = 8;
|
||
btn.style.borderTopLeftRadius = 0;
|
||
btn.style.borderTopRightRadius = 0;
|
||
btn.style.borderBottomLeftRadius = 0;
|
||
btn.style.borderBottomRightRadius = 0;
|
||
btn.style.borderLeftWidth = 0;
|
||
btn.style.borderRightWidth = 0;
|
||
btn.style.borderTopWidth = 0;
|
||
btn.style.borderBottomWidth = 0;
|
||
btn.style.backgroundColor = new StyleColor(Color.clear);
|
||
btn.style.marginBottom = 2;
|
||
|
||
// 图标(容错:图标名无效时跳过,不报错)
|
||
if (!string.IsNullOrEmpty(module.IconName))
|
||
{
|
||
var content = EditorGUIUtility.IconContent(module.IconName);
|
||
if (content?.image != null)
|
||
{
|
||
var icon = new Image { image = content.image };
|
||
icon.style.width = 16;
|
||
icon.style.height = 16;
|
||
icon.style.marginRight = 6;
|
||
btn.Add(icon);
|
||
}
|
||
}
|
||
|
||
var label = new Label(module.DisplayName);
|
||
label.style.flexGrow = 1;
|
||
btn.Add(label);
|
||
|
||
return btn;
|
||
}
|
||
|
||
// ── 模块切换 ──────────────────────────────────────────────────────────
|
||
|
||
private void ActivateModule(IDataModule module)
|
||
{
|
||
if (_activeModule == module) return;
|
||
_activeModule = module;
|
||
_selected = null;
|
||
|
||
// 更新导航项视觉状态
|
||
foreach (var m in _modules)
|
||
{
|
||
var navBtn = _navSidebar.Q<Button>("nav-" + m.ModuleId);
|
||
if (navBtn == null) continue;
|
||
|
||
if (m == module)
|
||
{
|
||
navBtn.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.18f));
|
||
navBtn.style.borderLeftWidth = 3;
|
||
navBtn.style.borderLeftColor = new StyleColor(new Color(0.4f, 0.65f, 1f, 1f));
|
||
}
|
||
else
|
||
{
|
||
navBtn.style.backgroundColor = new StyleColor(Color.clear);
|
||
navBtn.style.borderLeftWidth = 0;
|
||
}
|
||
}
|
||
|
||
// 初始化模块(首次激活时调用一次)
|
||
if (!_initializedIds.Contains(module.ModuleId))
|
||
{
|
||
_initializedIds.Add(module.ModuleId);
|
||
module.Initialize();
|
||
}
|
||
module.OnActivated();
|
||
|
||
// 重建列表区
|
||
_listWrapper.Clear();
|
||
module.BuildListPane(_listWrapper, OnModuleSelected);
|
||
|
||
// 清空详情区
|
||
RebuildDetailPane(null);
|
||
|
||
EditorPrefs.SetString(PrefKey, module.ModuleId);
|
||
}
|
||
|
||
private void OnModuleSelected(UnityEngine.Object selected)
|
||
{
|
||
_selected = selected;
|
||
RebuildDetailPane(selected);
|
||
}
|
||
|
||
private void RebuildDetailPane(UnityEngine.Object selected)
|
||
{
|
||
_detailWrapper.Clear();
|
||
|
||
if (_activeModule == null) return;
|
||
|
||
if (selected == null)
|
||
{
|
||
var placeholder = new Label("← 从左侧列表选择一项");
|
||
placeholder.style.opacity = 0.45f;
|
||
placeholder.style.marginTop = 60;
|
||
placeholder.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||
_detailWrapper.Add(placeholder);
|
||
return;
|
||
}
|
||
|
||
_activeModule.BuildDetailPane(_detailWrapper, selected);
|
||
}
|
||
|
||
// ── 公共辅助(供 Module 回调使用)────────────────────────────────────
|
||
|
||
/// <summary>通知 Hub 已完成重命名,需要刷新详情区标题。</summary>
|
||
public void NotifyRenamed(UnityEngine.Object asset)
|
||
{
|
||
if (_activeModule == null || asset == null) return;
|
||
RebuildDetailPane(asset);
|
||
}
|
||
}
|
||
}
|