refactor(runtime): unify generation reset for direct hosts
Move canonical per-session teardown into one retryable Runtime transaction, reduce App reset to projection acknowledgements, and prove the same GameRuntime graph through deterministic no-window lifecycle, gameplay, portal, fault, reconnect, and isolation gates.\n\nCo-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
7818494116
commit
a9a822f206
28 changed files with 2707 additions and 345 deletions
|
|
@ -968,15 +968,55 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
|||
|
||||
_sessionClearInProgress = true;
|
||||
Entities.BeginSessionClear();
|
||||
RuntimeEntityRecord[] retired = Entities.ActiveRecords.ToArray();
|
||||
foreach (RuntimeEntityRecord canonical in retired)
|
||||
RuntimeEntityRecord[] active = Entities.ActiveRecords.ToArray();
|
||||
foreach (RuntimeEntityRecord canonical in active)
|
||||
{
|
||||
if (!Entities.RemoveActive(canonical))
|
||||
continue;
|
||||
Entities.RetainTeardown(canonical);
|
||||
PublishEntity(RuntimeEntityChange.Deleted, canonical);
|
||||
}
|
||||
return retired;
|
||||
return active;
|
||||
}
|
||||
|
||||
public IReadOnlyList<RuntimeEntityRecord>
|
||||
CaptureSessionClearRetirements()
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (!_sessionClearInProgress)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Session-clear retirements are only available while the "
|
||||
+ "canonical clear transaction is active.");
|
||||
}
|
||||
return Entities.TeardownRecords.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes one exact incarnation retained by
|
||||
/// <see cref="BeginSessionClear"/> after the borrowed host has retired its
|
||||
/// presentation projection. Failure keeps the tombstone and local
|
||||
/// identity intact so the same transaction cursor can retry this exact
|
||||
/// record without reconstructing the retirement set.
|
||||
/// </summary>
|
||||
public void CompleteSessionEntityRetirement(
|
||||
RuntimeEntityRecord canonical)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
ArgumentNullException.ThrowIfNull(canonical);
|
||||
if (!_sessionClearInProgress
|
||||
|| !Entities.TryGetTeardown(
|
||||
canonical.ServerGuid,
|
||||
canonical.Incarnation,
|
||||
out RuntimeEntityRecord retained)
|
||||
|| !ReferenceEquals(retained, canonical))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{canonical.ServerGuid:X8}/{canonical.Incarnation} is not retained by the active session-clear transaction.");
|
||||
}
|
||||
|
||||
CompleteProjectionRetirement(canonical);
|
||||
Entities.ReleaseTeardown(canonical);
|
||||
}
|
||||
|
||||
public bool CompleteSessionClearIfConverged()
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ public readonly record struct GameRuntimeOwnershipSnapshot(
|
|||
RuntimeSimulationOwnershipSnapshot Simulation,
|
||||
RuntimeWorldEnvironmentOwnershipSnapshot Environment,
|
||||
RuntimeWorldTransitOwnershipSnapshot Transit,
|
||||
RuntimeGenerationResetSnapshot GenerationReset,
|
||||
GameRuntimeEventOwnershipSnapshot Events)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
|
|
@ -70,6 +71,7 @@ public readonly record struct GameRuntimeOwnershipSnapshot(
|
|||
&& PlayerIdentity.IsConverged
|
||||
&& Simulation.IsConverged
|
||||
&& Transit.IsSessionIdle
|
||||
&& GenerationReset.IsConverged
|
||||
&& Events.IsConverged;
|
||||
}
|
||||
|
||||
|
|
@ -241,8 +243,19 @@ public sealed class GameRuntime
|
|||
context,
|
||||
faultInjection);
|
||||
|
||||
var generationReset = new RuntimeGenerationReset(
|
||||
transit,
|
||||
context.Communication,
|
||||
context.Inventory,
|
||||
context.Actions,
|
||||
context.Movement,
|
||||
context.EntityObjects,
|
||||
context.Character,
|
||||
context.PlayerIdentity);
|
||||
|
||||
context.EntityObjects.BindEventContext(
|
||||
() => context.Session.Generation,
|
||||
() => generationReset.ActiveRetiringGeneration
|
||||
?? context.Session.Generation,
|
||||
() => clock.FrameNumber);
|
||||
context.Events = new GameRuntimeEventHub(
|
||||
context.EntityObjects,
|
||||
|
|
@ -264,6 +277,7 @@ public sealed class GameRuntime
|
|||
ActionOwner = context.Actions;
|
||||
EnvironmentOwner = environment;
|
||||
TransitOwner = transit;
|
||||
GenerationReset = generationReset;
|
||||
_events = context.Events;
|
||||
construction.Complete();
|
||||
}
|
||||
|
|
@ -285,6 +299,7 @@ public sealed class GameRuntime
|
|||
public RuntimeLocalPlayerMovementState MovementOwner { get; }
|
||||
public RuntimeWorldEnvironmentState EnvironmentOwner { get; }
|
||||
public RuntimeWorldTransitState TransitOwner { get; }
|
||||
public RuntimeGenerationReset GenerationReset { get; }
|
||||
|
||||
public RuntimeGenerationToken Generation => Session.Generation;
|
||||
|
||||
|
|
@ -347,6 +362,11 @@ public sealed class GameRuntime
|
|||
Portal.Snapshot,
|
||||
Portal.Ownership);
|
||||
|
||||
public void ResetGeneration(
|
||||
RuntimeGenerationToken retiringGeneration,
|
||||
IRuntimeGenerationResetHost host) =>
|
||||
GenerationReset.Reset(retiringGeneration, host);
|
||||
|
||||
public IDisposable Subscribe(IRuntimeEventObserver observer)
|
||||
{
|
||||
lock (_lifetimeGate)
|
||||
|
|
@ -393,6 +413,7 @@ public sealed class GameRuntime
|
|||
MovementOwner),
|
||||
EnvironmentOwner.CaptureOwnership(),
|
||||
TransitOwner.CaptureOwnership(),
|
||||
GenerationReset.CaptureSnapshot(),
|
||||
_events.CaptureOwnership());
|
||||
}
|
||||
}
|
||||
|
|
@ -534,6 +555,7 @@ public sealed class GameRuntime
|
|||
StopSession();
|
||||
return Session.CaptureOwnership().IsConverged;
|
||||
case 3:
|
||||
GenerationReset.DrainPending();
|
||||
TransitOwner.ResetSession();
|
||||
return TransitOwner.CaptureOwnership().IsSessionIdle;
|
||||
case 4:
|
||||
|
|
|
|||
|
|
@ -134,6 +134,26 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retires every character-generation movement owner while keeping the
|
||||
/// process-scoped Runtime root reusable for the next session.
|
||||
/// Graphical hosts release their camera/physics projections first; direct
|
||||
/// hosts have no parallel projection to coordinate.
|
||||
/// </summary>
|
||||
public void ResetSession()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
bool changed =
|
||||
_autoRunActive
|
||||
|| _controller is not null
|
||||
|| _preparingMotionOwner is not null;
|
||||
_autoRunActive = false;
|
||||
_controller = null;
|
||||
_preparingMotionOwner = null;
|
||||
if (changed)
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
public RuntimeLocalMovementOwnershipSnapshot CaptureOwnership() =>
|
||||
new(
|
||||
_disposed,
|
||||
|
|
|
|||
387
src/AcDream.Runtime/RuntimeGenerationReset.cs
Normal file
387
src/AcDream.Runtime/RuntimeGenerationReset.cs
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.World;
|
||||
|
||||
namespace AcDream.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// The only host-specific edge in a character-generation reset. The host
|
||||
/// retires presentation borrowed from an exact Runtime incarnation; Runtime
|
||||
/// retains the retirement set, cursor, canonical cleanup, and generation.
|
||||
/// </summary>
|
||||
public interface IRuntimeGenerationResetHost
|
||||
{
|
||||
void RetireEntityProjection(RuntimeEntityRecord entity);
|
||||
|
||||
void DrainEntityProjectionBoundary();
|
||||
|
||||
void CompleteEntityProjectionRetirement();
|
||||
}
|
||||
|
||||
public enum RuntimeGenerationResetStage
|
||||
{
|
||||
None = 0,
|
||||
Transit = 1,
|
||||
CommandTargets = 2,
|
||||
ExternalContainer = 3,
|
||||
Actions = 4,
|
||||
Movement = 5,
|
||||
ObjectTable = 6,
|
||||
Character = 7,
|
||||
ItemMana = 8,
|
||||
Friends = 9,
|
||||
Squelch = 10,
|
||||
NegotiatedChannels = 11,
|
||||
BeginEntityRetirement = 12,
|
||||
RetireEntities = 13,
|
||||
DrainHostProjection = 14,
|
||||
CompleteCanonicalEntities = 15,
|
||||
CompleteHostProjection = 16,
|
||||
ChatIdentity = 17,
|
||||
PlayerSnapshots = 18,
|
||||
PlayerIdentity = 19,
|
||||
Complete = 20,
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeGenerationResetSnapshot(
|
||||
bool IsActive,
|
||||
bool IsExecuting,
|
||||
RuntimeGenerationToken RetiringGeneration,
|
||||
RuntimeGenerationToken LastCompletedGeneration,
|
||||
RuntimeGenerationResetStage Stage,
|
||||
int RetirementCount,
|
||||
int RetirementCursor,
|
||||
bool CurrentProjectionAcknowledged,
|
||||
long TransactionId)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
!IsActive
|
||||
&& !IsExecuting;
|
||||
}
|
||||
|
||||
public sealed class RuntimeGenerationResetStageException(
|
||||
RuntimeGenerationToken retiringGeneration,
|
||||
RuntimeGenerationResetStage stage,
|
||||
Exception innerException) : Exception(
|
||||
$"Runtime generation {retiringGeneration.Value} reset stage "
|
||||
+ $"'{stage}' did not converge.",
|
||||
innerException)
|
||||
{
|
||||
public RuntimeGenerationToken RetiringGeneration { get; } =
|
||||
retiringGeneration;
|
||||
|
||||
public RuntimeGenerationResetStage Stage { get; } = stage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One persisted, retryable reset transaction for every canonical owner in a
|
||||
/// reusable <see cref="GameRuntime"/>. A failed call retains the exact suffix:
|
||||
/// completed stages and exact-incarnation retirements never replay.
|
||||
/// </summary>
|
||||
public sealed class RuntimeGenerationReset
|
||||
{
|
||||
private readonly RuntimeWorldTransitState _transit;
|
||||
private readonly RuntimeCommunicationState _communication;
|
||||
private readonly RuntimeInventoryState _inventory;
|
||||
private readonly RuntimeActionState _actions;
|
||||
private readonly RuntimeLocalPlayerMovementState _movement;
|
||||
private readonly RuntimeEntityObjectLifetime _entityObjects;
|
||||
private readonly RuntimeCharacterState _character;
|
||||
private readonly RuntimeLocalPlayerIdentityState _identity;
|
||||
private ResetState? _state;
|
||||
private RuntimeGenerationToken _lastCompletedGeneration;
|
||||
private bool _hasCompletedGeneration;
|
||||
private bool _executing;
|
||||
private long _nextTransactionId;
|
||||
|
||||
internal RuntimeGenerationReset(
|
||||
RuntimeWorldTransitState transit,
|
||||
RuntimeCommunicationState communication,
|
||||
RuntimeInventoryState inventory,
|
||||
RuntimeActionState actions,
|
||||
RuntimeLocalPlayerMovementState movement,
|
||||
RuntimeEntityObjectLifetime entityObjects,
|
||||
RuntimeCharacterState character,
|
||||
RuntimeLocalPlayerIdentityState identity)
|
||||
{
|
||||
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
|
||||
_communication = communication
|
||||
?? throw new ArgumentNullException(nameof(communication));
|
||||
_inventory = inventory
|
||||
?? throw new ArgumentNullException(nameof(inventory));
|
||||
_actions = actions ?? throw new ArgumentNullException(nameof(actions));
|
||||
_movement = movement
|
||||
?? throw new ArgumentNullException(nameof(movement));
|
||||
_entityObjects = entityObjects
|
||||
?? throw new ArgumentNullException(nameof(entityObjects));
|
||||
_character = character
|
||||
?? throw new ArgumentNullException(nameof(character));
|
||||
_identity = identity
|
||||
?? throw new ArgumentNullException(nameof(identity));
|
||||
}
|
||||
|
||||
public RuntimeGenerationToken? ActiveRetiringGeneration =>
|
||||
_state?.Generation;
|
||||
|
||||
public RuntimeGenerationResetSnapshot CaptureSnapshot()
|
||||
{
|
||||
ResetState? state = _state;
|
||||
return state is null
|
||||
? new RuntimeGenerationResetSnapshot(
|
||||
false,
|
||||
_executing,
|
||||
default,
|
||||
_lastCompletedGeneration,
|
||||
_hasCompletedGeneration
|
||||
? RuntimeGenerationResetStage.Complete
|
||||
: RuntimeGenerationResetStage.None,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
0)
|
||||
: new RuntimeGenerationResetSnapshot(
|
||||
true,
|
||||
_executing,
|
||||
state.Generation,
|
||||
_lastCompletedGeneration,
|
||||
state.Stage,
|
||||
state.Retirements?.Length ?? 0,
|
||||
state.RetirementCursor,
|
||||
state.CurrentProjectionAcknowledged,
|
||||
state.TransactionId);
|
||||
}
|
||||
|
||||
public void Reset(
|
||||
RuntimeGenerationToken retiringGeneration,
|
||||
IRuntimeGenerationResetHost host)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(host);
|
||||
if (_executing)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Runtime generation reset cannot run concurrently or reentrantly.");
|
||||
}
|
||||
|
||||
ResetState state = AcquireState(retiringGeneration, host);
|
||||
if (state.Stage is RuntimeGenerationResetStage.Complete)
|
||||
return;
|
||||
|
||||
_executing = true;
|
||||
try
|
||||
{
|
||||
Drain(state);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
throw new RuntimeGenerationResetStageException(
|
||||
state.Generation,
|
||||
state.Stage,
|
||||
error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_executing = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal void DrainPending()
|
||||
{
|
||||
ResetState? state = _state;
|
||||
if (state is null)
|
||||
return;
|
||||
Reset(state.Generation, state.Host);
|
||||
}
|
||||
|
||||
private ResetState AcquireState(
|
||||
RuntimeGenerationToken generation,
|
||||
IRuntimeGenerationResetHost host)
|
||||
{
|
||||
if (_state is { } pending)
|
||||
{
|
||||
if (pending.Generation != generation)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime generation {pending.Generation.Value} reset "
|
||||
+ $"must converge before generation {generation.Value} can reset.");
|
||||
}
|
||||
if (!ReferenceEquals(pending.Host, host))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"An in-progress Runtime generation reset cannot replace "
|
||||
+ "its borrowed projection host.");
|
||||
}
|
||||
return pending;
|
||||
}
|
||||
|
||||
if (_hasCompletedGeneration)
|
||||
{
|
||||
if (generation == _lastCompletedGeneration)
|
||||
return ResetState.Completed(generation, host);
|
||||
if (generation.Value < _lastCompletedGeneration.Value)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime generation {generation.Value} reset is stale; "
|
||||
+ $"generation {_lastCompletedGeneration.Value} already converged.");
|
||||
}
|
||||
}
|
||||
|
||||
var created = new ResetState(
|
||||
generation,
|
||||
host,
|
||||
checked(++_nextTransactionId));
|
||||
_state = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
private void Drain(ResetState state)
|
||||
{
|
||||
while (state.Stage is not RuntimeGenerationResetStage.Complete)
|
||||
{
|
||||
switch (state.Stage)
|
||||
{
|
||||
case RuntimeGenerationResetStage.Transit:
|
||||
Advance(state, _transit.ResetSession);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.CommandTargets:
|
||||
Advance(state, _communication.ResetCommandTargets);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.ExternalContainer:
|
||||
Advance(state, _inventory.ResetExternalContainer);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.Actions:
|
||||
Advance(state, _actions.ResetSession);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.Movement:
|
||||
Advance(state, _movement.ResetSession);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.ObjectTable:
|
||||
Advance(state, _entityObjects.ClearObjects);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.Character:
|
||||
Advance(state, _character.ResetSession);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.ItemMana:
|
||||
Advance(state, _inventory.ResetItemMana);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.Friends:
|
||||
Advance(state, _communication.ResetFriends);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.Squelch:
|
||||
Advance(state, _communication.ResetSquelch);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.NegotiatedChannels:
|
||||
Advance(
|
||||
state,
|
||||
_communication.ResetNegotiatedChannels);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.BeginEntityRetirement:
|
||||
_ = _entityObjects.BeginSessionClear();
|
||||
state.Retirements = _entityObjects
|
||||
.CaptureSessionClearRetirements()
|
||||
.ToArray();
|
||||
state.Stage = RuntimeGenerationResetStage.RetireEntities;
|
||||
break;
|
||||
case RuntimeGenerationResetStage.RetireEntities:
|
||||
RetireCurrentEntity(state);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.DrainHostProjection:
|
||||
state.Host.DrainEntityProjectionBoundary();
|
||||
state.Stage =
|
||||
RuntimeGenerationResetStage.CompleteCanonicalEntities;
|
||||
break;
|
||||
case RuntimeGenerationResetStage.CompleteCanonicalEntities:
|
||||
if (!_entityObjects.CompleteSessionClearIfConverged())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Canonical entity/object lifetime still owns an "
|
||||
+ "unacknowledged session retirement.");
|
||||
}
|
||||
state.Stage =
|
||||
RuntimeGenerationResetStage.CompleteHostProjection;
|
||||
break;
|
||||
case RuntimeGenerationResetStage.CompleteHostProjection:
|
||||
state.Host.CompleteEntityProjectionRetirement();
|
||||
state.Stage = RuntimeGenerationResetStage.ChatIdentity;
|
||||
break;
|
||||
case RuntimeGenerationResetStage.ChatIdentity:
|
||||
Advance(state, _communication.ResetChatIdentity);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.PlayerSnapshots:
|
||||
Advance(state, _inventory.ResetPlayerSnapshots);
|
||||
break;
|
||||
case RuntimeGenerationResetStage.PlayerIdentity:
|
||||
_identity.ResetSession();
|
||||
Complete(state);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unsupported Runtime generation reset stage {state.Stage}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RetireCurrentEntity(ResetState state)
|
||||
{
|
||||
RuntimeEntityRecord[] retirements = state.Retirements ?? [];
|
||||
if (state.RetirementCursor >= retirements.Length)
|
||||
{
|
||||
state.Stage = RuntimeGenerationResetStage.DrainHostProjection;
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeEntityRecord current = retirements[state.RetirementCursor];
|
||||
if (!state.CurrentProjectionAcknowledged)
|
||||
{
|
||||
state.Host.RetireEntityProjection(current);
|
||||
state.CurrentProjectionAcknowledged = true;
|
||||
}
|
||||
|
||||
_entityObjects.CompleteSessionEntityRetirement(current);
|
||||
state.CurrentProjectionAcknowledged = false;
|
||||
state.RetirementCursor++;
|
||||
}
|
||||
|
||||
private static void Advance(ResetState state, Action action)
|
||||
{
|
||||
action();
|
||||
state.Stage++;
|
||||
}
|
||||
|
||||
private void Complete(ResetState state)
|
||||
{
|
||||
state.Stage = RuntimeGenerationResetStage.Complete;
|
||||
_lastCompletedGeneration = state.Generation;
|
||||
_hasCompletedGeneration = true;
|
||||
_state = null;
|
||||
}
|
||||
|
||||
private sealed class ResetState
|
||||
{
|
||||
public ResetState(
|
||||
RuntimeGenerationToken generation,
|
||||
IRuntimeGenerationResetHost host,
|
||||
long transactionId)
|
||||
{
|
||||
Generation = generation;
|
||||
Host = host;
|
||||
TransactionId = transactionId;
|
||||
Stage = RuntimeGenerationResetStage.Transit;
|
||||
}
|
||||
|
||||
public RuntimeGenerationToken Generation { get; }
|
||||
public IRuntimeGenerationResetHost Host { get; }
|
||||
public long TransactionId { get; }
|
||||
public RuntimeGenerationResetStage Stage { get; set; }
|
||||
public RuntimeEntityRecord[]? Retirements { get; set; }
|
||||
public int RetirementCursor { get; set; }
|
||||
public bool CurrentProjectionAcknowledged { get; set; }
|
||||
|
||||
public static ResetState Completed(
|
||||
RuntimeGenerationToken generation,
|
||||
IRuntimeGenerationResetHost host) =>
|
||||
new(generation, host, 0)
|
||||
{
|
||||
Stage = RuntimeGenerationResetStage.Complete,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ public sealed record LiveSessionStartResult(
|
|||
public interface ILiveSessionLifecycleHost
|
||||
{
|
||||
LiveSessionBinding BindSession(WorldSession session);
|
||||
void ResetSessionState();
|
||||
void ResetSessionState(RuntimeGenerationToken retiringGeneration);
|
||||
void ReportConnecting(string host, int port, string user);
|
||||
void ReportConnected();
|
||||
void ApplySelectedCharacter(LiveSessionCharacterSelection selection);
|
||||
|
|
@ -185,12 +185,14 @@ public sealed class LiveSessionController
|
|||
{
|
||||
private sealed class SessionScope(
|
||||
WorldSession session,
|
||||
ILiveSessionLifecycleHost host)
|
||||
ILiveSessionLifecycleHost host,
|
||||
RuntimeGenerationToken generation)
|
||||
{
|
||||
private int _teardownStage;
|
||||
|
||||
public WorldSession Session { get; } = session;
|
||||
public ILiveSessionLifecycleHost Host { get; } = host;
|
||||
public RuntimeGenerationToken Generation { get; } = generation;
|
||||
public LiveSessionBinding? Binding { get; set; }
|
||||
public bool HostAttached { get; set; }
|
||||
public RuntimeTeardownStage CompletedStages { get; private set; }
|
||||
|
|
@ -226,7 +228,7 @@ public sealed class LiveSessionController
|
|||
}
|
||||
if (_teardownStage == 3)
|
||||
{
|
||||
Host.ResetSessionState();
|
||||
Host.ResetSessionState(Generation);
|
||||
CompletedStages |= RuntimeTeardownStage.HostReset;
|
||||
_teardownStage = 4;
|
||||
}
|
||||
|
|
@ -247,11 +249,15 @@ public sealed class LiveSessionController
|
|||
LiveSessionConnectOptions? Options = null,
|
||||
ILiveSessionLifecycleHost? Host = null);
|
||||
|
||||
private sealed record PendingHostReset(
|
||||
ILiveSessionLifecycleHost Host,
|
||||
RuntimeGenerationToken Generation);
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly ILiveSessionOperations _operations;
|
||||
private SessionScope? _scope;
|
||||
private SessionScope? _retiredScope;
|
||||
private ILiveSessionLifecycleHost? _pendingInitialResetHost;
|
||||
private PendingHostReset? _pendingInitialReset;
|
||||
private PendingOperation? _pendingOperation;
|
||||
private int _operationDepth;
|
||||
private bool _inWorld;
|
||||
|
|
@ -306,7 +312,7 @@ public sealed class LiveSessionController
|
|||
_inWorld,
|
||||
_scope is not null,
|
||||
_retiredScope is not null,
|
||||
_pendingInitialResetHost is not null,
|
||||
_pendingInitialReset is not null,
|
||||
_pendingOperation is not null,
|
||||
_operationDepth,
|
||||
_generation,
|
||||
|
|
@ -460,7 +466,9 @@ public sealed class LiveSessionController
|
|||
ILiveSessionLifecycleHost host)
|
||||
{
|
||||
ILiveSessionLifecycleHost? oldHost =
|
||||
_scope?.Host ?? _retiredScope?.Host ?? _pendingInitialResetHost;
|
||||
_scope?.Host
|
||||
?? _retiredScope?.Host
|
||||
?? _pendingInitialReset?.Host;
|
||||
ulong generationBeforeStop = _generation;
|
||||
try
|
||||
{
|
||||
|
|
@ -480,6 +488,7 @@ public sealed class LiveSessionController
|
|||
ILiveSessionLifecycleHost host,
|
||||
bool resetHost)
|
||||
{
|
||||
RuntimeGenerationToken resetGeneration = new(_generation);
|
||||
ulong generation = ++_generation;
|
||||
try
|
||||
{
|
||||
|
|
@ -487,7 +496,12 @@ public sealed class LiveSessionController
|
|||
if (_generation != generation)
|
||||
return new LiveSessionStartResult(LiveSessionStartStatus.Deferred);
|
||||
if (resetHost)
|
||||
ResetHostBeforeStart(host, generation);
|
||||
{
|
||||
ResetHostBeforeStart(
|
||||
host,
|
||||
generation,
|
||||
resetGeneration);
|
||||
}
|
||||
if (_generation != generation)
|
||||
return new LiveSessionStartResult(LiveSessionStartStatus.Deferred);
|
||||
}
|
||||
|
|
@ -509,7 +523,10 @@ public sealed class LiveSessionController
|
|||
return new LiveSessionStartResult(LiveSessionStartStatus.Deferred);
|
||||
Console.WriteLine($"live: connecting to {endpoint} as {options.User}");
|
||||
WorldSession session = _operations.CreateSession(endpoint);
|
||||
scope = new SessionScope(session, host);
|
||||
scope = new SessionScope(
|
||||
session,
|
||||
host,
|
||||
new RuntimeGenerationToken(generation));
|
||||
_scope = scope;
|
||||
_inWorld = false;
|
||||
if (!IsCurrent(scope, generation))
|
||||
|
|
@ -614,7 +631,7 @@ public sealed class LiveSessionController
|
|||
}
|
||||
DrainRetiredScope();
|
||||
DrainPendingInitialReset();
|
||||
if (_retiredScope is null && _pendingInitialResetHost is null)
|
||||
if (_retiredScope is null && _pendingInitialReset is null)
|
||||
_lastTeardownStages = RuntimeTeardownStage.Complete;
|
||||
}
|
||||
|
||||
|
|
@ -632,35 +649,38 @@ public sealed class LiveSessionController
|
|||
|
||||
private void ResetHostBeforeStart(
|
||||
ILiveSessionLifecycleHost host,
|
||||
ulong generation)
|
||||
ulong generation,
|
||||
RuntimeGenerationToken resetGeneration)
|
||||
{
|
||||
bool requestedHostAlreadyReset = false;
|
||||
if (_pendingInitialResetHost is { } pending)
|
||||
if (_pendingInitialReset is { } pending)
|
||||
{
|
||||
pending.ResetSessionState();
|
||||
_pendingInitialResetHost = null;
|
||||
requestedHostAlreadyReset = ReferenceEquals(pending, host);
|
||||
pending.Host.ResetSessionState(pending.Generation);
|
||||
_pendingInitialReset = null;
|
||||
requestedHostAlreadyReset =
|
||||
ReferenceEquals(pending.Host, host);
|
||||
}
|
||||
if (requestedHostAlreadyReset || _generation != generation)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
host.ResetSessionState();
|
||||
host.ResetSessionState(resetGeneration);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_pendingInitialResetHost = host;
|
||||
_pendingInitialReset =
|
||||
new PendingHostReset(host, resetGeneration);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainPendingInitialReset()
|
||||
{
|
||||
if (_pendingInitialResetHost is not { } host)
|
||||
if (_pendingInitialReset is not { } pending)
|
||||
return;
|
||||
host.ResetSessionState();
|
||||
_pendingInitialResetHost = null;
|
||||
pending.Host.ResetSessionState(pending.Generation);
|
||||
_pendingInitialReset = null;
|
||||
}
|
||||
|
||||
private void Schedule(PendingOperation operation)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public sealed record LiveSessionEnteredWorldBindings(
|
|||
|
||||
public sealed record LiveSessionHostBindings(
|
||||
LiveSessionRoutingFactories Routing,
|
||||
Action Reset,
|
||||
Action<RuntimeGenerationToken> Reset,
|
||||
LiveSessionSelectionBindings Selection,
|
||||
LiveSessionEnteredWorldBindings EnteredWorld,
|
||||
Action<string, int, string> Connecting,
|
||||
|
|
@ -84,7 +84,7 @@ public sealed class LiveSessionHost : IRuntimeSessionCommands
|
|||
private readonly LiveSessionRoutingFactories _routing;
|
||||
private readonly LiveSessionSelectionBindings _selection;
|
||||
private readonly LiveSessionEnteredWorldBindings _enteredWorld;
|
||||
private readonly Action _reset;
|
||||
private readonly Action<RuntimeGenerationToken> _reset;
|
||||
private readonly LiveSessionLifecycleHost _lifecycle;
|
||||
private PendingRouteRollback? _pendingRouteRollback;
|
||||
|
||||
|
|
@ -188,13 +188,14 @@ public sealed class LiveSessionHost : IRuntimeSessionCommands
|
|||
}
|
||||
}
|
||||
|
||||
private void ResetSessionState()
|
||||
private void ResetSessionState(
|
||||
RuntimeGenerationToken retiringGeneration)
|
||||
{
|
||||
// An incompletely detached route can still deliver callbacks into the
|
||||
// state below. Treat physical route convergence as the same hard
|
||||
// barrier used by normal LiveSessionBinding teardown.
|
||||
DrainPendingRouteRollback();
|
||||
_reset();
|
||||
_reset(retiringGeneration);
|
||||
}
|
||||
|
||||
private void ApplySelection(LiveSessionCharacterSelection selection)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ namespace AcDream.Runtime.Session;
|
|||
|
||||
public sealed record LiveSessionLifecycleBindings(
|
||||
Func<WorldSession, LiveSessionBinding> Bind,
|
||||
Action Reset,
|
||||
Action<RuntimeGenerationToken> Reset,
|
||||
Action<string, int, string> Connecting,
|
||||
Action Connected,
|
||||
Action<LiveSessionCharacterSelection> Selected,
|
||||
|
|
@ -42,7 +42,9 @@ public sealed class LiveSessionLifecycleHost : ILiveSessionLifecycleHost
|
|||
return binding;
|
||||
}
|
||||
|
||||
public void ResetSessionState() => _bindings.Reset();
|
||||
public void ResetSessionState(
|
||||
RuntimeGenerationToken retiringGeneration) =>
|
||||
_bindings.Reset(retiringGeneration);
|
||||
|
||||
public void ReportConnecting(string host, int port, string user) =>
|
||||
_bindings.Connecting(host, port, user);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue