From 78f49376703029aae66831a756542ab5a2391c78 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 19 Jul 2026 05:56:09 +0100 Subject: [PATCH] Lower exact hot body state in generic Unity clients --- .../EveUnityCultMeshLiveProviderTransport.cs | 124 +++++++++++------- .../Runtime/EveUnityBeamPresentation.cs | 2 + .../EveUnityBeamPresentationRenderer.cs | 9 +- .../Runtime/EveUnityEntitySoaPresenter.cs | 7 +- .../Runtime/EveUnityEntitySoaView.cs | 7 + .../EveUnityPlayableWorldLiveClient.cs | 10 +- .../Runtime/EveUnityPlayableWorldRuntime.cs | 5 +- .../Runtime/EveUnityPresentedEntities.cs | 8 +- .../Editor/EveUnityBeamPresentationTests.cs | 45 ++++++- .../EveUnitySceneSurfaceLowererTests.cs | 42 +++++- .../org.gamecult.eve.unity-scene/package.json | 4 +- 11 files changed, 204 insertions(+), 59 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index d95db2b..3184b9a 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -68,10 +68,13 @@ namespace GameCult.Eve.UnityScene private EveAdvertisedSurface? _advertisedSurface; private CultMeshBodyPublicationResolver? _bodyResolver; private EveEntitySoaViewDocument? _latestEntityLayout; + private CultMeshBodyPublicationDocument? _latestEntityPublication; private CultMeshMappedFrameBodyCursor? _mappedEntityFrameCursor; private CultMeshBodyDescriptor? _mappedEntityFrameContract; private long _lastQueuedEntityViewEpoch = -1; private long _lastQueuedEntityViewSequence = -1; + private long _lastPresentedEntityViewEpoch = -1; + private long _lastPresentedEntityViewSequence = -1; private bool _disposed; public EveUnityCultMeshLiveProviderTransport( @@ -192,8 +195,8 @@ namespace GameCult.Eve.UnityScene ThrowIfSubscriptionFailed(_subscriptions); while (_liveDocuments.TryDequeue(out var document)) { - if (document is EveEntitySoaViewDocument entityView) - PublishEntityView(entityView); + if (document is PendingEntityGeneration entityGeneration) + PublishEntityView(entityGeneration.View, entityGeneration.Publication); else if (document is EveFieldsSplatsDocument fields) FieldsSplatsAvailable?.Invoke(fields); else if (document is EveSurfaceDocument surface) @@ -217,17 +220,14 @@ namespace GameCult.Eve.UnityScene { TraceHotState($"layout epoch={document.ProducerEpoch} sequence={document.Sequence} identities={document.Identities?.Length ?? 0}"); _latestEntityLayout = document; - if (document.ProducerEpoch < _lastQueuedEntityViewEpoch || - (document.ProducerEpoch == _lastQueuedEntityViewEpoch && - document.Sequence <= _lastQueuedEntityViewSequence)) - return; - _lastQueuedEntityViewEpoch = document.ProducerEpoch; - _lastQueuedEntityViewSequence = document.Sequence; - _liveDocuments.Enqueue(document); + var publication = _latestEntityPublication; + if (publication != null && PublicationMatchesLayout(publication, document)) + QueueEntityGeneration(document, publication); } private void QueueBodyPublication(CultMeshBodyPublicationDocument publication) { + _latestEntityPublication = publication; var layout = _latestEntityLayout; if (layout == null || layout.Buffers == null || layout.Buffers.Length == 0 || !string.Equals(layout.Buffers[0].BufferId, publication.BodyId, StringComparison.Ordinal)) @@ -238,10 +238,43 @@ namespace GameCult.Eve.UnityScene TraceHotState($"publication body={publication.BodyId} epoch={publication.ProducerEpoch} sequence={publication.Sequence} plane={string.Join(",", publication.Representations.Select(value => value.TransportKind))}"); - QueueEntityView(MaterializeEntityGeneration( - layout, - publication.ProducerEpoch, - publication.Sequence)); + var generation = PublicationMatchesLayout(publication, layout) + ? layout + : MaterializeEntityGeneration(layout, publication.ProducerEpoch, publication.Sequence); + QueueEntityGeneration(generation, publication); + } + + private void QueueEntityGeneration( + EveEntitySoaViewDocument document, + CultMeshBodyPublicationDocument publication) + { + if (document.ProducerEpoch < _lastQueuedEntityViewEpoch || + (document.ProducerEpoch == _lastQueuedEntityViewEpoch && + document.Sequence <= _lastQueuedEntityViewSequence)) + return; + _lastQueuedEntityViewEpoch = document.ProducerEpoch; + _lastQueuedEntityViewSequence = document.Sequence; + _liveDocuments.Enqueue(new PendingEntityGeneration(document, publication)); + } + + private static bool PublicationMatchesLayout( + CultMeshBodyPublicationDocument publication, + EveEntitySoaViewDocument layout) => + publication.ProducerEpoch == layout.ProducerEpoch && + publication.Sequence == layout.Sequence; + + private sealed class PendingEntityGeneration + { + public PendingEntityGeneration( + EveEntitySoaViewDocument view, + CultMeshBodyPublicationDocument publication) + { + View = view; + Publication = publication; + } + + public EveEntitySoaViewDocument View { get; } + public CultMeshBodyPublicationDocument Publication { get; } } private static EveEntitySoaViewDocument MaterializeEntityGeneration( @@ -530,7 +563,9 @@ namespace GameCult.Eve.UnityScene return normalized.ToLowerInvariant(); } - private void PublishEntityView(EveEntitySoaViewDocument document) + private void PublishEntityView( + EveEntitySoaViewDocument document, + CultMeshBodyPublicationDocument publication) { ResolveAdvertisement(); if (!string.Equals(document.ProviderId, _advertisement!.ProviderId, StringComparison.Ordinal)) @@ -539,15 +574,13 @@ namespace GameCult.Eve.UnityScene if (document.Buffers == null || document.Buffers.Length == 0) throw new InvalidOperationException("Entity layout does not name a primary logical buffer."); var handle = BodyPublicationHandle(document); - var publicationKey = CultMeshBodyPublicationDocument.CreateLatestRecordKey(handle.BodyId); - var publication = _node!.Database.Cache.Get(publicationKey) - ?? throw new InvalidOperationException( - $"Subscribed CultMesh body publication '{publicationKey.Value}' is missing for generation {handle.Sequence}."); handle.Validate(publication); var now = DateTimeOffset.UtcNow; if (!IsPublicationLive(publication, now)) return; EnsureMappedEntityFrameCursor(publication); + if (_mappedEntityFrameCursor != null) + return; _bodyResolver ??= CreateBodyResolver(); var lease = _bodyResolver.ResolveReadOnly(publication, new CultMeshBodyValidationRequest { @@ -569,6 +602,8 @@ namespace GameCult.Eve.UnityScene try { handler(document, lease); + _lastPresentedEntityViewEpoch = document.ProducerEpoch; + _lastPresentedEntityViewSequence = document.Sequence; } catch { @@ -616,16 +651,14 @@ namespace GameCult.Eve.UnityScene return; var descriptor = lease.Descriptor; - if (descriptor.ProducerEpoch < _lastQueuedEntityViewEpoch || - (descriptor.ProducerEpoch == _lastQueuedEntityViewEpoch && - descriptor.Sequence <= _lastQueuedEntityViewSequence)) + if (descriptor.ProducerEpoch < _lastPresentedEntityViewEpoch || + (descriptor.ProducerEpoch == _lastPresentedEntityViewEpoch && + descriptor.Sequence <= _lastPresentedEntityViewSequence)) { lease.Dispose(); return; } - _lastQueuedEntityViewEpoch = descriptor.ProducerEpoch; - _lastQueuedEntityViewSequence = descriptor.Sequence; var generation = MaterializeEntityGeneration( layout, descriptor.ProducerEpoch, @@ -639,6 +672,8 @@ namespace GameCult.Eve.UnityScene try { handler(generation, lease); + _lastPresentedEntityViewEpoch = descriptor.ProducerEpoch; + _lastPresentedEntityViewSequence = descriptor.Sequence; } catch { @@ -728,33 +763,22 @@ namespace GameCult.Eve.UnityScene .Where(value => !string.IsNullOrWhiteSpace(value)) .Distinct(StringComparer.Ordinal) .ToArray(); - var liveRecordKeys = new List(fieldRefs); - var liveSchemaIds = new List(); - if (fieldRefs.Length > 0) - liveSchemaIds.Add(EveFieldsSchemas.Splats); if (!string.IsNullOrWhiteSpace(entityViewPointer)) { if (string.IsNullOrWhiteSpace(entityBodyId)) throw new InvalidOperationException( "The playable world advertises an entity-view pointer without its logical body id."); - liveRecordKeys.Insert(0, entityViewPointer); - liveRecordKeys.Insert(1, CultMeshBodyPublicationDocument.CreateLatestRecordKey(entityBodyId).Value); - liveSchemaIds.Insert(0, EveEntitySoaViewDocument.SchemaId); - liveSchemaIds.Insert(1, CultMeshBodyPublicationSchemaVersions.Publication); - } - if (liveRecordKeys.Count > 0) - { - var initial = _entitySubscriptions.SubscribeAsync( - "eve-unity-entity-view", - recordKeys: liveRecordKeys, - schemaIds: liveSchemaIds, - consumerRuntimeId: _runtimeId, - bodyIds: string.IsNullOrWhiteSpace(entityBodyId) ? null : new[] { entityBodyId }, - supportedBodyTransports: string.IsNullOrWhiteSpace(entityBodyId) ? null : new[] - { - CultMeshBodyTransportKind.SharedMemory.ToString(), - CultMeshBodyTransportKind.Network.ToString() - }) + var initial = CultMesh.SubscribeHotBodyAsync( + _entitySubscriptions, + _bodyResolver!, + new CultMeshHotBodySubscription( + "eve-unity-entity-view", + _runtimeId, + entityViewPointer, + EveEntitySoaViewDocument.SchemaId, + entityBodyId, + fieldRefs, + fieldRefs.Length > 0 ? new[] { EveFieldsSchemas.Splats } : null)) .GetAwaiter().GetResult(); TraceHotState($"initial subscription documents=[{string.Join(",", initial.Select(value => value.GetType().Name))}]"); foreach (var document in initial) @@ -767,6 +791,16 @@ namespace GameCult.Eve.UnityScene _liveDocuments.Enqueue(fields); } } + else if (fieldRefs.Length > 0) + { + var initial = _entitySubscriptions.SubscribeAsync( + "eve-unity-fields", + recordKeys: fieldRefs, + schemaIds: new[] { EveFieldsSchemas.Splats }) + .GetAwaiter().GetResult(); + foreach (var fields in initial.OfType()) + _liveDocuments.Enqueue(fields); + } _subscriptions.SubscribeAsync( "eve-unity-surface", recordKeys: new[] { _advertisedSurface!.RecordRef }, diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs index fc11c16..6a0e5a5 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs @@ -21,6 +21,7 @@ namespace GameCult.Eve.UnityScene RenderChannel = Get(node, "renderChannel"); ActivationActionId = Get(node, "activationActionId"); Power = NonNegative(node, "power", 0f); + PowerStateSemantic = Get(node, "powerStateSemantic"); ActivationThreshold = NonNegative(node, "activationThreshold", 0.01f); Radius = NonNegative(node, "radius", 0f); MaximumDistance = NonNegative(node, "maximumDistance", 0f); @@ -33,6 +34,7 @@ namespace GameCult.Eve.UnityScene public string RenderChannel { get; } public string ActivationActionId { get; } public float Power { get; } + public string PowerStateSemantic { get; } public float ActivationThreshold { get; } public float Radius { get; } public float MaximumDistance { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs index 4265b4c..3a28964 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs @@ -76,8 +76,13 @@ namespace GameCult.Eve.UnityScene beam.Root.transform.SetParent(source.Transform, false); } - var visiblePower = presentation.Power > presentation.ActivationThreshold - ? presentation.Power + var power = presentation.Power; + if (!string.IsNullOrWhiteSpace(presentation.PowerStateSemantic)) + power = source.Entity.TryGetScalar(presentation.PowerStateSemantic, out var livePower) + ? Math.Max(0f, livePower) + : 0f; + var visiblePower = power > presentation.ActivationThreshold + ? power : 0f; foreach (var particles in beam.Root.GetComponentsInChildren(true)) { diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs index c6ad595..d16691a 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs @@ -47,6 +47,10 @@ namespace GameCult.Eve.UnityScene var assetRef = string.IsNullOrWhiteSpace(identity?.AssetRef) ? renderGroup?.MeshAssetRef ?? "" : identity!.AssetRef; + var scalarState = new Dictionary(StringComparer.Ordinal); + foreach (var semantic in view.FloatSemantics) + if (view.TryReadFloat(semantic, row, out var scalar)) + scalarState[semantic] = scalar; var fact = new EveUnityPresentedEntity( entityIndex, entityId, @@ -62,7 +66,8 @@ namespace GameCult.Eve.UnityScene lod, identity?.Selectable ?? true, identity?.Controllable ?? false, - assetRef); + assetRef, + scalarState); presented.Add(fact); if (_sink is IEveUnityEntityGenerationSink) continue; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs index 8aab4de..5475ddf 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs @@ -14,6 +14,7 @@ namespace GameCult.Eve.UnityScene private readonly ICultMeshBodyReadLease _lease; private readonly Dictionary _buffers; private readonly Dictionary _columns; + private readonly string[] _floatSemantics; private EveUnityEntitySoaView(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) { @@ -21,12 +22,18 @@ namespace GameCult.Eve.UnityScene _lease = lease; _buffers = document.Buffers.ToDictionary(buffer => buffer.BufferId, StringComparer.Ordinal); _columns = document.Columns.ToDictionary(column => column.Semantic, StringComparer.Ordinal); + _floatSemantics = document.Columns + .Where(column => string.Equals(column.ScalarType, "float32", StringComparison.Ordinal)) + .Select(column => column.Semantic) + .Where(semantic => !string.IsNullOrWhiteSpace(semantic)) + .ToArray(); } public EveEntitySoaViewDocument Document { get; } public long Generation => Document.Sequence; public IReadOnlyList RenderGroups => Document.RenderGroups; public int EntityCount => Document.Columns.Length == 0 ? 0 : Document.Columns.Min(column => column.ElementCount); + public IReadOnlyList FloatSemantics => _floatSemantics; public static EveUnityEntitySoaView Open(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) { diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs index 9840feb..41ca20f 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs @@ -15,6 +15,7 @@ namespace GameCult.Eve.UnityScene private readonly EveUnityShotReceiptPresenter _shots = new EveUnityShotReceiptPresenter(); private readonly List _pendingReceipts = new List(); + private long _activeStateVersion; private bool _connected; private bool _receiptConnected; @@ -36,7 +37,14 @@ namespace GameCult.Eve.UnityScene public EveUnitySceneCommandReceipt? LastReceipt { get; private set; } - public long ActiveVersion => _connection.ActiveVersion; + public long ActiveVersion => Math.Max(_connection.ActiveVersion, _activeStateVersion); + + public void AdvanceStateVersion(long version) + { + if (version <= _activeStateVersion) return; + _activeStateVersion = version; + PublishReceiptsWhoseStateIsVisible(); + } public string SourcePointer => _connection.SourcePointer; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs index 36b034a..94ca473 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs @@ -230,8 +230,11 @@ namespace GameCult.Eve.UnityScene _entityViewsConnected = true; } - private void OnEntityViewAvailable(EveEntitySoaViewDocument document, GameCult.Mesh.ICultMeshBodyReadLease lease) => + private void OnEntityViewAvailable(EveEntitySoaViewDocument document, GameCult.Mesh.ICultMeshBodyReadLease lease) + { + _client.AdvanceStateVersion(document.FrameId); _entityPresenter.Apply(document, lease); + } } public sealed class EveUnityLivePlayableWorldAssetProvider : diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs index e34fff4..cb03b14 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs @@ -24,7 +24,8 @@ namespace GameCult.Eve.UnityScene int lod, bool selectable, bool controllable, - string assetRef) + string assetRef, + IReadOnlyDictionary? scalarState = null) { SourceIndex = sourceIndex; EntityId = entityId ?? ""; @@ -41,6 +42,7 @@ namespace GameCult.Eve.UnityScene Selectable = selectable; Controllable = controllable; AssetRef = assetRef ?? ""; + ScalarState = scalarState ?? new Dictionary(StringComparer.Ordinal); } public int SourceIndex { get; } @@ -58,6 +60,10 @@ namespace GameCult.Eve.UnityScene public bool Selectable { get; } public bool Controllable { get; } public string AssetRef { get; } + public IReadOnlyDictionary ScalarState { get; } + + public bool TryGetScalar(string semantic, out float value) => + ScalarState.TryGetValue(semantic ?? "", out value); public EveUnityPlayableWorldEntity ToPlayableWorldEntity() => new EveUnityPlayableWorldEntity( EntityId, diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs index 0af9e69..08186a3 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs @@ -76,6 +76,44 @@ namespace GameCult.Eve.UnityScene.Tests } } + [Test] + public void RendererReadsDaemonPowerFromThePresentedEntityGeneration() + { + var host = new GameObject("beam-state-test-host"); + var source = new GameObject("beam-state-test-source"); + var prefab = new GameObject("beam-state-test-prefab"); + prefab.AddComponent(); + var renderer = host.AddComponent(); + var registry = new Registry( + "source", + source.transform, + new Dictionary(StringComparer.Ordinal) { ["effect.beam.power"] = 0.75f }); + try + { + var presentation = new EveUnityBeamPresentation(Node( + "tractor", "beam.presentation", new Dictionary(StringComparer.Ordinal) + { + ["sourceEntityId"] = "source", + ["assetRole"] = "effect.beam.tractor", + ["directionMode"] = "source-forward.v1", + ["power"] = "0.125", + ["powerStateSemantic"] = "effect.beam.power", + ["activationThreshold"] = "0.01" + })); + + renderer.Apply(new[] { presentation }, registry, new Provider(prefab)); + + Assert.That(renderer.TryGetPower("tractor", out var power), Is.True); + Assert.That(power, Is.EqualTo(0.75f).Within(0.0001f)); + } + finally + { + UnityEngine.Object.DestroyImmediate(host); + UnityEngine.Object.DestroyImmediate(source); + UnityEngine.Object.DestroyImmediate(prefab); + } + } + private static EveUnityBeamPresentation Presentation(float power) => new EveUnityBeamPresentation(Node( "tractor", "beam.presentation", new Dictionary(StringComparer.Ordinal) { @@ -104,11 +142,14 @@ namespace GameCult.Eve.UnityScene.Tests private sealed class Registry : IEveUnityPresentedEntityRegistry { private readonly EveUnityPresentedEntityHandle _handle; - public Registry(string entityId, Transform transform) + public Registry( + string entityId, + Transform transform, + IReadOnlyDictionary? scalarState = null) { _handle = new EveUnityPresentedEntityHandle( new EveUnityPresentedEntity(0, entityId, "ship", "", "", Vector3.zero, 0, Vector3.zero, - 1, 1, 0, 0, true, true, ""), + 1, 1, 0, 0, true, true, "", scalarState), transform); } public EveUnityPresentedEntityGeneration? CurrentGeneration => null; 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 ace3b58..acb9916 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -73,11 +73,11 @@ namespace GameCult.Eve.UnityScene.Tests .GetValue(transport)!; queueView.Invoke(transport, new object[] { view }); - Assert.That(queue.TryDequeue(out _), Is.True); + Assert.That(queue, Is.Empty, "a layout without its body publication is not a complete generation"); queuePublication.Invoke(transport, new object[] { publication }); Assert.That(queue.TryDequeue(out var queued), Is.True); - var generation = queued as EveEntitySoaViewDocument; + var generation = queued!.GetType().GetProperty("View")!.GetValue(queued) as EveEntitySoaViewDocument; Assert.That(generation, Is.Not.Null); Assert.That(generation!.Sequence, Is.EqualTo(publication.Sequence)); Assert.That(generation.ProducerEpoch, Is.EqualTo(publication.ProducerEpoch)); @@ -87,6 +87,34 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(generation.DirtyRanges.All(range => range.Sequence == publication.Sequence), Is.True); } + [Test] + public void LiveTransportPairsBodyPublicationThatArrivesBeforeItsLayout() + { + using var transport = new EveUnityCultMeshLiveProviderTransport( + "test-replica.cc", + "cultnet://127.0.0.1:3075", + "aetheria", + "aetheria.pilot"); + var view = EntityLeaseDocument(); + var publication = BodyPublication(view, view.Sequence); + var queueView = typeof(EveUnityCultMeshLiveProviderTransport) + .GetMethod("QueueEntityView", BindingFlags.NonPublic | BindingFlags.Instance)!; + var queuePublication = typeof(EveUnityCultMeshLiveProviderTransport) + .GetMethod("QueueBodyPublication", BindingFlags.NonPublic | BindingFlags.Instance)!; + var queue = (System.Collections.Concurrent.ConcurrentQueue) + typeof(EveUnityCultMeshLiveProviderTransport) + .GetField("_liveDocuments", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(transport)!; + + queuePublication.Invoke(transport, new object[] { publication }); + Assert.That(queue, Is.Empty, "a publication without its layout is not a complete generation"); + queueView.Invoke(transport, new object[] { view }); + + Assert.That(queue.TryDequeue(out var queued), Is.True); + Assert.That(queued!.GetType().GetProperty("View")!.GetValue(queued), Is.SameAs(view)); + Assert.That(queued.GetType().GetProperty("Publication")!.GetValue(queued), Is.SameAs(publication)); + } + [Test] public void LiveTransportReadsLaterMappedFramesWithoutNewControlDocuments() { @@ -1887,7 +1915,7 @@ namespace GameCult.Eve.UnityScene.Tests } [Test] - public void LivePlayableWorldClientWaitsForProviderSurfaceUpdateAfterReceipt() + public void LivePlayableWorldClientReconcilesReceiptAfterReactiveStateVersionAdvances() { var source = new FakeProviderSurfaceSource( "aetheria", @@ -1944,12 +1972,18 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(client.LastReceipt.State, Is.EqualTo("pending")); Assert.That(client.ActiveVersion, Is.EqualTo(1)); + client.AdvanceStateVersion(2); + + Assert.That(client.LastReceipt.State, Is.EqualTo("reconciled")); + Assert.That(client.ActiveVersion, Is.EqualTo(2)); + Assert.That(sceneSink.ConfiguredWorlds.Count, Is.EqualTo(1), + "reactive state visibility must not require republishing heavy surface topology"); + source.Publish(new EveUnitySceneProviderSurfaceSnapshot( PlayableArpgDocument(includeRaider: false, playerPosition: "30,0,30"), Advertisement("aetheria.daemon.game"), "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", 2)); - Assert.That(client.LastReceipt.State, Is.EqualTo("reconciled")); Assert.That(client.ActiveVersion, Is.EqualTo(2)); Assert.That(client.LastPresentation, Is.Not.Null); Assert.That(client.LastPresentation!.ActiveEntities, Is.EqualTo(2)); diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index fb59f96..56681f1 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,11 +1,11 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.77", + "version": "0.3.78", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.22", + "org.gamecult.cultlib": "1.0.23", "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.4", "com.unity.mathematics": "1.3.2",