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>
This commit is contained in:
Erik 2026-08-04 21:03:41 +02:00
parent 30d3d114b0
commit 36255af0f6
19 changed files with 5390 additions and 393 deletions

View file

@ -24,6 +24,46 @@ What does NOT go here:
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
## #317`TryCommitAuthoritativeVelocity`'s call site has no established retail basis
**Status:** OPEN
**Severity:** LOW (tracking only; no known observable defect)
**Filed:** 2026-08-04, C4 route 5 (projectile authoritative placement) round-2
review, MINOR (c)
**Component:** physics / remote velocity
**Description:** `LiveEntityNetworkUpdateController.cs`'s 4a-family remote
velocity commit (`_liveEntities.TryCommitAuthoritativeVelocity(...)`, call
site around line 2444) carried a comment claiming
"`MoveOrTeleport` installs that exact vector with `set_velocity`." A
byte-level Capstone disassembly of the PDB-paired retail binary
(`CPhysicsObj::MoveOrTeleport` @0x00516330-@0x00516438, every branch,
performed as C4 route 5's mandatory Step 1 hard gate) shows
`MoveOrTeleport` never reads its velocity argument's stack slot at all, and
`UnpackPositionEvent` performs no `set_velocity` either — the only
`set_velocity` call in the whole accepted-Position chain zeroes the LOCAL
player (@0x004541B4), which is a different call site entirely. The comment
at the 4a call site has been corrected in place to state this plainly, but
the call itself was left in production unchanged (out of C4 route 5's
scope — the route's contract governs `RuntimeSetPositionOperationKind`
placement dispatch, not the pre-existing remote velocity commit) and
nothing currently tracks auditing or removing it.
**Root cause:** unverified assumption inherited from an earlier port pass;
never checked against the named retail decomp until C4 route 5's Step 1
gate incidentally required decoding the neighboring function.
**Files:** `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs`
(call site + corrected comment, ~line 2420); `LiveEntityRuntime`'s
`TryCommitAuthoritativeVelocity` (the method itself, unaudited).
**Acceptance:** A dedicated retail audit of the ENTIRE accepted-Position
velocity chain (not just `MoveOrTeleport`) — likely `UpdateObjectInternal`,
`update_object`, and whatever actually feeds the remote's `Velocity` field
retail-side — determines whether this call has a retail basis at all, and
either finds the correct source function to cite or removes the call as an
acdream-only adaptation with a divergence-register row.
## C4 route 6 — drops and split-recovery closure — 2026-08-04
#313 filed from the route 6 closure session (zero production lines; evidence +

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,338 @@
# C4 route 5 — architecture / adversarial DELTA review, round 2 (2026-08-04)
**Verdict: FAIL.** Three findings: two MAJOR, one MAJOR-coverage. Both MAJORs
are narrow and cheap to close (one expression, one call, one assertion, one
test), and one of them (**B2**) is a **new defect introduced by the fix
round**, in the same class as the finding it was fixing.
Round 1 raised 5 MAJOR + 6 MINOR. Eight are closed well, one is closed for the
teleport half only (**B1**), one is closed but with a regression attached
(**B2**), one is declined acceptably (A11). Detailed disposition in §3.
Scope: the uncommitted working tree, 1,626 insertions / 398 deletions across 10
files, against HEAD `30d3d114`. Independent gates run for this review: Release
build green (0 warnings / 0 errors); focused Runtime suites 59/59; focused App
suites (`ProjectileControllerTests` + `LiveEntityNetworkOnPositionCollapseMatrixTests`)
64/64. A green suite is not evidence — every claim below is traced to source.
---
## 1. New MAJOR findings
### B1 — A3's fix covers only the teleport branch; the adopted-body FAR snap still leaves the interpolation queue armed, and AP-141 now asserts the opposite as retail-faithful
**Severity: MAJOR (half-closed fix + a register row asserting behaviour the code
does not have — the defect class the contract lists by name).**
`LiveEntityNetworkUpdateController.cs:2151-2159` runs the six-action hook only
on `SetPosition`:
```csharp
if (route.Disposition
is RuntimeAuthoritativePositionDisposition.SetPosition
&& acceptedPositionCanonical.RemoteMotion is RemoteMotion adoptedRemote)
{
RunRemoteTeleportHook(...);
}
```
Retail's far branch is not covered by `teleport_hook` — it has its own
`StopInterpolating`, guarded on `position_manager != 0`
(@0x005163C9-@0x005163CB), and the remote far arm ports it exactly
(`ApplyAcceptedRemoteFarSnap`: `if (route.StopInterpolating) remote.Interp.Clear();`).
The projectile far arm ports nothing.
For the **adopted-body** shape — the one A3/R2 established as real and which
this round now has a passing test for
(`MissileAdoptedBody_TeleportCommit_UnConstrainsAndClearsInterpQueue`) — the
missile **does** have a `PositionManager` and a populated `Interp` queue, so
retail's guard passes and retail would clear it.
Concrete scenario: an ordinary remote with a live `Interp` queue gets the
Missile bit from a State packet; `TryBind`'s shared-body branch adopts the same
body; the next accepted Position classifies **far** (>=96 m), not teleport. The
projectile arm places the body at the destination and returns. The surviving
`RemoteMotion` is still in `_spatialRemotes` with a stale waypoint, and the
remote stepper drags the freshly-placed missile back toward it — the exact
symptom A3 described, on the branch the fix did not reach. The
`MissileAdoptedBody_…` test uses `teleportSequence: 5` (teleport); there is no
adopted-body far test.
Compounding this, **AP-141 (`docs/architecture/retail-divergence-register.md`)
now asserts the false justification unconditionally**:
> "The far branch's `StopInterpolating` skip is NOT part of this divergence —
> retail's own `position_manager != 0` guard @0x005163C9 already skips it for a
> never-interpolated missile, so acdream's identical skip is retail-faithful by
> consequence."
That justification is conditioned on "a never-interpolated missile", which the
adopted-body case is not. The row therefore claims retail-faithfulness for a
shape where the code is not retail-faithful. This is the recurring class the
route-5 contract names in its predecessor list ("a register row asserting
behaviour the code does not have") and which AP-136 was previously amended for.
**Fix direction:** on the `SetPositionSimple` arm, when
`record.RemoteMotion is RemoteMotion adopted && route.StopInterpolating`, clear
`adopted.Interp` before the placement — mirroring `ApplyAcceptedRemoteFarSnap`'s
one line and retail's own ordering (@0x005163CB before @0x005163D9). Then narrow
AP-141's far-branch sentence to the bare-missile case it is actually true of.
Add the adopted-body far test alongside the teleport one.
---
### B2 — R6 traded A1's wrong-cell symptom for a new one: on every STORED outcome the render entity is now positioned at the destination but parented to the cell it left
**Severity: MAJOR (regression introduced by this fix round).**
`ProjectileController.SyncPresentationFromResolvedBody:530` changed from
`record.FullCellId` to:
```csharp
entity.ParentCellId = runtime.Body.CellPosition.ObjCellId;
```
Trace the three outcome classes:
| outcome | `body.Position` after | `body.CellPosition.ObjCellId` | `record.FullCellId` |
|---|---|---|---|
| Committed | resolved destination | result cell (`SnapToCell`) | result cell (`CommitCanonical`'s `SetFullCell`) |
| Stored (`Refused`/`Contention`/`RejectedPreparation`/`NotApplicable`) | **accepted destination, resolved in the WIRE cell's frame** | **source cell — `StoreAcceptedDestinationPose` never writes the cell** | **wire cell — the merge's `RefreshDerivedState` → `SetFullCell`** |
| `Deferred`/`RejectedByPlacement` | ack does not run (A1 gate) | — | — |
On a committed outcome the two sources are identical, so R6 is a no-op there —
and `MissileTeleportCommit_…` asserts both, confirming the equality rather than
discriminating between the sources.
On a **stored** outcome they diverge, and R6 picked the wrong one.
`StoreAcceptedDestinationPose:1123-1132` computes
`accepted.PositionX + worldOffset(accepted.LandblockId)` — the world point of
the **wire** cell-local coordinates — and `record.FullCellId` is that same wire
cell. So `record.FullCellId` is the cell the stored position belongs to;
`body.CellPosition.ObjCellId` is the cell the body **left**.
The inconsistency is visible *inside this fix round*: on the same stored packet,
Runtime's own `SyncProjectilePresentation:1043-1051` publishes the shadow row
with `record.FullCellId` (wire cell) paired with the new position, while App
publishes the render entity with the **source** cell paired with the same new
position. Two presentation surfaces, two different cells, one body.
Concrete failure: a missile crosses a landblock boundary at the streaming edge;
the destination landblock is not yet in the collision service window, so
`CanAttemptDestination` refuses. The body stores to the destination. The render
entity is placed there but `entity.ParentCellId` is the source cell —
and `ParentCellId` is precisely what
`RetailPViewRenderer.cs:925/945` uses for the indoor/outdoor stage split and
`viewcone.SphereVisibleInCell(e.ParentCellId!.Value, …)`. The arrow is
visibility-tested against a cell it is no longer in: culled, or drawn in the
wrong stage.
The R6 doc comment's own retail citation argues against its choice — it states
that retail's `store_position` @0x00515CE2 "writes the object's whole
`Position` **including `objcell_id`**". If retail's store wrote the cell, the
body's cell after a store *would be* the wire cell; `record.FullCellId` is the
faithful stand-in for that, and the body's stale cell is the acdream residual
(AP-138), not the truth.
**Fix direction:** revert `ParentCellId` to `record.FullCellId` (round 1's
value) and keep the A1 status gate, which is what actually fixed the no-op
case. That pairing is correct on all three outcome classes and agrees with
Runtime's own shadow publication. If the body's stale cell is considered the
truth, then `StoreAcceptedDestinationPose` must write the cell too — but that is
AP-138 scope and would change the remote arms as well.
---
### B3 — the one branch where `SyncProjectilePresentation` is the sole writer — spatial+visible on the STORE path — is still unasserted at both layers, and it is exactly where B2 and the surviving A1 sabotage hide
**Severity: MAJOR (coverage; one assertion + one test wide).**
I independently reproduced the implementer's "2 of 5" trace and confirm it:
| new test | discriminates a gutted `SyncProjectilePresentation`? | why |
|---|---|---|
| `TeleportCommit_…ForceEndsCollision…` (shadow assertion) | **No** | the shared pipeline's `ShadowObjects.CommitSetPosition` publishes the same row |
| `FarCommit_…` (shadow assertion) | **No** | same |
| `TeleportCommit_ReenteringWorldReactivatesBody` | **No** | `RuntimeSetPositionState.cs:2983-2985` sets `EnteringWorldFromCelllessResidence \|= !body.InWorld \|\| FullCellId == 0` at **prepare** time (before `SnapToCell`), and `:4979-4984` then sets `Active` + `LastUpdateTime` itself |
| `TeleportCommit_HiddenSuspendsShadowStaysInWorld` | **Yes** | nothing else suspends on Hidden |
| `Refused_NonSpatialDeactivatesAndSuspends` | **Yes** | the Refused path never reaches the engine |
The implementer's trace is accurate and was reported honestly. But the table
also shows *why* only 2 discriminate: on the committed path the shared pipeline
independently produces the same observable, so `SyncProjectilePresentation`'s
spatial+visible branch is only load-bearing on the **store** path — and no test
asserts it there. `Refused_StillAdvancesPoseNoParkPredictionInvalidated` is
spatial+visible+Refused and asserts `body.Position`, prediction,
`body.InWorld` (vacuous — `AttachBody`'s `SnapToCell` already set it) and
`ObjectClock.IsActive`, but **not the shadow row**.
The same hole exists at the App layer: the collapse matrix now has teleport
commit, far commit, near no-op, airborne no-op, null classification, unbound
fall-through, and adopted-body teleport — but **no store/refused scenario**.
That is why the reported A1 sabotage survived: with R6 reading the body's own
cell, an unconditional ack on a *no-op* outcome writes nothing observable
(position and cell both unchanged), so the gate is invisible on the no-op path.
It is visible on the **store** path — which is untested.
Both gaps are cheap:
- **Runtime**: add `Assert.Equal(body.Position, shadowEntry.Position)` (using
the file's existing `AllEntriesForDebug` pattern) to
`Refused_StillAdvancesPoseNoParkPredictionInvalidated`. Gutting the sync then
leaves the shadow at the spawn pose and the test fails.
- **App**: add a `MissileFarRefused_…` scenario to the collapse matrix. The
fixture already supports it with **no new machinery**`PublishDestinationCollision()`
and `ServiceWindow.Allow(DestinationLandblock)` are separate opt-in calls, so
simply omitting `Allow` yields `Refused` from `CanAttemptDestination`. Assert
`entity.Position == body.Position` **and** `entity.ParentCellId` against the
wire cell. That single test catches B2 and gives the A1 gate a discriminating
home.
---
## 2. New MINOR findings
### B4 — the App hook call allocates a per-packet closure, contradicting its own #315-pattern claim
`LiveEntityNetworkUpdateController.cs:2155-2158` passes
`() => _liveEntities.IsCurrentPositionAuthority(acceptedPositionRecord, acceptedPositionAuthorityVersion)`
a fresh display class + delegate on every teleport-classified adopted-body
missile packet. The comment immediately above claims it uses "the SAME ordered
hook seam and per-packet currency check the remote teleport arm already uses
(`RunRemoteTeleportHook`, **#315 pattern**)". The remote arm's #315 pattern is
precisely the opposite: `_remoteArmCallbacks` cached delegates over scratch
fields (`:1474-1492`), introduced by the collapse's second commit to remove
per-packet closures from this exact method. D-P4 also states the wiring must be
"without per-packet closures". Narrow reach (SetPosition + RemoteMotion
present), so MINOR — but the comment asserts compliance the code does not have.
### B5 — the retained-retry arm invalidates prediction on an outcome that writes nothing
`RuntimeRemotePlacementDriveController.Advance:1265-1268`: the else branch calls
`pendingProjectile?.InvalidatePrediction()` and then `SubmitAndResolve`. When
`SubmitAndResolve` returns `Contention` (retryable → re-parked into `_pending`),
`Advance` performs **no** `StoreAcceptedDestinationPose` — so nothing was
written, yet the prediction version advanced. Contract invariant 4 pins
"prediction invalidation accompanies every body write on this route… The no-op
dispositions invalidate nothing." Effect is one aborted in-flight quantum per
re-park; small, but it is the invariant's stated shape violated on one arm only.
The entry point does not have this problem (Contention there always stores).
---
## 3. Round-1 findings — disposition
| # | status | verification |
|---|---|---|
| **A1** (unconditional ack) | **CLOSED, gate correct — but see B2** | `:2170-2179` gates on `placementStatus is not null and not Deferred and not RejectedByPlacement`, an exact set-equality mirror of the Runtime gate (`:975-983` plus the `null` returns). Verified by enumerating every `RuntimeRemotePlacementExecutionStatus` producer. |
| **A2** (Missile bit without a bound projectile) | **CLOSED, well** | The discriminator became conjunctive at the classifier (`RuntimeEntityObjectLifetime.cs:673-677`) with the App's null-route fallback mirroring it exactly (`:2122-2131`). Retail justification is sound and better than my suggested fix: retail places every non-player object unconditionally, so an unbindable missile taking the ordinary remote path *is* the faithful behaviour, not a fallback. Blast radius enumerated below. `MissileUnbound_FallsThroughToRemoteTail_TracksInsteadOfFreezing` is discriminating (revert the conjunct and the body never moves). |
| **A3** (teleport hook) | **PARTIALLY closed — see B1** | Teleport branch wired through the existing `RemoteTeleportHook` bundle with a real per-packet currency guard; the adopted-body test is genuinely discriminating (removing the call leaves `Constraint.IsConstrained` true and `Interp.IsActive` true). Far branch not covered. |
| **A4** (sync untested) | **PARTIALLY closed — see B3** | 2 of 5 new tests discriminate; the store-path spatial+visible branch remains unasserted. |
| **A5** (no App missile coverage) | **CLOSED, well** | 7 tests on the shared collapse fixture. Fixture-regression check below. |
| **A6** (`wasInWorld` read after placement) | **CLOSED (behaviourally inert, correctly so)** | `wasInWorld` is now captured before dispatch at both call sites (entry point `:942`, retry `:1226`) and threaded as a parameter. Note it has **no observable effect**: on commit, `EnteringWorldFromCelllessResidence` already re-activates from the same pre-`SnapToCell` condition; on store, `body.InWorld` is unchanged so before == after. Correct and trap-removing either way. |
| **A7** (hidden `LastUpdateTime`) | **CLOSED** | Restored at `:1055`. Clock basis verified equivalent: `UpdateFrameOrchestrator.CurrentScriptTime => _runtime.SimulationTimeSeconds`, i.e. `_physicsScriptGameTime` and `_clock.SimulationTimeSeconds` are the same clock, so no mixed-basis write into `body.LastUpdateTime`. |
| **A8** (silent shadow skip) | **CLOSED** | `else { physics.ThrowIfWorldFrameUnreachable(record.FullCellId); }` at `:1053`. Verified non-throwing during the legitimate pre-Create window (`RuntimePhysicsState.cs:596-606` returns early unless the local-player Create was observed with a zero frame). |
| **A9** (local player not fenced) | **CLOSED** | `update.Guid != _playerServerGuid &&` added to the fallback (`:2125`). |
| **A10** (`OwnsFarSnap` doc) | **CLOSED** | Doc-only change; the predicate body is byte-identical (verified — the diff adds only comment lines). No effect on the far arm the collapse and 4b-2 gated. |
| **A11** (stale cutover inventory) | **DECLINED — acceptable** | `docs/research/2026-08-02-cutover-route-inventory.md` is a dated research/planning record, and the project's documentation rules treat those as historical. D-P7's grep was scoped to `src/` and `docs/architecture/`, both of which are clean. Reasonable call; the risk is a future reader treating a dated inventory as current, which the date already signals. |
| **`_lastFiniteGameTime`** | **CLOSED** | `SyncPresentationFromResolvedBody:521-522` restores the rebase under `double.IsFinite`, matching the deleted method's semantics (it also skipped the assignment on a non-finite clock). |
### A2's fix — blast radius, enumerated
Everything that now depends on the conjunctive kind derivation, and its status:
1. `RuntimeAcceptedPositionRouteRequests.TryBuild``RuntimeAuthoritativePositionRouteClassifier` — re-verified that the classifier's only kind branch past `ValidEntityKind` is `LocalPlayer`, so `Projectile` and `Remote` remain disposition-, flag-, `StopInterpolating`- and `ConstrainPhase`-identical. Kind changes the `OperationKind` only.
2. `RuntimeRemotePlacementDriveController.OwnsPlacement` — admits both; single production reader (`TryExecuteAcceptedRemotePosition:640`).
3. `RuntimeRemoteFarSnapPosition.OwnsFarSnap` / `RuntimeRemoteTeleportPosition.OwnsTeleportPlacement` / `ApplyAcceptedRemoteFarSnap` / `ApplyAcceptedRemoteTeleport` / `TryArmConstraintAfterOperation` — still `RemoteAuthoritative`-gated. An **unbound** missile now legitimately reaches them (it is classified `Remote`), which is the intended pre-route-5 behaviour; a **bound** one never can. The throwing guards stay unreachable for projectiles.
4. App `isMissilePacket` — derived from `route.OperationKind` whenever a route exists, so it cannot disagree with the classifier; the null-route arm restates the identical conjunct.
5. **Kind is now derived from mutable binding state**, so it can flip between packets without the Missile bit changing. The one place that stores a kind across packets is `_pending[key].Route`; `Advance` re-validates `pending.Record.Projectile` and body identity before syncing (`:1214-1224`), and `TryPrepareAndSubmitAuthoredPlacement` compares `route.OperationKind` against the `operation.Kind` stamped at the same Begin. Consistent — this was handled deliberately (R3), not by luck.
6. First-entry admission, the residence Create half, and the continuation executor all pass explicit kinds and are untouched.
I found no dependent that broke.
### A5's fix — did the shared fixture regress for its original dual-guid purpose?
**No.** Verified line by line:
- The remote construction is moved verbatim into an `else` branch; the
`RemoteMotion` + `Shadows.Register(… Cylinder, cylHeight: 1.835f …)` block is
byte-identical to the original.
- `baseState` evaluates to exactly `PhysicsStateFlags.ReportCollisions` when
`isMissile: false`, so both `RawState` and `PhysicsState` are unchanged for
every pre-existing test (all of which use the default `isMissile: false`).
- `Remote` stays `null!` for missile fixtures; no missile test dereferences it.
The two tests that do use `fixture.Remote` (`MissileUnbound_…`,
`MissileAdoptedBody_…`) both construct with `isMissile: false` — deliberately,
because both scenarios *are* remote-shaped records that acquire the Missile
bit later. That is the right modelling.
- The new `Projectile` property is additive.
- All pre-existing dual-guid tests in the file pass in the 64/64 run.
`MissileAdoptedBody_…` is worth calling out as a genuinely good test: it builds
the adopted-body shape through the production seam (`BindProjectile` over the
record's canonical body, which `SetRemoteMotion` had already adopted from the
component), and its two discriminating assertions (`Constraint.IsConstrained`,
`Interp.IsActive`) fail if the hook call is removed.
---
## 4. The two reported residuals — judgment
### Residual 1 — A1's fix has no App-level regression test
**Judgment: the stated gap is acceptable; the surrounding gap is not.**
The narrow claim is correct and I verified it: `Deferred` requires a
`DeferredCell` park (collision-generation quiescence) and `RejectedByPlacement`
requires the engine's own sweep to refuse, and no sibling remote test in that
file constructs either — the fixture has no machinery for it. The gate is a
literal set-equality mirror of a Runtime gate whose behaviour *is* exercised by
`Refused`/no-op/rejected tests, so code review is a defensible verification for
those two statuses specifically.
But the reason the sabotage survived is not that `Deferred` is unreachable — it
is that **R6 made the ack idempotent on the no-op path**, so the gate has no
observable effect on any scenario the matrix currently contains. The status the
gate matters for that *is* trivially constructible is the store path, and the
fixture already supports it (omit `ServiceWindow.Allow`). That test is required
(B3), and it also catches B2. So: accept the `Deferred`/`RejectedByPlacement`
carve-out, reject the absence of any store-path App scenario.
### Residual 2 — only 2 of 5 A4 tests discriminate
**Judgment: 2 is not sufficient for invariant 2, but the shortfall is one
assertion, not a suite.**
The trace is accurate (I reproduced all five verdicts independently, including
the non-obvious `EnteringWorldFromCelllessResidence` mechanism behind the
reactivation test). Keeping the redundant assertions as correct facts is the
right call — they are true, cheap, and they pin the shared pipeline's
contribution.
The shortfall is specific: the spatial+visible branch is redundant **only on the
committed path**. On the store path `SyncProjectilePresentation` is the sole
writer of the shadow row, and no test asserts it. Invariant 2 is "#312's layer"
— an entity left rendered a packet behind its body — and the store path is
exactly the outcome class where that can happen without the shared pipeline
noticing. One `Assert.Equal(body.Position, shadowEntry.Position)` in the
existing `Refused_…` test converts the third branch from redundant to
discriminating and closes invariant 2 at the Runtime layer. Combined with the
App store scenario from B3, invariant 2 is then covered at both layers by
discriminating assertions.
---
## 5. What would make this PASS
1. **B2**: `entity.ParentCellId = record.FullCellId` (keep the A1 gate). One
expression.
2. **B1**: clear the adopted `Interp` queue on the `SetPositionSimple` arm when
`route.StopInterpolating`; narrow AP-141's far-branch sentence to the
bare-missile case. One call + one clause + one test.
3. **B3**: one shadow assertion in `Refused_StillAdvancesPoseNoParkPredictionInvalidated`;
one `MissileFarRefused_…` App scenario asserting position **and**
`ParentCellId` (which is the B2 regression test).
4. **B4/B5**: cached delegate for the hook currency check; move the retry arm's
`InvalidatePrediction` to the paths that actually write.
Nothing here needs new machinery, a new fixture, or a design change.

View file

@ -0,0 +1,188 @@
# C4 route 5 — architecture / adversarial DELTA review, round 3 (2026-08-04)
**Verdict: FAIL — one finding (C1), coverage-only, no identified defect.**
Every behavioural finding from rounds 1 and 2 is now closed and independently
verified. The single remaining item is that the `Advance()` retry arm's
projectile branch — added in round 2 (R3) and semantically modified in round 3
(B5, a prediction-invalidation reordering) — is executed by **no test at any
layer**. I traced the reordering and believe it is safe; but "safe by reading"
is the standard this project rejects, and it is the standard I applied to the
implementer twice (A4, A5). The templates to close it are already in the same
file. **The bar for round 4 is one test.**
Gates run for this review: `dotnet build AcDream.slnx -c Release --no-incremental`
**0 errors, 21 warnings, all pre-existing and none in any route-5 file** (they
are in `AcDream.App.Tests/Composition`, `AcDream.Core.Tests` nullability and
xUnit-analyzer nits; my earlier "0 warnings" readings were incremental builds
that skipped those projects — no regression). Focused Runtime suites 59/59;
focused App suites including `UpdateFrameOrchestratorTests` 94/94.
---
## 1. Finding
### C1 — the `Advance()` retry arm's projectile branch has never been executed by a test, and round 3 changed its prediction-invalidation semantics
**Severity: MAJOR (coverage). No defect identified — the code reads correct.**
`RuntimeRemotePlacementDriveController.Advance:1221-1305` contains the R3
projectile branch (kind/identity re-validation, `pendingWasInWorld` capture,
prediction invalidation, `SyncProjectilePresentation`) and the B5 reordering.
All six `drive.Advance()` call sites in
`RuntimeRemotePlacementDriveControllerTests.cs` (`:533`, `:549`, `:592`,
`:1015`, `:1021`, `:1755`) belong to **remote**-kind tests. No test parks a
`ProjectileAuthoritative` retry and pumps `Advance()`.
What is therefore unexercised:
- prediction invalidation on a retried projectile placement (trap T3 — a
straddling quantum clobbering a committed placement is exactly what this
guards);
- the B5 semantic change: invalidation **skipped** when `SubmitAndResolve`
returns `Contention`;
- `SyncProjectilePresentation` on the retry arm (invariant 2 on a second call
site);
- the stale-route guard (`pending.Route.OperationKind` + body/component
re-validation) that exists precisely because a retry can outlive its
binding.
The R3 comment states the intent plainly — *"Neither invariant this route pins
… may hold on the direct arm only"* — and both invariants are, in fact,
asserted only on the direct arm.
Reachability: `_pending` receives a projectile entry whenever
`SubmitAndResolve` returns a retryable preparation status
(`RetrySetupUnavailable`/`RetryWorldFrameUnavailable`) for a
`ProjectileAuthoritative` route — an ordinary streaming-edge condition — and
`Advance()` is the host cadence pump. Standing caveat: like the whole route,
ACE-unreachable in play.
**Fix direction (cheap — both templates are in the same file):** rebuild
`FarSnap_RetryablePreparation_StoresThePoseAndStillRetainsTheRetry` (`:985`)
and `Advance_DestinationLeavesTheWindow_StoresTheNewestDestinationPose`
(`:1707`) against `CreateProjectileRecord` + a `ProjectileAuthoritative` route
— the same borrowing the projectile ledger test already did from
`Teleport_LedgerConverges_…`. Assert, on the retry: the pose advanced, the
prediction version moved on the storing outcomes and **did not** move on a
re-parked `Contention`, and the shadow row followed the body.
---
## 2. The B5 reordering — safety verified
Asked to scrutinise this specifically. **It is safe**, and for a reason
stronger than the comment's "single-threaded and synchronous".
The invalidation exists solely so a straddling quantum aborts at `Complete`.
`PredictionAuthorityVersion` has exactly four consumers, enumerated across
`src/`: `RuntimeProjectilePhysicsUpdater.TryBegin`/`Complete`/`IsIdentityCurrent`,
`RuntimePhysicsState.CommitProjectileCell`'s `IsExactOwner`, and
`ProjectileController.IsCurrentQuantumIdentity` (`:837`). The only callers of
`CompleteQuantum` are `ProjectileController.AdvanceQuantum` (`:760`) and
`LiveEntityAnimationScheduler` (`:419`) — both frame-loop driven. Nothing in
the placement pipeline's synchronous publish chain (placement projection sink,
`RebucketLiveEntity`'s visibility callbacks, collision-report observers) can
reach either. Notably, the one projection callback that *does* re-enter the
projectile controller — `OnProjectionVisibilityChanged``TryBind` — returns
early on a retained runtime and never touches a quantum. So no `Complete` can
interleave between the write inside `SubmitAndResolve` and the invalidate on
the next statement.
The skip on `Contention` is also correct: `Contention` from `SubmitAndResolve`
means preparation returned a retryable status, so `PhysicsEngine.SetPosition`
never ran and nothing wrote the body — verified by walking
`TryPrepareAndSubmitAuthoredPlacement`'s non-`Prepared` path, which touches
only `operation.*` fields and reads (`body.InContact`/`OnWalkable`), never the
body's pose.
Two ordering details I checked and found sound: the `Refused` branch still
invalidates **before** `StoreAcceptedDestinationPose` (the one body write on
that arm); and `CancelToken`'s synchronous cancellation receipt — which the
class doc warns can delete or replace the incarnation — is harmless here
because `pendingProjectile` is a captured strong reference (invalidating a
displaced component is inert, and a displaced component's quantum already
fails `IsIdentityCurrent`'s `ReferenceEquals(record.Projectile, projectile)`),
while both `StoreAcceptedDestinationPose` and `SyncProjectilePresentation`
re-validate currency at entry.
Honest note on the invariant's status: contract §5 item 4 says invalidation is
"before the write". On this arm it is now after, resting on a reachability
argument rather than structural ordering. The argument holds today; it is
recorded here so a future change that lets a projection callback drive a
quantum knows it broke something. The comment states the reasoning openly,
which is the right handling.
---
## 3. The indoor staging in the B2 regression test — legitimate, and it corrects my round-2 finding
**Judgment: the staging is legitimate, the test genuinely pins production
behaviour, and the diagnosis behind it is correct.**
I verified the mechanism at source. `PhysicsBody.Position`'s setter
(`PhysicsBody.cs:153-162`) calls `SyncCellPositionDelta(delta)` on every write,
including `StoreAcceptedDestinationPose`'s. That method (`:287-308`) has two
branches:
- **outdoor** (low word `1..0x40`): `LandDefs.AdjustToOutside` re-derives the
cell index from the shifted local origin *and* bumps the landblock on a
192 m crossing — so after a store to a cross-landblock destination the body's
own cell id **self-corrects to the destination cell**;
- **indoor** (low word outside that range — an EnvCell id): the local origin
shifts but the cell id is kept verbatim, so it stays pinned at the source
EnvCell.
So the implementer's finding is right, and it **corrects my round-2 B2**: the
defect I reported was real but its blast radius was narrower than I stated. My
stated scenario ("a missile crosses a landblock boundary at the streaming
edge") is an *outdoor* case, which self-heals through `AdjustToOutside`; the
genuinely divergent case is a body in an EnvCell — a bolt fired inside a
dungeon. That correction is recorded here rather than smoothed over.
The staging is not a fixture artefact:
`IndoorSourceCell = SourceLandblock | 0x0100` is a well-formed EnvCell id (the
first EnvCell index), and `SnapToCell` seeds indoor claims verbatim by design
(`PhysicsBody.cs:190-205`, "Indoor EnvCell claims (low word >= 0x100) … seeded
verbatim"). The scenario — indoor body, outdoor wire destination, service
window refuses — is production-reachable.
One asymmetry worth naming, which does not affect the pin: the test stages the
indoor cell on the *body* while leaving `record.FullCellId` at the outdoor
`SourceCell`, a pairing production would not produce. It is inert here because
the merge overwrites `record.FullCellId` with the wire cell before the ack
runs, so the pre-packet value is never read. The assertion depends only on the
two expressions diverging, which they genuinely do.
Discrimination confirmed by construction: the test asserts
`body.CellPosition.ObjCellId == IndoorSourceCell` (the divergence actually
occurred) and then `entity.ParentCellId == DestinationCell`. Restoring round
1's `body.CellPosition.ObjCellId` read fails the second assertion.
---
## 4. Round-2 findings — disposition
| # | status | verification |
|---|---|---|
| **B1** (adopted-body far snap leaves `Interp` armed; AP-141 asserts the opposite) | **CLOSED, well** | The clear landed in the Runtime seam at `ApplyAcceptedProjectilePosition`'s `SetPositionSimple` case, **before** the placement — matching retail's @0x005163CB-before-@0x005163D9 ordering — guarded on `route.StopInterpolating && record.RemoteMotion is RemoteMotion`, the exact shape `ApplyAcceptedRemoteFarSnap` ports. Correctly scoped to one action, not the full hook. `MissileAdoptedBody_FarCommit_ClearsInterpQueueButLeavesConstraintArmed` is discriminating in both directions: it pins the queue clear **and** pins that `UnConstrain` did *not* run, proving the fix did not over-apply the teleport hook to the far branch. AP-141 is now narrowed to "faithful ONLY for a BARE missile" and additionally names the surviving `ConstrainTo` re-anchor divergence for the adopted case — a more accurate row than before the finding. |
| **B2** (`ParentCellId` regression) | **CLOSED, well** | Reverted to `record.FullCellId`; the A1 gate kept. The doc now carries three independent justifications, of which two I verified directly (the sibling remote arm pairs wire pose with wire cell; Runtime's own `SyncProjectilePresentation` publishes the shadow at `record.FullCellId` in the same call, so the body-cell read would have disagreed with the shadow for one body in one packet) and the third is retail's `store_position` @0x00515CE2 writing `objcell_id`. Regression-tested — see §3. |
| **B3** (store path unasserted at both layers) | **CLOSED at both layers** | Runtime: `Refused_StillAdvancesPoseNoParkPredictionInvalidated` now asserts `shadowEntry.Position == body.Position`; on the `Refused` path nothing but `SyncProjectilePresentation` publishes the shadow, so it discriminates. App: `MissileFarRefused_StorePathStillMovesEntityToDestinationParentCellIdAgreesWithWireCell` asserts entity position and `ParentCellId`; deleting the ack leaves the entity at its spawn pose. What remains untested from B3's neighbourhood is only the A1 gate's own two statuses (`Deferred`/`RejectedByPlacement`), which round 2 already accepted as an unconstructible carve-out, and the retry arm (C1). |
| **B4** (per-packet closure) | **CLOSED, correctly** | `IsCurrentProjectilePositionOwner` is now a third cached delegate inside `RemoteArmCallbacks`, bound to `IsCurrentProjectileArmPositionOwner`, reading `_projectileArmPosition*` scratch fields stamped immediately before use — byte-for-byte the same shape as the existing `_remoteArmPosition*` / `IsCurrentRemoteArmPositionOwner` pair (`:92-93`, `:1467-1468`, `:1502-1506`). Same reentrancy exposure as the incumbent #315 pattern, therefore parity rather than a new hazard. The `UpdateFrameOrchestratorTests` zero-`Delegate`-field guard holds — it ran green inside the 94/94 App suite, and the cache stayed inside the named type rather than becoming bare fields. |
| **B5** (retry arm invalidating with no write) | **CLOSED — see §2** | Correct, and the safety of the reordering verified independently. Untested (C1). |
| Round-1 **MINOR (c)** (4a velocity comment with no retail basis) | **FILED as #317** | `docs/ISSUES.md` — accurate description, correct scoping rationale, and an acceptance criterion that names the actual work (audit the whole accepted-Position velocity chain, then cite or remove with a register row). Better handling than a silent comment fix. |
No round-1 or round-2 finding regressed, and nothing in this round's diff
introduced a new behavioural defect that I could identify.
---
## 5. What would make this PASS
One test: a `ProjectileAuthoritative` retained retry driven through
`drive.Advance()`, asserting the pose advanced, the prediction version moved on
a storing outcome and did **not** move on a re-parked `Contention`, and the
shadow row followed the body. Two existing remote tests in the same file are
the templates.
Nothing else is outstanding.

View file

@ -0,0 +1,465 @@
# C4 route 5 — architecture / adversarial review (2026-08-04)
**Verdict: FAIL.**
Scope reviewed: the uncommitted working tree at HEAD `30d3d114`, branch
`claude/acdream-physics-divergence-5aa784``git diff HEAD` over 8 files plus
the untracked `tests/AcDream.Runtime.Tests/Entities/RuntimeProjectilePositionKindTests.cs`.
`docs/research/2026-08-04-c4-route-5-contract.md` is the contract, not under
review. Line numbers below are as-of the working tree and will go stale; every
citation also names the symbol (process rule 6).
Independent verification performed for this review: `dotnet build AcDream.slnx -c Release`
(succeeded, 0 warnings / 0 errors), focused Runtime suites 58/58 green, focused
`ProjectileControllerTests` 41/41 green. The complete Release suite was NOT run
here.
The design is sound and the Runtime seam is largely a faithful, well-argued
reproduction. The FAIL rests on four things: two real defects in the ~35 lines
of App dispatch glue (A1, A2), one pinned contract obligation left unwired with
a concrete failure scenario (A3), and the fact that the presentation invariant
(§5 item 2, "#312's layer — tests must assert it") and the entire App dispatch
have **zero** test coverage (A4, A5) — which is also the direct answer to the
scoping-gap question at the end.
---
## MAJOR findings
### A1 — the App projectile ack ignores the seam's status and writes cell identity on the outcomes the design pins as "write nothing"
**Severity: MAJOR.**
`src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2137-2144`
(`OnPosition`, the D-P6 projectile arm):
```csharp
if (earlyRemoteRoute is { } route)
{
_remotePlacementDrive.ApplyAcceptedProjectilePosition(
acceptedPositionCanonical,
route);
_projectileController?.SyncPresentationFromResolvedBody(
acceptedPositionRecord);
}
return;
```
The returned `RuntimeRemotePlacementExecutionStatus?` is discarded. The Runtime
half is deliberately selective — `RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition:975-983`
skips `SyncProjectilePresentation` for `Deferred`/`RejectedByPlacement`
("Invariant 2: presentation advances on every committed/stored outcome only")
and returns `null` without touching anything for `Interpolate`,
`NoPositionOperation`, `RejectedAuthority`, `RejectedData`, and every ownership
mismatch. App re-projects presentation for **all** of them.
Concrete failure scenario (the near no-op, D-P2's pinned `Interpolate` row):
1. An in-flight arrow's canonical body sits in cell A at world P_A.
2. A grounded, <96 m accepted Position for cell B arrives.
`RuntimeEntityObjectLifetime.TryApplyPosition` merges it; the merge's
`RefreshDerivedState``SetFullCell` (`RuntimeEntityRecord.cs:230-251`)
stamps `canonical.FullCellId = B` **before** any classification runs.
3. The classifier returns `Interpolate` — a pinned no-op. The Runtime seam
writes nothing: body stays at cell A / P_A, prediction version unchanged.
4. App nevertheless runs `ProjectileController.SyncPresentationFromResolvedBody:505-520`,
which does `entity.SetPosition(runtime.Body.Position)` (= P_A, correct) and
`entity.ParentCellId = record.FullCellId` (= **B**, wrong — `LiveEntityRecord.FullCellId`
is `Canonical.FullCellId`, `LiveEntityRuntime.cs:189-191`).
The render entity is now parented into a cell it is not geometrically inside,
with a position from the other cell. If B is an indoor EnvCell the arrow has not
entered, the arrow renders through the wall or is culled. The same write happens
on `RejectedByPlacement`, where the Runtime comment says in as many words that
the body must be left "at its prior (already-synced) pose".
This is a genuine regression, not parity: the deleted `ApplyAuthoritativePosition`
had no disposition concept — it always moved the body to the wire cell *and*
wrote the same cell into the entity, so position and cell were always
consistent. Splitting the body write out by disposition without splitting the
presentation write out with it is what creates the divergence.
**Fix direction:** return the status through the App boundary and call
`SyncPresentationFromResolvedBody` only on the outcomes `SyncProjectilePresentation`
itself covers (non-null status that is neither `Deferred` nor
`RejectedByPlacement`) — or move the render-entity projection into the same
gate inside the Runtime seam and have App acknowledge, not decide.
---
### A2 — a record carrying the Missile bit with no bound `RuntimeProjectile` now has every accepted Position silently dropped, where it previously fell through to the remote tail
**Severity: MAJOR (silent, permanent).**
Discriminator: `LiveEntityNetworkUpdateController.cs:2122-2146` (`isMissilePacket`);
refusal: `RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition:930-936`
(`record.Projectile is not RuntimeProjectile projectile``return null`).
`isMissilePacket` keys purely on the canonical record's Missile bit. It does not
require that a projectile component exists. The old short-circuit did: the
deleted `ProjectileController.ApplyAuthoritativePosition` returned `false` when
`TryGetCurrent` found no bound `RuntimeProjectile`, and `== true` failing meant
the packet **fell through** to the generic remote tail.
`record.Projectile` has exactly one producer: `ProjectileController.TryBind`
`LiveEntityRuntime.BindProjectileRuntime``RuntimePhysicsState.BindProjectile`
(verified by grep — no other writer of `Entities.SetProjectile(record, component)`).
`TryBind` has several ordinary, non-exceptional failure returns, e.g.
`ProjectileController.cs:160-166`:
```
"Missile 0x… Setup 0x… does not have the supported retail one-sphere collision shape."
```
Scenario: ACE spawns a missile whose Setup does not reduce to the supported
one-sphere shape (a diagnosed, expected case — it has its own log line).
`TryBind` returns false; `record.Projectile` stays null; `FinalPhysicsState &
Missile` stays set. Every subsequent accepted Position for that guid now returns
at `:2146` having done nothing at all — no `TryApplyGenericRemoteRenderPose`, no
`RebucketLiveEntity`, no `GetOrCreateRemoteMotionRuntime`. The object freezes at
its create pose for the rest of its life and never follows the server. Before
this diff it tracked, via the remote tail.
The contract's D-P6 does pin "dispatch the projectile arm and RETURN", so this is
a defect in the design as implemented rather than a slip against it — but it is
still a new, silent, permanent freeze, and the same defect class (`the frozen
entity`) that the 4b-family reviews have been hunting. Reachability caveat,
stated honestly: ACE never sends a missile `UpdatePosition`
(`WorldObject_Tick.cs:333-334`), so like every other half of this route it is
test-reachable only.
**Fix direction:** make the discriminator conjunctive — a packet takes the
projectile arm only when the arm can actually own it (Missile bit **and** a
bound projectile whose `Body` is the canonical `PhysicsBody`); otherwise let it
take the ordinary tail, which is what the record's shape actually is. If the
swallow is intended, it needs its own register row and a test that pins it, and
neither exists.
---
### A3 — D-P4's teleport-hook reduction is unwired for the adopted-body case, which the codebase explicitly supports
**Severity: MAJOR (contract obligation unmet; concrete scenario).**
`RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition:944`
runs exactly one of retail's six `teleport_hook` @0x00514ED0 actions:
```csharp
case RuntimeAuthoritativePositionDisposition.SetPosition:
_entityObjects.Physics.CollisionReports.LeaveWorld(record);
```
The contract pins more than that: *"The other five actions execute iff their
owning component exists — for the adopted-body case (a missile that also carries
a `RemoteMotion`), the existing hook actions' per-manager guards already express
retail's shape; the implementer wires this without per-packet closures"* (D-P4).
Nothing wires them, and no note records the omission.
The adopted-body case is not hypothetical. `ProjectileController.TryBind`'s
shared-body branch exists precisely for it — *"If a non-missile incarnation
already created its MovementManager or entered the Physics-Static animation
workset, classification adopts that same body"* — and
`RuntimePhysicsState.BindProjectile:757-830` does not exclude a record that has
a `RemoteMotion`. `RuntimePhysicsState.cs:697-708` shows the same key can sit in
both `_spatialRemotes` and `_spatialProjectiles`.
Scenario: an object is a live remote with a populated `RemoteMotion.Interp`
queue and a `ConstrainTo` leash armed by route 4a's
`RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation`. ACE then sets
the Missile bit (a State packet); `TryBind` adopts the shared body. The next
teleport/cell-less Position takes the projectile arm: collisions are force-ended
and the body is teleported, but `StopInterpolating`, `UnConstrain`,
`CancelMoveTo`, and `UnStick` never run. The `RemoteMotion` survives in the
remote workset with a stale waypoint and a live leash anchored at the
pre-teleport position, and drags the newly-placed missile body back. Retail
@0x00514EFD/@0x00514F31 runs all six, each guarded on its own manager, so
retail's own guards would no-op for a pure arrow and would fire here.
Note this is not a regression from HEAD — the old short-circuit did not run the
hook either — but D-P4 made wiring it an explicit obligation of this slice, and
the faithful port (`RemoteTeleportHook.Execute` + `RemoteTeleportHookActions`,
`src/AcDream.App/Physics/RemoteTeleportHook.cs`) is sitting in-tree unused for
this arm. That is exactly the 4b-3 round-2 R2 defect class the contract cited by
name.
**Fix direction:** drive the projectile teleport arm through the same six-action
bundle (cached delegates, #315 pattern), letting each action's per-component
guard decide — for an ordinary arrow all five extra actions are no-ops, and the
adopted-body case gets retail's shape for free.
---
### A4 — `SyncProjectilePresentation`, the highest-risk reproduced code in the diff, has no test that would fail if it were deleted
**Severity: MAJOR (test gap on the invariant the contract singles out).**
`RuntimeRemotePlacementDriveController.SyncProjectilePresentation:987-1039`.
Contract §5 invariant 2: *"Presentation still advances… the shadow registry is
synced (spatial+visible) or suspended (hidden/non-spatial) exactly per the
current tail's semantics. A projectile is never left rendered a packet behind
its body (#312's layer — **tests must assert it**)."*
Across all seven new Runtime tests, the only assertion that even touches this
method is `Assert.True(body.InWorld)` in
`ApplyAcceptedProjectilePosition_Refused_StillAdvancesPoseNoParkPredictionInvalidated`.
That assertion is vacuous with respect to the sync: the fixture's `AttachBody`
(`RuntimeRemotePlacementDriveControllerTests.cs:2860-2880`) calls
`body.SnapToCell(...)`, and `PhysicsBody.SnapToCell` sets `InWorld = true`
(`PhysicsBody.cs:201-205`). Nothing in the Refused path clears it. So the
assertion passes with `SyncProjectilePresentation` entirely removed.
Unasserted, at any layer: the shadow-registry publication
(`ShadowObjects.UpdatePosition`), the spatial+hidden `Suspend`, the non-spatial
`InWorld = false` + `Active` clear + `Suspend`, the `!body.InWorld` activation
edge, and the three currency guards at the method's head. The committed-outcome
tests (`…TeleportCommit…`, `…FarCommit…`) assert status, position, prediction
version, velocity, `RemoteMotion is null`, and collision-owner count — none of
which the sync produces.
**Fix direction:** one test per branch of the sync, keyed on the observable the
old tail produced: shadow row present at the resolved pose (spatial+visible),
`Suspend` called (spatial+hidden and non-spatial), `Active` cleared
(non-spatial). These are cheap; the fixture already registers shadows in
`SeedCollisionOwner`.
---
### A5 — no App-layer test exercises `OnPosition` with a missile packet at all; §7 items 8 and 9 are unwritten
**Severity: MAJOR (this is the scoping-gap answer).**
`grep -rn "Missile" tests/AcDream.App.Tests/Physics/LiveEntityNetwork*.cs`
returns **zero** hits. The only App file mentioning `Missile` at all is
`ProjectileControllerTests.cs`, which never calls `OnPosition`.
So none of the following is covered anywhere:
- the D-P1/D-P6 discriminator itself in its production call site;
- invariant 8's mutual exclusion (Missile-set ⇒ projectile arm and **no
`RemoteMotion` afterward**; Missile-clear ⇒ remote tail and no projectile
effect);
- the null-classification swallow (`earlyRemoteRoute is null` with the Missile
bit set) and its positive half (timestamps consumed, merge advanced);
- the "no early wire-pose write / no `RebucketLiveEntity`" pins;
- the presentation ack the App owns — i.e. #312's layer at the layer it renders.
Worse, the retired App test `MalformedFreshUpdates_DoNotPoisonCanonicalBodyOrPose`
justifies its Position half's deletion by pointing at *"LiveEntityNetworkUpdateController's
own 'invalid-payload swallow' test"*. **That test does not exist.** The
underlying claim (the shared `CanAcceptPositionPayload` gate rejects the payload
upstream) is correct and I verified it independently — but the cited successor
coverage is not there, so the assertion was retired against a coverage claim
that is false.
Both A1 and A2 live inside the ~35 lines the implementer argued were "thin glue
between two well-tested layers". That is the empirical refutation of the
argument.
---
## MINOR findings
### A6 — the `!body.InWorld` gate is read after the placement, where the deleted tail captured `wasInWorld` before it
`SyncProjectilePresentation:1012-1018` reads `body.InWorld` *after*
`TryExecuteAcceptedRemotePosition` has run. The canonical commit calls
`body.SnapToCell(...)` (`RuntimeSetPositionState.cs:4974`), which sets
`InWorld = true`. The deleted tail captured `bool wasInWorld = body.InWorld;`
**before** its `SnapToCell`. Consequence: on every committed outcome the
re-activation branch (`body.LastUpdateTime = clock` + `TransientState |= Active`)
is now dead — a projectile that had left the world and comes back through a
committed accepted Position is marked `InWorld` but never re-flagged `Active`,
and its legacy `LastUpdateTime` is not rebased. It self-heals on the next tick
because `RetailObjectActivityGate.Evaluate` re-sets the flag
(`RetailObjectActivityGate.cs:61-67`), which is the only reason this is MINOR
rather than a frozen body. Fix: capture `body.InWorld` before dispatching the
placement and pass it in.
### A7 — the spatial+hidden branch silently drops `body.LastUpdateTime = currentTime`
Deleted tail: `else if (spatial) { body.InWorld = true; body.LastUpdateTime = currentTime; Suspend(); }`.
New (`SyncProjectilePresentation:1031-1035`): the `LastUpdateTime` write is gone.
`ProjectileController.TryBind`'s equivalent branch documents why the write
exists — *"consume the hidden clock so UnHide cannot replay a time backlog"*.
Bounded by `TryBegin` refusing `Hidden` and by the `RetailObjectQuantumClock`
being canonical post-R6, but it was a deliberate write and its removal is
unremarked.
### A8 — the shadow publication is silently skipped when the Runtime world frame is unavailable
`SyncProjectilePresentation:1019-1030` wraps the shadow update in
`if (physics.TryGetWorldFrameOffset(...))` and does nothing on false. The deleted
`ShadowPositionSynchronizer.Sync` always published, using App's live centre, and
returned early only for `cellId == 0`. `TryGetWorldFrameOffset` additionally
returns false when `_worldFrameCenterLandblockId == 0`
(`RuntimePhysicsState.cs:613-632`). Two methods away,
`StoreAcceptedDestinationPose:1113-1119` treats exactly this case as
`ThrowIfWorldFrameUnreachable`#284's "a frame that can never arrive is
terminal, never silent" policy. The new code neither publishes nor escalates.
### A9 — the null-route fallback is not fenced off the local player
`LiveEntityNetworkUpdateController.cs:2122-2126`: when `earlyRemoteRoute` is
null, `isMissilePacket` falls back to a bare `FinalPhysicsState & Missile` test.
`earlyRemoteRoute` is *always* null for `update.Guid == _playerServerGuid`. A
local-player record that ever carried the Missile bit would therefore swallow
its own accepted Position and freeze the player. Currently impossible only
because ACE never sets Missile on a player — a one-token `update.Guid != _playerServerGuid &&`
would make it structurally impossible.
### A10 — `OwnsFarSnap`'s doc is now false in the kind dimension
`src/AcDream.Runtime/Physics/RuntimeRemoteFarSnapPosition.cs:78-79` still states
this predicate is *"a strict narrowing of `OwnsPlacement` to its far half"*.
After the widening it narrows only `OwnsPlacement`'s **remote** far half.
`RuntimeRemoteTeleportPosition.cs:33-35` hedges correctly ("`OwnsPlacement`'s
remote scope"); the far-snap doc does not. D-P3 asked for exactly this
correction where a comment would otherwise mislead (process rule 6).
### A11 — the cutover route ledger still lists the deleted methods as live routes
`docs/research/2026-08-02-cutover-route-inventory.md:666-694` still describes
`ProjectileController.ApplyAuthoritativePosition``RuntimeProjectilePhysicsUpdater.ApplyAuthoritativePosition`
as a live route with `SnapToCell` + `CommitProjectileCell`. D-P7 scoped the
re-point grep to `src/` and `docs/architecture/`, so this is outside the letter
of the obligation — but that file is the C4 route ledger this campaign reads
from, and it is now wrong about route 5.
---
## Verified — no finding
These were hunted and came back clean; recorded so the next round does not
re-litigate them.
- **P3 (`RuntimeSetPositionState` kind-agnosticism) is CORRECT, and I verified
it independently rather than accepting the argument.**
`TryBeginExclusiveAuthoredPlacement:1444-1472` returns `default` (invalid
token → `Contention`) whenever `_operations.ContainsKey(key)` or
`HasRetainedCompletion(key)`. Both constructors that stamp
`Kind = RemoteAuthoritative``ParkCollisionResidents`'s window-departure
park (`:3833`/`:3866`, which itself `continue`s past any record already in
`_operations`) and `CreateWithdrawalOperation` (`:5338`/`:5350`, reached only
from `Cancel` after `CancelCoreDeferred`) — install an operation into
`_operations` under the record's key. A projectile accepted Position arriving
while one is live cannot capture or be relabelled by it; it refuses with
`Contention`, which `StoresAcceptedDestination()` puts on the **storing** side,
so the pose still advances and no ledger column is mislabelled. The two
kind-conditional stage sites (`IsExactDormantLocalActivationCurrent:5445-5446`,
`IsDormantLocalActivationPrephaseCurrent:2554-2600`) additionally require
`InitialLogin`/`LocalAuthoritative` **and** `record.Projectile is null`, and
the family has zero production callers. Residual, benign and pre-existing: a
park operation created for a projectile record carries `Kind = RemoteAuthoritative`,
but it never reaches the drive controller's ledger.
- **The three ownership fences hold.** `OwnsPlacement` has exactly one
production reader — `TryExecuteAcceptedRemotePosition:640` (verified by grep
across `src/`; every other hit is a doc comment or a test). `OwnsFarSnap` and
`OwnsTeleportPlacement` keep their `RemoteAuthoritative` gate, and a
projectile route cannot reach `ApplyAcceptedRemoteFarSnap` /
`ApplyAcceptedRemoteTeleport` / `TryArmConstraintAfterOperation`: the App
returns before the remote tail whenever `isMissilePacket`, and
`isMissilePacket` is derived *from* `route.OperationKind` whenever a route
exists, so the two cannot disagree. The throwing guards stay unreachable, and
no `RemoteMotion` is ever created on this arm.
- **The remote route is unchanged.** Moving the `ClassifyRemoteAcceptedPosition`
call earlier hops only a pure-read guard block
(`TryGetRecord` / `ReferenceEquals` / `IsCurrentPositionAuthority`) with no
reentrancy in between; classification itself is side-effect-free. Zero remote
classifier tests changed expectation, which is the contract's own tripwire.
- **`StoreAcceptedDestinationPose` never writes a cell**, so on
`Refused`/`Contention`/`RejectedPreparation` the projectile body's
`CellPosition` stays at the source cell while `record.FullCellId` is the wire
cell. This is identical to the accepted remote behaviour (AP-138's residual)
and the quantum stepper takes the cell from `record.FullCellId` explicitly
(`RuntimeProjectilePhysicsUpdater.TryBegin`), so it is parity, not a new
defect. Recorded, not filed.
---
## Judgment on the four implementer claims
**Claim 1 — both `ApplyAuthoritativePosition` overloads and the Runtime updater's
copy deleted; nothing else called them; no behaviour lost.**
**Deletions and callers: VERIFIED.** No code reference survives anywhere in
`src/` or `tests/` (only comments, the contract, and the two research docs noted
in A11); Release build green with 0 warnings. **"No behaviour lost": PARTIALLY
FALSE.** Three deliberate behaviours went with them and only one is accounted
for: the origin-translated world-position finiteness check (genuinely redundant —
`worldPos` is a finite wire triple plus an integer landblock offset, so it cannot
be non-finite when `CanAcceptPositionPayload` passed); the `_lastFiniteGameTime`
rebase on a Position packet (App tick clock — unremarked, low impact); and the
`wasInWorld` activate edge plus the hidden-branch `LastUpdateTime` write (A6,
A7 — unremarked).
**Claim 2 — 7 call sites modified, 2 assertions retired as obsolete.**
**Mostly legitimate, with one false coverage claim.** The velocity assertions in
`FreshVectorAndPositionCorrectionsMutateSameBody` are genuinely obsolete under
D-P5 and have a real positive successor (`…TeleportCommit…` /
`…FarCommit…` assert `body.Velocity` bit-identical after a placement). The
`AuthoritativeMutation.Position` retirement has a genuine, load-bearing successor
I read and checked —
`ApplyAcceptedProjectilePosition_DuringOpenQuantum_CompleteAbortsAfterPredictionInvalidated`
opens a real quantum, runs the arm, and asserts `Complete` returns false with the
committed pose intact. The rewritten `SyncPresentation_ReentrantGuidReuseNeverTouchesTheReplacement`
is an improvement, not a dilution: it correctly notes that `RebucketLiveEntity`
self-suppresses its own guid's visibility callback and switches to a genuine
spatial edge. **But** the malformed-payload retirement in
`MalformedFreshUpdates_DoNotPoisonCanonicalBodyOrPose` cites a successor test in
`LiveEntityNetworkUpdateController` that does not exist (A5). The upstream gate
does cover the scenario; the citation does not.
**Claim 3 — `OwnsPlacement_FalseWhenOperationKindIsNotRemoteAuthoritative`
legitimately changed its expected outcome.**
**TRUE, not a test bent to fit the code.** The widening makes
`ProjectileAuthoritative` a positively-owned kind by construction, so leaving it
in a "these kinds are refused" list would assert the opposite of the design. The
positive case is separately and explicitly asserted
(`OwnsPlacement_TrueForProjectileAuthoritative_…`, including the Create
exclusion), and the two remaining negatives (`InitialLogin`,
`LocalAuthoritative`) still pin the predicate's kind dimension. The rename is
accurate.
**Claim 4 — the new tests use airborne destinations to remove a contact-response
confound.**
**Legitimate confound removal, with a named residual.** The claim is accurate:
the shared placement pipeline's contact response is retail landing behaviour,
shared verbatim with the remote arms, and outside route 5's scope; D-P5's pin is
specifically "no velocity *from the packet*", which an airborne destination
isolates cleanly. It is not a test dodging a defect. The residual worth stating:
**no test now places a projectile into ground contact at all**, so the
interaction between the projectile arm and contact response — including whether
the resulting velocity change is the right one for a ballistic body — is
entirely unexercised. That is a gap, not a dishonesty.
---
## Judgment on the known scoping gap (§7 item 8, the dual-kind `OnPosition` matrix)
**It is a FAIL-level gap. Definite answer: not acceptable.**
The implementer's two arguments are (a) the collapse's
`LiveEntityNetworkOnPositionCollapseMatrixTests` fixture unconditionally
constructs a `RemoteMotion` a projectile lacks, and (b) the D-P6 dispatch is
~35 lines of thin glue between two well-tested layers.
(a) is a fixture limitation, and a fixture limitation is a reason to extend the
fixture, not to ship zero coverage. The projectile half of the matrix needs a
record with the Missile bit, a bound projectile, and **no** `RemoteMotion`
which is precisely the assertion the matrix exists to make.
(b) is refuted by this diff itself. Two of the five MAJORs above (A1, A2) are
defects **inside those 35 lines**, and neither is visible from either
well-tested layer: A1 is an App/Runtime disagreement about which outcomes may
write presentation, and A2 is a discriminator whose reachable set is wider than
the arm that consumes it. This is the same shape as 4b-3's three MAJORs — a
mapping written against one caller's reachable set, and an invariant satisfied
on one arm only — and a dual-kind matrix was the structural fix there for the
same reason it is here.
Additionally, the gap is broader than §7 item 8 alone: item 9 (the App-level
invalid-payload swallow) is also unwritten, *and* was cited as existing coverage
in a retired assertion's justification.
The minimum this needs before landing: a dual-kind theory over `OnPosition`
covering at least far-commit, teleport-commit, near, airborne, and
null-classification, asserting on the projectile half that no `RemoteMotion`
exists afterward, no generic wire-pose write occurred, and the render entity's
position **and** `ParentCellId` agree with the resolved body — that last
assertion alone would have caught A1.

View file

@ -0,0 +1,804 @@
# C4 route 5 — projectile authoritative placement: pinned contract (2026-08-04)
**Scope:** make the post-residence accepted Position for a live projectile
canonical — classify it through the shared route classifier, execute the
placement dispositions through the one Runtime placement owner
(`RuntimeRemotePlacementDriveController` / `RuntimeSetPositionState`), and
delete the bespoke `ApplyAuthoritativePosition` authority that today
short-circuits a missile packet before the classifier ever sees it.
Pinned at HEAD **`30d3d114`**, clean tree, branch
`claude/acdream-physics-divergence-5aa784`. **Line numbers in this contract
are as-of `30d3d114` and WILL go stale; every citation also names the symbol —
trust the symbol** (process rule 6; line ranges went stale twice within single
review rounds during 4b-2/4b-3, and this route's own scoping doc is the third
demonstration).
Predecessor documents, binding where they still apply:
- [`2026-08-04-c4-route-5-scoping.md`](2026-08-04-c4-route-5-scoping.md) —
the research base. **Its file:line references predate the OnPosition
collapse (`edc911b0`) and the #315 fix (`aaf0811f`) and are stale; §10 of
this contract lists every claim the collapse invalidated or reshaped.** Its
retail research (§2), prediction analysis (§4), and trap list (§7) survive
and are folded in below, re-verified.
- [`2026-08-04-onposition-collapse-contract.md`](2026-08-04-onposition-collapse-contract.md)
— the unified remote tail this route dispatches AHEAD of (never into). Its
§7 non-goal ("Route 5 will add its arm against the collapsed single path —
that is the payoff, not the scope") is cashed out here.
- [`2026-08-04-c4-route-4b-3-contract.md`](2026-08-04-c4-route-4b-3-contract.md)
— its 13 "must REMAIN true" invariants and the D4 constraint-arm partition
still bind; §5 below extends the partition with the projectile column.
- The four 4b-3 review rounds
([retail](2026-08-04-c4-route-4b-3-retail-review.md) /
[round 2](2026-08-04-c4-route-4b-3-retail-review-round2.md) /
[architecture](2026-08-04-c4-route-4b-3-architecture-review.md) /
[round 2](2026-08-04-c4-route-4b-3-architecture-review-round2.md)) — the
recurring defect classes (a mapping written against one caller's reachable
set; an invariant satisfied on one arm only; a register row asserting
behaviour the code does not have; a mismapped retail action with the
faithful port already in-tree; tests that assert only negatives) are each
addressed by name below.
- [`2026-08-04-session-handoff-c4-remaining.md`](2026-08-04-session-handoff-c4-remaining.md)
— the six process rules apply verbatim.
- [`docs/plans/2026-08-02-placement-cutover.md`](../plans/2026-08-02-placement-cutover.md)
— C4 route list; route 5 is "projectile authoritative".
**Sequencing:** 4b-2 (`7f1c1f5a`), 4b-3 (landed, gate passed), the collapse
(`edc911b0`), and #315 (`aaf0811f`) are all in. The scoping's "route 5 lands
after 4b-2, preferably after 4b-3" constraint is satisfied; route 5 is next.
---
## 0. Facts settled before this contract — do not re-litigate
1. **Route 5 is half shipped.** The Create half
(`RuntimeInitialCreateResidenceState.Begin` decides
`RuntimePositionEntityKind.Projectile` from
`record.FinalPhysicsState & PhysicsStateFlags.Missile`, `:591-595`) and
the residence-window Position half
(`RuntimeInitialCreateContinuationExecutor.ApplyPositionAction:1874`,
kind recovered via `EntityKindOf:2571-2581`, one shared
`RuntimeAcceptedPositionRouteRequests.Build` at `:1914-1926`) are
canonical. What remains is exactly one thing: the accepted Position
arriving AFTER the residence window, short-circuited before the
classifier.
2. **NO LIVE GATE IS POSSIBLE.** ACE never sends `UpdatePosition` for a
missile. Re-verified at HEAD:
`references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Tick.cs:333-334`
is `/*if (PhysicsObj.IsGrounded) SendUpdatePosition();*/`, inside the
branch gated on `(PhysicsObj.State & PhysicsState.Missile) != 0` at
`:265`. Projectile classes broadcast `GameMessageVectorUpdate` and
`GameMessageSetState` on impact — never a Position. **This slice is
test-gated only** (§8). No connected gate is invented; if the implementer
finds evidence contradicting this, STOP and report — it changes the
slice.
3. Route 5 had to land after 4b-2 because it widens
`RuntimeRemotePlacementDriveController.OwnsPlacement`. Done; see §3.
---
## 1. Site inventory — re-located at `30d3d114`
Every site verified by reading at HEAD, not inherited from the scoping.
### 1.1 The duplicate authority to delete (the same three sites; new lines)
| # | site (symbol) | at HEAD | what it is |
|---|---|---|---|
| D1 | `LiveEntityNetworkUpdateController.OnPosition` — the projectile short-circuit (`_projectileController?.ApplyAuthoritativePosition(...) == true → return`) | `:2104-2125` (was `:1428-1448`) | Sits after `_movementTruthDiagnostics.OnServerEcho` (`:2103`) and BEFORE the remote classification (`earlyRemoteRoute`, `:2150-2158`), `TryApplyGenericRemoteRenderPose` (`:2160`), `RebucketLiveEntity` (`:2190`), and the whole `update.Guid != _playerServerGuid` RemoteMotion tail (`:2234` on). Carries the fabricated `acceptedSpawn.Physics?.Velocity ?? System.Numerics.Vector3.Zero` at `:2119-2120` (was `:1442-1443`). |
| D2 | `ProjectileController.ApplyAuthoritativePosition` (two overloads + doc) | `:492-590` (unchanged) | Validation (the `return true` swallow on invalid payload at `:551-554`), `ExternalOwnerValid` closure, render-pose acknowledgement (`:583-586`), delegation to the Runtime updater. |
| D3 | `RuntimeProjectilePhysicsUpdater.ApplyAuthoritativePosition` | `:301-424` (unchanged) | The real authority: `InvalidatePrediction` `:342`, `body.Orientation` `:345`, `body.SnapToCell` `:346`, `body.State = record.FinalPhysicsState` `:347`, the velocity commit `:348-358`, `CommitProjectileCell` `:370-381`, the presentation acknowledgement `:387`, the spatial/hidden `InWorld`/`Activate`/shadow-sync/suspend tail `:390-422`. |
Raw total 244 lines; roughly 180 non-comment — the scoping's figure holds.
### 1.2 The Runtime surfaces route 5 builds against
| site (symbol) | at HEAD | relevance |
|---|---|---|
| `RuntimeAuthoritativePositionRouteClassifier``RuntimePositionEntityKind.Projectile` | `:13`; `ValidEntityKind:535-538`; `OperationKind` switch `:564-575` (Projectile → `ProjectileAuthoritative` at `:572-573`) | The kind's ONLY behavioural effect in the classifier is the `OperationKind` mapping. `ClassifyAcceptedPosition` (`:308-478`): the single kind branch is `LocalPlayer` (`:349`); Projectile and Remote are byte-identical in disposition, flags, `StopInterpolating`, `TeleportHookPhase`, and `ConstrainPhase` — teleport/cell-less `:404-421`, `NoPositionOperation` `:430-448`, near-`Interpolate`/far-`SetPositionSimple` `:459-477`. Pinned by `RuntimeAuthoritativePositionRouteClassifierTests.ProjectilePosition_UsesRemoteMoveOrTeleportClassification` (`tests:418-436`). |
| `RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition` | `:617-659`; **hardcodes `RuntimePositionEntityKind.Remote` at `:642`** (was `:622`) | The one production remote-Position classification entry; feeds D1's `PreMergeCommittedCellId` (`:634`). Trap T4's site. |
| `RuntimeRemotePlacementDriveController.OwnsPlacement` | `:520-524` (was `:274-278`) | `OperationKind is RemoteAuthoritative && Disposition is SetPosition or SetPositionSimple && Teleport flag`**excludes `ProjectileAuthoritative`**. The widening target. |
| `RuntimeRemotePlacementDriveController.TryExecuteAcceptedRemotePosition` | `:619-667` | Kind-agnostic body: stale-pending self-heal, `CanAttemptDestination` pre-flight → `Refused`, `TryBeginExclusiveAuthoredPlacement(record, version, route.OperationKind)``SubmitAndResolve`. Gated only by `OwnsPlacement`. |
| `RuntimeRemotePlacementDriveController.ApplyAcceptedRemoteFarSnap` / `ApplyAcceptedRemoteTeleport` | `:760-784` / `:821-843` | **Both REQUIRE a `RemoteMotion` parameter and THROW unless `OwnsFarSnap` / `OwnsTeleportPlacement` own the route.** Not usable for projectiles — see §2. `StoreAcceptedDestinationPose` (`:845` on) writes the CANONICAL body and is reusable. |
| `RuntimeRemoteTeleportPosition.OwnsTeleportPlacement` | `:38-46` | **NEW since the scoping** (4b-3). Gates on `OperationKind: RemoteAuthoritative` — a projectile teleport-classified route is NOT owned. |
| `RuntimeRemoteFarSnapPosition.OwnsFarSnap` / `ResolveArm` | `:85-95` / `:137-148` | **NEW since the scoping** (4b-2). Same `RemoteAuthoritative` gate; a projectile far route would resolve `UnroutedCatchUp``ApplyInterpolate` against a RemoteMotion that does not exist. |
| `RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation` | `:240-259` | Requires `remote.Host` (an `EntityPhysicsHost`). A projectile has none. |
| `RuntimeSetPositionState.TryBeginExclusiveAuthoredPlacement` / `TryPrepareAndSubmitAuthoredPlacement` | `:1817-1861` region | `operationKind` is a pass-through parameter; the placement pipeline is kind-agnostic. **Caveat for the implementer:** two internal operation constructors stamp `Kind = RemoteAuthoritative` outright (`:3833/:3866` — the window-departure park constructor; `:5338/:5350` — the legacy-direct withdrawal shape), and the direct-command gate at `:5445-5446` admits local kinds only. Proof obligation P3 (§6) covers these. |
| `RuntimePhysicsState.CommitProjectileCell` | `:1376-1397` | NOT a bypass — routes into the shared `CommitCanonicalCell` with an exact-owner check including `PredictionAuthorityVersion`. The 2026-08-02 inventory's "ad hoc" label remains wrong (scoping already corrected this; re-verified). |
| `RuntimeProjectile` | `RuntimeProjectile.cs` (whole file) | `{ Body, CollisionSphere, PredictionAuthorityVersion, InvalidatePrediction() }`. No Host, no PositionManager, no InterpolationManager, no MovementManager. `RuntimeEntityRecord.Projectile` is the canonical slot (J5.6). |
| `RuntimeProjectilePhysicsUpdater` — quantum + Vector/State channels | `TryBegin`/`Complete` `:37-205`; `ApplyAuthoritativeVector` `:207-250`; `ApplyAuthoritativeState` `:252-299` | Out of scope (invariants 4/5). `InvalidatePrediction` is bumped at exactly `:241`, `:272`, `:342` — the sole cancellation mechanism for a split quantum (trap T3). |
| First-entry admission | `RuntimeRemoteFirstEntryState.cs:311-314` | Admits `RemoteAuthoritative or ProjectileAuthoritative`. Untouched. |
| Continuation-executor non-SetPosition gap | `RuntimeInitialCreateContinuationExecutor.cs:2019-2026` | `Interpolate`/`NoPositionOperation`/`AwaitFreshPosition` → typed trace + return ("Binding to the live interpolation owner is cutover work"). §3 D-P4 resolves the projectile half of that gap (no-op by policy); the REMOTE half stays open — do not touch. |
### 1.3 App-side context (dispatch site + presentation)
| site (symbol) | at HEAD | relevance |
|---|---|---|
| `OnPosition` shared prologue | `:1891-2103` | Authority gate + merge (`TryAcceptPosition`, `:1902`) — so `AcceptedPhysicsTimestamps.PreMergeCommittedCellId` is already measured for missile packets; hydration recovery; `EnsureWorldOrigin`; world-pos translation; `MarkLiveOwnerPoseDirty` (`:2068`); `OnServerEcho` (`:2103`). All runs for missiles today and keeps running. |
| `ProjectileController.CanAcceptPositionPayload` | `:102-126`, called unconditionally at `:1898-1901` (was `:1205-1208`) | The SHARED admission validator (returns true for non-projectiles); an invalid missile payload already fails the authority gate before the short-circuit. NOT a deletion target. |
| Unified remote tail (post-collapse) | `:2234-2810` | One guid-blind path; two named guid survivors (TS-44 sticky `:2537-2546`; the #316-preserved player `AirborneSnap` interp-clear/shadow-skip `:2581-2632`, `:2795-2809`). **Route 5 must not touch any of it.** |
| `RunRemoteArmTail` / cached #315 callbacks | `:1427-1463`, `_remoteArmCallbacks` backing methods `:1474-1492` | The allocation pattern to follow if the projectile arm needs any callback: cached delegates + scratch fields, never per-packet closures. |
| `TryApplyGenericRemoteRenderPose` | `:1010-1025`, called `:2160` | Gate is `OwnsSteadyState`. The projectile arm dispatches BEFORE this call (§3 D-P6) — a missile packet takes no early wire-pose write today and must not start taking one. |
| `ProjectileController.Tick` / `AdvanceQuantum` / `TryBeginQuantum` / `CompleteQuantum` / `IsCurrentQuantumIdentity` | `:640-766` / `:773-782` / `:784` / `:822-848` / `:850-859` | The per-quantum owner. Untouched. |
| `LiveEntityAnimationScheduler` split quantum | `src/AcDream.App/Rendering/LiveEntityAnimationScheduler.cs:404-426` | `TryBeginQuantum` `:405-409``_animationHooks.Capture``CompleteQuantum` `:419-423`. The straddle window trap T3 protects. Untouched. |
| `ProjectileController.TryBind` + create branch | `:133-330`; late-classification path `ApplyAuthoritativeState → TryBind` `:473-489`; projection-visible retry in `OnProjectionVisibilityChanged` `:860-897` | Still the live fallback for `initialResidenceActive == false` (`DatLiveEntityProjectionMaterializer.cs:848-865` — note: `src/AcDream.App/Rendering/`, not `World/`) and the mid-life Missile-bit flip. **NOT deleted here** (trap T8; C5's deletion pass owns it). |
| `ProjectileController.HandlesMovement` | `:599-602` | The Missile-bit ownership predicate the kind discriminator must agree with (§3 D-P1). |
---
## 2. How the OnPosition collapse changed route 5's shape
The scoping assumed route 5 would rewrite call sites inside two parallel
player/NPC copies of the remote tail. The collapse (`edc911b0`) replaced them
with **one unified, RemoteMotion-shaped path** plus two surviving named
guid-conditionals. Consequences, stated precisely:
1. **Route 5 touches the unified tail NOT AT ALL.** The projectile arm
dispatches from `OnPosition` at (approximately) the current
short-circuit's site — after the shared prologue, BEFORE
`TryApplyGenericRemoteRenderPose`, `RebucketLiveEntity`, and the
`update.Guid != _playerServerGuid` block — and returns. A missile packet
must never reach `GetOrCreateRemoteMotionRuntime`,
`SeedRemoteSpawnPlacement`, `TryCommitAuthoritativeVelocity`, the sticky
gate, `RunRemoteArmTail`, the arming site, the wire-cell adopt, or the
remote entity-sync/shadow tail. The entire tail assumes a `RemoteMotion`;
a projectile has none, and creating one is exactly the "second body or
interpolation owner" the current short-circuit's comment exists to
prevent.
2. **The "two copies to widen" work item is gone.** The scoping's §8 line
item "App call-site rewrite in OnPosition … 40-70 lines" shrinks to ONE
dispatch site. This is the collapse's payoff for route 5.
3. **Three ownership predicates now fence projectiles out, not one.** The
scoping knew only `OwnsPlacement` (§5.2). Since then 4b-2/4b-3 added
`OwnsFarSnap` and `OwnsTeleportPlacement`, both gated on
`OperationKind: RemoteAuthoritative`, and both remote arm methods
(`ApplyAcceptedRemoteFarSnap` / `ApplyAcceptedRemoteTeleport`) THROW on a
non-owned route and REQUIRE a `RemoteMotion`. Consequence: **only
`OwnsPlacement` is widened; the other two predicates and the two remote
arm methods are deliberately NOT widened and NOT reused** — the
projectile arm is a sibling seam over the shared
`TryExecuteAcceptedRemotePosition` + `StoreAcceptedDestinationPose` core
(§3 D-P2). The bright side: a mis-routed projectile packet now fails
loudly (throwing guards) instead of silently taking a RemoteMotion arm.
4. **The two surviving guid-conditionals are off-limits.** TS-44's sticky
gate and the #316-preserved player `AirborneSnap`
interp-clear/shadow-publish skip are named, contract-protected
asymmetries of the collapse. Route 5 has no business near either.
5. **#315's callback pattern binds new code.** The collapse's second commit
cached the remote-arm callbacks; the projectile arm must not reintroduce
per-packet closures on the packet path (the projectile hook reduction in
D-P4 needs at most a method-group/cached delegate).
---
## 3. Design decisions — pinned, not open for redesign
### D-P1 — one per-packet kind discriminator, derived where the classification already runs (resolves trap T4)
`RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition:642` stops
hardcoding `RuntimePositionEntityKind.Remote` and derives the kind from the
canonical record:
```
(canonical.FinalPhysicsState & PhysicsStateFlags.Missile) != 0
? RuntimePositionEntityKind.Projectile
: RuntimePositionEntityKind.Remote
```
This is the SAME data-driven predicate the Create half uses
(`RuntimeInitialCreateResidenceState.cs:591-595`), the same one
`EntityKindOf` recovers from the lease, and the same bit
`ProjectileController.HandlesMovement` keys movement ownership on —
data-driven, never display-name-gated (feedback_retail_dispatch_is_data_driven).
The App dispatch in `OnPosition` branches on the SAME underlying fact:
pinned, the App reads `route.OperationKind is ProjectileAuthoritative` off
the returned route wherever a route exists, and for the null-classification
case reads one `FinalPhysicsState & Missile` test on the same canonical
record in the same packet — so the classifier's kind and the App's dispatch
cannot disagree (a mis-tag is otherwise silent, because the dispositions are
identical for the two kinds; only the ledger/owner would be wrong).
The mid-life bit flip is thereby handled by construction: ACE clears Missile
on impact (a State packet), and `ApplyAuthoritativeState → TryBind` is where
an ordinary object becomes a missile. Whichever `FinalPhysicsState` the
packet's merge left on the canonical record decides the packet's arm; the
adopted-body case (a missile that also carries a `RemoteMotion`) follows the
Missile bit exactly as `HandlesMovement` already does, and the untouched
`RemoteMotion` is simply not written by that packet.
Route-1 semantics are untouched: the residence/continuation callers already
pass an explicit kind; only the remote PositionEvent entry changes its kind
input. The classifier itself does not change at all.
### D-P2 — the projectile execution seam: one controller, shared core, no RemoteMotion
New Runtime seam on the EXISTING `RuntimeRemotePlacementDriveController`
(never a sibling controller, never a second pending map — trap T9), shape at
the implementer's discretion but pinned in behaviour:
`ApplyAcceptedProjectilePosition(RuntimeEntityRecord record, in RuntimeAuthoritativePositionRoute route)`
(reads `record.Projectile` internally), which:
1. **Validates ownership** — the route's `OperationKind` is
`ProjectileAuthoritative`; `record.Projectile` and `record.PhysicsBody`
exist and agree (`ReferenceEquals(record.PhysicsBody, projectile.Body)`
the J5.6 canonical-body identity). Anything else: return
`NotApplicable`-equivalent, write nothing.
2. **Dispatches on disposition:**
- `SetPosition` (teleport / cell-less): run the projectile hook reduction
(D-P4) BEFORE the placement, then `InvalidatePrediction()`, then
`TryExecuteAcceptedRemotePosition(record, route)` (now
`OwnsPlacement`-admitted, D-P3), then `StoreAcceptedDestinationPose` on
the `StoresAcceptedDestination` partition — the partition itself is
extended UNCHANGED (4b-3 invariant 1; do not re-litigate which statuses
store).
- `SetPositionSimple` (far): `InvalidatePrediction()`
`TryExecuteAcceptedRemotePosition` → store on the same partition. No
interp clear (no queue exists; retail's own far-branch
`StopInterpolating` is guarded on `position_manager != 0`
@0x005163C9-@0x005163CB and a never-interpolated missile has none, so
the skip is retail-faithful by consequence, not a divergence).
- `Interpolate` (near, in contact): **no-op by pinned policy** — see
D-P4. No body write, no prediction invalidation (nothing changed).
- `NoPositionOperation` (airborne): no-op — retail's `return 0`
@0x0051636D, faithful. No prediction invalidation.
3. **Invalidates prediction before ANY body write on this route** — the
placement path and the store fallback both clobber the body retail-side
of an in-flight split quantum (trap T3). One `InvalidatePrediction()` at
the seam's placement dispatch (before Begin) covers both, mirroring
today's `:342`-before-`SnapToCell` ordering. The no-op dispositions do
NOT invalidate: the body is untouched, so a straddling quantum completing
is correct, and the Vector/State channels' invalidate-on-write semantics
stay coherent.
4. **Commits NO velocity** (D-P5).
5. **Arms NO constraint leash** (D-P4).
The post-commit lifecycle tail — `InWorld`/`Activate`/shadow-sync on
spatial+visible, suspend on spatial+hidden, deactivate+suspend on
non-spatial (today's `RuntimeProjectilePhysicsUpdater.ApplyAuthoritativePosition:390-422`)
— is preserved semantically on the committed/stored outcomes. Whether it
lives in the new seam or stays a small retained method on the projectile
updater is the implementer's choice; pinned is that it remains
Runtime-owned (J5.6 boundary, invariant 6) and keyed on the same
spatial/hidden facts.
App keeps exactly what J5.6 assigns it: the presentation acknowledgement.
`OnPosition`'s projectile dispatch (D-P6) projects the committed/stored
snapshot into the render entity (`SetPosition`/`Rotation`/`ParentCellId`
from the RESOLVED body) and `_rootPoses.UpdateRoot` — the same ack contract
`ProjectileController.ApplyAuthoritativePosition:583-586` performs today.
No `RebucketLiveEntity` on this arm (the canonical placement/cell commit is
the cell authority, exactly as the per-quantum commit is today; adding the
remote tail's bucket transaction here would be a new second writer).
### D-P3 — the `OwnsPlacement` widening, and every consequence
`RuntimeRemotePlacementDriveController.OwnsPlacement:520-524` first clause
widens to
`route.OperationKind is RuntimeSetPositionOperationKind.RemoteAuthoritative or RuntimeSetPositionOperationKind.ProjectileAuthoritative`
(+ doc update). Consequences, each pinned:
- **`TryExecuteAcceptedRemotePosition` admits projectile routes.** The
Teleport-flag conjunct keeps excluding Creates (the first-entry conductor
owns every Create — unchanged; a projectile Create carries
`InitialCreateFlags`, an accepted Position carries `0x1012`).
- **Pending/acknowledgement ledgers are shared.** Projectile operations land
in the same `_pending` / `_awaitingAcknowledgement` maps; `DetachRoute`'s
cancel-then-clear teardown and `CountLivePending`'s self-heal cover them
with zero new code. No parallel map, no sibling controller (T9).
- **Parks/refusals behave exactly as the remote arms'** (4b-3 invariant 3
extended): `Refused` (pre-flight) and `Contention`/`RejectedPreparation`
store the accepted destination; `DeferredCell` is cancelled synchronously
with `restoreCancelledPark: true` — no park survives the controller;
`RejectedByPlacement` does not store. The projectile body stays `InWorld`,
clock active, spatially projected on every non-commit outcome.
- **`OwnsFarSnap` and `OwnsTeleportPlacement` are NOT widened** (§2 item 3).
Their throwing guards and the two RemoteMotion-shaped arm methods are
untouched. State this in their docs only if a comment would otherwise
mislead (process rule 6).
- **The constraint arm:** see D-P4. The classifier's projectile routes carry
`ConstrainPhase.AfterPositionOperation` (kind-blind, `:417`/`:473`); the
projectile arm deliberately does not consume it. No change to the
classifier; the deviation is recorded (D-P7).
- **The interp queue:** nonexistent for projectiles;
`StopInterpolating: !nearby` on the far route is inert and
retail-faithfully so (D-P2).
- **`CommittedHostAcknowledgementPending`:** a committed placement can enter
`_awaitingAcknowledgement` (`:1062-1076`). Whether the graphical host's
projection-acknowledgement chain retires a `ProjectileAuthoritative`
commit identically is proof obligation P2 (§6) — verify, do not assume.
### D-P4 — retail's manager actions, reduced to what a missile has (resolves scoping §5.1; answers the constraint-arm question)
**Retail's mechanical answer, verified in the decomp (§4): YES, retail would
arm the leash for a missile.** `SmartBox::HandleReceivedPosition` @0x00453FD0
has NO kind test other than `arg2 != this->player` (@0x0045414D); every
nonzero `MoveOrTeleport` return reaches `ConstrainTo` @0x00454272; and
`CPhysicsObj::ConstrainTo` @0x00510520 calls `MakePositionManager`
(@0x00510523) — creating the manager on demand for any object, missile
included — after which `UpdateObjectInternal` ticks it
(`PositionManager::UseTime` @0x005159A9-@0x005159B3). Likewise retail's near
branch (`InterpolateTo` @0x005163AF, with `IsMovingTo` @0x0050EB10 returning
0 for a manager-less missile) would build interpolation machinery for it.
**acdream pins the divergence instead of the machinery.** Building an
`EntityPhysicsHost`/`PositionManager`/`InterpolationManager` chain for a
ballistic body — for a packet ACE never sends — is the route-5b split the
scoping warned against, and this contract REJECTS it. Pinned:
- The projectile arm **never arms `ConstrainTo`** — on any disposition, any
outcome.
- The near-`Interpolate` disposition is a **no-op** (no queue to feed, no
manager to create).
- Both are recorded in ONE new register row (D-P7) citing @0x00454272,
@0x00510523, @0x005163AF, and the ACE unreachability
(`WorldObject_Tick.cs:333-334`).
**The teleport hook, reduced.** Retail's teleport/cell-less branch runs
`teleport_hook` @0x00514ED0 before the placement — six actions, each guarded
on its manager existing. For a pure missile, five of six are structurally
absent (`MovementManager`/`PositionManager`×3/`TargetManager` are null →
retail no-ops through the guards). The sixth,
`report_collision_end(this, 1)` @0x00514F31@0x00514620 (force-end-all
with bidirectional notification), applies to ANY object with a collision
table. **Pinned: the projectile teleport/cell-less arm runs the faithful
force-end port — `RuntimeCollisionReportingState.LeaveWorld` (the exact seam
the 4b-3 R2 fix validated against @0x00514620, round-2 retail review §2) —
before the placement.** Omitting it would recreate R2's defect class with the
faithful port sitting unused in-tree. The other five actions execute iff
their owning component exists — 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). For the ordinary arrow/bolt they are
no-ops by the same guards retail uses.
**Null / `Rejected*` classifications for a missile packet: swallow.** Write
nothing, create nothing, return — never fall through to the remote tail
(preserving trap T5's semantics: today an unhandleable projectile packet is
swallowed, not re-routed). The remote `UnroutedCatchUp` policy is
RemoteMotion-shaped and does not apply. This is an acdream-only state
(retail rejects nothing here); it rides in the same register row.
### D-P5 — no velocity write from a Position packet (retires the zeroing defect)
Retail's `MoveOrTeleport` declares `arg5 = AC1Legacy::Vector3 const*` and
never references it in the decompiled body (@0x00516330-@0x00516438,
re-verified for this contract); `HandleReceivedPosition` threads its `arg6`
into it (@0x00454254) and does nothing else with it; `UnpackPositionEvent`
@0x004542C0 performs no `set_velocity`. Velocity authority for a missile is
the Vector channel (0xF74E / `ApplyAuthoritativeVector`), untouched here.
**Pinned: the projectile arm consumes no velocity from the Position packet.**
This deletes the `acceptedSpawn.Physics?.Velocity ?? Vector3.Zero`
fabrication (`:2119-2120`) and the updater's conditional commit
(`:348-358`), retiring the (never-filed) defect where a velocity-less
Position packet zeroes an in-flight missile's velocity. Because "textually
unreferenced" is strong-but-not-byte-confirmed (BN elision risk, scoping
§9.2), **the implementer byte-verifies 0x00516330-0x00516438 for any `arg5`
use before landing** (`tools/pdb-extract` PE byte-decode against the paired
`refs/acclient.exe` — the `reference_pe_byte_decode.md` recipe). If a use is
found: STOP and report — the velocity policy changes.
### D-P6 — the App dispatch site and ordering
In `OnPosition`, the D1 short-circuit is replaced in place by:
```
classify (kind-aware, D-P1) // one call, both kinds —
// replaces the :2150 remote-only call
if the packet is a missile packet (D-P1's discriminator):
dispatch the projectile arm (D-P2) and RETURN // before TryApplyGenericRemoteRenderPose,
// before RebucketLiveEntity,
// before the RemoteMotion tail
else: the unified remote tail runs UNCHANGED // earlyRemoteRoute now comes from
// the same classification call
```
Pinned facts of this shape:
- The projectile arm takes **no early wire-pose write**
(`TryApplyGenericRemoteRenderPose` is never reached — matching today,
where the short-circuit precedes it) and **no `RebucketLiveEntity`**.
Presentation advances from the RESOLVED body via the ack (D-P2), exactly
as the per-quantum path does.
- The shared prologue (authority gate/merge, hydration, world origin,
`MarkLiveOwnerPoseDirty`, `OnServerEcho`) runs for missiles exactly as
today — the dispatch sits at/after the current short-circuit's position.
- The classification for a REMOTE packet is byte-identical to today's
(`Remote` kind in → same route out); only its call site may merge with the
projectile classification. Zero remote-classifier test changes is the
regression tripwire (§8 stop condition).
### D-P7 — register and issue bookkeeping, in the implementation commit
- **ONE new AP row** (projectile accepted-Position divergences): (a)
near-`Interpolate` is a no-op where retail would `InterpolateTo` a
manager-on-demand missile (@0x005163AF, @0x00510523); (b) the
post-operation `ConstrainTo` @0x00454272 is never armed for a projectile
(no PositionManager machinery exists for a ballistic body); (c)
null/`Rejected*` missile packets are swallowed rather than caught up (no
RemoteMotion); with the shared context that ACE never emits the packet
(`WorldObject_Tick.cs:333-334` commented out inside the Missile branch) so
every half is test-gated. Note in the row that the far branch's
`StopInterpolating` skip is NOT part of the divergence — retail's own
`position_manager != 0` guard @0x005163C9 skips it for a never-interpolated
missile.
- **No row deletion:** the velocity-zeroing defect being retired was never
filed; record its retirement in the commit message (and this contract),
not the register.
- **Comment corrections (process rule 6):** the D1 site's comment block
("Missiles reconcile the same predicted PhysicsBody in place…",
`:2104-2108`) dies with the short-circuit; `RuntimeProjectilePhysicsUpdater`'s
class doc and any "Position correction" references are re-verified against
the new seam; `RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition`'s
doc gains the kind-derivation sentence. Grep for `ApplyAuthoritativePosition`
across `src/` and `docs/architecture/` and re-point every survivor.
- **ISSUES.md:** none closed by this slice (#315 already closed; #316 stays
OPEN and untouched).
---
## 4. Retail ground truth — verified in `acclient_2013_pseudo_c.txt` for this contract; verify again yourself
| claim | where verified | status |
|---|---|---|
| **Retail has NO projectile-specific branch anywhere in the accepted-Position chain.** `UnpackPositionEvent` @0x004542C0 resolves the object and calls `HandleReceivedPosition` @0x00454358 with no state/kind test; `HandleReceivedPosition` @0x00453FD0's only kind branch is `arg2 != this->player` @0x0045414D; `MoveOrTeleport` @0x00516330 tests TELEPORT_TS, `this_1->cell == 0` (the body's OWN cell, read at entry), `arg4`, and `player_distance` — never `state & Missile`. A missile takes the identical remote arm. | pseudo-C lines 92896-93054 (HandleReceivedPosition), 93055-93098 (UnpackPositionEvent), 284304-284366 (MoveOrTeleport); independently confirmed twice by the 4b-3 reviews | ✓ re-read |
| Teleport/cell-less branch: `teleport_hook` @0x005163EF`SetFlags(0x1012)` @0x00516414`SetPosition` @0x00516420`return 1` @0x00516438; decided before `arg4` is read @0x0051638E | same listing | ✓ |
| Near branch: `InterpolateTo(this, arg2, IsMovingTo(this))` @0x005163AF, `return 1` @0x005163BE; `IsMovingTo` @0x0050EB10 returns 0 without a `MovementManager` (a missile has none) | lines 276430-276440 | ✓ |
| Far branch: `StopInterpolating` ONLY if `position_manager != 0` @0x005163C9-@0x005163CB; `SetPositionSimple(this, arg2, 1)` @0x005163D9 (builds `0x1012`); `return 1` @0x005163E8 | same listing | ✓ |
| Airborne: `arg4 == 0``return 0` @0x0051636D — nothing written, and `ConstrainTo` skipped (it sits inside `if (MoveOrTeleport(...) != 0)` @0x00454254) | same | ✓ |
| **`ConstrainTo` @0x00510520 = `MakePositionManager` @0x00510523 then `PositionManager::ConstrainTo`** — retail creates the manager on demand for a missile; the single remote arming site @0x00454272 has no kind test. So retail WOULD arm a missile's leash on any nonzero return. | lines 278353-278364 | ✓ — the basis of D-P4's recorded divergence |
| `PositionManager::UseTime` ticks in `UpdateObjectInternal` @0x005159A9-@0x005159B3 for any object holding a manager — an armed missile leash would be live, not vestigial | lines 283611-283757 | ✓ |
| `player_distance` is maintained for missiles: `update_object` @0x00515D10 computes `player_vector` via `Position::get_offset` @0x00515D5B and stores `player_distance` @0x00515D95 for every active unparented in-cell object. (BN artifact: the decomp shows only `.x` feeding it @0x00515D7B — the standard x87-elision of a magnitude; do NOT "fix" acdream's Euclidean distance to `.x`.) | lines 283950-284055 | ✓ |
| `MoveOrTeleport`'s `arg5` (velocity) is textually unreferenced in the body; `HandleReceivedPosition` threads `arg6` into it and does nothing else with it; `UnpackPositionEvent` calls no `set_velocity` (the only `set_velocity` in the chain is the LOCAL player's zero @0x004541B4) | all three listings above | ✓ textual; **byte-verification required before landing** (D-P5) |
**What this means for the design — stated plainly:** retail's CLIENT
mechanically supports an authoritative missile Position (it is just the
generic remote path), but the packet does not exist against ACE, and the
manager machinery retail would lazily build for it does not exist in acdream
for a ballistic body. Route 5 therefore anchors the placement dispositions to
retail's generic remote path (faithful), and pins the manager-dependent
halves (near-interpolate, leash) as a recorded acdream divergence — **a
register row, not an invented retail justification** (D-P7). There is no
evidence of any dedicated retail missile-Position path to port; anyone
claiming otherwise must produce an address.
One reporting note, out of scope but found while verifying: the remote
prologue's comment at `LiveEntityNetworkUpdateController.cs:2296-2300`
("PositionPack::UnPack initializes an absent velocity to zero;
MoveOrTeleport installs that exact vector with set_velocity") is contradicted
by the decomp read above — `MoveOrTeleport` installs no velocity. That
comment justifies the REMOTE arm's `TryCommitAuthoritativeVelocity`, which is
4a-owned and untouched by this slice; flagged for a future 4a-family
correction, not acted on here.
---
## 5. What must REMAIN true (process rule 1 — for every path, including every refusal)
1. **The pose still advances on the storing partition.** A projectile
placement that never reached the engine (`Refused` / `Contention` /
`RejectedPreparation`) still commits the accepted destination via
`StoreAcceptedDestinationPose`; `Deferred` and `RejectedByPlacement` do
not. The partition is extended unchanged — not re-litigated.
2. **Presentation still advances.** On every committed/stored outcome the
render entity (`Position`/`Rotation`/`ParentCellId`) and the root pose
reflect the RESOLVED canonical body, and the shadow registry is synced
(spatial+visible) or suspended (hidden/non-spatial) exactly per the
current tail's semantics. A projectile is never left rendered a packet
behind its body (#312's layer — tests must assert it).
3. **The entity stays in-world on every non-commit outcome**: `body.InWorld`,
object clock active, `FullCellId` intact, spatial projection intact. No
park survives the controller.
4. **Prediction invalidation accompanies every body write on this route**
(placement AND store fallback), before the write; an in-flight split
quantum straddling the packet aborts at `Complete`. The no-op
dispositions invalidate nothing. The Vector/State channels' invalidation
sites (`:241`, `:272`) are untouched.
5. **Per-quantum integration is untouched** (trap T7): `TryBeginQuantum` /
`CompleteQuantum` / `AdvanceQuantum` / `CommitProjectileCell` and the
scheduler's split quantum never route through `RuntimeSetPositionState`,
and their allocation profile is unchanged (Slice I's 0 B/resolve
discipline; the placement budget is per accepted packet, not per
quantum).
6. **J5.6's ownership boundary does not regress**: the projectile component,
workset, stepper, and cell commit stay in Runtime; App supplies only DAT
shape resolution and retained projection/shadow/effect acknowledgements;
a rejected presentation acknowledgement invalidates the pending
prediction only — it never rolls back or redirects the canonical owner,
and never becomes collision authority.
7. **The unified remote tail is untouched** — including
`ApplyRemoteContactRouting`, `RunRemoteArmTail`, `ToConstraintArm`,
`TryAdoptWireCellAfterRouting`, `ApplyWireAirborneLeftoverBookkeeping`,
the TS-44 sticky gate, and the #316-preserved player `AirborneSnap`
asymmetry. A missile packet never creates or writes a `RemoteMotion`.
8. **Mutual exclusion is provable** (trap T4): one `FinalPhysicsState`
read per packet decides both the classifier's kind and the App's
dispatch; Missile-set ⇒ projectile arm, Missile-clear ⇒ remote tail;
asserted by the dual-kind theories (§7).
9. **Unhandleable missile packets are swallowed, with their bookkeeping
stated positively** (trap T5 + round-2 B1): an invalid payload still
fails the shared authority gate exactly as today
(`CanAcceptPositionPayload` unchanged); a null/`Rejected*` classification
writes nothing and falls through to NOTHING — and the test asserts what
DID happen (the gate consumed the timestamps; the merge advanced the
snapshot) as well as what did not.
10. **No velocity write from the Position packet**; an in-flight missile's
velocity survives a velocity-less packet bit-identically (the retired
zeroing defect's converse, asserted positively).
11. **Ledger convergence**: teardown, session reset, and generation change
converge `RemotePlacementDrivePendingCount` (both registrations) to zero
with projectile operations in flight — the same suite shape 4b-1 built,
driven through the projectile arm.
12. **`ParkCollisionResidents`'s overlap throw stays unreachable** — the
projectile arm adds packets to the same one-operation-per-key machinery
and opens no new operation shape; restate the argument (with 4b-1's B2
stall-not-throw caveat) in the implementation commit.
13. **The lost-cell reaper gains no production caller.**
14. **The Create half and residence-window half are untouched**: the
residence kind decision (`:591-595`), first-entry admission
(`:311-314`), the continuation executor (including its recorded
non-SetPosition trace gap at `:2019-2026` — the REMOTE half of that gap
stays open and untouched), the materializer's residence-gated `TryBind`
skip, and the projection-visible `TryBind` retry.
15. **`TryBind` and its create branch are NOT deleted** (trap T8) — still
the live fallback for `initialResidenceActive == false` and the mid-life
Missile flip; C5's deletion pass owns it. Record, don't touch.
16. **`OwnsFarSnap` / `OwnsTeleportPlacement` and the two RemoteMotion arm
methods are not widened, not reused, not weakened** — their throwing
guards are the loud failure mode protecting item 7.
**The D4 constraint-arm partition, extended (the projectile column is the
new content; the remote column is 4b-3's, unchanged and re-asserted):**
| disposition (wire contact) | retail return | remote arm | projectile arm |
|---|---|---|---|
| `SetPosition` (teleport/cell-less, any contact) | 1 | arms, every placement outcome | **never arms** (register row) — hook reduction runs (force-end), placement + store partition run |
| `SetPositionSimple` (far, grounded) | 1 | arms | **never arms** (row) — placement + store partition run |
| `Interpolate` (near, grounded) | 1 | arms | **never arms, no-op** (row) |
| `NoPositionOperation` (airborne) | 0 | no arm | no arm, no-op — **faithful, not a divergence** |
| null / `Rejected*` | n/a | `UnroutedCatchUp` arms (grounded) / D2 shape (airborne) | **swallow — no arm, no write** (row) |
---
## 6. Proof obligations (must prove, not assume; stated in the implementation commit)
- **P1 — invariant 12's unreachability argument**, restated with the B2
caveat.
- **P2 — the commit-acknowledgement path.** Verify what acknowledges/retires
a `ProjectileAuthoritative` commit that lands in
`_awaitingAcknowledgement` (`CommittedHostAcknowledgementPending`,
`RuntimeRemotePlacementDriveController:1062-1076`): either the shared
projection-acknowledgement chain is kind-agnostic (state where), or the
self-heal prune is the retiring mechanism (state that, and why it is
acceptable — or route the ack explicitly). Do not let a projectile commit
sit in a ledger dimension nothing retires.
- **P3 — `RuntimeSetPositionState` kind-agnosticism.** Walk the operation
lifecycle for a `ProjectileAuthoritative` operation: the two internal
constructors that stamp `Kind = RemoteAuthoritative` (`:3833/:3866`
window-departure park; `:5338/:5350` legacy-direct withdrawal) and any
kind-conditional stage logic (`:1530`, `:5445`). Establish that none can
capture, rewrite, or strand a projectile operation — or fix the stamp to
carry the originating kind if one can. (A park constructor relabeling a
projectile op as `RemoteAuthoritative` would silently move it between
ledger columns — the T4 mis-tag shape at the operation layer.)
- **P4 — the byte-decode of `MoveOrTeleport`'s `arg5`** (D-P5).
- **P5 — the force-end wiring** (D-P4): one assertion that a teleport/
cell-less projectile packet empties the owner's collision table (the same
cheap observable the 4b-3 round-2 review named for R2's regression risk).
---
## 7. Test plan
Rules: assert the layer that historically broke (presentation, prediction,
ledger — not only `InWorld`/clock); **assert positive facts, not only
negatives** (round-2 finding B1 — every "writes nothing" test also asserts
the two or three things that DID advance); every new test must fail against
a broken implementation (no source-text pins).
**The dual-KIND discipline** — this route's analog of the collapse
contract's dual-guid theories, because the discriminator here is the Missile
bit, not the guid range: every OnPosition-level scenario is a `[Theory]` run
twice against the same packet shape — once with `FinalPhysicsState` carrying
`Missile`, once without — asserting the projectile arm claimed one
(projectile facts advanced; NO `RemoteMotion` exists afterward) and the
remote tail claimed the other (`RemoteMotion` facts advanced; no projectile
seam effect). This is what makes a future silent mis-tag fail a test.
Focused Runtime tests (`tests/AcDream.Runtime.Tests`):
1. **Classification kind derivation** (D-P1): Missile-bit canonical record
classifies with `OperationKind == ProjectileAuthoritative` through the
production `ClassifyRemoteAcceptedPosition` entry; bit-clear classifies
`RemoteAuthoritative`; same packet, same record otherwise. Plus the
flip-mid-life pair (bit set after a State merge → next Position
classifies Projectile; bit cleared on impact → next Position classifies
Remote).
2. **`OwnsPlacement` widening**: projectile SetPosition/SetPositionSimple
routes admitted; projectile Create (`InitialCreateFlags`) still excluded;
remote routes unchanged (the existing OwnsPlacement expectations stay
green untouched).
3. **Per-disposition seam behaviour** (D-P2), one test per row of the §5
table's projectile column: teleport/cell-less commit (body at resolved
destination, cell committed canonically, collision table force-ended,
prediction version advanced, tail facts per invariant 2); far commit;
teleport/far refused (destination outside the service window → pose
STILL advances to the accepted destination, no park retained, `InWorld`,
prediction invalidated); `RejectedByPlacement` (pose does NOT move,
settled pose survives); near no-op (body bit-identical, prediction
version UNCHANGED — the positive fact that a straddling quantum may
complete); airborne no-op (same); null/`Rejected*` swallow (nothing
written; timestamps/merge advanced — positive).
4. **Prediction invalidation across a split quantum** (trap T3): begin a
quantum (`TryBeginQuantum`), apply an accepted far/teleport Position
through the new arm, then `CompleteQuantum` — the completion must abort
(return false / not overwrite the committed placement), and the committed
pose must survive. This is the test the scoping said is invisible if you
only read the classifier; it must exist before the old path is deleted.
5. **No-velocity invariant**: an in-flight missile with nonzero velocity
receives a velocity-less accepted far Position → body placed AND velocity
bit-identical (the zeroing defect's regression test, positive form).
6. **Ledger/teardown**: proof obligation-11 suite — reset/teardown with a
retained projectile preparation retry and with an awaiting-acknowledgement
commit in flight; both registrations converge to zero.
7. **Constraint never armed**: after every projectile disposition, no
`PositionManager`/constraint exists anywhere for the entity (and the D4
partition test for remotes is untouched).
App-layer tests (`tests/AcDream.App.Tests`):
8. **The dual-kind OnPosition matrix** (above) for at least: far commit,
teleport commit, near packet, airborne packet, null-classification
packet. Projectile half asserts: no `RemoteMotion` created, no generic
wire-pose write, no rebucket-driven bucket move beyond the canonical
commit, render entity == resolved body (#312's layer), root pose updated,
shadow synced/suspended per spatial state.
9. **The invalid-payload swallow** (T5): an invalid missile Position is
consumed (gate/validation) and the remote tail observably did not run —
asserted positively via the gate's timestamp state.
10. **Sabotage check (manual, once, before the commit is finalised)** — the
collapse contract's experiment, adapted: break the seam (skip the store
fallback, or skip `InvalidatePrediction`) and confirm the matrix fails
on the projectile half; break the kind derivation (hardcode Remote) and
confirm the dual-kind theories fail. If a sabotage survives, fix the
test, not the sabotage.
Existing tests: `ProjectilePosition_UsesRemoteMoveOrTeleportClassification`
survives (it pins the disposition identity, which is unchanged); the
`ProjectileController` position tests covering D2/D3's deleted bodies are
re-expressed against the seam (not deleted as collateral — each scenario
must map to a successor or be named as obsolete-with-reason in the commit).
---
## 8. Gates
- **Focused**: the §7 suites, green.
- **Complete Release suite**:
`$env:ACDREAM_PAK_PATH = "$env:USERPROFILE\Documents\Asheron's Call\acdream.pak"`,
`dotnet test AcDream.slnx -c Release -m:1`. **Baseline 11,036 passed / 4
skipped / 0 failed at `30d3d114`.** The count will rise; measure and
record the new figure — do not inherit the baseline. Two known flakes,
never chase and never conflate (they have been conflated twice): **#302**
(`PortalProjectionTests.ClipToRegion_FrameOwnedStore_…`, GC-allocation
assertion, App.Tests) and **#308** (`NakEmissionTests.LossSoak_…`,
wall-clock deadline, Core.Net.Tests, full-suite load only). If either
appears, re-run and say which.
- **NO CONNECTED GATE EXISTS, and none is invented.** ACE never sends
`UpdatePosition` for a missile (`WorldObject_Tick.cs:333-334`, commented
out inside the `PhysicsState.Missile` branch at `:265`; every live
`SendUpdatePosition` caller is a player/creature/pet/gamepiece/admin
path). The accepted-Position half of this route is unreachable in play
against ACE; deterministic tests are its ONLY gate, and this contract
records that explicitly so a green connected session is never presented
as evidence for it. **Optional, not a gate:** an ordinary bow/spell-bolt
smoke (arrow spawns at the launch point, flies a clean arc, vanishes on
impact, leaves no invisible collider at the impact point) exercises the
UNTOUCHED Create/Vector/State halves and is cheap insurance against a
wiring mistake in the deletion — record it as a no-regression observation
if run, never as coverage for the Position half.
---
## 9. Budget and stop conditions
**Budget** (scoping's figures re-validated against HEAD): ~244 raw lines
deleted across the three D-sites (~180 non-comment); added — seam 60-110,
kind derivation + builder plumbing 5-15, `OwnsPlacement` widening + docs
~10, App dispatch 25-50, hook-reduction wiring 10-25: **total added 110-210
non-comment production lines; net roughly flat-to-negative.** Tests are the
larger share, ~400-700 lines. The collapse SHRANK the App portion versus the
scoping's 40-70 (one dispatch site, not two).
**Stop and report rather than pushing through when:**
1. Added production lines exceed ~300, or the design starts needing a
`RemoteMotion`, `EntityPhysicsHost`, `PositionManager`, or
`InterpolationManager` for a projectile — that is the 5b split this
contract rejected; it does not get built ad hoc.
2. The unified remote tail, `ApplyRemoteContactRouting`, either surviving
guid-conditional, `OwnsFarSnap`/`OwnsTeleportPlacement`, or either
RemoteMotion arm method needs an edit beyond comment repointing.
3. Any remote-classification test changes expectation — the remote route
must be byte-identical.
4. Proof obligation P3 finds an operation-lifecycle site that can capture or
relabel a projectile operation.
5. P4's byte-decode finds a live `arg5` use in `MoveOrTeleport`.
6. Evidence appears that ACE (or the connected environment) DOES emit a
missile `UpdatePosition` — the gate section changes and the user decides.
7. The complete Release suite deviates from baseline beyond the two named
flakes.
---
## 10. What this slice does NOT do
- **AP-131** (shared merge call) — C5. **AP-135** — stays, untouched (its
sites are the remote tail's, which this slice does not enter).
- **#276** — untouched.
- **#316** — the player landing block's missing shadow publish: **OPEN,
unmeasured, and must not be "fixed" here** (it lives in the remote tail's
preserved asymmetry; measuring it first is its own issue's requirement).
- **`ProjectileController.TryBind` + create branch** — recorded, retained;
C5's deletion pass (trap T8).
- **The per-quantum stepper, Vector channel, State channel** — untouched
(invariants 4/5).
- **No PositionManager/interpolation machinery for projectiles** (the
rejected 5b) and no headless remote/projectile motion consumer.
- **No changes to routes 2/3/6/7**, the local-player paths, or the
continuation executor's remote trace gap.
- **The remote prologue's questionable velocity comment** (`:2296-2300`,
§4's reporting note) — reported, not edited; it belongs to a 4a-family
correction.
---
## 11. Contradictions and stale claims versus the scoping — reported, not smoothed
1. **Every scoping file:line in `LiveEntityNetworkUpdateController` is
stale** (the collapse rewrote the file): the short-circuit `:1428-1448`
`:2104-2125`; the fabricated velocity `:1442-1443``:2119-2120`; the
`CanAcceptPositionPayload` call `:1205-1208``:1898-1901`;
`OwnsPlacement` `:274-278``:520-524`; the lifetime's kind hardcode
`:622``:642`. §1 is the corrected inventory.
2. **The scoping's central structural assumption — that route 5 rewrites
call sites inside two parallel remote copies — is void.** There is one
unified tail, and route 5 does not enter it at all (§2). The scoping's §8
App-rewrite estimate shrinks accordingly.
3. **The scoping under-counted the ownership fences.** It named only
`OwnsPlacement` (§5.2); 4b-2/4b-3 added `OwnsFarSnap` and
`OwnsTeleportPlacement` (both `RemoteAuthoritative`-gated) and two
throwing, RemoteMotion-required arm methods. "The far and teleport
branches have no handler the instant the short-circuit is removed" is
still true, but the failure mode is now loud (throws/refusals), and the
fix is a sibling seam over the shared core — not merely the ~10-line
widening the scoping described.
4. **The scoping's §5.2 sequencing warning ("that file is route 4b-2's
active edit surface") is spent** — 4b-2/4b-3/collapse/#315 have all
landed; route 5 has the file to itself.
5. **The scoping's §1.1 claim that the classifier is "disposition-identical"
for Projectile and Remote remains true at HEAD** (re-verified against the
current classifier body and its pinning test) — the collapse changed the
App, not the classifier.
6. **The scoping's T5 swallow concern is narrower than written**: the
invalid-payload swallow largely happens UPSTREAM in the shared authority
gate (`CanAcceptPositionPayload` feeds `TryAcceptPosition`, which returns
before the short-circuit); D2/D3's internal `return true` swallows cover
only the origin-translated-world-position and secondary-velocity checks.
The invariant (no fall-through to the remote path) is pinned regardless
(§5 item 9); the mechanism the scoping described was partially stale even
at its own HEAD.
7. **New since the scoping and binding here:** #316 (filed by the collapse
contract; carried untouched), #315's cached-callback pattern (binds new
projectile wiring), and the collapse's two protected guid asymmetries.
8. **No contradiction found in the scoping's retail research** — every §2
claim re-verified (§4), including the ConstrainTo/MakePositionManager
chain, the arg5 non-use (still textual-only; byte check now mandatory),
and the ACE never-sends fact. The scoping's recommendation of §5.1
option 2 (record the divergence, build no machinery) is adopted and
pinned (D-P4).
9. **One adjacent-comment defect found while verifying** (not in the
scoping): the remote prologue's `:2296-2300` velocity justification
contradicts the decomp (§4 note). Out of scope; reported.

View file

@ -0,0 +1,418 @@
# 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<bool>` 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.

View file

@ -0,0 +1,282 @@
# C4 route 5 — projectile authoritative placement: retail-conformance review, ROUND 3 (narrow delta)
**Reviewer lens:** retail fidelity only. Delta against
[round 2](2026-08-04-c4-route-5-retail-review-round2.md) (FAIL: B1, B2), which
the architecture reviewer corroborated independently as its B2 and B1.
**Subject:** the uncommitted working tree, HEAD `30d3d114`.
---
## VERDICT: **PASS**, with one MUST-FIX-IN-COMMIT documentation correction (C1)
Both round-2 MAJORs are closed, and closed well. B1's revert is right *and* its
regression test is self-verifying rather than merely sabotage-checked. B2's
fix is placed in the correct layer, in the correct order, with the correct
guard.
The one required edit is a single sentence in AP-141's risk column that
asserts a consequence neither retail nor acdream has — **and I wrote the
mistake it was copied from.** Round 2's B2 claimed a stale leash would "drag
the freshly-placed missile back". That is wrong: acdream's `ConstraintManager`
port brakes, it never pulls. I retract it in §C1 and state what the residual
actually is. Because register rows land in the same commit as the behaviour
they describe, this is a must-fix now, not a follow-up.
---
## A. The four items you asked me to verify
### A1 — B1's revert: is the *reasoning* now right, not just the value?
**Yes. PASS on both.**
`ProjectileController.SyncPresentationFromResolvedBody` now writes
`entity.ParentCellId = record.FullCellId`. I re-derived each of the four
claims in the rewritten doc block rather than accepting them:
| claim in the comment | verified |
|---|---|
| on a committed outcome the two sources agree (`CommitCanonical` writes both) | ✓ — the choice is a genuine no-op there |
| `StoreAcceptedDestinationPose` composes `body.Position` from `accepted.PositionX + worldOffset(accepted.LandblockId)` — the **wire** cell's frame | ✓ read the method; it writes only `Position`/`Orientation`, offset from `accepted.LandblockId` |
| `record.FullCellId` is that same wire cell, stamped by the merge before classification | ✓ `RuntimeEntityRecord.RefreshDerivedState``SetFullCell(position.LandblockId, …)` at `:232-237` |
| the body's own `CellPosition.ObjCellId` is the **source** cell it left | ✓ untouched by the store fallback |
| cross-check (1): the sibling remote arm pairs wire position with wire cell | ✓ `TryApplyGenericRemoteRenderPose``entity.ParentCellId = landblockId` |
| cross-check (2): Runtime's `SyncProjectilePresentation` publishes the shadow at `record.FullCellId` in the same packet | ✓ — reading the body's cell here would disagree with the shadow for the same body in the same call |
| cross-check (3): retail's `store_position` @0x00515CE2 writes the whole `Position` **including `objcell_id`**, so after a retail store the object's cell IS the destination | ✓ |
The comment also correctly names the body's stale post-store cell as the
acdream-side residual (AP-138, shared with the remote arms) rather than
truth to project. The reasoning is sound and matches the code beside it.
### A2 — the indoor-staging test: does the discrimination argument hold, and is it legitimate?
**Yes to both. This is the strongest test in the change.**
I verified the mechanism at `PhysicsBody.SyncCellPositionDelta`
(`PhysicsBody.cs:286-306`), which runs whenever `Position` is written:
```csharp
if ((cell & 0xFFFFu) is not (>= 1u and <= 0x40u))
{
CellPosition = new Position(cell, new CellFrame(local, …)); // indoor: id PINNED
return;
}
uint adjusted = cell;
if (LandDefs.AdjustToOutside(ref adjusted, ref local)) // outdoor: id RE-DERIVED
CellPosition = new Position(adjusted, …);
```
So the implementer's account is exactly right: an **outdoor** source cell
(index 1..0x40) has its cell id re-derived — including the 192 m landblock
wrap — by `AdjustToOutside` as a side effect of the store's position write, so
`body.CellPosition.ObjCellId` converges toward the destination landblock on
its own and the two expressions stop discriminating. An **indoor** source cell
takes the early-return branch: the delta is carried into the local frame and
the id stays pinned. That is the only staging in which
`body.CellPosition.ObjCellId` and `record.FullCellId` provably differ across
the store path.
**Legitimate, not contrived.** `IndoorSourceCell = SourceLandblock | 0x0100u`
is the canonical first EnvCell in AC's cell-id encoding — outdoor landcells
occupy `0x0001``0x0040`, EnvCells start at `0x0100` — so this is an ordinary
dungeon cell, not a magic number chosen to break an assertion. A missile
in a dungeon is an ordinary scenario, and the branch it exercises is the one
production takes indoors.
**Better than the sabotage run the implementer also did:** the test asserts
the divergence *occurred* before asserting the outcome —
```csharp
Assert.Equal(IndoorSourceCell, body.CellPosition.ObjCellId); // the divergence is real
Assert.Equal(DestinationCell, fixture.Entity.ParentCellId); // and ParentCellId ignored it
```
— so if staging ever stops discriminating (a future change makes the store
write the cell, say), the test fails loudly instead of silently going vacuous.
That is a self-verifying discriminator, which is the right answer to "a green
suite is not evidence".
### A3 — B2's split: is "queue cleared, leash still armed" retail's far-branch behaviour?
**Two of the three halves are retail. The third is not, and it is now
recorded — so the outcome is acceptable, but the plain answer to your question
is: no, retail does not leave the leash as-is.**
Retail's far path, in order:
```
005163c1 position_manager = this_1->position_manager;
005163c9 if (position_manager != 0)
005163cb PositionManager::StopInterpolating(position_manager);
005163d9 CPhysicsObj::SetPositionSimple(this_1, arg2, 1);
005163e8 return 1;
↓ back in SmartBox::HandleReceivedPosition
00454254 if (MoveOrTeleport(...) != 0) {
00454258 GetMaxConstraintDistance / GetStartConstraintDistance
00454272 ConstrainTo(arg2, &arg2->m_position, start, max); ← re-anchor
}
```
| half | retail | acdream far arm | verdict |
|---|---|---|---|
| clear the interpolation queue | yes, whenever `position_manager != 0` @0x005163CB | `route.StopInterpolating && record.RemoteMotion is RemoteMotion → adopted.Interp.Clear()` | ✓ **correct** |
| `UnConstrain` | **no** — the far branch never calls `teleport_hook` | not run | ✓ **correct**; the test's "proving the far branch really does run only `StopInterpolating`, not the full hook" is right |
| re-anchor the leash | **yes**`&arg2->m_position` is the object's *just-updated* position, so retail re-anchors on every nonzero return | not run | ✗ **divergent** |
The fix's placement is right in every other respect: it lives in the Runtime
seam (not App), runs strictly **before** `TryExecuteAcceptedRemotePosition`
(mirroring @0x005163CB before @0x005163D9), uses the same
`route.StopInterpolating` gate and the same `Interp.Clear()` mapping the
sibling `ApplyAcceptedRemoteFarSnap` uses, and the storing partition still
runs afterwards so the 4b-2 "cleared-then-frozen" hazard cannot reappear.
`record.RemoteMotion is RemoteMotion` is the right analogue of retail's
`position_manager != 0` here, because `Interp` is a non-null field of
`RemoteMotion` — the same predicate the remote far arm relies on.
**What the missing re-anchor actually costs — correcting round 2.** See §C1.
It is one tick of brake-taper state, contact-gated, and cannot move the body.
### A4 — AP-141: does the row describe the shipped code exactly?
**Almost. The divergence description is now accurate; one sentence of the risk
column is not (C1).**
Verified accurate:
- the bare-missile far skip as *faithful by consequence* — retail's own
`position_manager != 0` guard @0x005163C9 skips it for a never-interpolated
object ✓;
- the adopted-body far clear as **ported**, matching the shipped
`route.StopInterpolating && record.RemoteMotion is RemoteMotion` arm ✓;
- clause (b) extended to say acdream never arms **or re-anchors** on any
disposition, with @0x00454272 cited ✓ — this is the honest recording of the
A3 residual;
- the "NARROWED … the far-branch clause was factually wrong for the
adopted-body case" preamble, which is the right way to retire a superseded
claim rather than quietly rewriting it ✓;
- clauses (a) and (c) unchanged and still accurate ✓.
### A5 — #317
**Discharges R8. PASS.** Filed OPEN with the byte-decode citation
(`MoveOrTeleport` @0x00516330-@0x00516438, every branch, velocity argument
never read), the correct scope note (the 4a call left in production
deliberately), a real root cause, and an acceptance criterion that names the
right next step — auditing the *whole* accepted-Position velocity chain rather
than just the one function. My round-2 judgment stands: deferring the audit
was always fine; what was missing was the tracking, and it now exists.
Nit only: the body cites the call site as "~line 2444" in one paragraph and
"~line 2420" in another. It names the symbol in both, which is what process
rule 6 says to trust, so this is cosmetic.
---
## B. Round-2 findings — delta status
| ID | status |
|---|---|
| B1 (`ParentCellId`) | **CLOSED** — reverted, reasoning verified, self-verifying indoor regression test |
| B2 (far-branch adopted body) | **CLOSED for the `StopInterpolating` half; the `ConstrainTo` re-anchor half is now a recorded divergence** in AP-141 clause (b) rather than an unrecorded one. Acceptable. |
| B3 (double `report_collision_end`) | still present (App hook action 6 + Runtime seam's `LeaveWorld`). Benign — the second call finds an empty table. Not re-raised. |
| B4 (per-packet closure) | **CLOSED**`_remoteArmCallbacks.IsCurrentProjectilePositionOwner` + `_projectileArmPosition*` scratch fields, the same cached shape #315 introduced for the remote arm. Comment now describes what the code does. |
| B5 (R8 tracking) | **CLOSED**#317 |
| B6 (retry-arm store gap) | unchanged; parity with the remote arm, pre-existing. Not re-raised. |
---
## C. Findings this round
### C1 — MINOR, **must fix in this commit** — AP-141's risk column asserts a consequence neither retail nor acdream has, and I am the source of the error
**Where:** `docs/architecture/retail-divergence-register.md`, AP-141, risk
column, final clause:
> "…but is never re-anchored at the new position by either — **if it survives
> un-cleared some other way, it would drag the body toward a stale anchor.**"
**This is wrong, and it is my round-2 wording.** I wrote that a leash anchored
at the pre-snap position would "drag the freshly-placed missile back". I
inferred it from retail's re-anchor existing, without reading acdream's port of
what a leash *does*. The in-tree port says otherwise, explicitly:
- `ConstraintManager.ConstraintPos` — *"+0x0c retail `constraint_pos` — the
leash anchor. Stored by `ConstrainTo`, **never read by `AdjustOffset`**
(retail + ACE — write-only in this class)."*
- `ConstraintManager.AdjustOffset` (retail `ConstraintManager::adjust_offset`
@0x00556180) only **brakes**: while `_host.InContact` it tapers the
already-composed per-tick offset between `ConstraintDistanceStart` and
`ConstraintDistanceMax`, or zeroes it past max — then unconditionally
overwrites `ConstraintPosOffset` with *that tick's step length*.
A leash therefore damps motion the interp/sticky chain already produced; it
has no mechanism to move anything toward the anchor. The dragging half of
round-2 B2 was the **interpolation queue**, which does move the body toward
waypoints — and that half is now fixed.
**What the missing re-anchor actually costs.** Retail's
`ConstrainTo(arg2, &arg2->m_position, …)` sets `ConstraintPos` to the object's
just-written position and re-initialises
`ConstraintPosOffset = Distance(anchor, host.Position)` = **0**, i.e. it
resets the brake accumulator at every accepted Position. acdream leaves
`ConstraintPosOffset` at the previous tick's step length. The observable
difference is confined to the single tick after the packet, only when the
object is `InContact`, and only if that step length already exceeded
`ConstraintDistanceStart` — and a far-snapped missile is airborne, where the
clamp branch does not run at all.
**Correct replacement for the sentence** (substance, not wording): *an
adopted-body missile's inherited leash is never re-anchored, so its brake
accumulator (`ConstraintPosOffset`) is not reset to zero at each accepted
Position as retail's @0x00454272 re-anchor does; the anchor itself is
write-only in both retail and the port, so a stale leash brakes rather than
pulls and cannot move the body.*
Keep the rest of the clause — "never re-anchored at the new position by
either" is the accurate divergence and should stay.
### C2 — MINOR (nit) — two comments the fix touched are slightly off
1. `ProjectileController.SyncPresentationFromResolvedBody`'s doc block uses
`<paramref name="record"/>` twice; the parameter is named `expectedRecord`
(`record` is a local from `TryGetCurrent`). The paramref will not resolve.
2. `MissileAdoptedBody_FarCommit_ClearsInterpQueueButLeavesConstraintArmed`'s
doc says the leash "must stay armed (proving the far branch really does run
only `StopInterpolating`, not the full hook)" — true and well-argued about
the *hook*, but silent on @0x00454272, which is the half retail does run.
One clause ("…armed but, unlike retail, not re-anchored — AP-141") keeps a
future reader from reading the far branch's leash handling as fully
faithful.
---
## D. Re-verified retail, no change
Spot-checked because the fix touched the surrounding code:
- `MoveOrTeleport` @0x00516330 far branch: `position_manager != 0` guard
@0x005163C9, `StopInterpolating` @0x005163CB, `SetPositionSimple(…, 1)`
@0x005163D9, `return 1` @0x005163E8 — the ported order is correct.
- `HandleReceivedPosition` @0x00454254/@0x00454272 — the single post-operation
arming site, no kind test, anchor is the object's own just-updated position.
- `teleport_hook` @0x00514ED0 — six actions, five per-manager guarded; the far
branch does not call it.
- `store_position` @0x00515CE2 — writes `objcell_id` with the frame (basis of
B1's revert).
- ACE never sends a missile `UpdatePosition`
(`WorldObject_Tick.cs:333-334` inside the `:265` Missile branch), so every
residual here remains test-reachable only. Stated for calibration, not as a
reason to ship anything.
- AP-131 / AP-135 untouched; #276 not closed; `docs/ISSUES.md` gains only #317.
---
## E. Summary
| # | Sev | Where | Action |
|---|---|---|---|
| C1 | MINOR, must-fix-in-commit | AP-141 risk column, final clause | replace the "drag the body toward a stale anchor" claim — the anchor is write-only; a leash brakes, never pulls. Retracts my own round-2 wording. |
| C2 | nit | `SyncPresentationFromResolvedBody` paramref; far-adopted test doc | one-line each |
No code changes required. With C1 corrected, route 5 is retail-conformant on
every path I have examined across three rounds.

View file

@ -0,0 +1,465 @@
# 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) |