From 890692e30f8327806b0fc93d3b7d33be2e7a10ca Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 21:18:10 +0100 Subject: [PATCH] Expose immutable presented entity generations --- .../org.gamecult.eve.unity-scene/README.md | 6 + .../Runtime/EveUnityEntitySoaPresenter.cs | 55 +++++--- ...veUnityGameObjectPlayableWorldSceneSink.cs | 113 ++++++++++++++- .../Runtime/EveUnityPlayableWorldCameraRig.cs | 15 +- .../EveUnityPlayableWorldClientHost.cs | 1 + .../Runtime/EveUnityPlayableWorldRuntime.cs | 3 + .../Runtime/EveUnityPresentedEntities.cs | 122 ++++++++++++++++ .../Runtime/EveUnityPresentedEntities.cs.meta | 2 + .../EveUnitySceneSurfaceLowererTests.cs | 132 ++++++++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 4 +- 10 files changed, 420 insertions(+), 33 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs.meta diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index d93183b..12ca9f7 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -15,6 +15,12 @@ selection filters; they are not product knowledge required by the client. The package never imports provider product assemblies. +`IEveUnityPresentedEntityRegistry` exposes each committed SoA generation as +immutable entity facts plus presentation transforms, addressable by Eve entity +id or source index. Camera, selection, and provider UI adapters consume that +registry instead of reconstructing provider gameplay objects. It owns +presentation lookup only; simulation state and commands remain provider-owned. + Retained provider feedback is lowered without becoming Unity authority. `EveUnityFeedbackPresenter` emits each new `feedback.event` identity once, and `EveUnityShotReceiptPresenter` does the same for `shot.receipt` trajectories. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs index eea519b..c6ad595 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs @@ -27,6 +27,7 @@ namespace GameCult.Eve.UnityScene public void Apply(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) { using var view = EveUnityEntitySoaView.Open(document, lease); + var presented = new List(); var next = new HashSet(StringComparer.Ordinal); for (var row = 0; row < view.EntityCount; row++) { @@ -34,7 +35,10 @@ namespace GameCult.Eve.UnityScene if (view.TryReadByte("render.visibility", row, out var visible) && visible == 0) continue; if (!view.TryReadVector3("transform.position", row, out var position)) continue; view.TryReadFloat("transform.rotation.radians", row, out var rotationRadians); + view.TryReadVector3("transform.velocity", row, out var velocity); + view.TryReadFloat("physics.body.radius", row, out var radius); view.TryReadFloat("render.scale", row, out var scale); + view.TryReadInt32("render.lod", row, out var lod); view.TryReadUInt32("render.group.id", row, out var groupId); var renderGroup = view.RenderGroups.FirstOrDefault(group => group.GroupId == groupId); var identity = document.Identities.FirstOrDefault(item => item.Index == entityIndex); @@ -43,34 +47,43 @@ namespace GameCult.Eve.UnityScene var assetRef = string.IsNullOrWhiteSpace(identity?.AssetRef) ? renderGroup?.MeshAssetRef ?? "" : identity!.AssetRef; + var fact = new EveUnityPresentedEntity( + entityIndex, + entityId, + identity?.EntityKind ?? "entity", + string.IsNullOrWhiteSpace(identity?.Label) ? entityId : identity!.Label, + identity?.Faction ?? "", + position, + rotationRadians * 57.2957795f, + velocity, + radius, + scale > 0f ? scale : renderGroup?.DefaultScale ?? 1f, + groupId, + lod, + identity?.Selectable ?? true, + identity?.Controllable ?? false, + assetRef); + presented.Add(fact); + if (_sink is IEveUnityEntityGenerationSink) + continue; _sink.UpsertEntity( - new EveUnityPlayableWorldEntity( - entityId, - entityId, - identity?.EntityKind ?? "entity", - string.IsNullOrWhiteSpace(identity?.Label) ? entityId : identity!.Label, - identity?.Faction ?? "", - assetRef, - position.x, - position.y, - position.z, - rotationRadians * 57.2957795f, - scale > 0f ? scale : renderGroup?.DefaultScale ?? 1f, - selectable: identity?.Selectable ?? true, - controllable: identity?.Controllable ?? false, - focusCommand: "", - moveCommand: "", - targetCommand: "", - actionCommand: "", - props: new Dictionary()), + fact.ToPlayableWorldEntity(), new EveUnityPlayableWorldAssetBinding( assetRef, "entity", string.IsNullOrWhiteSpace(assetRef) ? "unity-generated-placeholder" : "provider-asset-ref")); } - foreach (var entityId in _known.Where(entityId => !next.Contains(entityId)).ToArray()) - _sink.RemoveEntity(entityId); + if (_sink is IEveUnityEntityGenerationSink generationSink) + { + generationSink.ApplyGeneration(new EveUnityPresentedEntityGeneration( + document.ViewId, document.ProducerEpoch, document.Sequence, presented)); + } + else + { + foreach (var entityId in _known.Where(entityId => !next.Contains(entityId)).ToArray()) + _sink.RemoveEntity(entityId); + } _known.Clear(); foreach (var entityId in next) _known.Add(entityId); } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs index 75a9696..0035a83 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using UnityEngine; #nullable enable @@ -16,11 +17,18 @@ namespace GameCult.Eve.UnityScene UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType); } - public sealed class EveUnityGameObjectPlayableWorldSceneSink : IEveUnityPlayableWorldSceneSink + public sealed class EveUnityGameObjectPlayableWorldSceneSink : + IEveUnityPlayableWorldSceneSink, + IEveUnityEntityGenerationSink, + IEveUnityPresentedEntityRegistry { private readonly Transform _root; private readonly IEveUnityGameObjectAssetProvider _assetProvider; private readonly Dictionary _instances = new Dictionary(StringComparer.Ordinal); + private IReadOnlyDictionary _presentedById = + new Dictionary(StringComparer.Ordinal); + private IReadOnlyDictionary _presentedByIndex = + new Dictionary(); public EveUnityGameObjectPlayableWorldSceneSink( Transform root, @@ -31,6 +39,13 @@ namespace GameCult.Eve.UnityScene } public int ActiveEntityCount => _instances.Count; + public EveUnityPresentedEntityGeneration? CurrentGeneration { get; private set; } + + public bool TryGetByEntityId(string entityId, out EveUnityPresentedEntityHandle handle) => + _presentedById.TryGetValue(entityId ?? "", out handle!); + + public bool TryGetBySourceIndex(int sourceIndex, out EveUnityPresentedEntityHandle handle) => + _presentedByIndex.TryGetValue(sourceIndex, out handle!); public void ConfigureWorld(EveUnityPlayableWorldProjection world) { @@ -66,6 +81,90 @@ namespace GameCult.Eve.UnityScene ApplyEntity(instance, entity, asset); } + public void ApplyGeneration(EveUnityPresentedEntityGeneration generation) + { + if (generation == null) throw new ArgumentNullException(nameof(generation)); + if (generation.Entities.Any(entity => string.IsNullOrWhiteSpace(entity.EntityId))) + throw new InvalidOperationException("Presented entity generations require stable entity ids."); + var factsById = generation.Entities.ToDictionary(entity => entity.EntityId, StringComparer.Ordinal); + var factsByIndex = generation.Entities.ToDictionary(entity => entity.SourceIndex); + GameObject? staging = null; + var created = new Dictionary(StringComparer.Ordinal); + var nextInstances = new Dictionary(StringComparer.Ordinal); + try + { + foreach (var fact in generation.Entities) + { + if (_instances.TryGetValue(fact.EntityId, out var existing) && existing != null) + { + nextInstances.Add(fact.EntityId, existing); + continue; + } + + staging ??= new GameObject($"{_root.name}.generation.{generation.ProducerEpoch}.{generation.Sequence}"); + if (staging.transform.parent == null) + { + staging.SetActive(false); + staging.transform.SetParent(_root, false); + } + var entity = fact.ToPlayableWorldEntity(); + var asset = new EveUnityPlayableWorldAssetBinding( + fact.AssetRef, + fact.EntityKind, + string.IsNullOrWhiteSpace(fact.AssetRef) ? "unity-generated-placeholder" : "provider-asset-ref"); + var instance = InstantiateEntity(entity, asset, staging.transform); + created.Add(fact.EntityId, instance); + nextInstances.Add(fact.EntityId, instance); + } + + foreach (var fact in generation.Entities) + { + var entity = fact.ToPlayableWorldEntity(); + var asset = new EveUnityPlayableWorldAssetBinding( + fact.AssetRef, + fact.EntityKind, + string.IsNullOrWhiteSpace(fact.AssetRef) ? "unity-generated-placeholder" : "provider-asset-ref"); + ApplyEntity(nextInstances[fact.EntityId], entity, asset); + } + + var nextById = factsById.ToDictionary( + pair => pair.Key, + pair => new EveUnityPresentedEntityHandle(pair.Value, nextInstances[pair.Key].transform), + StringComparer.Ordinal); + var nextByIndex = factsByIndex.ToDictionary( + pair => pair.Key, + pair => nextById[pair.Value.EntityId]); + + foreach (var pair in _instances) + if (!nextInstances.ContainsKey(pair.Key) && pair.Value != null) + DestroyInstance(pair.Value); + _instances.Clear(); + foreach (var pair in nextInstances) + { + if (created.ContainsKey(pair.Key)) + { + pair.Value.transform.SetParent(_root, true); + pair.Value.SetActive(true); + } + _instances.Add(pair.Key, pair.Value); + } + _presentedById = nextById; + _presentedByIndex = nextByIndex; + CurrentGeneration = generation; + } + catch + { + foreach (var instance in created.Values) + if (instance != null) DestroyInstance(instance); + throw; + } + finally + { + if (staging != null) + DestroyInstance(staging); + } + } + public void RemoveEntity(string entityId) { if (string.IsNullOrWhiteSpace(entityId)) @@ -80,14 +179,18 @@ namespace GameCult.Eve.UnityScene DestroyInstance(instance); } - private GameObject InstantiateEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset) + private GameObject InstantiateEntity( + EveUnityPlayableWorldEntity entity, + EveUnityPlayableWorldAssetBinding asset, + Transform? parent = null) { + parent ??= _root; var prefab = _assetProvider.ResolvePrefab(asset); GameObject instance; if (prefab != null) { instance = new GameObject(entity.EntityId); - instance.transform.SetParent(_root, false); + instance.transform.SetParent(parent, false); var visual = UnityEngine.Object.Instantiate(prefab, instance.transform); visual.transform.localPosition = Vector3.zero; visual.transform.localRotation = Quaternion.identity; @@ -97,8 +200,8 @@ namespace GameCult.Eve.UnityScene instance = GameObject.CreatePrimitive(PrimitiveType.Capsule); } - if (instance.transform.parent != _root) - instance.transform.SetParent(_root, false); + if (instance.transform.parent != parent) + instance.transform.SetParent(parent, false); return instance; } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index 996c5bb..ade131c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -53,7 +53,7 @@ namespace GameCult.Eve.UnityScene } var camera = cameraTransform != null ? cameraTransform : transform; - var player = FindPlayerMarker(resolvedHost, activeWorld.PlayerEntityId); + var player = FindPlayer(resolvedHost, activeWorld.PlayerEntityId); if (player == null) return false; @@ -65,7 +65,7 @@ namespace GameCult.Eve.UnityScene cameraComponent.cullingMask = cullingMask; } var bounds = CalculateVisualBounds(player); - var target = bounds?.center ?? player.transform.position; + var target = bounds?.center ?? player.position; var radius = bounds?.extents.magnitude ?? 0f; var verticalFov = cameraComponent != null ? cameraComponent.fieldOfView : 60f; var framingDistance = radius > 0f @@ -84,7 +84,7 @@ namespace GameCult.Eve.UnityScene return true; } - private static Bounds? CalculateVisualBounds(EveUnityPlayableWorldEntityMarker player) + private static Bounds? CalculateVisualBounds(Transform player) { var renderers = player.GetComponentsInChildren(includeInactive: false); Bounds? bounds = null; @@ -121,18 +121,23 @@ namespace GameCult.Eve.UnityScene return host; } - private static EveUnityPlayableWorldEntityMarker? FindPlayerMarker( + private static Transform? FindPlayer( EveUnityPlayableWorldClientHost host, string playerEntityId) { if (string.IsNullOrWhiteSpace(playerEntityId)) return null; + if (host.PresentedEntities?.CurrentGeneration != null) + return host.PresentedEntities.TryGetByEntityId(playerEntityId, out var presented) + ? presented.Transform + : null; + var markers = host.SceneRoot.GetComponentsInChildren(); foreach (var marker in markers) { if (marker != null && marker.EntityId == playerEntityId) - return marker; + return marker.transform; } return null; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index 2dfe724..60fefac 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -31,6 +31,7 @@ namespace GameCult.Eve.UnityScene public EveUnityPlayableWorldPresentation? LastPresentation => Runtime?.LastPresentation; public EveUnitySceneCommandReceipt? LastReceipt => Runtime?.LastReceipt; + public IEveUnityPresentedEntityRegistry? PresentedEntities => Runtime?.PresentedEntities; public long ActiveVersion => Runtime?.ActiveVersion ?? 0; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs index 5871920..a5d7e5c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs @@ -14,6 +14,7 @@ namespace GameCult.Eve.UnityScene private readonly EveUnityPlayableWorldAssetManifestDocumentSource? _assetManifestSource; private readonly IEveUnityEntitySoaViewDocumentSource? _entityViews; private readonly EveUnityEntitySoaPresenter _entityPresenter; + private readonly IEveUnityPresentedEntityRegistry? _presentedEntities; private bool _entityViewsConnected; private bool _assetManifestConnected; @@ -33,6 +34,7 @@ namespace GameCult.Eve.UnityScene AssetManifests = assetManifests ?? new EveUnityPlayableWorldAssetManifestCache(); _surfaceSource = new EveUnitySceneProviderSurfaceDocumentSource(surfaceDocuments); _entityViews = surfaceDocuments as IEveUnityEntitySoaViewDocumentSource; + _presentedEntities = sceneSink as IEveUnityPresentedEntityRegistry; _entityPresenter = new EveUnityEntitySoaPresenter(sceneSink); _connection = new EveUnitySceneProviderConnection(_surfaceSource, commandSink); _client = new EveUnityPlayableWorldLiveClient( @@ -47,6 +49,7 @@ namespace GameCult.Eve.UnityScene public EveUnityPlayableWorldAssetManifestCache AssetManifests { get; } public IEveUnityGameObjectAssetProvider? GameObjectAssetProvider { get; private set; } + public IEveUnityPresentedEntityRegistry? PresentedEntities => _presentedEntities; public EveUnityPlayableWorldProjection? ActiveWorld => _client.ActiveWorld; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs new file mode 100644 index 0000000..e34fff4 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public sealed class EveUnityPresentedEntity + { + public EveUnityPresentedEntity( + int sourceIndex, + string entityId, + string entityKind, + string label, + string faction, + Vector3 position, + float rotationYDegrees, + Vector3 velocity, + float radius, + float scale, + uint renderGroupId, + int lod, + bool selectable, + bool controllable, + string assetRef) + { + SourceIndex = sourceIndex; + EntityId = entityId ?? ""; + EntityKind = entityKind ?? ""; + Label = label ?? ""; + Faction = faction ?? ""; + Position = position; + RotationYDegrees = rotationYDegrees; + Velocity = velocity; + Radius = radius; + Scale = scale; + RenderGroupId = renderGroupId; + Lod = lod; + Selectable = selectable; + Controllable = controllable; + AssetRef = assetRef ?? ""; + } + + public int SourceIndex { get; } + public string EntityId { get; } + public string EntityKind { get; } + public string Label { get; } + public string Faction { get; } + public Vector3 Position { get; } + public float RotationYDegrees { get; } + public Vector3 Velocity { get; } + public float Radius { get; } + public float Scale { get; } + public uint RenderGroupId { get; } + public int Lod { get; } + public bool Selectable { get; } + public bool Controllable { get; } + public string AssetRef { get; } + + public EveUnityPlayableWorldEntity ToPlayableWorldEntity() => new EveUnityPlayableWorldEntity( + EntityId, + EntityId, + EntityKind, + Label, + Faction, + AssetRef, + Position.x, + Position.y, + Position.z, + RotationYDegrees, + Scale > 0f ? Scale : Radius, + Selectable, + Controllable, + "", "", "", ""); + } + + public sealed class EveUnityPresentedEntityGeneration + { + public EveUnityPresentedEntityGeneration( + string viewId, + long producerEpoch, + long sequence, + IEnumerable entities) + { + ViewId = viewId ?? ""; + ProducerEpoch = producerEpoch; + Sequence = sequence; + Entities = (entities ?? throw new ArgumentNullException(nameof(entities))).ToArray(); + } + + public string ViewId { get; } + public long ProducerEpoch { get; } + public long Sequence { get; } + public IReadOnlyList Entities { get; } + } + + public sealed class EveUnityPresentedEntityHandle + { + public EveUnityPresentedEntityHandle(EveUnityPresentedEntity entity, Transform transform) + { + Entity = entity ?? throw new ArgumentNullException(nameof(entity)); + Transform = transform != null ? transform : throw new ArgumentNullException(nameof(transform)); + } + + public EveUnityPresentedEntity Entity { get; } + public Transform Transform { get; } + } + + public interface IEveUnityPresentedEntityRegistry + { + EveUnityPresentedEntityGeneration? CurrentGeneration { get; } + bool TryGetByEntityId(string entityId, out EveUnityPresentedEntityHandle handle); + bool TryGetBySourceIndex(int sourceIndex, out EveUnityPresentedEntityHandle handle); + } + + public interface IEveUnityEntityGenerationSink + { + void ApplyGeneration(EveUnityPresentedEntityGeneration generation); + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs.meta new file mode 100644 index 0000000..c16abad --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3f7e67e0c24449bab80f2dd15cf2e118 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 185bfa9..443fa6d 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -171,6 +171,102 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(entity.AssetRef, Is.EqualTo("cultmesh://assets/pilot")); } + [Test] + public void EntitySoaPresenterPublishesImmutableGenerationFactsAndRegistryLookups() + { + var root = new GameObject("presented-entities"); + try + { + var bytes = new byte[48]; + Buffer.BlockCopy(new[] { 12 }, 0, bytes, 0, 4); + Buffer.BlockCopy(new[] { 1f, 2f, 3f }, 0, bytes, 4, 12); + Buffer.BlockCopy(new[] { 0.5f }, 0, bytes, 16, 4); + Buffer.BlockCopy(new[] { 4f, 5f, 6f }, 0, bytes, 20, 12); + Buffer.BlockCopy(new[] { 7f }, 0, bytes, 32, 4); + Buffer.BlockCopy(new[] { 1.5f }, 0, bytes, 36, 4); + Buffer.BlockCopy(new[] { 9 }, 0, bytes, 40, 4); + Buffer.BlockCopy(new[] { 2 }, 0, bytes, 44, 4); + var document = EntityLeaseDocument(); + document.Buffers[0].ByteLength = bytes.Length; + document.Columns = new[] + { + SoaColumn("entity.index", "int32", 0, 4), + SoaColumn("transform.position", "float3", 4, 12), + SoaColumn("transform.rotation.radians", "float32", 16, 4), + SoaColumn("transform.velocity", "float3", 20, 12), + SoaColumn("physics.body.radius", "float32", 32, 4), + SoaColumn("render.scale", "float32", 36, 4), + SoaColumn("render.group.id", "uint32", 40, 4), + SoaColumn("render.lod", "int32", 44, 4) + }; + var sink = new EveUnityGameObjectPlayableWorldSceneSink( + root.transform, new FixedGameObjectAssetProvider(null)); + + new EveUnityEntitySoaPresenter(sink).Apply(document, new FakeBodyReadLease(document, bytes)); + + Assert.That(sink.CurrentGeneration, Is.Not.Null); + Assert.That(sink.CurrentGeneration!.ProducerEpoch, Is.EqualTo(document.ProducerEpoch)); + Assert.That(sink.CurrentGeneration.Sequence, Is.EqualTo(document.Sequence)); + Assert.That(sink.TryGetByEntityId("entity:pilot", out var byId), Is.True); + Assert.That(sink.TryGetBySourceIndex(12, out var byIndex), Is.True); + Assert.That(byIndex, Is.SameAs(byId)); + Assert.That(byId.Entity.Position, Is.EqualTo(new Vector3(1f, 2f, 3f))); + Assert.That(byId.Entity.Velocity, Is.EqualTo(new Vector3(4f, 5f, 6f))); + Assert.That(byId.Entity.Radius, Is.EqualTo(7f)); + Assert.That(byId.Entity.Scale, Is.EqualTo(1.5f)); + Assert.That(byId.Entity.RenderGroupId, Is.EqualTo(9)); + Assert.That(byId.Entity.Lod, Is.EqualTo(2)); + Assert.That(byId.Entity.Selectable, Is.True); + Assert.That(byId.Entity.Controllable, Is.True); + Assert.That(byId.Entity.AssetRef, Is.EqualTo("cultmesh://assets/pilot")); + } + finally + { + UnityEngine.Object.DestroyImmediate(root); + } + } + + [Test] + public void GameObjectGenerationRegistrySwapsOnlyAfterCompleteApplication() + { + var root = new GameObject("atomic-generation"); + EveUnityGameObjectPlayableWorldSceneSink? sink = null; + long observedSequenceDuringApply = -1; + try + { + var provider = new ObservingGameObjectAssetProvider(() => + observedSequenceDuringApply = sink?.CurrentGeneration?.Sequence ?? -1); + sink = new EveUnityGameObjectPlayableWorldSceneSink(root.transform, provider); + sink.ApplyGeneration(new EveUnityPresentedEntityGeneration( + "entities", 3, 10, + new[] { PresentedEntity(1, "first", "asset:first") })); + Assert.That(sink.TryGetByEntityId("first", out var firstGeneration), Is.True); + var firstTransform = firstGeneration.Transform; + + observedSequenceDuringApply = -1; + sink.ApplyGeneration(new EveUnityPresentedEntityGeneration( + "entities", 3, 11, + new[] + { + PresentedEntity(1, "first", "asset:first"), + PresentedEntity(2, "second", "asset:second") + })); + + Assert.That(observedSequenceDuringApply, Is.EqualTo(10)); + Assert.That(sink.CurrentGeneration!.Sequence, Is.EqualTo(11)); + Assert.That(sink.TryGetByEntityId("first", out var retained), Is.True); + Assert.That(retained.Transform, Is.SameAs(firstTransform)); + Assert.That(sink.TryGetByEntityId("second", out var second), Is.True); + Assert.That(sink.TryGetBySourceIndex(2, out var byIndex), Is.True); + Assert.That(byIndex, Is.SameAs(second)); + Assert.That(sink.ActiveEntityCount, Is.EqualTo(2)); + } + finally + { + UnityEngine.Object.DestroyImmediate(root); + } + } + [Test] public void LowerCarriesProviderAdvertisedWorldBoundary() { @@ -1935,6 +2031,26 @@ namespace GameCult.Eve.UnityScene.Tests }; } + private static EveEntitySoaColumn SoaColumn( + string semantic, + string scalarType, + long byteOffset, + int stride) => new EveEntitySoaColumn + { + ColumnId = semantic, + Semantic = semantic, + BufferId = "hot", + ScalarType = scalarType, + ByteOffset = byteOffset, + ElementStride = stride, + ElementCount = 1 + }; + + private static EveUnityPresentedEntity PresentedEntity(int index, string entityId, string assetRef) => + new EveUnityPresentedEntity( + index, entityId, "entity", entityId, "", Vector3.zero, 0f, Vector3.zero, + 1f, 1f, 0, 0, true, false, assetRef); + private sealed class FakeBodyReadLease : ICultMeshBodyReadLease { private readonly byte[] _bytes; @@ -1988,6 +2104,22 @@ namespace GameCult.Eve.UnityScene.Tests public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) => _prefab; } + private sealed class ObservingGameObjectAssetProvider : IEveUnityGameObjectAssetProvider + { + private readonly Action _onResolve; + + public ObservingGameObjectAssetProvider(Action onResolve) + { + _onResolve = onResolve; + } + + public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) + { + _onResolve(); + return null; + } + } + private sealed class FakePlayableWorldProviderComponent : MonoBehaviour, IEveUnitySceneProviderSurfaceDocumentSource, diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 47e736b..334ca56 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.2.3", + "version": "0.2.4", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.9", + "org.gamecult.cultlib": "1.0.10", "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.2.0", "com.unity.mathematics": "1.3.2"