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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue