acdream/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
Erik 6dc7ba51ee feat(physics): C4 route 4b-3 — remote teleport + cell-less through the canonical placement
Flips the last remote classification (SetPosition: teleport-advanced and
cell-less) onto 4b-1's RuntimeRemotePlacementDriveController, runs retail's
teleport_hook before the placement, and deletes the legacy remote-teleport
machinery. Contract: docs/research/2026-08-04-c4-route-4b-3-contract.md.

Retail: MoveOrTeleport @0x00516330's branch @0x00516386 -> teleport_hook
@0x005163EF -> SetFlags(0x1012) @0x00516414 -> SetPosition @0x00516420 ->
return 1 @0x00516438. The hook @0x00514ED0 runs BEFORE the placement and
regardless of its outcome. Retail places this branch unconditionally, at any
distance and any contact state (arg4 is read only @0x0051638E, after the
branch) — which is what retires AP-137's cell-less enqueue-vs-place delta.

D1 — the classifier's cell-less input is now the PRE-merge committed cell.
Retail's predicate is `this_1->cell == 0`, the BODY's own cell at
MoveOrTeleport entry (this_1 is assigned from this @0x00516334). acdream fed
the POST-merge canonical.FullCellId, which RefreshSnapshot ->
RefreshDerivedState -> SetFullCell has already stamped with the accepted wire
cell; a zero wire cell fails validation into RejectedData first. The shipped
remote cell-less predicate was therefore dead code, not merely different from
remotePlacementRequired. Threaded via a builder overload; route 1's overload
is untouched. The graphical !IsSpatiallyVisible arm of
projectionRequiresTeleportHook is deleted — a presentation predicate with no
retail analogue that fired the teleport machinery on a routine hot path.

Deleted: RemoteTeleportController (605), RemoteTeleportPlacement (85),
RemoteShadowPlacementSynchronizer (49), their 1,709 lines of tests, the
remotePlacementRequired predicate, the TeleportHookRequired plumbing, the
legacy pre-operation ConstrainTo fallback, and the player arm's legacy
!IsGrounded fallback. Net -2,030 lines.

Structural fix (two independent Opus reviews, round 1 FAIL/FAIL): three of the
four MAJORs were one defect — OnPosition carried two parallel inline copies of
the routing tail (player-guid, NPC-guid) that had drifted. Extracted
RunRemoteArmTail (3 call sites) and ApplyWireAirborneLeftoverBookkeeping (2),
both branches now share one implementation.

  A1  ToConstraintArm mapped AirborneSnap -> AirborneNoOperation, so the NPC
      arm armed ConstrainTo ZERO times for an out-of-contact wire-grounded
      creature — a regression this slice introduced while closing a
      structurally identical hole. Now maps to NearInterpolate; switch made
      total with a throwing default proven unreachable.
  R1  D2's write-nothing shape existed on the player arm only; NPC packets
      fell through and wrote the body. Retail makes no player/NPC distinction.
  R2  report_collision_end(this,1) @0x00514F31 was bound to
      ShadowObjects.Suspend, a port of a DIFFERENT retail function
      (remove_shadows_from_cells) that teleport_hook never calls. Now routes
      to RuntimeCollisionReportingState.LeaveWorld, which wraps the private
      ForceEnd in an admission-blocking transaction so a DoCollisionEnd
      callback cannot recreate the contact table.
  R3/A2 A teleported NPC synthesized ServerVelocity from the teleport distance
      (~1,000+ m/s) and planned a run cycle from it. Both the install and
      RemoteServerControlledVelocityCycle.Apply now gate on !isTeleportRoute.

BISECT HAZARD — A1's fix is correct only BECAUSE R1 landed. AirborneSnap is
reachable wire-airborne on the NPC arm only while D2's shape is missing there.
Reverting R1 alone silently inverts A1 into the opposite divergence: arming
where retail returns 0. Revert both or neither.

Also in the velocity hunk: the NPC block's two !IsPlayerGuid(update.Guid)
guards were dropped when it was wrapped in `if (!isTeleportRoute)`. Safe — all
five exit paths of the enclosing IsPlayerGuid block return, so the predicate is
unconditionally false below it — but it was unremarked by both reviews.

Register: AP-137 REWRITTEN (not deleted) to the surviving acdream-only
divergences — null classification during the login window and Rejected*
through UnroutedCatchUp keep a row. AD-42's RemoteTeleportController citation
retired; AP-136/AP-138 writer lists corrected to the two surviving non-Position
rebucket writers; AP-138 gains the teleport arm as a second producer of the
visible-without-collision residual (retirement path remains #309). AP-135 is
untouched and its two airborne bookkeeping writes are preserved on both arms.
AP-131 does not retire; #276 does not close.

Proof obligation 1: ParkCollisionResidents' overlap throw stays unreachable —
the teleport arm adds packets to the same TryBeginExclusiveAuthoredPlacement
one-operation-per-key machinery the far arm uses, opens no new operation shape,
and every DeferredCell outcome cancels synchronously with
restoreCancelledPark: true. The guarded property remains
HasOldPrefixPlacementDebt's stall, not a throw (4b-1's B2 caveat stands).

Correction to an earlier claim: LiveEntityPresentationController's
_activePlacementOwners was NOT write-never at HEAD —
remotePlacementRequired -> BeginPlacement -> Begin -> BeginAuthoritativePlacement
was a live writer chain. It becomes write-never BECAUSE this slice deletes that
chain, which is why deleting the dead half is behaviour-preserving.

Probe: ACDREAM_PROBE_REMOTE_TELEPORT=1 emits one [remote-teleport] line per
routed arm (guid, cause, hook-ran, placement status). TEMPORARY, strip with the
probe family.

Carried, disclosed not fixed: no dedicated bidirectional collision-partner test
for R2 (the wiring, not LeaveWorld itself, is what lacks coverage); the
stress test's teleport step drives hand-written field assignments rather than
the canonical arm; the per-packet runTeleportHook closure allocation (network
path, not the resolve path Slice I's 0 B discipline governs — file before
route 5 adds a fourth call site). B2: IRuntimeCollisionReportObserver has zero
production implementations, so retail's bidirectional DoCollisionEnd half still
reaches no gameplay consumer — this fix closes the wrong-function binding, not
that nobody listens.

Complete Release suite MEASURED at 11,013 passed / 4 skipped / 0 failed
(baseline 11,027/4/0; net -14 = ~33 deleted test cases against ~19 added).
Neither known flake fired (#302 PortalProjectionTests GC-allocation, #308
NakEmissionTests wall-clock).

STILL OWED: the two-client connected gate, which MUST use an NPC/creature
teleport target. Both round-1 MAJORs lived on the NPC arm and the velocity
cycle early-returns for 0x50xxxxxx guids, so a player target structurally
cannot observe A1, A2, or R3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:00:10 +02:00

2700 lines
109 KiB
C#

using System.Collections.Immutable;
using System.Numerics;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Entities;
public readonly record struct RuntimeEntityRegistrationResult(
InboundCreateResult Inbound,
RuntimeEntityRecord? Canonical,
bool LogicalRegistrationCreated,
bool ReplacedExistingGeneration,
Exception? PriorGenerationCleanupFailure = null,
bool DeferredForParent = false);
public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
int ActiveEntityCount,
int TeardownEntityCount,
int ClaimedLocalIdCount,
int AcceptedSnapshotCount,
int UnresolvedParentRelationCount,
int DeferredParentCreateCount,
int StagedParentRelationCount,
int RecoveryParentRelationCount,
int CommittedParentRelationCount,
int ObjectCount,
int ContainerCount,
int ContainerProjectionCount,
int EquipmentOwnerCount,
int PendingMoveCount,
int InitialCreateResidenceLeaseCount,
int InitialCreateExecutorProgressCount,
int StreamSubscriberCount,
int PlacementStreamSubscriberCount,
long StreamDispatchFailureCount,
bool HasLastStreamDispatchFailure,
int PendingDispatchCount,
bool IsDispatching,
bool IsSessionClearInProgress,
bool IsDisposed,
/// <summary>Round 5 R5-1: pending queue-by-parent-GUID accepted relations (see <see cref="ParentAttachmentState.DeferredAcceptedRelationCount"/>).</summary>
int DeferredAcceptedRelationCount = 0,
/// <summary>Round 5 R5-3: mirrors StreamDispatchFailureCount/HasLastStreamDispatchFailure for the executor's contained-replay failure surface. Diagnostic only - like its stream precedent, NOT gated by <see cref="IsConverged"/>.</summary>
long ReplayFailureCount = 0,
bool HasLastReplayFailure = false,
/// <summary>
/// F2: outstanding <see cref="RuntimeInitialCreateContinuationExecutor"/>
/// token-to-receipt correlation entries - one per completed drain whose
/// ExecutorCompleted receipt a host has not yet acknowledged. Gated by
/// <see cref="IsConverged"/>, mirroring
/// <see cref="RuntimeSetPositionOwnershipSnapshot.PendingProjectionAcknowledgementCount"/>'s
/// existing "unacknowledged receipt is outstanding debt" shape for the
/// SAME underlying receipt stream - unlike ReplayFailureCount above,
/// this is NOT a diagnostic-only counter.
/// </summary>
int PendingCompletionReceiptCount = 0,
/// <summary>
/// C3a/F2: outstanding <c>AcDream.Runtime.Gameplay.RuntimeLocalPlayerFirstEntryState</c>
/// tracked keys - dormant (no production caller of its own
/// <c>Advance</c>), but fully constructed/wired like every other owner
/// here, so its own ownership must converge to zero the same way.
/// </summary>
int LocalPlayerFirstEntryActiveCount = 0,
/// <summary>
/// C3b: outstanding <see cref="RuntimeRemoteFirstEntryState"/> tracked
/// keys - the remote/projectile Create-time body-construction conductor.
/// Dormant like its C3a sibling; converges to zero the same way.
/// </summary>
int RemoteFirstEntryActiveCount = 0,
/// <summary>
/// C3c-R1 review F5: outstanding host first-entry drive entries
/// (<c>RuntimeFirstEntryDriveController</c> pending keys, summed over
/// every drive registered against this lifetime via
/// <see cref="RuntimeEntityObjectLifetime.RegisterFirstEntryDriveOwnership"/>).
/// Previously outside every ledger; gated by <see cref="IsConverged"/>
/// like the conductor counts it pumps.
/// </summary>
int FirstEntryDrivePendingCount = 0,
/// <summary>
/// C4 route 2: outstanding host <c>RuntimeAcceptedPositionDriveController</c>
/// pending operations (a not-yet-committed or not-yet-acknowledged
/// ForcePosition on the local player), summed over every drive
/// registered against this lifetime via
/// <see cref="RuntimeEntityObjectLifetime.RegisterAcceptedPositionDriveOwnership"/>.
/// Gated by <see cref="IsConverged"/> — a leaked pending ack cannot hide.
/// </summary>
int AcceptedPositionDrivePendingCount = 0,
/// <summary>
/// C4 route 4b-1: outstanding
/// <c>AcDream.Runtime.Session.RuntimeRemotePlacementDriveController</c>
/// preparation-retry entries (a not-yet-resolved
/// <c>RetrySetupUnavailable</c>/<c>RetryWorldFrameUnavailable</c> for a
/// remote), summed over every drive registered against this lifetime via
/// <see cref="RuntimeEntityObjectLifetime.RegisterRemotePlacementDriveOwnership"/>.
/// Gated by <see cref="IsConverged"/>, mirroring
/// <see cref="AcceptedPositionDrivePendingCount"/> — steady-state
/// remotes hold no operations, and this count proves it at every
/// convergence checkpoint the same way.
/// </summary>
int RemotePlacementDrivePendingCount = 0)
{
public bool IsConverged =>
IsDisposed
&& ActiveEntityCount == 0
&& TeardownEntityCount == 0
&& ClaimedLocalIdCount == 0
&& AcceptedSnapshotCount == 0
&& UnresolvedParentRelationCount == 0
&& DeferredParentCreateCount == 0
&& DeferredAcceptedRelationCount == 0
&& StagedParentRelationCount == 0
&& RecoveryParentRelationCount == 0
&& CommittedParentRelationCount == 0
&& ObjectCount == 0
&& ContainerCount == 0
&& ContainerProjectionCount == 0
&& EquipmentOwnerCount == 0
&& PendingMoveCount == 0
&& InitialCreateResidenceLeaseCount == 0
&& InitialCreateExecutorProgressCount == 0
&& PendingCompletionReceiptCount == 0
&& LocalPlayerFirstEntryActiveCount == 0
&& RemoteFirstEntryActiveCount == 0
&& FirstEntryDrivePendingCount == 0
&& AcceptedPositionDrivePendingCount == 0
&& RemotePlacementDrivePendingCount == 0
&& StreamSubscriberCount == 0
&& PlacementStreamSubscriberCount == 0
&& PendingDispatchCount == 0
&& !IsDispatching
&& !IsSessionClearInProgress;
}
/// <summary>
/// One exact DeleteObject acceptance issued by a
/// <see cref="RuntimeEntityObjectLifetime"/>. The graphical host retires the
/// active identity before returning this token to the Runtime owner for the
/// retained-object mutation, preserving retail callback order.
/// </summary>
public sealed class RuntimeEntityDeleteAcceptance
{
internal RuntimeEntityDeleteAcceptance(
RuntimeEntityObjectLifetime owner,
DeleteObject.Parsed delete,
RuntimeEntityRecord? retiredCanonical,
bool removeRetainedObject)
{
Owner = owner;
Delete = delete;
RetiredCanonical = retiredCanonical;
RemoveRetainedObject = removeRetainedObject;
}
internal RuntimeEntityObjectLifetime Owner { get; }
internal bool Completed { get; set; }
public DeleteObject.Parsed Delete { get; }
public RuntimeEntityRecord? RetiredCanonical { get; }
public bool RemoveRetainedObject { get; }
}
/// <summary>
/// Presentation-free ownership root for one session's canonical entity and
/// retained object lifetimes. Graphical and direct hosts borrow these exact
/// instances; they never allocate a second directory or object table.
/// </summary>
public sealed class RuntimeEntityObjectLifetime : IDisposable
{
private bool _sessionClearInProgress;
private bool _disposed;
/// <summary>C3c: see <see cref="BindInitialResidenceBeginNotification"/>.</summary>
private Action<RuntimeEntityRecord>? _initialResidenceBegan;
/// <summary>C3c-R1 review F5: see <see cref="RegisterFirstEntryDriveOwnership"/>.</summary>
private readonly List<Func<int>> _firstEntryDriveOwnership = [];
/// <summary>C4 route 2: see <see cref="RegisterAcceptedPositionDriveOwnership"/>.</summary>
private readonly List<Func<int>> _acceptedPositionDriveOwnership = [];
/// <summary>C4 route 4b-1: see <see cref="RegisterRemotePlacementDriveOwnership"/>.</summary>
private readonly List<Func<int>> _remotePlacementDriveOwnership = [];
/// <summary>
/// C4 route 4a: captured by <see cref="BindEventContext"/> alongside the
/// other generation-consuming children so
/// <see cref="ClassifyRemoteAcceptedPosition"/> can build a real
/// <c>RuntimeAuthoritativePositionAuthority</c> from the SAME generation
/// source every other accepted-position authority in this lifetime uses.
/// Never exposed: a host must not be able to read a generation token out
/// of this lifetime and assemble its own authority beside it.
/// </summary>
private Func<RuntimeGenerationToken>? _generation;
/// <summary>
/// #297 (review round 2, preferred fix): keeps every canonical
/// snapshot's <c>ObjectDescriptionFlags</c> live against
/// <c>ClientObjectTable.PublicWeenieBitfield</c> — see
/// <see cref="RuntimeEntityPvpBitfieldSnapshotSync"/> for the full
/// rationale.
/// </summary>
private readonly RuntimeEntityPvpBitfieldSnapshotSync _pvpBitfieldSync;
public RuntimeEntityObjectLifetime(
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
TimeProvider? timeProvider = null,
IGameRuntimeClock? gameClock = null)
{
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
Physics = new RuntimePhysicsState(
Entities,
timeProvider: timeProvider,
gameClock: gameClock);
Objects = new ClientObjectTable();
_pvpBitfieldSync = new RuntimeEntityPvpBitfieldSnapshotSync(Entities, Objects);
// AP-129 (Campaign P Slice P4 review fix, 2026-07-30): the physics
// entry-restriction gate (ObjectInfo.CheckEntryRestrictions) resolves
// a restricted cell's owner/guest list through the SAME live
// ClientObjectTable every other subsystem borrows from this owner —
// never a second table. Without this, every restricted cell fails
// closed for everyone (see PhysicsEngine.Objects).
Physics.Engine.Objects = Objects;
var views = new RuntimeEntityObjectViews(Entities, Objects);
EntityView = views.Entities;
InventoryView = views.Inventory;
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
Physics.SetPosition.BindEventStream(Events);
InitialCreateResidences = new RuntimeInitialCreateResidenceState(
Entities,
Physics.SetPosition);
InitialCreateExecution = new RuntimeInitialCreateContinuationExecutor(
Entities,
InitialCreateResidences,
Physics,
Events,
(spawn, isLocalPlayer) =>
RegisterEntityWithInitialResidence(spawn, isLocalPlayer),
(canonical, version, spawn, replaceGeneration) =>
ApplyAcceptedSpawn(canonical, version, spawn, replaceGeneration));
// C3a/F2: dormant - no production caller of its own Advance - but
// constructed and wired exactly like the executor above so its
// ownership converges the same way. RuntimeLocalPlayerPhysicsPublicationState
// does not exist yet at this point (GameRuntime builds
// RuntimeLocalPlayerMovementState, then attaches its publication,
// only after this lifetime); BindPublication supplies it later.
LocalPlayerFirstEntry = new RuntimeLocalPlayerFirstEntryState(
InitialCreateResidences,
InitialCreateExecution,
Physics);
// C3b: the remote/projectile analog of the conductor above — retail
// body construction at Create time in place of the publication
// chain. Dormant like its sibling (no production caller of Advance);
// constructed and wired identically so its ownership converges the
// same way.
RemoteFirstEntry = new RuntimeRemoteFirstEntryState(
InitialCreateResidences,
InitialCreateExecution,
Physics);
// Round 3 B3: every residence retirement path - not only the
// executor's own DiscardProgress calls - must converge the
// executor's progress AND its separately-tracked pending
// continuation placement token. This class owns both sides of the
// relationship, so it binds the delegate here rather than the
// residence state referencing the executor type directly.
InitialCreateResidences.BindRetirementNotification(
key => InitialCreateExecution.DiscardProgress(key));
// C3a/F2: the SAME multicast retirement notification also reaps the
// first-entry conductor's tracked progress - see
// RuntimeInitialCreateResidenceState.BindRetirementNotification's
// updated doc comment for why this is now multicast.
InitialCreateResidences.BindRetirementNotification(
key => LocalPlayerFirstEntry.Forget(key));
// C3b: the remote conductor joins the SAME multicast retirement
// fan-out, third in registration order.
InitialCreateResidences.BindRetirementNotification(
key => RemoteFirstEntry.Forget(key));
// F2: reaps the executor's completion-receipt correlation entry
// exactly when a host acknowledges the ExecutorCompleted receipt it
// correlates - mirrors the residence-retirement binding immediately
// above.
Physics.SetPosition.BindExecutorCompletionAcknowledgement(
(key, sequence) =>
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition,
InitialCreateExecution);
}
internal RuntimeEntityObjectLifetime(
PhysicsDataCache physicsDataCache,
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
TimeProvider? timeProvider = null,
IGameRuntimeClock? gameClock = null)
{
ArgumentNullException.ThrowIfNull(physicsDataCache);
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
Physics = new RuntimePhysicsState(
Entities,
physicsDataCache,
timeProvider,
gameClock);
Objects = new ClientObjectTable();
_pvpBitfieldSync = new RuntimeEntityPvpBitfieldSnapshotSync(Entities, Objects);
Physics.Engine.Objects = Objects;
var views = new RuntimeEntityObjectViews(Entities, Objects);
EntityView = views.Entities;
InventoryView = views.Inventory;
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
Physics.SetPosition.BindEventStream(Events);
InitialCreateResidences = new RuntimeInitialCreateResidenceState(
Entities,
Physics.SetPosition);
InitialCreateExecution = new RuntimeInitialCreateContinuationExecutor(
Entities,
InitialCreateResidences,
Physics,
Events,
(spawn, isLocalPlayer) =>
RegisterEntityWithInitialResidence(spawn, isLocalPlayer),
(canonical, version, spawn, replaceGeneration) =>
ApplyAcceptedSpawn(canonical, version, spawn, replaceGeneration));
// C3a/F2: dormant - no production caller of its own Advance - but
// constructed and wired exactly like the executor above so its
// ownership converges the same way. RuntimeLocalPlayerPhysicsPublicationState
// does not exist yet at this point (GameRuntime builds
// RuntimeLocalPlayerMovementState, then attaches its publication,
// only after this lifetime); BindPublication supplies it later.
LocalPlayerFirstEntry = new RuntimeLocalPlayerFirstEntryState(
InitialCreateResidences,
InitialCreateExecution,
Physics);
// C3b: the remote/projectile analog of the conductor above — retail
// body construction at Create time in place of the publication
// chain. Dormant like its sibling (no production caller of Advance);
// constructed and wired identically so its ownership converges the
// same way.
RemoteFirstEntry = new RuntimeRemoteFirstEntryState(
InitialCreateResidences,
InitialCreateExecution,
Physics);
// Round 3 B3: every residence retirement path - not only the
// executor's own DiscardProgress calls - must converge the
// executor's progress AND its separately-tracked pending
// continuation placement token. This class owns both sides of the
// relationship, so it binds the delegate here rather than the
// residence state referencing the executor type directly.
InitialCreateResidences.BindRetirementNotification(
key => InitialCreateExecution.DiscardProgress(key));
// C3a/F2: the SAME multicast retirement notification also reaps the
// first-entry conductor's tracked progress - see
// RuntimeInitialCreateResidenceState.BindRetirementNotification's
// updated doc comment for why this is now multicast.
InitialCreateResidences.BindRetirementNotification(
key => LocalPlayerFirstEntry.Forget(key));
// C3b: the remote conductor joins the SAME multicast retirement
// fan-out, third in registration order.
InitialCreateResidences.BindRetirementNotification(
key => RemoteFirstEntry.Forget(key));
// F2: reaps the executor's completion-receipt correlation entry
// exactly when a host acknowledges the ExecutorCompleted receipt it
// correlates - mirrors the residence-retirement binding immediately
// above.
Physics.SetPosition.BindExecutorCompletionAcknowledgement(
(key, sequence) =>
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition,
InitialCreateExecution);
}
internal RuntimeEntityObjectLifetime(
PhysicsEngine physicsEngine,
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
TimeProvider? timeProvider = null,
IGameRuntimeClock? gameClock = null)
{
ArgumentNullException.ThrowIfNull(physicsEngine);
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
Physics = new RuntimePhysicsState(
Entities,
physicsEngine,
timeProvider,
gameClock);
Objects = new ClientObjectTable();
_pvpBitfieldSync = new RuntimeEntityPvpBitfieldSnapshotSync(Entities, Objects);
Physics.Engine.Objects = Objects;
var views = new RuntimeEntityObjectViews(Entities, Objects);
EntityView = views.Entities;
InventoryView = views.Inventory;
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
Physics.SetPosition.BindEventStream(Events);
InitialCreateResidences = new RuntimeInitialCreateResidenceState(
Entities,
Physics.SetPosition);
InitialCreateExecution = new RuntimeInitialCreateContinuationExecutor(
Entities,
InitialCreateResidences,
Physics,
Events,
(spawn, isLocalPlayer) =>
RegisterEntityWithInitialResidence(spawn, isLocalPlayer),
(canonical, version, spawn, replaceGeneration) =>
ApplyAcceptedSpawn(canonical, version, spawn, replaceGeneration));
// C3a/F2: dormant - no production caller of its own Advance - but
// constructed and wired exactly like the executor above so its
// ownership converges the same way. RuntimeLocalPlayerPhysicsPublicationState
// does not exist yet at this point (GameRuntime builds
// RuntimeLocalPlayerMovementState, then attaches its publication,
// only after this lifetime); BindPublication supplies it later.
LocalPlayerFirstEntry = new RuntimeLocalPlayerFirstEntryState(
InitialCreateResidences,
InitialCreateExecution,
Physics);
// C3b: the remote/projectile analog of the conductor above — retail
// body construction at Create time in place of the publication
// chain. Dormant like its sibling (no production caller of Advance);
// constructed and wired identically so its ownership converges the
// same way.
RemoteFirstEntry = new RuntimeRemoteFirstEntryState(
InitialCreateResidences,
InitialCreateExecution,
Physics);
// Round 3 B3: every residence retirement path - not only the
// executor's own DiscardProgress calls - must converge the
// executor's progress AND its separately-tracked pending
// continuation placement token. This class owns both sides of the
// relationship, so it binds the delegate here rather than the
// residence state referencing the executor type directly.
InitialCreateResidences.BindRetirementNotification(
key => InitialCreateExecution.DiscardProgress(key));
// C3a/F2: the SAME multicast retirement notification also reaps the
// first-entry conductor's tracked progress - see
// RuntimeInitialCreateResidenceState.BindRetirementNotification's
// updated doc comment for why this is now multicast.
InitialCreateResidences.BindRetirementNotification(
key => LocalPlayerFirstEntry.Forget(key));
// C3b: the remote conductor joins the SAME multicast retirement
// fan-out, third in registration order.
InitialCreateResidences.BindRetirementNotification(
key => RemoteFirstEntry.Forget(key));
// F2: reaps the executor's completion-receipt correlation entry
// exactly when a host acknowledges the ExecutorCompleted receipt it
// correlates - mirrors the residence-retirement binding immediately
// above.
Physics.SetPosition.BindExecutorCompletionAcknowledgement(
(key, sequence) =>
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition,
InitialCreateExecution);
}
public RuntimeEntityDirectory Entities { get; }
public RuntimePhysicsState Physics { get; }
public ClientObjectTable Objects { get; }
public IRuntimeEntityView EntityView { get; }
public IRuntimeInventoryView InventoryView { get; }
public RuntimeEntityObjectEventStream Events { get; }
public RuntimePlacementProjectionChannel Placements { get; }
internal RuntimeInitialCreateResidenceState InitialCreateResidences
{ get; }
internal RuntimeInitialCreateContinuationExecutor InitialCreateExecution
{ get; }
internal RuntimeLocalPlayerFirstEntryState LocalPlayerFirstEntry { get; }
internal RuntimeRemoteFirstEntryState RemoteFirstEntry { get; }
public RuntimeEntityObjectOwnershipSnapshot CaptureOwnership()
{
ParentAttachmentState parents = Entities.ParentAttachments;
RuntimeInitialCreateResidenceOwnershipSnapshot initialResidence =
InitialCreateResidences.CaptureOwnership();
return new RuntimeEntityObjectOwnershipSnapshot(
Entities.Count,
Entities.PendingTeardownCount,
Entities.ClaimedLocalIdCount,
Entities.Snapshots.Count,
parents.UnresolvedRelationCount,
parents.DeferredCreateCount,
parents.StagedRelationCount,
parents.RecoveryRelationCount,
parents.CommittedRelationCount,
Objects.ObjectCount,
Objects.ContainerCount,
Objects.ContainerProjectionCount,
Objects.EquipmentOwnerCount,
Objects.PendingMoveCount,
initialResidence.ActiveLeaseCount
+ initialResidence.PendingAdoptionCount,
InitialCreateExecution.ProgressCount,
Events.SubscriberCount,
Events.PlacementSubscriberCount,
Events.DispatchFailureCount,
Events.LastDispatchFailure is not null,
Events.PendingDispatchCount,
Events.IsDispatching,
_sessionClearInProgress,
_disposed,
parents.DeferredAcceptedRelationCount,
InitialCreateExecution.ReplayFailureCount,
InitialCreateExecution.LastReplayFailure is not null,
InitialCreateExecution.PendingCompletionReceiptCount,
LocalPlayerFirstEntry.CaptureOwnership().ActiveCount,
RemoteFirstEntry.CaptureOwnership().ActiveCount,
CaptureFirstEntryDrivePendingCount(),
CaptureAcceptedPositionDrivePendingCount(),
CaptureRemotePlacementDrivePendingCount());
}
private int CaptureFirstEntryDrivePendingCount()
{
int total = 0;
for (int i = 0; i < _firstEntryDriveOwnership.Count; i++)
total = checked(total + _firstEntryDriveOwnership[i]());
return total;
}
private int CaptureAcceptedPositionDrivePendingCount()
{
int total = 0;
for (int i = 0; i < _acceptedPositionDriveOwnership.Count; i++)
total = checked(total + _acceptedPositionDriveOwnership[i]());
return total;
}
private int CaptureRemotePlacementDrivePendingCount()
{
int total = 0;
for (int i = 0; i < _remotePlacementDriveOwnership.Count; i++)
total = checked(total + _remotePlacementDriveOwnership[i]());
return total;
}
/// <summary>
/// C3c-R1 review F5: registers one host first-entry drive controller's
/// pending-count provider into this lifetime's ownership snapshot, so
/// tracked-but-undriven entries can never sit outside every ledger. The
/// drive controller registers itself at construction (it already binds
/// <see cref="BindInitialResidenceBeginNotification"/> there); multiple
/// registrations sum, mirroring the multicast notification shape.
/// </summary>
public void RegisterFirstEntryDriveOwnership(Func<int> pendingCount)
{
ArgumentNullException.ThrowIfNull(pendingCount);
EnsureNotDisposed();
_firstEntryDriveOwnership.Add(pendingCount);
}
/// <summary>
/// C4 route 2: registers one host <c>RuntimeAcceptedPositionDriveController</c>'s
/// pending-count provider into this lifetime's ownership snapshot,
/// mirroring <see cref="RegisterFirstEntryDriveOwnership"/> — a leaked
/// pending ForcePosition ack must not sit outside every ledger. The
/// drive controller registers itself at construction; multiple
/// registrations sum (one per host route sharing this lifetime).
/// </summary>
public void RegisterAcceptedPositionDriveOwnership(Func<int> pendingCount)
{
ArgumentNullException.ThrowIfNull(pendingCount);
EnsureNotDisposed();
_acceptedPositionDriveOwnership.Add(pendingCount);
}
/// <summary>
/// C4 route 4b-1: registers one host
/// <c>RuntimeRemotePlacementDriveController</c>'s pending-count provider
/// into this lifetime's ownership snapshot, mirroring
/// <see cref="RegisterAcceptedPositionDriveOwnership"/> — a leaked
/// remote preparation retry must not sit outside every ledger. The drive
/// controller registers itself at construction; multiple registrations
/// sum (one per host route sharing this lifetime).
/// </summary>
public void RegisterRemotePlacementDriveOwnership(Func<int> pendingCount)
{
ArgumentNullException.ThrowIfNull(pendingCount);
EnsureNotDisposed();
_remotePlacementDriveOwnership.Add(pendingCount);
}
public void BindEventContext(
Func<RuntimeGenerationToken> generation,
Func<ulong> frameNumber)
{
EnsureNotDisposed();
_generation = generation;
Events.BindContext(generation, frameNumber);
Placements.BindGeneration(generation);
InitialCreateResidences.BindGeneration(generation);
InitialCreateExecution.BindGeneration(generation);
}
/// <summary>
/// C4 route 4a: classifies one REMOTE incarnation's accepted Position
/// through <see cref="RuntimeAuthoritativePositionRouteClassifier"/>, so
/// the graphical host and any future no-window remote-motion host make
/// the SAME airborne-no-op / near-interpolate decision from the same
/// generation, the same authority shape, and the same request builder the
/// deferred initial-create continuation uses.
///
/// <para>
/// Returns <see langword="null"/> when no classification can honestly be
/// made: the lifetime has no bound generation yet, the canonical record
/// has not claimed a local id, or there is no live local-player position
/// to derive retail's <c>player_distance</c> from. A null here is
/// "Runtime has no opinion", never "rejected".
/// </para>
///
/// <para>
/// C4 route 4b-2 review fix — this used to add "In every one of those
/// cases the caller's pre-existing legacy path runs completely
/// unchanged". That path no longer exists: 4b-2 deleted the graphical
/// caller's duplicated near/far blocks, and a null now takes the stated
/// <c>UnroutedCatchUp</c> policy (AP-137;
/// <c>RuntimeRemoteFarSnapPosition.ResolveArm</c>), which is AP-87's
/// shared catch-up rather than a re-derived 96 m test.
/// </para>
/// </summary>
internal RuntimeAuthoritativePositionRoute? ClassifyRemoteAcceptedPosition(
RuntimeEntityRecord canonical,
in WorldSession.EntityPositionUpdate update,
PositionTimestampDisposition disposition,
in AcceptedPhysicsTimestamps timestamps,
float? playerDistance)
{
ArgumentNullException.ThrowIfNull(canonical);
if (_generation is not { } generation
// C4 route 4b-3 (D1): a remote PositionEvent feeds the
// classifier's cell-less predicate the PRE-merge committed cell
// — the same value TryApplyPosition measured for THIS packet,
// never re-read from the (already merged) canonical record. A
// null here means the merge never observed a prior canonical
// record for this entity, which "Runtime has no opinion" — the
// same policy every other missing-input case in this method
// already uses — covers honestly rather than fabricating 0.
|| timestamps.PreMergeCommittedCellId is not { } preMergeCommittedCellId)
{
return null;
}
if (!RuntimeAcceptedPositionRouteRequests.TryBuild(
generation(),
canonical,
update,
RuntimePositionEntityKind.Remote,
RuntimeAcceptedPositionSource.PositionEvent,
disposition,
timestamps.PreviousTeleport,
timestamps.Teleport,
playerDistance,
// Retail's UsePositionFromServer is consumed by the local
// player branch only; the Remote branch never reads it.
usePositionFromServer: false,
preMergeCommittedCellId,
out RuntimeAcceptedPositionRouteRequest request))
{
return null;
}
return RuntimeAuthoritativePositionRouteClassifier
.ClassifyAcceptedPosition(request);
}
/// <summary>
/// C3c: registers one host callback fired for every FRESH initial-create
/// residence begin (never for a same-generation FIFO append). Multicast,
/// mirroring <see cref="RuntimeInitialCreateResidenceState.BindRetirementNotification"/>.
/// The callback runs synchronously inside the registration transaction —
/// subscribers must only record the entity for a later drive pump, never
/// call a conductor's Advance re-entrantly from it.
/// </summary>
public void BindInitialResidenceBeginNotification(
Action<RuntimeEntityRecord> began)
{
ArgumentNullException.ThrowIfNull(began);
EnsureNotDisposed();
_initialResidenceBegan += began;
}
/// <summary>
/// C0-2: forwards to <see cref="RuntimeInitialCreateContinuationExecutor.BindLiveInputs"/>,
/// the same fan-out shape <see cref="BindEventContext"/> already uses for
/// generation binding. Separate from <see cref="BindEventContext"/>
/// because <c>GameRuntime</c> constructs <c>RuntimeCharacterState</c>/
/// <c>RuntimeLocalPlayerMovementState</c> (the real source owners) AFTER
/// this lifetime, so the live-input bind necessarily happens at a later
/// point in <c>GameRuntime</c>'s construction sequence than the
/// generation bind.
/// </summary>
public void BindLiveInputs(
Func<bool> usePositionFromServer,
Func<Vector3?> localPlayerPosition)
{
EnsureNotDisposed();
InitialCreateExecution.BindLiveInputs(
usePositionFromServer, localPlayerPosition);
}
/// <summary>
/// Owns the presentation-free half of retail's CreateObject lifetime
/// transaction. An attached graphical host may synchronously retire the
/// displaced projection through <paramref name="retirePriorProjection"/>;
/// a direct host omits it and Runtime retires canonical-only state.
/// </summary>
public RuntimeEntityRegistrationResult RegisterEntity(
WorldSession.EntitySpawn incoming,
Func<RuntimeEntityRecord, Exception?>? retirePriorProjection = null) =>
RegisterEntityCore(
incoming,
beginInitialResidence: false,
isLocalPlayer: false,
retirePriorProjection);
internal RuntimeEntityRegistrationResult RegisterEntityWithInitialResidence(
WorldSession.EntitySpawn incoming,
bool isLocalPlayer,
Func<RuntimeEntityRecord, Exception?>? retirePriorProjection = null) =>
RegisterEntityCore(
incoming,
beginInitialResidence: true,
isLocalPlayer,
retirePriorProjection);
private RuntimeEntityRegistrationResult RegisterEntityCore(
WorldSession.EntitySpawn incoming,
bool beginInitialResidence,
bool isLocalPlayer,
Func<RuntimeEntityRecord, Exception?>? retirePriorProjection)
{
EnsureNotDisposed();
if (beginInitialResidence && isLocalPlayer)
{
// The accepted local Create establishes the shared world frame
// before any remote first-entry conductor converts its authored
// landblock-local coordinates. #284: observe the Create itself
// even when it carries no usable landblock - that is precisely
// the case in which no frame is ever published, and every remote
// placement would otherwise park forever without a diagnostic.
Physics.ObserveLocalPlayerCreate(
(incoming.Physics?.Position ?? incoming.Position)
?.LandblockId ?? 0u);
}
if (_sessionClearInProgress)
{
throw new InvalidOperationException(
"A Runtime entity cannot register while its session lifetime is clearing.");
}
if (beginInitialResidence
&& !HasConsistentCreateIdentityAndParent(incoming))
{
throw new InvalidOperationException(
$"CreateObject 0x{incoming.Guid:X8} has inconsistent instance or parent projections.");
}
if (beginInitialResidence)
incoming = RuntimeInitialCreateAdmissionFreezer.Freeze(incoming);
uint parentGuid = incoming.ParentGuid
?? incoming.Physics?.Parent?.Guid
?? 0u;
if (beginInitialResidence
&& parentGuid != 0u
&& !Entities.TryGetActive(parentGuid, out _))
{
// SmartBox::HandleCreateObject resolves a nonzero parent before
// object lookup or timestamp admission. Retain the complete raw
// CreateObject so no child gate/canonical/projection state can
// escape before the parent becomes addressable.
Entities.ParentAttachments.EnqueueDeferredCreate(
incoming,
isLocalPlayer);
return new RuntimeEntityRegistrationResult(
SupersededCreateResult(),
Canonical: null,
LogicalRegistrationCreated: false,
ReplacedExistingGeneration: false,
DeferredForParent: true);
}
CreateObjectTimestampDisposition preview =
Entities.PreviewCreateDisposition(incoming);
bool requiresFreshResidenceAdmission = preview is
CreateObjectTimestampDisposition.InitialGeneration
or CreateObjectTimestampDisposition.NewGeneration;
if (beginInitialResidence
&& requiresFreshResidenceAdmission
&& !InitialCreateResidences.CanAcceptCreate(incoming))
{
throw new InvalidOperationException(
$"CreateObject 0x{incoming.Guid:X8} cannot acquire a structurally valid initial residence lease.");
}
RuntimeEntityRecord? pendingResidenceRecord = null;
RuntimeInitialCreateResidenceLease pendingResidence = default;
bool admitIntoPendingResidence = beginInitialResidence
&& preview is CreateObjectTimestampDisposition.ExistingGeneration
&& Entities.TryGetActive(
incoming.Guid,
out pendingResidenceRecord)
&& InitialCreateResidences.TryGetTransaction(
pendingResidenceRecord,
out pendingResidence);
if (admitIntoPendingResidence
&& !InitialCreateResidences.CanEnqueue(
pendingResidenceRecord!,
pendingResidence))
{
throw new InvalidOperationException(
$"CreateObject 0x{incoming.Guid:X8} cannot append to its pending initial residence FIFO.");
}
if (admitIntoPendingResidence
&& !IsStructurallyValidDeferredCreate(incoming))
{
_ = Entities.TryGetAcceptedTimestamps(
incoming.Guid,
out AcceptedPhysicsTimestamps timestamps);
return new RuntimeEntityRegistrationResult(
new InboundCreateResult(
CreateObjectTimestampDisposition.ExistingGeneration,
pendingResidenceRecord!.Snapshot,
SameGenerationEvents: null,
timestamps),
pendingResidenceRecord,
LogicalRegistrationCreated: false,
ReplacedExistingGeneration: false);
}
InboundCreateResult result = admitIntoPendingResidence
? Entities.AcceptCreateDeferredSameGeneration(incoming)
: Entities.AcceptCreate(incoming);
if (result.Disposition
is CreateObjectTimestampDisposition.StaleGeneration)
{
return new RuntimeEntityRegistrationResult(
result,
Canonical: null,
LogicalRegistrationCreated: false,
ReplacedExistingGeneration: false);
}
ulong sessionVersion = Entities.SessionLifetimeVersion;
ulong operationVersion =
Entities.AdvanceLifetimeMutation(incoming.Guid);
if (result.Disposition
is CreateObjectTimestampDisposition.ExistingGeneration)
{
if (Entities.TryGetActive(
incoming.Guid,
out RuntimeEntityRecord retained))
{
if (admitIntoPendingResidence)
{
if (!ReferenceEquals(retained, pendingResidenceRecord)
|| !AdmitSameGenerationCreate(
retained,
pendingResidence,
incoming,
result,
isLocalPlayer))
{
throw FailInitialResidenceRegistration(
retained,
publishDeleted: true);
}
InboundCreateResult dormant = result with
{
Snapshot = retained.Snapshot,
SameGenerationEvents = null,
};
return new RuntimeEntityRegistrationResult(
dormant,
retained,
LogicalRegistrationCreated: false,
ReplacedExistingGeneration: false);
}
// Existing-generation CreateObject contributes untimestamped
// description fields here. Position/Parent/Pickup/State/etc.
// remain separate freshness-gated events and must not churn a
// pending initial placement or re-preclaim its wire cell.
Entities.RefreshSnapshot(
retained,
result.Snapshot,
refreshPosition: !beginInitialResidence);
if (!beginInitialResidence)
Entities.AdvanceCreateAuthority(retained);
PublishEntity(RuntimeEntityChange.Updated, retained);
if (!IsCurrentOperation(
incoming.Guid,
retained,
sessionVersion,
operationVersion))
{
return SupersededRegistration(
incoming.Guid,
replacedExistingGeneration: false);
}
return new RuntimeEntityRegistrationResult(
result,
retained,
LogicalRegistrationCreated: false,
ReplacedExistingGeneration: false);
}
if (Entities.TryGetTeardown(
incoming.Guid,
result.Snapshot.InstanceSequence,
out _))
{
return new RuntimeEntityRegistrationResult(
result,
Canonical: null,
LogicalRegistrationCreated: false,
ReplacedExistingGeneration: false);
}
if (beginInitialResidence
&& !InitialCreateResidences.CanAcceptCreate(result.Snapshot))
{
throw new InvalidOperationException(
$"Recovered CreateObject 0x{incoming.Guid:X8} cannot acquire a structurally valid initial residence lease.");
}
RuntimeEntityRecord recovered = Entities.AddActive(result.Snapshot);
if (!InitializeAcceptedCreateResidence(
recovered,
result,
beginInitialResidence,
isLocalPlayer))
{
throw FailInitialResidenceRegistration(
recovered,
publishDeleted: false);
}
PublishEntity(RuntimeEntityChange.Registered, recovered);
if (!IsCurrentOperation(
incoming.Guid,
recovered,
sessionVersion,
operationVersion))
{
return SupersededRegistration(
incoming.Guid,
replacedExistingGeneration: false);
}
return new RuntimeEntityRegistrationResult(
result,
recovered,
LogicalRegistrationCreated: true,
ReplacedExistingGeneration: false);
}
bool replaced = Entities.RemoveActive(
incoming.Guid,
out RuntimeEntityRecord? prior);
if (result.Disposition
is CreateObjectTimestampDisposition.NewGeneration)
{
Entities.ParentAttachments.EndGeneration(
incoming.Guid,
result.Snapshot.InstanceSequence);
}
Exception? cleanupFailure = null;
if (prior is not null)
{
// Removing the active GUID is an ownership transfer, not a gap.
// Retain the exact incarnation before arbitrary synchronous
// observers can re-enter registration, reset, or disposal.
Entities.RetainTeardown(prior);
try
{
PublishEntity(RuntimeEntityChange.Deleted, prior);
}
catch (Exception error)
{
cleanupFailure = error;
}
Exception? projectionFailure = retirePriorProjection is null
? RetireCanonicalOnly(prior)
: retirePriorProjection(prior);
cleanupFailure = Combine(cleanupFailure, projectionFailure);
}
if (Entities.SessionLifetimeVersion != sessionVersion
|| Entities.CurrentLifetimeMutation(incoming.Guid)
!= operationVersion)
{
if (cleanupFailure is not null)
{
throw new AggregateException(
$"Prior incarnation of live entity 0x{incoming.Guid:X8} failed teardown while its incoming replacement was superseded.",
cleanupFailure);
}
return new RuntimeEntityRegistrationResult(
SupersededCreateResult(),
Entities.TryGetActive(
incoming.Guid,
out RuntimeEntityRecord current)
? current
: null,
LogicalRegistrationCreated: false,
ReplacedExistingGeneration: replaced);
}
RuntimeEntityRecord canonical = Entities.AddActive(result.Snapshot);
if (!InitializeAcceptedCreateResidence(
canonical,
result,
beginInitialResidence,
isLocalPlayer))
{
throw FailInitialResidenceRegistration(
canonical,
publishDeleted: false);
}
try
{
PublishEntity(RuntimeEntityChange.Registered, canonical);
}
catch (Exception error)
{
if (cleanupFailure is not null)
{
throw new AggregateException(
$"Live entity 0x{incoming.Guid:X8} registered after prior cleanup and commit observers failed.",
cleanupFailure,
error);
}
throw;
}
if (!IsCurrentOperation(
incoming.Guid,
canonical,
sessionVersion,
operationVersion))
{
if (cleanupFailure is not null)
{
throw new AggregateException(
$"Prior incarnation of live entity 0x{incoming.Guid:X8} failed teardown while its committed replacement was superseded.",
cleanupFailure);
}
return SupersededRegistration(
incoming.Guid,
replaced);
}
return new RuntimeEntityRegistrationResult(
result,
canonical,
LogicalRegistrationCreated: true,
ReplacedExistingGeneration: replaced,
cleanupFailure);
}
/// <summary>
/// Completes a displaced incarnation that never crossed the graphical
/// projection acquisition edge.
/// </summary>
public Exception? RetireCanonicalOnly(RuntimeEntityRecord canonical)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(canonical);
try
{
Entities.RetainTeardown(canonical);
try
{
CompleteProjectionRetirement(canonical);
}
finally
{
Entities.ReleaseTeardown(canonical);
}
return null;
}
catch (Exception error)
{
return error;
}
}
/// <summary>
/// Completes the presentation acknowledgement for one retained
/// incarnation. Runtime retires every presentation-independent component
/// and releases its local identity only after the graphical host has
/// finished using those exact components for ExitWorld teardown.
/// </summary>
internal void CompleteProjectionRetirement(
RuntimeEntityRecord canonical)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(canonical);
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(canonical);
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(
canonical,
releasePreparedMover: true);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(initialCancellation, ordinaryCancellation);
Physics.CollisionReports.Forget(canonical);
Physics.RemoveSpatialProjection(canonical);
Entities.SetRemoteMotion(canonical, null);
Entities.SetRemoteMotionBindingInProgress(canonical, false);
Entities.SetProjectile(canonical, null);
Entities.SetProjectileBindingInProgress(canonical, false);
Entities.SetRequiresRemotePlacementRuntime(canonical, false);
Entities.SetPhysicsHost(canonical, null);
Entities.SetPhysicsBody(canonical, null);
Entities.SetPhysicsBodyAcquisitionInProgress(canonical, false);
Entities.SetHasPartArray(canonical, false);
Entities.ReleaseLocalId(canonical);
Physics.SetPosition.PublishCancellation(cancellation);
}
/// <summary>
/// Applies the retained-object half of an accepted CreateObject while the
/// exact canonical incarnation and its integration version remain current.
/// Object-table callbacks are synchronous and may re-enter entity lifetime,
/// so the acceptance predicate is checked before, during, and after apply.
/// </summary>
public bool ApplyAcceptedSpawn(
RuntimeEntityRecord canonical,
ulong expectedCreateIntegrationVersion,
WorldSession.EntitySpawn spawn,
bool replaceGeneration)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(canonical);
if (canonical.ServerGuid != spawn.Guid
|| canonical.Incarnation != spawn.InstanceSequence)
{
return false;
}
bool IsCurrent() =>
Entities.IsCurrent(canonical)
&& canonical.CreateIntegrationVersion
== expectedCreateIntegrationVersion;
return IsCurrent()
&& ObjectTableWiring.ApplyEntitySpawn(
Objects,
spawn,
replaceGeneration,
IsCurrent)
&& IsCurrent();
}
public bool TryApplyObjDesc(
ObjDescEvent.Parsed update,
Action<RuntimeEntityRecord>? acknowledgeProjection,
out WorldSession.EntitySpawn accepted)
{
EnsureNotDisposed();
if (TryGetPendingInitialResidence(
update.Guid,
out RuntimeEntityRecord pending,
out RuntimeInitialCreateResidenceLease lease))
{
if (!InitialCreateResidences.CanEnqueue(pending, lease))
{
accepted = pending.Snapshot;
return false;
}
bool acceptedByGate = Entities.TryAcceptDeferredObjDesc(
update,
out _);
accepted = pending.Snapshot;
if (!acceptedByGate)
return false;
EnqueueDormant(
pending,
lease,
RuntimeInitialCreateContinuationKind.ObjDesc,
RuntimeAcceptedPositionSource.Unknown,
new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.ObjDesc,
update.Guid,
ObjDesc: update));
return true;
}
bool applied = Entities.TryApplyObjDesc(update, out accepted);
if (!applied
|| !Entities.TryGetActive(
update.Guid,
out RuntimeEntityRecord canonical))
{
return applied;
}
Entities.RefreshSnapshot(canonical, accepted);
Entities.AdvanceObjDescAuthority(canonical);
ulong authorityVersion = canonical.ObjDescAuthorityVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Updated,
() => canonical.ObjDescAuthorityVersion == authorityVersion);
}
public bool TryApplyPickup(
PickupEvent.Parsed update,
Action<RuntimeEntityRecord>? acknowledgeProjection,
out WorldSession.EntitySpawn accepted)
{
EnsureNotDisposed();
if (TryGetPendingInitialResidence(
update.Guid,
out RuntimeEntityRecord pending,
out RuntimeInitialCreateResidenceLease lease))
{
if (!InitialCreateResidences.CanEnqueue(pending, lease))
{
accepted = pending.Snapshot;
return false;
}
bool acceptedByGate = Entities.TryAcceptDeferredPickup(
update,
out _);
accepted = pending.Snapshot;
if (!acceptedByGate)
return false;
EnqueueDormant(
pending,
lease,
RuntimeInitialCreateContinuationKind.Pickup,
RuntimeAcceptedPositionSource.Unknown,
new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.Pickup,
update.Guid,
Pickup: update));
return true;
}
bool applied = Entities.TryApplyPickup(update, out accepted);
if (!applied
|| !Entities.TryGetActive(
update.Guid,
out RuntimeEntityRecord canonical))
{
return applied;
}
Entities.RefreshSnapshot(canonical, accepted);
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(canonical);
Entities.AdvancePositionAuthority(canonical);
Physics.CollisionReports.LeaveWorld(canonical);
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(canonical);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(initialCancellation, ordinaryCancellation);
Entities.SuspendObjectClock(canonical);
Entities.SetFullCell(canonical, 0u, 0u);
Entities.ParentAttachments.EndChildProjection(update.Guid);
ulong positionVersion = canonical.PositionAuthorityVersion;
ulong spatialVersion = canonical.SpatialAuthorityVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Withdrawn,
() => canonical.PositionAuthorityVersion == positionVersion
&& canonical.SpatialAuthorityVersion == spatialVersion,
cancellation);
}
public bool TryApplyCreateParent(
CreateParentUpdate update,
Action<RuntimeEntityRecord>? acknowledgeProjection,
out WorldSession.EntitySpawn accepted)
{
EnsureNotDisposed();
if (TryGetPendingInitialResidence(
update.ChildGuid,
out _,
out _))
{
throw new InvalidOperationException(
"A CreateObject parent relation must be admitted inside its atomic same-generation Create envelope.");
}
bool applied = Entities.TryApplyCreateParent(update, out accepted);
return CommitPositionChannelUpdate(
applied,
update.ChildGuid,
accepted,
acknowledgeProjection);
}
public bool TryApplyParent(
ParentEvent.Parsed update,
Action<RuntimeEntityRecord>? acknowledgeProjection,
out WorldSession.EntitySpawn accepted)
{
EnsureNotDisposed();
if (TryGetPendingInitialResidence(
update.ChildGuid,
out RuntimeEntityRecord pending,
out RuntimeInitialCreateResidenceLease lease))
{
if (!InitialCreateResidences.CanEnqueue(pending, lease))
{
accepted = pending.Snapshot;
return false;
}
if (!Entities.TryGetActive(
update.ParentGuid,
out RuntimeEntityRecord parent)
|| parent.Incarnation != update.ParentInstanceSequence)
{
Entities.ParentAttachments.Enqueue(update);
accepted = pending.Snapshot;
return false;
}
bool acceptedByGate = Entities.TryAcceptDeferredParent(
update,
out AcceptedPhysicsTimestamps timestamps);
accepted = pending.Snapshot;
if (!acceptedByGate)
return false;
EnqueueDormant(
pending,
lease,
RuntimeInitialCreateContinuationKind.Parent,
RuntimeAcceptedPositionSource.Unknown,
new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.Parent,
update.ChildGuid,
Parent: update,
AcceptedTimestamps: timestamps));
return true;
}
bool applied = Entities.TryApplyParent(update, out accepted);
return CommitPositionChannelUpdate(
applied,
update.ChildGuid,
accepted,
acknowledgeProjection);
}
public bool TryCommitParent(
ParentAttachmentRelation relation,
Action<RuntimeEntityRecord>? acknowledgeProjection,
out WorldSession.EntitySpawn accepted)
{
EnsureNotDisposed();
bool committed = Entities.TryCommitParent(
relation.ChildGuid,
relation.ParentGuid,
relation.ParentLocation,
relation.PlacementId,
relation.ChildPositionSequence,
out accepted);
if (!committed
|| !Entities.TryGetActive(
relation.ChildGuid,
out RuntimeEntityRecord canonical))
{
return committed;
}
Entities.RefreshSnapshot(canonical, accepted);
// C0-4(a): this method had NO cancellation choke-point at all,
// leaving a residence lease or ordinary SetPosition operation
// dangling once ordinary placement traffic goes live - fixed with
// the SAME exactly-once ForgetInitialCreateResidence ->
// Physics.SetPosition.Forget -> PreferCancellation sequence every
// other commit in this family (CommitPositionChannelUpdate,
// TryApplyPickup, CommitAcceptedParentCellless, TryAcceptDelete)
// uses for THIS part of the job.
// F4 (deliberate, NOT an oversight): unlike CommitPositionChannelUpdate,
// this method does NOT also call Physics.CollisionReports.LeaveWorld.
// Retail set_parent (0x00515A90, lines 283832-283833) performs its
// single leave_world call gated behind the SAME add_child branch this
// method's staged/deferred-replay commit represents (App's
// EquippedChildRenderController realize sequence, the executor's own
// ParentRelationReplay) - a second LeaveWorld here would double-leave-
// world with no retail counterpart.
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(canonical);
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(canonical);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(initialCancellation, ordinaryCancellation);
Entities.AdvanceParentCommit(canonical);
ulong parentCommitVersion = canonical.ParentCommitVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Updated,
() => canonical.ParentCommitVersion
== parentCommitVersion,
cancellation);
}
public bool CommitAcceptedParentCellless(
RuntimeEntityRecord canonical,
ulong positionAuthorityVersion,
Action<RuntimeEntityRecord>? acknowledgeProjection)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(canonical);
if (!Entities.IsCurrent(canonical)
|| canonical.PositionAuthorityVersion != positionAuthorityVersion)
{
return false;
}
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(canonical);
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(canonical);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(initialCancellation, ordinaryCancellation);
Physics.CollisionReports.LeaveWorld(canonical);
Entities.SuspendObjectClock(canonical);
Entities.SetFullCell(canonical, 0u, 0u);
ulong spatialVersion = canonical.SpatialAuthorityVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Withdrawn,
() => canonical.PositionAuthorityVersion
== positionAuthorityVersion
&& canonical.SpatialAuthorityVersion == spatialVersion,
cancellation);
}
public bool TryApplyMotion(
WorldSession.EntityMotionUpdate update,
bool retainPayload,
Action<RuntimeEntityRecord>? acknowledgeProjection,
out WorldSession.EntitySpawn accepted,
out AcceptedPhysicsTimestamps timestamps)
{
EnsureNotDisposed();
if (TryGetPendingInitialResidence(
update.Guid,
out RuntimeEntityRecord pending,
out RuntimeInitialCreateResidenceLease lease))
{
if (!InitialCreateResidences.CanEnqueue(pending, lease))
{
accepted = pending.Snapshot;
timestamps = default;
return false;
}
bool payloadApplied = Entities.TryAcceptDeferredMotion(
update,
out timestamps,
out bool timestampMutation);
accepted = pending.Snapshot;
if (!payloadApplied && !timestampMutation)
return false;
EnqueueDormant(
pending,
lease,
RuntimeInitialCreateContinuationKind.Movement,
RuntimeAcceptedPositionSource.Unknown,
new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.Movement,
update.Guid,
Movement: update,
AcceptedTimestamps: timestamps,
AppliesMovementPayload: payloadApplied,
RetainMovementPayload: retainPayload,
HasTimestampMutation: timestampMutation));
return payloadApplied;
}
bool applied = Entities.TryApplyMotion(
update,
retainPayload,
out accepted,
out timestamps);
if (Entities.TryGetSnapshot(
update.Guid,
out WorldSession.EntitySpawn snapshot)
&& Entities.TryGetActive(
update.Guid,
out RuntimeEntityRecord canonical))
{
Entities.RefreshSnapshot(canonical, snapshot);
if (applied && retainPayload)
Entities.AdvanceMovementAuthority(canonical);
if (applied)
{
Entities.AdvanceMovementCommit(canonical);
ulong movementCommitVersion =
canonical.MovementCommitVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Updated,
() => canonical.MovementCommitVersion
== movementCommitVersion);
}
acknowledgeProjection?.Invoke(canonical);
}
return applied;
}
public bool TryApplyVector(
VectorUpdate.Parsed update,
Action<RuntimeEntityRecord>? acknowledgeProjection,
out WorldSession.EntitySpawn accepted)
{
EnsureNotDisposed();
if (TryGetPendingInitialResidence(
update.Guid,
out RuntimeEntityRecord pending,
out RuntimeInitialCreateResidenceLease lease))
{
if (!IsFinite(update.Velocity)
|| !IsFinite(update.Omega)
|| !InitialCreateResidences.CanEnqueue(pending, lease))
{
accepted = pending.Snapshot;
return false;
}
bool acceptedByGate = Entities.TryAcceptDeferredVector(
update,
out _);
accepted = pending.Snapshot;
if (!acceptedByGate)
return false;
EnqueueDormant(
pending,
lease,
RuntimeInitialCreateContinuationKind.Vector,
RuntimeAcceptedPositionSource.Unknown,
new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.Vector,
update.Guid,
Vector: update));
return true;
}
bool applied = Entities.TryApplyVector(update, out accepted);
if (!applied
|| !Entities.TryGetActive(
update.Guid,
out RuntimeEntityRecord canonical))
{
return applied;
}
Entities.RefreshSnapshot(canonical, accepted);
Entities.AdvanceVectorAuthority(canonical);
ulong authorityVersion = canonical.VectorAuthorityVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Updated,
() => canonical.VectorAuthorityVersion == authorityVersion);
}
public bool TryApplyState(
SetState.Parsed update,
Action<RuntimeEntityRecord, RetailPhysicsStateTransition>?
acknowledgeProjection,
out WorldSession.EntitySpawn accepted,
out RetailPhysicsStateTransition transition)
{
EnsureNotDisposed();
if (TryGetPendingInitialResidence(
update.Guid,
out RuntimeEntityRecord pending,
out RuntimeInitialCreateResidenceLease lease))
{
if (!InitialCreateResidences.CanEnqueue(pending, lease))
{
accepted = pending.Snapshot;
transition = default;
return false;
}
bool acceptedByGate = Entities.TryAcceptDeferredState(
update,
out _);
accepted = pending.Snapshot;
transition = default;
if (!acceptedByGate)
return false;
EnqueueDormant(
pending,
lease,
RuntimeInitialCreateContinuationKind.State,
RuntimeAcceptedPositionSource.Unknown,
new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.State,
update.Guid,
State: update));
return true;
}
bool applied = Entities.TryApplyState(update, out accepted);
transition = default;
if (!applied
|| !Entities.TryGetActive(
update.Guid,
out RuntimeEntityRecord canonical))
{
return applied;
}
Entities.RefreshSnapshot(canonical, accepted);
RetailPhysicsStateTransition preview =
RetailPhysicsStateTransitions.Apply(
canonical.FinalPhysicsState,
(PhysicsStateFlags)update.PhysicsState);
ulong priorPhysicsMutation = canonical.PhysicsStateMutationVersion;
if (preview.HiddenTransition is RetailHiddenTransition.BecameHidden)
{
Physics.CollisionReports.LeaveWorld(canonical);
if (!Entities.IsCurrent(canonical)
|| canonical.PhysicsStateMutationVersion
!= priorPhysicsMutation)
{
transition = default;
return false;
}
}
transition = Entities.ApplyRawPhysicsState(
canonical,
update.PhysicsState);
if (canonical.Key is { } key)
{
Physics.Engine.ShadowObjects.UpdatePhysicsState(
key.LocalEntityId,
(uint)canonical.FinalPhysicsState);
}
ulong stateVersion = canonical.StateAuthorityVersion;
ulong physicsMutationVersion =
canonical.PhysicsStateMutationVersion;
RetailPhysicsStateTransition committedTransition = transition;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(
canonical,
committedTransition),
committedTransition.HiddenTransition
is RetailHiddenTransition.BecameHidden
? RuntimeEntityChange.Hidden
: RuntimeEntityChange.Updated,
() => canonical.StateAuthorityVersion == stateVersion
&& canonical.PhysicsStateMutationVersion
== physicsMutationVersion);
}
public bool TryApplyPosition(
WorldSession.EntityPositionUpdate update,
bool isLocalPlayer,
System.Numerics.Quaternion? forcePositionRotation,
System.Numerics.Vector3? currentLocalVelocity,
Action<RuntimeEntityRecord>? acknowledgeProjection,
out PositionTimestampDisposition disposition,
out WorldSession.EntitySpawn accepted,
out AcceptedPhysicsTimestamps timestamps)
{
EnsureNotDisposed();
if (TryGetPendingInitialResidence(
update.Guid,
out RuntimeEntityRecord pendingCanonical,
out RuntimeInitialCreateResidenceLease pendingLease))
{
if (!RuntimeAuthoritativePositionRouteClassifier
.IsValidCreateWirePosition(update.Position)
|| update.Velocity is { } velocity
&& !IsFinite(velocity)
|| !InitialCreateResidences.CanEnqueue(
pendingCanonical,
pendingLease))
{
disposition = PositionTimestampDisposition.Rejected;
accepted = default;
timestamps = default;
return false;
}
bool deferredKnown = Entities.TryAcceptDeferredPosition(
update,
isLocalPlayer,
out disposition,
out timestamps,
out bool timestampMutation);
accepted = pendingCanonical.Snapshot;
if (!deferredKnown)
return false;
if (isLocalPlayer
&& disposition is not PositionTimestampDisposition.Rejected)
{
Physics.ObserveLocalWorldFrame(
update.Position.LandblockId,
timestamps.TeleportAdvanced);
}
if (disposition is PositionTimestampDisposition.Rejected
&& !timestampMutation)
{
return true;
}
EnqueueDormant(
pendingCanonical,
pendingLease,
RuntimeInitialCreateContinuationKind.Position,
RuntimeAcceptedPositionSource.PositionEvent,
new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.Position,
update.Guid,
Position: update,
PositionSource:
RuntimeAcceptedPositionSource.PositionEvent,
PositionDisposition: disposition,
PreviousTeleportSequence:
timestamps.PreviousTeleport,
AcceptedTimestamps: timestamps,
HasTimestampMutation: timestampMutation));
return true;
}
bool hadCanonical = Entities.TryGetActive(
update.Guid,
out RuntimeEntityRecord beforeCanonical);
uint beforeCell = beforeCanonical?.FullCellId ?? 0u;
bool known = Entities.TryApplyPosition(
update,
isLocalPlayer,
forcePositionRotation,
currentLocalVelocity,
out disposition,
out accepted,
out timestamps);
if (!known
|| !Entities.TryGetSnapshot(
update.Guid,
out WorldSession.EntitySpawn snapshot)
|| !Entities.TryGetActive(
update.Guid,
out RuntimeEntityRecord canonical))
{
return known;
}
if (isLocalPlayer
&& disposition is not PositionTimestampDisposition.Rejected)
{
Physics.ObserveLocalWorldFrame(
update.Position.LandblockId,
timestamps.TeleportAdvanced);
}
bool acceptedPosition =
disposition is not PositionTimestampDisposition.Rejected;
// C4 route 4b-3 (D1): the classifier's remote cell-less predicate
// needs the PRE-merge committed cell — the value this method just
// measured as `beforeCell`, before `RefreshSnapshot`
// below stamps the accepted wire cell onto the canonical record via
// `RefreshDerivedState` -> `SetFullCell`. Reading `canonical.FullCellId`
// AFTER that merge (as the classifier's default builder overload
// does for every other caller) always sees the wire cell, which is
// why the predicate as fed to a remote PositionEvent was dead before
// this fix. `hadCanonical` is what makes this an honest value rather
// than a fabricated 0 — see the field's own doc.
timestamps = timestamps with
{
PreMergeCommittedCellId = hadCanonical ? beforeCell : null,
};
RuntimePlacementCancellationReceipt cancellation = default;
if (acceptedPosition)
{
// A CANCELLATION, not a withdrawal: a newer accepted Position
// supersedes the in-flight placement but the entity stays in the
// world. If that placement was a DeferredCell park, cancelling it
// without rolling the withdrawal back left the entity invisible
// AND intangible with nothing able to wake it - see
// RuntimeSetPositionState.Forget. Every OTHER Forget call in this
// class is a withdrawal transaction and deliberately does not opt
// in.
cancellation = Physics.SetPosition.Forget(
canonical,
restoreCancelledPark: true);
}
Entities.RefreshSnapshot(
canonical,
snapshot,
refreshPosition: acceptedPosition);
if (acceptedPosition
&& ReferenceEquals(canonical, beforeCanonical))
{
Entities.AdvancePositionAuthority(canonical);
Entities.ParentAttachments.EndChildProjection(update.Guid);
}
ulong positionVersion = canonical.PositionAuthorityVersion;
ulong spatialVersion = canonical.SpatialAuthorityVersion;
if (!acceptedPosition)
{
acknowledgeProjection?.Invoke(canonical);
return IsExpectedCanonical(
canonical,
() => canonical.PositionAuthorityVersion == positionVersion
&& canonical.SpatialAuthorityVersion == spatialVersion);
}
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
beforeCell != canonical.FullCellId
? RuntimeEntityChange.Rebucketed
: RuntimeEntityChange.Updated,
() => canonical.PositionAuthorityVersion == positionVersion
&& canonical.SpatialAuthorityVersion == spatialVersion,
cancellation);
}
public bool CommitRebucket(
RuntimeEntityRecord canonical,
uint fullCellId,
uint canonicalLandblockId,
Action<RuntimeEntityRecord>? acknowledgeProjection = null)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(canonical);
if (!Entities.IsCurrent(canonical))
return false;
uint previous = canonical.FullCellId;
Entities.SetFullCell(
canonical,
fullCellId,
canonicalLandblockId);
ulong spatialVersion = canonical.SpatialAuthorityVersion;
if (previous == fullCellId)
{
acknowledgeProjection?.Invoke(canonical);
return IsExpectedCanonical(
canonical,
() => canonical.SpatialAuthorityVersion
== spatialVersion);
}
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Rebucketed,
() => canonical.SpatialAuthorityVersion == spatialVersion);
}
public bool CommitWithdrawal(
RuntimeEntityRecord canonical,
Action<RuntimeEntityRecord>? acknowledgeProjection = null)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(canonical);
if (!Entities.IsCurrent(canonical))
return false;
// C0-4(b): this cancelled the initial-create residence but never the
// ORDINARY placement family (Physics.SetPosition.Forget), unlike
// TryApplyPickup/CommitAcceptedParentCellless/TryAcceptDelete, which
// all cancel both. WithdrawLiveEntityProjectionToCellless routes
// through here, so a live ordinary SetPosition/lost-cell watch could
// dangle across a withdrawal-to-cellless once ordinary placement
// traffic goes live. Fixed symmetrically with the same
// Forget/PreferCancellation pair every sibling withdrawal uses.
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(canonical);
Physics.CollisionReports.LeaveWorld(canonical);
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(canonical);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(initialCancellation, ordinaryCancellation);
Entities.SuspendObjectClock(canonical);
Entities.SetFullCell(canonical, 0u, 0u);
ulong spatialVersion = canonical.SpatialAuthorityVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Withdrawn,
() => canonical.SpatialAuthorityVersion == spatialVersion,
cancellation);
}
public bool CommitChildNoDraw(
RuntimeEntityRecord canonical,
bool noDraw,
Action<RuntimeEntityRecord>? acknowledgeProjection = null)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(canonical);
if (!Entities.IsCurrent(canonical))
return false;
Entities.SetChildNoDraw(canonical, noDraw);
ulong physicsMutationVersion =
canonical.PhysicsStateMutationVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Updated,
() => canonical.PhysicsStateMutationVersion
== physicsMutationVersion);
}
public bool RetireAfterProjectionAcquisitionFailure(
RuntimeEntityRecord canonical)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(canonical);
if (!Entities.RemoveActive(canonical))
return false;
Entities.AdvanceLifetimeMutation(canonical.ServerGuid);
Entities.RetainTeardown(canonical);
PublishEntity(RuntimeEntityChange.Deleted, canonical);
return true;
}
/// <summary>
/// Accepts and retires the exact canonical DeleteObject generation without
/// yet publishing the retained-object removal. This split lets a graphical
/// host tear down the exact retired projection before completing the
/// retained-object mutation while Runtime remains the only freshness,
/// identity, and canonical-lifetime authority.
/// </summary>
public bool TryAcceptDelete(
DeleteObject.Parsed delete,
bool isLocalPlayer,
bool removeRetainedObject,
out RuntimeEntityDeleteAcceptance acceptance)
{
EnsureNotDisposed();
if (!isLocalPlayer)
{
// A child whose complete raw CreateObject is waiting on a missing
// parent has no timestamp gate yet. Cancel its exact/older raw
// generation before the normal known-object delete gate returns.
Entities.ParentAttachments.CancelDeferredChildGeneration(
delete.Guid,
delete.InstanceSequence);
}
if (!Entities.TryDelete(delete, isLocalPlayer))
{
acceptance = null!;
return false;
}
Entities.AdvanceLifetimeMutation(delete.Guid);
Entities.ParentAttachments.DeleteGeneration(
delete.Guid,
delete.InstanceSequence);
RuntimeEntityRecord? retiredCanonical = null;
if (Entities.TryGetActive(
delete.Guid,
out RuntimeEntityRecord active)
&& active.Incarnation == delete.InstanceSequence
&& Entities.RemoveActive(active))
{
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(active);
Physics.CollisionReports.Forget(active);
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(
active,
releasePreparedMover: true);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(
initialCancellation,
ordinaryCancellation);
retiredCanonical = active;
Entities.RetainTeardown(active);
Physics.SetPosition.PublishCancellation(cancellation);
PublishEntity(
removeRetainedObject
? RuntimeEntityChange.Deleted
: RuntimeEntityChange.Withdrawn,
active);
}
acceptance = new RuntimeEntityDeleteAcceptance(
this,
delete,
retiredCanonical,
removeRetainedObject);
return true;
}
/// <summary>
/// Completes a previously accepted delete exactly once. Marking the token
/// complete before synchronous callbacks preserves the current retry rule:
/// a throwing observer does not replay an already-committed removal.
/// </summary>
public void CompleteAcceptedDelete(
RuntimeEntityDeleteAcceptance acceptance)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(acceptance);
if (!ReferenceEquals(acceptance.Owner, this))
{
throw new InvalidOperationException(
"A delete acceptance belongs to a different Runtime lifetime.");
}
if (acceptance.Completed)
{
throw new InvalidOperationException(
"A delete acceptance has already been completed.");
}
acceptance.Completed = true;
if (acceptance.RemoveRetainedObject)
ObjectTableWiring.ApplyEntityDelete(Objects, acceptance.Delete);
}
/// <summary>
/// Applies a delete accepted by the dormant exact-incarnation owner after
/// the active Runtime directory correctly reports no live record.
/// </summary>
public void ApplyAcceptedDormantDelete(DeleteObject.Parsed delete)
{
EnsureNotDisposed();
ObjectTableWiring.ApplyEntityDelete(Objects, delete);
}
/// <summary>
/// Clears retained object state at the Runtime-owned reset stage. App
/// projection teardown remains a later acknowledged stage.
/// </summary>
public void ClearObjects()
{
EnsureNotDisposed();
Objects.Clear();
}
public IReadOnlyList<RuntimeEntityRecord> BeginSessionClear()
{
EnsureNotDisposed();
if (_sessionClearInProgress)
return Array.Empty<RuntimeEntityRecord>();
_sessionClearInProgress = true;
RuntimeEntityRecord[] active = Entities.ActiveRecords.ToArray();
InitialCreateResidences.Clear();
InitialCreateExecution.DiscardAll();
LocalPlayerFirstEntry.DiscardAll();
RemoteFirstEntry.DiscardAll();
Physics.CollisionReports.LeaveWorldBatch(active);
Physics.ResetSessionPhysics();
Entities.BeginSessionClear();
foreach (RuntimeEntityRecord canonical in active)
{
Physics.SetPosition.Forget(canonical);
if (!Entities.RemoveActive(canonical))
continue;
Entities.RetainTeardown(canonical);
PublishEntity(RuntimeEntityChange.Deleted, canonical);
}
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()
{
EnsureNotDisposed();
if (!_sessionClearInProgress
|| !Entities.CompleteSessionClearIfConverged())
{
return false;
}
_sessionClearInProgress = false;
return true;
}
public void Dispose()
{
if (_disposed)
return;
// Disposal is terminal, unlike a session reset. Quiesce borrowers
// before committing internal cleanup so no observer can re-enter a
// lifetime whose owner is being destroyed.
Events.DetachObservers();
List<Exception>? failures = null;
try
{
if (!_sessionClearInProgress)
_ = BeginSessionClear();
try
{
ClearObjects();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
foreach (RuntimeEntityRecord canonical
in Entities.TeardownRecords.ToArray())
{
Exception? failure = RetireCanonicalOnly(canonical);
if (failure is not null)
(failures ??= []).Add(failure);
}
if (!CompleteSessionClearIfConverged())
{
(failures ??= []).Add(new InvalidOperationException(
"Runtime entity/object disposal did not converge every canonical owner."));
}
}
finally
{
_disposed = true;
_generation = null;
_pvpBitfieldSync.Dispose();
Events.Dispose();
Physics.Dispose();
}
if (failures is not null)
{
throw new AggregateException(
"Runtime entity/object disposal failed to converge.",
failures);
}
}
private bool CommitPositionChannelUpdate(
bool applied,
uint guid,
WorldSession.EntitySpawn accepted,
Action<RuntimeEntityRecord>? acknowledgeProjection)
{
if (!applied
|| !Entities.TryGetActive(
guid,
out RuntimeEntityRecord canonical))
{
return applied;
}
Entities.RefreshSnapshot(canonical, accepted);
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(canonical);
Entities.AdvancePositionAuthority(canonical);
Physics.CollisionReports.LeaveWorld(canonical);
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(canonical);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(initialCancellation, ordinaryCancellation);
ulong positionVersion = canonical.PositionAuthorityVersion;
ulong spatialVersion = canonical.SpatialAuthorityVersion;
return AcknowledgeProjectionAndPublish(
canonical,
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Updated,
() => canonical.PositionAuthorityVersion == positionVersion
&& canonical.SpatialAuthorityVersion == spatialVersion,
cancellation);
}
private void PublishEntity(
RuntimeEntityChange change,
RuntimeEntityRecord canonical) =>
Events.PublishEntity(change, canonical);
private bool AcknowledgeProjectionAndPublish(
RuntimeEntityRecord canonical,
Action acknowledgeProjection,
RuntimeEntityChange change,
Func<bool> matchesCommittedMutation,
RuntimePlacementCancellationReceipt cancellation = default)
{
Physics.SetPosition.PublishCancellation(cancellation);
if (!IsExpectedCanonical(canonical, matchesCommittedMutation))
return false;
try
{
acknowledgeProjection();
}
finally
{
if (IsExpectedCanonical(
canonical,
matchesCommittedMutation))
{
PublishEntity(change, canonical);
}
}
return IsExpectedCanonical(
canonical,
matchesCommittedMutation);
}
private bool IsExpectedCanonical(
RuntimeEntityRecord canonical,
Func<bool> matchesCommittedMutation) =>
Entities.IsCurrent(canonical)
&& matchesCommittedMutation();
private bool TryGetPendingInitialResidence(
uint guid,
out RuntimeEntityRecord canonical,
out RuntimeInitialCreateResidenceLease lease)
{
if (Entities.TryGetActive(guid, out canonical)
&& InitialCreateResidences.TryGetTransaction(
canonical,
out lease))
{
return true;
}
canonical = null!;
lease = default;
return false;
}
private void EnqueueDormant(
RuntimeEntityRecord canonical,
in RuntimeInitialCreateResidenceLease prior,
RuntimeInitialCreateContinuationKind kind,
RuntimeAcceptedPositionSource positionSource,
in RuntimeInitialCreateTailAction action)
{
RuntimeInitialCreateResidenceLease retained = InitialCreateResidences
.EnqueueAccepted(
canonical,
prior,
kind,
positionSource,
ImmutableArray.Create(action));
if (!retained.IsValid)
{
throw new InvalidOperationException(
$"Accepted {kind} for 0x{canonical.ServerGuid:X8}/{canonical.Incarnation} could not be retained by its initial-placement FIFO.");
}
}
private static bool IsFinite(System.Numerics.Vector3 value) =>
float.IsFinite(value.X)
&& float.IsFinite(value.Y)
&& float.IsFinite(value.Z);
private static bool HasConsistentCreateIdentityAndParent(
in WorldSession.EntitySpawn incoming)
{
if (incoming.Guid == 0u)
return false;
bool hasTopParentGuid = incoming.ParentGuid is not null;
bool hasTopParentLocation = incoming.ParentLocation is not null;
if (hasTopParentGuid != hasTopParentLocation)
return false;
if (incoming.Physics is not { } physics)
{
// These flattened values are parser projections of PhysicsDesc.
// Without that source block, accepting any of them would create
// contradictory admission authority before the raw create is
// either queued for a parent or assigned a residence lease.
return incoming.Position is null
&& incoming.SetupTableId is null
&& incoming.MotionState is null
&& incoming.MotionTableId is null
&& incoming.PhysicsState is null
&& incoming.ObjScale is null
&& incoming.Friction is null
&& incoming.Elasticity is null
&& incoming.InstanceSequence == 0
&& incoming.MovementSequence == 0
&& incoming.ServerControlSequence == 0
&& incoming.PositionSequence == 0
&& !hasTopParentGuid
&& incoming.PlacementId is null;
}
if (physics.Timestamps.Instance != incoming.InstanceSequence
|| physics.Timestamps.Position != incoming.PositionSequence
|| physics.Timestamps.Movement != incoming.MovementSequence
|| physics.Timestamps.ServerControlledMove
!= incoming.ServerControlSequence
|| physics.Position != incoming.Position)
return false;
PhysicsAttachment? flattenedParent = incoming.ParentGuid is { } parentGuid
&& incoming.ParentLocation is { } parentLocation
? new PhysicsAttachment(parentGuid, parentLocation)
: null;
if (flattenedParent != physics.Parent
|| incoming.PlacementId != physics.AnimationFrame)
{
return false;
}
return true;
}
private static bool IsStructurallyValidDeferredCreate(
in WorldSession.EntitySpawn incoming)
{
if (incoming.Guid == 0u)
return false;
if (incoming.Physics is not { } physics)
return true;
if (physics.Parent is null
&& physics.Position is { LandblockId: not 0u } position
&& !RuntimeAuthoritativePositionRouteClassifier
.IsValidCreateWirePosition(position))
{
return false;
}
if (physics.Velocity is { } velocity && !IsFinite(velocity)
|| physics.Acceleration is { } acceleration
&& !IsFinite(acceleration)
|| physics.AngularVelocity is { } angularVelocity
&& !IsFinite(angularVelocity)
|| physics.Scale is { } scale && !float.IsFinite(scale)
|| physics.Friction is { } friction && !float.IsFinite(friction)
|| physics.Elasticity is { } elasticity
&& !float.IsFinite(elasticity)
|| physics.Translucency is { } translucency
&& !float.IsFinite(translucency))
{
return false;
}
return true;
}
private bool AdmitSameGenerationCreate(
RuntimeEntityRecord canonical,
in RuntimeInitialCreateResidenceLease prior,
in WorldSession.EntitySpawn incoming,
in InboundCreateResult admitted,
bool isLocalPlayer)
{
if (!ReferenceEquals(
canonical,
Entities.TryGetActive(
incoming.Guid,
out RuntimeEntityRecord current)
? current
: null)
|| canonical.Incarnation != incoming.InstanceSequence
|| admitted.Disposition
is not CreateObjectTimestampDisposition.ExistingGeneration
|| !InitialCreateResidences.CanEnqueue(canonical, prior))
{
return false;
}
var actions = ImmutableArray.CreateBuilder<
RuntimeInitialCreateTailAction>();
RuntimeAcceptedPositionSource positionSource =
RuntimeAcceptedPositionSource.Unknown;
if (admitted.SameGenerationEvents is { } events)
{
actions.Add(new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind
.PreTailDescriptionAdaptation,
incoming.Guid,
Description: events.Description));
if (Entities.TryAcceptDeferredObjDesc(
events.Appearance,
out _))
{
actions.Add(new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.ObjDesc,
incoming.Guid,
ObjDesc: events.Appearance));
}
if (events.Parent is { } parent)
{
if (Entities.TryAcceptDeferredCreateParent(
parent,
out _))
{
actions.Add(new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.CreateParent,
incoming.Guid,
CreateParent: parent));
}
}
else if (events.Position is { } position)
{
if (!RuntimeAuthoritativePositionRouteClassifier
.IsValidCreateWirePosition(position.Position)
|| position.Velocity is { } velocity
&& !IsFinite(velocity))
{
return false;
}
if (!Entities.TryAcceptDeferredPosition(
position,
isLocalPlayer,
out PositionTimestampDisposition disposition,
out AcceptedPhysicsTimestamps timestamps,
out bool timestampMutation))
{
return false;
}
if (disposition is not PositionTimestampDisposition.Rejected
|| timestampMutation)
{
positionSource = RuntimeAcceptedPositionSource
.SameIncarnationCreate;
actions.Add(new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.Position,
incoming.Guid,
Position: position,
PositionSource: positionSource,
PositionDisposition: disposition,
PreviousTeleportSequence:
timestamps.PreviousTeleport,
AcceptedTimestamps: timestamps,
HasTimestampMutation: timestampMutation));
}
}
else if (events.Pickup is { } pickup
&& Entities.TryAcceptDeferredPickup(pickup, out _))
{
actions.Add(new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.Pickup,
incoming.Guid,
Pickup: pickup));
}
if (events.Movement is { } movement)
{
bool payloadApplied = Entities.TryAcceptDeferredMotion(
movement,
out AcceptedPhysicsTimestamps timestamps,
out bool timestampMutation);
if (payloadApplied || timestampMutation)
{
actions.Add(new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.Movement,
incoming.Guid,
Movement: movement,
AcceptedTimestamps: timestamps,
AppliesMovementPayload: payloadApplied,
RetainMovementPayload: true,
HasTimestampMutation: timestampMutation));
}
}
if (Entities.TryAcceptDeferredState(events.State, out _))
{
actions.Add(new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.State,
incoming.Guid,
State: events.State));
}
if (Entities.TryAcceptDeferredVector(events.Vector, out _))
{
actions.Add(new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.Vector,
incoming.Guid,
Vector: events.Vector));
}
}
actions.Add(new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.WeenieDescription,
incoming.Guid,
WeenieDescription: incoming));
actions.Add(new RuntimeInitialCreateTailAction(
RuntimeInitialCreateTailActionKind.ResidentCellCleanup,
incoming.Guid));
RuntimeInitialCreateResidenceLease retained = InitialCreateResidences
.EnqueueAccepted(
canonical,
prior,
RuntimeInitialCreateContinuationKind.SameIncarnationCreate,
positionSource,
actions.ToImmutable());
return retained.IsValid;
}
private void EnsureNotDisposed() =>
ObjectDisposedException.ThrowIf(_disposed, this);
internal bool TryGetInitialCreateResidence(
RuntimeEntityRecord canonical,
out RuntimeInitialCreateResidenceLease lease)
{
EnsureNotDisposed();
return InitialCreateResidences.TryGetCurrent(canonical, out lease);
}
/// <summary>
/// C3c-R1 review F7: host seam for a bounded-collision-neighborhood
/// host to convert a remote/projectile Create's active residence to the
/// celless completion route when its destination landblock will never
/// be collision-published (a headless far remote). See
/// <see cref="RuntimeInitialCreateResidenceState.TryConvertToCellessRoute"/>.
/// </summary>
public bool TryConvertInitialResidenceToCellessRoute(
RuntimeEntityRecord canonical)
{
EnsureNotDisposed();
return InitialCreateResidences.TryConvertToCellessRoute(canonical);
}
internal RuntimeInitialCreateResidenceCompletionStatus
CompleteInitialCreateResidence(
RuntimeEntityRecord canonical,
in RuntimeInitialCreateResidenceToken token,
out RuntimeInitialCreateResidenceReceipt receipt)
{
EnsureNotDisposed();
return InitialCreateResidences.Complete(
canonical,
token,
out receipt);
}
internal bool AcknowledgeInitialCreateResidenceAdoption(
RuntimeEntityRecord canonical,
in RuntimeInitialCreateResidenceAdoptionToken token)
{
EnsureNotDisposed();
return InitialCreateResidences.AcknowledgeAdoption(
canonical,
token);
}
private RuntimePlacementCancellationReceipt ForgetInitialCreateResidence(
RuntimeEntityRecord canonical)
{
bool forgotten = InitialCreateResidences.Forget(
canonical,
out _,
out RuntimePlacementCancellationReceipt cancellation);
// Round 3 B3: InitialCreateResidences.Forget's own retirement
// notification already routes to InitialCreateExecution.DiscardProgress
// for a successful Forget. This explicit call is defensive-in-depth
// for the (record never held a residence) case where Forget returns
// false without ever reaching the notification - DiscardProgress is
// idempotent, so a redundant call after a successful Forget is a
// guaranteed no-op, never a double-discard.
if (canonical.Key is { } key)
InitialCreateExecution.DiscardProgress(key);
return forgotten ? cancellation : default;
}
private static RuntimePlacementCancellationReceipt PreferCancellation(
in RuntimePlacementCancellationReceipt initial,
in RuntimePlacementCancellationReceipt ordinary) =>
initial.IsValid ? initial : ordinary;
private bool InitializeAcceptedCreateResidence(
RuntimeEntityRecord canonical,
in InboundCreateResult accepted,
bool beginInitialResidence,
bool isLocalPlayer)
{
if (!beginInitialResidence)
return true;
// The explicit cutover path separates accepted wire authority from
// committed residence before any observer can hydrate it. Failure to
// own the exact route is fail-closed; the caller removes the canonical
// record before publishing Registered/Updated.
if (canonical.PositionAuthorityVersion == 0UL)
Entities.AdvancePositionAuthority(canonical);
if (canonical.FullCellId != 0u)
Entities.SetFullCell(canonical, 0u, 0u);
RuntimeInitialCreateResidenceLease lease =
InitialCreateResidences.Begin(
canonical,
accepted,
isLocalPlayer);
if (!lease.IsValid)
return false;
// C3c: host drive notification. Fires for EVERY fresh residence
// begin through this single choke point — wire-dispatch Creates AND
// the executor's deferred-child replays (which register through this
// class's own bound delegate, never through a host runtime). The
// subscriber must only RECORD the key for a later drive pump — this
// fires mid-registration, before Registered publishes, and a
// synchronous Advance here would interleave with the enclosing
// transaction (and, for a replayed child, with the parent's own
// in-flight Execute).
_initialResidenceBegan?.Invoke(canonical);
return true;
}
private Exception FailInitialResidenceRegistration(
RuntimeEntityRecord canonical,
bool publishDeleted)
{
if (!Entities.RemoveActive(canonical))
{
return new InvalidOperationException(
$"Initial residence for 0x{canonical.ServerGuid:X8} failed after its canonical incarnation was superseded.");
}
Exception? failure = null;
if (publishDeleted)
{
try
{
PublishEntity(RuntimeEntityChange.Deleted, canonical);
}
catch (Exception error)
{
failure = error;
}
}
failure = Combine(failure, RetireCanonicalOnly(canonical));
var cause = new InvalidOperationException(
$"Initial residence for 0x{canonical.ServerGuid:X8} could not acquire its exact Runtime placement lease.");
return failure is null
? cause
: new AggregateException(cause, failure);
}
private static Exception? Combine(
Exception? first,
Exception? second) =>
first is null
? second
: second is null
? first
: new AggregateException(first, second);
private static InboundCreateResult SupersededCreateResult() => new(
CreateObjectTimestampDisposition.StaleGeneration,
default,
null,
default);
private bool IsCurrentOperation(
uint guid,
RuntimeEntityRecord canonical,
ulong sessionVersion,
ulong operationVersion) =>
Entities.SessionLifetimeVersion == sessionVersion
&& Entities.CurrentLifetimeMutation(guid) == operationVersion
&& Entities.IsCurrent(canonical);
private RuntimeEntityRegistrationResult SupersededRegistration(
uint guid,
bool replacedExistingGeneration) =>
new(
SupersededCreateResult(),
Entities.TryGetActive(guid, out RuntimeEntityRecord current)
? current
: null,
LogicalRegistrationCreated: false,
ReplacedExistingGeneration: replacedExistingGeneration);
}