diff --git a/docs/plans/2026-08-02-placement-cutover.md b/docs/plans/2026-08-02-placement-cutover.md index 12da3be9..648d0232 100644 --- a/docs/plans/2026-08-02-placement-cutover.md +++ b/docs/plans/2026-08-02-placement-cutover.md @@ -74,12 +74,24 @@ same commit) → docs/handoff commit. No workarounds; no fused slices. deferral; refine at C3/C4 if the connected gates show it matters); (c) any future host exposure of `TrySetAutonomyLevel` must carry retail's `SendAutonomyLevelEvent` (699550). -- **C1 — atomic controller/body publication (prerequisite C).** One - Runtime-owned transaction publishing the exact same canonical body to - graphical and no-window controllers, prepared off-canonical, committed - atomically, respected by every body writer/binding/clock-epoch/teardown - path. The rejected snapshot-lease design stays rejected. Adversarial-gate - heavy (the campaign handoff's full list). +- **C1 — atomic controller/body publication — SATISFIED BY EXISTING + MECHANISM (research finding 2026-08-02, plan amended same session).** + `RuntimeLocalPlayerPhysicsPublicationState` (1,033 lines) plus the + ~15-method dormant local-activation family on `RuntimeSetPositionState` + already implement the full sanctioned option-2 transaction: + off-canonical preparation against a scratch quantum clock and a sealed + candidate controller, one validated atomic Commit, and a staged + Evaluate/Commit/FinalizeActivation chain re-validated against + PhysicsOwnershipEpoch/ObjectClockEpoch/ControllerOwnershipEpoch/session + identity at every entry — with zero production callers. See + [`2026-08-02-canonical-body-writer-map.md`](../research/2026-08-02-canonical-body-writer-map.md) + (6 canonical body writers; the two host escape hatches; both hosts' + divergences). The remaining work — routing both hosts' local-player + construction through the publication lifecycle, sealing the public + `RuntimeLocalPlayerMovementState.Controller` setter, retiring App's + direct object-clock bypasses, and containing headless's uncaught + prepared-collision `InvalidDataException` — IS the C3 route-1 flip and + moves there. No separate C1 commit. - **C2 — placement allocation budget.** Pool or eliminate the operation/ projection envelope allocations on the accepted placement path (root cause, not a raised cap), or obtain explicit user approval for a measured diff --git a/docs/research/2026-08-02-canonical-body-writer-map.md b/docs/research/2026-08-02-canonical-body-writer-map.md new file mode 100644 index 00000000..f49dce02 --- /dev/null +++ b/docs/research/2026-08-02-canonical-body-writer-map.md @@ -0,0 +1,681 @@ +# C1 body/controller-publication writer map (2026-08-02) + +Repo: `C:\Users\erikn\.codex\worktrees\af5e\acdream`, branch `codex/port-claude-agents`, +HEAD `ae296393`. READ-ONLY research; this file is the only write target. + +Context read: `docs/plans/2026-08-02-placement-cutover.md` (slice C1), +`docs/research/2026-07-31-remaining-physics-campaign-handoff.md` (rejected-prototype +section, lines 143-168; prerequisite C, lines 203-221), and +`docs/research/2026-08-02-cutover-route-inventory.md` route 1 + prerequisite-C +section (lines 174-220) + route 8 (headless). + +--- + +## 1. Every writer of `RuntimeEntityRecord.PhysicsBody` + +`PhysicsBody` is `public PhysicsBody? PhysicsBody { get; private set; }` +(`src/AcDream.Runtime/Entities/RuntimeEntityRecord.cs:72`). The ONLY mutator is +the internal method `SetPhysicsBody(PhysicsBody? body)` +(`RuntimeEntityRecord.cs:176-182`): +``` +internal void SetPhysicsBody(PhysicsBody? body) +{ + if (ReferenceEquals(PhysicsBody, body)) return; + PhysicsBody = body; + PhysicsOwnershipEpoch++; // <-- the ONLY place PhysicsOwnershipEpoch is bumped +} +``` +So every "writer" is a caller of `.SetPhysicsBody(...)` (all 6 call sites, confirmed +by full-repo grep, zero others): + +1. **`RuntimeEntityDirectory.cs:359`** — inside + `GetOrCreatePhysicsBody(RuntimeEntityRecord record, Func factory)` + (need exact surrounding signature — read below). Public/internal API used by + the route-1 "SECOND, narrower body-construction duplicate authority" for + non-player static-animating physics objects + (`DatLiveEntityProjectionMaterializer.cs:1003-1016`, per the route inventory). + Guard: only sets if record has no body yet (idempotent-create pattern) — see + full read below for exact guard. +2. **`RuntimeEntityObjectLifetime.cs:766`** — `Entities.SetPhysicsBody(canonical, null)` + inside a teardown method (need to confirm exact method — likely delete/retire + path, paired with `Entities.SetPhysicsBodyAcquisitionInProgress(canonical, false)` + at line 767 in the SAME method). Clears body on deletion/teardown. +3. **`RuntimeLocalPlayerPhysicsPublicationState.cs:405`** — `candidate.Record.SetPhysicsBody(candidate.Body)` + inside `Commit(token, out activationToken)` (lines 373-411). **THIS IS THE + DORMANT OPTION-2 MECHANISM** — see section 4 below. Guarded by `IsCurrent(candidate)` + (epoch/session/identity/null-state re-check, lines 891-922) immediately before, + and by `_physics.SetPosition.PrepareDormantLocalActivationOwnership(...)` called + first (line 394) as the "seal the exact SetPosition owner before the + irreversible no-fail suffix" step — i.e. this call site DOES chain into + PrepareDormantLocalActivationOwnership per task 4's target. +4. **`RuntimeLocalPlayerPhysicsPublicationState.cs:1025`** — `_entities.SetPhysicsBody(activation.Record, null)` + inside `DiscardActivation()` (994-1032), the rollback/teardown path for the + SAME dormant mechanism — only fires if `_entities.IsCurrent(activation.Record)` + AND `ReferenceEquals(activation.Record.PhysicsBody, activation.Body)` (i.e. + never clobbers a body some OTHER newer owner already installed — the exact + anti-pattern the rejected prototype failed on). +5. **`RuntimePhysicsState.cs:1558`** — `Entities.SetPhysicsBody(record, candidateBody)` + — need full read; this is inside the remote/projectile body-binding family + (see section 3). +6. **`RuntimePhysicsState.cs:1666`** — `Entities.SetPhysicsBody(record, candidate)` + — need full read; this is the OTHER binding site, guarded by + `PhysicsBodyAcquisitionInProgress` (set true at :1645, cleared at :1676/1678). + +**Writer count: 6 call sites, across 3 files** (`RuntimeEntityDirectory.cs` x1, +`RuntimeEntityObjectLifetime.cs` x1, `RuntimeLocalPlayerPhysicsPublicationState.cs` x2, +`RuntimePhysicsState.cs` x2). + +## Consumers of `PhysicsOwnershipEpoch` + +Only bumped in one place (`RuntimeEntityRecord.SetPhysicsBody`, above). Consumers +(all in `RuntimeLocalPlayerPhysicsPublicationState.cs`) treat it as a +compare-and-reject epoch stamped into every token/activation struct: +- `RuntimeLocalPlayerPhysicsPublicationToken.PhysicsOwnershipEpoch` (field, :53) + captured at `Prepare` time (:314). +- `RuntimeLocalPlayerPhysicsActivationToken.PhysicsOwnershipEpoch` (:70) captured + as `token.PhysicsOwnershipEpoch + 1UL` (:328) — i.e. the activation token + encodes "the epoch AFTER my own commit bumps it", so `IsActivationCurrent` + (931-962) and `IsActivationOwnershipEnvelopeCurrent` (717-745) comparing + `activation.Record.PhysicsOwnershipEpoch == activation.Token.PhysicsOwnershipEpoch` + will FAIL (reject) the instant any OTHER writer (remote/projectile bind, GC + clear, non-player static body creation via `RuntimeEntityDirectory` — none of + which should ever touch a local-player record, but the check is defense-in-depth) + touches the same record's PhysicsBody between prepare and commit. +- `IsCurrent(candidate)` (891-922, pre-Commit re-check) also compares + `candidate.Record.PhysicsOwnershipEpoch == candidate.Token.PhysicsOwnershipEpoch` + (unincremented — i.e. "nobody touched the body between Prepare and Commit"). + +**This IS the reentrancy defense the rejected prototype lacked** — see section 6. + +## 2. The two production local-player controller constructions, end to end + +### Graphical: `PlayerModeController.BuildControllerAndCamera` +`src/AcDream.App/Input/PlayerModeController.cs:244-525`. Constructor list +(52-74) shows it is injected with `RuntimeLocalPlayerMovementState controllerSlot` +(the SAME slot type headless writes) — confirms the route-inventory's "open +question" (2026-08-02-cutover-route-inventory.md:204-207): **App DOES write +`_controllerSlot.Controller = controller` directly, at line 486.** Not a +mystery/asymmetry — both hosts write the exact same public setter. + +Steps, in order: +1. `_approachCompletions.BeginControllerLifetime()` (250) — App-only approach + lifecycle token. +2. Capture rollback snapshots: `_camera.CaptureState()` (255), + `_shadow.Capture()` (256) — presentation-only. +3. `new PlayerMovementController(_physics, playerRecord.ObjectClock, PlayerMovementConstructionOptions.From(_skills.Snapshot))` + (259-262) — **uses the PUBLIC constructor**, whose default publication + lifecycle is `StandalonePublished` (`PlayerMovementController.cs:617-626`), + NOT `CandidatePreparing`/`CreatePublicationCandidate`. This is the key + divergence from the dormant mechanism (section 4): this controller never + enters the `CandidatePreparing -> CandidateSealed -> RuntimeOwnedDormant -> + RuntimePublished` lifecycle at all. +4. Builds `MoveToManager`/`EntityPhysicsHost` closures over captured locals + (267-346) — presentation-adjacent glue, host-specific. +5. `EntityPhysicsHostComposition.SelectStableHostWithoutRebind` (347-350) — + canonical-state read (checks `LiveEntityRecord.PhysicsHost`). +6. `RuntimeMovementSkillProjection.ApplyTo(_skills, controller)` (366-368). +7. `ApplyStepHeights(controller, playerEntity, playerGuid)` (375) — **reads + `DatReaderWriter.DBObjs.Setup` directly** (per headless's own comment + contrasting itself, `HeadlessSessionWorldProjection.cs:685-689`) — NOT + through the prepared-collision/`IPreparedCollisionSource` seam headless + uses. Divergence #1. +8. `_controllerSlot.BeginMotionPreparation(controller, drainPriorAnimationQueue)` + (404-407) — the ONE existing narrow "preparation lease" concept already in + `RuntimeLocalPlayerMovementState` (separate from the dormant physics + publication state) that lets a synchronous PartArray/type-5 completion + reach the candidate `MotionInterpreter` before publish. +9. **Duplicate authority** — `_physics.Resolve(...)` (409-413) then + `_physics.ResolvePlacement(...)` (422-430) — direct canonical-state-free + collision resolve, entirely outside `RuntimeSetPositionState`. +10. `controller.PreparePositionForCommit(...)` (434-437), + `controller.SetBodyOrientation(...)` (438). +11. Camera construction + `_camera.EnterChaseMode(...)` (440-447) — + presentation-only, but happens BEFORE the final canonical commit (445-447 + precede line 482-484) — i.e. camera activation today is NOT gated on a + Runtime placement acknowledgement. +12. Re-check host stability (449-458) — throws if the host changed during + camera activation (defensive, but ad hoc — not an epoch/token check, a + bespoke `ReferenceEquals` re-read). +13. Shadow sync (`_shadow.SyncPose(...)`, 460-466). +14. `EntityPhysicsHostComposition.InstallOrRebind(...)` (472-475) + another + `ReferenceEquals` stability re-check (476-480). +15. **Duplicate authority — final commit** (482-484): + `playerEntity.SetPosition(initial.Position); playerEntity.ParentCellId = + initial.CellId; controller.CommitPreparedPosition();` — direct writes to + the App-side `WorldEntity`/render sidecar AND `controller`'s own internal + frame, bypassing any Runtime `Place` receipt or `RuntimeEntityRecord` + write. **`RuntimeEntityRecord.PhysicsBody`/`PhysicsOwnershipEpoch` are + NEVER touched anywhere in this method** — `controller.PhysicsBody` (the + `_body` field created in step 3) stays a private field of the + `StandalonePublished` controller; nothing calls + `Entities.SetPhysicsBody(playerRecord, controller.PhysicsBody)`. This + means TODAY the canonical `RuntimeEntityRecord.PhysicsBody` slot for the + graphical local player is **never populated at all** by this path — a + previously-unstated confirmation that `SubmitPreparedPlacement`'s + `operation.Record.PhysicsBody is not { } body` requirement (section 3) + would REJECT any ordinary (non-initial) SetPosition submitted for the + graphical local player today, because no writer ever puts a body on that + record. (Route 2's "ForcePosition" duplicate authority, + `LocalForcePositionTransaction`, works around this by mutating + `PlayerMovementController`'s own body directly via `BlipPosition`, never + touching `RuntimeEntityRecord.PhysicsBody` either — internally consistent + with each other, both equally disconnected from the canonical record.) +16. Slot commits (485-492): `_hostSlot.Host`, `_controllerSlot.Controller = + controller` (the public, unguarded setter — bumps `ControllerOwnershipEpoch` + unconditionally, see section 4), `_chase.Legacy/Retail`, `_mode.IsPlayerMode + = true`. +17. `catch`: rolls back camera + shadow only (494-518); does NOT roll back + steps 15-16 because those are the LAST lines before `lifetimeCommitted = + true` — structurally "hope nothing after this throws" rather than an + explicit no-fail invariant. + +**Canonical-state mutations in this method: NONE on `RuntimeEntityRecord`** +(no `SetPhysicsBody`, no `SetFullCell`, no object-clock call) — everything +mutated is App-local (`WorldEntity`, `PlayerMovementController`'s private +body, `RuntimeLocalPlayerMovementState.Controller`, +`LocalPlayerPhysicsHostSlot`, camera, shadow). The ONLY canonical-record +writes for the local player's initial placement happen earlier in the +hydration pipeline (`LiveEntityRuntime.MaterializeLiveEntity`/ +`RebucketLiveEntity`, route 1 hops 9-11) — entirely disjoint from this method. + +### Headless: `HeadlessSessionWorldProjection.CreateController` + `SynchronizeLocalPlayer` +`src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs:566-655` +(read in full). + +`SynchronizeLocalPlayer` (566-615): +1. Guards on `record.ServerGuid == _runtime.PlayerIdentity.ServerGuid` and a + present `Snapshot.Position` (568-573). +2. `_collision.CenterOn(position.LandblockId)` (575) — headless collision- + neighborhood readiness, no graphical analog. +3. `_runtime.MovementOwner.Controller ?? CreateController(record)` (576-578) + — lazy-construct-once via the SAME public `Controller` getter/setter + `RuntimeLocalPlayerMovementState` exposes; no reentrancy guard against two + concurrent calls both observing `null` (single-threaded host loop makes + this safe in practice today, not structurally). +4. `_runtime.EntityObjects.Physics.Engine.Resolve(...)` (589-594) then + `.ResolvePlacement(...)` (595-605) — **the exact same duplicate-authority + shape as graphical step 9**, hardcoded `DefaultRadius`/`DefaultHeight` + constants (visible in the call, actual values not read here) instead of + `_motionBindings.GetSetupCylinder`. +5. `controller.SetPosition(...)` + `controller.SetBodyOrientation(...)` + (610-614) — **duplicate final commit**, headless's version of graphical + step 15. Also never touches `RuntimeEntityRecord.PhysicsBody`. + +`CreateController` (639-655): +1. `new PlayerMovementController(_runtime.EntityObjects.Physics.Engine, record.ObjectClock, PlayerMovementConstructionOptions.From(_runtime.CharacterOwner.MovementSkills.Snapshot))` + (642-646) — **same PUBLIC constructor / `StandalonePublished` lifecycle** + as graphical step 3. +2. `ApplySetupStepHeights(record, controller)` (649, body at 657-691) — + **reads via `_preparedCollision.ReadSetupCollision(setupId)`** (668-679), + the prepared-asset seam, NOT raw DAT — divergence #1 mirrored (headless + uses the "correct"/prerequisite-B-aligned source; graphical does not). + **Throws `InvalidDataException`** if the read status isn't `Loaded` + (672-675) — propagates uncaught up through `SynchronizeLocalPlayer` -> + `ProjectSpawn`/`ProjectPosition` -> the wire-dispatch call chain. This IS + gate 10 (late headless prepared-collision failure) manifesting today as an + unhandled exception, not a retry. +3. `RuntimeMovementSkillProjection.ApplyTo(_runtime.CharacterOwner.MovementSkills, controller)` + (650-652). +4. `_runtime.MovementOwner.Controller = controller;` (653) — **the exact + same public setter graphical step 16 uses.** + +**No headless equivalent of graphical steps 1 (approach lifetime), 8 (motion +preparation lease), 11-14 (camera + host-stability re-checks), 17 (camera/ +shadow rollback)** — headless has no camera/shadow/approach concept at all +(confirmed, matches the route inventory's "No headless equivalent of +graphical hops 12-14/19"). + +### Top divergences between the two hosts (summary) +1. **Setup/collision data source**: App reads raw DAT (`ApplyStepHeights` via + `_dats`/`_datLock`); headless reads the prepared/baked asset + (`ApplySetupStepHeights` via `IPreparedCollisionSource`). Same target + values, different pipeline — a real fidelity risk if the two ever diverge + (baking staleness). +2. **Default cylinder fallback**: App falls back to `0.48f`/`1.835f` inline + (`PlayerModeController.cs:416-420`) when `GetSetupCylinder` returns + `< 0.05f` radius; headless uses named `DefaultRadius`/`DefaultHeight` + constants at the `ResolvePlacement` call site (:599-600) — same intended + values, defined in two places. +3. **Failure handling**: App's `BuildControllerAndCamera` has an explicit + try/catch/rollback for camera+shadow; headless's `CreateController`/ + `ApplySetupStepHeights` has NO surrounding try/catch — a prepared-collision + read failure is a raw unhandled exception today. +4. **Presentation surface**: App additionally owns approach-completion + lifetime, motion-preparation lease, chase camera, shadow sync — none of + which headless has or needs. +5. **Neither host touches `RuntimeEntityRecord.PhysicsBody`, `PhysicsOwnershipEpoch`, + or any `RuntimeSetPositionState` API** — both are 100% off to the side of + the canonical record, confirmed by exhaustive grep (section 1's 6 writer + call sites do not include either `PlayerModeController.cs` or + `HeadlessSessionWorldProjection.cs`). + +--- + +## 3. Every other body binding/consumer + +- **Remote dead-reckoning** (`RuntimeRemotePhysicsUpdater.cs` — flagged + protected/dirty, read-only, NOT modified): grep confirms it only READS + `record.PhysicsBody` via `ReferenceEquals(record.PhysicsBody, remote.Body)` + currency checks (line 817) — it does not call `SetPhysicsBody`. The actual + writer for remote motion is `RuntimePhysicsState.SetRemoteMotion` + (`RuntimePhysicsState.cs:1446-1570`, full read) — throws + `InvalidOperationException` on: binding-already-in-progress (1460-1464), + body-would-be-replaced when a body already exists and doesn't match + (1487-1492), losing an existing remote-placement contract (1493-1498), or + post-callback ownership drift detected via a captured + `sessionVersion`/`expectedBody`/`expectedRuntime` triple re-checked after + the bind callback (1539-1549, "changed ownership during remote-motion + binding"). Calls `Entities.SetPhysicsBody(record, candidateBody)` (:1558) + ONLY when `expectedBody is null` (first bind) via `InitializeNewPhysicsBody` + (:1556) — i.e. this is throw-on-conflict exclusivity (Option-1 flavor), not + epoch/token gating. For a LOCAL PLAYER record this path should never fire + (remote motion is for non-local entities) but the guard is defense-in-depth + and IS one of the explicit gate checks + (`!activation.Record.RemoteMotionBindingInProgress`/`RemoteMotion is null`) + the dormant local-publication mechanism re-validates at every stage + (section 4). +- **Projectile binding**: `RuntimeProjectilePhysicsUpdater.cs` similarly only + READS `record.PhysicsBody` (lines 447, 459, `ReferenceEquals` currency + checks). The writer is `RuntimePhysicsState.BindProjectile` + (`RuntimePhysicsState.cs:1309-1382`, full read) — same throw-on-conflict + shape: binding-in-progress (1343-1347), body-mismatch on rebind + (1330-1337), must-already-own-canonical-body-before-binding + (1348-1352, "projectile must borrow its canonical physics body" — i.e. + UNLIKE remote motion, `BindProjectile` requires `record.PhysicsBody` to + ALREADY be non-null and matching BEFORE it will bind — it never calls + `InitializeNewPhysicsBody`/`SetPhysicsBody` itself for a first-time body; + something else (route 5's `ProjectileController.TryBind`, + `ProjectileController.cs:176-265` per the route inventory) must construct + the body ad hoc first via a DIFFERENT path than `GetOrCreatePhysicsBody` + — worth flagging: **this is a 7th, App-side, ad hoc body-construction site + not funneled through any of the 6 canonical writer methods** — App's + `ProjectileController.TryBind` constructs a body and must be setting it onto + the record through some other route (not confirmed by this pass; App-side + `ProjectileController.cs` was not read in full — flag as open item, but it + is explicitly OUT of C1's local-player scope per the campaign handoff's + gate list item "remote and projectile binding/update" being about + *interaction with* the local-player transaction, not projectile's own + authority). +- **`RuntimeSetPositionState.SubmitPreparedPlacement`** (`RuntimeSetPositionState.cs:2224-2274`, + full read): requires `operation.Record.PhysicsBody is not { } body` to + already be true (line 2254) — i.e. EVERY non-initial-construction + SetPosition submission (ForcePosition, portal, remote Position, projectile + correction) requires a body to already exist on the record, confirming the + 6 writer sites in section 1 are the exhaustive set of "who can put the + FIRST body on a record." For the local player specifically, only + `RuntimeLocalPlayerPhysicsPublicationState.Commit` (section 4) does this + today (dormant, unwired); in PRODUCTION, no writer ever populates + `RuntimeEntityRecord.PhysicsBody` for either host's local player (section + 2 finding) — meaning `SubmitPreparedPlacement` would reject a local-player + submission in production today, which is consistent with the route + inventory's finding that Route 2 (ForcePosition) and Route 3 (portal) both + bypass `RuntimeSetPositionState` entirely via their own duplicate + authorities instead. +- **`RuntimeSetPositionState.PrepareDormantLocalActivationOwnership`**: see + section 4 — the ONE place the local-player-specific dormant body attach + happens; requires a pre-opened `Operation` in stage `AwaitingPreparation` + from `TryBeginExclusiveAuthoredPlacement`. +- **Object-clock epoch transitions** + (`RuntimeEntityRecord.SuspendObjectClock`/`ResetObjectClockForEnterWorld`, + both `internal`, bump `ObjectClockEpoch`): full call-site grep found BOTH + the expected `RuntimeEntityDirectory` wrapper call sites (which run + `EnsureKnown(record)` first, `RuntimeEntityDirectory.cs:311-321`) AND + **direct unwrapped calls from `src/AcDream.App/World/LiveEntityRuntime.cs` + at lines 879, 891, 897, 1258, 3030, 3033** — App calls + `record.SuspendObjectClock()`/`record.ResetObjectClockForEnterWorld(...)` + straight on the `RuntimeEntityRecord` (accessible because these are + `internal` and `AcDream.App` has `InternalsVisibleTo`), bypassing the + `RuntimeEntityDirectory` facade's `EnsureKnown` check entirely. This is + inside `LiveEntityRuntime`'s `RebucketLiveEntity`-family code (comment + references `prepare_to_enter_world`/retail `update_object`'s parent + early-out — matches the already-known route-1/prerequisite-D + `RebucketLiveEntity` duplicate authority). **Previously-unstated + implication for C1**: the SAME record whose `ObjectClockEpoch` the dormant + publication mechanism gates on can have its epoch bumped by this + direct-call path DURING the window between + `RuntimeLocalPlayerPhysicsPublicationState.Prepare` and `.Commit()`/ + `.CommitActivation()` if `RebucketLiveEntity` runs concurrently for the + SAME entity (e.g. a second CreateObject/Position causing a re-rebucket + mid-construction) — the epoch check (`IsCurrent`/`IsActivationOwnershipEnvelopeCurrent` + comparing `record.ObjectClockEpoch == token.ObjectClockEpoch`) WOULD catch + and reject this correctly (fail-safe), but it confirms the gate is load- + bearing against a REAL, already-existing production writer, not a + hypothetical. +- **Deletion/teardown**: `RuntimeEntityObjectLifetime.TryAcceptDelete` + (`RuntimeEntityObjectLifetime.cs:1555+`) calls `Entities.TryDelete` then + `Entities.RemoveActive(active)` (1587) — this IMMEDIATELY flips + `Entities.IsCurrent(record)` to `false` for that record (removes it from + the active-by-guid table), which is the single check + `CanPrepare`/`IsCurrent`/`IsActivationCurrent`/every gate in section 4 + depends on — so a delete landing at any point rejects the in-flight + publication transaction on its NEXT check. Full body clear happens later + in `RuntimeEntityObjectLifetime.CompleteProjectionRetirement` + (:745-771, called from `RetireCanonicalOnly`/the graphical teardown-ack + path): `Entities.SetPhysicsBody(canonical, null)` (:766) after + `Physics.SetPosition.Forget(canonical, releasePreparedMover: true)` (:753, + cancels any in-flight ordinary placement) and + `ForgetInitialCreateResidence(canonical)` (:751, cancels any in-flight + residence lease) — i.e. deletion cancels BOTH placement-lease families + before clearing the body, consistent with prerequisite E's "quiesce before + demote" discipline (though for landblock collision, not entity teardown — + the pattern rhymes). +- **`RuntimePhysicsState` per-frame body access**: `RuntimeOrdinaryPhysicsUpdater.cs`, + `RuntimeRemotePhysicsUpdater.cs`, `RuntimeProjectilePhysicsUpdater.cs` each + gate their per-tick work on `record.PhysicsBody is not { } body` / + `ReferenceEquals(record.PhysicsBody, body)` currency checks (grep-confirmed, + e.g. `RuntimeOrdinaryPhysicsUpdater.cs:68,284`) — read-only w.r.t. the + `PhysicsBody` reference itself (they mutate the BODY's internal fields + every tick, which is expected/normal simulation, not an ownership-slot + write). No workset iterates and calls `SetPhysicsBody`. `RuntimePhysicsState.cs` + itself has only two visible "workset" mentions (`ClearSpatialWorksets` at + :1951, a doc-comment at :1307) — the ordinary/remote/projectile worksets + live in their respective `RuntimeXPhysicsUpdater` files, out of this pass's + read budget beyond the grep-confirmed read-only currency pattern above. + +--- + +## 4. `RuntimeSetPositionState.PrepareDormantLocalActivationOwnership` — nucleus or dead end? + +**Definition** (`RuntimeSetPositionState.cs:1026-1052`, full read): +```csharp +internal void PrepareDormantLocalActivationOwnership( + RuntimeEntityRecord record, PhysicsBody body, + in RuntimeEntityPlacementToken token) +{ + ... + if (!token.IsValid + || record.Key != token.Entity + || !_operations.TryGetValue(token.Entity, out Operation? operation) + || operation.Token != token + || operation.Stage is not RuntimeEntityPlacementStage.AwaitingPreparation + || !ReferenceEquals(operation.Record, record) + || record.PhysicsBody is not null // <- record must have NO body yet + || !IsCurrent(operation) + || body.InWorld + || (body.TransientState & TransientStateFlags.Active) != 0) + { + throw new InvalidOperationException( + "Dormant local activation must bind to the exact current placement owner."); + } + operation.Body = body; + operation.DormantLocalActivation = true; +} +``` +It THROWS (does not return a status) on any invariant violation — by design a +"this should be structurally impossible if the caller validated first" +assertion, not a retryable rejection. It requires a PRE-EXISTING placement +`Operation` already opened via `TryBeginExclusiveAuthoredPlacement` +(`RuntimeSetPositionState.cs:1004-1024`) in stage `AwaitingPreparation` — i.e. +it is NOT a standalone entry point; it is ONE STEP inside a larger chain that +also needs prerequisite B's mover-preparation authority +(`IsExactPreparedPlacementCurrent`, `RuntimeSetPositionState.cs:1318-1340`) +satisfied for the SAME token/command before +`RuntimeLocalPlayerPhysicsPublicationState.CanPrepare` will even call it. + +**What it was built for**: it is called from exactly ONE place in the whole +repo — `RuntimeLocalPlayerPhysicsPublicationState.Commit` +(`RuntimeLocalPlayerPhysicsPublicationState.cs:394-397`), as the "seal the +exact SetPosition owner before the irreversible no-fail suffix" step, +immediately before `candidate.Controller.CommitRuntimeOwnership(...)` and +`candidate.Record.SetPhysicsBody(candidate.Body)`. It exists purely to make +the PLACEMENT OPERATION (owned by `RuntimeSetPositionState`) and the BODY +(owned by `RuntimeEntityRecord`) become mutually aware atomically, so that +the SAME operation can later be walked through the full retail SetPosition +staged commit (ground phase -> collision dispatch -> response -> final +commit) via `TryEvaluateDormantLocalActivation` -> +`TryPrepareDormantLocalActivationCommit` -> +`TryApplyDormantLocalActivationCommit` -> +`TryPrepareDormantLocalActivationFinalCommit` -> +`TryApplyDormantLocalActivationFinalCommit` +(`RuntimeSetPositionState.cs:2025-2077`, full read of the final-commit +method) — the LAST of which is where `_entities.SetFullCell`, +`_entities.AdvancePlacementCommit`, `body.InWorld = true`, +`_entities.SetPhysicsHost`, `controller.CommitRuntimeActivationFrame()`, +`_physics.Engine.UpdatePlayerCurrCell`, `_physics.AcknowledgeSpatialProjection`, +`_entities.ResetObjectClockForEnterWorld` (object-clock epoch bump, task 3), +and `controller.ActivateRuntimePublication()` (controller goes LIVE) ALL +happen in one synchronous, no-branch-for-failure block (:2025-2077), gated +immediately before by `IsDormantLocalActivationPrephaseCurrent`/re-validated +epoch checks. **This is genuinely the full retail SetPosition commit, +already ported, already wired to the same body/controller the dormant +publication candidate built.** + +**Verdict: NUCLEUS, not a dead end** — but it is only ONE LOAD-BEARING STEP +inside a much larger, ALREADY-COMPLETE mechanism: +`RuntimeLocalPlayerPhysicsPublicationState` (1033 lines, +`src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs`) ++ its ~15 `RuntimeSetPositionState` dormant-activation methods. Constructed +once at `GameRuntime.cs:261` and exposed via +`RuntimeLocalPlayerMovementState.PhysicsPublication` (:59-61, itself +`internal`, throws if unbound). **Confirmed by exhaustive grep: ZERO +production callers in `src/AcDream.App/` or `src/AcDream.Headless/`** — the +only callers anywhere are `tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerPhysicsPublicationStateTests.cs`. +This is, functionally, **Option 2 from the rejected-prototype note ("Off- +canonical preparation followed by one validated atomic Runtime commit that +publishes the prepared controller/body relationship without copying stale +state over newer authority") already built end to end** — complete with: +- a `Prepare`/`Commit`/`Discard` triad for the BODY/CONTROLLER pair + (analogous to, and reusing, the SAME token-epoch pattern as + `RuntimeSetPositionState`'s ordinary placement operations); +- a SEPARATE `EvaluateActivation`/`CommitActivation`/`DiscardActivation` triad + for actually driving the body through retail SetPosition's staged commit + once the body/controller pair is sealed; +- re-validation of `PhysicsOwnershipEpoch`, `ObjectClockEpoch`, + `ControllerOwnershipEpoch`, `SessionLifetimeVersion`, identity + `ServerGuid`+`Revision`, and null/in-progress state for RemoteMotion/ + Projectile/PhysicsHost/DeleteAcceptedForTeardown at EVERY external entry + point (`CanPrepare`, `IsCurrent`, `IsActivationCurrent`, + `IsActivationOwnershipEnvelopeCurrent`, + `IsCommittedActivationSuffixCurrent`) — this IS the reentrancy defense the + rejected snapshot-lease prototype explicitly lacked (see section 6). + +**What is genuinely missing (the real C1 work), given this mechanism already +exists:** +1. Nobody calls `TryBeginExclusiveAuthoredPlacement` + prerequisite B's + `PrepareMover`/`ReadSetupCollision` chain + `PhysicsPublication.Prepare`/ + `Commit`/`EvaluateActivation`/`CommitActivation` from either host — this IS + the wiring gap, exactly like every other route in the cutover. +2. **The public `RuntimeLocalPlayerMovementState.Controller` setter + (`RuntimeLocalPlayerMovementState.cs:37-50`) remains a live, unguarded + escape hatch** — both `PlayerModeController.BuildControllerAndCamera:486` + and `HeadlessSessionWorldProjection.CreateController:653` write it + directly today, and NOTHING stops either host from continuing to do so + even after C1 wires the dormant mechanism, unless that direct-write path + is deleted/sealed off (e.g. made `internal` to only + `RuntimeLocalPlayerPhysicsPublicationState`/`CommitRuntimeOwnedController`). + The setter has NO epoch/token check on write (`CanCommitRuntimeOwnedController` + is a SEPARATE, unused-by-the-setter validation method) — it will happily + accept a second unguarded assignment even while a dormant activation is + in flight, silently retiring whatever the dormant mechanism just + published (`_controller?.RetireRuntimePublication()` at :45, which is a + real no-op for anything not currently `RuntimeOwnedDormant`/ + `RuntimePublished` — see section 6 gate 7). **This is the single most + important pre-existing defect C1 must close: the "exclusive" adjective in + prerequisite C's "one Runtime-owned exclusive/versioned...transaction" + is not yet true while this direct setter remains reachable from hosts.** +3. Neither `new PlayerMovementController(physics, objectClock, options)` + (public ctor, `StandalonePublished`) call site in the two hosts has been + swapped for `PlayerMovementController.CreatePublicationCandidate` — until + that swap happens, controllers built by either host never enter the + `CandidatePreparing/CandidateSealed/RuntimeOwnedDormant/RuntimePublished` + lifecycle the dormant mechanism's gates all key off of. + +--- + +## 5. `PlayerMovementController` construction requirements + +Constructor needs (from both direct-ctor call sites AND +`CreatePublicationCandidate`, `PlayerMovementController.cs:617-690`): +- `PhysicsEngine physics` (shared engine reference, both hosts pass their own + `RuntimePhysicsState`/`_runtime.EntityObjects.Physics.Engine`). +- `RetailObjectQuantumClock? objectClock` — App passes `playerRecord.ObjectClock` + (the CANONICAL record's clock, `RuntimeEntityRecord.ObjectClock` at + `RuntimeEntityRecord.cs:62`, always non-null per its field initializer); + headless passes `record.ObjectClock` identically. The dormant mechanism's + `CreatePublicationCandidate` instead passes a THROWAWAY + `new RetailObjectQuantumClock()` (`PlayerMovementController.cs:687-688`) at + construction time and only swaps in the REAL + `candidate.Record.ObjectClock` later, inside `Commit`, via + `controller.CommitRuntimeOwnership(candidate.Record.ObjectClock)` + (`RuntimeLocalPlayerPhysicsPublicationState.cs:403-404` -> + `PlayerMovementController.cs:774-786`) — i.e. the dormant candidate is + built against a SCRATCH clock so construction can never observe or mutate + the canonical record's real clock before the atomic commit swaps it in. + This is exactly the "off-canonical preparation" half of Option 2. +- `PlayerMovementConstructionOptions` (RunSkill/JumpSkill) — both hosts build + via `PlayerMovementConstructionOptions.From()`; the dormant mechanism's `Prepare` also takes this as a caller- + supplied parameter (`Prepare(..., PlayerMovementConstructionOptions + options, ...)`, :191) — no divergence in shape, only in WHERE the skill + snapshot is read from (App's local `_skills` field vs. headless's + `_runtime.CharacterOwner.MovementSkills` vs. the dormant mechanism taking + it as a caller parameter either way). + +What construction MUTATES (beyond the private `_body`): `LocalEntityId`, +`StepUpHeight`/`StepDownHeight` (Setup-derived), `SphereList` (Setup-derived, +prerequisite B territory), `ObjectScale`, initial position/orientation via +`PreparePositionForCommit`/`SetBodyOrientation`, physics state via +`ApplyPhysicsState`, `MoveToFactory`/`PositionManager` +(`MovementManager`/`MotionInterpreter` wiring). ALL of this is exactly what +`RuntimeLocalPlayerPhysicsPublicationState.Prepare` +(`RuntimeLocalPlayerPhysicsPublicationState.cs:187-355`) already does against +its private `CreatePublicationCandidate`-built controller, reading +`command.Physics.StepUpHeight/StepDownHeight/Spheres/Scale/Position/CellId/ +CellLocalPosition/Orientation` from the CALLER-SUPPLIED +`RuntimeSetPositionCommand` (i.e. the command already carries everything +prerequisite B's mover-preparation chain produces) rather than reaching into +DAT/prepared-collision itself. + +**What an "off-canonical preparation followed by one validated atomic commit" +must DEFER** (confirmed by the dormant mechanism's own design, section 4): +- The record's REAL `ObjectClock` (use a scratch clock during prep). +- `RuntimeEntityRecord.PhysicsBody`/`PhysicsOwnershipEpoch` (never touch the + canonical record during prep; only `SetPhysicsBody` inside `Commit`, and + only after `PrepareDormantLocalActivationOwnership` succeeds). +- `RuntimeLocalPlayerMovementState.Controller`/`ControllerOwnershipEpoch` + (only via `CommitRuntimeOwnedController`, never the public setter, during + prep). +- `body.InWorld`/`TransientState.Active` (explicitly forced false during prep, + `Prepare`, :292-293) — the body must not be simulatable until the LATER + activation commit flips it (`TryApplyDormantLocalActivationFinalCommit`, + `body.InWorld = true` at :2056). +- World-residence/host/shadow/camera publication (all deferred to the + activation phase / presentation-observer layer, never inside `Prepare`). + +--- + +## 6. Adversarial gate list — exact code paths that would race TODAY + +(Campaign handoff's list, `docs/research/2026-07-31-remaining-physics-campaign-handoff.md:210-221`.) +For each: what the ALREADY-BUILT dormant mechanism does (if wired) vs. what +the CURRENT production direct-construction paths do (today, unwired). + +1. **Nested construction** — Dormant: `CanPrepare` requires `_activation is + null` AND `record.PhysicsBody is null` AND `_movement.Controller is null` + (`RuntimeLocalPlayerPhysicsPublicationState.cs:862-889`) — a second + `Prepare` while one is in flight is REJECTED structurally. Today: NEITHER + `BuildControllerAndCamera` NOR `CreateController` has any such guard — + `CreateController`'s `_runtime.MovementOwner.Controller ?? CreateController(record)` + (`HeadlessSessionWorldProjection.cs:576-578`) is a bare null-coalesce, not + an atomic test-and-set; only single-threaded host-loop scheduling + prevents an actual race today. +2. **Reentrant SetPosition** — Dormant: every gate re-checks + `PhysicsOwnershipEpoch`/`record.PositionAuthorityVersion` currency. + Today: `BuildControllerAndCamera`'s final mutation + (`playerEntity.SetPosition`/`ParentCellId`/`CommitPreparedPosition`, + PlayerModeController.cs:482-484) has zero epoch check — a same-thread + reentrant call (e.g. from a nested wire dispatch) would silently + clobber with no detection. +3. **Remote and projectile binding/update** — Dormant: `CanPrepare`/ + `IsCurrent`/`IsActivationCurrent` all check `record.RemoteMotion is null`, + `record.Projectile is null`, `!RemoteMotionBindingInProgress`, + `!ProjectileBindingInProgress` (defense-in-depth; should never legitimately + fire for a local-player record). Today: no such check exists in either + host's direct construction path. +4. **Deletion and same-GUID new incarnation** — Dormant: `_entities.IsCurrent(record)` + checked at every gate; `TryAcceptDelete` -> `RemoveActive` flips this + immediately (section 3). Today: `BuildControllerAndCamera`/`CreateController` + take a fixed `RuntimeEntityRecord`/`WorldEntity` parameter with NO + re-validation against current canonical identity at the final commit. +5. **Projection-owner replacement** — Dormant: `_entities.SessionLifetimeVersion + == token.SessionGenerationAuthority` checked throughout. Today: no + generation check in either direct path. +6. **Object-clock epoch change** — Dormant: `ObjectClockEpoch` compared at + every gate (section 3/4). Today: no check; AND there is a REAL, live + concurrent writer already in production — + `LiveEntityRuntime.cs:879/891/897/1258/3030/3033`'s direct + `record.SuspendObjectClock()`/`ResetObjectClockForEnterWorld(...)` calls + inside the `RebucketLiveEntity` family (section 3) — this is not a + hypothetical gate, it is a currently-active call path on the SAME record + type. +7. **Reset and disposal** — Dormant: `ResetSession()`/`Dispose()` on + `RuntimeLocalPlayerMovementState` explicitly cascade into + `_physicsPublication?.ResetSession()`/`Dispose()` + (`RuntimeLocalPlayerMovementState.cs:244-295`), which tear down + candidate/activation state via `ReferenceEquals`-gated clears (never + clobbering a newer owner, `DiscardActivation`, + `RuntimeLocalPlayerPhysicsPublicationState.cs:1002-1032`). Today's direct- + construction controllers are built via the PUBLIC constructor + (`StandalonePublished` lifecycle) — **`RetireRuntimePublication()` + (`PlayerMovementController.cs:837-846`) only transitions + `RuntimeOwnedDormant`/`RuntimePublished` state; it is a NO-OP for + `StandalonePublished` controllers** — a previously-unstated finding: TODAY, + `ResetSession()`/`Dispose()`/replacing `.Controller` on either host's + directly-built controller produces NO explicit lifecycle transition at + all; the controller is simply dropped/GC'd. Not a visible bug today + (nothing reads `IsRuntimePublished` for these), but it means today's + controllers are invisible to the exact teardown bookkeeping C1's target + mechanism relies on. +8. **Commit and rollback after replacement** — Dormant: ALL validation + happens before the single canonical mutation + (`PrepareDormantLocalActivationOwnership`, which itself throws leaving + state untouched on failure); everything after is documented as + "callback-free, non-allocating, and cannot fail" + (`RuntimeLocalPlayerPhysicsPublicationState.cs:391-393`) — no rollback- + after-newer-authority path exists BECAUSE nothing after that point can + fail by construction. Today: `BuildControllerAndCamera`'s try/catch rolls + back camera+shadow only; the final `playerEntity.SetPosition`/ + `ParentCellId`/`CommitPreparedPosition` triad (482-484) has nothing after + it that can throw, so it's accidentally safe today, not structurally + guaranteed. +9. **Late graphical camera/shadow/host failure** — Today: `BuildControllerAndCamera` + DOES handle this (explicit `_camera.RestoreState`/`_shadow.Restore` in the + catch block, 494-518) — this is the ONE gate the CURRENT graphical path + already handles reasonably. The dormant Runtime-side mechanism has NO + camera/shadow concept (presentation-independent by design) — C1 must + layer this handling in the PRESENTATION/observer phase (post-Runtime- + commit), matching prerequisite D's rule that a host exception must not + roll Runtime back, only retry the FIFO head. +10. **Late headless prepared-collision failure** — Today: + `ApplySetupStepHeights` (`HeadlessSessionWorldProjection.cs:657-691`) + throws a raw, uncaught `InvalidDataException` (672-675) if the prepared + Setup collision isn't `Loaded` — this propagates up through + `CreateController` -> `SynchronizeLocalPlayer` -> `ProjectSpawn`/ + `ProjectPosition` with NO try/catch anywhere in between (grep-confirmed + no surrounding try/catch in `HeadlessSessionWorldProjection.cs`'s these + methods) — a genuinely unhandled-exception risk in production headless + TODAY, not just a hypothetical C1 gate. + +--- + +## Summary for the C1 contract + +- **Writer count**: 6 confirmed call sites of `RuntimeEntityRecord.SetPhysicsBody` + across 3 files (`RuntimeEntityDirectory.cs:359`, + `RuntimeEntityObjectLifetime.cs:766`, + `RuntimeLocalPlayerPhysicsPublicationState.cs:405,1025`, + `RuntimePhysicsState.cs:1558,1666`) — plus a probable 7th App-side ad hoc + projectile body-construction site not yet traced to a canonical writer + (flagged, out of local-player scope). +- The dormant `RuntimeLocalPlayerPhysicsPublicationState` + + `RuntimeSetPositionState`'s ~15 dormant-activation methods already + implement essentially the COMPLETE Option 2 transaction (off-canonical + prepare against a scratch clock/sealed candidate controller, single + validated atomic commit, full retail-staged SetPosition activation) with + epoch/token/generation/identity re-validation at every external entry + point — it has ZERO production callers in either host. +- The single largest remaining defect even AFTER wiring: the public + `RuntimeLocalPlayerMovementState.Controller` setter is an unguarded escape + hatch both hosts currently use directly; it must be sealed (made + unreachable from hosts, or itself epoch-gated) for the word "exclusive" in + prerequisite C to be true.