From 2cd2f13bd1304d390eb1c36feb5e1b34a1ec89df Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 10 Jul 2026 04:03:09 +0100 Subject: [PATCH] Gate receipts on visible provider state --- ...veUnity.GenericClient.PlayModeTests.asmdef | 6 +++ .../PlayMode/GenericWorldCaptureTests.cs | 46 +++++++++++++++++++ docs/authority-map.md | 4 +- .../EveUnityPlayableWorldLiveClient.cs | 46 +++++++++++++++++-- .../EveUnitySceneSurfaceLowererTests.cs | 21 +++++---- .../eve-runtime-capability.json | 2 +- 6 files changed, 111 insertions(+), 14 deletions(-) diff --git a/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef b/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef index 8cf2339..a7c4dd2 100644 --- a/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef +++ b/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef @@ -2,11 +2,17 @@ "name": "GameCult.EveUnity.GenericClient.PlayModeTests", "rootNamespace": "GameCult.EveUnity.GenericClient.PlayModeTests", "references": [ + "GameCult.CultLib", "GameCult.Eve.Surface", "GameCult.Eve.UnityScene" ], "includePlatforms": [], "excludePlatforms": [], + "overrideReferences": true, + "precompiledReferences": [ + "GameCult.Caching.dll", + "GameCult.Caching.MessagePack.dll" + ], "autoReferenced": false, "optionalUnityReferences": [ "TestAssemblies" diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 719a960..2d0feda 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -2,6 +2,8 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; +using GameCult.Caching; +using GameCult.Caching.MessagePack; using GameCult.Eve.Surface; using GameCult.Eve.UnityScene; using NUnit.Framework; @@ -12,6 +14,50 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests { public sealed class GenericWorldCaptureTests { + [Test] + public void CoreProviderDocumentsRoundTripThroughCultCacheSerialization() + { + var advertisement = new EveProviderAdvertisementDocument( + "eve.world-smoke", + "eve-world-smoke", + "eve.local", + "Generic world", + "game.daemon", + "127.0.0.1:7777", + "2026-07-10T00:00:00Z", + new EveProviderFreshness("fresh", "2026-07-10T00:00:00Z", 5000), + new[] { EveSurfaceDocument.SchemaId, EveCommandReceiptDocument.SchemaId }, + Array.Empty(), + new[] + { + new EveAdvertisedSurface( + "eve.world-smoke.surface", + EveSurfaceDocument.SchemaId, + "surface:eve.world-smoke", + "cultmesh-record", + "active", + "interactive-world", + new EveWorldInteractionAdvertisement( + "provider-authored-world-surface", + Array.Empty(), + "world.commands", + "commands:eve.world-smoke", + EveCommandReceiptDocument.SchemaId, + "receipts:eve.world-smoke", + "assets:eve.world-smoke", + new[] { "unity-scene" }, + "provider-owns-world-state-command-acceptance-and-receipts")) + }, + Array.Empty()); + + var payload = CultDocumentMessagePackSerialization.Serialize(advertisement); + var decoded = CultDocumentMessagePackSerialization.Deserialize(payload); + + Assert.That(decoded.ProviderId, Is.EqualTo("eve.world-smoke")); + Assert.That(decoded.Surfaces.Count, Is.EqualTo(1)); + Assert.That(decoded.Surfaces[0].WorldInteraction.CommandRecordRef, Is.EqualTo("commands:eve.world-smoke")); + } + [UnityTest] public IEnumerator GenericClientRendersWorldSmokeAndEmitsCommand() { diff --git a/docs/authority-map.md b/docs/authority-map.md index 95a9f39..87a7442 100644 --- a/docs/authority-map.md +++ b/docs/authority-map.md @@ -21,7 +21,7 @@ drivers, asset-provider hooks, package release, runtime tests, and captures. ## Outputs - disposable Unity scene projection -- `gamecult.eve.command.v1` intents sent through the advertised boundary +- `gamecult.eve.command_invocation.v1` intents sent through the advertised boundary - runtime capability, test, release, and capture evidence - owner-controlled PlayMode capture from `TestProject` @@ -41,7 +41,7 @@ projection shells may not implement Sai, Norn, or TeX semantics. Initial connect, refresh, reconnect, and terminal-receipt reconciliation all consume provider surface documents. Movement, focus, target, and action input -all become `gamecult.eve.command.v1` through the provider-advertised command +all become `gamecult.eve.command_invocation.v1` through the provider-advertised command boundary. ## Cut Line diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs index 6a50694..8fc1a98 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using GameCult.Eve.Surface; #nullable enable @@ -11,6 +12,8 @@ namespace GameCult.Eve.UnityScene private readonly EveUnityPlayableWorldPresenter _presenter; private readonly IEveUnitySceneCommandReceiptSource? _receiptSource; private readonly bool _refreshOnTerminalReceipt; + private readonly List _pendingReceipts = + new List(); private bool _connected; private bool _receiptConnected; @@ -113,6 +116,7 @@ namespace GameCult.Eve.UnityScene } _connection.Disconnect(); + _pendingReceipts.Clear(); } public void Dispose() @@ -142,15 +146,47 @@ namespace GameCult.Eve.UnityScene private void OnProjectionUpdated(EveUnitySceneProjection projection) { LastPresentation = _presenter.Apply(projection); + PublishReceiptsWhoseStateIsVisible(); } private void OnReceiptAvailable(EveUnitySceneCommandReceipt receipt) { - LastReceipt = receipt ?? throw new ArgumentNullException(nameof(receipt)); - ReceiptAvailable?.Invoke(receipt); + if (receipt == null) throw new ArgumentNullException(nameof(receipt)); + + if (receipt.SourceVersion > ActiveVersion) + { + _pendingReceipts.Add(receipt); + if (_refreshOnTerminalReceipt && receipt.ShouldRefreshProviderSurface) + { + Refresh(); + PublishReceiptsWhoseStateIsVisible(); + } + return; + } if (_refreshOnTerminalReceipt && receipt.ShouldRefreshProviderSurface) Refresh(); + + PublishReceipt(receipt); + } + + private void PublishReceiptsWhoseStateIsVisible() + { + for (var index = _pendingReceipts.Count - 1; index >= 0; index--) + { + var receipt = _pendingReceipts[index]; + if (receipt.SourceVersion > ActiveVersion) + continue; + + _pendingReceipts.RemoveAt(index); + PublishReceipt(receipt); + } + } + + private void PublishReceipt(EveUnitySceneCommandReceipt receipt) + { + LastReceipt = receipt; + ReceiptAvailable?.Invoke(receipt); } private EveUnityPlayableWorldPresentation RequirePresentation() @@ -179,7 +215,8 @@ namespace GameCult.Eve.UnityScene string providerId = "", string surfaceId = "", string message = "", - DateTimeOffset? issuedAtUtc = null) + DateTimeOffset? issuedAtUtc = null, + long sourceVersion = 0) { ReceiptId = receiptId ?? ""; Command = command ?? ""; @@ -192,6 +229,7 @@ namespace GameCult.Eve.UnityScene SurfaceId = surfaceId ?? ""; Message = message ?? ""; IssuedAtUtc = issuedAtUtc; + SourceVersion = sourceVersion; } public string Schema { get; } @@ -216,6 +254,8 @@ namespace GameCult.Eve.UnityScene public DateTimeOffset? IssuedAtUtc { get; } + public long SourceVersion { get; } + public bool IsProviderOwned => !string.IsNullOrWhiteSpace(OwnerRepo) && !string.Equals(OwnerRepo, "EveUnity", StringComparison.Ordinal); public bool ShouldRefreshProviderSurface => diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 65b01d3..ffadd0f 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -818,12 +818,6 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(client.LastReceipt.IsProviderOwned, Is.True); Assert.That(sceneSink.ConfiguredWorlds.Count, Is.EqualTo(1)); - source.Stage(new EveUnitySceneProviderSurfaceSnapshot( - PlayableArpgDocument(includeRaider: false, playerPosition: "30,0,30"), - Advertisement("aetheria.daemon.game"), - "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", - 2)); - receiptSource.Publish(new EveUnitySceneCommandReceipt( "aetheria.daemon.move_intent.reconciled", "aetheria.daemon.commands", @@ -831,9 +825,20 @@ namespace GameCult.Eve.UnityScene.Tests "reconciled", "Aetheria", "AetheriaRuntimeDaemonCommandBoundaryDocument", - "aetheria.eve_command_acceptance_status.v1", + "gamecult.eve.command_receipt.v1", "aetheria", - "aetheria.daemon.game")); + "aetheria.daemon.game", + sourceVersion: 2)); + + Assert.That(client.LastReceipt.State, Is.EqualTo("pending")); + Assert.That(client.ActiveVersion, Is.EqualTo(1)); + + source.Stage(new EveUnitySceneProviderSurfaceSnapshot( + PlayableArpgDocument(includeRaider: false, playerPosition: "30,0,30"), + Advertisement("aetheria.daemon.game"), + "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", + 2)); + client.Refresh(); Assert.That(client.LastReceipt.State, Is.EqualTo("reconciled")); Assert.That(client.ActiveVersion, Is.EqualTo(2)); diff --git a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json index c0ce1e8..417228f 100644 --- a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json +++ b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json @@ -102,7 +102,7 @@ } ], "commandTransport": { - "schema": "gamecult.eve.command.v1", + "schema": "gamecult.eve.command_invocation.v1", "status": "provider-shell-contract-skeleton", "emitter": "EveUnitySceneSurfaceLowerer.CreateCommandIntent", "clientId": "unity-scene",