Sync daemon authority migration

This commit is contained in:
Meta Vens 2026-06-21 09:37:54 +01:00
commit 39a0dc8737
295 changed files with 25121 additions and 7360 deletions

View file

@ -8,15 +8,49 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateCommitDocument.cs" Link="AetheriaRuntimeStateCommitDocument.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateBoundary.cs" Link="AetheriaRuntimeStateBoundary.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeSnapshotDocuments.cs" Link="AetheriaRuntimeSnapshotDocuments.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeObservedDaemonState.cs" Link="AetheriaRuntimeObservedDaemonState.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonDocuments.cs" Link="AetheriaRuntimeDaemonDocuments.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonSoaDocuments.cs" Link="AetheriaRuntimeDaemonSoaDocuments.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonSoaViewIndex.cs" Link="AetheriaRuntimeDaemonSoaViewIndex.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonSoaViewStore.cs" Link="AetheriaRuntimeDaemonSoaViewStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonOperationClient.cs" Link="AetheriaRuntimeDaemonOperationClient.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCommandPort.cs" Link="AetheriaRuntimeCommandPort.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonOperations.cs" Link="AetheriaRuntimeDaemonOperations.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeClientTargetCommands.cs" Link="AetheriaRuntimeClientTargetCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonIntentState.cs" Link="AetheriaRuntimeDaemonIntentState.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonFrameStore.cs" Link="AetheriaRuntimeDaemonFrameStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonPublicationStore.cs" Link="AetheriaRuntimeDaemonPublicationStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonTickRunner.cs" Link="AetheriaRuntimeDaemonTickRunner.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeLoadoutSnapshotProjector.cs" Link="AetheriaRuntimeLoadoutSnapshotProjector.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\AetheriaRuntimeEveCommandClient.cs" Link="AetheriaRuntimeEveCommandClient.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeEveSurfaceAdapter.cs" Link="AetheriaRuntimeEveSurfaceAdapter.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCultCacheDocumentStore.cs" Link="AetheriaRuntimeCultCacheDocumentStore.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeVerseHostCommands.cs" Link="AetheriaRuntimeVerseHostCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCatalogCommands.cs" Link="AetheriaRuntimeCatalogCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeOperationsCommands.cs" Link="AetheriaRuntimeOperationsCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCargoItemDetailsSurfaceBuilder.cs" Link="AetheriaRuntimeCargoItemDetailsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.cs" Link="AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeShipSettingsSurfaceBuilder.cs" Link="AetheriaRuntimeShipSettingsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeZoneDetailsSurfaceBuilder.cs" Link="AetheriaRuntimeZoneDetailsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeTradeItemDetailsSurfaceBuilder.cs" Link="AetheriaRuntimeTradeItemDetailsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeInventoryDropdownSurfaceBuilder.cs" Link="AetheriaRuntimeInventoryDropdownSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeTradeInteractionSurfaceBuilder.cs" Link="AetheriaRuntimeTradeInteractionSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.cs" Link="AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeMainMenuSurfaceBuilder.cs" Link="AetheriaRuntimeMainMenuSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeMenuTabsSurfaceBuilder.cs" Link="AetheriaRuntimeMenuTabsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimePlayerSettingsSurfaceBuilder.cs" Link="AetheriaRuntimePlayerSettingsSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonGameSurfaceBuilder.cs" Link="AetheriaRuntimeDaemonGameSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonEditorSurfaceBuilder.cs" Link="AetheriaRuntimeDaemonEditorSurfaceBuilder.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimePlayerSettingsCommands.cs" Link="AetheriaRuntimePlayerSettingsCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeInputSettingsCommands.cs" Link="AetheriaRuntimeInputSettingsCommands.cs" />
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeLoadoutTemplateCommands.cs" Link="AetheriaRuntimeLoadoutTemplateCommands.cs" />
<ProjectReference Include="..\..\CultLib\src\GameCult.Caching\GameCult.Caching.csproj" />
<ProjectReference Include="..\..\CultLib\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
<ProjectReference Include="..\..\CultLib\src\GameCult.Mesh\GameCult.Mesh.csproj" />
<ProjectReference Include="..\..\CultLib\src\GameCult.Networking\GameCult.Networking.csproj" />
<ProjectReference Include="..\GameCult.Eve.Surface.csproj" />
</ItemGroup>
</Project>

View file

@ -1,13 +1,14 @@
using System;
using System.Linq;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
namespace Aetheria.State;
public static class AetheriaCatalogSurfaceProjector
{
public const string SurfaceKey = "eve:surface:aetheria.catalog.operator";
public const string SurfaceId = "aetheria.catalog.operator";
public const string SurfaceId = AetheriaRuntimeCatalogCommands.SurfaceId;
public static EveSurfaceState Build(AetheriaCatalogSnapshot catalog, string updatedAtUtc, long version = 1)
{
@ -68,7 +69,7 @@ public static class AetheriaCatalogSurfaceProjector
[
new EveCommandTemplate
{
Command = "aetheria.catalog.refresh",
Command = AetheriaRuntimeCatalogCommands.Refresh,
Label = "Refresh",
Transport = "cultmesh"
}

View file

@ -1,5 +1,6 @@
using Aetheria.State.Documents;
using GameCult.Caching;
using GameCult.Aetheria.State.Unity;
using GameCult.Networking;
namespace Aetheria.State;
@ -38,8 +39,14 @@ public static class AetheriaDocumentRegistry
CultNetDocumentBinding.ForDocument<AetheriaZoneState>(registry),
CultNetDocumentBinding.ForDocument<AetheriaEntitySnapshot>(registry),
CultNetDocumentBinding.ForDocument<AetheriaVerseHostSettings>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeCommitDrainStatus>(registry),
CultNetDocumentBinding.ForDocument<AetheriaEveCommandDrainStatus>(registry),
CultNetDocumentBinding.ForDocument<AetheriaEveCommandAcceptanceStatus>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonProviderAdvertisementDocument>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonHealthDocument>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonCommandBoundaryDocument>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonFrameDocument>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonSoaViewDocument>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonCommandDocument>(registry),
CultNetDocumentBinding.ForDocument<AetheriaRuntimeEveCommandDocument>(registry),
CultNetDocumentBinding.ForDocument<EveSurfaceState>(registry),
CultNetDocumentBinding.ForDocument<EveProviderAdvertisementState>(registry)
});
@ -61,8 +68,14 @@ public static class AetheriaDocumentRegistry
typeof(AetheriaZoneState),
typeof(AetheriaEntitySnapshot),
typeof(AetheriaVerseHostSettings),
typeof(AetheriaRuntimeCommitDrainStatus),
typeof(AetheriaEveCommandDrainStatus),
typeof(AetheriaEveCommandAcceptanceStatus),
typeof(AetheriaRuntimeDaemonProviderAdvertisementDocument),
typeof(AetheriaRuntimeDaemonHealthDocument),
typeof(AetheriaRuntimeDaemonCommandBoundaryDocument),
typeof(AetheriaRuntimeDaemonFrameDocument),
typeof(AetheriaRuntimeDaemonSoaViewDocument),
typeof(AetheriaRuntimeDaemonCommandDocument),
typeof(AetheriaRuntimeEveCommandDocument),
typeof(EveSurfaceState),
typeof(EveProviderAdvertisementState)
];

View file

@ -3,15 +3,18 @@ using GameCult.Aetheria.State.Unity;
namespace Aetheria.State;
public sealed class AetheriaEveCommandApplyReport
public sealed class AetheriaEveCommandAcceptanceReport
{
public int AppliedCatalogRefreshes { get; set; }
public int AppliedOperationsRefreshes { get; set; }
public int AppliedPlayerSettingsCommands { get; set; }
public int AppliedVerseHostCommands { get; set; }
public int AcceptedCatalogRefreshes { get; set; }
public int AcceptedOperationsRefreshes { get; set; }
public int AcceptedPlayerSettingsCommands { get; set; }
public int AcceptedInputSettingsCommands { get; set; }
public int AcceptedLoadoutTemplateCommands { get; set; }
public int AcceptedVerseHostCommands { get; set; }
public int RejectedCommands { get; set; }
public string[] AcceptedPaths { get; set; } = [];
public string[] RejectedPaths { get; set; } = [];
public string[] AcceptedCommandIds { get; set; } = [];
public string[] RejectedCommandIds { get; set; } = [];
public string[] AccountedCommandIds { get; set; } = [];
public string LastRejectedCommand { get; set; } = "";
public string LastRejectedReason { get; set; } = "";
}
@ -20,61 +23,45 @@ public static class AetheriaEveCommandBridge
{
public const string CommandSchema = "gamecult.eve.command.v1";
public static string GetPendingDirectory(string stateFilePath)
{
if (string.IsNullOrWhiteSpace(stateFilePath))
throw new ArgumentException("State file path must be non-empty.", nameof(stateFilePath));
return stateFilePath + ".eve.pending";
}
public static async Task<AetheriaEveCommandApplyReport> ApplyPendingAsync(
public static async Task<AetheriaEveCommandAcceptanceReport> AcceptObservedAsync(
AetheriaStateNode node,
bool deleteAccounted = true)
IEnumerable<string>? accountedCommandIds = null)
{
if (node == null) throw new ArgumentNullException(nameof(node));
var pendingDirectory = GetPendingDirectory(node.StatePath);
var report = new AetheriaEveCommandApplyReport();
var report = new AetheriaEveCommandAcceptanceReport();
var accepted = new List<string>();
var rejected = new List<string>();
if (!Directory.Exists(pendingDirectory))
return report;
foreach (var path in Directory.EnumerateFiles(pendingDirectory, "*.cc").OrderBy(path => path, StringComparer.Ordinal))
var accounted = new HashSet<string>(accountedCommandIds ?? Array.Empty<string>(), StringComparer.Ordinal);
foreach (var command in node.ReadObservedEveCommands()
.Where(command => !string.IsNullOrWhiteSpace(command.CommandId))
.Where(command => !accounted.Contains(command.CommandId)))
{
var command = ReadCommand(path);
var rejection = Validate(command);
if (!string.IsNullOrWhiteSpace(rejection))
{
RecordRejection(report, command, rejection, path, rejected, deleteAccounted);
RecordRejection(report, command, rejection, rejected);
continue;
}
switch (command.Command)
{
case "aetheria.catalog.refresh":
case AetheriaRuntimeCatalogCommands.Refresh:
await node.PutCatalogSurfaceAsync(
AetheriaCatalogSurfaceProjector.Build(node.ReadCatalogSnapshot(), command.IssuedAtUtc)).ConfigureAwait(false);
report.AppliedCatalogRefreshes++;
report.AcceptedCatalogRefreshes++;
break;
case "aetheria.operations.refresh":
var commitStatus = await node.GetRuntimeCommitDrainStatusAsync().ConfigureAwait(false) ??
EmptyCommitDrainStatus(node.StatePath, command.IssuedAtUtc);
var eveStatus = await node.GetEveCommandDrainStatusAsync().ConfigureAwait(false) ??
EmptyEveCommandDrainStatus(node.StatePath, command.IssuedAtUtc);
case AetheriaRuntimeOperationsCommands.Refresh:
var eveStatus = await node.GetEveCommandAcceptanceStatusAsync().ConfigureAwait(false) ??
EmptyEveCommandAcceptanceStatus(node.StatePath, command.IssuedAtUtc);
var verseHostSettings = await node.GetVerseHostSettingsAsync().ConfigureAwait(false);
var runtimeSession = string.IsNullOrWhiteSpace(commitStatus.RuntimeId)
? null
: await node.GetRuntimeSessionAsync(commitStatus.RuntimeId).ConfigureAwait(false);
var runtimeSession = await node.GetRuntimeSessionAsync(eveStatus.RuntimeId).ConfigureAwait(false);
await node.PutOperationsSurfaceAsync(
AetheriaOperationsSurfaceProjector.Build(
commitStatus,
eveStatus,
verseHostSettings,
runtimeSession)).ConfigureAwait(false);
report.AppliedOperationsRefreshes++;
report.AcceptedOperationsRefreshes++;
break;
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.Refresh:
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.SetPlayerName:
@ -83,32 +70,41 @@ public static class AetheriaEveCommandBridge
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.IncrementSignificantDigits:
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.CycleNebulaQuality:
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.ToggleShowAsteroidsInMinimap:
await ApplyPlayerSettingsCommandAsync(node, command).ConfigureAwait(false);
report.AppliedPlayerSettingsCommands++;
await ExecutePlayerSettingsCommandAsync(node, command).ConfigureAwait(false);
report.AcceptedPlayerSettingsCommands++;
break;
case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.Refresh:
case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SetBindingOverride:
case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SetActionBarEnabled:
await ExecuteInputSettingsCommandAsync(node, command).ConfigureAwait(false);
report.AcceptedInputSettingsCommands++;
break;
case GameCult.Aetheria.State.Unity.AetheriaRuntimeLoadoutTemplateCommands.Save:
await ExecuteLoadoutTemplateCommandAsync(node, command).ConfigureAwait(false);
report.AcceptedLoadoutTemplateCommands++;
break;
case GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.Refresh:
case GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.CycleVisibility:
await ApplyVerseHostCommandAsync(node, command).ConfigureAwait(false);
report.AppliedVerseHostCommands++;
await ExecuteVerseHostCommandAsync(node, command).ConfigureAwait(false);
report.AcceptedVerseHostCommands++;
break;
}
accepted.Add(path);
if (deleteAccounted)
File.Delete(path);
accepted.Add(command.CommandId ?? "");
}
await node.FlushAsync().ConfigureAwait(false);
report.AcceptedPaths = accepted.ToArray();
report.RejectedPaths = rejected.ToArray();
report.AcceptedCommandIds = accepted.ToArray();
report.RejectedCommandIds = rejected.ToArray();
report.AccountedCommandIds = accounted
.Concat(report.AcceptedCommandIds)
.Concat(report.RejectedCommandIds)
.Where(commandId => !string.IsNullOrWhiteSpace(commandId))
.Distinct(StringComparer.Ordinal)
.ToArray();
return report;
}
private static AetheriaRuntimeEveCommandDocument ReadCommand(string path)
{
return AetheriaRuntimePendingCultCacheStore.ReadEveCommand(path);
}
private static string Validate(AetheriaRuntimeEveCommandDocument command)
{
if (!string.Equals(command.Schema, CommandSchema, StringComparison.Ordinal))
@ -123,17 +119,21 @@ public static class AetheriaEveCommandBridge
private static bool KnownCommand(string surfaceId, string command)
{
return (string.Equals(surfaceId, AetheriaCatalogSurfaceProjector.SurfaceId, StringComparison.Ordinal) &&
string.Equals(command, "aetheria.catalog.refresh", StringComparison.Ordinal)) ||
(string.Equals(surfaceId, AetheriaOperationsSurfaceProjector.SurfaceId, StringComparison.Ordinal) &&
string.Equals(command, "aetheria.operations.refresh", StringComparison.Ordinal)) ||
return (string.Equals(surfaceId, AetheriaRuntimeCatalogCommands.SurfaceId, StringComparison.Ordinal) &&
AetheriaRuntimeCatalogCommands.IsKnown(command)) ||
(string.Equals(surfaceId, AetheriaRuntimeOperationsCommands.SurfaceId, StringComparison.Ordinal) &&
AetheriaRuntimeOperationsCommands.IsKnown(command)) ||
(string.Equals(surfaceId, AetheriaPlayerSettingsSurfaceProjector.SurfaceId, StringComparison.Ordinal) &&
GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.IsKnown(command)) ||
(string.Equals(surfaceId, GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SurfaceId, StringComparison.Ordinal) &&
GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.IsKnown(command)) ||
(string.Equals(surfaceId, GameCult.Aetheria.State.Unity.AetheriaRuntimeLoadoutTemplateCommands.SurfaceId, StringComparison.Ordinal) &&
GameCult.Aetheria.State.Unity.AetheriaRuntimeLoadoutTemplateCommands.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(
private static async Task ExecutePlayerSettingsCommandAsync(
AetheriaStateNode node,
AetheriaRuntimeEveCommandDocument command)
{
@ -145,9 +145,7 @@ public static class AetheriaEveCommandBridge
switch (command.Command)
{
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.SetPlayerName:
settings.PlayerName = command.Payload.TryGetValue("value", out var playerName)
? playerName ?? ""
: "";
settings.PlayerName = command.PlayerSettings.PlayerName ?? "";
persistSettings = true;
break;
case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.CycleTemperatureUnit:
@ -193,7 +191,66 @@ public static class AetheriaEveCommandBridge
.ConfigureAwait(false);
}
private static async Task ApplyVerseHostCommandAsync(
private static async Task ExecuteInputSettingsCommandAsync(
AetheriaStateNode node,
AetheriaRuntimeEveCommandDocument command)
{
var settings = await node.GetPlayerSettingsAsync().ConfigureAwait(false) ?? new AetheriaPlayerSettings();
settings.Input ??= new AetheriaPlayerInputSettings();
var persistSettings = false;
switch (command.Command)
{
case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SetBindingOverride:
var actionName = command.InputSettings.ActionName ?? "";
var inputSystemPath = command.InputSettings.InputSystemPath ?? "";
var bindingIndex = command.InputSettings.BindingIndex;
if (!string.IsNullOrWhiteSpace(actionName) && bindingIndex >= 0)
{
settings.Input.BindingOverrides = settings.Input.BindingOverrides
.Where(binding => !string.Equals(binding.ActionName, actionName, StringComparison.Ordinal) ||
binding.BindingIndex != bindingIndex)
.Concat(new[]
{
new AetheriaInputBindingOverride
{
ActionName = actionName,
BindingIndex = bindingIndex,
BindingPath = inputSystemPath
}
})
.OrderBy(binding => binding.ActionName, StringComparer.Ordinal)
.ThenBy(binding => binding.BindingIndex)
.ToArray();
persistSettings = true;
}
break;
case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SetActionBarEnabled:
var inputPath = command.InputSettings.InputSystemPath ?? "";
var enabled = command.InputSettings.Enabled;
if (!string.IsNullOrWhiteSpace(inputPath))
{
var inputs = settings.Input.ActionBarInputs
.Where(path => !string.Equals(path, inputPath, StringComparison.Ordinal))
.ToList();
if (enabled)
inputs.Add(inputPath);
settings.Input.ActionBarInputs = inputs
.OrderBy(path => path, StringComparer.Ordinal)
.ToArray();
persistSettings = true;
}
break;
}
if (persistSettings)
{
settings.LastUpdatedAtUtc = command.IssuedAtUtc;
await node.PutPlayerSettingsAsync(settings).ConfigureAwait(false);
}
}
private static async Task ExecuteVerseHostCommandAsync(
AetheriaStateNode node,
AetheriaRuntimeEveCommandDocument command)
{
@ -215,17 +272,12 @@ public static class AetheriaEveCommandBridge
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);
var eveStatus = await node.GetEveCommandAcceptanceStatusAsync().ConfigureAwait(false) ??
EmptyEveCommandAcceptanceStatus(node.StatePath, command.IssuedAtUtc);
var runtimeSession = await node.GetRuntimeSessionAsync(eveStatus.RuntimeId).ConfigureAwait(false);
await node.PutOperationsSurfaceAsync(
AetheriaOperationsSurfaceProjector.Build(
commitStatus,
eveStatus,
normalized,
runtimeSession)).ConfigureAwait(false);
@ -233,6 +285,18 @@ public static class AetheriaEveCommandBridge
AetheriaProviderAdvertisementProjector.Build(normalized, node.StatePath, command.IssuedAtUtc)).ConfigureAwait(false);
}
private static async Task ExecuteLoadoutTemplateCommandAsync(
AetheriaStateNode node,
AetheriaRuntimeEveCommandDocument command)
{
var commit = command.LoadoutTemplate ?? throw new InvalidOperationException(
$"Loadout template command '{command.CommandId}' is missing its typed payload.");
var loadout = AetheriaRuntimeStateMapper.ToLoadoutTemplate(commit, command.IssuedAtUtc);
await node.PutLoadoutTemplateAsync(
AetheriaRuntimeStateMapper.LoadoutKey(loadout.Name),
loadout).ConfigureAwait(false);
}
private static string Cycle(string current, params string[] values)
{
if (values.Length == 0)
@ -246,37 +310,22 @@ public static class AetheriaEveCommandBridge
}
private static void RecordRejection(
AetheriaEveCommandApplyReport report,
AetheriaEveCommandAcceptanceReport report,
AetheriaRuntimeEveCommandDocument command,
string reason,
string path,
List<string> rejected,
bool deleteAccounted)
List<string> rejected)
{
report.RejectedCommands++;
report.LastRejectedCommand = string.IsNullOrWhiteSpace(command.Command)
? command.CommandId
: command.Command;
report.LastRejectedReason = reason;
rejected.Add(path);
if (deleteAccounted)
File.Delete(path);
rejected.Add(command.CommandId ?? "");
}
private static AetheriaRuntimeCommitDrainStatus EmptyCommitDrainStatus(string statePath, string now)
private static AetheriaEveCommandAcceptanceStatus EmptyEveCommandAcceptanceStatus(string statePath, string now)
{
return new AetheriaRuntimeCommitDrainStatus
{
RuntimeId = "aetheria-state",
StatePath = statePath,
LastPollAtUtc = now,
Status = "idle"
};
}
private static AetheriaEveCommandDrainStatus EmptyEveCommandDrainStatus(string statePath, string now)
{
return new AetheriaEveCommandDrainStatus
return new AetheriaEveCommandAcceptanceStatus
{
RuntimeId = "aetheria-state",
StatePath = statePath,

View file

@ -1,22 +1,21 @@
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
namespace Aetheria.State;
public static class AetheriaOperationsSurfaceProjector
{
public const string SurfaceKey = "eve:surface:aetheria.operations";
public const string SurfaceId = "aetheria.operations";
public const string SurfaceId = AetheriaRuntimeOperationsCommands.SurfaceId;
public static EveSurfaceState Build(
AetheriaRuntimeCommitDrainStatus drainStatus,
AetheriaEveCommandDrainStatus? eveCommandStatus = null,
AetheriaEveCommandAcceptanceStatus? eveCommandStatus = null,
AetheriaVerseHostSettings? verseHostSettings = null,
AetheriaRuntimeSession? runtimeSession = null,
long version = 1)
{
var normalizedVerseHost = AetheriaVerseHostSettingsNormalizer.Normalize(verseHostSettings);
var updatedAtUtc = LatestTimestamp(
drainStatus.LastPollAtUtc,
eveCommandStatus?.LastPollAtUtc,
normalizedVerseHost.LastUpdatedAtUtc,
runtimeSession?.LastSeenAtUtc);
@ -35,37 +34,22 @@ public static class AetheriaOperationsSurfaceProjector
"surface",
[],
Node(
"aetheria.operations.commitDrain",
"aetheria.operations.eveCommandAcceptance",
"card",
[("title", "Runtime Commit Drain")],
Metric("commitDrain.status", "Status", drainStatus.Status),
Metric("commitDrain.pending", "Pending Before Apply", drainStatus.PendingBeforeApply.ToString()),
Metric("commitDrain.applied", "Commands Applied", drainStatus.CommandsApplied.ToString()),
Metric("commitDrain.settings", "Settings", drainStatus.AppliedPlayerSettings.ToString()),
Metric("commitDrain.loadouts", "Loadouts", drainStatus.AppliedLoadoutTemplates.ToString()),
Metric("commitDrain.runs", "Run Checkpoints", drainStatus.AppliedRunCheckpoints.ToString()),
Metric("commitDrain.failures", "Consecutive Failures", drainStatus.ConsecutiveFailures.ToString()),
[("title", "Eve Request Acceptance")],
Metric("eveCommandAcceptance.status", "Status", eveCommandStatus?.Status ?? "missing"),
Metric("eveCommandAcceptance.observed", "Observed Before Accept", (eveCommandStatus?.ObservedBeforeAccept ?? 0).ToString()),
Metric("eveCommandAcceptance.accepted", "Commands Accepted", (eveCommandStatus?.CommandsAccepted ?? 0).ToString()),
Metric("eveCommandAcceptance.rejected", "Commands Rejected", (eveCommandStatus?.CommandsRejected ?? 0).ToString()),
Metric("eveCommandAcceptance.catalogRefreshes", "Catalog Refreshes", (eveCommandStatus?.AppliedCatalogRefreshes ?? 0).ToString()),
Metric("eveCommandAcceptance.operationsRefreshes", "Operations Refreshes", (eveCommandStatus?.AppliedOperationsRefreshes ?? 0).ToString()),
Metric("eveCommandAcceptance.playerSettings", "Player Settings Commands", (eveCommandStatus?.AppliedPlayerSettingsCommands ?? 0).ToString()),
Metric("eveCommandAcceptance.inputSettings", "Input Settings Commands", (eveCommandStatus?.AppliedInputSettingsCommands ?? 0).ToString()),
Metric("eveCommandAcceptance.loadoutTemplates", "Loadout Template Commands", (eveCommandStatus?.AppliedLoadoutTemplateCommands ?? 0).ToString()),
Metric("eveCommandAcceptance.verseHost", "Verse Host Commands", (eveCommandStatus?.AppliedVerseHostCommands ?? 0).ToString()),
Metric("eveCommandAcceptance.failures", "Consecutive Failures", (eveCommandStatus?.ConsecutiveFailures ?? 0).ToString()),
Row(
"commitDrain.last",
("runtime", drainStatus.RuntimeId),
("lastPoll", drainStatus.LastPollAtUtc),
("lastApplied", drainStatus.LastAppliedAtUtc),
("error", drainStatus.LastError))),
Node(
"aetheria.operations.eveCommandDrain",
"card",
[("title", "Eve Command Drain")],
Metric("eveCommandDrain.status", "Status", eveCommandStatus?.Status ?? "missing"),
Metric("eveCommandDrain.pending", "Pending Before Apply", (eveCommandStatus?.PendingBeforeApply ?? 0).ToString()),
Metric("eveCommandDrain.accepted", "Commands Accepted", (eveCommandStatus?.CommandsAccepted ?? 0).ToString()),
Metric("eveCommandDrain.rejected", "Commands Rejected", (eveCommandStatus?.CommandsRejected ?? 0).ToString()),
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",
"eveCommandAcceptance.last",
("runtime", eveCommandStatus?.RuntimeId ?? ""),
("lastPoll", eveCommandStatus?.LastPollAtUtc ?? ""),
("lastAccepted", eveCommandStatus?.LastAcceptedAtUtc ?? ""),
@ -101,7 +85,7 @@ public static class AetheriaOperationsSurfaceProjector
[
new EveCommandTemplate
{
Command = "aetheria.operations.refresh",
Command = AetheriaRuntimeOperationsCommands.Refresh,
Label = "Refresh",
Transport = "cultmesh"
}

View file

@ -1,11 +1,18 @@
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
namespace Aetheria.State;
public static class AetheriaProviderAdvertisementProjector
{
public const string AdvertisementKey = "eve:provider:aetheria";
public const string DaemonGameSurfaceKey = "eve:surface:aetheria.daemon.game";
public const string DaemonGameTuiSurfaceKey = "eve:surface:aetheria.daemon.game.tui";
public const string DaemonEditorSurfaceKey = "eve:surface:aetheria.daemon.editor";
public const string DaemonEditorTuiSurfaceKey = "eve:surface:aetheria.daemon.editor.tui";
public const string ProviderId = "aetheria";
private const string DaemonCommandBoundaryId = "aetheria.daemon.commands";
private const string DaemonWitnessTransport = "cultcache-witness";
public static EveProviderAdvertisementState Build(
AetheriaVerseHostSettings settings,
@ -44,10 +51,17 @@ public static class AetheriaProviderAdvertisementProjector
"aetheria.entity_snapshot.v1",
"aetheria.verse_host_settings.v1",
"aetheria.runtime_session.v1",
"aetheria.runtime_commit_drain_status.v1",
"aetheria.eve_command_drain_status.v1",
"aetheria.eve_command_acceptance_status.v1",
"gamecult.eve.surface.v1",
"gamecult.eve.command.v1"
"gamecult.eve.command.v1",
AetheriaRuntimeDaemonSchemas.ProviderAdvertisement,
AetheriaRuntimeDaemonSchemas.Frame,
AetheriaRuntimeDaemonSchemas.SoaView,
AetheriaRuntimeDaemonSchemas.Health,
AetheriaRuntimeDaemonSchemas.CommandBoundary,
AetheriaRuntimeDaemonSchemas.GameSurface,
AetheriaRuntimeDaemonSchemas.EditorSurface,
AetheriaRuntimeDaemonSchemas.Command
],
Witnesses =
[
@ -56,6 +70,60 @@ public static class AetheriaProviderAdvertisementProjector
Kind = "cultcache",
Ref = statePath,
Summary = "Aetheria typed CultCache state file"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonProviderPath(statePath),
Summary = "Aetheria daemon-owned provider advertisement"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonFramePath(statePath),
Summary = "Aetheria daemon simulation frame"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonSoaViewPath(statePath),
Summary = "Aetheria daemon SoA view for thin clients"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonHealthPath(statePath),
Summary = "Aetheria daemon health publication"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonCommandBoundaryPath(statePath),
Summary = "Aetheria daemon typed command boundary"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonGameSurfacePath(statePath),
Summary = "Aetheria daemon game Eve GUI surface"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonGameTuiSurfacePath(statePath),
Summary = "Aetheria daemon game Eve TUI surface"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonEditorSurfacePath(statePath),
Summary = "Aetheria daemon editor Eve GUI surface"
},
new EveProviderWitness
{
Kind = DaemonWitnessTransport,
Ref = AetheriaRuntimeStateBoundary.GetDaemonEditorTuiSurfacePath(statePath),
Summary = "Aetheria daemon editor Eve TUI surface"
}
],
Surfaces =
@ -74,48 +142,74 @@ public static class AetheriaProviderAdvertisementProjector
{
SurfaceId = AetheriaPlayerSettingsSurfaceProjector.SurfaceId,
Key = AetheriaPlayerSettingsSurfaceProjector.SurfaceKey
},
new EveProviderSurfaceRef
{
SurfaceId = AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId,
Key = DaemonGameSurfaceKey
},
new EveProviderSurfaceRef
{
SurfaceId = AetheriaRuntimeDaemonGameSurfaceBuilder.TuiSurfaceId,
Key = DaemonGameTuiSurfaceKey
},
new EveProviderSurfaceRef
{
SurfaceId = AetheriaRuntimeDaemonEditorSurfaceBuilder.SurfaceId,
Key = DaemonEditorSurfaceKey
},
new EveProviderSurfaceRef
{
SurfaceId = AetheriaRuntimeDaemonEditorSurfaceBuilder.TuiSurfaceId,
Key = DaemonEditorTuiSurfaceKey
}
],
Commands =
[
new EveProviderCommandRef
{
Command = "aetheria.catalog.refresh",
Command = DaemonCommandBoundaryId,
Transport = DaemonWitnessTransport,
Summary = "Aetheria daemon typed command boundary"
},
new EveProviderCommandRef
{
Command = AetheriaRuntimeCatalogCommands.Refresh,
Summary = "Refresh catalog projection"
},
new EveProviderCommandRef
{
Command = "aetheria.operations.refresh",
Command = AetheriaRuntimeOperationsCommands.Refresh,
Summary = "Refresh operations projection"
},
new EveProviderCommandRef
{
Command = GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.Refresh,
Command = AetheriaRuntimePlayerSettingsCommands.Refresh,
Summary = "Refresh player settings projection"
},
new EveProviderCommandRef
{
Command = GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.CycleTemperatureUnit,
Command = AetheriaRuntimePlayerSettingsCommands.CycleTemperatureUnit,
Summary = "Cycle the typed player temperature unit"
},
new EveProviderCommandRef
{
Command = GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.DecrementSignificantDigits,
Command = AetheriaRuntimePlayerSettingsCommands.DecrementSignificantDigits,
Summary = "Decrease typed player significant digits"
},
new EveProviderCommandRef
{
Command = GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.IncrementSignificantDigits,
Command = AetheriaRuntimePlayerSettingsCommands.IncrementSignificantDigits,
Summary = "Increase typed player significant digits"
},
new EveProviderCommandRef
{
Command = GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.CycleNebulaQuality,
Command = AetheriaRuntimePlayerSettingsCommands.CycleNebulaQuality,
Summary = "Cycle typed player nebula quality"
},
new EveProviderCommandRef
{
Command = GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.ToggleShowAsteroidsInMinimap,
Command = AetheriaRuntimePlayerSettingsCommands.ToggleShowAsteroidsInMinimap,
Summary = "Toggle typed player minimap asteroid visibility"
}
]

View file

@ -1,756 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
using GameCult.Caching;
namespace Aetheria.State;
public sealed class AetheriaRuntimeCommitApplyReport
{
public int AppliedPlayerSettings { get; set; }
public int AppliedLoadoutTemplates { get; set; }
public int AppliedRunCheckpoints { get; set; }
public string[] AppliedPaths { get; set; } = [];
}
public static class AetheriaRuntimeCommitLogApplier
{
public static async Task<AetheriaRuntimeCommitApplyReport> ApplyPendingAsync(
AetheriaStateNode node,
bool deleteApplied = true)
{
if (node == null) throw new ArgumentNullException(nameof(node));
var pendingDirectory = node.StatePath + ".pending";
var report = new AetheriaRuntimeCommitApplyReport();
var applied = new List<string>();
if (!Directory.Exists(pendingDirectory))
return report;
foreach (var path in Directory.EnumerateFiles(pendingDirectory, "*.cc").OrderBy(path => path, StringComparer.Ordinal))
{
var command = ReadCommand(path);
switch (command.Kind)
{
case "player_settings":
var settings = ToPlayerSettings(
command.PlayerSettings ?? throw MissingPayload(command),
command.CreatedAtUtc);
await node.PutPlayerSettingsAsync(settings).ConfigureAwait(false);
await node.PutPlayerSettingsSurfaceAsync(
AetheriaPlayerSettingsSurfaceProjector.Build(settings, command.CreatedAtUtc))
.ConfigureAwait(false);
report.AppliedPlayerSettings++;
break;
case "loadout_template":
var loadout = ToLoadoutTemplate(
command.LoadoutTemplate ?? throw MissingPayload(command),
command.CreatedAtUtc);
await node.PutLoadoutTemplateAsync(LoadoutKey(loadout.Name), loadout).ConfigureAwait(false);
report.AppliedLoadoutTemplates++;
break;
case "run_checkpoint":
await ApplyRunCheckpointAsync(
node,
command.RunCheckpoint ?? throw MissingPayload(command),
command.CreatedAtUtc)
.ConfigureAwait(false);
report.AppliedRunCheckpoints++;
break;
default:
throw new InvalidDataException($"Unknown Aetheria runtime commit kind '{command.Kind}' in {path}.");
}
applied.Add(path);
if (deleteApplied)
File.Delete(path);
}
await node.FlushAsync().ConfigureAwait(false);
report.AppliedPaths = applied.ToArray();
return report;
}
private static AetheriaRuntimeStateCommitDocument ReadCommand(string path)
{
var command = AetheriaRuntimePendingCultCacheStore.ReadStateCommit(path);
if (!string.Equals(command.Schema, AetheriaRuntimeStateCommitDocument.SchemaId, StringComparison.Ordinal))
throw new InvalidDataException($"Unexpected Aetheria runtime commit schema '{command.Schema}' in {path}.");
return command;
}
private static AetheriaPlayerSettings ToPlayerSettings(
AetheriaRuntimePlayerSettingsCommit settings,
string updatedAtUtc)
{
return new AetheriaPlayerSettings
{
PlayerName = settings.PlayerName ?? "",
TutorialPassed = settings.TutorialPassed,
StoryFileHashes = (settings.StoryFileHashes ?? Array.Empty<AetheriaRuntimeStoryFileHashCommit>())
.Select(hash => new AetheriaStoryFileHash
{
StoryPath = hash.StoryPath ?? "",
Hash = hash.Hash ?? ""
})
.ToArray(),
Gameplay = new AetheriaPlayerGameplaySettings
{
TemperatureUnit = settings.TemperatureUnit ?? "",
SignificantDigits = settings.SignificantDigits
},
Graphics = new AetheriaPlayerGraphicsSettings
{
NebulaQuality = settings.NebulaQuality ?? "",
ShowAsteroidsInMinimap = settings.ShowAsteroidsInMinimap
},
Input = new AetheriaPlayerInputSettings
{
BindingOverrides = (settings.BindingOverrides ?? Array.Empty<AetheriaRuntimeInputBindingCommit>())
.Select(binding => new AetheriaInputBindingOverride
{
ActionName = binding.ActionName ?? "",
BindingIndex = binding.BindingIndex,
BindingPath = binding.BindingPath ?? ""
})
.ToArray(),
ActionBarInputs = (settings.ActionBarInputs ?? Array.Empty<string>())
.Select(input => input ?? "")
.ToArray()
},
LastUpdatedAtUtc = updatedAtUtc
};
}
private static AetheriaLoadoutTemplate ToLoadoutTemplate(
AetheriaRuntimeLoadoutTemplateCommit loadout,
string updatedAtUtc)
{
return new AetheriaLoadoutTemplate
{
Name = loadout.Name ?? "",
OwnerPlayerKey = loadout.OwnerPlayerKey ?? "",
RootEntity = ToEntityLoadout(loadout.RootEntity),
CreatedAtUtc = updatedAtUtc,
UpdatedAtUtc = updatedAtUtc
};
}
private static AetheriaEntityLoadout ToEntityLoadout(AetheriaRuntimeEntityLoadoutCommit? entity)
{
entity ??= new AetheriaRuntimeEntityLoadoutCommit();
return new AetheriaEntityLoadout
{
Name = entity.Name ?? "",
Kind = entity.Kind ?? "",
FactionKey = entity.FactionKey ?? "",
Hull = ToLoadoutItem(entity.Hull),
Equipment = ToItemSlots(entity.Equipment),
CargoBays = ToItemSlots(entity.CargoBays),
DockingBays = ToItemSlots(entity.DockingBays),
CargoContents = ToCargoBays(entity.CargoContents),
DockingBayContents = ToCargoBays(entity.DockingBayContents),
DockingBayAssignments = ToIntArray(entity.DockingBayAssignments),
WeaponGroups = ToIntArrayArray(entity.WeaponGroups),
Children = (entity.Children ?? Array.Empty<AetheriaRuntimeEntityLoadoutCommit>())
.Select(ToEntityLoadout)
.ToArray()
};
}
private static AetheriaLoadoutItem ToLoadoutItem(AetheriaRuntimeLoadoutItemCommit? item)
{
item ??= new AetheriaRuntimeLoadoutItemCommit();
return new AetheriaLoadoutItem
{
ItemKey = item.ItemKey ?? "",
Quality = item.Quality,
Durability = item.Durability,
Quantity = item.Quantity,
Enabled = item.Enabled,
OverrideShutdown = item.OverrideShutdown
};
}
private static AetheriaLoadoutItemSlot[] ToItemSlots(
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit>? slots)
{
return (slots ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
.Select(slot => new AetheriaLoadoutItemSlot
{
Position = new AetheriaGridCoord
{
X = slot.X,
Y = slot.Y
},
Item = ToLoadoutItem(slot.Item)
})
.ToArray();
}
private static AetheriaEntityItemSlot[] ToEntityItemSlots(
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit>? slots)
{
return (slots ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
.Select(slot =>
{
var item = ToLoadoutItem(slot.Item);
return new AetheriaEntityItemSlot
{
Position = new AetheriaGridCoord
{
X = slot.X,
Y = slot.Y
},
ItemKey = item.ItemKey,
Quality = item.Quality,
Durability = item.Durability,
Quantity = item.Quantity,
Enabled = item.Enabled,
OverrideShutdown = item.OverrideShutdown
};
})
.ToArray();
}
private static AetheriaCargoBayLoadout[] ToCargoBays(
IReadOnlyList<AetheriaRuntimeCargoBayLoadoutCommit>? cargoBays)
{
return (cargoBays ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>())
.Select(bay => new AetheriaCargoBayLoadout
{
Items = ToItemSlots(bay.Items)
})
.ToArray();
}
private static async Task ApplyRunCheckpointAsync(
AetheriaStateNode node,
AetheriaRuntimeRunCheckpointCommit checkpoint,
string updatedAtUtc)
{
var run = new AetheriaRunState
{
RunId = checkpoint.RunId ?? "",
IsTutorial = checkpoint.IsTutorial,
EntranceZoneIndex = checkpoint.EntranceZoneIndex,
ExitZoneIndex = checkpoint.ExitZoneIndex,
CurrentZoneIndex = checkpoint.CurrentZoneIndex,
DiscoveredZoneIndices = ToIntArray(checkpoint.DiscoveredZoneIndices),
ActionBarBindings = ToActionBarBindings(checkpoint.ActionBarBindings),
FactionRelationships = ToFactionRelationships(checkpoint.FactionRelationships),
UpdatedAtUtc = updatedAtUtc,
GenerationSeed = checkpoint.GenerationSeed
};
var zoneKeys = new List<string>();
foreach (var zone in checkpoint.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
{
var zoneKey = ZoneKey(run.RunId, zone.ZoneIndex);
var entityKeys = new List<string>();
foreach (var entity in zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
{
var entityKey = EntityKey(run.RunId, zone.ZoneIndex, entity.EntityIndex);
var snapshot = ToEntitySnapshot(entity, run.RunId, zone.ZoneIndex);
await node.PutEntitySnapshotAsync(entityKey, snapshot).ConfigureAwait(false);
entityKeys.Add(entityKey.ToString());
}
await node.PutZoneStateAsync(
zoneKey,
new AetheriaZoneState
{
Name = zone.Name ?? "",
Position = new AetheriaVector2
{
X = zone.PositionX,
Y = zone.PositionY
},
AdjacentZoneIndices = ToIntArray(zone.AdjacentZoneIndices),
FactionIndices = ToIntArray(zone.FactionIndices),
OwnerFactionIndex = zone.OwnerFactionIndex,
EntityKeys = entityKeys.ToArray(),
Orbits = ToOrbitSnapshots(zone.Orbits),
Bodies = ToBodySnapshots(zone.Bodies, run.RunId, zone.ZoneIndex),
DroppedPickups = ToDroppedPickups(zone.DroppedPickups)
})
.ConfigureAwait(false);
zoneKeys.Add(zoneKey.ToString());
}
if (zoneKeys.Count > 0)
run.ZoneKeys = zoneKeys.ToArray();
run.CurrentEntityKey = checkpoint.CurrentEntityKey ?? "";
await node.PutRunStateAsync(RunKey(run.RunId), run).ConfigureAwait(false);
}
private static AetheriaDroppedPickupSnapshot[] ToDroppedPickups(
IReadOnlyList<AetheriaRuntimeDroppedPickupCommit>? pickups)
{
return (pickups ?? Array.Empty<AetheriaRuntimeDroppedPickupCommit>())
.Select(pickup => new AetheriaDroppedPickupSnapshot
{
PickupIndex = pickup.PickupIndex,
Position = new AetheriaVector3
{
X = pickup.PositionX,
Y = pickup.PositionY,
Z = pickup.PositionZ
},
Velocity = new AetheriaVector3
{
X = pickup.VelocityX,
Y = pickup.VelocityY,
Z = pickup.VelocityZ
},
Item = ToLoadoutItem(pickup.Item)
})
.ToArray();
}
private static AetheriaOrbitSnapshot[] ToOrbitSnapshots(
IReadOnlyList<AetheriaRuntimeOrbitSnapshotCommit>? orbits)
{
return (orbits ?? Array.Empty<AetheriaRuntimeOrbitSnapshotCommit>())
.Select(orbit => new AetheriaOrbitSnapshot
{
OrbitKey = orbit.OrbitKey ?? "",
ParentOrbitKey = orbit.ParentOrbitKey ?? "",
Distance = orbit.Distance,
Phase = orbit.Phase,
FixedPosition = new AetheriaVector2
{
X = orbit.FixedPositionX,
Y = orbit.FixedPositionY
}
})
.ToArray();
}
private static AetheriaBodySnapshot[] ToBodySnapshots(
IReadOnlyList<AetheriaRuntimeBodySnapshotCommit>? bodies,
string runId,
int zoneIndex)
{
return (bodies ?? Array.Empty<AetheriaRuntimeBodySnapshotCommit>())
.Select(body => new AetheriaBodySnapshot
{
BodyKey = body.BodyKey ?? "",
Kind = body.Kind ?? "",
Name = body.Name ?? "",
OrbitKey = body.OrbitKey ?? "",
Mass = body.Mass,
Resources = ToBodyResources(body.Resources),
BodyRadiusMultiplier = body.BodyRadiusMultiplier,
GravityRadiusMultiplier = body.GravityRadiusMultiplier,
GravityDepthMultiplier = body.GravityDepthMultiplier,
GravityDepthExponent = body.GravityDepthExponent,
Asteroids = ToAsteroidSnapshots(body.Asteroids, runId, zoneIndex),
GasGiantVisual = ToGasGiantVisual(body.GasGiantVisual),
SunVisual = ToSunVisual(body.SunVisual)
})
.ToArray();
}
private static AetheriaBodyResource[] ToBodyResources(
IReadOnlyList<AetheriaRuntimeBodyResourceCommit>? resources)
{
return (resources ?? Array.Empty<AetheriaRuntimeBodyResourceCommit>())
.Select(resource => new AetheriaBodyResource
{
ItemKey = resource.ItemKey ?? "",
Amount = resource.Amount
})
.ToArray();
}
private static AetheriaAsteroidSnapshot[] ToAsteroidSnapshots(
IReadOnlyList<AetheriaRuntimeAsteroidCommit>? asteroids,
string runId,
int zoneIndex)
{
return (asteroids ?? Array.Empty<AetheriaRuntimeAsteroidCommit>())
.Select(asteroid => new AetheriaAsteroidSnapshot
{
Distance = asteroid.Distance,
Phase = asteroid.Phase,
Size = asteroid.Size,
RotationSpeed = asteroid.RotationSpeed,
Damage = asteroid.Damage,
RespawnTimer = asteroid.RespawnTimer,
MiningAccumulators = ToAsteroidMiningAccumulators(asteroid.MiningAccumulators, runId, zoneIndex)
})
.ToArray();
}
private static AetheriaAsteroidMiningAccumulatorSnapshot[] ToAsteroidMiningAccumulators(
IReadOnlyList<AetheriaRuntimeAsteroidMiningAccumulatorCommit>? accumulators,
string runId,
int zoneIndex)
{
return (accumulators ?? Array.Empty<AetheriaRuntimeAsteroidMiningAccumulatorCommit>())
.Where(accumulator => accumulator.MinerEntityIndex >= 0)
.Select(accumulator => new AetheriaAsteroidMiningAccumulatorSnapshot
{
MinerEntityKey = EntityKey(runId, zoneIndex, accumulator.MinerEntityIndex).ToString(),
Amount = accumulator.Amount
})
.ToArray();
}
private static AetheriaGasGiantVisualState ToGasGiantVisual(AetheriaRuntimeGasGiantVisualCommit? visual)
{
if (visual == null)
return new AetheriaGasGiantVisualState();
return new AetheriaGasGiantVisualState
{
FirstOffsetDomainRotationSpeed = visual.FirstOffsetDomainRotationSpeed,
FirstOffsetRotationSpeed = visual.FirstOffsetRotationSpeed,
SecondOffsetDomainRotationSpeed = visual.SecondOffsetDomainRotationSpeed,
SecondOffsetRotationSpeed = visual.SecondOffsetRotationSpeed,
AlbedoRotationSpeed = visual.AlbedoRotationSpeed,
WaveRadiusMultiplier = visual.WaveRadiusMultiplier,
WaveDepthMultiplier = visual.WaveDepthMultiplier,
WaveDepthExponent = visual.WaveDepthExponent,
WaveSpeedMultiplier = visual.WaveSpeedMultiplier,
MaterialOverrides = (visual.MaterialOverrides ?? Array.Empty<string>()).ToArray(),
Colors = (visual.Colors ?? Array.Empty<AetheriaRuntimeColorCommit>())
.Select(ToColor)
.ToArray()
};
}
private static AetheriaSunVisualState ToSunVisual(AetheriaRuntimeSunVisualCommit? visual)
{
if (visual == null)
return new AetheriaSunVisualState();
return new AetheriaSunVisualState
{
LightColor = new AetheriaVector3
{
X = visual.LightColorX,
Y = visual.LightColorY,
Z = visual.LightColorZ
},
FogTintColor = new AetheriaVector3
{
X = visual.FogTintColorX,
Y = visual.FogTintColorY,
Z = visual.FogTintColorZ
},
LightRadiusMultiplier = visual.LightRadiusMultiplier
};
}
private static AetheriaColor ToColor(AetheriaRuntimeColorCommit color)
{
return new AetheriaColor
{
X = color.X,
Y = color.Y,
Z = color.Z,
W = color.W
};
}
private static AetheriaEntitySnapshot ToEntitySnapshot(
AetheriaRuntimeEntitySnapshotCommit entity,
string runId,
int zoneIndex)
{
return new AetheriaEntitySnapshot
{
Name = entity.Name ?? "",
Kind = entity.Kind ?? "",
Position = new AetheriaVector3
{
X = entity.PositionX,
Y = entity.PositionY,
Z = entity.PositionZ
},
Direction = new AetheriaVector2
{
X = entity.DirectionX,
Y = entity.DirectionY
},
Velocity = new AetheriaVector2
{
X = entity.VelocityX,
Y = entity.VelocityY
},
TargetEntityKey = entity.TargetEntityIndex >= 0
? EntityKey(runId, zoneIndex, entity.TargetEntityIndex).ToString()
: "",
IsActive = entity.IsActive,
HeatsinksEnabled = entity.HeatsinksEnabled,
OverrideShutdown = entity.OverrideShutdown,
TractorPower = entity.TractorPower,
Heatstroke = entity.Heatstroke,
Hypothermia = entity.Hypothermia,
FactionKey = entity.FactionKey ?? "",
HullItemKey = entity.HullItemKey ?? "",
Equipment = ToEntityItemSlots(entity.Equipment),
CargoBays = ToEntityItemSlots(entity.CargoBays),
DockingBays = ToEntityItemSlots(entity.DockingBays),
ChildEntityKeys = (entity.ChildEntityIndices ?? Array.Empty<int>())
.Select(childIndex => EntityKey(runId, zoneIndex, childIndex).ToString())
.ToArray(),
WeaponGroups = ToIntArrayArray(entity.WeaponGroups)
.Select(group => new AetheriaWeaponGroupSnapshot { EquipmentIndices = group })
.ToArray(),
StatGrids = ToEntityStatGrids(entity.StatGrids),
ActiveConsumables = ToActiveConsumables(entity.ActiveConsumables),
BehaviorProgress = ToBehaviorProgress(entity.BehaviorProgress),
WeaponStates = ToWeaponStates(entity.WeaponStates, runId, zoneIndex),
BehaviorStates = ToBehaviorStates(entity.BehaviorStates),
CargoContents = ToCargoBays(entity.CargoContents),
DockingBayContents = ToCargoBays(entity.DockingBayContents),
DockingBayAssignments = ToIntArray(entity.DockingBayAssignments),
Visibility = entity.Visibility,
VisibilitySourceCount = entity.VisibilitySourceCount,
Contacts = ToEntityContacts(entity.Contacts, runId, zoneIndex)
};
}
private static AetheriaEntityContactSnapshot[] ToEntityContacts(
IReadOnlyList<AetheriaRuntimeEntityContactCommit>? contacts,
string runId,
int zoneIndex)
{
return (contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
.Where(contact => contact.TargetEntityIndex >= 0)
.Select(contact => new AetheriaEntityContactSnapshot
{
TargetEntityKey = EntityKey(runId, zoneIndex, contact.TargetEntityIndex).ToString(),
InfoGathered = contact.InfoGathered,
Hostile = contact.Hostile,
Visible = contact.Visible
})
.ToArray();
}
private static AetheriaBehaviorStateSnapshot[] ToBehaviorStates(
IReadOnlyList<AetheriaRuntimeBehaviorStateCommit>? states)
{
return (states ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
.Select(state => new AetheriaBehaviorStateSnapshot
{
OwnerKind = state.OwnerKind ?? "",
OwnerIndex = state.OwnerIndex,
BehaviorIndex = state.BehaviorIndex,
BehaviorKind = state.BehaviorKind ?? "",
Pinging = state.Pinging,
PingCooldown = state.PingCooldown,
PingLerp = state.PingLerp,
PingRadius = state.PingRadius,
PingedEntityCount = state.PingedEntityCount,
RadiatorTemperature = state.RadiatorTemperature,
Emissivity = state.Emissivity,
PumpedHeat = state.PumpedHeat,
WasteHeat = state.WasteHeat,
EnergyUsage = state.EnergyUsage,
ReactorDraw = state.ReactorDraw,
ReactorLoadRatio = state.ReactorLoadRatio,
CapacitorCharge = state.CapacitorCharge,
CapacitorCapacity = state.CapacitorCapacity,
CapacitorEfficiency = state.CapacitorEfficiency,
AetherDriveAxisX = state.AetherDriveAxisX,
AetherDriveAxisY = state.AetherDriveAxisY,
AetherDriveAxisZ = state.AetherDriveAxisZ,
AetherDriveThrustX = state.AetherDriveThrustX,
AetherDriveThrustY = state.AetherDriveThrustY,
AetherDriveThrustZ = state.AetherDriveThrustZ,
AetherDriveRpmX = state.AetherDriveRpmX,
AetherDriveRpmY = state.AetherDriveRpmY,
AetherDriveRpmZ = state.AetherDriveRpmZ,
AetherDriveMaximumRpm = state.AetherDriveMaximumRpm,
AetherDriveThrustDirectionX = state.AetherDriveThrustDirectionX,
AetherDriveThrustDirectionY = state.AetherDriveThrustDirectionY,
ResourceScannerTargetBodyKey = state.ResourceScannerTargetBodyKey ?? "",
ResourceScannerAsteroidIndex = state.ResourceScannerAsteroidIndex,
ResourceScannerScanTime = state.ResourceScannerScanTime,
ResourceScannerRange = state.ResourceScannerRange,
ResourceScannerMinimumDensity = state.ResourceScannerMinimumDensity,
ResourceScannerScanDuration = state.ResourceScannerScanDuration,
MiningToolAsteroidBeltKey = state.MiningToolAsteroidBeltKey ?? "",
MiningToolAsteroidIndex = state.MiningToolAsteroidIndex,
MiningToolRange = state.MiningToolRange,
ThrusterAxis = state.ThrusterAxis,
ThrusterThrust = state.ThrusterThrust,
ThrusterTorque = state.ThrusterTorque,
ShieldEfficiency = state.ShieldEfficiency,
ShieldEnergyUsage = state.ShieldEnergyUsage,
VelocityLimit = state.VelocityLimit,
ThermotoggleTargetTemperature = state.ThermotoggleTargetTemperature,
SwitchActivated = state.SwitchActivated,
TriggerPulled = state.TriggerPulled,
StatModifierApplied = state.StatModifierApplied,
StatModifierExecuted = state.StatModifierExecuted,
StatModifierTargetStatCount = state.StatModifierTargetStatCount,
TurretControllerWeaponCount = state.TurretControllerWeaponCount,
TurretControllerShotSpeed = state.TurretControllerShotSpeed,
TurretControllerPredictShots = state.TurretControllerPredictShots
})
.ToArray();
}
private static AetheriaWeaponStateSnapshot[] ToWeaponStates(
IReadOnlyList<AetheriaRuntimeWeaponStateCommit>? states,
string runId,
int zoneIndex)
{
return (states ?? Array.Empty<AetheriaRuntimeWeaponStateCommit>())
.Select(state => new AetheriaWeaponStateSnapshot
{
OwnerKind = state.OwnerKind ?? "",
OwnerIndex = state.OwnerIndex,
BehaviorIndex = state.BehaviorIndex,
BehaviorKind = state.BehaviorKind ?? "",
Firing = state.Firing,
Ammo = state.Ammo,
BurstRemaining = state.BurstRemaining,
BurstTimer = state.BurstTimer,
BurstInterval = state.BurstInterval,
CooldownProgress = state.CooldownProgress,
CoolingDown = state.CoolingDown,
Charging = state.Charging,
Charged = state.Charged,
Charge = state.Charge,
Reloading = state.Reloading,
ReloadProgress = state.ReloadProgress,
AmmoIntervalProgress = state.AmmoIntervalProgress,
LockProgress = state.LockProgress,
LockTargetEntityKey = state.LockTargetEntityIndex >= 0
? EntityKey(runId, zoneIndex, state.LockTargetEntityIndex).ToString()
: ""
})
.ToArray();
}
private static AetheriaBehaviorProgressSnapshot[] ToBehaviorProgress(
IReadOnlyList<AetheriaRuntimeBehaviorProgressCommit>? progress)
{
return (progress ?? Array.Empty<AetheriaRuntimeBehaviorProgressCommit>())
.Select(entry => new AetheriaBehaviorProgressSnapshot
{
OwnerKind = entry.OwnerKind ?? "",
OwnerIndex = entry.OwnerIndex,
BehaviorIndex = entry.BehaviorIndex,
BehaviorKind = entry.BehaviorKind ?? "",
Progress = entry.Progress
})
.ToArray();
}
private static AetheriaActiveConsumableSnapshot[] ToActiveConsumables(
IReadOnlyList<AetheriaRuntimeActiveConsumableCommit>? consumables)
{
return (consumables ?? Array.Empty<AetheriaRuntimeActiveConsumableCommit>())
.Select(consumable => new AetheriaActiveConsumableSnapshot
{
ItemKey = consumable.ItemKey ?? "",
Quality = consumable.Quality,
RemainingDuration = consumable.RemainingDuration,
Duration = consumable.Duration
})
.ToArray();
}
private static AetheriaEntityStatGrid[] ToEntityStatGrids(
IReadOnlyList<AetheriaRuntimeEntityStatGridCommit>? grids)
{
return (grids ?? Array.Empty<AetheriaRuntimeEntityStatGridCommit>())
.Select(grid => new AetheriaEntityStatGrid
{
Name = grid.Name ?? "",
Width = grid.Width,
Height = grid.Height,
Values = (grid.Values ?? Array.Empty<double>()).ToArray()
})
.ToArray();
}
private static int[] ToIntArray(IReadOnlyList<int>? values)
{
return (values ?? Array.Empty<int>()).ToArray();
}
private static int[][] ToIntArrayArray(IReadOnlyList<IReadOnlyList<int>>? values)
{
return (values ?? Array.Empty<IReadOnlyList<int>>())
.Select(ToIntArray)
.ToArray();
}
private static AetheriaActionBarBinding[] ToActionBarBindings(
IReadOnlyList<AetheriaRuntimeActionBarBindingCommit>? bindings)
{
return (bindings ?? Array.Empty<AetheriaRuntimeActionBarBindingCommit>())
.Select(binding => new AetheriaActionBarBinding
{
ControlPath = binding.ControlPath ?? "",
Kind = binding.Kind ?? "",
TargetKey = binding.ItemKey ?? "",
EquipmentIndex = binding.EquipmentIndex,
BehaviorIndex = binding.BehaviorIndex,
WeaponGroup = binding.WeaponGroup
})
.ToArray();
}
private static AetheriaFactionRelationshipState[] ToFactionRelationships(
IReadOnlyList<AetheriaRuntimeFactionRelationshipCommit>? relationships)
{
return (relationships ?? Array.Empty<AetheriaRuntimeFactionRelationshipCommit>())
.Select(relationship => new AetheriaFactionRelationshipState
{
FactionKey = relationship.FactionKey ?? "",
Relationship = relationship.Relationship ?? "",
Standing = relationship.Standing
})
.ToArray();
}
private static CultRecordKey LoadoutKey(string name)
{
return new CultRecordKey($"global:aetheria.loadout_template.{StableToken(name)}.v1");
}
private static CultRecordKey RunKey(string runId)
{
return new CultRecordKey($"global:aetheria.run_state.{StableToken(runId)}.v1");
}
private static CultRecordKey ZoneKey(string runId, int zoneIndex)
{
return new CultRecordKey($"global:aetheria.run_state.{StableToken(runId)}.zone.{zoneIndex}.v1");
}
private static CultRecordKey EntityKey(string runId, int zoneIndex, int entityIndex)
{
return new CultRecordKey($"global:aetheria.run_state.{StableToken(runId)}.zone.{zoneIndex}.entity.{entityIndex}.v1");
}
private static string StableToken(string value)
{
var chars = (string.IsNullOrWhiteSpace(value) ? "unnamed" : value.Trim().ToLowerInvariant())
.Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')
.ToArray();
var token = new string(chars).Trim('-');
while (token.Contains("--", StringComparison.Ordinal))
token = token.Replace("--", "-", StringComparison.Ordinal);
return string.IsNullOrWhiteSpace(token) ? "unnamed" : token;
}
private static InvalidDataException MissingPayload(AetheriaRuntimeStateCommitDocument command)
{
return new InvalidDataException($"Aetheria runtime commit '{command.CommandId}' has no {command.Kind} payload.");
}
}

View file

@ -0,0 +1,55 @@
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
namespace Aetheria.State;
public static class AetheriaRuntimeEveSurfaceStateProjector
{
public static EveSurfaceState ToState(AetheriaRuntimeSurfaceDocument document)
{
return new EveSurfaceState
{
ProviderId = document.ProviderId,
ProviderKind = document.ProviderKind,
Title = document.Title,
Version = document.Version,
UpdatedAtUtc = document.UpdatedAtUtc,
Surface = ToSurface(document.Surface),
Commands = document.Commands
.Select(command => new EveCommandTemplate
{
Command = command.Command,
Label = command.Label,
Transport = command.Transport
})
.ToArray()
};
}
private static EveSurface ToSurface(AetheriaRuntimeSurfaceTree surface)
{
return new EveSurface
{
Id = surface.Id,
Root = ToComponent(surface.Root),
Styles = surface.Styles
.Select(style => new EveStyleToken
{
Name = style.Name,
Value = style.Value
})
.ToArray()
};
}
private static EveSurfaceComponent ToComponent(AetheriaRuntimeSurfaceComponent component)
{
return new EveSurfaceComponent
{
Id = component.Id,
Kind = component.Kind,
Props = new Dictionary<string, string>(component.Props, StringComparer.Ordinal),
Children = component.Children.Select(ToComponent).ToArray()
};
}
}

View file

@ -0,0 +1,113 @@
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
using GameCult.Caching;
namespace Aetheria.State;
public static class AetheriaRuntimeStateMapper
{
public static AetheriaLoadoutTemplate ToLoadoutTemplate(
AetheriaRuntimeLoadoutTemplateCommit loadout,
string updatedAtUtc)
{
return new AetheriaLoadoutTemplate
{
Name = loadout.Name ?? "",
OwnerPlayerKey = loadout.OwnerPlayerKey ?? "",
RootEntity = ToEntityLoadout(loadout.RootEntity),
CreatedAtUtc = updatedAtUtc,
UpdatedAtUtc = updatedAtUtc
};
}
public static CultRecordKey LoadoutKey(string name)
{
return new CultRecordKey($"global:aetheria.loadout_template.{StableToken(name)}.v1");
}
private static AetheriaEntityLoadout ToEntityLoadout(AetheriaRuntimeEntityLoadoutCommit? entity)
{
entity ??= new AetheriaRuntimeEntityLoadoutCommit();
return new AetheriaEntityLoadout
{
Name = entity.Name ?? "",
Kind = entity.Kind ?? "",
FactionKey = entity.FactionKey ?? "",
Hull = ToLoadoutItem(entity.Hull),
Equipment = ToItemSlots(entity.Equipment),
CargoBays = ToItemSlots(entity.CargoBays),
DockingBays = ToItemSlots(entity.DockingBays),
CargoContents = ToCargoBays(entity.CargoContents),
DockingBayContents = ToCargoBays(entity.DockingBayContents),
DockingBayAssignments = ToIntArray(entity.DockingBayAssignments),
WeaponGroups = ToIntArrayArray(entity.WeaponGroups),
Children = (entity.Children ?? Array.Empty<AetheriaRuntimeEntityLoadoutCommit>())
.Select(ToEntityLoadout)
.ToArray()
};
}
private static AetheriaLoadoutItem ToLoadoutItem(AetheriaRuntimeLoadoutItemCommit? item)
{
item ??= new AetheriaRuntimeLoadoutItemCommit();
return new AetheriaLoadoutItem
{
ItemKey = item.ItemKey ?? "",
Quality = item.Quality,
Durability = item.Durability,
Quantity = item.Quantity,
Enabled = item.Enabled,
OverrideShutdown = item.OverrideShutdown
};
}
private static AetheriaLoadoutItemSlot[] ToItemSlots(
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit>? slots)
{
return (slots ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
.Select(slot => new AetheriaLoadoutItemSlot
{
Position = new AetheriaGridCoord
{
X = slot.X,
Y = slot.Y
},
Item = ToLoadoutItem(slot.Item)
})
.ToArray();
}
private static AetheriaCargoBayLoadout[] ToCargoBays(
IReadOnlyList<AetheriaRuntimeCargoBayLoadoutCommit>? cargoBays)
{
return (cargoBays ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>())
.Select(bay => new AetheriaCargoBayLoadout
{
Items = ToItemSlots(bay.Items)
})
.ToArray();
}
private static int[] ToIntArray(IReadOnlyList<int>? values)
{
return (values ?? Array.Empty<int>()).ToArray();
}
private static int[][] ToIntArrayArray(IReadOnlyList<IReadOnlyList<int>>? values)
{
return (values ?? Array.Empty<IReadOnlyList<int>>())
.Select(ToIntArray)
.ToArray();
}
private static string StableToken(string value)
{
var chars = (string.IsNullOrWhiteSpace(value) ? "unnamed" : value.Trim().ToLowerInvariant())
.Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')
.ToArray();
var token = new string(chars).Trim('-');
while (token.Contains("--", StringComparison.Ordinal))
token = token.Replace("--", "-", StringComparison.Ordinal);
return string.IsNullOrWhiteSpace(token) ? "unnamed" : token;
}
}

View file

@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Threading.Tasks;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Unity;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using GameCult.Mesh;
@ -207,6 +208,164 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
new CultRecordKey(AetheriaProviderAdvertisementProjector.AdvertisementKey));
}
public Task<CultRecordHandle<AetheriaRuntimeDaemonProviderAdvertisementDocument>> PutDaemonProviderAdvertisementAsync(
AetheriaRuntimeDaemonProviderAdvertisementDocument advertisement)
{
return Database.PutAsync(new CultRecordKey("daemon:aetheria.provider_advertisement.v1"), advertisement);
}
public Task<AetheriaRuntimeDaemonProviderAdvertisementDocument?> GetDaemonProviderAdvertisementAsync()
{
return Database.GetAsync<AetheriaRuntimeDaemonProviderAdvertisementDocument>(
new CultRecordKey("daemon:aetheria.provider_advertisement.v1"));
}
public Task<CultRecordHandle<AetheriaRuntimeDaemonHealthDocument>> PutDaemonHealthAsync(
AetheriaRuntimeDaemonHealthDocument health)
{
return Database.PutAsync(new CultRecordKey("daemon:aetheria.health.v1"), health);
}
public Task<AetheriaRuntimeDaemonHealthDocument?> GetDaemonHealthAsync()
{
return Database.GetAsync<AetheriaRuntimeDaemonHealthDocument>(
new CultRecordKey("daemon:aetheria.health.v1"));
}
public Task<CultRecordHandle<AetheriaRuntimeDaemonCommandBoundaryDocument>> PutDaemonCommandBoundaryAsync(
AetheriaRuntimeDaemonCommandBoundaryDocument boundary)
{
return Database.PutAsync(new CultRecordKey("daemon:aetheria.command_boundary.v1"), boundary);
}
public Task<AetheriaRuntimeDaemonCommandBoundaryDocument?> GetDaemonCommandBoundaryAsync()
{
return Database.GetAsync<AetheriaRuntimeDaemonCommandBoundaryDocument>(
new CultRecordKey("daemon:aetheria.command_boundary.v1"));
}
public Task<CultRecordHandle<AetheriaRuntimeDaemonCommandDocument>> SubmitDaemonCommandAsync(
AetheriaRuntimeDaemonCommandDocument command)
{
if (command == null) throw new ArgumentNullException(nameof(command));
command.Schema = AetheriaRuntimeDaemonSchemas.Command;
if (string.IsNullOrWhiteSpace(command.CommandId))
command.CommandId = Guid.NewGuid().ToString("N");
if (string.IsNullOrWhiteSpace(command.IssuedAtUtc))
command.IssuedAtUtc = DateTime.UtcNow.ToString("O");
return Database.PutAsync(DaemonCommandKey(command.CommandId), command);
}
public IReadOnlyList<AetheriaRuntimeDaemonCommandDocument> ReadObservedDaemonCommands()
{
return Cache
.GetAll<AetheriaRuntimeDaemonCommandDocument>()
.OrderBy(command => command.IssuedAtUtc ?? "", StringComparer.Ordinal)
.ThenBy(command => command.CommandId ?? "", StringComparer.Ordinal)
.ToArray();
}
private static CultRecordKey DaemonCommandKey(string commandId)
{
return new CultRecordKey($"daemon:commands:{StableToken(commandId)}:gamecult.aetheria.daemon_command.v1");
}
public Task<CultRecordHandle<AetheriaRuntimeEveCommandDocument>> SubmitEveCommandAsync(
AetheriaRuntimeEveCommandDocument command)
{
if (command == null) throw new ArgumentNullException(nameof(command));
command.Schema = AetheriaRuntimeEveCommandDocument.SchemaId;
if (string.IsNullOrWhiteSpace(command.CommandId))
command.CommandId = Guid.NewGuid().ToString("N");
if (string.IsNullOrWhiteSpace(command.IssuedAtUtc))
command.IssuedAtUtc = DateTime.UtcNow.ToString("O");
return Database.PutAsync(EveCommandKey(command.CommandId), command);
}
public IReadOnlyList<AetheriaRuntimeEveCommandDocument> ReadObservedEveCommands()
{
return Cache
.GetAll<AetheriaRuntimeEveCommandDocument>()
.OrderBy(command => command.IssuedAtUtc ?? "", StringComparer.Ordinal)
.ThenBy(command => command.CommandId ?? "", StringComparer.Ordinal)
.ToArray();
}
private static CultRecordKey EveCommandKey(string commandId)
{
return new CultRecordKey($"eve:commands:{StableToken(commandId)}:gamecult.eve.command.v1");
}
public Task<CultRecordHandle<AetheriaRuntimeDaemonFrameDocument>> PutDaemonFrameAsync(
AetheriaRuntimeDaemonFrameDocument frame)
{
return Database.PutAsync(new CultRecordKey("daemon:aetheria.frame.latest.v1"), frame);
}
public Task<AetheriaRuntimeDaemonFrameDocument?> GetDaemonFrameAsync()
{
return Database.GetAsync<AetheriaRuntimeDaemonFrameDocument>(
new CultRecordKey("daemon:aetheria.frame.latest.v1"));
}
public Task<CultRecordHandle<AetheriaRuntimeDaemonSoaViewDocument>> PutDaemonSoaViewAsync(
AetheriaRuntimeDaemonSoaViewDocument view)
{
return Database.PutAsync(new CultRecordKey("daemon:aetheria.soa_view.latest.v1"), view);
}
public Task<AetheriaRuntimeDaemonSoaViewDocument?> GetDaemonSoaViewAsync()
{
return Database.GetAsync<AetheriaRuntimeDaemonSoaViewDocument>(
new CultRecordKey("daemon:aetheria.soa_view.latest.v1"));
}
public Task<CultRecordHandle<EveSurfaceState>> PutDaemonGameSurfaceAsync(EveSurfaceState surface)
{
return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonGameSurfaceKey), surface);
}
public Task<EveSurfaceState?> GetDaemonGameSurfaceAsync()
{
return Database.GetAsync<EveSurfaceState>(
new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonGameSurfaceKey));
}
public Task<CultRecordHandle<EveSurfaceState>> PutDaemonGameTuiSurfaceAsync(EveSurfaceState surface)
{
return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonGameTuiSurfaceKey), surface);
}
public Task<EveSurfaceState?> GetDaemonGameTuiSurfaceAsync()
{
return Database.GetAsync<EveSurfaceState>(
new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonGameTuiSurfaceKey));
}
public Task<CultRecordHandle<EveSurfaceState>> PutDaemonEditorSurfaceAsync(EveSurfaceState surface)
{
return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonEditorSurfaceKey), surface);
}
public Task<EveSurfaceState?> GetDaemonEditorSurfaceAsync()
{
return Database.GetAsync<EveSurfaceState>(
new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonEditorSurfaceKey));
}
public Task<CultRecordHandle<EveSurfaceState>> PutDaemonEditorTuiSurfaceAsync(EveSurfaceState surface)
{
return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonEditorTuiSurfaceKey), surface);
}
public Task<EveSurfaceState?> GetDaemonEditorTuiSurfaceAsync()
{
return Database.GetAsync<EveSurfaceState>(
new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonEditorTuiSurfaceKey));
}
public Task<CultRecordHandle<AetheriaRuntimeSession>> PutRuntimeSessionAsync(AetheriaRuntimeSession session)
{
return Database.PutAsync(new CultRecordKey($"runtime:{session.RuntimeId}:aetheria.runtime_session.v1"), session);
@ -284,28 +443,16 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
new CultRecordKey("global:aetheria.verse_host_settings.v1"));
}
public Task<CultRecordHandle<AetheriaRuntimeCommitDrainStatus>> PutRuntimeCommitDrainStatusAsync(
AetheriaRuntimeCommitDrainStatus status)
public Task<CultRecordHandle<AetheriaEveCommandAcceptanceStatus>> PutEveCommandAcceptanceStatusAsync(
AetheriaEveCommandAcceptanceStatus status)
{
return Database.PutAsync(new CultRecordKey("global:aetheria.runtime_commit_drain_status.v1"), status);
return Database.PutAsync(new CultRecordKey("global:aetheria.eve_command_acceptance_status.v1"), status);
}
public Task<AetheriaRuntimeCommitDrainStatus?> GetRuntimeCommitDrainStatusAsync()
public Task<AetheriaEveCommandAcceptanceStatus?> GetEveCommandAcceptanceStatusAsync()
{
return Database.GetAsync<AetheriaRuntimeCommitDrainStatus>(
new CultRecordKey("global:aetheria.runtime_commit_drain_status.v1"));
}
public Task<CultRecordHandle<AetheriaEveCommandDrainStatus>> PutEveCommandDrainStatusAsync(
AetheriaEveCommandDrainStatus status)
{
return Database.PutAsync(new CultRecordKey("global:aetheria.eve_command_drain_status.v1"), status);
}
public Task<AetheriaEveCommandDrainStatus?> GetEveCommandDrainStatusAsync()
{
return Database.GetAsync<AetheriaEveCommandDrainStatus>(
new CultRecordKey("global:aetheria.eve_command_drain_status.v1"));
return Database.GetAsync<AetheriaEveCommandAcceptanceStatus>(
new CultRecordKey("global:aetheria.eve_command_acceptance_status.v1"));
}
public Task FlushAsync(bool soft = false)
@ -313,6 +460,20 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
return _node.FlushAsync(soft);
}
private static string StableToken(string value)
{
if (string.IsNullOrWhiteSpace(value))
return "empty";
var chars = value
.Select(character => char.IsLetterOrDigit(character) ? character : '-')
.ToArray();
var token = new string(chars).Trim('-').ToLowerInvariant();
while (token.Contains("--", StringComparison.Ordinal))
token = token.Replace("--", "-", StringComparison.Ordinal);
return string.IsNullOrWhiteSpace(token) ? "empty" : token;
}
public void Dispose()
{
_node.Dispose();

View file

@ -3,14 +3,14 @@ using MessagePack;
namespace Aetheria.State.Documents;
[CultDocument("aetheria.eve_command_drain_status", "aetheria.eve_command_drain_status.v1")]
[CultDocument("aetheria.eve_command_acceptance_status", "aetheria.eve_command_acceptance_status.v1")]
[CultGlobal]
[MessagePackObject]
public sealed class AetheriaEveCommandDrainStatus
public sealed class AetheriaEveCommandAcceptanceStatus
{
[Key(0)]
[CultName]
public string Name { get; set; } = "Aetheria Eve command drain";
public string Name { get; set; } = "Aetheria Eve command acceptance";
[Key(1)]
[CultIndex("runtimeId")]
@ -26,7 +26,7 @@ public sealed class AetheriaEveCommandDrainStatus
public string LastAcceptedAtUtc { get; set; } = "";
[Key(5)]
public int PendingBeforeApply { get; set; }
public int ObservedBeforeAccept { get; set; }
[Key(6)]
public int CommandsAccepted { get; set; }
@ -60,4 +60,13 @@ public sealed class AetheriaEveCommandDrainStatus
[Key(16)]
public int AppliedVerseHostCommands { get; set; }
[Key(17)]
public int AppliedInputSettingsCommands { get; set; }
[Key(18)]
public int AppliedLoadoutTemplateCommands { get; set; }
[Key(19)]
public string[] AccountedCommandIds { get; set; } = [];
}

View file

@ -1,51 +0,0 @@
using GameCult.Caching;
using MessagePack;
namespace Aetheria.State.Documents;
[CultDocument("aetheria.runtime_commit_drain_status", "aetheria.runtime_commit_drain_status.v1")]
[CultGlobal]
[MessagePackObject]
public sealed class AetheriaRuntimeCommitDrainStatus
{
[Key(0)]
[CultName]
public string Name { get; set; } = "Aetheria runtime commit drain";
[Key(1)]
[CultIndex("runtimeId")]
public string RuntimeId { get; set; } = "";
[Key(2)]
public string StatePath { get; set; } = "";
[Key(3)]
public string LastPollAtUtc { get; set; } = "";
[Key(4)]
public string LastAppliedAtUtc { get; set; } = "";
[Key(5)]
public int PendingBeforeApply { get; set; }
[Key(6)]
public int CommandsApplied { get; set; }
[Key(7)]
public int AppliedPlayerSettings { get; set; }
[Key(8)]
public int AppliedLoadoutTemplates { get; set; }
[Key(9)]
public int AppliedRunCheckpoints { get; set; }
[Key(10)]
public int ConsecutiveFailures { get; set; }
[Key(11)]
public string LastError { get; set; } = "";
[Key(12)]
public string Status { get; set; } = "ok";
}

View file

@ -38,27 +38,28 @@ The Unity client no longer writes `PlayerSettings.msgpack`, `.loadout`, or
replacement for bespoke loadout files, with structured hull/equipment/cargo/
docking item slots, nested child-entity loadouts, and weapon groups.
`RuntimePlayerSettings` is the Unity session projection for menus, formatting,
and input binding edits; menu and input-screen changes queue typed
`.cc.pending` player-settings commits and are not portable state authority.
Unity settings, loadout templates, and run checkpoints now queue typed
`.cc.pending` runtime commit documents beside
`GameData/aetheria-world.cc`. `Aetheria.State` owns applying those documents
into canonical typed state through `AetheriaRuntimeCommitLogApplier`; the
Unity-side command log is command-only and cannot become durable truth by
itself.
`AetheriaRuntimeSession` is the typed Verse signal for the running state host
or one-shot pending drain. `Economy.Server` owns the long-running
`cultmesh-state-host` heartbeat and publishes `starting`, `running`, and
`stopping` states through `Aetheria.State`; drain status remains operation
telemetry, not daemon-session ownership.
Unity Eve surfaces also emit typed `gamecult.eve.command.v1` documents under
`GameData/aetheria-world.cc.eve.pending`. `AetheriaEveCommandBridge` is the
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 and typed Verse-host visibility
commands. Renderer callbacks do not accept commands or mutate game state
locally.
and input binding display. Menu and input-screen changes send typed Eve request
documents to the provider-owned Aetheria Verse service; they are not portable
state authority and must not mutate daemon-owned state inside Unity.
The old `.cc.pending` runtime commit document lane is deleted. Gameplay state,
loadout templates, and run checkpoints are accepted by the daemon/Verse owner
through typed state operations and daemon command documents, not by
renderer-side commit authority.
`AetheriaRuntimeSession` is the typed Verse signal for the running daemon.
`Aetheria.State.Daemon` owns the long-running `aetheria-daemon` heartbeat and
publishes `starting`, `running`, and `stopping` states through `Aetheria.State`;
legacy outbox status remains operation telemetry, not daemon-session ownership.
Unity Eve surfaces construct typed `gamecult.eve.command.v1` command documents.
The neutral `AetheriaCommandPort` owns command submission for headless and
non-Unity clients; Unity input providers reach the same records through the
`AetheriaRuntimeCommandPort` facade. The Verse daemon uses `AetheriaStateNode`
to observe and accept those CultNet records in the Aetheria state graph.
`AetheriaEveCommandBridge` is the provider-owned command acceptor for those
records: 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 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
@ -66,6 +67,15 @@ only a package sample.
The old `SavedGame`/`SavedZone` DTOs and `Galaxy` loader constructor have also
been deleted; the new document family is live Verse run/zone state, not a
bespoke save-file format.
Daemon run frames project into Unity through the explicit
`EntityConstructionBlueprintProjector.ProjectObservedFromBlueprint` lowering
path; authoritative blueprint construction remains named separately as a
migration target for code that still has to move into the daemon.
Main-menu gameplay boot also projects the daemon run through
`Galaxy.ProjectObservedDaemonRun` rather than constructing or generating a
Unity-owned run. The Unity handle for this shell is
`ActionGameManager.ObservedGalaxy` to keep the client-side object named as an
observation.
The dead `SavedStory` JSON DTO is gone. `ZoneConstructionBlueprint` and
`RuntimeEntityBlueprint` remain in-memory construction/loadout projections for
Unity runtime code, with `RuntimeEntityBlueprintProjector` capturing and
@ -104,13 +114,13 @@ lookup overloads and the derived behavior `AmmoType`/`Item` GUID properties
have been deleted. Action-bar consumable bindings now read the target `ItemKey`
directly from typed runtime catalog rows instead of reconstructing it from
legacy item IDs.
Unity runtime commit DTOs carry typed `ItemKey`/`HullItemKey` fields only for
Daemon snapshot DTOs carry typed `ItemKey`/`HullItemKey` fields only for
loadout items, action-bar targets, body resources, entity hulls, and active
consumables; the commit applier no longer reconstructs item keys from legacy
item GUID fields.
Unity runtime commit DTOs now carry typed `FactionKey` only for loadout
entities, run entity snapshots, and faction relationship state. The old
pending-command `CorporationLegacyId` fallback fields are deleted.
consumables; daemon command application no longer reconstructs item keys from
legacy item GUID fields.
Daemon snapshot DTOs now carry typed `FactionKey` only for loadout entities,
run entity snapshots, and faction relationship state. The old command-document
`CorporationLegacyId` fallback fields are deleted.
`EntityConstructionBlueprint` also carries `FactionKey` only. Loadout restore,
entity construction, run entity snapshots, and faction relationship commits no
longer rebuild corporation references from legacy `Faction.ID` GUIDs.
@ -124,10 +134,10 @@ GUID link fields.
sector-map link/influence rendering, zone security ownership checks, and
runtime faction-relationship ordering now use `FactionKey`. The legacy
`Faction.ID` projection field is deleted from the temporary simulation shell.
Run checkpoint commit DTOs carry typed `OrbitKey`, `ParentOrbitKey`, and
`BodyKey` only for zone orbit/body snapshots. The old pending-command
Run checkpoint snapshot DTOs carry typed `OrbitKey`, `ParentOrbitKey`, and
`BodyKey` only for zone orbit/body snapshots. The old command-document
`OrbitLegacyId`, `ParentLegacyId`, and `BodyLegacyId` fallback fields are
deleted. Behavior-state runtime commits and readback snapshots carry typed
deleted. Behavior-state daemon snapshots and readback snapshots carry typed
body-key fields for resource scanner targets and mining tool asteroid belts;
the old raw body ID commit/readback field names are deleted. `ResourceScanner`
and `MiningTool` now retain typed body-key runtime state directly, while
@ -319,8 +329,8 @@ 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.
visibility changes append provider-owned Eve request documents 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`
@ -346,13 +356,13 @@ reviving `PlayerSettings.msgpack`, `.loadout`, or `.zone` serialization.
Unity `MainMenu` now lowers that same player-settings Eve document contract
locally through UI Toolkit instead of hand-authoring separate gameplay/graphics
widgets; the menu shell keeps name entry and back-navigation while gameplay
still owns command semantics through `ActionGameManager.CommitRuntimePlayerSettingsCommand`.
still owns command semantics through `ActionGameManager.RequestRuntimePlayerSettingsCommand`.
The run document also carries `CurrentEntityKey`, so Continue restore binds the
saved player entity by exact typed record identity instead of reconstructing it
from a transient entity-slot index. Canonical typed run state no longer stores
`CurrentZoneEntityIndex`; older stored runs are translated through the package
reader into `CurrentEntityKey`, and older queued runtime commits are translated
through the pending commit reader when those compatibility seams are needed.
reader into `CurrentEntityKey`. The older runtime commit importer has
been removed from the first-party state host and Unity package.
`Aetheria.State.Verify` opens a materialized state file and checks that the
typed migration ledger matches the actual item, corporation, and name-file
@ -371,38 +381,38 @@ symbols such as MessagePack, Newtonsoft, JsonKnownTypes, RethinkDB, or old
`DatabaseEntry`/`DatabaseLink` references; the remaining MessagePack usage is a
named package/import CultCache transport boundary, not gameplay state authority.
The package boundary is also fenced: MessagePack symbols may only appear in the
runtime catalog reader, pending CultCache envelope store, and typed runtime/Eve
runtime catalog reader, CultCache document store, and typed runtime/Eve
command document files until generated CultCache serializers replace that
temporary Unity-side codec. The verifier also checks that the Eve runtime
bootstrap exists, mounts the operations surface through the UI Toolkit presenter,
and keeps renderer commands routed through the typed Eve command log.
and keeps renderer commands routed through the typed Eve command inbox.
Use it after import when `GameData/aetheria-world.cc` changes.
`Aetheria.State.Unity.Smoke` opens the materialized state through the runtime
facade and proves read-only Unity-facing catalog access without the legacy
catalog reader. It also proves runtime commit envelopes for settings and run
zone/entity snapshots can be queued, applied through `Aetheria.State`, and
cleared. Pending runtime state and Eve command queue files are CultCache-shaped
`.cc` records with embedded schema catalogs; their current record payload codec
is still temporary package code until Unity consumes generated CultCache
catalog reader. Live Unity code uses typed Eve or daemon request documents
instead of runtime commit outboxes. Runtime and Eve requests are typed `.cc`
records with embedded schema catalogs; their current record payload codec is
still temporary package code until Unity consumes generated CultCache
serializers directly.
`Economy.Server` opens the CultMesh state host, applies pending runtime commits
and pending Eve surface commands on startup, and polls for new pending work
while running. For a bounded one-shot drain without leaving the host running:
`Aetheria.State.Daemon` opens the CultMesh Verse daemon, publishes daemon-owned
Verse identity/health/game/editor surfaces, accepts observed Eve command
documents on startup, and executes observed daemon command records each tick.
For a bounded one-shot daemon tick:
```powershell
dotnet run --project .\Economy.Server\Economy.Server.csproj -- --apply-pending-once
dotnet run --project .\Aetheria.State.Daemon\Aetheria.State.Daemon.csproj -- --once
```
The server publishes `AetheriaRuntimeCommitDrainStatus`,
`AetheriaEveCommandDrainStatus`, the `aetheria.operations` Eve surface, and the
typed `aetheria.player_settings` Eve surface after drain attempts, so pending
depth, accepted/applied/rejected counts, failures, timestamps, and player
settings controls are typed state rather than console-only status. It also
The daemon publishes `AetheriaEveCommandAcceptanceStatus`, the
`aetheria.operations` Eve surface, and the
typed `aetheria.player_settings` Eve surface after request acceptance attempts,
so observed depth, accepted/applied/rejected counts, failures, timestamps, and
player settings controls are typed state rather than console-only status. It also
persists daemon-owned `aetheria.verse_host_settings.v1`, with boot-time
defaults or overrides for service id, Verse id, visibility, and CultMesh
address. When visibility is `public`, the long-running daemon also serves a
address. When visibility is `public`, the daemon also serves a
derived CultMesh Verse catalog from that same typed host document, using the
located service name as the discoverable `cultnet://...:3075` endpoint. The
operations surface and provider advertisement project from that typed host
@ -427,19 +437,10 @@ The Unity Verse-settings shell can also trigger the same one-shot sync through
the `Aetheria.State.Replica` bridge when the repo-local tool is available. That
keeps target selection local while the replica organ owns daemon transport.
`Aetheria.State.ApplyPending` is the smaller local operator applicator for
queued Unity runtime commits and Eve commands when the server host is not being
used. It also republishes the typed drain status, operations surface,
player-settings surface, typed `aetheria.verse_host_settings.v1`, and provider
advertisement after applying pending files:
```powershell
dotnet run --project .\Aetheria.State.ApplyPending\Aetheria.State.ApplyPending.csproj -- .
```
Pass an explicit state path as the second argument when applying a non-default
`.cc` file. Use `--keep` only for diagnostics; normal application deletes
successfully applied command files.
The old standalone `Aetheria.State.DrainCommands` applicator is deleted. Command
acceptance is daemon behavior now; bounded operation uses `Aetheria.State.Daemon
--once` so the same Verse member publishes the same provider, health, command
boundary, and Eve surfaces.
Current rebuild notes:

View file

@ -0,0 +1,52 @@
# Aetheria Verse Daemon Shape
Aetheria should follow the Odin/VoidBot daemon shape instead of growing a private Unity-shaped command system.
## Authority
Odin is the all-seer, not the owner. It discovers Aetheria's provider advertisement and interface bindings, indexes the schema/catalog surface, and gives agents a registry overview. Idunn keeps the daemon alive from daemon-published health and command-boundary records. Bifrost hosts the MCP crossing for Codex and other xeno agents. Eve/CultUI defines presentation shape. The Aetheria daemon remains the side-effect owner.
The daemon owns Aetheria simulation state, typed command application, health publication, command boundaries, provider advertisement, and provider-owned Eve GUI/TUI surfaces. Unity is a renderer and input lowerer. A browser, a terminal, Blender, or another runtime should see the same daemon-owned Verse through the same typed publications.
Unity-side entity graph creation must name the boundary it is crossing. `EntityConstructionBlueprintProjector.InstantiateAuthoritativeFromBlueprint` is for local authoritative construction paths that still need to be migrated into the daemon; daemon frames use `EntityConstructionBlueprintProjector.ProjectObservedFromBlueprint` to lower observed state into Unity objects. A Unity frame consumer must not call the authoritative construction entry point.
The same rule applies at the galaxy shell. `Galaxy.ProjectObservedDaemonRun` may build a Unity navigation/rendering projection from an authoritative daemon run frame. Main menu boot must not call a `Galaxy` constructor directly or run procedural generation; the daemon owns the run and Unity lowers it. The Unity-side public handle is `ActionGameManager.ObservedGalaxy`, not `CurrentGalaxy`, because it is a projection of daemon state.
VoidBot's Odin MCP shape is the useful boundary model for agents: list providers through Odin, list Verses through Odin, load a provider-owned interface context through Odin, and invoke only commands that the provider interface advertises. The MCP is not a Brokkr special case and not a raw daemon socket wrapper.
Brokkr is just one daemon that happens to publish Unity editor surfaces. Aetheria should behave the same way: publish provider advertisements and Eve GUI/TUI surfaces into the local Verse, let Odin index them, and let Bifrost expose ergonomic MCP tools that lower or invoke those surfaces without taking ownership.
## Publications
Required Aetheria daemon publications are the live typed schemas in `AetheriaRuntimeDaemonSchemas`:
- `gamecult.aetheria.daemon_provider_advertisement.v1`: Verse id, provider ids, daemon id, schema catalog, state witnesses, Eve surface keys, command boundaries, and transport profile metadata.
- `gamecult.aetheria.daemon_health.v1`: daemon id, Verse id, state path, frame id, command acceptance status, simulation status, publication source, transport, and command-boundary path.
- `gamecult.aetheria.daemon_command_boundary.v1`: typed command schemas accepted by the daemon, authority requirements, receipt schema, and delivery transport.
- `gamecult.aetheria.daemon_frame.v1` plus `gamecult.aetheria.daemon_soa_view.v1`: authoritative simulation state for renderer clients.
- `gamecult.aetheria.daemon_game_surface.v1` and `gamecult.aetheria.daemon_editor_surface.v1`: GUI/TUI views of the same provider-owned state.
The provider advertisement is not a dashboard index. It is the daemon's promise about what it owns, where its witnesses live, what command boundary it accepts, and what transports can reach it.
Aetheria currently has two advertisement layers during the bridge:
- `gamecult.eve.provider_advertisement.v1` is the Odin-visible Eve provider card for the Aetheria Verse daemon and compatibility surfaces such as catalog, operations, and player settings.
- `gamecult.aetheria.daemon_provider_advertisement.v1` is the daemon-owned Aetheria runtime contract for authoritative frames, SoA views, game/editor GUI surfaces, game/editor TUI surfaces, health, and command boundaries.
The bridge layer must point at daemon-owned witnesses instead of becoming a second source of truth. Long term, Odin should discover the daemon-owned provider advertisement and interface bindings directly; the older state-host card should become a compatibility adapter or disappear.
## Queues
Queues are an implementation detail. Eve commands and daemon commands are typed `gamecult.eve.command.v1` and `gamecult.aetheria.daemon_command.v1` records in the Aetheria state graph; the old `.pending` runtime commit outbox is gone. None of those folders are the public API, daemon identity, or a substitute for CultNet/CultMesh command ports. Anything still named `Queue*`, `CommandLog`, `Inbox`, `mailbox`, `.eve.commands`, `.daemon.commands`, `.cc.pending`, or `PendingCultCacheStore` in Unity-facing daemon/Eve command code should lower to typed command documents and be demoted behind typed operation methods as the CultNet command boundaries come online.
`AetheriaCommandPort` is the neutral command submission implementation for typed command records. Unity-side command lowerers may use `AetheriaRuntimeCommandPort` as a facade over that same port. They must not drain those records or mutate state in response; ownership stays with the daemon Verse member.
Migration rule: do not expose string command names and payload maps as public Aetheria APIs. Compatibility adapters may decode external Eve requests at the edge, but persisted Aetheria command documents and Unity gameplay calls should use typed command bodies and typed operation methods.
Daemon-published game surface buttons select typed daemon command documents by command id (`aetheria.daemon.commands.*`). They must not smuggle daemon command fields through generic Eve payload maps such as `commandKind`, `weaponGroup`, or `scalarValue`; richer commands need explicit typed body builders.
Unity menu handlers should not construct Aetheria command bodies by spelunking `EveSurfaceCommandRequest.Payload`. Pass the request to the shared typed command client at the Eve edge, then submit the resulting `gamecult.eve.command.v1` or `gamecult.aetheria.daemon_command.v1` document.
## Cut Line
If a status/control surface matters to operators or agents, publish it as typed CultCache state or a `.cc` witness plus an Eve/CultUI CultMesh surface. Do not add a private HTTP dashboard, Unity-only inspector, JSON status blob, or agent-specific daemon wrapper as canonical truth.