Gate receipts on visible provider state

This commit is contained in:
Meta Vens 2026-07-10 04:03:09 +01:00
commit 2cd2f13bd1
6 changed files with 111 additions and 14 deletions

View file

@ -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"

View file

@ -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<EveProviderWitness>(),
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<string>(),
"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<EveAdvertisedCommand>());
var payload = CultDocumentMessagePackSerialization.Serialize(advertisement);
var decoded = CultDocumentMessagePackSerialization.Deserialize<EveProviderAdvertisementDocument>(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()
{

View file

@ -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

View file

@ -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<EveUnitySceneCommandReceipt> _pendingReceipts =
new List<EveUnitySceneCommandReceipt>();
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 =>

View file

@ -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));

View file

@ -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",