diff --git a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj index 16839dd..57ef76e 100644 --- a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj +++ b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj @@ -5,6 +5,8 @@ latest enable Portable Eve surface and provider contracts. + $(MSBuildThisFileDirectory)..\..\..\.dotnet\bin\GameCult.Eve.Surface\ + $(MSBuildThisFileDirectory)..\..\..\.dotnet\obj\GameCult.Eve.Surface\ diff --git a/packages/org.gamecult.eve.surface/package.json b/packages/org.gamecult.eve.surface/package.json index 8d57c63..bc5c6d0 100644 --- a/packages/org.gamecult.eve.surface/package.json +++ b/packages/org.gamecult.eve.surface/package.json @@ -5,7 +5,7 @@ "unity": "2021.3", "description": "Unity-compatible DTOs for gamecult.eve.surface.v1 documents.", "dependencies": { - "org.gamecult.cultlib": "1.0.3" + "org.gamecult.cultlib": "1.0.4" }, "keywords": [ "Eve", 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 24c8a70..e9bfde7 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -28,6 +28,7 @@ namespace GameCult.Eve.UnityScene typeof(EveSurfaceCommandRequest), typeof(EveCommandReceiptDocument), typeof(EveAssetCatalogDocument), + typeof(EveEntitySoaViewDocument), typeof(CultMeshCdnArtifactManifest), typeof(CultMeshCdnArtifactChunk) }; @@ -47,6 +48,7 @@ namespace GameCult.Eve.UnityScene private CultMeshSnapshotSession? _snapshot; private CultNetDocumentRegistry? _networkRegistry; private CultNetShardDescriptor? _replicaShard; + private CultNetDatabaseSubscriptionClient? _subscriptions; private EveProviderAdvertisementDocument? _advertisement; private EveAdvertisedSurface? _advertisedSurface; @@ -90,6 +92,8 @@ namespace GameCult.Eve.UnityScene public event Action? CommandReceiptAvailable; + public event Action? EntityViewAvailable; + public void Connect() { EnsureOpen(); @@ -152,8 +156,10 @@ namespace GameCult.Eve.UnityScene _nativeAssets.Clear(); _node?.Dispose(); _snapshot?.Dispose(); + _subscriptions?.Dispose(); _node = null; _snapshot = null; + _subscriptions = null; _networkRegistry = null; _replicaShard = null; _pendingCommandIds.Clear(); @@ -273,6 +279,47 @@ namespace GameCult.Eve.UnityScene surface.Version); SurfaceDocumentAvailable?.Invoke(CurrentSurfaceDocument); AssetManifestDocumentAvailable?.Invoke(CurrentAssetManifestDocument); + EnsureLiveSubscriptions(); + } + + private void EnsureLiveSubscriptions() + { + if (_subscriptions != null) return; + var endpoint = new Uri(_endpoint); + var client = CultNetSchemaClients.CreateForEndpoint(_endpoint); + client.Connect(endpoint.Host, endpoint.Port); + var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5); + while (!client.Connected && DateTime.UtcNow < deadline) + System.Threading.Thread.Sleep(1); + if (!client.Connected) + { + client.Dispose(); + throw new TimeoutException($"Timed out connecting live Eve state subscription to '{_endpoint}'."); + } + + _subscriptions = new CultNetDatabaseSubscriptionClient(client, _node!.Database.Cache, _networkRegistry!); + _subscriptions.Changed += OnReplicatedDocumentChanged; + var lowered = new EveUnitySceneSurfaceLowerer() + .Lower(CurrentSurfaceDocument.SurfaceDocument, CurrentSurfaceDocument.AdvertisedSurface); + string entityViewPointer = lowered.PlayableWorld == null + ? "" + : lowered.PlayableWorld.EntityViewPointerId; + if (!string.IsNullOrWhiteSpace(entityViewPointer)) + { + _subscriptions.SubscribeAsync( + "eve-unity-entity-view", + recordKeys: new[] { entityViewPointer }, + schemaIds: new[] { EveEntitySoaViewDocument.SchemaId }) + .GetAwaiter().GetResult(); + if (_node.Database.Cache.Get(new CultRecordKey(entityViewPointer)) is EveEntitySoaViewDocument current) + EntityViewAvailable?.Invoke(current); + } + } + + private void OnReplicatedDocumentChanged(CultNetReplicatedDocumentChange change) + { + if (change.Document is EveEntitySoaViewDocument entityView) + EntityViewAvailable?.Invoke(entityView); } private void RefreshAssetCatalog() 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 bbc6b35..94fbbee 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Collections.Concurrent; using GameCult.Eve.Surface; using UnityEngine; @@ -14,6 +15,7 @@ namespace GameCult.Eve.UnityScene IEveUnitySceneCommandSink, IEveUnitySceneCommandReceiptSource, IEveUnityProviderRefreshSource, + IEveUnityEntitySoaViewDocumentSource, IEveUnityCameraRenderPolicySource, IEveUnityNativeAssetProvider { @@ -27,6 +29,11 @@ namespace GameCult.Eve.UnityScene private EveUnityCultMeshLiveProviderTransport? _transport; private EveUnitySceneLiveProviderBridge? _bridge; + private readonly ConcurrentQueue _pendingEntityViews = new ConcurrentQueue(); + + public EveEntitySoaViewDocument? CurrentEntityView { get; private set; } + + public event Action? EntityViewAvailable; public EveUnityCultMeshProviderSelection? Selection { get; private set; } @@ -117,6 +124,15 @@ namespace GameCult.Eve.UnityScene Selection = null; } + private void Update() + { + EveEntitySoaViewDocument? latest = null; + while (_pendingEntityViews.TryDequeue(out var next)) latest = next; + if (latest == null) return; + CurrentEntityView = latest; + EntityViewAvailable?.Invoke(latest); + } + private EveUnitySceneLiveProviderBridge Bridge { get @@ -148,6 +164,7 @@ namespace GameCult.Eve.UnityScene Selection.ProviderId, Selection.SurfaceId, runtimeId); + _transport.EntityViewAvailable += view => _pendingEntityViews.Enqueue(view); _bridge = new EveUnitySceneLiveProviderBridge(_transport); } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs new file mode 100644 index 0000000..de83f46 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GameCult.Eve.Surface; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public interface IEveUnityEntitySoaViewDocumentSource + { + EveEntitySoaViewDocument? CurrentEntityView { get; } + event Action EntityViewAvailable; + } + + /// Projects portable entity SoA generations into the ordinary Unity scene sink. + public sealed class EveUnityEntitySoaPresenter + { + private readonly IEveUnityPlayableWorldSceneSink _sink; + private readonly HashSet _known = new HashSet(StringComparer.Ordinal); + + public EveUnityEntitySoaPresenter(IEveUnityPlayableWorldSceneSink sink) + { + _sink = sink ?? throw new ArgumentNullException(nameof(sink)); + } + + public void Apply(EveEntitySoaViewDocument document) + { + using var view = EveUnityEntitySoaView.Open(document); + var next = new HashSet(StringComparer.Ordinal); + for (var row = 0; row < view.EntityCount; row++) + { + if (!view.TryReadInt32("entity.index", row, out var entityIndex)) continue; + 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.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}"; + next.Add(entityId); + var assetRef = renderGroup?.MeshAssetRef ?? ""; + _sink.UpsertEntity( + new EveUnityPlayableWorldEntity( + entityId, + entityId, + "entity", + entityId, + "", + assetRef, + position.x, + position.y, + position.z, + rotationRadians * 57.2957795f, + scale > 0f ? scale : renderGroup?.DefaultScale ?? 1f, + selectable: true, + controllable: false, + focusCommand: "", + moveCommand: "", + targetCommand: "", + actionCommand: "", + props: new Dictionary()), + 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); + _known.Clear(); + foreach (var entityId in next) _known.Add(entityId); + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs.meta new file mode 100644 index 0000000..281db18 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 28748c85c1677c04593ae92d59399070 \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs index 2c2253e..f1498ef 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs @@ -47,7 +47,7 @@ namespace GameCult.Eve.UnityScene public bool TryReadVector3(string semantic, int index, out Vector3 value) { value = default; - if (!TryColumn(semantic, index, "float32", 12, out var column, out var accessor)) return false; + 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)); return true; @@ -69,6 +69,18 @@ namespace GameCult.Eve.UnityScene 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); + 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; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index c16284f..2dfe724 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -16,7 +16,7 @@ namespace GameCult.Eve.UnityScene [SerializeField] private MonoBehaviour? receiptSource; [SerializeField] private MonoBehaviour? fallbackAssetProvider; [SerializeField] private bool connectOnEnable; - [SerializeField] private bool refreshInUpdate = true; + [SerializeField] private bool refreshInUpdate; [SerializeField] private float refreshIntervalSeconds = 1f; [SerializeField] private bool renderShotTrajectories = true; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs index 995eb49..0fc0ad1 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs @@ -12,6 +12,9 @@ namespace GameCult.Eve.UnityScene private readonly EveUnitySceneProviderConnection _connection; private readonly EveUnityPlayableWorldLiveClient _client; private readonly EveUnityPlayableWorldAssetManifestDocumentSource? _assetManifestSource; + private readonly IEveUnityEntitySoaViewDocumentSource? _entityViews; + private readonly EveUnityEntitySoaPresenter _entityPresenter; + private bool _entityViewsConnected; private bool _assetManifestConnected; public EveUnityPlayableWorldRuntime( @@ -29,6 +32,8 @@ namespace GameCult.Eve.UnityScene AssetManifests = assetManifests ?? new EveUnityPlayableWorldAssetManifestCache(); _surfaceSource = new EveUnitySceneProviderSurfaceDocumentSource(surfaceDocuments); + _entityViews = surfaceDocuments as IEveUnityEntitySoaViewDocumentSource; + _entityPresenter = new EveUnityEntitySoaPresenter(sceneSink); _connection = new EveUnitySceneProviderConnection(_surfaceSource, commandSink); _client = new EveUnityPlayableWorldLiveClient( _connection, @@ -104,7 +109,9 @@ namespace GameCult.Eve.UnityScene public EveUnityPlayableWorldPresentation Connect() { EnsureAssetManifestConnected(); - return _client.Connect(); + var presentation = _client.Connect(); + EnsureEntityViewsConnected(); + return presentation; } public EveUnityPlayableWorldPresentation Refresh() @@ -158,6 +165,11 @@ namespace GameCult.Eve.UnityScene public void Disconnect() { + if (_entityViewsConnected && _entityViews != null) + { + _entityViews.EntityViewAvailable -= OnEntityViewAvailable; + _entityViewsConnected = false; + } _client.Disconnect(); if (_assetManifestConnected && _assetManifestSource != null) @@ -184,6 +196,17 @@ namespace GameCult.Eve.UnityScene AssetManifests.Connect(_assetManifestSource); _assetManifestConnected = true; } + + private void EnsureEntityViewsConnected() + { + 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); } 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 9e133dd..131d941 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -37,7 +37,7 @@ namespace GameCult.Eve.UnityScene.Tests Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", Backend = "memory_mapped_file", Location = location, ByteLength = 32, Generation = 7 } }, Columns = new[] { - new EveEntitySoaColumn { ColumnId = "position", Semantic = "transform.position", BufferId = "hot", ScalarType = "float32", ElementStride = 12, ElementCount = 2 }, + 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 } } }; diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index fd38100..6c7f9e5 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -5,7 +5,7 @@ "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.3", + "org.gamecult.cultlib": "1.0.4", "org.gamecult.eve.plugin-fields": "0.1.1", "org.gamecult.eve.surface": "0.1.0", "com.unity.mathematics": "1.3.2"