Reconcile Unity commands from daemon facts

This commit is contained in:
Meta Vens 2026-07-10 03:15:17 +01:00
commit 13e27e5572
8 changed files with 145 additions and 10 deletions

View file

@ -246,13 +246,9 @@ static async Task PublishCommittedCommandFactsAsync(
options.VerseId)).ConfigureAwait(false);
}
var policyRejectedIds = new HashSet<string>(
policyRejectedCommandIds ?? Array.Empty<string>(),
StringComparer.Ordinal);
foreach (var commandId in frame.RejectedCommandIds ?? Array.Empty<string>())
{
if (string.IsNullOrWhiteSpace(commandId) ||
!policyRejectedIds.Contains(commandId) ||
!byCommandId.TryGetValue(commandId, out var command))
continue;

View file

@ -446,6 +446,44 @@ public class DaemonRuntimeDocumentTests
Assert.AreEqual("aetheria-remote-cultmesh-replica", bridge.TransportKind);
}
[Test]
public void RemoteReceiptTrackerWaitsForAuthoritativeDaemonFrame()
{
var tracker = new AetheriaEveUnityRemoteReceiptTracker();
var request = new EveSurfaceCommandRequest(
"aetheria.daemon",
AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId,
CultMesh.OperationInvocation("aetheria.daemon.commands.SetMoveVector"),
CultMesh.OperationPayload(("directionX", "1.0"), ("directionY", "0.0")),
DateTimeOffset.Parse("2026-07-10T03:00:00Z"),
"unity-remote-test",
"aetheria.daemon.commands",
AetheriaRuntimeDaemonSchemas.Frame);
tracker.Track("move-remote-1", request);
Assert.AreEqual(
0,
tracker.Reconcile(Array.Empty<AetheriaRuntimeCommittedCommandFactDocument>(), 10).Count);
Assert.IsTrue(tracker.HasPending);
var acceptedFrame = AetheriaRuntimeDaemonFrameDocument.Create(
RunWithTwoEntities(), "aetheria-daemon", "session", 11, 1.02, 0.02);
var fact = AetheriaRuntimeCommittedCommandFactDocument.FromAppliedCommand(
acceptedFrame,
new AetheriaRuntimeDaemonCommandDocument { CommandId = "move-remote-1" },
"aetheria.local");
Assert.AreEqual(0, tracker.Reconcile(new[] { fact }, 10).Count);
var receipts = tracker.Reconcile(new[] { fact }, 11);
Assert.AreEqual(1, receipts.Count);
Assert.AreEqual("accepted", receipts[0].State);
Assert.AreEqual("move-remote-1", receipts[0].CommandId);
Assert.AreEqual("aetheria-daemon", receipts[0].Authority);
Assert.AreEqual(AetheriaRuntimeDaemonSchemas.CommittedCommandFact, receipts[0].Schema);
Assert.IsFalse(tracker.HasPending);
Assert.AreEqual(0, tracker.Reconcile(new[] { fact }, 11).Count);
}
[Test]
public void TickRunnerAppliesObservedCommandsAndPublishesFrame()
{
@ -553,7 +591,7 @@ public class DaemonRuntimeDocumentTests
advertisedGameSurface.WorldInteraction.ProjectionKind);
Assert.AreEqual("aetheria.daemon.commands", advertisedGameSurface.WorldInteraction.CommandBoundary);
Assert.AreEqual(
AetheriaRuntimeDaemonSchemas.EveCommandAcceptanceStatus,
AetheriaRuntimeDaemonSchemas.CommittedCommandFact,
advertisedGameSurface.WorldInteraction.ReceiptSchema);
Assert.AreEqual(
"provider-owns-world-state-assets-command-acceptance-and-receipts",
@ -2970,7 +3008,7 @@ public class DaemonRuntimeDocumentTests
"aetheria.daemon.commands",
liveBridge.CurrentSurfaceDocument.AdvertisedSurface.WorldInteraction.CommandBoundary);
Assert.AreEqual(
AetheriaRuntimeDaemonSchemas.EveCommandAcceptanceStatus,
AetheriaRuntimeDaemonSchemas.CommittedCommandFact,
liveBridge.CurrentSurfaceDocument.AdvertisedSurface.WorldInteraction.ReceiptSchema);
Assert.AreEqual(1, presentation.ActiveEntities);
Assert.AreEqual(1, sceneSink.Upserts.Count);
@ -2978,7 +3016,7 @@ public class DaemonRuntimeDocumentTests
Assert.IsNotNull(runtime.ActiveProjection);
Assert.AreEqual("provider-authored-world-surface", runtime.ActiveProjection.ProjectionKind);
Assert.AreEqual("aetheria.daemon.commands", runtime.ActiveProjection.CommandBoundary);
Assert.AreEqual(AetheriaRuntimeDaemonSchemas.EveCommandAcceptanceStatus, runtime.ActiveProjection.ReceiptSchema);
Assert.AreEqual(AetheriaRuntimeDaemonSchemas.CommittedCommandFact, runtime.ActiveProjection.ReceiptSchema);
Assert.AreEqual(
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest.ToString(),
runtime.ActiveWorld.StatePointerId);
@ -3014,7 +3052,7 @@ public class DaemonRuntimeDocumentTests
Assert.AreEqual("aetheria.daemon", moveIntent.ProviderId);
Assert.AreEqual(AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId, moveIntent.SurfaceId);
Assert.AreEqual("aetheria.daemon.commands", moveIntent.CommandBoundary);
Assert.AreEqual(AetheriaRuntimeDaemonSchemas.EveCommandAcceptanceStatus, moveIntent.ReceiptSchema);
Assert.AreEqual(AetheriaRuntimeDaemonSchemas.CommittedCommandFact, moveIntent.ReceiptSchema);
Assert.AreEqual(
AetheriaRuntimeDaemonSurfaceCommandCatalog.CommandName(AetheriaRuntimeDaemonCommandKinds.SetMoveVector),
moveIntent.Payload.GetString("commandId"));

View file

@ -11,6 +11,12 @@ through CultMesh, and forward writes to the daemon's primary shard. The generic
EveUnity PlayMode client must compose that remote path before this package can
claim live daemon playability.
Remote command completion is derived only from provider-owned
`gamecult.aetheria.committed_command_fact.v1` documents. The adapter correlates
`CommandId` and waits until the synchronized game surface version reaches the
fact's `SourceFrameId` before emitting a terminal EveUnity receipt. Submission
success is never treated as command acceptance.
`AetheriaEveSurfacePresenter` is a typed surface document lowerer:
- it resolves the active client target from `GameData/aetheria-client.cc`;

View file

@ -28,6 +28,8 @@ namespace GameCult.Aetheria.EveRuntime
private AetheriaRuntimeStateBootReport? _stateBoot;
private AetheriaClientState? _runtimeState;
private AetheriaClient? _remoteClient;
private readonly AetheriaEveUnityRemoteReceiptTracker _remoteReceipts =
new AetheriaEveUnityRemoteReceiptTracker();
private event Action<EveUnityPlayableWorldAssetManifestDocument>? AssetManifestDocumentAvailable;
public AetheriaEveUnitySceneProviderBridge(
@ -144,6 +146,7 @@ namespace GameCult.Aetheria.EveRuntime
CurrentDocumentManifest = ReadAssetManifest(runtimeState);
AssetManifestDocumentAvailable?.Invoke(CurrentDocumentManifest);
ReconcileRemoteReceipts();
}
public void Submit(EveSurfaceCommandRequest request)
@ -158,6 +161,7 @@ namespace GameCult.Aetheria.EveRuntime
Debug.Log(
$"Forwarded Aetheria operation to remote CultMesh authority: {submitted!.Kind} {submitted.CommandId}");
_remoteReceipts.Track(submitted.CommandId, request);
return;
}
@ -189,6 +193,7 @@ namespace GameCult.Aetheria.EveRuntime
{
_remoteClient?.Dispose();
_remoteClient = null;
_remoteReceipts.Clear();
}
private bool IsRemote => !string.IsNullOrWhiteSpace(_cultMeshEndpoint);
@ -213,6 +218,17 @@ namespace GameCult.Aetheria.EveRuntime
return _remoteClient;
}
private void ReconcileRemoteReceipts()
{
if (!IsRemote || !_remoteReceipts.HasPending || _remoteClient == null)
return;
foreach (var receipt in _remoteReceipts.Reconcile(
_remoteClient.CommittedCommandFacts(),
CurrentDocument.Version))
ReceiptAvailable?.Invoke(receipt);
}
private AetheriaRuntimeStateBootReport ResolveStateBoot()
{
var stateBoot = AetheriaEveRuntimeUnityHooks.RequireStateBoot(_stateFilePathOverride);
@ -378,4 +394,65 @@ namespace GameCult.Aetheria.EveRuntime
: asset.AssetKey.Replace('.', '/');
}
}
public sealed class AetheriaEveUnityRemoteReceiptTracker
{
private readonly Dictionary<string, EveSurfaceCommandRequest> _pending =
new Dictionary<string, EveSurfaceCommandRequest>(StringComparer.Ordinal);
public bool HasPending => _pending.Count > 0;
public void Track(string commandId, EveSurfaceCommandRequest request)
{
if (string.IsNullOrWhiteSpace(commandId))
throw new ArgumentException("Remote command id must be non-empty.", nameof(commandId));
_pending[commandId] = request ?? throw new ArgumentNullException(nameof(request));
}
public IReadOnlyList<EveUnitySceneCommandReceipt> Reconcile(
IReadOnlyList<AetheriaRuntimeCommittedCommandFactDocument> facts,
long observedSurfaceVersion)
{
if (facts == null) throw new ArgumentNullException(nameof(facts));
var receipts = new List<EveUnitySceneCommandReceipt>();
var byCommandId = facts
.Where(fact => fact != null && !string.IsNullOrWhiteSpace(fact.CommandId))
.GroupBy(fact => fact.CommandId, StringComparer.Ordinal)
.ToDictionary(
group => group.Key,
group => group.OrderByDescending(fact => fact.SourceFrameId).First(),
StringComparer.Ordinal);
foreach (var commandId in _pending.Keys.ToArray())
{
if (!byCommandId.TryGetValue(commandId, out var fact) ||
observedSurfaceVersion < fact.SourceFrameId)
continue;
var request = _pending[commandId];
_pending.Remove(commandId);
var accepted = string.Equals(
fact.Outcome,
AetheriaRuntimeCommandFactOutcomes.Applied,
StringComparison.Ordinal);
receipts.Add(new EveUnitySceneCommandReceipt(
fact.FactId,
request.Operation?.OperationId ?? "",
commandId,
accepted ? "accepted" : "denied",
"Aetheria",
fact.SourceDaemonId,
AetheriaRuntimeDaemonSchemas.CommittedCommandFact,
request.ProviderId,
request.SurfaceId,
accepted ? "Command applied by authoritative daemon." : "Command rejected by authoritative daemon.",
DateTimeOffset.TryParse(fact.CommittedAtUtc, out var committedAt) ? committedAt : request.IssuedAt));
}
return receipts;
}
public void Clear()
{
_pending.Clear();
}
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using GameCult.Eve.Surface;
@ -76,6 +77,12 @@ namespace GameCult.Aetheria.State.Verse
return _verse.RefreshRemoteAsync();
}
public IReadOnlyList<AetheriaRuntimeCommittedCommandFactDocument> CommittedCommandFacts()
{
ThrowIfDisposed();
return _verse.CommittedCommandFacts();
}
public static Task<AetheriaClient> OpenLocalAsync(
DirectoryInfo gameDataDirectory,
string runtimeId,

View file

@ -202,7 +202,7 @@ namespace GameCult.Aetheria.State.Verse
"tui"
},
CommandBoundary = "aetheria.daemon.commands",
ReceiptSchema = AetheriaRuntimeDaemonSchemas.EveCommandAcceptanceStatus,
ReceiptSchema = AetheriaRuntimeDaemonSchemas.CommittedCommandFact,
Ownership = "provider-owns-world-state-assets-command-acceptance-and-receipts"
};
}

View file

@ -151,7 +151,8 @@ namespace GameCult.Aetheria.State.Verse
typeof(AetheriaRuntimeSurfaceDocument),
typeof(EveSurfaceDocument),
typeof(AetheriaRuntimeDaemonCommandDocument),
typeof(AetheriaRuntimeEveCommandDocument)
typeof(AetheriaRuntimeEveCommandDocument),
typeof(AetheriaRuntimeCommittedCommandFactDocument)
};
public static CultDocumentRegistry CreateCultCacheRegistry()
@ -1053,6 +1054,12 @@ namespace GameCult.Aetheria.State.Verse
return AetheriaRuntimeCatalogStore.OpenReadOnly(StatePath);
}
public IReadOnlyList<AetheriaRuntimeCommittedCommandFactDocument> CommittedCommandFacts()
{
ThrowIfDisposed();
return Cache.GetAll<AetheriaRuntimeCommittedCommandFactDocument>().ToArray();
}
private AetheriaRuntimeLoadoutTemplatesDocument BootstrapLoadoutTemplatesDocument()
{
return new AetheriaRuntimeLoadoutTemplatesDocument(

View file

@ -1491,6 +1491,10 @@ First Aetheria surfaces to publish:
the local `.cc` scene adapter is no longer described as a live daemon
transport. The remaining proof is a separately running daemon, observed
receipts, and PlayMode reconciliation in an EveUnity-owned client.
- Done: publish committed facts for every applied or rejected daemon command,
including operation-level rejection. The remote Unity adapter correlates
those facts by command ID and emits a terminal receipt only after the
synchronized game surface reaches the fact's source frame.
6. Eve UI
- Done: publish the typed catalog operator surface from `Aetheria.State`.