Stage Eve navigation presentation before commit
This commit is contained in:
parent
ccc5a2605c
commit
49f49203c8
4 changed files with 263 additions and 23 deletions
|
|
@ -179,6 +179,8 @@ namespace GameCult.Eve.UnityScene
|
|||
var previous = _previousProvider;
|
||||
if (previous == null) return;
|
||||
_previousProvider = null;
|
||||
if (previous.Bridge != null)
|
||||
previous.Bridge.ReceiptAvailable -= ForwardReceipt;
|
||||
previous.Bridge?.Dispose();
|
||||
previous.Transport?.Dispose();
|
||||
}
|
||||
|
|
@ -188,6 +190,8 @@ namespace GameCult.Eve.UnityScene
|
|||
var previous = _previousProvider;
|
||||
if (previous == null) return;
|
||||
_previousProvider = null;
|
||||
if (previous.Bridge != null)
|
||||
previous.Bridge.ReceiptAvailable -= ForwardReceipt;
|
||||
DetachPrepared(_transport, _bridge);
|
||||
_bridge?.Dispose();
|
||||
_transport?.Dispose();
|
||||
|
|
@ -209,15 +213,19 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
public void Refresh()
|
||||
{
|
||||
if (Bridge.IsConnected)
|
||||
Bridge.Refresh();
|
||||
var activeBridge = _previousProvider?.Bridge ?? Bridge;
|
||||
if (activeBridge.IsConnected)
|
||||
activeBridge.Refresh();
|
||||
else
|
||||
Bridge.Connect();
|
||||
activeBridge.Connect();
|
||||
}
|
||||
|
||||
public void Submit(EveSurfaceCommandRequest request)
|
||||
{
|
||||
Bridge.Submit(request);
|
||||
// Navigation preparation is not a command-routing commit. The mounted
|
||||
// presentation continues to target its old provider until the visual
|
||||
// candidate has been lowered and accepted by the bootstrap.
|
||||
(_previousProvider?.Bridge ?? Bridge).Submit(request);
|
||||
}
|
||||
|
||||
public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset)
|
||||
|
|
@ -419,6 +427,8 @@ namespace GameCult.Eve.UnityScene
|
|||
surfaceKind,
|
||||
_authorityTrust);
|
||||
DetachPrepared(_transport, _bridge);
|
||||
if (_previousProvider.Bridge != null)
|
||||
_previousProvider.Bridge.ReceiptAvailable += ForwardReceipt;
|
||||
Selection = prepared.Selection;
|
||||
_transport = prepared.Transport;
|
||||
_bridge = prepared.Bridge;
|
||||
|
|
|
|||
|
|
@ -81,31 +81,26 @@ namespace GameCult.Eve.UnityScene
|
|||
yield break;
|
||||
_navigationInProgress = true;
|
||||
var navigation = navigable.NavigateAsync(target);
|
||||
var mountAttempted = false;
|
||||
StagedPresentation? staged = null;
|
||||
PresentationSwap? swap = null;
|
||||
while (!navigation.IsCompleted)
|
||||
yield return null;
|
||||
try
|
||||
{
|
||||
navigation.GetAwaiter().GetResult();
|
||||
mountAttempted = true;
|
||||
Mount();
|
||||
staged = PrepareStagedPresentation();
|
||||
swap = ActivateStagedPresentation(staged);
|
||||
navigable.CommitNavigation();
|
||||
swap.Complete();
|
||||
swap = null;
|
||||
staged = null;
|
||||
LastNavigationFailure = null;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
swap?.Rollback();
|
||||
staged?.Dispose();
|
||||
navigable.RollbackNavigation();
|
||||
if (mountAttempted)
|
||||
{
|
||||
try
|
||||
{
|
||||
Mount();
|
||||
}
|
||||
catch (Exception restoreError)
|
||||
{
|
||||
Debug.LogError($"Eve provider navigation rollback could not remount the previous surface. {restoreError}");
|
||||
}
|
||||
}
|
||||
var failure = new EveUnitySceneNavigationFailure(target, error.Message, DateTimeOffset.UtcNow);
|
||||
LastNavigationFailure = failure;
|
||||
NavigationFailed?.Invoke(failure);
|
||||
|
|
@ -117,6 +112,231 @@ namespace GameCult.Eve.UnityScene
|
|||
}
|
||||
}
|
||||
|
||||
private StagedPresentation PrepareStagedPresentation()
|
||||
{
|
||||
var resolvedProvider = provider;
|
||||
var surfaceDocuments = ResolveBehaviour<IEveUnitySceneProviderSurfaceDocumentSource>(
|
||||
providerSurfaceDocuments,
|
||||
resolvedProvider,
|
||||
nameof(providerSurfaceDocuments));
|
||||
var resolvedCommandSink = ResolveBehaviour<IEveUnitySceneCommandSink>(
|
||||
commandSink,
|
||||
resolvedProvider,
|
||||
nameof(commandSink));
|
||||
var resolvedAssetDocuments = ResolveOptionalBehaviour<IEveUnityPlayableWorldAssetManifestDocumentSource>(
|
||||
assetManifestDocuments,
|
||||
resolvedProvider);
|
||||
var resolvedReceiptSource = ResolveOptionalBehaviour<IEveUnitySceneCommandReceiptSource>(
|
||||
receiptSource,
|
||||
resolvedProvider);
|
||||
var resolvedFallbackAssetProvider = ResolveOptionalBehaviour<IEveUnityGameObjectAssetProvider>(
|
||||
fallbackAssetProvider,
|
||||
resolvedProvider);
|
||||
|
||||
var stagingObject = new GameObject("Eve Unity Candidate Presentation");
|
||||
stagingObject.SetActive(false);
|
||||
stagingObject.transform.SetParent(transform, false);
|
||||
var stagingRootObject = new GameObject("Eve Unity Candidate Scene Root");
|
||||
stagingRootObject.transform.SetParent(stagingObject.transform, false);
|
||||
var stagingHost = stagingObject.AddComponent<EveUnityPlayableWorldClientHost>();
|
||||
stagingHost.ConnectOnEnable = false;
|
||||
try
|
||||
{
|
||||
stagingHost.Configure(
|
||||
stagingRootObject.transform,
|
||||
surfaceDocuments,
|
||||
resolvedCommandSink,
|
||||
resolvedAssetDocuments,
|
||||
resolvedReceiptSource,
|
||||
resolvedFallbackAssetProvider);
|
||||
var presentation = stagingHost.Connect();
|
||||
return new StagedPresentation(stagingObject, stagingRootObject.transform, stagingHost, presentation);
|
||||
}
|
||||
catch
|
||||
{
|
||||
stagingHost.Disconnect();
|
||||
DestroyObject(stagingObject);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private PresentationSwap ActivateStagedPresentation(StagedPresentation staged)
|
||||
{
|
||||
var previousHost = host;
|
||||
var previousRoot = sceneRoot;
|
||||
var previousPresentation = LastPresentation;
|
||||
var previousRootWasActive = previousRoot == null || previousRoot.gameObject.activeSelf;
|
||||
var input = GetComponent<EveUnityPlayableWorldInputDriver>();
|
||||
var previousInputHost = input?.Host;
|
||||
var previousInputCamera = input?.CameraTransform;
|
||||
var rig = GetComponent<EveUnityPlayableWorldCameraRig>();
|
||||
var previousRigHost = rig?.Host;
|
||||
var previousRigCamera = rig?.CameraTransform;
|
||||
var previousRigPolicy = rig?.RenderPolicySource;
|
||||
|
||||
try
|
||||
{
|
||||
staged.GameObject.SetActive(true);
|
||||
if (attachInputDriver)
|
||||
ConfigureInputDriver(staged.Host);
|
||||
if (attachCameraRig)
|
||||
ConfigureCameraRig(staged.Host);
|
||||
host = staged.Host;
|
||||
sceneRoot = staged.SceneRoot;
|
||||
LastPresentation = staged.Presentation;
|
||||
if (previousRoot != null && !ReferenceEquals(previousRoot, transform) &&
|
||||
!ReferenceEquals(previousRoot, staged.SceneRoot))
|
||||
previousRoot.gameObject.SetActive(false);
|
||||
return new PresentationSwap(
|
||||
this,
|
||||
staged,
|
||||
previousHost,
|
||||
previousRoot,
|
||||
previousPresentation,
|
||||
previousRootWasActive,
|
||||
input,
|
||||
previousInputHost,
|
||||
previousInputCamera,
|
||||
rig,
|
||||
previousRigHost,
|
||||
previousRigCamera,
|
||||
previousRigPolicy);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (input != null)
|
||||
{
|
||||
input.Host = previousInputHost;
|
||||
input.CameraTransform = previousInputCamera;
|
||||
}
|
||||
if (rig != null)
|
||||
{
|
||||
rig.Host = previousRigHost;
|
||||
rig.CameraTransform = previousRigCamera;
|
||||
rig.RenderPolicySource = previousRigPolicy;
|
||||
}
|
||||
host = previousHost;
|
||||
sceneRoot = previousRoot;
|
||||
LastPresentation = previousPresentation;
|
||||
if (previousRoot != null)
|
||||
previousRoot.gameObject.SetActive(previousRootWasActive);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DestroyObject(UnityEngine.Object value)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
Destroy(value);
|
||||
else
|
||||
DestroyImmediate(value);
|
||||
}
|
||||
|
||||
private sealed class StagedPresentation : IDisposable
|
||||
{
|
||||
public StagedPresentation(
|
||||
GameObject gameObject,
|
||||
Transform sceneRoot,
|
||||
EveUnityPlayableWorldClientHost host,
|
||||
EveUnityPlayableWorldPresentation presentation)
|
||||
{
|
||||
GameObject = gameObject;
|
||||
SceneRoot = sceneRoot;
|
||||
Host = host;
|
||||
Presentation = presentation;
|
||||
}
|
||||
|
||||
public GameObject GameObject { get; }
|
||||
public Transform SceneRoot { get; }
|
||||
public EveUnityPlayableWorldClientHost Host { get; }
|
||||
public EveUnityPlayableWorldPresentation Presentation { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Host.Disconnect();
|
||||
DestroyObject(GameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PresentationSwap
|
||||
{
|
||||
private readonly EveUnityPlayableWorldClientBootstrap _owner;
|
||||
private readonly StagedPresentation _candidate;
|
||||
private readonly EveUnityPlayableWorldClientHost? _previousHost;
|
||||
private readonly Transform? _previousRoot;
|
||||
private readonly EveUnityPlayableWorldPresentation? _previousPresentation;
|
||||
private readonly bool _previousRootWasActive;
|
||||
private readonly EveUnityPlayableWorldInputDriver? _input;
|
||||
private readonly EveUnityPlayableWorldClientHost? _previousInputHost;
|
||||
private readonly Transform? _previousInputCamera;
|
||||
private readonly EveUnityPlayableWorldCameraRig? _rig;
|
||||
private readonly EveUnityPlayableWorldClientHost? _previousRigHost;
|
||||
private readonly Transform? _previousRigCamera;
|
||||
private readonly IEveUnityCameraRenderPolicySource? _previousRigPolicy;
|
||||
private bool _settled;
|
||||
|
||||
public PresentationSwap(
|
||||
EveUnityPlayableWorldClientBootstrap owner,
|
||||
StagedPresentation candidate,
|
||||
EveUnityPlayableWorldClientHost? previousHost,
|
||||
Transform? previousRoot,
|
||||
EveUnityPlayableWorldPresentation? previousPresentation,
|
||||
bool previousRootWasActive,
|
||||
EveUnityPlayableWorldInputDriver? input,
|
||||
EveUnityPlayableWorldClientHost? previousInputHost,
|
||||
Transform? previousInputCamera,
|
||||
EveUnityPlayableWorldCameraRig? rig,
|
||||
EveUnityPlayableWorldClientHost? previousRigHost,
|
||||
Transform? previousRigCamera,
|
||||
IEveUnityCameraRenderPolicySource? previousRigPolicy)
|
||||
{
|
||||
_owner = owner;
|
||||
_candidate = candidate;
|
||||
_previousHost = previousHost;
|
||||
_previousRoot = previousRoot;
|
||||
_previousPresentation = previousPresentation;
|
||||
_previousRootWasActive = previousRootWasActive;
|
||||
_input = input;
|
||||
_previousInputHost = previousInputHost;
|
||||
_previousInputCamera = previousInputCamera;
|
||||
_rig = rig;
|
||||
_previousRigHost = previousRigHost;
|
||||
_previousRigCamera = previousRigCamera;
|
||||
_previousRigPolicy = previousRigPolicy;
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
if (_settled) return;
|
||||
if (_previousHost != null && !ReferenceEquals(_previousHost, _candidate.Host))
|
||||
_previousHost.Disconnect();
|
||||
_settled = true;
|
||||
}
|
||||
|
||||
public void Rollback()
|
||||
{
|
||||
if (_settled) return;
|
||||
_settled = true;
|
||||
_owner.host = _previousHost;
|
||||
_owner.sceneRoot = _previousRoot;
|
||||
_owner.LastPresentation = _previousPresentation;
|
||||
if (_input != null)
|
||||
{
|
||||
_input.Host = _previousInputHost;
|
||||
_input.CameraTransform = _previousInputCamera;
|
||||
}
|
||||
if (_rig != null)
|
||||
{
|
||||
_rig.Host = _previousRigHost;
|
||||
_rig.CameraTransform = _previousRigCamera;
|
||||
_rig.RenderPolicySource = _previousRigPolicy;
|
||||
}
|
||||
if (_previousRoot != null)
|
||||
_previousRoot.gameObject.SetActive(_previousRootWasActive);
|
||||
_candidate.GameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldPresentation Mount()
|
||||
{
|
||||
var resolvedHost = ResolveHost();
|
||||
|
|
|
|||
|
|
@ -1171,6 +1171,7 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
var bootstrap = hostObject.AddComponent<EveUnityPlayableWorldClientBootstrap>();
|
||||
bootstrap.ConfigureProvider(provider);
|
||||
bootstrap.Mount();
|
||||
var originalHost = bootstrap.Host;
|
||||
|
||||
provider.PublishReceipt(new EveUnitySceneCommandReceipt(
|
||||
"receipt:launch",
|
||||
|
|
@ -1192,7 +1193,8 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
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));
|
||||
Assert.That(bootstrap.Host, Is.Not.SameAs(originalHost));
|
||||
Assert.That(bootstrap.Host!.ConnectionEpoch, Is.EqualTo(1));
|
||||
Assert.That(provider.CommitNavigationCount, Is.EqualTo(1));
|
||||
Assert.That(provider.RollbackNavigationCount, Is.Zero);
|
||||
}
|
||||
|
|
@ -1203,7 +1205,7 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator PlayableWorldClientBootstrapRollsBackAndRemountsAfterNavigationMountFailure()
|
||||
public IEnumerator PlayableWorldClientBootstrapKeepsMountedPresentationAfterNavigationMountFailure()
|
||||
{
|
||||
var hostObject = new GameObject("generic-eve-navigation-rollback-client");
|
||||
try
|
||||
|
|
@ -1222,6 +1224,10 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
var bootstrap = hostObject.AddComponent<EveUnityPlayableWorldClientBootstrap>();
|
||||
bootstrap.ConfigureProvider(provider);
|
||||
bootstrap.Mount();
|
||||
var originalHost = bootstrap.Host;
|
||||
var originalRuntime = originalHost!.Runtime;
|
||||
var originalRoot = bootstrap.SceneRoot;
|
||||
var originalPresentation = bootstrap.LastPresentation;
|
||||
provider.FailNextNavigationMount = true;
|
||||
LogAssert.Expect(LogType.Error, new Regex("Eve provider navigation failed; the current surface remains mounted"));
|
||||
|
||||
|
|
@ -1245,10 +1251,14 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
Assert.That(provider.CommitNavigationCount, Is.Zero);
|
||||
Assert.That(provider.RollbackNavigationCount, Is.EqualTo(1));
|
||||
Assert.That(bootstrap.LastNavigationFailure, Is.Not.Null);
|
||||
Assert.That(bootstrap.Host, Is.SameAs(originalHost));
|
||||
Assert.That(bootstrap.Host!.Runtime, Is.SameAs(originalRuntime));
|
||||
Assert.That(bootstrap.SceneRoot, Is.SameAs(originalRoot));
|
||||
Assert.That(bootstrap.LastPresentation, Is.SameAs(originalPresentation));
|
||||
Assert.That(bootstrap.Host!.Runtime, Is.Not.Null);
|
||||
Assert.That(bootstrap.Host.ActiveWorld, Is.Not.Null);
|
||||
Assert.That(bootstrap.Host.ActiveWorld!.PlayerEntityId, Is.EqualTo("player-vanguard"));
|
||||
Assert.That(bootstrap.Host.ConnectionEpoch, Is.EqualTo(2));
|
||||
Assert.That(bootstrap.Host.ConnectionEpoch, Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "org.gamecult.eve.unity-scene",
|
||||
"displayName": "Eve Unity Scene Lowering",
|
||||
"version": "0.3.113",
|
||||
"version": "0.3.114",
|
||||
"unity": "6000.0",
|
||||
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
|
||||
"dependencies": {
|
||||
"org.gamecult.cultlib": "1.0.50",
|
||||
"org.gamecult.cultlib": "1.0.51",
|
||||
"org.gamecult.eve.plugin-fields": "0.2.3",
|
||||
"org.gamecult.eve.surface": "0.3.3",
|
||||
"org.gamecult.eve.unity-uitoolkit": "0.1.6",
|
||||
|
|
|
|||
Reference in a new issue