Keep embedded surfaces out of base finality

This commit is contained in:
Meta Vens 2026-08-19 09:19:04 +02:00
commit 44923edc22
4 changed files with 63 additions and 15 deletions

View file

@ -183,4 +183,6 @@ its exact receipt lease alive, makes the obsolete surface read-only, and emits
the terminal receipt only after that surface generation and its qualified asset
catalog have committed together. Asset preparation failure therefore cannot
leave an accepted Verse switch paired with actionable controls from the prior
Verse.
Verse. Embedded surfaces may recompose the visible tree, but their versions are
derived rendering inputs and cannot satisfy or clear the advertised base
surface's finality barrier.

View file

@ -71,6 +71,7 @@ namespace GameCult.Eve.UnityScene
private PendingAssetCatalogUpdate? _pendingAssetCatalog;
private bool _assetCatalogUpdateRunning;
private long _surfaceAssetGeneration;
private long _mountedBaseSurfaceVersion;
private long _presentationBarrierVersion = -1;
private long _lastQueuedEntityViewEpoch = -1;
private long _lastQueuedEntityViewSequence = -1;
@ -905,11 +906,13 @@ namespace GameCult.Eve.UnityScene
private void PublishBaseSurface(EveSurfaceDocument surface)
{
if (surface == null) throw new ArgumentNullException(nameof(surface));
if (_baseSurface != null && surface.Version <= MountedBaseSurfaceVersion)
return;
var generation = Interlocked.Increment(ref _surfaceAssetGeneration);
if (!_bootstrapped)
{
_baseSurface = surface;
PublishComposedSurface();
PublishComposedSurface(surface.Version);
return;
}
@ -917,7 +920,7 @@ namespace GameCult.Eve.UnityScene
if (string.Equals(source.Identity, CurrentAssetSourceIdentity, StringComparison.Ordinal))
{
_baseSurface = surface;
PublishComposedSurface();
PublishComposedSurface(surface.Version);
return;
}
@ -937,10 +940,11 @@ namespace GameCult.Eve.UnityScene
PublishComposedSurface();
}
private void PublishComposedSurface()
private void PublishComposedSurface(long? committedBaseVersion = null)
{
ApplyPreparedSurface(PrepareComposedSurface(
_baseSurface ?? throw new InvalidOperationException("The base Eve surface is unavailable.")));
_baseSurface ?? throw new InvalidOperationException("The base Eve surface is unavailable.")),
committedBaseVersion);
}
private PreparedSurface PrepareComposedSurface(EveSurfaceDocument baseSurface)
@ -969,14 +973,15 @@ namespace GameCult.Eve.UnityScene
return new PreparedSurface(document, continuousCommands);
}
private void ApplyPreparedSurface(PreparedSurface prepared)
private void ApplyPreparedSurface(PreparedSurface prepared, long? committedBaseVersion = null)
{
CurrentSurfaceDocument = prepared.Document;
_continuousCommandIds.Clear();
foreach (var command in prepared.ContinuousCommands)
_continuousCommandIds.Add(command);
SurfaceDocumentAvailable?.Invoke(CurrentSurfaceDocument);
CompletePresentationTransition(CurrentSurfaceDocument.Version);
if (committedBaseVersion.HasValue)
CompletePresentationTransition(committedBaseVersion.Value);
}
internal static EveSurfaceDocument ComposeSurface(
@ -1463,7 +1468,7 @@ namespace GameCult.Eve.UnityScene
_baseSurface = surface;
try
{
ApplyPreparedSurface(prepared);
ApplyPreparedSurface(prepared, surface?.Version);
}
finally
{
@ -2000,28 +2005,30 @@ namespace GameCult.Eve.UnityScene
{
lock (_presentationFinalityGate)
{
if (_presentationBarrierVersion > CurrentSurfaceDocument.Version)
if (_presentationBarrierVersion > _mountedBaseSurfaceVersion)
throw new InvalidOperationException(
$"The Eve presentation is awaiting provider surface version {_presentationBarrierVersion}; " +
$"mounted version {CurrentSurfaceDocument.Version} is read-only until that generation commits.");
$"mounted base version {_mountedBaseSurfaceVersion} is read-only until that generation commits.");
}
}
private void BeginPresentationTransition(long sourceVersion)
{
if (sourceVersion <= CurrentSurfaceDocument.Version)
return;
lock (_presentationFinalityGate)
{
if (sourceVersion <= _mountedBaseSurfaceVersion)
return;
_presentationBarrierVersion = Math.Max(_presentationBarrierVersion, sourceVersion);
}
}
private bool DeferTerminalReceiptUntilPresentation(EveCommandReceiptDocument receipt)
{
lock (_presentationFinalityGate)
{
if (receipt.SourceVersion > CurrentSurfaceDocument.Version)
if (receipt.SourceVersion > _mountedBaseSurfaceVersion)
_presentationBarrierVersion = Math.Max(_presentationBarrierVersion, receipt.SourceVersion);
if (_presentationBarrierVersion <= CurrentSurfaceDocument.Version)
if (_presentationBarrierVersion <= _mountedBaseSurfaceVersion)
return false;
_deferredTerminalReceipts[receipt.CommandId] = receipt;
return true;
@ -2033,6 +2040,7 @@ namespace GameCult.Eve.UnityScene
EveCommandReceiptDocument[] ready;
lock (_presentationFinalityGate)
{
_mountedBaseSurfaceVersion = Math.Max(_mountedBaseSurfaceVersion, committedVersion);
if (_presentationBarrierVersion > committedVersion)
return;
_presentationBarrierVersion = -1;
@ -2046,6 +2054,15 @@ namespace GameCult.Eve.UnityScene
FinalizeReceipt(receipt, terminal: true);
}
private long MountedBaseSurfaceVersion
{
get
{
lock (_presentationFinalityGate)
return _mountedBaseSurfaceVersion;
}
}
private void FinalizeReceipt(EveCommandReceiptDocument receipt, bool terminal)
{
if (terminal)

View file

@ -208,6 +208,7 @@ namespace GameCult.Eve.UnityScene.Tests
BindingFlags.NonPublic | BindingFlags.Instance)!;
type.GetField("_bootstrapped", BindingFlags.NonPublic | BindingFlags.Instance)!
.SetValue(transport, true);
complete.Invoke(transport, new object[] { 1L });
pending.Add(request.CommandId, request);
var observed = new List<EveUnitySceneCommandReceipt>();
transport.CommandReceiptAvailable += observed.Add;
@ -219,6 +220,7 @@ namespace GameCult.Eve.UnityScene.Tests
Assert.That(pending.ContainsKey(request.CommandId), Is.True);
Assert.That(observed, Is.Empty);
type.GetProperty("CurrentSurfaceDocument")!.SetValue(transport, SceneSurface(100));
var blocked = Assert.Throws<InvalidOperationException>(() =>
transport.SubmitCommand(Request("stale-launch", "launch")));
StringAssert.Contains("read-only", blocked!.Message);
@ -335,6 +337,33 @@ namespace GameCult.Eve.UnityScene.Tests
sourceVersion,
invocationHash: EveCommandInvocationHash.Compute(request));
private static EveUnitySceneProviderSurfaceDocument SceneSurface(long version)
{
var surface = new EveSurfaceDocument(
"test.provider",
"",
"",
version,
"",
new EveSurfaceTree(
"test.surface",
new EveSurfaceComponent(
"root",
"surface",
new Dictionary<string, string>(),
Array.Empty<EveSurfaceComponent>()),
Array.Empty<EveStyleToken>()),
Array.Empty<EveCommandTemplate>());
return new EveUnitySceneProviderSurfaceDocument(
surface,
new EveUnitySceneProviderSurfaceAdvertisement(
"test.surface",
"interactive-world",
new EveUnitySceneWorldInteraction("", "", EveCommandReceiptDocument.SchemaId, "")),
"test:surface",
version);
}
private static async Task AwaitOrCancel(Task task, CancellationToken cancellationToken)
{
var cancelled = Task.Delay(Timeout.Infinite, cancellationToken);

View file

@ -1,7 +1,7 @@
{
"name": "org.gamecult.eve.unity-scene",
"displayName": "Eve Unity Scene Lowering",
"version": "0.3.129",
"version": "0.3.130",
"unity": "6000.0",
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
"dependencies": {