From 9833d40172126c2b7c7467ac010137124166473a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 17 Aug 2026 21:02:01 +0200 Subject: [PATCH] Route Eve Unity through stable CultMesh identity --- README.md | 5 +- .../PlayMode/GenericWorldCaptureTests.cs | 6 +- .../PlayMode/GenericWorldCaptureTests.cs | 6 +- TestProject/Packages/packages-lock.json | 7 +- docs/authority-map.md | 23 +- .../org.gamecult.eve.unity-scene/README.md | 6 + .../EveUnityCultMeshLiveProviderTransport.cs | 410 +++++------------- .../EveUnityCultMeshPlayableWorldProvider.cs | 19 +- .../EveUnityCultMeshProviderDiscovery.cs | 63 +-- .../EveUnityCultMeshAssetLookupTests.cs | 34 ++ .../EveUnitySceneSurfaceLowererTests.cs | 33 +- .../org.gamecult.eve.unity-scene/package.json | 2 +- scripts/run-aetheria-daemon-world-witness.ps1 | 10 +- scripts/run-interactive-aetheria.ps1 | 8 +- 14 files changed, 256 insertions(+), 376 deletions(-) diff --git a/README.md b/README.md index 3e36f56..2f76872 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,10 @@ assembly is part of the client. `EveUnityCultMeshPlayableWorldProvider` is the shipped composition root. Its provider, surface, and Verse filters default to empty, so a client can select by -the generic `interactive-world` contract alone. +the generic `interactive-world` contract alone. After selection, Unity retains +the rendezvous address, Verse ID, and stable provider identity only. `CultMeshClient` owns +physical route discovery, reconnection, typed live-resource leases, command +submission, and content/body sessions. ```powershell powershell -ExecutionPolicy Bypass -File .\scripts\run-package-tests.ps1 diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 8cb8b1d..0bfa008 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -182,8 +182,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var providerId = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_ID") ?? ""; var surfaceId = Environment.GetEnvironmentVariable("EVEUNITY_SURFACE_ID") ?? ""; - var replicaPath = Environment.GetEnvironmentVariable("EVEUNITY_REPLICA_PATH") ?? - Path.Combine(Application.temporaryCachePath, $"eve-unity-{Guid.NewGuid():N}.cc"); + var cacheDirectory = Environment.GetEnvironmentVariable("EVEUNITY_CACHE_DIRECTORY") ?? + Path.Combine(Application.temporaryCachePath, $"eve-unity-{Guid.NewGuid():N}"); var capturePath = Environment.GetEnvironmentVariable("EVEUNITY_AETHERIA_CAPTURE_PATH") ?? Path.Combine(Application.temporaryCachePath, "aetheria-daemon-world.png"); var mapCapturePath = Environment.GetEnvironmentVariable("EVEUNITY_AETHERIA_MAP_CAPTURE_PATH") ?? @@ -258,7 +258,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests provider = root.AddComponent(); provider.Configure( rendezvousEndpoint, - replicaPath, + cacheDirectory, providerId, surfaceId, requiredSurfaceKind: "interactive-world", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 8cb8b1d..0bfa008 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -182,8 +182,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var providerId = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_ID") ?? ""; var surfaceId = Environment.GetEnvironmentVariable("EVEUNITY_SURFACE_ID") ?? ""; - var replicaPath = Environment.GetEnvironmentVariable("EVEUNITY_REPLICA_PATH") ?? - Path.Combine(Application.temporaryCachePath, $"eve-unity-{Guid.NewGuid():N}.cc"); + var cacheDirectory = Environment.GetEnvironmentVariable("EVEUNITY_CACHE_DIRECTORY") ?? + Path.Combine(Application.temporaryCachePath, $"eve-unity-{Guid.NewGuid():N}"); var capturePath = Environment.GetEnvironmentVariable("EVEUNITY_AETHERIA_CAPTURE_PATH") ?? Path.Combine(Application.temporaryCachePath, "aetheria-daemon-world.png"); var mapCapturePath = Environment.GetEnvironmentVariable("EVEUNITY_AETHERIA_MAP_CAPTURE_PATH") ?? @@ -258,7 +258,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests provider = root.AddComponent(); provider.Configure( rendezvousEndpoint, - replicaPath, + cacheDirectory, providerId, surfaceId, requiredSurfaceKind: "interactive-world", diff --git a/TestProject/Packages/packages-lock.json b/TestProject/Packages/packages-lock.json index 8034b4d..7b602ba 100644 --- a/TestProject/Packages/packages-lock.json +++ b/TestProject/Packages/packages-lock.json @@ -145,9 +145,10 @@ "depth": 0, "source": "local", "dependencies": { - "org.gamecult.cultlib": "1.0.14", + "org.gamecult.cultlib": "1.0.43", "org.gamecult.eve.plugin-fields": "0.2.3", - "org.gamecult.eve.surface": "0.2.2", + "org.gamecult.eve.surface": "0.2.4", + "org.gamecult.eve.unity-uitoolkit": "0.1.2", "com.unity.mathematics": "1.3.2", "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" @@ -158,7 +159,7 @@ "depth": 0, "source": "local", "dependencies": { - "org.gamecult.eve.surface": "0.1.0" + "org.gamecult.eve.surface": "0.2.4" } }, "com.unity.modules.assetbundle": { diff --git a/docs/authority-map.md b/docs/authority-map.md index ac00b6d..4b25203 100644 --- a/docs/authority-map.md +++ b/docs/authority-map.md @@ -9,6 +9,9 @@ importing provider product code into the generic runtime. EveUnity owns Unity scene projection, `GameObject` lifecycle, camera and input drivers, asset-provider hooks, package release, runtime tests, and captures. +`CultMeshClient` owns physical route selection, reconnection, shared typed +resource sessions, and their lease lifetime. Unity receives stable Verse and +provider identities and cannot preserve a physical route as gameplay authority. ## Inputs @@ -40,6 +43,10 @@ presentation counters, and receipt display state are projections. They are not world truth. Temporal targets reset when the volume program, node, or render size changes and can never become provider state. +The optional Unity cache directory contains verified content-transfer state and +mapped body bytes only. It is disposable derived data, not a CultCache replica +of provider gameplay documents. + Camera-relative field viewports have one generic resolver. The advertised viewport frame owns its extent, spatial-cell scale, raster texels per cell, and snap policy; fog volumes and stateless particle programs consume the identical @@ -65,6 +72,11 @@ temporal pass must also advertise its current-sample, history, previous-view-projection, and reset-history ports; partial temporal programs fail closed. +Unity code may not open provider snapshot sessions, schema clients, write +forwarders, subscriptions, or replica shards against a discovered physical +route. Reads, watches, content/body transfer, realtime frames, and command +submissions share the same stable-identity `CultMeshClient` owner. + Viewport-sized shader inputs are likewise presentation state. A provider may relate a logical vector port to a logical native texture port through `viewportTextureScaleBindings`; EveUnity derives the vector from the active @@ -86,11 +98,12 @@ modes into one camera opinion or infer a product camera from entity kind. ## Shared Paths -Initial connect discovers a Verse endpoint and selects a provider surface by -semantic kind. Refresh, reconnect, and terminal-receipt reconciliation consume -that provider's surface documents. Movement, focus, target, and action input all -become `gamecult.eve.command_invocation.v1` through the provider-advertised -command boundary. +Initial connect asks one rendezvous for Verses, selects a provider surface by +semantic kind, then leases it through the selected stable provider identity. +Refresh, route rotation, reconnect, and terminal-receipt reconciliation remain +inside the same identity-owned session. Movement, focus, target, and action +input all become `gamecult.eve.command_invocation.v1` through the +provider-advertised command boundary. ## Cut Line diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index 005e991..6edac13 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -17,6 +17,12 @@ networked client. Give it one Verse rendezvous endpoint and it discovers an advertised `interactive-world`, constructs the generic CultMesh transport, and provides every bootstrap port. Provider, surface, and Verse IDs are optional selection filters; they are not product knowledge required by the client. +The selected provider ID remains the stable CultMesh endpoint identity while +the Verse ID names the selected rule/progression world. Physical routes are +resolved and re-resolved inside `CultMeshClient`; they are never +published as provider selection state or cached as Unity authority. The optional +local path is only an ephemeral content/body cache, not a replicated gameplay +database. Await `PrepareAsync()` before mounting the bootstrap or reading provider ports. Discovery and session setup never run synchronously from Unity lifecycle or input getters, so an unavailable provider cannot stall the editor main thread. 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 edf1e38..4ee1b88 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -7,13 +7,11 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using GameCult.Caching; -using GameCult.Caching.MessagePack; using GameCult.Eve.PluginFields; using GameCult.Eve.Surface; using GameCult.Eve.UnityScene.Fields; using GameCult.Mesh; using GameCult.Mesh.Quic.Native; -using GameCult.Networking; using UnityEngine; using Stopwatch = System.Diagnostics.Stopwatch; @@ -26,25 +24,9 @@ namespace GameCult.Eve.UnityScene IEveUnityGameObjectAssetProvider, IDisposable { - private const string RemoteShardId = "provider"; - - private static readonly Type[] WireDocumentTypes = - { - typeof(EveProviderAdvertisementDocument), - typeof(EveSurfaceDocument), - typeof(EveInputCapabilityDocument), - typeof(EveSurfaceCommandRequest), - typeof(EveCommandReceiptDocument), - typeof(EveAssetCatalogDocument), - typeof(EveEntitySoaViewDocument), - typeof(EveFieldsSplatsDocument), - typeof(CultMeshBodyPublicationDocument), - typeof(CultMeshCdnArtifactManifest), - typeof(CultMeshContentTransferStateDocument) - }; - - private readonly string _replicaPath; - private readonly string _endpoint; + private readonly string _cachePath; + private readonly string _rendezvousEndpoint; + private readonly string _endpointId; private readonly string _providerId; private readonly string _surfaceId; private readonly string _runtimeId; @@ -73,15 +55,11 @@ namespace GameCult.Eve.UnityScene new HashSet(StringComparer.Ordinal); private readonly Dictionary _embeddedSurfaces = new Dictionary(StringComparer.Ordinal); - private CultMeshNode? _node; - private CultMeshSnapshotSession? _snapshot; - private CultNetDocumentRegistry? _networkRegistry; - private CultNetShardDescriptor? _replicaShard; - private CultNetDatabaseSubscriptionClient? _subscriptions; - private CultNetDatabaseSubscriptionClient? _entitySubscriptions; - private CultMeshLiveBody? _entityBody; - private CultNetLiveValue? _inputCapability; + private readonly List _documentLeases = new List(); + private readonly List _documentWatches = new List(); private CultMeshClient? _meshClient; + private bool _liveDocumentsReady; + private CultCache? _contentState; private CultMeshContentTransferService? _contentTransfer; private CultMeshVerifiedBodyMappingBroker? _assetBodyMappings; private EveProviderAdvertisementDocument? _advertisement; @@ -103,19 +81,23 @@ namespace GameCult.Eve.UnityScene private bool _disposed; public EveUnityCultMeshLiveProviderTransport( - string replicaPath, - string endpoint, + string cachePath, + string rendezvousEndpoint, + string endpointId, string providerId, string surfaceId, string runtimeId = "eve-unity", CultMeshBodyPublicationResolver? bodyResolver = null) { - _replicaPath = string.IsNullOrWhiteSpace(replicaPath) - ? throw new ArgumentException("Replica path must be non-empty.", nameof(replicaPath)) - : Path.GetFullPath(replicaPath); - _endpoint = string.IsNullOrWhiteSpace(endpoint) - ? throw new ArgumentException("CultMesh endpoint must be non-empty.", nameof(endpoint)) - : endpoint.Trim(); + _cachePath = string.IsNullOrWhiteSpace(cachePath) + ? throw new ArgumentException("Cache path must be non-empty.", nameof(cachePath)) + : Path.GetFullPath(cachePath); + _rendezvousEndpoint = string.IsNullOrWhiteSpace(rendezvousEndpoint) + ? throw new ArgumentException("CultMesh rendezvous endpoint must be non-empty.", nameof(rendezvousEndpoint)) + : rendezvousEndpoint.Trim(); + _endpointId = string.IsNullOrWhiteSpace(endpointId) + ? throw new ArgumentException("CultMesh stable endpoint identity must be non-empty.", nameof(endpointId)) + : endpointId.Trim(); _providerId = string.IsNullOrWhiteSpace(providerId) ? throw new ArgumentException("Provider id must be non-empty.", nameof(providerId)) : providerId.Trim(); @@ -129,7 +111,7 @@ namespace GameCult.Eve.UnityScene CurrentInputCapability = new EveInputCapabilityDocument(); } - public string TransportKind => "eve-cultmesh-remote-replica"; + public string TransportKind => "eve-cultmesh-identity-session"; public string SurfacePointer => CurrentSurfaceDocument.SourcePointer; @@ -193,7 +175,6 @@ namespace GameCult.Eve.UnityScene try { - _snapshot ??= CreateSnapshotSession(); var elapsed = Stopwatch.StartNew(); ResolveAdvertisement(forceRefresh: true); TraceStartup("advertisement", elapsed); @@ -201,15 +182,14 @@ namespace GameCult.Eve.UnityScene TraceStartup("surface", elapsed); RefreshAssetCatalog(); TraceStartup("asset-catalog-and-bundles", elapsed); - EnsureLiveSubscriptions(); + EnsureLiveDocuments(); TraceStartup("subscriptions", elapsed); _bootstrapped = true; } catch (Exception error) when (error is IOException || error is SocketException || error is TimeoutException) { - ResetSnapshotSession(); throw new InvalidOperationException( - $"CultMesh provider '{_providerId}' bootstrap transport failed; the snapshot lane was reset for retry.", + $"CultMesh provider '{_providerId}' bootstrap through stable endpoint '{_endpointId}' failed.", error); } } @@ -237,8 +217,6 @@ namespace GameCult.Eve.UnityScene public void PumpLiveEvents() { - ThrowIfSubscriptionFailed(_entitySubscriptions); - ThrowIfSubscriptionFailed(_subscriptions); while (_liveDocuments.TryDequeue(out var document)) { if (document is PendingEntityGeneration entityGeneration) @@ -266,15 +244,6 @@ namespace GameCult.Eve.UnityScene PumpMappedEntityFrame(); } - private static void ThrowIfSubscriptionFailed(CultNetDatabaseSubscriptionClient? subscription) - { - var failure = subscription?.BackgroundFailure; - if (failure?.IsCompletedSuccessfully == true) - throw new InvalidOperationException( - "The retained CultMesh state subscription failed.", - failure.Result); - } - private void QueueEntityView(EveEntitySoaViewDocument document) { TraceHotState($"layout epoch={document.ProducerEpoch} sequence={document.Sequence} identities={document.Identities?.Length ?? 0}"); @@ -391,17 +360,12 @@ namespace GameCult.Eve.UnityScene try { var recordKey = ChildRecordKey(interaction.CommandRecordRef, commandId); - var message = _networkRegistry!.CreateRawDocumentPutMessage( - $"eve-unity-{commandId}", - new CultRecordHandle(new CultRecordKey(recordKey)), - request, - new CultNetDocumentMessageOptions - { - SourceRuntimeId = _runtimeId, - SourceRole = "eve-unity" - }); - new CultNetSchemaWriteForwarder(new CultNetSchemaWriteForwarderOptions()) - .ForwardPutAsync(_replicaShard!, message) + _meshClient!.SubmitDocumentAsync( + _endpointId, + recordKey, + request, + _runtimeId, + "eve-unity") .GetAwaiter() .GetResult(); } @@ -441,24 +405,13 @@ namespace GameCult.Eve.UnityScene _realtimePump = null; _realtimeLifetime?.Dispose(); _realtimeLifetime = null; - _entityBody?.Dispose(); - _entityBody = null; - _inputCapability?.Dispose(); - _inputCapability = null; - _subscriptions?.Dispose(); - _entitySubscriptions?.Dispose(); - _node?.Dispose(); - _snapshot?.Dispose(); + DisposeLiveDocuments(); + _contentState?.Dispose(); + _contentState = null; _meshClient?.Dispose(); - _node = null; - _snapshot = null; - _subscriptions = null; - _entitySubscriptions = null; _meshClient = null; _contentTransfer = null; _assetBodyMappings = null; - _networkRegistry = null; - _replicaShard = null; _baseSurface = null; _embeddedSurfaces.Clear(); _pendingCommandIds.Clear(); @@ -496,44 +449,11 @@ namespace GameCult.Eve.UnityScene private void EnsureOpen() { - if (_node != null) + if (_meshClient != null) return; - - var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(WireDocumentTypes); - _networkRegistry = CultMesh.CreateCultNetDocumentRegistry(WireDocumentTypes, cacheRegistry); - _replicaShard = new CultNetShardDescriptor( - RemoteShardId, - _providerId, - epoch: 1, - isPrimary: false, - schemaIds: WireDocumentTypes.Select(type => cacheRegistry.GetRequired(type).SchemaId), - primaryEndpoints: new[] { _endpoint }); - _node = CultMesh.CreateNodeAsync( - _replicaPath, - new CultMeshNodeOptions - { - StartServer = false, - EnableDurableShardLogs = true, - CacheOptions = new CultCacheOpenOptions - { - Registry = cacheRegistry, - PullOnOpen = false, - StoreFlushOnDispose = true, - UseDirectoryStore = true - }, - DatabaseOptions = new CultNetDatabaseOptions - { - RuntimeId = _runtimeId, - Shards = new[] { _replicaShard }, - DocumentRegistry = _networkRegistry - } - }) - .GetAwaiter() - .GetResult(); - _snapshot = CreateSnapshotSession(); _meshClient = new CultMeshClient(new CultMeshClientOptions { - RendezvousEndpoints = new[] { _endpoint }, + RendezvousEndpoints = new[] { _rendezvousEndpoint }, RealtimeConnectors = new ICultMeshRealtimeTransportConnector[] { new CultMeshNativeQuicRealtimeTransportConnector() @@ -541,36 +461,15 @@ namespace GameCult.Eve.UnityScene }); } - private CultMeshSnapshotSession CreateSnapshotSession() => - CultMesh.SnapshotSession( - _endpoint, - new CultMeshSnapshotRequestOptions - { - ShardId = RemoteShardId, - ShardEpoch = 1, - ConnectTimeout = TimeSpan.FromSeconds(5), - ResponseTimeout = TimeSpan.FromSeconds(30), - MessageIdPrefix = "eve-unity", - RudpRuntimeId = _runtimeId, - RudpMaxFragmentBytes = 1024 - }, - _networkRegistry); - - private void ResetSnapshotSession() - { - _snapshot?.Dispose(); - _snapshot = null; - } - private void ResolveAdvertisement(bool forceRefresh = false) { if (!forceRefresh && _advertisement != null && _advertisedSurface != null) return; - var documents = _snapshot! - .FetchDocumentsAsync() + using var advertisements = _meshClient! + .LeaseCollectionAsync(_endpointId) .GetAwaiter() .GetResult(); - _advertisement = documents.FirstOrDefault(document => + _advertisement = advertisements.Handle.LatestAsync().GetAwaiter().GetResult().FirstOrDefault(document => string.Equals(document.ProviderId, _providerId, StringComparison.Ordinal)) ?? throw new InvalidOperationException($"Provider '{_providerId}' did not publish an Eve advertisement."); _advertisedSurface = _advertisement.Surfaces.FirstOrDefault(surface => @@ -588,7 +487,7 @@ namespace GameCult.Eve.UnityScene if (string.IsNullOrWhiteSpace(_advertisement!.ServiceId)) throw new InvalidOperationException( $"Eve provider '{_advertisement.ProviderId}' has no service-instance identity for body transport."); - var mappedRoot = Path.GetDirectoryName(_replicaPath) ?? "."; + var mappedRoot = _cachePath; var networkBodies = _meshClient!.BodyProvider( _advertisement.ProviderId, _advertisement.ServiceId, @@ -923,14 +822,10 @@ namespace GameCult.Eve.UnityScene private void RefreshSurface() { - var surface = _snapshot! - .FetchDocumentsAsync( - recordKeys: new[] { _advertisedSurface!.RecordRef }, - schemaIds: new[] { EveSurfaceDocument.SchemaId }) + var surface = _meshClient! + .ReadAsync(_endpointId, _advertisedSurface!.RecordRef) .GetAwaiter() - .GetResult() - .FirstOrDefault() - ?? throw new InvalidOperationException("The advertised Eve surface record was not available."); + .GetResult(); PublishBaseSurface(surface); } @@ -1013,37 +908,24 @@ namespace GameCult.Eve.UnityScene component.Style); } - private void EnsureLiveSubscriptions() + private void EnsureLiveDocuments() { - if (_subscriptions != null && _entitySubscriptions != null) return; - _entityBody?.Dispose(); - _entityBody = null; - _inputCapability?.Dispose(); - _inputCapability = null; - _subscriptions?.Dispose(); - _entitySubscriptions?.Dispose(); - _subscriptions = null; - _entitySubscriptions = null; + if (_liveDocumentsReady) return; + DisposeLiveDocuments(); try { - _subscriptions = CreateSubscriptionClient(); - _entitySubscriptions = CreateSubscriptionClient(); + OpenLiveDocuments(); + _liveDocumentsReady = true; } catch { - _entityBody?.Dispose(); - _entityBody = null; - _inputCapability?.Dispose(); - _inputCapability = null; - _subscriptions?.Dispose(); - _entitySubscriptions?.Dispose(); - _subscriptions = null; - _entitySubscriptions = null; + DisposeLiveDocuments(); throw; } - _subscriptions.Changed += OnReplicatedDocumentChanged; - _entitySubscriptions.Changed += OnReplicatedDocumentChanged; + } + private void OpenLiveDocuments() + { var lowered = new EveUnitySceneSurfaceLowerer() .Lower(CurrentSurfaceDocument.SurfaceDocument, CurrentSurfaceDocument.AdvertisedSurface); string entityViewPointer = lowered.PlayableWorld == null @@ -1065,140 +947,91 @@ namespace GameCult.Eve.UnityScene throw new InvalidOperationException( "The playable world advertises an entity-view pointer without its logical body id."); _bodyResolver ??= CreateBodyResolver(); - var initialLayout = _entitySubscriptions.SubscribeAsync( - "eve-unity-entity-layout", - recordKeys: new[] { entityViewPointer }, - schemaIds: new[] { EveEntitySoaViewDocument.SchemaId }, - deliveryMode: CultNetDatabaseSubscriptionDeliveryMode.Live) - .GetAwaiter().GetResult(); - foreach (var layout in initialLayout.OfType()) - QueueEntityView(layout); - - _entityBody = CultMesh.SubscribeLiveBodyAsync( - _entitySubscriptions, - _bodyResolver, - new CultMeshLiveBodySubscription( - "eve-unity-entity-body", - _runtimeId, - entityBodyId)) - .GetAwaiter().GetResult(); - if (_entityBody.HasValue) - QueueBodyPublication(_entityBody.Current); - _entityBody.Changed += QueueBodyPublication; - _entityBody.Removed += () => _liveDocuments.Enqueue(new InvalidOperationException( - $"The provider withdrew required entity body '{entityBodyId}'.")); - - if (fieldRefs.Length > 0) + LeaseDocument(entityViewPointer, QueueEntityView); + LeaseCollection(publication => { - var initialFields = _entitySubscriptions.SubscribeAsync( - "eve-unity-fields", - recordKeys: fieldRefs, - schemaIds: new[] { EveFieldsSchemas.Splats }, - deliveryMode: CultNetDatabaseSubscriptionDeliveryMode.Live) - .GetAwaiter().GetResult(); - foreach (var fields in initialFields.OfType()) - _liveDocuments.Enqueue(fields); - } + if (string.Equals(publication.BodyId, entityBodyId, StringComparison.Ordinal)) + QueueBodyPublication(publication); + }); } - else if (fieldRefs.Length > 0) - { - var initial = _entitySubscriptions.SubscribeAsync( - "eve-unity-fields", - recordKeys: fieldRefs, - schemaIds: new[] { EveFieldsSchemas.Splats }, - deliveryMode: CultNetDatabaseSubscriptionDeliveryMode.Live) - .GetAwaiter().GetResult(); - foreach (var fields in initial.OfType()) - _liveDocuments.Enqueue(fields); - } - _subscriptions.SubscribeAsync( - "eve-unity-surface", - recordKeys: new[] { _advertisedSurface!.RecordRef }, - schemaIds: new[] { EveSurfaceDocument.SchemaId }, - includeSnapshot: false) - .GetAwaiter().GetResult(); + foreach (var fieldRef in fieldRefs) + LeaseDocument(fieldRef, fields => _liveDocuments.Enqueue(fields)); + + LeaseDocument( + _advertisedSurface!.RecordRef, + surface => _liveDocuments.Enqueue(new PendingSurfaceDocument(_advertisedSurface.RecordRef, surface)), + publishInitial: false); var embeddedSlots = EnumerateEmbeddedDocuments(CurrentSurfaceDocument.SurfaceDocument.Surface.Root) .Where(slot => string.Equals(slot.SchemaId, EveSurfaceDocument.SchemaId, StringComparison.Ordinal)) .Where(slot => !string.IsNullOrWhiteSpace(slot.DocumentId)) .GroupBy(slot => slot.DocumentId, StringComparer.Ordinal) .Select(group => group.First()) .ToArray(); - for (var index = 0; index < embeddedSlots.Length; index++) + foreach (var slot in embeddedSlots) { - var slot = embeddedSlots[index]; - var initialEmbedded = _subscriptions.SubscribeAsync( - $"eve-unity-embedded-surface-{index}", - recordKeys: new[] { slot.DocumentId }, - schemaIds: new[] { EveSurfaceDocument.SchemaId }, - deliveryMode: CultNetDatabaseSubscriptionDeliveryMode.Live) - .GetAwaiter().GetResult() - .OfType() - .FirstOrDefault(); - if (initialEmbedded != null) - PublishEmbeddedSurface(slot.DocumentId, initialEmbedded); + LeaseDocument( + slot.DocumentId, + surface => _liveDocuments.Enqueue(new PendingSurfaceDocument(slot.DocumentId, surface))); } var inputCapabilityRef = FindComponentProp( CurrentSurfaceDocument.SurfaceDocument.Surface.Root, "inputCapability"); if (!string.IsNullOrWhiteSpace(inputCapabilityRef)) { - _inputCapability = _subscriptions - .SubscribeLiveValueAsync( - "eve-unity-input-capability", - inputCapabilityRef) - .GetAwaiter().GetResult(); - if (_inputCapability.HasValue) - CurrentInputCapability = _inputCapability.Current; - _inputCapability.Changed += document => _liveDocuments.Enqueue(document); - _inputCapability.Removed += () => _liveDocuments.Enqueue(new InvalidOperationException( - $"The provider withdrew required input capability '{inputCapabilityRef}'.")); + LeaseDocument( + inputCapabilityRef, + document => _liveDocuments.Enqueue(document)); } var assetCatalogRef = RequireWorldInteraction().AssetManifestRecordRef; if (!string.IsNullOrWhiteSpace(assetCatalogRef)) { - _subscriptions.SubscribeAsync( - "eve-unity-asset-catalog", - recordKeys: new[] { assetCatalogRef }, - schemaIds: new[] { EveAssetCatalogDocument.SchemaId }, - includeSnapshot: false) - .GetAwaiter().GetResult(); + LeaseDocument( + assetCatalogRef, + document => _liveDocuments.Enqueue(document), + publishInitial: false); } - _subscriptions.SubscribeAsync( - "eve-unity-receipts", - schemaIds: new[] { EveCommandReceiptDocument.SchemaId }, - includeSnapshot: false, - deliveryMode: CultNetDatabaseSubscriptionDeliveryMode.Live) + LeaseCollection( + receipt => _liveDocuments.Enqueue(receipt), + includeInitialSnapshot: false); + } + + private void DisposeLiveDocuments() + { + foreach (var watch in _documentWatches) watch.Dispose(); + _documentWatches.Clear(); + foreach (var lease in _documentLeases) lease.Dispose(); + _documentLeases.Clear(); + _liveDocumentsReady = false; + } + + private void LeaseDocument( + string recordKey, + Action publish, + bool publishInitial = true) + where TDocument : class + { + var lease = _meshClient!.LeaseDocumentAsync(_endpointId, recordKey) .GetAwaiter().GetResult(); + _documentLeases.Add(lease); + if (publishInitial) publish(lease.Handle.Latest()); + _documentWatches.Add(lease.Handle.Watch(publish)); } - private CultNetDatabaseSubscriptionClient CreateSubscriptionClient() + private void LeaseCollection( + Action publish, + bool includeInitialSnapshot = true) + where TDocument : class { - 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) + var lease = _meshClient!.LeaseCollectionAsync(_endpointId, includeInitialSnapshot) + .GetAwaiter().GetResult(); + _documentLeases.Add(lease); + if (includeInitialSnapshot) + foreach (var document in lease.Handle.LatestAsync().GetAwaiter().GetResult()) + publish(document); + _documentWatches.Add(lease.Handle.WatchChanges(change => { - client.Dispose(); - throw new TimeoutException($"Timed out connecting live Eve state subscription to '{_endpoint}'."); - } - return new CultNetDatabaseSubscriptionClient(client, _node!.Database.Cache, _networkRegistry!); - } - - private void OnReplicatedDocumentChanged(CultNetReplicatedDocumentChange change) - { - TraceHotState($"change subscription={change.SubscriptionId} kind={change.ChangeKind} document={change.Document?.GetType().Name ?? "null"}"); - if (change.Document is EveEntitySoaViewDocument entityView) - QueueEntityView(entityView); - else if (change.Document is CultMeshBodyPublicationDocument publication) - QueueBodyPublication(publication); - else if (change.Document is EveSurfaceDocument surface) - _liveDocuments.Enqueue(new PendingSurfaceDocument(change.RecordKey, surface)); - else if (change.Document is EveAssetCatalogDocument or - EveCommandReceiptDocument or EveFieldsSplatsDocument) - _liveDocuments.Enqueue(change.Document); + if (change.Document != null) publish(change.Document); + })); } private static IEnumerable EnumerateEmbeddedDocuments(EveSurfaceComponent component) @@ -1234,15 +1067,11 @@ namespace GameCult.Eve.UnityScene if (string.IsNullOrWhiteSpace(interaction.AssetManifestRecordRef)) return; - var catalog = _snapshot! - .FetchDocumentsAsync( - recordKeys: new[] { interaction.AssetManifestRecordRef }, - schemaIds: new[] { EveAssetCatalogDocument.SchemaId }) + var catalog = _meshClient! + .ReadAsync(_endpointId, interaction.AssetManifestRecordRef) .GetAwaiter() - .GetResult() - .FirstOrDefault(); - if (catalog != null) - PublishAssetCatalog(catalog); + .GetResult(); + PublishAssetCatalog(catalog); } private void PublishAssetCatalog(EveAssetCatalogDocument catalog) @@ -1328,14 +1157,9 @@ namespace GameCult.Eve.UnityScene EnsureBundleLoaded(dependency); var groupElapsed = Stopwatch.StartNew(); - var descriptor = _snapshot!.FetchDocumentsAsync( - recordKeys: new[] { uri }, - schemaIds: new[] { CultMeshCdnSchemaVersions.ArtifactManifest }) + var descriptor = _meshClient!.ReadAsync(_endpointId, uri) .GetAwaiter() - .GetResult() - .FirstOrDefault(); - if (descriptor == null) - throw new InvalidOperationException($"Provider asset bundle manifest '{uri}' was not available."); + .GetResult(); TraceStartup("asset-manifest", groupElapsed); var bundle = LoadVerifiedBundle(descriptor, variant); TraceStartup("asset-transfer-and-bundle-load", groupElapsed); @@ -1448,8 +1272,10 @@ namespace GameCult.Eve.UnityScene if (string.IsNullOrWhiteSpace(cacheRoot)) cacheRoot = Path.Combine(Application.persistentDataPath, "EveUnity", "assets"); _assetBodyMappings = new CultMeshVerifiedBodyMappingBroker(cacheRoot); + _contentState ??= new CultCache( + CultMesh.CreateCultCacheDocumentRegistry(typeof(CultMeshContentTransferStateDocument))); _contentTransfer = new CultMeshContentTransferService( - _node!.Cache, + _contentState, new[] { _meshClient!.ContentProvider( 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 a9e263c..f93be26 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -9,6 +9,7 @@ using GameCult.Eve.PluginFields; using GameCult.Eve.UnityScene.Fields; using GameCult.Mesh; using UnityEngine; +using UnityEngine.Serialization; #nullable enable @@ -33,7 +34,8 @@ namespace GameCult.Eve.UnityScene [SerializeField] private string surfaceFilter = ""; [SerializeField] private string verseFilter = ""; [SerializeField] private string surfaceKind = "interactive-world"; - [SerializeField] private string replicaPath = ""; + [FormerlySerializedAs("replicaPath")] + [SerializeField] private string cacheDirectory = ""; [SerializeField] private string runtimeId = "eve-unity"; private EveUnityCultMeshLiveProviderTransport? _transport; @@ -76,7 +78,7 @@ namespace GameCult.Eve.UnityScene public void Configure( string endpoint, - string replicaStorePath = "", + string localCacheDirectory = "", string providerId = "", string surfaceId = "", string verseId = "", @@ -87,7 +89,7 @@ namespace GameCult.Eve.UnityScene throw new InvalidOperationException("Disconnect the active provider before changing discovery configuration."); rendezvousEndpoint = endpoint ?? ""; - replicaPath = replicaStorePath ?? ""; + cacheDirectory = localCacheDirectory ?? ""; providerFilter = providerId ?? ""; surfaceFilter = surfaceId ?? ""; verseFilter = verseId ?? ""; @@ -215,12 +217,13 @@ namespace GameCult.Eve.UnityScene verseFilter); TraceStartup($"discovery {elapsed.Elapsed.TotalMilliseconds:0.###}ms"); elapsed.Restart(); - var resolvedReplicaPath = string.IsNullOrWhiteSpace(replicaPath) - ? Path.Combine(Application.temporaryCachePath, $"eve-unity-{GetInstanceID()}.cc") - : replicaPath; + var resolvedCachePath = string.IsNullOrWhiteSpace(cacheDirectory) + ? Path.Combine(Application.temporaryCachePath, $"eve-unity-{GetInstanceID()}") + : cacheDirectory; _transport = new EveUnityCultMeshLiveProviderTransport( - resolvedReplicaPath, - Selection.Endpoint, + resolvedCachePath, + Selection.RendezvousEndpoint, + Selection.EndpointId, Selection.ProviderId, Selection.SurfaceId, runtimeId); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs index 11438de..91b9aef 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs @@ -13,20 +13,23 @@ namespace GameCult.Eve.UnityScene public sealed class EveUnityCultMeshProviderSelection { public EveUnityCultMeshProviderSelection( - string endpoint, + string rendezvousEndpoint, + string endpointId, string verseId, string providerId, string surfaceId, string surfaceKind) { - Endpoint = endpoint ?? ""; + RendezvousEndpoint = rendezvousEndpoint ?? ""; + EndpointId = endpointId ?? ""; VerseId = verseId ?? ""; ProviderId = providerId ?? ""; SurfaceId = surfaceId ?? ""; SurfaceKind = surfaceKind ?? ""; } - public string Endpoint { get; } + public string RendezvousEndpoint { get; } + public string EndpointId { get; } public string VerseId { get; } public string ProviderId { get; } public string SurfaceId { get; } @@ -35,11 +38,6 @@ namespace GameCult.Eve.UnityScene public sealed class EveUnityCultMeshProviderDiscovery { - private static readonly Type[] AdvertisementDocumentTypes = - { - typeof(EveProviderAdvertisementDocument) - }; - public EveUnityCultMeshProviderSelection Discover( string rendezvousEndpoint, string providerId = "", @@ -80,22 +78,31 @@ namespace GameCult.Eve.UnityScene var candidates = response.Verses .Where(verse => string.IsNullOrWhiteSpace(verseId) || string.Equals(verse.VerseId, verseId, StringComparison.Ordinal)) - .SelectMany(verse => (verse.DiscoveryEndpoints ?? Array.Empty()) - .Select(endpoint => new { Verse = verse, Endpoint = endpoint })) - .Where(candidate => !string.IsNullOrWhiteSpace(candidate.Endpoint)) + .Where(verse => !string.IsNullOrWhiteSpace(verse.VerseId) && + (verse.DiscoveryEndpoints ?? Array.Empty()).Any(endpoint => + !string.IsNullOrWhiteSpace(endpoint))) + .GroupBy(verse => verse.VerseId, StringComparer.Ordinal) + .Select(group => group.First()) .ToArray(); if (candidates.Length == 0) throw new InvalidOperationException("The rendezvous endpoint advertised no compatible Verse endpoints."); var failures = new List(); var observed = new List(); + using var mesh = new CultMeshClient(new CultMeshClientOptions + { + RendezvousEndpoints = new[] { rendezvousEndpoint } + }); foreach (var candidate in candidates) { try { - var advertisements = await FetchAdvertisementsAsync(candidate.Endpoint); + using var advertisementsLease = await mesh + .LeaseCollectionAsync(candidate.VerseId) + .ConfigureAwait(false); + var advertisements = await advertisementsLease.Handle.LatestAsync().ConfigureAwait(false); observed.AddRange(advertisements.Select(document => - $"{candidate.Endpoint}: {document.ProviderId}[{string.Join(",", document.Surfaces.Select(surface => $"{surface.SurfaceId}:{surface.SurfaceKind}"))}]")); + $"{candidate.VerseId}: {document.ProviderId}[{string.Join(",", document.Surfaces.Select(surface => $"{surface.SurfaceId}:{surface.SurfaceKind}"))}]")); var advertisement = advertisements .Where(document => string.IsNullOrWhiteSpace(providerId) || string.Equals(document.ProviderId, providerId, StringComparison.Ordinal)) @@ -113,15 +120,16 @@ namespace GameCult.Eve.UnityScene continue; return new EveUnityCultMeshProviderSelection( - candidate.Endpoint, - candidate.Verse.VerseId, + rendezvousEndpoint, + advertisement.Document.ProviderId, + candidate.VerseId, advertisement.Document.ProviderId, advertisement.Surface.SurfaceId, advertisement.Surface.SurfaceKind); } catch (Exception error) { - failures.Add($"{candidate.Endpoint}: {error.Message}"); + failures.Add($"{candidate.VerseId}: {error.Message}"); } } @@ -131,28 +139,5 @@ namespace GameCult.Eve.UnityScene throw new InvalidOperationException($"No advertised Eve surface matched {filter}.{detail}{observedDetail}"); } - private static async Task FetchAdvertisementsAsync(string endpoint) - { - var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(AdvertisementDocumentTypes); - var networkRegistry = CultMesh.CreateCultNetDocumentRegistry(AdvertisementDocumentTypes, cacheRegistry); - var snapshot = CultMesh.SnapshotEndpoint( - endpoint, - new CultMeshSnapshotEndpointOptions - { - Context = CultMesh.Verse("eve.discovery", "eve-unity").Context, - DocumentRegistry = networkRegistry, - Request = new CultMeshSnapshotRequestOptions - { - ShardId = "provider", - ShardEpoch = 1, - ConnectTimeout = TimeSpan.FromSeconds(5), - ResponseTimeout = TimeSpan.FromSeconds(10), - MessageIdPrefix = "eve-unity-discovery", - RudpRuntimeId = "eve-unity.discovery", - RudpMaxFragmentBytes = 1024 - } - }); - return (await snapshot.FetchDocumentsAsync()).ToArray(); - } } } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs index ddc3e21..7cf72b6 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs @@ -14,6 +14,39 @@ namespace GameCult.Eve.UnityScene.Tests { public sealed class EveUnityCultMeshAssetLookupTests { + [Test] + public void ProviderSelectionCarriesStableIdentityWithoutExposingAPhysicalRoute() + { + var selection = new EveUnityCultMeshProviderSelection( + "cultnet+tcp://odin.example:3075", + "aetheria.daemon", + "aetheria.public", + "aetheria.daemon", + "aetheria.pilot", + "interactive-world"); + + Assert.That(selection.RendezvousEndpoint, Is.EqualTo("cultnet+tcp://odin.example:3075")); + Assert.That(selection.EndpointId, Is.EqualTo("aetheria.daemon")); + Assert.That(selection.VerseId, Is.EqualTo("aetheria.public")); + Assert.That(typeof(EveUnityCultMeshProviderSelection).GetProperty("Endpoint"), Is.Null); + } + + [Test] + public void LiveTransportOwnsOneCultMeshClientAndNoDirectSnapshotReplica() + { + var fields = typeof(EveUnityCultMeshLiveProviderTransport) + .GetFields(BindingFlags.Instance | BindingFlags.NonPublic); + var names = Array.ConvertAll(fields, field => field.Name); + + CollectionAssert.Contains(names, "_meshClient"); + CollectionAssert.DoesNotContain(names, "_node"); + CollectionAssert.DoesNotContain(names, "_snapshot"); + CollectionAssert.DoesNotContain(names, "_networkRegistry"); + CollectionAssert.DoesNotContain(names, "_replicaShard"); + CollectionAssert.DoesNotContain(names, "_subscriptions"); + CollectionAssert.DoesNotContain(names, "_entitySubscriptions"); + } + [Test] public void UnavailableTcpRendezvousIsReportedAsRetryablePreparationFailure() { @@ -146,6 +179,7 @@ namespace GameCult.Eve.UnityScene.Tests Path.Combine(Path.GetTempPath(), $"eve-lazy-assets-{Guid.NewGuid():N}.cc"), "cultnet+tcp://127.0.0.1:1", "provider", + "provider", "surface"); var variant = new EveAssetVariant( "unity-scene", 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 a65771c..acae3fe 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -25,9 +25,10 @@ namespace GameCult.Eve.UnityScene.Tests Assert.DoesNotThrow(() => { using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-replica.cc", + "test-cache", "cultnet://127.0.0.1:3075", "aetheria", + "aetheria", "aetheria.pilot"); }); } @@ -59,9 +60,10 @@ namespace GameCult.Eve.UnityScene.Tests public void LiveTransportReusesStableLayoutForNewBodyPublications() { using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-replica.cc", + "test-cache", "cultnet://127.0.0.1:3075", "aetheria", + "aetheria", "aetheria.pilot"); var view = EntityLeaseDocument(); var publication = BodyPublication(view, view.Sequence + 1); @@ -93,9 +95,10 @@ namespace GameCult.Eve.UnityScene.Tests public void LiveTransportPairsBodyPublicationThatArrivesBeforeItsLayout() { using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-replica.cc", + "test-cache", "cultnet://127.0.0.1:3075", "aetheria", + "aetheria", "aetheria.pilot"); var view = EntityLeaseDocument(); var publication = BodyPublication(view, view.Sequence); @@ -121,9 +124,10 @@ namespace GameCult.Eve.UnityScene.Tests public void LiveTransportKeepsOnlyLatestPendingRealtimeGenerationPerBody() { using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-replica.cc", + "test-cache", "cultnet://127.0.0.1:3075", "provider", + "provider", "provider.pilot"); var enqueue = typeof(EveUnityCultMeshLiveProviderTransport) .GetMethod("EnqueueRealtimeEntityFrame", BindingFlags.NonPublic | BindingFlags.Instance)!; @@ -150,9 +154,10 @@ namespace GameCult.Eve.UnityScene.Tests public void LiveTransportReadsLaterMappedFramesWithoutNewControlDocuments() { using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-replica.cc", + "test-cache", "cultnet://127.0.0.1:3075", "provider", + "provider", "provider.pilot"); var view = EntityLeaseDocument(); view.Sequence = 0; @@ -231,14 +236,20 @@ namespace GameCult.Eve.UnityScene.Tests } [Test] - public void LiveTransportDoesNotRegisterBodyBytesAsSnapshotDocuments() + public void LiveTransportHasNoSnapshotWireRegistryForBodyBytes() { - var wireTypes = (Type[])typeof(EveUnityCultMeshLiveProviderTransport) - .GetField("WireDocumentTypes", BindingFlags.NonPublic | BindingFlags.Static)! - .GetValue(null)!; + var transport = typeof(EveUnityCultMeshLiveProviderTransport); + var wireRegistry = transport.GetField( + "WireDocumentTypes", + BindingFlags.NonPublic | BindingFlags.Static); + var fieldTypes = transport + .GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static) + .Select(field => field.FieldType) + .ToArray(); - Assert.That(wireTypes.Any(type => type == typeof(CultMeshCdnArtifactChunk)), Is.False); - Assert.That(wireTypes.Any(type => type.Name == "CultMeshNetworkBodyDocument"), Is.False); + Assert.That(wireRegistry, Is.Null); + Assert.That(fieldTypes.Any(type => type == typeof(CultMeshCdnArtifactChunk)), Is.False); + Assert.That(fieldTypes.Any(type => type.Name == "CultMeshNetworkBodyDocument"), Is.False); } [Test] diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index c84f62f..980c07b 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.103", + "version": "0.3.104", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 25aa7cb..afcaaee 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -40,7 +40,7 @@ $mapCapturePath = Join-Path $outputRoot "aetheria-daemon-map.png" $factsPath = Join-Path $outputRoot "witness-facts.json" $providerReadyPath = Join-Path $outputRoot "provider-ready.txt" $witnessPath = Join-Path $outputRoot "runtime-witness.json" -$replicaPath = Join-Path $outputRoot "eve-unity-replica.cc" +$clientCachePath = Join-Path $outputRoot "eve-unity-cache" $statePath = Join-Path $outputRoot "aetheria-witness-state.cc" $providerBundleDirectory = Join-Path $AetheriaRoot "Build\EveAssets\StandaloneWindows64" $daemonProject = Join-Path $AetheriaRoot "Aetheria.State.Daemon\Aetheria.State.Daemon.csproj" @@ -86,9 +86,7 @@ $witnessProfile = if ($observedCacheState -eq "cold") { } $witnessStartedAt = [DateTimeOffset]::UtcNow foreach ($ephemeralPath in @( - $replicaPath, - "$replicaPath.records", - "$replicaPath.cultmesh", + $clientCachePath, $statePath, "$statePath.records", "$statePath.cultmesh", @@ -211,7 +209,7 @@ $env:AETHERIA_TRACE_STARTUP_PHASES = "1" $env:EVEUNITY_RENDEZVOUS_ENDPOINT = "cultnet+tcp://127.0.0.1:$Port" $env:EVEUNITY_PROVIDER_ID = "aetheria" Remove-Item Env:EVEUNITY_SURFACE_ID -ErrorAction SilentlyContinue -$env:EVEUNITY_REPLICA_PATH = $replicaPath +$env:EVEUNITY_CACHE_DIRECTORY = $clientCachePath $env:EVEUNITY_AETHERIA_CAPTURE_PATH = $capturePath $env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH = $mapCapturePath $env:EVEUNITY_DISABLE_AUTO_LAUNCHER = "1" @@ -474,6 +472,6 @@ try { finally { if ($null -ne $unity -and -not $unity.HasExited) { Stop-Process -Id $unity.Id -Force -ErrorAction SilentlyContinue } if ($null -ne $daemon -and -not $daemon.HasExited) { Stop-Process -Id $daemon.Id -Force } - Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH, Env:EVEUNITY_DISABLE_AUTO_LAUNCHER, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH, Env:EVEUNITY_PROVIDER_READY_PATH, Env:EVEUNITY_WITNESS_PROFILE, Env:EVEUNITY_WITNESS_GAMEPLAY_SCENARIO -ErrorAction SilentlyContinue + Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_CACHE_DIRECTORY, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH, Env:EVEUNITY_DISABLE_AUTO_LAUNCHER, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH, Env:EVEUNITY_PROVIDER_READY_PATH, Env:EVEUNITY_WITNESS_PROFILE, Env:EVEUNITY_WITNESS_GAMEPLAY_SCENARIO -ErrorAction SilentlyContinue Remove-Item Env:AETHERIA_TRACE_EVE_SNAPSHOTS, Env:AETHERIA_TRACE_CLIENT_TRANSPORT -ErrorAction SilentlyContinue } diff --git a/scripts/run-interactive-aetheria.ps1 b/scripts/run-interactive-aetheria.ps1 index 3451b20..51097c7 100644 --- a/scripts/run-interactive-aetheria.ps1 +++ b/scripts/run-interactive-aetheria.ps1 @@ -19,7 +19,7 @@ $daemonLog = Join-Path $outputRoot "daemon.log" $importLog = Join-Path $outputRoot "import.log" $importErrorLog = Join-Path $outputRoot "import.error.log" $state = Join-Path $outputRoot "aetheria.cc" -$replica = Join-Path $outputRoot "eve-unity-replica.cc" +$clientCache = Join-Path $outputRoot "eve-unity-cache" $assetCache = Join-Path $outputRoot "asset-cache" $exe = Join-Path $project "Build\Windows\EveUnity.exe" $importProject = Join-Path $AetheriaRoot "Aetheria.State.Import\Aetheria.State.Import.csproj" @@ -32,7 +32,7 @@ if (Get-NetUDPEndpoint -LocalPort $Port -ErrorAction SilentlyContinue) { throw "UDP port $Port is already occupied." } New-Item -ItemType Directory -Force $outputRoot, $assetCache | Out-Null -foreach ($ephemeralPath in @($state, "$state.records", "$state.cultmesh", $replica, "$replica.records", "$replica.cultmesh")) { +foreach ($ephemeralPath in @($state, "$state.records", "$state.cultmesh", $clientCache)) { $resolved = [IO.Path]::GetFullPath($ephemeralPath) if (-not $resolved.StartsWith([IO.Path]::GetFullPath($outputRoot), [StringComparison]::OrdinalIgnoreCase)) { throw "Interactive cleanup escaped its artifact directory: $resolved" @@ -106,7 +106,7 @@ try { $env:EVEUNITY_RENDEZVOUS_ENDPOINT = "rudp://127.0.0.1:$Port" $env:EVEUNITY_PROVIDER_ID = "aetheria" - $env:EVEUNITY_REPLICA_PATH = $replica + $env:EVEUNITY_CACHE_DIRECTORY = $clientCache $env:EVEUNITY_ASSET_CACHE_PATH = $assetCache Remove-Item Env:EVEUNITY_SURFACE_ID -ErrorAction SilentlyContinue $client = Start-Process $exe -ArgumentList "-force-d3d11" -PassThru @@ -117,5 +117,5 @@ try { } finally { if ($null -ne $daemon -and -not $daemon.HasExited) { Stop-Process $daemon.Id -Force } - Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_ASSET_CACHE_PATH -ErrorAction SilentlyContinue + Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_CACHE_DIRECTORY, Env:EVEUNITY_ASSET_CACHE_PATH -ErrorAction SilentlyContinue }