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>
418 lines
23 KiB
Markdown
418 lines
23 KiB
Markdown
# 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.
|