acdream/docs/research/2026-08-04-c4-route-5-retail-review.md
Erik 36255af0f6 fix(physics): C4 route 5 — projectile authoritative placement (#276 partial)
Ports retail's missile Position handling into the canonical Runtime
placement owner instead of the deleted ApplyAuthoritativePosition
short-circuit. The Create/residence-window halves of the projectile
pipeline (RuntimeProjectile binding, TryBind's adopted-body branch,
the collision/shadow registration) were already canonical from prior
slices; this closes the remaining gap — how an ACCEPTED Position for
an in-flight missile is classified, placed, and presented.

Byte-decode (Step 1 hard gate, before any code was written):
CPhysicsObj::MoveOrTeleport @0x00516330-0x00516438 disassembled from
the PDB-paired binary (Capstone, x86 32-bit thiscall). `ret 0x10`
establishes four stack args; [esp+0x7c] (arg5, the velocity pointer)
is never referenced in any of the three branches (teleport/near/far).
The retail reviewer independently reproduced this by searching the
whole function body for the `24 7c` mod/rm+disp8 encoding a
`[esp+0x7c]` read would require and found zero occurrences. This
retired a fabricated `?? Vector3.Zero` fallback in the deleted method
— retail's PositionPack::UnPack initializes an absent velocity to
zero and MoveOrTeleport never installs it; the projectile's Vector
channel (RuntimeProjectilePhysicsUpdater.ApplyAuthoritativeVector)
remains the sole velocity authority for a missile. D-P5 in the
contract; the Runtime seam commits no velocity from the Position
packet at all.

The unbound-missile fix: RuntimeEntityObjectLifetime's
ClassifyRemoteAcceptedPosition now derives ProjectileAuthoritative
from a CONJUNCTIVE predicate — the Missile bit AND a bound
RuntimeProjectile whose Body is the canonical PhysicsBody — never the
bit alone. Retail places every non-player CPhysicsObj unconditionally
(there is no missile-specific placement gate in MoveOrTeleport or its
callers), so an unbindable or not-yet-bound missile taking the
ordinary remote tail is retail-faithful, not a fallback: the earlier
bit-only discriminator would have silently frozen it instead.

AP-141 records this as a deliberate, recorded divergence, not
fidelity. Retail mechanically WOULD arm a missile's ConstrainTo leash
on any nonzero MoveOrTeleport return: HandleReceivedPosition
@0x00453FD0's only kind test is player-vs-not, ConstrainTo
@0x00454272 has no kind test of its own, and CPhysicsObj::ConstrainTo
@0x00510520 creates a PositionManager on demand via
MakePositionManager @0x00510523 if one doesn't exist. acdream
deliberately does not construct that EntityPhysicsHost/
PositionManager/InterpolationManager chain for a ballistic body — the
route-5b split the C4 route 5 contract rejected — so a live missile
never shows an armed leash and never catches up via the near/
UnroutedCatchUp policy. This divergence is safe specifically because
ACE never sends UpdatePosition for a missile
(references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Tick.cs:
333-334, SendUpdatePosition() commented out inside the
PhysicsState.Missile branch at :265) — every half of this row is
deterministic-test-gated only, never exercised against a real server.

AP-141 also records the surviving ConstrainTo re-anchor divergence
under clause (b): for the adopted-body case (TryBind's shared-body
branch — an ordinary remote whose Missile bit is set by a later
State packet, so it still carries a live RemoteMotion), acdream now
ports retail's teleport-branch and far-branch StopInterpolating
action (Interp.Clear()), but never re-arms or re-anchors the
inherited ConstrainTo leash the way retail's HandleReceivedPosition
@0x00454254/@0x00454272 does on every nonzero return. The risk
column's earlier wording — that a stale leash "would drag the body
toward a stale anchor" — was wrong and is retracted in this same
commit: ConstraintManager.ConstraintPos is write-only in both retail
and the port (never read by AdjustOffset), and
ConstraintManager::adjust_offset @0x00556180 only tapers or zeroes an
already-composed per-tick offset while InContact — a leash brakes
motion the interp/sticky chain already produced, it cannot pull
anything toward the anchor. The real residual is one tick of un-reset
brake accumulator, contact-gated, and it cannot move an airborne
far-snapped missile at all (the clamp branch does not run while
airborne).

NO CONNECTED GATE EXISTS for this route, by design: ACE never sends a
missile UpdatePosition (see above), so retail's own server never
exercises this code path in play. Every proof obligation here is
test-gated only — Runtime and App-level fixtures constructing the
packet directly — never a live client/server capture.

Three review rounds closed 8 MAJOR findings before this landed:
round 1 (A1 App discarded the seam's status; A2/R1 silent swallow on
an unbound missile; A3/R2 the adopted-body teleport_hook never
wired; A4/A5 zero Runtime/App test coverage); round 2 (a
ParentCellId regression introduced by round 1's own R6 finding,
which the retail reviewer retracted the following round as factually
wrong — the fix here is the REVERT to record.FullCellId, not the
relocation round 1 shipped; B2 the far-branch StopInterpolating skip
never extended to the adopted-body case; residual App/Runtime store-
path coverage; a per-packet closure contradicting the file's own
#315 cached-delegate pattern). Round 3 closed on coverage alone (no
defect): the Advance() retry arm's projectile branch — added at
round 2, semantically reordered at round 2's B5 fix (skip prediction
invalidation on a re-parked Contention, since it writes nothing) —
had never been executed by any test; two new tests drive it directly
and are sabotage-verified against both the reordering and the
retry-arm's own SyncProjectilePresentation call site. The one
recorded defect this campaign produced (the ParentCellId regression)
was caused by complying with a review finding that its own author
later retracted — the standing lesson recorded for future rounds is
that review findings are evidence to re-verify against the code, not
commands to obey unconditionally.

Complete Release suite: 11,063 passed / 4 skipped / 0 failed
(baseline 11,036 at 30d3d114, +27 new tests across this campaign).

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

465 lines
25 KiB
Markdown

# C4 route 5 — projectile authoritative placement: retail-conformance review (2026-08-04)
**Reviewer lens:** retail fidelity only ("is this what the retail client does?").
Architecture is a separate reviewer's job.
**Subject:** the uncommitted working-tree diff at branch
`claude/acdream-physics-divergence-5aa784`, HEAD `30d3d114`
(`git diff HEAD` + the untracked
`tests/AcDream.Runtime.Tests/Entities/RuntimeProjectilePositionKindTests.cs`).
`docs/research/2026-08-04-c4-route-5-contract.md` is the pinned contract, not
part of the change under review.
---
## VERDICT: **FAIL**
Two MAJOR findings, both **unrecorded divergences** — the project rule the
divergence register exists to enforce ("any commit that introduces a deviation
adds its register row IN THE SAME COMMIT; a deviation found without a row is a
bug twice over"). Neither is a hard-to-fix design problem: R1 is a one-clause
amendment to AP-141 (or a three-line fall-through decision), R2 is either a
wiring of the already-in-tree hook actions or a second clause on the same row.
**What is right, and independently verified** (Section A below): the retail
chain the contract asserts is correct in every particular I checked, the
byte-decode retiring the fabricated velocity is correct and I reproduced it
from the paired binary, and AP-141 describes retail's mechanical behaviour
honestly rather than dressing the divergence up as fidelity.
---
## A. Retail claims verified independently (all PASS)
Verified against `docs/research/named-retail/acclient_2013_pseudo_c.txt` and,
for the byte-decode, against `C:/Users/erikn/Downloads/acclient.exe`
(PDB-paired, image base `0x00400000`).
### A1. `MoveOrTeleport` has no `state & Missile` test — CONFIRMED
`CPhysicsObj::MoveOrTeleport` @0x00516330 (pseudo-C 284304-284366) reads
exactly four object fields: `update_times[4]` (`mov cx,[esi+0x16c]`), `cell`
(`mov eax,[esi+0x90]`), `player_distance` (`fld dword [esi+0x20]`), and
`position_manager` (`mov ecx,[esi+0xc8]`). There is no physics-state read
anywhere in the 272-byte function. A missile takes the identical generic
remote path. The contract's §4 row 1 is correct.
`SmartBox::HandleReceivedPosition` @0x00453FD0's only kind branch is
`arg2 != this->player` @0x0045414D — confirmed at pseudo-C 92896-93054.
`UnpackPositionEvent` @0x004542C0 calls it @0x00454358 with no state/kind
test.
### A2. Branch structure — CONFIRMED
| retail branch | address | acdream disposition | verdict |
|---|---|---|---|
| teleport/cell-less: `teleport_hook``SetFlags(0x1012)``SetPosition``return 1` | @0x005163EF / @0x00516414 / @0x00516420 / @0x00516438 | `SetPosition` | faithful |
| near: `InterpolateTo(this, arg2, IsMovingTo(this))`, `return 1` | @0x005163AF, @0x005163BE | `Interpolate`**no-op** | recorded divergence (AP-141a) |
| far: `if (position_manager != 0) StopInterpolating`, `SetPositionSimple(this, arg2, 1)`, `return 1` | @0x005163C9-@0x005163CB, @0x005163D9, @0x005163E8 | `SetPositionSimple` | faithful; the `StopInterpolating` skip is retail's OWN guard for a manager-less object, correctly excluded from the divergence row |
| airborne (`arg4 == 0`): `return 0` | @0x0051636D | `NoPositionOperation` → no-op, no arm | faithful, correctly NOT claimed as a divergence |
The far branch's third argument to `SetPositionSimple` is `1` (byte
`6a 01` at @0x005163D3) — matches.
### A3. The `ConstrainTo` chain — CONFIRMED; retail WOULD arm a missile's leash
- `HandleReceivedPosition` @0x00454254: `if (MoveOrTeleport(...) != 0)`
@0x00454272 `ConstrainTo(arg2, &arg2->m_position, ...)`. No kind test, no
manager-existence test.
- `CPhysicsObj::ConstrainTo` @0x00510520`MakePositionManager(this)`
@0x00510523`PositionManager::ConstrainTo` @0x00510533. The manager is
created **on demand**.
- The sibling `CPhysicsObj::InterpolateTo` @0x005104F0 does the same
(`MakePositionManager` @0x005104F3 then `PositionManager::InterpolateTo`
@0x00510508), so the near branch would likewise build machinery for a
manager-less missile.
- `PositionManager::UseTime` is ticked from `UpdateObjectInternal`
@0x005159A9 — an armed missile leash would be live, not vestigial.
**The central design decision is therefore a deliberate divergence, and the
change records it as one.** AP-141 states plainly that "retail's single arming
site has no kind test, so retail WOULD build a `PositionManager` on demand and
arm a missile's leash on any nonzero `MoveOrTeleport` return", and cites
@0x00454272 / @0x00510523 / @0x005163AF / @0x0050EB10 plus the ACE
unreachability. It does **not** claim fidelity it does not have. PASS.
### A4. The byte-decode of `arg5` — CONFIRMED, reproduced independently
I did not take the implementer's disassembly on trust; I re-derived the stack
offset and scanned the function's bytes.
Prologue at @0x00516330: `83 ec 64` (`sub esp,0x64`), `56` (`push esi`),
`8b f1`, `66 8b 8e 6c 01 00 00`, `57` (`push edi`). Displacement from entry
`esp` is therefore `0x64 + 4 + 4 = 0x6C`, so:
| arg | slot after prologue |
|---|---|
| `arg2` (Position*) | `[esp+0x70]` |
| `arg3` (uint16 timestamp) | `[esp+0x74]` |
| `arg4` (int32) | `[esp+0x78]` |
| `arg5` (Vector3 const*, velocity) | **`[esp+0x7C]`** |
Every stack-argument read in the function body: `8b 7c 24 74` @0x0051633E
(arg3 → edi), `8b 44 24 78` @0x00516387 (arg4), `8b 44 24 74` @0x005163A7
(arg2 — `esp` is 4 lower after the preceding `push eax`), `8b 4c 24 70`
@0x005163CF (arg2), `8b 54 24 70` @0x005163FC (arg2). **Zero occurrences of
the SIB+disp8 byte pair `24 7c` in the whole function**, and no disp32
(`84 24 7c 00 00 00`) form either; the only other `0x7c` bytes in the range
belong to the `edi` ModRM byte and to the constant address `0x007c6afc` (the
96 m literal). All four exits are `c2 10 00` (`ret 0x10`) — four dwords of
stack args, confirming the four-argument frame.
**`arg5` is never read. P4 holds. The `?? Vector3.Zero` fabrication was
genuinely fabricated, and deleting it is correct.** PASS.
### A5. The teleport-hook reduction — retail side CONFIRMED
`CPhysicsObj::teleport_hook` @0x00514ED0 (pseudo-C 283115-283151) is exactly
six actions:
1. `MovementManager::CancelMoveTo` — guarded `movement_manager != 0` @0x00514EDB
2. `PositionManager::UnStick` — guarded @0x00514EEC
3. `PositionManager::StopInterpolating` — guarded @0x00514EFB
4. `PositionManager::UnConstrain` — guarded @0x00514F0A
5. `TargetManager::ClearTarget` + `NotifyVoyeurOfEvent(Teleported)` — guarded @0x00514F19
6. `CPhysicsObj::report_collision_end(this, 1)` @0x00514F31**unguarded**
So for a *bare* arrow/bolt (no `MovementManager`, no `PositionManager`, no
`TargetManager`) five of six are retail no-ops through retail's own guards,
and the sixth applies. The projectile arm's single
`_entityObjects.Physics.CollisionReports.LeaveWorld(record)` is the right
port: it is the same seam `RunRemoteTeleportHook`'s `ReportCollisionEnd`
action reaches (`LiveEntityRuntime.ForceEndCollisionReporting`
`RuntimeCollisionReportingState.LeaveWorld`), i.e. the 4b-3 R2-validated port
of @0x00514620. Ordering is also right — it runs strictly before
`TryExecuteAcceptedRemotePosition`, mirroring @0x005163EF before @0x00516420
and it runs unconditionally of the placement's outcome, exactly as retail
discards `SetPosition`'s result and returns 1 @0x00516438.
**Caveat: "the other five are genuinely per-manager-guarded no-ops for a bare
missile" is true, but the contract also pinned the adopted-body case, and that
half was not delivered — see R2.**
### A6. Register / issue discipline — PARTIAL
- AP-141 added in the same working tree as the behaviour. PASS.
- AP-131 and AP-135 are untouched (the register diff is exactly the section-3
count line plus the AP-141 row). PASS.
- `docs/ISSUES.md` is not modified; #276 is not closed. PASS.
- AP-141's retail description is accurate (A3). PASS.
- **AP-141 is incomplete** — see R1 and R2.
### A7. Comment corrections — the touched ones verified
The deliberate separate correction at
`src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2309-2323`
replaces the false claim "MoveOrTeleport installs that exact vector with
set_velocity". Every assertion in the replacement is true: `MoveOrTeleport`
never reads the velocity slot (A4); `UnpackPositionEvent` @0x004542C0 performs
no `set_velocity`; the only `set_velocity` in the chain is the local player's
zero @0x004541B4. The replacement is honest about the consequence ("this
call's actual retail justification is therefore NOT yet established"). PASS
on truthfulness — but see R8 on the bookkeeping.
The two other comment rewrites (`ClassifyRemoteAcceptedPosition`'s
kind-derivation paragraph, `OwnsPlacement`'s widening paragraph) match the
code beside them. `RuntimeProjectilePhysicsUpdater`'s tombstone comment
accurately describes what was deleted and what remains.
---
## B. Findings
### R1 — MAJOR — a Missile-flagged entity with no bound projectile now has its accepted Position silently dropped; retail places it, and AP-141 does not cover this shape
**Where:**
`src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2222-2247`
(the `isMissilePacket` dispatch) together with
`src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs:1004-1012`
(`ApplyAcceptedProjectilePosition`'s ownership gate).
**Retail contradicted:** `CPhysicsObj::MoveOrTeleport` @0x00516330
reached from `HandleReceivedPosition` @0x00454254 for **every** non-player
`CPhysicsObj`, with no test of any kind on what client-side machinery the
object happens to have bound. Retail always places the object.
**What changed.** The deleted short-circuit was
`if (_projectileController?.ApplyAuthoritativePosition(...) == true) return;`.
That method returned **false** — i.e. *fell through to the generic remote
tail, which applied the position* — whenever `TryGetCurrent` failed, i.e.
whenever the record had the Missile bit but no bound `RuntimeProjectile`.
The new dispatch decides `isMissilePacket` purely from
`route.OperationKind`/the Missile bit, and `ApplyAcceptedProjectilePosition`
returns `null` (write nothing, return) when `record.Projectile` is null. The
packet is now dropped with no writer of any kind.
**Why this is reachable state, not a hypothetical.** `ProjectileController.TryBind`
(`src/AcDream.App/Physics/ProjectileController.cs:133-330`) can fail
*permanently* for a Missile-flagged entity — `TryGetCollisionSphere` rejects
any Setup that is not "the supported retail one-sphere collision shape" and
logs `Missile 0x… Setup 0x… does not have the supported retail one-sphere
collision shape`. It also has a legitimate not-yet-bound window
(`initialResidenceActive == false`, and the projection-visible retry in
`OnProjectionVisibilityChanged`). In all of those states the entity used to
track its server position through the remote path; now it freezes for the rest
of its life.
**Bookkeeping.** AP-141 clause (c) covers "a null-classified or `Rejected*`
accepted Position"; it does **not** cover "an accepted Position for a
Missile-flagged entity whose projectile component is absent or does not agree
with the canonical body". The XML doc on `ApplyAcceptedProjectilePosition`
does mention the ownership-mismatch swallow, so this is a real design
decision that was made and then not written into the register.
**Correct behaviour:** either (a) fall through to the generic remote tail when
`record.Projectile` is absent — which is what retail does and what the code
did yesterday — or (b) keep the swallow and extend AP-141 with a fourth
clause naming it, its trigger (unbindable Setup / pre-bind window), and the
observable (a Missile-flagged object stops tracking permanently). Do not
leave it undocumented.
*Mitigating (do not use as a reason to skip the row):* unreachable against ACE
for the same reason the rest of the row is — `WorldObject_Tick.cs:333-334`.
---
### R2 — MAJOR — the adopted-body teleport-hook case was pinned by the contract, not implemented, and not registered; retail runs five actions acdream skips
**Where:** `src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs:1017`
— the `SetPosition` arm runs `CollisionReports.LeaveWorld(record)` and
nothing else.
**Retail contradicted:** `CPhysicsObj::teleport_hook` @0x00514ED0 actions 1-5
(@0x00514EDB, @0x00514EEC, @0x00514EFB, @0x00514F0A, @0x00514F19). Each is
guarded on *its manager existing* — not on the object being a non-missile.
**The gap.** A missile can carry the full remote manager set. `TryBind`'s own
adopted-body branch exists for exactly this
(`ProjectileController.cs:176-181`: "Retail owns one CPhysicsObj. If a
non-missile incarnation already created its MovementManager … classification
adopts that same body"), and the late-classification entry
`ApplyAuthoritativeState → TryBind` is the production path that turns an
ordinary remote — which by then has a `RemoteMotion` with an `Interp` queue
and, if hosted, a `PositionManager` — into a missile. For such an object
retail's guards are *satisfied* and retail runs `CancelMoveTo`, `UnStick`,
`StopInterpolating`, `UnConstrain` and `ClearTarget`/`NotifyVoyeurOfEvent`.
acdream now runs none of them.
The contract pinned this explicitly (D-P4): "for the adopted-body case (a
missile that carries a `RemoteMotion`), the existing hook actions'
per-manager guards already express retail's shape; the implementer wires this
without per-packet closures (#315 pattern)". The in-tree faithful port is
sitting unused — `RemoteTeleportHook.Execute` +
`RemoteTeleportHookActions`, the ordered six-action seam with a currency check
between each step. This is the 4b-3 round-2 R2 defect class verbatim: *a
retail action skipped with the faithful port already in-tree.*
**Not a regression** — the deleted `ApplyAuthoritativePosition` ran no hook
either — but it is a divergence this commit had the obligation to close or
record, and it did neither.
**Correct behaviour:** run the hook through the existing ordered seam when the
record carries the managers (`record.RemoteMotion`/its host), letting the
per-manager guards no-op for a bare arrow exactly as retail's do; or state in
AP-141 that only action 6 of `teleport_hook` @0x00514ED0 is ported for a
projectile and that actions 1-5 are skipped even when the managers exist.
*Note on the stale-queue half specifically:* while the Missile bit is set the
remote interpolation is inert (`LiveEntityAnimationScheduler.cs:336` gates on
`projectileHandlesMovement`), so the skipped `StopInterpolating` is latent
rather than immediately observable — it becomes live again the moment ACE
clears Missile on impact. `ClearTarget`/`NotifyVoyeurOfEvent` and
`CancelMoveTo` have no such shield.
---
### R3 — MINOR — invariant 4 ("prediction invalidated before every body write on this route") and invariant 2 (presentation) hold on the direct arm only; the retained-retry arm satisfies neither
**Where:** `src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs:1150-1213`
(`Advance()`), reached from
`src/AcDream.App/Net/GraphicalSessionEventRoute.cs:121`.
A `Contention` outcome parks the packet in `_pending` and re-submits it one or
more host cadence pumps later. That retry path calls `SubmitAndResolve`
(a canonical body commit) and, on a destination that left the window,
`StoreAcceptedDestinationPose` (a raw body write) — **without**
`projectile.InvalidatePrediction()` and without `SyncProjectilePresentation`.
The version bumped at packet time does not protect a quantum begun *after*
that bump and straddling the retry.
Presentation is the same shape: the App's `SyncPresentationFromResolvedBody`
is called only from `OnPosition`, and `RuntimePlacementPresentationSink.TryPublishPlace`
(`src/AcDream.App/World/RuntimePlacementPresentationSink.cs:210-241`) does not
write `entity.Position`/`Rotation` — it snapshots whatever the entity already
holds. So a retried projectile commit advances the canonical body with no
presentation projection at all until the next quantum re-projects.
Both are bounded (the retry pump runs from the network retry lease, not from
inside the scheduler's `TryBeginQuantum`/`CompleteQuantum` straddle; the
presentation lag self-heals on the next tick), which is why this is MINOR
rather than MAJOR. But the invariants are stated absolutely, and this is the
"an invariant satisfied on one arm only" class both 4b-3 rounds named.
---
### R4 — MINOR — `SyncProjectilePresentation` silently drops the spatial+hidden clock consumption while its doc claims a faithful reduction
**Where:** `src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs:975-985`
(doc) and `:1028-1042` (the `else if (spatial)` branch).
The deleted tail was:
```
else if (spatial)
{
body.InWorld = true;
body.LastUpdateTime = currentTime; // <-- gone
_physics.Engine.ShadowObjects.Suspend(localId);
}
```
The replacement omits `body.LastUpdateTime`. The doc immediately above claims
the method is that tail "reduced … to this controller's own two-argument seam
`_clock` supplies the same clock source that method took as an explicit
`currentTime` parameter", which is true of the *visible* branch and false of
this one. The intent of the deleted write is documented elsewhere in the same
subsystem (`ProjectileController.TryBind`: "consume the hidden clock so UnHide
cannot replay a time backlog").
Consequence is small — the projectile stepper takes its quantum from
`RetailObjectQuantumBatch`, not from `body.LastUpdateTime`, and
`ProjectileController.Tick:601-604` re-stamps `LastUpdateTime` on the
`!InWorld` re-entry edge — but this is an unclaimed behaviour deletion inside
a change whose comment asserts equivalence. (Clock *domain* is fine:
`UpdateFrameOrchestrator.CurrentScriptTime => _runtime.SimulationTimeSeconds`,
so `_clock.SimulationTimeSeconds` is the same base the App used to pass.)
---
### R5 — MINOR — the App ignores the seam's returned status, so the "presentation advances on committed/stored outcomes only" gate exists on the Runtime side only
**Where:** `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2237-2245`.
Runtime deliberately withholds `SyncProjectilePresentation` for `Deferred` and
`RejectedByPlacement` (with the comment "Deferred/RejectedByPlacement leave the
body at its prior (already-synced) pose"). The App then calls
`_projectileController.SyncPresentationFromResolvedBody(...)` unconditionally,
discarding the returned `RuntimeRemotePlacementExecutionStatus?` entirely —
including on `Deferred`, where `ParkDeferred` has already snapped the body to
the parked result and withdrawn the entity. The App write moves the render
entity to a pose Runtime declined to publish; if the park is later cancelled
and restored (`WithdrawalRestored`), the entity re-shows there. That is
AP-136's family.
On the no-op and swallow dispositions the write is inert (same value), so this
is MINOR, but the two layers disagree about the same invariant.
---
### R6 — MINOR — on a stored (non-committed) outcome the render cell is left behind while the render position moves; retail's `store_position` writes `objcell_id` too
**Where:** `src/AcDream.App/Physics/ProjectileController.cs:512-517`
(`entity.ParentCellId = record.FullCellId`) with
`RuntimeRemotePlacementDriveController.StoreAcceptedDestinationPose:1104-1136`.
`StoreAcceptedDestinationPose` writes `body.Position` and `body.Orientation`
only — not the cell, and D-P2 correctly forbids a `RebucketLiveEntity` on this
arm. So after `Refused`/`Contention`/`RejectedPreparation` the render entity
gets the new world position with the *old* `ParentCellId`. The deleted path
could not produce this pairing: it always did `body.SnapToCell(fullCellId, …)`
+ `CommitProjectileCell`.
Retail's `store_position(this, arg2)` @0x00515CE2 writes the object's whole
`Position` including `objcell_id`, and the branch then calls
`CObjectMaint::GotoLostCell` @0x00515CF2 — i.e. retail moves the cell *and*
hides the object pending load. This is largely AP-138's existing residual
(shared with the remote far arm), but it is newly reachable for projectiles
and worth naming when AP-141 is amended.
---
### R7 — MINOR — the contract's App-layer dual-kind matrix (§7 items 8-10) is absent; the D-P6 dispatch has no test at all
Added coverage is Runtime-only: `RuntimeProjectilePositionKindTests` (3 facts,
kind derivation + mid-life flip) and 11 new
`RuntimeRemotePlacementDriveControllerTests` facts. These are good tests —
positive assertions, real behaviour, no source-text pins, and the split-quantum
abort (T3) and no-velocity (D-P5) invariants are both genuinely exercised.
But there is no test anywhere under `tests/AcDream.App.Tests` that drives
`OnPosition`. The contract made the dual-kind `[Theory]` "what makes a future
silent mis-tag fail a test", and item 10 required a sabotage check
("hardcode Remote and confirm the dual-kind theories fail"). With no App-level
test, hardcoding `isMissilePacket = false` at
`LiveEntityNetworkUpdateController.cs:2229` would leave the suite green — the
exact failure mode the discipline was written to catch. The swallow ordering,
the presentation-ack ordering, and the "no `RemoteMotion` is ever created for a
missile packet" claim are all untested.
---
### R8 — MINOR — the corrected 4a comment now records an unjustified production behaviour with nothing tracking it
`LiveEntityNetworkUpdateController.cs:2309-2323` now states that the retail
justification for the remote arm's `TryCommitAuthoritativeVelocity` "is
therefore NOT yet established and needs its own audit". That is the honest
call, and the contract (§10) said to report rather than edit. But an
acknowledged-unjustified velocity commit on a live production path now exists
with no AP row and no `docs/ISSUES.md` entry — only a code comment. One line
in ISSUES.md (or an AP row) would keep it findable.
---
## C. Anything wrong in the CONTRACT itself
Nothing factually wrong. Three notes:
1. **§4's `teleport_hook` address is cited two ways and both are right** — the
contract's §4 row 2 cites @0x005163EF (the call site inside
`MoveOrTeleport`) and D-P4 cites @0x00514ED0 (the function). Confirmed
consistent; no action.
2. **D-P4's adopted-body sentence is the only pinned obligation the diff did
not discharge** (R2). The contract's wording ("the implementer wires this
without per-packet closures") is an instruction, not a claim, so the
contract is not wrong — the implementation is incomplete against it.
3. **D-P2 step 1's "Anything else: return `NotApplicable`-equivalent, write
nothing"** is the source of R1. The contract pinned it without noticing that
it removes the deleted path's fall-through and therefore *introduces* a new
divergence needing its own register clause. Worth a sentence when AP-141 is
amended, so the next reader does not read the contract as having cleared it.
Everything else I checked in the contract holds: the retail chain (§4, all
seven rows), the ACE unreachability (`WorldObject_Tick.cs:333-334` inside the
`PhysicsState.Missile` branch at `:265`), the classifier's kind-blindness past
`LocalPlayer` (`RuntimeAuthoritativePositionRouteClassifier.cs:349`,
`:535-538`, `:564-575`), and the three ownership fences (`OwnsPlacement`
widened; `OwnsFarSnap`/`OwnsTeleportPlacement` untouched, both arms still
`ArgumentNullException.ThrowIfNull(remote)` + throwing route guards).
I also spot-checked proof obligation **P3** and found no capture hazard: both
`RemoteAuthoritative` stamps in `RuntimeSetPositionState`
(`:3833/:3866` window-departure park, `:5338/:5350` legacy-direct withdrawal)
construct *fresh* operations and the park loop skips records already in
`_operations`, so neither can relabel a live `ProjectileAuthoritative`
operation; `route.OperationKind` is threaded intact through
`TryBeginExclusiveAuthoredPlacement` and `TryPrepareAndSubmitAuthoredPlacement`,
so the `command.Kind != operation.Kind` consistency check at `:2963` is
satisfied; and the direct-command gate at `:5445` admits local kinds only, which
this route never uses. `MoverPhysicsState = record.FinalPhysicsState` is carried
into the placement request (`:2007`, `:3007`, `:3810`, `:5496`), so the Missile
bit still reaches the engine's `missile_ignore` handling.
---
## D. Summary table
| # | Sev | Where | Retail address contradicted |
|---|---|---|---|
| R1 | MAJOR | `LiveEntityNetworkUpdateController.cs:2222-2247` + `RuntimeRemotePlacementDriveController.cs:1004-1012` | `MoveOrTeleport` @0x00516330 via @0x00454254 — placement is unconditional on client machinery |
| R2 | MAJOR | `RuntimeRemotePlacementDriveController.cs:1017` | `teleport_hook` @0x00514ED0 actions 1-5 (@0x00514EDB/@0x00514EEC/@0x00514EFB/@0x00514F0A/@0x00514F19) |
| R3 | MINOR | `RuntimeRemotePlacementDriveController.cs:1150-1213` | — (invariant 4/2, one arm only) |
| R4 | MINOR | `RuntimeRemotePlacementDriveController.cs:975-985, 1028-1042` | — (unclaimed behaviour deletion + stale doc) |
| R5 | MINOR | `LiveEntityNetworkUpdateController.cs:2237-2245` | — (App/Runtime disagree on invariant 2) |
| R6 | MINOR | `ProjectileController.cs:512-517`, `RuntimeRemotePlacementDriveController.cs:1104-1136` | `store_position` @0x00515CE2 / `GotoLostCell` @0x00515CF2 (writes cell too) |
| R7 | MINOR | `tests/AcDream.App.Tests/**` (absent) | — (contract §7 items 8-10 undelivered) |
| R8 | MINOR | `LiveEntityNetworkUpdateController.cs:2309-2323` | — (bookkeeping) |