Follow provider-owned Verse navigation
This commit is contained in:
parent
000917fa63
commit
c6b7ecebc5
8 changed files with 200 additions and 11 deletions
|
|
@ -1389,7 +1389,15 @@ namespace GameCult.Eve.UnityScene
|
|||
receipt.SurfaceId,
|
||||
receipt.Message,
|
||||
DateTimeOffset.TryParse(receipt.IssuedAtUtc, out var issuedAt) ? issuedAt : null,
|
||||
receipt.SourceVersion));
|
||||
receipt.SourceVersion,
|
||||
receipt.Navigation == null
|
||||
? null
|
||||
: new EveUnitySceneNavigationTarget(
|
||||
receipt.Navigation.VerseId,
|
||||
receipt.Navigation.ProviderId,
|
||||
receipt.Navigation.SurfaceId,
|
||||
receipt.Navigation.SurfaceKind,
|
||||
receipt.Navigation.RendezvousEndpoints)));
|
||||
if (string.Equals(receipt.State, "accepted", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(receipt.State, "denied", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(receipt.State, "reconciled", StringComparison.OrdinalIgnoreCase))
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.IO;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Stopwatch = System.Diagnostics.Stopwatch;
|
||||
using GameCult.Eve.Surface;
|
||||
|
|
@ -27,7 +28,8 @@ namespace GameCult.Eve.UnityScene
|
|||
IEveUnityCameraRenderPolicySource,
|
||||
IEveUnityNativeAssetProvider,
|
||||
IEveUnityNativeAssetMetadataProvider,
|
||||
IEveUnityInputCapabilitySource
|
||||
IEveUnityInputCapabilitySource,
|
||||
IEveUnityNavigableProvider
|
||||
{
|
||||
[SerializeField] private string rendezvousEndpoint = "";
|
||||
[SerializeField] private string providerFilter = "";
|
||||
|
|
@ -116,6 +118,26 @@ namespace GameCult.Eve.UnityScene
|
|||
_bridge?.Disconnect();
|
||||
}
|
||||
|
||||
public async Task NavigateAsync(EveUnitySceneNavigationTarget target)
|
||||
{
|
||||
if (target == null) throw new ArgumentNullException(nameof(target));
|
||||
if (string.IsNullOrWhiteSpace(target.VerseId))
|
||||
throw new ArgumentException("Eve provider navigation requires a stable Verse identity.", nameof(target));
|
||||
if (string.IsNullOrWhiteSpace(target.SurfaceId))
|
||||
throw new ArgumentException("Eve provider navigation requires a surface identity.", nameof(target));
|
||||
|
||||
ReleaseTransport();
|
||||
var navigationEndpoint = target.RendezvousEndpoints
|
||||
.FirstOrDefault(endpoint => !string.IsNullOrWhiteSpace(endpoint));
|
||||
if (!string.IsNullOrWhiteSpace(navigationEndpoint))
|
||||
rendezvousEndpoint = navigationEndpoint;
|
||||
verseFilter = target.VerseId;
|
||||
providerFilter = target.ProviderId;
|
||||
surfaceFilter = target.SurfaceId;
|
||||
surfaceKind = string.IsNullOrWhiteSpace(target.SurfaceKind) ? "interactive-world" : target.SurfaceKind;
|
||||
await PrepareAsync();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (Bridge.IsConnected)
|
||||
|
|
@ -156,6 +178,11 @@ namespace GameCult.Eve.UnityScene
|
|||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
ReleaseTransport();
|
||||
}
|
||||
|
||||
private void ReleaseTransport()
|
||||
{
|
||||
if (_bridge != null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
return new EveUnityCultMeshProviderSelection(
|
||||
rendezvousEndpoint,
|
||||
advertisement.Document.ProviderId,
|
||||
candidate.VerseId,
|
||||
candidate.VerseId,
|
||||
advertisement.Document.ProviderId,
|
||||
advertisement.Surface.SurfaceId,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
|
@ -29,13 +30,64 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
public EveUnityPlayableWorldPresentation? LastPresentation { get; private set; }
|
||||
|
||||
private IEveUnitySceneCommandReceiptSource? _navigationReceiptSource;
|
||||
private bool _navigationInProgress;
|
||||
|
||||
public void ConfigureProvider(MonoBehaviour providerComponent)
|
||||
{
|
||||
UnbindNavigation();
|
||||
provider = providerComponent != null ? providerComponent : throw new ArgumentNullException(nameof(providerComponent));
|
||||
providerSurfaceDocuments = provider;
|
||||
commandSink = provider;
|
||||
assetManifestDocuments = provider;
|
||||
receiptSource = provider;
|
||||
BindNavigation();
|
||||
}
|
||||
|
||||
private void OnDestroy() => UnbindNavigation();
|
||||
|
||||
private void BindNavigation()
|
||||
{
|
||||
if (provider is not IEveUnityNavigableProvider || provider is not IEveUnitySceneCommandReceiptSource source)
|
||||
return;
|
||||
_navigationReceiptSource = source;
|
||||
source.ReceiptAvailable += OnReceiptAvailable;
|
||||
}
|
||||
|
||||
private void UnbindNavigation()
|
||||
{
|
||||
if (_navigationReceiptSource != null)
|
||||
_navigationReceiptSource.ReceiptAvailable -= OnReceiptAvailable;
|
||||
_navigationReceiptSource = null;
|
||||
}
|
||||
|
||||
private void OnReceiptAvailable(EveUnitySceneCommandReceipt receipt)
|
||||
{
|
||||
if (_navigationInProgress || receipt.Navigation == null)
|
||||
return;
|
||||
if (!string.Equals(receipt.State, "accepted", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(receipt.State, "reconciled", StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
StartCoroutine(NavigateAsync(receipt.Navigation));
|
||||
}
|
||||
|
||||
private IEnumerator NavigateAsync(EveUnitySceneNavigationTarget target)
|
||||
{
|
||||
if (provider is not IEveUnityNavigableProvider navigable)
|
||||
yield break;
|
||||
_navigationInProgress = true;
|
||||
var navigation = navigable.NavigateAsync(target);
|
||||
while (!navigation.IsCompleted)
|
||||
yield return null;
|
||||
try
|
||||
{
|
||||
navigation.GetAwaiter().GetResult();
|
||||
Mount();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_navigationInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldPresentation Mount()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Eve.Surface;
|
||||
|
||||
#nullable enable
|
||||
|
|
@ -234,6 +235,34 @@ namespace GameCult.Eve.UnityScene
|
|||
event Action<EveUnitySceneCommandReceipt> ReceiptAvailable;
|
||||
}
|
||||
|
||||
public interface IEveUnityNavigableProvider
|
||||
{
|
||||
Task NavigateAsync(EveUnitySceneNavigationTarget target);
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneNavigationTarget
|
||||
{
|
||||
public EveUnitySceneNavigationTarget(
|
||||
string verseId,
|
||||
string providerId,
|
||||
string surfaceId,
|
||||
string surfaceKind,
|
||||
IReadOnlyList<string>? rendezvousEndpoints = null)
|
||||
{
|
||||
VerseId = verseId ?? "";
|
||||
ProviderId = providerId ?? "";
|
||||
SurfaceId = surfaceId ?? "";
|
||||
SurfaceKind = surfaceKind ?? "";
|
||||
RendezvousEndpoints = rendezvousEndpoints ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
public string VerseId { get; }
|
||||
public string ProviderId { get; }
|
||||
public string SurfaceId { get; }
|
||||
public string SurfaceKind { get; }
|
||||
public IReadOnlyList<string> RendezvousEndpoints { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneCommandReceipt
|
||||
{
|
||||
public EveUnitySceneCommandReceipt(
|
||||
|
|
@ -248,7 +277,8 @@ namespace GameCult.Eve.UnityScene
|
|||
string surfaceId = "",
|
||||
string message = "",
|
||||
DateTimeOffset? issuedAtUtc = null,
|
||||
long sourceVersion = 0)
|
||||
long sourceVersion = 0,
|
||||
EveUnitySceneNavigationTarget? navigation = null)
|
||||
{
|
||||
ReceiptId = receiptId ?? "";
|
||||
Command = command ?? "";
|
||||
|
|
@ -262,6 +292,7 @@ namespace GameCult.Eve.UnityScene
|
|||
Message = message ?? "";
|
||||
IssuedAtUtc = issuedAtUtc;
|
||||
SourceVersion = sourceVersion;
|
||||
Navigation = navigation;
|
||||
}
|
||||
|
||||
public string Schema { get; }
|
||||
|
|
@ -288,6 +319,8 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
public long SourceVersion { get; }
|
||||
|
||||
public EveUnitySceneNavigationTarget? Navigation { get; }
|
||||
|
||||
public bool IsProviderOwned => !string.IsNullOrWhiteSpace(OwnerRepo) && !string.Equals(OwnerRepo, "EveUnity", StringComparison.Ordinal);
|
||||
|
||||
public bool ShouldRefreshProviderSurface =>
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
{
|
||||
var selection = new EveUnityCultMeshProviderSelection(
|
||||
"cultnet+tcp://odin.example:3075",
|
||||
"aetheria.daemon",
|
||||
"aetheria.public",
|
||||
"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.EndpointId, Is.EqualTo("aetheria.public"));
|
||||
Assert.That(selection.VerseId, Is.EqualTo("aetheria.public"));
|
||||
Assert.That(typeof(EveUnityCultMeshProviderSelection).GetProperty("Endpoint"), Is.Null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Collections;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Eve.PluginFields;
|
||||
using GameCult.Eve.Surface;
|
||||
|
|
@ -12,6 +14,7 @@ using UnityEngine;
|
|||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
#nullable enable
|
||||
|
||||
|
|
@ -912,11 +915,19 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
"aetheria.daemon.move_intent",
|
||||
"accepted",
|
||||
"Aetheria",
|
||||
"provider-owned-daemon"));
|
||||
"provider-owned-daemon",
|
||||
navigation: new EveUnitySceneNavigationTarget(
|
||||
"gamecult.aetheria",
|
||||
"aetheria",
|
||||
"aetheria.pilot",
|
||||
"interactive-world")));
|
||||
|
||||
Assert.That(runtime.LastReceipt, Is.Not.Null);
|
||||
Assert.That(runtime.LastReceipt!.IsProviderOwned, Is.True);
|
||||
Assert.That(runtime.LastReceipt.ShouldRefreshProviderSurface, Is.True);
|
||||
Assert.That(runtime.LastReceipt.Navigation, Is.Not.Null);
|
||||
Assert.That(runtime.LastReceipt.Navigation!.VerseId, Is.EqualTo("gamecult.aetheria"));
|
||||
Assert.That(runtime.LastReceipt.Navigation.SurfaceId, Is.EqualTo("aetheria.pilot"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -1134,6 +1145,55 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator PlayableWorldClientBootstrapFollowsProviderOwnedReceiptNavigation()
|
||||
{
|
||||
var hostObject = new GameObject("generic-eve-navigation-client");
|
||||
try
|
||||
{
|
||||
var provider = hostObject.AddComponent<FakePlayableWorldProviderComponent>();
|
||||
provider.Set(
|
||||
new EveUnitySceneProviderSurfaceDocument(
|
||||
PlayableArpgDocument(),
|
||||
Advertisement("aetheria.daemon.game"),
|
||||
"cultmesh://aetheria/eve/surfaces/aetheria.daemon.game",
|
||||
1),
|
||||
new EveUnityPlayableWorldAssetManifestDocument(
|
||||
"cultmesh://aetheria/assets/manifest",
|
||||
Array.Empty<EveUnityPlayableWorldAssetManifestDocumentEntry>(),
|
||||
"aetheria"));
|
||||
var bootstrap = hostObject.AddComponent<EveUnityPlayableWorldClientBootstrap>();
|
||||
bootstrap.ConfigureProvider(provider);
|
||||
bootstrap.Mount();
|
||||
|
||||
provider.PublishReceipt(new EveUnitySceneCommandReceipt(
|
||||
"receipt:launch",
|
||||
"aetheria.hangar.launch",
|
||||
"launch",
|
||||
"accepted",
|
||||
"Aetheria",
|
||||
"commander-daemon",
|
||||
navigation: new EveUnitySceneNavigationTarget(
|
||||
"gamecult.aetheria",
|
||||
"aetheria.daemon",
|
||||
"aetheria.pilot",
|
||||
"interactive-world",
|
||||
new[] { "cultnet+tcp://odin.example:3076" })));
|
||||
|
||||
yield return null;
|
||||
yield return null;
|
||||
|
||||
Assert.That(provider.LastNavigation, Is.Not.Null);
|
||||
Assert.That(provider.LastNavigation!.VerseId, Is.EqualTo("gamecult.aetheria"));
|
||||
Assert.That(provider.LastNavigation.RendezvousEndpoints, Is.EqualTo(new[] { "cultnet+tcp://odin.example:3076" }));
|
||||
Assert.That(bootstrap.Host!.ConnectionEpoch, Is.EqualTo(2));
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(hostObject);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PlayableWorldClientHostSubmitsProviderOwnedMoveVector()
|
||||
{
|
||||
|
|
@ -3219,12 +3279,15 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
IEveUnitySceneCommandSink,
|
||||
IEveUnitySceneCommandReceiptSource,
|
||||
IEveUnityProviderRefreshSource,
|
||||
IEveUnityInputCapabilitySource
|
||||
IEveUnityInputCapabilitySource,
|
||||
IEveUnityNavigableProvider
|
||||
{
|
||||
public List<EveSurfaceCommandRequest> Submitted { get; } = new List<EveSurfaceCommandRequest>();
|
||||
|
||||
public int RefreshCount { get; private set; }
|
||||
|
||||
public EveUnitySceneNavigationTarget? LastNavigation { get; private set; }
|
||||
|
||||
public string SinkKind => "fake-provider-command-sink";
|
||||
|
||||
public string ManifestRef => CurrentDocument.ManifestRef;
|
||||
|
|
@ -3286,6 +3349,12 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
{
|
||||
ReceiptAvailable?.Invoke(receipt);
|
||||
}
|
||||
|
||||
public Task NavigateAsync(EveUnitySceneNavigationTarget target)
|
||||
{
|
||||
LastNavigation = target;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"name": "org.gamecult.eve.unity-scene",
|
||||
"displayName": "Eve Unity Scene Lowering",
|
||||
"version": "0.3.104",
|
||||
"version": "0.3.105",
|
||||
"unity": "6000.0",
|
||||
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
|
||||
"dependencies": {
|
||||
"org.gamecult.cultlib": "1.0.45",
|
||||
"org.gamecult.cultlib": "1.0.46",
|
||||
"org.gamecult.eve.plugin-fields": "0.2.3",
|
||||
"org.gamecult.eve.surface": "0.3.0",
|
||||
"org.gamecult.eve.surface": "0.3.1",
|
||||
"org.gamecult.eve.unity-uitoolkit": "0.1.3",
|
||||
"com.unity.mathematics": "1.3.2",
|
||||
"com.unity.render-pipelines.universal": "17.4.0",
|
||||
|
|
|
|||
Reference in a new issue