Make Eve navigation one rollbackable presentation transaction
This commit is contained in:
parent
07f1df9cf4
commit
7ba4041320
5 changed files with 112 additions and 16 deletions
|
|
@ -48,6 +48,7 @@ namespace GameCult.Eve.UnityScene
|
|||
private EveUnityCultMeshLiveProviderTransport? _transport;
|
||||
private EveUnitySceneLiveProviderBridge? _bridge;
|
||||
private PreviousProvider? _previousProvider;
|
||||
private bool _navigationRouteCommitted;
|
||||
private Task? _preparation;
|
||||
private CancellationTokenSource? _preparationLifetime;
|
||||
private readonly ConcurrentQueue<EntityViewLease> _pendingEntityViews = new ConcurrentQueue<EntityViewLease>();
|
||||
|
|
@ -178,24 +179,32 @@ namespace GameCult.Eve.UnityScene
|
|||
{
|
||||
var previous = _previousProvider;
|
||||
if (previous == null) return;
|
||||
_previousProvider = null;
|
||||
if (_navigationRouteCommitted) return;
|
||||
if (previous.Bridge != null)
|
||||
previous.Bridge.ReceiptAvailable -= ForwardReceipt;
|
||||
AttachPrepared(_transport, _bridge);
|
||||
previous.Bridge?.Dispose();
|
||||
previous.Transport?.Dispose();
|
||||
_navigationRouteCommitted = true;
|
||||
}
|
||||
|
||||
public void FinalizeNavigation()
|
||||
{
|
||||
var previous = _previousProvider;
|
||||
if (previous == null) return;
|
||||
_previousProvider = null;
|
||||
_navigationRouteCommitted = false;
|
||||
SafeDispose(previous.Bridge);
|
||||
SafeDispose(previous.Transport);
|
||||
}
|
||||
|
||||
public void RollbackNavigation()
|
||||
{
|
||||
var previous = _previousProvider;
|
||||
if (previous == null) return;
|
||||
_previousProvider = null;
|
||||
if (previous.Bridge != null)
|
||||
previous.Bridge.ReceiptAvailable -= ForwardReceipt;
|
||||
DetachPrepared(_transport, _bridge);
|
||||
_bridge?.Dispose();
|
||||
_transport?.Dispose();
|
||||
var rejectedBridge = _bridge;
|
||||
var rejectedTransport = _transport;
|
||||
while (_pendingEntityViews.TryDequeue(out var pending)) pending.Lease.Dispose();
|
||||
while (_pendingFields.TryDequeue(out _)) { }
|
||||
|
||||
|
|
@ -209,12 +218,18 @@ namespace GameCult.Eve.UnityScene
|
|||
surfaceKind = previous.SurfaceKind;
|
||||
_authorityTrust = previous.AuthorityTrust;
|
||||
AttachPrepared(_transport, _bridge);
|
||||
_previousProvider = null;
|
||||
_navigationRouteCommitted = false;
|
||||
_preparation = Task.CompletedTask;
|
||||
SafeDispose(rejectedBridge);
|
||||
SafeDispose(rejectedTransport);
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
var activeBridge = _previousProvider?.Bridge ?? Bridge;
|
||||
var activeBridge = _navigationRouteCommitted
|
||||
? Bridge
|
||||
: _previousProvider?.Bridge ?? Bridge;
|
||||
if (activeBridge.IsConnected)
|
||||
activeBridge.Refresh();
|
||||
else
|
||||
|
|
@ -226,7 +241,7 @@ namespace GameCult.Eve.UnityScene
|
|||
// 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);
|
||||
(_navigationRouteCommitted ? Bridge : _previousProvider?.Bridge ?? Bridge).Submit(request);
|
||||
}
|
||||
|
||||
public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset)
|
||||
|
|
@ -273,6 +288,7 @@ namespace GameCult.Eve.UnityScene
|
|||
_previousProvider?.Bridge?.Dispose();
|
||||
_previousProvider?.Transport?.Dispose();
|
||||
_previousProvider = null;
|
||||
_navigationRouteCommitted = false;
|
||||
while (_pendingEntityViews.TryDequeue(out var pending)) pending.Lease.Dispose();
|
||||
while (_pendingFields.TryDequeue(out _)) { }
|
||||
_bridge = null;
|
||||
|
|
@ -428,6 +444,7 @@ namespace GameCult.Eve.UnityScene
|
|||
surfaceFilter,
|
||||
surfaceKind,
|
||||
_authorityTrust);
|
||||
_navigationRouteCommitted = false;
|
||||
DetachPrepared(_transport, _bridge);
|
||||
if (_previousProvider.Bridge != null)
|
||||
_previousProvider.Bridge.ReceiptAvailable += ForwardReceipt;
|
||||
|
|
@ -479,6 +496,18 @@ namespace GameCult.Eve.UnityScene
|
|||
private void ForwardReceipt(EveUnitySceneCommandReceipt receipt) =>
|
||||
ReceiptAvailable?.Invoke(receipt);
|
||||
|
||||
private static void SafeDispose(IDisposable? disposable)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable?.Dispose();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
Debug.LogWarning($"Eve provider cleanup failed after navigation ownership changed: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void TraceStartup(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("EVEUNITY_TRACE_STARTUP_PHASES"), "1", StringComparison.Ordinal))
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
private IEveUnitySceneCommandReceiptSource? _navigationReceiptSource;
|
||||
private bool _navigationInProgress;
|
||||
private StagedPresentation? _ownedPresentation;
|
||||
|
||||
public void ConfigureProvider(MonoBehaviour providerComponent)
|
||||
{
|
||||
|
|
@ -48,7 +49,12 @@ namespace GameCult.Eve.UnityScene
|
|||
BindNavigation();
|
||||
}
|
||||
|
||||
private void OnDestroy() => UnbindNavigation();
|
||||
private void OnDestroy()
|
||||
{
|
||||
UnbindNavigation();
|
||||
_ownedPresentation?.Dispose();
|
||||
_ownedPresentation = null;
|
||||
}
|
||||
|
||||
private void BindNavigation()
|
||||
{
|
||||
|
|
@ -89,8 +95,9 @@ namespace GameCult.Eve.UnityScene
|
|||
{
|
||||
navigation.GetAwaiter().GetResult();
|
||||
staged = PrepareStagedPresentation();
|
||||
swap = ActivateStagedPresentation(staged);
|
||||
navigable.CommitNavigation();
|
||||
swap = ActivateStagedPresentation(staged);
|
||||
navigable.FinalizeNavigation();
|
||||
swap.Complete();
|
||||
swap = null;
|
||||
staged = null;
|
||||
|
|
@ -190,6 +197,7 @@ namespace GameCult.Eve.UnityScene
|
|||
return new PresentationSwap(
|
||||
this,
|
||||
staged,
|
||||
_ownedPresentation,
|
||||
previousHost,
|
||||
previousRoot,
|
||||
previousPresentation,
|
||||
|
|
@ -253,8 +261,18 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
Host.Disconnect();
|
||||
DestroyObject(GameObject);
|
||||
try
|
||||
{
|
||||
Host.Disconnect();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
Debug.LogWarning($"Eve presentation disconnect failed during cleanup: {error}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
DestroyObject(GameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -262,6 +280,7 @@ namespace GameCult.Eve.UnityScene
|
|||
{
|
||||
private readonly EveUnityPlayableWorldClientBootstrap _owner;
|
||||
private readonly StagedPresentation _candidate;
|
||||
private readonly StagedPresentation? _previousOwnedPresentation;
|
||||
private readonly EveUnityPlayableWorldClientHost? _previousHost;
|
||||
private readonly Transform? _previousRoot;
|
||||
private readonly EveUnityPlayableWorldPresentation? _previousPresentation;
|
||||
|
|
@ -278,6 +297,7 @@ namespace GameCult.Eve.UnityScene
|
|||
public PresentationSwap(
|
||||
EveUnityPlayableWorldClientBootstrap owner,
|
||||
StagedPresentation candidate,
|
||||
StagedPresentation? previousOwnedPresentation,
|
||||
EveUnityPlayableWorldClientHost? previousHost,
|
||||
Transform? previousRoot,
|
||||
EveUnityPlayableWorldPresentation? previousPresentation,
|
||||
|
|
@ -292,6 +312,7 @@ namespace GameCult.Eve.UnityScene
|
|||
{
|
||||
_owner = owner;
|
||||
_candidate = candidate;
|
||||
_previousOwnedPresentation = previousOwnedPresentation;
|
||||
_previousHost = previousHost;
|
||||
_previousRoot = previousRoot;
|
||||
_previousPresentation = previousPresentation;
|
||||
|
|
@ -308,9 +329,23 @@ namespace GameCult.Eve.UnityScene
|
|||
public void Complete()
|
||||
{
|
||||
if (_settled) return;
|
||||
if (_previousHost != null && !ReferenceEquals(_previousHost, _candidate.Host))
|
||||
_previousHost.Disconnect();
|
||||
_settled = true;
|
||||
_owner._ownedPresentation = _candidate;
|
||||
if (_previousOwnedPresentation != null &&
|
||||
!ReferenceEquals(_previousOwnedPresentation, _candidate))
|
||||
{
|
||||
_previousOwnedPresentation.Dispose();
|
||||
return;
|
||||
}
|
||||
if (_previousHost == null || ReferenceEquals(_previousHost, _candidate.Host)) return;
|
||||
try
|
||||
{
|
||||
_previousHost.Disconnect();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
Debug.LogWarning($"Eve previous presentation disconnect failed after navigation commit: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Rollback()
|
||||
|
|
|
|||
|
|
@ -241,6 +241,8 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
void CommitNavigation();
|
||||
|
||||
void FinalizeNavigation();
|
||||
|
||||
void RollbackNavigation();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1196,7 +1196,30 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
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.FinalizeNavigationCount, Is.EqualTo(1));
|
||||
Assert.That(provider.RollbackNavigationCount, Is.Zero);
|
||||
|
||||
provider.PublishReceipt(new EveUnitySceneCommandReceipt(
|
||||
"receipt:launch-again",
|
||||
"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.CommitNavigationCount, Is.EqualTo(2));
|
||||
Assert.That(provider.FinalizeNavigationCount, Is.EqualTo(2));
|
||||
Assert.That(hostObject.transform.Cast<Transform>()
|
||||
.Count(child => child.name == "Eve Unity Candidate Presentation"), Is.EqualTo(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -3362,6 +3385,8 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
|
||||
public int CommitNavigationCount { get; private set; }
|
||||
|
||||
public int FinalizeNavigationCount { get; private set; }
|
||||
|
||||
public int RollbackNavigationCount { get; private set; }
|
||||
|
||||
public bool FailNextNavigationMount { get; set; }
|
||||
|
|
@ -3446,6 +3471,11 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
_failSurfaceRead = false;
|
||||
}
|
||||
|
||||
public void FinalizeNavigation()
|
||||
{
|
||||
FinalizeNavigationCount++;
|
||||
}
|
||||
|
||||
public void RollbackNavigation()
|
||||
{
|
||||
RollbackNavigationCount++;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "org.gamecult.eve.unity-scene",
|
||||
"displayName": "Eve Unity Scene Lowering",
|
||||
"version": "0.3.114",
|
||||
"version": "0.3.115",
|
||||
"unity": "6000.0",
|
||||
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
|
||||
"dependencies": {
|
||||
"org.gamecult.cultlib": "1.0.51",
|
||||
"org.gamecult.cultlib": "1.0.52",
|
||||
"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