diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md
index 0e5decb4..35d2cf94 100644
--- a/docs/architecture/retail-divergence-register.md
+++ b/docs/architecture/retail-divergence-register.md
@@ -62,7 +62,7 @@ accepted-divergence entries (#96, #49, #50).
---
-## 2. Adaptation (AD) — 44 active rows
+## 2. Adaptation (AD) — 46 active rows (AD-59/AD-60 filed 2026-08-02, continuation-executor slice)
Recent retirements: AD-3/AD-4 retired 2026-07-31 by exact active/per-candidate
visible-cell availability, full-catalog containment-root validation, and the
@@ -144,10 +144,12 @@ readiness/requeue adaptation. See
| AD-45 | App teardown can overlap a newer `INSTANCE_TS` record after retiring the old active identity. `TargetManager` therefore retains the exact target host and each `TargettedVoyeurInfo` retains the exact watcher host; unsubscribe, Sticky live-target reads, inbound sender validation, and ExitWorld delivery compare/use those pointer-like tokens rather than resolving a reused GUID. Retail stores only GUIDs because `DeleteObject` finishes `exit_world`/`leave_world` while the retiring `CPhysicsObj` remains the sole object-table entry. | `src/AcDream.Core/Physics/Motion/TargetManager.cs`; `StickyManager.cs`; `TargettedVoyeurInfo.cs`; `IPhysicsObjHost` exact relationship seams | This preserves retail's effective object-pointer identity while allowing App resource teardown to fail and retry without blocking an accepted newer server generation. Ordinary `GetObjectA` remains active-record-only, so tombstones cannot accept new relationships. | If any target/voyeur path bypasses the exact token, retrying an old teardown can remove or notify a newer same-GUID relationship, or Sticky can steer toward the replacement; retained tokens also keep the small manager graph alive until teardown converges. | `CPhysicsObj::exit_world @ 0x00514E60`; `CObjectMaint::DeleteObject(CPhysicsObj*) @ 0x00508460`; `ACCObjectMaint::DeleteObject(uint) @ 0x005576F0`; `TargetManager::SetTarget @ 0x0051AC30`; `ClearTarget @ 0x0051A7E0`; `AddVoyeur @ 0x0051A830`; `RemoveVoyeur @ 0x0051AD90` |
| AD-57 | **Re-argued from TS-24 at Campaign P P7 (2026-07-30).** Outbound `RawMotionState.Actions` is always empty at runtime. The packer emits `num_actions` + per-action pairs (L.2b, `RawMotionState::Pack` 0x0051ed10) and the R3-W1 action FIFO capability exists (`AddAction`/`RemoveAction`/`ApplyMotion`/`RemoveMotion`); no production input path ENQUEUES autonomous actions yet because the emote/autonomous-motion feature surface is unimplemented. An empty list is byte-identical to retail's own no-pending-actions state, so this is a feature gap, not a divergence of existing behavior. | packer `src/AcDream.Core.Net/Messages/RawMotionStatePacker.cs`; FIFO `src/AcDream.Core/Physics/RawMotionState.cs` | Every currently-shipped movement packet matches retail byte-shape; the gap only manifests when emote-class autonomous actions are implemented. | When emotes land, forgetting to route them through the FIFO would silently drop them from the wire. | `RawMotionState::Pack` 0x0051ed10 |
| AD-58 | **Re-argued from TS-40 at Campaign P P7 (2026-07-30).** Retail's `physics_obj->cell` null test ("placed in the world") is proxied by the explicit `PhysicsBody.InWorld` flag — set by `SnapToCell` and `RemoteMotion` construction, consumed by `CMotionInterp`'s detached-object link-strip guards. Equivalence: every acdream body that would have a null retail cell pointer has `InWorld == false` (bodies exist only for world entities; the flag flips exactly at placement/withdrawal), so the guards fire on the same population. A structural adaptation of retail's pointer-as-state idiom to acdream's explicit-flag idiom, not scheduled debt. | `src/AcDream.Core/Physics/PhysicsBody.cs` (`InWorld`); `src/AcDream.Core/Physics/MotionInterpreter.cs` (3 guard sites) | If a future path creates a body before world placement without clearing `InWorld`, the link-strip guards misfire where retail's null-cell test would not. | `CMotionInterp` link-strip guards raw @305xxx |
+| AD-59 | **Filed 2026-08-02 (physics campaign, continuation-executor slice).** The `SameIncarnationCreate` envelope buffers one publish per committed stage and flushes them ALL, in stage order, only after the LAST stage commits (constant-true per-field predicate, `IsCurrent`-checked at flush - the per-field closure variant was invalidated by WeenieDescription's six-field `AdvanceCreateAuthority`). A subscriber sees N back-to-back events with no interleaved observation point, each carrying the FINAL merged post-envelope record state, not per-stage state. | `src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs` (`ApplyEnvelope` buffered-publish tail; `Publish`/`PublishNow`) | Retail's own tail is one synchronous critical section, and retail emits ONE notice per Create (`ECM_Physics::SendNotice_CreateObject`, fired whenever a weenie exists, independent of the physics-registration outcome) - never N per-internal-step notices. The buffered flush is closer to retail's one-signal model than per-step publication would be, though not a literal 1:1 match. | A subscriber diffing consecutive `Updated` events from the SAME envelope to isolate one stage's delta gets every stage's cumulative state on each event - silently wrong incremental-diff logic, not a crash. | `SmartBox::HandleCreateObject` 0x00454C80 same-incarnation tail (one synchronous critical section); `ACCObjectMaint::CreateObject` 0x00558870 step 11 (`ECM_Physics::SendNotice_CreateObject`) |
+| AD-60 | **Filed 2026-08-02 (physics campaign, continuation-executor slice).** Executor Position-continuation merges never directly commit residency: `ApplyPositionAction` refreshes `canonical.Snapshot.Position` with the retained wire pose but withholds the derived `FullCellId` (`RefreshSnapshot(..., refreshPosition: false)`); only a Runtime `SetPosition` commit (the continuation's own classified placement) or a later simulation full-cell commit may change residency. The LEGACY immediate-apply path's `RefreshSnapshot(canonical, snapshot, refreshPosition: acceptedPosition)` (`RuntimeEntityObjectLifetime.cs:1338`) still derives `FullCellId` from bare wire acceptance - that coarser rule is part of the AP-1 divergence this campaign is removing, not something this row blesses. | `src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs` (`ApplyPositionAction`, the CANONICAL CELL SEMANTICS comment) | Matches retail exactly: `HandleReceivedPosition` never writes a resident cell - `enter_world`/`MoveOrTeleport`'s placement commit and `SetPosition` do; also matches the classifier's documented cellless rule. | If a future change passes `refreshPosition: true` here, a wire Position would make a cellless canonical body resident without any placement/collision commit - the classic AP-1-shaped bug this campaign exists to close. | `SmartBox::HandleReceivedPosition` 0x00453FD0; `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition` comment |
---
-## 3. Documented approximation (AP) — 87 active rows (AP-1 narrowed 2026-07-31 by placement/streaming Slice 4A — the pure canonical retail `SetPosition` transaction exists, but production routes and lost-cell lifetime remain on the legacy resolver until Slice 4B; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55)
+## 3. Documented approximation (AP) — 90 active rows (AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-1 narrowed 2026-07-31 by placement/streaming Slice 4A — the pure canonical retail `SetPosition` transaction exists, but production routes and lost-cell lifetime remain on the legacy resolver until Slice 4B; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55)
Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84
collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered
@@ -264,8 +266,11 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| AP-126 | One monotonic Stopwatch-backed clock (`TransportClock`) drives every transport gate (2.0 s ack, 0.6 s NAK, 0.333 s handshake retry, 0.5 s interval, 5 s assembler sweep); retail splits gates between `Timer::cur_time` (server-adjusted) and `Timer::local_time`. | `src/AcDream.Core.Net/Transport/TransportClock.cs` | The cur/local split only matters for gates that must track server clock adjustments; none of the ported gates semantically depend on server time — they are local cadences. A single injectable source also gives the virtual-clock test seam every conformance suite relies on. | A future port of a genuinely server-clock-relative gate could silently use the wrong clock if it reuses TransportClock without checking this row. | `SharedNet::EnqueuePak @ 0x00543B10` (cur_time); `ClientNet::ProcessConnection @ 0x00545450` (local_time for the 140 s check) |
| ~~AP-127~~ | **RETIRED 2026-07-31 (#268).** `PlayerSkillMath` now owns retail `CACQualities::InqSkill` ordering for both panel values and Runtime run/jump prediction: intrinsic + positive 0x16D all-skills + the exact +10 category switch, then `EnchantSkill`, then 0x146 Jack of All Trades +5 and specialized-only `2 × 0x158`. Live player PropertyInt changes refresh the immutable Runtime augmentation snapshot. The separately described current-stamina local-copy nuance was re-audited: the query reads current stamina, but ordinary max-vital buffs target the max-secondary key and do not create stamina when current is zero; no independently observable residual remains. | `src/AcDream.Core/Player/PlayerSkillMath.cs`; `src/AcDream.Core/Player/LocalPlayerState.cs`; `src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs`; `src/AcDream.Runtime/Session/LiveSessionEventRouter.cs` | — | — | `CACQualities::InqSkill @ 0x00592660`; `CACQualities::InqRunRate @ 0x00592800`; `CEnchantmentRegistry::EnchantSkill @ 0x005947B0` |
| AP-128 | **PK-timer jump-cost clock basis unconfirmed** (filed at the P3 Opus review, 2026-07-30): `PlayerWeenie.JumpStaminaCost` evaluates retail's 20-second PK-recency window (`LastPkAttackTimestamp` PropertyFloat 0x91 + 20.0 >= now) against `Environment.TickCount64` process-uptime seconds. The magnitude argument is sound (a 32-bit float cannot hold a Unix epoch with sub-second precision — a conformance test caught the ±128 s swallow), but the wire timestamp's own basis is the SERVER's, so a cross-base compare is latent. INERT today: ACE models neither property, so `_lastPkAttackTimestamp` is never pushed and the branch never fires. | `src/AcDream.Core/Physics/PlayerWeenie.cs` (`JumpStaminaCost` remarks) | Branch unreachable against every ACE-family server; non-PK cost is bit-identical to pre-P3. The basis question is cdb-answerable (`Timer::cur_time` epoch) if a PK server is ever targeted. | Against a hypothetical server that sends PropertyFloat 0x91, the PK cost bump fires arbitrarily (always/never) instead of on the 20-second window. | `CACQualities::JumpStaminaCost 0x00591b90` pc 412934-412968; `Timer::cur_time`; stat-coupled pseudocode doc §12b |
+| AP-130 | **Filed 2026-08-02 (physics campaign, continuation-executor slice).** The classifier's `HasAnimations` input is the static proxy `(Snapshot.MotionTableId ?? Snapshot.Physics?.MotionTableId) != 0` - "does the Create carry a nonzero motion table" - uniformly for every position source. Retail's `HasAnims` bit is live animation-QUEUE non-emptiness (`CSequence::has_anims` = `anim_list.head_ != 0`), which can differ from mere table assignment. The only confirmed retail `HasAnims` call site on this path is inside `HandleReceivedPosition` itself. | `src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs` (`ApplyPositionAction`, `hasAnimations` local) | Best static proxy available without wiring a live animation-queue read into presentation-independent Position classification; deterministic and testable; gates only `ApplyPlacementFrameBeforeRouting` (placement-FRAME install), never pose or cell placement. | An entity with an assigned motion table but an empty animation queue (or vice versa) gets the wrong placement-frame decision - a one-frame animation-blend glitch on a Position-driven correction where retail would have done the opposite. | `SmartBox::HandleReceivedPosition` 0x00453FD0 (the `HasAnims` gate, pseudo-C ~92992); `CPhysicsObj::HasAnims` 0x0050F770 -> `CSequence::has_anims` 0x00524BD0 |
+| AP-131 | **Filed 2026-08-02 (physics campaign, continuation-executor slice).** The legacy Position merge (`TryApplyPosition`, today's ONLY production Position wire caller) passes `installPlacementFrame: true, clearParent: true` to the shared `ApplyAcceptedPosition` body - byte-identical to its pre-refactor unconditional behavior. Retail gates `SetPlacementFrame` on `!HasAnims` and skips `unset_parent`/`SetPlacementFrame` entirely on the FORCE_POSITION early return (Gate A); the continuation executor's caller threads the classified route's real flags and is retail-exact. | `src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs` (`TryApplyPosition` call site) | Exact pre-existing production behavior, deliberately unchanged by the executor slice; the retail-gated behavior exists in the same shared body and is exercised by the executor's tests. The legacy caller is deleted at the production cutover, retiring this row by construction. | Until cutover, an animated entity's ordinary Position update installs a placement frame retail would skip (animation snap/reset), and a ForcePosition on a parented entity unparents where retail's Gate A never reaches `unset_parent`. | `SmartBox::HandleReceivedPosition` 0x00453FD0 (the `!HasAnims` `SetPlacementFrame` gate ~92992; the FORCE_POSITION early return ~92932 before `unset_parent` ~92990) |
+| AP-132 | **Filed 2026-08-02 (physics campaign, continuation-executor slice).** acdream gates queued parent relations on parent INCARNATION where retail's queue-by-GUID replay is pointer-only. Retail queues a missing-parent relation blob under the PARENT's GUID (`QueueBlobForObject` ~92326; GUID-keyed `CObjectMaint` placeholder bucket ~271082-271088) and replays it on GUID (re)creation with only an addressability check (~92312) - no PARENT INSTANCE_TS comparison anywhere on that path (retail's only instance check there is on the CHILD, ~92316-92317). acdream additionally compares the relation's `ParentInstanceSequence` at admission (pre-existing `TryApplyParent`/`Resolve` rules) and at executor replay (`ApplyReplayedParentRelation`): live-parent-newer discards, relation-newer stays queued for an exact match. The replay's child-missing arm also drops where retail would re-queue under the child's GUID; child-scoped bucket filtering (`RemoveObject`/`RemoveChild`) proactively covers the same ledger tradeoff. | `src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs` (`ApplyReplayedParentRelation`); `RuntimeEntityObjectLifetime.cs` (`TryApplyParent` admission gate); `ParentAttachmentState.cs` (`Resolve` staleness rules) | The wire event names a SPECIFIC parent incarnation (`ParentEvent.Parsed.ParentInstanceSequence`) - the gate honors data the server explicitly sent. acdream's own established admission-time rules (`ParentAttachmentState.Resolve`, predating this slice) already fixed incarnation-gating as the project's parent-staleness posture; the replay path only extends that SAME posture for consistency. | Server GUID reuse between admission and replay: retail would attach the old queued relation to whatever NEW object now holds the GUID (retail's own recycling quirk); acdream discards it (parent newer) or leaves it queued (parent older) - silent loss of a relation retail would have applied, tied to server GUID-recycling cadence, not ordinary play. | Standalone parent handler 0x004535D0 (~92310-92326); `CObjectMaint::QueueBlobForObject` 0x005092D0 (~271082-271088); child instance check ~92316-92317 |
-## 4. Temporary stopgap (TS) — 34 active rows (TS-4 and TS-8 retired 2026-07-31; Campaign P's goal-enumerated physics stopgaps are now zero. TS-4's graph/flat Path-6 branches match retail's foot SetCollide/Adjusted and head CollisionNormal/Collided split with no BSP-layer sliding-normal write; TS-8's live 0x02C2 carries its complete StatMod through the canonical enchantment record and updates effective stats immediately. Campaign P P7 2026-07-30: TS-25 retired — outbound stance has shipped via RawState.CurrentStyle since #219; TS-24 re-argued to AD-57; TS-40 re-argued to AD-58; TS-35 retired at P5; earlier same campaign: TS-1/TS-5/TS-23/TS-46 retired by ports; TS-23 retired 2026-07-30 at Campaign P Slice P3 — every mover-flags call site (local player world-entry ×2, remote DR sweep ×2, remote teleport, ordinary movers) now ORs in the mover's real PK/PKLite/Impenetrable `ObjectInfoState` bits via the new `ClientObjectTable`-backed `EntityCollisionFlagsExt.ResolveMoverPvpState`, and `PlayerWeenie.JumpStaminaCost`'s `pk` parameter reads the real `PlayerKillerStatus`/`LastPkAttackTimestamp` pair against a 20-second window instead of a hardcoded `false`; the non-PK invariant (every ACE default-created character) is bit-identical to the pre-P3 value since `ResolveMoverPvpState` and the PK-timer predicate both resolve to a no-op for `PublicWeenieBitfield` absent/0; TS-46 retired 2026-07-30 at Campaign P Slice P3 — the Setup's verbatim ≤2-sphere list (`CPhysicsObj::transition` 0x00512dc0 → `SPHEREPATH::init_sphere` 0x0050c670) now seeds the sweep for the local player, remote dead-reckoning, and ordinary movers alike, replacing the two-scalar (radius, height) capsule reconstruction; remote/ordinary step-up/step-down are now Setup-derived (`CPartArray::GetStepUpHeight`/`GetStepDownHeight`, 0x005180d0/0x005180f0, ×ObjScale) instead of a hardcoded 0.4 m, closing both residuals the row named; TS-5 retired 2026-07-30 at Campaign P Slice P1 — real burden-gated CanJump + real JumpStaminaCost, both decomp-verbatim; TS-1 retired 2026-07-30 at Campaign P Slice P2 — the row was stale; the EdgeSlide → PrecipiceSlide/CliffSlide chain is already a real, tested port; TS-57..TS-61 filed 2026-07-29 during Campaign N — no outbound RejectRetransmit; TS-27 narrowed same slice to the inbound direction) + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains)
+## 4. Temporary stopgap (TS) — 36 active rows (TS-62/TS-63 filed 2026-08-02, continuation-executor slice; TS-4 and TS-8 retired 2026-07-31; Campaign P's goal-enumerated physics stopgaps are now zero. TS-4's graph/flat Path-6 branches match retail's foot SetCollide/Adjusted and head CollisionNormal/Collided split with no BSP-layer sliding-normal write; TS-8's live 0x02C2 carries its complete StatMod through the canonical enchantment record and updates effective stats immediately. Campaign P P7 2026-07-30: TS-25 retired — outbound stance has shipped via RawState.CurrentStyle since #219; TS-24 re-argued to AD-57; TS-40 re-argued to AD-58; TS-35 retired at P5; earlier same campaign: TS-1/TS-5/TS-23/TS-46 retired by ports; TS-23 retired 2026-07-30 at Campaign P Slice P3 — every mover-flags call site (local player world-entry ×2, remote DR sweep ×2, remote teleport, ordinary movers) now ORs in the mover's real PK/PKLite/Impenetrable `ObjectInfoState` bits via the new `ClientObjectTable`-backed `EntityCollisionFlagsExt.ResolveMoverPvpState`, and `PlayerWeenie.JumpStaminaCost`'s `pk` parameter reads the real `PlayerKillerStatus`/`LastPkAttackTimestamp` pair against a 20-second window instead of a hardcoded `false`; the non-PK invariant (every ACE default-created character) is bit-identical to the pre-P3 value since `ResolveMoverPvpState` and the PK-timer predicate both resolve to a no-op for `PublicWeenieBitfield` absent/0; TS-46 retired 2026-07-30 at Campaign P Slice P3 — the Setup's verbatim ≤2-sphere list (`CPhysicsObj::transition` 0x00512dc0 → `SPHEREPATH::init_sphere` 0x0050c670) now seeds the sweep for the local player, remote dead-reckoning, and ordinary movers alike, replacing the two-scalar (radius, height) capsule reconstruction; remote/ordinary step-up/step-down are now Setup-derived (`CPartArray::GetStepUpHeight`/`GetStepDownHeight`, 0x005180d0/0x005180f0, ×ObjScale) instead of a hardcoded 0.4 m, closing both residuals the row named; TS-5 retired 2026-07-30 at Campaign P Slice P1 — real burden-gated CanJump + real JumpStaminaCost, both decomp-verbatim; TS-1 retired 2026-07-30 at Campaign P Slice P2 — the row was stale; the EdgeSlide → PrecipiceSlide/CliffSlide chain is already a real, tested port; TS-57..TS-61 filed 2026-07-29 during Campaign N — no outbound RejectRetransmit; TS-27 narrowed same slice to the inbound direction) + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains)
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
@@ -309,6 +314,8 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| TS-60 | No 140 s dead-link declaration or referral auto-reconnect in the transport; a silent server is only visible through `LinkStatusSnapshot.SecondsSinceLastPacket` (presentational). | `src/AcDream.Core.Net/Transport/ReliableTransport.cs`; `src/AcDream.Core.Net/WorldSession.cs` (`BuildLinkStatus`) | The input (seconds since last inbound) is already exposed; session lifecycle/reconnect is Runtime's ownership domain and deserves its own campaign rather than a transport-embedded side effect. Every ACE transport death is silence, so nothing server-side depends on the client reacting at 140 s. | A dead link idles until the user acts; no automatic recall/referral reconnect where retail would attempt one. | `ClientNet::ProcessConnection @ 0x00545450` tail (the two 140.0 literals) |
| TS-61 | A UDP send failure burns the reliable sequence and its ISAAC word (the encode commits before `_net.Send`); retail keeps the sealed packet at the queue head and retries with the same key. | `src/AcDream.Core.Net/Transport/OutboundFlowQueue.cs` (`SendGameMessage`) | A connectionless-socket `SendTo` failure is effectively unreachable in practice (no route/ICMP errors surface on later receives, not sends, on Windows UDP); recovering it faithfully needs a full outbound packet queue. The N1 review accepted the exposure explicitly. | One `SocketException` on send would desync the outbound cipher permanently (session death; observable as `[net-out-EX]` followed by silence). | `FlowQueue::TransmitNewPackets @ 0x00547C2C` (retry-from-head) |
| TS-56 | Chase-camera mouse input retains acdream's invented post-filter yaw/pitch scalars (`0.004`/`0.003` radians per count), and held-key pitch/zoom retain their non-retail integration shapes. Retail mouse look passes `FilterMouseInput(delta) × configured sensitivity × 1/15` as the replacement scale to `CameraSet::Rotate`, which then applies the shared 8° angle; retail held pitch uses the same angle and zoom scales the viewer offset multiplicatively. | `src/AcDream.App/Input/CameraPointerInputController.cs`; `src/AcDream.App/Input/MouseLookController.cs`; `src/AcDream.App/Rendering/CameraFrameController.cs` | Slice 8 is behavior-preserving ownership work. The named-retail audit proves the mismatch but has not yet extracted the configured mouse-sensitivity default or the exact caller flags needed for a complete feel port; changing only one scalar here would create a mixed input model. | RMB/MMB orbit, held pitch, and zoom can feel slower, faster, or differently accelerated than retail even though callback ordering and filtering are correct. | `CameraSet::Rotate @ 0x00458310`; `CameraSet::MouseLookHandler` call at `0x00458EF9`; `CameraSet::Raise @ 0x00457B00`; `CameraSet::Closer @ 0x004586D0`; `docs/research/2026-06-11-holistic-map/wf2-camera-viewer.md` |
+| TS-62 | **Filed 2026-08-02 (physics campaign, continuation-executor slice).** NO Position route in the dormant executor runs a live `ConstrainTo` binding - including the `SetPosition`/`SetPositionSimple` routes. `RuntimeAuthoritativePositionRoute.ConstrainPhase` (None/Before/After) is classified for EVERY accepted route and recorded into the execution trace, but the constrain-before-vs-after distinction exists purely as classified metadata pending a live binding at the production cutover. | `src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs` (`ApplyPositionAction`/`BuildPositionTrace`); `RuntimeAuthoritativePositionRouteClassifier.cs` (`ConstrainPhase`) | Host-cutover work with no Runtime-side owner to bind to yet; the canonical snapshot's Position IS refreshed on every accepted route, so the fact is retained - only the live constrain/smoothing behavior is deferred. The trace carries the exact phase a host must bind. | Until a host wires it, ANY Position continuation applies its raw pose with no constrain-distance clamp or smoothing - a visible pop instead of retail's constrained correction, on exactly the entities created while an authored placement was in flight. | `SmartBox::HandleReceivedPosition` 0x00453FD0, the three `ConstrainTo` sites (~93007 remote-after, ~93024 teleport-after, ~93041 local-ordinary-before) |
+| TS-63 | **Filed 2026-08-02 (physics campaign, continuation-executor slice).** `ApplyResidentCellCleanup`'s three branches: (1) claimed-cell + celless + NOT under lost-cell/deferred ownership - retail's genuine `AddObjectToBeDestroyed` case - has no safe Runtime destruction owner yet, so the executor performs a typed ABANDONMENT (`RejectedAuthority`) instead of destroying; (2) claimed + celless + deferred returns `DeferredUnderLostCellOwnership` - retail's destruction bookkeeping for this exact entity is already owned by the lost-cell/deferred `SetPosition` lifetime (a statement, not a parallel mechanism); (3) claimedCell==0 returns `CelllessNoWeenieMarkUnreachable` and is NOT a divergence - every admitted envelope structurally carries a WeenieDescription (`HasValidShape`), so retail's no-weenie destruction alternative is unreachable through this construction. | `src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs` (`ApplyResidentCellCleanup`; the Abandon conversion in `ApplyEnvelope`) | No production caller yet; every branch is typed and test-observable; building a parallel destruction mechanism ahead of the object-table/lost-cell cutover wiring would be the exact workaround class CLAUDE.md forbids - failing closed is the honest interim. | Branch (1): a genuinely claimed-but-celless-undeferred entity aborts the drain and SURVIVES where retail destroys it, until the cutover wiring lands. Branch (3): a future envelope construction without a WeenieDescription would break the premise and needs re-examination. | `SmartBox::HandleCreateObject` 0x00454C80 tail (~93933 destruction mark; ~93942-93943 un-mark/no-weenie) |
---
diff --git a/src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs b/src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs
index 8f7674f0..ac548d0e 100644
--- a/src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs
+++ b/src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs
@@ -114,6 +114,48 @@ public sealed class InboundPhysicsStateController
return false;
}
+ accepted = ApplyAcceptedObjDesc(old, update);
+ _snapshots[update.Guid] = accepted;
+ return true;
+ }
+
+ ///
+ /// Shared ObjDesc snapshot mutation. A retained residence continuation was
+ /// only ever enqueued after the exact same
+ /// call already
+ /// succeeded at admission time, so the retained update's own
+ /// IS the stamped gate
+ /// value; the executor must not re-derive it from a live gate.
+ ///
+ ///
+ /// Instance seam for : reads
+ /// _snapshots[guid] as the merge base and writes the result back,
+ /// keeping this store and the continuation executor's canonical
+ /// RuntimeEntityRecord.Snapshot in lockstep (Round 3 A1). Without
+ /// this seam the executor merged directly against the record's own
+ /// snapshot and never touched _snapshots, so the FIRST later
+ /// legacy TryApplyXxx call would re-merge onto a stale base and
+ /// silently revert every drained continuation.
+ ///
+ internal bool ApplyAcceptedObjDescSnapshot(
+ uint guid,
+ ObjDescEvent.Parsed update,
+ out WorldSession.EntitySpawn accepted)
+ {
+ if (!_snapshots.TryGetValue(guid, out WorldSession.EntitySpawn old))
+ {
+ accepted = default;
+ return false;
+ }
+ accepted = ApplyAcceptedObjDesc(old, update);
+ _snapshots[guid] = accepted;
+ return true;
+ }
+
+ internal static WorldSession.EntitySpawn ApplyAcceptedObjDesc(
+ WorldSession.EntitySpawn old,
+ ObjDescEvent.Parsed update)
+ {
PhysicsSpawnData? physics = old.Physics;
if (physics is { } desc)
physics = desc with
@@ -121,7 +163,7 @@ public sealed class InboundPhysicsStateController
Timestamps = desc.Timestamps with { ObjDesc = update.ObjDescSequence },
};
- accepted = old with
+ return old with
{
AnimPartChanges = update.ModelData.AnimPartChanges,
TextureChanges = update.ModelData.TextureChanges,
@@ -129,8 +171,6 @@ public sealed class InboundPhysicsStateController
BasePaletteId = update.ModelData.BasePaletteId,
Physics = physics,
};
- _snapshots[update.Guid] = accepted;
- return true;
}
public bool TryApplyPickup(
@@ -145,11 +185,33 @@ public sealed class InboundPhysicsStateController
return false;
}
- accepted = ApplyUnparentedPosition(old, null, update.PositionSequence);
+ accepted = ApplyAcceptedPickup(old, update);
_snapshots[update.Guid] = accepted;
return true;
}
+ internal static WorldSession.EntitySpawn ApplyAcceptedPickup(
+ WorldSession.EntitySpawn old,
+ PickupEvent.Parsed update) =>
+ ApplyUnparentedPosition(old, null, update.PositionSequence);
+
+ /// Instance seam for - see the
+ /// remarks on .
+ internal bool ApplyAcceptedPickupSnapshot(
+ uint guid,
+ PickupEvent.Parsed update,
+ out WorldSession.EntitySpawn accepted)
+ {
+ if (!_snapshots.TryGetValue(guid, out WorldSession.EntitySpawn old))
+ {
+ accepted = default;
+ return false;
+ }
+ accepted = ApplyAcceptedPickup(old, update);
+ _snapshots[guid] = accepted;
+ return true;
+ }
+
///
/// Applies the parent branch embedded in a same-generation PhysicsDesc.
/// Unlike standalone ParentEvent it carries no parent INSTANCE_TS, so only
@@ -167,11 +229,33 @@ public sealed class InboundPhysicsStateController
return false;
}
- accepted = ApplyPositionTimestampOnly(child, update.ChildPositionSequence);
+ accepted = ApplyAcceptedCreateParent(child, update);
_snapshots[update.ChildGuid] = accepted;
return true;
}
+ internal static WorldSession.EntitySpawn ApplyAcceptedCreateParent(
+ WorldSession.EntitySpawn child,
+ CreateParentUpdate update) =>
+ ApplyPositionTimestampOnly(child, update.ChildPositionSequence);
+
+ /// Instance seam for -
+ /// see the remarks on .
+ internal bool ApplyAcceptedCreateParentSnapshot(
+ uint guid,
+ CreateParentUpdate update,
+ out WorldSession.EntitySpawn accepted)
+ {
+ if (!_snapshots.TryGetValue(guid, out WorldSession.EntitySpawn old))
+ {
+ accepted = default;
+ return false;
+ }
+ accepted = ApplyAcceptedCreateParent(old, update);
+ _snapshots[guid] = accepted;
+ return true;
+ }
+
public bool TryApplyParent(
ParentEvent.Parsed update,
out WorldSession.EntitySpawn accepted)
@@ -186,11 +270,33 @@ public sealed class InboundPhysicsStateController
return false;
}
- accepted = ApplyPositionTimestampOnly(child, update.ChildPositionSequence);
+ accepted = ApplyAcceptedParent(child, update);
_snapshots[update.ChildGuid] = accepted;
return true;
}
+ internal static WorldSession.EntitySpawn ApplyAcceptedParent(
+ WorldSession.EntitySpawn child,
+ ParentEvent.Parsed update) =>
+ ApplyPositionTimestampOnly(child, update.ChildPositionSequence);
+
+ /// Instance seam for - see the
+ /// remarks on .
+ internal bool ApplyAcceptedParentSnapshot(
+ uint guid,
+ ParentEvent.Parsed update,
+ out WorldSession.EntitySpawn accepted)
+ {
+ if (!_snapshots.TryGetValue(guid, out WorldSession.EntitySpawn old))
+ {
+ accepted = default;
+ return false;
+ }
+ accepted = ApplyAcceptedParent(old, update);
+ _snapshots[guid] = accepted;
+ return true;
+ }
+
public bool TryCommitParent(
uint childGuid,
uint parentGuid,
@@ -235,17 +341,28 @@ public sealed class InboundPhysicsStateController
update.MovementSequence,
update.ServerControlSequence);
timestamps = Current(gate);
- WorldSession.EntitySpawn stamped = MirrorGateTimestamps(old, gate) with
- {
- MovementSequence = gate.MovementTimestamp,
- ServerControlSequence = gate.ServerControlledMoveTimestamp,
- };
- _snapshots[update.Guid] = stamped;
// Retail consumes MOVEMENT_TS before it discovers that the
- // SERVER_CONTROLLED_MOVE_TS is stale. Preserve that timestamp-only
- // mutation in the canonical snapshot even though no motion payload
- // is applied.
+ // SERVER_CONTROLLED_MOVE_TS is stale (PhysicsTimestampGate.
+ // TryAcceptMovementEvent checks MOVEMENT_TS first and always advances
+ // it before the SERVER_CONTROLLED_MOVE_TS check can fail). Preserve
+ // that timestamp-only mutation in the canonical snapshot even though
+ // no motion payload is applied. Stamp from the GATE's post-call
+ // values, not the wire's own proposed values: TryAcceptMovementEvent
+ // has THREE rejection flavors (bad instance; stale MOVEMENT_TS; stale
+ // SERVER_CONTROLLED_MOVE_TS) and only the last one actually advances
+ // MOVEMENT_TS. Stamping update.MovementSequence unconditionally would
+ // move the snapshot to a rejected packet's value in the first two
+ // flavors - gate.MovementTimestamp is a no-op there and correct in
+ // the third, exactly like this method's legacy predecessor.
+ WorldSession.EntitySpawn stamped = ApplyAcceptedMotion(
+ old,
+ gate.MovementTimestamp,
+ gate.ServerControlledMoveTimestamp,
+ update,
+ retainPayload: false);
+ _snapshots[update.Guid] = stamped;
+
if (!applyPayload)
{
accepted = default;
@@ -258,6 +375,76 @@ public sealed class InboundPhysicsStateController
return true;
}
+ accepted = ApplyAcceptedMotion(
+ stamped,
+ gate.MovementTimestamp,
+ gate.ServerControlledMoveTimestamp,
+ update,
+ retainPayload: true);
+ _snapshots[update.Guid] = accepted;
+ return true;
+ }
+
+ ///
+ /// Shared Movement snapshot mutation. The top-level and nested
+ /// Movement/ServerControlledMove timestamp fields are ALWAYS stamped to
+ /// exactly /
+ /// (regardless of ); only the actual
+ /// movement payload (raw bytes/MotionState) is gated on it. This
+ /// deliberately does NOT mirror every OTHER timestamp channel from a
+ /// live gate the way the legacy path's old MirrorGateTimestamps
+ /// helper did (redundant there, since gate and snapshot stay in
+ /// lockstep on the immediate-apply path, but wrong for the executor's
+ /// out-of-band residence replay, where other channels may have advanced
+ /// far beyond what THIS retained continuation is allowed to observe).
+ ///
+ /// The movement/server-control VALUES are explicit inputs, not derived
+ /// from internally - one shared apply body,
+ /// two different sources of truth for its two callers. The legacy
+ /// immediate-apply caller MUST pass the live gate's own post-call
+ /// MovementTimestamp/ServerControlledMoveTimestamp
+ /// (PhysicsTimestampGate.TryAcceptMovementEvent has three rejection
+ /// flavors - bad instance, stale MOVEMENT_TS, stale
+ /// SERVER_CONTROLLED_MOVE_TS - and only reading the gate AFTER the call
+ /// is a no-op in the first two and correct in the third; the wire's own
+ /// proposed
+ /// would silently corrupt the snapshot to a rejected packet's value in
+ /// the first two flavors). The continuation executor instead passes the
+ /// retained action's own Movement.Value.MovementSequence/
+ /// - safe
+ /// there specifically because a Movement continuation is only ever
+ /// retained when AppliesMovementPayload || HasTimestampMutation,
+ /// which structurally guarantees MOVEMENT_TS itself already advanced to
+ /// that exact wire value at admission time (the same three-flavor gate
+ /// logic that makes stamping the wire value unsafe for an UNGATED legacy
+ /// call makes it exactly correct for an ALREADY-GATED retained one).
+ ///
+ internal static WorldSession.EntitySpawn ApplyAcceptedMotion(
+ WorldSession.EntitySpawn old,
+ ushort movementSequence,
+ ushort acceptedServerControlledMove,
+ WorldSession.EntityMotionUpdate update,
+ bool retainPayload)
+ {
+ PhysicsSpawnData? stampedPhysics = old.Physics;
+ if (stampedPhysics is { } stampedDesc)
+ stampedPhysics = stampedDesc with
+ {
+ Timestamps = stampedDesc.Timestamps with
+ {
+ Movement = movementSequence,
+ ServerControlledMove = acceptedServerControlledMove,
+ },
+ };
+ WorldSession.EntitySpawn stamped = old with
+ {
+ MovementSequence = movementSequence,
+ ServerControlSequence = acceptedServerControlledMove,
+ Physics = stampedPhysics,
+ };
+ if (!retainPayload)
+ return stamped;
+
PhysicsSpawnData? physics = stamped.Physics;
if (physics is { } desc)
physics = desc with
@@ -266,19 +453,37 @@ public sealed class InboundPhysicsStateController
ReadOnlyMemory.Empty,
update.MotionState,
update.IsAutonomous),
- Timestamps = desc.Timestamps with
- {
- Movement = gate.MovementTimestamp,
- ServerControlledMove = gate.ServerControlledMoveTimestamp,
- },
};
- accepted = stamped with
+ return stamped with
{
MotionState = update.MotionState,
Physics = physics,
};
- _snapshots[update.Guid] = accepted;
+ }
+
+ /// Instance seam for - see the
+ /// remarks on .
+ internal bool ApplyAcceptedMotionSnapshot(
+ uint guid,
+ ushort movementSequence,
+ ushort acceptedServerControlledMove,
+ WorldSession.EntityMotionUpdate update,
+ bool retainPayload,
+ out WorldSession.EntitySpawn accepted)
+ {
+ if (!_snapshots.TryGetValue(guid, out WorldSession.EntitySpawn old))
+ {
+ accepted = default;
+ return false;
+ }
+ accepted = ApplyAcceptedMotion(
+ old,
+ movementSequence,
+ acceptedServerControlledMove,
+ update,
+ retainPayload);
+ _snapshots[guid] = accepted;
return true;
}
@@ -293,6 +498,15 @@ public sealed class InboundPhysicsStateController
return false;
}
+ accepted = ApplyAcceptedVector(old, update);
+ _snapshots[update.Guid] = accepted;
+ return true;
+ }
+
+ internal static WorldSession.EntitySpawn ApplyAcceptedVector(
+ WorldSession.EntitySpawn old,
+ VectorUpdate.Parsed update)
+ {
PhysicsSpawnData? physics = old.Physics;
if (physics is { } desc)
physics = desc with
@@ -302,8 +516,23 @@ public sealed class InboundPhysicsStateController
Timestamps = desc.Timestamps with { Vector = update.VectorSequence },
};
- accepted = old with { Physics = physics };
- _snapshots[update.Guid] = accepted;
+ return old with { Physics = physics };
+ }
+
+ /// Instance seam for - see the
+ /// remarks on .
+ internal bool ApplyAcceptedVectorSnapshot(
+ uint guid,
+ VectorUpdate.Parsed update,
+ out WorldSession.EntitySpawn accepted)
+ {
+ if (!_snapshots.TryGetValue(guid, out WorldSession.EntitySpawn old))
+ {
+ accepted = default;
+ return false;
+ }
+ accepted = ApplyAcceptedVector(old, update);
+ _snapshots[guid] = accepted;
return true;
}
@@ -318,6 +547,15 @@ public sealed class InboundPhysicsStateController
return false;
}
+ accepted = ApplyAcceptedState(old, update);
+ _snapshots[update.Guid] = accepted;
+ return true;
+ }
+
+ internal static WorldSession.EntitySpawn ApplyAcceptedState(
+ WorldSession.EntitySpawn old,
+ SetState.Parsed update)
+ {
PhysicsSpawnData? physics = old.Physics;
if (physics is { } desc)
physics = desc with
@@ -326,12 +564,27 @@ public sealed class InboundPhysicsStateController
Timestamps = desc.Timestamps with { State = update.StateSequence },
};
- accepted = old with
+ return old with
{
PhysicsState = update.PhysicsState,
Physics = physics,
};
- _snapshots[update.Guid] = accepted;
+ }
+
+ /// Instance seam for - see the
+ /// remarks on .
+ internal bool ApplyAcceptedStateSnapshot(
+ uint guid,
+ SetState.Parsed update,
+ out WorldSession.EntitySpawn accepted)
+ {
+ if (!_snapshots.TryGetValue(guid, out WorldSession.EntitySpawn old))
+ {
+ accepted = default;
+ return false;
+ }
+ accepted = ApplyAcceptedState(old, update);
+ _snapshots[guid] = accepted;
return true;
}
@@ -339,6 +592,20 @@ public sealed class InboundPhysicsStateController
/// Returns true when the addressed live incarnation exists, even when the
/// position payload is rejected. This lets callers publish a freshly
/// consumed FORCE_POSITION_TS without applying a stale pose.
+ ///
+ /// Round 4 R4-15: this legacy immediate-apply path has no HasContact or
+ /// route-classification concept at all - it merges unconditionally on
+ /// the retained alone. The
+ /// continuation executor's ApplyPositionAction instead runs
+ /// RuntimeAuthoritativePositionRouteClassifier and derives
+ /// contact solely from the retained wire packet's own
+ /// IsGrounded bit. This is internal refactor debt tracked for
+ /// the eventual cutover unification (this file's TryApplyPosition
+ /// is today's only PRODUCTION Position wire caller; the classifier-based
+ /// path is test-only until a host wires the executor) - it is NOT a
+ /// retail divergence and does not belong in
+ /// docs/architecture/retail-divergence-register.md. See docs/ISSUES.md
+ /// for the tracked follow-up.
///
public bool TryApplyPosition(
WorldSession.EntityPositionUpdate update,
@@ -370,12 +637,153 @@ public sealed class InboundPhysicsStateController
gate,
teleportAdvanced: disposition is PositionTimestampDisposition.Apply
&& advancesTeleport);
- if (disposition is PositionTimestampDisposition.Rejected)
+ accepted = ApplyAcceptedPosition(
+ old,
+ update,
+ disposition,
+ timestamps,
+ isLocalPlayer,
+ forcePositionRotation,
+ currentLocalVelocity,
+ // Legacy immediate-apply reproduces EXACT prior behavior: the
+ // placement frame and parent clear were always unconditional
+ // here (see the Round 3 A1/B6 admission handoff). Only the
+ // continuation executor threads the classified route's own
+ // ApplyPlacementFrameBeforeRouting/UnparentBeforeRouting flags.
+ installPlacementFrame: true,
+ clearParent: true);
+ _snapshots[update.Guid] = accepted;
+ return true;
+ }
+
+ /// Instance seam for - see
+ /// the remarks on . The
+ /// executor passes its classified route's own
+ /// ApplyPlacementFrameBeforeRouting/UnparentBeforeRouting
+ /// flags rather than the legacy path's unconditional true/true.
+ internal bool ApplyAcceptedPositionSnapshot(
+ uint guid,
+ WorldSession.EntityPositionUpdate update,
+ PositionTimestampDisposition disposition,
+ AcceptedPhysicsTimestamps timestamps,
+ bool isLocalPlayer,
+ System.Numerics.Quaternion? forcePositionRotation,
+ System.Numerics.Vector3? currentLocalVelocity,
+ bool installPlacementFrame,
+ bool clearParent,
+ out WorldSession.EntitySpawn accepted)
+ {
+ if (!_snapshots.TryGetValue(guid, out WorldSession.EntitySpawn old))
{
- accepted = MirrorGateTimestamps(old, gate);
- _snapshots[update.Guid] = accepted;
- return true;
+ accepted = default;
+ return false;
}
+ accepted = ApplyAcceptedPosition(
+ old,
+ update,
+ disposition,
+ timestamps,
+ isLocalPlayer,
+ forcePositionRotation,
+ currentLocalVelocity,
+ installPlacementFrame,
+ clearParent);
+ _snapshots[guid] = accepted;
+ return true;
+ }
+
+ ///
+ /// Round 3 B10: a retained Position action whose ADMISSION-time
+ /// disposition was Apply/ForcePosition (the gate genuinely advanced
+ /// POSITION_TS/TELEPORT_TS/FORCE_POSITION_TS at admission) but whose
+ /// EXECUTION-time classification rejects (RejectedAuthority/RejectedData
+ /// from the live route classifier - e.g. a malformed live input) must
+ /// still stamp every timestamp channel the gate actually moved; it must
+ /// not silently freeze the snapshot at pre-admission values. Distinct
+ /// from , which is the
+ /// ADMISSION-time-gate-rejected case where only FORCE_POSITION_TS can
+ /// have moved - here Position, Teleport, AND ForcePosition are all
+ /// replayed, still without installing any pose/parent/placement field.
+ ///
+ internal bool ApplyAcceptedPositionExecutionRejectedSnapshot(
+ uint guid,
+ ushort acceptedPositionSequence,
+ AcceptedPhysicsTimestamps timestamps,
+ out WorldSession.EntitySpawn accepted)
+ {
+ if (!_snapshots.TryGetValue(guid, out WorldSession.EntitySpawn old))
+ {
+ accepted = default;
+ return false;
+ }
+ PhysicsSpawnData? physics = old.Physics;
+ if (physics is { } desc)
+ physics = desc with
+ {
+ Timestamps = desc.Timestamps with
+ {
+ Position = acceptedPositionSequence,
+ Teleport = timestamps.Teleport,
+ ForcePosition = timestamps.ForcePosition,
+ },
+ };
+ accepted = old with
+ {
+ PositionSequence = acceptedPositionSequence,
+ Physics = physics,
+ };
+ _snapshots[guid] = accepted;
+ return true;
+ }
+
+ ///
+ /// Shared Position snapshot mutation, reconstructed from the RETAINED
+ /// disposition + accepted-gate facts rather than a live gate read (the
+ /// executor drains a Position continuation long after the timestamp gate
+ /// itself moved on to later packets). Mirrors
+ /// SmartBox::HandleReceivedPosition (0x00453FD0) /
+ /// PositionPack::UnPack (0x00516740) exactly as the legacy
+ /// immediate-apply path did.
+ ///
+ /// A retained
+ /// continuation only ever exists because
+ /// -adjacent
+ /// bookkeeping mutated (see :
+ /// a Rejected outcome always leaves POSITION_TS and TELEPORT_TS net
+ /// unchanged, so the ONLY dimension that can differ is FORCE_POSITION_TS
+ /// from the local-player force-position fallthrough branch) — apply the
+ /// timestamp-only mutation and nothing else.
+ ///
+ /// For Apply/ForcePosition, the retained wire's own
+ /// IS the
+ /// stamped POSITION_TS value (PhysicsTimestampGate always sets the stored
+ /// channel to exactly the incoming value on acceptance); Teleport and
+ /// ForcePosition stamps come from the retained
+ /// captured at admission time.
+ ///
+ /// /
+ /// (Round 3 B6) let the two callers reproduce two different retail
+ /// gates: the legacy immediate-apply path always passes true/true
+ /// (retail's HandleReceivedPosition unconditionally runs
+ /// unset_parent/SetPlacementFrame there), while the continuation
+ /// executor passes its classified route's own
+ /// ApplyPlacementFrameBeforeRouting/UnparentBeforeRouting -
+ /// both false only for the FORCE_POSITION branch, which retail's
+ /// MoveOrTeleport returns from immediately, BEFORE either call.
+ ///
+ internal static WorldSession.EntitySpawn ApplyAcceptedPosition(
+ WorldSession.EntitySpawn old,
+ WorldSession.EntityPositionUpdate update,
+ PositionTimestampDisposition disposition,
+ AcceptedPhysicsTimestamps timestamps,
+ bool isLocalPlayer,
+ System.Numerics.Quaternion? forcePositionRotation,
+ System.Numerics.Vector3? currentLocalVelocity,
+ bool installPlacementFrame,
+ bool clearParent)
+ {
+ if (disposition is PositionTimestampDisposition.Rejected)
+ return ApplyAcceptedPositionTimestampOnly(old, timestamps);
CreateObject.ServerPosition appliedPosition = update.Position;
if (disposition is PositionTimestampDisposition.ForcePosition
@@ -392,9 +800,14 @@ public sealed class InboundPhysicsStateController
// PositionPack::UnPack (0x00516740) initializes an absent placement
// id to zero; HandleReceivedPosition (0x00453FD0) forwards that exact
- // value to SetPlacementFrame on a normal accepted update.
- uint? appliedPlacement = disposition is PositionTimestampDisposition.Apply
- ? update.PlacementId ?? 0u
+ // value to SetPlacementFrame on a normal accepted update - but only
+ // when the caller's route actually runs that step
+ // (installPlacementFrame; retail skips it entirely while HasAnimations
+ // is true).
+ uint? appliedPlacement = installPlacementFrame
+ ? (disposition is PositionTimestampDisposition.Apply
+ ? update.PlacementId ?? 0u
+ : old.PlacementId)
: old.PlacementId;
System.Numerics.Vector3? appliedVelocity = disposition switch
@@ -407,7 +820,7 @@ public sealed class InboundPhysicsStateController
// A fresh local teleport explicitly installs zero velocity. A
// normal local correction does not consume PositionPack velocity.
PositionTimestampDisposition.Apply when isLocalPlayer =>
- advancesTeleport
+ timestamps.TeleportAdvanced
? System.Numerics.Vector3.Zero
: currentLocalVelocity ?? old.Physics?.Velocity,
@@ -418,6 +831,22 @@ public sealed class InboundPhysicsStateController
_ => old.Physics?.Velocity,
};
+ // Round 4 R4-12: FORCE_POSITION_TS's retail Gate A
+ // (retail-notes.md function 3, SmartBox::HandleReceivedPosition
+ // 0x00453FD0, "GATE A: local-player force-position self-echo
+ // shortcut") returns BEFORE CPhysicsObj::unset_parent ever runs -
+ // clearParent stays false on that route (Round 3 B6), so a
+ // ForcePosition-merged snapshot can legitimately carry BOTH a
+ // non-null Position (the force-applied pose, below) AND a non-null
+ // ParentGuid/ParentLocation/Physics.Parent (the retained
+ // attachment) simultaneously. This combined shape is deliberate,
+ // not a bug: every OTHER accepted disposition either clears the
+ // parent (a genuine unparented Position) or never touches Position
+ // at all (a Parent/CreateParent-only merge) - ForcePosition is the
+ // one case that does both without touching parent state at all.
+ uint? parentGuid = clearParent ? null : old.ParentGuid;
+ uint? parentLocation = clearParent ? null : old.ParentLocation;
+ PhysicsAttachment? physicsParent = clearParent ? null : old.Physics?.Parent;
PhysicsSpawnData? physics = old.Physics;
if (physics is { } desc)
physics = desc with
@@ -425,26 +854,47 @@ public sealed class InboundPhysicsStateController
Position = appliedPosition,
AnimationFrame = appliedPlacement,
Velocity = appliedVelocity,
- Parent = null,
+ Parent = physicsParent,
Timestamps = desc.Timestamps with
{
- Position = gate.PositionTimestamp,
- Teleport = gate.TeleportTimestamp,
- ForcePosition = gate.ForcePositionTimestamp,
+ Position = update.PositionSequence,
+ Teleport = timestamps.Teleport,
+ ForcePosition = timestamps.ForcePosition,
},
};
- accepted = old with
+ return old with
{
Position = appliedPosition,
- PositionSequence = gate.PositionTimestamp,
- ParentGuid = null,
- ParentLocation = null,
+ PositionSequence = update.PositionSequence,
+ ParentGuid = parentGuid,
+ ParentLocation = parentLocation,
PlacementId = appliedPlacement,
Physics = physics,
};
- _snapshots[update.Guid] = accepted;
- return true;
+ }
+
+ ///
+ /// The Rejected-disposition-but-mutated branch of
+ /// : only FORCE_POSITION_TS can have
+ /// legitimately moved (see that method's remarks). Deliberately narrower
+ /// than the legacy path's old full-gate mirror, which was only safe
+ /// in-lockstep and is unsound for the executor's out-of-band replay.
+ ///
+ private static WorldSession.EntitySpawn ApplyAcceptedPositionTimestampOnly(
+ WorldSession.EntitySpawn old,
+ AcceptedPhysicsTimestamps timestamps)
+ {
+ PhysicsSpawnData? physics = old.Physics;
+ if (physics is { } desc)
+ physics = desc with
+ {
+ Timestamps = desc.Timestamps with
+ {
+ ForcePosition = timestamps.ForcePosition,
+ },
+ };
+ return old with { Physics = physics };
}
///
@@ -675,31 +1125,6 @@ public sealed class InboundPhysicsStateController
TeleportHookRequired: false,
previousTeleport);
- private static WorldSession.EntitySpawn MirrorGateTimestamps(
- WorldSession.EntitySpawn spawn,
- PhysicsTimestampGate gate)
- {
- if (spawn.Physics is not { } desc)
- return spawn;
-
- return spawn with
- {
- Physics = desc with
- {
- Timestamps = new PhysicsTimestamps(
- gate.PositionTimestamp,
- gate.MovementTimestamp,
- gate.StateTimestamp,
- gate.VectorTimestamp,
- gate.TeleportTimestamp,
- gate.ServerControlledMoveTimestamp,
- gate.ForcePositionTimestamp,
- gate.ObjDescTimestamp,
- gate.InstanceTimestamp),
- },
- };
- }
-
private static WorldSession.EntitySpawn MergeUntimestampedCreate(
WorldSession.EntitySpawn retained,
WorldSession.EntitySpawn incoming) =>
@@ -727,6 +1152,32 @@ public sealed class InboundPhysicsStateController
Physics = retained.Physics,
};
+ ///
+ /// Instance seam for the SameIncarnationCreate envelope's
+ /// WeenieDescription stage (Round 3 A2). This must NOT be a wholesale
+ /// snapshot replacement of the raw retained packet - it merges exactly
+ /// like every other same-generation Create
+ /// (), keeping the retained
+ /// Position/appearance/physics-timestamp fields that earlier stages in
+ /// THIS envelope (and any earlier FIFO entry) already committed to
+ /// _snapshots, and taking only the incoming packet's untimestamped
+ /// identity/description fields.
+ ///
+ internal bool ApplyAcceptedWeenieDescriptionSnapshot(
+ uint guid,
+ WorldSession.EntitySpawn incoming,
+ out WorldSession.EntitySpawn merged)
+ {
+ if (!_snapshots.TryGetValue(guid, out WorldSession.EntitySpawn retained))
+ {
+ merged = default;
+ return false;
+ }
+ merged = MergeUntimestampedCreate(retained, incoming);
+ _snapshots[guid] = merged;
+ return true;
+ }
+
private static SameGenerationCreateObjectEvents BuildSameGenerationEvents(
WorldSession.EntitySpawn incoming)
{
diff --git a/src/AcDream.Runtime/Entities/ParentAttachmentState.cs b/src/AcDream.Runtime/Entities/ParentAttachmentState.cs
index 5405ecc7..1ec4a209 100644
--- a/src/AcDream.Runtime/Entities/ParentAttachmentState.cs
+++ b/src/AcDream.Runtime/Entities/ParentAttachmentState.cs
@@ -1,3 +1,4 @@
+using System.Collections.Immutable;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
@@ -20,8 +21,53 @@ public sealed class ParentAttachmentState
private readonly Dictionary> _committedChildrenByParent = new();
private readonly Dictionary>
_deferredCreatesByParent = [];
+ ///
+ /// Round 5 R5-1: retail-faithful queue-by-parent-GUID deferral for an
+ /// ACCEPTED parent relation (standalone Parent continuation or envelope
+ /// CreateParent stage) whose parent is unaddressable or names a
+ /// not-yet-arrived incarnation. Shares the SAME per-guid "blobs waiting
+ /// on guid X" shape as - retail's
+ /// QueueBlobForObject/CObjectMaint bucket does not
+ /// distinguish a raw Create blob from any other blob type queued
+ /// against the same guid.
+ ///
+ private readonly Dictionary>
+ _deferredAcceptedRelationsByParent = [];
private ulong _nextDeferredCreateAdmissionId;
+ ///
+ /// Round 5 R5-2: cancellation-aware detach/restore window state, shared
+ /// by BOTH deferred buckets. and
+ /// register one window
+ /// entry per detach; while it is open, every cancellation primitive
+ /// (, ,
+ /// , ,
+ /// ) ADDITIONALLY records its own retain
+ /// predicate into every currently-open window of the matching bucket
+ /// kind, so a later Restore call can apply the SAME filtering to the
+ /// detached remainder that would have applied had the batch never left
+ /// the live dictionary. wipes both window
+ /// dictionaries outright, which is what makes a stale token
+ /// (post-Clear/Dispose) restore nothing - the token's Id is simply
+ /// gone, an ABA-safe no-op via the same "TryRemove fails" pattern
+ /// already relies on.
+ ///
+ private sealed class CreateWindowState
+ {
+ internal required uint ParentGuid { get; init; }
+ internal List> Filters { get; } = [];
+ }
+
+ private sealed class RelationWindowState
+ {
+ internal required uint ParentGuid { get; init; }
+ internal List> Filters { get; } = [];
+ }
+
+ private readonly Dictionary _createWindows = [];
+ private readonly Dictionary _relationWindows = [];
+ private ulong _nextWindowId;
+
public int UnresolvedRelationCount =>
_unresolvedByChild.Values.Sum(queue => queue.Count);
public int StagedRelationCount => _stagedByChild.Count;
@@ -29,6 +75,8 @@ public sealed class ParentAttachmentState
public int CommittedRelationCount => _lastAcceptedByChild.Count;
internal int DeferredCreateCount =>
_deferredCreatesByParent.Values.Sum(queue => queue.Count);
+ internal int DeferredAcceptedRelationCount =>
+ _deferredAcceptedRelationsByParent.Values.Sum(queue => queue.Count);
///
/// Retains the complete unaccepted CreateObject packet when its nonzero
@@ -102,6 +150,81 @@ public sealed class ParentAttachmentState
return true;
}
+ ///
+ /// Round 3 B7: retail's PartArray::add_child-owning CreateObject
+ /// handler detaches the ENTIRE queued netblob list for one parent
+ /// atomically before dispatching any of it (pseudo-C ~93617) - there is
+ /// no separate peek-then-remove step; the detach itself IS the consume.
+ /// Returns an empty array when nothing was queued. Structurally rules
+ /// out the stale-AdmissionId race the previous peek/consume replay loop
+ /// had to special-case: a Create arriving for this parent AFTER this
+ /// call enqueues into a brand-new queue instance, never the one already
+ /// removed here. Round 5 R5-2: opens a cancellation-aware window
+ /// () for the detached batch - see the window-
+ /// machinery remarks at this class's field declarations.
+ ///
+ internal ImmutableArray DetachDeferredCreates(
+ uint parentGuid,
+ out DeferredReplayWindowToken window)
+ {
+ if (!_deferredCreatesByParent.Remove(
+ parentGuid,
+ out Queue? queue))
+ {
+ window = default;
+ return ImmutableArray.Empty;
+ }
+ ulong id = ++_nextWindowId;
+ _createWindows[id] = new CreateWindowState { ParentGuid = parentGuid };
+ window = new DeferredReplayWindowToken(id, parentGuid, DeferredReplayBucketKind.Creates);
+ return [.. queue];
+ }
+
+ ///
+ /// Round 4 R4-1 / Round 5 R5-2: restores the unprocessed remainder of a
+ /// previously-detached replay batch, in original FIFO order and with
+ /// original values, at the
+ /// FRONT of the window's parent guid's queue - ahead of anything enqueued
+ /// for the same parent guid AFTER the detach. Every cancellation
+ /// primitive that fired WHILE this exact window was open recorded its
+ /// own retain predicate; those predicates are applied here before
+ /// re-insertion, so a child deleted (or otherwise cancelled) mid-replay
+ /// is never resurrected. ALWAYS call this once replay of the detached
+ /// batch concludes - successful or not - passing the empty remainder on
+ /// full success; this releases the window (a stale/already-released/
+ /// Clear-invalidated token is an ABA-safe no-op, since its Id is simply
+ /// no longer tracked).
+ ///
+ internal void RestoreDeferredCreates(
+ in DeferredReplayWindowToken window,
+ ReadOnlySpan entries)
+ {
+ if (window.Kind != DeferredReplayBucketKind.Creates
+ || !_createWindows.Remove(window.Id, out CreateWindowState? state))
+ {
+ return;
+ }
+ if (entries.Length == 0)
+ return;
+ IEnumerable filtered = entries.ToArray();
+ foreach (Func filter in state.Filters)
+ filtered = filtered.Where(filter);
+ DeferredParentCreate[] survivors = filtered.ToArray();
+ if (survivors.Length == 0)
+ return;
+ var restored = new Queue(survivors.Length);
+ foreach (DeferredParentCreate entry in survivors)
+ restored.Enqueue(entry);
+ if (_deferredCreatesByParent.TryGetValue(
+ window.ParentGuid,
+ out Queue? existing))
+ {
+ foreach (DeferredParentCreate entry in existing)
+ restored.Enqueue(entry);
+ }
+ _deferredCreatesByParent[window.ParentGuid] = restored;
+ }
+
internal bool ContainsDeferredCreate(
uint childGuid,
ushort instanceSequence)
@@ -119,18 +242,151 @@ public sealed class ParentAttachmentState
return false;
}
+ ///
+ /// Round 5 R5-1: retail-faithful replacement for the Round 4 discard -
+ /// a missing/not-yet-arrived parent QUEUES the accepted relation under
+ /// the PARENT's guid (standalone parent handler 0x004535D0 ->
+ /// QueueBlobForObject, pseudo-C 92326; GUID-keyed placeholder
+ /// bucket in CObjectMaint, 271082-271088) and replays it when
+ /// that guid is created, exactly like a raw missing-parent Create.
+ /// Shares the SAME monotonic AdmissionId source as
+ /// (never reset) - both buckets are
+ /// "blobs waiting on guid X," the same general retail mechanism.
+ ///
+ internal void EnqueueDeferredAcceptedRelation(
+ uint childGuid,
+ RuntimeEntityKey childKey,
+ ParentEvent.Parsed? standalone,
+ CreateParentUpdate? envelope,
+ AcceptedPhysicsTimestamps acceptedTimestamps)
+ {
+ uint parentGuid = standalone?.ParentGuid ?? envelope?.ParentGuid ?? 0u;
+ if (parentGuid == 0u || childGuid == 0u)
+ {
+ throw new ArgumentException(
+ "A deferred accepted parent relation requires nonzero parent and child GUIDs.");
+ }
+ if (_nextDeferredCreateAdmissionId == ulong.MaxValue)
+ {
+ throw new InvalidOperationException(
+ "The deferred parent CreateObject admission sequence is exhausted.");
+ }
+ ulong admissionId = _nextDeferredCreateAdmissionId + 1UL;
+ EnqueueDeferredAcceptedRelation(new DeferredAcceptedParentRelation(
+ admissionId, childGuid, childKey, standalone, envelope, acceptedTimestamps));
+ _nextDeferredCreateAdmissionId = admissionId;
+ }
+
+ ///
+ /// Re-enqueues an EXISTING relation verbatim, preserving its original
+ /// - used at
+ /// replay time when the relation still names a parent incarnation that
+ /// has not yet arrived (wait for the next matching incarnation).
+ ///
+ internal void EnqueueDeferredAcceptedRelation(
+ in DeferredAcceptedParentRelation relation)
+ {
+ uint parentGuid = relation.Standalone?.ParentGuid
+ ?? relation.Envelope?.ParentGuid
+ ?? 0u;
+ if (!_deferredAcceptedRelationsByParent.TryGetValue(
+ parentGuid,
+ out Queue? queue))
+ {
+ queue = new Queue();
+ _deferredAcceptedRelationsByParent.Add(parentGuid, queue);
+ }
+ queue.Enqueue(relation);
+ }
+
+ /// Round 5 R5-2 window-aware detach - see 's remarks.
+ internal ImmutableArray DetachDeferredAcceptedRelations(
+ uint parentGuid,
+ out DeferredReplayWindowToken window)
+ {
+ if (!_deferredAcceptedRelationsByParent.Remove(
+ parentGuid,
+ out Queue? queue))
+ {
+ window = default;
+ return ImmutableArray.Empty;
+ }
+ ulong id = ++_nextWindowId;
+ _relationWindows[id] = new RelationWindowState { ParentGuid = parentGuid };
+ window = new DeferredReplayWindowToken(id, parentGuid, DeferredReplayBucketKind.AcceptedRelations);
+ return [.. queue];
+ }
+
+ /// Round 5 R5-2 window-aware restore - see 's remarks.
+ internal void RestoreDeferredAcceptedRelations(
+ in DeferredReplayWindowToken window,
+ ReadOnlySpan entries)
+ {
+ if (window.Kind != DeferredReplayBucketKind.AcceptedRelations
+ || !_relationWindows.Remove(window.Id, out RelationWindowState? state))
+ {
+ return;
+ }
+ if (entries.Length == 0)
+ return;
+ IEnumerable filtered = entries.ToArray();
+ foreach (Func filter in state.Filters)
+ filtered = filtered.Where(filter);
+ DeferredAcceptedParentRelation[] survivors = filtered.ToArray();
+ if (survivors.Length == 0)
+ return;
+ var restored = new Queue(survivors.Length);
+ foreach (DeferredAcceptedParentRelation entry in survivors)
+ restored.Enqueue(entry);
+ if (_deferredAcceptedRelationsByParent.TryGetValue(
+ window.ParentGuid,
+ out Queue? existing))
+ {
+ foreach (DeferredAcceptedParentRelation entry in existing)
+ restored.Enqueue(entry);
+ }
+ _deferredAcceptedRelationsByParent[window.ParentGuid] = restored;
+ }
+
+ internal bool ContainsDeferredAcceptedRelation(
+ uint childGuid,
+ RuntimeEntityKey childKey)
+ {
+ foreach (Queue queue
+ in _deferredAcceptedRelationsByParent.Values)
+ {
+ if (queue.Any(candidate =>
+ candidate.ChildGuid == childGuid
+ && candidate.ChildKey == childKey))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
///
/// Cancels only the raw, still-unaccepted child generation addressed by a
/// terminal packet. Instance zero is a normal retail timestamp and is not
- /// treated as an empty sentinel.
+ /// treated as an empty sentinel. Round 5 R5-1: also cancels a deferred
+ /// ACCEPTED relation for the same child incarnation - "child-addressed
+ /// candidates die with the child" applies identically to both buckets.
///
internal void CancelDeferredChildGeneration(
uint childGuid,
- ushort terminalInstanceSequence) => FilterDeferredCreates(
+ ushort terminalInstanceSequence)
+ {
+ FilterDeferredCreates(
candidate => candidate.Spawn.Guid != childGuid
|| PhysicsTimestampGate.IsNewer(
terminalInstanceSequence,
candidate.Spawn.InstanceSequence));
+ FilterDeferredAcceptedRelations(
+ candidate => candidate.ChildGuid != childGuid
+ || PhysicsTimestampGate.IsNewer(
+ terminalInstanceSequence,
+ candidate.ChildKey.Incarnation));
+ }
public void AcceptCreateObjectRelation(ParentAttachmentRelation relation)
{
@@ -379,6 +635,7 @@ public sealed class ParentAttachmentState
public void RemoveObject(uint guid)
{
RemoveDeferredChildCreates(guid);
+ RemoveDeferredAcceptedRelationsForChild(guid);
_stagedByChild.Remove(guid);
_recoveryByChild.Remove(guid);
RemoveCommittedChild(guid);
@@ -413,6 +670,12 @@ public sealed class ParentAttachmentState
|| PhysicsTimestampGate.IsNewer(
replacementGeneration,
candidate.Spawn.InstanceSequence));
+ FilterDeferredAcceptedRelations(candidate =>
+ candidate.ChildGuid != guid
+ || candidate.ChildKey.Incarnation == replacementGeneration
+ || PhysicsTimestampGate.IsNewer(
+ replacementGeneration,
+ candidate.ChildKey.Incarnation));
FilterChildCandidates(
guid,
relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent);
@@ -468,6 +731,7 @@ public sealed class ParentAttachmentState
public void RemoveChild(uint childGuid)
{
RemoveDeferredChildCreates(childGuid);
+ RemoveDeferredAcceptedRelationsForChild(childGuid);
_stagedByChild.Remove(childGuid);
_recoveryByChild.Remove(childGuid);
RemoveCommittedChild(childGuid);
@@ -477,6 +741,12 @@ public sealed class ParentAttachmentState
public void Clear()
{
_deferredCreatesByParent.Clear();
+ _deferredAcceptedRelationsByParent.Clear();
+ // Round 5 R5-2: wipes every open window outright - a later
+ // Restore call for a token minted before this Clear() finds
+ // nothing to remove by Id and correctly no-ops (ABA-safe).
+ _createWindows.Clear();
+ _relationWindows.Clear();
_unresolvedByChild.Clear();
_stagedByChild.Clear();
_recoveryByChild.Clear();
@@ -490,6 +760,15 @@ public sealed class ParentAttachmentState
=> FilterDeferredCreates(
candidate => candidate.Spawn.Guid != childGuid);
+ private void RemoveDeferredAcceptedRelationsForChild(uint childGuid)
+ => FilterDeferredAcceptedRelations(
+ candidate => candidate.ChildGuid != childGuid);
+
+ ///
+ /// Round 5 R5-2: filters the LIVE bucket exactly as before, then
+ /// records the SAME retain predicate into every currently-open create
+ /// window so a later Restore applies it to the detached remainder too.
+ ///
private void FilterDeferredCreates(
Func retain)
{
@@ -504,6 +783,27 @@ public sealed class ParentAttachmentState
else
_deferredCreatesByParent[parentGuid] = retained;
}
+ foreach (CreateWindowState state in _createWindows.Values)
+ state.Filters.Add(retain);
+ }
+
+ /// Round 5 R5-2 relation-bucket counterpart of .
+ private void FilterDeferredAcceptedRelations(
+ Func retain)
+ {
+ uint[] parents = _deferredAcceptedRelationsByParent.Keys.ToArray();
+ for (int index = 0; index < parents.Length; index++)
+ {
+ uint parentGuid = parents[index];
+ Queue retained = new(
+ _deferredAcceptedRelationsByParent[parentGuid].Where(retain));
+ if (retained.Count == 0)
+ _deferredAcceptedRelationsByParent.Remove(parentGuid);
+ else
+ _deferredAcceptedRelationsByParent[parentGuid] = retained;
+ }
+ foreach (RelationWindowState state in _relationWindows.Values)
+ state.Filters.Add(retain);
}
private void RemoveCommittedChild(uint childGuid)
@@ -609,6 +909,52 @@ internal readonly record struct DeferredParentCreate(
&& Spawn.Guid != 0u;
}
+///
+/// Round 5 R5-1: one ACCEPTED parent relation (the gate was already
+/// consumed - the child's own POSITION_TS channel advanced at admission)
+/// queued under its parent's guid because that parent was unaddressable or
+/// named a not-yet-arrived incarnation. Carries exactly ONE of
+/// (a standalone Parent continuation, which HAS a
+/// parent incarnation to compare) or (an envelope
+/// CreateParent stage, which does not).
+///
+internal readonly record struct DeferredAcceptedParentRelation(
+ ulong AdmissionId,
+ uint ChildGuid,
+ RuntimeEntityKey ChildKey,
+ ParentEvent.Parsed? Standalone,
+ CreateParentUpdate? Envelope,
+ AcceptedPhysicsTimestamps AcceptedTimestamps)
+{
+ internal bool IsValid => AdmissionId != 0UL
+ && ChildGuid != 0u
+ && ChildKey.LocalEntityId != 0u
+ && (Standalone.HasValue ^ Envelope.HasValue);
+
+ /// Null for the envelope flavor - carries no parent INSTANCE_TS.
+ internal ushort? ParentInstanceSequence => Standalone?.ParentInstanceSequence;
+}
+
+/// Round 5 R5-2: which deferred bucket a belongs to.
+internal enum DeferredReplayBucketKind : byte
+{
+ Creates,
+ AcceptedRelations,
+}
+
+///
+/// Round 5 R5-2: opaque handle for one open detach/restore window. See the
+/// window-machinery remarks at 's field
+/// declarations for the full cancellation-awareness contract.
+///
+internal readonly record struct DeferredReplayWindowToken(
+ ulong Id,
+ uint ParentGuid,
+ DeferredReplayBucketKind Kind)
+{
+ internal bool IsValid => Id != 0UL;
+}
+
public readonly record struct ParentAttachmentRelation(
uint ParentGuid,
uint ChildGuid,
diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs b/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs
index f037dbf5..a9ac7b05 100644
--- a/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs
+++ b/src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs
@@ -547,6 +547,101 @@ public sealed class RuntimeEntityDirectory
public bool IsFreshTeleportStart(uint guid, ushort teleportSequence) =>
_inbound.IsFreshTeleportStart(guid, teleportSequence);
+ // Round 3 A1: gate-less instance seams for the initial-Create
+ // continuation executor. Each merges against _inbound's OWN
+ // _snapshots[guid] (never a caller-supplied base) and writes the result
+ // back, keeping this store and RuntimeEntityRecord.Snapshot in lockstep.
+ internal bool ApplyAcceptedObjDescSnapshot(
+ uint guid,
+ ObjDescEvent.Parsed update,
+ out WorldSession.EntitySpawn accepted) =>
+ _inbound.ApplyAcceptedObjDescSnapshot(guid, update, out accepted);
+
+ internal bool ApplyAcceptedPickupSnapshot(
+ uint guid,
+ PickupEvent.Parsed update,
+ out WorldSession.EntitySpawn accepted) =>
+ _inbound.ApplyAcceptedPickupSnapshot(guid, update, out accepted);
+
+ internal bool ApplyAcceptedCreateParentSnapshot(
+ uint guid,
+ CreateParentUpdate update,
+ out WorldSession.EntitySpawn accepted) =>
+ _inbound.ApplyAcceptedCreateParentSnapshot(guid, update, out accepted);
+
+ internal bool ApplyAcceptedParentSnapshot(
+ uint guid,
+ ParentEvent.Parsed update,
+ out WorldSession.EntitySpawn accepted) =>
+ _inbound.ApplyAcceptedParentSnapshot(guid, update, out accepted);
+
+ internal bool ApplyAcceptedMotionSnapshot(
+ uint guid,
+ ushort movementSequence,
+ ushort acceptedServerControlledMove,
+ WorldSession.EntityMotionUpdate update,
+ bool retainPayload,
+ out WorldSession.EntitySpawn accepted) =>
+ _inbound.ApplyAcceptedMotionSnapshot(
+ guid,
+ movementSequence,
+ acceptedServerControlledMove,
+ update,
+ retainPayload,
+ out accepted);
+
+ internal bool ApplyAcceptedStateSnapshot(
+ uint guid,
+ SetState.Parsed update,
+ out WorldSession.EntitySpawn accepted) =>
+ _inbound.ApplyAcceptedStateSnapshot(guid, update, out accepted);
+
+ internal bool ApplyAcceptedVectorSnapshot(
+ uint guid,
+ VectorUpdate.Parsed update,
+ out WorldSession.EntitySpawn accepted) =>
+ _inbound.ApplyAcceptedVectorSnapshot(guid, update, out accepted);
+
+ internal bool ApplyAcceptedPositionSnapshot(
+ uint guid,
+ WorldSession.EntityPositionUpdate update,
+ PositionTimestampDisposition disposition,
+ AcceptedPhysicsTimestamps timestamps,
+ bool isLocalPlayer,
+ System.Numerics.Quaternion? forcePositionRotation,
+ System.Numerics.Vector3? currentLocalVelocity,
+ bool installPlacementFrame,
+ bool clearParent,
+ out WorldSession.EntitySpawn accepted) =>
+ _inbound.ApplyAcceptedPositionSnapshot(
+ guid,
+ update,
+ disposition,
+ timestamps,
+ isLocalPlayer,
+ forcePositionRotation,
+ currentLocalVelocity,
+ installPlacementFrame,
+ clearParent,
+ out accepted);
+
+ internal bool ApplyAcceptedPositionExecutionRejectedSnapshot(
+ uint guid,
+ ushort acceptedPositionSequence,
+ AcceptedPhysicsTimestamps timestamps,
+ out WorldSession.EntitySpawn accepted) =>
+ _inbound.ApplyAcceptedPositionExecutionRejectedSnapshot(
+ guid,
+ acceptedPositionSequence,
+ timestamps,
+ out accepted);
+
+ internal bool ApplyAcceptedWeenieDescriptionSnapshot(
+ uint guid,
+ WorldSession.EntitySpawn incoming,
+ out WorldSession.EntitySpawn merged) =>
+ _inbound.ApplyAcceptedWeenieDescriptionSnapshot(guid, incoming, out merged);
+
private bool IsKnown(RuntimeEntityRecord record)
{
if (IsCurrent(record))
diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
index a238d27a..a98e8c4b 100644
--- a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
+++ b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
@@ -31,6 +31,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
int EquipmentOwnerCount,
int PendingMoveCount,
int InitialCreateResidenceLeaseCount,
+ int InitialCreateExecutorProgressCount,
int StreamSubscriberCount,
int PlacementStreamSubscriberCount,
long StreamDispatchFailureCount,
@@ -38,7 +39,12 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
int PendingDispatchCount,
bool IsDispatching,
bool IsSessionClearInProgress,
- bool IsDisposed)
+ bool IsDisposed,
+ /// Round 5 R5-1: pending queue-by-parent-GUID accepted relations (see ).
+ int DeferredAcceptedRelationCount = 0,
+ /// Round 5 R5-3: mirrors StreamDispatchFailureCount/HasLastStreamDispatchFailure for the executor's contained-replay failure surface. Diagnostic only - like its stream precedent, NOT gated by .
+ long ReplayFailureCount = 0,
+ bool HasLastReplayFailure = false)
{
public bool IsConverged =>
IsDisposed
@@ -48,6 +54,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
&& AcceptedSnapshotCount == 0
&& UnresolvedParentRelationCount == 0
&& DeferredParentCreateCount == 0
+ && DeferredAcceptedRelationCount == 0
&& StagedParentRelationCount == 0
&& RecoveryParentRelationCount == 0
&& CommittedParentRelationCount == 0
@@ -57,6 +64,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
&& EquipmentOwnerCount == 0
&& PendingMoveCount == 0
&& InitialCreateResidenceLeaseCount == 0
+ && InitialCreateExecutorProgressCount == 0
&& StreamSubscriberCount == 0
&& PlacementStreamSubscriberCount == 0
&& PendingDispatchCount == 0
@@ -127,6 +135,23 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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));
+ // 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));
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
@@ -155,6 +180,23 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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));
+ // 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));
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
@@ -183,6 +225,23 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
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));
+ // 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));
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
@@ -197,6 +256,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
public RuntimePlacementProjectionChannel Placements { get; }
internal RuntimeInitialCreateResidenceState InitialCreateResidences
{ get; }
+ internal RuntimeInitialCreateContinuationExecutor InitialCreateExecution
+ { get; }
public RuntimeEntityObjectOwnershipSnapshot CaptureOwnership()
{
@@ -220,6 +281,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Objects.PendingMoveCount,
initialResidence.ActiveLeaseCount
+ initialResidence.PendingAdoptionCount,
+ InitialCreateExecution.ProgressCount,
Events.SubscriberCount,
Events.PlacementSubscriberCount,
Events.DispatchFailureCount,
@@ -227,7 +289,10 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Events.PendingDispatchCount,
Events.IsDispatching,
_sessionClearInProgress,
- _disposed);
+ _disposed,
+ parents.DeferredAcceptedRelationCount,
+ InitialCreateExecution.ReplayFailureCount,
+ InitialCreateExecution.LastReplayFailure is not null);
}
public void BindEventContext(
@@ -238,6 +303,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Events.BindContext(generation, frameNumber);
Placements.BindGeneration(generation);
InitialCreateResidences.BindGeneration(generation);
+ InitialCreateExecution.BindGeneration(generation);
}
///
@@ -1515,6 +1581,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
_sessionClearInProgress = true;
RuntimeEntityRecord[] active = Entities.ActiveRecords.ToArray();
InitialCreateResidences.Clear();
+ InitialCreateExecution.DiscardAll();
Physics.CollisionReports.LeaveWorldBatch(active);
Physics.ResetSessionPhysics();
Entities.BeginSessionClear();
@@ -2028,12 +2095,20 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
private RuntimePlacementCancellationReceipt ForgetInitialCreateResidence(
RuntimeEntityRecord canonical)
{
- return InitialCreateResidences.Forget(
+ bool forgotten = InitialCreateResidences.Forget(
canonical,
out _,
- out RuntimePlacementCancellationReceipt cancellation)
- ? cancellation
- : default;
+ 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(
diff --git a/src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs b/src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs
new file mode 100644
index 00000000..9905b538
--- /dev/null
+++ b/src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs
@@ -0,0 +1,2107 @@
+using System.Collections.Immutable;
+using System.Numerics;
+using AcDream.Core.Net;
+using AcDream.Core.Net.Messages;
+using AcDream.Core.Physics;
+using AcDream.Runtime.Physics;
+
+namespace AcDream.Runtime.Entities;
+
+internal enum RuntimeInitialCreateExecutionStatus : byte
+{
+ /// Initial tail + entire FIFO revision applied + residence consumed.
+ Completed,
+ /// Initial authored placement not yet acknowledged; retry later.
+ PendingPlacement,
+ ///
+ /// Yielded mid-drain: a Position continuation began an authored placement
+ /// that is not yet acknowledged; retry later. Two distinct flavors share
+ /// this one status (Round 4 R4-14): (1) the ORDINARY flavor, where
+ ///
+ /// returns the exact token to drive prepare/submit/acknowledge on; and
+ /// (2) transient operation-slot CONTENTION (Round 3 B1) - another
+ /// operation already occupies this entity's SetPosition slot at the
+ /// moment the continuation's own merge committed. In flavor (2),
+ /// TryGetPendingContinuationPlacement returns false (no
+ /// token was ever begun) even though the overall status is still
+ /// AwaitingContinuationPlacement; the caller's only correct action is to
+ /// retry Execute again later with no placement work of its own -
+ /// the retry re-attempts ONLY the placement begin against the
+ /// already-committed merge, never re-running the merge or re-publishing.
+ ///
+ AwaitingContinuationPlacement,
+ RejectedToken,
+ /// Residence retired/superseded - abandoned, ledgers converged.
+ RejectedAuthority,
+}
+
+///
+/// Executor-time inputs sampled at the retail decision point. These cannot be
+/// retained at admission time because they describe LIVE state (the local
+/// player, the current physics simulation) rather than the accepted wire
+/// packet itself. Round 3 A3: contact is NOT one of these - it comes solely
+/// from the retained wire packet's own IsGrounded bit (PositionPack
+/// bit 0x4, server-asserted contact at admission time), never from a live
+/// body query or a caller-supplied fallback.
+///
+internal readonly record struct RuntimeInitialCreateExecutionInputs(
+ bool UsePositionFromServer,
+ float PlayerDistance);
+
+internal enum RuntimeInitialCreateExecutedActionKind : byte
+{
+ InitialAdoption,
+ TeleportHookRequest,
+ DeferredChildReplay,
+ /// Round 5 R5-1: one queued accepted parent relation replayed in this parent's own initial tail.
+ ParentRelationReplay,
+ PreTailDescriptionAdaptation,
+ ObjDesc,
+ CreateParent,
+ Parent,
+ Pickup,
+ Position,
+ Movement,
+ State,
+ Vector,
+ WeenieDescription,
+ ResidentCellCleanup,
+}
+
+///
+/// Round 4 R4-6: outcome of one deferred child's replay registration
+/// ().
+/// Replaces the previous bool DeferredChildRegistered field, which
+/// collapsed a genuine re-defer (the grandparent is ALSO still missing)
+/// into the same "true" value as an outright successful registration.
+///
+internal enum RuntimeDeferredChildReplayOutcome : byte
+{
+ /// was non-null.
+ Registered,
+ /// was true - the replayed child itself still has a missing (grand)parent.
+ ReDeferred,
+ /// Neither Canonical nor DeferredForParent - registration was rejected outright, or the registration callback threw (Round 4 R4-1).
+ Rejected,
+}
+
+///
+/// Round 4 R4-6: distinct outcome for a Parent/CreateParent relation
+/// applied at execution time. Replaces the Round 3 B9 dead-letter
+/// re-Enqueue with retail-faithful discard (Round 4 R4-5).
+///
+internal enum RuntimeParentRelationOutcome : byte
+{
+ /// The parent was addressable and current (or, at replay, named the exact live incarnation); the attach commit ran.
+ Applied,
+ ///
+ /// Round 5 R5-1: the LIVE parent incarnation is newer than the one this
+ /// relation named - retail-faithful discard, mirroring
+ /// 's own "current parent
+ /// newer than the packet" branch. The already-accepted position-
+ /// timestamp merge already ran (at the relation's original drain, not
+ /// repeated here); no leave-world, no placement forget.
+ ///
+ DiscardedStaleParent,
+ ///
+ /// Round 5 R5-1: the parent is unaddressable, or (standalone Parent
+ /// only) names a parent incarnation that has not yet arrived - queued
+ /// under the parent's guid exactly like retail's QueueBlobForObject
+ /// (pseudo-C 92326), replayed when that guid is created.
+ ///
+ DeferredAwaitingParent,
+ ///
+ /// Round 5 R5-3: the queued relation's child is no longer valid at
+ /// replay time (not current, or a different incarnation than when
+ /// queued) - a contained failure, not an exception; recorded and
+ /// skipped, never resurrected.
+ ///
+ Rejected,
+}
+
+///
+/// Retail's exact three-way ResidentCellCleanup disposition (retail-notes.md
+/// function 1, SmartBox::HandleCreateObject 0x00454c80, lines ~788-801).
+///
+internal enum RuntimeResidentCellCleanupDisposition : byte
+{
+ ///
+ /// objcell_id != 0 && cell != 0: already resident -
+ /// un-mark (RemoveObjectToBeDestroyed).
+ ///
+ ResidentUnmarked,
+ ///
+ /// objcell_id != 0 && cell == 0 while an existing
+ /// lost-cell/deferred SetPosition operation already owns this exact
+ /// entity: the destruction mark belongs to that existing lifetime, not
+ /// to this tail action.
+ ///
+ DeferredUnderLostCellOwnership,
+ ///
+ /// No cell claimed at all (objcell_id == 0). Retail's own third
+ /// case (HandleCreateObject, retail-notes.md function 1, lines
+ /// ~93942-93943) additionally requires NO weenie description before
+ /// marking for destruction. That second half is structurally
+ /// UNREACHABLE through this exact envelope path: every
+ /// SameIncarnationCreate continuation this codebase constructs
+ /// carries a WeenieDescription action immediately before
+ /// ResidentCellCleanup, never optionally
+ /// (,
+ /// RuntimeInitialCreateResidenceState.cs:277-284, enforces
+ /// Actions[^2].Kind is WeenieDescription for every admitted
+ /// envelope). This value records the conservative claimed-but-celless
+ /// fact for that case without asserting it matches retail's documented
+ /// no-weenie destruction mark, and without building a second destruction
+ /// mechanism ahead of the object-table wiring that would let the
+ /// executor distinguish the two.
+ ///
+ CelllessNoWeenieMarkUnreachable,
+}
+
+///
+/// One immutable trace entry. is the owning
+/// continuation's FIFO sequence (0 for initial-tail-only facts that precede
+/// the FIFO entirely). is the same-incarnation envelope
+/// action index, or -1 outside an envelope.
+/// and are only meaningful for Position/hook-request
+/// entries; only for
+/// .
+///
+internal readonly record struct RuntimeInitialCreateExecutedAction(
+ RuntimeInitialCreateExecutedActionKind Kind,
+ ulong Sequence,
+ int Stage,
+ RuntimeAuthoritativePositionDisposition? PositionDisposition,
+ RuntimeTeleportHookPhase HookPhase,
+ RuntimeDeferredChildReplayOutcome? DeferredChildOutcome = null,
+ RuntimeResidentCellCleanupDisposition? ResidentCellCleanupDisposition = null,
+ RuntimePositionConstrainPhase ConstrainPhase = RuntimePositionConstrainPhase.None,
+ bool StopInterpolating = false,
+ bool ZeroVelocity = false,
+ bool PreserveHeading = false,
+ bool SendPositionImmediately = false,
+ bool UnparentBeforeRouting = false,
+ RuntimeParentRelationOutcome? ParentRelationOutcome = null);
+
+///
+/// Host-independent immutable execution result. Hosts/tests consume this; the
+/// executor never calls presentation.
+///
+internal readonly record struct RuntimeInitialCreateExecutionReceipt(
+ RuntimeEntityKey Entity,
+ uint FullCellId,
+ RuntimeTeleportHookPhase TeleportHookPhase,
+ ImmutableArray Trace,
+ int ReplayedDeferredChildCount);
+
+///
+/// Applies one entity's completed initial-Create residence: adopts the
+/// initial placement exactly once, emits the AfterEnterWorld teleport-hook
+/// request, replays raw missing-parent child Creates in FIFO order, drains
+/// every retained continuation strictly by sequence (classifying Position
+/// continuations at execution time against LIVE inputs), and releases the
+/// residence once the drained prefix matches the lease's current length.
+/// Execute is synchronous and retry-idempotent: a caller re-invokes it
+/// after or
+///
+/// once the placement token in the returned trace has been prepared,
+/// submitted, and acknowledged by whatever drives
+/// (a test harness today; a host at
+/// cutover). This type never references App/UI/Silk.NET/OpenGL/OpenAL/
+/// Headless and is reached only by tests in this slice - no production
+/// caller exists yet.
+///
+internal sealed class RuntimeInitialCreateContinuationExecutor
+{
+ private enum InitialTailPhase : byte
+ {
+ NotStarted,
+ Adopted,
+ HookRecorded,
+ DeferredReplayed,
+ /// Round 5 R5-1: the accepted-relation queue for this guid has been drained.
+ RelationsReplayed,
+ }
+
+ private readonly record struct PendingPublish(
+ RuntimeEntityChange Change,
+ Func Matches,
+ RuntimePlacementCancellationReceipt Cancellation);
+
+ private sealed class Progress
+ {
+ internal required ulong LeaseId { get; init; }
+ internal ulong AppliedThroughSequence { get; set; }
+ internal InitialTailPhase TailPhase { get; set; }
+ internal int EnvelopeStageIndex { get; set; } = -1;
+ internal RuntimeEntityPlacementToken PendingContinuationPlacement { get; set; }
+ internal ulong PendingContinuationSequence { get; set; }
+ internal RuntimeAuthoritativePositionRoute PendingContinuationRoute { get; set; }
+ ///
+ /// Round 3 B1: true once a Position action's merge+publish has
+ /// committed but TryBeginExclusiveAuthoredPlacement failed on
+ /// transient operation-slot contention (another operation currently
+ /// owns this entity's SetPosition slot) rather than genuine
+ /// staleness. While true, a re-entry into ApplyPositionAction
+ /// for the SAME continuation/stage skips the merge/publish entirely
+ /// and retries only the placement begin - closing the "duplicate
+ /// publish on every retry" hole a naive full re-apply would open.
+ ///
+ internal bool PositionMergeCommittedForRetry { get; set; }
+ internal ulong PositionMergeCommittedVersion { get; set; }
+ internal int ReplayedDeferredChildCount { get; set; }
+ internal List EnvelopeBuffer { get; } = [];
+ internal ImmutableArray.Builder Trace { get; } =
+ ImmutableArray.CreateBuilder();
+ }
+
+ private readonly RuntimeEntityDirectory _entities;
+ private readonly RuntimeInitialCreateResidenceState _residences;
+ private readonly RuntimePhysicsState _physics;
+ private readonly RuntimeEntityObjectEventStream _events;
+ private readonly Func
+ _registerDeferredChild;
+ ///
+ /// Round 3 B12: mirrors
+ /// for the
+ /// residence path's WeenieDescription tail action. The executor holds no
+ /// direct reference to RuntimeEntityObjectLifetime (it is
+ /// constructed BY that owner) or its ClientObjectTable, so the
+ /// lifetime binds this delegate at construction the same way it binds
+ /// .
+ ///
+ private readonly Func
+ _applyAcceptedSpawn;
+ private readonly Dictionary _progress = [];
+ private readonly HashSet _executing = [];
+ private Func? _generation;
+
+ internal RuntimeInitialCreateContinuationExecutor(
+ RuntimeEntityDirectory entities,
+ RuntimeInitialCreateResidenceState residences,
+ RuntimePhysicsState physics,
+ RuntimeEntityObjectEventStream events,
+ Func
+ registerDeferredChild,
+ Func
+ applyAcceptedSpawn)
+ {
+ _entities = entities ?? throw new ArgumentNullException(nameof(entities));
+ _residences = residences
+ ?? throw new ArgumentNullException(nameof(residences));
+ _physics = physics ?? throw new ArgumentNullException(nameof(physics));
+ _events = events ?? throw new ArgumentNullException(nameof(events));
+ _registerDeferredChild = registerDeferredChild
+ ?? throw new ArgumentNullException(nameof(registerDeferredChild));
+ _applyAcceptedSpawn = applyAcceptedSpawn
+ ?? throw new ArgumentNullException(nameof(applyAcceptedSpawn));
+ }
+
+ internal void BindGeneration(Func generation)
+ {
+ ArgumentNullException.ThrowIfNull(generation);
+ if (_generation is not null)
+ {
+ throw new InvalidOperationException(
+ "The initial-create continuation executor's generation source is already bound.");
+ }
+ _generation = generation;
+ }
+
+ internal int ProgressCount => _progress.Count;
+
+ ///
+ /// Round 5 R5-3: mirrors the
+ /// DispatchFailureCount/LastDispatchFailure precedent for the deferred-
+ /// replay containment introduced by Round 4 R4-1 and extended by this
+ /// round's deferred-relation replay. A contained catch never silently
+ /// swallows - it increments this counter and records the exception,
+ /// then keeps draining the remaining entries.
+ ///
+ internal long ReplayFailureCount { get; private set; }
+ internal Exception? LastReplayFailure { get; private set; }
+
+ private void RecordReplayFailure(Exception error)
+ {
+ ReplayFailureCount++;
+ LastReplayFailure = error;
+ }
+
+ ///
+ /// Exposes the exact placement token a
+ ///
+ /// yield is waiting on, so a caller (a test harness today; a host at
+ /// cutover) can drive 's ordinary
+ /// prepare/submit/acknowledge cycle on it, exactly like it already does
+ /// for the initial lease's own placement token.
+ ///
+ internal bool TryGetPendingContinuationPlacement(
+ RuntimeEntityKey key,
+ out RuntimeEntityPlacementToken placement)
+ {
+ if (_progress.TryGetValue(key, out Progress? progress)
+ && progress.PendingContinuationPlacement.IsValid)
+ {
+ placement = progress.PendingContinuationPlacement;
+ return true;
+ }
+ placement = default;
+ return false;
+ }
+
+ ///
+ /// Exposes the exact classified route the pending continuation placement
+ /// is running, so a caller can drive
+ /// with the matching
+ /// /
+ /// - the same information
+ /// already exposes for the initial placement.
+ ///
+ internal bool TryGetPendingContinuationRoute(
+ RuntimeEntityKey key,
+ out RuntimeAuthoritativePositionRoute route)
+ {
+ if (_progress.TryGetValue(key, out Progress? progress)
+ && progress.PendingContinuationPlacement.IsValid)
+ {
+ route = progress.PendingContinuationRoute;
+ return true;
+ }
+ route = default;
+ return false;
+ }
+
+ ///
+ /// Deterministic cleanup hook wired into the SAME choke points that
+ /// forget a residence lease ().
+ /// A retired residence can never leave orphaned executor progress
+ /// behind. Also forgets any in-flight CONTINUATION placement token
+ /// (distinct from the residence's own initial-lease placement, which
+ /// ForgetInitialCreateResidence already forgets separately, and
+ /// distinct from the unconditional Physics.SetPosition.Forget
+ /// every existing ForgetInitialCreateResidence caller already
+ /// runs alongside it - which independently cancels whatever operation
+ /// currently exists for this key, continuation placement included).
+ /// This is defensive-in-depth: DiscardProgress owns cleanup of the
+ /// state IT introduces (PendingContinuationPlacement) rather than
+ /// relying on every current AND future caller pairing it with an
+ /// ordinary Forget of its own.
+ ///
+ internal void DiscardProgress(RuntimeEntityKey key)
+ {
+ if (!_progress.Remove(key, out Progress? progress))
+ return;
+ if (progress.PendingContinuationPlacement.IsValid)
+ {
+ RuntimePlacementCancellationReceipt cancellation =
+ _physics.SetPosition.ForgetExactPlacement(
+ progress.PendingContinuationPlacement);
+ _physics.SetPosition.PublishCancellation(cancellation);
+ }
+ }
+
+ ///
+ /// Deterministic bulk cleanup wired into
+ /// 's call site
+ /// (). Also
+ /// forgets every in-flight continuation placement token, defensively -
+ /// Physics.ResetSessionPhysics() runs immediately after this in
+ /// the same session-clear sequence and would otherwise be the only
+ /// thing to reap them.
+ ///
+ internal void DiscardAll()
+ {
+ foreach (Progress progress in _progress.Values)
+ {
+ if (!progress.PendingContinuationPlacement.IsValid)
+ continue;
+ RuntimePlacementCancellationReceipt cancellation =
+ _physics.SetPosition.ForgetExactPlacement(
+ progress.PendingContinuationPlacement);
+ _physics.SetPosition.PublishCancellation(cancellation);
+ }
+ _progress.Clear();
+ }
+
+ internal RuntimeInitialCreateExecutionStatus Execute(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ in RuntimeInitialCreateExecutionInputs inputs,
+ out RuntimeInitialCreateExecutionReceipt receipt)
+ {
+ ArgumentNullException.ThrowIfNull(canonical);
+ receipt = default;
+ if (!token.IsValid || canonical.Key is not { } key)
+ return RuntimeInitialCreateExecutionStatus.RejectedToken;
+
+ // A reentrant Execute for the SAME entity while one is already on the
+ // stack (e.g. a synchronous event observer re-entering) fails closed
+ // rather than interleaving two drains of the same FIFO.
+ if (!_executing.Add(key))
+ return RuntimeInitialCreateExecutionStatus.RejectedAuthority;
+
+ try
+ {
+ return ExecuteCore(canonical, token, inputs, key, out receipt);
+ }
+ finally
+ {
+ _executing.Remove(key);
+ }
+ }
+
+ private RuntimeInitialCreateExecutionStatus ExecuteCore(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ in RuntimeInitialCreateExecutionInputs inputs,
+ RuntimeEntityKey key,
+ out RuntimeInitialCreateExecutionReceipt receipt)
+ {
+ receipt = default;
+
+ // An existing Progress for a DIFFERENT (older or ABA-reused) lease
+ // id is discarded here, and THIS exact call fails closed - an old
+ // incarnation's progress can never leak into a reused GUID/key. A
+ // retry with no prior progress starts fresh and succeeds normally.
+ if (_progress.TryGetValue(key, out Progress? existing)
+ && existing.LeaseId != token.LeaseId)
+ {
+ DiscardProgress(key);
+ return RuntimeInitialCreateExecutionStatus.RejectedAuthority;
+ }
+
+ // Round 3 B11: a FRESH Progress for the CURRENT lease id is never
+ // materialized here - only lazily below, once Complete() actually
+ // reports Completed. A PendingPlacement/RejectedToken/RejectedAuthority
+ // outcome on THIS call must leave the ownership ledger (ProgressCount)
+ // untouched when nothing was ever tracked before - it should reflect
+ // drain work actually in flight, not a placeholder for a residence
+ // that has not even resolved yet.
+ Progress? progress = existing;
+
+ // Resume a placement that a PREVIOUS Execute call began and yielded
+ // on, before doing anything else. This can belong either to a
+ // standalone Position continuation or to a Position stage inside a
+ // SameIncarnationCreate envelope; ApplyContinuation/ApplyEnvelope
+ // both check PendingContinuationPlacement first for exactly this
+ // reason.
+ while (true)
+ {
+ // The ONLY legitimate window where one of the four baseline
+ // fields can move BETWEEN Execute calls without the executor's
+ // own synchronous code running is a pending continuation
+ // placement's host-driven prepare/submit/acknowledge cycle
+ // (RuntimeSetPositionState's own commit machinery advances
+ // FullCellId/PlacementCommitVersion there). Re-sync the
+ // baseline ONLY when that exact window was left open by a
+ // PREVIOUS call - never unconditionally, or every call with
+ // nothing in flight would bless an external race on these
+ // fields before Complete() ever gets a chance to see it. Safe
+ // even when the placement was displaced/cancelled instead of
+ // committed: ResumePendingPlacement below independently
+ // re-derives that outcome from
+ // IsPlacementCurrent/TryPeekAcknowledgedPlacement, not from
+ // these four fields.
+ if (progress is not null && progress.PendingContinuationPlacement.IsValid)
+ {
+ // Round 4 R4-4: only FullCellId/PlacementCommitVersion can
+ // legitimately move in this exact window (RuntimeSetPositionState's
+ // own commit machinery, not the executor) - PositionAuthorityVersion/
+ // CreateIntegrationVersion moving here would be a genuine
+ // external race Complete() must still catch.
+ _residences.AdvanceExecutorBaseline(
+ canonical,
+ token,
+ RuntimeExecutorBaselineFields.FullCellId
+ | RuntimeExecutorBaselineFields.PlacementCommitVersion);
+ }
+ RuntimeInitialCreateResidenceCompletionStatus completion =
+ _residences.Complete(canonical, token, out RuntimeInitialCreateResidenceReceipt residenceReceipt);
+ switch (completion)
+ {
+ case RuntimeInitialCreateResidenceCompletionStatus.PendingPlacement:
+ // Nothing has been drained yet for this exact lease -
+ // leave _progress exactly as found (untouched if it
+ // never existed).
+ return RuntimeInitialCreateExecutionStatus.PendingPlacement;
+ case RuntimeInitialCreateResidenceCompletionStatus.RejectedToken:
+ DiscardProgress(key);
+ return RuntimeInitialCreateExecutionStatus.RejectedToken;
+ case RuntimeInitialCreateResidenceCompletionStatus.RejectedAuthority:
+ DiscardProgress(key);
+ return RuntimeInitialCreateExecutionStatus.RejectedAuthority;
+ }
+
+ // Completed: a residence now exists to drain. Materialize
+ // Progress exactly once, lazily, only at this point.
+ if (progress is null)
+ {
+ progress = new Progress { LeaseId = token.LeaseId };
+ _progress[key] = progress;
+ }
+
+ if (progress.TailPhase != InitialTailPhase.DeferredReplayed)
+ {
+ RuntimeInitialCreateExecutionStatus tailStatus =
+ RunInitialTail(canonical, token, residenceReceipt, progress);
+ if (tailStatus != RuntimeInitialCreateExecutionStatus.Completed)
+ return Abandon(canonical, key);
+ }
+
+ while (progress.AppliedThroughSequence
+ < (ulong)residenceReceipt.Continuations.Length)
+ {
+ if (!_entities.IsCurrent(canonical) || canonical.Key != token.Entity)
+ return Abandon(canonical, key);
+
+ int index = (int)progress.AppliedThroughSequence;
+ RuntimeInitialCreateResidenceContinuation continuation =
+ residenceReceipt.Continuations[index];
+ if (continuation.InstanceSequence != canonical.Incarnation)
+ return Abandon(canonical, key);
+
+ RuntimeInitialCreateExecutionStatus applyStatus =
+ ApplyContinuation(canonical, token, key, continuation, inputs, progress);
+ // Round 3 B2: every apply method below now rebaselines
+ // itself immediately after its own canonical mutation and
+ // BEFORE its own publish (mutate -> rebaseline -> publish),
+ // closing the reentrant-retirement window a synchronous
+ // Publish observer could otherwise see (the baseline would
+ // still show the PRE-mutation values while the observer
+ // reenters residence/executor state). No blanket
+ // re-synchronize belongs here anymore - each apply already
+ // guarantees its own baseline is current before ANY
+ // observer can run.
+ if (applyStatus
+ == RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement)
+ {
+ return applyStatus;
+ }
+ if (applyStatus != RuntimeInitialCreateExecutionStatus.Completed)
+ return applyStatus;
+
+ progress.AppliedThroughSequence = continuation.Sequence;
+ progress.EnvelopeStageIndex = -1;
+ }
+
+ RuntimeInitialCreateResidenceExecutorReleaseStatus release =
+ _residences.ConsumeExecuted(
+ canonical,
+ residenceReceipt.Adoption,
+ progress.AppliedThroughSequence);
+ switch (release)
+ {
+ case RuntimeInitialCreateResidenceExecutorReleaseStatus.Released:
+ receipt = new RuntimeInitialCreateExecutionReceipt(
+ key,
+ residenceReceipt.FullCellId,
+ residenceReceipt.TeleportHookPhase,
+ progress.Trace.ToImmutable(),
+ progress.ReplayedDeferredChildCount);
+ _progress.Remove(key);
+ return RuntimeInitialCreateExecutionStatus.Completed;
+ case RuntimeInitialCreateResidenceExecutorReleaseStatus.Revised:
+ // A new continuation arrived mid-drain (Enqueue bumps the
+ // completed entry's Adoption.Revision in place). Re-fetch
+ // via Complete and drain only the newly-appended tail -
+ // AppliedThroughSequence already reflects everything this
+ // progress has committed, so the outer while(true) loop's
+ // inner drain loop naturally continues from there.
+ continue;
+ default:
+ return Abandon(canonical, key);
+ }
+ }
+ }
+
+ ///
+ /// Round 3 B1: the ONE choke point every abandonment path routes
+ /// through. Retiring the RESIDENCE itself (not just this executor's own
+ /// progress) is essential here - a caller that only discarded progress
+ /// and returned RejectedAuthority would leave the residence's own
+ /// completed entry sitting there fully current; the NEXT Execute call
+ /// for the same key would re-fetch it via Complete(), start a FRESH
+ /// Progress at sequence zero, and REPLAY every continuation already
+ /// committed to the canonical snapshot in this attempt.
+ /// 's own
+ /// retirement notification (bound at
+ /// construction) already
+ /// routes back to for a successful Forget;
+ /// the explicit call here is the same idempotent defense-in-depth every
+ /// other DiscardProgress caller uses, covering the case where Forget
+ /// finds no matching residence at all (nothing left to retire, but this
+ /// key's own progress must still go).
+ ///
+ private RuntimeInitialCreateExecutionStatus Abandon(
+ RuntimeEntityRecord canonical,
+ RuntimeEntityKey key)
+ {
+ if (_residences.Forget(
+ canonical,
+ out _,
+ out RuntimePlacementCancellationReceipt cancellation))
+ {
+ _physics.SetPosition.PublishCancellation(cancellation);
+ }
+ DiscardProgress(key);
+ return RuntimeInitialCreateExecutionStatus.RejectedAuthority;
+ }
+
+ private RuntimeInitialCreateExecutionStatus RunInitialTail(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ in RuntimeInitialCreateResidenceReceipt residenceReceipt,
+ Progress progress)
+ {
+ if (progress.TailPhase == InitialTailPhase.NotStarted)
+ {
+ // Resolves the runtime-surface.md 3.1 deadlock: BeginAcceptedPlacementCore
+ // (every placement-begin entry point) rejects while HasRetainedCompletion
+ // is true for this key. Consuming the initial placement's
+ // acknowledged completion here - exactly once, guarded by
+ // PlacementAdopted - is what lets a later Position continuation
+ // begin its OWN authored placement for the same key.
+ if (!_residences.AdoptCompletedPlacement(canonical, token))
+ return RuntimeInitialCreateExecutionStatus.RejectedAuthority;
+ progress.Trace.Add(new RuntimeInitialCreateExecutedAction(
+ RuntimeInitialCreateExecutedActionKind.InitialAdoption,
+ 0UL,
+ -1,
+ null,
+ RuntimeTeleportHookPhase.None));
+ progress.TailPhase = InitialTailPhase.Adopted;
+ }
+
+ if (progress.TailPhase == InitialTailPhase.Adopted)
+ {
+ // Retail: SmartBox new-object player branch, init_player /
+ // PlayerPositionUpdated (function 1 in retail-notes.md,
+ // SmartBox::HandleCreateObject 0x00454c80). The hook REQUEST is
+ // the Runtime-side fact; a host runs the actual after-enter
+ // teleport suffix at cutover.
+ if (residenceReceipt.TeleportHookPhase
+ == RuntimeTeleportHookPhase.AfterEnterWorld)
+ {
+ progress.Trace.Add(new RuntimeInitialCreateExecutedAction(
+ RuntimeInitialCreateExecutedActionKind.TeleportHookRequest,
+ 0UL,
+ -1,
+ null,
+ RuntimeTeleportHookPhase.AfterEnterWorld));
+ }
+ progress.TailPhase = InitialTailPhase.HookRecorded;
+ }
+
+ if (progress.TailPhase == InitialTailPhase.HookRecorded)
+ {
+ if (!ReplayDeferredChildren(canonical, progress))
+ return RuntimeInitialCreateExecutionStatus.RejectedAuthority;
+ progress.TailPhase = InitialTailPhase.DeferredReplayed;
+ }
+
+ if (progress.TailPhase == InitialTailPhase.DeferredReplayed)
+ {
+ // Round 5 R5-1: drains the accepted-relation queue keyed to
+ // THIS guid AFTER the raw-Create replay above - wire-arrival
+ // order means any relation waiting on this exact guid was
+ // queued no earlier than the raw children were (retail's
+ // ProcessObjectNetBlobs replays both classes of blob from the
+ // SAME per-guid bucket; our queues are split by shape but
+ // drained in the same relative order).
+ if (!ReplayDeferredAcceptedRelations(canonical, progress))
+ return RuntimeInitialCreateExecutionStatus.RejectedAuthority;
+ progress.TailPhase = InitialTailPhase.RelationsReplayed;
+ }
+
+ return RuntimeInitialCreateExecutionStatus.Completed;
+ }
+
+ ///
+ /// Retail: SmartBox::ProcessObjectNetBlobs 0x00454b20, called at the tail
+ /// of HandleCreateObject's new-object path for the object that was just
+ /// created - a parent's own successful Create replays every blob queued
+ /// waiting on ITS guid, synchronously, in the same call stack, in FIFO
+ /// order. Wire-arrival order means this replay runs BEFORE the FIFO
+ /// drain below: children were queued before any continuation targeting
+ /// this entity itself could exist.
+ ///
+ /// Round 3 B7: retail detaches the ENTIRE queued netblob list for one
+ /// parent atomically before dispatching any of it (pseudo-C ~93617) -
+ /// "detach" IS retail's "consume"; there is no separate peek-then-remove
+ /// step. This replaces the previous peek/consume loop, which needed a
+ /// stale-AdmissionId escape hatch for a race that an atomic detach makes
+ /// structurally impossible: a NEW Create arriving for this same parent
+ /// during replay enqueues into a brand-new queue instance, since
+ /// DetachDeferredCreates already removed the old one from the
+ /// dictionary before this loop starts.
+ ///
+ /// Round 4 R4-1: two containment gaps closed. (1) one child's
+ /// registration THROWING no longer escapes Execute or strands
+ /// the remaining siblings - each registration runs inside a try/catch,
+ /// recording a
+ /// outcome and continuing with the next entry on an exception. (2) a
+ /// mid-loop abandonment (this entity no longer current - e.g. a
+ /// reentrant delete/reset fired synchronously from an earlier sibling's
+ /// own registration callback) restores the UNPROCESSED remainder into
+ /// - in original FIFO order, with
+ /// original AdmissionIds - rather than permanently destroying it.
+ /// Retail's own queued blobs live on CObjectMaint (per-GUID), not
+ /// on the object instance being replayed, so they survive the object
+ /// and replay again against a recreated GUID; our GUID-keyed
+ /// persistence already pins this, RestoreDeferredCreates just makes an
+ /// abandoned-mid-replay attempt honor it too.
+ ///
+ private bool ReplayDeferredChildren(RuntimeEntityRecord canonical, Progress progress)
+ {
+ if (!_entities.IsCurrent(canonical))
+ return false;
+
+ ImmutableArray detached =
+ _entities.ParentAttachments.DetachDeferredCreates(
+ canonical.ServerGuid, out DeferredReplayWindowToken window);
+ for (int index = 0; index < detached.Length; index++)
+ {
+ if (!_entities.IsCurrent(canonical))
+ {
+ _entities.ParentAttachments.RestoreDeferredCreates(
+ window,
+ detached.AsSpan()[index..]);
+ return false;
+ }
+
+ DeferredParentCreate deferred = detached[index];
+ RuntimeDeferredChildReplayOutcome outcome;
+ try
+ {
+ RuntimeEntityRegistrationResult result =
+ _registerDeferredChild(deferred.Spawn, deferred.IsLocalPlayer);
+ outcome = result.Canonical is not null
+ ? RuntimeDeferredChildReplayOutcome.Registered
+ : result.DeferredForParent
+ ? RuntimeDeferredChildReplayOutcome.ReDeferred
+ : RuntimeDeferredChildReplayOutcome.Rejected;
+ }
+ catch (Exception error)
+ {
+ // Round 4 R4-1: contain the exception here - one bad child
+ // must not strand the remaining siblings or escape Execute
+ // as a typed status. Round 5 R5-3: record it on the
+ // observable failure surface rather than swallowing it.
+ RecordReplayFailure(error);
+ outcome = RuntimeDeferredChildReplayOutcome.Rejected;
+ }
+ progress.Trace.Add(new RuntimeInitialCreateExecutedAction(
+ RuntimeInitialCreateExecutedActionKind.DeferredChildReplay,
+ 0UL,
+ -1,
+ null,
+ RuntimeTeleportHookPhase.None,
+ outcome));
+ progress.ReplayedDeferredChildCount++;
+ }
+ // Nothing left to restore on a full pass - releases the window.
+ _entities.ParentAttachments.RestoreDeferredCreates(
+ window, ReadOnlySpan.Empty);
+ return true;
+ }
+
+ ///
+ /// Round 5 R5-1: drains the accepted-relation queue keyed to this exact
+ /// guid, replaying every relation a standalone Parent continuation or
+ /// envelope CreateParent stage deferred because this parent was
+ /// unaddressable or named a not-yet-arrived incarnation. Mirrors
+ /// 's detach-first / cancellation-
+ /// aware-window / contained-failure shape exactly - see that method's
+ /// remarks for the retail citations and the R4-1/R5-2 rationale, which
+ /// apply identically here.
+ ///
+ private bool ReplayDeferredAcceptedRelations(RuntimeEntityRecord canonical, Progress progress)
+ {
+ if (!_entities.IsCurrent(canonical))
+ return false;
+
+ ImmutableArray detached =
+ _entities.ParentAttachments.DetachDeferredAcceptedRelations(
+ canonical.ServerGuid, out DeferredReplayWindowToken window);
+ for (int index = 0; index < detached.Length; index++)
+ {
+ if (!_entities.IsCurrent(canonical))
+ {
+ _entities.ParentAttachments.RestoreDeferredAcceptedRelations(
+ window,
+ detached.AsSpan()[index..]);
+ return false;
+ }
+
+ DeferredAcceptedParentRelation entry = detached[index];
+ RuntimeParentRelationOutcome outcome;
+ try
+ {
+ outcome = ApplyReplayedParentRelation(canonical, entry);
+ }
+ catch (Exception error)
+ {
+ RecordReplayFailure(error);
+ outcome = RuntimeParentRelationOutcome.Rejected;
+ }
+ progress.Trace.Add(new RuntimeInitialCreateExecutedAction(
+ RuntimeInitialCreateExecutedActionKind.ParentRelationReplay,
+ 0UL,
+ -1,
+ null,
+ RuntimeTeleportHookPhase.None,
+ null,
+ null,
+ RuntimePositionConstrainPhase.None,
+ false,
+ false,
+ false,
+ false,
+ false,
+ outcome));
+ if (outcome == RuntimeParentRelationOutcome.DeferredAwaitingParent)
+ {
+ // Relation still names an incarnation that has not arrived
+ // yet - wait for the NEXT one. Re-enqueues into a BRAND NEW
+ // queue instance (the whole bucket was already detached
+ // above), so this same detach loop never re-observes it.
+ _entities.ParentAttachments.EnqueueDeferredAcceptedRelation(entry);
+ }
+ }
+ _entities.ParentAttachments.RestoreDeferredAcceptedRelations(
+ window, ReadOnlySpan.Empty);
+ return true;
+ }
+
+ ///
+ /// Round 5 R5-1: incarnation dispatch vs THIS parent (),
+ /// mirroring 's own rules -
+ /// equal incarnation (or the envelope flavor, which has none to compare)
+ /// commits the attach; THIS parent newer than the relation discards it
+ /// (stale); the relation newer than THIS parent re-enqueues (wait for
+ /// the next incarnation - handled by the caller). The merge already
+ /// committed at the relation's ORIGINAL drain (its own position-
+ /// timestamp-only stamp); replay commits ONLY the attach tail, never
+ /// re-runs it.
+ ///
+ private RuntimeParentRelationOutcome ApplyReplayedParentRelation(
+ RuntimeEntityRecord parent,
+ in DeferredAcceptedParentRelation entry)
+ {
+ if (!_entities.TryGetActive(entry.ChildGuid, out RuntimeEntityRecord child)
+ || child.Key != entry.ChildKey)
+ {
+ return RuntimeParentRelationOutcome.Rejected;
+ }
+
+ if (entry.ParentInstanceSequence is { } relationParentInstance
+ && parent.Incarnation != relationParentInstance)
+ {
+ return PhysicsTimestampGate.IsNewer(relationParentInstance, parent.Incarnation)
+ ? RuntimeParentRelationOutcome.DiscardedStaleParent
+ : RuntimeParentRelationOutcome.DeferredAwaitingParent;
+ }
+
+ // Round 5 R5-3 note: the child's OWN residence token (if its
+ // initial-tail is somehow still open at this exact moment) is not
+ // held here - only the parent's is in scope. AdvanceExecutorBaseline
+ // is deliberately SKIPPED (rebaseline: false) rather than guessed at;
+ // if the child's own residence is still active, its own next
+ // Complete() call will correctly observe this PositionAuthorityVersion
+ // bump as an external race and fail closed - the safe direction -
+ // rather than this call silently blessing a baseline it does not
+ // own.
+ CommitParentAttachment(child, default, rebaseline: false, buffer: null);
+ return RuntimeParentRelationOutcome.Applied;
+ }
+
+ private RuntimeInitialCreateExecutionStatus ApplyContinuation(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ RuntimeEntityKey key,
+ in RuntimeInitialCreateResidenceContinuation continuation,
+ in RuntimeInitialCreateExecutionInputs inputs,
+ Progress progress)
+ {
+ if (continuation.Kind
+ == RuntimeInitialCreateContinuationKind.SameIncarnationCreate)
+ {
+ return ApplyEnvelope(canonical, token, key, continuation, inputs, progress);
+ }
+
+ if (progress.PendingContinuationPlacement.IsValid)
+ {
+ RuntimeInitialCreateExecutionStatus resumeStatus =
+ ResumePendingPlacement(canonical, key, progress, out RuntimeAuthoritativePositionRoute route);
+ if (resumeStatus != RuntimeInitialCreateExecutionStatus.Completed)
+ return resumeStatus;
+ progress.Trace.Add(BuildPositionTrace(continuation.Sequence, -1, route));
+ return RuntimeInitialCreateExecutionStatus.Completed;
+ }
+
+ RuntimeInitialCreateTailAction action = continuation.Actions[0];
+ switch (continuation.Kind)
+ {
+ case RuntimeInitialCreateContinuationKind.ObjDesc:
+ if (!ApplyObjDescAction(canonical, token, action, null))
+ return Abandon(canonical, key);
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.ObjDesc,
+ continuation.Sequence));
+ return RuntimeInitialCreateExecutionStatus.Completed;
+ case RuntimeInitialCreateContinuationKind.Parent:
+ return ApplyParentContinuation(canonical, token, key, continuation, action, progress);
+ case RuntimeInitialCreateContinuationKind.Pickup:
+ if (!ApplyPickupAction(canonical, token, action, null))
+ return Abandon(canonical, key);
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.Pickup,
+ continuation.Sequence));
+ return RuntimeInitialCreateExecutionStatus.Completed;
+ case RuntimeInitialCreateContinuationKind.Movement:
+ if (!ApplyMovementAction(canonical, token, action, null))
+ return Abandon(canonical, key);
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.Movement,
+ continuation.Sequence));
+ return RuntimeInitialCreateExecutionStatus.Completed;
+ case RuntimeInitialCreateContinuationKind.State:
+ if (!ApplyStateAction(canonical, token, action, null))
+ return Abandon(canonical, key);
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.State,
+ continuation.Sequence));
+ return RuntimeInitialCreateExecutionStatus.Completed;
+ case RuntimeInitialCreateContinuationKind.Vector:
+ if (!ApplyVectorAction(canonical, token, action, null))
+ return Abandon(canonical, key);
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.Vector,
+ continuation.Sequence));
+ return RuntimeInitialCreateExecutionStatus.Completed;
+ case RuntimeInitialCreateContinuationKind.Position:
+ return ApplyPositionAction(
+ canonical,
+ token,
+ key,
+ continuation.Sequence,
+ -1,
+ action,
+ inputs,
+ progress,
+ null);
+ default:
+ throw new InvalidOperationException(
+ $"Unsupported initial-Create continuation kind {continuation.Kind}.");
+ }
+ }
+
+ ///
+ /// Round 3 B9 revalidated the standalone Parent continuation's parent
+ /// incarnation at EXECUTION time. Round 4 R4-5 replaced admission's
+ /// dead-letter re-Enqueue with a DISCARD; Round 5 R5-1 OVERTURNS that
+ /// discard with hard retail evidence: a missing/stale parent QUEUES the
+ /// raw blob under the PARENT's guid (standalone parent handler
+ /// 0x004535D0 -> QueueBlobForObject, pseudo-C 92326; GUID-keyed
+ /// placeholder bucket in CObjectMaint, 271082-271088) and replays
+ /// it via ProcessObjectNetBlobs when that guid is created - retail
+ /// NEVER discards on this path; its only check is pointer addressability
+ /// (92312). The already-accepted position-timestamp merge still runs
+ /// exactly once here (gate/snapshot lockstep preserved); the dispatch
+ /// that follows mirrors 's
+ /// OWN established staleness rules verbatim: unaddressable parent or a
+ /// relation naming a not-yet-arrived incarnation both ENQUEUE (wait);
+ /// only a relation whose named incarnation the LIVE parent has already
+ /// superseded is discarded.
+ /// drains the queue this enqueues into, in the target parent's own
+ /// initial tail, after its raw-Create replay.
+ ///
+ private RuntimeInitialCreateExecutionStatus ApplyParentContinuation(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ RuntimeEntityKey key,
+ in RuntimeInitialCreateResidenceContinuation continuation,
+ RuntimeInitialCreateTailAction action,
+ Progress progress)
+ {
+ ParentEvent.Parsed parentUpdate = action.Parent!.Value;
+ if (!ApplyParentPositionTimestampOnly(canonical, parentUpdate))
+ return Abandon(canonical, key);
+
+ RuntimeParentRelationOutcome outcome;
+ if (!_entities.TryGetActive(parentUpdate.ParentGuid, out RuntimeEntityRecord parent))
+ {
+ _entities.ParentAttachments.EnqueueDeferredAcceptedRelation(
+ canonical.ServerGuid, key, parentUpdate, null, action.AcceptedTimestamps);
+ outcome = RuntimeParentRelationOutcome.DeferredAwaitingParent;
+ }
+ else if (parent.Incarnation != parentUpdate.ParentInstanceSequence)
+ {
+ if (PhysicsTimestampGate.IsNewer(parentUpdate.ParentInstanceSequence, parent.Incarnation))
+ {
+ // Live parent is NEWER than the relation's named incarnation
+ // - stale, discard (Resolve's own discard branch).
+ outcome = RuntimeParentRelationOutcome.DiscardedStaleParent;
+ }
+ else
+ {
+ // Relation names a FUTURE incarnation - wait for it.
+ _entities.ParentAttachments.EnqueueDeferredAcceptedRelation(
+ canonical.ServerGuid, key, parentUpdate, null, action.AcceptedTimestamps);
+ outcome = RuntimeParentRelationOutcome.DeferredAwaitingParent;
+ }
+ }
+ else
+ {
+ CommitParentAttachment(canonical, token, rebaseline: true, null);
+ outcome = RuntimeParentRelationOutcome.Applied;
+ }
+
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.Parent,
+ continuation.Sequence,
+ parentRelationOutcome: outcome));
+ return RuntimeInitialCreateExecutionStatus.Completed;
+ }
+
+ ///
+ /// The position-timestamp-only stamp that ALWAYS runs, exactly once,
+ /// when a standalone Parent continuation is first drained - regardless
+ /// of whether the dispatch that follows applies, defers, or discards
+ /// the relation.
+ /// is exactly retail's ApplyPositionTimestampOnly. None of the
+ /// four executor-tracked baseline fields move here (Round 4 R4-4), so
+ /// no AdvanceExecutorBaseline call belongs here either.
+ ///
+ private bool ApplyParentPositionTimestampOnly(
+ RuntimeEntityRecord canonical,
+ ParentEvent.Parsed update)
+ {
+ if (!_entities.ApplyAcceptedParentSnapshot(
+ canonical.ServerGuid,
+ update,
+ out WorldSession.EntitySpawn stamped))
+ {
+ return false;
+ }
+ _entities.RefreshSnapshot(canonical, stamped);
+ return true;
+ }
+
+ ///
+ /// The envelope's CreateParent stage revalidates parent ADDRESSABILITY
+ /// only - carries no
+ /// ParentInstanceSequence at all (retail-notes.md's
+ /// TryApplyCreateParent remarks: "unlike standalone ParentEvent
+ /// it carries no parent INSTANCE_TS"), so there is no incarnation to
+ /// compare - only whether the parent is addressable at all. Round 5
+ /// R5-1: an unaddressable parent now QUEUES (same retail-faithful
+ /// deferral as the standalone Parent continuation), not discards - the
+ /// merge already ran once, unconditionally, before this dispatch.
+ ///
+ private (bool Success, RuntimeParentRelationOutcome Outcome) ApplyCreateParentContinuation(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ RuntimeEntityKey key,
+ RuntimeInitialCreateTailAction action,
+ List? buffer)
+ {
+ CreateParentUpdate createParentUpdate = action.CreateParent!.Value;
+ if (!ApplyCreateParentPositionTimestampOnly(canonical, createParentUpdate))
+ return (false, default);
+
+ if (!_entities.TryGetActive(createParentUpdate.ParentGuid, out _))
+ {
+ _entities.ParentAttachments.EnqueueDeferredAcceptedRelation(
+ canonical.ServerGuid, key, null, createParentUpdate, action.AcceptedTimestamps);
+ return (true, RuntimeParentRelationOutcome.DeferredAwaitingParent);
+ }
+ CommitParentAttachment(canonical, token, rebaseline: true, buffer);
+ return (true, RuntimeParentRelationOutcome.Applied);
+ }
+
+ /// Instance-seam-only stamp - see 's remarks.
+ private bool ApplyCreateParentPositionTimestampOnly(
+ RuntimeEntityRecord canonical,
+ CreateParentUpdate update)
+ {
+ if (!_entities.ApplyAcceptedCreateParentSnapshot(
+ canonical.ServerGuid,
+ update,
+ out WorldSession.EntitySpawn stamped))
+ {
+ return false;
+ }
+ _entities.RefreshSnapshot(canonical, stamped);
+ return true;
+ }
+
+ private RuntimeInitialCreateExecutionStatus ApplyEnvelope(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ RuntimeEntityKey key,
+ in RuntimeInitialCreateResidenceContinuation continuation,
+ in RuntimeInitialCreateExecutionInputs inputs,
+ Progress progress)
+ {
+ int startStage = progress.EnvelopeStageIndex < 0 ? 0 : progress.EnvelopeStageIndex;
+
+ if (progress.PendingContinuationPlacement.IsValid)
+ {
+ RuntimeInitialCreateExecutionStatus resumeStatus =
+ ResumePendingPlacement(canonical, key, progress, out RuntimeAuthoritativePositionRoute route);
+ if (resumeStatus != RuntimeInitialCreateExecutionStatus.Completed)
+ return resumeStatus;
+ progress.Trace.Add(BuildPositionTrace(continuation.Sequence, startStage, route));
+ startStage++;
+ progress.EnvelopeStageIndex = startStage;
+ }
+
+ for (int i = startStage; i < continuation.Actions.Length; i++)
+ {
+ if (!_entities.IsCurrent(canonical))
+ return Abandon(canonical, key);
+
+ RuntimeInitialCreateTailAction action = continuation.Actions[i];
+ switch (action.Kind)
+ {
+ case RuntimeInitialCreateTailActionKind.PreTailDescriptionAdaptation:
+ // AP-119 compatibility: retail does NOT re-run
+ // set_description for an equal-generation Create tail.
+ // The retained PhysicsSpawnData is a presentation-side
+ // compat artifact only; no canonical mutation here.
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.PreTailDescriptionAdaptation,
+ continuation.Sequence,
+ i));
+ break;
+ case RuntimeInitialCreateTailActionKind.ObjDesc:
+ if (!ApplyObjDescAction(canonical, token, action, progress.EnvelopeBuffer))
+ return Abandon(canonical, key);
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.ObjDesc,
+ continuation.Sequence,
+ i));
+ break;
+ case RuntimeInitialCreateTailActionKind.CreateParent:
+ {
+ (bool success, RuntimeParentRelationOutcome outcome) =
+ ApplyCreateParentContinuation(canonical, token, key, action, progress.EnvelopeBuffer);
+ if (!success)
+ return Abandon(canonical, key);
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.CreateParent,
+ continuation.Sequence,
+ i,
+ parentRelationOutcome: outcome));
+ break;
+ }
+ case RuntimeInitialCreateTailActionKind.Pickup:
+ if (!ApplyPickupAction(canonical, token, action, progress.EnvelopeBuffer))
+ return Abandon(canonical, key);
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.Pickup,
+ continuation.Sequence,
+ i));
+ break;
+ case RuntimeInitialCreateTailActionKind.Position:
+ {
+ progress.EnvelopeStageIndex = i;
+ RuntimeInitialCreateExecutionStatus status = ApplyPositionAction(
+ canonical,
+ token,
+ key,
+ continuation.Sequence,
+ i,
+ action,
+ inputs,
+ progress,
+ progress.EnvelopeBuffer);
+ if (status
+ == RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement)
+ {
+ // A mid-envelope yield publishes NOTHING - the
+ // buffer accumulated so far stays on Progress and is
+ // flushed only once the whole envelope completes.
+ return status;
+ }
+ if (status != RuntimeInitialCreateExecutionStatus.Completed)
+ return status;
+ break;
+ }
+ case RuntimeInitialCreateTailActionKind.Movement:
+ if (!ApplyMovementAction(canonical, token, action, progress.EnvelopeBuffer))
+ return Abandon(canonical, key);
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.Movement,
+ continuation.Sequence,
+ i));
+ break;
+ case RuntimeInitialCreateTailActionKind.State:
+ if (!ApplyStateAction(canonical, token, action, progress.EnvelopeBuffer))
+ return Abandon(canonical, key);
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.State,
+ continuation.Sequence,
+ i));
+ break;
+ case RuntimeInitialCreateTailActionKind.Vector:
+ if (!ApplyVectorAction(canonical, token, action, progress.EnvelopeBuffer))
+ return Abandon(canonical, key);
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.Vector,
+ continuation.Sequence,
+ i));
+ break;
+ case RuntimeInitialCreateTailActionKind.WeenieDescription:
+ if (!ApplyWeenieDescriptionAction(canonical, token, action, progress.EnvelopeBuffer))
+ return Abandon(canonical, key);
+ progress.Trace.Add(Simple(
+ RuntimeInitialCreateExecutedActionKind.WeenieDescription,
+ continuation.Sequence,
+ i));
+ break;
+ case RuntimeInitialCreateTailActionKind.ResidentCellCleanup:
+ {
+ RuntimeResidentCellCleanupDisposition? cleanupDisposition =
+ ApplyResidentCellCleanup(canonical);
+ if (cleanupDisposition is null)
+ {
+ // Round 3 B1: the fail-closed invariant violation
+ // (claimed+celless+not-deferred) is a typed
+ // abandonment, never a throw escaping Execute.
+ return Abandon(canonical, key);
+ }
+ progress.Trace.Add(new RuntimeInitialCreateExecutedAction(
+ RuntimeInitialCreateExecutedActionKind.ResidentCellCleanup,
+ continuation.Sequence,
+ i,
+ null,
+ RuntimeTeleportHookPhase.None,
+ null,
+ cleanupDisposition));
+ break;
+ }
+ default:
+ throw new InvalidOperationException(
+ $"Unsupported same-incarnation tail action {action.Kind}.");
+ }
+
+ // Round 3 B1: persist EnvelopeStageIndex after EVERY committed
+ // stage, not only Position. Without this, a retry after an
+ // unexpected mid-envelope failure (or any future non-Position
+ // yield point) would resume from a stale index and REPLAY
+ // stages already committed to the canonical snapshot -
+ // Position's own yield/resume already tracks this correctly;
+ // this makes every other stage kind do the same.
+ progress.EnvelopeStageIndex = i + 1;
+ }
+
+ // Retail's tail is one synchronous critical section; no observer
+ // boundary between stages. Publish every buffered per-stage event
+ // consecutively, in stage order, only now that every stage committed.
+ foreach (PendingPublish pending in progress.EnvelopeBuffer)
+ PublishNow(canonical, pending.Change, pending.Matches, pending.Cancellation);
+ progress.EnvelopeBuffer.Clear();
+ progress.EnvelopeStageIndex = -1;
+ return RuntimeInitialCreateExecutionStatus.Completed;
+ }
+
+ ///
+ /// Round 3 B4: matches
+ /// 's FULL
+ /// record/projection agreement rather than a subset of it - a
+ /// continuation's own authored placement deserves the same staleness
+ /// rigor as the initial lease's placement. Beyond the projection's own
+ /// reported facts, this also re-checks the LIVE canonical record's
+ /// PositionAuthorityVersion (has something ELSE moved the record since
+ /// this exact placement began?) and FullCellId/PlacementCommitVersion
+ /// (does the projection's committed cell/version still match reality?).
+ ///
+ private RuntimeInitialCreateExecutionStatus ResumePendingPlacement(
+ RuntimeEntityRecord canonical,
+ RuntimeEntityKey key,
+ Progress progress,
+ out RuntimeAuthoritativePositionRoute route)
+ {
+ route = progress.PendingContinuationRoute;
+ RuntimeEntityPlacementToken placementToken = progress.PendingContinuationPlacement;
+ if (_physics.SetPosition.IsPlacementCurrent(placementToken))
+ return RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement;
+
+ if (!_physics.SetPosition.TryPeekAcknowledgedPlacement(
+ placementToken,
+ out RuntimePlacementProjectionToken projection)
+ || projection.Entity != placementToken.Entity
+ || projection.SessionLifetimeVersion != placementToken.SessionLifetimeVersion
+ || projection.PositionAuthorityVersion != placementToken.PositionAuthorityVersion
+ || canonical.PositionAuthorityVersion != placementToken.PositionAuthorityVersion
+ || projection.ExactCellId == 0u
+ || projection.ExactCellId != canonical.FullCellId
+ || projection.PlacementCommitVersion != canonical.PlacementCommitVersion)
+ {
+ // Round 4 R4-2: forget -> clear -> Abandon. Neither still in
+ // flight nor acknowledged with matching facts - cancelled or
+ // superseded by a newer authoritative operation. ForgetExactPlacement
+ // removes the retained _acknowledgedPlacementCompletions entry
+ // (via ForgetPlacementCompletionCore) even in this mismatch
+ // case - without it, HasRetainedCompletion for this key would
+ // stay true forever and block EVERY later placement begin (the
+ // runtime-surface.md 3.1 deadlock this executor exists to
+ // resolve). A newer owner now has the entity; abandon this
+ // execution.
+ RuntimePlacementCancellationReceipt forgotten =
+ _physics.SetPosition.ForgetExactPlacement(placementToken);
+ _physics.SetPosition.PublishCancellation(forgotten);
+ progress.PendingContinuationPlacement = default;
+ return Abandon(canonical, key);
+ }
+
+ if (!_physics.SetPosition.ConsumeAcknowledgedPlacement(placementToken, projection))
+ {
+ // Round 4 R4-2: same forget -> clear -> Abandon ordering - a
+ // concurrent consumer raced this exact acknowledgement away
+ // between TryPeek and here; still forget defensively so no
+ // stale watch/ack entry survives under this token.
+ RuntimePlacementCancellationReceipt forgotten =
+ _physics.SetPosition.ForgetExactPlacement(placementToken);
+ _physics.SetPosition.PublishCancellation(forgotten);
+ progress.PendingContinuationPlacement = default;
+ return Abandon(canonical, key);
+ }
+
+ progress.PendingContinuationPlacement = default;
+ progress.PendingContinuationSequence = 0UL;
+ return RuntimeInitialCreateExecutionStatus.Completed;
+ }
+
+ private RuntimeInitialCreateExecutionStatus ApplyPositionAction(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ RuntimeEntityKey key,
+ ulong sequence,
+ int stage,
+ RuntimeInitialCreateTailAction action,
+ in RuntimeInitialCreateExecutionInputs inputs,
+ Progress progress,
+ List? buffer)
+ {
+ if (!_residences.TryGetTransaction(canonical, out RuntimeInitialCreateResidenceLease lease)
+ || canonical.Key != key)
+ {
+ return Abandon(canonical, key);
+ }
+
+ WorldSession.EntityPositionUpdate update = action.Position!.Value;
+ RuntimePositionEntityKind entityKind = EntityKindOf(lease.Route.OperationKind);
+ bool isLocalPlayer = entityKind is RuntimePositionEntityKind.LocalPlayer;
+
+ RuntimeAuthoritativePositionRoute route;
+ if (progress.PositionMergeCommittedForRetry)
+ {
+ // Round 3 B1: a PREVIOUS attempt already merged and published
+ // this exact position continuation; TryBeginExclusiveAuthoredPlacement
+ // failed on transient operation-slot contention rather than
+ // staleness, and this re-entry retries ONLY the placement
+ // begin. Re-running the merge here would double-publish.
+ route = progress.PendingContinuationRoute;
+ }
+ else
+ {
+ var authority = new RuntimeAuthoritativePositionAuthority(
+ CurrentGeneration(),
+ key,
+ canonical.PositionAuthorityVersion,
+ update.PositionSequence,
+ action.PreviousTeleportSequence,
+ action.AcceptedTimestamps.Teleport,
+ action.PositionDisposition);
+
+ // Round 3 A3: contact comes SOLELY from the retained wire
+ // packet's own IsGrounded bit (PositionPack bit 0x4,
+ // server-asserted contact at admission time) - never a live
+ // body query, never an Inputs fallback.
+ bool hasContact = update.IsGrounded;
+ // Round 3 B5: HasAnimations is the SAME data-driven proxy for
+ // every position source, SameIncarnationCreate included - no
+ // PositionSource short-circuit. Round 4 R4-13: fall back to the
+ // nested PhysicsSpawnData's own MotionTableId when the
+ // top-level snapshot field is null (WeenieDescription/ObjDesc
+ // merges only ever populate one of the two, depending on
+ // which stage last touched appearance vs description).
+ bool hasAnimations = (canonical.Snapshot.MotionTableId
+ ?? canonical.Snapshot.Physics?.MotionTableId) is { } motionTableId
+ && motionTableId != 0u;
+
+ var request = new RuntimeAcceptedPositionRouteRequest(
+ authority,
+ entityKind,
+ action.PositionSource,
+ update.Position,
+ update.PlacementId,
+ update.Velocity,
+ canonical.FullCellId,
+ hasContact,
+ inputs.PlayerDistance,
+ inputs.UsePositionFromServer,
+ hasAnimations,
+ new RuntimePositionPlacementFacts(
+ canonical.FinalPhysicsState,
+ canonical.Snapshot.SetupTableId is not null));
+
+ route = RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition(request);
+
+ if (!route.Accepted)
+ {
+ // Round 3 B10: distinguish two retained-action shapes.
+ // When admission ITSELF already rejected (only
+ // FORCE_POSITION_TS could have moved), the ordinary Rejected
+ // merge is the correct stamp-only path. When admission
+ // ACCEPTED (Apply/ForcePosition - POSITION_TS/TELEPORT_TS/
+ // FORCE_POSITION_TS genuinely advanced) but EXECUTION-time
+ // classification now rejects, the snapshot must still
+ // reflect every channel the gate actually moved, not just
+ // ForcePosition.
+ bool stampedOk = action.PositionDisposition
+ is PositionTimestampDisposition.Rejected
+ ? _entities.ApplyAcceptedPositionSnapshot(
+ canonical.ServerGuid,
+ update,
+ PositionTimestampDisposition.Rejected,
+ action.AcceptedTimestamps,
+ isLocalPlayer,
+ null,
+ null,
+ installPlacementFrame: false,
+ clearParent: false,
+ out WorldSession.EntitySpawn stampedOnly)
+ : _entities.ApplyAcceptedPositionExecutionRejectedSnapshot(
+ canonical.ServerGuid,
+ update.PositionSequence,
+ action.AcceptedTimestamps,
+ out stampedOnly);
+ if (!stampedOk)
+ return Abandon(canonical, key);
+ _entities.RefreshSnapshot(canonical, stampedOnly);
+ progress.Trace.Add(BuildPositionTrace(sequence, stage, route));
+ return RuntimeInitialCreateExecutionStatus.Completed;
+ }
+
+ // CANONICAL CELL SEMANTICS (deliberate difference from the
+ // legacy direct-commit InboundPhysicsStateController.TryApplyPosition
+ // caller): refreshPosition stays false. A wire position never
+ // directly makes the record resident; only a Runtime SetPosition
+ // commit (below) or a simulation full-cell commit may change
+ // FullCellId. This matches retail (HandleReceivedPosition never
+ // sets a resident cell) and the classifier's own documented rule
+ // that a target frame with a nonzero cell does not make a
+ // cellless canonical body resident. The snapshot's Position
+ // field itself IS refreshed; only the derived FullCellId write
+ // is withheld.
+ //
+ // Round 3 B6: installPlacementFrame/clearParent come from the
+ // classified route's OWN ApplyPlacementFrameBeforeRouting/
+ // UnparentBeforeRouting flags, not the legacy path's
+ // unconditional true/true.
+ PhysicsBody? body = canonical.PhysicsBody;
+ bool mergedOk = _entities.ApplyAcceptedPositionSnapshot(
+ canonical.ServerGuid,
+ update,
+ action.PositionDisposition,
+ action.AcceptedTimestamps,
+ isLocalPlayer,
+ body?.Orientation,
+ body?.Velocity,
+ installPlacementFrame: route.ApplyPlacementFrameBeforeRouting,
+ clearParent: route.UnparentBeforeRouting,
+ out WorldSession.EntitySpawn merged);
+ if (!mergedOk)
+ return Abandon(canonical, key);
+ _entities.RefreshSnapshot(canonical, merged, refreshPosition: false);
+ _entities.AdvancePositionAuthority(canonical);
+ _entities.ParentAttachments.EndChildProjection(canonical.ServerGuid);
+ // Round 3 B2: mutate -> rebaseline -> publish. Rebaselining
+ // BEFORE Publish closes the reentrant-retirement window a
+ // synchronous observer could otherwise see (the baseline would
+ // still show pre-mutation values while the observer reenters
+ // residence/executor state). Round 4 R4-4: only
+ // PositionAuthorityVersion moved (AdvancePositionAuthority also
+ // bumps VelocityAuthorityVersion, which is not one of the four
+ // executor-tracked baseline fields).
+ _residences.AdvanceExecutorBaseline(
+ canonical, token, RuntimeExecutorBaselineFields.PositionAuthorityVersion);
+ ulong positionVersion = canonical.PositionAuthorityVersion;
+ ulong spatialVersion = canonical.SpatialAuthorityVersion;
+ Publish(
+ canonical,
+ RuntimeEntityChange.Updated,
+ () => canonical.PositionAuthorityVersion == positionVersion
+ && canonical.SpatialAuthorityVersion == spatialVersion,
+ default,
+ buffer);
+
+ if (!route.PerformsSetPosition)
+ {
+ // Interpolate / NoPositionOperation / AwaitFreshPosition:
+ // typed trace result only. Binding to the live
+ // interpolation owner is cutover work.
+ progress.Trace.Add(BuildPositionTrace(sequence, stage, route));
+ return RuntimeInitialCreateExecutionStatus.Completed;
+ }
+
+ progress.PositionMergeCommittedForRetry = true;
+ progress.PendingContinuationRoute = route;
+ progress.PositionMergeCommittedVersion = canonical.PositionAuthorityVersion;
+ }
+
+ RuntimeEntityPlacementToken placement = _physics.SetPosition
+ .TryBeginExclusiveAuthoredPlacement(
+ canonical,
+ canonical.PositionAuthorityVersion,
+ route.OperationKind);
+ if (!placement.IsValid)
+ {
+ // Round 3 B1: distinguish genuine staleness (abandon) from
+ // transient operation-slot contention (retry - the SAME merge
+ // stays committed; only the begin attempt repeats).
+ if (!_entities.IsCurrent(canonical)
+ || canonical.PositionAuthorityVersion != progress.PositionMergeCommittedVersion)
+ {
+ progress.PositionMergeCommittedForRetry = false;
+ return Abandon(canonical, key);
+ }
+ return RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement;
+ }
+ if (!_physics.SetPosition.WatchPlacementCompletion(placement))
+ {
+ _ = _physics.SetPosition.ForgetExactPlacement(placement);
+ progress.PositionMergeCommittedForRetry = false;
+ return Abandon(canonical, key);
+ }
+
+ progress.PositionMergeCommittedForRetry = false;
+ progress.PendingContinuationPlacement = placement;
+ progress.PendingContinuationSequence = sequence;
+ progress.PendingContinuationRoute = route;
+ return RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement;
+ }
+
+ private bool ApplyObjDescAction(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ RuntimeInitialCreateTailAction action,
+ List? buffer)
+ {
+ if (!_entities.ApplyAcceptedObjDescSnapshot(
+ canonical.ServerGuid,
+ action.ObjDesc!.Value,
+ out WorldSession.EntitySpawn merged))
+ {
+ return false;
+ }
+ _entities.RefreshSnapshot(canonical, merged);
+ _entities.AdvanceObjDescAuthority(canonical);
+ // Round 4 R4-4: ObjDescAuthorityVersion is not one of the four
+ // executor-tracked baseline fields (PositionAuthorityVersion/
+ // CreateIntegrationVersion/FullCellId/PlacementCommitVersion) - no
+ // AdvanceExecutorBaseline call belongs here at all; calling it
+ // unconditionally would silently bless an external race on those
+ // four fields that this apply never touched.
+ ulong version = canonical.ObjDescAuthorityVersion;
+ Publish(
+ canonical,
+ RuntimeEntityChange.Updated,
+ () => canonical.ObjDescAuthorityVersion == version,
+ default,
+ buffer);
+ return true;
+ }
+
+ ///
+ /// Round 5 R5-1: the SHARED attach-commit tail for BOTH the standalone
+ /// Parent continuation and the envelope CreateParent stage - the two
+ /// were byte-identical bodies before this round. The merge step that
+ /// precedes them (ApplyAcceptedParentSnapshot/
+ /// ApplyAcceptedCreateParentSnapshot, factored out into
+ /// /
+ /// ) runs EXACTLY
+ /// once at the relation's original drain and is deliberately
+ /// position-timestamp-only - it never sets ParentGuid/ParentLocation on
+ /// the snapshot. Per the test
+ /// StandaloneParentContinuationAppliesPositionTimestampOnlyMergeWithNoReDefer's
+ /// established, pre-Round-5 precedent (verified against it directly:
+ /// an earlier revision of this method wrongly called
+ /// here and broke
+ /// that test), the actual attach commit is out of scope for
+ /// this residence-continuation drain - it is the App-layer
+ /// EquippedChildRenderController's job, invoked through
+ /// only after
+ /// it validates the parent's render-side PartArray/holding-location can
+ /// actually host the child (see LiveEntityRuntime.CommitStagedParent's
+ /// remarks). This method therefore commits ONLY the residence tail
+ /// (AdvancePositionAuthority/LeaveWorld/Forget/rebaseline/publish) and is
+ /// payload-agnostic, so it serves the live-continuation apply AND
+ /// 's replayed apply
+ /// identically - "apply through the SAME parent-apply body used by a
+ /// live Parent continuation" (round5-fixes.md R5-1) means exactly this
+ /// tail, not a new attach step neither live nor replay ever performed.
+ /// Unlike the legacy CommitPositionChannelUpdate helper, this does NOT
+ /// call ForgetInitialCreateResidence - the executor IS the residence
+ /// owner mid-drain; forgetting it here would cancel our own in-progress
+ /// lease. Residence teardown is exclusively the adoption/release
+ /// machinery's job (RunInitialTail / ConsumeExecuted).
+ /// is false ONLY at replay time, when the
+ /// child's own residence token is not held here - see
+ /// 's remarks for why that is
+ /// safe (fails closed, never silently blesses a baseline it does not
+ /// own).
+ ///
+ private void CommitParentAttachment(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ bool rebaseline,
+ List? buffer)
+ {
+ _entities.AdvancePositionAuthority(canonical);
+ _physics.CollisionReports.LeaveWorld(canonical);
+ RuntimePlacementCancellationReceipt cancellation =
+ _physics.SetPosition.Forget(canonical);
+ // Round 3 B2: mutate -> rebaseline -> publish. Round 4 R4-4:
+ // AdvancePositionAuthority only moves PositionAuthorityVersion of
+ // the four tracked fields.
+ if (rebaseline)
+ {
+ _residences.AdvanceExecutorBaseline(
+ canonical, token, RuntimeExecutorBaselineFields.PositionAuthorityVersion);
+ }
+ ulong positionVersion = canonical.PositionAuthorityVersion;
+ ulong spatialVersion = canonical.SpatialAuthorityVersion;
+ Publish(
+ canonical,
+ RuntimeEntityChange.Updated,
+ () => canonical.PositionAuthorityVersion == positionVersion
+ && canonical.SpatialAuthorityVersion == spatialVersion,
+ cancellation,
+ buffer);
+ }
+
+ private bool ApplyPickupAction(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ RuntimeInitialCreateTailAction action,
+ List? buffer)
+ {
+ if (!_entities.ApplyAcceptedPickupSnapshot(
+ canonical.ServerGuid,
+ action.Pickup!.Value,
+ out WorldSession.EntitySpawn merged))
+ {
+ return false;
+ }
+ _entities.RefreshSnapshot(canonical, merged);
+ // Retail: the object entered world (this residence's initial tail),
+ // then was picked up - a FIFO entry always executes AFTER the
+ // initial placement committed. This is a leave-world edge, but it
+ // must not tear down the residence mid-drain: only ordinary
+ // SetPosition.Forget runs here, never ForgetInitialCreateResidence.
+ _entities.AdvancePositionAuthority(canonical);
+ _physics.CollisionReports.LeaveWorld(canonical);
+ RuntimePlacementCancellationReceipt cancellation =
+ _physics.SetPosition.Forget(canonical);
+ _entities.SuspendObjectClock(canonical);
+ _entities.SetFullCell(canonical, 0u, 0u);
+ _entities.ParentAttachments.EndChildProjection(canonical.ServerGuid);
+ // Round 3 B2: mutate -> rebaseline -> publish. Round 4 R4-4:
+ // AdvancePositionAuthority + SetFullCell(0,0) move
+ // PositionAuthorityVersion and FullCellId, the only two of the
+ // four tracked fields this apply touches.
+ _residences.AdvanceExecutorBaseline(
+ canonical,
+ token,
+ RuntimeExecutorBaselineFields.PositionAuthorityVersion
+ | RuntimeExecutorBaselineFields.FullCellId);
+ ulong positionVersion = canonical.PositionAuthorityVersion;
+ ulong spatialVersion = canonical.SpatialAuthorityVersion;
+ Publish(
+ canonical,
+ RuntimeEntityChange.Withdrawn,
+ () => canonical.PositionAuthorityVersion == positionVersion
+ && canonical.SpatialAuthorityVersion == spatialVersion,
+ cancellation,
+ buffer);
+ return true;
+ }
+
+ private bool ApplyMovementAction(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ RuntimeInitialCreateTailAction action,
+ List? buffer)
+ {
+ WorldSession.EntityMotionUpdate update = action.Movement!.Value;
+ // Safe to pass the retained wire's own MovementSequence directly
+ // here (unlike the legacy caller, which must read the live gate -
+ // see ApplyAcceptedMotion's remarks): a Movement continuation is
+ // only ever retained when AppliesMovementPayload || HasTimestampMutation,
+ // which structurally guarantees MOVEMENT_TS itself already advanced
+ // to this exact value at admission time.
+ if (!_entities.ApplyAcceptedMotionSnapshot(
+ canonical.ServerGuid,
+ update.MovementSequence,
+ action.AcceptedTimestamps.ServerControlledMove,
+ update,
+ retainPayload: false,
+ out WorldSession.EntitySpawn stamped))
+ {
+ return false;
+ }
+ _entities.RefreshSnapshot(canonical, stamped);
+ if (!action.AppliesMovementPayload)
+ {
+ // Timestamp-only entry: stamp landed above; no publish beyond
+ // that, matching legacy's own timestamp-only branch. Round 4
+ // R4-4: the stamp only moves MovementSequence/ServerControlSequence
+ // (nested Physics.Timestamps), never any of the four
+ // executor-tracked baseline fields - no AdvanceExecutorBaseline
+ // call here.
+ return true;
+ }
+
+ if (action.RetainMovementPayload)
+ {
+ if (!_entities.ApplyAcceptedMotionSnapshot(
+ canonical.ServerGuid,
+ update.MovementSequence,
+ action.AcceptedTimestamps.ServerControlledMove,
+ update,
+ retainPayload: true,
+ out WorldSession.EntitySpawn merged))
+ {
+ return false;
+ }
+ _entities.RefreshSnapshot(canonical, merged);
+ _entities.AdvanceMovementAuthority(canonical);
+ }
+ _entities.AdvanceMovementCommit(canonical);
+ // Round 4 R4-4: MovementAuthorityVersion/MovementCommitVersion are
+ // not among the four executor-tracked baseline fields - no
+ // AdvanceExecutorBaseline call belongs here.
+ ulong movementCommitVersion = canonical.MovementCommitVersion;
+ Publish(
+ canonical,
+ RuntimeEntityChange.Updated,
+ () => canonical.MovementCommitVersion == movementCommitVersion,
+ default,
+ buffer);
+ return true;
+ }
+
+ ///
+ /// Round 4 R4-11: the BecameHidden branch's currency-failure path
+ /// returns false (routes the caller to the shared
+ /// Abandon/RejectedAuthority), not true as an earlier
+ /// revision of this method did - the legacy equivalent reports failure
+ /// there too, and the record genuinely mutated under us mid-apply.
+ /// Not independently unit-tested with a live reentrancy seam: this
+ /// harness has no constructible way to make
+ /// RuntimeCollisionReportingState.LeaveWorld invoke an observer
+ /// callback for a residence-fresh entity - EndExpiredObjectCollisions
+ /// returns immediately whenever _owners has no established
+ /// collision record for this key (see
+ /// RuntimeCollisionReportingState.cs's own early-return guard),
+ /// which is always true for an entity that has never yet run a real
+ /// collision batch. Building a synthetic seam to force that callback
+ /// would be exactly the kind of workaround this project's CLAUDE.md
+ /// forbids; the fix is verified by direct code review of the
+ /// now-symmetric bool contract instead (every OTHER Apply*Action
+ /// method already returns false, never true, on its own currency
+ /// failure).
+ ///
+ private bool ApplyStateAction(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ RuntimeInitialCreateTailAction action,
+ List? buffer)
+ {
+ SetState.Parsed update = action.State!.Value;
+ if (!_entities.ApplyAcceptedStateSnapshot(
+ canonical.ServerGuid,
+ update,
+ out WorldSession.EntitySpawn merged))
+ {
+ return false;
+ }
+ _entities.RefreshSnapshot(canonical, merged);
+ 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)
+ {
+ // Round 4 R4-11: the record mutated out from under us mid-apply
+ // (LeaveWorld's own synchronous collision-report callbacks can
+ // reenter and either invalidate currency or bump
+ // PhysicsStateMutationVersion again) - that IS an external
+ // race, not a successfully-applied continuation. Return false
+ // so the caller routes through the shared Abandon
+ // (RejectedAuthority), matching the legacy equivalent's
+ // failure report instead of silently claiming success.
+ return false;
+ }
+ }
+ RetailPhysicsStateTransition transition =
+ _entities.ApplyRawPhysicsState(canonical, update.PhysicsState);
+ if (canonical.Key is { } key)
+ {
+ _physics.Engine.ShadowObjects.UpdatePhysicsState(
+ key.LocalEntityId,
+ (uint)canonical.FinalPhysicsState);
+ }
+ // Round 4 R4-4: StateAuthorityVersion/PhysicsStateMutationVersion
+ // are not among the four executor-tracked baseline fields - no
+ // AdvanceExecutorBaseline call belongs here.
+ ulong stateVersion = canonical.StateAuthorityVersion;
+ ulong physicsMutationVersion = canonical.PhysicsStateMutationVersion;
+ Publish(
+ canonical,
+ transition.HiddenTransition is RetailHiddenTransition.BecameHidden
+ ? RuntimeEntityChange.Hidden
+ : RuntimeEntityChange.Updated,
+ () => canonical.StateAuthorityVersion == stateVersion
+ && canonical.PhysicsStateMutationVersion == physicsMutationVersion,
+ default,
+ buffer);
+ return true;
+ }
+
+ private bool ApplyVectorAction(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ RuntimeInitialCreateTailAction action,
+ List? buffer)
+ {
+ if (!_entities.ApplyAcceptedVectorSnapshot(
+ canonical.ServerGuid,
+ action.Vector!.Value,
+ out WorldSession.EntitySpawn merged))
+ {
+ return false;
+ }
+ _entities.RefreshSnapshot(canonical, merged);
+ _entities.AdvanceVectorAuthority(canonical);
+ // Round 4 R4-4: VectorAuthorityVersion is not among the four
+ // executor-tracked baseline fields - no AdvanceExecutorBaseline
+ // call belongs here.
+ ulong version = canonical.VectorAuthorityVersion;
+ Publish(
+ canonical,
+ RuntimeEntityChange.Updated,
+ () => canonical.VectorAuthorityVersion == version,
+ default,
+ buffer);
+ return true;
+ }
+
+ private bool ApplyWeenieDescriptionAction(
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ RuntimeInitialCreateTailAction action,
+ List? buffer)
+ {
+ // Round 3 A2: this must NOT be a wholesale RefreshSnapshot of the
+ // raw retained packet - it merges exactly like every other
+ // same-generation Create (MergeUntimestampedCreate via the instance
+ // seam), keeping the retained Position/appearance/physics-timestamp
+ // fields earlier stages already committed to _snapshots.
+ if (!_entities.ApplyAcceptedWeenieDescriptionSnapshot(
+ canonical.ServerGuid,
+ action.WeenieDescription!.Value,
+ out WorldSession.EntitySpawn merged))
+ {
+ return false;
+ }
+ _entities.RefreshSnapshot(canonical, merged, refreshPosition: false);
+ // RuntimeEntityObjectLifetime.RegisterEntityCore's ExistingGeneration
+ // branch only calls Entities.AdvanceCreateAuthority when
+ // !beginInitialResidence - a residence-pending entity's admission
+ // deliberately skipped it. This deferred WeenieDescription tail
+ // action is where that authority mutation actually lands.
+ _entities.AdvanceCreateAuthority(canonical);
+ ulong createVersion = canonical.CreateIntegrationVersion;
+ // Round 3 B12: RuntimeLiveEntitySessionController.OnSpawned (the
+ // non-residence direct-host Create path) drives
+ // ApplyAcceptedSpawn(canonical, integrationVersion, canonical.Snapshot,
+ // replaceGeneration: NewGeneration) for EVERY accepted Create - the
+ // prior "zero callers" claim for this object-table wiring was false.
+ // This tail action is the residence path's exact counterpart: it
+ // only ever runs for an ExistingGeneration same-incarnation Create
+ // (a residence is admitted only when preview is ExistingGeneration),
+ // so replaceGeneration is always false here.
+ //
+ // Round 4 R4-3: order is AdvanceCreateAuthority -> AdvanceExecutorBaseline
+ // -> _applyAcceptedSpawn -> (false -> typed Abandon) -> buffered
+ // publish. Rebaselining BEFORE the object-table apply (rather than
+ // after, as an earlier revision did) means a reentrant callback
+ // FROM WITHIN _applyAcceptedSpawn's own synchronous
+ // ObjectAdded/ObjectUpdated dispatch already observes a current
+ // baseline. _applyAcceptedSpawn's own result is now actually
+ // OBSERVED rather than discarded: RuntimeEntityObjectLifetime.
+ // ApplyAcceptedSpawn re-checks currency before, during, AND after
+ // its own object-table apply (its callback is synchronous and may
+ // re-enter entity lifetime) - a nested replacement racing in from
+ // that same callback invalidates the exact canonical incarnation
+ // this drain is still executing against, mirroring
+ // RuntimeLiveEntitySessionController.cs:87's own gate on that same
+ // call's result. The remaining tail cannot run against a record a
+ // nested replacement has already superseded.
+ _residences.AdvanceExecutorBaseline(
+ canonical,
+ token,
+ RuntimeExecutorBaselineFields.PositionAuthorityVersion
+ | RuntimeExecutorBaselineFields.CreateIntegrationVersion);
+ if (!_applyAcceptedSpawn(canonical, createVersion, merged, /* replaceGeneration: */ false))
+ return false;
+ Publish(
+ canonical,
+ RuntimeEntityChange.Updated,
+ () => canonical.CreateIntegrationVersion == createVersion,
+ default,
+ buffer);
+ return true;
+ }
+
+ ///
+ /// Retail: SmartBox::HandleCreateObject's same-incarnation tail, final
+ /// step (retail-notes.md function 1, 0x00454c80, lines ~788-801) -
+ /// objcell_id != 0 && cell == 0 marks for destruction,
+ /// objcell_id != 0 && cell != 0 un-marks, and no cell
+ /// claimed with no weenie also marks for destruction. Asserts the
+ /// invariant rather than building a new destruction mechanism: any
+ /// claimed-but-celless outcome must already be under lost-cell/deferred
+ /// SetPosition ownership.
+ ///
+ private RuntimeResidentCellCleanupDisposition? ApplyResidentCellCleanup(
+ RuntimeEntityRecord canonical)
+ {
+ uint claimedCell = canonical.Snapshot.Physics?.Position?.LandblockId
+ ?? canonical.Snapshot.Position?.LandblockId
+ ?? 0u;
+ if (claimedCell == 0u)
+ {
+ // No cell claimed. See RuntimeResidentCellCleanupDisposition.
+ // CelllessNoWeenieMarkUnreachable's remarks: retail's matching
+ // no-weenie destruction-mark condition is structurally
+ // unreachable through this exact envelope path.
+ return RuntimeResidentCellCleanupDisposition
+ .CelllessNoWeenieMarkUnreachable;
+ }
+ if (canonical.FullCellId != 0u)
+ return RuntimeResidentCellCleanupDisposition.ResidentUnmarked;
+ if (!_physics.SetPosition.IsDeferred(canonical))
+ {
+ // Round 3 B1: the fail-closed invariant violation
+ // (claimed+celless+not-deferred) is a typed abandonment - null
+ // signals the caller to Abandon rather than letting an
+ // exception escape Execute.
+ return null;
+ }
+ // Claimed but celless, and an existing lost-cell/deferred
+ // SetPosition operation already owns this exact entity: the
+ // destruction mark belongs to that existing lifetime (retail:
+ // AddObjectToBeDestroyed was already reached via that path), not to
+ // this tail action - assert the invariant, do not invent a second
+ // destruction mechanism.
+ return RuntimeResidentCellCleanupDisposition.DeferredUnderLostCellOwnership;
+ }
+
+ private void Publish(
+ RuntimeEntityRecord canonical,
+ RuntimeEntityChange change,
+ Func matches,
+ RuntimePlacementCancellationReceipt cancellation,
+ List? buffer)
+ {
+ if (buffer is not null)
+ {
+ // Buffered (same-incarnation envelope) publishes flush only
+ // after EVERY stage has committed. The per-field "matches"
+ // check the IMMEDIATE (standalone-continuation) path uses
+ // exists to catch a reentrant race between one mutation and
+ // its own publish - but a LATER stage in the SAME envelope
+ // legitimately advances the SAME field again as normal,
+ // expected progression (e.g. WeenieDescription's
+ // AdvanceCreateAuthority bumps Position/State/Vector/ObjDesc
+ // authority all at once), which would make an EARLIER stage's
+ // captured matches() go stale by flush time even though
+ // nothing external raced it. IsCurrent (checked unconditionally
+ // by PublishNow below) is the only currency guard a buffered
+ // entry needs: envelope processing dispatches no event until
+ // the flush, so there is no opportunity for reentrancy mid-
+ // envelope except at a Position-stage yield, and THAT window is
+ // independently guarded by ApplyEnvelope's own IsCurrent check
+ // at the top of the resumed loop and by ResumePendingPlacement.
+ buffer.Add(new PendingPublish(change, static () => true, cancellation));
+ return;
+ }
+ PublishNow(canonical, change, matches, cancellation);
+ }
+
+ private void PublishNow(
+ RuntimeEntityRecord canonical,
+ RuntimeEntityChange change,
+ Func matches,
+ RuntimePlacementCancellationReceipt cancellation)
+ {
+ _physics.SetPosition.PublishCancellation(cancellation);
+ if (_entities.IsCurrent(canonical) && matches())
+ _events.PublishEntity(change, canonical);
+ }
+
+ private static RuntimeInitialCreateExecutedAction BuildPositionTrace(
+ ulong sequence,
+ int stage,
+ in RuntimeAuthoritativePositionRoute route) => new(
+ RuntimeInitialCreateExecutedActionKind.Position,
+ sequence,
+ stage,
+ route.Disposition,
+ route.TeleportHookPhase,
+ null,
+ null,
+ // Round 3 B6: record the route's own flags in the trace.
+ route.ConstrainPhase,
+ route.StopInterpolating,
+ route.ZeroVelocity,
+ route.PreserveHeading,
+ route.SendPositionImmediately,
+ route.UnparentBeforeRouting);
+
+ private static RuntimeInitialCreateExecutedAction Simple(
+ RuntimeInitialCreateExecutedActionKind kind,
+ ulong sequence,
+ int stage = -1,
+ RuntimeParentRelationOutcome? parentRelationOutcome = null) => new(
+ kind,
+ sequence,
+ stage,
+ null,
+ RuntimeTeleportHookPhase.None,
+ ParentRelationOutcome: parentRelationOutcome);
+
+ private static RuntimePositionEntityKind EntityKindOf(
+ RuntimeSetPositionOperationKind operationKind) => operationKind switch
+ {
+ RuntimeSetPositionOperationKind.InitialLogin
+ or RuntimeSetPositionOperationKind.LocalAuthoritative =>
+ RuntimePositionEntityKind.LocalPlayer,
+ RuntimeSetPositionOperationKind.ProjectileAuthoritative =>
+ RuntimePositionEntityKind.Projectile,
+ _ => RuntimePositionEntityKind.Remote,
+ };
+
+ private RuntimeGenerationToken CurrentGeneration() => _generation?.Invoke() ?? default;
+}
diff --git a/src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs b/src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs
index c8e1388c..636f1725 100644
--- a/src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs
+++ b/src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs
@@ -362,6 +362,20 @@ internal enum RuntimeInitialCreateResidenceCompletionStatus : byte
RejectedAuthority,
}
+///
+/// Result of ,
+/// the executor-only release that supersedes the host's
+/// once
+/// the initial placement has been adopted.
+///
+internal enum RuntimeInitialCreateResidenceExecutorReleaseStatus : byte
+{
+ Released,
+ Revised,
+ RejectedToken,
+ RejectedAuthority,
+}
+
///
/// Exact post-residence receipt. A local graphical or no-window host may run
/// the retail after-enter teleport suffix only when this receipt carries
@@ -385,6 +399,29 @@ internal readonly record struct RuntimeInitialCreateResidenceOwnershipSnapshot(
&& PendingAdoptionCount == 0;
}
+///
+/// Round 4 R4-4: field-masked precision for
+/// .
+/// The blanket four-field re-sync the executor previously called after
+/// EVERY apply silently absorbed an external race on whichever field(s) a
+/// given apply did NOT itself move - e.g. an ObjDesc/Movement/State/Vector
+/// apply never touches PositionAuthorityVersion/CreateIntegrationVersion/
+/// FullCellId/PlacementCommitVersion, so blanket-resyncing all four there
+/// would mask a genuine concurrent bump to one of them instead of letting
+/// the next
+/// check catch it. Each caller now passes exactly the field(s) its OWN
+/// mutation moved.
+///
+[Flags]
+internal enum RuntimeExecutorBaselineFields : byte
+{
+ None = 0,
+ PositionAuthorityVersion = 1 << 0,
+ CreateIntegrationVersion = 1 << 1,
+ FullCellId = 1 << 2,
+ PlacementCommitVersion = 1 << 3,
+}
+
///
/// Owns only initial CreateObject residence leases. DAT lookup, body creation,
/// and presentation stay outside this owner; their immutable preparation is
@@ -403,6 +440,48 @@ internal sealed class RuntimeInitialCreateResidenceState
internal required RuntimeEntityRecord Record { get; init; }
internal required RuntimeInitialCreateResidenceLease Lease { get; set; }
internal required RuntimeInitialCreateResidenceReceipt Receipt { get; set; }
+
+ ///
+ /// True once the continuation executor has consumed the initial
+ /// placement's acknowledged completion through
+ /// . A retained
+ /// _acknowledgedPlacementCompletions entry on
+ /// blocks EVERY later placement
+ /// begin for the same key (see
+ /// 's
+ /// HasRetainedCompletion guard) — a Position continuation could
+ /// never start its own authored placement while the initial one still
+ /// sits unconsumed. Adoption resolves that deadlock by consuming the
+ /// proof exactly once, while this flag keeps the completed entry
+ /// itself "current" for placement-tracking purposes even though the
+ /// placement token is no longer separately tracked.
+ ///
+ internal bool PlacementAdopted { get; set; }
+
+ ///
+ /// Executor-tracked baseline for the four version/cell fields
+ /// compares against the LIVE record.
+ /// Seeded from 's own (frozen, identity-matching)
+ /// Token/FullCellId/PlacementCommitVersion at the
+ /// moment first produces this entry, then kept
+ /// in sync by every time the
+ /// continuation executor legitimately advances one of them while
+ /// applying a retained continuation. .Token
+ /// itself must NEVER be rebaselined — a caller (the executor) always
+ /// re-presents the SAME original token instance on every retry, and
+ /// 's own token-identity match
+ /// (completed.Receipt.Token == token) depends on that struct
+ /// staying byte-identical. Splitting "identity" (the frozen token)
+ /// from "expected current value" (these fields) is what lets the
+ /// executor's own sequential mutations keep the entry current
+ /// without the residence mistaking its own controlled progress for
+ /// an external race - see the 2026-08-01 admission handoff's own
+ /// warning about exactly this risk.
+ ///
+ internal ulong ExpectedPositionAuthorityVersion { get; set; }
+ internal ulong ExpectedCreateIntegrationVersion { get; set; }
+ internal uint ExpectedFullCellId { get; set; }
+ internal ulong ExpectedPlacementCommitVersion { get; set; }
}
private readonly RuntimeEntityDirectory _entities;
@@ -410,6 +489,7 @@ internal sealed class RuntimeInitialCreateResidenceState
private readonly Dictionary _entries = [];
private readonly Dictionary _completed = [];
private Func? _generation;
+ private Action? _retirementNotification;
private ulong _nextLeaseId;
internal RuntimeInitialCreateResidenceState(
@@ -432,6 +512,31 @@ internal sealed class RuntimeInitialCreateResidenceState
_generation = generation;
}
+ ///
+ /// Round 3 B3: the ONE choke point every residence retirement path -
+ /// , ,
+ /// , and - notifies through,
+ /// regardless of which caller (a host query, a staleness check inside
+ /// this class, or the continuation executor itself) triggered the
+ /// retirement. Without this, a residence retired by a path OTHER than
+ /// the executor's own DiscardProgress call (e.g. a host's
+ /// silently discovering staleness) would
+ /// leave the executor's progress AND its separately-tracked pending
+ /// continuation placement token orphaned - this class owns no reference
+ /// to the executor type, so the lifetime binds a plain delegate here
+ /// instead.
+ ///
+ internal void BindRetirementNotification(Action notify)
+ {
+ ArgumentNullException.ThrowIfNull(notify);
+ if (_retirementNotification is not null)
+ {
+ throw new InvalidOperationException(
+ "The initial Create residence retirement notification is already bound.");
+ }
+ _retirementNotification = notify;
+ }
+
internal bool CanAcceptCreate(WorldSession.EntitySpawn incoming)
{
bool parented = (incoming.ParentGuid
@@ -788,10 +893,59 @@ internal sealed class RuntimeInitialCreateResidenceState
Record = record,
Lease = lease,
Receipt = receipt,
+ ExpectedPositionAuthorityVersion = token.PositionAuthorityVersion,
+ ExpectedCreateIntegrationVersion = token.CreateIntegrationVersion,
+ ExpectedFullCellId = receipt.FullCellId,
+ ExpectedPlacementCommitVersion = receipt.PlacementCommitVersion,
});
return RuntimeInitialCreateResidenceCompletionStatus.Completed;
}
+ ///
+ /// Executor-only: re-synchronizes the completed entry's staleness
+ /// baseline (see
+ /// remarks) to the record's CURRENT live values, but ONLY for the
+ /// field(s) named in (Round 4 R4-4). Called
+ /// after the continuation executor legitimately advances one or more of
+ /// PositionAuthorityVersion/CreateIntegrationVersion/FullCellId/
+ /// PlacementCommitVersion while applying a retained continuation, so a
+ /// LATER / check
+ /// does not mistake the executor's own controlled progress for an
+ /// external race. Passing a field NOT actually moved by the caller's own
+ /// mutation would defeat the whole point - it would silently bless an
+ /// external race on that field instead of letting the next currency
+ /// check catch it - so every call site names exactly its own field(s);
+ /// an apply that moves none of the four tracked fields (ObjDesc,
+ /// Movement, State, Vector) must not call this method at all. A no-op
+ /// (returns false) if the token no longer matches a live completed
+ /// entry - the executor's own currency checks catch that condition
+ /// independently and this call is purely advisory bookkeeping, never a
+ /// source of truth by itself.
+ ///
+ internal bool AdvanceExecutorBaseline(
+ RuntimeEntityRecord record,
+ in RuntimeInitialCreateResidenceToken token,
+ RuntimeExecutorBaselineFields fields)
+ {
+ ArgumentNullException.ThrowIfNull(record);
+ if (!token.IsValid
+ || !_completed.TryGetValue(token.Entity, out CompletedEntry? entry)
+ || !ReferenceEquals(entry.Record, record)
+ || entry.Receipt.Token != token)
+ {
+ return false;
+ }
+ if ((fields & RuntimeExecutorBaselineFields.PositionAuthorityVersion) != 0)
+ entry.ExpectedPositionAuthorityVersion = record.PositionAuthorityVersion;
+ if ((fields & RuntimeExecutorBaselineFields.CreateIntegrationVersion) != 0)
+ entry.ExpectedCreateIntegrationVersion = record.CreateIntegrationVersion;
+ if ((fields & RuntimeExecutorBaselineFields.FullCellId) != 0)
+ entry.ExpectedFullCellId = record.FullCellId;
+ if ((fields & RuntimeExecutorBaselineFields.PlacementCommitVersion) != 0)
+ entry.ExpectedPlacementCommitVersion = record.PlacementCommitVersion;
+ return true;
+ }
+
internal bool AcknowledgeAdoption(
RuntimeEntityRecord record,
in RuntimeInitialCreateResidenceAdoptionToken token)
@@ -816,7 +970,15 @@ internal sealed class RuntimeInitialCreateResidenceState
// discard accepted packets.
if (!current.Lease.Continuations.IsEmpty)
return false;
+ // The executor's own release path is ConsumeExecuted, not this host
+ // method. If the executor already adopted the placement proof
+ // (RuntimeInitialCreateContinuationExecutor.AdoptCompletedPlacement),
+ // it is gone from RuntimeSetPositionState's tracking table entirely —
+ // do not re-consume it a second time, just tolerate the already-
+ // satisfied state and fall through to the same removal every other
+ // caller of this host method observes.
if (current.Lease.Route.PerformsSetPosition
+ && !current.PlacementAdopted
&& !_setPosition.ConsumeAcknowledgedPlacement(
current.Lease.Placement,
current.Receipt.Projection))
@@ -841,6 +1003,7 @@ internal sealed class RuntimeInitialCreateResidenceState
lease = entry.Lease;
cancellation = _setPosition.ForgetExactPlacement(
lease.Placement);
+ _retirementNotification?.Invoke(key);
return true;
}
if (record.Key is { } completedKey
@@ -853,6 +1016,7 @@ internal sealed class RuntimeInitialCreateResidenceState
lease = completed.Lease;
cancellation = _setPosition.ForgetExactPlacement(
lease.Placement);
+ _retirementNotification?.Invoke(completedKey);
return true;
}
lease = default;
@@ -888,6 +1052,13 @@ internal sealed class RuntimeInitialCreateResidenceState
{
_setPosition.PublishCancellation(cancellations[index]);
}
+ if (_retirementNotification is { } notify)
+ {
+ foreach (Entry entry in active)
+ notify(entry.Lease.Token.Entity);
+ foreach (CompletedEntry entry in completed)
+ notify(entry.Receipt.Token.Entity);
+ }
}
internal RuntimeInitialCreateResidenceOwnershipSnapshot CaptureOwnership() =>
@@ -917,6 +1088,24 @@ internal sealed class RuntimeInitialCreateResidenceState
return _generation?.Invoke() ?? default;
}
+ ///
+ /// The staleness check every completed-entry caller shares. Compares the
+ /// live record against
+ /// et al — an executor-tracked, continuously re-synchronized baseline —
+ /// rather than against 's
+ /// own FROZEN admission-time fields directly. This is what lets the
+ /// continuation executor's own legitimate mutations
+ /// (AdvancePositionAuthority, AdvanceCreateAuthority, SetFullCell,
+ /// AdvancePlacementCommit — all driven by applying a retained
+ /// continuation) keep this entry current across the many
+ /// re-entries a multi-call drain requires, while
+ /// still correctly detecting a genuine EXTERNAL race (anything that
+ /// changes one of these fields WITHOUT going through
+ /// ) exactly as it always did. The
+ /// token itself remains the untouched identity/match key -
+ /// 's completed.Receipt.Token == token check
+ /// depends on that.
+ ///
private bool IsCompletedCurrent(CompletedEntry entry)
{
RuntimeInitialCreateResidenceReceipt receipt = entry.Receipt;
@@ -925,35 +1114,147 @@ internal sealed class RuntimeInitialCreateResidenceState
&& _entities.SessionLifetimeVersion
== receipt.Token.SessionLifetimeVersion
&& entry.Record.PositionAuthorityVersion
- == receipt.Token.PositionAuthorityVersion
+ == entry.ExpectedPositionAuthorityVersion
&& entry.Record.CreateIntegrationVersion
- == receipt.Token.CreateIntegrationVersion
- && entry.Record.FullCellId == receipt.FullCellId
+ == entry.ExpectedCreateIntegrationVersion
+ && entry.Record.FullCellId == entry.ExpectedFullCellId
&& entry.Record.PlacementCommitVersion
- == receipt.PlacementCommitVersion
+ == entry.ExpectedPlacementCommitVersion
&& entry.Lease.Route.Authority.Generation
== CurrentGeneration()
&& receipt.Token.SessionLifetimeVersion
== receipt.Adoption.SessionLifetimeVersion
&& receipt.Token.LeaseId == receipt.Adoption.LeaseId
+ // A completed entry whose placement proof the executor already
+ // adopted remains current on the placement dimension without
+ // re-querying RuntimeSetPositionState: AdoptCompletedPlacement
+ // consumed (removed) the exact tracked token, so
+ // IsPlacementCompletionTracked would now report false even though
+ // nothing here has gone stale.
&& (!entry.Lease.Route.PerformsSetPosition
+ || entry.PlacementAdopted
|| _setPosition.IsPlacementCompletionTracked(
entry.Lease.Placement));
}
+ ///
+ /// Executor-only: consumes the initial placement's acknowledged
+ /// completion exactly once so a later retained Position continuation can
+ /// begin its own authored placement for the same
+ /// (see the remarks on
+ /// for why this is
+ /// necessary). Idempotent: a retry after is
+ /// already true is a no-op success, never a double-consume.
+ ///
+ internal bool AdoptCompletedPlacement(
+ RuntimeEntityRecord record,
+ in RuntimeInitialCreateResidenceToken token)
+ {
+ ArgumentNullException.ThrowIfNull(record);
+ if (!token.IsValid
+ || !_completed.TryGetValue(token.Entity, out CompletedEntry? entry)
+ || !ReferenceEquals(entry.Record, record)
+ || entry.Receipt.Token != token)
+ {
+ return false;
+ }
+ if (entry.PlacementAdopted)
+ return IsCompletedCurrent(entry);
+ if (!IsCompletedCurrent(entry))
+ {
+ Retire(entry);
+ return false;
+ }
+ if (!entry.Lease.Route.PerformsSetPosition)
+ {
+ // A Parented/PickedUp lease never captured a real placement
+ // token; there is nothing to consume, but the tail must still be
+ // able to progress past this step exactly once.
+ entry.PlacementAdopted = true;
+ return true;
+ }
+ if (!_setPosition.ConsumeAcknowledgedPlacement(
+ entry.Lease.Placement,
+ entry.Receipt.Projection))
+ {
+ return false;
+ }
+ entry.PlacementAdopted = true;
+ return true;
+ }
+
+ ///
+ /// Executor-only release: consumes the residence entirely once the exact
+ /// adoption token still matches AND the caller has applied every
+ /// continuation through the CURRENT lease's full length. Placement
+ /// consumption already happened via ,
+ /// so this does not call
+ /// a second time for an adopted entry — unlike the host-facing
+ /// , which only ever runs for entries the
+ /// executor has not touched.
+ ///
+ internal RuntimeInitialCreateResidenceExecutorReleaseStatus ConsumeExecuted(
+ RuntimeEntityRecord record,
+ in RuntimeInitialCreateResidenceAdoptionToken token,
+ ulong executedThroughSequence)
+ {
+ ArgumentNullException.ThrowIfNull(record);
+ if (!token.IsValid
+ || !_completed.TryGetValue(token.Entity, out CompletedEntry? entry)
+ || !ReferenceEquals(entry.Record, record)
+ || entry.Receipt.Adoption.Entity != token.Entity
+ || entry.Receipt.Adoption.LeaseId != token.LeaseId)
+ {
+ return RuntimeInitialCreateResidenceExecutorReleaseStatus
+ .RejectedToken;
+ }
+ if (!IsCompletedCurrent(entry))
+ {
+ Retire(entry);
+ return RuntimeInitialCreateResidenceExecutorReleaseStatus
+ .RejectedAuthority;
+ }
+ if (entry.Receipt.Adoption.Revision != token.Revision)
+ {
+ // A newer continuation arrived mid-drain (Enqueue bumps Revision
+ // in place on the SAME completed entry). The executor must
+ // re-fetch via Complete and drain the tail, never replay the
+ // already-applied prefix.
+ return RuntimeInitialCreateResidenceExecutorReleaseStatus.Revised;
+ }
+ if (!entry.PlacementAdopted && entry.Lease.Route.PerformsSetPosition)
+ {
+ throw new InvalidOperationException(
+ "Executor release requires the initial placement to have been adopted first.");
+ }
+ if ((ulong)entry.Lease.Continuations.Length != executedThroughSequence)
+ {
+ return RuntimeInitialCreateResidenceExecutorReleaseStatus
+ .RejectedAuthority;
+ }
+ return _completed.Remove(token.Entity)
+ ? RuntimeInitialCreateResidenceExecutorReleaseStatus.Released
+ : RuntimeInitialCreateResidenceExecutorReleaseStatus
+ .RejectedAuthority;
+ }
+
private void Retire(Entry entry)
{
- _entries.Remove(entry.Lease.Token.Entity);
+ RuntimeEntityKey key = entry.Lease.Token.Entity;
+ _entries.Remove(key);
RuntimePlacementCancellationReceipt cancellation =
_setPosition.ForgetExactPlacement(entry.Lease.Placement);
_setPosition.PublishCancellation(cancellation);
+ _retirementNotification?.Invoke(key);
}
private void Retire(CompletedEntry entry)
{
- _completed.Remove(entry.Receipt.Token.Entity);
+ RuntimeEntityKey key = entry.Receipt.Token.Entity;
+ _completed.Remove(key);
RuntimePlacementCancellationReceipt cancellation =
_setPosition.ForgetExactPlacement(entry.Lease.Placement);
_setPosition.PublishCancellation(cancellation);
+ _retirementNotification?.Invoke(key);
}
}
diff --git a/tests/AcDream.Runtime.Tests/Entities/InboundPhysicsStateControllerTests.cs b/tests/AcDream.Runtime.Tests/Entities/InboundPhysicsStateControllerTests.cs
index 0b0b0259..4edd2dbf 100644
--- a/tests/AcDream.Runtime.Tests/Entities/InboundPhysicsStateControllerTests.cs
+++ b/tests/AcDream.Runtime.Tests/Entities/InboundPhysicsStateControllerTests.cs
@@ -339,6 +339,76 @@ public sealed class InboundPhysicsStateControllerTests
Assert.Equal((ushort)2, retained.Physics!.Value.Timestamps.Movement);
}
+ [Fact]
+ public void StaleMovementTimestampLeavesLegacySnapshotByteIdentical()
+ {
+ var controller = new InboundPhysicsStateController();
+ WorldSession.EntitySpawn spawn = WithTimestamps(
+ Spawn(0x70000008u, 3, 1, 1, Position(0x0101FFFFu, 10f), 0x408u),
+ movement: 5,
+ serverControl: 5);
+ controller.AcceptCreate(spawn);
+ Assert.True(controller.TryGetSnapshot(spawn.Guid, out WorldSession.EntitySpawn before));
+
+ // MOVEMENT_TS 3 is OLDER than the stored 5:
+ // PhysicsTimestampGate.TryAcceptMovementEvent's
+ // AdvanceStrict(Movement, ...) fails before ever consulting
+ // SERVER_CONTROLLED_MOVE_TS - the gate is entirely untouched, and
+ // the canonical snapshot must not move either (the regression this
+ // guards: ApplyAcceptedMotion stamping the WIRE's own stale
+ // MovementSequence instead of the gate's unchanged post-call value).
+ bool applied = controller.TryApplyMotion(
+ new WorldSession.EntityMotionUpdate(
+ spawn.Guid,
+ new CreateObject.ServerMotionState(0x3d, 0x99),
+ InstanceSequence: 3,
+ MovementSequence: 3,
+ ServerControlSequence: 9,
+ IsAutonomous: false),
+ retainPayload: true,
+ out WorldSession.EntitySpawn accepted,
+ out _);
+
+ Assert.False(applied);
+ Assert.Equal(default, accepted);
+ Assert.True(controller.TryGetSnapshot(spawn.Guid, out WorldSession.EntitySpawn after));
+ Assert.Equal(before, after);
+ }
+
+ [Fact]
+ public void InstanceMismatchedMovementLeavesLegacySnapshotByteIdentical()
+ {
+ var controller = new InboundPhysicsStateController();
+ WorldSession.EntitySpawn spawn = WithTimestamps(
+ Spawn(0x70000009u, 3, 1, 1, Position(0x0101FFFFu, 10f), 0x408u),
+ movement: 1,
+ serverControl: 1);
+ controller.AcceptCreate(spawn);
+ Assert.True(controller.TryGetSnapshot(spawn.Guid, out WorldSession.EntitySpawn before));
+
+ // A different INSTANCE_TS: TryAcceptInstance fails immediately,
+ // before MOVEMENT_TS or SERVER_CONTROLLED_MOVE_TS are ever
+ // consulted. Nothing in the gate or the canonical snapshot may move
+ // - a stale duplicate or foreign-incarnation packet must not
+ // silently drag a live entity's movement timestamps forward.
+ bool applied = controller.TryApplyMotion(
+ new WorldSession.EntityMotionUpdate(
+ spawn.Guid,
+ new CreateObject.ServerMotionState(0x3d, 0x99),
+ InstanceSequence: 4,
+ MovementSequence: 9,
+ ServerControlSequence: 9,
+ IsAutonomous: false),
+ retainPayload: true,
+ out WorldSession.EntitySpawn accepted,
+ out _);
+
+ Assert.False(applied);
+ Assert.Equal(default, accepted);
+ Assert.True(controller.TryGetSnapshot(spawn.Guid, out WorldSession.EntitySpawn after));
+ Assert.Equal(before, after);
+ }
+
[Fact]
public void FreshForceWithOlderTeleportMirrorsForceButRejectsPose()
{
diff --git a/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs b/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs
new file mode 100644
index 00000000..ab1b8a9b
--- /dev/null
+++ b/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs
@@ -0,0 +1,4464 @@
+using System.Collections.Immutable;
+using System.Numerics;
+using AcDream.Core.Net;
+using AcDream.Core.Net.Messages;
+using AcDream.Core.Physics;
+using AcDream.Runtime.Entities;
+using AcDream.Runtime.Physics;
+
+namespace AcDream.Runtime.Tests.Entities;
+
+public sealed class RuntimeInitialCreateContinuationExecutorTests
+{
+ private const uint Landblock = 0xA9B50000u;
+ private const uint Cell = Landblock | 0x0001u;
+ private static readonly RuntimeInitialCreateExecutionInputs NoContact =
+ new(UsePositionFromServer: false, PlayerDistance: 0f);
+
+ // ---------------------------------------------------------------
+ // A. Basic
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void CompletedTopLevelCreateAdoptsOnceEmitsHookAndConvergesEveryLedger()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 1UL);
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(0x70020001u, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Equal(Cell, receipt.FullCellId);
+ Assert.Equal(RuntimeTeleportHookPhase.AfterEnterWorld, receipt.TeleportHookPhase);
+ Assert.Equal(0, receipt.ReplayedDeferredChildCount);
+ Assert.Equal(
+ [
+ RuntimeInitialCreateExecutedActionKind.InitialAdoption,
+ RuntimeInitialCreateExecutedActionKind.TeleportHookRequest,
+ ],
+ receipt.Trace.Select(static a => a.Kind));
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.Equal(
+ 0,
+ lifetime.Physics.SetPosition.CaptureOwnership().AcknowledgedPlacementCompletionCount);
+ Assert.False(lifetime.TryGetInitialCreateResidence(canonical, out _));
+
+ // Retrying with the now-stale token is a distinct, safe no-op.
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedToken,
+ lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out _));
+ }
+
+ [Fact]
+ public void MixedSimpleContinuationsDrainInExactSequenceOrderAndMutateSnapshot()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 2UL);
+ const uint parentGuid = 0x70021100u;
+ const uint guid = 0x70021000u;
+ _ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ Assert.False(lease.Placement.IsValid); // parented -> AwaitFreshPosition, no placement
+
+ var appearance = new ObjDescEvent.Parsed(
+ guid,
+ new CreateObject.ModelData(
+ 0x04000002u,
+ Array.Empty(),
+ Array.Empty(),
+ Array.Empty()),
+ InstanceSequence: 1,
+ ObjDescSequence: 2);
+ Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
+ var vector = new VectorUpdate.Parsed(
+ guid, new Vector3(1f, 2f, 3f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(vector, null, out _));
+ var state = new SetState.Parsed(
+ guid,
+ (uint)PhysicsStateFlags.Gravity,
+ InstanceSequence: 1,
+ StateSequence: 2);
+ Assert.True(lifetime.TryApplyState(state, null, out _, out _));
+
+ var observed = new List();
+ using IDisposable subscription = lifetime.Events.Subscribe(
+ new EntityObserver(delta => observed.Add(delta.Change)));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Equal(
+ [
+ RuntimeInitialCreateExecutedActionKind.InitialAdoption,
+ RuntimeInitialCreateExecutedActionKind.ObjDesc,
+ RuntimeInitialCreateExecutedActionKind.Vector,
+ RuntimeInitialCreateExecutedActionKind.State,
+ ],
+ receipt.Trace.Select(static a => a.Kind));
+ Assert.Equal([1UL, 2UL, 3UL],
+ receipt.Trace
+ .Where(static a => a.Sequence != 0UL)
+ .Select(static a => a.Sequence));
+ Assert.Equal(
+ [RuntimeEntityChange.Updated, RuntimeEntityChange.Updated, RuntimeEntityChange.Updated],
+ observed);
+ Assert.Equal(0x04000002u, canonical.Snapshot.BasePaletteId);
+ Assert.Equal(new Vector3(1f, 2f, 3f), canonical.Snapshot.Physics!.Value.Velocity);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // Round 4 R4-4 (mandated regression test): a FIFO of ObjDesc then Vector
+ // - NEITHER touches any of the four executor-tracked baseline fields
+ // (PositionAuthorityVersion/CreateIntegrationVersion/FullCellId/
+ // PlacementCommitVersion), so NEITHER may call AdvanceExecutorBaseline
+ // at all. An observer bumps PositionAuthorityVersion externally during
+ // ObjDesc's own publish; because ObjDesc's apply never rebaselines that
+ // field, the STALE Expected value survives into the next check. The
+ // drain must detect this external race at ConsumeExecuted (the "next
+ // step") and abandon - NOT silently absorb it and report Released, which
+ // a blanket four-field rebaseline (the pre-R4-4 shape) would have done.
+ [Fact]
+ public void FieldMaskedBaselinePrecisionDetectsAnExternalPositionRaceDuringAnUnrelatedObjDescPublish()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 96UL);
+ const uint parentGuid = 0x70038000u;
+ const uint guid = 0x70038001u;
+ _ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+
+ var appearance = new ObjDescEvent.Parsed(
+ guid,
+ new CreateObject.ModelData(
+ 0x07000002u,
+ Array.Empty(),
+ Array.Empty(),
+ Array.Empty()),
+ InstanceSequence: 1,
+ ObjDescSequence: 2);
+ Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
+ var vector = new VectorUpdate.Parsed(
+ guid, new Vector3(1f, 2f, 3f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(vector, null, out _));
+
+ bool bumped = false;
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ if (bumped || delta.Change is not RuntimeEntityChange.Updated)
+ return;
+ bumped = true;
+ // External, non-executor mutation of PositionAuthorityVersion
+ // during the ObjDesc stage's OWN publish.
+ lifetime.Entities.AdvancePositionAuthority(canonical);
+ }));
+
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedAuthority,
+ lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
+ Assert.True(bumped);
+ Assert.Equal(default, receipt);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // Round 3 A1 (mandated regression test): the executor's applies must
+ // write InboundPhysicsStateController's OWN _snapshots[guid] - the
+ // legacy merge base - in lockstep with RuntimeEntityRecord.Snapshot. If
+ // they only wrote the record's own Snapshot (as before A1), the FIRST
+ // legacy wire apply reached after the residence drain would re-merge
+ // against a STALE _snapshots[guid] base and silently revert every fact
+ // the drain just committed.
+ [Fact]
+ public void DrainedAppearanceAndPoseSurviveTheNextLegacyWireApply()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 30UL);
+ const uint parentGuid = 0x7002D100u;
+ const uint guid = 0x7002D000u;
+ _ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+
+ // Drain an ObjDesc continuation that changes appearance (a fresh
+ // BasePaletteId) as part of the residence FIFO.
+ var appearance = new ObjDescEvent.Parsed(
+ guid,
+ new CreateObject.ModelData(
+ 0x06000002u,
+ Array.Empty(),
+ Array.Empty(),
+ Array.Empty()),
+ InstanceSequence: 1,
+ ObjDescSequence: 2);
+ Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+ Assert.Contains(
+ RuntimeInitialCreateExecutedActionKind.ObjDesc,
+ receipt.Trace.Select(static a => a.Kind));
+ Assert.Equal(0x06000002u, canonical.Snapshot.BasePaletteId);
+
+ // Now run an ORDINARY legacy wire apply (Vector) on a channel the
+ // drain never touched. Its merge base is InboundPhysicsStateController's
+ // OWN _snapshots[guid] - if that store still held the pre-drain
+ // appearance (A1's bug), this apply's `old with { ... }` merge would
+ // carry the STALE BasePaletteId back into canonical.Snapshot via
+ // RefreshSnapshot, silently reverting the drained fact.
+ var vector = new VectorUpdate.Parsed(
+ guid, new Vector3(4f, 5f, 6f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(vector, null, out _));
+
+ Assert.Equal(0x06000002u, canonical.Snapshot.BasePaletteId);
+ Assert.Equal(new Vector3(4f, 5f, 6f), canonical.Snapshot.Physics!.Value.Velocity);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // Round 3 B12: the WeenieDescription tail action must drive the object
+ // table the same way RuntimeLiveEntitySessionController.OnSpawned does
+ // for the non-residence direct-host Create path (ApplyAcceptedSpawn) -
+ // the prior "zero callers" claim for this wiring was false. A
+ // residence-pending admission deliberately never wires the object table
+ // at the initial Create (RegisterEntityCore's beginInitialResidence
+ // branch); the FIRST time this guid's entry can appear is exactly here.
+ [Fact]
+ public void WeenieDescriptionStageWiresTheObjectTableExactlyOnce()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 34UL);
+ const uint guid = 0x7002E400u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ int objectCountBeforeDrain = lifetime.Objects.ObjectCount;
+
+ WorldSession.EntitySpawn sameCreate = Spawn(
+ guid, 1, includePosition: false, positionSequence: 2);
+ PhysicsSpawnData physics = sameCreate.Physics!.Value;
+ sameCreate = sameCreate with
+ {
+ Physics = physics with
+ {
+ Timestamps = physics.Timestamps with { State = 2, Vector = 2 },
+ },
+ };
+ _ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Contains(
+ RuntimeInitialCreateExecutedActionKind.WeenieDescription,
+ receipt.Trace.Select(static a => a.Kind));
+ Assert.Equal(objectCountBeforeDrain + 1, lifetime.Objects.ObjectCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // Round 4 R4-3: ApplyWeenieDescriptionAction's object-table apply window.
+ // ClientObjectTable.Ingest publishes ObjectAdded/ObjectUpdated
+ // SYNCHRONOUSLY - a subscriber may re-enter a wire apply for the SAME
+ // entity from inside that dispatch. Because AdvanceExecutorBaseline now
+ // runs BEFORE _applyAcceptedSpawn (not after), the residence's baseline
+ // is already current at the moment the reentrant call runs; the
+ // residence is not retired and the envelope still completes.
+ [Fact]
+ public void ObjectTableSubscriberReenteringAWireApplyDuringIngestDoesNotRetireTheResidenceAndTheEnvelopeCompletes()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 94UL);
+ const uint guid = 0x70037000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+
+ WorldSession.EntitySpawn sameCreate = Spawn(
+ guid, 1, includePosition: false, positionSequence: 2);
+ PhysicsSpawnData physics = sameCreate.Physics!.Value;
+ sameCreate = sameCreate with
+ {
+ Physics = physics with
+ {
+ Timestamps = physics.Timestamps with { State = 2, Vector = 2 },
+ },
+ };
+ _ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
+
+ bool reentered = false;
+ lifetime.Objects.ObjectAdded += addedObject =>
+ {
+ if (reentered)
+ return;
+ reentered = true;
+ // A reentrant wire apply for a channel this envelope does NOT
+ // itself carry a dedicated stage for at this admission
+ // (Vector IS one of this envelope's own stages here, but the
+ // residence is still "pending" mid-drain, so this legitimately
+ // defers into the SAME FIFO rather than applying immediately -
+ // proving the residence survives the reentrant call).
+ var vector = new VectorUpdate.Parsed(
+ guid, new Vector3(9f, 8f, 7f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 3);
+ Assert.True(lifetime.TryApplyVector(vector, null, out _));
+ };
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.True(reentered);
+ Assert.Contains(
+ RuntimeInitialCreateExecutedActionKind.WeenieDescription,
+ receipt.Trace.Select(static a => a.Kind));
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // Round 4 R4-3: a NESTED REPLACEMENT (a newer incarnation Create for the
+ // SAME guid) arriving from within Ingest's own synchronous dispatch
+ // invalidates the exact canonical incarnation the drain is still
+ // executing against. ApplyAcceptedSpawn re-checks currency AFTER its own
+ // object-table apply (mirroring RuntimeLiveEntitySessionController.cs:87's
+ // gate on that same call's result) and returns false; the executor must
+ // treat this as a typed abandonment - the remaining tail (ResidentCellCleanup)
+ // never runs.
+ [Fact]
+ public void NestedReplacementDuringObjectTableIngestAbandonsTheWeenieDescriptionStageWithNoFurtherStages()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 95UL);
+ const uint guid = 0x70037100u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+
+ WorldSession.EntitySpawn sameCreate = Spawn(
+ guid, 1, includePosition: false, positionSequence: 2);
+ PhysicsSpawnData physics = sameCreate.Physics!.Value;
+ sameCreate = sameCreate with
+ {
+ Physics = physics with
+ {
+ Timestamps = physics.Timestamps with { State = 2, Vector = 2 },
+ },
+ };
+ _ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
+
+ bool reentered = false;
+ lifetime.Objects.ObjectAdded += addedObject =>
+ {
+ if (reentered)
+ return;
+ reentered = true;
+ _ = lifetime.RegisterEntity(Spawn(guid, 2, includePosition: false));
+ };
+
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedAuthority,
+ lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
+ Assert.True(reentered);
+ Assert.Equal(default, receipt);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // Round 3 A2 (mandated regression test): a standalone ObjDesc
+ // continuation admits a fresh palette (bumping the gate's ObjDesc
+ // channel and proving A1's snapshot lockstep feeds entry 2's merge
+ // base), THEN a same-incarnation Create arrives whose OWN raw
+ // WeenieDescription packet carries a DIFFERENT MotionTableId. Appearance
+ // fields (BasePaletteId etc.) are legitimately re-applied by the
+ // envelope's OWN dedicated ObjDesc stage (retail-faithful: a same-
+ // generation Create's tail always re-runs its own ObjDesc first) - that
+ // is NOT what A2 fixes. MotionTableId has NO dedicated envelope stage;
+ // WeenieDescription's merge is the ONLY place it can move, which
+ // isolates MergeUntimestampedCreate's "retained wins" rule cleanly: the
+ // prior wholesale RefreshSnapshot bug would have silently overwritten it
+ // with the incoming raw packet's value instead.
+ [Fact]
+ public void SameIncarnationWeenieDescriptionMergePreservesRetainedMotionTableId()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 31UL);
+ const uint parentGuid = 0x7002D300u;
+ const uint guid = 0x7002D200u;
+ _ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
+
+ WorldSession.EntitySpawn initial = Spawn(guid, 1, includePosition: false, parentGuid: parentGuid);
+ PhysicsSpawnData initialPhysics = initial.Physics!.Value;
+ initial = initial with
+ {
+ MotionTableId = 0x12345678u,
+ Physics = initialPhysics with { MotionTableId = 0x12345678u },
+ };
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(initial, isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+
+ // FIFO entry 1: standalone ObjDesc with a NEW palette, bumping the
+ // gate's ObjDesc channel to 2.
+ var newAppearance = new ObjDescEvent.Parsed(
+ guid,
+ new CreateObject.ModelData(
+ 0x07000002u,
+ Array.Empty(),
+ Array.Empty(),
+ Array.Empty()),
+ InstanceSequence: 1,
+ ObjDescSequence: 2);
+ Assert.True(lifetime.TryApplyObjDesc(newAppearance, null, out _));
+
+ // FIFO entry 2: a same-incarnation Create whose OWN raw
+ // WeenieDescription packet carries a DIFFERENT MotionTableId and a
+ // NEWER-still ObjDesc channel stamp (3).
+ WorldSession.EntitySpawn sameCreate = Spawn(
+ guid, 1, includePosition: false, parentGuid: parentGuid, positionSequence: 2);
+ PhysicsSpawnData samePhysics = sameCreate.Physics!.Value;
+ sameCreate = sameCreate with
+ {
+ MotionTableId = 0x99999999u,
+ Physics = samePhysics with
+ {
+ MotionTableId = 0x99999999u,
+ Timestamps = samePhysics.Timestamps with { ObjDesc = 3, State = 2, Vector = 2 },
+ },
+ };
+ RuntimeEntityRegistrationResult same = lifetime
+ .RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
+ Assert.Equal(CreateObjectTimestampDisposition.ExistingGeneration, same.Inbound.Disposition);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Contains(
+ RuntimeInitialCreateExecutedActionKind.WeenieDescription,
+ receipt.Trace.Select(static a => a.Kind));
+ // The envelope's OWN ObjDesc stage correctly re-applies THIS
+ // Create's own appearance (sameCreate never set its own palette, so
+ // it legitimately reverts to null - retail-faithful, not what A2
+ // fixes). Entry 1's transient fresh palette was always going to be
+ // superseded by entry 2's OWN ObjDesc stage; that stage - not
+ // WeenieDescription - owns this field.
+ Assert.Null(canonical.Snapshot.BasePaletteId);
+ // MotionTableId has no dedicated envelope stage - WeenieDescription's
+ // merge must keep the RETAINED value, never adopt incoming's raw
+ // packet wholesale.
+ Assert.Equal(0x12345678u, canonical.Snapshot.Physics!.Value.MotionTableId);
+ Assert.Equal(0x12345678u, canonical.Snapshot.MotionTableId);
+ // ObjDesc's timestamp DOES have a dedicated stage that legitimately
+ // advances it further as part of entry 2's own admission (unlike
+ // MotionTableId).
+ Assert.Equal(3, canonical.Snapshot.Physics!.Value.Timestamps.ObjDesc);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // ---------------------------------------------------------------
+ // B. Standalone continuations (Movement/Pickup/Parent) + enqueue-
+ // during-drain (ConsumeExecuted's Revised arm).
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void StandaloneMovementContinuationAppliesPayloadAndPublishesUpdated()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 40UL);
+ const uint guid = 0x7002F000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+
+ var motion = new WorldSession.EntityMotionUpdate(
+ guid,
+ new CreateObject.ServerMotionState(0x3d, 0x11),
+ InstanceSequence: 1,
+ MovementSequence: 2,
+ ServerControlSequence: 1,
+ IsAutonomous: false);
+ // Movement (2) advances and ServerControl (1) is equal-not-stale, so
+ // the gate accepts the payload outright.
+ Assert.True(lifetime.TryApplyMotion(motion, retainPayload: true, null, out _, out _));
+
+ var observed = new List();
+ using IDisposable subscription = lifetime.Events.Subscribe(
+ new EntityObserver(delta => observed.Add(delta.Change)));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Equal(
+ [
+ RuntimeInitialCreateExecutedActionKind.InitialAdoption,
+ RuntimeInitialCreateExecutedActionKind.Movement,
+ ],
+ receipt.Trace.Select(static a => a.Kind));
+ Assert.Equal(new CreateObject.ServerMotionState(0x3d, 0x11),
+ canonical.Snapshot.MotionState);
+ Assert.Equal(2, canonical.Snapshot.Physics!.Value.Timestamps.Movement);
+ Assert.Equal([RuntimeEntityChange.Updated], observed);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ [Fact]
+ public void StandaloneMovementContinuationTimestampOnlyStampsWithoutPayloadOrPublish()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 41UL);
+ const uint guid = 0x7002F100u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ Assert.Null(canonical.Snapshot.MotionState);
+
+ var motion = new WorldSession.EntityMotionUpdate(
+ guid,
+ new CreateObject.ServerMotionState(0x3d, 0x11),
+ InstanceSequence: 1,
+ MovementSequence: 2,
+ // Retail consumes MOVEMENT_TS before discovering a stale
+ // SERVER_CONTROLLED_MOVE_TS (0, older than the gate's seeded 1) -
+ // Movement itself still advances (hasTimestampMutation) even
+ // though the overall event, and thus the payload, is rejected.
+ ServerControlSequence: 0,
+ IsAutonomous: false);
+ Assert.False(lifetime.TryApplyMotion(motion, retainPayload: true, null, out _, out _));
+ Assert.True(lifetime.InitialCreateResidences.TryGetTransaction(
+ canonical, out RuntimeInitialCreateResidenceLease retained));
+ Assert.Single(retained.Continuations);
+
+ var observed = new List();
+ using IDisposable subscription = lifetime.Events.Subscribe(
+ new EntityObserver(delta => observed.Add(delta.Change)));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Contains(
+ RuntimeInitialCreateExecutedActionKind.Movement,
+ receipt.Trace.Select(static a => a.Kind));
+ // Timestamp landed; no payload, no publish (matches the legacy
+ // timestamp-only branch).
+ Assert.Equal(2, canonical.Snapshot.Physics!.Value.Timestamps.Movement);
+ Assert.Null(canonical.Snapshot.MotionState);
+ Assert.Empty(observed);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ [Fact]
+ public void StandalonePickupContinuationLeavesWorldThroughTheDrainAndResidenceStillReleases()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 42UL);
+ const uint guid = 0x7002F200u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+ Assert.Equal(Cell, canonical.FullCellId);
+
+ // A committed parent projection so Pickup's own EndChildProjection
+ // has something real to tear down.
+ var relation = new ParentAttachmentRelation(
+ 0x7002F300u, guid, ParentLocation: 1u, PlacementId: 0u,
+ ParentInstanceSequence: 1, ChildPositionSequence: 1);
+ lifetime.Entities.ParentAttachments.AcceptCreateObjectRelation(relation);
+ Assert.True(lifetime.Entities.ParentAttachments.CommitProjection(relation));
+ Assert.True(lifetime.Entities.ParentAttachments.HasCommittedParent(guid));
+
+ Assert.True(lifetime.TryApplyPickup(
+ new PickupEvent.Parsed(guid, InstanceSequence: 1, PositionSequence: 2),
+ null,
+ out _));
+
+ var observed = new List();
+ using IDisposable subscription = lifetime.Events.Subscribe(
+ new EntityObserver(delta => observed.Add(delta.Change)));
+ ulong clockEpochBefore = canonical.ObjectClockEpoch;
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Equal(
+ [
+ RuntimeInitialCreateExecutedActionKind.InitialAdoption,
+ RuntimeInitialCreateExecutedActionKind.Pickup,
+ ],
+ receipt.Trace.Select(static a => a.Kind));
+ Assert.Equal([RuntimeEntityChange.Withdrawn], observed);
+ Assert.Equal(0u, canonical.FullCellId);
+ // SuspendObjectClock bumps the epoch - the clock is no longer live.
+ Assert.NotEqual(clockEpochBefore, canonical.ObjectClockEpoch);
+ Assert.False(lifetime.Entities.ParentAttachments.HasCommittedParent(guid));
+ Assert.False(lifetime.Entities.ParentAttachments.TryGetRecoveryProjection(guid, out _));
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ // Residence still releases cleanly even though this continuation
+ // left the world mid-drain.
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ [Fact]
+ public void StandaloneParentContinuationAppliesPositionTimestampOnlyMergeWithNoReDefer()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 43UL);
+ const uint parentGuid = 0x7002F400u;
+ const uint childGuid = 0x7002F500u;
+ _ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+
+ var parentUpdate = new ParentEvent.Parsed(
+ parentGuid,
+ childGuid,
+ ParentLocation: 1u,
+ PlacementId: 0u,
+ ParentInstanceSequence: 1,
+ ChildPositionSequence: 2);
+ // The parent is alive at admission AND stays alive all the way
+ // through the drain - contrast with
+ // ParentContinuationRevalidatesLiveParentAtExecutionAndReDefersOnMismatch,
+ // which deletes the parent between admission and execution.
+ Assert.True(lifetime.TryApplyParent(parentUpdate, null, out _));
+
+ var observed = new List();
+ using IDisposable subscription = lifetime.Events.Subscribe(
+ new EntityObserver(delta => observed.Add(delta.Change)));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Equal(
+ [
+ RuntimeInitialCreateExecutedActionKind.InitialAdoption,
+ RuntimeInitialCreateExecutedActionKind.Parent,
+ ],
+ receipt.Trace.Select(static a => a.Kind));
+ // ApplyAcceptedParent is position-timestamp-only: the shared
+ // POSITION_TS channel advances but no pose/ParentGuid field moves
+ // (the actual attach commit is TryCommitParent's job, out of scope
+ // here).
+ Assert.Equal(2, canonical.Snapshot.Physics!.Value.Timestamps.Position);
+ Assert.Null(canonical.Snapshot.ParentGuid);
+ Assert.Equal([RuntimeEntityChange.Updated], observed);
+ // The successful path hands NOTHING to ParentAttachments' unresolved
+ // bucket - the direct contrast with the re-defer test's
+ // UnresolvedRelationCount == 1.
+ Assert.Equal(0, lifetime.Entities.ParentAttachments.UnresolvedRelationCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // Round 3 arc-10 (enqueue-during-drain): a continuation enqueued from an
+ // observer mid-drain bumps the completed entry's Adoption.Revision in
+ // place; ConsumeExecuted must return Revised (not Released) so the SAME
+ // Execute() call's outer while(true) loop re-fetches via Complete and
+ // drains only the newly-appended tail. The already-applied prefix must
+ // never replay.
+ [Fact]
+ public void EnqueueDuringDrainExercisesConsumeExecutedRevisedArmAndDrainsTailOnlyOnce()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 44UL);
+ const uint guid = 0x7002F600u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+
+ var appearance = new ObjDescEvent.Parsed(
+ guid,
+ new CreateObject.ModelData(
+ 0x08000002u,
+ Array.Empty(),
+ Array.Empty(),
+ Array.Empty()),
+ InstanceSequence: 1,
+ ObjDescSequence: 2);
+ Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
+
+ var observed = new List();
+ bool enqueuedFromObserver = false;
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ observed.Add(delta.Change);
+ if (enqueuedFromObserver)
+ return;
+ enqueuedFromObserver = true;
+ // Reentrant enqueue, mid-drain, from inside the ObjDesc
+ // continuation's own publish: the residence is still in
+ // RuntimeInitialCreateResidenceState's _completed dictionary
+ // (not yet ConsumeExecuted'd), so this routes through
+ // CanEnqueue's completed-entry branch and bumps Revision in
+ // place.
+ var vector = new VectorUpdate.Parsed(
+ guid, new Vector3(7f, 8f, 9f), Vector3.Zero,
+ InstanceSequence: 1, VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(vector, null, out _));
+ }));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ // Each action appears exactly once - the Revised loop-around must
+ // not replay the already-applied ObjDesc prefix.
+ Assert.Equal(
+ [
+ RuntimeInitialCreateExecutedActionKind.InitialAdoption,
+ RuntimeInitialCreateExecutedActionKind.ObjDesc,
+ RuntimeInitialCreateExecutedActionKind.Vector,
+ ],
+ receipt.Trace.Select(static a => a.Kind));
+ Assert.Equal(
+ [RuntimeEntityChange.Updated, RuntimeEntityChange.Updated],
+ observed);
+ Assert.Equal(0x08000002u, canonical.Snapshot.BasePaletteId);
+ Assert.Equal(new Vector3(7f, 8f, 9f), canonical.Snapshot.Physics!.Value.Velocity);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // ---------------------------------------------------------------
+ // C. Envelope atomicity
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void SameIncarnationEnvelopePublishesNothingUntilEveryStageCommitsThenPublishesInStageOrder()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 3UL);
+ const uint guid = 0x70022000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+
+ WorldSession.EntitySpawn sameCreate = Spawn(
+ guid, 1, includePosition: false, positionSequence: 2) with
+ {
+ Name = "same-incarnation",
+ };
+ PhysicsSpawnData physics = sameCreate.Physics!.Value;
+ sameCreate = sameCreate with
+ {
+ Physics = physics with
+ {
+ Timestamps = physics.Timestamps with { ObjDesc = 2, State = 2, Vector = 2 },
+ },
+ };
+ RuntimeEntityRegistrationResult same = lifetime
+ .RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
+ Assert.Equal(CreateObjectTimestampDisposition.ExistingGeneration, same.Inbound.Disposition);
+
+ var observed = new List();
+ int countAtFirstObservation = -1;
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ observed.Add(delta.Change);
+ countAtFirstObservation = observed.Count;
+ }));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ // No event fired until the whole envelope committed: the very first
+ // observation must already carry every publish, not a partial subset
+ // trickling in one at a time.
+ Assert.True(observed.Count >= 1);
+ Assert.Equal(observed.Count, countAtFirstObservation);
+
+ RuntimeInitialCreateExecutedActionKind[] envelopeStages = receipt.Trace
+ .Where(static a => a.Sequence == 1UL)
+ .Select(static a => a.Kind)
+ .ToArray();
+ // A same-Create with neither a parent nor a position decomposes into
+ // a Pickup branch (retail priority: Parent > Position > Pickup - see
+ // InboundPhysicsStateController.BuildSameGenerationEvents), plus the
+ // AP-119 PreTailDescriptionAdaptation stage this envelope always
+ // carries when SameGenerationEvents is present.
+ Assert.Equal(
+ [
+ RuntimeInitialCreateExecutedActionKind.PreTailDescriptionAdaptation,
+ RuntimeInitialCreateExecutedActionKind.ObjDesc,
+ RuntimeInitialCreateExecutedActionKind.Pickup,
+ RuntimeInitialCreateExecutedActionKind.State,
+ RuntimeInitialCreateExecutedActionKind.Vector,
+ RuntimeInitialCreateExecutedActionKind.WeenieDescription,
+ RuntimeInitialCreateExecutedActionKind.ResidentCellCleanup,
+ ],
+ envelopeStages);
+ // This entity never claimed a cell (PickedUp residence, then the
+ // envelope's own Pickup stage nulls the position again) - Gap 4(c),
+ // the no-cell-claimed destruction-mark disposition.
+ RuntimeInitialCreateExecutedAction cleanup = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ResidentCellCleanup);
+ Assert.Equal(
+ RuntimeResidentCellCleanupDisposition.CelllessNoWeenieMarkUnreachable,
+ cleanup.ResidentCellCleanupDisposition);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ [Fact]
+ public void ResidentCellCleanupUnmarksWhenCellClaimedAndAlreadyResident()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 18UL);
+ const uint guid = 0x7002A000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+ Assert.Equal(Cell, canonical.FullCellId);
+
+ // A same-Create carrying its own position: PositionSource ==
+ // SameIncarnationCreate makes effectiveContact always true for the
+ // non-local classify branch, and the entity is already resident
+ // (FullCellId == Cell, unchanged since neither Position's own
+ // refreshPosition:false merge nor WeenieDescription's touch it), so
+ // this classifies to Interpolate - no new placement, no yield.
+ WorldSession.EntitySpawn sameCreate = Spawn(guid, 1, positionSequence: 2, positionX: 15f);
+ PhysicsSpawnData physics = sameCreate.Physics!.Value;
+ sameCreate = sameCreate with
+ {
+ Physics = physics with
+ {
+ Timestamps = physics.Timestamps with { ObjDesc = 2 },
+ },
+ };
+ _ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
+
+ var inputs = new RuntimeInitialCreateExecutionInputs(
+ UsePositionFromServer: false, PlayerDistance: 10f);
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, inputs);
+
+ RuntimeInitialCreateExecutedAction cleanup = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ResidentCellCleanup);
+ Assert.Equal(
+ RuntimeResidentCellCleanupDisposition.ResidentUnmarked,
+ cleanup.ResidentCellCleanupDisposition);
+ Assert.Equal(Cell, canonical.FullCellId);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ [Fact]
+ public void ResidentCellCleanupFailsClosedWhenClaimedCelllessOutcomeIsNotUnderLostCellOwnership()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 19UL);
+ const uint guid = 0x7002A100u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ Assert.False(lease.Placement.IsValid);
+ Assert.Equal(0u, canonical.FullCellId);
+
+ // The same-Create's own position exists on the wire (claims a
+ // cell), but with UsePositionFromServer=false this LocalPlayer
+ // classify branch resolves to NoPositionOperation - no SetPosition
+ // ever begins, so nothing tracks a lost-cell/deferred operation
+ // for this entity either. The entity was never resident (PickedUp
+ // initial, no placement ever ran) - claimed + celless + NOT under
+ // lost-cell ownership is the exact invariant violation this test
+ // requires. Round 3 B1: this is now a typed abandonment (Abandon
+ // retires the residence and discards progress), never a throw
+ // escaping Execute - the ledger must still fully converge.
+ WorldSession.EntitySpawn sameCreate = Spawn(guid, 1, positionSequence: 2, positionX: 15f);
+ PhysicsSpawnData physics = sameCreate.Physics!.Value;
+ sameCreate = sameCreate with
+ {
+ Physics = physics with
+ {
+ Timestamps = physics.Timestamps with { ObjDesc = 2 },
+ },
+ };
+ _ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: true);
+
+ var inputs = new RuntimeInitialCreateExecutionInputs(
+ UsePositionFromServer: false, PlayerDistance: 0f);
+
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, inputs, out RuntimeInitialCreateExecutionReceipt receipt);
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.RejectedAuthority, status);
+ Assert.Equal(default, receipt);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.False(lifetime.TryGetInitialCreateResidence(canonical, out _));
+
+ // A retry with the now-retired token is a distinct, safe no-op -
+ // proving Abandon actually retired the residence rather than
+ // leaving it sitting fully current for a replay.
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedToken,
+ lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, inputs, out RuntimeInitialCreateExecutionReceipt retryReceipt));
+ Assert.Equal(default, retryReceipt);
+ }
+
+ [Fact]
+ public void EnvelopePositionStageRequiringSetPositionYieldsResumesAndPublishesOnceAfterCompletion()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 20UL);
+ const uint guid = 0x7002B000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ Assert.False(lease.Placement.IsValid);
+ Assert.Equal(0u, canonical.FullCellId);
+ AttachDormantBody(lifetime, canonical);
+
+ // Cellless (CommittedCellId == canonical.FullCellId == 0) forces
+ // SetPosition regardless of contact/distance for the non-local
+ // classify branch - this same-Create's OWN Position stage will
+ // require a real placement and yield mid-envelope.
+ WorldSession.EntitySpawn sameCreate = Spawn(guid, 1, positionSequence: 2, positionX: 15f);
+ PhysicsSpawnData physics = sameCreate.Physics!.Value;
+ sameCreate = sameCreate with
+ {
+ Physics = physics with
+ {
+ Timestamps = physics.Timestamps with { ObjDesc = 2, State = 2, Vector = 2 },
+ },
+ };
+ _ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
+
+ var observed = new List();
+ using IDisposable subscription = lifetime.Events.Subscribe(
+ new EntityObserver(delta => observed.Add(delta.Change)));
+
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt pending);
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
+ Assert.Equal(default, pending);
+ // ObjDesc already committed (buffered) before Position began its
+ // own placement lifecycle - prove NOTHING has published yet, even
+ // though a stage before the yield point already ran.
+ Assert.Empty(observed);
+
+ RuntimeEntityKey key = canonical.Key!.Value;
+ Assert.True(lifetime.InitialCreateExecution
+ .TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
+ Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPosition, route.Disposition);
+ CompletePendingContinuationPlacement(lifetime, key, route);
+ Assert.Empty(observed);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ // Every buffered per-stage event from this ONE envelope publishes
+ // exactly once, only now that the envelope fully committed.
+ Assert.Equal(
+ [
+ RuntimeEntityChange.Updated,
+ RuntimeEntityChange.Updated,
+ RuntimeEntityChange.Updated,
+ RuntimeEntityChange.Updated,
+ RuntimeEntityChange.Updated,
+ ],
+ observed);
+ RuntimeInitialCreateExecutedActionKind[] envelopeStages = receipt.Trace
+ .Where(static a => a.Sequence == 1UL)
+ .Select(static a => a.Kind)
+ .ToArray();
+ Assert.Equal(
+ [
+ RuntimeInitialCreateExecutedActionKind.PreTailDescriptionAdaptation,
+ RuntimeInitialCreateExecutedActionKind.ObjDesc,
+ RuntimeInitialCreateExecutedActionKind.Position,
+ RuntimeInitialCreateExecutedActionKind.State,
+ RuntimeInitialCreateExecutedActionKind.Vector,
+ RuntimeInitialCreateExecutedActionKind.WeenieDescription,
+ RuntimeInitialCreateExecutedActionKind.ResidentCellCleanup,
+ ],
+ envelopeStages);
+ RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ Assert.Equal(
+ RuntimeAuthoritativePositionDisposition.SetPosition,
+ positionAction.PositionDisposition);
+ // The continuation's own SetPosition committed a real cell -
+ // ResidentCellCleanup now sees a claimed, resident entity.
+ Assert.Equal(Cell, canonical.FullCellId);
+ RuntimeInitialCreateExecutedAction cleanup = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ResidentCellCleanup);
+ Assert.Equal(
+ RuntimeResidentCellCleanupDisposition.ResidentUnmarked,
+ cleanup.ResidentCellCleanupDisposition);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ }
+
+ // Gap 2 (failure injection between every same-Create envelope stage):
+ // the Position stage is the ONLY boundary inside a SameIncarnationCreate
+ // envelope that this suite can interrupt via a REAL yield. Every other
+ // stage kind - PreTailDescriptionAdaptation, ObjDesc, CreateParent/
+ // Parent/Pickup, Movement, State, Vector, WeenieDescription,
+ // ResidentCellCleanup - is a pure, synchronous, in-memory
+ // RuntimeEntityRecord/InboundPhysicsStateController mutation with NO
+ // async placement lifecycle and NO event dispatched until the whole
+ // envelope's buffered publish flush at the very end (see ApplyEnvelope):
+ // there is no callback, no I/O, and no point where control ever returns
+ // to a caller (or where an event observer could reenter) mid-stage.
+ // Injecting a synthetic "failure" between two purely synchronous stages
+ // would require adding a diagnostic seam to production code purely to
+ // make a test possible, which this task's own instructions forbid. The
+ // Position stage is structurally different: it is the one action kind
+ // whose disposition can require a real RuntimeSetPositionState
+ // placement round-trip (Begin/Watch/yield/host-prepare-submit-
+ // acknowledge/resume) - the SAME mechanism a standalone (non-envelope)
+ // Position continuation uses. The two tests below exhaust that
+ // reachable boundary: a successful resume (see
+ // EnvelopePositionStageRequiringSetPositionYieldsResumesAndPublishesOnceAfterCompletion
+ // above) and an abandonment at that exact boundary (immediately below).
+
+ [Fact]
+ public void EnvelopeAbandonedDuringPositionStageYieldPublishesNothingAndConvergesEveryLedger()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 21UL);
+ const uint guid = 0x7002B100u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ Assert.False(lease.Placement.IsValid);
+ AttachDormantBody(lifetime, canonical);
+
+ // Same recipe as the successful-resume test: cellless forces
+ // SetPosition, guaranteeing a mid-envelope yield after ObjDesc (and
+ // Position's own snapshot-merge commit) already ran.
+ WorldSession.EntitySpawn sameCreate = Spawn(guid, 1, positionSequence: 2, positionX: 15f);
+ PhysicsSpawnData physics = sameCreate.Physics!.Value;
+ sameCreate = sameCreate with
+ {
+ Physics = physics with
+ {
+ Timestamps = physics.Timestamps with { ObjDesc = 2, State = 2, Vector = 2 },
+ },
+ };
+ _ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
+
+ var observed = new List();
+ using IDisposable subscription = lifetime.Events.Subscribe(
+ new EntityObserver(delta => observed.Add(delta.Change)));
+
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out _);
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
+ Assert.Empty(observed);
+
+ // Abandon between Execute retries at the yield: delete the entity
+ // rather than ever completing the continuation's placement.
+ Assert.True(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(guid, canonical.Incarnation),
+ isLocalPlayer: false,
+ removeRetainedObject: true,
+ out RuntimeEntityDeleteAcceptance acceptance));
+ lifetime.CompleteAcceptedDelete(acceptance);
+
+ // The delete's own Deleted is the only observed event - NONE of the
+ // envelope's buffered stages (ObjDesc, and Position's own snapshot
+ // merge, both of which had ALREADY committed before the yield) ever
+ // publish. TryAcceptDelete's own unconditional Physics.SetPosition.Forget
+ // already cancels the still-pending continuation placement here
+ // (same key, any token); DiscardProgress ALSO forgets it
+ // defensively (see its remarks) for callers that retire a
+ // residence without going through the full delete path.
+ Assert.Equal([RuntimeEntityChange.Deleted], observed);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ Assert.Equal(
+ 0,
+ lifetime.Physics.SetPosition.CaptureOwnership().AcknowledgedPlacementCompletionCount);
+
+ // A stale Execute() with the original token, after the entity is
+ // gone, must not resurrect anything either.
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedToken,
+ lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
+ Assert.Equal(default, receipt);
+ }
+
+ [Fact]
+ public void EnvelopeStageRetryDoesNotDuplicateAlreadyCommittedStages()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 4UL);
+ const uint guid = 0x70022100u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+
+ WorldSession.EntitySpawn sameCreate = Spawn(
+ guid, 1, includePosition: false, positionSequence: 2);
+ PhysicsSpawnData physics = sameCreate.Physics!.Value;
+ sameCreate = sameCreate with
+ {
+ Physics = physics with
+ {
+ Timestamps = physics.Timestamps with { Vector = 2 },
+ },
+ };
+ _ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
+
+ // The first Execute call fully drains the envelope in one synchronous
+ // pass (no yield point exists in this envelope). A SECOND call with
+ // the same (now-stale, released) token must not resurrect or
+ // reapply anything.
+ RuntimeInitialCreateExecutionReceipt first = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+ ulong vectorAuthorityAfterFirst = canonical.VectorAuthorityVersion;
+
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedToken,
+ lifetime.InitialCreateExecution.Execute(canonical, lease.Token, NoContact, out _));
+ Assert.Equal(vectorAuthorityAfterFirst, canonical.VectorAuthorityVersion);
+ Assert.NotEmpty(first.Trace);
+ }
+
+ // ---------------------------------------------------------------
+ // D. Position routes
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void LocalOrdinaryPositionInterpolatesWithoutWorldPlacement()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 5UL);
+ const uint guid = 0x70023000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 15f);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: true, null, null, false, null,
+ out PositionTimestampDisposition disposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+
+ var inputs = new RuntimeInitialCreateExecutionInputs(
+ UsePositionFromServer: true, PlayerDistance: 0f);
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, inputs);
+
+ RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ Assert.Equal(
+ RuntimeAuthoritativePositionDisposition.Interpolate,
+ positionAction.PositionDisposition);
+ Assert.Equal(15f, canonical.Snapshot.Position!.Value.PositionX);
+ // Interpolate never runs a physics placement: the cell stays exactly
+ // what the initial SetPosition already committed.
+ Assert.Equal(Cell, canonical.FullCellId);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ // Round 4 R4-8: local ordinary route trace flags - CONSTRAIN-BEFORE
+ // (retail-notes.md function 3 line ~93041: ConstrainTo runs
+ // unconditionally BEFORE InterpolateTo on this route), unparent
+ // always runs on a received Position, and no teleport hook.
+ Assert.Equal(RuntimePositionConstrainPhase.BeforePositionOperation, positionAction.ConstrainPhase);
+ Assert.True(positionAction.UnparentBeforeRouting);
+ Assert.Equal(RuntimeTeleportHookPhase.None, positionAction.HookPhase);
+ }
+
+ [Fact]
+ public void LocalTeleportContinuationDrivesItsOwnAuthoredPlacementLifecycle()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 6UL);
+ const uint guid = 0x70023100u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 1, forcePositionSequence: 0, positionX: 40f);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: true, null, null, true, null,
+ out PositionTimestampDisposition disposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt pending);
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
+ Assert.Equal(default, pending);
+ // The snapshot's own position field is refreshed even before the
+ // physics placement resolves; the derived cell residency is not.
+ Assert.Equal(40f, canonical.Snapshot.Position!.Value.PositionX);
+ Assert.Equal(Cell, canonical.FullCellId);
+
+ RuntimeEntityKey key = canonical.Key!.Value;
+ Assert.True(lifetime.InitialCreateExecution
+ .TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
+ Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, route.Disposition);
+ CompletePendingContinuationPlacement(lifetime, key, route);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+ RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ Assert.Equal(
+ RuntimeAuthoritativePositionDisposition.SetPositionSimple,
+ positionAction.PositionDisposition);
+ // Round 4 R4-8: local teleport route trace flags - ZeroVelocity,
+ // CONSTRAIN-AFTER (retail: TeleportPlayer runs first, ConstrainTo
+ // second), the AfterPositionOperation teleport hook, and unparent
+ // (a received Position always unsets parent).
+ Assert.True(positionAction.ZeroVelocity);
+ Assert.Equal(RuntimePositionConstrainPhase.AfterPositionOperation, positionAction.ConstrainPhase);
+ Assert.Equal(RuntimeTeleportHookPhase.AfterPositionOperation, positionAction.HookPhase);
+ Assert.True(positionAction.UnparentBeforeRouting);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ }
+
+ [Fact]
+ public void RemoteNearContactPositionInterpolatesAfterInitialResidency()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 7UL);
+ const uint guid = 0x70023200u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 25f, isGrounded: true);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: false, null, null, false, null,
+ out PositionTimestampDisposition disposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+
+ var inputs = new RuntimeInitialCreateExecutionInputs(
+ UsePositionFromServer: false, PlayerDistance: 10f);
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, inputs);
+
+ RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ Assert.Equal(
+ RuntimeAuthoritativePositionDisposition.Interpolate,
+ positionAction.PositionDisposition);
+ Assert.Equal(25f, canonical.Snapshot.Position!.Value.PositionX);
+ Assert.Equal(Cell, canonical.FullCellId);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ // Round 4 R4-8: remote near route trace flag - constrain-after
+ // (remote route: MoveOrTeleport runs first, ConstrainTo second).
+ Assert.Equal(RuntimePositionConstrainPhase.AfterPositionOperation, positionAction.ConstrainPhase);
+ }
+
+ // Round 4 R4-7: wire IsGrounded=true disagrees with the body's OWN
+ // contact bit (forced false) - the local ordinary interpolate gate must
+ // follow the WIRE fact, not the body, per Round 3 A3.
+ [Fact]
+ public void LocalOrdinaryPositionRouteFollowsWireGroundedTrueWhenBodyContactIsFalse()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 97UL);
+ const uint guid = 0x70039000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+ // Force the body's OWN contact bit to DISAGREE with the wire fact
+ // below - the route must ignore this.
+ ForceContact(canonical, inContact: false);
+
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 15f,
+ isGrounded: true);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: true, null, null, false, null,
+ out PositionTimestampDisposition disposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+
+ var inputs = new RuntimeInitialCreateExecutionInputs(
+ UsePositionFromServer: true, PlayerDistance: 0f);
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, inputs);
+
+ RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ Assert.Equal(
+ RuntimeAuthoritativePositionDisposition.Interpolate,
+ positionAction.PositionDisposition);
+ }
+
+ // Round 4 R4-7: wire IsGrounded=false disagrees with the body's OWN
+ // contact bit (forced true) - the remote effective-contact gate must
+ // follow the WIRE fact, not the body.
+ [Fact]
+ public void RemotePositionRouteFollowsWireGroundedFalseWhenBodyContactIsTrue()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 98UL);
+ const uint guid = 0x70039100u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+ // Force the body's OWN contact bit to DISAGREE with the wire fact
+ // below - the route must ignore this.
+ ForceContact(canonical, inContact: true);
+
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 25f,
+ isGrounded: false);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: false, null, null, false, null,
+ out PositionTimestampDisposition disposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+
+ var inputs = new RuntimeInitialCreateExecutionInputs(
+ UsePositionFromServer: false, PlayerDistance: 10f);
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, inputs);
+
+ RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ Assert.Equal(
+ RuntimeAuthoritativePositionDisposition.NoPositionOperation,
+ positionAction.PositionDisposition);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ }
+
+ [Fact]
+ public void RemoteFarPositionStopsInterpolatingAndRunsSetPositionSimple()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 8UL);
+ const uint guid = 0x70023300u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 25f);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: false, null, null, false, null,
+ out PositionTimestampDisposition disposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+
+ var inputs = new RuntimeInitialCreateExecutionInputs(
+ UsePositionFromServer: false, PlayerDistance: 200f);
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, inputs, out _);
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
+
+ RuntimeEntityKey key = canonical.Key!.Value;
+ Assert.True(lifetime.InitialCreateExecution
+ .TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
+ Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, route.Disposition);
+ Assert.True(route.StopInterpolating);
+ CompletePendingContinuationPlacement(lifetime, key, route);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, inputs);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ // Round 4 R4-8: remote far route trace flags - StopInterpolating
+ // and constrain-after.
+ Assert.True(positionAction.StopInterpolating);
+ Assert.Equal(RuntimePositionConstrainPhase.AfterPositionOperation, positionAction.ConstrainPhase);
+ }
+
+ [Fact]
+ public void ParentedInitialCreateNeverRunsAWorldPlacementThroughTheExecutor()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 9UL);
+ const uint parentGuid = 0x70024100u;
+ const uint guid = 0x70024000u;
+ _ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ Assert.False(lease.Placement.IsValid);
+ Assert.Equal(RuntimeAuthoritativePositionDisposition.AwaitFreshPosition, lease.Route.Disposition);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Equal(0u, receipt.FullCellId);
+ Assert.Equal(RuntimeTeleportHookPhase.None, receipt.TeleportHookPhase);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ [Fact]
+ public void ForcePositionContinuationRecordsSetPositionSimpleWithPreservedHeadingAndNoParentClear()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 45UL);
+ const uint guid = 0x7002F700u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ // Round 4 R4-12: give the entity a parent attachment BEFORE the
+ // ForcePosition update arrives, to structurally pin the merge
+ // body's deliberate combined shape - retail's FORCE_POSITION Gate A
+ // returns before CPhysicsObj::unset_parent ever runs, so the
+ // parent attachment must survive alongside the newly-applied
+ // Position after this drain.
+ const uint parentGuid = 0x7002F701u;
+ const uint parentLocation = 3u;
+ Assert.True(lifetime.Entities.TryCommitParent(
+ guid, parentGuid, parentLocation, placementId: 0u, positionSequence: 1,
+ out WorldSession.EntitySpawn parented));
+ lifetime.Entities.RefreshSnapshot(canonical, parented);
+
+ // isLocalPlayer + a fresh FORCE_POSITION_TS (1, newer than the
+ // seeded 0) + an EQUAL teleport sequence (0 == gate's current 0)
+ // blips immediately per PhysicsTimestampGate.TryAcceptPositionEvent.
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 1, positionX: 40f);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: true, null, null, false, null,
+ out PositionTimestampDisposition disposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
+
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out _);
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
+ RuntimeEntityKey key = canonical.Key!.Value;
+ Assert.True(lifetime.InitialCreateExecution
+ .TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
+ Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, route.Disposition);
+ CompletePendingContinuationPlacement(lifetime, key, route);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ Assert.Equal(
+ RuntimeAuthoritativePositionDisposition.SetPositionSimple,
+ positionAction.PositionDisposition);
+ Assert.True(positionAction.PreserveHeading);
+ Assert.True(positionAction.SendPositionImmediately);
+ Assert.False(positionAction.StopInterpolating);
+ Assert.False(positionAction.ZeroVelocity);
+ Assert.Equal(RuntimePositionConstrainPhase.None, positionAction.ConstrainPhase);
+ // The FORCE_POSITION branch precedes unset_parent in retail's
+ // MoveOrTeleport - no parent clearing here.
+ Assert.False(positionAction.UnparentBeforeRouting);
+ // Round 4 R4-12: structurally pin the deliberate combined shape -
+ // Position AND the pre-existing Parent attachment are BOTH
+ // non-null after this merge.
+ Assert.NotNull(canonical.Snapshot.Position);
+ Assert.Equal(40f, canonical.Snapshot.Position!.Value.PositionX);
+ Assert.Equal(parentGuid, canonical.Snapshot.ParentGuid);
+ Assert.Equal(parentLocation, canonical.Snapshot.ParentLocation);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ }
+
+ [Fact]
+ public void MissileFlaggedEntityPositionContinuationClassifiesToProjectileAuthoritativeOperationKind()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 46UL);
+ const uint guid = 0x7002F800u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, missile: true),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ Assert.Equal(
+ RuntimeSetPositionOperationKind.ProjectileAuthoritative,
+ lease.Route.OperationKind);
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ // Already resident + far distance forces SetPositionSimple (the
+ // same "remote-shaped" branch a Projectile entity kind shares with
+ // Remote - the classifier has no Projectile-specific branch, only a
+ // Projectile-specific OperationKind mapping).
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 25f);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: false, null, null, false, null,
+ out PositionTimestampDisposition disposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+
+ var inputs = new RuntimeInitialCreateExecutionInputs(
+ UsePositionFromServer: false, PlayerDistance: 200f);
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, inputs, out _);
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
+
+ RuntimeEntityKey key = canonical.Key!.Value;
+ Assert.True(lifetime.InitialCreateExecution
+ .TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
+ Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, route.Disposition);
+ Assert.Equal(RuntimeSetPositionOperationKind.ProjectileAuthoritative, route.OperationKind);
+ Assert.True(route.StopInterpolating);
+ CompletePendingContinuationPlacement(lifetime, key, route);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, inputs);
+ Assert.NotEmpty(receipt.Trace);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ // Round 4 R4-8: projectile route trace flags - same StopInterpolating
+ // + constrain-after shape as the remote-far branch (the classifier
+ // has no Projectile-specific branch, only an OperationKind mapping).
+ RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ Assert.True(positionAction.StopInterpolating);
+ Assert.Equal(RuntimePositionConstrainPhase.AfterPositionOperation, positionAction.ConstrainPhase);
+ }
+
+ // Contrasts with ParentedInitialCreateNeverRunsAWorldPlacementThroughTheExecutor:
+ // the PickedUp residence kind (no parent AND no position) is the OTHER
+ // branch that classifies to AwaitFreshPosition at Begin/Create time (see
+ // RuntimeInitialCreateResidenceState.Begin's residence-kind switch).
+ // NOTE (mandated-case coverage gap): a standalone Position CONTINUATION
+ // (as opposed to this initial-lease route) can never itself classify to
+ // AwaitFreshPosition - RuntimeAuthoritativePositionRouteClassifier.
+ // ClassifyAcceptedPosition (the only classifier ApplyPositionAction ever
+ // calls) has no branch that returns AwaitFreshPosition; that disposition
+ // is produced exclusively by ClassifyCreate (Parented/PickedUp) and
+ // ClassifyLeaveWorld (neither of which ApplyPositionAction calls). A
+ // parented/picked entity's raw Position wire events are retained as
+ // Position continuations exactly like any other entity's and are
+ // classified with the SAME Remote/LocalPlayer logic once drained - the
+ // "picked" or "parented" fact plays no role in that classification. This
+ // is a structural property of the current classifier, not a gap in this
+ // test suite; the reachable form of "AwaitFreshPosition for a
+ // parented/picked entity" is the INITIAL LEASE route exercised here and
+ // by ParentedInitialCreateNeverRunsAWorldPlacementThroughTheExecutor.
+ [Fact]
+ public void PickedUpInitialCreateNeverRunsAWorldPlacementThroughTheExecutor()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 47UL);
+ const uint guid = 0x7002F900u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ Assert.False(lease.Placement.IsValid);
+ Assert.Equal(RuntimeAuthoritativePositionDisposition.AwaitFreshPosition, lease.Route.Disposition);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Equal(0u, receipt.FullCellId);
+ Assert.Equal(RuntimeTeleportHookPhase.None, receipt.TeleportHookPhase);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // Round 3 B10 (execution-time-rejected retained Position): admission
+ // ACCEPTED (disposition Apply - POSITION_TS genuinely advanced), but
+ // execution-time classification REJECTS on a nonfinite derived distance.
+ // The gate-consumed timestamps must still land in the snapshot; no pose
+ // ever applies.
+ [Fact]
+ public void ExecutionTimeRejectedPositionStampsRetainedTimestampsWithoutPoseApplication()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 48UL);
+ const uint guid = 0x7002FA00u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+ Assert.Equal(Cell, canonical.FullCellId);
+ float originalPositionX = canonical.Snapshot.Position!.Value.PositionX;
+
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 25f);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: false, null, null, false, null,
+ out PositionTimestampDisposition disposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+
+ var inputs = new RuntimeInitialCreateExecutionInputs(
+ UsePositionFromServer: false, PlayerDistance: float.NaN);
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, inputs);
+
+ RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ Assert.Equal(
+ RuntimeAuthoritativePositionDisposition.RejectedData,
+ positionAction.PositionDisposition);
+ // The retained Position/Teleport/ForcePosition channels the gate
+ // actually moved land in the snapshot ...
+ Assert.Equal(2, canonical.Snapshot.Physics!.Value.Timestamps.Position);
+ Assert.Equal(0, canonical.Snapshot.Physics!.Value.Timestamps.Teleport);
+ Assert.Equal(0, canonical.Snapshot.Physics!.Value.Timestamps.ForcePosition);
+ // ... but no pose ever applies.
+ Assert.Equal(originalPositionX, canonical.Snapshot.Position!.Value.PositionX);
+ Assert.Equal(Cell, canonical.FullCellId);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // ---------------------------------------------------------------
+ // E. Missing-parent raw deferred-create replay
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void MissingParentReplayConsumesExactAdmissionIdAndRegistersChildThroughCanonicalRoute()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 10UL);
+ const uint parentGuid = 0x70025100u;
+ const uint childGuid = 0x70025000u;
+ RuntimeEntityRegistrationResult deferred = lifetime.RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false);
+ Assert.True(deferred.DeferredForParent);
+ Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent,
+ out RuntimeInitialCreateResidenceLease parentLease));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, parent, parentLease.Token, NoContact);
+
+ Assert.Equal(1, receipt.ReplayedDeferredChildCount);
+ RuntimeInitialCreateExecutedAction replay = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.DeferredChildReplay);
+ Assert.Equal(RuntimeDeferredChildReplayOutcome.Registered, replay.DeferredChildOutcome);
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord child));
+ Assert.True(lifetime.TryGetInitialCreateResidence(child, out _));
+ }
+
+ [Fact]
+ public void StaleAdmissionIdCannotConsumeAReplacementQueuedAfterThePeek()
+ {
+ var parents = new ParentAttachmentState();
+ const uint parentGuid = 0x70025200u;
+ WorldSession.EntitySpawn spawn = Spawn(
+ 0x70025300u, 1, includePosition: false, parentGuid: parentGuid);
+ parents.EnqueueDeferredCreate(spawn, isLocalPlayer: false);
+ Assert.True(parents.TryPeekDeferredCreate(parentGuid, out DeferredParentCreate stale));
+
+ parents.Clear();
+ parents.EnqueueDeferredCreate(spawn, isLocalPlayer: false);
+ Assert.True(parents.TryPeekDeferredCreate(parentGuid, out DeferredParentCreate replacement));
+ Assert.NotEqual(stale.AdmissionId, replacement.AdmissionId);
+
+ Assert.False(parents.ConsumeDeferredCreate(parentGuid, stale));
+ Assert.Equal(1, parents.DeferredCreateCount);
+ Assert.True(parents.ConsumeDeferredCreate(parentGuid, replacement));
+ Assert.Equal(0, parents.DeferredCreateCount);
+ }
+
+ // Round 3 B7: multiple children queued behind the SAME missing parent
+ // all replay atomically through the executor's whole-bucket detach
+ // (ParentAttachmentState.DetachDeferredCreates), in FIFO admission
+ // order, exercised through the canonical RunInitialTail path rather than
+ // against ParentAttachmentState directly.
+ [Fact]
+ public void MultipleChildrenQueuedBehindOneMissingParentReplayInFifoOrderThroughTheExecutor()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 32UL);
+ const uint parentGuid = 0x7002E100u;
+ const uint firstChildGuid = 0x7002E000u;
+ const uint secondChildGuid = 0x7002E001u;
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(firstChildGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(secondChildGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.Equal(2, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent, out RuntimeInitialCreateResidenceLease parentLease));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, parent, parentLease.Token, NoContact);
+
+ Assert.Equal(2, receipt.ReplayedDeferredChildCount);
+ RuntimeInitialCreateExecutedAction[] replays = receipt.Trace
+ .Where(static a => a.Kind is RuntimeInitialCreateExecutedActionKind.DeferredChildReplay)
+ .ToArray();
+ Assert.Equal(2, replays.Length);
+ Assert.All(
+ replays,
+ static a => Assert.Equal(RuntimeDeferredChildReplayOutcome.Registered, a.DeferredChildOutcome));
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ Assert.True(lifetime.Entities.TryGetActive(firstChildGuid, out _));
+ Assert.True(lifetime.Entities.TryGetActive(secondChildGuid, out _));
+ }
+
+ // Round 4 R4-1: one child's registration THROWING must not strand the
+ // remaining siblings or escape Execute as an exception. Three children
+ // queued behind one missing parent; the SECOND child's own registration
+ // callback is made to throw (via a reflection-swapped
+ // _registerDeferredChild delegate - the standard fault-injection
+ // technique this file already uses for private-state pokes, e.g.
+ // SetCompletedAdoptionRevision). The third child must still register.
+ [Fact]
+ public void DeferredChildReplayContainsOneChildsThrowingRegistrationAndContinuesWithSiblings()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 92UL);
+ const uint parentGuid = 0x70035000u;
+ const uint firstChildGuid = 0x70035001u;
+ const uint secondChildGuid = 0x70035002u;
+ const uint thirdChildGuid = 0x70035003u;
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(firstChildGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(secondChildGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(thirdChildGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.Equal(3, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ WrapDeferredChildRegistration(lifetime, (original, spawn, isLocalPlayer) =>
+ {
+ if (spawn.Guid == secondChildGuid)
+ {
+ throw new InvalidOperationException(
+ "Injected R4-1 containment test failure.");
+ }
+ return original(spawn, isLocalPlayer);
+ });
+
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent, out RuntimeInitialCreateResidenceLease parentLease));
+
+ // No exception escapes Execute - it returns a typed status.
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, parent, parentLease.Token, NoContact);
+
+ Assert.Equal(3, receipt.ReplayedDeferredChildCount);
+ RuntimeInitialCreateExecutedAction[] replays = receipt.Trace
+ .Where(static a => a.Kind is RuntimeInitialCreateExecutedActionKind.DeferredChildReplay)
+ .ToArray();
+ Assert.Equal(3, replays.Length);
+ Assert.Equal(RuntimeDeferredChildReplayOutcome.Registered, replays[0].DeferredChildOutcome);
+ Assert.Equal(RuntimeDeferredChildReplayOutcome.Rejected, replays[1].DeferredChildOutcome);
+ Assert.Equal(RuntimeDeferredChildReplayOutcome.Registered, replays[2].DeferredChildOutcome);
+ Assert.True(lifetime.Entities.TryGetActive(firstChildGuid, out _));
+ Assert.False(lifetime.Entities.TryGetActive(secondChildGuid, out _));
+ Assert.True(lifetime.Entities.TryGetActive(thirdChildGuid, out _));
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ // Round 5 R5-3: the contained exception is recorded on the
+ // observable failure surface, not silently swallowed.
+ Assert.Equal(1, lifetime.CaptureOwnership().ReplayFailureCount);
+ Assert.True(lifetime.CaptureOwnership().HasLastReplayFailure);
+ }
+
+ // Round 4 R4-1: a mid-loop abandonment (the parent entity is no longer
+ // current) must RESTORE the unprocessed remainder rather than
+ // permanently destroy it. Two children queued behind one missing
+ // parent; the FIRST child's registration callback reentrantly deletes
+ // the PARENT (simulating a synchronous observer reaction fired from
+ // within registration). The second child's raw Create must be back in
+ // the deferred bucket afterward (observable via ContainsDeferredCreate),
+ // and every ledger must still converge.
+ [Fact]
+ public void DeferredChildReplayRestoresTheUnprocessedRemainderWhenTheParentIsDeletedReentrantlyMidReplay()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 93UL);
+ const uint parentGuid = 0x70036000u;
+ const uint firstChildGuid = 0x70036001u;
+ const uint secondChildGuid = 0x70036002u;
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(firstChildGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(secondChildGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.Equal(2, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ WrapDeferredChildRegistration(lifetime, (original, spawn, isLocalPlayer) =>
+ {
+ RuntimeEntityRegistrationResult result = original(spawn, isLocalPlayer);
+ if (spawn.Guid == firstChildGuid)
+ {
+ Assert.True(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(parentGuid, 1),
+ isLocalPlayer: false,
+ removeRetainedObject: true,
+ out RuntimeEntityDeleteAcceptance acceptance));
+ lifetime.CompleteAcceptedDelete(acceptance);
+ }
+ return result;
+ });
+
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent, out RuntimeInitialCreateResidenceLease parentLease));
+
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedAuthority,
+ lifetime.InitialCreateExecution.Execute(
+ parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
+ Assert.Equal(default, receipt);
+
+ // Child 1 legitimately registered before the reentrant delete
+ // fired; child 2's raw Create is restored to the deferred bucket
+ // rather than lost.
+ Assert.True(lifetime.Entities.TryGetActive(firstChildGuid, out RuntimeEntityRecord firstChild));
+ Assert.True(lifetime.Entities.ParentAttachments.ContainsDeferredCreate(secondChildGuid, 1));
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ // Only the PARENT's own residence/progress was touched by this
+ // abandoned Execute call (matching the existing
+ // DeleteDuringDeferredChildReplayAbandonsExecutionWithoutResurrection
+ // precedent) - child 1 is a distinct entity the deferred replay
+ // legitimately registered through the canonical route before the
+ // reentrant delete happened; its own (never executed) residence
+ // lease correctly remains open, accounting for the sole surviving
+ // lease count.
+ Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.True(lifetime.TryGetInitialCreateResidence(firstChild, out _));
+ }
+
+ ///
+ /// Round 4 R4-1 test-only fault injection: swaps the executor's private
+ /// _registerDeferredChild delegate for one that wraps the
+ /// original, matching this file's existing reflection-based
+ /// private-state pokes (e.g. SetCompletedAdoptionRevision below).
+ ///
+ private static void WrapDeferredChildRegistration(
+ RuntimeEntityObjectLifetime lifetime,
+ Func<
+ Func,
+ WorldSession.EntitySpawn,
+ bool,
+ RuntimeEntityRegistrationResult> wrapper)
+ {
+ Type executorType = typeof(RuntimeInitialCreateContinuationExecutor);
+ System.Reflection.FieldInfo field = executorType.GetField(
+ "_registerDeferredChild",
+ System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;
+ var original = (Func)
+ field.GetValue(lifetime.InitialCreateExecution)!;
+ Func wrapped =
+ (spawn, isLocalPlayer) => wrapper(original, spawn, isLocalPlayer);
+ field.SetValue(lifetime.InitialCreateExecution, wrapped);
+ }
+
+ ///
+ /// Round 5 R5-1 test helper: both RuntimeEntityObjectLifetime.
+ /// TryApplyParent (line ~908) and RegisterEntityCore's own
+ /// missing-parent gate divert an UNADDRESSABLE-at-admission parent
+ /// relation to a DIFFERENT mechanism entirely (the legacy unresolved-
+ /// wait queue, or a raw deferred Create) - neither ever reaches the
+ /// residence continuation FIFO this round's executor dispatch code
+ /// operates on. The scenarios this round's tests exercise (drain-time/
+ /// replay-time staleness) all require the parent to be ADDRESSABLE at
+ /// the moment of admission and only become stale/unaddressable
+ /// afterward - exactly the shape the original Round 3/4 test used.
+ /// This helper registers a real parent at
+ /// , admits the standalone
+ /// Parent continuation naming it, then removes the parent from the
+ /// ACTIVE directory directly (RuntimeEntityDirectory.RemoveActive)
+ /// rather than through the full DeleteObject-acceptance ceremony - a
+ /// real delete retains a permanent per-(guid,generation) teardown
+ /// tombstone (RuntimeEntityDirectory.RetainTeardown), which would
+ /// collide if a later test step re-registers the SAME guid at the SAME
+ /// incarnation (an invalid combination this scenario has no reason to
+ /// exercise - retail incarnations only ever increase). RemoveActive
+ /// makes the parent unaddressable for TryGetActive purposes
+ /// without that permanent marker, while the gate stays intact so a
+ /// LATER same-incarnation re-registration takes the codebase's own
+ /// existing "recovered CreateObject" ExistingGeneration path.
+ ///
+ private static void AdmitThenOrphanParentRelation(
+ RuntimeEntityObjectLifetime lifetime,
+ uint parentGuid,
+ uint childGuid,
+ ushort namedParentIncarnation,
+ uint parentLocation = 1u)
+ {
+ // includePosition:false - besides needing no physics engine, this
+ // also matters for the "same incarnation returns" scenario: a
+ // position-bearing retained snapshot would carry over through
+ // MergeUntimestampedCreate's Position=retained.Position copy when
+ // the SAME (guid, incarnation) is later re-registered on the
+ // "recovered CreateObject" ExistingGeneration path, reclassifying
+ // the recovered residence as TopLevel (SetPosition) instead of
+ // PickedUp (AwaitFreshPosition) and stalling RunToCompletion on an
+ // unrequested placement ack.
+ RuntimeEntityRecord parent = lifetime.RegisterEntity(
+ Spawn(parentGuid, namedParentIncarnation, includePosition: false)).Canonical!;
+ var parentUpdate = new ParentEvent.Parsed(
+ parentGuid,
+ childGuid,
+ ParentLocation: parentLocation,
+ PlacementId: 0u,
+ ParentInstanceSequence: namedParentIncarnation,
+ ChildPositionSequence: 2);
+ Assert.True(lifetime.TryApplyParent(parentUpdate, null, out _));
+ Assert.True(lifetime.Entities.RemoveActive(parent));
+ }
+
+ // Round 3 B9 revalidated a standalone Parent continuation's parent
+ // incarnation at EXECUTION time, not just admission time - admission
+ // succeeds while the parent is still active; the parent is then
+ // deleted before the drain reaches this continuation. Round 4 R4-5
+ // pinned the mismatch outcome as a DISCARD; Round 5 R5-1 OVERTURNS
+ // that with hard retail evidence (QueueBlobForObject, pseudo-C 92326;
+ // GUID-keyed CObjectMaint bucket, 271082-271088) - retail QUEUES a
+ // relation whose parent is unaddressable under the PARENT's guid and
+ // replays it when that guid is created; it never discards on this
+ // path. This test now covers R5-1's mandated "parent-returns -> queued
+ // relation applies exactly once" scenario end-to-end.
+ [Fact]
+ public void ParentContinuationRevalidatesLiveParentAtExecutionAndQueuesOnMismatchThenAppliesWhenTheParentArrives()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 33UL);
+ const uint parentGuid = 0x7002E200u;
+ const uint childGuid = 0x7002E300u;
+ _ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+
+ var parentUpdate = new ParentEvent.Parsed(
+ parentGuid,
+ childGuid,
+ ParentLocation: 1u,
+ PlacementId: 0u,
+ ParentInstanceSequence: 1,
+ ChildPositionSequence: 2);
+ // Admission succeeds - the parent is still active at this moment.
+ Assert.True(lifetime.TryApplyParent(parentUpdate, null, out _));
+
+ // The parent is deleted BEFORE the drain ever reaches this
+ // continuation - admission-time validation cannot see this.
+ Assert.True(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(parentGuid, 1),
+ isLocalPlayer: false,
+ removeRetainedObject: true,
+ out RuntimeEntityDeleteAcceptance acceptance));
+ lifetime.CompleteAcceptedDelete(acceptance);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ // Queued (retail-faithful), not discarded: the residence still
+ // converges cleanly and the child's own canonical record survives.
+ RuntimeInitialCreateExecutedAction parentAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Parent);
+ Assert.Equal(
+ RuntimeParentRelationOutcome.DeferredAwaitingParent,
+ parentAction.ParentRelationOutcome);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.True(lifetime.Entities.TryGetActive(childGuid, out _));
+ Assert.True(lifetime.Entities.ParentAttachments
+ .ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
+ Assert.Null(canonical.Snapshot.ParentGuid);
+ }
+
+ // Round 5 R5-1 mandated: "parent-returns -> queued relation applies
+ // exactly once" (trace + attach), end-to-end. Uses
+ // AdmitThenOrphanParentRelation (RemoveActive, not a full
+ // DeleteObject-acceptance) specifically so the SAME (guid, incarnation)
+ // can validly reappear afterward through the codebase's own existing
+ // "recovered CreateObject" ExistingGeneration path, without an
+ // artificial teardown-tombstone collision the scenario has no reason
+ // to exercise.
+ [Fact]
+ public void DeferredAcceptedParentRelationAppliesExactlyOnceWhenTheSameParentIncarnationReturns()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 113UL);
+ const uint parentGuid = 0x70049000u;
+ const uint childGuid = 0x70049001u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ AdmitThenOrphanParentRelation(lifetime, parentGuid, childGuid, namedParentIncarnation: 1);
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+ RuntimeInitialCreateExecutedAction parentAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Parent);
+ Assert.Equal(RuntimeParentRelationOutcome.DeferredAwaitingParent, parentAction.ParentRelationOutcome);
+ Assert.True(lifetime.Entities.ParentAttachments
+ .ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
+
+ // The SAME parent incarnation (1) reappears - the queued relation
+ // replays and applies exactly once.
+ RuntimeEntityRecord recreatedParent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ recreatedParent, out RuntimeInitialCreateResidenceLease parentLease));
+ RuntimeInitialCreateExecutionReceipt parentReceipt = RunToCompletion(
+ lifetime, recreatedParent, parentLease.Token, NoContact);
+
+ RuntimeInitialCreateExecutedAction replayAction = Assert.Single(
+ parentReceipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ParentRelationReplay);
+ Assert.Equal(RuntimeParentRelationOutcome.Applied, replayAction.ParentRelationOutcome);
+ // Applied commits the SAME position-timestamp-only merge a live
+ // Parent continuation commits (already stamped at the relation's
+ // original drain, before it was queued) - it does not itself set
+ // ParentGuid/ParentLocation, matching
+ // StandaloneParentContinuationAppliesPositionTimestampOnlyMergeWithNoReDefer's
+ // established precedent that the actual attach commit is
+ // TryCommitParent's (App-layer EquippedChildRenderController's) job.
+ Assert.Equal(2, canonical.Snapshot.Physics!.Value.Timestamps.Position);
+ Assert.Null(canonical.Snapshot.ParentGuid);
+ Assert.False(lifetime.Entities.ParentAttachments
+ .ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
+ Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // Round 5 R5-1 mandated: the arriving parent's incarnation is NEWER
+ // than the one the relation named - discard per Resolve's own rule
+ // (the current live parent supersedes the packet).
+ [Fact]
+ public void DeferredAcceptedParentRelationDiscardsWhenTheArrivingParentIsANewerIncarnationThanTheRelationNamed()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 104UL);
+ const uint parentGuid = 0x7003F000u;
+ const uint childGuid = 0x7003F001u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ // Admits the standalone Parent continuation while the parent IS
+ // addressable (required - see AdmitThenOrphanParentRelation's
+ // remarks), then deletes the parent so the child's OWN drain
+ // discovers it stale and queues the relation.
+ AdmitThenOrphanParentRelation(lifetime, parentGuid, childGuid, namedParentIncarnation: 1);
+ _ = RunToCompletion(lifetime, canonical, lease.Token, NoContact);
+ Assert.True(lifetime.Entities.ParentAttachments
+ .ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
+
+ // Parent arrives directly at incarnation 2 - NEWER than the
+ // relation's named incarnation 1.
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 2, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent, out RuntimeInitialCreateResidenceLease parentLease));
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, parent, parentLease.Token, NoContact);
+
+ RuntimeInitialCreateExecutedAction replayAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ParentRelationReplay);
+ Assert.Equal(RuntimeParentRelationOutcome.DiscardedStaleParent, replayAction.ParentRelationOutcome);
+ Assert.False(lifetime.Entities.ParentAttachments
+ .ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
+ Assert.Null(canonical.Snapshot.ParentGuid);
+ Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
+ }
+
+ // Round 5 R5-1 mandated: the arriving parent's incarnation is OLDER
+ // than the one the relation named - stays queued (wait), then applies
+ // on the NEXT matching incarnation. This scenario is not constructible
+ // through the normal admission ceremony: naming ParentInstanceSequence
+ // 5 in a Parent continuation requires the LIVE parent to already BE at
+ // instance 5 at admission time (TryApplyParent's own gate), which
+ // pins the parent's OWN PhysicsTimestampGate at 5 - a client can never
+ // subsequently see that SAME guid at an OLDER instance 3 afterward
+ // (retail incarnations are per-guid monotonic; the gate enforces it).
+ // This test instead enqueues the relation directly through
+ // ParentAttachmentState's own public API (the same technique the ABA
+ // test below uses) to isolate ApplyReplayedParentRelation's OWN
+ // incarnation-compare logic from that inapplicable precondition.
+ [Fact]
+ public void DeferredAcceptedParentRelationStaysQueuedWhenTheArrivingParentIsOlderThanTheRelationNamedAndAppliesOnTheNextMatchingIncarnation()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 105UL);
+ const uint parentGuid = 0x70040000u;
+ const uint childGuid = 0x70040001u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ _ = RunToCompletion(lifetime, canonical, lease.Token, NoContact);
+ var parentUpdate = new ParentEvent.Parsed(
+ parentGuid, childGuid, ParentLocation: 1u, PlacementId: 0u,
+ ParentInstanceSequence: 5, ChildPositionSequence: 2);
+ lifetime.Entities.ParentAttachments.EnqueueDeferredAcceptedRelation(
+ childGuid, canonical.Key!.Value, parentUpdate, null, default);
+
+ // Parent arrives at incarnation 3 - OLDER than the relation's
+ // named incarnation 5.
+ RuntimeEntityRecord parentAtThree = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 3, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parentAtThree, out RuntimeInitialCreateResidenceLease leaseThree));
+ RuntimeInitialCreateExecutionReceipt receiptThree = RunToCompletion(
+ lifetime, parentAtThree, leaseThree.Token, NoContact);
+ RuntimeInitialCreateExecutedAction replayThree = Assert.Single(
+ receiptThree.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ParentRelationReplay);
+ Assert.Equal(RuntimeParentRelationOutcome.DeferredAwaitingParent, replayThree.ParentRelationOutcome);
+ Assert.True(lifetime.Entities.ParentAttachments
+ .ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
+
+ // Parent recreated at incarnation 5 - now matches.
+ RuntimeEntityRecord parentAtFive = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 5, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parentAtFive, out RuntimeInitialCreateResidenceLease leaseFive));
+ RuntimeInitialCreateExecutionReceipt receiptFive = RunToCompletion(
+ lifetime, parentAtFive, leaseFive.Token, NoContact);
+ RuntimeInitialCreateExecutedAction replayFive = Assert.Single(
+ receiptFive.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ParentRelationReplay);
+ Assert.Equal(RuntimeParentRelationOutcome.Applied, replayFive.ParentRelationOutcome);
+ // Applied never sets ParentGuid itself - see
+ // StandaloneParentContinuationAppliesPositionTimestampOnlyMergeWithNoReDefer's
+ // established precedent (the actual attach commit is
+ // TryCommitParent's job, out of scope for this residence drain).
+ Assert.Null(canonical.Snapshot.ParentGuid);
+ Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
+ }
+
+ // Round 5 R5-1 mandated: the child is deleted while its relation is
+ // still queued (parent never showed up) - the entry is cancelled and
+ // every ledger converges.
+ [Fact]
+ public void DeferredAcceptedParentRelationIsCancelledWhenTheChildIsDeletedWhileQueued()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 106UL);
+ const uint parentGuid = 0x70041000u;
+ const uint childGuid = 0x70041001u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ AdmitThenOrphanParentRelation(lifetime, parentGuid, childGuid, namedParentIncarnation: 1);
+ _ = RunToCompletion(lifetime, canonical, lease.Token, NoContact);
+ Assert.Equal(1, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
+
+ Assert.True(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(childGuid, 1),
+ isLocalPlayer: false,
+ removeRetainedObject: true,
+ out RuntimeEntityDeleteAcceptance acceptance));
+ lifetime.CompleteAcceptedDelete(acceptance);
+
+ Assert.False(lifetime.Entities.ParentAttachments
+ .ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
+ Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
+ }
+
+ // Round 5 R5-1 mandated: a full session reset while a relation is
+ // queued clears it and converges.
+ [Fact]
+ public void DeferredAcceptedParentRelationClearsOnSessionResetAndConverges()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 107UL);
+ const uint parentGuid = 0x70042000u;
+ const uint childGuid = 0x70042001u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ AdmitThenOrphanParentRelation(lifetime, parentGuid, childGuid, namedParentIncarnation: 1);
+ _ = RunToCompletion(lifetime, canonical, lease.Token, NoContact);
+ Assert.Equal(1, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
+
+ IReadOnlyList retirements = lifetime.BeginSessionClear();
+ Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
+ foreach (RuntimeEntityRecord record in retirements)
+ lifetime.CompleteSessionEntityRetirement(record);
+ Assert.True(lifetime.CompleteSessionClearIfConverged());
+ }
+
+ // Round 5 R5-1 mandated ABA coverage, tested directly against
+ // ParentAttachmentState (mirroring
+ // StaleAdmissionIdCannotConsumeAReplacementQueuedAfterThePeek's own
+ // direct-state style): a window token minted BEFORE a Clear() must not
+ // restore anything afterward, even though a NEW window for the same
+ // parent guid could otherwise reuse its Id space.
+ [Fact]
+ public void DeferredAcceptedParentRelationStaleWindowTokenCannotRestoreAfterClearAndRequeue()
+ {
+ var parents = new ParentAttachmentState();
+ const uint parentGuid = 0x70043000u;
+ const uint childGuid = 0x70043001u;
+ var childKey = new RuntimeEntityKey(1u, 1);
+ var parentUpdate = new ParentEvent.Parsed(
+ parentGuid, childGuid, ParentLocation: 1u, PlacementId: 0u,
+ ParentInstanceSequence: 1, ChildPositionSequence: 2);
+ parents.EnqueueDeferredAcceptedRelation(childGuid, childKey, parentUpdate, null, default);
+
+ ImmutableArray detached =
+ parents.DetachDeferredAcceptedRelations(parentGuid, out DeferredReplayWindowToken staleWindow);
+ DeferredAcceptedParentRelation stale = Assert.Single(detached);
+
+ // Reentrant Clear() (session reset) while the window is still open.
+ parents.Clear();
+
+ // A restore against the now-stale token must be a silent no-op.
+ parents.RestoreDeferredAcceptedRelations(staleWindow, [stale]);
+ Assert.Equal(0, parents.DeferredAcceptedRelationCount);
+ Assert.False(parents.ContainsDeferredAcceptedRelation(childGuid, childKey));
+ }
+
+ // Round 5 R5-1 mandated: the envelope's CreateParent stage, end-to-end,
+ // for the unaddressable-parent flavor (addressability-only, no
+ // incarnation to compare). RegisterEntityCore's OWN missing-parent gate
+ // (checked before PreviewCreateDisposition, for ANY beginInitialResidence
+ // call) diverts a same-incarnation update naming an UNADDRESSABLE parent
+ // to a raw deferred Create entirely, bypassing the envelope/CreateParent
+ // stage - so, exactly like the standalone Parent continuation, this
+ // scenario requires the parent to be ADDRESSABLE at the moment of
+ // admission and only orphaned afterward.
+ [Fact]
+ public void EnvelopeCreateParentQueuesForUnaddressableParentAndAppliesWhenTheParentArrives()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 108UL);
+ const uint parentGuid = 0x70044000u;
+ const uint childGuid = 0x70044001u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ // includePosition:false, matching AdmitThenOrphanParentRelation's own
+ // remarks - avoids both the physics-engine dependency and a
+ // position-bearing retained snapshot bleeding into the LATER
+ // same-incarnation "recovered CreateObject" merge below.
+ RuntimeEntityRecord orphanedParent = lifetime.RegisterEntity(
+ Spawn(parentGuid, 1, includePosition: false)).Canonical!;
+
+ WorldSession.EntitySpawn sameCreate = Spawn(
+ childGuid, 1, includePosition: false, positionSequence: 2, parentGuid: parentGuid);
+ PhysicsSpawnData physics = sameCreate.Physics!.Value;
+ sameCreate = sameCreate with
+ {
+ Physics = physics with
+ {
+ Timestamps = physics.Timestamps with { State = 2, Vector = 2 },
+ },
+ };
+ _ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
+
+ // The parent is now orphaned - removed from the active directory
+ // AFTER the envelope's CreateParent stage was admitted while it was
+ // still addressable. RemoveActive (not a full DeleteObject-acceptance)
+ // for the SAME reason AdmitThenOrphanParentRelation uses it: a real
+ // delete retains a permanent per-(guid,generation) teardown tombstone
+ // that would collide when the SAME incarnation is re-registered below.
+ Assert.True(lifetime.Entities.RemoveActive(orphanedParent));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+ RuntimeInitialCreateExecutedAction createParentAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.CreateParent);
+ Assert.Equal(RuntimeParentRelationOutcome.DeferredAwaitingParent, createParentAction.ParentRelationOutcome);
+ Assert.True(lifetime.Entities.ParentAttachments
+ .ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
+
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent, out RuntimeInitialCreateResidenceLease parentLease));
+ RuntimeInitialCreateExecutionReceipt parentReceipt = RunToCompletion(
+ lifetime, parent, parentLease.Token, NoContact);
+ RuntimeInitialCreateExecutedAction replayAction = Assert.Single(
+ parentReceipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ParentRelationReplay);
+ Assert.Equal(RuntimeParentRelationOutcome.Applied, replayAction.ParentRelationOutcome);
+ // Applied never sets ParentGuid itself - see
+ // StandaloneParentContinuationAppliesPositionTimestampOnlyMergeWithNoReDefer's
+ // established precedent (the actual attach commit is
+ // TryCommitParent's job, out of scope for this residence drain).
+ Assert.Null(canonical.Snapshot.ParentGuid);
+ Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
+ }
+
+ // ---------------------------------------------------------------
+ // Round 5 R5-2: cancellation-aware detach/restore window, both
+ // deferred buckets.
+ // ---------------------------------------------------------------
+
+ // Reviewer scenario (a) for the CREATES bucket: two children queued
+ // behind one missing parent; C1's own registration callback delivers
+ // DeleteObject(C2) then DeleteObject(parent) - C2 must NOT be restored,
+ // the recreated parent drains nothing, and every ledger converges.
+ [Fact]
+ public void DeferredChildReplayWindowFiltersASiblingDeletedMidReplayFromTheRestoredRemainder()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 109UL);
+ const uint parentGuid = 0x70045000u;
+ const uint firstChildGuid = 0x70045001u;
+ const uint secondChildGuid = 0x70045002u;
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(firstChildGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(secondChildGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+
+ RuntimeEntityRecord? firstChild = null;
+ WrapDeferredChildRegistration(lifetime, (original, spawn, isLocalPlayer) =>
+ {
+ RuntimeEntityRegistrationResult result = original(spawn, isLocalPlayer);
+ if (spawn.Guid == firstChildGuid)
+ {
+ firstChild = result.Canonical;
+ // C2 was never individually registered (still a raw
+ // deferred blob) - its own delete legitimately reports
+ // false (TryAcceptDelete's known-object gate never
+ // accepted it), but CancelDeferredChildGeneration still
+ // runs unconditionally before that gate and records the
+ // window filter.
+ _ = lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(secondChildGuid, 1), isLocalPlayer: false,
+ removeRetainedObject: true, out _);
+
+ Assert.True(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(parentGuid, 1), isLocalPlayer: false,
+ removeRetainedObject: true, out RuntimeEntityDeleteAcceptance parentAcceptance));
+ lifetime.CompleteAcceptedDelete(parentAcceptance);
+ }
+ return result;
+ });
+
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent, out RuntimeInitialCreateResidenceLease parentLease));
+
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedAuthority,
+ lifetime.InitialCreateExecution.Execute(
+ parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
+ Assert.Equal(default, receipt);
+ Assert.True(lifetime.Entities.TryGetActive(firstChildGuid, out _));
+ Assert.False(lifetime.Entities.ParentAttachments.ContainsDeferredCreate(secondChildGuid, 1));
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredCreateCount);
+
+ // C1 was itself admitted as its own ("Parented") initial-residence
+ // registration - a REAL entity, not a raw blob - and that lease is
+ // independent of the parentGuid-keyed deferred-create window this
+ // test targets. Drain it too so every ledger genuinely converges
+ // (round5-fixes.md's own "ledgers converge" wording), rather than
+ // leaving an unrelated open lease that has nothing to do with the
+ // window/filter mechanism under test.
+ Assert.NotNull(firstChild);
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ firstChild!, out RuntimeInitialCreateResidenceLease firstChildLease));
+ _ = RunToCompletion(lifetime, firstChild!, firstChildLease.Token, NoContact);
+
+ RuntimeEntityRecord recreatedParent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 2, includePosition: false), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ recreatedParent, out RuntimeInitialCreateResidenceLease recreatedLease));
+ RuntimeInitialCreateExecutionReceipt recreatedReceipt = RunToCompletion(
+ lifetime, recreatedParent, recreatedLease.Token, NoContact);
+ Assert.Equal(0, recreatedReceipt.ReplayedDeferredChildCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ }
+
+ // Reviewer scenario (b) for the CREATES bucket: C1's callback drives a
+ // full session clear - restore no-ops, DeferredParentCreateCount stays
+ // 0, IsConverged holds after teardown.
+ [Fact]
+ public void DeferredChildReplayWindowReleasesSilentlyWhenSessionClearFiresMidReplay()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 110UL);
+ const uint parentGuid = 0x70046000u;
+ const uint firstChildGuid = 0x70046001u;
+ const uint secondChildGuid = 0x70046002u;
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(firstChildGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(secondChildGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+
+ WrapDeferredChildRegistration(lifetime, (original, spawn, isLocalPlayer) =>
+ {
+ RuntimeEntityRegistrationResult result = original(spawn, isLocalPlayer);
+ if (spawn.Guid == firstChildGuid)
+ {
+ IReadOnlyList retirements = lifetime.BeginSessionClear();
+ foreach (RuntimeEntityRecord record in retirements)
+ lifetime.CompleteSessionEntityRetirement(record);
+ }
+ return result;
+ });
+
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent, out RuntimeInitialCreateResidenceLease parentLease));
+
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedAuthority,
+ lifetime.InitialCreateExecution.Execute(
+ parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
+ Assert.Equal(default, receipt);
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ Assert.True(lifetime.CompleteSessionClearIfConverged());
+ }
+
+ // Reviewer scenario (a), relation-queue flavor: two queued relations
+ // for the same parent; C1's own attach-commit publish delivers
+ // DeleteObject(C2) then DeleteObject(parent) - C2's relation must NOT
+ // be restored.
+ [Fact]
+ public void DeferredAcceptedRelationReplayWindowFiltersASiblingRelationDeletedMidReplay()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 111UL);
+ const uint parentGuid = 0x70047000u;
+ const uint firstChildGuid = 0x70047001u;
+ const uint secondChildGuid = 0x70047002u;
+
+ RuntimeEntityRecord firstChild = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(firstChildGuid, 1, includePosition: false), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(firstChild, out RuntimeInitialCreateResidenceLease firstLease));
+ RuntimeEntityRecord secondChild = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(secondChildGuid, 1, includePosition: false), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(secondChild, out RuntimeInitialCreateResidenceLease secondLease));
+
+ // Both continuations admit while the FIRST parent incarnation is
+ // addressable, then the parent is orphaned once for both.
+ // includePosition:false + RemoveActive (not a full DeleteObject-
+ // acceptance) for the same reason AdmitThenOrphanParentRelation uses
+ // them: a real delete retains a permanent per-(guid,generation)
+ // teardown tombstone and a position-bearing retained snapshot would
+ // bleed into the SAME-incarnation "recovered CreateObject" merge
+ // when parentGuid is re-registered at incarnation 1 again below.
+ RuntimeEntityRecord orphanedParent = lifetime.RegisterEntity(
+ Spawn(parentGuid, 1, includePosition: false)).Canonical!;
+ var firstParentUpdate = new ParentEvent.Parsed(
+ parentGuid, firstChildGuid, ParentLocation: 1u, PlacementId: 0u,
+ ParentInstanceSequence: 1, ChildPositionSequence: 2);
+ Assert.True(lifetime.TryApplyParent(firstParentUpdate, null, out _));
+ var secondParentUpdate = new ParentEvent.Parsed(
+ parentGuid, secondChildGuid, ParentLocation: 1u, PlacementId: 0u,
+ ParentInstanceSequence: 1, ChildPositionSequence: 2);
+ Assert.True(lifetime.TryApplyParent(secondParentUpdate, null, out _));
+ Assert.True(lifetime.Entities.RemoveActive(orphanedParent));
+
+ _ = RunToCompletion(lifetime, firstChild, firstLease.Token, NoContact);
+ _ = RunToCompletion(lifetime, secondChild, secondLease.Token, NoContact);
+
+ Assert.Equal(2, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
+
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ if (delta.Change is not RuntimeEntityChange.Updated
+ || delta.Entity.Identity.ServerGuid != firstChildGuid)
+ {
+ return;
+ }
+ Assert.True(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(secondChildGuid, 1), isLocalPlayer: false,
+ removeRetainedObject: true, out RuntimeEntityDeleteAcceptance secondAcceptance));
+ lifetime.CompleteAcceptedDelete(secondAcceptance);
+
+ Assert.True(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(parentGuid, 1), isLocalPlayer: false,
+ removeRetainedObject: true, out RuntimeEntityDeleteAcceptance parentAcceptance));
+ lifetime.CompleteAcceptedDelete(parentAcceptance);
+ }));
+
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent, out RuntimeInitialCreateResidenceLease parentLease));
+
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedAuthority,
+ lifetime.InitialCreateExecution.Execute(
+ parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
+ Assert.Equal(default, receipt);
+ Assert.False(lifetime.Entities.ParentAttachments
+ .ContainsDeferredAcceptedRelation(secondChildGuid, secondChild.Key!.Value));
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // Reviewer scenario (b), relation-queue flavor: C1's own attach-commit
+ // publish drives a full session clear - restore no-ops for C2's
+ // relation, converges.
+ [Fact]
+ public void DeferredAcceptedRelationReplayWindowReleasesSilentlyWhenSessionClearFiresMidReplay()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 112UL);
+ const uint parentGuid = 0x70048000u;
+ const uint firstChildGuid = 0x70048001u;
+ const uint secondChildGuid = 0x70048002u;
+
+ RuntimeEntityRecord firstChild = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(firstChildGuid, 1, includePosition: false), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(firstChild, out RuntimeInitialCreateResidenceLease firstLease));
+ RuntimeEntityRecord secondChild = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(secondChildGuid, 1, includePosition: false), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(secondChild, out RuntimeInitialCreateResidenceLease secondLease));
+
+ // includePosition:false + RemoveActive (not a full DeleteObject-
+ // acceptance) for the same reason AdmitThenOrphanParentRelation uses
+ // them: a real delete retains a permanent per-(guid,generation)
+ // teardown tombstone and a position-bearing retained snapshot would
+ // bleed into the SAME-incarnation "recovered CreateObject" merge
+ // when parentGuid is re-registered at incarnation 1 again below -
+ // and the reentrant BeginSessionClear below retires every currently
+ // active record (including the recreated parent), which would
+ // collide with a stale tombstone left by a full delete here.
+ RuntimeEntityRecord orphanedParent = lifetime.RegisterEntity(
+ Spawn(parentGuid, 1, includePosition: false)).Canonical!;
+ var firstParentUpdate = new ParentEvent.Parsed(
+ parentGuid, firstChildGuid, ParentLocation: 1u, PlacementId: 0u,
+ ParentInstanceSequence: 1, ChildPositionSequence: 2);
+ Assert.True(lifetime.TryApplyParent(firstParentUpdate, null, out _));
+ var secondParentUpdate = new ParentEvent.Parsed(
+ parentGuid, secondChildGuid, ParentLocation: 1u, PlacementId: 0u,
+ ParentInstanceSequence: 1, ChildPositionSequence: 2);
+ Assert.True(lifetime.TryApplyParent(secondParentUpdate, null, out _));
+ Assert.True(lifetime.Entities.RemoveActive(orphanedParent));
+
+ _ = RunToCompletion(lifetime, firstChild, firstLease.Token, NoContact);
+ _ = RunToCompletion(lifetime, secondChild, secondLease.Token, NoContact);
+
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ if (delta.Change is not RuntimeEntityChange.Updated
+ || delta.Entity.Identity.ServerGuid != firstChildGuid)
+ {
+ return;
+ }
+ IReadOnlyList retirements = lifetime.BeginSessionClear();
+ foreach (RuntimeEntityRecord record in retirements)
+ lifetime.CompleteSessionEntityRetirement(record);
+ }));
+
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent, out RuntimeInitialCreateResidenceLease parentLease));
+
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedAuthority,
+ lifetime.InitialCreateExecution.Execute(
+ parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
+ Assert.Equal(default, receipt);
+ Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
+ Assert.True(lifetime.CompleteSessionClearIfConverged());
+ }
+
+ // Round 3 E-matrix (a)+(d): TRUE ParentAttachmentState semantics, verified
+ // by reading the source before writing this test. ParentAttachmentState.
+ // DeleteGeneration(parentGuid, oldIncarnation) - called from
+ // TryAcceptDelete when the PARENT is what's being deleted - only ever
+ // touches the _unresolvedByChild ParentEvent-relation queue (via
+ // FilterParentCandidates, keeping relations addressed to a STRICTLY
+ // newer parent incarnation). It never touches _deferredCreatesByParent,
+ // the raw-netblob bucket ReplayDeferredChildren/DetachDeferredCreates
+ // actually replays - that bucket is keyed ONLY by parent GUID, with no
+ // per-incarnation filtering at all (PhysicsAttachment carries no parent
+ // instance sequence to filter by). So a child queued while parent
+ // incarnation 1 was missing survives the parent's own delete untouched,
+ // and DOES replay against a same-GUID recreated incarnation 2 - this
+ // mirrors retail's own GUID-keyed netblob dispatch (see
+ // ReplayDeferredChildren's remarks: "a parent's own successful Create
+ // replays every blob queued waiting on ITS guid"), not a defect.
+ [Fact]
+ public void DeferredChildQueuedForDeletedParentIncarnationStillReplaysAgainstTheRecreatedParentGuid()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 50UL);
+ const uint parentGuid = 0x70030100u;
+ const uint childGuid = 0x70030000u;
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ // Parent incarnation 1 arrives, then is deleted, WITHOUT ever
+ // completing a residence (simulating "gone before the child's
+ // replay").
+ RuntimeEntityRecord parentGen1 = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(parentGuid, 1),
+ isLocalPlayer: false,
+ removeRetainedObject: true,
+ out RuntimeEntityDeleteAcceptance acceptance));
+ lifetime.CompleteAcceptedDelete(acceptance);
+ Assert.False(lifetime.Entities.IsCurrent(parentGen1));
+ // The deferred child create survives the parent's delete untouched -
+ // the TRUE, verified behavior.
+ Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ // Parent GUID reused at a NEWER incarnation.
+ RuntimeEntityRecord parentGen2 = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 2, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parentGen2, out RuntimeInitialCreateResidenceLease parentLease));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, parentGen2, parentLease.Token, NoContact);
+
+ Assert.Equal(1, receipt.ReplayedDeferredChildCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord child));
+ Assert.True(lifetime.TryGetInitialCreateResidence(child, out _));
+ }
+
+ // Round 3 E-matrix (b): a child's own exact Delete before the parent's
+ // replay cancels only that child (CancelDeferredChildGeneration's side
+ // effect runs even though TryAcceptDelete's overall return is false for
+ // a guid with no seeded gate - a purely-deferred child was never
+ // registered, so it never got one).
+ [Fact]
+ public void ChildExactDeleteBeforeParentReplayCancelsOnlyThatChild()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 51UL);
+ const uint parentGuid = 0x70030200u;
+ const uint firstChildGuid = 0x70030300u;
+ const uint secondChildGuid = 0x70030400u;
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(firstChildGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(secondChildGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.Equal(2, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ // The purely-deferred child has no gate; TryAcceptDelete's overall
+ // gate-consuming path fails, but the deferred-create cancellation
+ // side effect at the top of the method already ran.
+ Assert.False(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(firstChildGuid, 1),
+ isLocalPlayer: false,
+ removeRetainedObject: true,
+ out _));
+ Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent, out RuntimeInitialCreateResidenceLease parentLease));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, parent, parentLease.Token, NoContact);
+
+ Assert.Equal(1, receipt.ReplayedDeferredChildCount);
+ Assert.False(lifetime.Entities.TryGetActive(firstChildGuid, out _));
+ Assert.True(lifetime.Entities.TryGetActive(secondChildGuid, out _));
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ }
+
+ // Round 3 E-matrix (c): two raw Creates queued for the SAME child guid
+ // (instance 1 and instance 2, both waiting on the same missing parent).
+ // Deleting the child at instance 1 cancels only the older, same-or-older
+ // queued generation; the strictly-newer instance 2 survives and replays.
+ [Fact]
+ public void NewerDeferredChildGenerationSurvivesOlderGenerationCleanupAndReplays()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 52UL);
+ const uint parentGuid = 0x70030500u;
+ const uint childGuid = 0x70030600u;
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 2, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.Equal(2, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ Assert.False(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(childGuid, 1),
+ isLocalPlayer: false,
+ removeRetainedObject: true,
+ out _));
+ Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent, out RuntimeInitialCreateResidenceLease parentLease));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, parent, parentLease.Token, NoContact);
+
+ Assert.Equal(1, receipt.ReplayedDeferredChildCount);
+ Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord child));
+ Assert.Equal(2, child.Incarnation);
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ }
+
+ // Round 3 E-matrix (e): child GUID reuse across a COMPLETE lifecycle -
+ // register+replay+run-to-completion the first incarnation, delete it
+ // fully (gate removed), then reuse the SAME guid for a fresh incarnation
+ // that is AGAIN deferred (different missing parent) and replays cleanly
+ // with no leakage from the torn-down first generation.
+ [Fact]
+ public void ChildGuidReuseAfterFullLifecycleReplaysCleanlyWithoutStaleState()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 53UL);
+ const uint firstParentGuid = 0x70030700u;
+ const uint secondParentGuid = 0x70030800u;
+ const uint childGuid = 0x70030900u;
+
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false, parentGuid: firstParentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ RuntimeEntityRecord firstParent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(firstParentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ firstParent, out RuntimeInitialCreateResidenceLease firstParentLease));
+ _ = RunToCompletion(lifetime, firstParent, firstParentLease.Token, NoContact);
+ Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord firstChild));
+ Assert.True(lifetime.TryGetInitialCreateResidence(firstChild, out RuntimeInitialCreateResidenceLease childLease));
+ _ = RunToCompletion(lifetime, firstChild, childLease.Token, NoContact);
+
+ // Fully delete the first incarnation (its gate is removed too).
+ Assert.True(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(childGuid, 1),
+ isLocalPlayer: false,
+ removeRetainedObject: true,
+ out RuntimeEntityDeleteAcceptance deleteAcceptance));
+ lifetime.CompleteAcceptedDelete(deleteAcceptance);
+ Assert.False(lifetime.Entities.TryGetActive(childGuid, out _));
+
+ // The SAME guid reused for a fresh incarnation, deferred behind a
+ // DIFFERENT missing parent.
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 2, includePosition: false, parentGuid: secondParentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ RuntimeEntityRecord secondParent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(secondParentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ secondParent, out RuntimeInitialCreateResidenceLease secondParentLease));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, secondParent, secondParentLease.Token, NoContact);
+
+ Assert.Equal(1, receipt.ReplayedDeferredChildCount);
+ Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord reusedChild));
+ Assert.Equal(2, reusedChild.Incarnation);
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ }
+
+ // Round 3 E-matrix (f): a full session reset before the missing parent
+ // ever arrives clears the deferred-create bucket to zero.
+ [Fact]
+ public void SessionResetBeforeParentArrivalClearsDeferredBucket()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 54UL);
+ const uint parentGuid = 0x70030A00u;
+ const uint childGuid = 0x70030B00u;
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ IReadOnlyList retirements = lifetime.BeginSessionClear();
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ foreach (RuntimeEntityRecord record in retirements)
+ lifetime.CompleteSessionEntityRetirement(record);
+ Assert.True(lifetime.CompleteSessionClearIfConverged());
+ }
+
+ // Round 3 E-matrix (g): a full session reset triggered REENTRANTLY from
+ // inside the deferred-child replay's own Registered callback. The
+ // parent's own in-flight Execute() must abandon cleanly rather than
+ // resurrect anything, and the session-clear transaction itself must
+ // still converge once its own retirements are drained.
+ [Fact]
+ public void SessionResetFromWithinReplayDrivenCallbackConverges()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 55UL);
+ const uint parentGuid = 0x70030C00u;
+ const uint childGuid = 0x70030D00u;
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent, out RuntimeInitialCreateResidenceLease parentLease));
+
+ IReadOnlyList? retirements = null;
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ if (delta.Change is not RuntimeEntityChange.Registered
+ || delta.Entity.Identity.ServerGuid != childGuid
+ || retirements is not null)
+ {
+ return;
+ }
+ retirements = lifetime.BeginSessionClear();
+ }));
+
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt);
+
+ // The parent's own in-flight execution is no longer current once the
+ // reentrant reset retires everything - a typed abandonment, not a
+ // resurrection or an escaping exception.
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.RejectedAuthority, status);
+ Assert.Equal(default, receipt);
+ Assert.NotNull(retirements);
+ foreach (RuntimeEntityRecord record in retirements!)
+ lifetime.CompleteSessionEntityRetirement(record);
+ Assert.True(lifetime.CompleteSessionClearIfConverged());
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ }
+
+ // Round 3 E-matrix (h): instance sequence zero is a normal retail
+ // timestamp, not an empty sentinel (see ParentAttachmentState's own
+ // remarks on CancelDeferredChildGeneration) - a deferred child at
+ // incarnation 0 must replay exactly like any other incarnation.
+ [Fact]
+ public void InstanceSequenceZeroChildReplaysCorrectly()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 56UL);
+ const uint parentGuid = 0x70030E00u;
+ const uint childGuid = 0x70030F00u;
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 0, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
+
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent, out RuntimeInitialCreateResidenceLease parentLease));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, parent, parentLease.Token, NoContact);
+
+ Assert.Equal(1, receipt.ReplayedDeferredChildCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
+ Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord child));
+ Assert.Equal(0, child.Incarnation);
+ Assert.True(lifetime.TryGetInitialCreateResidence(child, out _));
+ }
+
+ // ---------------------------------------------------------------
+ // F. Failure boundaries.
+ //
+ // (b) delete from the hook-emission window (after InitialAdoption,
+ // before the FIFO drain) has NO reachable callback of its own:
+ // RunInitialTail's Adopted phase (InitialAdoption) and HookRecorded
+ // phase (the AfterEnterWorld teleport-hook trace entry) are both pure
+ // trace-only mutations with no event dispatch - there is no observer
+ // boundary between them. The ONLY publish inside RunInitialTail is
+ // ReplayDeferredChildren's per-child Registered event (the
+ // DeferredReplayed phase), which already has dedicated coverage:
+ // DeleteDuringDeferredChildReplayAbandonsExecutionWithoutResurrection
+ // (above, in section G) deletes reentrantly from exactly that callback
+ // and proves the abandonment converges without resurrecting anything.
+ // Adding a second, structurally-identical test here would only
+ // duplicate that coverage; the closest reachable boundary already has a
+ // test.
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void DeleteFromObserverBeforeInitialAdoptionAbandonsFirstExecuteCleanly()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 60UL);
+ const uint guid = 0x70031000u;
+
+ // RegisterEntityCore's InitializeAcceptedCreateResidence (Begin())
+ // runs BEFORE PublishEntity(Registered, ...) - a real lease/token
+ // already exists by the time this observer fires, so it can be
+ // captured and later handed to Execute() to observe that FIRST
+ // call's own outcome directly, rather than merely proving no lease
+ // survives.
+ RuntimeEntityRecord? capturedCanonical = null;
+ RuntimeInitialCreateResidenceLease capturedLease = default;
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ if (delta.Change is not RuntimeEntityChange.Registered
+ || delta.Entity.Identity.ServerGuid != guid)
+ {
+ return;
+ }
+ Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord active));
+ capturedCanonical = active;
+ Assert.True(lifetime.TryGetInitialCreateResidence(active, out capturedLease));
+ // Reentrant delete BEFORE Execute() is ever called for this
+ // entity at all - before RunInitialTail's InitialAdoption phase,
+ // the very first thing an Execute() call would otherwise do.
+ Assert.True(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(guid, active.Incarnation),
+ isLocalPlayer: false,
+ removeRetainedObject: true,
+ out RuntimeEntityDeleteAcceptance acceptance));
+ lifetime.CompleteAcceptedDelete(acceptance);
+ }));
+
+ // The reentrant delete bumps the lifetime-mutation counter for this
+ // guid DURING the Registered publish, so RegisterEntityCore's own
+ // post-publish currency check reports this registration as
+ // superseded (Canonical: null in the returned result) - the record
+ // captured from inside the observer, above, is the one to assert
+ // against.
+ _ = lifetime.RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false);
+ Assert.NotNull(capturedCanonical);
+ RuntimeEntityRecord canonical = capturedCanonical!;
+ Assert.False(lifetime.Entities.IsCurrent(canonical));
+ // TryAcceptDelete's own ForgetInitialCreateResidence already retired
+ // the residence reentrantly - nothing was ever adopted.
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+
+ var observed = new List();
+ using IDisposable secondSubscription = lifetime.Events.Subscribe(
+ new EntityObserver(delta => observed.Add(delta.Change)));
+
+ // The FIRST (and only) Execute() call for this entity, using the
+ // token captured before the reentrant delete: the residence is
+ // already gone (RejectedToken, not RejectedAuthority - there was
+ // never a Progress to Abandon), publishes nothing new, and
+ // converges.
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedToken,
+ lifetime.InitialCreateExecution.Execute(
+ canonical, capturedLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
+ Assert.Equal(default, receipt);
+ Assert.Empty(observed);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ [Fact]
+ public void DeleteFromObserverBetweenFifoEntriesAppliesFirstOnlyAndConverges()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 61UL);
+ const uint guid = 0x70031100u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+
+ var appearance = new ObjDescEvent.Parsed(
+ guid,
+ new CreateObject.ModelData(
+ 0x09000002u,
+ Array.Empty(),
+ Array.Empty(),
+ Array.Empty()),
+ InstanceSequence: 1,
+ ObjDescSequence: 2);
+ Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
+ var vector = new VectorUpdate.Parsed(
+ guid, new Vector3(11f, 12f, 13f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(vector, null, out _));
+
+ var observed = new List();
+ bool deletedFromObserver = false;
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ observed.Add(delta.Change);
+ if (deletedFromObserver || delta.Change is not RuntimeEntityChange.Updated)
+ return;
+ deletedFromObserver = true;
+ // Between FIFO entry 1 (ObjDesc, already applied+published) and
+ // entry 2 (Vector, not yet reached) - delete reentrantly.
+ Assert.True(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(guid, canonical.Incarnation),
+ isLocalPlayer: false,
+ removeRetainedObject: true,
+ out RuntimeEntityDeleteAcceptance acceptance));
+ lifetime.CompleteAcceptedDelete(acceptance);
+ }));
+
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt);
+
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.RejectedAuthority, status);
+ Assert.Equal(default, receipt);
+ // ObjDesc applied+published exactly once; Vector never applied
+ // (canonical's velocity stays at its pre-drain value); the delete's
+ // own Deleted is the only other observed event.
+ Assert.Equal(
+ [RuntimeEntityChange.Updated, RuntimeEntityChange.Deleted],
+ observed);
+ Assert.Equal(0x09000002u, canonical.Snapshot.BasePaletteId);
+ Assert.Null(canonical.Snapshot.Physics!.Value.Velocity);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+
+ // A stale retry never resurrects anything or reapplies Vector.
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedToken,
+ lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt retryReceipt));
+ Assert.Equal(default, retryReceipt);
+ Assert.Null(canonical.Snapshot.Physics!.Value.Velocity);
+ }
+
+ // Round 3 F(d): an observer that throws during a publish must not
+ // corrupt the drain or escape Execute() - RuntimeEntityObjectEventStream.
+ // Dispatch contains each observer's exception per-call (RecordDispatchFailure)
+ // and continues to the next observer/pending item. Pin that actual,
+ // verified behavior here rather than assuming propagation.
+ [Fact]
+ public void ObserverThrowDuringPublishIsContainedAndDrainConvergesWithNoDuplicateRetry()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 62UL);
+ const uint guid = 0x70031200u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ var vector = new VectorUpdate.Parsed(
+ guid, Vector3.One, Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(vector, null, out _));
+
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ throw new InvalidOperationException("Deliberate observer failure for F(d).")));
+ long failuresBefore = lifetime.Events.DispatchFailureCount;
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Contains(
+ RuntimeInitialCreateExecutedActionKind.Vector,
+ receipt.Trace.Select(static a => a.Kind));
+ Assert.Equal(Vector3.One, canonical.Snapshot.Physics!.Value.Velocity);
+ Assert.True(lifetime.Events.DispatchFailureCount > failuresBefore);
+ Assert.NotNull(lifetime.Events.LastDispatchFailure);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+
+ // A stale retry after the (contained) throw does not duplicate any
+ // side effect.
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedToken,
+ lifetime.InitialCreateExecution.Execute(canonical, lease.Token, NoContact, out _));
+ Assert.Equal(Vector3.One, canonical.Snapshot.Physics!.Value.Velocity);
+ }
+
+ // ---------------------------------------------------------------
+ // G. Reentrancy
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void ReentrantExecuteForTheSameEntityFailsClosedRatherThanInterleaving()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 11UL);
+ const uint guid = 0x70026000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+
+ var reentrantStatuses = new List();
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ if (delta.Change is not RuntimeEntityChange.Updated || reentrantStatuses.Count != 0)
+ return;
+ // Firing a nested Execute from inside an event observer while the
+ // outer Execute is still on the stack must fail closed.
+ reentrantStatuses.Add(lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out _));
+ }));
+ var vector = new VectorUpdate.Parsed(
+ guid, Vector3.One, Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(vector, null, out _));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Equal(
+ [RuntimeInitialCreateExecutionStatus.RejectedAuthority],
+ reentrantStatuses);
+ Assert.NotEmpty(receipt.Trace);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ [Fact]
+ public void DeleteDuringDeferredChildReplayAbandonsExecutionWithoutResurrection()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 12UL);
+ const uint parentGuid = 0x70026100u;
+ const uint childGuid = 0x70026200u;
+ Assert.True(lifetime.RegisterEntityWithInitialResidence(
+ Spawn(childGuid, 1, includePosition: false, parentGuid: parentGuid),
+ isLocalPlayer: false)
+ .DeferredForParent);
+ RuntimeEntityRecord parent = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(parentGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ parent,
+ out RuntimeInitialCreateResidenceLease parentLease));
+
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ if (delta.Change is not RuntimeEntityChange.Registered
+ || delta.Entity.Identity.ServerGuid != childGuid)
+ {
+ return;
+ }
+ // A reentrant delete of the PARENT itself, triggered from inside
+ // the child-registration callback the deferred replay drives.
+ Assert.True(lifetime.TryAcceptDelete(
+ new DeleteObject.Parsed(parentGuid, parent.Incarnation),
+ isLocalPlayer: false,
+ removeRetainedObject: true,
+ out RuntimeEntityDeleteAcceptance acceptance));
+ lifetime.CompleteAcceptedDelete(acceptance);
+ }));
+
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt);
+
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.RejectedAuthority, status);
+ Assert.Equal(default, receipt);
+ Assert.False(lifetime.Entities.IsCurrent(parent));
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ // Only the PARENT's own residence/progress was ever touched by this
+ // abandoned Execute call. The CHILD is a distinct entity the
+ // deferred replay legitimately registered through the canonical
+ // route before the reentrant delete happened - its own (never
+ // executed) residence lease correctly remains open; deleting the
+ // parent does not cascade-tear-down an unrelated child.
+ Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord child));
+ Assert.True(lifetime.TryGetInitialCreateResidence(child, out _));
+ }
+
+ // Round 3 B2 regression (reentrancy from a publish callback): a wire
+ // apply (TryApplyMotion) reentrantly enqueued from inside an EARLIER
+ // continuation's own publish must NOT retire the residence, and the
+ // continuation that was ALREADY queued before the drain even started
+ // (Vector) must still drain - proving the reentrant enqueue never
+ // displaces or skips the pre-existing FIFO tail.
+ [Fact]
+ public void WireApplyDuringDrainPublishDoesNotRetireResidenceAndPreExistingFifoStillDrains()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 63UL);
+ const uint guid = 0x70031300u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+
+ var appearance = new ObjDescEvent.Parsed(
+ guid,
+ new CreateObject.ModelData(
+ 0x0A000002u,
+ Array.Empty(),
+ Array.Empty(),
+ Array.Empty()),
+ InstanceSequence: 1,
+ ObjDescSequence: 2);
+ Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
+ var vector = new VectorUpdate.Parsed(
+ guid, new Vector3(21f, 22f, 23f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(vector, null, out _));
+
+ var observed = new List();
+ bool reentered = false;
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ observed.Add(delta.Change);
+ if (reentered || delta.Change is not RuntimeEntityChange.Updated)
+ return;
+ reentered = true;
+ var motion = new WorldSession.EntityMotionUpdate(
+ guid,
+ new CreateObject.ServerMotionState(0x3d, 0x11),
+ InstanceSequence: 1,
+ MovementSequence: 2,
+ ServerControlSequence: 1,
+ IsAutonomous: false);
+ Assert.True(lifetime.TryApplyMotion(motion, retainPayload: true, null, out _, out _));
+ }));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ // Vector (already queued before the drain started) drains BEFORE
+ // the reentrantly-enqueued Motion, and both drain exactly once.
+ Assert.Equal(
+ [
+ RuntimeInitialCreateExecutedActionKind.InitialAdoption,
+ RuntimeInitialCreateExecutedActionKind.ObjDesc,
+ RuntimeInitialCreateExecutedActionKind.Vector,
+ RuntimeInitialCreateExecutedActionKind.Movement,
+ ],
+ receipt.Trace.Select(static a => a.Kind));
+ Assert.Equal(
+ [RuntimeEntityChange.Updated, RuntimeEntityChange.Updated, RuntimeEntityChange.Updated],
+ observed);
+ Assert.Equal(new Vector3(21f, 22f, 23f), canonical.Snapshot.Physics!.Value.Velocity);
+ Assert.Equal(new CreateObject.ServerMotionState(0x3d, 0x11), canonical.Snapshot.MotionState);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ [Fact]
+ public void RegisterDifferentEntityFromCallbackDuringDrainConvergesBothIndependently()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 64UL);
+ const uint firstGuid = 0x70031400u;
+ const uint secondGuid = 0x70031500u;
+ RuntimeEntityRecord first = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(firstGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ first, out RuntimeInitialCreateResidenceLease firstLease));
+ var vector = new VectorUpdate.Parsed(
+ firstGuid, Vector3.One, Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(vector, null, out _));
+
+ RuntimeEntityRecord? secondCanonical = null;
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ if (delta.Change is not RuntimeEntityChange.Updated
+ || delta.Entity.Identity.ServerGuid != firstGuid
+ || secondCanonical is not null)
+ {
+ return;
+ }
+ // A completely unrelated entity registered reentrantly, from
+ // inside the FIRST entity's own drain publish.
+ secondCanonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(secondGuid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ }));
+
+ RuntimeInitialCreateExecutionReceipt firstReceipt = RunToCompletion(
+ lifetime, first, firstLease.Token, NoContact);
+
+ Assert.NotEmpty(firstReceipt.Trace);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.NotNull(secondCanonical);
+ Assert.True(lifetime.Entities.IsCurrent(secondCanonical!));
+ Assert.True(lifetime.TryGetInitialCreateResidence(secondCanonical!, out RuntimeInitialCreateResidenceLease secondLease));
+
+ // The second entity's own independent residence still drains
+ // normally afterward.
+ RuntimeInitialCreateExecutionReceipt secondReceipt = RunToCompletion(
+ lifetime, secondCanonical!, secondLease.Token, NoContact);
+ Assert.Contains(
+ RuntimeInitialCreateExecutedActionKind.InitialAdoption,
+ secondReceipt.Trace.Select(static a => a.Kind));
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // Round 3 G(c)/I2: recreating the SAME guid at a NEWER incarnation from
+ // a reentrant callback mid-drain. The old execution must abandon
+ // cleanly (its own progress+residence converge) with NO FIFO transfer
+ // to the new incarnation; the new incarnation is a completely
+ // independent, unaffected registration. This is the same scenario the
+ // task's "replacement convergence" item names - one test covers both.
+ [Fact]
+ public void RecreateSameGuidNewerIncarnationFromCallbackAbandonsOldExecutionWithoutFifoTransfer()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 65UL);
+ const uint guid = 0x70031600u;
+ RuntimeEntityRecord canonicalGen1 = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonicalGen1, out RuntimeInitialCreateResidenceLease lease));
+
+ var appearance = new ObjDescEvent.Parsed(
+ guid,
+ new CreateObject.ModelData(
+ 0x0B000002u,
+ Array.Empty(),
+ Array.Empty(),
+ Array.Empty()),
+ InstanceSequence: 1,
+ ObjDescSequence: 2);
+ Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
+ var vector = new VectorUpdate.Parsed(
+ guid, new Vector3(31f, 32f, 33f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(vector, null, out _));
+
+ bool recreated = false;
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ if (recreated || delta.Change is not RuntimeEntityChange.Updated)
+ return;
+ recreated = true;
+ // Same guid, NEWER incarnation, from inside the FIRST
+ // continuation's (ObjDesc's) own publish - a bare Create with no
+ // residence, to isolate the replacement mechanics from any
+ // second drain.
+ _ = lifetime.RegisterEntity(Spawn(guid, 2, includePosition: false));
+ }));
+
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonicalGen1, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt);
+
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.RejectedAuthority, status);
+ Assert.Equal(default, receipt);
+ Assert.False(lifetime.Entities.IsCurrent(canonicalGen1));
+ // Old progress+residence fully converge.
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+
+ // The new incarnation is unaffected: no residence pending (a bare
+ // RegisterEntity never begins one), and none of generation 1's
+ // FIFO (Vector, still undrained) ever transferred to it.
+ Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord canonicalGen2));
+ Assert.Equal(2, canonicalGen2.Incarnation);
+ Assert.False(lifetime.TryGetInitialCreateResidence(canonicalGen2, out _));
+ Assert.Null(canonicalGen2.Snapshot.Physics!.Value.Velocity);
+ }
+
+ // Round 3 G(d): Dispose() called reentrantly from a publish callback.
+ // Pin the ACTUAL observed behavior (verified by running this test)
+ // rather than assuming either containment or propagation.
+ [Fact]
+ public void DisposeFromCallbackDuringDrainConvergesTheCompleteLedger()
+ {
+ var lifetime = new RuntimeEntityObjectLifetime();
+ Bind(lifetime, 66UL);
+ const uint guid = 0x70031700u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ var vector = new VectorUpdate.Parsed(
+ guid, Vector3.One, Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(vector, null, out _));
+
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ if (delta.Change is RuntimeEntityChange.Updated)
+ lifetime.Dispose();
+ }));
+
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt);
+
+ // Dispose() reentrantly retires everything (BeginSessionClear ->
+ // teardown -> converge) before the outer Execute() call's own
+ // currency check runs again; that check sees a no-longer-current
+ // canonical and abandons in the SAME typed way any other reentrant
+ // teardown does. No invalid enumeration/exception escapes.
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.RejectedAuthority, status);
+ Assert.Equal(default, receipt);
+ Assert.True(lifetime.CaptureOwnership().IsConverged);
+ }
+
+ // ---------------------------------------------------------------
+ // H. Saturation / stale-progress
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void StaleProgressLeaseIdIsDiscardedAndFailsClosedThenRetrySucceedsCleanly()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 13UL);
+ const uint guid = 0x70027000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ RuntimeEntityKey key = canonical.Key!.Value;
+
+ // Directly plant a stale Progress entry under a LeaseId that does
+ // NOT match the current lease's token - the exact "an old
+ // incarnation's progress leaking into a reused key" scenario the
+ // contract requires Execute to discard and fail closed on for this
+ // one call, never silently reusing it.
+ PlantStaleProgress(lifetime, key, lease.Token.LeaseId + 1_000UL);
+ Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedAuthority,
+ lifetime.InitialCreateExecution.Execute(canonical, lease.Token, NoContact, out _));
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+ Assert.NotEmpty(receipt.Trace);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ private static void PlantStaleProgress(
+ RuntimeEntityObjectLifetime lifetime,
+ RuntimeEntityKey key,
+ ulong staleLeaseId)
+ {
+ Type progressType = typeof(RuntimeInitialCreateContinuationExecutor)
+ .GetNestedType("Progress", System.Reflection.BindingFlags.NonPublic)!;
+ object stale = System.Runtime.CompilerServices.RuntimeHelpers
+ .GetUninitializedObject(progressType);
+ System.Reflection.PropertyInfo leaseIdProperty = progressType.GetProperty(
+ "LeaseId",
+ System.Reflection.BindingFlags.Instance
+ | System.Reflection.BindingFlags.Public
+ | System.Reflection.BindingFlags.NonPublic)!;
+ leaseIdProperty.SetValue(stale, staleLeaseId);
+ System.Reflection.FieldInfo progressField = typeof(RuntimeInitialCreateContinuationExecutor)
+ .GetField(
+ "_progress",
+ System.Reflection.BindingFlags.Instance
+ | System.Reflection.BindingFlags.NonPublic)!;
+ var dictionary = (System.Collections.IDictionary)progressField.GetValue(
+ lifetime.InitialCreateExecution)!;
+ dictionary[key] = stale;
+ }
+
+ // Round 3 H (saturation): adoption Revision pinned at ulong.MaxValue -
+ // CanEnqueue's completed-entry branch requires
+ // "Revision < ulong.MaxValue", so a NEW continuation fails closed BEFORE
+ // any wire consumption; whatever was ALREADY committed before saturation
+ // still drains and converges normally through the executor.
+ [Fact]
+ public void AdoptionRevisionSaturationFailsClosedBeforeAnyNewContinuationCanEnqueue()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 70UL);
+ const uint guid = 0x70032000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ var firstVector = new VectorUpdate.Parsed(
+ guid, Vector3.One, Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(firstVector, null, out _));
+
+ Assert.Equal(
+ RuntimeInitialCreateResidenceCompletionStatus.Completed,
+ lifetime.InitialCreateResidences.Complete(canonical, lease.Token, out _));
+ SetCompletedAdoptionRevision(lifetime, canonical.Key!.Value, ulong.MaxValue);
+
+ var secondVector = new VectorUpdate.Parsed(
+ guid, new Vector3(2f, 2f, 2f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 3);
+ Assert.False(lifetime.TryApplyVector(secondVector, null, out _));
+ Assert.True(lifetime.InitialCreateResidences.TryGetTransaction(
+ canonical, out RuntimeInitialCreateResidenceLease afterFailedEnqueue));
+ Assert.Single(afterFailedEnqueue.Continuations);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ // Only the ONE continuation committed before saturation ever drains -
+ // exactly once, never duplicated by the failed second attempt.
+ Assert.Equal(
+ [
+ RuntimeInitialCreateExecutedActionKind.InitialAdoption,
+ RuntimeInitialCreateExecutedActionKind.Vector,
+ ],
+ receipt.Trace.Select(static a => a.Kind));
+ Assert.Equal(Vector3.One, canonical.Snapshot.Physics!.Value.Velocity);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ ///
+ /// Local copy of RuntimeInitialCreateResidenceStateTests'
+ /// SetCompletedAdoptionRevision reflection helper (per this task's
+ /// explicit instruction to reuse the pattern locally rather than share
+ /// test-project internals across files).
+ ///
+ private static void SetCompletedAdoptionRevision(
+ RuntimeEntityObjectLifetime lifetime,
+ RuntimeEntityKey key,
+ ulong revision)
+ {
+ Type stateType = typeof(RuntimeInitialCreateResidenceState);
+ System.Reflection.FieldInfo completedField = stateType.GetField(
+ "_completed",
+ System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;
+ var completed = (System.Collections.IDictionary)completedField.GetValue(
+ lifetime.InitialCreateResidences)!;
+ object entry = completed[key]!;
+ System.Reflection.PropertyInfo receiptProperty = entry.GetType().GetProperty(
+ "Receipt",
+ System.Reflection.BindingFlags.Instance
+ | System.Reflection.BindingFlags.Public
+ | System.Reflection.BindingFlags.NonPublic)!;
+ var receipt = (RuntimeInitialCreateResidenceReceipt)receiptProperty.GetValue(entry)!;
+ receiptProperty.SetValue(
+ entry,
+ receipt with { Adoption = receipt.Adoption with { Revision = revision } });
+ }
+
+ // ---------------------------------------------------------------
+ // External-race detection on the executor's staleness baseline
+ // (regression coverage for the narrowed pre-Complete
+ // AdvanceExecutorBaseline guard - Finding 1)
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void ExternalPositionAuthorityMutationWithNoPendingPlacementFailsClosedAndConverges()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 16UL);
+ const uint guid = 0x70029000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+
+ // Drive the residence directly to "completed + adopted" - the exact
+ // state RunInitialTail leaves behind before draining any
+ // continuation, with NO pending continuation placement. This is
+ // the only state in which the narrowed pre-Complete
+ // AdvanceExecutorBaseline guard does NOT resync the baseline
+ // before the next Complete() call runs.
+ Assert.Equal(
+ RuntimeInitialCreateResidenceCompletionStatus.Completed,
+ lifetime.InitialCreateResidences.Complete(canonical, lease.Token, out _));
+ Assert.True(lifetime.InitialCreateResidences.AdoptCompletedPlacement(canonical, lease.Token));
+
+ var observed = new List();
+ using IDisposable subscription = lifetime.Events.Subscribe(
+ new EntityObserver(delta => observed.Add(delta.Change)));
+
+ // External, non-executor mutation of one of the four baseline
+ // fields (never through AdvanceExecutorBaseline) - simulating a
+ // genuine race between Execute calls with nothing in flight. Before
+ // Finding 1's fix, the OLD unconditional pre-Complete rebaseline
+ // would have silently absorbed this and returned Completed instead.
+ lifetime.Entities.AdvancePositionAuthority(canonical);
+
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedAuthority,
+ lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
+ Assert.Equal(default, receipt);
+ Assert.Empty(observed);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ [Fact]
+ public void ExternalFullCellMutationWithNoPendingPlacementFailsClosedAndConverges()
+ {
+ using RuntimeEntityObjectLifetime lifetime = new();
+ Bind(lifetime, 17UL);
+ const uint guid = 0x70029100u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+
+ Assert.Equal(
+ RuntimeInitialCreateResidenceCompletionStatus.Completed,
+ lifetime.InitialCreateResidences.Complete(canonical, lease.Token, out _));
+ Assert.True(lifetime.InitialCreateResidences.AdoptCompletedPlacement(canonical, lease.Token));
+
+ var observed = new List();
+ using IDisposable subscription = lifetime.Events.Subscribe(
+ new EntityObserver(delta => observed.Add(delta.Change)));
+
+ // FullCellId is the second field the reviewer explicitly named -
+ // exercise it independently of PositionAuthorityVersion.
+ lifetime.Entities.SetFullCell(canonical, Cell, Landblock);
+
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedAuthority,
+ lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
+ Assert.Equal(default, receipt);
+ Assert.Empty(observed);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ }
+
+ // Round 4 R4-2 / R4-9(b): the reviewer's exact scenario - external
+ // SetFullCell DURING the AwaitingContinuationPlacement window (after the
+ // continuation's OWN placement has been acknowledged but before the
+ // executor consumes it), not merely with no placement in flight at all
+ // (that is the PRECEDING test above). Before the fix, both failure arms
+ // in ResumePendingPlacement cleared PendingContinuationPlacement and
+ // abandoned WITHOUT ever forgetting the retained acknowledged-placement
+ // entry - HasRetainedCompletion for this key would then stay true
+ // forever, blocking EVERY later placement begin (the runtime-surface.md
+ // 3.1 deadlock this executor exists to resolve).
+ [Fact]
+ public void ExternalFullCellMutationDuringAwaitingContinuationPlacementForgetsThePendingPlacementAndAllowsAFreshOneToBegin()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 90UL);
+ const uint guid = 0x70034000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 1, forcePositionSequence: 0, positionX: 40f);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: true, null, null, true, null, out _, out _, out _));
+
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out _);
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
+ RuntimeEntityKey key = canonical.Key!.Value;
+ Assert.True(lifetime.InitialCreateExecution
+ .TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
+ // Drives the continuation's own placement through prepare/submit/
+ // acknowledge - the acknowledged projection is now retained under
+ // this exact token, ready for the NEXT Execute call to consume.
+ CompletePendingContinuationPlacement(lifetime, key, route);
+
+ // External mutation of the record's cell AFTER the continuation's
+ // own placement was acknowledged but BEFORE the executor consumes
+ // it - ResumePendingPlacement's projection/record agreement check
+ // must catch this exactly like Complete() does for the initial
+ // placement.
+ lifetime.Entities.SetFullCell(canonical, canonical.FullCellId + 999u, Landblock);
+
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.RejectedAuthority,
+ lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
+ Assert.Equal(default, receipt);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ RuntimeSetPositionOwnershipSnapshot physicsOwnership =
+ lifetime.Physics.SetPosition.CaptureOwnership();
+ Assert.Equal(0, physicsOwnership.ActiveOperationCount);
+ // The central R4-2 claim: no retained-completion leak.
+ Assert.Equal(0, physicsOwnership.AcknowledgedPlacementCompletionCount);
+
+ // A subsequent FRESH authored placement for the SAME entity can
+ // begin - proves HasRetainedCompletion no longer blocks it.
+ RuntimeEntityPlacementToken fresh = lifetime.Physics.SetPosition
+ .TryBeginExclusiveAuthoredPlacement(
+ canonical,
+ canonical.PositionAuthorityVersion,
+ RuntimeSetPositionOperationKind.LocalAuthoritative);
+ Assert.True(fresh.IsValid);
+ }
+
+ // Round 4 R4-9(a): guards B3's residence-retirement notification edge
+ // for a placement STILL IN FLIGHT (not yet acknowledged) - a THIRD
+ // PARTY (not the executor) discovers residence staleness through the
+ // residence's own host-facing TryGetTransaction query, which internally
+ // Retires the completed entry. That Retire must notify the executor so
+ // it forgets its OWN separately-tracked pending continuation placement
+ // token, not just the residence's initial-lease placement.
+ [Fact]
+ public void ThirdPartyTryGetTransactionRetireWhileAwaitingContinuationPlacementDiscardsExecutorProgressAndForgetsThePendingPlacement()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 91UL);
+ const uint guid = 0x70034100u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 1, forcePositionSequence: 0, positionX: 40f);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: true, null, null, true, null, out _, out _, out _));
+
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out _);
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
+ Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ RuntimeEntityKey key = canonical.Key!.Value;
+ Assert.True(lifetime.InitialCreateExecution
+ .TryGetPendingContinuationPlacement(key, out RuntimeEntityPlacementToken pendingPlacement));
+ Assert.True(lifetime.Physics.SetPosition.IsPlacementCurrent(pendingPlacement));
+
+ // External, non-executor mutation of one of the four executor-
+ // tracked baseline fields - simulates something OTHER than the
+ // executor discovering staleness through a THIRD-PARTY call to the
+ // residence's own host-facing TryGetTransaction, never through
+ // Execute at all.
+ lifetime.Entities.AdvancePositionAuthority(canonical);
+
+ Assert.False(lifetime.InitialCreateResidences.TryGetTransaction(canonical, out _));
+
+ // B3's notification edge: the residence's own Retire (triggered by
+ // a THIRD PARTY, not the executor) must notify the executor so it
+ // forgets the pending continuation placement, not just the
+ // residence's own initial-lease placement.
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.False(lifetime.Physics.SetPosition.IsPlacementCurrent(pendingPlacement));
+ RuntimeSetPositionOwnershipSnapshot physicsOwnership =
+ lifetime.Physics.SetPosition.CaptureOwnership();
+ Assert.Equal(0, physicsOwnership.ActiveOperationCount);
+ Assert.Equal(0, physicsOwnership.PlacementCompletionWatchCount);
+ Assert.Equal(0, physicsOwnership.AcknowledgedPlacementCompletionCount);
+
+ // A subsequent FRESH authored placement for the SAME entity can
+ // begin - no lingering block from the forgotten pending placement.
+ RuntimeEntityPlacementToken fresh = lifetime.Physics.SetPosition
+ .TryBeginExclusiveAuthoredPlacement(
+ canonical,
+ canonical.PositionAuthorityVersion,
+ RuntimeSetPositionOperationKind.LocalAuthoritative);
+ Assert.True(fresh.IsValid);
+ }
+
+ // ---------------------------------------------------------------
+ // I. Ownership convergence
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void ResetDuringAwaitingContinuationPlacementConvergesEveryLedger()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 14UL);
+ const uint guid = 0x70028000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 1, forcePositionSequence: 0, positionX: 40f);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: true, null, null, true, null, out _, out _, out _));
+
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, NoContact, out _);
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
+ Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+
+ IReadOnlyList retirements = lifetime.BeginSessionClear();
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ foreach (RuntimeEntityRecord record in retirements)
+ lifetime.CompleteSessionEntityRetirement(record);
+ Assert.True(lifetime.CompleteSessionClearIfConverged());
+ }
+
+ [Fact]
+ public void DisposalConvergesTheCompleteOwnershipLedgerAfterASuccessfulExecution()
+ {
+ var lifetime = new RuntimeEntityObjectLifetime();
+ Bind(lifetime, 15UL);
+ const uint guid = 0x70028100u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, includePosition: false),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ _ = RunToCompletion(lifetime, canonical, lease.Token, NoContact);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+
+ lifetime.Dispose();
+
+ Assert.True(lifetime.CaptureOwnership().IsConverged);
+ }
+
+ // ---------------------------------------------------------------
+ // J. Gap closures (B1 operation-slot contention).
+ // ---------------------------------------------------------------
+
+ // Round 3 B1-gap: a Position continuation's own placement-begin can fail
+ // on TRANSIENT operation-slot contention (another operation already owns
+ // this entity's SetPosition slot) rather than genuine staleness. The
+ // merge+publish must already have committed by that point (this is NOT
+ // an abandonment); a retry after the slot frees must complete WITHOUT
+ // re-running the merge or re-publishing.
+ [Fact]
+ public void OperationSlotContentionYieldsRetryableWithoutAbandoningThenRetryCompletesWithNoDuplicatePublish()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 80UL);
+ const uint guid = 0x70033000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical, out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+ Assert.Equal(Cell, canonical.FullCellId);
+
+ // Entry 1: a Vector continuation whose own publish is the injection
+ // point for occupying the entity's SetPosition slot from OUTSIDE the
+ // executor - by the time this fires, RunInitialTail's
+ // AdoptCompletedPlacement has already freed the slot the INITIAL
+ // placement held, so an external begin here genuinely succeeds.
+ var vector = new VectorUpdate.Parsed(
+ guid, Vector3.One, Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
+ Assert.True(lifetime.TryApplyVector(vector, null, out _));
+
+ // Entry 2: a far-distance Remote Position continuation that will
+ // require its OWN real SetPosition placement.
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 25f);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: false, null, null, false, null,
+ out PositionTimestampDisposition disposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+
+ RuntimeEntityPlacementToken unrelatedOccupant = default;
+ var observed = new List();
+ using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
+ {
+ observed.Add(delta.Change);
+ if (unrelatedOccupant.IsValid || delta.Change is not RuntimeEntityChange.Updated)
+ return;
+ unrelatedOccupant = lifetime.Physics.SetPosition.TryBeginExclusiveAuthoredPlacement(
+ canonical,
+ canonical.PositionAuthorityVersion,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative);
+ Assert.True(unrelatedOccupant.IsValid);
+ }));
+
+ var inputs = new RuntimeInitialCreateExecutionInputs(
+ UsePositionFromServer: false, PlayerDistance: 200f);
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, inputs, out RuntimeInitialCreateExecutionReceipt receipt);
+
+ // Contention, not staleness: retryable, residence/progress both
+ // still open (no abandonment).
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
+ Assert.Equal(default, receipt);
+ Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ // The merge+publish for BOTH entries already committed before the
+ // contention was even discovered.
+ Assert.Equal(
+ [RuntimeEntityChange.Updated, RuntimeEntityChange.Updated],
+ observed);
+ Assert.Equal(25f, canonical.Snapshot.Position!.Value.PositionX);
+
+ // Free the slot; retry.
+ lifetime.Physics.SetPosition.PublishCancellation(
+ lifetime.Physics.SetPosition.ForgetExactPlacement(unrelatedOccupant));
+
+ RuntimeInitialCreateExecutionStatus retryStatus = lifetime.InitialCreateExecution.Execute(
+ canonical, lease.Token, inputs, out RuntimeInitialCreateExecutionReceipt retryReceipt);
+ Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, retryStatus);
+ Assert.Equal(default, retryReceipt);
+ // No re-merge, no re-publish on the contention retry itself.
+ Assert.Equal(
+ [RuntimeEntityChange.Updated, RuntimeEntityChange.Updated],
+ observed);
+
+ RuntimeEntityKey key = canonical.Key!.Value;
+ Assert.True(lifetime.InitialCreateExecution
+ .TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
+ Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, route.Disposition);
+ CompletePendingContinuationPlacement(lifetime, key, route);
+
+ RuntimeInitialCreateExecutionReceipt finalReceipt = RunToCompletion(
+ lifetime, canonical, lease.Token, inputs);
+
+ Assert.Equal(
+ [
+ RuntimeInitialCreateExecutedActionKind.InitialAdoption,
+ RuntimeInitialCreateExecutedActionKind.Vector,
+ RuntimeInitialCreateExecutedActionKind.Position,
+ ],
+ finalReceipt.Trace.Select(static a => a.Kind));
+ // Still exactly two publishes across the entire contention +
+ // retry + completion sequence.
+ Assert.Equal(
+ [RuntimeEntityChange.Updated, RuntimeEntityChange.Updated],
+ observed);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
+ }
+
+ // ---------------------------------------------------------------
+ // Harness
+ // ---------------------------------------------------------------
+
+ private static RuntimeEntityObjectLifetime EngineLifetime()
+ {
+ var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
+ engine.AddLandblock(
+ Landblock,
+ new TerrainSurface(new byte[81], new float[256]),
+ Array.Empty(),
+ Array.Empty(),
+ worldOffsetX: 0f,
+ worldOffsetY: 0f);
+ return new RuntimeEntityObjectLifetime(engine);
+ }
+
+ private static RuntimeInitialCreateExecutionReceipt RunToCompletion(
+ RuntimeEntityObjectLifetime lifetime,
+ RuntimeEntityRecord canonical,
+ in RuntimeInitialCreateResidenceToken token,
+ RuntimeInitialCreateExecutionInputs inputs,
+ int maxSteps = 25)
+ {
+ for (int step = 0; step < maxSteps; step++)
+ {
+ RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
+ canonical, token, inputs, out RuntimeInitialCreateExecutionReceipt receipt);
+ switch (status)
+ {
+ case RuntimeInitialCreateExecutionStatus.Completed:
+ return receipt;
+ case RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement:
+ {
+ RuntimeEntityKey key = canonical.Key!.Value;
+ Assert.True(lifetime.InitialCreateExecution
+ .TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
+ CompletePendingContinuationPlacement(lifetime, key, route);
+ continue;
+ }
+ default:
+ throw new InvalidOperationException(
+ $"RunToCompletion hit unexpected status {status}; drive its precondition explicitly instead.");
+ }
+ }
+ throw new InvalidOperationException("RunToCompletion exceeded its step budget.");
+ }
+
+ private static void CompleteInitialPlacement(
+ RuntimeEntityObjectLifetime lifetime,
+ in RuntimeInitialCreateResidenceLease lease)
+ {
+ RuntimeSetPositionCommand command = Prepare(
+ lifetime,
+ lease.Placement,
+ lease.Route.OperationKind,
+ lease.Route.SetPositionFlags);
+ RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition
+ .SubmitPreparedPlacement(lease.Placement, command);
+ Assert.Equal(
+ RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
+ outcome.Status);
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(outcome.Projection));
+ }
+
+ private static void CompletePendingContinuationPlacement(
+ RuntimeEntityObjectLifetime lifetime,
+ RuntimeEntityKey key,
+ in RuntimeAuthoritativePositionRoute route)
+ {
+ Assert.True(lifetime.InitialCreateExecution
+ .TryGetPendingContinuationPlacement(key, out RuntimeEntityPlacementToken placement));
+ RuntimeSetPositionCommand command = Prepare(
+ lifetime, placement, route.OperationKind, route.SetPositionFlags);
+ RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition
+ .SubmitPreparedPlacement(placement, command);
+ Assert.Equal(
+ RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
+ outcome.Status);
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(outcome.Projection));
+ }
+
+ private static RuntimeSetPositionCommand Prepare(
+ RuntimeEntityObjectLifetime lifetime,
+ in RuntimeEntityPlacementToken placement,
+ RuntimeSetPositionOperationKind operationKind,
+ PhysicsSetPositionFlags flags)
+ {
+ var preparation = new RuntimeSetPositionMoverPreparation(
+ RuntimeSetPositionMoverSetup.ResolvedAbsent,
+ operationKind,
+ GameTime: 1d,
+ PhysicsPlacementClass.Ordinary,
+ flags);
+ Assert.Equal(
+ RuntimeSetPositionMoverPreparationStatus.Prepared,
+ lifetime.Physics.SetPosition.PrepareMover(
+ placement, preparation, out RuntimeSetPositionCommand command));
+ return command;
+ }
+
+ private static void AttachDormantBody(
+ RuntimeEntityObjectLifetime lifetime,
+ RuntimeEntityRecord canonical,
+ bool inContact = false)
+ {
+ var body = new PhysicsBody
+ {
+ State = canonical.FinalPhysicsState,
+ Orientation = Quaternion.Identity,
+ InWorld = false,
+ TransientState = inContact ? TransientStateFlags.Contact : TransientStateFlags.None,
+ };
+ lifetime.Entities.SetPhysicsBody(canonical, body);
+ }
+
+ ///
+ /// The real physics sweep an initial-placement commit runs against the
+ /// test landblock decides the body's own contact bit from scratch,
+ /// clobbering whatever set beforehand.
+ /// Call this AFTER the placement commits to pin the contact bit a
+ /// Position-route test actually needs.
+ ///
+ private static void ForceContact(RuntimeEntityRecord canonical, bool inContact)
+ {
+ if (canonical.PhysicsBody is not { } body)
+ return;
+ body.TransientState = inContact
+ ? body.TransientState | TransientStateFlags.Contact
+ : body.TransientState & ~TransientStateFlags.Contact;
+ }
+
+ private static void Bind(RuntimeEntityObjectLifetime lifetime, ulong generation)
+ {
+ var token = new RuntimeGenerationToken(generation);
+ lifetime.BindEventContext(() => token, static () => 1UL);
+ }
+
+ private static WorldSession.EntityPositionUpdate PositionUpdate(
+ uint guid,
+ ushort positionSequence,
+ ushort teleportSequence,
+ ushort forcePositionSequence,
+ float positionX,
+ bool isGrounded = true)
+ {
+ return new WorldSession.EntityPositionUpdate(
+ guid,
+ new CreateObject.ServerPosition(Cell, positionX, 20f, 7f, 1f, 0f, 0f, 0f),
+ new Vector3(positionSequence, 2f, 3f),
+ PlacementId: positionSequence,
+ IsGrounded: isGrounded,
+ InstanceSequence: 1,
+ PositionSequence: positionSequence,
+ TeleportSequence: teleportSequence,
+ ForcePositionSequence: forcePositionSequence);
+ }
+
+ private static WorldSession.EntitySpawn Spawn(
+ uint guid,
+ ushort incarnation,
+ bool includePosition = true,
+ uint? parentGuid = null,
+ ushort positionSequence = 1,
+ float positionX = 10f,
+ bool missile = false,
+ ushort teleportSequence = 0,
+ ushort forcePositionSequence = 0,
+ ushort movementSequence = 1,
+ ushort serverControlSequence = 1)
+ {
+ CreateObject.ServerPosition? position = includePosition
+ ? new CreateObject.ServerPosition(Cell, positionX, 20f, 7f, 1f, 0f, 0f, 0f)
+ : null;
+ uint rawState = (uint)(PhysicsStateFlags.Gravity
+ | (missile ? PhysicsStateFlags.Missile : 0));
+ var timestamps = new PhysicsTimestamps(
+ Position: positionSequence,
+ Movement: movementSequence,
+ State: 1,
+ Vector: 1,
+ Teleport: teleportSequence,
+ ServerControlledMove: serverControlSequence,
+ ForcePosition: forcePositionSequence,
+ ObjDesc: 1,
+ Instance: incarnation);
+ var physics = new PhysicsSpawnData(
+ rawState,
+ position,
+ Movement: null,
+ AnimationFrame: null,
+ SetupTableId: null,
+ MotionTableId: null,
+ SoundTableId: null,
+ PhysicsScriptTableId: null,
+ Parent: parentGuid is { } parent ? new PhysicsAttachment(parent, 1u) : null,
+ Children: null,
+ Scale: null,
+ Friction: null,
+ Elasticity: null,
+ Translucency: null,
+ Velocity: null,
+ Acceleration: null,
+ AngularVelocity: null,
+ DefaultScriptType: null,
+ DefaultScriptIntensity: null,
+ timestamps);
+ return new WorldSession.EntitySpawn(
+ guid,
+ position,
+ SetupTableId: null,
+ Array.Empty(),
+ Array.Empty(),
+ Array.Empty(),
+ BasePaletteId: null,
+ ObjScale: null,
+ Name: "initial-create",
+ ItemType: null,
+ MotionState: null,
+ MotionTableId: null,
+ PhysicsState: rawState,
+ InstanceSequence: incarnation,
+ MovementSequence: timestamps.Movement,
+ ServerControlSequence: timestamps.ServerControlledMove,
+ PositionSequence: positionSequence,
+ ParentGuid: parentGuid,
+ ParentLocation: parentGuid is null ? null : 1u,
+ Physics: physics);
+ }
+
+ private sealed class EntityObserver(Action onEntity)
+ : IRuntimeEntityObjectObserver
+ {
+ public void OnEntity(in RuntimeEntityDelta delta) => onEntity(delta);
+ public void OnInventory(in RuntimeInventoryDelta delta)
+ {
+ }
+ }
+}