refactor(physics): collapse OnPosition's dual player/NPC remote tail into one
C4 route 4b-3 collapse (docs/research/2026-08-04-onposition-collapse-contract.md). Behaviour-preserving: the ~640-line duplicated player-guid and NPC-guid copies of the remote routing tail in LiveEntityNetworkUpdateController.OnPosition become one guid-blind tail, reached by every remote guid through the single ApplyRemoteContactRouting/RunRemoteArmTail seam. Two guid-conditionals survive, both named and justified: - Row 8 (TS-44 sticky suppression, creature-only): retail's sticky is independent of this acdream-only steady-state gate; the register row already describes it as NPC-only and this collapse does not widen it. - The AirborneSnap arm's interp-clear + shadow-publish (rows 2a/2b, player-only preserve): unifying either way would be an unauthorized behaviour change. #316 (shadow publish) is a real, unmeasured pre-existing defect, deliberately preserved not fixed. The interp-clear's equivalence could not be proven for the steep-non-walkable-landing edge case (AdjustOffset's CONTACT-keyed gate vs. AP-139's WALKABLE-keyed per-tick clear) — preserved per contract stop condition 2 rather than shipped on an incomplete proof. Category-(c) resolutions (contract §2.1-2.5), each with its evidence: - Row 2a (interp clear): PRESERVED — AdjustOffset's `if (!inContact) return` proves inertness on flat landings, but not on the steep-contact edge case. - Row 2b (shadow publish / #316): PRESERVED — no design note ever sanctioned the player-guid skip; the file's own #184 Slice 2b comments contradict it. - Row 2c (EnsureRemoteMotionBindings): UNIFIED — the method is idempotent (`if (rm.Host is not null) return rm.Sink;`), so "always ensure" is safe. - Row 3 (wire-cell adopt ordering): UNIFIED — RebucketLiveEntity already commits the wire cell before either guid branch runs, so the deleted player-guid pre-write was a proven no-op. - Row 4 (LastServerPos/Time sample timing): UNIFIED — on a genuine first UP, InterpolationManager.Enqueue's already-close branch and the Snapped branch both converge on the same body pose/orientation for a zero-distance target. - Row 12 (wall-clock capture): UNIFIED — one shared `nowSec`, a microsecond-scale skew in acdream-only bookkeeping/diagnostics. Sabotage check (contract §5, performed and reverted, not committed): deleting the one remaining TryArmConstraintAfterOperation call failed 10/16 dual-guid matrix tests, spanning BOTH guid halves of every arming-dependent scenario (teleport, landing, near, far, sticky) — proof the matrix discriminates a defect regardless of which guid range exercises it, closing the class of bug that let 4b-3's A1/A2/R3 findings survive review when only one copy's tests were green. New tests/AcDream.App.Tests/Physics/LiveEntityNetworkOnPositionCollapseMatrixTests.cs drives 8 scenarios x 2 guid ranges (0x50xxxxxx player, 0x8xxxxxxx creature) through the complete production OnPosition entry point. Doc comments on ApplyRemoteContactRouting, RunRemoteArmTail, ApplyWireAirborneLeftoverBookkeeping, TryAdoptWireCellAfterRouting, and the AirborneNoOperation throw guard updated to describe the collapsed one-path world (the "two callers stay one decision" claim was true before this commit and false after — fixed in the same commit that makes it false). One branch-routing source-text pin (LiveEntityNetworkBranchRoutingTests.cs) updated to follow the AP-140 CONTACT gate to its new address inside ApplyRemoteContactRouting. #316 stays OPEN, deliberately not fixed here — see its updated ISSUES.md entry. dotnet build AcDream.slnx -c Release: 0 errors. Verified independently bisectable at this exact commit: AcDream.App.Tests 4104/4107 (3 pre-existing skips), AcDream.Runtime.Tests 1125/1125. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a89bcb39b2
commit
edc911b042
5 changed files with 2225 additions and 641 deletions
|
|
@ -13133,3 +13133,14 @@ already instruments the packet side of exactly this edge.
|
||||||
by contract; adding a publish would be a behaviour change smuggled into a
|
by contract; adding a publish would be a behaviour change smuggled into a
|
||||||
refactor, and its severity is unmeasured. Split-on-discovery: own commit, with
|
refactor, and its severity is unmeasured. Split-on-discovery: own commit, with
|
||||||
a dual-guid test, after the measurement.
|
a dual-guid test, after the measurement.
|
||||||
|
|
||||||
|
**Post-collapse update (2026-08-04):** the OnPosition collapse dissolved the
|
||||||
|
standalone player-guid LANDING TRANSITION block into the unified remote
|
||||||
|
routing tail's `AirborneSnap` arm. The defect this row describes is
|
||||||
|
UNCHANGED and now lives as an explicit, commented, guid-gated skip at that
|
||||||
|
arm's shadow-publish step (`arm is RemoteContactArm.AirborneSnap &&
|
||||||
|
IsPlayerGuid(...)` in `LiveEntityNetworkUpdateController.OnPosition`) —
|
||||||
|
preserved verbatim, not fixed, per this row's own resolution above. Covered
|
||||||
|
by `LandingPacket_PlayerGuid_QueueClearedNoShadowPublish_316Preserved` /
|
||||||
|
`LandingPacket_CreatureGuid_ShadowPublishedQueueNotCleared` in
|
||||||
|
`tests/AcDream.App.Tests/Physics/LiveEntityNetworkOnPositionCollapseMatrixTests.cs`.
|
||||||
|
|
|
||||||
561
docs/research/2026-08-04-onposition-collapse-contract.md
Normal file
561
docs/research/2026-08-04-onposition-collapse-contract.md
Normal file
|
|
@ -0,0 +1,561 @@
|
||||||
|
# OnPosition remote-tail collapse — pinned contract (2026-08-04)
|
||||||
|
|
||||||
|
**Scope:** collapse the two parallel inline copies of the remote-position
|
||||||
|
routing tail in
|
||||||
|
`src/AcDream.App/Physics/LiveEntityNetworkUpdateController.OnPosition` — the
|
||||||
|
player-guid copy (`IsPlayerGuid(update.Guid)`, `:2264-2679` at HEAD) and the
|
||||||
|
NPC-guid copy (`:2681-2951`) — into one guid-blind routing path. **This is a
|
||||||
|
behaviour-preserving refactor.** Pinned at HEAD `b260bcd1`, clean tree, branch
|
||||||
|
`claude/acdream-physics-divergence-5aa784`.
|
||||||
|
|
||||||
|
**Line numbers in this contract are as-of `b260bcd1` and WILL go stale the
|
||||||
|
moment the collapse starts. Every citation also names the symbol or the
|
||||||
|
comment banner; trust the symbol.** (Process rule 6 — line ranges went stale
|
||||||
|
twice within single review rounds during 4b-2/4b-3.)
|
||||||
|
|
||||||
|
Predecessor documents, all binding where they still apply:
|
||||||
|
- [`2026-08-04-c4-route-4b-3-contract.md`](2026-08-04-c4-route-4b-3-contract.md)
|
||||||
|
— its 13 "must REMAIN true" invariants carry forward (§6 below).
|
||||||
|
- [`2026-08-04-c4-route-4b-3-retail-review.md`](2026-08-04-c4-route-4b-3-retail-review.md)
|
||||||
|
/ [`-round2.md`](2026-08-04-c4-route-4b-3-retail-review-round2.md) and
|
||||||
|
[`2026-08-04-c4-route-4b-3-architecture-review.md`](2026-08-04-c4-route-4b-3-architecture-review.md)
|
||||||
|
/ [`-round2.md`](2026-08-04-c4-route-4b-3-architecture-review-round2.md) —
|
||||||
|
round 2's call-site-by-call-site verification of `RunRemoteArmTail` and
|
||||||
|
`ApplyWireAirborneLeftoverBookkeeping` is the starting map and the method
|
||||||
|
template for §5.
|
||||||
|
- [`2026-08-04-session-handoff-c4-remaining.md`](2026-08-04-session-handoff-c4-remaining.md)
|
||||||
|
— the six process rules apply verbatim.
|
||||||
|
|
||||||
|
## 1. Why this exists
|
||||||
|
|
||||||
|
Retail's `CPhysicsObj::MoveOrTeleport` @0x00516330 has **zero player/NPC
|
||||||
|
branching** — verified independently in
|
||||||
|
`docs/research/named-retail/acclient_2013_pseudo_c.txt` by the 4b-3 retail
|
||||||
|
review (round 1, Part 1) and re-confirmed for this contract: it branches on
|
||||||
|
the body's own cell (`this_1->cell == 0` @0x00516386), TELEPORT_TS
|
||||||
|
(`newer_event` @0x00516375), `player_distance` (@0x005163AF/@0x005163C1), and
|
||||||
|
the wire contact argument (`arg4` @0x0051638E) — never on what KIND of object
|
||||||
|
it is. The only kind-branch in the whole retail chain is
|
||||||
|
`SmartBox::HandleReceivedPosition` @0x00453FD0's `arg2 == this->player`
|
||||||
|
(@0x0045414D) — **local player vs everything else**, never remote-player vs
|
||||||
|
remote-NPC. acdream's two copies are therefore an architecture artifact, not
|
||||||
|
a retail port.
|
||||||
|
|
||||||
|
The cost is documented history, not speculation. In route 4b-3's first review
|
||||||
|
round, THREE of five MAJOR findings were this duplication: **R1** (the D2
|
||||||
|
wire-airborne return-0 shape implemented on the player copy only, with a
|
||||||
|
register row asserting otherwise), **R3/A2** (the player copy's teleport block
|
||||||
|
returns above its synth-velocity code; the NPC copy had no such boundary, so a
|
||||||
|
teleported NPC installed a ~1,000 m/s synthesized velocity and sprinted in
|
||||||
|
place), and **A1** (`ToConstraintArm` written against the player copy's
|
||||||
|
reality — which provably never produces `AirborneSnap` — and wrong for the NPC
|
||||||
|
copy, which can, silently dropping the leash to zero arms). An implementer
|
||||||
|
also sabotaged one copy and watched tests stay green because coverage hit the
|
||||||
|
other. The route 4b-3 fix round extracted two shared helpers
|
||||||
|
(`RunRemoteArmTail`, 3 call sites; `ApplyWireAirborneLeftoverBookkeeping`,
|
||||||
|
2 call sites) as a partial fix. **This slice is the remaining collapse.**
|
||||||
|
|
||||||
|
## 2. Complete behavioural-difference inventory
|
||||||
|
|
||||||
|
Every difference between the two copies at `b260bcd1`, walked top to bottom.
|
||||||
|
Categories: **(a)** genuine, justified difference that must survive (with the
|
||||||
|
justification); **(b)** proven-equivalent copy-shape difference — unify, with
|
||||||
|
the equivalence proof stated per §5; **(c)** unknown — evidence required
|
||||||
|
before either unifying or preserving. **A difference missed here is a silent
|
||||||
|
behaviour change; the implementer must re-walk both copies against this table
|
||||||
|
before writing code and STOP if they find a row this table lacks** (§10 stop
|
||||||
|
condition 3).
|
||||||
|
|
||||||
|
The shared prologue (observation tracker, `RemoteMotion` get-or-create +
|
||||||
|
seed + `SeedRemoteSpawnPlacement`, `TryCommitAuthoritativeVelocity`, the
|
||||||
|
`[remote-slide-up]` probe, `RebucketLiveEntity`, classification,
|
||||||
|
`TryApplyGenericRemoteRenderPose`) is already shared and is NOT part of this
|
||||||
|
inventory. `SeedRemoteSpawnPlacement`'s guid-driven mover flags
|
||||||
|
(`IsPlayer | EdgeSlide` vs `EdgeSlide`) are per-object data retail itself
|
||||||
|
carries on OBJECTINFO and are already in the correct data-driven shape — out
|
||||||
|
of scope.
|
||||||
|
|
||||||
|
| # | difference | player copy | NPC copy | category |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | Teleport dispatch shape | dedicated pre-routing block (`OwnsTeleportPlacement` check → `RunRemoteArmTail` → arm → tail → return, `:2340-2378`) | rides the common routing (D5 inside `ApplyRemoteContactRouting`), with `isTeleportRoute` exclusions sprinkled (D2 conjunct `:2727`, synth gate `:2751`, cycle gate `:2896`, sticky-gate widening `:2804`) | **(b)** — outcome-equivalence verified call-site-by-call-site by the round-2 architecture review Part 1; unify on the in-routing shape (§3) |
|
||||||
|
| 2 | Landing-family decision site | the LANDING TRANSITION block (`!rmState.Body.InContact` pre-routing, `:2454-2552`), which is what makes `playerArm` provably never `AirborneSnap` (round-2 architecture review, "LANDING TRANSITION block" section) | `ApplyRemoteContactRouting`'s free-flight carve-out (`!remote.Body.InContact` → `AirborneSnap`, `:1177-1204`) plus the shared tail | structural container for 2a-2d; the decision itself (hard-snap, arm `NearInterpolate`) is equivalent — `ToConstraintArm(AirborneSnap) => NearInterpolate` is the A1 fix and equals the landing block's hard-coded arm |
|
||||||
|
| 2a | Interp queue clear at landing | `rmState.Interp.Clear()` at packet time (`:2456`) | deliberately NOT cleared — the carve-out's own comment ("queue deliberately NOT cleared … clearing stale waypoints is owned by the per-tick LANDING detection", AP-139) forbids restating "already empty" | **(c)** — see §2.1 |
|
||||||
|
| 2b | Collision-shadow publish at landing | **NOT published** — the landing block returns at `:2551` without `LiveEntityShadowPublisher.TryPublishRemote` (grep: publishes exist at `:2366`, `:2667`, `:2940` only) | published — `AirborneSnap` falls through to the NPC tail's publish (`:2940`) | **(c), suspected pre-existing defect on the player copy** — see §2.2. **New finding: no 4b-3 review flagged this.** |
|
||||||
|
| 2c | `EnsureRemoteMotionBindings` at landing | called (`:2499-2503`, "the motion bindings still have to exist before the next per-tick commit can dispatch this remote's ground edge") | never called on the OnPosition path (NPC bindings come from UM dispatch / `OnVector`) | **(c)** — see §2.3 |
|
||||||
|
| 2d | `[remote-landing]` probes (`site: "controller"`) | emitted (`:2514-2550`) | not emitted | **(b)** — diagnostic-only (TEMPORARY family, #32 investigation); carry to the unified landing arm for all guids, or accept NPC lines appearing. Not behaviour. |
|
||||||
|
| 3 | Wire-cell adopt ordering | `rmState.CellId = p.LandblockId` unconditionally BEFORE routing (`:2279`, AP-135 comment) | adopted AFTER routing via `TryAdoptWireCellAfterRouting` (`:2886`), suppressed for the two placement arms | **(b)** with a proof obligation — see §2.4 |
|
||||||
|
| 4 | `LastServerPos`/`LastServerPosTime` sample timing | sampled BEFORE routing, inside the diagnostic roll-forward block (`:2306-2310` — the writes are unconditional; only the print is env-gated) | sampled AFTER routing (`:2893-2894`) | **(c)** — see §2.5 (the `firstUp` hint in `ApplyInterpolate` reads this) |
|
||||||
|
| 5 | `PrevServerPos`/`PrevServerPosTime` roll + `MaxRootMotionSpeedSinceLastUP` reset | player-only (`:2306-2308`), feeds the `[VEL_DIAG]` pace comparison and difference 6 | absent | **(b)** — diagnostic-feeding state; keep as an explicitly-diagnostic step (its only non-diag consumer is difference 6, which is deleted) |
|
||||||
|
| 6 | Post-routing synth-velocity install | grounded-routing tail only (`:2618-2628`): synth from the `Prev` pair, never `update.Velocity`, no else-clear; comment says "for diagnostics" | pre-routing (`:2751-2770`): `update.Velocity` preferred, else `LastServerPos` delta, else zero/false; gated `!isTeleportRoute` | **(b)** — the player install is **write-only state**: `rm.ServerVelocity`/`HasServerVelocity` production readers are exactly two, both player-excluded (`RemoteServerControlledVelocityCycle.Apply` internal `IsPlayerGuid` return; `RuntimeRemotePhysicsUpdater.cs:198` `!IsPlayerGuid` watchdog — reader enumeration done for this contract, grep `\.HasServerVelocity\|\.ServerVelocity` in `src/`). Delete the player install; the NPC formula becomes the single site. For player guids the unified formula writes values nothing reads — state-different, observably identical; the proof is the reader enumeration, restated in the implementation commit. |
|
||||||
|
| 7 | Velocity-cycle apply (`RemoteServerControlledVelocityCycle.Apply`) | not called (returns before reaching NPC section) | called (`:2896-2927`), gated `!isTeleportRoute && HasServerVelocity && !snapSuppressedByStick && ae exists` | **(a)** — survives via the helper's own internal `IsPlayerGuid` early return (`RemoteServerControlledVelocityCycle.cs:27-48`, AP-80 row + DEV-2: retail has NO pace-derived cycle refinement anywhere; the NPC half is a recorded acdream adaptation, the player exclusion is the retail-faithful half). The unified path calls it for all remotes; the internal data-driven guard carries the distinction. Provably identical: the guard exists and is first-class, not incidental. |
|
||||||
|
| 8 | TS-44 sticky suppression (`snapSuppressedByStick`) | absent | gates routing (`:2787-2840`), widened `\|\| isTeleportRoute`; arm site deliberately OUTSIDE the gate | **(a)** — the ONE named difference that survives the collapse. NOT vacuous for players: `LiveEntityMotionRuntimeController.StickToObjectFromWire` (`:342-353`, retail `stick_to_object` 0x005127e0, the mt-0 wire sticky trailer) can arm sticky on ANY remote host, player remotes included. Applying the gate uniformly would be a behaviour change for a stuck remote player (toward retail, whose `adjust_offset`-chain sticky overwrite is guid-blind — but that is TS-44's own recorded divergence to retire on its own evidence, not this slice's). Preserve as an explicit, named, commented gate applied to non-player guids only; the TS-44 register row already describes exactly this ("an NPC-only steady-state gate") and stays true. |
|
||||||
|
| 9 | Arm-call structure | three arm sites (teleport tail `:2355`, landing block `:2477`, grounded tail `:2606`) | one arm site (`:2854`), outside the sticky gate; `npcArm` initialised `UnroutedCatchUp` so a **sticky-suppressed packet still arms** (comment `:2842-2853`: retail's leash is independent of the acdream-only suppression) | **(b)** — one post-routing arm site in the unified tail; partition (D4 table) already proven identical by round-2 retail review §1. MUST preserve: sticky-suppressed-still-arms (with `UnroutedCatchUp`), guard-before-arm (R5), AP-138(3)'s one-packet unarmed residual on superseded incarnations. |
|
||||||
|
| 10 | D2 wire-airborne gate conjunct | `!update.IsGrounded` alone (`:2380` — teleport already returned above, conjunct structurally inert) | `!update.IsGrounded && !isTeleportRoute` (`:2727`) | **(b)** — trivially equivalent once dispatch shape (row 1) is unified; the unified gate keeps the explicit conjunct (positive exclusion, the R3-fix principle) |
|
||||||
|
| 11 | `AirborneNoOperation` return shape | bare `return` (`:2323-2327` — bookkeeping already written by rows 3/4's pre-writes) | writes `CellId`/`LastServerPos`/`LastServerPosTime` then returns (`:2702-2709`) | **(b)** — same observable outcome (AP-135's writes happen exactly once either way); the unified path writes them at the return via the existing helper |
|
||||||
|
| 12 | Wall-clock capture | multiple independent `DateTime.UtcNow` reads (`:2287`, `:2404`) | one `now` captured in the shared prologue, `nowSec` derived once (`:2681`) | **(b)** — microsecond-scale timestamp skew in acdream-only bookkeeping/diagnostics; unify on one capture (R9 already ruled the duplicate-write concern a clarity issue) |
|
||||||
|
| 13 | Entity-sync + shadow-publish tail | two inline copies (`:2363-2376` teleport, `:2664-2677` grounded) | third inline copy (`:2937-2950`) | **(b)** — byte-equivalent triplet (`entity.SetPosition(body.Position)`; `ParentCellId = rmState.CellId`; `Rotation = body.Orientation`; `TryPublishRemote(...)`); dedup into one helper/tail. The landing block's PARTIAL tail (sync without publish) is row 2b, not this row. |
|
||||||
|
| 14 | `RemoteServerControlledVelocityCycle.Apply`'s internal `0x50xxxxxx` early return | (helper-internal, not a branch copy) | (same) | **(a)** — already correctly placed as data-driven logic in the helper; the collapse makes it the ONLY carrier of the player/NPC animation distinction. Do not move, do not "simplify" — its doc block is the DEV-2 evidence chain. |
|
||||||
|
|
||||||
|
**Guid ranges, load-bearing for tests:** `IsPlayerGuid` is
|
||||||
|
`(guid & 0xFF000000) == 0x50000000` (`:158-159`). The connected-gate evidence
|
||||||
|
in the 4b-3 contract confirms creatures arrive as `0x8xxxxxxx`. Every
|
||||||
|
dual-guid test in §5 uses one guid from each range.
|
||||||
|
|
||||||
|
### 2.1 Row 2a — the landing interp-queue clear (c)
|
||||||
|
|
||||||
|
The two copies contradict each other's comments for the same scenario
|
||||||
|
(wire-grounded packet, body not in contact). The carve-out's comment
|
||||||
|
explicitly warns a reader who believes the queue is empty could delete
|
||||||
|
AP-139's per-tick landing clear; the player block clears at packet time with
|
||||||
|
its own rationale ("pre-arc waypoints are stale").
|
||||||
|
|
||||||
|
**Equivalence hypothesis to prove or refute:** between the packet and the
|
||||||
|
next DR tick, a populated queue on a not-in-contact body is inert — retail's
|
||||||
|
`InterpolationManager::adjust_offset` @0x00555D30 gates its entire body on
|
||||||
|
`CONTACT_TS` (the AP-140 retirement evidence, comment at `:1154-1176`), and
|
||||||
|
acdream's per-tick walk honours the same gate — and the tick that derives
|
||||||
|
contact fires AP-139's clear on the `!previousOnWalkable && finalOnWalkable`
|
||||||
|
edge **before** any queue walk in that same tick. If both halves hold, the
|
||||||
|
clear-vs-no-clear difference is unobservable and the unified landing arm
|
||||||
|
adopts the NO-clear shape (the one with the register row and the do-not-restate
|
||||||
|
warning). If either half fails — e.g. the per-tick order walks the queue
|
||||||
|
before the landing clear — the difference is live; STOP, report, and preserve
|
||||||
|
the player clear as an explicit landing-arm step for BOTH guids only if the
|
||||||
|
evidence shows the clear is the correct behaviour (that would be a
|
||||||
|
behaviour change on the NPC arm and needs its own justification + test —
|
||||||
|
split-on-discovery, process rule 2).
|
||||||
|
|
||||||
|
### 2.2 Row 2b — the landing shadow publish (c, suspected player-copy defect)
|
||||||
|
|
||||||
|
The #184 Slice 2b design comments in this very file (`:2090-2100`,
|
||||||
|
`:2655-2663`) state the design as "player shadows now follow the RESOLVED
|
||||||
|
body — via the DR-tick loop … and the player UP-branch tail below … exactly
|
||||||
|
like NPCs" and "this keeps collision == render for the first UP". The landing
|
||||||
|
block hard-moves the body and syncs the render entity but does NOT publish
|
||||||
|
the shadow, leaving collision ≠ render for up to one DR tick — against the
|
||||||
|
file's own stated design. The NPC copy publishes on the same scenario.
|
||||||
|
|
||||||
|
**Resolution path:** this looks like a pre-existing oversight in the player
|
||||||
|
copy, not a justified difference. Per process rule 2 (split on discovery), if
|
||||||
|
investigation confirms it: fix it as its **own commit** (add the publish to
|
||||||
|
the landing block, with a dual-guid test asserting shadow-follows-body after
|
||||||
|
a landing packet for both guid ranges) BEFORE the collapse commit, so the
|
||||||
|
collapse itself stays behaviour-identical and the fix is separately
|
||||||
|
revertible. If investigation instead finds a deliberate reason the player
|
||||||
|
landing must not publish (none is written down anywhere — check
|
||||||
|
`LiveEntityShadowPublisher.TryPublishRemote`'s own gates first), preserve it
|
||||||
|
as a named difference with a new register row (register rule 1). Do not fold
|
||||||
|
the change silently into the collapse either way.
|
||||||
|
|
||||||
|
### 2.3 Row 2c — landing bindings (c)
|
||||||
|
|
||||||
|
`EnsureRemoteMotionBindings` at landing exists so the next per-tick commit
|
||||||
|
can dispatch the ground edge (`HitGround` via `set_on_walkable`, Bug B). For
|
||||||
|
the NPC copy, bindings normally exist already (every UM dispatch and every
|
||||||
|
`OnVector` jump ensures them), but "normally" is not "provably": enumerate
|
||||||
|
whether an NPC can reach the landing scenario with no prior UM/vector (e.g.
|
||||||
|
spawned airborne, first packet is the landing UP). If yes, the NPC copy has a
|
||||||
|
latent missing-bindings gap and the unified landing arm ensures bindings for
|
||||||
|
all guids (a strictly-additive, idempotent call — verify idempotence in
|
||||||
|
`LiveEntityMotionRuntimeController.EnsureRemoteMotionBindings` before
|
||||||
|
claiming it). If no, the call is player-load-bearing only and unifying on
|
||||||
|
"always ensure" is still safe by idempotence — prefer that, stating the
|
||||||
|
argument. This row should resolve to (b) with a short proof; it is (c) only
|
||||||
|
until the idempotence + reachability walk is written down.
|
||||||
|
|
||||||
|
### 2.4 Row 3 — wire-cell adopt ordering (b), proof obligation
|
||||||
|
|
||||||
|
The player pre-write and the NPC post-adopt end at the same value on every
|
||||||
|
path: placement arms overwrite `remote.CellId` with the resolved cell
|
||||||
|
(`RuntimeSetPositionState.cs:5013`), non-placement arms adopt the wire cell
|
||||||
|
(player: pre-write; NPC: `TryAdoptWireCellAfterRouting`, which adopts for
|
||||||
|
`AirborneSnap`/`SteadyStateInterpolate`/`UnroutedCatchUp`), and the D2/no-op
|
||||||
|
early returns write it through the helper. The residual question is whether
|
||||||
|
anything READS `remote.CellId` **inside the synchronous routing window**
|
||||||
|
where the two orderings differ. Traced for this contract:
|
||||||
|
|
||||||
|
- the constraint anchor does NOT — `ArmConstraintAfterOperation` reads
|
||||||
|
`host.Position`, and the remote host's `getPosition`
|
||||||
|
(`LiveEntityMotionRuntimeController.cs:167-170`) builds it from
|
||||||
|
`hostRecord.FullCellId` (committed by the shared prologue's
|
||||||
|
`RebucketLiveEntity`, or by the placement) — never `rm.CellId`;
|
||||||
|
- `ApplyInterpolate`, the free-flight carve-out, and
|
||||||
|
`WillAdvanceRemoteMotion` do not read it;
|
||||||
|
- the tails read it AFTER the adopt on both copies today.
|
||||||
|
|
||||||
|
The implementer re-verifies this reader enumeration at implementation time
|
||||||
|
(one grep + walk, stated in the commit), then unifies on the NPC shape
|
||||||
|
(post-routing, arm-suppressed adopt — it is the one with the documented
|
||||||
|
suppression rule) with the early-return paths writing via the helper.
|
||||||
|
|
||||||
|
### 2.5 Row 4 — sample timing and `firstUp` (c)
|
||||||
|
|
||||||
|
`ApplyInterpolate`'s AP-87 backstop reads
|
||||||
|
`firstUp = remote.LastServerPosTime <= 0.0`
|
||||||
|
(`RuntimeRemoteSteadyStatePosition.cs:138`), and its own doc states the
|
||||||
|
asymmetry: the player caller stamps before routing, so `firstUp` is
|
||||||
|
"structurally false there". Unifying on post-routing sampling makes a player
|
||||||
|
remote's genuine first UP evaluate `firstUp == true` → forced `Snapped`
|
||||||
|
instead of possibly `Enqueued`.
|
||||||
|
|
||||||
|
**Equivalence hypothesis:** on a genuine first UP the `RemoteMotion` was
|
||||||
|
created this same packet with `Body.Position = worldPos` (the creation branch
|
||||||
|
`:2144-2171`), so `bodyToTarget == 0` and `Snapped` vs `Enqueued` differ only
|
||||||
|
in (i) clearing an already-empty queue and (ii) `Enqueue`'s possible
|
||||||
|
immediate-orientation return vs the snap's direct orientation write — both
|
||||||
|
ending at the same orientation for a zero-distance target. Also enumerate the
|
||||||
|
non-creation `firstUp` producers (a UM's locomotion-entry refresh stamps
|
||||||
|
`LastServerPosTime` in `OnMotion`, making `firstUp` false before the first UP
|
||||||
|
— both copies inherit that identically). If the hypothesis survives the walk,
|
||||||
|
unify on ONE post-routing sample (row 4 → (b)) and keep the player diagnostic
|
||||||
|
roll-forward reading the OLD value before the sample point. If it does not,
|
||||||
|
preserve the pre-routing stamp as an explicit named step and record why. Do
|
||||||
|
not hand-wave the `Enqueue` immediate-orientation subtlety — read
|
||||||
|
`InterpolationManager.Enqueue` before claiming equivalence.
|
||||||
|
|
||||||
|
## 3. Target shape
|
||||||
|
|
||||||
|
One remote routing tail, zero `IsPlayerGuid` branching in `OnPosition`'s
|
||||||
|
remote section except the single named survivor:
|
||||||
|
|
||||||
|
```
|
||||||
|
shared prologue (unchanged) // tracker, get-or-create+seed,
|
||||||
|
// velocity install, rebucket,
|
||||||
|
// classification, generic pose
|
||||||
|
if IsAirborneNoOperation(route):
|
||||||
|
ApplyWireAirborneLeftoverBookkeeping(...) // row 11 unified shape
|
||||||
|
return
|
||||||
|
isTeleportRoute = OwnsTeleportPlacement(route)
|
||||||
|
if !update.IsGrounded && !isTeleportRoute: // D2, one site (row 10)
|
||||||
|
ApplyWireAirborneLeftoverBookkeeping(...)
|
||||||
|
return
|
||||||
|
[NPC-only, row 8 — THE named survivor]
|
||||||
|
snapSuppressedByStick = ... (non-player guids only, unchanged predicate)
|
||||||
|
if !snapSuppressedByStick || isTeleportRoute:
|
||||||
|
routing = RunRemoteArmTail(...) // teleport decided INSIDE
|
||||||
|
if routing is null: return // routing (row 1); landing
|
||||||
|
arm = routing.Arm // family = AirborneSnap arm
|
||||||
|
// (row 2), incl. its resolved
|
||||||
|
// 2a/2b/2c steps
|
||||||
|
else: arm = UnroutedCatchUp // sticky-suppressed still arms
|
||||||
|
TryArmConstraintAfterOperation(ToConstraintArm(arm), rmState) // ONE site (row 9)
|
||||||
|
TryAdoptWireCellAfterRouting(rmState, arm, p.LandblockId) // ONE site (row 3)
|
||||||
|
one post-routing LastServerPos/Time sample // row 4 resolution
|
||||||
|
if !isTeleportRoute: synth-velocity install // row 6: NPC formula, all guids
|
||||||
|
if !isTeleportRoute && HasServerVelocity && !snapSuppressedByStick && ae:
|
||||||
|
RemoteServerControlledVelocityCycle.Apply(...) // row 7: internal guid guard
|
||||||
|
one entity-sync + shadow-publish tail // row 13
|
||||||
|
```
|
||||||
|
|
||||||
|
The teleport arm needs no dedicated pre-block: `ApplyRemoteContactRouting`
|
||||||
|
already dispatches it first (D5), and the post-routing steps are all either
|
||||||
|
teleport-suppressed by existing predicates (`TryAdoptWireCellAfterRouting`,
|
||||||
|
the two `!isTeleportRoute` gates) or teleport-correct (the arm partition, the
|
||||||
|
tail sync from the resolved body — invariant 2). The landing family
|
||||||
|
dissolves into the `AirborneSnap` arm: `ToConstraintArm(AirborneSnap) =>
|
||||||
|
NearInterpolate` (the A1 fix) already produces the landing block's exact arm
|
||||||
|
value, and rows 2a-2d state how each remaining landing-block extra resolves.
|
||||||
|
|
||||||
|
**Surviving differences: exactly one branch (row 8, TS-44 sticky gate,
|
||||||
|
non-player guids), plus two data-driven distinctions that live inside helpers
|
||||||
|
and involve no branching in `OnPosition`** — row 7/14's
|
||||||
|
`RemoteServerControlledVelocityCycle.Apply` internal player return (AP-80 /
|
||||||
|
DEV-2) and the shared prologue's `SeedRemoteSpawnPlacement` mover flags.
|
||||||
|
Anything else surviving means a (c) row resolved to "preserve" — each such
|
||||||
|
outcome must be reported, not silently kept.
|
||||||
|
|
||||||
|
Whether the unified tail lives as a private method on the controller or
|
||||||
|
inline in `OnPosition` is the implementer's choice; what is pinned is ONE
|
||||||
|
copy, the step order above, and that `ApplyRemoteContactRouting`,
|
||||||
|
`RunRemoteArmTail`, `ToConstraintArm`, `TryArmConstraintAfterOperation`,
|
||||||
|
`TryAdoptWireCellAfterRouting`, and `ApplyWireAirborneLeftoverBookkeeping`
|
||||||
|
keep their current semantics unchanged (their internals are NOT in scope —
|
||||||
|
they were verified twice in round 2).
|
||||||
|
|
||||||
|
## 4. Behaviour-preservation strategy
|
||||||
|
|
||||||
|
This is a refactor: **every row of §2 is either preserved with its stated
|
||||||
|
justification or unified with its stated proof — never silently merged.**
|
||||||
|
Concretely:
|
||||||
|
|
||||||
|
1. **Characterisation first.** Before touching production code, write the
|
||||||
|
dual-guid matrix tests of §5 against the CURRENT code and confirm they
|
||||||
|
pass, encoding today's behaviour — including the known asymmetries (rows
|
||||||
|
2b, 8) asserted AS asymmetries where they exist today. These tests are the
|
||||||
|
refactor's referee.
|
||||||
|
2. **Per-row disposition in the commit message.** The implementation commit
|
||||||
|
carries a conformance section listing every §2 row and its outcome:
|
||||||
|
`(a) preserved — <justification>`, `(b) unified — <proof>`, or
|
||||||
|
`(c) resolved to (a)/(b) — <evidence>`. The round-2 architecture review's
|
||||||
|
Part 1 (call-site table: pre-fix sequence, post-fix sequence, verdict,
|
||||||
|
what-was-checked-for-and-not-found) is the format to follow.
|
||||||
|
3. **(c) rows resolve before merge.** A (c) row that cannot be resolved to
|
||||||
|
proven-equivalent without changing behaviour is PRESERVED as an explicit
|
||||||
|
named difference and reported — this slice never ships a behaviour change
|
||||||
|
(§7). A (c) row that turns out to be a pre-existing defect (2b is the
|
||||||
|
prime suspect) is fixed in its own commit per process rule 2, with its own
|
||||||
|
test, before the collapse commit.
|
||||||
|
4. **No helper-internal edits.** If the collapse seems to require changing
|
||||||
|
`ApplyRemoteContactRouting` / `RuntimeRemoteSteadyStatePosition` /
|
||||||
|
`RuntimeRemoteFarSnapPosition` / classifier internals, stop — that is a
|
||||||
|
scope breach, not a refactor step.
|
||||||
|
5. **Comment hygiene** (process rule 6): every comment inside the collapsed
|
||||||
|
region is re-verified against the code beside it; the comments that
|
||||||
|
NARRATE the duplication ("the SAME shared entry point the NPC arm below
|
||||||
|
calls", "mirror of the player-remote arm above", `RunRemoteArmTail`'s
|
||||||
|
three-copies rationale, `ApplyWireAirborneLeftoverBookkeeping`'s
|
||||||
|
"shared by both remote branches" paragraph, `ApplyRemoteContactRouting`'s
|
||||||
|
"the two callers stay one decision") are rewritten for the one-path world.
|
||||||
|
Symbol references over line numbers, always.
|
||||||
|
|
||||||
|
## 5. Test strategy — the one that would have caught the 4b-3 defects
|
||||||
|
|
||||||
|
The 4b-3 defects survived because every test drove ONE copy. The antidote is
|
||||||
|
structural: **every scenario test in this slice is a `[Theory]` parameterised
|
||||||
|
on guid — one `0x50xxxxxx` player-range guid, one `0x8xxxxxxx`
|
||||||
|
creature-range guid — running the identical packet sequence through
|
||||||
|
`OnPosition` and asserting on the full observable surface.** After the
|
||||||
|
collapse there is one path, so this is cheap; the point is it STAYS a theory
|
||||||
|
so a future re-divergence (a guid-gated edit to the unified tail) fails a
|
||||||
|
test instead of hiding.
|
||||||
|
|
||||||
|
Scenario matrix (each × both guids):
|
||||||
|
|
||||||
|
1. **Teleport commit** (TELEPORT_TS advance, in-view destination): body and
|
||||||
|
entity at resolved destination, hook ran (moveto cancelled, stick
|
||||||
|
released, interp queue empty), leash armed exactly once post-operation,
|
||||||
|
shadow published, **sequencer cycle unchanged and
|
||||||
|
`HasServerVelocity == false`** (the R3/A2 assertion — for BOTH guids;
|
||||||
|
pre-collapse this was true for players by control flow and for NPCs by
|
||||||
|
the `!isTeleportRoute` gate; post-collapse one mechanism must serve both).
|
||||||
|
2. **Landing packet** (wire grounded, body not in contact): body snapped to
|
||||||
|
wire pose, entity synced, armed exactly once (the A1 assertion —
|
||||||
|
`PositionManager.Constraint` null → non-null, the round-2-verified
|
||||||
|
direct-proof observable), plus the resolved 2a/2b/2c outcomes (queue
|
||||||
|
state, shadow state, bindings state) as pinned by their (c) resolutions.
|
||||||
|
3. **Wire-airborne, null-classified** (login-window shape): exactly AP-135's
|
||||||
|
bookkeeping (`CellId`, `LastServerPos`, `LastServerPosTime` — assert the
|
||||||
|
writes HAPPENED, the round-2 B1 gap, not only that nothing else did),
|
||||||
|
no body/queue/render/shadow write, no arm (the R1 assertion).
|
||||||
|
4. **Airborne no-op** (`NoPositionOperation`): AP-135 writes only, no arm.
|
||||||
|
5. **Near interpolate** (grounded, < 96 m, body in contact): enqueued (or
|
||||||
|
AP-87-snapped per its three conditions), armed exactly once.
|
||||||
|
6. **Far snap** (grounded, >= 96 m): placement executed, armed on every
|
||||||
|
placement outcome, wire-cell adopt suppressed.
|
||||||
|
7. **Sticky-suppressed steady-state** (NPC guid: sticky armed, near packet):
|
||||||
|
no snap/enqueue, but STILL armed once with `UnroutedCatchUp`. Player-guid
|
||||||
|
half of this theory asserts the CURRENT player behaviour (gate absent →
|
||||||
|
routing runs) — this is row 8's asymmetry, asserted explicitly as the
|
||||||
|
named difference so it is recorded in test, not hidden.
|
||||||
|
8. **NPC velocity-cycle** (grounded packet with synthesizable velocity):
|
||||||
|
creature guid gets a planned cycle; player guid's sequencer is untouched
|
||||||
|
(row 7/14's data-driven distinction, asserted as the intended difference).
|
||||||
|
|
||||||
|
Assertion surface per scenario (assert ALL, not a subset — process rule 4,
|
||||||
|
#312's lesson): `rmState.Body.Position/Orientation`, `entity.Position`/
|
||||||
|
`ParentCellId`/`Rotation`, shadow entries (`AllEntriesForDebug`),
|
||||||
|
`Interp` queue depth, `PositionManager.Constraint` (arm count via
|
||||||
|
null→non-null, or a counting seam if a packet can legally arm twice — it
|
||||||
|
cannot: D4), `rmState.CellId`, `LastServerPos`/`LastServerPosTime`,
|
||||||
|
`ServerVelocity`/`HasServerVelocity`, sequencer style/motion.
|
||||||
|
|
||||||
|
**Sabotage check (manual, once, before the collapse commit is finalised):**
|
||||||
|
re-run the 4b-3 experiment — introduce a deliberate defect into the unified
|
||||||
|
tail (e.g. skip the arm call) and confirm BOTH guid halves of the matrix
|
||||||
|
fail. If only one fails, the matrix has a per-guid hole; fix the test, not
|
||||||
|
the sabotage.
|
||||||
|
|
||||||
|
Existing tests: the three round-2 App tests
|
||||||
|
(`NpcAirborneSnap_LandingPacket_StillArmsTheLeash`,
|
||||||
|
`NpcTeleport_DoesNotInstallASynthesizedVelocity`,
|
||||||
|
`NullClassifiedNpc_WireAirbornePacket_WritesOnlyBookkeepingNoBodyOrShadow`)
|
||||||
|
are absorbed into the matrix as the creature-guid halves of scenarios 2/1/3 —
|
||||||
|
extended, not deleted. `LiveEntityNetworkRemoteTeleportPresentationTests`
|
||||||
|
and the Runtime teleport suites are untouched.
|
||||||
|
|
||||||
|
## 6. What must remain true
|
||||||
|
|
||||||
|
The 4b-3 contract's invariants bind unchanged; restated here as they apply to
|
||||||
|
this slice, plus the collapse-specific ones:
|
||||||
|
|
||||||
|
1. **The pose still advances on every refusal** — teleport/far placements
|
||||||
|
that never reach the engine still commit the accepted destination through
|
||||||
|
the `StoresAcceptedDestination` partition; `Deferred`/`RejectedByPlacement`
|
||||||
|
still do not. (Runtime-side; this slice must simply not perturb the call.)
|
||||||
|
2. **Presentation still syncs** — every arm that moves the body syncs the
|
||||||
|
render entity from the RESOLVED body and publishes the shadow (subject to
|
||||||
|
row 2b's resolution); a remote is never left rendered a packet behind its
|
||||||
|
body (#312's layer).
|
||||||
|
3. **The entity stays in-world** on every outcome: `InWorld`, clock active,
|
||||||
|
`FullCellId != 0`, spatial projection intact.
|
||||||
|
4. **The leash arms exactly once per accepted packet per the D4 partition** —
|
||||||
|
never zero (A1's class), never twice; sticky-suppressed still arms;
|
||||||
|
guard-before-arm on the re-entrant arms (R5); AP-138(3)'s superseded-
|
||||||
|
incarnation residual unchanged.
|
||||||
|
5. **AP-135's two writes survive on every early-return path** (the
|
||||||
|
register row stays).
|
||||||
|
6. **The per-packet prologue runs for every classification**: generic render
|
||||||
|
pose (gate `OwnsSteadyState`, unchanged), `RebucketLiveEntity`, the
|
||||||
|
`TryCommitAuthoritativeVelocity` install, currency re-validation chain.
|
||||||
|
7. **Teleport semantics unchanged**: hook before placement, hook regardless
|
||||||
|
of outcome, queue cleared by the hook not the route flag, no velocity
|
||||||
|
write on the teleport arm, sticky does not suppress it, decided ahead of
|
||||||
|
every contact carve-out (D5).
|
||||||
|
8. **AP-87's snap conditions, AP-139's landing clear, and AP-140's
|
||||||
|
contact-not-walkability routing are untouched** (their owners are outside
|
||||||
|
this slice's edit surface; rows 2a/4 touch only their CALLERS' timing and
|
||||||
|
must prove equivalence first).
|
||||||
|
9. **`ApplyRemoteContactRouting`'s `AirborneNoOperation` throw stays
|
||||||
|
unreachable** — both early returns (now one) still precede routing.
|
||||||
|
10. **The local-player paths are untouched**: force-position block, F751
|
||||||
|
`OfferDestination` tail, streaming observer, projectile short-circuit —
|
||||||
|
everything above the remote section and the `update.Guid ==
|
||||||
|
_playerServerGuid` filter semantics.
|
||||||
|
|
||||||
|
## 7. Explicit non-goals
|
||||||
|
|
||||||
|
- **No behaviour change.** Any (c) row resolving to "the difference is real
|
||||||
|
and one side is wrong" becomes its own separately-committed, separately-
|
||||||
|
tested fix (2b's suspected missing publish) or is preserved and reported —
|
||||||
|
the collapse commit itself is behaviour-identical by the §5 matrix.
|
||||||
|
- **No register-row retirement.** AP-135, AP-137, AP-138, AP-87, TS-44, the
|
||||||
|
AP-80 adaptation — all stay. Row TEXT may be touched only to repoint
|
||||||
|
citations at moved/renamed symbols (bookkeeping, same commit), never to
|
||||||
|
change what a row claims.
|
||||||
|
- **No absorption of C4 route 5 (projectile) or route 7 (pickup/parent).**
|
||||||
|
Route 5 will add its arm against the collapsed single path — that is the
|
||||||
|
payoff, not the scope. `OwnsPlacement` keeps excluding
|
||||||
|
`ProjectileAuthoritative`.
|
||||||
|
- **No edits inside** `ApplyRemoteContactRouting`, the classifier,
|
||||||
|
`RuntimeRemoteSteadyStatePosition`, `RuntimeRemoteFarSnapPosition`,
|
||||||
|
`RuntimeRemotePlacementDriveController`, `RemoteTeleportHook`, or
|
||||||
|
`RemoteServerControlledVelocityCycle` — except comment repointing.
|
||||||
|
- **No probe deletions.** The TEMPORARY families (`REMOTE_LANDING`,
|
||||||
|
`REMOTE_SLIDE`, `REMOTE_TELEPORT`, sticky) move with their code; stripping
|
||||||
|
them is the physics-settling cleanup, not this slice.
|
||||||
|
- **No OnMotion/OnVector/OnState changes** beyond comment repointing if a
|
||||||
|
cited symbol moves.
|
||||||
|
|
||||||
|
## 8. #315 — recommendation: fix it in this slice, as its own commit
|
||||||
|
|
||||||
|
#315 records the per-packet `Func<bool>` closure allocation at the three
|
||||||
|
`RunRemoteArmTail` call sites; its root cause is
|
||||||
|
`ApplyRemoteContactRouting`'s `Func<bool>` parameter, and its acceptance
|
||||||
|
criterion is "the call sites do not allocate a fresh delegate per packet."
|
||||||
|
|
||||||
|
**Fix it here.** The collapse converges the three call sites into one and
|
||||||
|
redesigns exactly the seam (`RunRemoteArmTail`'s signature and its
|
||||||
|
`isCurrentPositionOwner` plumbing) the fix must touch; a standalone #315
|
||||||
|
session afterwards would rewrite the same lines a second time, and route 5 is
|
||||||
|
about to add its arm against whichever shape exists. Both round-2 reviews
|
||||||
|
deferred it only because it was out of 4b-3's scope — "file it with the probe
|
||||||
|
family rather than churning the seam now"; this slice IS the seam churn.
|
||||||
|
|
||||||
|
**But as the SECOND commit of the slice, not fused into the collapse
|
||||||
|
commit.** Commit 1: the collapse, behaviour-identical, keeping the current
|
||||||
|
closure shape so the collapse diff is purely structural and the §5 matrix
|
||||||
|
referees it alone. Commit 2: the allocation fix on the now-single call site
|
||||||
|
(cached per-controller delegate, a small readonly state struct with a static
|
||||||
|
lambda, or an interface-shaped callback — implementer's choice; the
|
||||||
|
`Func<bool>` parameter is `internal`, so test call sites update mechanically),
|
||||||
|
closing #315 with its ISSUES.md move in the same commit. Two commits keep the
|
||||||
|
behaviour-preservation review and the allocation review independently
|
||||||
|
revertible; one session avoids the double churn. This is a recommendation
|
||||||
|
with a reason, not a hedge: the only argument against ("two concerns") is
|
||||||
|
answered by the commit split.
|
||||||
|
|
||||||
|
## 9. Gates
|
||||||
|
|
||||||
|
- **Focused:** the §5 dual-guid matrix (new), plus the existing Runtime
|
||||||
|
teleport/steady-state suites and App physics suites, all 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,020 passed /
|
||||||
|
4 skipped / 0 failed at `b260bcd1`.** The count will rise with the new
|
||||||
|
matrix; measure and record the new figure. 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.
|
||||||
|
- **Connected evidence — argued, not assumed.** If the slice lands as
|
||||||
|
contracted — zero behaviour deltas; every §2 row (a)-preserved or
|
||||||
|
(b)-proven — then no new connected gate is REQUIRED: the behaviours routed
|
||||||
|
through this code passed their live gates days ago on this exact branch
|
||||||
|
(4b-2's far-snap walk; 4b-3's 16-probe-line creature-teleport session,
|
||||||
|
user-accepted "all works"), and a proven-identical refactor cannot alter
|
||||||
|
what those sessions verified. The thing the previous live gates could not
|
||||||
|
see — a defect hiding in the un-exercised copy — is precisely what the §5
|
||||||
|
dual-guid matrix now covers deterministically, which is stronger evidence
|
||||||
|
than another look-around session (process rule 5: a clean-looking session
|
||||||
|
proves little). **Two exceptions re-arm the connected requirement:**
|
||||||
|
(i) any behaviour-affecting (c) resolution shipped as its own commit (2b's
|
||||||
|
publish fix would warrant the landing-observation half of the handoff's
|
||||||
|
far-snap recipe: observer watches a second character jump/run off a ledge
|
||||||
|
and land, watching for any snap-back or collision oddity at landing);
|
||||||
|
(ii) any unplanned behaviour delta discovered late (stop condition anyway).
|
||||||
|
If the user happens to be running a session, an opportunistic creature
|
||||||
|
`@teleto` + landing walk with `ACDREAM_PROBE_REMOTE_TELEPORT=1` is cheap
|
||||||
|
insurance — optional, not a gate; record probe lines if run.
|
||||||
|
|
||||||
|
## 10. Budget and stop conditions
|
||||||
|
|
||||||
|
**Budget:** ~250-450 changed non-comment production lines, **net negative in
|
||||||
|
`LiveEntityNetworkUpdateController.cs`** (the remote section is ~690 lines at
|
||||||
|
`b260bcd1`; the collapse should remove roughly a copy's worth minus the
|
||||||
|
unified tail). New test code: the §5 matrix, unbounded by this figure but
|
||||||
|
expected ~400-700 lines. Commit 2 (#315): ~40-100 lines.
|
||||||
|
|
||||||
|
**Stop and report rather than pushing through when:**
|
||||||
|
|
||||||
|
1. Production-line delta exceeds ~500, or the unified path needs a THIRD
|
||||||
|
guid-conditional beyond row 8's named survivor.
|
||||||
|
2. Any (c) row resolves to "cannot prove equivalence AND cannot preserve
|
||||||
|
without contorting the unified path" — report the row and the evidence;
|
||||||
|
the fallback of keeping both copies for that one step with a tracking
|
||||||
|
issue is a legitimate outcome, silent unification is not.
|
||||||
|
3. **A behavioural difference is found that §2 does not list.** Add it to
|
||||||
|
the inventory, classify it, and get it reviewed before proceeding — a
|
||||||
|
missed difference is this contract's failure mode, and discovering one
|
||||||
|
mid-implementation means the inventory walk must be redone, not patched.
|
||||||
|
4. The §5 characterisation matrix FAILS against the current `b260bcd1` code
|
||||||
|
in a way this contract does not predict — that is a pre-existing defect
|
||||||
|
or a wrong row here; split it out (process rule 2) or correct the
|
||||||
|
contract first.
|
||||||
|
5. The complete Release suite deviates from baseline beyond the two named
|
||||||
|
flakes.
|
||||||
|
|
||||||
|
## 11. Contradictions found while writing this contract — reported, not smoothed
|
||||||
|
|
||||||
|
1. **The reviews' "the two callers stay one decision" framing understates
|
||||||
|
the residual duplication.** Round 2 verified the five extracted-helper
|
||||||
|
call sites are behaviour-identical — true — but no review inventoried the
|
||||||
|
copies' remaining differences as a set. Specifically, **row 2b (the
|
||||||
|
player landing block performs the entity sync but NOT the shadow publish,
|
||||||
|
while the NPC copy's identical scenario publishes) appears in no review,
|
||||||
|
no register row, and contradicts the file's own #184 Slice 2b design
|
||||||
|
comments** ("player shadows now follow the resolved body … exactly like
|
||||||
|
NPCs", "keeps collision == render for the first UP"). It is at most a
|
||||||
|
one-DR-tick collision/render divergence, but it is exactly the class
|
||||||
|
(#184/#312 — presentation/collision desync) this campaign treats as real.
|
||||||
|
2. **`ApplyRemoteContactRouting`'s doc** ("the player-remote caller reaches
|
||||||
|
this method only with `Body.InContact == true` … so the carve-out is
|
||||||
|
inert there and the two callers stay one decision") is accurate today but
|
||||||
|
will be FALSE after the collapse (the landing family becomes the
|
||||||
|
`AirborneSnap` arm for player guids too). It is on §4.5's rewrite list;
|
||||||
|
flagged here because it is the one comment whose staleness would actively
|
||||||
|
misdirect the implementer.
|
||||||
|
3. **The 4b-3 contract's baseline figure (11,027 at `2eb39a02`) vs this
|
||||||
|
task's (11,020 / 4 at `b260bcd1`)** — not a true contradiction (different
|
||||||
|
HEADs; route 6 and #314 landed between), but stated so nobody "corrects"
|
||||||
|
one to the other.
|
||||||
|
4. **AP-137's "unifies player and NPC remotes on one behaviour"** is true
|
||||||
|
exactly as scoped (the D2 wire-airborne leftover shape) and must not be
|
||||||
|
read as claiming the copies are otherwise unified — rows 2-6, 8, 12-13
|
||||||
|
remain distinct at `b260bcd1`. The row needs no edit; the caution is for
|
||||||
|
readers of it.
|
||||||
|
5. **`ApplyInterpolate`'s doc claims its `firstUp` evaluation "is exact for
|
||||||
|
BOTH kinds"** because the player caller pre-stamps. That is a description
|
||||||
|
of the asymmetry, not equivalence — the collapse (row 4) is where the
|
||||||
|
claim gets cashed out or the stamp order preserved; the doc will need the
|
||||||
|
matching rewrite either way.
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -120,9 +120,9 @@ public sealed class LiveEntityNetworkBranchRoutingTests
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AP-140 (retired 2026-08-04): the SECOND accepted-Position routing
|
/// AP-140 (retired 2026-08-04): the accepted-Position routing gate
|
||||||
/// gate — <c>OnPosition</c>'s player-remote landing block — must
|
/// for the free-flight/landing decision must select the hard snap on
|
||||||
/// select the hard snap on retail's CONTACT predicate
|
/// retail's CONTACT predicate
|
||||||
/// (<c>InterpolationManager::adjust_offset</c> @0x00555D30 gates its
|
/// (<c>InterpolationManager::adjust_offset</c> @0x00555D30 gates its
|
||||||
/// whole body on <c>transient_state & 1</c> @0x00555D52, and bit 0
|
/// whole body on <c>transient_state & 1</c> @0x00555D52, and bit 0
|
||||||
/// is <c>CONTACT_TS</c>), not on the client <c>Airborne</c> flag,
|
/// is <c>CONTACT_TS</c>), not on the client <c>Airborne</c> flag,
|
||||||
|
|
@ -130,13 +130,23 @@ public sealed class LiveEntityNetworkBranchRoutingTests
|
||||||
/// set that also captures a remote sliding on a steep face.
|
/// set that also captures a remote sliding on a steep face.
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
|
/// C4 route 4b-3's OnPosition collapse (2026-08-04) dissolved
|
||||||
|
/// <c>OnPosition</c>'s former standalone player-remote LANDING
|
||||||
|
/// TRANSITION block — which used to carry its own
|
||||||
|
/// <c>if (!rmState.Body.InContact)</c> copy of this gate — into
|
||||||
|
/// <c>ApplyRemoteContactRouting</c>'s free-flight carve-out, now the
|
||||||
|
/// ONE site (for every guid) that decides this. The gate itself did
|
||||||
|
/// not move in spirit, only in address: this pin follows it there.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
/// A source pin rather than a behavioural fixture for the reason this
|
/// A source pin rather than a behavioural fixture for the reason this
|
||||||
/// class already documents: the controller's dependency set is
|
/// class already documents: the controller's dependency set is
|
||||||
/// composition-only. The twin gate inside
|
/// composition-only. The gate inside <c>ApplyRemoteContactRouting</c>
|
||||||
/// <c>ApplyRemoteContactRouting</c> — a static method, so reachable —
|
/// — a static method, so reachable — IS covered behaviourally, in
|
||||||
/// IS covered behaviourally, in
|
|
||||||
/// <c>LiveEntityNetworkRemoteSteadyStateIntegrationTests</c>. Restore
|
/// <c>LiveEntityNetworkRemoteSteadyStateIntegrationTests</c>. Restore
|
||||||
/// <c>if (rmState.Airborne)</c> here and this test fails.
|
/// <c>if (rmState.Airborne)</c> or <c>if (remote.Airborne)</c> here
|
||||||
|
/// and this test fails.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -145,7 +155,7 @@ public sealed class LiveEntityNetworkBranchRoutingTests
|
||||||
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
|
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
|
||||||
|
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"if (!rmState.Body.InContact)",
|
"if (!remote.Body.InContact)",
|
||||||
source,
|
source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
// `rmState.Airborne` survives as a WRITE target and in prose (the
|
// `rmState.Airborne` survives as a WRITE target and in prose (the
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue