Expose immutable presented entity generations
This commit is contained in:
parent
ce6a95c4f6
commit
890692e30f
10 changed files with 420 additions and 33 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<EveUnityPresentedEntity>();
|
||||
var next = new HashSet<string>(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<string, string>()),
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, GameObject> _instances = new Dictionary<string, GameObject>(StringComparer.Ordinal);
|
||||
private IReadOnlyDictionary<string, EveUnityPresentedEntityHandle> _presentedById =
|
||||
new Dictionary<string, EveUnityPresentedEntityHandle>(StringComparer.Ordinal);
|
||||
private IReadOnlyDictionary<int, EveUnityPresentedEntityHandle> _presentedByIndex =
|
||||
new Dictionary<int, EveUnityPresentedEntityHandle>();
|
||||
|
||||
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<string, GameObject>(StringComparer.Ordinal);
|
||||
var nextInstances = new Dictionary<string, GameObject>(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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Renderer>(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<EveUnityPlayableWorldEntityMarker>();
|
||||
foreach (var marker in markers)
|
||||
{
|
||||
if (marker != null && marker.EntityId == playerEntityId)
|
||||
return marker;
|
||||
return marker.transform;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<EveUnityPresentedEntity> 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<EveUnityPresentedEntity> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3f7e67e0c24449bab80f2dd15cf2e118
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Reference in a new issue