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) |

View file

@ -93,6 +93,21 @@ internal sealed class LiveEntityNetworkUpdateController
private ulong _remoteArmPositionAuthorityVersion;
private AcDream.Core.World.WorldEntity? _remoteArmExpectedEntity;
/// <summary>
/// B4 fix (C4 route 5 round-2 architecture review): the same #315
/// cached-delegate discipline for the adopted-body missile arm's
/// teleport-hook currency check
/// (<see cref="RemoteArmCallbacks.IsCurrentProjectilePositionOwner"/>).
/// The missile dispatch block stamps these scratch fields immediately
/// before calling <see cref="RunRemoteTeleportHook"/> instead of
/// allocating a fresh <c>Func&lt;bool&gt;</c> closure every accepted
/// missile packet — the same defect class the remote arm's own #315
/// collapse already closed, flagged independently by both round-2
/// reviews for this arm.
/// </summary>
private LiveEntityRecord? _projectileArmPositionRecord;
private ulong _projectileArmPositionAuthorityVersion;
/// <summary>
/// #315: the two per-packet delegates cached ONCE (constructed here,
/// reused for every accepted remote Position) rather than allocated
@ -113,10 +128,22 @@ internal sealed class LiveEntityNetworkUpdateController
internal readonly Func<bool> IsCurrentPositionOwner;
internal readonly Func<bool> RunTeleportHook;
/// <summary>
/// B4 fix: the projectile (missile) arm's currency check, cached
/// the same way as the two remote-arm delegates above rather than
/// allocated fresh per accepted missile packet. Bound to
/// <see cref="IsCurrentProjectileArmPositionOwner"/>, which reads
/// the <c>_projectileArmPosition*</c> scratch fields the missile
/// dispatch block stamps immediately before use.
/// </summary>
internal readonly Func<bool> IsCurrentProjectilePositionOwner;
internal RemoteArmCallbacks(LiveEntityNetworkUpdateController owner)
{
IsCurrentPositionOwner = owner.IsCurrentRemoteArmPositionOwner;
RunTeleportHook = owner.RunCachedRemoteTeleportHook;
IsCurrentProjectilePositionOwner =
owner.IsCurrentProjectileArmPositionOwner;
}
}
@ -1478,6 +1505,25 @@ internal sealed class LiveEntityNetworkUpdateController
&& (_remoteArmExpectedEntity is null
|| ReferenceEquals(record.WorldEntity, _remoteArmExpectedEntity));
/// <summary>
/// B4 fix: the cached backing method for
/// <see cref="RemoteArmCallbacks.IsCurrentProjectilePositionOwner"/>.
/// Reads the <c>_projectileArmPosition*</c> scratch fields the missile
/// dispatch block in <c>OnPosition</c> stamps immediately before calling
/// <see cref="RunRemoteTeleportHook"/> for the adopted-body case. No
/// expected-entity check here — unlike the remote arm, the missile
/// dispatch block never captured one; parity with the remote arm's
/// extra guard is not required because the caller already re-validates
/// <c>ReferenceEquals(positionRecord.WorldEntity, entity)</c> earlier in
/// the SAME accepted-Position dispatch for the non-missile tail, and
/// the missile tail's own record is the SAME instance stamped here —
/// there is no second entity to disagree with.
/// </summary>
private bool IsCurrentProjectileArmPositionOwner() =>
_projectileArmPositionRecord is { } record
&& _liveEntities.IsCurrentPositionAuthority(
record, _projectileArmPositionAuthorityVersion);
/// <summary>
/// #315: the cached backing method for
/// <see cref="RemoteArmCallbacks.RunTeleportHook"/>. Only ever invoked on
@ -2102,27 +2148,113 @@ internal sealed class LiveEntityNetworkUpdateController
: new System.Numerics.Quaternion(p.RotationX, p.RotationY, p.RotationZ, p.RotationW);
_movementTruthDiagnostics.OnServerEcho(update, worldPos);
// Missiles reconcile the same predicted PhysicsBody in place. The
// timestamp gate above already rejected stale corrections; returning
// here prevents the generic remote locomotion path from allocating a
// second body or interpolation owner for the projectile.
if (_projectileController?.ApplyAuthoritativePosition(
acceptedPositionRecord,
acceptedPositionAuthorityVersion,
acceptedPositionVelocityAuthorityVersion,
worldPos,
new System.Numerics.Vector3(
p.PositionX,
p.PositionY,
p.PositionZ),
rot,
acceptedSpawn.Physics?.Velocity
?? System.Numerics.Vector3.Zero,
p.LandblockId,
_physicsScriptGameTime,
_origin.CenterX,
_origin.CenterY) == true)
// C4 route 5 (D-P1/D-P6, REVISED after the review round — A2/R1,
// A9): classify ONCE, kind-aware. This single call now decides both
// the remote route (unchanged for a non-missile packet — see the
// reuse below) AND whether this packet is a missile packet,
// replacing the former ApplyAuthoritativePosition short-circuit.
// The null-classification arm's test is the SAME conjunctive
// predicate the classifier itself applies
// (RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition,
// D-P1) — Missile bit AND a bound RuntimeProjectile whose Body is
// the canonical PhysicsBody, never the bit alone, so an unbindable
// or not-yet-bound missile takes the ordinary remote tail exactly
// as it did before this route (the deleted method's TryGetCurrent
// fall-through) — and explicitly fenced off the local player (A9):
// update.Guid == _playerServerGuid always takes the null branch
// below, and ACE never sets Missile on a player, but the fence
// makes that structurally true rather than incidentally true.
RuntimeAuthoritativePositionRoute? earlyRemoteRoute =
update.Guid != _playerServerGuid
? ClassifyRemoteAcceptedPosition(
update,
acceptedPositionCanonical,
timestampDisposition,
timestamps,
worldPos)
: null;
bool isMissilePacket = earlyRemoteRoute is { } classifiedRoute
? classifiedRoute.OperationKind
is RuntimeSetPositionOperationKind.ProjectileAuthoritative
: update.Guid != _playerServerGuid
&& (acceptedPositionCanonical.FinalPhysicsState
& AcDream.Core.Physics.PhysicsStateFlags.Missile) != 0
&& acceptedPositionCanonical.Projectile is { } boundProjectile
&& ReferenceEquals(
acceptedPositionCanonical.PhysicsBody,
boundProjectile.Body);
if (isMissilePacket)
{
// The projectile arm over the canonical Runtime placement owner
// (RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition)
// — never the generic remote locomotion path below, which would
// allocate a second body or interpolation owner for the
// projectile. A null classification (login-window shape) or a
// RejectedAuthority/RejectedData disposition is swallowed:
// nothing to route, the shared authority gate above already
// rejected an invalid payload.
if (earlyRemoteRoute is { } route)
{
// A3/R2 fix: retail's teleport_hook @0x00514ED0 runs five
// manager-guarded actions BEFORE the placement, in addition
// to the collision force-end the Runtime seam performs on
// its own (action 6). For a bare arrow/bolt (no RemoteMotion
// adopted) all five are structurally absent no-ops through
// retail's own per-manager guards — matching what the
// Runtime seam already does unaided. For the ADOPTED-BODY
// case (TryBind's shared-body branch: an ordinary remote
// whose Missile bit was set by a later State packet, so it
// still carries a live RemoteMotion with a populated Interp
// queue and possibly an armed ConstrainTo leash) those five
// actions are LIVE and must run — using the SAME ordered
// hook seam and per-packet currency check the remote
// teleport arm already uses (RunRemoteTeleportHook, #315
// pattern), so retail's per-manager guards decide for
// themselves rather than being re-derived here.
if (route.Disposition
is RuntimeAuthoritativePositionDisposition.SetPosition
&& acceptedPositionCanonical.RemoteMotion is RemoteMotion adoptedRemote)
{
// B4 fix (round-2 review): cache the currency-check
// delegate the same way the remote arm's #315 collapse
// already does, instead of allocating a fresh closure
// every accepted missile packet. Scratch fields stamped
// immediately before use; nothing reads them between
// calls, so last-packet staleness is harmless.
_projectileArmPositionRecord = acceptedPositionRecord;
_projectileArmPositionAuthorityVersion =
acceptedPositionAuthorityVersion;
RunRemoteTeleportHook(
acceptedPositionCanonical,
adoptedRemote,
_remoteArmCallbacks.IsCurrentProjectilePositionOwner);
}
RuntimeRemotePlacementExecutionStatus? placementStatus =
_remotePlacementDrive.ApplyAcceptedProjectilePosition(
acceptedPositionCanonical,
route);
// A1/R5 fix: mirror Runtime's OWN presentation gate
// (ApplyAcceptedProjectilePosition/SyncProjectilePresentation
// — every outcome except Deferred/RejectedByPlacement) rather
// than acknowledging unconditionally. Deferred already
// snapped the body to the PARKED result and withdrew the
// entity; RejectedByPlacement leaves the body exactly where
// it was. Acknowledging either would move the render entity
// to (or through) a pose/cell Runtime explicitly declined to
// publish — the concrete Interpolate/RejectedByPlacement
// wrong-cell scenario the review found.
if (placementStatus is not null
and not RuntimeRemotePlacementExecutionStatus.Deferred
and not RuntimeRemotePlacementExecutionStatus.RejectedByPlacement)
{
_projectileController?.SyncPresentationFromResolvedBody(
acceptedPositionRecord,
_physicsScriptGameTime);
}
}
return;
}
if (!_liveEntities.TryGetRecord(
update.Guid,
@ -2136,26 +2268,18 @@ internal sealed class LiveEntityNetworkUpdateController
return;
}
// C4 route 4a: classify BEFORE the generic write below so a remote
// whose accepted Position resolves to NoPositionOperation (retail's
// airborne no-op — writes nothing at all) or Interpolate (retail's
// near InterpolateTo queue — no direct body write here) never
// receives it. The local player never reaches this generic-remote
// code path at all. C4 route 4b-2 routes the >=96 m far snap and C4
// route 4b-3 routes the teleport/cell-less classification through
// the canonical Runtime placement owner (ApplyRemoteContactRouting);
// a rejected authority or payload, and "no classification at all",
// take the stated UnroutedCatchUp policy
// (RuntimeRemoteFarSnapPosition.ResolveArm).
RuntimeAuthoritativePositionRoute? earlyRemoteRoute =
update.Guid != _playerServerGuid
? ClassifyRemoteAcceptedPosition(
update,
acceptedPositionCanonical,
timestampDisposition,
timestamps,
worldPos)
: null;
// C4 route 4a/5: `earlyRemoteRoute` was already classified above
// (D-P6) — this is the SAME value, reused so a remote whose accepted
// Position resolves to NoPositionOperation (retail's airborne
// no-op — writes nothing at all) or Interpolate (retail's near
// InterpolateTo queue — no direct body write here) never receives
// the generic write below. The local player never reaches this
// generic-remote code path at all. C4 route 4b-2 routes the >=96 m
// far snap and C4 route 4b-3 routes the teleport/cell-less
// classification through the canonical Runtime placement owner
// (ApplyRemoteContactRouting); a rejected authority or payload, and
// "no classification at all", take the stated UnroutedCatchUp
// policy (RuntimeRemoteFarSnapPosition.ResolveArm).
TryApplyGenericRemoteRenderPose(
entity,

View file

@ -490,103 +490,84 @@ internal sealed class ProjectileController
}
/// <summary>
/// Applies a timestamp-gated server correction to the same predicted body.
/// The caller supplies both render-world and wire cell-local coordinates so
/// streaming origin changes never leak into the canonical cell frame.
/// C4 route 5 (D-P2 closing paragraph): the presentation acknowledgement
/// App still owns after
/// <c>RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition</c>
/// has committed or stored an accepted Position for
/// <paramref name="expectedRecord"/>. Projects the Runtime-resolved
/// canonical body into the render entity and root pose — the same ack
/// contract the deleted <c>ApplyAuthoritativePosition</c> performed via
/// its <c>acknowledgeProjection</c> closure, now read directly off the
/// canonical body rather than an intermediate snapshot, since the
/// placement already committed (or the store fallback already wrote)
/// before this is called.
///
/// <para>
/// <b>B1/B2 fix (round-2 review — reverting round 1's own R6 "fix",
/// which the retail reviewer retracted as factually wrong).</b>
/// <c>ParentCellId</c> is read from <c>record</c>'s (the local resolved
/// by <c>TryGetCurrent</c> below, equal to
/// <paramref name="expectedRecord"/> once the currency check passes)
/// <c>FullCellId</c> — the WIRE cell — never the body's own
/// <c>CellPosition.ObjCellId</c>. On a committed outcome the two agree
/// (<c>CommitCanonical</c> writes both together), so the choice is a
/// no-op there. On a STORED outcome
/// (<c>Refused</c>/<c>Contention</c>/<c>RejectedPreparation</c>),
/// <c>StoreAcceptedDestinationPose</c> composes <c>body.Position</c>
/// from <c>accepted.PositionX + worldOffset(accepted.LandblockId)</c> —
/// the WIRE cell's world frame — while <c>record.FullCellId</c> is that
/// SAME wire cell (stamped by the merge's
/// <c>RefreshDerivedState</c>/<c>SetFullCell</c>, before classification
/// ever runs). The body's OWN <c>CellPosition.ObjCellId</c>, in
/// contrast, is never touched by the store fallback and is therefore
/// the SOURCE cell the body left — reading it here would pair the new
/// destination position with the cell the body is no longer in. Three
/// independent checks confirm <c>record.FullCellId</c> is the correct
/// source: (1) the sibling remote arm's
/// <c>TryApplyGenericRemoteRenderPose</c> pairs the wire world position
/// with the wire cell the same way; (2) Runtime's own
/// <c>SyncProjectilePresentation</c>, forty lines away in the SAME
/// packet, publishes the shadow row at <c>record.FullCellId</c> — so
/// reading the body's cell here would disagree with the shadow for the
/// SAME body in the SAME call; (3) retail's <c>store_position</c>
/// @0x00515CE2 writes the object's whole <c>Position</c> INCLUDING
/// <c>objcell_id</c>, so after a retail store the object's cell IS the
/// destination (the wire cell) — never the stale one. The body's own
/// stale cell after a store is the acdream-side residual (AP-138,
/// shared with the remote arms), not the truth to project.
/// </para>
///
/// <para>
/// Rebases <paramref name="currentTime"/> into the SAME
/// <c>_lastFiniteGameTime</c> field the deleted method's <c>currentTime</c>
/// parameter used to (unconditionally, when finite) — restoring the
/// controller's own tick-elapsed-time basis instead of leaving it
/// pinned at the last Vector/State/Tick call.
/// </para>
/// </summary>
internal bool ApplyAuthoritativePosition(
internal bool SyncPresentationFromResolvedBody(
LiveEntityRecord expectedRecord,
Vector3 worldPosition,
Vector3 cellLocalPosition,
Quaternion orientation,
Vector3 velocity,
uint fullCellId,
double currentTime,
int liveCenterX,
int liveCenterY) =>
ApplyAuthoritativePosition(
expectedRecord,
expectedRecord.PositionAuthorityVersion,
expectedRecord.VelocityAuthorityVersion,
worldPosition,
cellLocalPosition,
orientation,
velocity,
fullCellId,
currentTime,
liveCenterX,
liveCenterY);
internal bool ApplyAuthoritativePosition(
LiveEntityRecord expectedRecord,
ulong expectedPositionAuthorityVersion,
ulong expectedVelocityAuthorityVersion,
Vector3 worldPosition,
Vector3 cellLocalPosition,
Quaternion orientation,
Vector3 velocity,
uint fullCellId,
double currentTime,
int liveCenterX,
int liveCenterY)
double currentTime)
{
ArgumentNullException.ThrowIfNull(expectedRecord);
uint serverGuid = expectedRecord.ServerGuid;
if (!TryGetCurrent(serverGuid, out LiveEntityRecord record, out RuntimeProjectile runtime)
if (!TryGetCurrent(
expectedRecord.ServerGuid,
out LiveEntityRecord record,
out RuntimeProjectile runtime)
|| !ReferenceEquals(record, expectedRecord)
|| record.PositionAuthorityVersion != expectedPositionAuthorityVersion
|| (record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0
|| record.WorldEntity is not { } entity)
return false;
if (!double.IsFinite(currentTime)
|| !IsFinite(worldPosition)
|| !IsFinite(cellLocalPosition)
|| !IsFinite(velocity)
|| !PositionFrameValidation.IsValid(
fullCellId,
cellLocalPosition,
orientation))
{
DiagnosticSink?.Invoke(
$"Rejected invalid PositionUpdate for missile 0x{serverGuid:X8}.");
return true;
return false;
}
_lastFiniteGameTime = currentTime;
bool ExternalOwnerValid() =>
TryGetCurrent(
serverGuid,
out LiveEntityRecord current,
out RuntimeProjectile currentRuntime)
&& ReferenceEquals(current, record)
&& ReferenceEquals(currentRuntime, runtime)
&& ReferenceEquals(current.WorldEntity, entity)
&& current.PositionAuthorityVersion
== expectedPositionAuthorityVersion;
return _runtimeUpdater.ApplyAuthoritativePosition(
record.Canonical,
expectedPositionAuthorityVersion,
expectedVelocityAuthorityVersion,
worldPosition,
cellLocalPosition,
orientation,
velocity,
fullCellId,
currentTime,
liveCenterX,
liveCenterY,
snapshot =>
{
if (!ExternalOwnerValid())
return false;
entity.SetPosition(snapshot.Position);
entity.Rotation = snapshot.Orientation;
entity.ParentCellId = snapshot.FullCellId;
_rootPoses?.UpdateRoot(entity);
return ExternalOwnerValid();
},
ExternalOwnerValid);
if (double.IsFinite(currentTime))
_lastFiniteGameTime = currentTime;
entity.SetPosition(runtime.Body.Position);
entity.Rotation = runtime.Body.Orientation;
entity.ParentCellId = record.FullCellId;
_rootPoses?.UpdateRoot(entity);
return true;
}
/// <summary>

View file

@ -589,14 +589,38 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
}
/// <summary>
/// C4 route 4a: classifies one REMOTE incarnation's accepted Position
/// through <see cref="RuntimeAuthoritativePositionRouteClassifier"/>, so
/// the graphical host and any future no-window remote-motion host make
/// C4 route 4a: classifies one non-local-player incarnation's accepted
/// Position through <see cref="RuntimeAuthoritativePositionRouteClassifier"/>,
/// so the graphical host and any future no-window remote-motion host make
/// the SAME airborne-no-op / near-interpolate decision from the same
/// generation, the same authority shape, and the same request builder the
/// deferred initial-create continuation uses.
///
/// <para>
/// C4 route 5 (D-P1, REVISED after the retail/architecture review round —
/// A2/R1): the caller's incarnation may be an ordinary remote OR a live
/// missile — this method derives <c>RuntimePositionEntityKind</c> from
/// <c>canonical.FinalPhysicsState &amp; PhysicsStateFlags.Missile</c>
/// **conjoined with a bound, body-agreeing <see cref="RuntimeProjectile"/>**,
/// never the Missile bit alone. Retail's <c>MoveOrTeleport</c> places
/// EVERY non-player object unconditionally — it has no concept of
/// "client-side machinery not yet bound". A Missile-flagged record whose
/// <c>ProjectileController.TryBind</c> permanently refused (an
/// unsupported multi-sphere Setup) or has not yet run (the pre-bind
/// window) still classifies <c>Remote</c> here, so it takes the SAME
/// generic remote placement path retail's client would drive for it and
/// keeps tracking the server — exactly the pre-route-5 behaviour, which
/// fell through the deleted <c>ApplyAuthoritativePosition</c>'s
/// <c>TryGetCurrent</c> failure to the remote tail. The classifier's own
/// disposition/flag shape is unchanged either way (Projectile and Remote
/// are disposition-identical); only the returned route's
/// <c>OperationKind</c> differs, which is what lets the App dispatch
/// (route 5's <c>OnPosition</c> arm) and
/// <see cref="AcDream.Runtime.Session.RuntimeRemotePlacementDriveController.OwnsPlacement"/>
/// tell the two apart.
/// </para>
///
/// <para>
/// Returns <see langword="null"/> when no classification can honestly be
/// made: the lifetime has no bound generation yet, the canonical record
/// has not claimed a local id, or there is no live local-player position
@ -635,11 +659,32 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
{
return null;
}
// C4 route 5 (D-P1, revised — A2/R1): the Missile bit alone is
// data-driven but not sufficient — retail places every non-player
// object regardless of what client-side machinery exists for it, so
// this packet must classify Projectile ONLY when the arm can
// actually own the placement (a bound RuntimeProjectile whose Body
// is the exact canonical PhysicsBody). Anything else — no
// component bound (TryBind refused or has not run yet), or a
// component bound to a stale/displaced body — classifies Remote,
// matching retail's unconditional placement and the pre-route-5
// fall-through the deleted ApplyAuthoritativePosition performed via
// its own TryGetCurrent failure. A mid-life Missile flip (ACE clears
// it on impact; a State packet installs it, and TryBind subsequently
// binds the component) is handled by construction: whichever
// FinalPhysicsState AND binding state the canonical record carries
// for THIS packet decides the packet's kind.
RuntimePositionEntityKind kind =
(canonical.FinalPhysicsState & PhysicsStateFlags.Missile) != 0
&& canonical.Projectile is RuntimeProjectile boundProjectile
&& ReferenceEquals(canonical.PhysicsBody, boundProjectile.Body)
? RuntimePositionEntityKind.Projectile
: RuntimePositionEntityKind.Remote;
if (!RuntimeAcceptedPositionRouteRequests.TryBuild(
generation(),
canonical,
update,
RuntimePositionEntityKind.Remote,
kind,
RuntimeAcceptedPositionSource.PositionEvent,
disposition,
timestamps.PreviousTeleport,

View file

@ -298,130 +298,15 @@ internal sealed class RuntimeProjectilePhysicsUpdater
return true;
}
internal bool ApplyAuthoritativePosition(
RuntimeEntityRecord record,
ulong expectedPositionAuthorityVersion,
ulong expectedVelocityAuthorityVersion,
Vector3 worldPosition,
Vector3 cellLocalPosition,
Quaternion orientation,
Vector3 velocity,
uint fullCellId,
double currentTime,
int liveCenterX,
int liveCenterY,
Func<RuntimePhysicsFrameSnapshot, bool> acknowledgeProjection,
Func<bool>? externalOwnerValid = null)
{
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(acknowledgeProjection);
if (!TryGetCurrent(
record,
externalOwnerValid,
out RuntimeProjectile projectile)
|| record.PositionAuthorityVersion
!= expectedPositionAuthorityVersion
|| (record.FinalPhysicsState
& PhysicsStateFlags.Missile) == 0)
{
return false;
}
if (!double.IsFinite(currentTime)
|| !IsFinite(worldPosition)
|| !IsFinite(cellLocalPosition)
|| !IsFinite(velocity)
|| !PositionFrameValidation.IsValid(
fullCellId,
cellLocalPosition,
orientation))
{
return true;
}
PhysicsBody body = projectile.Body;
projectile.InvalidatePrediction();
ulong predictionVersion = projectile.PredictionAuthorityVersion;
bool wasInWorld = body.InWorld;
body.Orientation = orientation;
body.SnapToCell(fullCellId, worldPosition, cellLocalPosition);
body.State = record.FinalPhysicsState;
if (record.VelocityAuthorityVersion
== expectedVelocityAuthorityVersion)
{
_ = _physics.TryCommitAuthoritativeVector(
record,
body,
velocity,
angularVelocity: null,
currentTime,
externalOwnerValid);
}
bool IsExactOwner() =>
TryGetCurrent(
record,
externalOwnerValid,
out RuntimeProjectile current)
&& ReferenceEquals(current, projectile)
&& current.PredictionAuthorityVersion == predictionVersion
&& record.PositionAuthorityVersion
== expectedPositionAuthorityVersion;
if (!_physics.CommitProjectileCell(
record,
projectile,
predictionVersion,
body.CellPosition.ObjCellId,
IsExactOwner)
|| !IsExactOwner())
{
// This packet was accepted for the old incarnation. A re-entrant
// observer displaced it, so the replacement owns another body.
return true;
}
var snapshot = new RuntimePhysicsFrameSnapshot(
body.Position,
body.Orientation,
body.CellPosition.ObjCellId);
if (!acknowledgeProjection(snapshot) || !IsExactOwner())
return true;
bool spatial = _physics.IsSpatialProjectile(record, projectile);
bool hidden =
(record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
uint localId = record.LocalEntityId ?? 0u;
if (spatial && !hidden)
{
if (!wasInWorld)
{
body.LastUpdateTime = currentTime;
Activate(body, currentTime);
}
body.InWorld = true;
ShadowPositionSynchronizer.Sync(
_physics.Engine.ShadowObjects,
localId,
body.Position,
body.Orientation,
record.FullCellId,
liveCenterX,
liveCenterY);
}
else if (spatial)
{
body.InWorld = true;
body.LastUpdateTime = currentTime;
_physics.Engine.ShadowObjects.Suspend(localId);
}
else
{
body.InWorld = false;
Deactivate(body);
_physics.Engine.ShadowObjects.Suspend(localId);
}
return true;
}
// C4 route 5 (2026-08-04): the position-packet authority that used to
// live here — ApplyAuthoritativePosition — is deleted. A live missile's
// accepted Position now routes through the canonical
// RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition,
// the same shared placement pipeline (TryExecuteAcceptedRemotePosition /
// StoreAcceptedDestinationPose / CommitCanonical) the remote teleport/far
// arms use, instead of this class's bespoke SnapToCell + CommitProjectileCell
// + manual shadow-sync tail. CommitProjectileCell remains the per-quantum
// path's own cell commit (TryBegin/Complete below) — untouched.
private bool IsSpatialCurrent(
RuntimeEntityRecord record,
@ -481,18 +366,6 @@ internal sealed class RuntimeProjectilePhysicsUpdater
0f);
}
private static void Activate(PhysicsBody body, double currentTime)
{
if ((body.State & PhysicsStateFlags.Static) != 0)
return;
if ((body.TransientState & TransientStateFlags.Active) == 0)
body.LastUpdateTime = currentTime;
body.TransientState |= TransientStateFlags.Active;
}
private static void Deactivate(PhysicsBody body) =>
body.TransientState &= ~TransientStateFlags.Active;
private static bool IsFinite(Vector3 value) =>
float.IsFinite(value.X)
&& float.IsFinite(value.Y)

View file

@ -78,6 +78,20 @@ internal static class RuntimeRemoteFarSnapPosition
/// <c>arg3 != 0</c>), so this predicate is a strict narrowing of
/// <c>OwnsPlacement</c> to its far half.
/// </para>
///
/// <para>
/// C4 route 5 (D-P3/A10 fix): after the widening,
/// <c>OwnsPlacement</c> ALSO admits
/// <c>RuntimeSetPositionOperationKind.ProjectileAuthoritative</c> — this
/// predicate's <c>OperationKind: RemoteAuthoritative</c> gate below is
/// therefore a strict narrowing of <c>OwnsPlacement</c>'s REMOTE far
/// half only, not the whole predicate. A projectile far route never
/// satisfies this method (it takes the sibling seam,
/// <c>RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition</c>,
/// which does not call this method and does not go through
/// <see cref="AcDream.Runtime.Session.RuntimeRemotePlacementDriveController.ApplyAcceptedRemoteFarSnap"/>/<see cref="ResolveArm"/> —
/// both require a <c>RemoteMotion</c> a projectile does not have).
/// </para>
/// </summary>
internal static bool OwnsFarSnap(RuntimeAuthoritativePositionRoute? route) =>
route is

View file

@ -516,9 +516,25 @@ internal sealed class RuntimeRemotePlacementDriveController
/// POSITION-only route; the first-entry conductor owns every Create)
/// without excluding either remote Position shape.
/// </para>
/// <para>
/// C4 route 5 (D-P3): widened to admit
/// <c>ProjectileAuthoritative</c> alongside <c>RemoteAuthoritative</c> —
/// a live missile's teleport/cell-less and far accepted-Position
/// dispositions are disposition-identical to a remote's (the classifier
/// never branches on kind past <c>LocalPlayer</c>), and route 5's
/// <see cref="ApplyAcceptedProjectilePosition"/> is the sibling seam over
/// this SAME shared core (<see cref="TryExecuteAcceptedRemotePosition"/> +
/// <see cref="StoreAcceptedDestinationPose"/>) — never a second pending
/// map, never a sibling controller. <c>OwnsFarSnap</c> and
/// <c>OwnsTeleportPlacement</c> are deliberately NOT widened: both remote
/// arm methods (<see cref="ApplyAcceptedRemoteFarSnap"/>/
/// <see cref="ApplyAcceptedRemoteTeleport"/>) require and throw without a
/// <c>RemoteMotion</c>, which a projectile never has.
/// </para>
/// </summary>
internal static bool OwnsPlacement(RuntimeAuthoritativePositionRoute route) =>
route.OperationKind is RuntimeSetPositionOperationKind.RemoteAuthoritative
or RuntimeSetPositionOperationKind.ProjectileAuthoritative
&& route.Disposition is RuntimeAuthoritativePositionDisposition.SetPosition
or RuntimeAuthoritativePositionDisposition.SetPositionSimple
&& (route.SetPositionFlags & PhysicsSetPositionFlags.Teleport) != 0;
@ -842,6 +858,234 @@ internal sealed class RuntimeRemotePlacementDriveController
return status;
}
/// <summary>
/// C4 route 5 (D-P2): the projectile arm over this SAME shared core. A
/// live missile carries no <see cref="RemoteMotion"/>, so
/// <see cref="ApplyAcceptedRemoteFarSnap"/>/<see cref="ApplyAcceptedRemoteTeleport"/>
/// are not reusable — both require one and throw without it. This is the
/// sibling seam the D-P2 design pins: same
/// <see cref="TryExecuteAcceptedRemotePosition"/> +
/// <see cref="StoreAcceptedDestinationPose"/> core, same
/// <see cref="_pending"/>/<see cref="_awaitingAcknowledgement"/> ledgers,
/// no second pending map, no sibling controller (trap T9).
///
/// <para>
/// Returns <see langword="null"/> for every disposition this route does
/// not own: <c>Interpolate</c> (near, in contact) and
/// <c>NoPositionOperation</c> (airborne) are pinned NO-OPS — retail would
/// lazily build interpolation/leash machinery for a manager-less missile
/// (@0x005163AF / @0x00454272-@0x00510523), which acdream deliberately
/// does not construct for a ballistic body (the register row this route
/// adds); <c>RejectedAuthority</c>/<c>RejectedData</c> and an ownership
/// mismatch are SWALLOWED — write nothing, never fall through to the
/// remote tail (trap T5). A caller must not fall back to any remote arm
/// when this returns <see langword="null"/>.
/// </para>
///
/// <para>
/// <b>No velocity write (D-P5).</b> Retail's <c>MoveOrTeleport</c>
/// @0x00516330 never references its velocity argument in the decompiled
/// body, and a byte-level disassembly of the PDB-paired binary
/// (@0x00516330-@0x00516438) confirms no instruction anywhere in the
/// function reads that argument's stack slot. This seam commits no
/// velocity from the Position packet at all — the Vector channel
/// (<see cref="RuntimeProjectilePhysicsUpdater.ApplyAuthoritativeVector"/>)
/// remains the sole velocity authority for a missile.
/// </para>
///
/// <para>
/// <b>No constraint leash armed (D-P4).</b> Unlike the remote arms, this
/// method never calls <c>TryArmConstraintAfterOperation</c> — the
/// classifier's projectile routes still carry
/// <c>ConstrainPhase.AfterPositionOperation</c> (kind-blind), but this arm
/// deliberately does not consume it, matching the pinned divergence.
/// </para>
///
/// <para>
/// <b>Teleport hook reduction (D-P4).</b> Of retail's six
/// <c>teleport_hook</c> @0x00514ED0 actions, five are structurally absent
/// for a manager-less missile (no <c>MovementManager</c>/
/// <c>PositionManager</c>/<c>TargetManager</c>). The sixth,
/// <c>report_collision_end(this, 1)</c> @0x00514F31-@0x00514620, applies
/// to any object with a collision table and runs BEFORE the placement —
/// ported here as <c>RuntimeCollisionReportingState.LeaveWorld</c> (the
/// exact force-end seam the 4b-3 round-2 review validated against the
/// same retail address).
/// </para>
///
/// <para>
/// Prediction is invalidated once per packet, before any body write on
/// this route (placement or the store fallback) — mirroring
/// <c>RuntimeProjectilePhysicsUpdater</c>'s existing invalidate-before-
/// write ordering — so an in-flight split quantum straddling this packet
/// aborts at <c>Complete</c> rather than clobbering a canonical
/// placement. The no-op dispositions invalidate nothing: the body is
/// untouched, so a straddling quantum completing over them is correct.
/// </para>
/// </summary>
internal RuntimeRemotePlacementExecutionStatus? ApplyAcceptedProjectilePosition(
RuntimeEntityRecord record,
in RuntimeAuthoritativePositionRoute route)
{
ArgumentNullException.ThrowIfNull(record);
if (route.OperationKind
is not RuntimeSetPositionOperationKind.ProjectileAuthoritative
|| record.Projectile is not RuntimeProjectile projectile
|| record.PhysicsBody is not { } body
|| !ReferenceEquals(body, projectile.Body))
{
return null;
}
// A6 fix (review round): captured BEFORE the placement dispatch,
// mirroring the deleted tail's `bool wasInWorld = body.InWorld;`
// ordering — TryExecuteAcceptedRemotePosition's canonical commit
// calls body.SnapToCell, which sets InWorld = true, so reading this
// AFTER the dispatch (as the first cut of this seam did) makes the
// re-activation branch below permanently dead on every committed
// outcome.
bool wasInWorld = body.InWorld;
RuntimeRemotePlacementExecutionStatus status;
switch (route.Disposition)
{
case RuntimeAuthoritativePositionDisposition.SetPosition:
_entityObjects.Physics.CollisionReports.LeaveWorld(record);
projectile.InvalidatePrediction();
status = TryExecuteAcceptedRemotePosition(record, route);
break;
case RuntimeAuthoritativePositionDisposition.SetPositionSimple:
// B1/B2 fix (round-2 review): retail's far branch runs
// `StopInterpolating` @0x005163C9-@0x005163CB whenever
// `position_manager != 0` — the SAME guard the remote far
// arm ports as `ApplyAcceptedRemoteFarSnap`'s
// `if (route.StopInterpolating) remote.Interp.Clear();`. A
// bare missile has no RemoteMotion so this is structurally
// inert, but the ADOPTED-BODY case (TryBind's shared-body
// branch: an ordinary remote whose Missile bit was set by a
// later State packet) carries a live Interp queue the far
// branch must clear too — the teleport hook only covers the
// SetPosition disposition.
if (route.StopInterpolating
&& record.RemoteMotion is RemoteMotion adoptedFar)
{
adoptedFar.Interp.Clear();
}
projectile.InvalidatePrediction();
status = TryExecuteAcceptedRemotePosition(record, route);
break;
default:
// Interpolate / NoPositionOperation: pinned no-op (D-P4).
// RejectedAuthority / RejectedData: swallow (T5) — the
// shared authority gate already rejected an invalid payload
// upstream; there is nothing left to route.
return null;
}
if (status.StoresAcceptedDestination())
StoreAcceptedDestinationPose(record);
if (status is not RuntimeRemotePlacementExecutionStatus.Deferred
and not RuntimeRemotePlacementExecutionStatus.RejectedByPlacement)
{
// Invariant 2: presentation advances on every committed/stored
// outcome only — Deferred/RejectedByPlacement leave the body at
// its prior (already-synced) pose.
SyncProjectilePresentation(record, projectile, body, wasInWorld);
}
return status;
}
/// <summary>
/// C4 route 5 (REVISED after the review round — A6/A7/A8): the
/// J5.6-owned post-commit lifecycle tail (InWorld/Activate/shadow-sync
/// on spatial+visible, suspend on spatial+hidden, deactivate+suspend on
/// non-spatial), reduced from the deleted
/// <c>RuntimeProjectilePhysicsUpdater.ApplyAuthoritativePosition</c>
/// tail (former <c>:390-422</c>) to this controller's own seam —
/// <paramref name="wasInWorld"/> is the caller's pre-dispatch capture
/// (A6: reading <c>body.InWorld</c> here, after the canonical commit's
/// own <c>SnapToCell</c> already forced it true, made the re-activation
/// branch permanently dead); <see cref="_clock"/> supplies the same
/// clock source the deleted method took as an explicit
/// <c>currentTime</c> parameter; the world-frame offset comes from
/// <see cref="RuntimePhysicsState.TryGetWorldFrameOffset"/> (the same
/// source <see cref="StoreAcceptedDestinationPose"/> uses) instead of an
/// App-supplied live-center pair.
/// </summary>
private void SyncProjectilePresentation(
RuntimeEntityRecord record,
RuntimeProjectile projectile,
PhysicsBody body,
bool wasInWorld)
{
if (!_entityObjects.Entities.IsCurrent(record)
|| !ReferenceEquals(record.Projectile, projectile)
|| !ReferenceEquals(record.PhysicsBody, body))
{
return;
}
RuntimePhysicsState physics = _entityObjects.Physics;
bool spatial = physics.IsSpatialProjectile(record, projectile);
bool hidden =
(record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
uint localId = record.LocalEntityId ?? 0u;
if (spatial && !hidden)
{
if (!wasInWorld)
{
body.LastUpdateTime = _clock.SimulationTimeSeconds;
if ((body.State & PhysicsStateFlags.Static) == 0)
body.TransientState |= TransientStateFlags.Active;
}
body.InWorld = true;
// A8 fix: #284's policy ("a frame that can never arrive is
// terminal, never silent") applies here exactly as it does to
// StoreAcceptedDestinationPose. A false result during the
// legitimate pre-local-player-Create window silently skips the
// publish (self-heals once the frame arrives); a false result
// AFTER that window is a genuinely stuck frame, and
// ThrowIfWorldFrameUnreachable escalates it instead of leaving
// the shadow silently stale forever.
if (physics.TryGetWorldFrameOffset(
record.FullCellId,
out float offsetX,
out float offsetY))
{
physics.Engine.ShadowObjects.UpdatePosition(
localId,
body.Position,
body.Orientation,
offsetX,
offsetY,
record.FullCellId,
seedCellId: record.FullCellId);
}
else
{
physics.ThrowIfWorldFrameUnreachable(record.FullCellId);
}
}
else if (spatial)
{
body.InWorld = true;
// A7 fix: retail's hidden-branch clock consumption — restored,
// matching ProjectileController.TryBind's equivalent branch
// ("consume the hidden clock so UnHide cannot replay a time
// backlog").
body.LastUpdateTime = _clock.SimulationTimeSeconds;
physics.Engine.ShadowObjects.Suspend(localId);
}
else
{
body.InWorld = false;
body.TransientState &= ~TransientStateFlags.Active;
physics.Engine.ShadowObjects.Suspend(localId);
}
}
/// <summary>
/// Retail <c>CPhysicsObj::store_position</c> @0x00515CE2, reached from
/// <c>SetPositionInternal</c>'s no-resolvable-cell branch @0x00515C1D.
@ -975,6 +1219,29 @@ internal sealed class RuntimeRemotePlacementDriveController
}
_pending.Remove(key);
// R3 fix (review round): a retained retry can belong to a
// projectile operation exactly as it can belong to a
// remote's — this is the SAME shared _pending map (trap T9:
// no second map), and pending.Route carries the OperationKind
// that was classified when the retry was first parked.
// Neither invariant this route pins (prediction invalidated
// before every body write; presentation advances on every
// committed/stored outcome) may hold on the direct arm only.
// Real nullable locals (not a stored bool) so the compiler
// can track definite assignment through the branches below.
RuntimeProjectile? pendingProjectile = null;
PhysicsBody? pendingBody = null;
if (pending.Route.OperationKind
is RuntimeSetPositionOperationKind.ProjectileAuthoritative
&& pending.Record.Projectile is RuntimeProjectile candidateProjectile
&& pending.Record.PhysicsBody is { } candidateBody
&& ReferenceEquals(candidateBody, candidateProjectile.Body))
{
pendingProjectile = candidateProjectile;
pendingBody = candidateBody;
}
bool pendingWasInWorld = pendingBody?.InWorld ?? false;
// B3 review fix: a retry can sit retained across many host
// cadence pumps (bounded only by how long the asset stayed
// unavailable) while its destination's collision publication
@ -1000,17 +1267,55 @@ internal sealed class RuntimeRemotePlacementDriveController
CreateObject.ServerPosition? destination =
pending.Record.Snapshot.Physics?.Position
?? pending.Record.Snapshot.Position;
RuntimeRemotePlacementExecutionStatus retryStatus;
if (destination is not { } accepted
|| !CanAttemptDestination(
setPosition,
accepted.LandblockId))
{
CancelToken(setPosition, pending.Token);
pendingProjectile?.InvalidatePrediction();
StoreAcceptedDestinationPose(pending.Record);
continue;
retryStatus = RuntimeRemotePlacementExecutionStatus.Refused;
}
else
{
// B5 fix (round-2 review): invalidating BEFORE this call
// unconditionally was wrong when SubmitAndResolve itself
// re-parks (returns Contention) — that outcome writes
// NOTHING (no store, no commit), so invalidating for it
// violates invariant 4's "the no-op dispositions
// invalidate nothing" on this arm specifically (unlike
// the entry point, where StoresAcceptedDestination()
// treats Contention as a storing outcome via the
// caller's own StoreAcceptedDestinationPose — this retry
// arm does not store on a re-parked Contention, matching
// the pre-existing residual A3/round-1 already named).
// Invalidating AFTER the call instead of before is safe
// here: this method is single-threaded and synchronous,
// so a write performed inside SubmitAndResolve and the
// very next statement's invalidate are never observably
// separated by a quantum's Complete call.
retryStatus = SubmitAndResolve(
pending.Record, pending.Token, pending.Route);
if (retryStatus
is not RuntimeRemotePlacementExecutionStatus.Contention)
{
pendingProjectile?.InvalidatePrediction();
}
}
_ = SubmitAndResolve(pending.Record, pending.Token, pending.Route);
if (pendingProjectile is { } confirmedProjectile
&& pendingBody is { } confirmedBody
&& retryStatus is not RuntimeRemotePlacementExecutionStatus.Deferred
and not RuntimeRemotePlacementExecutionStatus.RejectedByPlacement)
{
SyncProjectilePresentation(
pending.Record,
confirmedProjectile,
confirmedBody,
pendingWasInWorld);
}
}
}
finally

View file

@ -500,6 +500,434 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
// both, because there is one call site left to sabotage. Reverted before
// committing.
// ── C4 route 5 (A5 fix): missile matrix ─────────────────────────────
//
// The architecture review's FAIL-level finding: zero App tests drove
// OnPosition with a missile packet, so A1 (App discards the seam's
// status) and A2 (an unbound missile's Position silently dropped) both
// lived in the ~35 lines of "thin glue" the implementer argued were
// covered by two well-tested layers. These tests extend THIS file's own
// fixture — never a lighter one — with a genuine Missile-flagged,
// RuntimeProjectile-bound, RemoteMotion-less incarnation, and assert the
// SAME observable surface the remote scenarios above assert: body/entity
// position and ParentCellId (that pair is the exact A1 assertion), plus
// the projectile-specific half — no RemoteMotion is EVER created
// (invariant 8's mutual exclusion) and no early wire-pose write occurs.
// Destinations for the commit scenarios are airborne (well above
// PublishDestinationCollision's flat terrain), isolating the placement
// assertions from the shared pipeline's ordinary ground-contact response
// (Claim 4's confound, restated here for the App layer).
private const uint MissileGuid = 0x80007101u;
private static readonly Vector3 MissileAirborneDestination =
new(12f, 14f, SpawnHeight + 10f);
[Fact]
public void MissileTeleportCommit_PlacesBodyNoRemoteMotionParentCellIdAgreesWithBody()
{
using var fixture = new Fixture(MissileGuid, isMissile: true);
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
fixture.Controller.OnPosition(fixture.Update(
MissileAirborneDestination, DestinationCell, teleportSequence: 5,
guid: MissileGuid, isGrounded: true));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
Assert.Null(canonical.RemoteMotion);
Assert.NotNull(canonical.Projectile);
PhysicsBody body = canonical.PhysicsBody!;
Vector3 resolved = MissileAirborneDestination + DestinationWorldOffset;
Assert.Equal(resolved, body.Position);
Assert.Equal(body.Position, fixture.Entity.Position);
// A1's own assertion: ParentCellId agrees with the RESOLVED body's
// OWN cell — the review's concrete wrong-cell scenario, checked
// positively here rather than only on the no-op scenarios below.
Assert.Equal(body.CellPosition.ObjCellId, fixture.Entity.ParentCellId);
Assert.Equal(DestinationCell, fixture.Entity.ParentCellId);
// D-P4: the collision table was force-ended (no seeded owner here to
// assert a 1->0 transition — that half is covered at
// tests/AcDream.Runtime.Tests — but the call must not throw or
// leave the entity uncollidable long-term; Tick below proves it is
// still a live, ordinary object).
ShadowEntry shadowEntry = Assert.Single(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == fixture.Entity.Id);
Assert.Equal(body.Position, shadowEntry.Position);
fixture.DrainPlacementFifo();
}
[Fact]
public void MissileFarCommit_PlacesBodyNoRemoteMotionParentCellIdAgreesWithBody()
{
using var fixture = new Fixture(MissileGuid, isMissile: true);
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
fixture.Controller.OnPosition(fixture.Update(
MissileAirborneDestination, DestinationCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: true));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
Assert.Null(canonical.RemoteMotion);
PhysicsBody body = canonical.PhysicsBody!;
Vector3 resolved = MissileAirborneDestination + DestinationWorldOffset;
Assert.Equal(resolved, body.Position);
Assert.Equal(body.Position, fixture.Entity.Position);
Assert.Equal(body.CellPosition.ObjCellId, fixture.Entity.ParentCellId);
Assert.Equal(DestinationCell, fixture.Entity.ParentCellId);
fixture.DrainPlacementFifo();
}
/// <summary>
/// B1 EnvCell id — an indoor-format low word (0x100+, outside
/// <c>LandDefs.AdjustToOutside</c>'s 1-0x40 outdoor range) staged onto
/// the body BEFORE the store-path dispatch below. This is what makes
/// the regression assertion actually discriminate: for an OUTDOOR
/// source/destination pair, <c>PhysicsBody.Position</c>'s setter
/// delta-syncs <c>CellPosition</c> through <c>AdjustToOutside</c> and
/// happens to re-derive the correct destination landblock anyway (pure
/// geometry, no collision data needed) — so a wrong
/// <c>body.CellPosition.ObjCellId</c> read would coincidentally agree
/// with <c>record.FullCellId</c> and the test would pass either way.
/// An INDOOR source cell takes <c>SyncCellPositionDelta</c>'s OTHER
/// branch (<c>PhysicsBody.cs:295-300</c>): it carries the position delta
/// but never re-derives the cell id, so <c>body.CellPosition.ObjCellId</c>
/// stays PINNED at the stale indoor source cell through the store path.
/// <c>record.FullCellId</c> has no such blind spot — it is merged from
/// the wire unconditionally — so this is the scenario where the two
/// expressions genuinely diverge and MAJOR 1's revert is provable.
/// </summary>
private const uint IndoorSourceCell = SourceLandblock | 0x0100u;
/// <summary>
/// Residual 1 close (round-2 review): the STORE path — <c>Refused</c> —
/// at the App layer, closing both the coverage gap AND standing as the
/// regression test for MAJOR 1 (the <c>ParentCellId</c> revert). The
/// destination is deliberately left outside the service window
/// (<see cref="RemoteServiceWindow.Allow"/> is never called for
/// <see cref="DestinationLandblock"/>), so
/// <c>CanAttemptDestination</c> refuses before the engine ever runs and
/// <c>StoreAcceptedDestinationPose</c> resolves the destination through
/// Runtime's own world frame instead of a commit. <c>Refused</c> is
/// still a storing (A1-admitted) outcome, so the App-level presentation
/// sync runs — the entity's position AND <c>ParentCellId</c> must both
/// move to the DESTINATION (the wire cell), not the stale INDOOR source
/// cell a <c>body.CellPosition.ObjCellId</c> read would have produced
/// (see <see cref="IndoorSourceCell"/>'s doc comment for why an outdoor
/// source cell would not have discriminated here).
/// </summary>
[Fact]
public void MissileFarRefused_StorePathStillMovesEntityToDestinationParentCellIdAgreesWithWireCell()
{
using var fixture = new Fixture(MissileGuid, isMissile: true);
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonicalBeforeStage));
PhysicsBody stagedBody = canonicalBeforeStage.PhysicsBody!;
stagedBody.SnapToCell(
IndoorSourceCell, stagedBody.Position, stagedBody.Position);
Assert.Equal(IndoorSourceCell, stagedBody.CellPosition.ObjCellId);
fixture.PublishDestinationCollision();
// Deliberately NOT allowed — CanAttemptDestination refuses.
fixture.Controller.OnPosition(fixture.Update(
MissileAirborneDestination, DestinationCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: true));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
Assert.Null(canonical.RemoteMotion);
Assert.NotNull(canonical.Projectile);
PhysicsBody body = canonical.PhysicsBody!;
Vector3 resolved = MissileAirborneDestination + DestinationWorldOffset;
Assert.Equal(resolved, body.Position);
Assert.True(body.InWorld);
// The store fallback never re-derives an INDOOR cell id — confirms
// the divergence this test is built to exercise actually occurred.
Assert.Equal(IndoorSourceCell, body.CellPosition.ObjCellId);
// The exact B1 regression check: ParentCellId is the DESTINATION
// (wire) cell, matching record.FullCellId — never
// body.CellPosition.ObjCellId, which just asserted it is STILL the
// stale indoor source cell.
Assert.Equal(body.Position, fixture.Entity.Position);
Assert.Equal(DestinationCell, fixture.Entity.ParentCellId);
fixture.DrainPlacementFifo();
}
[Fact]
public void MissileNear_NoOp_BodyUnchangedNoRemoteMotionNoWirePoseWrite()
{
using var fixture = new Fixture(MissileGuid, isMissile: true);
Vector3 spawnPose = fixture.Entity.Position;
var target = new Vector3(4f, 3f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
target, SourceCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: true));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
Assert.Null(canonical.RemoteMotion);
PhysicsBody body = canonical.PhysicsBody!;
Assert.Equal(spawnPose, body.Position);
// A1's regression, asserted directly: no early wire-pose write —
// the render entity was never moved to `target`.
Assert.Equal(spawnPose, fixture.Entity.Position);
Assert.NotEqual(target, fixture.Entity.Position);
}
[Fact]
public void MissileAirborne_NoOp_BodyUnchangedNoRemoteMotion()
{
using var fixture = new Fixture(MissileGuid, isMissile: true);
Vector3 spawnPose = fixture.Entity.Position;
var wirePos = new Vector3(50f, 50f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
wirePos, SourceCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: false));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
Assert.Null(canonical.RemoteMotion);
PhysicsBody body = canonical.PhysicsBody!;
Assert.Equal(spawnPose, body.Position);
Assert.Equal(spawnPose, fixture.Entity.Position);
}
[Fact]
public void MissileNullClassification_Swallowed_NoRemoteMotionNoWriteNoPredictionChange()
{
using var fixture = new Fixture(
MissileGuid, nullClassification: true, isMissile: true);
Vector3 spawnPose = fixture.Entity.Position;
ulong predictionBefore = fixture.Projectile!.PredictionAuthorityVersion;
var wirePos = new Vector3(50f, 50f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
wirePos, SourceCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: true));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
Assert.Null(canonical.RemoteMotion);
PhysicsBody body = canonical.PhysicsBody!;
Assert.Equal(spawnPose, body.Position);
Assert.Equal(spawnPose, fixture.Entity.Position);
Assert.Equal(
predictionBefore, fixture.Projectile.PredictionAuthorityVersion);
}
/// <summary>
/// A2/R1's regression scenario, driven end to end: Missile bit set, but
/// no <c>RuntimeProjectile</c> bound (TryBind refused, or has not run
/// yet — <c>ProjectileController.cs:160-166</c>'s unsupported-Setup
/// case, or the pre-bind window). Retail places every non-player object
/// unconditionally; the fixed classifier must classify this packet
/// Remote and route it through the SAME generic remote placement path
/// an ordinary remote uses — never the frozen silent drop the
/// unconjoined discriminator produced.
/// </summary>
[Fact]
public void MissileUnbound_FallsThroughToRemoteTail_TracksInsteadOfFreezing()
{
using var fixture = new Fixture(MissileGuid, isMissile: false);
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
// Flip Missile AFTER construction, deliberately WITHOUT binding a
// RuntimeProjectile — the unbound shape A2 names.
fixture.Lifetime.Entities.SetFinalPhysicsState(
canonical,
canonical.FinalPhysicsState | PhysicsStateFlags.Missile);
Assert.Null(canonical.Projectile);
EntityPhysicsHost host = fixture.InstallHost();
fixture.Remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
Vector3 spawnPose = fixture.Entity.Position;
fixture.Controller.OnPosition(fixture.Update(
MissileAirborneDestination, DestinationCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: true));
// Placed via the ordinary remote far-snap arm — not frozen, and
// armed exactly like FarSnap_BothGuids above.
Assert.NotEqual(spawnPose, fixture.Remote.Body.Position);
Assert.Equal(fixture.Remote.Body.Position, fixture.Entity.Position);
Assert.Equal(DestinationCell, fixture.Entity.ParentCellId);
Assert.NotNull(host.PositionManager.Constraint);
fixture.DrainPlacementFifo();
}
/// <summary>
/// A3/R2's adopted-body scenario: an ordinary remote (populated Interp
/// queue, an armed ConstrainTo leash) whose Missile bit is later set by
/// a State packet — <c>ProjectileController.TryBind</c>'s shared-body
/// branch adopts the SAME <c>RemoteMotion</c>/body rather than replacing
/// it. Retail's <c>teleport_hook</c> per-manager guards are satisfied
/// for this shape, so all six actions run; the App-level pre-dispatch
/// hook call (mirroring the remote teleport arm's own
/// <c>RunRemoteTeleportHook</c> wiring) must un-arm the leash and clear
/// the queue before the placement, exactly like retail's ordering.
/// </summary>
[Fact]
public void MissileAdoptedBody_TeleportCommit_UnConstrainsAndClearsInterpQueue()
{
using var fixture = new Fixture(MissileGuid, isMissile: false);
EntityPhysicsHost host = fixture.InstallHost();
fixture.Remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
// Arm the leash and populate the queue directly — the pre-teleport
// "live remote" state the adopted-body scenario requires.
host.PositionManager.ConstrainTo(
new AcDream.Core.Physics.Position(
SourceCell, fixture.Remote.Body.Position, Quaternion.Identity),
startDistance: 1f,
maxDistance: 5f);
Assert.NotNull(host.PositionManager.Constraint);
fixture.Remote.Interp.Enqueue(
fixture.Remote.Body.Position + Vector3.UnitX,
heading: 0f,
isMovingTo: false,
currentBodyPosition: fixture.Remote.Body.Position);
Assert.True(fixture.Remote.Interp.IsActive);
// TryBind's shared-body branch: adopt the SAME body into a
// RuntimeProjectile, and set Missile — the record now carries BOTH
// a RemoteMotion and a bound projectile, exactly like production.
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
fixture.Lifetime.Entities.SetFinalPhysicsState(
canonical,
canonical.FinalPhysicsState | PhysicsStateFlags.Missile);
fixture.Lifetime.Physics.BindProjectile(
canonical,
canonical.PhysicsBody!,
new ProjectileCollisionSphere(Vector3.Zero, 0.1f, 1f));
Assert.NotNull(canonical.Projectile);
Assert.NotNull(canonical.RemoteMotion);
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
fixture.Controller.OnPosition(fixture.Update(
MissileAirborneDestination, DestinationCell, teleportSequence: 5,
guid: MissileGuid, isGrounded: true));
// UnConstrain and StopInterpolating both ran. UnConstrain unmarks
// IsConstrained rather than nulling the manager back out (it was
// lazily CREATED by the ConstrainTo test-setup call above, and
// creation is one-way) — this is the exact discriminator
// WireAirborneNullClassified_BothGuids_WritesOnlyAP135Bookkeeping's
// own comment names ("Constraint is lazily created only on a
// genuine arm").
Assert.NotNull(host.PositionManager.Constraint);
Assert.False(host.PositionManager.Constraint!.IsConstrained);
Assert.False(fixture.Remote.Interp.IsActive);
// The placement itself still committed through the projectile arm.
Assert.Equal(
MissileAirborneDestination + DestinationWorldOffset,
canonical.PhysicsBody!.Position);
fixture.DrainPlacementFifo();
}
/// <summary>
/// B1/B2 fix (round-2 review): the far-branch counterpart to
/// <see cref="MissileAdoptedBody_TeleportCommit_UnConstrainsAndClearsInterpQueue"/>.
/// Retail's far branch (<c>SetPositionSimple</c>,
/// <c>player_distance &gt;= 96 m</c>) runs <c>StopInterpolating</c>
/// @0x005163C9-@0x005163CB whenever <c>position_manager != 0</c> — the
/// SAME guard the teleport branch's full <c>teleport_hook</c> shares
/// for its own <c>StopInterpolating</c> action — but the far branch
/// does NOT run the other five teleport_hook actions (no
/// <c>UnConstrain</c>). For a bare missile this is structurally inert
/// (no <c>RemoteMotion</c>), which is why the pre-round-2 AP-141 row
/// could call the far-branch skip "faithful by consequence." The
/// adopted-body case breaks that: it carries a live <c>Interp</c>
/// queue the far branch must ALSO clear, while its armed
/// <c>ConstrainTo</c> leash must stay armed (proving the far branch
/// really does run only <c>StopInterpolating</c>, not the full hook).
///
/// <para>
/// Round-3 nit (C2): "leash still armed" pins acdream's OWN divergence,
/// not retail's behaviour. Retail's <c>HandleReceivedPosition</c>
/// @0x00454254/@0x00454272 re-anchors an existing leash at the object's
/// just-updated position on every nonzero <c>MoveOrTeleport</c> return —
/// including the far branch's. acdream's far arm never calls
/// <c>ConstrainTo</c> at all (D-P4, AP-141 clause (b)), so "still armed"
/// here means "left exactly as staged," not "correctly re-anchored." A
/// future reader should not read this assertion as full far-branch
/// leash fidelity — only the <c>StopInterpolating</c> half is ported.
/// </para>
/// </summary>
[Fact]
public void MissileAdoptedBody_FarCommit_ClearsInterpQueueButLeavesConstraintArmed()
{
using var fixture = new Fixture(MissileGuid, isMissile: false);
EntityPhysicsHost host = fixture.InstallHost();
fixture.Remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
host.PositionManager.ConstrainTo(
new AcDream.Core.Physics.Position(
SourceCell, fixture.Remote.Body.Position, Quaternion.Identity),
startDistance: 1f,
maxDistance: 5f);
Assert.NotNull(host.PositionManager.Constraint);
Assert.True(host.PositionManager.Constraint!.IsConstrained);
fixture.Remote.Interp.Enqueue(
fixture.Remote.Body.Position + Vector3.UnitX,
heading: 0f,
isMovingTo: false,
currentBodyPosition: fixture.Remote.Body.Position);
Assert.True(fixture.Remote.Interp.IsActive);
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
fixture.Lifetime.Entities.SetFinalPhysicsState(
canonical,
canonical.FinalPhysicsState | PhysicsStateFlags.Missile);
fixture.Lifetime.Physics.BindProjectile(
canonical,
canonical.PhysicsBody!,
new ProjectileCollisionSphere(Vector3.Zero, 0.1f, 1f));
Assert.NotNull(canonical.Projectile);
Assert.NotNull(canonical.RemoteMotion);
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
// teleportSequence: 1 (unchanged from the fixture's baseline) with a
// cross-landblock destination classifies as the FAR disposition
// (SetPositionSimple) — the same discriminator
// MissileFarCommit_PlacesBodyNoRemoteMotionParentCellIdAgreesWithBody
// uses above, just against the adopted-body shape instead of a bare
// missile.
fixture.Controller.OnPosition(fixture.Update(
MissileAirborneDestination, DestinationCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: true));
// StopInterpolating ran — the queue is cleared.
Assert.False(fixture.Remote.Interp.IsActive);
// UnConstrain did NOT run — the far branch is one action, not six.
// The leash is still armed.
Assert.NotNull(host.PositionManager.Constraint);
Assert.True(host.PositionManager.Constraint!.IsConstrained);
// The placement itself still committed through the projectile arm.
Assert.Equal(
MissileAirborneDestination + DestinationWorldOffset,
canonical.PhysicsBody!.Position);
fixture.DrainPlacementFifo();
}
private sealed class Fixture : IDisposable
{
internal RuntimeEntityObjectLifetime Lifetime { get; }
@ -510,6 +938,13 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
internal WorldEntity Entity { get; }
internal RemoteMotion Remote { get; private set; } = null!;
internal LiveEntityAnimationState? Animated { get; private set; }
/// <summary>
/// C4 route 5 (A5 fix): the bound projectile component for an
/// <c>isMissile</c> fixture — <see langword="null"/> for an ordinary
/// remote fixture. Exposed so tests can assert prediction-version
/// movement without re-deriving it from <see cref="Lifetime"/>.
/// </summary>
internal RuntimeProjectile? Projectile { get; private set; }
private readonly GpuWorldState _spatial;
private readonly uint _guid;
private readonly bool _nullClassification;
@ -517,7 +952,8 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
internal Fixture(
uint guid,
bool nullClassification = false,
bool withAnimation = false)
bool withAnimation = false,
bool isMissile = false)
{
_guid = guid;
_nullClassification = nullClassification;
@ -559,8 +995,16 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
ForcePosition: 1,
ObjDesc: 1,
Instance: 1);
// C4 route 5 (A5 fix): a missile fixture carries the Missile bit
// from spawn — the SAME data-driven bit
// RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition
// (D-P1) reads, so this fixture's record classifies exactly like
// a real missile would once TryBind/BindProjectile below binds
// the component.
PhysicsStateFlags baseState = PhysicsStateFlags.ReportCollisions
| (isMissile ? PhysicsStateFlags.Missile : PhysicsStateFlags.None);
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.ReportCollisions,
RawState: (uint)baseState,
Position: wirePosition,
Movement: null,
AnimationFrame: null,
@ -593,7 +1037,7 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
null,
null,
0x09000001u,
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
PhysicsState: (uint)baseState,
InstanceSequence: 1,
PositionSequence: 1,
MovementSequence: 1,
@ -606,24 +1050,65 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
"fixture failed to materialize the remote entity");
Assert.True(Runtime.RebucketLiveEntity(_guid, SourceCell));
var remote = new RemoteMotion();
remote.Body.SnapToCell(SourceCell, Entity.Position, Entity.Position);
remote.CellId = SourceCell;
Runtime.SetRemoteMotionRuntime(_guid, remote);
Remote = remote;
Shadows.Register(
Entity.Id,
0x02000001u,
Entity.Position,
Entity.Rotation,
radius: 0.48f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: SourceLandblock,
collisionType: ShadowCollisionType.Cylinder,
cylHeight: 1.835f,
seedCellId: SourceCell,
isStatic: false);
// C4 route 5 (A5 fix): a missile fixture binds a
// RuntimeProjectile directly through the SAME production Runtime
// entry point ProjectileController.TryBind eventually calls
// (RuntimePhysicsState.BindProjectile) — never a RemoteMotion.
// This is the exact shape invariant 8's mutual exclusion pins:
// Missile-set means a projectile arm and NO RemoteMotion ever
// exists for the entity.
if (isMissile)
{
var body = new PhysicsBody
{
Position = Entity.Position,
Orientation = Entity.Rotation,
LastUpdateTime = 1d,
State = baseState,
TransientState = TransientStateFlags.Active,
};
body.SnapToCell(SourceCell, Entity.Position, Entity.Position);
RuntimeEntityRecord canonical = record.Canonical!;
Lifetime.Entities.SetPhysicsBody(canonical, body);
canonical.ObjectClock.Activate();
Lifetime.Physics.AcknowledgeSpatialProjection(canonical, spatial: true);
Projectile = (RuntimeProjectile)Lifetime.Physics.BindProjectile(
canonical, body, new ProjectileCollisionSphere(Vector3.Zero, 0.1f, 1f));
Shadows.Register(
Entity.Id,
0x02000001u,
Entity.Position,
Entity.Rotation,
radius: 0.1f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: SourceLandblock,
collisionType: ShadowCollisionType.Sphere,
state: (uint)baseState,
seedCellId: SourceCell,
isStatic: false);
}
else
{
var remote = new RemoteMotion();
remote.Body.SnapToCell(SourceCell, Entity.Position, Entity.Position);
remote.CellId = SourceCell;
Runtime.SetRemoteMotionRuntime(_guid, remote);
Remote = remote;
Shadows.Register(
Entity.Id,
0x02000001u,
Entity.Position,
Entity.Rotation,
radius: 0.48f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: SourceLandblock,
collisionType: ShadowCollisionType.Cylinder,
cylHeight: 1.835f,
seedCellId: SourceCell,
isStatic: false);
}
var origin = new LiveWorldOriginState();
origin.SetPlaceholder(

View file

@ -164,16 +164,21 @@ public sealed class ProjectileControllerTests
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 2.0));
IRuntimeProjectile runtime = record.ProjectileRuntime!;
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
record,
worldPosition: new Vector3(10f, 202f, 5f),
cellLocalPosition: new Vector3(10f, 10f, 5f),
orientation: Quaternion.Identity,
velocity: new Vector3(1f, 2f, 3f),
fullCellId: CellB,
currentTime: 2.1,
liveCenterX: 1,
liveCenterY: 1));
// C4 route 5: accepted-Position placement now routes through
// RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition
// (Runtime-owned, tested at tests/AcDream.Runtime.Tests). This test is
// about Tick/hydration behaviour for a pending destination, not about
// the placement seam itself, so the destination is written directly —
// mirroring what a committed placement leaves on the canonical body —
// and the presentation ack uses the SAME production method the seam's
// caller uses (SyncPresentationFromResolvedBody).
runtime.Body.SnapToCell(
CellB,
new Vector3(10f, 202f, 5f),
new Vector3(10f, 10f, 5f));
runtime.Body.Orientation = Quaternion.Identity;
Assert.True(fixture.Live.RebucketLiveEntity(Guid, CellB));
Assert.True(fixture.Controller.SyncPresentationFromResolvedBody(record, 2.1));
Assert.False(record.IsSpatiallyVisible);
Assert.True(record.IsSpatiallyProjected);
@ -365,8 +370,16 @@ public sealed class ProjectileControllerTests
}
[Fact]
public void FreshVectorAndPositionCorrectionsMutateSameBody()
public void FreshVectorCorrectionsMutateTheCanonicalBody()
{
// C4 route 5: the Position half of this test (a correction commits a
// velocity too) is RETIRED by design (D-P5) — a byte-level decode of
// retail's MoveOrTeleport (@0x00516330-@0x00516438) confirms it never
// reads its velocity argument, and RuntimeRemotePlacementDriveController
// .ApplyAcceptedProjectilePosition commits no velocity from a Position
// packet. The no-velocity invariant (a Position correction leaves an
// in-flight body's velocity bit-identical) is exercised at
// tests/AcDream.Runtime.Tests, the seam's own layer, not here.
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 3);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 4.0));
@ -379,43 +392,20 @@ public sealed class ProjectileControllerTests
currentTime: 4.5));
Assert.Equal(new Vector3(7f, 8f, 9f), body.Velocity);
Assert.Equal(new Vector3(0f, 0f, 2f), body.Omega);
var correction = new Vector3(30f, 31f, 32f);
var correctedVelocity = new Vector3(3f, 4f, 5f);
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
record,
correction,
new Vector3(30f, 31f, 32f),
Quaternion.Identity,
correctedVelocity,
CellA,
currentTime: 5.0,
liveCenterX: 1,
liveCenterY: 1));
Assert.Same(body, record.PhysicsBody);
Assert.Equal(correction, body.Position);
Assert.Equal(correction, record.WorldEntity!.Position);
Assert.Equal(correctedVelocity, body.Velocity);
// InboundPhysicsStateController normalizes an absent PositionPack
// velocity to zero before this contract is called.
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
record,
correction,
new Vector3(30f, 31f, 32f),
Quaternion.Identity,
Vector3.Zero,
CellA,
currentTime: 5.1,
liveCenterX: 1,
liveCenterY: 1));
Assert.Equal(Vector3.Zero, body.Velocity);
}
// C4 route 5: the AuthoritativeMutation.Position case of this theory is
// RETIRED — accepted-Position corrections no longer route through
// ProjectileController at all (RuntimeRemotePlacementDriveController
// .ApplyAcceptedProjectilePosition owns them). The successor —
// "a split quantum straddling an accepted far/teleport Position aborts
// Complete rather than clobbering the committed placement" — is a
// Runtime-level test (trap T3) at tests/AcDream.Runtime.Tests, exercised
// directly against RuntimeProjectilePhysicsUpdater.TryBegin/Complete and
// the new seam, since that pairing is what now shares the prediction
// version this scenario discards.
[Theory]
[InlineData(AuthoritativeMutation.Vector)]
[InlineData(AuthoritativeMutation.Position)]
[InlineData(AuthoritativeMutation.State)]
public void AuthoritativeMutationBetweenQuantumHalvesDiscardsPrediction(
AuthoritativeMutation mutation)
@ -429,7 +419,6 @@ public sealed class ProjectileControllerTests
quantum: 0.05f,
out ProjectileController.QuantumStep step));
Vector3 correctedPosition = new(30f, 31f, 32f);
Vector3 correctedVelocity = new(7f, 8f, 9f);
Vector3 correctedOmega = new(0f, 0f, 2f);
PhysicsStateFlags correctedState = MissileState | PhysicsStateFlags.Gravity;
@ -443,19 +432,6 @@ public sealed class ProjectileControllerTests
currentTime: 1.02));
break;
case AuthoritativeMutation.Position:
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
record,
correctedPosition,
correctedPosition,
Quaternion.Identity,
correctedVelocity,
CellA,
currentTime: 1.02,
liveCenterX: 1,
liveCenterY: 1));
break;
case AuthoritativeMutation.State:
record.FinalPhysicsState = correctedState;
Assert.True(fixture.Controller.ApplyAuthoritativeState(
@ -477,10 +453,6 @@ public sealed class ProjectileControllerTests
Assert.Equal(correctedVelocity, body.Velocity);
Assert.Equal(correctedOmega, body.Omega);
break;
case AuthoritativeMutation.Position:
Assert.Equal(correctedPosition, body.Position);
Assert.Equal(correctedVelocity, body.Velocity);
break;
case AuthoritativeMutation.State:
Assert.Equal(correctedState, body.State);
break;
@ -856,14 +828,21 @@ public sealed class ProjectileControllerTests
}
[Fact]
public void MalformedFreshUpdates_DoNotPoisonCanonicalBodyOrPose()
public void MalformedFreshVectorUpdate_DoesNotPoisonCanonicalBody()
{
// C4 route 5: the Position half of this test moved. A malformed
// accepted-Position payload for a missile is rejected further
// upstream now — the shared CanAcceptPositionPayload gate
// (unchanged, its own tests still cover it directly) runs
// unconditionally in OnPosition BEFORE the D-P6 dispatch even
// decides this is a missile packet, so the malformed-payload swallow
// is exercised at that layer (LiveEntityNetworkUpdateController's own
// "invalid-payload swallow" test), not here.
var fixture = new Fixture();
LiveEntityRecord record = fixture.Spawn(instance: 1);
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
PhysicsBody body = record.PhysicsBody!;
Vector3 velocity = body.Velocity;
Vector3 position = body.Position;
Assert.True(fixture.Controller.ApplyAuthoritativeVector(
record,
@ -871,19 +850,6 @@ public sealed class ProjectileControllerTests
Vector3.Zero,
currentTime: 2.0));
Assert.Equal(velocity, body.Velocity);
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
record,
new Vector3(float.PositiveInfinity, 0f, 0f),
new Vector3(float.PositiveInfinity, 0f, 0f),
Quaternion.Identity,
Vector3.Zero,
CellA,
currentTime: 2.1,
liveCenterX: 1,
liveCenterY: 1));
Assert.Equal(position, body.Position);
Assert.Equal(position, record.WorldEntity!.Position);
}
[Fact]
@ -1333,16 +1299,33 @@ public sealed class ProjectileControllerTests
accepted.Position!.Value.LandblockId,
_ => throw new InvalidOperationException("re-entry recreated the entity")));
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
record,
worldPosition: new Vector3(12f, 10f, 5f),
cellLocalPosition: new Vector3(12f, 10f, 5f),
orientation: Quaternion.Identity,
velocity: Vector3.Zero,
fullCellId: CellA,
currentTime: 10.0,
liveCenterX: 1,
liveCenterY: 1));
// C4 route 5: the placement itself is Runtime-owned now
// (RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition,
// tested at tests/AcDream.Runtime.Tests). This test is about the
// shadow/clock re-entry after a pickup+leave-world, so the resolved
// destination is written directly and the presentation ack reuses
// the same production method the seam's caller uses.
// ApplyAuthoritativeState (unchanged) rebases the controller's own
// `_lastFiniteGameTime` to the packet's receipt time exactly like the
// deleted ApplyAuthoritativePosition's `currentTime` parameter used
// to — it is a same-value state re-assert (no functional State
// change), used here purely as the clock anchor.
Assert.True(fixture.Controller.ApplyAuthoritativeState(
record, record.FinalPhysicsState, currentTime: 10.0, 1, 1));
record.PhysicsBody!.SnapToCell(
CellA,
new Vector3(12f, 10f, 5f),
new Vector3(12f, 10f, 5f));
record.PhysicsBody.Orientation = Quaternion.Identity;
// D-P5: a Position packet no longer commits velocity (the retired
// behaviour this test used to lean on via the deleted method's
// `velocity: Vector3.Zero` argument). The Vector channel is a
// separate concern (untouched by route 5); stop the body directly so
// this test's clock/shadow assertions are not confounded by ordinary
// straight-line motion.
record.PhysicsBody.set_velocity(Vector3.Zero);
Assert.True(fixture.Live.RebucketLiveEntity(Guid, CellA));
Assert.True(fixture.Controller.SyncPresentationFromResolvedBody(record, 10.0));
// The incarnation-stable RetailObjectQuantumClock is canonical after
// the R6 cutover; PhysicsBody.LastUpdateTime is only a legacy absolute
// clock mirror and need not equal the packet receipt time once the
@ -1431,8 +1414,20 @@ public sealed class ProjectileControllerTests
Assert.Equal(0, fixture.Controller.Count);
}
/// <summary>
/// C4 route 5: the accepted-Position placement itself moved to
/// RuntimeRemotePlacementDriveController.ApplyAcceptedProjectilePosition,
/// which re-validates currency internally before writing (proof
/// obligation P3's argument, tested at tests/AcDream.Runtime.Tests). This
/// test now exercises the successor of the OLD method's post-commit
/// currency guard: ProjectileController.SyncPresentationFromResolvedBody
/// (D-P2's closing paragraph) must refuse to touch presentation once a
/// reentrant guid-reuse callback has already replaced the incarnation it
/// was called for — a stale ack must never write through to the
/// replacement.
/// </summary>
[Fact]
public void AuthoritativePosition_ReentrantGuidReuseStopsOldPostRebucketWork()
public void SyncPresentation_ReentrantGuidReuseNeverTouchesTheReplacement()
{
var fixture = new Fixture();
LiveEntityRecord first = fixture.Spawn(instance: 7);
@ -1450,22 +1445,28 @@ public sealed class ProjectileControllerTests
replacement = fixture.Spawn(instance: 8);
};
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
first,
worldPosition: new Vector3(10f, 202f, 5f),
cellLocalPosition: new Vector3(10f, 10f, 5f),
orientation: Quaternion.Identity,
velocity: new Vector3(44f, 0f, 0f),
fullCellId: CellB,
currentTime: 1.1,
liveCenterX: 1,
liveCenterY: 1));
// The trigger: a landblock unload is the SAME visibility-loss edge
// LandblockUnload_SuspendsProjectileAtVisibilityEdgeWithoutFrameScan
// uses. RebucketLiveEntity is deliberately NOT the trigger here — it
// suppresses ProjectionVisibilityChanged for its OWN guid mid-call
// (LiveEntityRuntime.OnSpatialVisibilityChanged's `_rebucketingGuid`
// guard), which is exactly why the real placement seam's reentrant
// hazard is observable at the SPATIAL edge, not through a caller's
// own rebucket.
fixture.Spatial.RemoveLandblock(0x0101FFFFu);
Assert.True(replaced);
// The stale ack for the SUPERSEDED incarnation must refuse.
Assert.False(fixture.Controller.SyncPresentationFromResolvedBody(first, 1.1));
Assert.NotNull(replacement);
Assert.True(fixture.Live.TryGetRecord(Guid, out var current));
Assert.Same(replacement, current);
Assert.Null(current.ProjectileRuntime);
Assert.Null(current.PhysicsBody);
// The replacement's own spawn position (Fixture.Spawn's default),
// untouched by the stale ack — the positive half of the assertion,
// not merely "the ack returned false".
Assert.Equal(new Vector3(10f, 10f, 5f), current.WorldEntity!.Position);
Assert.Equal(0, fixture.Controller.Count);
}
@ -1580,10 +1581,11 @@ public sealed class ProjectileControllerTests
}
}
// C4 route 5: Position is retired from this enum — see the comment on
// AuthoritativeMutationBetweenQuantumHalvesDiscardsPrediction.
public enum AuthoritativeMutation
{
Vector,
Position,
State,
}

View file

@ -0,0 +1,322 @@
using System.Numerics;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Tests.Entities;
/// <summary>
/// C4 route 5 (D-P1, REVISED after the review round — A2/R1):
/// <see cref="RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition"/>
/// derives <c>RuntimePositionEntityKind</c> from
/// <c>canonical.FinalPhysicsState &amp; PhysicsStateFlags.Missile</c>
/// CONJOINED with a bound, body-agreeing <c>RuntimeProjectile</c> — never
/// the Missile bit alone. Retail places every non-player object
/// unconditionally; a Missile-flagged record with no bound projectile
/// (<c>TryBind</c> permanently refused, or has not run yet) must still
/// classify Remote so it keeps tracking through the ordinary remote
/// placement path, exactly like the deleted <c>ApplyAuthoritativePosition</c>'s
/// <c>TryGetCurrent</c> fall-through. Mirrors
/// <see cref="RuntimeRemoteTeleportClassificationTests"/>'s fixture shape.
/// </summary>
public sealed class RuntimeProjectilePositionKindTests
{
private const uint Cell = 0x0101FFFFu;
private const uint OtherCell = 0x0102FFFFu;
[Fact]
public void MissileBitSetAndBound_ClassifiesProjectileAuthoritative()
{
using var lifetime = new RuntimeEntityObjectLifetime();
lifetime.BindEventContext(static () => new RuntimeGenerationToken(1), static () => 1UL);
const uint guid = 0x70006001u;
RuntimeEntityRecord canonical =
lifetime.RegisterEntity(Spawn(guid, Cell, instance: 1)).Canonical!;
lifetime.Entities.SetFinalPhysicsState(
canonical,
canonical.FinalPhysicsState | PhysicsStateFlags.Missile);
BindProjectile(lifetime, canonical, Cell);
WorldSession.EntityPositionUpdate update = PositionUpdate(
guid, OtherCell, positionSequence: 2, teleportSequence: 0);
Assert.True(lifetime.TryApplyPosition(
update,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
acknowledgeProjection: null,
out PositionTimestampDisposition disposition,
out _,
out AcceptedPhysicsTimestamps timestamps));
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord after));
Assert.True((after.FinalPhysicsState & PhysicsStateFlags.Missile) != 0);
Assert.NotNull(after.Projectile);
RuntimeAuthoritativePositionRoute? route = lifetime.ClassifyRemoteAcceptedPosition(
after, update, disposition, timestamps, playerDistance: 10f);
Assert.NotNull(route);
Assert.Equal(
RuntimeSetPositionOperationKind.ProjectileAuthoritative,
route!.Value.OperationKind);
}
/// <summary>
/// A2/R1: the Missile bit alone is not sufficient. TryBind's permanent
/// refusal (an unsupported multi-sphere Setup) or the pre-bind window
/// leaves <c>record.Projectile</c> null while the bit stays set — this
/// must still classify Remote so the packet is placed by the ordinary
/// remote path, never silently dropped.
/// </summary>
[Fact]
public void MissileBitSetButUnbound_ClassifiesRemoteAuthoritative()
{
using var lifetime = new RuntimeEntityObjectLifetime();
lifetime.BindEventContext(static () => new RuntimeGenerationToken(1), static () => 1UL);
const uint guid = 0x70006005u;
RuntimeEntityRecord canonical =
lifetime.RegisterEntity(Spawn(guid, Cell, instance: 1)).Canonical!;
lifetime.Entities.SetFinalPhysicsState(
canonical,
canonical.FinalPhysicsState | PhysicsStateFlags.Missile);
// Deliberately never bind a RuntimeProjectile.
Assert.Null(canonical.Projectile);
WorldSession.EntityPositionUpdate update = PositionUpdate(
guid, OtherCell, positionSequence: 2, teleportSequence: 0);
Assert.True(lifetime.TryApplyPosition(
update,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
acknowledgeProjection: null,
out PositionTimestampDisposition disposition,
out _,
out AcceptedPhysicsTimestamps timestamps));
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord after));
Assert.True((after.FinalPhysicsState & PhysicsStateFlags.Missile) != 0);
Assert.Null(after.Projectile);
RuntimeAuthoritativePositionRoute? route = lifetime.ClassifyRemoteAcceptedPosition(
after, update, disposition, timestamps, playerDistance: 10f);
Assert.NotNull(route);
Assert.Equal(
RuntimeSetPositionOperationKind.RemoteAuthoritative,
route!.Value.OperationKind);
}
[Fact]
public void MissileBitClear_ClassifiesRemoteAuthoritative()
{
using var lifetime = new RuntimeEntityObjectLifetime();
lifetime.BindEventContext(static () => new RuntimeGenerationToken(1), static () => 1UL);
const uint guid = 0x70006002u;
RuntimeEntityRecord canonical =
lifetime.RegisterEntity(Spawn(guid, Cell, instance: 1)).Canonical!;
Assert.True((canonical.FinalPhysicsState & PhysicsStateFlags.Missile) == 0);
WorldSession.EntityPositionUpdate update = PositionUpdate(
guid, OtherCell, positionSequence: 2, teleportSequence: 0);
Assert.True(lifetime.TryApplyPosition(
update,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
acknowledgeProjection: null,
out PositionTimestampDisposition disposition,
out _,
out AcceptedPhysicsTimestamps timestamps));
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord after));
RuntimeAuthoritativePositionRoute? route = lifetime.ClassifyRemoteAcceptedPosition(
after, update, disposition, timestamps, playerDistance: 10f);
Assert.NotNull(route);
Assert.Equal(
RuntimeSetPositionOperationKind.RemoteAuthoritative,
route!.Value.OperationKind);
}
/// <summary>
/// Trap T4 / invariant 8's mutual-exclusion proof, exercised at the flip
/// itself: a State packet installing Missile mid-life (ACE's ordinary
/// arrow-becomes-live-missile edge, or its converse on impact) makes the
/// VERY NEXT Position packet classify the OTHER kind — no stale
/// classification survives the flip.
/// </summary>
[Fact]
public void MissileBitFlipMidLife_NextPositionReclassifies()
{
using var lifetime = new RuntimeEntityObjectLifetime();
lifetime.BindEventContext(static () => new RuntimeGenerationToken(1), static () => 1UL);
const uint guid = 0x70006003u;
RuntimeEntityRecord canonical =
lifetime.RegisterEntity(Spawn(guid, Cell, instance: 1)).Canonical!;
Assert.True((canonical.FinalPhysicsState & PhysicsStateFlags.Missile) == 0);
WorldSession.EntityPositionUpdate firstUpdate = PositionUpdate(
guid, OtherCell, positionSequence: 2, teleportSequence: 0);
Assert.True(lifetime.TryApplyPosition(
firstUpdate,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
acknowledgeProjection: null,
out PositionTimestampDisposition firstDisposition,
out _,
out AcceptedPhysicsTimestamps firstTimestamps));
Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord beforeFlip));
RuntimeAuthoritativePositionRoute? beforeRoute =
lifetime.ClassifyRemoteAcceptedPosition(
beforeFlip, firstUpdate, firstDisposition, firstTimestamps, playerDistance: 10f);
Assert.Equal(
RuntimeSetPositionOperationKind.RemoteAuthoritative,
beforeRoute!.Value.OperationKind);
// A State packet (0x0013-family) installs Missile — the classifier
// itself never sees a State packet; only the NEXT Position does.
lifetime.Entities.SetFinalPhysicsState(
beforeFlip,
beforeFlip.FinalPhysicsState | PhysicsStateFlags.Missile);
// TryBind's production ordering: a State packet setting Missile is
// immediately followed by binding (ApplyAuthoritativeState ->
// TryBind). A2/R1 pins the classifier on the BOUND shape, so this
// scenario's flip is only complete once the component exists too.
BindProjectile(lifetime, beforeFlip, OtherCell);
WorldSession.EntityPositionUpdate secondUpdate = PositionUpdate(
guid, Cell, positionSequence: 3, teleportSequence: 0);
Assert.True(lifetime.TryApplyPosition(
secondUpdate,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
acknowledgeProjection: null,
out PositionTimestampDisposition secondDisposition,
out _,
out AcceptedPhysicsTimestamps secondTimestamps));
Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord afterFlip));
RuntimeAuthoritativePositionRoute? afterRoute =
lifetime.ClassifyRemoteAcceptedPosition(
afterFlip, secondUpdate, secondDisposition, secondTimestamps, playerDistance: 10f);
Assert.NotNull(afterRoute);
Assert.Equal(
RuntimeSetPositionOperationKind.ProjectileAuthoritative,
afterRoute!.Value.OperationKind);
}
/// <summary>
/// Attaches a canonical body (if not already present) and binds a
/// <c>RuntimeProjectile</c> to it through the SAME production entry
/// point <c>ProjectileController.TryBind</c> eventually calls
/// (<c>RuntimePhysicsState.BindProjectile</c>) — the classifier's
/// conjunctive test (A2/R1) reads exactly this state.
/// </summary>
private static void BindProjectile(
RuntimeEntityObjectLifetime lifetime,
RuntimeEntityRecord record,
uint cellId)
{
if (record.PhysicsBody is not { } body)
{
body = new PhysicsBody
{
Position = new Vector3(10f, 20f, 5f),
Orientation = Quaternion.Identity,
LastUpdateTime = 1d,
State = record.FinalPhysicsState,
TransientState = TransientStateFlags.Active,
};
body.SnapToCell(cellId, body.Position, body.Position);
lifetime.Entities.SetPhysicsBody(record, body);
}
lifetime.Physics.BindProjectile(
record, body, new ProjectileCollisionSphere(Vector3.Zero, 0.1f, 1f));
}
private static WorldSession.EntityPositionUpdate PositionUpdate(
uint guid,
uint cellId,
ushort positionSequence,
ushort teleportSequence) =>
new(
guid,
new CreateObject.ServerPosition(
cellId, 12f, 14f, 7f, 1f, 0f, 0f, 0f),
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: positionSequence,
TeleportSequence: teleportSequence,
ForcePositionSequence: 0);
private static WorldSession.EntitySpawn Spawn(
uint guid,
uint cellId,
ushort instance)
{
var position = new CreateObject.ServerPosition(
cellId, 10f, 20f, 5f, 1f, 0f, 0f, 0f);
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: instance);
var physics = new PhysicsSpawnData(
RawState: 0x408u,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
position,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
null,
null,
"remote-projectile-kind",
null,
null,
0x09000001u,
PhysicsState: 0x408u,
InstanceSequence: instance,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
}

View file

@ -117,9 +117,16 @@ public sealed class RuntimeRemotePlacementDriveControllerTests
/// teleport branches (<c>RuntimeSetPositionOperationKind.LocalAuthoritative</c>),
/// not only for remotes. The static predicate itself is exercised
/// directly (no entity/body needed) since it takes only the route.
///
/// <para>
/// C4 route 5 (D-P3): <c>ProjectileAuthoritative</c> is REMOVED from this
/// negative list — the widening makes it a positively-owned kind now
/// (see <see cref="OwnsPlacement_TrueForProjectileAuthoritative_SetPositionAndSetPositionSimple"/>).
/// Only the two kinds that stay excluded remain here.
/// </para>
/// </summary>
[Fact]
public void OwnsPlacement_FalseWhenOperationKindIsNotRemoteAuthoritative()
public void OwnsPlacement_FalseWhenOperationKindIsNotRemoteOrProjectileAuthoritative()
{
// RuntimeSetPositionOperationKind is internal, so a public [Theory]
// cannot take it as a parameter (CS0051) — iterate directly instead,
@ -130,7 +137,6 @@ public sealed class RuntimeRemotePlacementDriveControllerTests
{
RuntimeSetPositionOperationKind.InitialLogin,
RuntimeSetPositionOperationKind.LocalAuthoritative,
RuntimeSetPositionOperationKind.ProjectileAuthoritative,
})
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
@ -2256,6 +2262,845 @@ public sealed class RuntimeRemotePlacementDriveControllerTests
AssertConverged(lifetime);
}
// ── C4 route 5: projectile arm (D-P2/D-P3/D-P4/D-P5) ───────────────────
/// <summary>
/// D-P3: the widening itself, isolated from any entity/body — mirrors
/// <see cref="OwnsPlacement_FalseWhenOperationKindIsNotRemoteAuthoritative"/>'s
/// shape but for the positive case.
/// </summary>
[Fact]
public void OwnsPlacement_TrueForProjectileAuthoritative_SetPositionAndSetPositionSimple()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70004001u);
foreach (RuntimeAuthoritativePositionDisposition disposition in
new[]
{
RuntimeAuthoritativePositionDisposition.SetPosition,
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
})
{
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
disposition,
DestinationCell,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.True(RuntimeRemotePlacementDriveController.OwnsPlacement(route));
}
// A projectile Create (InitialCreateFlags, no Teleport bit) is still
// excluded — the same Teleport-flag discriminator that excludes a
// remote top-level Create.
RuntimeAuthoritativePositionRoute createRoute = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative,
setPositionFlags: PhysicsSetPositionFlags.Placement
| PhysicsSetPositionFlags.Slide);
Assert.False(RuntimeRemotePlacementDriveController.OwnsPlacement(createRoute));
}
[Fact]
public void ApplyAcceptedProjectilePosition_Null_WhenOperationKindIsNotProjectileAuthoritative()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
(RuntimeEntityRecord record, _) = CreateProjectileRecord(
lifetime, 0x70004002u, SourceCell);
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
operationKind: RuntimeSetPositionOperationKind.RemoteAuthoritative);
Assert.Null(drive.ApplyAcceptedProjectilePosition(record, route));
Assert.Equal(0, drive.PendingCount);
AssertConverged(lifetime);
}
[Fact]
public void ApplyAcceptedProjectilePosition_Null_WhenNoProjectileComponentIsBound()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70004003u);
AttachBody(lifetime, record, SourceCell);
// Deliberately never BindProjectile — record.Projectile stays null.
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.Null(drive.ApplyAcceptedProjectilePosition(record, route));
}
/// <summary>
/// D-P2's teleport/cell-less row + D-P4's force-end + D-P5's no-velocity,
/// asserted together on the ONE committed outcome (process rule 4 —
/// assert the full observable surface, not a subset). The body moves to
/// the resolved (world-frame-shifted) destination, the prediction version
/// advances (trap T3's guard), an in-flight nonzero velocity survives
/// bit-identical (D-P5), no <c>RemoteMotion</c>/constraint host exists
/// anywhere for the entity (D-P4's never-armed pin), and the collision
/// table the teleport hook reduction force-ends is empty afterward
/// (proof obligation P5).
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_TeleportCommit_MovesBodyForceEndsCollisionNoVelocityNoConstraint()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
CommitLandblockCollision(lifetime, DestinationLandblock);
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(lifetime, 0x70004004u, SourceCell);
PhysicsBody body = record.PhysicsBody!;
var inFlightVelocity = new Vector3(5f, 0f, -2f);
body.set_velocity(inFlightVelocity);
ulong predictionBefore = projectile.PredictionAuthorityVersion;
SeedCollisionOwner(lifetime, record, 0x70004104u, SourceCell);
Assert.Equal(
1, lifetime.Physics.CollisionReports.CaptureOwnership().OwnerCount);
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
// Airborne (well above CommitLandblockCollision's flat terrain at
// SpawnHeight): landing in ground contact would legitimately let the
// shared placement pipeline's ordinary contact response touch
// velocity (retail landing behaviour, not this route's concern) —
// an airborne destination isolates the no-velocity-FROM-THE-PACKET
// assertion from that confound.
var destination = new Vector3(12f, 14f, SpawnHeight + 10f);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
RuntimeRemotePlacementExecutionStatus? status =
drive.ApplyAcceptedProjectilePosition(record, route);
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Committed, status);
Assert.Equal(destination + new Vector3(192f, 0f, 0f), body.Position);
Assert.NotEqual(predictionBefore, projectile.PredictionAuthorityVersion);
Assert.Equal(inFlightVelocity, body.Velocity);
Assert.Null(record.RemoteMotion);
Assert.Equal(
0, lifetime.Physics.CollisionReports.CaptureOwnership().OwnerCount);
// A4 fix (review round): the shadow-sync half of
// SyncProjectilePresentation, asserted directly rather than left
// vacuous — the shadow row moves to the RESOLVED body position.
Assert.True(body.InWorld);
ShadowEntry shadowEntry = Assert.Single(
lifetime.Physics.Engine.ShadowObjects.AllEntriesForDebug(),
entry => entry.EntityId == record.Key!.Value.LocalEntityId);
Assert.Equal(body.Position, shadowEntry.Position);
DrainPlacementFifo(lifetime);
AssertConverged(lifetime);
}
/// <summary>D-P2's far row, mirroring the teleport commit's assertions.</summary>
[Fact]
public void ApplyAcceptedProjectilePosition_FarCommit_MovesBodyNoVelocityNoConstraint()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
CommitLandblockCollision(lifetime, DestinationLandblock);
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(lifetime, 0x70004005u, SourceCell);
PhysicsBody body = record.PhysicsBody!;
var inFlightVelocity = new Vector3(0f, 7f, 1f);
body.set_velocity(inFlightVelocity);
ulong predictionBefore = projectile.PredictionAuthorityVersion;
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight + 10f);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
RuntimeRemotePlacementExecutionStatus? status =
drive.ApplyAcceptedProjectilePosition(record, route);
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Committed, status);
Assert.Equal(destination + new Vector3(192f, 0f, 0f), body.Position);
Assert.NotEqual(predictionBefore, projectile.PredictionAuthorityVersion);
Assert.Equal(inFlightVelocity, body.Velocity);
Assert.Null(record.RemoteMotion);
Assert.True(body.InWorld);
ShadowEntry shadowEntry = Assert.Single(
lifetime.Physics.Engine.ShadowObjects.AllEntriesForDebug(),
entry => entry.EntityId == record.Key!.Value.LocalEntityId);
Assert.Equal(body.Position, shadowEntry.Position);
DrainPlacementFifo(lifetime);
AssertConverged(lifetime);
}
/// <summary>
/// A4 fix (review round): the spatial+hidden branch — the entity stays
/// <c>InWorld</c> (retail keeps a Hidden object as a retained live
/// <c>CPhysicsObj</c>, not a leave-world) but its shadow row is
/// suspended, not published at the resolved pose.
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_TeleportCommit_HiddenSuspendsShadowStaysInWorld()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
CommitLandblockCollision(lifetime, DestinationLandblock);
(RuntimeEntityRecord record, _) =
CreateProjectileRecord(lifetime, 0x7000400Bu, SourceCell);
Assert.Equal(1, lifetime.Physics.Engine.ShadowObjects.TotalRegistered);
lifetime.Entities.SetFinalPhysicsState(
record, record.FinalPhysicsState | PhysicsStateFlags.Hidden);
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight + 10f);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.Equal(
RuntimeRemotePlacementExecutionStatus.Committed,
drive.ApplyAcceptedProjectilePosition(record, route));
Assert.True(record.PhysicsBody!.InWorld);
Assert.Equal(0, lifetime.Physics.Engine.ShadowObjects.TotalRegistered);
DrainPlacementFifo(lifetime);
AssertConverged(lifetime);
}
/// <summary>
/// A4 fix (review round): the non-spatial branch — a record that never
/// became a spatial root (e.g. still pending a landblock) is left
/// <c>InWorld = false</c>, its <c>Active</c> transient flag cleared, and
/// its shadow suspended.
///
/// <para>
/// Uses the STORE (<c>Refused</c>) path rather than a commit: a
/// successful canonical commit re-establishes spatial-root status as
/// part of entering the world, so the non-spatial branch is reachable
/// only through the outcomes that never touch spatial registration —
/// exactly the store fallback's shape.
/// </para>
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_Refused_NonSpatialDeactivatesAndSuspends()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
lifetime.Physics.ObserveLocalWorldFrame(SourceCell, teleportAdvanced: false);
(RuntimeEntityRecord record, _) =
CreateProjectileRecord(lifetime, 0x7000400Cu, SourceCell);
Assert.Equal(1, lifetime.Physics.Engine.ShadowObjects.TotalRegistered);
// Withdraw spatial-root status — AcknowledgeSpatialProjection
// (spatial: false) is a no-op (only its `true` branch touches
// _spatialRoots); RemoveSpatialProjection is the actual withdrawal.
lifetime.Physics.RemoveSpatialProjection(record);
var window = new FakeServiceWindow();
// Deliberately NOT allowed — the pre-flight refuses, so the store
// fallback runs without ever touching spatial registration.
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight + 10f);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.Equal(
RuntimeRemotePlacementExecutionStatus.Refused,
drive.ApplyAcceptedProjectilePosition(record, route));
Assert.False(record.PhysicsBody!.InWorld);
Assert.Equal(
TransientStateFlags.None,
record.PhysicsBody.TransientState & TransientStateFlags.Active);
Assert.Equal(0, lifetime.Physics.Engine.ShadowObjects.TotalRegistered);
DrainPlacementFifo(lifetime);
AssertConverged(lifetime);
}
/// <summary>
/// A6 fix (review round): the re-entry activation edge. A projectile
/// that had left the world (suspended, <c>InWorld = false</c>) and comes
/// back through a committed accepted Position must be re-flagged
/// <c>Active</c> and have its legacy <c>LastUpdateTime</c> rebased — the
/// exact branch that reading <c>body.InWorld</c> AFTER the placement
/// (instead of capturing it before) made permanently dead.
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_TeleportCommit_ReenteringWorldReactivatesBody()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
CommitLandblockCollision(lifetime, DestinationLandblock);
(RuntimeEntityRecord record, _) =
CreateProjectileRecord(lifetime, 0x7000400Du, SourceCell);
PhysicsBody body = record.PhysicsBody!;
body.InWorld = false;
body.TransientState &= ~TransientStateFlags.Active;
body.LastUpdateTime = -1d;
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight + 10f);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.Equal(
RuntimeRemotePlacementExecutionStatus.Committed,
drive.ApplyAcceptedProjectilePosition(record, route));
Assert.True(body.InWorld);
Assert.Equal(
TransientStateFlags.Active,
body.TransientState & TransientStateFlags.Active);
Assert.NotEqual(-1d, body.LastUpdateTime);
DrainPlacementFifo(lifetime);
AssertConverged(lifetime);
}
/// <summary>
/// D3's <c>Refused</c> row: the destination is outside the service
/// window, so the pre-flight declines before the engine ever runs — but
/// the accepted destination STILL advances through
/// <c>StoreAcceptedDestinationPose</c> (4b-3 invariant 1, extended by
/// D-P3 to the projectile column). Positive assertions throughout
/// (round-2 finding B1): the pose moved, the entity stayed in-world, and
/// prediction still invalidated once (the store fallback is a body write
/// too).
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_Refused_StillAdvancesPoseNoParkPredictionInvalidated()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
// StoreAcceptedDestinationPose resolves the destination through
// Runtime's OWN world frame (never a caller-supplied position) —
// establish it exactly like CommitLandblockCollision's first step,
// without needing the destination's collision generation to commit
// (this test never reaches the engine).
lifetime.Physics.ObserveLocalWorldFrame(SourceCell, teleportAdvanced: false);
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(lifetime, 0x70004006u, SourceCell);
PhysicsBody body = record.PhysicsBody!;
ulong predictionBefore = projectile.PredictionAuthorityVersion;
var window = new FakeServiceWindow();
// Deliberately NOT allowed — the pre-flight refuses.
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
RuntimeRemotePlacementExecutionStatus? status =
drive.ApplyAcceptedProjectilePosition(record, route);
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Refused, status);
// The store fallback resolves through Runtime's world frame, the
// same +192m shift on X the committed-outcome tests observe.
Assert.Equal(destination + new Vector3(192f, 0f, 0f), body.Position);
Assert.NotEqual(predictionBefore, projectile.PredictionAuthorityVersion);
Assert.True(body.InWorld);
Assert.True(record.ObjectClock.IsActive);
Assert.Equal(0, drive.PendingCount);
// Residual 2 close (round-2 review): the store path publishes the
// shadow row too, not only the commit path the teleport/far commit
// tests already assert — SyncProjectilePresentation runs on every
// storing outcome, Refused included.
ShadowEntry shadowEntry = Assert.Single(
lifetime.Physics.Engine.ShadowObjects.AllEntriesForDebug(),
entry => entry.EntityId == record.Key!.Value.LocalEntityId);
Assert.Equal(body.Position, shadowEntry.Position);
AssertConverged(lifetime);
}
/// <summary>
/// D-P4's pinned no-op pair: <c>Interpolate</c> (near) and
/// <c>NoPositionOperation</c> (airborne) write nothing and do not
/// invalidate prediction — the positive fact that a straddling quantum
/// may complete over either.
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_PinnedNoOps_BodyAndPredictionUnchanged()
{
// RuntimeAuthoritativePositionDisposition is internal, so a public
// [Theory] cannot take it as a parameter (CS0051) — iterate directly,
// mirroring OwnsPlacement_FalseWhenOperationKindIsNotRemoteAuthoritative's
// own foreach-over-internal-enum shape.
foreach (RuntimeAuthoritativePositionDisposition disposition in
new[]
{
RuntimeAuthoritativePositionDisposition.Interpolate,
RuntimeAuthoritativePositionDisposition.NoPositionOperation,
})
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(lifetime, 0x70004007u, SourceCell);
PhysicsBody body = record.PhysicsBody!;
Vector3 positionBefore = body.Position;
Quaternion orientationBefore = body.Orientation;
ulong predictionBefore = projectile.PredictionAuthorityVersion;
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
disposition,
DestinationCell,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.Null(drive.ApplyAcceptedProjectilePosition(record, route));
Assert.Equal(positionBefore, body.Position);
Assert.Equal(orientationBefore, body.Orientation);
Assert.Equal(predictionBefore, projectile.PredictionAuthorityVersion);
Assert.Null(record.RemoteMotion);
Assert.Equal(0, drive.PendingCount);
AssertConverged(lifetime);
}
}
/// <summary>
/// D-P4's swallow rule (trap T5): a <c>RejectedAuthority</c>/
/// <c>RejectedData</c> classification for a missile packet writes
/// nothing — no body write, no store, no fall-through to any remote arm
/// (there is none reachable from this method regardless).
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_RejectedClassification_Swallowed()
{
foreach (RuntimeAuthoritativePositionDisposition disposition in
new[]
{
RuntimeAuthoritativePositionDisposition.RejectedAuthority,
RuntimeAuthoritativePositionDisposition.RejectedData,
})
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(lifetime, 0x70004008u, SourceCell);
PhysicsBody body = record.PhysicsBody!;
Vector3 positionBefore = body.Position;
ulong predictionBefore = projectile.PredictionAuthorityVersion;
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
disposition,
DestinationCell,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.Null(drive.ApplyAcceptedProjectilePosition(record, route));
Assert.Equal(positionBefore, body.Position);
Assert.Equal(predictionBefore, projectile.PredictionAuthorityVersion);
Assert.Equal(0, drive.PendingCount);
AssertConverged(lifetime);
}
}
/// <summary>
/// Test-plan item 6 / proof obligation-11: the SAME "retryable
/// preparation" shape <see cref="Teleport_LedgerConverges_AfterDetachRouteClearsARetainedRetry"/>
/// uses, driven through the projectile arm — proves the shared
/// <c>_pending</c>/<c>_awaitingAcknowledgement</c> ledgers converge for a
/// projectile operation with no new code (trap T9: no second map).
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_LedgerConverges_AfterDetachRouteClearsARetainedRetry()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
CommitLandblockCollision(lifetime, DestinationLandblock);
(RuntimeEntityRecord record, _) = CreateProjectileRecord(
lifetime, 0x70004009u, SourceCell);
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var routeOwner = new object();
drive.AttachRoute(routeOwner);
var destination = new Vector3(12f, 14f, SpawnHeight);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
RuntimeRemotePlacementExecutionStatus? status =
drive.ApplyAcceptedProjectilePosition(record, route);
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Committed, status);
// A committed placement's Place receipt is never acknowledged in
// this bare fixture (no host subscription wired) — exactly the
// awaiting-acknowledgement dimension the ledger must also converge.
Assert.Equal(
1, lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
drive.DetachRoute(routeOwner);
Assert.Equal(0, drive.PendingCount);
Assert.Equal(
0, lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
AssertConverged(lifetime);
}
/// <summary>
/// Round-3 architecture review C1: <c>Advance()</c>'s projectile branch
/// (parked at round 2 / R3, reordered at round 2 / B5) had never been
/// executed by any test — all six pre-existing <c>drive.Advance()</c>
/// call sites in this file are remote-kind. This is the re-parked-
/// <c>Contention</c> half, mirroring
/// <see cref="FarSnap_RetryablePreparation_StoresThePoseAndStillRetainsTheRetry"/>
/// against a projectile instead of a remote: <c>UnusedCollisionSource</c>
/// never resolves a nonzero Setup id, so BOTH the entry-point call and
/// the retry keep returning <c>RetrySetupUnavailable</c> —
/// <c>Contention</c> — and the pending entry never drains on its own.
///
/// <para>
/// The B5 semantic change under test: the entry-point call invalidates
/// prediction unconditionally BEFORE the write (the existing, already-
/// asserted behaviour); the RETRY call must NOT invalidate a second time
/// when it re-parks, because a re-parked <c>Contention</c> writes
/// nothing (no store, no commit) — invalidating for it would violate
/// "the no-op dispositions invalidate nothing" on an arm that wrote
/// nothing. <see cref="RuntimeProjectile.PredictionAuthorityVersion"/>
/// captured immediately before and after <c>Advance()</c> must be equal.
/// </para>
/// </summary>
[Fact]
public void Advance_ProjectileRetryReParksAsContention_PredictionNotInvalidatedASecondTime()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
// Deliberately NOT committing DestinationLandblock's collision
// generation — CanAttemptDestination only tests the service window
// and the collision PREFIX quiescence, neither of which this
// scenario needs to fail; the retryable failure comes from the
// Setup read below, exactly like FarSnap_RetryablePreparation_….
lifetime.Physics.ObserveLocalWorldFrame(SourceCell, teleportAdvanced: false);
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(
lifetime,
0x7000400Bu,
SourceCell,
setupTableId: 0x02000001u);
PhysicsBody body = record.PhysicsBody!;
Vector3 positionBefore = body.Position;
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative,
stopInterpolating: true);
// Entry point: Contention, retained, and prediction invalidated
// exactly once (the pre-existing, already-tested entry-point
// behaviour — asserted again here only as the retry's baseline).
Assert.Equal(
RuntimeRemotePlacementExecutionStatus.Contention,
drive.ApplyAcceptedProjectilePosition(record, route));
Assert.Equal(1, drive.PendingCount);
Assert.Equal(destination + new Vector3(192f, 0f, 0f), body.Position);
Assert.NotEqual(positionBefore, body.Position);
ulong predictionAfterEntry = projectile.PredictionAuthorityVersion;
// The retry: Setup is still unresolved, so SubmitAndResolve returns
// Contention again and re-parks — the B5 no-invalidate branch.
drive.Advance();
Assert.Equal(1, drive.PendingCount);
Assert.Equal(
predictionAfterEntry, projectile.PredictionAuthorityVersion);
// The re-park wrote nothing — the stored pose from the entry point
// is untouched.
Assert.Equal(destination + new Vector3(192f, 0f, 0f), body.Position);
RuntimePlacementCancellationReceipt cancellation =
lifetime.Physics.SetPosition.Forget(record);
if (cancellation.IsValid)
lifetime.Physics.SetPosition.PublishCancellation(cancellation);
drive.Advance();
Assert.Equal(0, drive.PendingCount);
AssertConverged(lifetime);
}
/// <summary>
/// Round-3 architecture review C1, the second half: a retained
/// projectile retry whose destination leaves the service window before
/// the next cadence pump — mirroring
/// <see cref="Advance_DestinationLeavesTheWindow_StoresTheNewestDestinationPose"/>
/// against a projectile. This exercises the STORING side C1 named as
/// unexercised: the retry's window-drop branch invalidates prediction
/// unconditionally (unlike the re-parked-<c>Contention</c> branch above)
/// because it runs <c>StoreAcceptedDestinationPose</c> — a real body
/// write — and it is the second call site (besides the entry point) that
/// must run <see cref="RuntimeRemotePlacementDriveController"/>'s
/// <c>SyncProjectilePresentation</c>, so the shadow row must follow the
/// body here too, closing B3's remaining retry-arm gap.
/// </summary>
[Fact]
public void Advance_ProjectileRetryDestinationLeavesTheWindow_StoresNewestPoseInvalidatesPredictionSyncsShadow()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
lifetime.Physics.ObserveLocalWorldFrame(SourceCell, teleportAdvanced: false);
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(
lifetime,
0x7000400Cu,
SourceCell,
setupTableId: 0x02000001u);
PhysicsBody body = record.PhysicsBody!;
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var firstDestination = new Vector3(12f, 14f, SpawnHeight);
Assert.Equal(
RuntimeRemotePlacementExecutionStatus.Contention,
drive.ApplyAcceptedProjectilePosition(
record,
MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
DestinationCell,
firstDestination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative,
stopInterpolating: true)));
Assert.Equal(1, drive.PendingCount);
ulong predictionAfterEntry = projectile.PredictionAuthorityVersion;
// The server keeps broadcasting while the retry sits retained: the
// accepted snapshot moves on, and the destination falls out of the
// service window before the next cadence pump.
var newestDestination = new Vector3(40f, 50f, SpawnHeight);
record.Snapshot = record.Snapshot with
{
Position = new CreateObject.ServerPosition(
DestinationCell,
newestDestination.X,
newestDestination.Y,
newestDestination.Z,
1f,
0f,
0f,
0f),
};
window.Forbid(DestinationLandblock);
drive.Advance();
Assert.Equal(0, drive.PendingCount);
Assert.Equal(
newestDestination + new Vector3(192f, 0f, 0f), body.Position);
Assert.NotEqual(
predictionAfterEntry, projectile.PredictionAuthorityVersion);
// SyncProjectilePresentation ran on the retry arm too — the shadow
// row followed the body to the newest stored pose.
ShadowEntry shadowEntry = Assert.Single(
lifetime.Physics.Engine.ShadowObjects.AllEntriesForDebug(),
entry => entry.EntityId == record.Key!.Value.LocalEntityId);
Assert.Equal(body.Position, shadowEntry.Position);
Assert.Equal(
0, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
AssertConverged(lifetime);
}
/// <summary>
/// Test-plan item 4 (trap T3): a split quantum straddling an accepted
/// far/teleport Position must abort at <c>Complete</c> rather than
/// clobber the committed placement — the scenario invisible from reading
/// the classifier alone, and the one this route's App-level predecessor
/// test (<c>AuthoritativeMutationBetweenQuantumHalvesDiscardsPrediction</c>)
/// used to cover before its Position case retired.
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_DuringOpenQuantum_CompleteAbortsAfterPredictionInvalidated()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
CommitLandblockCollision(lifetime, DestinationLandblock);
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(lifetime, 0x7000400Au, SourceCell);
var updater = new RuntimeProjectilePhysicsUpdater(lifetime.Physics);
Assert.True(updater.TryBegin(
record,
quantum: 0.05f,
record.ObjectClockEpoch,
externalOwnerValid: null,
out RuntimeProjectilePhysicsCommit commit));
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
RuntimeRemotePlacementExecutionStatus? status =
drive.ApplyAcceptedProjectilePosition(record, route);
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Committed, status);
Vector3 committedPosition = record.PhysicsBody!.Position;
bool completed = updater.Complete(
commit,
liveCenterX: 0,
liveCenterY: 0,
acknowledgeProjection: static _ => true);
Assert.False(completed);
Assert.Equal(committedPosition, record.PhysicsBody.Position);
DrainPlacementFifo(lifetime);
AssertConverged(lifetime);
}
private static (RuntimeEntityRecord Record, RuntimeProjectile Projectile) CreateProjectileRecord(
RuntimeEntityObjectLifetime lifetime,
uint guid,
uint cellId,
bool registerShadow = true,
// C1 fix (round-3 architecture review): a nonzero setupTableId is
// what makes CanonicalSetupTableId != 0, which is what makes
// TryPrepareAuthoredMover actually consult UnusedCollisionSource
// (RuntimeSetPositionState.cs:1900-1918) instead of taking the
// ResolvedAbsent no-Setup path every other projectile fixture in
// this file relies on. Default null preserves every existing
// caller's behaviour exactly (id 0, ResolvedAbsent, always
// Prepared) — only the two new retry-arm tests pass a real id to
// deliberately provoke RetrySetupUnavailable.
uint? setupTableId = null)
{
RuntimeEntityRecord record = CreateRemoteRecord(
lifetime, guid, setupTableId);
lifetime.Entities.SetFinalPhysicsState(
record,
PhysicsStateFlags.Gravity
| PhysicsStateFlags.Missile
| PhysicsStateFlags.ReportCollisions);
PhysicsBody body = AttachBody(lifetime, record, cellId);
var sphere = new ProjectileCollisionSphere(Vector3.Zero, 0.1f, 1f);
var projectile = (RuntimeProjectile)lifetime.Physics.BindProjectile(
record, body, sphere);
// A4 fix (review round): a shadow registration is the prerequisite
// for ShadowObjectRegistry.UpdatePosition to do anything at all
// (it early-returns "not registered" otherwise) — without this, a
// test could assert the shadow-sync branch ran while
// SyncProjectilePresentation's shadow write was silently a no-op.
if (registerShadow)
{
lifetime.Physics.Engine.ShadowObjects.Register(
record.Key!.Value.LocalEntityId,
gfxObjId: 0u,
body.Position,
body.Orientation,
radius: 0.1f,
worldOffsetX: 0f,
worldOffsetY: 0f,
cellId & 0xFFFF0000u,
ShadowCollisionType.Sphere,
state: (uint)record.FinalPhysicsState,
seedCellId: cellId,
isStatic: false);
}
return (record, projectile);
}
/// <summary>
/// Seeds one collision-table owner row on <paramref name="owner"/> via
/// a peer entity's dynamic shadow + one reported collision — the same
/// mechanism <c>RuntimeCollisionReportingStateTests</c> uses, reduced to
/// the minimum this file's proof obligation P5 needs.
/// </summary>
private static void SeedCollisionOwner(
RuntimeEntityObjectLifetime lifetime,
RuntimeEntityRecord owner,
uint peerGuid,
uint cellId)
{
RuntimeEntityRecord peer = CreateRemoteRecord(lifetime, peerGuid);
AttachBody(lifetime, peer, cellId);
uint peerLocalId = peer.Key!.Value.LocalEntityId;
lifetime.Physics.Engine.ShadowObjects.Register(
peerLocalId,
gfxObjId: 0u,
peer.PhysicsBody!.Position,
Quaternion.Identity,
radius: 0.4f,
worldOffsetX: 0f,
worldOffsetY: 0f,
cellId & 0xFFFF0000u,
ShadowCollisionType.Sphere,
state: (uint)peer.FinalPhysicsState,
seedCellId: cellId,
isStatic: false);
var report = new PhysicsSetPositionCollisionReport(
ContactPlaneValid: false,
ContactPlane: default,
ContactPlaneCellId: 0u,
ContactPlaneIsWater: false,
LastKnownContactPlaneValid: false,
LastKnownContactPlane: default,
LastKnownContactPlaneCellId: 0u,
LastKnownContactPlaneIsWater: false,
SlidingNormalValid: false,
SlidingNormal: default,
CollisionNormalValid: false,
CollisionNormal: default,
CollidedWithEnvironment: false,
FramesStationaryFall: 0,
AdjustOffset: default,
LastCollidedObjectId: peerLocalId,
CollidedObjectIds: System.Collections.Immutable.ImmutableArray
.Create(peerLocalId));
Assert.True(lifetime.Physics.HandleSetPositionCollisions(
owner,
owner.PositionAuthorityVersion,
owner.SpatialAuthorityVersion,
owner.VelocityAuthorityVersion,
physicsTime: 1d,
previousContact: false,
previousOnWalkable: false,
report));
}
// ── Fixture ──────────────────────────────────────────────────────────
/// <summary>