Add typed verse settings shell and command bridge

This commit is contained in:
Meta Vens 2026-06-17 11:24:36 +01:00
commit fea05967f8
14 changed files with 765 additions and 5 deletions

View file

@ -53,6 +53,7 @@ var eveStatus = new AetheriaEveCommandDrainStatus
AppliedCatalogRefreshes = eveReport.AppliedCatalogRefreshes,
AppliedOperationsRefreshes = eveReport.AppliedOperationsRefreshes,
AppliedPlayerSettingsCommands = eveReport.AppliedPlayerSettingsCommands,
AppliedVerseHostCommands = eveReport.AppliedVerseHostCommands,
LastRejectedCommand = eveReport.LastRejectedCommand,
LastRejectedReason = eveReport.LastRejectedReason,
Status = eveReport.RejectedCommands > 0 ? "rejected" : "ok"
@ -89,6 +90,7 @@ Console.WriteLine($"Eve commands rejected: {eveReport.RejectedCommands}");
Console.WriteLine($"Eve catalog refreshes: {eveReport.AppliedCatalogRefreshes}");
Console.WriteLine($"Eve operations refreshes: {eveReport.AppliedOperationsRefreshes}");
Console.WriteLine($"Eve player settings commands: {eveReport.AppliedPlayerSettingsCommands}");
Console.WriteLine($"Eve verse host commands: {eveReport.AppliedVerseHostCommands}");
if (!deleteApplied)
Console.WriteLine("Applied command files were kept because --keep was supplied.");

View file

@ -11,6 +11,8 @@
<Compile Include="..\..\Eve\packages\org.gamecult.eve.surface\Runtime\EveSurfaceDocument.cs" Link="EveSurfaceDocument.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCatalogSnapshot.cs" Link="AetheriaRuntimeCatalogSnapshot.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCatalogStore.cs" Link="AetheriaRuntimeCatalogStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeClientTargetCommands.cs" Link="AetheriaRuntimeClientTargetCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeClientTargetSurfaceBuilder.cs" Link="AetheriaRuntimeClientTargetSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeClientTargetStore.cs" Link="AetheriaRuntimeClientTargetStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateBoundary.cs" Link="AetheriaRuntimeStateBoundary.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateBoot.cs" Link="AetheriaRuntimeStateBoot.cs" />

View file

@ -51,6 +51,7 @@ RequireNoDeadPopupShells(root);
RequirePlayerSettingsEveSurface(root);
RequireVerseHostSettingsAuthority(root);
RequireClientTargetBootAuthority(root);
RequireVerseSettingsShellAndBridge(root);
RequireMainMenuVerseHostProjection(root);
RequireMainMenuContinueRunState(root);
RequireDeadPropertiesPanelShellDeleted(root);
@ -538,6 +539,7 @@ Console.WriteLine("NameTools editor shell: the remaining name helper window lowe
Console.WriteLine("Runtime state reader authority: Unity gameplay/UI read typed state through a shared runtime reader instead of direct store spelunking");
Console.WriteLine("Verse host authority: daemon-owned typed verse host settings now drive provider advertisement and operations telemetry");
Console.WriteLine("Client target boot authority: Unity boot resolves the active Verse through a typed client target instead of local path folklore");
Console.WriteLine("Verse settings shell: client target edits and Verse-host visibility commands lower through typed Eve surfaces");
Console.WriteLine("Main-menu Verse projection: the Unity Eve shell lowers daemon-owned verse identity instead of ad-libbing local menu copy");
Console.WriteLine("Runtime simulation tuning authority: UI writes flow through gameplay checkpoint commits");
Console.WriteLine("Hull conductivity authority: inventory UI toggles flow through gameplay checkpoint commits");
@ -2464,11 +2466,14 @@ static void RequireMainMenuSettingsShellUsesEveSurface(string root)
{
"RenderMenuSurface(",
"BuildSettingsSurfaceDefinition()",
"BuildVerseSettingsSurfaceDefinition()",
"BuildInputSettingsSurfaceDefinition(",
"HandleSettingsSurfaceCommand(",
"HandleVerseSettingsSurfaceCommand(",
"HandleInputSettingsSurfaceCommand(",
"WithBackAction(",
"ShowPlayerSettingsCommand",
"ShowVerseSettingsCommand",
"ShowInputSettingsCommand",
"BackToMainCommand",
"BackToSettingsCommand"
@ -2488,6 +2493,7 @@ static void RequireMainMenuSettingsShellUsesEveSurface(string root)
var forbiddenSymbols = new[]
{
"_nextMenu.panel.AddButton(\"Player Settings\"",
"_nextMenu.panel.AddButton(\"Verse\"",
"_nextMenu.panel.AddButton(\"Input\"",
"_nextMenu.panel.AddButton(\"Audio\"",
"BuildAudioSettingsSurfaceDefinition(",
@ -4035,6 +4041,225 @@ static void RequireClientTargetBootAuthority(string root)
}
}
static void RequireVerseSettingsShellAndBridge(string root)
{
var unityFacadeProjectPath = Path.Combine(root, "Aetheria.State.Unity", "Aetheria.State.Unity.csproj");
var stateProjectPath = Path.Combine(root, "Aetheria.State", "Aetheria.State.csproj");
var clientTargetCommandsPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeClientTargetCommands.cs");
var clientTargetSurfaceBuilderPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeClientTargetSurfaceBuilder.cs");
var verseHostCommandsPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeVerseHostCommands.cs");
var clientTargetStorePath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeClientTargetStore.cs");
var mainMenuPath = Path.Combine(root, "Assets", "Scripts", "UI", "MainMenu.cs");
var commandBridgePath = Path.Combine(root, "Aetheria.State", "AetheriaEveCommandBridge.cs");
var drainStatusPath = Path.Combine(root, "Aetheria.State", "Documents", "AetheriaEveCommandDrainStatus.cs");
var operationsProjectorPath = Path.Combine(root, "Aetheria.State", "AetheriaOperationsSurfaceProjector.cs");
var applyPendingPath = Path.Combine(root, "Aetheria.State.ApplyPending", "Program.cs");
var economyServerPath = Path.Combine(root, "Economy.Server", "Program.cs");
var requiredFiles = new[]
{
unityFacadeProjectPath,
stateProjectPath,
clientTargetCommandsPath,
clientTargetSurfaceBuilderPath,
verseHostCommandsPath,
clientTargetStorePath,
mainMenuPath,
commandBridgePath,
drainStatusPath,
operationsProjectorPath,
applyPendingPath,
economyServerPath
};
var missingFiles = requiredFiles
.Where(path => !File.Exists(path))
.Select(path => Path.GetRelativePath(root, path))
.ToArray();
if (missingFiles.Length > 0)
{
throw new InvalidOperationException(
"Verse settings shell/bridge cannot be verified because required files are missing: " +
string.Join(", ", missingFiles));
}
var unityFacadeProject = File.ReadAllText(unityFacadeProjectPath);
var stateProject = File.ReadAllText(stateProjectPath);
var clientTargetCommands = File.ReadAllText(clientTargetCommandsPath);
var clientTargetSurfaceBuilder = File.ReadAllText(clientTargetSurfaceBuilderPath);
var verseHostCommands = File.ReadAllText(verseHostCommandsPath);
var clientTargetStore = File.ReadAllText(clientTargetStorePath);
var mainMenu = File.ReadAllText(mainMenuPath);
var commandBridge = File.ReadAllText(commandBridgePath);
var drainStatus = File.ReadAllText(drainStatusPath);
var operationsProjector = File.ReadAllText(operationsProjectorPath);
var applyPending = File.ReadAllText(applyPendingPath);
var economyServer = File.ReadAllText(economyServerPath);
var requiredUnityFacadeSymbols = new[]
{
"AetheriaRuntimeClientTargetCommands.cs",
"AetheriaRuntimeClientTargetSurfaceBuilder.cs"
};
var missingUnityFacadeSymbols = requiredUnityFacadeSymbols
.Where(symbol => !unityFacadeProject.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingUnityFacadeSymbols.Length > 0)
{
throw new InvalidOperationException(
"Aetheria.State.Unity.csproj does not compile the Verse settings shell files: " +
string.Join(", ", missingUnityFacadeSymbols));
}
if (!stateProject.Contains("AetheriaRuntimeVerseHostCommands.cs", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Aetheria.State.csproj does not compile the Verse-host Eve command contract.");
}
var requiredClientTargetCommandSymbols = new[]
{
"SurfaceId = \"aetheria.client_target\"",
"CycleTargetKind",
"SetTitle",
"SetVerseId",
"SetCultMeshAddress",
"SetStateFilePath",
"IsKnown"
};
var missingClientTargetCommandSymbols = requiredClientTargetCommandSymbols
.Where(symbol => !clientTargetCommands.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingClientTargetCommandSymbols.Length > 0)
{
throw new InvalidOperationException(
"Client-target command contract is incomplete: " +
string.Join(", ", missingClientTargetCommandSymbols));
}
var requiredVerseHostCommandSymbols = new[]
{
"SurfaceId = \"aetheria.verse_host_settings\"",
"CycleVisibility",
"Refresh",
"IsKnown"
};
var missingVerseHostCommandSymbols = requiredVerseHostCommandSymbols
.Where(symbol => !verseHostCommands.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingVerseHostCommandSymbols.Length > 0)
{
throw new InvalidOperationException(
"Verse-host command contract is incomplete: " +
string.Join(", ", missingVerseHostCommandSymbols));
}
var requiredSurfaceBuilderSymbols = new[]
{
"public sealed class AetheriaRuntimeClientTargetSurfaceState",
"\"Client Target\"",
"\"Target Fields\"",
"\"Daemon Verse Host\"",
"AetheriaRuntimeClientTargetCommands.SetStateFilePath",
"AetheriaRuntimeVerseHostCommands.CycleVisibility",
"AetheriaRuntimeVerseHostCommands.Refresh"
};
var missingSurfaceBuilderSymbols = requiredSurfaceBuilderSymbols
.Where(symbol => !clientTargetSurfaceBuilder.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingSurfaceBuilderSymbols.Length > 0)
{
throw new InvalidOperationException(
"Client-target Eve surface builder is incomplete: " +
string.Join(", ", missingSurfaceBuilderSymbols));
}
if (!clientTargetStore.Contains("Update(", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Client-target store no longer exposes typed update ownership for local target edits.");
}
var requiredMainMenuSymbols = new[]
{
"VerseSettingsShellSurfaceId",
"ShowVerseSettingsCommand",
"ShowVerseSettingsSurface()",
"HandleVerseSettingsSurfaceCommand(EveSurfaceCommandRequest request)",
"BuildVerseSettingsSurfaceDefinition()",
"AetheriaRuntimeClientTargetSurfaceBuilder.Build(",
"TryCommitClientTargetCommand(",
"TryQueueVerseHostCommand(",
"AetheriaRuntimeClientTargetStore.Update(",
"new EveSurfaceCommandRequest(",
"AetheriaRuntimeVerseHostCommands.SurfaceId"
};
var missingMainMenuSymbols = requiredMainMenuSymbols
.Where(symbol => !mainMenu.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingMainMenuSymbols.Length > 0)
{
throw new InvalidOperationException(
"MainMenu no longer owns the typed Verse settings shell handoff: " +
string.Join(", ", missingMainMenuSymbols));
}
var requiredBridgeSymbols = new[]
{
"AppliedVerseHostCommands",
"AetheriaRuntimeVerseHostCommands.Refresh",
"AetheriaRuntimeVerseHostCommands.CycleVisibility",
"ApplyVerseHostCommandAsync",
"PutVerseHostSettingsAsync",
"PutProviderAdvertisementAsync",
"AetheriaOperationsSurfaceProjector.Build(",
"AetheriaRuntimeVerseHostCommands.IsKnown(command)"
};
var missingBridgeSymbols = requiredBridgeSymbols
.Where(symbol => !commandBridge.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingBridgeSymbols.Length > 0)
{
throw new InvalidOperationException(
"Aetheria Eve command bridge no longer routes Verse-host commands through the provider owner: " +
string.Join(", ", missingBridgeSymbols));
}
var requiredDrainSymbols = new[]
{
"AppliedVerseHostCommands"
};
var missingDrainSymbols = requiredDrainSymbols
.Where(symbol => !drainStatus.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingDrainSymbols.Length > 0)
{
throw new InvalidOperationException(
"Aetheria Eve command drain status no longer records Verse-host command counts.");
}
if (!operationsProjector.Contains("Verse Host Commands", StringComparison.Ordinal) ||
!operationsProjector.Contains("AppliedVerseHostCommands", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Operations surface no longer projects Verse-host Eve command counts.");
}
if (!applyPending.Contains("AppliedVerseHostCommands = eveReport.AppliedVerseHostCommands", StringComparison.Ordinal) ||
!applyPending.Contains("Eve verse host commands: {eveReport.AppliedVerseHostCommands}", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Aetheria.State.ApplyPending no longer publishes Verse-host Eve command totals.");
}
if (!economyServer.Contains("AppliedVerseHostCommands = report.AppliedVerseHostCommands", StringComparison.Ordinal) ||
!economyServer.Contains("verseHost={report.AppliedVerseHostCommands}", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Economy.Server no longer publishes Verse-host Eve command totals.");
}
}
static void RequireMainMenuVerseHostProjection(string root)
{
var packageSnapshotPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeCatalogSnapshot.cs");

View file

@ -11,6 +11,7 @@
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateCommitDocument.cs" Link="AetheriaRuntimeStateCommitDocument.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeEveCommandDocument.cs" Link="AetheriaRuntimeEveCommandDocument.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimePendingCultCacheStore.cs" Link="AetheriaRuntimePendingCultCacheStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeVerseHostCommands.cs" Link="AetheriaRuntimeVerseHostCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimePlayerSettingsSurfaceBuilder.cs" Link="AetheriaRuntimePlayerSettingsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimePlayerSettingsCommands.cs" Link="AetheriaRuntimePlayerSettingsCommands.cs" />
<ProjectReference Include="..\..\CultLib\src\GameCult.Caching\GameCult.Caching.csproj" />

View file

@ -8,6 +8,7 @@ public sealed class AetheriaEveCommandApplyReport
public int AppliedCatalogRefreshes { get; set; }
public int AppliedOperationsRefreshes { get; set; }
public int AppliedPlayerSettingsCommands { get; set; }
public int AppliedVerseHostCommands { get; set; }
public int RejectedCommands { get; set; }
public string[] AcceptedPaths { get; set; } = [];
public string[] RejectedPaths { get; set; } = [];
@ -85,6 +86,11 @@ public static class AetheriaEveCommandBridge
await ApplyPlayerSettingsCommandAsync(node, command).ConfigureAwait(false);
report.AppliedPlayerSettingsCommands++;
break;
case GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.Refresh:
case GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.CycleVisibility:
await ApplyVerseHostCommandAsync(node, command).ConfigureAwait(false);
report.AppliedVerseHostCommands++;
break;
}
accepted.Add(path);
@ -122,7 +128,9 @@ public static class AetheriaEveCommandBridge
(string.Equals(surfaceId, AetheriaOperationsSurfaceProjector.SurfaceId, StringComparison.Ordinal) &&
string.Equals(command, "aetheria.operations.refresh", StringComparison.Ordinal)) ||
(string.Equals(surfaceId, AetheriaPlayerSettingsSurfaceProjector.SurfaceId, StringComparison.Ordinal) &&
GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.IsKnown(command));
GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.IsKnown(command)) ||
(string.Equals(surfaceId, GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.SurfaceId, StringComparison.Ordinal) &&
GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.IsKnown(command));
}
private static async Task ApplyPlayerSettingsCommandAsync(
@ -185,6 +193,46 @@ public static class AetheriaEveCommandBridge
.ConfigureAwait(false);
}
private static async Task ApplyVerseHostCommandAsync(
AetheriaStateNode node,
AetheriaRuntimeEveCommandDocument command)
{
var settings = await node.GetVerseHostSettingsAsync().ConfigureAwait(false) ?? new AetheriaVerseHostSettings();
var normalized = AetheriaVerseHostSettingsNormalizer.Normalize(settings);
var persistSettings = false;
switch (command.Command)
{
case GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.CycleVisibility:
normalized.Visibility = Cycle(normalized.Visibility, "private", "public");
persistSettings = true;
break;
}
if (persistSettings)
{
normalized.LastUpdatedAtUtc = command.IssuedAtUtc;
await node.PutVerseHostSettingsAsync(normalized).ConfigureAwait(false);
}
var commitStatus = await node.GetRuntimeCommitDrainStatusAsync().ConfigureAwait(false) ??
EmptyCommitDrainStatus(node.StatePath, command.IssuedAtUtc);
var eveStatus = await node.GetEveCommandDrainStatusAsync().ConfigureAwait(false) ??
EmptyEveCommandDrainStatus(node.StatePath, command.IssuedAtUtc);
var runtimeSession = string.IsNullOrWhiteSpace(commitStatus.RuntimeId)
? null
: await node.GetRuntimeSessionAsync(commitStatus.RuntimeId).ConfigureAwait(false);
await node.PutOperationsSurfaceAsync(
AetheriaOperationsSurfaceProjector.Build(
commitStatus,
eveStatus,
normalized,
runtimeSession)).ConfigureAwait(false);
await node.PutProviderAdvertisementAsync(
AetheriaProviderAdvertisementProjector.Build(normalized, node.StatePath, command.IssuedAtUtc)).ConfigureAwait(false);
}
private static string Cycle(string current, params string[] values)
{
if (values.Length == 0)

View file

@ -62,6 +62,7 @@ public static class AetheriaOperationsSurfaceProjector
Metric("eveCommandDrain.catalogRefreshes", "Catalog Refreshes", (eveCommandStatus?.AppliedCatalogRefreshes ?? 0).ToString()),
Metric("eveCommandDrain.operationsRefreshes", "Operations Refreshes", (eveCommandStatus?.AppliedOperationsRefreshes ?? 0).ToString()),
Metric("eveCommandDrain.playerSettings", "Player Settings Commands", (eveCommandStatus?.AppliedPlayerSettingsCommands ?? 0).ToString()),
Metric("eveCommandDrain.verseHost", "Verse Host Commands", (eveCommandStatus?.AppliedVerseHostCommands ?? 0).ToString()),
Metric("eveCommandDrain.failures", "Consecutive Failures", (eveCommandStatus?.ConsecutiveFailures ?? 0).ToString()),
Row(
"eveCommandDrain.last",

View file

@ -57,4 +57,7 @@ public sealed class AetheriaEveCommandDrainStatus
[Key(15)]
public int AppliedPlayerSettingsCommands { get; set; }
[Key(16)]
public int AppliedVerseHostCommands { get; set; }
}

View file

@ -56,8 +56,9 @@ Unity Eve surfaces also emit typed `gamecult.eve.command.v1` documents under
provider-owned acceptance organ for those commands: it validates the provider,
surface, and command template before running the narrow refresh handlers for
the catalog and operations surfaces plus the typed player-settings mutation
handlers for gameplay/graphics Eve controls. Renderer callbacks do not accept
commands or mutate game state locally.
handlers for gameplay/graphics Eve controls and typed Verse-host visibility
commands. Renderer callbacks do not accept commands or mutate game state
locally.
`AetheriaEveRuntimeBootstrap` mounts the `aetheria.operations` surface through a
runtime-created `UIDocument` after scene load, with environment and command-line
switches for diagnostics. UI Toolkit is now a live runtime lowering path, not
@ -316,6 +317,10 @@ can read typed player settings for Unity boot, and can read the published Eve
catalog surface. It does not deserialize legacy runtime projection objects and
does not write daemon-owned Verse state. Unity can use this as the first
package boundary once CultLib/Eve runtime packaging is available.
Unity `MainMenu` now lowers a typed Verse settings shell over that split:
client-target edits persist locally in `aetheria-client.cc`, while Verse-host
visibility changes queue provider-owned Eve commands against the selected local
Verse state file.
`Galaxy` now consumes this package-owned runtime catalog for faction selection
and name generation, so generated sectors use typed corporation v2 and
`aetheria.name_file.v2` records instead of legacy `Faction`/`NameFile`

View file

@ -28,12 +28,14 @@ public class MainMenu : MonoBehaviour
private const string SettingsSurfaceId = "aetheria.main_menu.settings";
private const string InputSettingsSurfaceId = "aetheria.main_menu.input_settings";
private const string PlayerSettingsShellSurfaceId = "aetheria.main_menu.player_settings";
private const string VerseSettingsShellSurfaceId = "aetheria.main_menu.verse_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 OpenRuntimeInputScreenCommand = "aetheria.main_menu.input_settings.open_runtime_screen";
private const string ShowPlayerSettingsCommand = "aetheria.main_menu.settings.show_player_settings";
private const string ShowVerseSettingsCommand = "aetheria.main_menu.settings.show_verse_settings";
private const string ShowInputSettingsCommand = "aetheria.main_menu.settings.show_input_settings";
private const string BackToMainCommand = "aetheria.main_menu.settings.back_to_main";
private const string BackToSettingsCommand = "aetheria.main_menu.settings.back_to_settings";
@ -275,6 +277,17 @@ public class MainMenu : MonoBehaviour
HandlePlayerSettingsSurfaceCommand);
}
private void ShowVerseSettingsSurface()
{
RenderMenuSurface(
WithBackAction(
ToEveSurfaceDocument(BuildVerseSettingsSurfaceDefinition()),
VerseSettingsShellSurfaceId,
BackToSettingsCommand,
"Back"),
HandleVerseSettingsSurfaceCommand);
}
private void RenderMenuSurface(
EveSurfaceDocument document,
Action<EveSurfaceCommandRequest> commandHandler)
@ -315,6 +328,9 @@ public class MainMenu : MonoBehaviour
case ShowPlayerSettingsCommand:
ShowPlayerSettingsSurface();
return;
case ShowVerseSettingsCommand:
ShowVerseSettingsSurface();
return;
case ShowInputSettingsCommand:
ShowInputSettings();
return;
@ -366,6 +382,29 @@ public class MainMenu : MonoBehaviour
ShowPlayerSettingsSurface();
}
private void HandleVerseSettingsSurfaceCommand(EveSurfaceCommandRequest request)
{
if (string.Equals(request.Command, BackToSettingsCommand, StringComparison.Ordinal))
{
ShowSettings();
return;
}
if (TryCommitClientTargetCommand(request.Command, request.Payload))
{
ShowVerseSettingsSurface();
return;
}
if (TryQueueVerseHostCommand(request.Command, request.Payload))
{
ShowVerseSettingsSurface();
return;
}
Debug.LogWarning($"Unknown verse-settings command: {request.Command}");
}
private void HideMenuSurface()
{
if (_menuSurfaceDocument == null)
@ -403,6 +442,27 @@ public class MainMenu : MonoBehaviour
DateTime.UtcNow.ToString("O")));
}
private static AetheriaRuntimeSurfaceDocument BuildVerseSettingsSurfaceDefinition()
{
var stateBoot = CurrentStateBoot();
var verseHost = LatestVerseHostSettings(stateBoot);
return AetheriaRuntimeClientTargetSurfaceBuilder.Build(
new AetheriaRuntimeClientTargetSurfaceState(
stateBoot.TargetKind,
stateBoot.Title,
stateBoot.VerseId,
stateBoot.CultMeshAddress,
stateBoot.StateFilePath,
stateBoot.TargetSource,
stateBoot.SupportsLocalStateFileRead,
stateBoot.FailureMessage,
verseHost?.Title ?? stateBoot.Title,
verseHost?.VerseId ?? stateBoot.VerseId,
verseHost?.Visibility ?? "unknown",
verseHost?.CultMeshAddress ?? stateBoot.CultMeshAddress,
DateTime.UtcNow.ToString("O")));
}
private static EveSurfaceDocument BuildMainSurfaceDefinition(
AetheriaRuntimeStateBootReport stateBoot,
AetheriaRuntimeRunStateSnapshot continueRun,
@ -505,6 +565,7 @@ public class MainMenu : MonoBehaviour
new[]
{
new EveCommandTemplate(ShowPlayerSettingsCommand, "Player Settings", "unity-uitoolkit"),
new EveCommandTemplate(ShowVerseSettingsCommand, "Verse", "unity-uitoolkit"),
new EveCommandTemplate(ShowInputSettingsCommand, "Input", "unity-uitoolkit"),
new EveCommandTemplate(BackToMainCommand, "Back", "unity-uitoolkit")
},
@ -513,10 +574,11 @@ public class MainMenu : MonoBehaviour
"Settings",
Text(
"aetheria.mainMenu.settings.note",
"Typed Verse settings already lower through Eve. Input rebinding now opens the runtime Eve input screen, and audio still has no typed surface."),
"Player settings, client target selection, and input rebinding all lower through typed Eve surfaces. Audio still has no typed owner."),
ButtonRow(
"aetheria.mainMenu.settings.actions",
Button("aetheria.mainMenu.settings.playerSettings", "Player Settings", ShowPlayerSettingsCommand),
Button("aetheria.mainMenu.settings.verse", "Verse", ShowVerseSettingsCommand),
Button("aetheria.mainMenu.settings.input", "Input", ShowInputSettingsCommand),
Button("aetheria.mainMenu.settings.back", "Back", BackToMainCommand))));
}
@ -540,6 +602,101 @@ public class MainMenu : MonoBehaviour
return true;
}
private static bool TryCommitClientTargetCommand(string command, IReadOnlyDictionary<string, string> payload)
{
if (!AetheriaRuntimeClientTargetCommands.IsKnown(command))
return false;
if (string.Equals(command, AetheriaRuntimeClientTargetCommands.Refresh, StringComparison.Ordinal))
return true;
try
{
var gameDataDirectory = ActionGameManager.GameDataDirectory;
var clientTargetPath = AetheriaRuntimeStateBoundary.GetClientTargetPath(gameDataDirectory);
var defaultStateFilePath = AetheriaRuntimeStateBoundary.GetStateFilePath(gameDataDirectory);
AetheriaRuntimeClientTargetStore.Update(
clientTargetPath,
defaultStateFilePath,
document =>
{
switch (command)
{
case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.CycleTargetKind, StringComparison.Ordinal):
document.TargetKind = string.Equals(document.TargetKind, AetheriaRuntimeClientTargetKinds.CultMeshVerse, StringComparison.Ordinal)
? AetheriaRuntimeClientTargetKinds.StateFile
: AetheriaRuntimeClientTargetKinds.CultMeshVerse;
break;
case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetTitle, StringComparison.Ordinal):
document.Title = ReadPayloadValue(payload, "value");
break;
case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetVerseId, StringComparison.Ordinal):
document.VerseId = ReadPayloadValue(payload, "value");
break;
case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetCultMeshAddress, StringComparison.Ordinal):
document.CultMeshAddress = ReadPayloadValue(payload, "value");
break;
case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetStateFilePath, StringComparison.Ordinal):
document.StateFilePath = ReadPayloadValue(payload, "value");
break;
}
if (string.IsNullOrWhiteSpace(document.TargetKind))
document.TargetKind = AetheriaRuntimeClientTargetKinds.StateFile;
if (string.IsNullOrWhiteSpace(document.StateFilePath))
document.StateFilePath = defaultStateFilePath;
document.UpdatedAtUtc = DateTime.UtcNow.ToString("O");
});
return true;
}
catch (Exception ex)
{
Debug.LogError($"Failed to update typed Aetheria client target: {ex}");
return true;
}
}
private static bool TryQueueVerseHostCommand(string command, IReadOnlyDictionary<string, string> payload)
{
if (!AetheriaRuntimeVerseHostCommands.IsKnown(command))
return false;
var stateBoot = CurrentStateBoot();
if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists)
{
Debug.LogWarning(
$"Cannot queue Verse-host command '{command}' because the active target is not a readable local Verse state file.");
return true;
}
try
{
var request = new EveSurfaceCommandRequest(
"aetheria",
AetheriaRuntimeVerseHostCommands.SurfaceId,
command,
payload?.ToDictionary(entry => entry.Key, entry => entry.Value ?? "", StringComparer.Ordinal)
?? new Dictionary<string, string>(StringComparer.Ordinal),
DateTimeOffset.UtcNow,
"unity-main-menu");
var envelope = AetheriaRuntimeEveCommandLog.QueueCommand(stateBoot.StateFilePath, request);
Debug.Log($"Queued Aetheria Verse-host Eve command: {envelope.Path}");
return true;
}
catch (Exception ex)
{
Debug.LogError($"Failed to queue typed Aetheria Verse-host Eve command: {ex}");
return true;
}
}
private static string ReadPayloadValue(IReadOnlyDictionary<string, string> payload, string key)
{
return payload != null && payload.TryGetValue(key, out var value)
? value ?? ""
: "";
}
private static EveSurfaceDocument BuildInputSettingsSurfaceDefinition(bool canOpenRuntimeInputScreen, bool inGame)
{
var commands = new List<EveCommandTemplate>

View file

@ -159,6 +159,7 @@ internal static class Program
AppliedCatalogRefreshes = report.AppliedCatalogRefreshes,
AppliedOperationsRefreshes = report.AppliedOperationsRefreshes,
AppliedPlayerSettingsCommands = report.AppliedPlayerSettingsCommands,
AppliedVerseHostCommands = report.AppliedVerseHostCommands,
LastRejectedCommand = report.LastRejectedCommand,
LastRejectedReason = report.LastRejectedReason,
ConsecutiveFailures = 0,
@ -178,7 +179,8 @@ internal static class Program
$"rejected={report.RejectedCommands}, " +
$"catalogRefreshes={report.AppliedCatalogRefreshes}, " +
$"operationsRefreshes={report.AppliedOperationsRefreshes}, " +
$"playerSettings={report.AppliedPlayerSettingsCommands}");
$"playerSettings={report.AppliedPlayerSettingsCommands}, " +
$"verseHost={report.AppliedVerseHostCommands}");
}
catch (Exception ex)
{

View file

@ -0,0 +1,23 @@
namespace GameCult.Aetheria.State.Unity
{
public static class AetheriaRuntimeClientTargetCommands
{
public const string SurfaceId = "aetheria.client_target";
public const string Refresh = "aetheria.client_target.refresh";
public const string CycleTargetKind = "aetheria.client_target.kind.cycle";
public const string SetTitle = "aetheria.client_target.title.set";
public const string SetVerseId = "aetheria.client_target.verse_id.set";
public const string SetCultMeshAddress = "aetheria.client_target.cultmesh_address.set";
public const string SetStateFilePath = "aetheria.client_target.state_file_path.set";
public static bool IsKnown(string command)
{
return command == Refresh ||
command == CycleTargetKind ||
command == SetTitle ||
command == SetVerseId ||
command == SetCultMeshAddress ||
command == SetStateFilePath;
}
}
}

View file

@ -83,6 +83,19 @@ namespace GameCult.Aetheria.State.Unity
MessagePackSerializer.Serialize(document));
}
public static AetheriaRuntimeClientTargetDocument Update(
string clientTargetPath,
string defaultStateFilePath,
Action<AetheriaRuntimeClientTargetDocument> mutate)
{
if (mutate == null) throw new ArgumentNullException(nameof(mutate));
var document = ReadOrInitialize(clientTargetPath, defaultStateFilePath);
mutate(document);
Write(clientTargetPath, document);
return document;
}
public static AetheriaRuntimeClientTargetDocument Read(string clientTargetPath)
{
return MessagePackSerializer.Deserialize<AetheriaRuntimeClientTargetDocument>(

View file

@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace GameCult.Aetheria.State.Unity
{
public sealed class AetheriaRuntimeClientTargetSurfaceState
{
public AetheriaRuntimeClientTargetSurfaceState(
string targetKind,
string targetTitle,
string targetVerseId,
string targetCultMeshAddress,
string targetStateFilePath,
string targetSource,
bool supportsLocalStateFileRead,
string bootFailureMessage,
string hostTitle,
string hostVerseId,
string hostVisibility,
string hostCultMeshAddress,
string updatedAtUtc)
{
TargetKind = targetKind ?? "";
TargetTitle = targetTitle ?? "";
TargetVerseId = targetVerseId ?? "";
TargetCultMeshAddress = targetCultMeshAddress ?? "";
TargetStateFilePath = targetStateFilePath ?? "";
TargetSource = targetSource ?? "";
SupportsLocalStateFileRead = supportsLocalStateFileRead;
BootFailureMessage = bootFailureMessage ?? "";
HostTitle = hostTitle ?? "";
HostVerseId = hostVerseId ?? "";
HostVisibility = hostVisibility ?? "";
HostCultMeshAddress = hostCultMeshAddress ?? "";
UpdatedAtUtc = updatedAtUtc ?? "";
}
public string TargetKind { get; }
public string TargetTitle { get; }
public string TargetVerseId { get; }
public string TargetCultMeshAddress { get; }
public string TargetStateFilePath { get; }
public string TargetSource { get; }
public bool SupportsLocalStateFileRead { get; }
public string BootFailureMessage { get; }
public string HostTitle { get; }
public string HostVerseId { get; }
public string HostVisibility { get; }
public string HostCultMeshAddress { get; }
public string UpdatedAtUtc { get; }
public string TargetLabel =>
string.IsNullOrWhiteSpace(TargetTitle)
? (string.IsNullOrWhiteSpace(TargetVerseId) ? "Unknown Verse" : TargetVerseId)
: (string.IsNullOrWhiteSpace(TargetVerseId) || string.Equals(TargetTitle, TargetVerseId, StringComparison.Ordinal)
? TargetTitle
: $"{TargetTitle} ({TargetVerseId})");
public string HostLabel =>
string.IsNullOrWhiteSpace(HostTitle)
? (string.IsNullOrWhiteSpace(HostVerseId) ? "Unknown Verse" : HostVerseId)
: (string.IsNullOrWhiteSpace(HostVerseId) || string.Equals(HostTitle, HostVerseId, StringComparison.Ordinal)
? HostTitle
: $"{HostTitle} ({HostVerseId})");
}
public static class AetheriaRuntimeClientTargetSurfaceBuilder
{
public static AetheriaRuntimeSurfaceDocument Build(
AetheriaRuntimeClientTargetSurfaceState state,
long version = 1)
{
state ??= new AetheriaRuntimeClientTargetSurfaceState(
AetheriaRuntimeClientTargetKinds.StateFile,
"",
"",
"",
"",
"",
supportsLocalStateFileRead: true,
bootFailureMessage: "",
hostTitle: "",
hostVerseId: "",
hostVisibility: "",
hostCultMeshAddress: "",
updatedAtUtc: "");
var targetKindLabel = string.Equals(state.TargetKind, AetheriaRuntimeClientTargetKinds.CultMeshVerse, StringComparison.Ordinal)
? "CultMesh Verse"
: "Local State File";
var visibilityLabel = string.IsNullOrWhiteSpace(state.HostVisibility) ? "unknown" : state.HostVisibility;
var visibilityActionLabel = string.Equals(visibilityLabel, "public", StringComparison.OrdinalIgnoreCase)
? "Make Private"
: "Make Public";
var summaryNote = !state.SupportsLocalStateFileRead && !string.IsNullOrWhiteSpace(state.BootFailureMessage)
? state.BootFailureMessage
: string.Equals(state.TargetSource, "state-path-override", StringComparison.Ordinal)
? "AETHERIA_STATE_PATH is overriding the persisted client target. Update the environment if you want boot to follow the saved target again."
: "Client target edits persist in aetheria-client.cc. Verse visibility changes queue typed Eve commands for the provider bridge.";
return new AetheriaRuntimeSurfaceDocument(
providerId: "aetheria",
providerKind: "game.menu",
title: "Aetheria Verse Settings",
version: version,
updatedAtUtc: state.UpdatedAtUtc,
surface: new AetheriaRuntimeSurfaceTree(
AetheriaRuntimeClientTargetCommands.SurfaceId,
Node(
"aetheria.clientTarget.root",
"surface",
Array.Empty<(string Key, string Value)>(),
Node(
"aetheria.clientTarget.summary",
"card",
new[] { ("title", "Client Target") },
Metric("aetheria.clientTarget.summary.target", "Target", state.TargetLabel),
Metric("aetheria.clientTarget.summary.transport", "Transport", targetKindLabel),
Metric("aetheria.clientTarget.summary.source", "Target Source", state.TargetSource),
Text("aetheria.clientTarget.summary.note", summaryNote)),
Node(
"aetheria.clientTarget.target",
"card",
new[] { ("title", "Target Fields") },
TextInput(
"aetheria.clientTarget.target.title",
"Title",
state.TargetTitle,
AetheriaRuntimeClientTargetCommands.SetTitle),
TextInput(
"aetheria.clientTarget.target.verseId",
"Verse Id",
state.TargetVerseId,
AetheriaRuntimeClientTargetCommands.SetVerseId),
TextInput(
"aetheria.clientTarget.target.cultMeshAddress",
"CultMesh Address",
state.TargetCultMeshAddress,
AetheriaRuntimeClientTargetCommands.SetCultMeshAddress),
TextInput(
"aetheria.clientTarget.target.stateFilePath",
"State File Path",
state.TargetStateFilePath,
AetheriaRuntimeClientTargetCommands.SetStateFilePath),
ButtonRow(
"aetheria.clientTarget.target.actions",
Button(
"aetheria.clientTarget.target.cycleTransport",
string.Equals(state.TargetKind, AetheriaRuntimeClientTargetKinds.CultMeshVerse, StringComparison.Ordinal)
? "Use Local State File"
: "Use CultMesh Verse",
AetheriaRuntimeClientTargetCommands.CycleTargetKind),
Button(
"aetheria.clientTarget.target.refresh",
"Refresh",
AetheriaRuntimeClientTargetCommands.Refresh))),
Node(
"aetheria.clientTarget.host",
"card",
new[] { ("title", "Daemon Verse Host") },
Metric("aetheria.clientTarget.host.verse", "Verse", state.HostLabel),
Metric("aetheria.clientTarget.host.visibility", "Visibility", visibilityLabel),
Metric("aetheria.clientTarget.host.cultMesh", "CultMesh", state.HostCultMeshAddress),
ButtonRow(
"aetheria.clientTarget.host.actions",
Button(
"aetheria.clientTarget.host.toggleVisibility",
visibilityActionLabel,
AetheriaRuntimeVerseHostCommands.CycleVisibility),
Button(
"aetheria.clientTarget.host.refresh",
"Refresh Host",
AetheriaRuntimeVerseHostCommands.Refresh))),
Node(
"aetheria.clientTarget.host.noteCard",
"card",
new[] { ("title", "Ownership") },
Text(
"aetheria.clientTarget.host.note",
"Client target edits are local. Visibility changes queue provider-owned Eve commands against the selected local Verse state file."))),
Array.Empty<AetheriaRuntimeSurfaceStyleToken>()),
commands: new[]
{
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeClientTargetCommands.Refresh,
"Refresh",
"unity-uitoolkit"),
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeClientTargetCommands.CycleTargetKind,
"Cycle Transport",
"unity-uitoolkit"),
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeClientTargetCommands.SetTitle,
"Set Title",
"unity-uitoolkit"),
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeClientTargetCommands.SetVerseId,
"Set Verse Id",
"unity-uitoolkit"),
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeClientTargetCommands.SetCultMeshAddress,
"Set CultMesh Address",
"unity-uitoolkit"),
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeClientTargetCommands.SetStateFilePath,
"Set State File Path",
"unity-uitoolkit"),
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeVerseHostCommands.CycleVisibility,
"Toggle Visibility",
"cultmesh"),
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeVerseHostCommands.Refresh,
"Refresh Host",
"cultmesh")
});
}
private static AetheriaRuntimeSurfaceComponent Metric(string id, string label, string value)
{
return Node(id, "metric", new[] { ("label", label), ("value", value ?? "") });
}
private static AetheriaRuntimeSurfaceComponent Text(string id, string value)
{
return Node(id, "text", new[] { ("value", value ?? "") });
}
private static AetheriaRuntimeSurfaceComponent Button(string id, string label, string command)
{
return Node(id, "control.button", new[] { ("label", label ?? ""), ("command", command ?? "") });
}
private static AetheriaRuntimeSurfaceComponent TextInput(string id, string label, string value, string command)
{
return Node(
id,
"control.text",
new[] { ("label", label ?? ""), ("value", value ?? ""), ("command", command ?? "") });
}
private static AetheriaRuntimeSurfaceComponent ButtonRow(
string id,
params AetheriaRuntimeSurfaceComponent[] children)
{
return Node(id, "row", Array.Empty<(string Key, string Value)>(), children);
}
private static AetheriaRuntimeSurfaceComponent Node(
string id,
string kind,
IEnumerable<(string Key, string Value)> props,
params AetheriaRuntimeSurfaceComponent[] children)
{
return new AetheriaRuntimeSurfaceComponent(
id,
kind,
props.ToDictionary(prop => prop.Key, prop => prop.Value, StringComparer.Ordinal),
children ?? Array.Empty<AetheriaRuntimeSurfaceComponent>());
}
}
}

View file

@ -0,0 +1,15 @@
namespace GameCult.Aetheria.State.Unity
{
public static class AetheriaRuntimeVerseHostCommands
{
public const string SurfaceId = "aetheria.verse_host_settings";
public const string Refresh = "aetheria.verse_host_settings.refresh";
public const string CycleVisibility = "aetheria.verse_host_settings.visibility.cycle";
public static bool IsKnown(string command)
{
return command == Refresh ||
command == CycleVisibility;
}
}
}