diff --git a/Aetheria.State.ApplyPending/Program.cs b/Aetheria.State.ApplyPending/Program.cs
index 30d80f62d..ae8a28177 100644
--- a/Aetheria.State.ApplyPending/Program.cs
+++ b/Aetheria.State.ApplyPending/Program.cs
@@ -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.");
diff --git a/Aetheria.State.Unity/Aetheria.State.Unity.csproj b/Aetheria.State.Unity/Aetheria.State.Unity.csproj
index efe2a08e4..ff839461a 100644
--- a/Aetheria.State.Unity/Aetheria.State.Unity.csproj
+++ b/Aetheria.State.Unity/Aetheria.State.Unity.csproj
@@ -11,6 +11,8 @@
+
+
diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs
index f9bdfcdf0..a60b81987 100644
--- a/Aetheria.State.Verify/Program.cs
+++ b/Aetheria.State.Verify/Program.cs
@@ -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");
diff --git a/Aetheria.State/Aetheria.State.csproj b/Aetheria.State/Aetheria.State.csproj
index be368c6f4..ac1c3d428 100644
--- a/Aetheria.State/Aetheria.State.csproj
+++ b/Aetheria.State/Aetheria.State.csproj
@@ -11,6 +11,7 @@
+
diff --git a/Aetheria.State/AetheriaEveCommandBridge.cs b/Aetheria.State/AetheriaEveCommandBridge.cs
index 7dc3b2fee..1084dc748 100644
--- a/Aetheria.State/AetheriaEveCommandBridge.cs
+++ b/Aetheria.State/AetheriaEveCommandBridge.cs
@@ -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)
diff --git a/Aetheria.State/AetheriaOperationsSurfaceProjector.cs b/Aetheria.State/AetheriaOperationsSurfaceProjector.cs
index c9635f122..52ba5b738 100644
--- a/Aetheria.State/AetheriaOperationsSurfaceProjector.cs
+++ b/Aetheria.State/AetheriaOperationsSurfaceProjector.cs
@@ -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",
diff --git a/Aetheria.State/Documents/AetheriaEveCommandDrainStatus.cs b/Aetheria.State/Documents/AetheriaEveCommandDrainStatus.cs
index 4e1a2345e..d8ce33e67 100644
--- a/Aetheria.State/Documents/AetheriaEveCommandDrainStatus.cs
+++ b/Aetheria.State/Documents/AetheriaEveCommandDrainStatus.cs
@@ -57,4 +57,7 @@ public sealed class AetheriaEveCommandDrainStatus
[Key(15)]
public int AppliedPlayerSettingsCommands { get; set; }
+
+ [Key(16)]
+ public int AppliedVerseHostCommands { get; set; }
}
diff --git a/Aetheria.State/README.md b/Aetheria.State/README.md
index 6cefedd2e..529164cfa 100644
--- a/Aetheria.State/README.md
+++ b/Aetheria.State/README.md
@@ -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`
diff --git a/Assets/Scripts/UI/MainMenu.cs b/Assets/Scripts/UI/MainMenu.cs
index cfb07175c..fbb14da60 100644
--- a/Assets/Scripts/UI/MainMenu.cs
+++ b/Assets/Scripts/UI/MainMenu.cs
@@ -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 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 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 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(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 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
diff --git a/Economy.Server/Program.cs b/Economy.Server/Program.cs
index d1c081102..6e0d38693 100644
--- a/Economy.Server/Program.cs
+++ b/Economy.Server/Program.cs
@@ -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)
{
diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeClientTargetCommands.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeClientTargetCommands.cs
new file mode 100644
index 000000000..d48929e03
--- /dev/null
+++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeClientTargetCommands.cs
@@ -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;
+ }
+ }
+}
diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeClientTargetStore.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeClientTargetStore.cs
index 047b73a9e..58ae8b8e9 100644
--- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeClientTargetStore.cs
+++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeClientTargetStore.cs
@@ -83,6 +83,19 @@ namespace GameCult.Aetheria.State.Unity
MessagePackSerializer.Serialize(document));
}
+ public static AetheriaRuntimeClientTargetDocument Update(
+ string clientTargetPath,
+ string defaultStateFilePath,
+ Action 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(
diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeClientTargetSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeClientTargetSurfaceBuilder.cs
new file mode 100644
index 000000000..df3994eca
--- /dev/null
+++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeClientTargetSurfaceBuilder.cs
@@ -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()),
+ 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());
+ }
+ }
+}
diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseHostCommands.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseHostCommands.cs
new file mode 100644
index 000000000..023a1b40b
--- /dev/null
+++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseHostCommands.cs
@@ -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;
+ }
+ }
+}