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:
parent
30d3d114b0
commit
36255af0f6
19 changed files with 5390 additions and 393 deletions
|
|
@ -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
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
465
docs/research/2026-08-04-c4-route-5-architecture-review.md
Normal file
465
docs/research/2026-08-04-c4-route-5-architecture-review.md
Normal 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.
|
||||
804
docs/research/2026-08-04-c4-route-5-contract.md
Normal file
804
docs/research/2026-08-04-c4-route-5-contract.md
Normal 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.
|
||||
418
docs/research/2026-08-04-c4-route-5-retail-review-round2.md
Normal file
418
docs/research/2026-08-04-c4-route-5-retail-review-round2.md
Normal 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.
|
||||
282
docs/research/2026-08-04-c4-route-5-retail-review-round3.md
Normal file
282
docs/research/2026-08-04-c4-route-5-retail-review-round3.md
Normal 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.
|
||||
465
docs/research/2026-08-04-c4-route-5-retail-review.md
Normal file
465
docs/research/2026-08-04-c4-route-5-retail-review.md
Normal 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) |
|
||||
Loading…
Add table
Add a link
Reference in a new issue