Route Eve Unity through stable CultMesh identity
This commit is contained in:
parent
7c1dd5a17d
commit
9833d40172
14 changed files with 257 additions and 377 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<EveUnityCultMeshPlayableWorldProvider>();
|
||||
provider.Configure(
|
||||
rendezvousEndpoint,
|
||||
replicaPath,
|
||||
cacheDirectory,
|
||||
providerId,
|
||||
surfaceId,
|
||||
requiredSurfaceKind: "interactive-world",
|
||||
|
|
|
|||
|
|
@ -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<EveUnityCultMeshPlayableWorldProvider>();
|
||||
provider.Configure(
|
||||
rendezvousEndpoint,
|
||||
replicaPath,
|
||||
cacheDirectory,
|
||||
providerId,
|
||||
surfaceId,
|
||||
requiredSurfaceKind: "interactive-world",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<string>(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, EveSurfaceDocument> _embeddedSurfaces =
|
||||
new Dictionary<string, EveSurfaceDocument>(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<EveInputCapabilityDocument>? _inputCapability;
|
||||
private readonly List<IDisposable> _documentLeases = new List<IDisposable>();
|
||||
private readonly List<IDisposable> _documentWatches = new List<IDisposable>();
|
||||
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<EveSurfaceCommandRequest>(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<EveProviderAdvertisementDocument>()
|
||||
using var advertisements = _meshClient!
|
||||
.LeaseCollectionAsync<EveProviderAdvertisementDocument>(_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<EveSurfaceDocument>(
|
||||
recordKeys: new[] { _advertisedSurface!.RecordRef },
|
||||
schemaIds: new[] { EveSurfaceDocument.SchemaId })
|
||||
var surface = _meshClient!
|
||||
.ReadAsync<EveSurfaceDocument>(_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<EveEntitySoaViewDocument>())
|
||||
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<EveEntitySoaViewDocument>(entityViewPointer, QueueEntityView);
|
||||
LeaseCollection<CultMeshBodyPublicationDocument>(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<EveFieldsSplatsDocument>())
|
||||
_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<EveFieldsSplatsDocument>())
|
||||
_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<EveFieldsSplatsDocument>(fieldRef, fields => _liveDocuments.Enqueue(fields));
|
||||
|
||||
LeaseDocument<EveSurfaceDocument>(
|
||||
_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<EveSurfaceDocument>()
|
||||
.FirstOrDefault();
|
||||
if (initialEmbedded != null)
|
||||
PublishEmbeddedSurface(slot.DocumentId, initialEmbedded);
|
||||
LeaseDocument<EveSurfaceDocument>(
|
||||
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<EveInputCapabilityDocument>(
|
||||
"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<EveInputCapabilityDocument>(
|
||||
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<EveAssetCatalogDocument>(
|
||||
assetCatalogRef,
|
||||
document => _liveDocuments.Enqueue(document),
|
||||
publishInitial: false);
|
||||
}
|
||||
_subscriptions.SubscribeAsync(
|
||||
"eve-unity-receipts",
|
||||
schemaIds: new[] { EveCommandReceiptDocument.SchemaId },
|
||||
includeSnapshot: false,
|
||||
deliveryMode: CultNetDatabaseSubscriptionDeliveryMode.Live)
|
||||
LeaseCollection<EveCommandReceiptDocument>(
|
||||
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<TDocument>(
|
||||
string recordKey,
|
||||
Action<TDocument> publish,
|
||||
bool publishInitial = true)
|
||||
where TDocument : class
|
||||
{
|
||||
var lease = _meshClient!.LeaseDocumentAsync<TDocument>(_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<TDocument>(
|
||||
Action<TDocument> 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<TDocument>(_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<EveEmbeddedDocumentSlot> EnumerateEmbeddedDocuments(EveSurfaceComponent component)
|
||||
|
|
@ -1234,15 +1067,11 @@ namespace GameCult.Eve.UnityScene
|
|||
if (string.IsNullOrWhiteSpace(interaction.AssetManifestRecordRef))
|
||||
return;
|
||||
|
||||
var catalog = _snapshot!
|
||||
.FetchDocumentsAsync<EveAssetCatalogDocument>(
|
||||
recordKeys: new[] { interaction.AssetManifestRecordRef },
|
||||
schemaIds: new[] { EveAssetCatalogDocument.SchemaId })
|
||||
var catalog = _meshClient!
|
||||
.ReadAsync<EveAssetCatalogDocument>(_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<CultMeshCdnArtifactManifest>(
|
||||
recordKeys: new[] { uri },
|
||||
schemaIds: new[] { CultMeshCdnSchemaVersions.ArtifactManifest })
|
||||
var descriptor = _meshClient!.ReadAsync<CultMeshCdnArtifactManifest>(_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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<string>())
|
||||
.Select(endpoint => new { Verse = verse, Endpoint = endpoint }))
|
||||
.Where(candidate => !string.IsNullOrWhiteSpace(candidate.Endpoint))
|
||||
.Where(verse => !string.IsNullOrWhiteSpace(verse.VerseId) &&
|
||||
(verse.DiscoveryEndpoints ?? Array.Empty<string>()).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<string>();
|
||||
var observed = new List<string>();
|
||||
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<EveProviderAdvertisementDocument>(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<EveProviderAdvertisementDocument[]> 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<EveProviderAdvertisementDocument>()).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue