Lower main menu root shell through Eve

This commit is contained in:
Meta Vens 2026-06-17 04:57:56 +01:00
commit 0ced284cab
4 changed files with 252 additions and 156 deletions

View file

@ -32,6 +32,7 @@ RequireEveRuntimeBootstrap(root);
RequireNoRendererLocalConsole(root);
RequireNoRendererLocalDebugPanels(root);
RequireMainMenuSettingsCommit(root);
RequireMainMenuRootUsesEveSurface(root);
RequireMainMenuSettingsShellUsesEveSurface(root);
RequireSectorMapZoneDetailsUseEveSurface(root);
RequireRuntimeMenuTabsUseEveSurface(root);
@ -507,6 +508,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 root shell: root navigation lowers through an Eve UI Toolkit surface instead of the legacy PropertiesPanel/fade shell");
Console.WriteLine("Main-menu settings shell: settings/input/audio subpages lower through Eve UI Toolkit surfaces instead of PropertiesPanel buttons");
Console.WriteLine("Sector-map zone details shell: zone inspection lowers through an Eve UI Toolkit surface instead of PropertiesPanel rows");
Console.WriteLine("Runtime menu tab shell: MenuPanel owns tab metadata and lowers navigation through an Eve UI Toolkit surface");
@ -2491,6 +2493,101 @@ static void RequireMainMenuSettingsShellUsesEveSurface(string root)
}
}
static void RequireMainMenuRootUsesEveSurface(string root)
{
var mainMenuPath = Path.Combine(root, "Assets", "Scripts", "UI", "MainMenu.cs");
if (!File.Exists(mainMenuPath))
{
throw new InvalidOperationException("Cannot verify main-menu root shell; MainMenu.cs is missing.");
}
var source = File.ReadAllText(mainMenuPath);
var requiredSymbols = new[]
{
"MainSurfaceId",
"ContinueRunCommand",
"NewGameCommand",
"ShowSettingsCommand",
"QuitCommand",
"BuildMainSurfaceDefinition(",
"HandleMainSurfaceCommand(",
"RenderMenuSurface(BuildMainSurfaceDefinition(LatestContinueRun(), InGame), HandleMainSurfaceCommand);",
"HideMenuSurface();"
};
var missingSymbols = requiredSymbols
.Where(symbol => !source.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingSymbols.Length > 0)
{
throw new InvalidOperationException(
"MainMenu no longer lowers the root shell through Eve surfaces: " +
string.Join(", ", missingSymbols));
}
var forbiddenSymbols = new[]
{
"PanelPrototype",
"FadeTime",
"FadeDistance",
"FadeAlphaExponent",
"FadePositionExponent",
"_currentMenu",
"_nextMenu",
"_fadeFromRight",
"_fadeLerp",
"_fading",
"_panelPosition",
"TitleSubtitle(",
"IsMenuSurfaceVisible(",
"_nextMenu.panel.AddButton(\"Continue\"",
"_nextMenu.panel.AddButton(\"New Game\"",
"_nextMenu.panel.AddButton(\"Settings\"",
"_nextMenu.panel.AddButton(\"Quit\"",
"Fade(true)",
"Fade(false)"
};
var hits = forbiddenSymbols
.Where(symbol => source.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (hits.Length > 0)
{
throw new InvalidOperationException(
"MainMenu still owns root navigation through the legacy PropertiesPanel/fade shell: " +
string.Join(", ", hits));
}
var prefabPath = Path.Combine(root, "Assets", "Prefabs", "UI", "Main Menu Canvas.prefab");
if (!File.Exists(prefabPath))
{
throw new InvalidOperationException("Cannot verify main-menu prefab shell; Main Menu Canvas.prefab is missing.");
}
var prefab = File.ReadAllText(prefabPath);
var forbiddenPrefabSymbols = new[]
{
"PanelPrototype:",
"FadeTime:",
"FadeDistance:",
"FadeAlphaExponent:",
"FadePositionExponent:"
};
var prefabHits = forbiddenPrefabSymbols
.Where(symbol => prefab.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (prefabHits.Length > 0)
{
throw new InvalidOperationException(
"Main-menu prefab still serializes the dead root shell: " +
string.Join(", ", prefabHits));
}
}
static void RequireSectorMapZoneDetailsUseEveSurface(string root)
{
var sectorRendererPath = Path.Combine(root, "Assets", "Scripts", "UI", "Menu", "SectorRenderer.cs");

View file

@ -916,11 +916,6 @@ MonoBehaviour:
Settings: {fileID: 11400000, guid: cd2344e2001a37544aab0960c164d4ff, type: 2}
Dialog: {fileID: 4533226043449861657}
InGame: 0
PanelPrototype: {fileID: 7329274282942065594}
FadeTime: 0.5
FadeDistance: 1024
FadeAlphaExponent: 2
FadePositionExponent: 2
--- !u!1 &4533226043526995715
GameObject:
m_ObjectHideFlags: 0

View file

@ -24,10 +24,15 @@ public class MainMenu : MonoBehaviour
private const string MenuSurfaceSchema = "gamecult.eve.surface.v1";
private const string MenuSurfaceProviderId = "aetheria";
private const string MenuSurfaceProviderKind = "game.menu";
private const string MainSurfaceId = "aetheria.main_menu.root";
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 ContinueRunCommand = "aetheria.main_menu.root.continue";
private const string NewGameCommand = "aetheria.main_menu.root.new_game";
private const string ShowSettingsCommand = "aetheria.main_menu.root.show_settings";
private const string QuitCommand = "aetheria.main_menu.root.quit";
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";
@ -38,160 +43,112 @@ public class MainMenu : MonoBehaviour
public GameSettings Settings;
public ConfirmationDialog Dialog;
public bool InGame;
public Prototype PanelPrototype;
public float FadeTime = .5f;
public float FadeDistance = 512;
public float FadeAlphaExponent = 2;
public float FadePositionExponent = 2;
private (PropertiesPanel panel, CanvasGroup group) _currentMenu, _nextMenu;
private bool _fadeFromRight;
private float _fadeLerp;
private bool _fading;
private Vector3 _panelPosition;
private UIDocument _menuSurfaceDocument;
void Start()
{
_panelPosition = PanelPrototype.transform.position;
var panel1 = PanelPrototype.Instantiate<PropertiesPanel>();
_currentMenu = (panel1, panel1.GetComponent<CanvasGroup>());
var panel2 = PanelPrototype.Instantiate<PropertiesPanel>();
_nextMenu = (panel2, panel2.GetComponent<CanvasGroup>());
_currentMenu.panel.gameObject.SetActive(false);
ShowMain();
Fade(true);
}
private void Update()
{
if (_fading)
{
_fadeLerp += Time.deltaTime / FadeTime;
_currentMenu.panel.transform.position =
_panelPosition + (_fadeFromRight ? Vector3.left : Vector3.right) * (FadeDistance * pow(_fadeLerp, FadePositionExponent));
_nextMenu.panel.transform.position =
_panelPosition + (_fadeFromRight ? Vector3.right : Vector3.left) * (FadeDistance * pow(1-_fadeLerp, FadePositionExponent));
_currentMenu.group.alpha = pow(1 - _fadeLerp, FadeAlphaExponent);
_nextMenu.group.alpha = pow(_fadeLerp, FadeAlphaExponent);
if (_fadeLerp > 1)
{
_fading = false;
_currentMenu.panel.gameObject.SetActive(false);
var temp = _currentMenu;
_currentMenu = _nextMenu;
_nextMenu = temp;
if (IsMenuSurfaceVisible())
_currentMenu.panel.gameObject.SetActive(false);
}
}
}
private string TitleSubtitle(string title, string subtitle) => $"{title}\n<smallcaps><size=50%>{subtitle}";
private void Fade(bool fromRight)
{
_nextMenu.panel.gameObject.SetActive(true);
_nextMenu.group.alpha = 0;
_fading = true;
_fadeLerp = 0;
_fadeFromRight = fromRight;
}
private bool IsMenuSurfaceVisible()
{
return _menuSurfaceDocument != null && _menuSurfaceDocument.gameObject.activeSelf;
}
private void ShowMain()
{
HideMenuSurface();
_nextMenu.panel.Clear();
_nextMenu.panel.Title.text = TitleSubtitle("aetheria", "terminus");
if (!InGame)
RenderMenuSurface(BuildMainSurfaceDefinition(LatestContinueRun(), InGame), HandleMainSurfaceCommand);
}
private void HandleMainSurfaceCommand(EveSurfaceCommandRequest request)
{
switch (request.Command)
{
var continueRun = LatestContinueRun();
_nextMenu.panel.AddButton("Continue", continueRun == null ? null : () => ContinueGame(continueRun));
}
_nextMenu.panel.AddButton("New Game",
() =>
{
var generatorState = "Loading typed catalog";
Action<string> setState = s => generatorState = s;
Fade(true);
_nextMenu.panel.gameObject.SetActive(false);
Dialog.Clear();
Dialog.Title.text = "Generating Galaxy";
Dialog.AddProperty(() => generatorState);
Dialog.Show();
if (ActionGameManager.RuntimePlayerSettings.TutorialPassed)
case ContinueRunCommand:
var continueRun = LatestContinueRun();
if (continueRun == null)
{
Settings.SectorBackgroundSettings.NoisePosition = Random.value * 1000;
ActionGameManager.IsTutorial = false;
var generationSeed = NextGenerationSeed();
Task.Run(() =>
{
var sector = new Galaxy(
Settings.SectorGenerationSettings,
Settings.SectorBackgroundSettings,
Settings.NameGeneratorSettings,
ActionGameManager.RuntimeCatalog,
Debug.Log,
setState,
generationSeed);
Observable.NextFrame().Subscribe(_ =>
{
ActionGameManager.CurrentGalaxy = sector;
SceneManager.LoadScene("ARPG");
});
}).WrapErrors();
Debug.LogWarning("Main-menu Continue requested without a typed run state.");
ShowMain();
return;
}
else
{
int iteration = 1;
do
{
Settings.TutorialBackgroundSettings.NoisePosition = Random.value * 1000;
setState($"Finding Galaxy Position: iteration {iteration++}");
} while (Settings.TutorialBackgroundSettings.CloudDensity(float2(0.5f)) < .5f);
ActionGameManager.IsTutorial = true;
var generationSeed = NextGenerationSeed();
Task.Run(() =>
{
var sector = new Galaxy(
Settings.TutorialGenerationSettings,
Settings.TutorialBackgroundSettings,
Settings.NameGeneratorSettings,
ActionGameManager.RuntimeCatalog,
ActionGameManager.RuntimePlayerSettings,
ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"),
Debug.Log,
setState,
generationSeed);
Observable.NextFrame().Subscribe(_ =>
{
ActionGameManager.CurrentGalaxy = sector;
SceneManager.LoadScene("ARPG");
});
}).WrapErrors();
}
});
_nextMenu.panel.AddButton("Settings",
() =>
{
ContinueGame(continueRun);
return;
case NewGameCommand:
StartNewGame();
return;
case ShowSettingsCommand:
ShowSettings();
Fade(true);
});
_nextMenu.panel.AddButton("Quit", Application.Quit);
return;
case QuitCommand:
Application.Quit();
return;
default:
Debug.LogWarning($"Unknown main menu command: {request.Command}");
return;
}
}
private void StartNewGame()
{
var generatorState = "Loading typed catalog";
Action<string> setState = s => generatorState = s;
HideMenuSurface();
Dialog.Clear();
Dialog.Title.text = "Generating Galaxy";
Dialog.AddProperty(() => generatorState);
Dialog.Show();
if (ActionGameManager.RuntimePlayerSettings.TutorialPassed)
{
Settings.SectorBackgroundSettings.NoisePosition = Random.value * 1000;
ActionGameManager.IsTutorial = false;
var generationSeed = NextGenerationSeed();
Task.Run(() =>
{
var sector = new Galaxy(
Settings.SectorGenerationSettings,
Settings.SectorBackgroundSettings,
Settings.NameGeneratorSettings,
ActionGameManager.RuntimeCatalog,
Debug.Log,
setState,
generationSeed);
Observable.NextFrame().Subscribe(_ =>
{
ActionGameManager.CurrentGalaxy = sector;
SceneManager.LoadScene("ARPG");
});
}).WrapErrors();
}
else
{
int iteration = 1;
do
{
Settings.TutorialBackgroundSettings.NoisePosition = Random.value * 1000;
setState($"Finding Galaxy Position: iteration {iteration++}");
} while (Settings.TutorialBackgroundSettings.CloudDensity(float2(0.5f)) < .5f);
ActionGameManager.IsTutorial = true;
var generationSeed = NextGenerationSeed();
Task.Run(() =>
{
var sector = new Galaxy(
Settings.TutorialGenerationSettings,
Settings.TutorialBackgroundSettings,
Settings.NameGeneratorSettings,
ActionGameManager.RuntimeCatalog,
ActionGameManager.RuntimePlayerSettings,
ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"),
Debug.Log,
setState,
generationSeed);
Observable.NextFrame().Subscribe(_ =>
{
ActionGameManager.CurrentGalaxy = sector;
SceneManager.LoadScene("ARPG");
});
}).WrapErrors();
}
}
private static uint NextGenerationSeed()
@ -226,8 +183,7 @@ public class MainMenu : MonoBehaviour
var generatorState = "Loading typed run";
Action<string> setState = s => generatorState = s;
Fade(true);
_nextMenu.panel.gameObject.SetActive(false);
HideMenuSurface();
Dialog.Clear();
Dialog.Title.text = "Continuing Run";
Dialog.AddProperty(() => generatorState);
@ -270,29 +226,21 @@ public class MainMenu : MonoBehaviour
private void ShowSettings()
{
_nextMenu.panel.Clear();
_nextMenu.panel.gameObject.SetActive(false);
RenderMenuSurface(BuildSettingsSurfaceDefinition(), HandleSettingsSurfaceCommand);
}
private void ShowInputSettings()
{
_nextMenu.panel.Clear();
_nextMenu.panel.gameObject.SetActive(false);
RenderMenuSurface(BuildInputSettingsSurfaceDefinition(), HandleInputSettingsSurfaceCommand);
}
private void ShowAudioSettings()
{
_nextMenu.panel.Clear();
_nextMenu.panel.gameObject.SetActive(false);
RenderMenuSurface(BuildAudioSettingsSurfaceDefinition(), HandleAudioSettingsSurfaceCommand);
}
private void ShowPlayerSettingsSurface()
{
_nextMenu.panel.Clear();
_nextMenu.panel.gameObject.SetActive(false);
RenderMenuSurface(
WithBackAction(
ToEveSurfaceDocument(BuildPlayerSettingsSurfaceDefinition()),
@ -349,9 +297,7 @@ public class MainMenu : MonoBehaviour
ShowAudioSettings();
return;
case BackToMainCommand:
HideMenuSurface();
ShowMain();
Fade(false);
return;
default:
Debug.LogWarning($"Unknown settings menu command: {request.Command}");
@ -436,6 +382,56 @@ public class MainMenu : MonoBehaviour
DateTime.UtcNow.ToString("O")));
}
private static EveSurfaceDocument BuildMainSurfaceDefinition(
AetheriaRuntimeRunStateSnapshot continueRun,
bool inGame)
{
var commands = new List<EveCommandTemplate>();
var actionButtons = new List<EveSurfaceComponent>();
var cardChildren = new List<EveSurfaceComponent>();
if (!inGame)
{
if (continueRun == null)
{
cardChildren.Add(Text(
$"{MainSurfaceId}.continue.note",
"No typed run state is available yet. Start a new galaxy or connect to a Verse that already has one."));
}
else
{
commands.Add(new EveCommandTemplate(ContinueRunCommand, "Continue", "unity-uitoolkit"));
actionButtons.Add(Button($"{MainSurfaceId}.continue", "Continue", ContinueRunCommand));
cardChildren.Add(Metric(
$"{MainSurfaceId}.continue.run",
"Latest Run",
continueRun.RunId));
}
}
commands.Add(new EveCommandTemplate(NewGameCommand, "New Game", "unity-uitoolkit"));
commands.Add(new EveCommandTemplate(ShowSettingsCommand, "Settings", "unity-uitoolkit"));
commands.Add(new EveCommandTemplate(QuitCommand, "Quit", "unity-uitoolkit"));
actionButtons.Add(Button($"{MainSurfaceId}.newGame", "New Game", NewGameCommand));
actionButtons.Add(Button($"{MainSurfaceId}.settings", "Settings", ShowSettingsCommand));
actionButtons.Add(Button($"{MainSurfaceId}.quit", "Quit", QuitCommand));
cardChildren.Add(Text(
$"{MainSurfaceId}.note",
"The client lowers this shell through Eve. Verse state and game truth belong to the daemon."));
cardChildren.Add(ButtonColumn($"{MainSurfaceId}.actions", actionButtons.ToArray()));
return BuildMenuSurfaceDocument(
MainSurfaceId,
"Aetheria Terminus",
commands,
Card(
$"{MainSurfaceId}.card",
"Aetheria Terminus",
cardChildren.ToArray()));
}
private static EveSurfaceDocument BuildSettingsSurfaceDefinition()
{
return BuildMenuSurfaceDocument(
@ -594,6 +590,13 @@ public class MainMenu : MonoBehaviour
return Node(id, "row", Array.Empty<(string Key, string Value)>(), children);
}
private static EveSurfaceComponent ButtonColumn(
string id,
params EveSurfaceComponent[] children)
{
return Node(id, "column", Array.Empty<(string Key, string Value)>(), children);
}
private static EveSurfaceComponent Node(
string id,
string kind,

View file

@ -1562,12 +1562,13 @@ 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.
- 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: replace the whole main-menu shell with Eve-owned UI Toolkit
surfaces. Root navigation, settings, input, and audio now lower through a
single Eve host instead of the old `PropertiesPanel`/fade shell. The menu
remains a client-side projection: the Aetheria daemon owns Verse state and
Unity renders and submits commands. 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: replace the sector-map zone details panel with an Eve-owned UI
Toolkit surface. `SectorRenderer` still owns reveal/camera behavior, but
zone inspection no longer rebuilds `PropertiesPanel` rows when the player