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) + { + } + } +}