Route Eve Unity through stable CultMesh identity

This commit is contained in:
Meta Vens 2026-08-17 21:02:01 +02:00
commit 9833d40172
14 changed files with 257 additions and 377 deletions

View file

@ -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

View file

@ -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",

View file

@ -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",

View file

@ -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": {

View file

@ -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

View file

@ -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.

View file

@ -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(

View file

@ -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);

View file

@ -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();
}
}
}

View file

@ -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",

View file

@ -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]

View file

@ -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": {

View file

@ -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
}

View file

@ -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
}