# C4 route 5 — projectile authoritative placement: retail-conformance review, ROUND 2 (delta) **Reviewer lens:** retail fidelity only. Delta against [`2026-08-04-c4-route-5-retail-review.md`](2026-08-04-c4-route-5-retail-review.md) (round 1, FAIL) and cross-read against [`2026-08-04-c4-route-5-architecture-review.md`](2026-08-04-c4-route-5-architecture-review.md) (A1-A11). **Subject:** the uncommitted working tree, HEAD `30d3d114`, branch `claude/acdream-physics-divergence-5aa784` — 1,626 insertions / 398 deletions across 10 files plus two untracked test files. --- ## VERDICT: **FAIL** A narrow FAIL. Eleven of the twelve round-1/architecture findings are properly closed, several of them better than the fix direction I suggested. The FAIL rests on two MAJORs: - **B1** — the fix to my own round-1 R6 went the wrong way and introduced a position/cell mismatch on the stored partition. **R6 as I wrote it was factually wrong, and I own this**: I claimed the render cell was "left behind" while the position moved; it was not — `record.FullCellId` is already the wire cell at ack time, so the original code was self-consistent. Switching to the body's own cell made it inconsistent. This is a one-token revert. - **B2** — the R2/A3 adopted-body fix closed the **teleport** branch and left the **far** branch. Retail's far branch @0x005163C1-@0x005163CB runs `StopInterpolating` whenever `position_manager != 0`, and re-anchors the leash @0x00454272 on any nonzero return. For an adopted missile both managers exist, both actions are live in retail, and acdream now performs neither. I confirmed the harm is real, not theoretical. Both are the same shape as the findings they descend from: an invariant satisfied on one arm only. --- ## A. Round-1 / architecture findings — delta status | ID | round-1 / arch | status | notes | |---|---|---|---| | R1 / A2 | unbound missile dropped | **CLOSED — better than my fix direction** | see A1 below | | R2 / A3 | adopted-body hook unwired | **PARTIAL** | teleport branch closed; far branch open → **B2** | | R3 | retry arm skips invalidate/sync | **CLOSED** | see A3 | | R4 / A7 | hidden-branch `LastUpdateTime` dropped | **CLOSED** | restored at `SyncProjectilePresentation`'s `else if (spatial)` with the correct rationale quoted from `TryBind` | | R5 / A1 | App ignores seam status | **CLOSED** | see A4 | | R6 | stored-outcome cell | **REGRESSED — my finding was wrong** | → **B1** | | R7 / A5 | no App-level coverage | **CLOSED** | 7 new `OnPosition` tests incl. the unbound fall-through and the adopted-body hook | | R8 | unjustified-velocity comment untracked | **OPEN, deliberately** | judgment in D below — partially acceptable | | A4 | `SyncProjectilePresentation` untested | **CLOSED** | real `ShadowObjects` entry / `TotalRegistered` / `Active`-flag assertions across four new tests | | A6 | `wasInWorld` read after placement | **CLOSED** | captured pre-dispatch and threaded as a parameter; `…ReenteringWorldReactivatesBody` pins it | | A8 | silent shadow skip | **CLOSED** | escalates via `ThrowIfWorldFrameUnreachable`, matching `StoreAcceptedDestinationPose`'s #284 policy | | A9 | null-route fallback unfenced from the player | **CLOSED** | `update.Guid != _playerServerGuid &&` added | | A10 | `OwnsFarSnap` doc false in the kind dimension | **CLOSED** | corrected paragraph added | | A11 | cutover ledger stale | not checked here (outside D-P7's letter; architecture reviewer's call) | | --- ## B. Verification of the four items you asked me to judge ### A1 — the conjunctive kind predicate: is it retail-CORRECT, not merely regression-free? **Yes. PASS, and the reasoning is stronger than "it restores the old fall-through".** `RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition` now derives: ``` Missile bit && canonical.Projectile is bound && ReferenceEquals(canonical.PhysicsBody, projectile.Body) ? Projectile : Remote ``` **Why this is retail-correct rather than an implementation detail smuggled into a classification:** 1. **`RuntimePositionEntityKind` is not a retail concept and has no retail effect.** I re-verified `RuntimeAuthoritativePositionRouteClassifier`: the only kind branch in `ClassifyAcceptedPosition` is `LocalPlayer` (`:349`); `ValidEntityKind` (`:535-538`) admits all three; the sole downstream difference is `OperationKind` (`:564-575`). Projectile and Remote produce byte-identical disposition, `SetPositionFlags`, `StopInterpolating`, `TeleportHookPhase` and `ConstrainPhase`. The kind is therefore an **acdream ownership label selecting which arm executes**, not a reproduction of any retail decision. Retail's `MoveOrTeleport` @0x00516330 has no state test at all (round-1 §A1, byte-confirmed), so there is no retail predicate for this code to be unfaithful to. 2. **The check lives in the right place.** It is in the *caller* that maps acdream state → kind, not inside `ClassifyAcceptedPosition`, which remains a pure port of retail's decision tree. Nothing acdream-specific entered the retail-faithful classifier. 3. **Mis-routing a genuinely-bound missile is harmless in the retail direction.** The only way a live missile classifies `Remote` is if `record.PhysicsBody` was replaced out from under a surviving `RuntimeProjectile` — already a broken state, and one that `ApplyAcceptedProjectilePosition`'s identical guard would refuse anyway (round 1: it dropped the packet). Taking the Remote arm instead **places the canonical body**, which is what retail does; it also arms the leash and runs the full hook, i.e. *more* of retail's behaviour, not less. There is no direction in which the conjunctive predicate produces less retail-faithful output than the arm it diverts from. 4. **The reverse mis-route cannot happen.** A non-missile can never satisfy clause 1, so no ordinary remote is diverted into the projectile arm. The App's null-classification fallback (`LiveEntityNetworkUpdateController.cs:2132-2140`) now applies the **same three conjuncts plus the A9 player fence**, so the two discriminators still cannot disagree. Verified by reading both. The doc comment's claim — *"Retail's `MoveOrTeleport` places EVERY non-player object unconditionally — it has no concept of 'client-side machinery not yet bound'"* — is true and matches my byte-level read of the function. ### A2 — is "has `RemoteMotion`" the right proxy for "has managers"? **For actions 1-4, yes, provably. For action 5, the proxy is imperfect but skipping is still the retail-correct outcome.** PASS with a note. Mapping retail's six `teleport_hook` @0x00514ED0 actions onto acdream's owners: | retail action | guard | acdream owner | reachable without `RemoteMotion`? | |---|---|---|---| | `MovementManager::CancelMoveTo` @0x00514EDD | `movement_manager != 0` | `RemoteMotion.Movement` (a field on `RemoteMotion`, `RemoteMotion.cs:36`) | **no** | | `PositionManager::UnStick` @0x00514EEE | `position_manager != 0` | `RemoteMotion.Host.PositionManager` (`Host` is gated on `_fullPhysicsHostBound`, `:87`) | **no** | | `PositionManager::StopInterpolating` @0x00514EFD | same | `RemoteMotion.Interp` (`:233`) | **no** | | `PositionManager::UnConstrain` @0x00514F0C | same | `RemoteMotion.Host.PositionManager` | **no** | | `TargetManager::ClearTarget`/`NotifyVoyeurOfEvent` @0x00514F1B | `target_manager != 0` | `EntityPhysicsHost.TargetManager` | **yes** — see below | | `report_collision_end` @0x00514F31 | *unguarded* | `RuntimeCollisionReportingState.LeaveWorld` | run by the Runtime seam regardless ✓ | The one leak: `LiveEntityMotionRuntimeController.ResolvePhysicsHost:246-250` installs a **minimal** `EntityPhysicsHost` for any record the target/moveto resolver touches, and that host carries a `TargetManager`. So a bare missile *can* hold a `TargetManager` while `record.RemoteMotion` is null, and the reduction skips its `ClearTarget`/`NotifyVoyeurOfEvent`. **This does not make skipping wrong.** Retail's missile has **no** `TargetManager` — `MakeTargetManager` is lazy and nothing in a ballistic object's life creates one — so retail's guard @0x00514F19 no-ops. acdream's eager minimal-host creation is a pre-existing structural difference from retail, not something route 5 introduced, and running the action would be the divergence, not skipping it. Recorded so the next round does not re-open it. ### A3 — the retry arm (my R3) **CLOSED and correct.** `Advance()` now derives the projectile/body pair from `pending.Route.OperationKind` under the same three conjuncts, captures `pendingWasInWorld` **before** the resubmit (matching A6's ordering fix), calls `InvalidatePrediction()` before both the store fallback and the resubmit, and gates `SyncProjectilePresentation` on the same non-`Deferred`/non-`RejectedByPlacement` partition. The invariant now holds on both arms. One residual, **pre-existing and shared with the remote arm, not a finding**: the retry path never calls `StoreAcceptedDestinationPose` after a `SubmitAndResolve` that returns `Contention`/`RejectedPreparation`, so invariant 1's pose advance is skipped on a re-parked retry. That was true before this slice (`_ = SubmitAndResolve(...)`) and is the remote arm's behaviour too. Recorded, not filed. ### A4 — the App presentation gate, and whether "stored outcomes write the cell too" is now true **The gate is CLOSED and correct. The cell question is NOT — and it is worse than before. See B1.** The gate itself: App now calls `SyncPresentationFromResolvedBody` only when the status is non-null and neither `Deferred` nor `RejectedByPlacement`, mirroring Runtime's own partition exactly. I walked every outcome: | outcome | body state | ack? | retail analogue | |---|---|---|---| | `Committed` | at resolved destination, cell committed | yes | `SetPosition` success | | `Refused`/`Contention`/`RejectedPreparation`/`NotApplicable` | position stored at destination, cell NOT written | yes | `store_position` @0x00515CE2 | | `Deferred` | snapped to parked result, withdrawn | no | park (acdream-only, AP-136/138) | | `RejectedByPlacement` | untouched | no | @0x00515CB2 / @0x00515CD5 — retail's non-storing failures also leave the object where it was ✓ | | `null` (no-op / swallow) | untouched | no | `return 0` @0x0051636D, or acdream-only | Correct on every row. --- ## C. New MAJOR findings ### B1 — MAJOR — the R6 "fix" pairs the destination position with the pre-packet cell on every stored outcome; the correct source was the one it replaced **Where:** `src/AcDream.App/Physics/ProjectileController.cs`, `SyncPresentationFromResolvedBody` — `entity.ParentCellId = runtime.Body.CellPosition.ObjCellId` (was `record.FullCellId`). **Retail contradicted:** `CPhysicsObj::store_position` @0x00515CE2, reached from `SetPositionInternal`'s no-resolvable-cell branch @0x00515C1D. It writes the object's **whole** `Position` — `objcell_id` together with the frame — so after a retail store the object's cell and position are the *same* cell, the destination. It is never left describing a position in cell B while claiming membership of cell A. **My round-1 R6 was wrong and I induced this.** I wrote that on a stored outcome "the render cell is left behind while the render position moves". It is not: `RuntimeEntityRecord.RefreshDerivedState` → `SetFullCell` (`RuntimeEntityRecord.cs:232-237`) stamps `FullCellId = position.LandblockId` at merge time, *before* classification, and `StoreAcceptedDestinationPose` composes `body.Position` from `accepted.PositionX + worldOffsetX` where the offset comes from `accepted.LandblockId` — **the same cell**. So the original `entity.ParentCellId = record.FullCellId` paired a wire-frame position with the wire cell: self-consistent, and retail's store semantics. The architecture reviewer had this right in its "Verified — no finding" section; I did not. **What the change now produces on a stored outcome:** - `entity.Position` = destination, expressed in cell **B**'s world frame - `entity.ParentCellId` = `body.CellPosition.ObjCellId` = cell **A** (`StoreAcceptedDestinationPose` never writes the cell — the AP-138 residual) That is precisely the defect shape the architecture review's A1 named — a render entity parented into a cell it is not geometrically inside, culled or drawn through walls when A is an indoor EnvCell — relocated from the no-op partition (now fixed) to the stored partition (now broken). **Three independent cross-checks all say `record.FullCellId`:** 1. **The sibling remote arm.** `TryApplyGenericRemoteRenderPose` (`LiveEntityNetworkUpdateController.cs:1010-1025`) writes `entity.ParentCellId = landblockId` — the **wire** cell — paired with the wire world position. Parity demands the projectile arm do the same. 2. **Runtime's own shadow publish, forty lines away.** `SyncProjectilePresentation` publishes `ShadowObjects.UpdatePosition(..., record.FullCellId, seedCellId: record.FullCellId)` with `body.Position`. After this change the shadow says cell B and the render entity says cell A **for the same body in the same call**. 3. **Retail**, as above. **The doc comment defending the change is self-refuting.** It states *"Retail's own `store_position` @0x00515CE2 writes the object's whole `Position` including `objcell_id`; reading the body's own cell here is the client-side analogue"* — the premise is right and the conclusion inverts it. Retail's `objcell_id` after a store is the **destination** (`record.FullCellId`), not the stale one. **Untested.** Both new App commit tests (`MissileTeleportCommit_…ParentCellIdAgreesWithBody`, `MissileFarCommit_…`) assert on **committed** outcomes, where `body.CellPosition.ObjCellId == record.FullCellId == DestinationCell` and the two sources coincide. No test drives a stored outcome through the App ack, so the suite is green either way — a green suite is not evidence. **Correct behaviour:** revert to `entity.ParentCellId = record.FullCellId`, and add an App-layer stored-outcome test (`Refused`) asserting the render position and `ParentCellId` are both the **destination** cell's. --- ### B2 — MAJOR — the adopted-body fix closed the teleport branch only; retail's FAR branch also runs `StopInterpolating`, and re-anchors the leash, whenever the manager exists **Where:** `LiveEntityNetworkUpdateController.cs` — the hook is gated on `route.Disposition is …SetPosition && acceptedPositionCanonical.RemoteMotion is RemoteMotion`; and `RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition`'s `case …SetPositionSimple:` does nothing but invalidate and place. **Retail contradicted, two sites:** ``` 005163c1 position_manager = this_1->position_manager; 005163c9 if (position_manager != 0) 005163cb PositionManager::StopInterpolating(position_manager); 005163d9 CPhysicsObj::SetPositionSimple(this_1, arg2, 1); ``` and, for **every** nonzero return including this one, `ConstrainTo(arg2, &arg2->m_position, …)` @0x00454272 — which re-anchors the leash at the object's **just-updated** position. **Why the contract's justification does not cover the adopted case.** D-P2 pins the far branch's interp skip as *"retail-faithful by consequence"* because *"a never-interpolated missile has none"*. That premise is exactly what the adopted-body case violates: `TryBind`'s shared-body branch (`ProjectileController.cs:176-181`) exists for an object that was a live remote first, so it carries a populated `RemoteMotion.Interp` and, if hosted, a `PositionManager` whose leash route 4a (`RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation`) already armed. Retail's `position_manager != 0` guard is **satisfied** there. **The harm is real, not structural.** I traced it: - `RuntimePhysicsState.cs:697-708` puts the same key in **both** `_spatialRemotes` and `_spatialProjectiles` when the record has both components. - `RuntimeRemotePhysicsUpdater` consumes `rm.Interp` for every `_spatialRemotes` entry (`:288`, `:331`, `:941`). - `ProjectileController.Tick`'s per-quantum tail runs `RetailObjectManagerTail.Run(remote.Host?.TargetManager, remote.Movement, null, remote.Host?.PositionManager)` for exactly this record shape. So after a far snap, a stale waypoint and a leash anchored at the *pre-snap* position both remain live and drag the freshly-placed missile back — the identical scenario the architecture review's A3 used to justify wiring the teleport branch. The teleport branch got the fix; the far branch, which is the more common disposition for a moving object at >=96 m, did not. **AP-141 is now factually wrong in its risk column.** It states *"a live missile never shows a constraint leash"*. After the round-2 fix that is false for the adopted-body case: such a missile can carry a leash inherited from its pre-Missile remote life, the teleport branch now clears it, and the far branch neither clears nor re-anchors it. This is the "a register row asserting behaviour the code does not have" defect class the 4b-3 reviews named. **Correct behaviour:** either run the same hook seam on the far branch's manager-bearing case (retail runs only `StopInterpolating` there, not the full hook — so the minimal faithful action is `remote.Interp.Clear()` gated on the host existing, mirroring @0x005163C9), and decide the leash re-anchor explicitly; or amend AP-141 to state that for an adopted body the far branch skips retail's guarded `StopInterpolating` @0x005163CB and leaves an inherited leash un-re-anchored. Do not leave the row asserting the opposite. --- ## D. MINOR findings ### B3 — MINOR — `report_collision_end` now runs twice for the adopted teleport case, and the hook is split across two owners App's `RunRemoteTeleportHook` executes all six actions including `ReportCollisionEnd → LiveEntityRuntime.ForceEndCollisionReporting → RuntimeCollisionReportingState.LeaveWorld`; the Runtime seam then runs `CollisionReports.LeaveWorld(record)` again before the placement. Retail calls @0x00514F31 once. The second call is benign (the table is already empty, so `ForceEnd` finds nothing; it only bumps `_mutationRevision`), and the ordering relative to `SetPosition` is preserved, so this is cosmetic — but one retail function now has two owners across a layer boundary, which is how the *order* of a six-step sequence gets broken later. Cleanest: have the Runtime seam skip its own `LeaveWorld` when the caller supplied the full hook, or move the whole hook behind the Runtime seam. ### B4 — MINOR — the projectile hook allocates a per-packet closure while its comment claims the "#315 pattern" `LiveEntityNetworkUpdateController.cs`, projectile arm: ```csharp RunRemoteTeleportHook( acceptedPositionCanonical, adoptedRemote, () => _liveEntities.IsCurrentPositionAuthority( // fresh closure, every packet acceptedPositionRecord, acceptedPositionAuthorityVersion)); ``` The comment says this uses *"the SAME ordered hook seam and per-packet currency check the remote teleport arm already uses (`RunRemoteTeleportHook`, #315 pattern)"*. The remote arm does not do this: it passes `_remoteArmCallbacks.RunTeleportHook`, a **cached** method group over scratch fields (`RunCachedRemoteTeleportHook`), which is precisely what #315 introduced to remove per-packet closures from the packet path. The contract restates that constraint (§2 item 5). The seam is shared; the allocation discipline is not, and the comment asserts otherwise. ### B5 — MINOR — R8 remains open Judgment you asked for: **deferring the audit is acceptable; deferring the tracking is not.** The comment now asserts, in production source, that a live call's retail justification is unestablished. With no `docs/ISSUES.md` row and no AP row, that assertion is discoverable only by reading `LiveEntityNetworkUpdateController.cs`. One line in ISSUES.md ("4a remote arm's `TryCommitAuthoritativeVelocity` has no established retail basis — see the comment at the call site; `MoveOrTeleport` @0x00516330 byte-confirmed not to install a velocity") costs nothing and should land with this commit. The substantive audit belongs to the 4a family, as the contract says. ### B6 — MINOR — the stored-outcome shadow publish uses an unadvanced body on a re-parked retry `Advance()`'s retry arm calls `SyncProjectilePresentation` after a `SubmitAndResolve` that returned `Contention`, but never `StoreAcceptedDestinationPose` on that path (B-section A3's residual). The shadow is therefore published at `record.FullCellId` (the wire cell) with a body still at its pre-packet pose — the mirror image of B1, inside Runtime. Parity with the remote arm, pre-existing, and it disappears if B1's residual (store never writes the cell, AP-138) is ever closed. Recorded, not filed. --- ## E. Retail claims re-verified this round (no change) Re-checked because the fix touched the surrounding code, not re-derived from scratch (round 1 §A has the full derivations): - `MoveOrTeleport` @0x00516330 — no state/kind test; `arg5` at `[esp+0x7C]` never read (byte-confirmed round 1); `ret 0x10` at all four exits. - `teleport_hook` @0x00514ED0 — six actions, five per-manager guarded, the sixth unguarded. The App fix drives the in-tree ordered port (`RemoteTeleportHook.Execute`) with per-action `host?.` guards, so retail's guards decide rather than being re-derived. Correct for the teleport branch. - `ConstrainTo` @0x00510520 → `MakePositionManager` @0x00510523; the single arming site @0x00454272 has no kind test. AP-141 clauses (a) and (b) remain accurate descriptions of retail; only the row's **risk** column is now wrong (B2). - `store_position` @0x00515CE2 writes `objcell_id` with the frame — the basis of B1. - ACE never sends a missile `UpdatePosition` (`WorldObject_Tick.cs:333-334` inside the `PhysicsState.Missile` branch at `:265`), so B1 and B2 are both test-reachable only. Stated for calibration, not as a reason to ship them. - AP-131 and AP-135 untouched; `docs/ISSUES.md` unmodified; #276 not closed. --- ## F. Summary | # | Sev | Where | Retail address contradicted | |---|---|---|---| | B1 | MAJOR | `ProjectileController.SyncPresentationFromResolvedBody` (`entity.ParentCellId`) | `store_position` @0x00515CE2 — writes `objcell_id` with the frame; also breaks parity with `TryApplyGenericRemoteRenderPose` and with the shadow publish in the same call | | B2 | MAJOR | `LiveEntityNetworkUpdateController` hook gate (`SetPosition` only) + `ApplyAcceptedProjectilePosition`'s `SetPositionSimple` arm | `StopInterpolating` @0x005163C9-@0x005163CB (guard satisfied for an adopted body); `ConstrainTo` @0x00454272 re-anchor | | B3 | MINOR | App hook action 6 + Runtime seam `LeaveWorld` | @0x00514F31 called once in retail | | B4 | MINOR | projectile arm's `Func` closure | — (contract §2 item 5; comment false) | | B5 | MINOR | no ISSUES/AP row for the acknowledged-unjustified velocity commit | — | | B6 | MINOR | `Advance()` retry shadow publish | — (mirror of B1 inside Runtime; parity, pre-existing) | **Both MAJORs are small edits.** B1 is one token. B2 is the far-branch half of a fix already written for the teleport branch, plus an AP-141 risk-column correction.