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>
465 lines
25 KiB
Markdown
465 lines
25 KiB
Markdown
# C4 route 5 — projectile authoritative placement: retail-conformance review (2026-08-04)
|
|
|
|
**Reviewer lens:** retail fidelity only ("is this what the retail client does?").
|
|
Architecture is a separate reviewer's job.
|
|
|
|
**Subject:** the uncommitted working-tree diff at branch
|
|
`claude/acdream-physics-divergence-5aa784`, HEAD `30d3d114`
|
|
(`git diff HEAD` + the untracked
|
|
`tests/AcDream.Runtime.Tests/Entities/RuntimeProjectilePositionKindTests.cs`).
|
|
`docs/research/2026-08-04-c4-route-5-contract.md` is the pinned contract, not
|
|
part of the change under review.
|
|
|
|
---
|
|
|
|
## VERDICT: **FAIL**
|
|
|
|
Two MAJOR findings, both **unrecorded divergences** — the project rule the
|
|
divergence register exists to enforce ("any commit that introduces a deviation
|
|
adds its register row IN THE SAME COMMIT; a deviation found without a row is a
|
|
bug twice over"). Neither is a hard-to-fix design problem: R1 is a one-clause
|
|
amendment to AP-141 (or a three-line fall-through decision), R2 is either a
|
|
wiring of the already-in-tree hook actions or a second clause on the same row.
|
|
|
|
**What is right, and independently verified** (Section A below): the retail
|
|
chain the contract asserts is correct in every particular I checked, the
|
|
byte-decode retiring the fabricated velocity is correct and I reproduced it
|
|
from the paired binary, and AP-141 describes retail's mechanical behaviour
|
|
honestly rather than dressing the divergence up as fidelity.
|
|
|
|
---
|
|
|
|
## A. Retail claims verified independently (all PASS)
|
|
|
|
Verified against `docs/research/named-retail/acclient_2013_pseudo_c.txt` and,
|
|
for the byte-decode, against `C:/Users/erikn/Downloads/acclient.exe`
|
|
(PDB-paired, image base `0x00400000`).
|
|
|
|
### A1. `MoveOrTeleport` has no `state & Missile` test — CONFIRMED
|
|
|
|
`CPhysicsObj::MoveOrTeleport` @0x00516330 (pseudo-C 284304-284366) reads
|
|
exactly four object fields: `update_times[4]` (`mov cx,[esi+0x16c]`), `cell`
|
|
(`mov eax,[esi+0x90]`), `player_distance` (`fld dword [esi+0x20]`), and
|
|
`position_manager` (`mov ecx,[esi+0xc8]`). There is no physics-state read
|
|
anywhere in the 272-byte function. A missile takes the identical generic
|
|
remote path. The contract's §4 row 1 is correct.
|
|
|
|
`SmartBox::HandleReceivedPosition` @0x00453FD0's only kind branch is
|
|
`arg2 != this->player` @0x0045414D — confirmed at pseudo-C 92896-93054.
|
|
`UnpackPositionEvent` @0x004542C0 calls it @0x00454358 with no state/kind
|
|
test.
|
|
|
|
### A2. Branch structure — CONFIRMED
|
|
|
|
| retail branch | address | acdream disposition | verdict |
|
|
|---|---|---|---|
|
|
| teleport/cell-less: `teleport_hook` → `SetFlags(0x1012)` → `SetPosition` → `return 1` | @0x005163EF / @0x00516414 / @0x00516420 / @0x00516438 | `SetPosition` | faithful |
|
|
| near: `InterpolateTo(this, arg2, IsMovingTo(this))`, `return 1` | @0x005163AF, @0x005163BE | `Interpolate` → **no-op** | recorded divergence (AP-141a) |
|
|
| far: `if (position_manager != 0) StopInterpolating`, `SetPositionSimple(this, arg2, 1)`, `return 1` | @0x005163C9-@0x005163CB, @0x005163D9, @0x005163E8 | `SetPositionSimple` | faithful; the `StopInterpolating` skip is retail's OWN guard for a manager-less object, correctly excluded from the divergence row |
|
|
| airborne (`arg4 == 0`): `return 0` | @0x0051636D | `NoPositionOperation` → no-op, no arm | faithful, correctly NOT claimed as a divergence |
|
|
|
|
The far branch's third argument to `SetPositionSimple` is `1` (byte
|
|
`6a 01` at @0x005163D3) — matches.
|
|
|
|
### A3. The `ConstrainTo` chain — CONFIRMED; retail WOULD arm a missile's leash
|
|
|
|
- `HandleReceivedPosition` @0x00454254: `if (MoveOrTeleport(...) != 0)` →
|
|
@0x00454272 `ConstrainTo(arg2, &arg2->m_position, ...)`. No kind test, no
|
|
manager-existence test.
|
|
- `CPhysicsObj::ConstrainTo` @0x00510520 → `MakePositionManager(this)`
|
|
@0x00510523 → `PositionManager::ConstrainTo` @0x00510533. The manager is
|
|
created **on demand**.
|
|
- The sibling `CPhysicsObj::InterpolateTo` @0x005104F0 does the same
|
|
(`MakePositionManager` @0x005104F3 then `PositionManager::InterpolateTo`
|
|
@0x00510508), so the near branch would likewise build machinery for a
|
|
manager-less missile.
|
|
- `PositionManager::UseTime` is ticked from `UpdateObjectInternal`
|
|
@0x005159A9 — an armed missile leash would be live, not vestigial.
|
|
|
|
**The central design decision is therefore a deliberate divergence, and the
|
|
change records it as one.** AP-141 states plainly that "retail's single arming
|
|
site has no kind test, so retail WOULD build a `PositionManager` on demand and
|
|
arm a missile's leash on any nonzero `MoveOrTeleport` return", and cites
|
|
@0x00454272 / @0x00510523 / @0x005163AF / @0x0050EB10 plus the ACE
|
|
unreachability. It does **not** claim fidelity it does not have. PASS.
|
|
|
|
### A4. The byte-decode of `arg5` — CONFIRMED, reproduced independently
|
|
|
|
I did not take the implementer's disassembly on trust; I re-derived the stack
|
|
offset and scanned the function's bytes.
|
|
|
|
Prologue at @0x00516330: `83 ec 64` (`sub esp,0x64`), `56` (`push esi`),
|
|
`8b f1`, `66 8b 8e 6c 01 00 00`, `57` (`push edi`). Displacement from entry
|
|
`esp` is therefore `0x64 + 4 + 4 = 0x6C`, so:
|
|
|
|
| arg | slot after prologue |
|
|
|---|---|
|
|
| `arg2` (Position*) | `[esp+0x70]` |
|
|
| `arg3` (uint16 timestamp) | `[esp+0x74]` |
|
|
| `arg4` (int32) | `[esp+0x78]` |
|
|
| `arg5` (Vector3 const*, velocity) | **`[esp+0x7C]`** |
|
|
|
|
Every stack-argument read in the function body: `8b 7c 24 74` @0x0051633E
|
|
(arg3 → edi), `8b 44 24 78` @0x00516387 (arg4), `8b 44 24 74` @0x005163A7
|
|
(arg2 — `esp` is 4 lower after the preceding `push eax`), `8b 4c 24 70`
|
|
@0x005163CF (arg2), `8b 54 24 70` @0x005163FC (arg2). **Zero occurrences of
|
|
the SIB+disp8 byte pair `24 7c` in the whole function**, and no disp32
|
|
(`84 24 7c 00 00 00`) form either; the only other `0x7c` bytes in the range
|
|
belong to the `edi` ModRM byte and to the constant address `0x007c6afc` (the
|
|
96 m literal). All four exits are `c2 10 00` (`ret 0x10`) — four dwords of
|
|
stack args, confirming the four-argument frame.
|
|
|
|
**`arg5` is never read. P4 holds. The `?? Vector3.Zero` fabrication was
|
|
genuinely fabricated, and deleting it is correct.** PASS.
|
|
|
|
### A5. The teleport-hook reduction — retail side CONFIRMED
|
|
|
|
`CPhysicsObj::teleport_hook` @0x00514ED0 (pseudo-C 283115-283151) is exactly
|
|
six actions:
|
|
|
|
1. `MovementManager::CancelMoveTo` — guarded `movement_manager != 0` @0x00514EDB
|
|
2. `PositionManager::UnStick` — guarded @0x00514EEC
|
|
3. `PositionManager::StopInterpolating` — guarded @0x00514EFB
|
|
4. `PositionManager::UnConstrain` — guarded @0x00514F0A
|
|
5. `TargetManager::ClearTarget` + `NotifyVoyeurOfEvent(Teleported)` — guarded @0x00514F19
|
|
6. `CPhysicsObj::report_collision_end(this, 1)` @0x00514F31 — **unguarded**
|
|
|
|
So for a *bare* arrow/bolt (no `MovementManager`, no `PositionManager`, no
|
|
`TargetManager`) five of six are retail no-ops through retail's own guards,
|
|
and the sixth applies. The projectile arm's single
|
|
`_entityObjects.Physics.CollisionReports.LeaveWorld(record)` is the right
|
|
port: it is the same seam `RunRemoteTeleportHook`'s `ReportCollisionEnd`
|
|
action reaches (`LiveEntityRuntime.ForceEndCollisionReporting` →
|
|
`RuntimeCollisionReportingState.LeaveWorld`), i.e. the 4b-3 R2-validated port
|
|
of @0x00514620. Ordering is also right — it runs strictly before
|
|
`TryExecuteAcceptedRemotePosition`, mirroring @0x005163EF before @0x00516420 —
|
|
and it runs unconditionally of the placement's outcome, exactly as retail
|
|
discards `SetPosition`'s result and returns 1 @0x00516438.
|
|
|
|
**Caveat: "the other five are genuinely per-manager-guarded no-ops for a bare
|
|
missile" is true, but the contract also pinned the adopted-body case, and that
|
|
half was not delivered — see R2.**
|
|
|
|
### A6. Register / issue discipline — PARTIAL
|
|
|
|
- AP-141 added in the same working tree as the behaviour. PASS.
|
|
- AP-131 and AP-135 are untouched (the register diff is exactly the section-3
|
|
count line plus the AP-141 row). PASS.
|
|
- `docs/ISSUES.md` is not modified; #276 is not closed. PASS.
|
|
- AP-141's retail description is accurate (A3). PASS.
|
|
- **AP-141 is incomplete** — see R1 and R2.
|
|
|
|
### A7. Comment corrections — the touched ones verified
|
|
|
|
The deliberate separate correction at
|
|
`src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2309-2323`
|
|
replaces the false claim "MoveOrTeleport installs that exact vector with
|
|
set_velocity". Every assertion in the replacement is true: `MoveOrTeleport`
|
|
never reads the velocity slot (A4); `UnpackPositionEvent` @0x004542C0 performs
|
|
no `set_velocity`; the only `set_velocity` in the chain is the local player's
|
|
zero @0x004541B4. The replacement is honest about the consequence ("this
|
|
call's actual retail justification is therefore NOT yet established"). PASS
|
|
on truthfulness — but see R8 on the bookkeeping.
|
|
|
|
The two other comment rewrites (`ClassifyRemoteAcceptedPosition`'s
|
|
kind-derivation paragraph, `OwnsPlacement`'s widening paragraph) match the
|
|
code beside them. `RuntimeProjectilePhysicsUpdater`'s tombstone comment
|
|
accurately describes what was deleted and what remains.
|
|
|
|
---
|
|
|
|
## B. Findings
|
|
|
|
### R1 — MAJOR — a Missile-flagged entity with no bound projectile now has its accepted Position silently dropped; retail places it, and AP-141 does not cover this shape
|
|
|
|
**Where:**
|
|
`src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2222-2247`
|
|
(the `isMissilePacket` dispatch) together with
|
|
`src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs:1004-1012`
|
|
(`ApplyAcceptedProjectilePosition`'s ownership gate).
|
|
|
|
**Retail contradicted:** `CPhysicsObj::MoveOrTeleport` @0x00516330 —
|
|
reached from `HandleReceivedPosition` @0x00454254 for **every** non-player
|
|
`CPhysicsObj`, with no test of any kind on what client-side machinery the
|
|
object happens to have bound. Retail always places the object.
|
|
|
|
**What changed.** The deleted short-circuit was
|
|
`if (_projectileController?.ApplyAuthoritativePosition(...) == true) return;`.
|
|
That method returned **false** — i.e. *fell through to the generic remote
|
|
tail, which applied the position* — whenever `TryGetCurrent` failed, i.e.
|
|
whenever the record had the Missile bit but no bound `RuntimeProjectile`.
|
|
The new dispatch decides `isMissilePacket` purely from
|
|
`route.OperationKind`/the Missile bit, and `ApplyAcceptedProjectilePosition`
|
|
returns `null` (write nothing, return) when `record.Projectile` is null. The
|
|
packet is now dropped with no writer of any kind.
|
|
|
|
**Why this is reachable state, not a hypothetical.** `ProjectileController.TryBind`
|
|
(`src/AcDream.App/Physics/ProjectileController.cs:133-330`) can fail
|
|
*permanently* for a Missile-flagged entity — `TryGetCollisionSphere` rejects
|
|
any Setup that is not "the supported retail one-sphere collision shape" and
|
|
logs `Missile 0x… Setup 0x… does not have the supported retail one-sphere
|
|
collision shape`. It also has a legitimate not-yet-bound window
|
|
(`initialResidenceActive == false`, and the projection-visible retry in
|
|
`OnProjectionVisibilityChanged`). In all of those states the entity used to
|
|
track its server position through the remote path; now it freezes for the rest
|
|
of its life.
|
|
|
|
**Bookkeeping.** AP-141 clause (c) covers "a null-classified or `Rejected*`
|
|
accepted Position"; it does **not** cover "an accepted Position for a
|
|
Missile-flagged entity whose projectile component is absent or does not agree
|
|
with the canonical body". The XML doc on `ApplyAcceptedProjectilePosition`
|
|
does mention the ownership-mismatch swallow, so this is a real design
|
|
decision that was made and then not written into the register.
|
|
|
|
**Correct behaviour:** either (a) fall through to the generic remote tail when
|
|
`record.Projectile` is absent — which is what retail does and what the code
|
|
did yesterday — or (b) keep the swallow and extend AP-141 with a fourth
|
|
clause naming it, its trigger (unbindable Setup / pre-bind window), and the
|
|
observable (a Missile-flagged object stops tracking permanently). Do not
|
|
leave it undocumented.
|
|
|
|
*Mitigating (do not use as a reason to skip the row):* unreachable against ACE
|
|
for the same reason the rest of the row is — `WorldObject_Tick.cs:333-334`.
|
|
|
|
---
|
|
|
|
### R2 — MAJOR — the adopted-body teleport-hook case was pinned by the contract, not implemented, and not registered; retail runs five actions acdream skips
|
|
|
|
**Where:** `src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs:1017`
|
|
— the `SetPosition` arm runs `CollisionReports.LeaveWorld(record)` and
|
|
nothing else.
|
|
|
|
**Retail contradicted:** `CPhysicsObj::teleport_hook` @0x00514ED0 actions 1-5
|
|
(@0x00514EDB, @0x00514EEC, @0x00514EFB, @0x00514F0A, @0x00514F19). Each is
|
|
guarded on *its manager existing* — not on the object being a non-missile.
|
|
|
|
**The gap.** A missile can carry the full remote manager set. `TryBind`'s own
|
|
adopted-body branch exists for exactly this
|
|
(`ProjectileController.cs:176-181`: "Retail owns one CPhysicsObj. If a
|
|
non-missile incarnation already created its MovementManager … classification
|
|
adopts that same body"), and the late-classification entry
|
|
`ApplyAuthoritativeState → TryBind` is the production path that turns an
|
|
ordinary remote — which by then has a `RemoteMotion` with an `Interp` queue
|
|
and, if hosted, a `PositionManager` — into a missile. For such an object
|
|
retail's guards are *satisfied* and retail runs `CancelMoveTo`, `UnStick`,
|
|
`StopInterpolating`, `UnConstrain` and `ClearTarget`/`NotifyVoyeurOfEvent`.
|
|
acdream now runs none of them.
|
|
|
|
The contract pinned this explicitly (D-P4): "for the adopted-body case (a
|
|
missile that carries a `RemoteMotion`), the existing hook actions'
|
|
per-manager guards already express retail's shape; the implementer wires this
|
|
without per-packet closures (#315 pattern)". The in-tree faithful port is
|
|
sitting unused — `RemoteTeleportHook.Execute` +
|
|
`RemoteTeleportHookActions`, the ordered six-action seam with a currency check
|
|
between each step. This is the 4b-3 round-2 R2 defect class verbatim: *a
|
|
retail action skipped with the faithful port already in-tree.*
|
|
|
|
**Not a regression** — the deleted `ApplyAuthoritativePosition` ran no hook
|
|
either — but it is a divergence this commit had the obligation to close or
|
|
record, and it did neither.
|
|
|
|
**Correct behaviour:** run the hook through the existing ordered seam when the
|
|
record carries the managers (`record.RemoteMotion`/its host), letting the
|
|
per-manager guards no-op for a bare arrow exactly as retail's do; or state in
|
|
AP-141 that only action 6 of `teleport_hook` @0x00514ED0 is ported for a
|
|
projectile and that actions 1-5 are skipped even when the managers exist.
|
|
|
|
*Note on the stale-queue half specifically:* while the Missile bit is set the
|
|
remote interpolation is inert (`LiveEntityAnimationScheduler.cs:336` gates on
|
|
`projectileHandlesMovement`), so the skipped `StopInterpolating` is latent
|
|
rather than immediately observable — it becomes live again the moment ACE
|
|
clears Missile on impact. `ClearTarget`/`NotifyVoyeurOfEvent` and
|
|
`CancelMoveTo` have no such shield.
|
|
|
|
---
|
|
|
|
### R3 — MINOR — invariant 4 ("prediction invalidated before every body write on this route") and invariant 2 (presentation) hold on the direct arm only; the retained-retry arm satisfies neither
|
|
|
|
**Where:** `src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs:1150-1213`
|
|
(`Advance()`), reached from
|
|
`src/AcDream.App/Net/GraphicalSessionEventRoute.cs:121`.
|
|
|
|
A `Contention` outcome parks the packet in `_pending` and re-submits it one or
|
|
more host cadence pumps later. That retry path calls `SubmitAndResolve`
|
|
(a canonical body commit) and, on a destination that left the window,
|
|
`StoreAcceptedDestinationPose` (a raw body write) — **without**
|
|
`projectile.InvalidatePrediction()` and without `SyncProjectilePresentation`.
|
|
The version bumped at packet time does not protect a quantum begun *after*
|
|
that bump and straddling the retry.
|
|
|
|
Presentation is the same shape: the App's `SyncPresentationFromResolvedBody`
|
|
is called only from `OnPosition`, and `RuntimePlacementPresentationSink.TryPublishPlace`
|
|
(`src/AcDream.App/World/RuntimePlacementPresentationSink.cs:210-241`) does not
|
|
write `entity.Position`/`Rotation` — it snapshots whatever the entity already
|
|
holds. So a retried projectile commit advances the canonical body with no
|
|
presentation projection at all until the next quantum re-projects.
|
|
|
|
Both are bounded (the retry pump runs from the network retry lease, not from
|
|
inside the scheduler's `TryBeginQuantum`/`CompleteQuantum` straddle; the
|
|
presentation lag self-heals on the next tick), which is why this is MINOR
|
|
rather than MAJOR. But the invariants are stated absolutely, and this is the
|
|
"an invariant satisfied on one arm only" class both 4b-3 rounds named.
|
|
|
|
---
|
|
|
|
### R4 — MINOR — `SyncProjectilePresentation` silently drops the spatial+hidden clock consumption while its doc claims a faithful reduction
|
|
|
|
**Where:** `src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs:975-985`
|
|
(doc) and `:1028-1042` (the `else if (spatial)` branch).
|
|
|
|
The deleted tail was:
|
|
|
|
```
|
|
else if (spatial)
|
|
{
|
|
body.InWorld = true;
|
|
body.LastUpdateTime = currentTime; // <-- gone
|
|
_physics.Engine.ShadowObjects.Suspend(localId);
|
|
}
|
|
```
|
|
|
|
The replacement omits `body.LastUpdateTime`. The doc immediately above claims
|
|
the method is that tail "reduced … to this controller's own two-argument seam
|
|
— `_clock` supplies the same clock source that method took as an explicit
|
|
`currentTime` parameter", which is true of the *visible* branch and false of
|
|
this one. The intent of the deleted write is documented elsewhere in the same
|
|
subsystem (`ProjectileController.TryBind`: "consume the hidden clock so UnHide
|
|
cannot replay a time backlog").
|
|
|
|
Consequence is small — the projectile stepper takes its quantum from
|
|
`RetailObjectQuantumBatch`, not from `body.LastUpdateTime`, and
|
|
`ProjectileController.Tick:601-604` re-stamps `LastUpdateTime` on the
|
|
`!InWorld` re-entry edge — but this is an unclaimed behaviour deletion inside
|
|
a change whose comment asserts equivalence. (Clock *domain* is fine:
|
|
`UpdateFrameOrchestrator.CurrentScriptTime => _runtime.SimulationTimeSeconds`,
|
|
so `_clock.SimulationTimeSeconds` is the same base the App used to pass.)
|
|
|
|
---
|
|
|
|
### R5 — MINOR — the App ignores the seam's returned status, so the "presentation advances on committed/stored outcomes only" gate exists on the Runtime side only
|
|
|
|
**Where:** `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2237-2245`.
|
|
|
|
Runtime deliberately withholds `SyncProjectilePresentation` for `Deferred` and
|
|
`RejectedByPlacement` (with the comment "Deferred/RejectedByPlacement leave the
|
|
body at its prior (already-synced) pose"). The App then calls
|
|
`_projectileController.SyncPresentationFromResolvedBody(...)` unconditionally,
|
|
discarding the returned `RuntimeRemotePlacementExecutionStatus?` entirely —
|
|
including on `Deferred`, where `ParkDeferred` has already snapped the body to
|
|
the parked result and withdrawn the entity. The App write moves the render
|
|
entity to a pose Runtime declined to publish; if the park is later cancelled
|
|
and restored (`WithdrawalRestored`), the entity re-shows there. That is
|
|
AP-136's family.
|
|
|
|
On the no-op and swallow dispositions the write is inert (same value), so this
|
|
is MINOR, but the two layers disagree about the same invariant.
|
|
|
|
---
|
|
|
|
### R6 — MINOR — on a stored (non-committed) outcome the render cell is left behind while the render position moves; retail's `store_position` writes `objcell_id` too
|
|
|
|
**Where:** `src/AcDream.App/Physics/ProjectileController.cs:512-517`
|
|
(`entity.ParentCellId = record.FullCellId`) with
|
|
`RuntimeRemotePlacementDriveController.StoreAcceptedDestinationPose:1104-1136`.
|
|
|
|
`StoreAcceptedDestinationPose` writes `body.Position` and `body.Orientation`
|
|
only — not the cell, and D-P2 correctly forbids a `RebucketLiveEntity` on this
|
|
arm. So after `Refused`/`Contention`/`RejectedPreparation` the render entity
|
|
gets the new world position with the *old* `ParentCellId`. The deleted path
|
|
could not produce this pairing: it always did `body.SnapToCell(fullCellId, …)`
|
|
+ `CommitProjectileCell`.
|
|
|
|
Retail's `store_position(this, arg2)` @0x00515CE2 writes the object's whole
|
|
`Position` including `objcell_id`, and the branch then calls
|
|
`CObjectMaint::GotoLostCell` @0x00515CF2 — i.e. retail moves the cell *and*
|
|
hides the object pending load. This is largely AP-138's existing residual
|
|
(shared with the remote far arm), but it is newly reachable for projectiles
|
|
and worth naming when AP-141 is amended.
|
|
|
|
---
|
|
|
|
### R7 — MINOR — the contract's App-layer dual-kind matrix (§7 items 8-10) is absent; the D-P6 dispatch has no test at all
|
|
|
|
Added coverage is Runtime-only: `RuntimeProjectilePositionKindTests` (3 facts,
|
|
kind derivation + mid-life flip) and 11 new
|
|
`RuntimeRemotePlacementDriveControllerTests` facts. These are good tests —
|
|
positive assertions, real behaviour, no source-text pins, and the split-quantum
|
|
abort (T3) and no-velocity (D-P5) invariants are both genuinely exercised.
|
|
|
|
But there is no test anywhere under `tests/AcDream.App.Tests` that drives
|
|
`OnPosition`. The contract made the dual-kind `[Theory]` "what makes a future
|
|
silent mis-tag fail a test", and item 10 required a sabotage check
|
|
("hardcode Remote and confirm the dual-kind theories fail"). With no App-level
|
|
test, hardcoding `isMissilePacket = false` at
|
|
`LiveEntityNetworkUpdateController.cs:2229` would leave the suite green — the
|
|
exact failure mode the discipline was written to catch. The swallow ordering,
|
|
the presentation-ack ordering, and the "no `RemoteMotion` is ever created for a
|
|
missile packet" claim are all untested.
|
|
|
|
---
|
|
|
|
### R8 — MINOR — the corrected 4a comment now records an unjustified production behaviour with nothing tracking it
|
|
|
|
`LiveEntityNetworkUpdateController.cs:2309-2323` now states that the retail
|
|
justification for the remote arm's `TryCommitAuthoritativeVelocity` "is
|
|
therefore NOT yet established and needs its own audit". That is the honest
|
|
call, and the contract (§10) said to report rather than edit. But an
|
|
acknowledged-unjustified velocity commit on a live production path now exists
|
|
with no AP row and no `docs/ISSUES.md` entry — only a code comment. One line
|
|
in ISSUES.md (or an AP row) would keep it findable.
|
|
|
|
---
|
|
|
|
## C. Anything wrong in the CONTRACT itself
|
|
|
|
Nothing factually wrong. Three notes:
|
|
|
|
1. **§4's `teleport_hook` address is cited two ways and both are right** — the
|
|
contract's §4 row 2 cites @0x005163EF (the call site inside
|
|
`MoveOrTeleport`) and D-P4 cites @0x00514ED0 (the function). Confirmed
|
|
consistent; no action.
|
|
2. **D-P4's adopted-body sentence is the only pinned obligation the diff did
|
|
not discharge** (R2). The contract's wording ("the implementer wires this
|
|
without per-packet closures") is an instruction, not a claim, so the
|
|
contract is not wrong — the implementation is incomplete against it.
|
|
3. **D-P2 step 1's "Anything else: return `NotApplicable`-equivalent, write
|
|
nothing"** is the source of R1. The contract pinned it without noticing that
|
|
it removes the deleted path's fall-through and therefore *introduces* a new
|
|
divergence needing its own register clause. Worth a sentence when AP-141 is
|
|
amended, so the next reader does not read the contract as having cleared it.
|
|
|
|
Everything else I checked in the contract holds: the retail chain (§4, all
|
|
seven rows), the ACE unreachability (`WorldObject_Tick.cs:333-334` inside the
|
|
`PhysicsState.Missile` branch at `:265`), the classifier's kind-blindness past
|
|
`LocalPlayer` (`RuntimeAuthoritativePositionRouteClassifier.cs:349`,
|
|
`:535-538`, `:564-575`), and the three ownership fences (`OwnsPlacement`
|
|
widened; `OwnsFarSnap`/`OwnsTeleportPlacement` untouched, both arms still
|
|
`ArgumentNullException.ThrowIfNull(remote)` + throwing route guards).
|
|
|
|
I also spot-checked proof obligation **P3** and found no capture hazard: both
|
|
`RemoteAuthoritative` stamps in `RuntimeSetPositionState`
|
|
(`:3833/:3866` window-departure park, `:5338/:5350` legacy-direct withdrawal)
|
|
construct *fresh* operations and the park loop skips records already in
|
|
`_operations`, so neither can relabel a live `ProjectileAuthoritative`
|
|
operation; `route.OperationKind` is threaded intact through
|
|
`TryBeginExclusiveAuthoredPlacement` and `TryPrepareAndSubmitAuthoredPlacement`,
|
|
so the `command.Kind != operation.Kind` consistency check at `:2963` is
|
|
satisfied; and the direct-command gate at `:5445` admits local kinds only, which
|
|
this route never uses. `MoverPhysicsState = record.FinalPhysicsState` is carried
|
|
into the placement request (`:2007`, `:3007`, `:3810`, `:5496`), so the Missile
|
|
bit still reaches the engine's `missile_ignore` handling.
|
|
|
|
---
|
|
|
|
## D. Summary table
|
|
|
|
| # | Sev | Where | Retail address contradicted |
|
|
|---|---|---|---|
|
|
| R1 | MAJOR | `LiveEntityNetworkUpdateController.cs:2222-2247` + `RuntimeRemotePlacementDriveController.cs:1004-1012` | `MoveOrTeleport` @0x00516330 via @0x00454254 — placement is unconditional on client machinery |
|
|
| R2 | MAJOR | `RuntimeRemotePlacementDriveController.cs:1017` | `teleport_hook` @0x00514ED0 actions 1-5 (@0x00514EDB/@0x00514EEC/@0x00514EFB/@0x00514F0A/@0x00514F19) |
|
|
| R3 | MINOR | `RuntimeRemotePlacementDriveController.cs:1150-1213` | — (invariant 4/2, one arm only) |
|
|
| R4 | MINOR | `RuntimeRemotePlacementDriveController.cs:975-985, 1028-1042` | — (unclaimed behaviour deletion + stale doc) |
|
|
| R5 | MINOR | `LiveEntityNetworkUpdateController.cs:2237-2245` | — (App/Runtime disagree on invariant 2) |
|
|
| R6 | MINOR | `ProjectileController.cs:512-517`, `RuntimeRemotePlacementDriveController.cs:1104-1136` | `store_position` @0x00515CE2 / `GotoLostCell` @0x00515CF2 (writes cell too) |
|
|
| R7 | MINOR | `tests/AcDream.App.Tests/**` (absent) | — (contract §7 items 8-10 undelivered) |
|
|
| R8 | MINOR | `LiveEntityNetworkUpdateController.cs:2309-2323` | — (bookkeeping) |
|