diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 3e45fe310..4b38c141f 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -32,6 +32,7 @@ RequireEveRuntimeBootstrap(root); RequireNoRendererLocalConsole(root); RequireNoRendererLocalDebugPanels(root); RequireMainMenuSettingsCommit(root); +RequireMainMenuSettingsShellUsesEveSurface(root); RequirePlayerSettingsEveSurface(root); RequireMainMenuContinueRunState(root); RequirePropertiesPanelReadOnlyInspector(root); @@ -496,6 +497,7 @@ Console.WriteLine("Eve runtime bootstrap: operations surface mounts through UI T Console.WriteLine("Renderer-local console authority: deleted; UI commands flow through Eve command documents"); Console.WriteLine("Renderer-local debug panels: obsolete uGUI field tester authority is deleted"); Console.WriteLine("Main-menu settings authority: player name, gameplay, and graphics settings return through typed player-settings commits"); +Console.WriteLine("Main-menu settings shell: settings/input/audio subpages lower through Eve UI Toolkit surfaces instead of PropertiesPanel buttons"); Console.WriteLine("Main-menu Continue authority: Continue selects typed run state instead of a null button"); Console.WriteLine("PropertiesPanel inspector authority: dead generic reflection inspector path is deleted"); Console.WriteLine("Typed behavior metadata authority: live heat/mining/thermotoggle payload kinds stay owned by package metadata"); @@ -2411,6 +2413,65 @@ static void RequireMainMenuSettingsCommit(string root) } } +static void RequireMainMenuSettingsShellUsesEveSurface(string root) +{ + var mainMenuPath = Path.Combine(root, "Assets", "Scripts", "UI", "MainMenu.cs"); + if (!File.Exists(mainMenuPath)) + { + throw new InvalidOperationException("Cannot verify main-menu settings shell; MainMenu.cs is missing."); + } + + var source = File.ReadAllText(mainMenuPath); + var requiredSymbols = new[] + { + "RenderMenuSurface(", + "BuildSettingsSurfaceDefinition()", + "BuildInputSettingsSurfaceDefinition()", + "BuildAudioSettingsSurfaceDefinition()", + "HandleSettingsSurfaceCommand(", + "HandleInputSettingsSurfaceCommand(", + "HandleAudioSettingsSurfaceCommand(", + "WithBackAction(", + "ShowPlayerSettingsCommand", + "ShowInputSettingsCommand", + "ShowAudioSettingsCommand", + "BackToMainCommand", + "BackToSettingsCommand" + }; + + var missingSymbols = requiredSymbols + .Where(symbol => !source.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (missingSymbols.Length > 0) + { + throw new InvalidOperationException( + "MainMenu no longer lowers the settings shell through Eve surfaces: " + + string.Join(", ", missingSymbols)); + } + + var forbiddenSymbols = new[] + { + "_nextMenu.panel.AddButton(\"Player Settings\"", + "_nextMenu.panel.AddButton(\"Input\"", + "_nextMenu.panel.AddButton(\"Audio\"", + "_nextMenu.panel.Title.text = \"settings\"", + "_nextMenu.panel.Title.text = TitleSubtitle(\"input\", \"settings\")", + "_nextMenu.panel.Title.text = TitleSubtitle(\"audio\", \"settings\")" + }; + + var hits = forbiddenSymbols + .Where(symbol => source.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (hits.Length > 0) + { + throw new InvalidOperationException( + "MainMenu still owns settings/input/audio subpages through the old PropertiesPanel shell: " + + string.Join(", ", hits)); + } +} + static void RequirePlayerSettingsEveSurface(string root) { var projectorPath = Path.Combine(root, "Aetheria.State", "AetheriaPlayerSettingsSurfaceProjector.cs"); diff --git a/Assets/Scripts/UI/MainMenu.cs b/Assets/Scripts/UI/MainMenu.cs index ceedc968d..9871af66b 100644 --- a/Assets/Scripts/UI/MainMenu.cs +++ b/Assets/Scripts/UI/MainMenu.cs @@ -20,6 +20,20 @@ using Random = UnityEngine.Random; public class MainMenu : MonoBehaviour { + private const string MenuSurfaceType = "surface-state"; + private const string MenuSurfaceSchema = "gamecult.eve.surface.v1"; + private const string MenuSurfaceProviderId = "aetheria"; + private const string MenuSurfaceProviderKind = "game.menu"; + private const string SettingsSurfaceId = "aetheria.main_menu.settings"; + private const string InputSettingsSurfaceId = "aetheria.main_menu.input_settings"; + private const string AudioSettingsSurfaceId = "aetheria.main_menu.audio_settings"; + private const string PlayerSettingsShellSurfaceId = "aetheria.main_menu.player_settings"; + private const string ShowPlayerSettingsCommand = "aetheria.main_menu.settings.show_player_settings"; + private const string ShowInputSettingsCommand = "aetheria.main_menu.settings.show_input_settings"; + private const string ShowAudioSettingsCommand = "aetheria.main_menu.settings.show_audio_settings"; + private const string BackToMainCommand = "aetheria.main_menu.settings.back_to_main"; + private const string BackToSettingsCommand = "aetheria.main_menu.settings.back_to_settings"; + public VolumeCloudRenderer CloudRenderer; public GameSettings Settings; public ConfirmationDialog Dialog; @@ -35,7 +49,7 @@ public class MainMenu : MonoBehaviour private float _fadeLerp; private bool _fading; private Vector3 _panelPosition; - private UIDocument _playerSettingsSurfaceDocument; + private UIDocument _menuSurfaceDocument; void Start() { @@ -73,6 +87,8 @@ public class MainMenu : MonoBehaviour var temp = _currentMenu; _currentMenu = _nextMenu; _nextMenu = temp; + if (IsMenuSurfaceVisible()) + _currentMenu.panel.gameObject.SetActive(false); } } } @@ -88,9 +104,14 @@ public class MainMenu : MonoBehaviour _fadeFromRight = fromRight; } + private bool IsMenuSurfaceVisible() + { + return _menuSurfaceDocument != null && _menuSurfaceDocument.gameObject.activeSelf; + } + private void ShowMain() { - HidePlayerSettingsSurface(); + HideMenuSurface(); _nextMenu.panel.Clear(); _nextMenu.panel.Title.text = TitleSubtitle("aetheria", "terminus"); if (!InGame) @@ -249,74 +270,46 @@ public class MainMenu : MonoBehaviour private void ShowSettings() { - HidePlayerSettingsSurface(); _nextMenu.panel.Clear(); - _nextMenu.panel.Title.text = "settings"; - _nextMenu.panel.AddButton("Player Settings", - () => - { - ShowPlayerSettingsSurface(); - Fade(true); - }); - _nextMenu.panel.AddButton("Input", - () => - { - ShowInputSettings(); - Fade(true); - }); - _nextMenu.panel.AddButton("Audio", - () => - { - ShowAudioSettings(); - Fade(true); - }); - _nextMenu.panel.AddButton("Back", - () => - { - ShowMain(); - Fade(false); - }); + _nextMenu.panel.gameObject.SetActive(false); + RenderMenuSurface(BuildSettingsSurfaceDefinition(), HandleSettingsSurfaceCommand); } private void ShowInputSettings() { - HidePlayerSettingsSurface(); _nextMenu.panel.Clear(); - _nextMenu.panel.Title.text = TitleSubtitle("input", "settings"); - _nextMenu.panel.AddButton("Back", - () => - { - ShowSettings(); - Fade(false); - }); + _nextMenu.panel.gameObject.SetActive(false); + RenderMenuSurface(BuildInputSettingsSurfaceDefinition(), HandleInputSettingsSurfaceCommand); } private void ShowAudioSettings() { - HidePlayerSettingsSurface(); _nextMenu.panel.Clear(); - _nextMenu.panel.Title.text = TitleSubtitle("audio", "settings"); - _nextMenu.panel.AddButton("Back", - () => - { - ShowSettings(); - Fade(false); - }); + _nextMenu.panel.gameObject.SetActive(false); + RenderMenuSurface(BuildAudioSettingsSurfaceDefinition(), HandleAudioSettingsSurfaceCommand); } private void ShowPlayerSettingsSurface() { _nextMenu.panel.Clear(); _nextMenu.panel.gameObject.SetActive(false); - RenderPlayerSettingsSurface(); + RenderMenuSurface( + WithBackAction( + ToEveSurfaceDocument(BuildPlayerSettingsSurfaceDefinition()), + PlayerSettingsShellSurfaceId, + BackToSettingsCommand, + "Back"), + HandlePlayerSettingsSurfaceCommand); } - private void RenderPlayerSettingsSurface() + private void RenderMenuSurface( + EveSurfaceDocument document, + Action commandHandler) { - var document = ResolvePlayerSettingsSurfaceDocument(); - document.gameObject.SetActive(true); + var surfaceDocument = ResolveMenuSurfaceDocument(); + surfaceDocument.gameObject.SetActive(true); - var root = document.rootVisualElement; + var root = surfaceDocument.rootVisualElement; root.Clear(); root.style.flexGrow = 1; root.style.justifyContent = Justify.Center; @@ -339,58 +332,93 @@ public class MainMenu : MonoBehaviour root.Add(shell); var lowerer = new EveUiToolkitSurfaceLowerer(); - var surface = lowerer.Lower( - ToEveSurfaceDocument(BuildPlayerSettingsSurfaceDefinition()), - request => - { - if (!ActionGameManager.CommitRuntimePlayerSettingsCommand(request.Command, request.Payload)) - { - Debug.LogWarning($"Unknown player-settings command: {request.Command}"); - return; - } - - CloudRenderer.quality = ActionGameManager.RuntimePlayerSettings.GraphicsSettings.NebulaQuality; - RenderPlayerSettingsSurface(); - }); - surface.style.marginBottom = 14; - shell.Add(surface); - - var actions = new VisualElement(); - actions.style.flexDirection = FlexDirection.Row; - shell.Add(actions); - - var back = new UnityEngine.UIElements.Button(() => - { - HidePlayerSettingsSurface(); - ShowSettings(); - Fade(false); - }) - { - text = "Back" - }; - actions.Add(back); + shell.Add(lowerer.Lower(document, commandHandler)); } - private void HidePlayerSettingsSurface() + private void HandleSettingsSurfaceCommand(EveSurfaceCommandRequest request) { - if (_playerSettingsSurfaceDocument == null) + switch (request.Command) + { + case ShowPlayerSettingsCommand: + ShowPlayerSettingsSurface(); + return; + case ShowInputSettingsCommand: + ShowInputSettings(); + return; + case ShowAudioSettingsCommand: + ShowAudioSettings(); + return; + case BackToMainCommand: + HideMenuSurface(); + ShowMain(); + Fade(false); + return; + default: + Debug.LogWarning($"Unknown settings menu command: {request.Command}"); + return; + } + } + + private void HandleInputSettingsSurfaceCommand(EveSurfaceCommandRequest request) + { + if (string.Equals(request.Command, BackToSettingsCommand, StringComparison.Ordinal)) + { + ShowSettings(); + return; + } + + Debug.LogWarning($"Unknown input settings command: {request.Command}"); + } + + private void HandleAudioSettingsSurfaceCommand(EveSurfaceCommandRequest request) + { + if (string.Equals(request.Command, BackToSettingsCommand, StringComparison.Ordinal)) + { + ShowSettings(); + return; + } + + Debug.LogWarning($"Unknown audio settings command: {request.Command}"); + } + + private void HandlePlayerSettingsSurfaceCommand(EveSurfaceCommandRequest request) + { + if (string.Equals(request.Command, BackToSettingsCommand, StringComparison.Ordinal)) + { + ShowSettings(); + return; + } + + if (!ActionGameManager.CommitRuntimePlayerSettingsCommand(request.Command, request.Payload)) + { + Debug.LogWarning($"Unknown player-settings command: {request.Command}"); + return; + } + + CloudRenderer.quality = ActionGameManager.RuntimePlayerSettings.GraphicsSettings.NebulaQuality; + ShowPlayerSettingsSurface(); + } + + private void HideMenuSurface() + { + if (_menuSurfaceDocument == null) return; - _playerSettingsSurfaceDocument.rootVisualElement.Clear(); - _playerSettingsSurfaceDocument.gameObject.SetActive(false); + _menuSurfaceDocument.rootVisualElement.Clear(); + _menuSurfaceDocument.gameObject.SetActive(false); } - private UIDocument ResolvePlayerSettingsSurfaceDocument() + private UIDocument ResolveMenuSurfaceDocument() { - if (_playerSettingsSurfaceDocument != null) - return _playerSettingsSurfaceDocument; + if (_menuSurfaceDocument != null) + return _menuSurfaceDocument; - var host = new GameObject("Aetheria Player Settings Surface"); + var host = new GameObject("Aetheria Menu Surface"); host.transform.SetParent(transform, false); var document = host.AddComponent(); document.sortingOrder = 1000; host.SetActive(false); - _playerSettingsSurfaceDocument = document; + _menuSurfaceDocument = document; return document; } @@ -408,6 +436,177 @@ public class MainMenu : MonoBehaviour DateTime.UtcNow.ToString("O"))); } + private static EveSurfaceDocument BuildSettingsSurfaceDefinition() + { + return BuildMenuSurfaceDocument( + SettingsSurfaceId, + "Aetheria Settings", + new[] + { + new EveCommandTemplate(ShowPlayerSettingsCommand, "Player Settings", "unity-uitoolkit"), + new EveCommandTemplate(ShowInputSettingsCommand, "Input", "unity-uitoolkit"), + new EveCommandTemplate(ShowAudioSettingsCommand, "Audio", "unity-uitoolkit"), + new EveCommandTemplate(BackToMainCommand, "Back", "unity-uitoolkit") + }, + Card( + "aetheria.mainMenu.settings.card", + "Settings", + Text( + "aetheria.mainMenu.settings.note", + "Typed Verse settings already lower through Eve. Input rebinding and audio still have narrower local authority."), + ButtonRow( + "aetheria.mainMenu.settings.actions", + Button("aetheria.mainMenu.settings.playerSettings", "Player Settings", ShowPlayerSettingsCommand), + Button("aetheria.mainMenu.settings.input", "Input", ShowInputSettingsCommand), + Button("aetheria.mainMenu.settings.audio", "Audio", ShowAudioSettingsCommand), + Button("aetheria.mainMenu.settings.back", "Back", BackToMainCommand)))); + } + + private static EveSurfaceDocument BuildInputSettingsSurfaceDefinition() + { + return BuildMenuSurfaceDocument( + InputSettingsSurfaceId, + "Aetheria Input Settings", + new[] + { + new EveCommandTemplate(BackToSettingsCommand, "Back", "unity-uitoolkit") + }, + Card( + "aetheria.mainMenu.input.card", + "Input Settings", + Metric( + "aetheria.mainMenu.input.bindingOverrides", + "Binding Overrides", + ActionGameManager.RuntimePlayerSettings.InputSettings.InputActionMap.Count.ToString()), + Metric( + "aetheria.mainMenu.input.actionBarInputs", + "Action-Bar Inputs", + ActionGameManager.RuntimePlayerSettings.InputSettings.ActionBarInputs.Count.ToString()), + Text( + "aetheria.mainMenu.input.note", + "Typed input rebinding controls are not lowered through Eve yet. The live remapping screen still owns drag/drop rebinding and low-level InputSystem edits."), + ButtonRow( + "aetheria.mainMenu.input.actions", + Button("aetheria.mainMenu.input.back", "Back", BackToSettingsCommand)))); + } + + private static EveSurfaceDocument BuildAudioSettingsSurfaceDefinition() + { + return BuildMenuSurfaceDocument( + AudioSettingsSurfaceId, + "Aetheria Audio Settings", + new[] + { + new EveCommandTemplate(BackToSettingsCommand, "Back", "unity-uitoolkit") + }, + Card( + "aetheria.mainMenu.audio.card", + "Audio Settings", + Text( + "aetheria.mainMenu.audio.note", + "No typed audio controls are published yet. This screen is lowered through Eve so the old menu shell stops owning the page while the real audio surface catches up."), + ButtonRow( + "aetheria.mainMenu.audio.actions", + Button("aetheria.mainMenu.audio.back", "Back", BackToSettingsCommand)))); + } + + private static EveSurfaceDocument WithBackAction( + EveSurfaceDocument document, + string surfaceId, + string backCommand, + string backLabel) + { + return new EveSurfaceDocument( + document.Type, + document.Schema, + document.ProviderId, + document.ProviderKind, + document.Title, + document.Version, + document.UpdatedAtUtc, + new EveSurfaceTree( + surfaceId, + Node( + $"{surfaceId}.root", + "surface", + Array.Empty<(string Key, string Value)>(), + document.Surface.Root, + ButtonRow( + $"{surfaceId}.actions", + Button($"{surfaceId}.back", backLabel, backCommand))), + document.Surface.Styles), + document.Commands + .Concat(new[] + { + new EveCommandTemplate(backCommand, backLabel, "unity-uitoolkit") + }) + .ToArray()); + } + + private static EveSurfaceDocument BuildMenuSurfaceDocument( + string surfaceId, + string title, + IReadOnlyList commands, + params EveSurfaceComponent[] children) + { + return new EveSurfaceDocument( + MenuSurfaceType, + MenuSurfaceSchema, + MenuSurfaceProviderId, + MenuSurfaceProviderKind, + title, + version: 1, + DateTime.UtcNow.ToString("O"), + new EveSurfaceTree( + surfaceId, + Node($"{surfaceId}.root", "surface", Array.Empty<(string Key, string Value)>(), children), + Array.Empty()), + commands); + } + + private static EveSurfaceComponent Card( + string id, + string title, + params EveSurfaceComponent[] children) + { + return Node(id, "card", new[] { ("title", title) }, children); + } + + private static EveSurfaceComponent Metric(string id, string label, string value) + { + return Node(id, "metric", new[] { ("label", label), ("value", value) }); + } + + private static EveSurfaceComponent Text(string id, string value) + { + return Node(id, "text", new[] { ("value", value) }); + } + + private static EveSurfaceComponent Button(string id, string label, string command) + { + return Node(id, "control.button", new[] { ("label", label), ("command", command) }); + } + + private static EveSurfaceComponent ButtonRow( + string id, + params EveSurfaceComponent[] children) + { + return Node(id, "row", Array.Empty<(string Key, string Value)>(), children); + } + + private static EveSurfaceComponent Node( + string id, + string kind, + IEnumerable<(string Key, string Value)> props, + params EveSurfaceComponent[] children) + { + return new EveSurfaceComponent( + id, + kind, + props.ToDictionary(prop => prop.Key, prop => prop.Value, StringComparer.Ordinal), + children ?? Array.Empty()); + } + private static EveSurfaceDocument ToEveSurfaceDocument(AetheriaRuntimeSurfaceDocument document) { return new EveSurfaceDocument( @@ -440,10 +639,10 @@ public class MainMenu : MonoBehaviour private void OnDestroy() { - if (_playerSettingsSurfaceDocument != null) + if (_menuSurfaceDocument != null) { - Destroy(_playerSettingsSurfaceDocument.gameObject); - _playerSettingsSurfaceDocument = null; + Destroy(_menuSurfaceDocument.gameObject); + _menuSurfaceDocument = null; } } } diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 8816e51e7..b440c54e7 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -112,7 +112,10 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. equipped non-launcher weapons through typed behavior kind rows instead of inspecting runtime behavior config classes. Unity boot reads typed player settings back through the package-owned CultCache reader before falling back - to defaults. + to defaults. The main-menu settings navigation, input, and audio subpages now + also lower through local Eve/UI Toolkit surfaces instead of repopulating the + old `PropertiesPanel` button shell, even though typed rebinding controls and + typed audio controls are still future work. - The combat schematic HUD uses typed runtime catalog weapon facets for its static weapon icon strip; missing typed weapon facets no longer fall back to legacy `WeaponItemData`. Schematic weapon-row selection now uses typed @@ -1537,7 +1540,12 @@ First Aetheria surfaces to publish: `aetheria.player_settings` Eve surface contract. `MainMenu` no longer owns a special-case text field; UI Toolkit lowers the shared Eve text control and gameplay still owns the typed player-settings commit. - - Replace the next concrete uGUI screen with an Eve-owned surface. + - Done: replace the main-menu settings/input/audio subpages with + Eve-owned UI Toolkit surfaces. `MainMenu` still owns root navigation and + entry/exit flow, but the old `PropertiesPanel` settings shell no longer + owns those pages. Input rebinding itself remains on the runtime-owned + remap screen until Eve grows typed rebinding controls, and audio still + lacks a typed state surface. - Done: move the shared `org.gamecult.eve.surface` and `org.gamecult.eve.unity-uitoolkit` packages into the Eve repo and retarget Aetheria to import them from there instead of carrying local staged @@ -1893,6 +1901,9 @@ First Aetheria surfaces to publish: typed run checkpoint immediately, and typed continue-run restore rebuilds slot bindings onto the live current entity instead of leaving stale `ActionBarBinding` instances pointed at the previous entity body. +- Main-menu settings navigation, input, and audio pages now lower through a + shared UI Toolkit `UIDocument` host and local Eve surface documents instead + of rebuilding those pages through `PropertiesPanel.AddButton(...)` calls. - Keyboard display layout parsing is no longer JSON-backed: `InputDisplayLayout` builds its static ANSI-104 display projection from typed `InputLayout` rows/columns. The dead commented Ink `ToJson` write path and