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

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

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

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

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

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

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

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

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

23 KiB

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 (round 1, FAIL) and cross-read against 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 TargetManagerMakeTargetManager 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, SyncPresentationFromResolvedBodyentity.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 Positionobjcell_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.RefreshDerivedStateSetFullCell (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.LandblockIdthe 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:

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.