diff --git a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj
index 16839dd..f07f7e8 100644
--- a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj
+++ b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj
@@ -5,10 +5,11 @@
latest
enable
Portable Eve surface and provider contracts.
+ $(MSBuildThisFileDirectory)..\..\..\CultLib
-
-
+
+
diff --git a/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs b/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs
index 222b4a1..961ea2f 100644
--- a/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs
+++ b/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs
@@ -10,31 +10,30 @@ namespace GameCult.Eve.Surface
[MessagePackObject]
public sealed class EveEntitySoaViewDocument
{
- public const string SchemaId = "gamecult.eve.entity_soa_view.v1";
+ public const string SchemaId = "gamecult.eve.entity_soa_view.v2";
[Key(0)] public string Schema { get; set; } = SchemaId;
[Key(1)] public string ProviderId { get; set; } = "";
[Key(2)] public string ViewId { get; set; } = "";
- [Key(3)] public long FrameId { get; set; }
- [Key(4)] public long Generation { get; set; }
- [Key(5)] public string PublishedAtUtc { get; set; } = "";
- [Key(6)] public string Backend { get; set; } = "cultcache";
- [Key(7)] public string SynchronizationMode { get; set; } = "immutable_frame";
- [Key(8)] public EveEntitySoaBuffer[] Buffers { get; set; } = Array.Empty();
- [Key(9)] public EveEntitySoaColumn[] Columns { get; set; } = Array.Empty();
- [Key(10)] public EveEntitySoaDirtyRange[] DirtyRanges { get; set; } = Array.Empty();
- [Key(11)] public EveEntityRenderGroup[] RenderGroups { get; set; } = Array.Empty();
+ [Key(3)] public string PublishedAtUtc { get; set; } = "";
+ [Key(4)] public string BodySchemaId { get; set; } = "";
+ [Key(5)] public int LayoutVersion { get; set; }
+ [Key(6)] public long ProducerEpoch { get; set; }
+ [Key(7)] public long Sequence { get; set; }
+ [Key(8)] public int Capacity { get; set; }
+ [Key(9)] public EveEntitySoaBuffer[] Buffers { get; set; } = Array.Empty();
+ [Key(10)] public EveEntitySoaColumn[] Columns { get; set; } = Array.Empty();
+ [Key(11)] public EveEntitySoaDirtyRange[] DirtyRanges { get; set; } = Array.Empty();
+ [Key(12)] public EveEntityRenderGroup[] RenderGroups { get; set; } = Array.Empty();
+ [Key(13)] public EveEntityIdentity[] Identities { get; set; } = Array.Empty();
}
[MessagePackObject]
public sealed class EveEntitySoaBuffer
{
[Key(0)] public string BufferId { get; set; } = "";
- [Key(1)] public string Backend { get; set; } = "cultcache";
- [Key(2)] public string Location { get; set; } = "";
- [Key(3)] public long ByteOffset { get; set; }
- [Key(4)] public long ByteLength { get; set; }
- [Key(5)] public long Generation { get; set; }
+ [Key(1)] public long ByteOffset { get; set; }
+ [Key(2)] public long ByteLength { get; set; }
}
[MessagePackObject]
@@ -57,7 +56,15 @@ namespace GameCult.Eve.Surface
[Key(0)] public string ColumnId { get; set; } = "";
[Key(1)] public int StartIndex { get; set; }
[Key(2)] public int Count { get; set; }
- [Key(3)] public long Generation { get; set; }
+ [Key(3)] public long Sequence { get; set; }
+ }
+
+ [MessagePackObject]
+ public sealed class EveEntityIdentity
+ {
+ [Key(0)] public int Index { get; set; }
+ [Key(1)] public string EntityId { get; set; } = "";
+ [Key(2)] public string EntityKind { get; set; } = "";
}
[MessagePackObject]
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 566b19e..7b0d8ed 100644
--- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs
+++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs
@@ -30,6 +30,7 @@ namespace GameCult.Eve.UnityScene
typeof(EveCommandReceiptDocument),
typeof(EveAssetCatalogDocument),
typeof(EveEntitySoaViewDocument),
+ typeof(CultMeshBodyPublicationDocument),
typeof(CultMeshCdnArtifactManifest),
typeof(CultMeshCdnArtifactChunk)
};
@@ -53,13 +54,15 @@ namespace GameCult.Eve.UnityScene
private CultNetDatabaseSubscriptionClient? _subscriptions;
private EveProviderAdvertisementDocument? _advertisement;
private EveAdvertisedSurface? _advertisedSurface;
+ private readonly CultMeshBodyPublicationResolver _bodyResolver;
public EveUnityCultMeshLiveProviderTransport(
string replicaPath,
string endpoint,
string providerId,
string surfaceId,
- string runtimeId = "eve-unity")
+ string runtimeId = "eve-unity",
+ CultMeshBodyPublicationResolver? bodyResolver = null)
{
_replicaPath = string.IsNullOrWhiteSpace(replicaPath)
? throw new ArgumentException("Replica path must be non-empty.", nameof(replicaPath))
@@ -74,6 +77,7 @@ namespace GameCult.Eve.UnityScene
? throw new ArgumentException("Surface id must be non-empty.", nameof(surfaceId))
: surfaceId.Trim();
_runtimeId = string.IsNullOrWhiteSpace(runtimeId) ? "eve-unity" : runtimeId.Trim();
+ _bodyResolver = bodyResolver ?? CreateBodyResolver();
CurrentSurfaceDocument = EmptySurfaceDocument();
CurrentAssetManifestDocument = EmptyAssetManifest();
}
@@ -94,7 +98,7 @@ namespace GameCult.Eve.UnityScene
public event Action? CommandReceiptAvailable;
- public event Action? EntityViewAvailable;
+ public event Action? EntityViewAvailable;
public void Connect()
{
@@ -119,7 +123,7 @@ namespace GameCult.Eve.UnityScene
while (_liveDocuments.TryDequeue(out var document))
{
if (document is EveEntitySoaViewDocument entityView)
- EntityViewAvailable?.Invoke(entityView);
+ PublishEntityView(entityView);
else if (document is EveSurfaceDocument surface)
PublishSurface(surface);
else if (document is EveCommandReceiptDocument receipt)
@@ -268,6 +272,62 @@ namespace GameCult.Eve.UnityScene
throw new InvalidOperationException("The provider surface advertisement does not publish a record reference.");
}
+ private CultMeshBodyPublicationResolver CreateBodyResolver()
+ {
+ var mappedRoot = Path.GetDirectoryName(_replicaPath) ?? ".";
+ return new CultMeshBodyPublicationResolver(new CultMeshBodyTransportService(
+ new ICultMeshBodyTransportAdapter[]
+ {
+ new CultMeshSharedMemoryBodyAdapter(),
+ new CultMeshMappedBodyAdapter(mappedRoot),
+ new CultMeshNetworkBodyAdapter(_ => throw new NotSupportedException(
+ "The connected Verse does not expose a CultMesh network body byte reader."))
+ },
+ (producerId, _) => string.Equals(producerId, _providerId, StringComparison.Ordinal)));
+ }
+
+ private void PublishEntityView(EveEntitySoaViewDocument document)
+ {
+ ResolveAdvertisement();
+ if (!string.Equals(document.ProviderId, _advertisement!.ProviderId, StringComparison.Ordinal))
+ throw new UnauthorizedAccessException("Entity layout provider does not match the advertised Eve provider.");
+ if (document.Buffers == null || document.Buffers.Length == 0)
+ throw new InvalidOperationException("Entity layout does not name a primary logical buffer.");
+ var bodyId = document.Buffers[0].BufferId;
+ var publication = _snapshot!
+ .FetchDocumentsAsync(
+ recordKeys: new[] { CultMeshBodyPublicationDocument.CreateRecordKey(bodyId).Value },
+ schemaIds: new[] { CultMeshBodyPublicationSchemaVersions.Publication })
+ .GetAwaiter().GetResult().FirstOrDefault()
+ ?? throw new InvalidOperationException($"Provider did not publish CultMesh body '{bodyId}'.");
+ var lease = _bodyResolver.ResolveReadOnly(publication, new CultMeshBodyValidationRequest
+ {
+ BodyId = bodyId,
+ SchemaId = document.BodySchemaId,
+ LayoutVersion = document.LayoutVersion,
+ ProducerEpoch = document.ProducerEpoch,
+ Sequence = document.Sequence,
+ Capacity = document.Capacity,
+ AccessMode = CultMeshBodyAccessMode.ReadOnly,
+ NowUtc = DateTimeOffset.UtcNow
+ });
+ var handler = EntityViewAvailable;
+ if (handler == null)
+ {
+ lease.Dispose();
+ return;
+ }
+ try
+ {
+ handler(document, lease);
+ }
+ catch
+ {
+ lease.Dispose();
+ throw;
+ }
+ }
+
private void RefreshSurface()
{
var surface = _snapshot!
diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs
index ac39266..ec1a239 100644
--- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs
+++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs
@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Collections.Concurrent;
using GameCult.Eve.Surface;
+using GameCult.Mesh;
using UnityEngine;
#nullable enable
@@ -29,11 +30,9 @@ namespace GameCult.Eve.UnityScene
private EveUnityCultMeshLiveProviderTransport? _transport;
private EveUnitySceneLiveProviderBridge? _bridge;
- private readonly ConcurrentQueue _pendingEntityViews = new ConcurrentQueue();
+ private readonly ConcurrentQueue _pendingEntityViews = new ConcurrentQueue();
- public EveEntitySoaViewDocument? CurrentEntityView { get; private set; }
-
- public event Action? EntityViewAvailable;
+ public event Action? EntityViewAvailable;
public EveUnityCultMeshProviderSelection? Selection { get; private set; }
@@ -119,6 +118,7 @@ namespace GameCult.Eve.UnityScene
{
_bridge?.Dispose();
_transport?.Dispose();
+ while (_pendingEntityViews.TryDequeue(out var pending)) pending.Lease.Dispose();
_bridge = null;
_transport = null;
Selection = null;
@@ -127,11 +127,16 @@ namespace GameCult.Eve.UnityScene
private void Update()
{
_transport?.PumpLiveEvents();
- EveEntitySoaViewDocument? latest = null;
- while (_pendingEntityViews.TryDequeue(out var next)) latest = next;
+ EntityViewLease? latest = null;
+ while (_pendingEntityViews.TryDequeue(out var next))
+ {
+ latest?.Lease.Dispose();
+ latest = next;
+ }
if (latest == null) return;
- CurrentEntityView = latest;
- EntityViewAvailable?.Invoke(latest);
+ var handler = EntityViewAvailable;
+ if (handler == null) latest.Lease.Dispose();
+ else handler(latest.Document, latest.Lease);
}
private EveUnitySceneLiveProviderBridge Bridge
@@ -165,8 +170,20 @@ namespace GameCult.Eve.UnityScene
Selection.ProviderId,
Selection.SurfaceId,
runtimeId);
- _transport.EntityViewAvailable += view => _pendingEntityViews.Enqueue(view);
+ _transport.EntityViewAvailable += (view, lease) => _pendingEntityViews.Enqueue(new EntityViewLease(view, lease));
_bridge = new EveUnitySceneLiveProviderBridge(_transport);
}
+
+ private sealed class EntityViewLease
+ {
+ public EntityViewLease(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease)
+ {
+ Document = document;
+ Lease = lease;
+ }
+
+ public EveEntitySoaViewDocument Document { get; }
+ public ICultMeshBodyReadLease Lease { get; }
+ }
}
}
diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs
index de83f46..17a89a0 100644
--- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs
+++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Eve.Surface;
+using GameCult.Mesh;
#nullable enable
@@ -9,8 +10,7 @@ namespace GameCult.Eve.UnityScene
{
public interface IEveUnityEntitySoaViewDocumentSource
{
- EveEntitySoaViewDocument? CurrentEntityView { get; }
- event Action EntityViewAvailable;
+ event Action EntityViewAvailable;
}
/// Projects portable entity SoA generations into the ordinary Unity scene sink.
@@ -24,9 +24,9 @@ namespace GameCult.Eve.UnityScene
_sink = sink ?? throw new ArgumentNullException(nameof(sink));
}
- public void Apply(EveEntitySoaViewDocument document)
+ public void Apply(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease)
{
- using var view = EveUnityEntitySoaView.Open(document);
+ using var view = EveUnityEntitySoaView.Open(document, lease);
var next = new HashSet(StringComparer.Ordinal);
for (var row = 0; row < view.EntityCount; row++)
{
@@ -37,14 +37,15 @@ namespace GameCult.Eve.UnityScene
view.TryReadFloat("render.scale", row, out var scale);
view.TryReadUInt32("render.group.id", row, out var groupId);
var renderGroup = view.RenderGroups.FirstOrDefault(group => group.GroupId == groupId);
- var entityId = $"entity:{entityIndex}";
+ var identity = document.Identities.FirstOrDefault(item => item.Index == entityIndex);
+ var entityId = identity?.EntityId ?? $"entity:{entityIndex}";
next.Add(entityId);
var assetRef = renderGroup?.MeshAssetRef ?? "";
_sink.UpsertEntity(
new EveUnityPlayableWorldEntity(
entityId,
entityId,
- "entity",
+ identity?.EntityKind ?? "entity",
entityId,
"",
assetRef,
diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs
index f1498ef..8aab4de 100644
--- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs
+++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
-using System.IO.MemoryMappedFiles;
using System.Linq;
using GameCult.Eve.Surface;
+using GameCult.Mesh;
using UnityEngine;
#nullable enable
@@ -11,104 +11,124 @@ namespace GameCult.Eve.UnityScene
{
public sealed class EveUnityEntitySoaView : IDisposable
{
- private readonly Dictionary _buffers =
- new Dictionary(StringComparer.Ordinal);
+ private readonly ICultMeshBodyReadLease _lease;
+ private readonly Dictionary _buffers;
private readonly Dictionary _columns;
- private EveUnityEntitySoaView(EveEntitySoaViewDocument document)
+ private EveUnityEntitySoaView(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease)
{
Document = document;
+ _lease = lease;
+ _buffers = document.Buffers.ToDictionary(buffer => buffer.BufferId, StringComparer.Ordinal);
_columns = document.Columns.ToDictionary(column => column.Semantic, StringComparer.Ordinal);
- foreach (var buffer in document.Buffers)
- {
- if (!string.Equals(buffer.Backend, "memory_mapped_file", StringComparison.Ordinal))
- throw new NotSupportedException($"EveUnity does not support entity SoA backend '{buffer.Backend}'.");
- var mapped = MemoryMappedFile.OpenExisting(buffer.Location, MemoryMappedFileRights.Read);
- _buffers.Add(buffer.BufferId, mapped.CreateViewAccessor(
- buffer.ByteOffset,
- buffer.ByteLength,
- MemoryMappedFileAccess.Read));
- mapped.Dispose();
- }
}
public EveEntitySoaViewDocument Document { get; }
- public long Generation => Document.Generation;
+ 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 static EveUnityEntitySoaView Open(EveEntitySoaViewDocument document)
+ public static EveUnityEntitySoaView Open(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease)
{
if (document == null) throw new ArgumentNullException(nameof(document));
- if (!string.Equals(document.Schema, EveEntitySoaViewDocument.SchemaId, StringComparison.Ordinal))
- throw new ArgumentException($"Expected {EveEntitySoaViewDocument.SchemaId}, got '{document.Schema}'.", nameof(document));
- return new EveUnityEntitySoaView(document);
+ if (lease == null) throw new ArgumentNullException(nameof(lease));
+ Validate(document, lease);
+ return new EveUnityEntitySoaView(document, lease);
}
public bool TryReadVector3(string semantic, int index, out Vector3 value)
{
value = default;
- if (!TryColumn(semantic, index, "float3", 12, out var column, out var accessor)) return false;
- var offset = column.ByteOffset + (long)index * column.ElementStride;
- value = new Vector3(accessor.ReadSingle(offset), accessor.ReadSingle(offset + 4), accessor.ReadSingle(offset + 8));
+ if (!TryColumn(semantic, index, "float3", 12, out var offset)) return false;
+ value = new Vector3(_lease.ReadSingle(offset), _lease.ReadSingle(offset + 4), _lease.ReadSingle(offset + 8));
return true;
}
public bool TryReadFloat(string semantic, int index, out float value)
{
value = default;
- if (!TryColumn(semantic, index, "float32", 4, out var column, out var accessor)) return false;
- value = accessor.ReadSingle(column.ByteOffset + (long)index * column.ElementStride);
+ if (!TryColumn(semantic, index, "float32", 4, out var offset)) return false;
+ value = _lease.ReadSingle(offset);
return true;
}
public bool TryReadInt32(string semantic, int index, out int value)
{
value = default;
- if (!TryColumn(semantic, index, "int32", 4, out var column, out var accessor)) return false;
- value = accessor.ReadInt32(column.ByteOffset + (long)index * column.ElementStride);
+ if (!TryColumn(semantic, index, "int32", 4, out var offset)) return false;
+ value = _lease.ReadInt32(offset);
return true;
}
public bool TryReadUInt32(string semantic, int index, out uint value)
{
value = default;
- if (!TryColumn(semantic, index, "uint32", 4, out var column, out var accessor)) return false;
- value = accessor.ReadUInt32(column.ByteOffset + (long)index * column.ElementStride);
+ if (!TryColumn(semantic, index, "uint32", 4, out var offset)) return false;
+ value = unchecked((uint)_lease.ReadInt32(offset));
return true;
}
- public int EntityCount => Document.Columns.Length == 0
- ? 0
- : Document.Columns.Min(column => column.ElementCount);
-
public bool TryReadByte(string semantic, int index, out byte value)
{
value = default;
- if (!TryColumn(semantic, index, "uint8", 1, out var column, out var accessor)) return false;
- value = accessor.ReadByte(column.ByteOffset + (long)index * column.ElementStride);
+ if (!TryColumn(semantic, index, "uint8", 1, out var offset)) return false;
+ value = _lease.ReadByte(offset);
return true;
}
- public void Dispose()
+ public void Dispose() => _lease.Dispose();
+
+ private static void Validate(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease)
{
- foreach (var accessor in _buffers.Values) accessor.Dispose();
- _buffers.Clear();
+ if (!string.Equals(document.Schema, EveEntitySoaViewDocument.SchemaId, StringComparison.Ordinal))
+ throw new ArgumentException($"Expected {EveEntitySoaViewDocument.SchemaId}, got '{document.Schema}'.", nameof(document));
+ if (document.Buffers == null || document.Buffers.Length == 0)
+ throw new ArgumentException("Entity SoA view requires a primary logical buffer.", nameof(document));
+ var descriptor = lease.Descriptor ?? throw new ArgumentException("Body lease has no descriptor.", nameof(lease));
+ var primary = document.Buffers[0];
+ if (!string.Equals(descriptor.BodyId, primary.BufferId, StringComparison.Ordinal) ||
+ !string.Equals(descriptor.SchemaId, document.BodySchemaId, StringComparison.Ordinal) ||
+ descriptor.LayoutVersion != document.LayoutVersion ||
+ descriptor.ProducerEpoch != document.ProducerEpoch ||
+ descriptor.Sequence != document.Sequence ||
+ descriptor.Capacity != document.Capacity)
+ throw new InvalidOperationException("CultMesh body lease generation does not match the Eve entity layout.");
+ if (document.Capacity < 0 || document.Buffers.Any(buffer =>
+ string.IsNullOrWhiteSpace(buffer.BufferId) || buffer.ByteOffset < 0 || buffer.ByteLength < 0 ||
+ buffer.ByteOffset > descriptor.ByteSize - buffer.ByteLength))
+ throw new InvalidOperationException("Eve entity layout contains an invalid logical buffer range.");
+ var buffers = document.Buffers.ToDictionary(buffer => buffer.BufferId, StringComparer.Ordinal);
+ foreach (var column in document.Columns ?? Array.Empty())
+ {
+ if (!buffers.TryGetValue(column.BufferId, out var buffer) || column.ByteOffset < 0 ||
+ column.ElementStride <= 0 || column.ElementCount < 0 || column.ElementCount > document.Capacity)
+ throw new InvalidOperationException($"Eve entity column '{column.ColumnId}' has invalid layout metadata.");
+ var width = ScalarWidth(column.ScalarType);
+ var occupied = column.ElementCount == 0 ? 0 : (long)(column.ElementCount - 1) * column.ElementStride + width;
+ if (width == 0 || column.ByteOffset > buffer.ByteLength - occupied)
+ throw new InvalidOperationException($"Eve entity column '{column.ColumnId}' exceeds its logical buffer.");
+ }
}
- private bool TryColumn(
- string semantic,
- int index,
- string scalarType,
- int minimumStride,
- out EveEntitySoaColumn column,
- out MemoryMappedViewAccessor accessor)
+ private bool TryColumn(string semantic, int index, string scalarType, int minimumStride, out long offset)
{
- column = null!;
- accessor = null!;
- if (index < 0 || !_columns.TryGetValue(semantic ?? "", out column)) return false;
+ offset = 0;
+ if (index < 0 || !_columns.TryGetValue(semantic ?? "", out var column)) return false;
if (index >= column.ElementCount || column.ElementStride < minimumStride) return false;
if (!string.Equals(column.ScalarType, scalarType, StringComparison.Ordinal)) return false;
- return _buffers.TryGetValue(column.BufferId, out accessor);
+ if (!_buffers.TryGetValue(column.BufferId, out var buffer)) return false;
+ offset = buffer.ByteOffset + column.ByteOffset + (long)index * column.ElementStride;
+ return true;
}
+
+ private static int ScalarWidth(string scalarType) => scalarType switch
+ {
+ "float3" => 12,
+ "float32" => 4,
+ "int32" => 4,
+ "uint32" => 4,
+ "uint8" => 1,
+ _ => 0
+ };
}
}
diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs
index 0fc0ad1..5871920 100644
--- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs
+++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs
@@ -202,11 +202,10 @@ namespace GameCult.Eve.UnityScene
if (_entityViewsConnected || _entityViews == null) return;
_entityViews.EntityViewAvailable += OnEntityViewAvailable;
_entityViewsConnected = true;
- if (_entityViews.CurrentEntityView != null)
- _entityPresenter.Apply(_entityViews.CurrentEntityView);
}
- private void OnEntityViewAvailable(EveEntitySoaViewDocument document) => _entityPresenter.Apply(document);
+ private void OnEntityViewAvailable(EveEntitySoaViewDocument document, GameCult.Mesh.ICultMeshBodyReadLease lease) =>
+ _entityPresenter.Apply(document, lease);
}
public sealed class EveUnityLivePlayableWorldAssetProvider : IEveUnityNativeAssetProvider
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 10db12d..a153dee 100644
--- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs
+++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.IO.MemoryMappedFiles;
using GameCult.Eve.Surface;
using GameCult.Mesh;
using NUnit.Framework;
@@ -13,42 +12,58 @@ namespace GameCult.Eve.UnityScene.Tests
public sealed class EveUnitySceneSurfaceLowererTests
{
[Test]
- public void EntitySoaViewReadsGenericSemanticColumnsFromAdvertisedBackend()
+ public void EntitySoaViewReadsGenericSemanticColumnsFromInjectedLease()
{
- var location = $"eve-unity-soa-{Guid.NewGuid():N}";
- using var mapped = MemoryMappedFile.CreateNew(location, 32);
- using (var writer = mapped.CreateViewAccessor())
- {
- writer.Write(0, 4f);
- writer.Write(4, 5f);
- writer.Write(8, 6f);
- writer.Write(12, 7f);
- writer.Write(16, 8f);
- writer.Write(20, 9f);
- writer.Write(24, 41);
- writer.Write(28, 42);
- }
+ var bytes = new byte[32];
+ Buffer.BlockCopy(new[] { 4f, 5f, 6f, 7f, 8f, 9f }, 0, bytes, 0, 24);
+ Buffer.BlockCopy(new[] { 41, 42 }, 0, bytes, 24, 8);
var document = new EveEntitySoaViewDocument
{
ProviderId = "provider",
ViewId = "pilot",
- Generation = 7,
- Backend = "memory_mapped_file",
- Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", Backend = "memory_mapped_file", Location = location, ByteLength = 32, Generation = 7 } },
+ BodySchemaId = "test.entity.slab.v1",
+ LayoutVersion = 3,
+ ProducerEpoch = 5,
+ Sequence = 7,
+ Capacity = 2,
+ Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", ByteLength = 32 } },
Columns = new[]
{
new EveEntitySoaColumn { ColumnId = "position", Semantic = "transform.position", BufferId = "hot", ScalarType = "float3", ElementStride = 12, ElementCount = 2 },
new EveEntitySoaColumn { ColumnId = "entity-index", Semantic = "entity.index", BufferId = "hot", ScalarType = "int32", ByteOffset = 24, ElementStride = 4, ElementCount = 2 }
}
};
+ var lease = new FakeBodyReadLease(document, bytes);
- using var view = EveUnityEntitySoaView.Open(document);
+ using var view = EveUnityEntitySoaView.Open(document, lease);
Assert.That(view.TryReadVector3("transform.position", 1, out var position), Is.True);
Assert.That(position, Is.EqualTo(new Vector3(7f, 8f, 9f)));
Assert.That(view.TryReadInt32("entity.index", 0, out var entityIndex), Is.True);
Assert.That(entityIndex, Is.EqualTo(41));
Assert.That(view.Generation, Is.EqualTo(7));
+ Assert.That(lease.Disposed, Is.False);
+ }
+
+ [TestCase("body")]
+ [TestCase("schema")]
+ [TestCase("layout")]
+ [TestCase("epoch")]
+ [TestCase("sequence")]
+ [TestCase("capacity")]
+ public void EntitySoaViewRejectsLeaseThatDisagreesWithLogicalLayout(string mismatch)
+ {
+ var document = EntityLeaseDocument();
+ var descriptor = FakeBodyReadLease.DescriptorFor(document, 4);
+ if (mismatch == "body") descriptor.BodyId = "wrong";
+ if (mismatch == "schema") descriptor.SchemaId = "wrong";
+ if (mismatch == "layout") descriptor.LayoutVersion++;
+ if (mismatch == "epoch") descriptor.ProducerEpoch++;
+ if (mismatch == "sequence") descriptor.Sequence++;
+ if (mismatch == "capacity") descriptor.Capacity++;
+
+ Assert.Throws(() => EveUnityEntitySoaView.Open(
+ document, new FakeBodyReadLease(descriptor, new byte[4])));
}
[Test]
@@ -1746,6 +1761,71 @@ namespace GameCult.Eve.UnityScene.Tests
"", "", "", "");
}
+ private static EveEntitySoaViewDocument EntityLeaseDocument() => new EveEntitySoaViewDocument
+ {
+ ProviderId = "provider",
+ ViewId = "entities",
+ BodySchemaId = "test.entity.slab.v1",
+ LayoutVersion = 3,
+ ProducerEpoch = 5,
+ Sequence = 7,
+ Capacity = 1,
+ Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", ByteLength = 4 } },
+ Columns = new[]
+ {
+ new EveEntitySoaColumn
+ {
+ ColumnId = "entity-index",
+ Semantic = "entity.index",
+ BufferId = "hot",
+ ScalarType = "int32",
+ ElementStride = 4,
+ ElementCount = 1
+ }
+ }
+ };
+
+ private sealed class FakeBodyReadLease : ICultMeshBodyReadLease
+ {
+ private readonly byte[] _bytes;
+
+ public FakeBodyReadLease(EveEntitySoaViewDocument document, byte[] bytes)
+ : this(DescriptorFor(document, bytes.Length), bytes) { }
+
+ public FakeBodyReadLease(CultMeshBodyDescriptor descriptor, byte[] bytes)
+ {
+ Descriptor = descriptor;
+ _bytes = bytes;
+ }
+
+ public CultMeshBodyDescriptor Descriptor { get; }
+ public CultMeshBodyTransportKind TransportKind => CultMeshBodyTransportKind.Network;
+ public bool Disposed { get; private set; }
+ public byte ReadByte(long offset) => _bytes[checked((int)offset)];
+ public int ReadInt32(long offset) => BitConverter.ToInt32(_bytes, checked((int)offset));
+ public long ReadInt64(long offset) => BitConverter.ToInt64(_bytes, checked((int)offset));
+ public float ReadSingle(long offset) => BitConverter.ToSingle(_bytes, checked((int)offset));
+ public double ReadDouble(long offset) => BitConverter.ToDouble(_bytes, checked((int)offset));
+ public int CopyTo(long offset, byte[] destination, int destinationOffset, int count)
+ {
+ Buffer.BlockCopy(_bytes, checked((int)offset), destination, destinationOffset, count);
+ return count;
+ }
+ public void Dispose() => Disposed = true;
+
+ public static CultMeshBodyDescriptor DescriptorFor(EveEntitySoaViewDocument document, long byteSize) =>
+ new CultMeshBodyDescriptor
+ {
+ BodyId = document.Buffers[0].BufferId,
+ SchemaId = document.BodySchemaId,
+ LayoutVersion = document.LayoutVersion,
+ ByteSize = byteSize,
+ Capacity = document.Capacity,
+ ProducerEpoch = document.ProducerEpoch,
+ Sequence = document.Sequence
+ };
+ }
+
private sealed class FixedGameObjectAssetProvider : IEveUnityGameObjectAssetProvider
{
private readonly GameObject? _prefab;
diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs
index da482df..6b348ab 100644
--- a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs
+++ b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs
@@ -9,6 +9,41 @@ namespace GameCult.Eve.Surface.Tests;
[TestFixture]
public sealed class EveSurfaceSerializationCompatibilityTests
{
+ [Test]
+ public void EntitySoaV2SerializesLogicalLayoutWithoutTransportFields()
+ {
+ var document = new EveEntitySoaViewDocument
+ {
+ ProviderId = "provider",
+ ViewId = "entities",
+ BodySchemaId = "entity.slab.v1",
+ LayoutVersion = 2,
+ ProducerEpoch = 3,
+ Sequence = 4,
+ Capacity = 8,
+ Buffers = new[] { new EveEntitySoaBuffer { BufferId = "entity-hot", ByteLength = 64 } }
+ };
+
+ var bytes = MessagePackSerializer.Serialize(document, Options);
+ var reader = new MessagePackReader(bytes);
+ var restored = MessagePackSerializer.Deserialize(bytes, Options);
+
+ Assert.That(reader.ReadArrayHeader(), Is.EqualTo(14));
+ Assert.That(restored.Schema, Is.EqualTo(EveEntitySoaViewDocument.SchemaId));
+ Assert.That(restored.Buffers[0].BufferId, Is.EqualTo("entity-hot"));
+ }
+
+ [Test]
+ public void EntitySoaContractsExposeNoTransportAuthorityFields()
+ {
+ var forbidden = new[] { "Backend", "Location", "CapabilityToken", "Synchronization", "SynchronizationMode", "Descriptor" };
+ var exposed = new[] { typeof(EveEntitySoaViewDocument), typeof(EveEntitySoaBuffer), typeof(EveEntitySoaColumn) }
+ .SelectMany(type => type.GetProperties())
+ .Select(property => property.Name);
+
+ Assert.That(exposed, Has.None.Matches(name => forbidden.Contains(name, StringComparer.Ordinal)));
+ }
+
private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard;
[Test]