Prepare CultMesh providers off the Unity main thread
This commit is contained in:
parent
603e998b30
commit
f75468e7f4
6 changed files with 77 additions and 20 deletions
|
|
@ -57,8 +57,12 @@ public sealed class GenericEveUnityLauncher : MonoBehaviour
|
|||
while (true)
|
||||
{
|
||||
var connected = false;
|
||||
var preparation = _provider.PrepareAsync();
|
||||
while (!preparation.IsCompleted)
|
||||
yield return null;
|
||||
try
|
||||
{
|
||||
preparation.GetAwaiter().GetResult();
|
||||
var presentation = _bootstrap.Mount();
|
||||
LowerSurface(_provider.CurrentDocument);
|
||||
Debug.Log($"Connected generic Eve client: {_provider.Selection?.ProviderId} / {_provider.Selection?.SurfaceId} / {presentation.ActiveEntities} entities");
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ 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.
|
||||
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.
|
||||
|
||||
The package never imports provider product assemblies.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.IO;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Eve.PluginFields;
|
||||
using GameCult.Eve.UnityScene.Fields;
|
||||
|
|
@ -36,6 +37,7 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
private EveUnityCultMeshLiveProviderTransport? _transport;
|
||||
private EveUnitySceneLiveProviderBridge? _bridge;
|
||||
private Task? _preparation;
|
||||
private readonly ConcurrentQueue<EntityViewLease> _pendingEntityViews = new ConcurrentQueue<EntityViewLease>();
|
||||
private readonly ConcurrentQueue<EveFieldsSplatsDocument> _pendingFields = new ConcurrentQueue<EveFieldsSplatsDocument>();
|
||||
|
||||
|
|
@ -53,7 +55,7 @@ namespace GameCult.Eve.UnityScene
|
|||
{
|
||||
get
|
||||
{
|
||||
EnsureTransport();
|
||||
RequirePrepared();
|
||||
return _transport!.CurrentInputCapability;
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +64,7 @@ namespace GameCult.Eve.UnityScene
|
|||
{
|
||||
get
|
||||
{
|
||||
EnsureTransport();
|
||||
RequirePrepared();
|
||||
return _transport!.CurrentAssetBodyTransportKind;
|
||||
}
|
||||
}
|
||||
|
|
@ -88,7 +90,7 @@ namespace GameCult.Eve.UnityScene
|
|||
string requiredSurfaceKind = "interactive-world",
|
||||
string clientRuntimeId = "eve-unity")
|
||||
{
|
||||
if (_bridge != null)
|
||||
if (_bridge != null || _preparation != null)
|
||||
throw new InvalidOperationException("Disconnect the active provider before changing discovery configuration.");
|
||||
|
||||
rendezvousEndpoint = endpoint ?? "";
|
||||
|
|
@ -105,6 +107,15 @@ namespace GameCult.Eve.UnityScene
|
|||
Bridge.Connect();
|
||||
}
|
||||
|
||||
public Task PrepareAsync()
|
||||
{
|
||||
if (_bridge != null)
|
||||
return Task.CompletedTask;
|
||||
if (_preparation == null || _preparation.IsCanceled || _preparation.IsFaulted)
|
||||
_preparation = PrepareTransportAsync();
|
||||
return _preparation;
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
_bridge?.Disconnect();
|
||||
|
|
@ -125,13 +136,13 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset)
|
||||
{
|
||||
EnsureTransport();
|
||||
RequirePrepared();
|
||||
return _transport!.ResolvePrefab(asset);
|
||||
}
|
||||
|
||||
public UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType)
|
||||
{
|
||||
EnsureTransport();
|
||||
RequirePrepared();
|
||||
return _transport!.ResolveAsset(asset, assetType);
|
||||
}
|
||||
|
||||
|
|
@ -139,13 +150,13 @@ namespace GameCult.Eve.UnityScene
|
|||
EveUnityPlayableWorldAssetBinding asset,
|
||||
out IReadOnlyDictionary<string, string> metadata)
|
||||
{
|
||||
EnsureTransport();
|
||||
RequirePrepared();
|
||||
return _transport!.TryResolveAssetMetadata(asset, out metadata);
|
||||
}
|
||||
|
||||
public bool TryGetRenderChannelLayer(string channel, out int layer)
|
||||
{
|
||||
EnsureTransport();
|
||||
RequirePrepared();
|
||||
return _transport!.TryGetRenderChannelLayer(channel, out layer);
|
||||
}
|
||||
|
||||
|
|
@ -157,6 +168,7 @@ namespace GameCult.Eve.UnityScene
|
|||
while (_pendingFields.TryDequeue(out _)) { }
|
||||
_bridge = null;
|
||||
_transport = null;
|
||||
_preparation = null;
|
||||
Selection = null;
|
||||
}
|
||||
|
||||
|
|
@ -184,19 +196,19 @@ namespace GameCult.Eve.UnityScene
|
|||
{
|
||||
get
|
||||
{
|
||||
EnsureTransport();
|
||||
RequirePrepared();
|
||||
return _bridge!;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureTransport()
|
||||
private async Task PrepareTransportAsync()
|
||||
{
|
||||
if (_transport != null)
|
||||
return;
|
||||
if (string.IsNullOrWhiteSpace(rendezvousEndpoint))
|
||||
throw new InvalidOperationException("EveUnity requires a CultMesh rendezvous endpoint.");
|
||||
|
||||
Selection = new EveUnityCultMeshProviderDiscovery().Discover(
|
||||
Selection = await new EveUnityCultMeshProviderDiscovery().DiscoverAsync(
|
||||
rendezvousEndpoint,
|
||||
providerFilter,
|
||||
surfaceFilter,
|
||||
|
|
@ -216,6 +228,13 @@ namespace GameCult.Eve.UnityScene
|
|||
_bridge = new EveUnitySceneLiveProviderBridge(_transport);
|
||||
}
|
||||
|
||||
private void RequirePrepared()
|
||||
{
|
||||
if (_transport == null || _bridge == null)
|
||||
throw new InvalidOperationException(
|
||||
"The EveUnity CultMesh provider is not prepared. Await PrepareAsync before mounting the playable world.");
|
||||
}
|
||||
|
||||
private sealed class EntityViewLease
|
||||
{
|
||||
public EntityViewLease(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,15 @@ namespace GameCult.Eve.UnityScene
|
|||
};
|
||||
|
||||
public EveUnityCultMeshProviderSelection Discover(
|
||||
string rendezvousEndpoint,
|
||||
string providerId = "",
|
||||
string surfaceId = "",
|
||||
string surfaceKind = "interactive-world",
|
||||
string verseId = "") =>
|
||||
DiscoverAsync(rendezvousEndpoint, providerId, surfaceId, surfaceKind, verseId)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
public async Task<EveUnityCultMeshProviderSelection> DiscoverAsync(
|
||||
string rendezvousEndpoint,
|
||||
string providerId = "",
|
||||
string surfaceId = "",
|
||||
|
|
@ -50,13 +59,13 @@ namespace GameCult.Eve.UnityScene
|
|||
if (string.IsNullOrWhiteSpace(rendezvousEndpoint))
|
||||
throw new ArgumentException("Rendezvous endpoint must be non-empty.", nameof(rendezvousEndpoint));
|
||||
|
||||
var response = RunNetwork(() => CultMesh.CreateVerseDiscoveryClient().FetchAsync(
|
||||
var response = await CultMesh.CreateVerseDiscoveryClient().FetchAsync(
|
||||
rendezvousEndpoint,
|
||||
new CultMeshVerseCatalogRequestMessage
|
||||
{
|
||||
VerseIds = string.IsNullOrWhiteSpace(verseId) ? null : new[] { verseId },
|
||||
TransportVersion = "cultmesh.v0"
|
||||
}));
|
||||
});
|
||||
|
||||
var candidates = response.Verses
|
||||
.Where(verse => string.IsNullOrWhiteSpace(verseId) ||
|
||||
|
|
@ -73,7 +82,7 @@ namespace GameCult.Eve.UnityScene
|
|||
{
|
||||
try
|
||||
{
|
||||
var advertisement = FetchAdvertisements(candidate.Endpoint)
|
||||
var advertisement = (await FetchAdvertisementsAsync(candidate.Endpoint))
|
||||
.Where(document => string.IsNullOrWhiteSpace(providerId) ||
|
||||
string.Equals(document.ProviderId, providerId, StringComparison.Ordinal))
|
||||
.Select(document => new
|
||||
|
|
@ -107,7 +116,7 @@ namespace GameCult.Eve.UnityScene
|
|||
throw new InvalidOperationException($"No advertised Eve surface matched {filter}.{detail}");
|
||||
}
|
||||
|
||||
private static EveProviderAdvertisementDocument[] FetchAdvertisements(string endpoint)
|
||||
private static async Task<EveProviderAdvertisementDocument[]> FetchAdvertisementsAsync(string endpoint)
|
||||
{
|
||||
var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(AdvertisementDocumentTypes);
|
||||
var networkRegistry = CultMesh.CreateCultNetDocumentRegistry(AdvertisementDocumentTypes, cacheRegistry);
|
||||
|
|
@ -128,11 +137,7 @@ namespace GameCult.Eve.UnityScene
|
|||
RudpMaxFragmentBytes = 1024
|
||||
}
|
||||
});
|
||||
return RunNetwork(() => snapshot.FetchDocumentsAsync<EveProviderAdvertisementDocument>())
|
||||
.ToArray();
|
||||
return (await snapshot.FetchDocumentsAsync<EveProviderAdvertisementDocument>()).ToArray();
|
||||
}
|
||||
|
||||
private static T RunNetwork<T>(Func<Task<T>> operation) =>
|
||||
Task.Run(operation).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,38 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using GameCult.Eve.UnityScene;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameCult.Eve.UnityScene.Tests
|
||||
{
|
||||
public sealed class EveUnityCultMeshAssetLookupTests
|
||||
{
|
||||
[Test]
|
||||
public void UnpreparedProviderGetterFailsFastWithoutStartingNetworkDiscovery()
|
||||
{
|
||||
var gameObject = new GameObject("unprepared-eve-provider");
|
||||
try
|
||||
{
|
||||
var provider = gameObject.AddComponent<EveUnityCultMeshPlayableWorldProvider>();
|
||||
provider.Configure("rudp://127.0.0.1:1");
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
var error = Assert.Throws<InvalidOperationException>(() => _ = provider.CurrentInputCapability);
|
||||
|
||||
stopwatch.Stop();
|
||||
StringAssert.Contains("Await PrepareAsync", error!.Message);
|
||||
Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(100));
|
||||
Assert.That(provider.Selection, Is.Null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResolvesUnityNormalizedBundleNamesWithoutChangingLogicalAssetIdentity()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "org.gamecult.eve.unity-scene",
|
||||
"displayName": "Eve Unity Scene Lowering",
|
||||
"version": "0.3.67",
|
||||
"version": "0.3.68",
|
||||
"unity": "6000.0",
|
||||
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
|
||||
"dependencies": {
|
||||
|
|
|
|||
Reference in a new issue