diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 59bbcb594..17b4d99b8 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -3897,15 +3897,19 @@ static void RequireRuntimeMenuTabsUseEveSurface(string root) "HandleTabSurfaceCommand(", "AetheriaEveUnitySurfaceHost.RenderRuntime(", "AetheriaEveUnitySurfaceHost.Hide(_tabSurfaceDocument)", - "AetheriaRuntimeMenuTabsSurfaceBuilder.Build(ProjectTabSurfaceState())", - "ProjectTabSurfaceState(", + "AetheriaRuntimeMenuTabsSurfaceBuilder.Build(ProjectTabSurface())", + "ProjectTabSurface(", + "AetheriaRuntimeMenuTabsSurfaceBuilder.Project(", + "new AetheriaRuntimeMenuTabProjectionOption(", "ResolveVisibleTabs(", "GameManager.IsObservedDocked", "GameManager.TryGetObservedDockedLocalStory(out _)", "GetTabLabel(", + "ToRuntimeTabKey(", + "AetheriaRuntimeMenuTabsSurfaceBuilder.NormalizeTabKey(tab.ToString())", "AetheriaRuntimeMenuTabsSurfaceCommands.TryRead(request, out var command)", "AetheriaRuntimeMenuTabCommandKind.SelectTab", - "string.Equals(command.TabKey, TabKey(tab), StringComparison.Ordinal)", + "string.Equals(command.TabKey, ToRuntimeTabKey(tab), StringComparison.Ordinal)", "TabButtons.gameObject.SetActive(false)" }; @@ -3951,7 +3955,10 @@ static void RequireRuntimeMenuTabsUseEveSurface(string root) "private static EveSurfaceComponent Node(", "ResolveTabSurfaceDocument(", "new EveUiToolkitSurfaceLowerer()", - "string.Equals(request.Command, AetheriaRuntimeMenuTabsSurfaceBuilder.CommandFor(" + "string.Equals(request.Command, AetheriaRuntimeMenuTabsSurfaceBuilder.CommandFor(", + "ProjectTabSurfaceState(", + "new AetheriaRuntimeMenuTabSurfaceEntry(", + "private static string TabKey(" }; var hits = forbiddenSymbols @@ -3965,6 +3972,26 @@ static void RequireRuntimeMenuTabsUseEveSurface(string root) string.Join(", ", hits)); } + var requiredProjectionBuilderSymbols = new[] + { + "public sealed class AetheriaRuntimeMenuTabProjectionOption", + "public static string NormalizeTabKey(string tabKey)", + "public static AetheriaRuntimeMenuTabsSurfaceState Project(", + "public int Order { get; }", + ".OrderBy(tab => tab.Order)", + "new AetheriaRuntimeMenuTabSurfaceEntry(", + "string.Equals(key, normalizedCurrent, StringComparison.Ordinal)" + }; + var missingProjectionBuilderSymbols = requiredProjectionBuilderSymbols + .Where(symbol => !menuTabsSurfaceBuilder.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + if (missingProjectionBuilderSymbols.Length > 0) + { + throw new InvalidOperationException( + "Shared runtime menu tab surface builder no longer owns tab projection semantics: " + + string.Join(", ", missingProjectionBuilderSymbols)); + } + var requiredDockedStoryObserverSymbols = new[] { "public bool IsObservedDocked => TryGetObservedDockingBay(out _);", diff --git a/Assets/Scripts/UI/Menu/MenuPanel.cs b/Assets/Scripts/UI/Menu/MenuPanel.cs index 818ab549d..95a44c060 100644 --- a/Assets/Scripts/UI/Menu/MenuPanel.cs +++ b/Assets/Scripts/UI/Menu/MenuPanel.cs @@ -108,7 +108,7 @@ public class MenuPanel : MonoBehaviour transform, _tabSurfaceDocument, "Aetheria Runtime Menu Tabs Surface", - AetheriaRuntimeMenuTabsSurfaceBuilder.Build(ProjectTabSurfaceState()), + AetheriaRuntimeMenuTabsSurfaceBuilder.Build(ProjectTabSurface()), HandleTabSurfaceCommand, _tabSurfaceChrome); } @@ -124,7 +124,7 @@ public class MenuPanel : MonoBehaviour foreach (var tab in _tabs.Keys) { if (command.Kind == AetheriaRuntimeMenuTabCommandKind.SelectTab && - string.Equals(command.TabKey, TabKey(tab), StringComparison.Ordinal)) + string.Equals(command.TabKey, ToRuntimeTabKey(tab), StringComparison.Ordinal)) { ShowTab(tab); return; @@ -142,16 +142,16 @@ public class MenuPanel : MonoBehaviour AetheriaEveUnitySurfaceHost.Hide(_tabSurfaceDocument); } - private AetheriaRuntimeMenuTabsSurfaceState ProjectTabSurfaceState() + private AetheriaRuntimeMenuTabsSurfaceState ProjectTabSurface() { var visibleTabs = ResolveVisibleTabs(); - return new AetheriaRuntimeMenuTabsSurfaceState( - TabKey(CurrentTab), + return AetheriaRuntimeMenuTabsSurfaceBuilder.Project( + ToRuntimeTabKey(CurrentTab), visibleTabs - .Select(tabBinding => new AetheriaRuntimeMenuTabSurfaceEntry( - TabKey(tabBinding.Tab), + .Select(tabBinding => new AetheriaRuntimeMenuTabProjectionOption( + ToRuntimeTabKey(tabBinding.Tab), GetTabLabel(tabBinding), - tabBinding.Tab == CurrentTab)) + (int)tabBinding.Tab)) .ToArray(), DateTime.UtcNow.ToString("O")); } @@ -172,9 +172,9 @@ public class MenuPanel : MonoBehaviour : tabBinding.Label; } - private static string TabKey(MenuTab tab) + private static string ToRuntimeTabKey(MenuTab tab) { - return tab.ToString().ToLowerInvariant(); + return AetheriaRuntimeMenuTabsSurfaceBuilder.NormalizeTabKey(tab.ToString()); } // void Start() diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMenuTabsSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMenuTabsSurfaceBuilder.cs index 2f494fa7d..9536d420a 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMenuTabsSurfaceBuilder.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMenuTabsSurfaceBuilder.cs @@ -36,16 +36,58 @@ namespace GameCult.Aetheria.State.Verse public string UpdatedAtUtc { get; } } + public sealed class AetheriaRuntimeMenuTabProjectionOption + { + public AetheriaRuntimeMenuTabProjectionOption(string key, string label, int order) + { + Key = key ?? ""; + Label = label ?? ""; + Order = order; + } + + public string Key { get; } + public string Label { get; } + public int Order { get; } + } + public static class AetheriaRuntimeMenuTabsSurfaceBuilder { public const string SurfaceId = "aetheria.runtime_menu.tabs"; - public static string CommandFor(string tabKey) + public static string NormalizeTabKey(string tabKey) { - var normalized = string.IsNullOrWhiteSpace(tabKey) + return string.IsNullOrWhiteSpace(tabKey) ? "unknown" : tabKey.Trim().ToLowerInvariant(); - return $"aetheria.runtime_menu.tab.{normalized}"; + } + + public static string CommandFor(string tabKey) + { + return $"aetheria.runtime_menu.tab.{NormalizeTabKey(tabKey)}"; + } + + public static AetheriaRuntimeMenuTabsSurfaceState Project( + string currentTabKey, + IEnumerable visibleTabs, + string updatedAtUtc) + { + var normalizedCurrent = NormalizeTabKey(currentTabKey); + return new AetheriaRuntimeMenuTabsSurfaceState( + normalizedCurrent, + (visibleTabs ?? Array.Empty()) + .Where(tab => tab != null) + .OrderBy(tab => tab.Order) + .ThenBy(tab => NormalizeTabKey(tab.Key), StringComparer.Ordinal) + .Select(tab => + { + var key = NormalizeTabKey(tab.Key); + return new AetheriaRuntimeMenuTabSurfaceEntry( + key, + string.IsNullOrWhiteSpace(tab.Label) ? key : tab.Label, + string.Equals(key, normalizedCurrent, StringComparison.Ordinal)); + }) + .ToArray(), + updatedAtUtc); } public static AetheriaRuntimeSurfaceDocument Build(