acdream/docs/research/2026-08-04-c4-route-5-contract.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

804 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.