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
|
|
@ -815,6 +815,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
d.PlayerController,
|
||||
d.WorldOrigin),
|
||||
new LiveSessionDomainRuntime(
|
||||
d.Runtime,
|
||||
d.EntityObjects,
|
||||
d.Character,
|
||||
d.Actions,
|
||||
|
|
|
|||
|
|
@ -476,6 +476,29 @@ internal sealed class SelectionInteractionController
|
|||
failures);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retires only App approach and retained item-interaction presentation.
|
||||
/// Runtime resets the canonical interaction and selection owners once at
|
||||
/// the shared generation boundary.
|
||||
/// </summary>
|
||||
internal void ResetGenerationPresentation()
|
||||
{
|
||||
List<Exception> failures = [];
|
||||
try { CancelPendingApproach(); }
|
||||
catch (Exception error) { failures.Add(error); }
|
||||
try { _items.ResetGenerationPresentation(); }
|
||||
catch (Exception error) { failures.Add(error); }
|
||||
try { _approachCompletions.Clear(); }
|
||||
catch (Exception error) { failures.Add(error); }
|
||||
|
||||
if (failures.Count != 0)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"One or more selection-interaction presentation reset stages failed.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidatePickupTarget(uint serverGuid, bool showToast)
|
||||
{
|
||||
if (_query.IsCreature(serverGuid))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
namespace AcDream.App.Net;
|
||||
|
||||
using AcDream.App.World;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Focused owner operations composed by the App host for one character-session
|
||||
|
|
@ -11,31 +13,18 @@ internal sealed class LiveSessionResetBindings
|
|||
{
|
||||
public required Action MouseCapture { get; init; }
|
||||
public required Action PlayerPresentation { get; init; }
|
||||
public required Action TeleportTransit { get; init; }
|
||||
public required Action TeleportPresentation { get; init; }
|
||||
public required Action SessionDialogs { get; init; }
|
||||
public required Action ChatCommandTargets { get; init; }
|
||||
public required Action SettingsCharacterContext { get; init; }
|
||||
public required Action EquippedChildren { get; init; }
|
||||
public required Action ExternalContainer { get; init; }
|
||||
public required Action InteractionAndSelection { get; init; }
|
||||
public required Action InventoryTransactions { get; init; }
|
||||
public required Action InteractionPresentation { get; init; }
|
||||
public required Action SelectionPresentation { get; init; }
|
||||
public required Action ObjectTable { get; init; }
|
||||
public required Action Spellbook { get; init; }
|
||||
public required Action MagicRuntime { get; init; }
|
||||
public required Action CombatAttack { get; init; }
|
||||
public required Action CombatState { get; init; }
|
||||
public required Action ItemMana { get; init; }
|
||||
public required Action LocalPlayer { get; init; }
|
||||
public required Action Friends { get; init; }
|
||||
public required Action Squelch { get; init; }
|
||||
public required Action TurbineChat { get; init; }
|
||||
public required Action ParticleVisibility { get; init; }
|
||||
public required Action InboundEventFifo { get; init; }
|
||||
public required Action LiveLiveness { get; init; }
|
||||
public required Action LiveRuntime { get; init; }
|
||||
public required Action RenderSceneProjection { get; init; }
|
||||
public required Action SessionIdentity { get; init; }
|
||||
public required Action<RuntimeGenerationToken> RuntimeGeneration { get; init; }
|
||||
public required Action<RuntimeGenerationToken> SessionIdentityPresentation
|
||||
{ get; init; }
|
||||
public required Action RemoteTeleport { get; init; }
|
||||
public required Action NetworkEffects { get; init; }
|
||||
public required Action AnimationHookFrames { get; init; }
|
||||
|
|
@ -59,38 +48,23 @@ internal static class LiveSessionResetManifest
|
|||
[
|
||||
new("mouse capture", bindings.MouseCapture),
|
||||
new("player presentation", bindings.PlayerPresentation),
|
||||
new("teleport transit", bindings.TeleportTransit),
|
||||
new("teleport presentation", bindings.TeleportPresentation),
|
||||
new("session dialogs", bindings.SessionDialogs),
|
||||
new("chat command targets", bindings.ChatCommandTargets),
|
||||
new("settings character context", bindings.SettingsCharacterContext),
|
||||
// Attachment projections own GL-backed registrations and must leave
|
||||
// before canonical live GUID records are released.
|
||||
new("equipped children", bindings.EquippedChildren),
|
||||
new("external container", bindings.ExternalContainer),
|
||||
new("interaction and selection", bindings.InteractionAndSelection),
|
||||
new("inventory transactions", bindings.InventoryTransactions),
|
||||
new("interaction presentation", bindings.InteractionPresentation),
|
||||
new("selection presentation", bindings.SelectionPresentation),
|
||||
new("object table", bindings.ObjectTable),
|
||||
new("spellbook", bindings.Spellbook),
|
||||
new("magic runtime", bindings.MagicRuntime),
|
||||
new("combat attack", bindings.CombatAttack),
|
||||
new("combat state", bindings.CombatState),
|
||||
new("item mana", bindings.ItemMana),
|
||||
new("local player", bindings.LocalPlayer),
|
||||
new("friends", bindings.Friends),
|
||||
new("squelch", bindings.Squelch),
|
||||
new("turbine chat", bindings.TurbineChat),
|
||||
new("particle visibility", bindings.ParticleVisibility),
|
||||
new("inbound event fifo", bindings.InboundEventFifo),
|
||||
new("live liveness", bindings.LiveLiveness),
|
||||
new("live runtime", bindings.LiveRuntime),
|
||||
// Canonical teardown appends exact projection removals. Drain them
|
||||
// before identity changes so no prior-session scene record or
|
||||
// journal entry can carry into the next character generation.
|
||||
new("render scene projection", bindings.RenderSceneProjection),
|
||||
new("runtime generation", bindings.RuntimeGeneration),
|
||||
// Identity must remain A until live teardown has converged so
|
||||
// player-specific teardown can still classify the old record.
|
||||
new("session identity", bindings.SessionIdentity),
|
||||
new(
|
||||
"session identity presentation",
|
||||
bindings.SessionIdentityPresentation),
|
||||
new("remote teleport", bindings.RemoteTeleport),
|
||||
// F754/F755 can precede CreateObject, so pending network effects
|
||||
// must clear even when no LiveEntityRecord was constructed.
|
||||
|
|
@ -102,31 +76,33 @@ internal static class LiveSessionResetManifest
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Shared convergence gate for canonical live runtime teardown.</summary>
|
||||
internal static class LiveSessionEntityRuntimeReset
|
||||
/// <summary>
|
||||
/// App's narrow borrowed projection acknowledgement for Runtime's canonical
|
||||
/// generation-reset transaction. It owns no reset cursor or entity identity.
|
||||
/// </summary>
|
||||
internal sealed class GraphicalRuntimeGenerationResetHost
|
||||
: IRuntimeGenerationResetHost
|
||||
{
|
||||
public static void ClearAndRequireConvergence(LiveEntityRuntime? runtime)
|
||||
private readonly LiveEntityRuntime _entities;
|
||||
private readonly Action _drainRenderProjection;
|
||||
|
||||
public GraphicalRuntimeGenerationResetHost(
|
||||
LiveEntityRuntime entities,
|
||||
Action drainRenderProjection)
|
||||
{
|
||||
if (runtime is null)
|
||||
return;
|
||||
runtime.Clear();
|
||||
RequireConvergence(runtime);
|
||||
_entities = entities
|
||||
?? throw new ArgumentNullException(nameof(entities));
|
||||
_drainRenderProjection = drainRenderProjection
|
||||
?? throw new ArgumentNullException(nameof(drainRenderProjection));
|
||||
}
|
||||
|
||||
public static void RequireConvergence(LiveEntityRuntime? runtime)
|
||||
{
|
||||
if (runtime is null)
|
||||
return;
|
||||
if (runtime.Count == 0
|
||||
&& runtime.PendingTeardownCount == 0
|
||||
&& runtime.MaterializedCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
public void RetireEntityProjection(
|
||||
RuntimeEntityRecord entity) =>
|
||||
_entities.RetireGenerationProjection(entity);
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"The live-entity runtime has not converged after session clear " +
|
||||
$"(records={runtime.Count}, pending={runtime.PendingTeardownCount}, " +
|
||||
$"materialized={runtime.MaterializedCount}).");
|
||||
}
|
||||
public void DrainEntityProjectionBoundary() =>
|
||||
_drainRenderProjection();
|
||||
|
||||
public void CompleteEntityProjectionRetirement() =>
|
||||
_entities.CompleteGenerationProjectionRetirement();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,19 @@
|
|||
namespace AcDream.App.Net;
|
||||
|
||||
/// <summary>One named owner operation in the live-session reset transaction.</summary>
|
||||
internal sealed record LiveSessionResetStage(string Name, Action Reset);
|
||||
using AcDream.Runtime;
|
||||
|
||||
/// <summary>One named host operation around Runtime's generation reset.</summary>
|
||||
internal sealed record LiveSessionResetStage(
|
||||
string Name,
|
||||
Action<RuntimeGenerationToken> Reset)
|
||||
{
|
||||
public LiveSessionResetStage(string name, Action reset)
|
||||
: this(
|
||||
name,
|
||||
_ => (reset ?? throw new ArgumentNullException(nameof(reset)))())
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes every session-state reset stage even when an earlier owner fails.
|
||||
|
|
@ -35,6 +47,9 @@ internal sealed class LiveSessionResetPlan
|
|||
Array.ConvertAll(_stages, static stage => stage.Name);
|
||||
|
||||
public void Execute()
|
||||
=> Execute(default);
|
||||
|
||||
public void Execute(RuntimeGenerationToken retiringGeneration)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _executing, 1) != 0)
|
||||
throw new InvalidOperationException(
|
||||
|
|
@ -47,7 +62,7 @@ internal sealed class LiveSessionResetPlan
|
|||
{
|
||||
try
|
||||
{
|
||||
stage.Reset();
|
||||
stage.Reset(retiringGeneration);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ using AcDream.Core.Social;
|
|||
using AcDream.Core.Spells;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Session;
|
||||
|
|
@ -39,6 +40,7 @@ internal sealed record LiveSessionPlayerRuntime(
|
|||
LiveWorldOriginState WorldOrigin);
|
||||
|
||||
internal sealed record LiveSessionDomainRuntime(
|
||||
GameRuntime Runtime,
|
||||
RuntimeEntityObjectLifetime EntityObjects,
|
||||
RuntimeCharacterState Character,
|
||||
RuntimeActionState Actions,
|
||||
|
|
@ -122,12 +124,18 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(controller);
|
||||
ArgumentNullException.ThrowIfNull(connectOptions);
|
||||
var resetHost = new GraphicalRuntimeGenerationResetHost(
|
||||
_world.LiveEntities,
|
||||
() => _world.RenderSceneShadow?.DrainUpdateBoundary());
|
||||
LiveSessionResetPlan reset =
|
||||
LiveSessionResetManifest.Create(
|
||||
CreateResetBindings(resetHost));
|
||||
return new LiveSessionHost(controller, new LiveSessionHostBindings(
|
||||
Routing: new(
|
||||
CreateEventRouter,
|
||||
session => _commands.Attach(new LiveSessionCommandRouter(
|
||||
CreateCommandBindings(session)))),
|
||||
Reset: LiveSessionResetManifest.Create(CreateResetBindings()).Execute,
|
||||
Reset: reset.Execute,
|
||||
Selection: new(
|
||||
SetPlayerIdentity: id => _player.Identity.ServerGuid = id,
|
||||
SetVitalsIdentity: id => _ui.Vitals?.SetLocalPlayerGuid(id),
|
||||
|
|
@ -152,37 +160,26 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
connectOptions);
|
||||
}
|
||||
|
||||
private LiveSessionResetBindings CreateResetBindings() => new()
|
||||
private LiveSessionResetBindings CreateResetBindings(
|
||||
IRuntimeGenerationResetHost resetHost) => new()
|
||||
{
|
||||
MouseCapture = _interaction.GameplayInput.ResetSession,
|
||||
PlayerPresentation = ResetPlayerPresentation,
|
||||
TeleportTransit = _world.Teleport.ResetSession,
|
||||
TeleportPresentation =
|
||||
_world.Teleport.ResetGenerationPresentation,
|
||||
SessionDialogs = () => _ui.RetailUi?.ResetSessionTransientUi(),
|
||||
ChatCommandTargets = _domain.Communication.ResetCommandTargets,
|
||||
SettingsCharacterContext =
|
||||
_interaction.Settings.RestoreDefaultCharacterContext,
|
||||
EquippedChildren = _world.EquippedChildren.Clear,
|
||||
ExternalContainer = _domain.Inventory.ResetExternalContainer,
|
||||
InteractionAndSelection = _interaction.SelectionInteractions.ResetSession,
|
||||
InventoryTransactions = _domain.Inventory.ResetTransactions,
|
||||
InteractionPresentation =
|
||||
_interaction.SelectionInteractions.ResetGenerationPresentation,
|
||||
SelectionPresentation = _world.SelectionScene.Reset,
|
||||
ObjectTable = _domain.EntityObjects.ClearObjects,
|
||||
Spellbook = _domain.Character.ResetSpellbook,
|
||||
MagicRuntime = () => _ui.Magic?.Reset(),
|
||||
CombatAttack = _interaction.CombatAttack.ResetSession,
|
||||
CombatState = _domain.Actions.Combat.Clear,
|
||||
ItemMana = _domain.Inventory.ResetItemMana,
|
||||
LocalPlayer = _domain.Character.ResetLocalPlayer,
|
||||
Friends = _domain.Communication.ResetFriends,
|
||||
Squelch = _domain.Communication.ResetSquelch,
|
||||
TurbineChat = _domain.Communication.ResetNegotiatedChannels,
|
||||
ParticleVisibility = _world.ParticleVisibility.Reset,
|
||||
InboundEventFifo = _world.InboundEvents.Clear,
|
||||
LiveLiveness = _world.Liveness.Clear,
|
||||
LiveRuntime = ResetLiveRuntime,
|
||||
RenderSceneProjection =
|
||||
() => _world.RenderSceneShadow?.DrainUpdateBoundary(),
|
||||
SessionIdentity = ResetIdentity,
|
||||
RuntimeGeneration = generation =>
|
||||
_domain.Runtime.ResetGeneration(generation, resetHost),
|
||||
SessionIdentityPresentation = ResetIdentityPresentation,
|
||||
RemoteTeleport = _world.RemoteTeleport.Clear,
|
||||
NetworkEffects = _world.EntityEffects.ClearNetworkState,
|
||||
AnimationHookFrames = _world.AnimationHookFrames.Clear,
|
||||
|
|
@ -196,28 +193,28 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
_world.SpawnClaims.Reset();
|
||||
}
|
||||
|
||||
private void ResetLiveRuntime() =>
|
||||
LiveSessionEntityRuntimeReset.ClearAndRequireConvergence(
|
||||
_world.LiveEntities);
|
||||
|
||||
private void ResetIdentity()
|
||||
private void ResetIdentityPresentation(
|
||||
RuntimeGenerationToken retiringGeneration)
|
||||
{
|
||||
LiveSessionEntityRuntimeReset.RequireConvergence(_world.LiveEntities);
|
||||
RuntimeGenerationResetSnapshot reset =
|
||||
_domain.Runtime.GenerationReset.CaptureSnapshot();
|
||||
if (reset.IsActive
|
||||
|| reset.LastCompletedGeneration != retiringGeneration)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime generation {retiringGeneration.Value} has not "
|
||||
+ "converged before App identity projection teardown.");
|
||||
}
|
||||
|
||||
// PlayerModule::Clear @ 0x005D48A0 clears shortcuts, desired
|
||||
// components, and character option objects. CPlayerSystem::Begin
|
||||
// @ 0x0055D410 resets player identity and session fields. The option
|
||||
// default comes from PlayerModule::PlayerModule @ 0x005D51F0.
|
||||
_player.Identity.ServerGuid = 0u;
|
||||
_ui.Vitals?.SetLocalPlayerGuid(0u);
|
||||
_domain.Communication.ResetChatIdentity();
|
||||
EntityVanishProbe.PlayerGuid = 0u;
|
||||
_interaction.Settings.ResetActiveCharacterKey();
|
||||
_domain.Character.Options.ResetSession();
|
||||
_domain.Character.MovementSkills.ResetSession();
|
||||
_world.NetworkUpdates.ResetSessionState();
|
||||
_world.Hydration.ResetSessionState();
|
||||
_domain.Inventory.ResetPlayerSnapshots();
|
||||
_ui.Paperdoll?.ResetSession();
|
||||
|
||||
// X/Y are ignored until the next logical player CreateObject claims a
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ internal interface ILocalPlayerTeleportNetworkSink
|
|||
bool teleportTimestampAdvanced);
|
||||
|
||||
void ResetSession();
|
||||
|
||||
void ResetGenerationPresentation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -62,6 +64,9 @@ internal sealed class DeferredLocalPlayerTeleportNetworkSink
|
|||
|
||||
public void ResetSession() => Required().ResetSession();
|
||||
|
||||
public void ResetGenerationPresentation() =>
|
||||
Required().ResetGenerationPresentation();
|
||||
|
||||
private ILocalPlayerTeleportNetworkSink Required() =>
|
||||
_inner ?? throw new InvalidOperationException(
|
||||
"The local teleport sink was used before composition completed.");
|
||||
|
|
@ -567,7 +572,17 @@ internal sealed class LocalPlayerTeleportController
|
|||
public void ResetSession()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ResetTransit(clearSession: true);
|
||||
ResetTransit(
|
||||
clearSession: true,
|
||||
resetCanonicalTransit: true);
|
||||
}
|
||||
|
||||
public void ResetGenerationPresentation()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ResetTransit(
|
||||
clearSession: true,
|
||||
resetCanonicalTransit: false);
|
||||
}
|
||||
|
||||
public Matrix4x4 ApplyViewPlane(Matrix4x4 projection) =>
|
||||
|
|
@ -718,7 +733,9 @@ internal sealed class LocalPlayerTeleportController
|
|||
return true;
|
||||
}
|
||||
|
||||
private long ResetTransit(bool clearSession)
|
||||
private long ResetTransit(
|
||||
bool clearSession,
|
||||
bool resetCanonicalTransit = false)
|
||||
{
|
||||
long generation = checked(++_lifetimeGeneration);
|
||||
|
||||
|
|
@ -733,7 +750,12 @@ internal sealed class LocalPlayerTeleportController
|
|||
return generation;
|
||||
|
||||
if (clearSession)
|
||||
_worldReveal.ResetSession();
|
||||
{
|
||||
if (resetCanonicalTransit)
|
||||
_worldReveal.ResetSession();
|
||||
else
|
||||
_worldReveal.ResetHostSession();
|
||||
}
|
||||
else
|
||||
{
|
||||
_transit.EndTeleport();
|
||||
|
|
|
|||
|
|
@ -286,12 +286,22 @@ internal sealed class WorldRevealCoordinator
|
|||
}
|
||||
|
||||
public void ResetSession()
|
||||
{
|
||||
ResetHostSession();
|
||||
_transit.ResetSession();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retires only App streaming/presentation acknowledgements. The shared
|
||||
/// Runtime generation-reset transaction clears canonical transit state
|
||||
/// after this host boundary has converged.
|
||||
/// </summary>
|
||||
internal void ResetHostSession()
|
||||
{
|
||||
long generation = _transit.Snapshot.Generation;
|
||||
if (generation != 0)
|
||||
_transit.Cancel(generation);
|
||||
RetryPendingHostWork();
|
||||
_transit.ResetSession();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1182,27 +1182,44 @@ public sealed class ItemInteractionController : IDisposable
|
|||
/// same numeric GUID can identify a different object.
|
||||
/// </summary>
|
||||
public void ResetSession()
|
||||
=> ResetSessionCore(resetRuntime: true);
|
||||
|
||||
/// <summary>
|
||||
/// Clears only retained UI transaction presentation. Runtime's canonical
|
||||
/// transaction, interaction, and busy owners are reset once by the shared
|
||||
/// generation-reset transaction.
|
||||
/// </summary>
|
||||
internal void ResetGenerationPresentation()
|
||||
=> ResetSessionCore(resetRuntime: false);
|
||||
|
||||
private void ResetSessionCore(bool resetRuntime)
|
||||
{
|
||||
PendingBackpackPlacement? pendingPlacement = _pendingBackpackPlacement;
|
||||
_pendingBackpackPlacement = null;
|
||||
// Runtime owns the transaction reset. This controller preserves the
|
||||
// pre-J4 single UI notification emitted by InteractionState below.
|
||||
_transactions.StateChanged -= OnTransactionStateChanged;
|
||||
try
|
||||
if (resetRuntime)
|
||||
{
|
||||
_runtimeTransactions.ResetSession();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!_disposed)
|
||||
_transactions.StateChanged += OnTransactionStateChanged;
|
||||
// Runtime owns the transaction reset. This controller preserves
|
||||
// the pre-J4 single UI notification emitted by InteractionState.
|
||||
_transactions.StateChanged -= OnTransactionStateChanged;
|
||||
try
|
||||
{
|
||||
_runtimeTransactions.ResetSession();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!_disposed)
|
||||
_transactions.StateChanged += OnTransactionStateChanged;
|
||||
}
|
||||
}
|
||||
_consumedPrimaryClickTarget = 0u;
|
||||
_consumedPrimaryClickMs = long.MinValue / 2;
|
||||
|
||||
List<Exception> failures = [];
|
||||
try { _interactionState.ResetSession(); }
|
||||
catch (Exception error) { failures.Add(error); }
|
||||
if (resetRuntime)
|
||||
{
|
||||
try { _interactionState.ResetSession(); }
|
||||
catch (Exception error) { failures.Add(error); }
|
||||
}
|
||||
|
||||
if (pendingPlacement is { } pending)
|
||||
DispatchAll(PendingBackpackPlacementCancelled, pending, failures);
|
||||
|
|
|
|||
|
|
@ -2230,6 +2230,76 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
IsCurrentRecord(expectedRecord)
|
||||
&& expectedRecord.CreateIntegrationVersion == expectedCreateIntegrationVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Retires only the graphical sidecar for an exact Runtime-owned
|
||||
/// incarnation. The Runtime generation-reset transaction retains and
|
||||
/// completes the canonical tombstone after this acknowledgement returns.
|
||||
/// </summary>
|
||||
internal void RetireGenerationProjection(
|
||||
RuntimeEntityRecord canonical)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(canonical);
|
||||
if (_isClearing || _isRegisteringResources)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
_isClearing
|
||||
? "Live entity projection teardown is already in progress."
|
||||
: "Live entity projection teardown cannot begin inside atomic resource registration.");
|
||||
}
|
||||
|
||||
LiveEntityRecord? record = null;
|
||||
if (_projections.TryGet(canonical, out LiveEntityRecord active))
|
||||
{
|
||||
if (!_projections.RemoveActive(active))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Exact App projection for 0x{canonical.ServerGuid:X8}/{canonical.Incarnation} could not be retired during generation reset.");
|
||||
}
|
||||
_projections.RetainTeardown(active);
|
||||
record = active;
|
||||
}
|
||||
else if (_projections.TryGetTeardown(
|
||||
canonical,
|
||||
out LiveEntityRecord retained))
|
||||
{
|
||||
record = retained;
|
||||
}
|
||||
|
||||
if (record is null)
|
||||
return;
|
||||
|
||||
_isClearing = true;
|
||||
try
|
||||
{
|
||||
TearDownRecord(record, completeCanonical: false);
|
||||
_projections.ReleaseTeardown(record);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isClearing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the App-only projection boundary after Runtime has retired
|
||||
/// every exact canonical incarnation. No canonical owner is mutated here.
|
||||
/// </summary>
|
||||
internal void CompleteGenerationProjectionRetirement()
|
||||
{
|
||||
if (_projections.ActiveCount != 0
|
||||
|| _projections.TeardownCount != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Live entity projection storage has not converged at the generation-reset boundary.");
|
||||
}
|
||||
|
||||
_spatialAnimations.Clear();
|
||||
_physics.ClearSpatialWorksets();
|
||||
_projections.ClearConverged();
|
||||
_spatial.ClearLiveEntityLifetimeState();
|
||||
_sessionClearPendingFinalization = false;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
if (_isClearing || _isRegisteringResources)
|
||||
|
|
@ -2670,7 +2740,9 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
_sessionClearPendingFinalization = false;
|
||||
}
|
||||
|
||||
private void TearDownRecord(LiveEntityRecord record)
|
||||
private void TearDownRecord(
|
||||
LiveEntityRecord record,
|
||||
bool completeCanonical = true)
|
||||
{
|
||||
if (record.TeardownInProgress)
|
||||
throw new InvalidOperationException(
|
||||
|
|
@ -2730,8 +2802,11 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
|
||||
if (record.WorldEntity is not null)
|
||||
_ = RequireProjectionKey(record);
|
||||
_entityObjects.CompleteProjectionRetirement(
|
||||
record.Canonical);
|
||||
if (completeCanonical)
|
||||
{
|
||||
_entityObjects.CompleteProjectionRetirement(
|
||||
record.Canonical);
|
||||
}
|
||||
|
||||
record.AnimationRuntime = null;
|
||||
record.EffectProfile = null;
|
||||
|
|
|
|||
|
|
@ -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