Commit graph

3246 commits

Author SHA1 Message Date
Erik
2687d89363 docs: replace route 7's unfalsifiable gate criterion with a positive assertion (#319)
The original criterion — "the session counts ONLY if cause=propagate lines
appear" — cannot fail in the presence of the bug it exists to catch. #319
makes a player-parented child emit no probe line at all, so the defect's
signature is ABSENCE, which that wording reads as "not exercised" rather than
"broken". Two captured gate logs contain #319 and neither flags it; the second
was run specifically to thicken this gate and still missed it.

Replaced with a positive assertion: read the equipped child's FullCellId and
require it to EQUAL the parent's after a crossing — a zero child cell is a
failure, not a silence. Probe-line volume drops to a secondary check. And the
gate must now be run with a PLAYER parent as well as a creature parent, since
#319 exists precisely because every probe-firing parent in both logs was
instance-sequence 0 and the sole player parent was the sole failure.

The rule this generalises, added to the handoff's process findings: a gate
whose failure mode is indistinguishable from a not-run manufactures
confidence. Counting evidence-of-success is not the same as asserting the
property; only the latter can fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 10:00:16 +02:00
Erik
195667db94 research: file #319 — player-parented children never get a canonical cell, and route 7's gate cannot see it
Found by asking why route 7's connected gate stayed thin (one cause=propagate
across 5-6 equipped landblock crossings) instead of recording the thinness and
moving on.

EquippedChildRenderController.cs:134 hardcodes ParentInstanceSequence: 0 for a
parented CreateObject. Correct for creatures and statics, which are genuinely
sequence 0; wrong for players, whose ObjectInstance is Character.TotalLogins
(ACE Player_Networking.cs:37). The relation files under (playerGuid, 0) while
the record carries TotalLogins, so both route-7 write sites — D1's attach
re-cell and D2's propagation lookup — key on an incarnation that never matches.
TryCommitParent does not validate the sequence, so the attach succeeds and
prints normally.

This is a cd3129e9 (route 7) REGRESSION that un-masked a latent bug: the
TickChild call route 7 deleted was keyed on the child guid alone and was
structurally immune to a wrong parent key. Scope is wider than the local
player — every remote player's equipment is affected. Proven by class: every
probe-firing parent across both captured gate logs is 0x7/0x8 (sequence 0);
the sole 0x5 player parent is the sole failure.

User-visible consequence is NIL and that was verified rather than assumed —
rendering has an explicit fallback and attached children are structurally
excluded from spatial roots, physics worksets, collision retirement, radar and
picking.

THE FINDING THAT OUTRANKS THE DEFECT, and it is a flaw in my own gate design:
route 7's owed gate accepts a session "only if cause=propagate lines appear".
A zero-cell player child emits NO line, so the defect's signature is ABSENCE,
which that criterion reads as "not exercised" rather than "broken". Two
captured gate logs contain the defect and neither flags it. A gate that cannot
fail in the presence of its own target bug is worse than no gate — it
manufactures confidence. This is the same shape as route 3's round-2
regression, which I criticised at length in the closeout while shipping this.

The fix is deliberately NOT attempted here: it has more blast radius than the
bug. The player's canonical cell does not track the player during ordinary
movement, so correcting the key alone yields a stale cell rather than a right
one; and three sites are inert only because the cell is zero and would wake on
a fix (the hydration projectionCellId filter, RestoreShadow's broadphase row,
and the initial-create residence FullCellId refusal).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 09:59:35 +02:00
Erik
0991182198 research: scope C5 — the deletion is smaller than the plan implies, and half the rows are blocked on a behaviour change
Verified by symbol at HEAD 52175aa1, behind two exhaustive caller censuses
(legacy placement writers; the probe family).

DELETION INVENTORY: 6 deletable symbol groups (~540 production lines), 2 more
deletable only via #275, and 5 that the plan or handoff names as deletable but
are NOT. Deletable now, all with zero production callers: PhysicsEngine.Resolve
(~360 lines — this IS AD-1's legacy demote/lift body) plus its private
HasCellSurface; PhysicsEngine.ResolvePlacement; PlayerMovementController
.SetPosition (both overloads); CommitPreparedPosition; the BeginAcceptedPlacement
/BeginAuthoredPlacement test wrappers. Hazard recorded: production
IsSpawnCellReady sits physically inside the Resolve block.

THE PLAN'S FRAMING IS WRONG FOR HALF THE ROWS. AP-1 and AD-1 are provably
retirable in the deletion commit — both rows' "production still routes through
the legacy resolver" sentences are already false at HEAD. But AP-131 and
AD-60's legacy half are blocked on #275, which is a BEHAVIOUR CHANGE
(classify-then-merge on every steady-state Position, retail Gate A, wire-cell
withhold across a 45+ site residency-predicate blast radius) — not a deletion.
"C5 deletes the paths and the rows retire" holds for two rows and not the
other two.

SEVEN TEST-ONLY-CALLER CASES, each with a disposition, because this is where a
deletion slice silently removes coverage: Resolve's unit tests (behaviour gone
— delete); Issue133DungeonTeleportPrefixTests (behaviour MOVED — re-point, it
is a named-bug regression pin); InitialPlacementOverlapTests and
TransitionScratchDifferentialTests (verify-then-delete / re-point the
differential arm); ~19 files using PlayerMovementController.SetPosition as
fixture setup (mechanical re-point); CommitPreparedPosition's tests (re-point
at the leash-arm replacement); the Begin* wrappers' ~40 sites (keep as a seam).
Also flagged: the #316-preserving test pins a defect verbatim and inverts by
design once measured.

THE CELL-LESS TRIGGER, ESTABLISHED BY READING rather than deferred to a live
session: pickup-then-drop. TryApplyPickup leaves an ACTIVE cell-0 record, and
ACE's drop sends that guid a bare UpdatePosition (Player_Inventory.cs:1443)
with no client-side delete. What remains unknown is only whether a CreateObject
from NotifyPlayers (Landblock.cs:900) precedes it on the ordered stream, which
depends on when ACE populates the fresh item's known-players set — a two-minute
probe run settles it, with route 5's "the gate cannot exist, record that" as
the honest fallback.

SEQUENCING: run the owed gates FIRST as one cheap user session on the current
binary (route-7 thickening, the cell-less falsification, #316's measurement),
then C5a (deletions + #318 + route-2's B2 parity test + retire AP-1/AD-1), then
C5b (#275 + AP-131/AD-60, own contract and dual review), then C5c (closeout
gates after #280 per the plan's own ordering, the ~1,340-line probe strip, the
ledger). The probe strip goes LAST because the owed gates depend on the probes,
with a REMOTE_LANDING carve-out if #316 stays unmeasured.

Seven false or stale plan claims found and listed, including AP-1/AD-1's own
row texts, ILocalPlayerTeleportPlacement being called deletable when it is a
live post-commit suffix, and the 4b "to delete" list being overtaken — the
pre-op ConstrainTo and duplicate constants are already gone and the remainder
is now canonical post-collapse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 09:26:11 +02:00
Erik
52175aa14a docs: flip CLAUDE.md current state — C4 complete, pointing at the closeout handoff
Records the five routes plus the OnPosition collapse with their SHAs, the
11,027 -> 11,090 suite trajectory, and the gate status including the two
honest gaps (route 7's single cause=propagate sample; 4b-3's cause=cellless
still unrun with an UNESTABLISHED trigger after route 7 invalidated its
recipe).

Points successors at the closeout handoff before any C5 or placement work,
and names the two costliest process findings inline so they are visible
without opening it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:13:48 +02:00
Erik
934fe8dd2e docs: close route 3's autorun-cancel gate — verified live, correcting my own note
An earlier revision of the gate-results section recorded route 3's autorun
cancel as live-unverified, because the first three portals all reported
autorun=unchanged. That was true of those three and wrong as a conclusion: the
user portalled again with autorun engaged and the fourth line reads

  [local-tp] cause=portal host=graphical status=Committed gen=5 seq=4
  dest=0x00070145 resolved=0x00070145 hookTail=ran leash=armed
  autorun=cancelled

This verifies the PlayerTeleported @0x006B32B0 SetAutoRun(0,1) +
SendMovementEvent port in live play. It was a real gap before this slice —
nothing cancelled the J5.4 autorun latch on arrival, so auto-running into a
portal left you running on the far side where retail stops you.

All four load-bearing fields on that line read correctly: Committed,
hookTail=ran (inversion B — the local teleport_hook runs AFTER placement,
opposite to 4b-3's remote arm), leash=armed (inversion A — the leash IS armed
here, opposite to route 2's ForcePosition rule), and autorun=cancelled.

Route 3's connected gate is now fully exercised. Route 7's remains thin (one
cause=propagate) and gate 4 remains unrun with an UNESTABLISHED trigger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:12:40 +02:00
Erik
8ed4a7b24a docs: record the C4 connected gate results — three of four run, with their gaps
User-run against the exact e0f96a55 Release binary with the retail UI and both
probes; user verdict "works great".

Route 3 passes unambiguously: three [local-tp] lines, all Committed,
hookTail=ran, leash=armed, across three destinations. leash=armed is the
load-bearing observation — before the round-2 R3 fix that field read
IsFullyConstrained() and could only ever print unarmed, so the line proves
both the leash arm and the corrected probe.

Route 7 passes but THINLY: 17 [child-cell] lines, of which 13 attach, 3
delete, and exactly ONE propagate. The stated criterion (at least one
propagate) is met, so the gate passes — but propagation across a parent cell
crossing is the slice's whole purpose, and one sample shows the path executes
rather than that it holds across repeated crossings. Recorded as thin rather
than counted as full coverage; a future session should expect double-digit
propagate counts from several equipped boundary crossings.

Route 6 passes on visual confirmation only, which is inherent to a route with
zero production lines and therefore no probe.

Gate 4 (4b-3's cause=cellless) remains unrun, as expected — route 7
invalidated its recorded trigger and the replacement is UNESTABLISHED.

Two things this session did NOT exercise, recorded rather than glossed: route
3's autorun cancel never fired (all three portals report autorun=unchanged, so
the PlayerTeleported @0x006B32B0 SetAutoRun port is live-unverified — engage
autorun before a portal to close it), and AP-144's autonomy divergence stays
structurally unreachable. Per both round-2 reviewers' condition, this session
is explicitly NOT scored as covering #318.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:10:59 +02:00
Erik
e0f788255d docs: close out C4 — all routes landed, with the four owed gates and the process findings
C4's route work is complete. Records the landings, corrects the campaign
plan's now-false claims, updates the roadmap, and writes the successor
handoff at docs/research/2026-08-05-c4-closeout-handoff.md.

Routes, with review rounds and gate status:
  4a    44830a0e   |  4b-1  2e8e09ac
  4b-3  6dc7ba51   2 rounds; gate PASSED-partial (21cd6e9b), cellless unexercised
  5     36255af0   3 rounds, 8 MAJORs; NO live gate possible by design
  6     1b484937   zero production lines; its tests found #314
  7     cd3129e9   2 rounds + a required third pass, 5 MAJORs
  3     e0f96a55   3 rounds; found a 100%-dead production path
plus edc911b0 (the OnPosition dual-tail collapse), aaf0811f (#315),
daef7c98 (#314), a89bcb39 (#316 filed).

Suite 11,027 -> 11,090 passed / 4 skipped / 0 failed. Every checkpoint
0-failed; no test weakened, no Skip introduced.

FOUR CONNECTED GATES ARE OWED and none has been run. Each is recorded with
its recipe and a probe-gated pass criterion, because a clean-looking session
is not a pass: route 6 drops; route 7 equip/carry with
ACDREAM_PROBE_CHILD_CELL=1 (counts only if cause=propagate appears); route 3
portal/recall with ACDREAM_PROBE_LOCAL_TELEPORT=1 (counts only if [local-tp]
appears, and is explicitly NOT scored as covering #318); and 4b-3's
cause=cellless case — whose recorded recipe route 7 INVALIDATED, since
unwield-to-3D no longer yields a cell-less pre-merge cell. Its replacement
trigger is stated as UNESTABLISHED rather than guessed.

Campaign-plan corrections beyond the C4 section, all found by checking
against HEAD rather than trusting the text:
  - "six fixture failures ... classify before C5" — resolved as #281, and
    "six" was a mis-measurement; the measured baseline was 43.
  - "fold in #276 and #277" — #276 only partially (projectile half); #277 not
    at all, its trigger never fired.
  - "#269 slope-glide visual check" — #269 was closed 2026-07-31, BEFORE the
    plan was written. The surviving item is #278(b).
  - the 4b-2 bullet's "Still outstanding: #309" — re-scoped 2026-08-04;
    only the GotoLostCell half survives.

Seven process findings, each cited to a commit so a successor can check them:
  (a) THE CONTRACT CAUSES THE DEFECT — three defects this campaign came from
      a contract asserting a mechanism that did not exist; route 3's "Place
      re-fires" assumption released the player at the pre-teleport position.
      Route 7 adds the variant: enter_cell's part_array guard was correctly
      called load-bearing by the research, dropped by the contract, and
      inherited as an omission by the code — a right finding that evaporated
      across two handoffs with nobody re-reading the source.
  (b) INFERRING A FACT YOU CAN OBSERVE IS HOW A FIX GOES SILENT — route 3's
      round-2 fix inferred "committed" from a global PendingCount that three
      non-committing paths also clear, so the same bug completed cleanly and
      PASSED its invariant. Strictly worse than the defect it replaced.
  (c) PLANNING DOCS GO STALE ACROSS CUTOVERS — at least five were wrong
      against HEAD. Re-verify by symbol, never by line number; route 3's
      by-symbol sweep proved only 2 of 5 flagged files actually intersected.
  (d) A SKIPPED TEST IS A PERMANENT FALSE SIGNAL — refusing 7 skips uncovered
      a production bug that had made the entire portal arm dead code.
  (e) SABOTAGE-VERIFY, AND WATCH FOR TESTS READING A CONSTANT THEY PERTURB —
      one built a 64,000-node chain and stack-overflowed the host; another
      survived deleting the whole behaviour it claimed to pin, because its
      assertion read a field written unconditionally one line earlier.
  (f) REVIEWERS RETRACT, AND THAT IS THE PROCESS WORKING — three self-
      retractions, two of which prevented shipping a wrong register row or a
      relocated defect.
  (g) A GATE MUST BE ABLE TO SEE THE DEFECT IT GATES — three gates were
      unpassable or blind as specified and were corrected BEFORE being run.

Issues: #313, #316, #317, #318 open; #314, #315 closed. Register rows AP-141
through AP-145 added; AD-42 deleted, AD-2 amended.

C5 inherits #318's composition test (discriminating assertion:
PhysicsEngine.ShadowObjects must hold a row at the destination, not merely
the dedup cache), AP-145's cache-without-publish asymmetry, the #276/#277
remainders, and the probe-family strip.

Three things the closeout could NOT verify are stated as such rather than
smoothed: route 3 has no standalone round-3 review document (acceptance lives
in e0f96a55's message and both round-2 pass conditions); route 7's round
terminology differs across its own artifacts; and route 6's lack of dual
reviews is inferred from absent review docs, not stated anywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 04:13:08 +02:00
Erik
e0f96a55bf fix(physics): C4 route 3 — portal placement authority (local player)
Removes a duplicate placement authority for local-player portal arrival.
Portalling worked before this change and works after it — this is not a
bug fix, EXCEPT that it found and fixed one dead-code production bug.

THE PRODUCTION BUG: TryExecuteCanonicalPortalPlacement re-read the
accepted destination at Place time, but TryBeginPortalReveal already
consumes that slot at Aim time — so the arm was 100% dead code and every
real portal Place refused with host-token-unavailable. Found only
because we refused to accept 7 skipped tests instead of chasing the
count to zero.

RETAIL IS THE GENERIC PATH FOR THE THIRD ROUTE RUNNING:
SmartBox::TeleportPlayer @0x00453910 = SetPositionSimple(dest, 1) with
flags 0x1012, followed by PlayerPositionUpdated.

BOTH INVERSIONS, WITH THEIR ANCHORS: unlike route 2, the leash IS armed
here (ConstrainTo @0x0045418A) and velocity is zeroed
(set_velocity @0x004541B4); unlike route 4b-3, the local teleport_hook
runs AFTER placement (@0x004538AE).

THE THREE-ROUND DEFECT CHAIN, HONESTLY:
- Round 1 released the player at the pre-teleport position while the
  anim stream marched on — the contract wrongly assumed Place re-fires
  (process rule 1's third occurrence this campaign).
- Round 2's fix inferred commit from a global PendingCount, which three
  non-committing paths also clear — making the SAME bug complete
  cleanly and silently. Strictly worse than round 1: round 1 at least
  tripped portal-complete-before-materialized.
- Round 3 latches the commit where it actually happens
  (ReconcileAndAcknowledgePortal), keyed on reveal generation and
  teleport sequence, via TryConsumePortalCommit. Two of the three
  required regression tests landed and are sabotage-verified on both
  hosts (ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted /
  HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted).
  The third (force-arm-takes-the-slot) was judged unnecessary on review:
  with the inference gone, PendingCount is only a "don't ask yet" guard
  at both gates, so a force operation occupying or vacating the slot no
  longer changes an input the commit decision reads — the case collapses
  into what the landed test already discriminates.

THE B2/P3 RESOLUTION: both round-2 reviews were right about different
branches of the same synchronous call. RuntimePlacementProjectionSubscription
.OnPlacement acknowledges the FIFO head only when TryApply returns true;
a Place whose portal authority went stale (transit ended/superseded
while parked) used to return false, wedging every later entity's
placement receipt behind it forever. Both sinks
(RuntimePlacementPresentationSink, HeadlessRuntimePlacementProjectionSink)
now acknowledge-and-ignore a stale-authority Place instead of refusing
it. The regression test (RuntimePlacementPresentationSinkTests
.PortalPlace_StaleTransitHostOrSequenceIsAcknowledgedAndIgnored) had
been asserting the old, wrong `false` behaviour; it now asserts and
sabotage-verifies the fix.

Also lands: AP-144 (register discipline — the portal movement-event
send reuses the stricter UsePositionFromServer gate where retail's
SendMovementEvent is the looser autonomy_level != 0 test, diverging
only at level 1, currently unreachable), AP-145 + issue #318 (the
local-player collision-shadow presentation write bypasses its own
publisher's ShadowObjects write via a direct cache .Set(), self-healing
only once dedup diverges — filed, not fixed, pending a composition
test), AD-42 deleted (its last citation retired by the canonical portal
arm), AD-2 updated (the wait-cue's trigger predicate now covers a
second cause), and two documentation corrections: the enter_world
misattribution (both call sites are in SmartBox::HandleCreateObject,
only one in the player branch — portal arrival is TeleportPlayer, not
enter_world) and the stale "local player never reaches this path"
comment on the generic-remote-render-pose write.

Suite: 11,090 passed / 4 skipped / 0 failed. No new skips, nothing
weakened.

STILL OWED: the connected two-client gate, with
ACDREAM_PROBE_LOCAL_TELEPORT=1, scored only if [local-tp] lines
actually appear in the capture — and explicitly NOT scored as covering
issue #318 (no composition test yet asserts PhysicsEngine.ShadowObjects
directly).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 03:57:37 +02:00
Erik
cd3129e9d6 fix(physics): C4 route 7 — child cell propagation moves from a render tick into Runtime
Retail re-cells children when their parent crosses a cell, recursively, to
unbounded depth. acdream did it from a RENDER tick, so headless parented
children were cell-less forever and the canonical cell had two writers. This
slice makes Runtime the sole authority and demotes App's tick to
presentation-only. Contract:
docs/research/2026-08-04-c4-route-7-contract.md; the research that unblocked
it is docs/research/2026-08-04-retail-parent-cell-propagation.md (ca96ea5e).

Retail: SetPositionInternal @0x00515330 branches on `this->cell == curr_cell`
@0x0051536d; the changed branch reaches change_cell @0x00513390, whose
delegates leave_cell @0x00510f50 and enter_cell @0x00510ed0 self-recurse over
children and write the FULL identity (add_object @0x00510ee2, objcell_id
@0x00510f1e, part-array cell id @0x00510f2b, cell pointer @0x00510f35).
change_cell itself has no child loop.

THE TRAP, recorded because it nearly shipped: the depth-1 loop
@0x0051539c-0x005153d8 is the SAME-CELL fast path (objcell_id and part-array
id only, deliberately not the cell pointer), NOT the propagation. An
implementer who finds it first concludes "depth-1, id-only" and strands every
equipped item at a landblock boundary — the #184 class. The clincher against
that reading: update_object @0x00515d10 early-returns on `parent != 0`
@0x00515d40, so a child never runs its own physics tick and parent
propagation is the ONLY mechanism maintaining its cell.

Route 7 performs NO placement (DoPickupEvent @0x00452240 = unset_parent +
leave_world; DoParentEvent @0x00452290 = set_parent + SetPlacementFrame), so
it arms ConstrainTo nowhere — the leash rule INVERTS relative to routes
2/4/5, and both reviewers confirmed nothing arms.

Propagation is an ITERATIVE WORKLIST, not recursion. The first implementation
recursed with a depth-64 cap; both reviews independently found the cap left a
truncated tail at a stale NON-ZERO cell — permanently unrecoverable, logged
only under a probe flag, and on the withdraw path exactly the #184 shape
AP-142 clause (a) exists to reject. Shipping a fresh #184 instance inside the
slice that fixes stranded children was not acceptable, so the cap was removed
rather than tuned. The worklist retires the cap, the constant, its register
clause, and the failure mode together. Termination: every record on the stack
is already at the target pair, so nothing can be pushed twice and a hostile
A->B->A cycle collapses without a visited set.

The child write deliberately bypasses the public RuntimeEntityDirectory
.SetFullCell and calls the record method directly. This is LOAD-BEARING:
the public method re-enters PropagateFullCellToChildren, which opens with
_propagationWorklist.Clear() — routing children through it mid-drain would
wipe the shared stack and silently drop every unprocessed sibling. Any future
side effect added to the public SetFullCell must be mirrored by hand at that
call site.

Deliberate divergence, recorded not disguised: retail's removal path leaves
children with a null cell pointer but a STALE nonzero objcell_id @0x005133c1.
acdream does not reproduce it, because FullCellId != 0 is the liveness
predicate at 45+ sites — faithful porting would mark dead children live.
AP-142 records this; clause (d) records that acdream cannot gate propagation
on HasPartArray the way enter_cell gates on part_array @0x00510ed8, because
the flag's only writers are graphical and headless never sets it — the reason
is Slice J LAYERING, not a semantic difference (retail's part_array is itself
a mesh-construction product, single assignment site makeAnimObject
@0x0050e930 -> CPartArray::CreateSetup @0x0050e93e).

D7 adopts retail's unset_parent-before-leave_world order @0x0045227f ->
@0x00452286, applied to BOTH pickup paths including the dormant executor
replay. Its inertness was verified by reverting it and finding all 12
propagation tests still green — reported honestly rather than papered over
with a manufactured test, and independently confirmed by both reviewers.

ClassifyLeaveWorld and its request/cause types are DELETED: retail has no
classification here, and method-per-cause IS the retail dispatch shape.
Wiring it would have forced a vacuous teleport-sequence predicate with the
#307 shape.

Two review rounds plus a coordinator-required third pass; 5 MAJORs. One was a
handoff failure worth recording: enter_cell's part_array guard was correctly
identified as load-bearing by the research, dropped by the contract when it
enumerated the writes, and inherited as an omission by the code — a right
finding that evaporated across two handoffs with nobody re-reading the source.
Another was a test that survived deleting the entire behaviour it claimed to
pin, because its assertion read a field written unconditionally one line
earlier.

NoProjection is structurally unreachable from TickChild (TryResolveExactAttachment
performs a strictly stronger form of the same guard one call earlier). Kept as
a fail-safe, unit-tested directly, and documented in two places rather than
wrapped in a fabricated end-to-end test.

Headless regression test — the direct gate for this defect, which FAILED
before this work because no code path existed:
RuntimeLiveEntitySessionControllerTests
.DirectSink_D5_StandaloneParentEventCommitsChildToParentsExactCell.

Probe: ACDREAM_PROBE_CHILD_CELL=1 emits [child-cell] lines at all four write
sites (attach / headless-attach / propagate / withdraw / delete). TEMPORARY.

Complete Release suite MEASURED at 11,079 passed / 4 skipped / 0 failed
(baseline 11,063 at cff52c44, +16). An allocation flake appeared once under
load and was proven NOT this slice by reachability — RuntimeCollisionReportingState
contains zero SetFullCell and zero ParentAttachments references.

STILL OWED: the two-client connected gate (equip/unequip, carry across
landblock boundaries, pickup, loot, reconnect) with ACDREAM_PROBE_CHILD_CELL=1,
and a session counts only if [child-cell] cause=propagate lines appear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 23:53:05 +02:00
Erik
19ebf043e3 docs: pin the C4 route 3 contract (portal producer adapter)
The last C4 route. Portalling works today; route 3 removes a duplicate
placement authority (LocalPlayerTeleportPlacement.Place plus headless's
ResynchronizeLocalPlayerForPortalArrival), it does not fix a bug.

~225-400 added non-comment production lines, one slice. Every site
re-verified at HEAD by reading, not inherited from the scoping.

The authority's shape is RIGHT as-is and redesign is forbidden:
RuntimePortalPlacementAuthority's 4-tuple is exactly what transit owns per
reveal, and IsValid already cross-checks generation. Only the PRODUCER is
missing — the campaign plan's "the adapter does not exist" overstates the gap,
since consumption and validation are live production code at three layers.
The producer additionally needs no new WorldRevealCoordinator exposure: it
re-derives the host token through transit's idempotent
TryRegisterHostProjection, which makes a superseded token unobtainable by
construction.

Retail's local portal arrival is the GENERIC path for the third route running:
SmartBox::TeleportPlayer @0x00453910 is SetPositionSimple(player, dest, 1)
with flags 0x1012 — route 2's exact primitive — plus PlayerPositionUpdated.

TWO rule inversions are the contract's loudest section, because an implementer
arriving from the routes just landed will otherwise carry the wrong rule:
route 2's "never re-arm the leash" INVERTS (the teleport branch arms
ConstrainTo @0x0045418A and zeroes velocity @0x004541B4), and 4b-3's
hook-before-placement ordering INVERTS (the local teleport_hook runs AFTER
placement, from PlayerPositionUpdated @0x004538AE).

Three findings new since scoping: PlayerTeleported @0x006B32B0 byte-confirmed
as SetAutoRun(0,1) + SendMovementEvent, with the autorun-cancel gap verified
real — nothing cancels the J5.4 latch on arrival today; TryPublishPlace writes
no pose, so the committed-receipt suffix is the render entity's mover; and
headless TryCompletePortal's fully-synchronous suffix creates a
receipt-past-EndTeleport FIFO-wedge hazard, covered by proof obligation P3.

SEQUENCING BLOCKER recorded in the contract's front matter: route 7's
concurrent diff modifies five route-3 surfaces. The collision is textual, not
semantic — route 7 adds parent-cell machinery and touches neither the portal
transit, the drive controller, nor either duplicate authority — but route 3
must not start until route 7 commits, and must then re-verify its inventory by
symbol and re-measure the Release baseline.

#280 is SPLIT OUT, siding with the campaign plan's own separate sequencing
over the session handoff's "rides with route 3" claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 22:24:41 +02:00
Erik
ca96ea5e32 research: settle retail parent-cell propagation and scope C4 route 3
Two read-only research landings that unblock the last two C4 routes.

**Parent-cell propagation (unblocks route 7).** Retail DOES re-cell children
when the parent crosses a cell, recursively to unbounded depth.
SetPositionInternal @0x00515330 branches on `this->cell == curr_cell`
@0x0051536d; the changed branch reaches change_cell @0x00513390, whose
delegates leave_cell @0x00510f50 and enter_cell @0x00510ed0 self-recurse over
children and write the FULL identity (add_object @0x00510ee2, objcell_id
@0x00510f1e, part-array cell id @0x00510f2b, cell pointer @0x00510f35).
change_cell itself has no child loop — the recursion is in the delegates.

The clincher: update_object @0x00515d10 early-returns on `parent != 0`
@0x00515d40, so a child never runs its own physics tick and parent
propagation is the ONLY mechanism maintaining its cell.

The trap this retires: the depth-1 loop @0x0051539c-0x005153d8 is the
SAME-CELL fast path (objcell_id and part-array id only, deliberately not the
cell pointer), not the propagation. An implementer finding it first would
conclude "depth-1, id-only" and ship equipped items stranded at landblock
boundaries — the #184 class. Route 7's planned set_parent-only write would
have done exactly that.

Settled by READING, not by a debugger trace. The scoping had listed this as
needing live cdb evidence, but change_cell/set_cell's child handling had
simply never been read; the project's grep -> decompile -> verify order had
not been exhausted. One BN field-name gap was closed by walking
struct CPhysicsObj in the verbatim acclient.h, so no PE byte-decode was
needed either. A breakpoint set is recorded for optional confirmation only.

**Route 3 scoping (portal, the last route).** ~225-400 added non-comment
production lines, ONE slice, contingent on #280 splitting out. Portalling
works today; route 3 removes a duplicate authority
(LocalPlayerTeleportPlacement.Place), it does not fix a bug.

Eight dated-inventory claims are now false, the most consequential being "the
binding machinery is 100% dormant end to end" — the portal authority's
CONSUMPTION and validation side is live production code at three layers and
is exercised by every placement; only the PRODUCER adapter is missing. That
makes route 3 materially smaller than the campaign plan implies.

#280 SPLITS from route 3, definitively: it is a reveal-gate/prefetch-window
concern (WorldRevealReadinessBarrier's neighbourhood radius versus retail's
mid_radius, LScape::PreFetchCells @0x00505660 / SmartBox::SetRegion
@0x00453227), mechanically disjoint from the placement cutover — route 3
reads the ready predicate, #280 rewrites it. The campaign plan already
sequences #280 separately; only the session handoff said it "rides with"
route 3, and the plan is right.

Retail's local portal arrival is the GENERIC path for the third route running:
SmartBox::TeleportPlayer @0x00453910 is SetPositionSimple(player, dest, 1)
with flags 0x1012 — route 2's exact primitive — plus PlayerPositionUpdated.

Two rule inversions recorded so route 3's implementer cannot carry the wrong
rule forward from the routes just landed: route 2's "never re-arm the leash"
INVERTS here (the teleport branch arms ConstrainTo @0x0045418A and zeroes
velocity @0x004541B4), and 4b-3's hook-before-placement ordering INVERTS (the
local teleport_hook runs AFTER placement, from PlayerPositionUpdated
@0x004538AE). The classifier's dormant LocalPlayer-teleport route already
encodes both.

Two documentation defects found in passing and recorded, not fixed: the
2026-07-16 portal pseudocode attributes portal arrival to enter_world (that
is the login path), and a stale comment hides a live second writer — the
generic wire-pose write does run for the local player (AP-131/C5 scope).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 21:23:56 +02:00
Erik
cff52c44f4 docs(physics): correct stale set_velocity claim in the 4a remote velocity comment
LiveEntityNetworkUpdateController.cs's TryCommitAuthoritativeVelocity
call site carried a comment claiming "MoveOrTeleport installs that
exact vector with set_velocity". The C4 route 5 byte-decode of
CPhysicsObj::MoveOrTeleport @0x00516330-0x00516438 (the immediately
preceding commit) shows this is false: the function never reads its
velocity argument's stack slot in any branch, and UnpackPositionEvent
performs no set_velocity either. The only set_velocity in the whole
accepted-Position chain zeroes the LOCAL player @0x004541B4, a
different call site entirely.

This commit only corrects the comment. The 4a call itself is left
unchanged in production, deliberately out of C4 route 5's scope — the
route governs RuntimeSetPositionOperationKind placement dispatch, not
the pre-existing remote velocity commit. #317 (docs/ISSUES.md, filed
in the previous commit) tracks the follow-up: audit the whole
accepted-Position velocity chain and either find the correct retail
source or remove the call with a divergence-register row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 21:04:06 +02:00
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
Erik
30d3d114b0 docs: correct #315's closing SHA to aaf0811f
The entry cited `ddb38f37`, which does not exist on this branch. My
instruction to the implementer asked a commit to record its own SHA — a git
impossibility, since a commit hash covers its own content. The implementer
committed, then amended the SHA into the text, and the amend necessarily
produced a different final hash; it flagged the contradiction rather than
leaving it to be discovered.

Corrected to the real commit and recorded here as its own docs commit, which
is the pattern #314 already uses (fix commit, then a separate closing note
citing it). A wrong SHA in ISSUES.md is worse than no SHA: it is a citation a
later session trusts and cannot resolve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:12:20 +02:00
Erik
aaf0811f18 fix(physics): close #315 — cache the remote-arm callbacks instead of allocating per packet
The OnPosition collapse (previous commit) converged
RunRemoteArmTail's three duplicated call sites into one, which is what
makes caching worthwhile: one cached pair of delegates now serves every
remote guid instead of a fresh closure allocated on every accepted remote
Position (5-10 Hz per remote), regardless of whether the packet was a
teleport.

RunRemoteArmTail's signature changes from a caller-constructed
`Func<bool> isCurrentPositionOwner` closure to two plain value parameters
(`ulong positionAuthorityVersion`, `WorldEntity? expectedEntity`). It stamps
five per-packet scratch fields (`_remoteArmCanonical`, `_remoteArmMotion`,
`_remoteArmPositionRecord`, `_remoteArmPositionAuthorityVersion`,
`_remoteArmExpectedEntity`) from its own parameters, then passes the two
CACHED delegates into ApplyRemoteContactRouting. Observably identical: the
currency check reads the exact same
positionRecord/positionAuthorityVersion/expectedEntity triple either way.

Deviation from a bare cached-Func<bool>-field design, and why:
UpdateFrameOrchestratorTests.ProductionFrameAdaptersRetainTypedOwnersWithoutWindowCallbacks
asserts every typed production owner (LiveEntityNetworkUpdateController
included) carries zero Delegate-typed fields — the GameWindow decomposition
campaign's guard against a callback silently smuggling a window reference
back onto one of these owners. Neither cached delegate here touches a
window (both are bound to this controller alone), but the rule is written
as a blanket field-type check, not a window-specific one. The two
delegates are wrapped in a small nested RemoteArmCallbacks type instead of
being bare fields, which satisfies the guard and keeps the cache a single
named, auditable unit rather than working around the test.

RunRemoteTeleportHook's own allocation (the six-action
RemoteTeleportHookActions bundle) is unaffected — it stays teleport-path-only,
already judged acceptable to defer by the C4 route 4b-3 round-2
architecture review's B4 finding.

dotnet build AcDream.slnx -c Release: 0 errors. Focused suites green at
this commit: AcDream.App.Tests 4104/4107 (3 pre-existing skips),
AcDream.Runtime.Tests 1125/1125.

Closes #315.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:11:36 +02:00
Erik
edc911b042 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>
2026-08-04 18:07:54 +02:00
Erik
a89bcb39b2 docs: file #316 — player landing block omits the collision-shadow publish
Found while scoping the OnPosition collapse and confirmed by reading the
block: the player-guid LANDING TRANSITION hard-snap syncs body and render
entity but never calls LiveEntityShadowPublisher.TryPublishRemote, while the
NPC-guid copy's tail does. Contradicts the file's own #184 Slice 2b comments.

Filed with severity UNKNOWN deliberately: the per-tick remote commit may
republish the shadow on the next tick, which would make this a ~33 ms lag
rather than the #184 invisible-but-solid class. Measuring that is the first
step, not fixing it — and it is explicitly out of the behaviour-preserving
collapse's scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 17:03:18 +02:00
Erik
b260bcd12c docs: close #314 in the issue log (fixed by daef7c98)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:49:31 +02:00
Erik
daef7c9835 fix(inventory): reset a split result's movement timestamps so recovery cannot throw (#314)
Found by C4 route 6's integration tests (1b484937) and split out of that
zero-production closure per the standing split-on-discovery rule.

PendingSplitToWorldProjection.BuildSpawn zeroes the top-level
MovementSequence/ServerControlSequence, but its Physics.Timestamps `with`
block overrode only Position/Teleport/ForcePosition/Instance — leaving
Timestamps.Movement and .ServerControlledMove at the SOURCE item's values.
RuntimeEntityObjectLifetime.HasConsistentCreateIdentityAndParent requires the
PhysicsDesc timestamps and their flattened projections to agree, so the
synthetic spawn failed the predicate and TryRecoverUnknownPosition threw
`CreateObject 0x… has inconsistent instance or parent projections` instead of
completing the canonical create-placement transaction.

Reachable in ordinary play: retail's per-object update_times channels are
monotonic and do not reset when an item re-enters a container, so any item
that ever had world presence — dropped once, picked back up, then split —
carries nonzero values in exactly those two fields. The split pile then never
appears.

Fix is the honest value, not a placation of the predicate: a fresh split GUID
has no movement history by construction, so both channels are zero in both
projections. Deliberately NOT fixed by loosening
HasConsistentCreateIdentityAndParent — the predicate was right and the
producer was wrong.

The route-6 test that documented the throw
(SplitSourceWithRetainedMovementTimestamps_ThrowsInsteadOfRecovering) is
renamed to …_StillRecovers and now pins the fix. It asserts more than "no
throw": the result's movement channels must be ZERO in both projections, so
the test cannot pass against a lenient-predicate workaround. Sabotage-verified
in both directions — restoring the old BuildSpawn reproduces the exact
original InvalidOperationException.

Notable for the campaign record: this is a crash in the precise mechanism
route 6's scoping cited as EVIDENCE that drops already converge on the
canonical transaction. Reading the code said the path converges; driving it
said it throws. The zero-production route was still correct — and building its
tests anyway is what found this.

Complete Release suite 11,020 passed / 4 skipped / 0 failed, unchanged from
1b484937 (the test flipped its assertion rather than being added). Neither
known flake fired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:48:41 +02:00
Erik
1b484937b6 test(physics): C4 route 6 — drops/split-recovery closure, zero production lines
Route 6 needs no production change and this commit contains none: C3c
(529e0e9d) already flipped both hosts' Create paths onto the residence lease,
so a dropped item is byte-for-byte route 1's create classification
(RuntimePositionEntityKind.Remote, RuntimeCreateResidenceKind.TopLevel,
ClassifyCreate -> SetPosition with InitialCreateFlags = Placement | Slide).
Route 6 is a SOURCE of route-1 traffic, not a route of its own. Both drop
flavours converge on LiveEntityHydrationController.OnCreate ->
RegisterEntityWithInitialResidence — the whole-item drop through
ItemInteractionController's DropToWorld (no physics, no position; the server
decides), and split-to-world through TryRecoverUnknownPosition's call to the
identical entry point. Contract:
docs/research/2026-08-04-c4-route-6-contract.md.

Retires a FALSE PREMISE from the campaign plan (:97-100), which claimed
split-recovery creates "need an effect-replay suppression signal". Verified
against the decomp instead of assumed: play_default_script @0x005132B0 /
@0x00513300 has exactly three call sites in the entire pseudo-C dump —
DefaultScriptPartHook::Execute @0x00526c08, DefaultScriptHook::Execute
@0x00526c14, and ACCWeenieObject::DoCollision @0x0058c3b4 — and NONE from
set_description or CreateObject. Neither client plays a default script at
create, so there is nothing to suppress. acdream's only create-time replay is
the F754/F755 queue drain keyed by server GUID, which is retail's own
HandleCreateObject @0x00454C80 behaviour. The plan's other two clauses were
closed at C0 (TryCommitParent/CommitWithdrawal cancellation symmetry;
host-visible cancellation receipts); the list now states what actually
remains — route 7's child-cell two-writer split and the headless
parent-realize gap.

Retail split marking recorded for the record: UIAttemptSplitTo3D @0x0058D850
stores only splitStackSize/splitClassID/splitTime and performs no placement;
DeclareValid @0x0058E340's recovery action is SetSelectedObject @0x0058E481 —
a SELECTION transfer with a 10-second expiry, not effect suppression and not
placement. UIAttemptPutIn3D @0x0058D700 records no marker at all.

Seven tests over the now-flipped path (whole item, split stack, new-GUID
recovery, second drop, unavailable destination, newer Position after the
pending identity is consumed, plus the #314 repro), each sabotage-verified:
the production path was broken on purpose, the test was confirmed to fail,
and the sabotage reverted. R6-c is now settled by assertion rather than
argument — BuildSpawn's wholesale clone of Children/Movement/AnimationFrame/
SetupTableId is measured, not reasoned about.

FOUND WHILE TESTING — #314, filed not fixed (this route is zero-production by
contract). BuildSpawn resets top-level MovementSequence/ServerControlSequence
to 0 but its Timestamps `with` block overrides only Position/Teleport/
ForcePosition/Instance, leaving Physics.Timestamps.Movement and
.ServerControlledMove at the SOURCE item's values.
HasConsistentCreateIdentityAndParent requires the two projections to agree, so
a split whose source carries nonzero Movement timestamps — plausible for any
item dropped once, picked up, and split again — fails the predicate and throws
instead of completing the canonical transaction. Verified in source, not taken
on report. Note this is a crash in the exact mechanism the scoping cited as
EVIDENCE that drops already converge: code reading said the path converges,
driving it said it throws. Fixed in the immediately following commit.

Also filed: #313 (DeclareValid's SetSelectedObject port is missing and the
container-split flavour records no marker — selection UX, deliberately not
implemented inside a placement closure) and #315 (route 4b-3's per-packet
runTeleportHook Func<bool> closure at three RunRemoteArmTail call sites; the
network packet path, not Slice I's per-frame resolve path — filed now because
route 5 adds a fourth site). AP-124 stays open and registered.

Test lines are 410 against a 150-250 guidance, accepted: the excess is a real
ItemInteractionController harness plus the #314 repro, which is what found the
defect. A mock that proved nothing would have been shorter and worthless.

Complete Release suite MEASURED at 11,020 passed / 4 skipped / 0 failed
(baseline 11,013/4/0 at 6dc7ba51; +7 new). Neither known flake fired.

Connected gate (user-run) still owed: drop a whole item, split a stack to the
ground, drop a second within ~1 m, repeat indoors and after a portal recall,
then walk two landblocks away and back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:45:57 +02:00
Erik
21cd6e9b2b docs: record C4 route 4b-3's connected gate as passed, with its cell-less gap
User-run against the exact 6dc7ba51 Release binary with the retail UI; user
verdict "all works". The probe proves the run actually exercised the arm: 16
[remote-teleport] lines over 7 creatures, all hookRan=True placement=Committed,
every guid in the 0x8xxxxxxx creature range rather than the 0x50xxxxxx player
range — so the corrected creature-target recipe reached the NPC-guid branch
where all three NPC-arm MAJORs lived.

Recorded as a partial pass, not a blanket one: all 16 lines are
cause=teleport-ts and cause=cellless was never observed, so the cell-less half
of the same arm remains test-covered only. Folding that into "gate passed"
would repeat 4b-2's #309 shape, where every park probe shared one cause and the
unexercised cause went unrecorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:09:32 +02:00
Erik
8c269ad1a6 docs: C4 route 4b-3 contract and both dual-review rounds
The pinned contract plus the four review documents behind 6dc7ba51:
round 1 (retail FAIL 3 MAJOR / architecture FAIL 2 MAJOR) and round 2
(both PASS on the fixed diff).

Process findings worth carrying into the remaining routes:

1. The contract's 13 explicit "what must REMAIN true" invariants were the
   fix for 4b-2's round-1 defect (a contract that said what must change but
   not what must stay). They did NOT prevent a round-1 FAIL here. What the
   round-1 MAJORs actually shared was a STRUCTURAL cause the invariant list
   could not express: two parallel inline copies of the same routing tail.
   An invariant list constrains behaviour; it cannot see duplication.

2. Both reviews independently found the same NPC synth-velocity defect
   (retail R3 = architecture A2). Independent convergence on one finding is
   the strongest signal this process produces — weight it accordingly.

3. The specified two-client gate could not have observed three of the four
   MAJORs: it teleported a player character, and all three live on the NPC
   arm. Caught by both reviewers before the gate ran, not after. Check that
   a gate can structurally see the defect class it is gating.

4. A reviewer named a symbol that does not exist (RuntimeCollisionReportingState
   .ForceEnd is a private helper, not the public entry point). The implementer
   silently substituted the correct one (LeaveWorld) while reporting "no
   disagreements with either review". Verify implementer claims against source
   even when they report full agreement.

5. A new test asserted only what must NOT happen, so an emptied
   ApplyWireAirborneLeftoverBookkeeping passed every test in the tree. Caught
   in round 2 and closed with positive assertions, sabotage-verified. Negative
   assertions alone cannot detect a deleted write.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:00:26 +02:00
Erik
6dc7ba51ee feat(physics): C4 route 4b-3 — remote teleport + cell-less through the canonical placement
Flips the last remote classification (SetPosition: teleport-advanced and
cell-less) onto 4b-1's RuntimeRemotePlacementDriveController, runs retail's
teleport_hook before the placement, and deletes the legacy remote-teleport
machinery. Contract: docs/research/2026-08-04-c4-route-4b-3-contract.md.

Retail: MoveOrTeleport @0x00516330's branch @0x00516386 -> teleport_hook
@0x005163EF -> SetFlags(0x1012) @0x00516414 -> SetPosition @0x00516420 ->
return 1 @0x00516438. The hook @0x00514ED0 runs BEFORE the placement and
regardless of its outcome. Retail places this branch unconditionally, at any
distance and any contact state (arg4 is read only @0x0051638E, after the
branch) — which is what retires AP-137's cell-less enqueue-vs-place delta.

D1 — the classifier's cell-less input is now the PRE-merge committed cell.
Retail's predicate is `this_1->cell == 0`, the BODY's own cell at
MoveOrTeleport entry (this_1 is assigned from this @0x00516334). acdream fed
the POST-merge canonical.FullCellId, which RefreshSnapshot ->
RefreshDerivedState -> SetFullCell has already stamped with the accepted wire
cell; a zero wire cell fails validation into RejectedData first. The shipped
remote cell-less predicate was therefore dead code, not merely different from
remotePlacementRequired. Threaded via a builder overload; route 1's overload
is untouched. The graphical !IsSpatiallyVisible arm of
projectionRequiresTeleportHook is deleted — a presentation predicate with no
retail analogue that fired the teleport machinery on a routine hot path.

Deleted: RemoteTeleportController (605), RemoteTeleportPlacement (85),
RemoteShadowPlacementSynchronizer (49), their 1,709 lines of tests, the
remotePlacementRequired predicate, the TeleportHookRequired plumbing, the
legacy pre-operation ConstrainTo fallback, and the player arm's legacy
!IsGrounded fallback. Net -2,030 lines.

Structural fix (two independent Opus reviews, round 1 FAIL/FAIL): three of the
four MAJORs were one defect — OnPosition carried two parallel inline copies of
the routing tail (player-guid, NPC-guid) that had drifted. Extracted
RunRemoteArmTail (3 call sites) and ApplyWireAirborneLeftoverBookkeeping (2),
both branches now share one implementation.

  A1  ToConstraintArm mapped AirborneSnap -> AirborneNoOperation, so the NPC
      arm armed ConstrainTo ZERO times for an out-of-contact wire-grounded
      creature — a regression this slice introduced while closing a
      structurally identical hole. Now maps to NearInterpolate; switch made
      total with a throwing default proven unreachable.
  R1  D2's write-nothing shape existed on the player arm only; NPC packets
      fell through and wrote the body. Retail makes no player/NPC distinction.
  R2  report_collision_end(this,1) @0x00514F31 was bound to
      ShadowObjects.Suspend, a port of a DIFFERENT retail function
      (remove_shadows_from_cells) that teleport_hook never calls. Now routes
      to RuntimeCollisionReportingState.LeaveWorld, which wraps the private
      ForceEnd in an admission-blocking transaction so a DoCollisionEnd
      callback cannot recreate the contact table.
  R3/A2 A teleported NPC synthesized ServerVelocity from the teleport distance
      (~1,000+ m/s) and planned a run cycle from it. Both the install and
      RemoteServerControlledVelocityCycle.Apply now gate on !isTeleportRoute.

BISECT HAZARD — A1's fix is correct only BECAUSE R1 landed. AirborneSnap is
reachable wire-airborne on the NPC arm only while D2's shape is missing there.
Reverting R1 alone silently inverts A1 into the opposite divergence: arming
where retail returns 0. Revert both or neither.

Also in the velocity hunk: the NPC block's two !IsPlayerGuid(update.Guid)
guards were dropped when it was wrapped in `if (!isTeleportRoute)`. Safe — all
five exit paths of the enclosing IsPlayerGuid block return, so the predicate is
unconditionally false below it — but it was unremarked by both reviews.

Register: AP-137 REWRITTEN (not deleted) to the surviving acdream-only
divergences — null classification during the login window and Rejected*
through UnroutedCatchUp keep a row. AD-42's RemoteTeleportController citation
retired; AP-136/AP-138 writer lists corrected to the two surviving non-Position
rebucket writers; AP-138 gains the teleport arm as a second producer of the
visible-without-collision residual (retirement path remains #309). AP-135 is
untouched and its two airborne bookkeeping writes are preserved on both arms.
AP-131 does not retire; #276 does not close.

Proof obligation 1: ParkCollisionResidents' overlap throw stays unreachable —
the teleport arm adds packets to the same TryBeginExclusiveAuthoredPlacement
one-operation-per-key machinery the far arm uses, opens no new operation shape,
and every DeferredCell outcome cancels synchronously with
restoreCancelledPark: true. The guarded property remains
HasOldPrefixPlacementDebt's stall, not a throw (4b-1's B2 caveat stands).

Correction to an earlier claim: LiveEntityPresentationController's
_activePlacementOwners was NOT write-never at HEAD —
remotePlacementRequired -> BeginPlacement -> Begin -> BeginAuthoritativePlacement
was a live writer chain. It becomes write-never BECAUSE this slice deletes that
chain, which is why deleting the dead half is behaviour-preserving.

Probe: ACDREAM_PROBE_REMOTE_TELEPORT=1 emits one [remote-teleport] line per
routed arm (guid, cause, hook-ran, placement status). TEMPORARY, strip with the
probe family.

Carried, disclosed not fixed: no dedicated bidirectional collision-partner test
for R2 (the wiring, not LeaveWorld itself, is what lacks coverage); the
stress test's teleport step drives hand-written field assignments rather than
the canonical arm; the per-packet runTeleportHook closure allocation (network
path, not the resolve path Slice I's 0 B discipline governs — file before
route 5 adds a fourth call site). B2: IRuntimeCollisionReportObserver has zero
production implementations, so retail's bidirectional DoCollisionEnd half still
reaches no gameplay consumer — this fix closes the wrong-function binding, not
that nobody listens.

Complete Release suite MEASURED at 11,013 passed / 4 skipped / 0 failed
(baseline 11,027/4/0; net -14 = ~33 deleted test cases against ~19 added).
Neither known flake fired (#302 PortalProjectionTests GC-allocation, #308
NakEmissionTests wall-clock).

STILL OWED: the two-client connected gate, which MUST use an NPC/creature
teleport target. Both round-1 MAJORs lived on the NPC arm and the velocity
cycle early-returns for 0x50xxxxxx guids, so a player target structurally
cannot observe A1, A2, or R3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:00:10 +02:00
Erik
3e002993dd docs: C4 handoff for routes 4b-3, 5, 6, 7, 3
Closes #312 as user-confirmed, with the caveat recorded rather than buried: the
accepting session's probe capture showed 22 parks and zero park-restores, so the
restoration path was not observed executing. If an invisible-remote report
recurs, that is where to start.

Re-scopes #309 as largely superseded by #312 — the presentation restore is the
behaviour its connected check was written to probe. What survives is the
narrower faithfulness question: retail's GotoLostCell keeps a lost-cell object
hidden until reenter_visibility, where acdream re-shows it on cancel.

Adds the handoff itself: branch state and the measured 11,027 baseline, the two
known flakes and the standing instruction not to conflate them, per-route
scope with the retail addresses and the traps already paid for (teleport_hook
runs BEFORE the placement; route 5 has no possible live gate because ACE never
sends UpdatePosition for a missile; route 6 needs zero production lines and the
campaign plan carries a false premise about create-time effects; route 7 must
write the child cell at BOTH the set_parent analog and the per-commit position
analog), and the six process rules this session paid for — chiefly that the
contract causes the defect, that a slice must split on discovery, and that a
green suite and a clean-looking live session are both non-evidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:00:15 +02:00
Erik
2eb39a0250 fix(physics): route remote Positions on contact, not walkability (AP-140)
The two gates that decide whether an accepted remote Position is interpolated
or hard-snapped read `Airborne`, which is `!Body.OnWalkable` — WALKABILITY.
Retail reads CONTACT: InterpolationManager::adjust_offset @0x00555D30 gates its
entire body on `transient_state & 1` @0x00555D52, so a retail body in contact
with a non-walkable face still interpolates.

The two predicates disagree in exactly one state — in contact, not on walkable
ground — which 204d0ae0 turned from unreachable into ordinary. Before it, the
per-tick forge made every non-airborne remote walkable by construction, so the
disagreement could not occur.

Both gates now read `!Body.InContact`: ApplyRemoteContactRouting's flight
carve-out and OnPosition's player-remote arm.

`Airborne` is deliberately NOT re-derived from CONTACT. That would perturb all
five of its writers and contradict a pinned assertion in
RemoteTeleportPlacementTests.Apply_PendingGroundToSteepContact_ (InContact:
true, OnWalkable: false -> Assert.True(remote.Airborne)); a previous
implementer attempted it and correctly backed out rather than editing the
assertion. This narrower shape touches no existing test.

AP-140's register row is retired in this commit, as the row itself specified.

Honest scope: this is a faithfulness fix, not a visible one. ACE derives its
IsGrounded flag with the same floor_z test, so during a slide it almost
certainly reports not-grounded, the classifier returns NoPositionOperation, and
neither arm is taken. Expect no observable change against ACE.

Suite 11,027 passed / 4 skipped / 0 failed (baseline 11,023).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:53:48 +02:00
Erik
f4f2579575 docs: correct #312 — the live gate was never exercised
I closed #312 as user-passed on a two-client run that looked correct. The probe
capture from that same run shows 22 [park] lines, all cause=unplaceable, and
ZERO [park-restore] lines: no park was cancelled, so the restoration path never
executed. The session observed the intermittent failure not reproducing, which
it also did on the prior day's second recall. It neither confirms nor refutes
the fix.

Reopened to FIXED / live-gate-not-exercised, with the acceptance signal stated
explicitly: a [park-restore] ... presentation=True line for the remote's guid
under ACDREAM_PROBE_PARK=1. Absence of that line means the gate did not run
regardless of what the screen showed.

This is the same class of mistake the probe was added to prevent one commit
earlier — treating a clean-looking session as evidence that a rarely-taken path
works. The mechanism remains pinned by four tests with a seven-revert
discrimination table; only the live confirmation is missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:48:32 +02:00
Erik
97b22b8606 docs: close #312 and #32's remote half — both user-passed
#312 (cancelled park restored Runtime state but never the presentation half)
CLOSED at b1f914d5; the two-client gate passed — the recalled remote appears in
world and on radar and stays correct after going idle, which is the specific
shape that failed (a moving remote self-heals via the per-packet prologue
rebucket; only one that parks on its final Position and then goes idle sticks).

#32's remote half closed at 204d0ae0; a remote observed in acdream now slides
down a steep face under gravity instead of freezing and then blipping. Left
open and named rather than absorbed: the LeaveGround chatter bound, the !Ok
airborne latch, the contact_allows_move action-animation watch item, the AP-140
follow-up (point the two routing gates at Body.InContact rather than
re-deriving Airborne), and local-player edge-slide, which this work did not
touch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:24:25 +02:00
Erik
b1f914d508 fix(physics): restore presentation when a park is cancelled (#312)
Regression from 7f1c1f5a (C4 route 4b-2). A remote player who recalled in,
arrived, and stood still was permanently absent from the world render AND the
radar while remaining fully simulated — 71 healthy physics ticks with contact
and walkable, interpolation enqueues, equipment attached, chat visible.

Route 4b-2 is the first commit that lets an ordinary remote UpdatePosition open
a canonical SetPosition. A park publishes a synchronous Withdraw that tears down
presentation registrations; only TryPublishPlace restores them.
RestoreParkWithdrawal — added in the same slice — restores InWorld, the object
clock, and canonical residency, i.e. the Runtime half only. Eight Opus reviews
verified those three fields and the tests asserted exactly them, so the suite
stayed green while the entity was invisible.

Why it is intermittent: the presentation half IS restored incidentally by the
per-packet prologue rebucket for a MOVING remote. It only sticks when the
entity parks on its FINAL accepted Position and then goes idle, because ACE
stops broadcasting for a stationary entity, so no later packet arrives to
re-publish it and nothing else re-drives.

The fix publishes a RuntimePlacementProjectionKind.WithdrawalRestored receipt on
the one ordered placement stream, acknowledge-only in Runtime (the parked
operation is already retired by CancelCoreDeferred), which the App sink maps to
the exact inverse of its own TryPublishWithdrawal: the projection half (bucket,
IsSpatiallyProjected, IsSpatiallyVisible, spatial indexes, RefreshPresentation)
plus the publish half (_worldState, _worldEvents, _effectPoses,
_localPlayerShadow, visibility sinks). Applied with commitPose: false, because
the withdrawal never moved the sidecar; a test feeds a deliberately wrong
position to pin that.

Two alternatives were refuted on measurement, not preference. Routing the
restore's SetFullCell through CommitCanonicalCell cannot fire on the shipped
remote path at all — the prologue rebucket has already recommitted a non-zero
FullCellId before the merge cancels the park, so no cell edge remains — and it
never touches the publish half regardless. Extending RestoreParkWithdrawal
directly reduces to the same receipt, since Runtime must not reach behind the
host sink.

Gated on the entity ending the rollback canonically whole (FullCellId != 0 &&
InWorld) rather than on residencyRestored, which is false on the shipped remote
path and would have made the fix a no-op. AP-136's quiescing-prefix refusal arm
is preserved: no receipt, entity stays withdrawn.

Corrects my own framing of the defect: _worldState/_worldEvents/_effectPoses are
lost but are NOT what kills render and radar (_worldState is the plugin
IGameState; _effectPoses is the pose registry, not entity.MeshRefs). The
load-bearing casualties are the visibility sinks and the
IsSpatiallyProjected/IsSpatiallyVisible + bucket removal that gates the radar.

Register: AD-63 filed (selection deliberately not restored — user intent),
AP-136 amended (its "restored visible" claim covered only the canonical half;
the gap was a defect, not a divergence). ShadowObjectRegistry.Suspend stays
out of scope per AP-136.

Seven-revert discrimination table including one that proves the test is not
merely re-checking the bucket. Suite 11,023 passed / 4 skipped / 0 failed.

Live gate is user-run and folds into #309: two clients, ACDREAM_PROBE_PARK=1,
recall a remote in and let it stand still; acceptance is
[park-restore] ... presentation=True for that guid plus a visible model and a
radar blip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:21:45 +02:00
Erik
204d0ae047 fix(physics): remote bodies slide on steep faces instead of freezing (#32)
A remote observed in acdream landed on a sloped roof and froze; the server slid
on, the gap passed AP-87's 4 m threshold, and the body snapped — the visible
blip. Live probe capture, two adjacent ticks 63 ms apart:

  t=88420671  rsInContact=True rsOnWalkable=False rsIsOnGround=True
              bodyCpNz=0.6097 floorZ=0.6642 steep=True gravity=True
              vel=(2.146,2.264,-3.549)
  t=88420734  contact=True onWalkable=True   <- forced against the sweep
              gravity=False                   <- cleared
              velBeforeZero=(2.146,2.264,0.000)
              moved=0.0000                    <- and every tick after

The roof is 52.4 degrees against a 48.4 degree limit, so acdream's classifier
was CORRECT and was then overruled. Four independent links each froze the body
on their own: a per-tick force of Contact|OnWalkable, a per-tick velocity zero,
a Gravity clear at landing, and a landing edge testing IsOnGround
(= inContact || ...) instead of OnWalkable. The tick called
HandleAllCollisions alone — the tail of SetPositionInternal without its prefix.

Retail simulates remotes locally and derives these bits rather than asserting
them: CPhysics::UseTime @0x00509950 iterates the whole object table;
update_object @0x00515D10 gates only on parent/cell/FROZEN with no
is_player fork; SetPositionInternal @0x00515330 sets CONTACT from
contact_plane_valid @0x00515430 and ON_WALKABLE from contact_plane.N.z vs
floor_z @0x00515465-@0x0051548E before handle_all_collisions @0x005154FE;
set_on_walkable @0x00511310 fires HitGround @0x00511364 / LeaveGround
@0x00511346 edge-triggered with no ownership gate; calc_acceleration
@0x00510950 zeroes only when CONTACT && ON_WALKABLE && !Sledding @0x0051096B;
calc_friction @0x0050EE70 returns at its first line when ON_WALKABLE is clear.
acdream had copied retail's airborne no-op WITHOUT retail's local simulation.

The fix is mostly deletion: stop forging the transients, stop discarding the
authoritative velocity, stop clearing Gravity, and route the remote tick
through the same SetPositionInternal commit TickHidden and the local player
already use, with the landing edge derived from the sweep's own OnWalkable.
AP-87's threshold and conditions and InterpolationManager's node_fail_counter
snap-to-tail are deliberately untouched — this removes the CAUSE of the
divergence rather than weakening the backstop.

Cross-checked against ACE: its only creature-side VectorUpdate emitters are the
jump broadcast and spell projectiles, so integrating the wire velocity cannot
double-move a walking remote; and PhysicsGlobals.DefaultState already carries
Gravity, so deleting the manufactured State |= Gravity is safe.

Register: AP-81 narrowed (its GRAVITY half retired outright), AP-87 annotated,
AP-139 filed (the interpolation-queue clear on the landing edge), AP-140 filed
(the two routing gates select snap-vs-interpolate on walkability where retail
uses CONTACT — adjust_offset @0x00555D30 gates on transient_state & 1
@0x00555D52). AP-140's follow-up is deliberately shaped as "point the two gates
at Body.InContact", NOT "re-derive Airborne", which would perturb five writers
and collide with a pinned RemoteTeleportPlacementTests assertion.

Three gaps recorded in #32 rather than papered over: the new LeaveGround
dispatch is untested for chatter; a persistently !Ok transition can latch a
remote airborne; and — the visual-gate watch item — the deleted forge was a
blanket guarantee of Contact|OnWalkable, and contact_allows_move @0x00528dd0
silently refuses action animations without both, which is the literal root
cause of closed #270. Retail-correct on a steep face, a regression anywhere
else.

10 discriminating tests over a real PhysicsEngine landblock whose contact
normal Z is 0.61 against FloorZ 0.6642 — the live roof's exact relationship.
Suite 11,019 passed / 4 skipped / 0 failed. Includes the temporary
ACDREAM_PROBE_REMOTE_LANDING / ACDREAM_PROBE_REMOTE_SLIDE probe family that
produced the capture above; strip with the family.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:21:16 +02:00
Erik
f058dfc9f9 docs: record C4 route 4b-2 landing and its user-passed far-snap gate
Route 4b-2 landed at 7f1c1f5a and the two-client far-snap walk passed
(2026-08-04): a remote crossing 96 m in both directions stays visible and
correctly positioned at range and resumes smooth interpolation on the way in,
with no freeze, Z pop, vanish, or invisible-but-solid.

Records that #309 is still outstanding and why we know it: the
ACDREAM_PROBE_PARK=1 capture from the accepting session shows 11 parks, every
one cause=unplaceable and zero cause=quiescence, so the shared-core park change
has not been exercised live. That probe was added precisely because the prior
#309 steps could pass while broken — without it the session would have been
recorded as a full pass.

Also records the corrected 10,968 baseline and the two process lessons: the
round-1 defect traces to the contract omitting "and still advance the pose",
and the park defect should have been split into its own slice when it surfaced
in round 2 rather than riding inside 4b-2 for three more review rounds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 08:36:32 +02:00
Erik
7f1c1f5aa6 feat(physics): C4 route 4b-2 — remote far snap through the canonical placement
Flips the SetPositionSimple classification (contact, PlayerDistance >= 96 m) for
remotes onto 4b-1's drive controller and deletes both legacy far blocks, both
duplicated 96f/4f constant pairs, and both `?? Vector3.Zero` fabrications. The
4 m constant now exists exactly once. Teleport and cell-less stay legacy for
4b-3.

Retail: MoveOrTeleport @0x00516330's far branch runs StopInterpolating
@0x005163CB before SetPositionSimple @0x005163D9 and returns 1 @0x005163E8
regardless — the SetPositionError is discarded — so HandleReceivedPosition arms
ConstrainTo @0x00454272 post-move on commit AND on failure. The x87 parity
decode at @0x00516393-@0x0051639E puts exactly 96.0 on the far branch.
SetPositionSimple @0x005162B0 builds flags 0x1012 at @0x005162C4.

Non-commit outcomes still advance the body, because retail's SetPositionInternal
@0x00515BD0 commits the destination via store_position @0x00515CE2 when no cell
resolves. The partition is by STAGE, not heuristic, enforced by an exhaustive
switch: Refused/Contention/NotApplicable/RejectedPreparation store (the placement
never executed); Committed/Deferred/RejectedByPlacement do not (the engine ran
and refused, matching retail's non-storing returns @0x00515CB2 and @0x00515CD5).
Without this a refused far snap froze the remote with an emptied queue.

Also fixes a shipped defect this route made live: ParkDeferred's quiescence parks
withdrew the entity (InWorld=false, clock suspended, residency removed) and were
never restorable, while Forget(restoreCancelledPark: true) runs for every
accepted Position on every entity. The restorable decision now lives inside
ParkDeferred AFTER SnapToCell, reading body.CellPosition.ObjCellId — the value
RestoreParkWithdrawal actually restores at — against every live quiescence
rather than one minimum-OperationId token. The three pre-snap fields are hoisted
into locals because SnapToCell ends with InWorld = true. ParkCollisionResidents
passes restorableOnCancel: false explicitly; the plain unplaceable park is
provably unchanged. RestoreParkWithdrawal re-tests the prefix at restore time so
a retained route-2 park cannot re-admit into a prefix that began quiescing
during the park.

CanAttemptDestination is retained as an OPTIMISATION only, with the two Core
predicates it cannot reproduce written down at the pre-flight, plus the two
properties that depend on it staying there.

Four fix rounds and eight Opus reviews. The slice was fully green at 10,990,
10,997 and 11,004 while containing real defects — a frozen remote pinned as
correct by its own test, a fallback that over-wrote on the exact retail paths
that decline to store, and a park guard incomplete on two independent axes.

Register: AP-137 (leftover classifications take AP-87's catch-up; states the
cell-less enqueue-vs-place delta deferred to 4b-3, that RejectedData is applied
anyway, and the headless divergence), AP-138 (the refusable far placement),
AP-136 narrowed to match the relocation. #309's acceptance steps rewritten —
step 5 previously asserted a recovery the code does not perform — and gated on a
new ACDREAM_PROBE_PARK=1 signal so the check cannot pass while broken.

Suite 11,009 passed / 4 skipped / 0 failed against a measured 10,968 baseline.
The 10,973 figure recorded earlier was wrong and is corrected here.

Connected gate outstanding: the two-client far-snap walk and #309.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 07:55:56 +02:00
Erik
1b631f127d research: settle retail child-cell ownership (unblocks C4 route 7)
Route 7 wants to demote App's render-tick child rebucket
(EquippedChildRenderController.TickChild) into Runtime. Whether that is correct
depended entirely on whether retail propagates a parent's cell change to its
children. It does — via two independent mechanisms, both on the physics path.

Recursive: change_cell @0x00513390 -> enter_cell @0x00510ed0 recurses into
children @0x00510f03 passing the same CObjCell* down, each frame writing
m_position.objcell_id @0x00510f1e and cell @0x00510f35 and registering the
child individually via add_object @0x00510ee2. leave_cell @0x00510f50 mirrors
it @0x00510f84.

Per-commit: SetPositionInternal(CTransition const*) @0x00515330 has an explicit
depth-1 child loop @0x0051539c-@0x005153d8 writing child+0x4c from the parent's
curr_pos.objcell_id @0x005153bd.

The structural reason it must exist: update_object @0x00515d10 early-returns on
parent != 0 @0x00515d40, so a parented child is NEVER independently simulated.
The parent's tick is its only source of cell and frame.

This corrects the routes-6-7 scoping doc, which said retail re-cells "inside
set_parent". It does not — set_parent @0x00515a90 (both overloads read in full)
contains no cell write and delegates to change_cell @0x00515ad6.
recalc_cross_cells @0x00515a30 only READS objcell_id as a guard @0x00515a3f.
The prior UpdateChild finding is CONFIRMED: @0x00512d50 -> set_frame @0x00514090
writes m_position.frame @0x005140e9 only.

Consequence pinned for route 7's implementer: the Runtime replacement must
write the child's cell at BOTH the set_parent analog AND the per-commit
position analog. A set_parent-only write is correct at attach and stale on the
parent's first cell crossing — which is the natural misreading of the earlier
scoping.

Also recorded: unset_parent @0x00513470 does zero cell work (full body read),
leaving the child with a stale objcell_id still in that cell's object list;
all six call sites resolve it externally via leave_world @0x005155a0 (which
zeroes objcell_id @0x005155f4) or an explicit re-placement. +0x4c verified as
m_position.objcell_id by closing the offset chain against acclient.h rather
than trusting a Binary Ninja identifier. set_cell_id_recursive @0x00510da0 is a
red herring — its only caller is sky-object handling @0x00506eba.

Two adjacent facts remain NOT ESTABLISHED with the cdb breakpoints that would
settle them; neither affects the verdict.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 04:34:01 +02:00
Erik
11a8742884 docs: scope C4 routes 6 and 7 — route 6 needs zero production lines
Scoped together because both concern item/container placement; they turn out to
share no production file, but the shared inventory premise had to be corrected
once rather than twice.

The governing correction: both routes' inventory sections rest on
"RegisterEntityWithInitialResidence is never called from any production path."
C3c (529e0e9d) made that false — LiveEntityRuntime.cs:544-548 and
RuntimeLiveEntitySessionController.cs:118-123. Every conclusion reasoning from
"nothing upstream to cancel" is obsolete.

Route 6 (drops) is COMPLETE. No route-6 disposition exists: a dropped item is
Remote + TopLevel -> SetPosition/Placement|Slide, which is route 1's
classification exactly, and both drop flavors already converge on
LiveEntityHydrationController.OnCreate (split-recovery at
InventoryWorldDropProjectionController.cs:66). BuildSpawn overrides every
positional field, so the stale-source-position requirement is met.

Route 6 also retires a false premise in this campaign's own plan
(2026-08-02-placement-cutover.md:98-100): "replay create-time effects" is NOT
established as a defect. acdream's only create-time replay is the F754/F755
queue keyed by GUID, which is retail's own HandleCreateObject @0x00454C80
behaviour. The one plausible mechanism — a cloned DefaultScriptType — never
fires at create in either client; retail's play_default_script is reached only
from DoCollision @0x0058C3A0 and the hook dispatcher @0x00526C08/@0x00526C14.
And retail's "split-recovery marking" is a SELECTION transfer, not effect
suppression: UIAttemptSplitTo3D @0x0058D850 records WCID/stack/time and
DeclareValid @0x0058E340 re-selects at @0x0058E481. That port is missing but is
selection UX, not placement — file it outside C4.

Route 7 is one slice, ~300-490 lines, and must not be split: the Runtime commit
and the App demotion are two halves of one transfer. Retail performs NO
placement here — DoPickupEvent @0x00452240 is unset_parent + leave_world;
DoParentEvent @0x00452290 is set_parent + SetPlacementFrame. No SetPosition, no
leash. So route 7 inverts 4b-2's rule: never arm ConstrainTo.

Route 7's real defect: the child's canonical cell has two writers. Retail
re-cells inside set_parent (change_cell @0x00515AD6 + recalc_cross_cells
@0x00515B15); acdream commits the child cell-less in Runtime and re-cells it
from a RENDER TICK (EquippedChildRenderController.TickChild:408 ->
RebucketLiveEntity). Headless has no such controller, so every headless
parented child stays cell-less forever — the headless gap and the two-writer
split are the same bug.

Load-bearing unestablished item, recorded rather than guessed: whether retail
re-cells children when the parent crosses a cell. UpdateChild @0x00512D50 ->
set_frame @0x00514090 is frame-only and never writes objcell_id;
change_cell/set_cell's child handling was not read. Settle that before
demoting App's rebucket — demoting it blind risks route 4a's R1 / #184
invisible-but-solid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 04:27:13 +02:00
Erik
07e979393b docs: scope C4 route 5 (projectile authoritative placement)
Route 5 is RuntimePositionEntityKind.Projectile + ProjectileAuthoritative, and
the entity kind turns out to be behaviourally inert in the classifier —
ClassifyAcceptedPosition has exactly one EntityKind test (LocalPlayer, :349);
Projectile and Remote fall through identical code from :393, differing only in
OperationKind. Route 5 is an execution/ownership consolidation, not a
classification change.

Roughly half already shipped: the Create half and the residence-window Position
half are canonical and live. What remains is the post-residence accepted
Position, short-circuited before the classifier at
LiveEntityNetworkUpdateController.cs:1428-1448 — ~180 non-comment lines.

Retail has no missile branch: HandleReceivedPosition @0x00453FD0 and
MoveOrTeleport @0x00516330 route a projectile through the identical remote arm
@0x0045414D, ConstrainTo @0x00454272 IS armed for projectiles, and for an
in-flight missile with a cell and no teleport/contact retail does nothing
(return 0 @0x0051636D). This forecloses the plausible "projectiles are special"
implementation before anyone writes it.

Records that route 5 must land AFTER 4b-2 — it widens
RuntimeRemotePlacementDriveController.OwnsPlacement, which excludes
ProjectileAuthoritative today, leaving the far and teleport/cell-less branches
with no owner.

Names the gate problem honestly: ACE never sends UpdatePosition for a missile
(the one site is commented out at WorldObject_Tick.cs:333-334), so the Position
half is unreachable in ordinary play and has no cheap live trigger. The gate
covers the Create half and regressions; the four dispositions are test-gated.

Corrects a prior inventory claim: CommitProjectileCell is not ad hoc, it routes
into the shared CommitCanonicalCell (RuntimePhysicsState.cs:1376-1397). The
bypass is SnapToCell plus the InWorld/shadow tail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 04:26:30 +02:00
Erik
d5bdc35598 docs: pin the C4 route 4b-2 contract (remote far-snap)
4b-2 flips one classifier branch on: SetPositionSimple for remotes, contact,
PlayerDistance >= 96 m, routed through 4b-1's dormant owner. Teleport and
cell-less stay legacy for 4b-3.

Pins the two retail facts that decide the slice: StopInterpolating runs BEFORE
SetPositionSimple (@0x005163CB before @0x005163D9), and the branch returns 1 so
HandleReceivedPosition arms ConstrainTo @0x00454272 anchored post-move. Since
MoveOrTeleport discards SetPositionSimple's error return and returns 1
regardless, the leash must be armed on refusal and rejection too — "arm on
Committed" is the natural misreading and is the same shape as the already-filed
unarmed-leash bug.

Names the trap up front: deleting the legacy far block removes the only handler
for null and Rejected* classifications, and during the login window null is
every remote packet, so remotes would not move at all until the local
controller exists. Requires a stated policy rather than a silent drop — the
same shape as route 4a's "'not Interpolate' is not 'far'" finding.

Records that AP-87's 4 m / !willBeDrTicked guards are near-branch only and are
subsumed by the far branch's unconditional snap, so they must not be carried
forward — while the near-branch copies stay, since those are 4a's and still
load-bearing.

Requires behavioural App tests explicitly: route 2 settled for a source pin
(#292) and route 4a's first attempt shipped tautologies that passed with
production reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 04:09:15 +02:00
Erik
2e8e09acd0 feat(physics): C4 route 4b-1 — remote placement infrastructure (dormant)
Builds the machinery route 4b-2 and 4b-3 will flip on, and changes no remote
behaviour: it has no production caller, so RemotePlacementDrivePendingCount is
provably 0 and IsConverged is unchanged.

Five pieces: a per-entity remote placement owner (RuntimeRemotePlacementDriveController),
a Position-time service-window guard with a Runtime interface plus BOTH host
implementations, N3's headless RetryPending pump, parked-count observability in
the ownership ledger, and the service-window optimisation that avoids parks we
can cheaply predict.

Landed alone because it is where the park-withdraws-the-entity failure was
decided; that decision is fixed at the source in the preceding commit and must
not share a review signal with a behaviour flip.

Two parts of route 2's controller are deliberately NOT ported, both verified
against retail rather than assumed. There is no ack: SendPositionEvent is called
only inside HandleReceivedPosition's local-player FORCE_POSITION gate
@0x0045400C-@0x00454091, and the remote arm @0x0045414D has no equivalent. There
is no re-issue funnel: retail never re-attempts a position it could not apply —
stale timestamps merely bump error_count @0x004542AC — and re-issuing packet N
after N+1 has merged would apply a pose the newer packet already superseded,
which is correct for a one-shot ForcePosition and wrong for a 5-10 Hz stream.

The service-window guard is an OPTIMISATION, not the correctness mechanism. The
original contract had it the other way round, justified by a claim that retail
cannot represent "arrived but not placeable" — false, and corrected in the
review findings: retail's GotoLostCell/reenter_visibility path represents it
exactly. A pre-flight guard also cannot be complete, because Core defers on the
entity's CURRENT cell, on the swept QueriedCellIds footprint spanning
neighbouring landblocks, and on residency evaluated after AdjustToOutside —
conditions only Core can see.

Review found and this commit fixes: DetachRoute cleared two maps of LIVE Core
operations without cancelling them (route 2's AbandonPending is the correct
mirror, not the first-entry controller) and its test asserted that blindness as
convergence; the headless predicate answered "can ever publish" rather than "is
published", and after the first fix still matched only 1 of the 9 landblocks
this host publishes; OwnsPlacement admitted remote top-level Creates until
gated on the Teleport flag as well as the disposition; Advance re-submitted
without re-checking the window; and four comments cited a report that did not
exist.

Contract item 6 is met by the structural proof, not the earlier test:
HasOldPrefixPlacementDebt refuses collision-prefix mutation permission before
ParkCollisionResidents is ever entered, so its overlap throw is unreachable.
That same mechanism is the unbounded stall filed as #310, which 4b-1 does not
bound — it only avoids widening it.

#311 files the remaining per-tick allocation in RetryPendingProjections; the
early-out for the empty-FIFO case landed via a new HasPendingReceipts accessor
so hosts still never touch .Placements. directly.

Gates: complete Release solution 10,973 passed / 4 skipped / 0 failed (baseline
10,938). Four review rounds; every fix discrimination-verified by revert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 04:08:19 +02:00
Erik
634bc5513a fix(physics): restore a cancelled park instead of leaving the entity withdrawn
Shipped-code defect affecting committed route-2 code, found while reviewing
route 4b-1.

RuntimeSetPositionState.ParkDeferred withdraws an entity from the world:
body.InWorld = false, TransientStateFlags.Active cleared, WithdrawCanonical,
SuspendObjectClock. CancelCoreDeferred then removed the operation and rewrote
the pending Withdraw into a Discard while restoring NONE of it. So cancelling a
wakeable park was strictly worse than keeping one — the park is wakeable, the
cancel destroys the only object that could ever wake it, and the entity is left
invisible AND intangible with nothing to bring it back.

Route 2's re-issue funnel masked this: re-issuing is correct for a one-shot
ForcePosition ACE never repeats, and wrong for a repeated remote stream, so the
hole was hidden rather than fixed.

Retail's own answer is a working park, verified in the decomp rather than
assumed: CPhysicsObj::SetPositionInternal @0x00515BD0, when AdjustPosition
yields no cell @0x00515C1D, calls prepare_to_leave_visibility @0x00515CDA,
store_position @0x00515CE2 (the DESTINATION pose is committed), GotoLostCell
@0x00515CF2 registering at m_position.objcell_id read AFTER store_position (so
the destination cell), clears transient 0x80 @0x00515CF7, and returns OK
@0x00515D07. InitObjCell @0x00508260 drains the lost list on cell load and calls
reenter_visibility @0x00516250, which re-places from the object's OWN
m_position with flags 0x11.

Two corrections to the direction I gave, both forced by evidence and both right:

The pose must NOT be rolled back — only the withdrawal. Three shipped route-2
tests capture positionAtPark AFTER the park and assert it survives the cancel,
and retail agrees: store_position commits the destination and nothing
un-commits it. Restoring residency at the body's committed cell is therefore
retail's own cell choice, not merely self-consistent.

The gate defaults to FALSE with four explicit opt-ins, rather than defaulting
true with opt-outs at the withdrawal callers. That keeps every one of the ~20
shipped Forget/ForgetExactPlacement sites at exactly its current behaviour
instead of depending on having correctly enumerated the withdrawal transactions.
Review had already found the broad version corrupting five of them
(TryApplyPickup, CommitAcceptedParent, CommitAcceptedParentCellless,
CommitWithdrawal, CommitPositionChannelUpdate): they hand-roll a partial
re-withdrawal that undoes the clock and FullCellId but not InWorld or the
_spatialRoots re-registration, leaving a picked-up item both in inventory and an
InWorld cellless spatial root in the physics workset.

ParkDeferred's restorableOnCancel is opt-in for exactly one of its four callers
— the plain unplaceable-destination park. Every quiescence and retirement park
is excluded deliberately: those entities are withdrawn because their world is
going away, and restoring residency inside a quiescing prefix blocks its
retirement.

VerifyPositionChannelCancellation now asserts InWorld and IsSpatialRoot per
channel — Position is a cancellation and must restore; Pickup and Parent are
withdrawals and must not. It previously asserted only !IsDeferred and counts,
which is why five green states hid this.

Register row AP-136 measured against GotoLostCell/reenter_visibility rather than
labelled "retail-shaped". Files #309 (the restore-on-cancel residual, with
park-survives recorded as the retail-faithful target and its two blockers named:
the NewerPositionPickupAndParentEachCancelExactLostOperation invariant and
teardown convergence) and #310 (an unbounded retirement stall — a retained
preparation retry pins its prefix through HasOldPrefixPlacementDebt forever, and
TickLostCellDeadlines has no production caller so the 25 s timer never fires).

This is a user-observable change to shipped paths: restorableOnCancel: true sits
in SubmitPreparedPlacementCore, the shared core behind every production
placement. AP-136 and #309 carry the proposed two-client check.

Gates: complete Release solution 10,973 passed / 4 skipped / 0 failed (baseline
10,938). Every new test discrimination-verified by reverting the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 04:07:39 +02:00
Erik
9e97be1896 docs: pin the C4 route 4b-1 contract (remote placement infrastructure)
4b-1 builds the machinery and changes no remote behaviour: a per-entity remote
placement owner, a Position-time service-window guard on both hosts, the
refuse-rather-than-park policy, N3's headless RetryPending pump, and parked-count
observability. No production caller, so 4b-2 and 4b-3 flip it on afterwards.

It lands alone because it is where the park-withdraws-the-entity failure mode is
decided, and that decision needs its own review signal rather than sharing one
with a ~700-line class deletion.

Pins the central decision: refuse rather than park. A DeferredCell park withdraws
the entity (InWorld false, Active cleared, clock suspended, residency dropped),
and Forget-on-every-accepted-Position kills the park without restoring any of it
— so a remote that parks and is then superseded by an Interpolate packet stays
withdrawn indefinitely, invisible and intangible.

Names the two transfer errors that would look correct to anyone copying route
2's controller: do not port the ack machinery (retail's remote arm has no
SendPositionEvent) and do not port the re-issue funnel (re-issuing a superseded
pose is wrong for a repeated 5-10 Hz stream).

Makes ParkCollisionResidents' overlap throw a gate item rather than a note — it
is unreachable today only because steady-state remotes hold no operations, and
with N remotes an ordinary streaming retirement would become session-fatal.

Lists what 4b-1 must not touch, including AP-135's two writes (4a-owned
dispositions that sit inside the method 4b rewrites — the trap) and the single
retail ConstrainTo arming site.

Records both known flakes by number and mechanism so they cannot be conflated
again: #302 is a GC-allocation assertion in App.Tests, #308 a wall-clock deadline
in Core.Net.Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 01:11:42 +02:00
Erik
eeec4fb42a diag(physics): remote landing-edge probe; record the two live jump defects
The user live-tested route 4a and reported two defects on player remotes: a
remote holds the falling animation after landing before finally landing, and a
remote jumping onto a house plants on the roof where retail slides off, then
blips to the slid-down position.

Neither is a route 4a regression. Do NOT revert 44830a0e — reverting would
restore the per-packet render slam 4a removed without touching either defect.

Bug B's root cause is identified and already covered by open issue #32, whose
text names both symptoms in one sentence. Both landing sites assert
TransientState |= Contact | OnWalkable unconditionally, where retail derives it
from the contact plane — CPhysicsObj::SetPositionInternal @0x00515330
(`if (contact_plane.N.z < floor_z) set_on_walkable(0) else set_on_walkable(1)`).
A steep roof is contact but NOT on_walkable; asserting both suppresses the slide
response, so the body sits until the server's positions walk 4 m away and
AP-87's threshold snaps it. That is the blip. Verified byte-identical pre-4a via
`git show 19d95094:`.

Bug B's *visible shape* IS 4a's: pre-4a every packet slammed the render entity
to the wire pose, so a stuck body flickered toward the true sliding position
5-10x per second — jitter rather than a clean hold.

Bug A stops at the goal's stop-condition rather than getting a speculative fix.
Three hypotheses with non-overlapping fixes; picking wrong means changing a
retail-ported gate on a guess. Retail's mechanism is already fully decoded, so
what is missing is OUR runtime state — no cdb trace against retail is needed.

Adds ACDREAM_PROBE_REMOTE_LANDING (PhysicsDiagnostics, read once at startup per
the diagnostic-owner rule, one bool check when off). It logs both landing sites
immediately before HitGround, and — the most diagnostic signal — emits a
separate line when a site is reached but the gravity gate is about to no-op,
which is hypothesis 1 (a wholesale Body.State write wiping the transient Gravity
bit mid-air, exactly AP-81's stated risk). Temporary instrumentation, marked for
stripping once the evidence is in.

Evidence recorded rather than new bugs filed: #32 gains the observation, the
root cause and the #173/AD-10 dependency caveat; AP-87 gains a live instance of
its stated risk; AD-10's stale file:line is corrected to RemoteMotionCombiner
with a note that its terrain-only normal cannot see a house roof at all.

Also files #308 — a SECOND flaky test, distinct from #302, which was twice
misattributed to it before being written down. #302 is a GC-allocation assertion
in App.Tests; #308 is a wall-clock deadline loop in Core.Net.Tests that fails
only under full-suite CPU contention (0 failures in 4 isolated runs). Conflating
them hides one, and an agent told to "ignore the known flake" would wave through
a real transport regression.

Gates: complete Release solution 10,938 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 01:10:03 +02:00
Erik
dda1e2a03a docs: C4 route 4b scoping — split three ways, and two corrections
Scoping at 44830a0e puts 4b at 1,300-2,200 production lines (centred ~1,700)
plus ~2,500-3,500 lines of test work — 4-6x route 4a and ~2x route 2, the two
largest landings in this campaign, which took 4 and 5 review rounds. Split into
4b-1 (infrastructure, no behaviour change), 4b-2 (far branch), 4b-3 (teleport
and cell-less, with the ~739-line class deletions). 4b-1 stays separate
regardless of appetite for landings.

Corrects two errors in documents from yesterday:

AP-135 does NOT retire with 4b. Its own condition is retirement with the
free-fall sweep gate, which 4b does not touch, and its sites are the airborne
no-op branches — 4a-owned dispositions. The trap is that its two writes sit
inside OnPosition, which 4b rewrites heavily.

Retail has exactly ONE ConstrainTo on the remote arm (@0x00454272); all three
nonzero-returning MoveOrTeleport branches funnel through it. My route-4 scoping
implied a distinct remote-teleport arming site. There is none, so 4b must not
add a second one — the post-operation arm 4a introduced becomes the only arm.

Records a new failure mode 4b must not create: a DeferredCell park WITHDRAWS the
entity (InWorld false, Active cleared, clock suspended, residency dropped), and
Forget-on-every-accepted-Position kills the park without restoring any of it. If
the next packet classifies Interpolate, no placement runs and the remote stays
withdrawn indefinitely — invisible AND intangible, the #184 class through a
third door. Direction: refuse rather than park; the next packet is the retry,
because remote Positions are a 5-10 Hz stream.

Two transfer errors named explicitly so they are not repeated: do not port route
2's re-issue funnel (re-issuing a superseded pose is wrong for a repeated
stream), and do not port its ack machinery (retail's remote arm has no
SendPositionEvent).

Also records that remotePlacementRequired fires for every non-visible remote on
the graphical host — a routine hot path, not a teleport rarity — and that
deleting the legacy blocks removes the only handler for null/Rejected*, which
during the login window is every remote packet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 00:51:32 +02:00
Erik
44830a0eb3 feat(physics): C4 route 4a — remote steady-state Position through the seam
Routes the classifier's two NO-PLACEMENT remote branches — Interpolate
(contact, PlayerDistance < 96 m) and NoPositionOperation (no contact) — through
a Runtime-owned seam, and fixes the two divergences they carried. Teleport,
far-snap and cell-less stay on the legacy App path; 4b owns them.

Route 4 was split into 4a/4b after scoping put the whole route at 1,500-2,500
lines against a ~400 budget. 4a's branches perform no SetPosition, so this slice
carries no deferred-cell park, no service-window guard and no allocation
exposure — which is what made the split worth doing.

Divergences fixed, both previously unfiled:

* D1 — the NPC airborne branch hard-snapped Body.Position/Orientation and
  branched on the client-tracked rmState.Airborne, never consulting the wire
  IsGrounded bit. Retail's MoveOrTeleport @0x00516330 returns 0 at 0x0051636D
  and writes nothing. Player remotes were already correct; NPCs were not.
* D2 — ConstrainTo was armed before the operation, unconditionally, so it fired
  on the airborne no-op retail skips and anchored to the PRE-move position.
  Retail arms it at 0x00454272, only when MoveOrTeleport returns nonzero,
  anchored to &arg2->m_position read live, i.e. post-move.

AP-87 and TS-44 were carried deliberately, not delegated away. AP-87's three
conditions — including firstUp, which one round silently dropped — are preserved
as an explicit acdream policy layer applied AFTER the classifier commits to
Interpolate; the two previously separate player/NPC copies are now one. TS-44
stays an NPC-only caller gate; extending sticky suppression to player remotes has
no retail basis and no live evidence, so it was declined rather than absorbed.

Landing is explicitly carved out of 4a's ownership on both arms. A landing packet
classifies Interpolate, so an ordering slip would ENQUEUE a body that must PLANT
and a creature knocked off a ledge would glide down over a packet interval. The
carve-out is a named entry point returning AirborneSnap/SteadyStateInterpolate/
Legacy precisely so the PRECEDENCE is observable and testable rather than implied
by statement order — that is how the slip happened once and was caught.

The player/NPC asymmetry on landing is real and NOT resolved here: retail draws
no such distinction, but converging them is a behaviour decision needing its own
evidence. Filed into the 4b plan.

Register: AP-135 filed for the two bookkeeping writes the airborne branch
deliberately retains (rmState.CellId, LastServerPos/Time) — not retail's model,
but load-bearing for our catch-up sweep and staleness timer, and verified not to
be a canonical cell commit for ordinary remotes. AP-87 and TS-44 rewritten to
describe the code.

Honest remainder: App still owns branch selection, the airborne return, the cell
write, the entity write and the shadow publish, and headless satisfies "both
hosts drive the identical entry point" only vacuously since it returns early for
remotes. That is written into the 4b bullet rather than left implicit.

Cost: 364 non-comment production lines, 91% of the ~400 budget — the split did
isolate the cheap half, but not by much. Do not carry "well under" into 4b's
scoping.

Gates: complete Release solution 10,938 passed / 4 skipped / 0 failed (pre-4a
baseline 10,909). Four review rounds; the first three each introduced a new
behavioural defect while fixing another, and each left a comment asserting
behaviour that no longer matched — the final round's precedence matrix was
traced cell-by-cell against HEAD with only the D1-intended difference. App tests
call production entry points against a real WorldEntity and real classifier
output, closing route 2's #292 gap rather than repeating it.

Connected acceptance NOT run — needs a live second character.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 00:19:05 +02:00
Erik
19d9509497 fix(physics): #307 — PreviousTeleport was always 0 on the live Position path
Shipped defect in route 2 (9966b531), found while reviewing route 4a.

`InboundPhysicsStateController.TryApplyPosition` built its AcceptedPhysicsTimestamps
via `Current(gate, teleportAdvanced: ...)`, omitting `previousTeleport`, which
defaulted to a literal 0. The only site that populated it was the deferred
initial-create path — which is why the continuation executor was correct and
every newer consumer was not.

Consequence in shipped code: route 2 feeds this into
`ValidAcceptedAuthority`, which requires Previous == Accepted for a
ForcePosition. Any local player whose TELEPORT_TS is nonzero — anyone who has
portalled or recalled this session — had the authority rejected and the force
correction SILENTLY DROPPED. The user's @pklite acceptance was genuine but
narrow: that character had not teleported, so the stamp was still 0.

Second latent consequence: with an accepted stamp >= 0x8000, wrap-safe
TeleportRegressed also fires against the 0 and rejects ordinary Apply positions,
not just ForcePosition.

The fix captures `previousTeleport = gate.TeleportTimestamp` BEFORE
`TryAcceptPositionEvent` mutates it, matching the shape the deferred path
already used. Ordering is the whole point: capturing after would make
Previous == Accepted unconditionally, so ValidAcceptedAuthority's check would
pass vacuously — the symptom would disappear while the semantics broke.

Also removes the footgun that allowed it. `Current`'s parameter is now
`ushort? previousTeleport = null` resolving to `gate.TeleportTimestamp`, so the
eleven non-Position channels — none of which can move TELEPORT_TS — get
"previous == current" by omission rather than a literal 0 that is
indistinguishable from a genuine "never teleported".

Consumer audit: only TryApplyPosition was defective. The two route-2 call sites
trace back to it; the RuntimeEntityObjectLifetime sites source from
TryAcceptDeferredPosition and were already correct.

Tests discrimination-verified by reverting the argument to 0: the stamp test
fails Expected 10 / Actual 0, and the classifier test fails Expected
SetPositionSimple / Actual RejectedAuthority — the shipped defect reproduced
exactly.

Gates: complete Release solution 10,935 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:48:09 +02:00
Erik
b633b10967 fix(chat): display retail's text for WeenieError 0x0504 and three PK siblings
User saw "[System] WeenieError 0x0504" on login after a PK Lite reversion.
0x0504 is YouAreNonPKAgain; only ~56 of 378 codes had strings, so the raw hex
fallback fired.

Retail's source is ClientCommunicationSystem::HandleFailureEvent @0x00571990 —
a switch with per-case literal UTF-16 strings, not a DAT string-table lookup, so
hardcoding them is retail-faithful.

The decomp could not be trusted for the text. Its dump of data_7d32c0 declares
[0x5f] and shows 95 characters ending mid-word at "...protection of the Lig".
The real string is 139 characters. The 0x5f is Binary Ninja's PREVIEW
TRUNCATION LENGTH, not the array size — worth remembering for the rest of the
switch, since a copy-paste from the dump would have shipped a truncated
sentence. Recovered by PE byte read (VA 0x007D32C0 -> RVA -> .rdata file
offset), cross-confirmed against the raw hex the pseudo-C carries immediately
after the preview.

Mapped 0x0504, 0x0505, 0x04EC, 0x04ED, each byte-verified and cited with its
case address. Retail's trailing newline is dropped deliberately (documented
in-comment): acdream renders one ChatEntry per system message where retail has a
single scrolling buffer. Adjacent codes are deliberately left unmapped with a
test pinning that 0x04EE still falls back to hex — a wrong message is worse than
a raw code.

Files #306 for the full port, with three findings that make it more than a
string table: the switch is SIX compiler-lowered blocks spanning 339 distinct
case values from 0x17 to 0x593, not one contiguous band; retail passes a colour
argument with three values in use (0 x162, 0x1a x113, 7 x59) and acdream's chat
has no colour concept; and HandleFailureEvent aborts an in-progress automatic
attack on 0x43/0x3f7/0x3e/0x23/0x36 — verified against the decomp, with the
nuance that 0x43 has no display case at all and is abort-only, so that one is a
pure gameplay gap.

Gates: complete Release solution 10,909 passed / 4 skipped / 0 failed (baseline
10,904; +5 = the five new tests).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:20:31 +02:00
Erik
2f0c26c8a4 docs: split C4 route 4 into 4a/4b and pin the 4a contract
User-directed after scoping put whole-route 4 at 1,500-2,500 production lines
against a stated ~400 budget.

4a is the steady state: the classifier's Interpolate (contact, < 96 m) and
NoPositionOperation (no contact) branches. Neither performs a SetPosition, so 4a
carries no deferred-cell park, no service-window guard, no allocation exposure,
and no interaction with the Forget-on-every-accepted-Position behaviour that
dominated route 2's review rounds. It also fixes two of the three unfiled
divergences: the NPC airborne hard-snap that ignores the wire IsGrounded bit
(retail returns 0 and writes nothing, MoveOrTeleport @0x0051636D), and
ConstrainTo armed before the operation instead of after (retail arms it post-move
only on a nonzero return, @0x00454272).

4b takes the edges — teleport, far-snap, cell-less — where the parks, the
Position-time service-window guard, #277's broken bound, N3, and the third
divergence live.

The contract sanctions exactly one dual path: 4a routes its two classifications
through the new seam and leaves the other two on the legacy path until 4b. That
is a staged cutover rather than a duplicate authority ONLY because the
discriminator is the classifier itself and the classifications are mutually
exclusive; the contract says so explicitly and requires the fallback deleted in
4b.

Two carried acdream additions are called out as load-bearing rather than left to
be discovered: AP-87's 4 m / !willBeDrTicked snap conditions (which prevent the
#184 invisible-but-solid monster and are NOT in the classifier) and TS-44's
sticky suppression. Silently dropping them by delegating to the classifier is
named as the failure mode.

Acceptance requires a BEHAVIOURAL App test, not the source-text pin route 2
settled for (#292).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:17:22 +02:00
Erik
62e906b136 docs: #297/#298 user-accepted live
The user confirmed melee and bow now work against a PKLite player in a live
two-client session ("melee and bow works, all good"). That accepts #298
directly, and #297 indirectly but conclusively: the both-PKLite arm of
ObjectIsAttackable cannot pass unless the LOCAL player's own PKLite bit is
live, which is exactly what #297 fixed.

Not separately confirmed by the user and therefore NOT recorded as accepted:
the collision-after-equip case (#297's round-2 defect) and combat-camera
tracking (#298's second site). Both are implemented, suite-green and
review-passed; they remain unverified by observation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:03:12 +02:00
Erik
b17f5cee49 docs: close #297-#299 with SHAs; session handoff
Marks #297 (9b1e6fc6), #298 (bc0077a5) and #299 (88348f67) DONE per the
issue-tracking rule, and adds a handoff covering what landed, what still needs
the user's eyes, and the route 4 decision waiting on them.

Three items are implemented and suite-green but NOT user-verified: collision
with PKLite players (including the equip/unequip case round 1 got wrong),
melee/bow on a PKLite player plus the auto-target guard, and combat-camera
tracking of a PKLite opponent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 21:30:17 +02:00
Erik
bc0077a55f fix(combat): #298 — admit player targets to melee/missile attack and the camera
Selecting a PKLite player and attacking did nothing: with auto-target on it
retargeted to the nearest monster, with auto-target off it logged
"combat: attack ignored; no creature target found". Spells on the same target
worked, which was the clue.

Root cause: CombatTargetPolicy.IsHostileMonster:31-33 rejects any candidate
carrying BfPlayer BEFORE reaching ObjectIsAttackable, so the both-PKLite pool
match at SelectedObjectHealthPolicy.cs:70-71 was unreachable for players. Melee
and missile targeting never supported player targets at all — the gate is named
IsHostileMonster and does exactly what it says. Nobody could hit it until
69ba9486 made PK Lite reachable.

Retail uses ONE predicate for monsters and players, with no player exclusion:
ClientCombatSystem::ExecuteAttack @0x0056BB70 gates unconditionally on
ObjectIsAttackable @0x0056A600 (creature type, Free-PK short-circuit on either
side, then IsPlayer -> bothPK || bothPKLite, else BF_ATTACKABLE with pets
excluded). acdream already ported that predicate verbatim; it was simply
unreachable.

The fix SPLITS the two concerns rather than relaxing the shared predicate:
explicit-target admission routes through ObjectIsAttackable, while auto-target
ACQUISITION keeps the monster-only gate. That is required by register row
IA-19 — explicit product direction that Auto Target must never select NPCs,
players or pets. IA-19 is not overridden here; its own justification promises
"manual player-selection commands remain available", and that promise was never
implemented, so this makes the row true. Review confirmed no path lets
auto-acquisition select a player: every automatic Select is fed by a
FindClosest* filtered through IsHostileMonster.

Review also found a second site with the same bug, which the first pass froze in
place on my instruction: retail gates combat-camera tracking on the SAME
predicate as the attack. ClientCombatSystem::UpdateTargetTracking @0x0056A950
reads GetAttackTarget() then gates CameraSet::TrackTarget on ObjectIsAttackable.
Ours used the monster-only gate, so with ViewCombatTarget on by default the
attack would land while the camera refused to track the opponent — user-visible
in exactly the duel this fix enables. GetCombatCameraTargetPoint now uses the
wide predicate. IA-19 does not reach the camera: it performs no acquisition,
only presentation on an already-chosen target. The first pass had added a
source comment asserting IA-19 covered it; that comment and the matching text in
docs/ISSUES.md are corrected, since a wrong citation is how a real divergence
becomes invisible.

Depends on 9b1e6fc6 (#297): the both-PKLite arm needs the LOCAL player's own bit
to be live. Review confirmed both admission sites read ClientObjectTable on every
call, so this is not inert in production.

Newly reachable and now pinned: ObjectIsAttackable's pet-exclusion arm, which
CombatTargetPolicy rejected before it could ever run.

Follow-ups filed: #304 (SelectionInteractionController.GetSelectedOrClosestCombatTarget
has no production caller — one of the two widened call sites is dead code),
#305 (HeadlessGameplayOperations has the identical pre-existing bug, so the
graphical/headless hosts now diverge).

Gates: complete Release solution 10,904 passed / 4 skipped / 0 failed (baseline
10,900). Adversarial + retail-conformance review PASS after one FAIL round; the
predicate was re-verified branch-for-branch against 0x0056A600 since it goes
live here for the first time. Camera fix discrimination-verified by revert.
Connected acceptance NOT run — needs a live two-client PKLite session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 21:29:15 +02:00
Erik
9b1e6fc637 fix(physics): #297 — keep the PWD bitfield live so PK status reaches the client
The user typed @pklite and then walked straight through other PKLite players.

Root cause: ClientObject.PublicWeenieBitfield was written exactly once, from the
0xF745 CreateObject parse, and never refreshed. ACE's only PK-change message is
PropertyInt.PlayerKillerStatus (134) over 0x02CE/0x02CD, which we parsed and
stored into Properties.Ints[134] but never translated back into the bitfield —
and ACE never re-sends a PublicWeenieDesc at all (EnqueueBroadcastUpdateObject
has zero live callers), so that property is the ONLY signal a client can learn
from. Both sides of the collision test read the frozen value, so
CollisionExemption's "4c. both PKLite -> collide" rule could never fire.

Retail's missing port: PublicWeenieDesc::SetPlayerKillerStatus @0x005AC7C0
rewrites _bitfield in place — PK(4) -> (b & 0xfddfffff) | 0x20; PKLite(0x40) ->
(b & 0xffdfffdf) | 0x2000000; Free(0x20) -> (b & 0xfdffffdf) | 0x200000; else
b &= 0xfddfffdf. Mutually exclusive, verified byte-for-byte, with input values
confirmed against retail's own PKStatusEnum (acclient.h:6412-6427), not just
ACE's. Driven from ACCWeenieObject::OnStatUpdated @0x0058DF20 case 0x86.

The fix rewrites the value at its source rather than patching consumers. Two
review rounds were needed because the first pass missed that there are TWO
snapshot stores: InboundPhysicsStateController keeps its own private _snapshots
dictionary, and every untimestamped-field merge (ApplyAcceptedObjDesc and
friends) reads `old` from THAT store, not from RuntimeEntityRecord.Snapshot.
Refreshing only the active record left the target-side shadow flags correct
until the remote's next equip or unequip — ACE broadcasts an ObjDesc on every
one — at which point the appearance path rebuilt the registration from the
frozen spawn and dropped the bit permanently. The regression test demanded by
review is what surfaced that; it is verified discriminating (reverting gives
Actual: 8 instead of 33554440).

Five stores now hold this value, kept coherent from one source by two
ObjectUpdated subscribers plus the appearance-rebuild path. The two shadow-flag
writers are the same invalidation applied at the two edges that can invalidate
it, not competing authorities — review enumerated every drift path and closed
each. That coherence invariant is new as of this commit and is recorded as
register row AP-134, with AP-133 as the precedent for filing a row when the
danger is a future writer rather than current behaviour.

Also corrects TS-23's retirement narrative, which claimed every mover-flags call
site read the mover's "real" PK bits from 2026-07-30. The bits existed but their
source was frozen, so that only became true here; the site enumeration also
missed RuntimeSetPositionMoverPreparation, a seventh site that decodes the
snapshot directly.

Unblocks #298 (melee/missile admission needs the local player's own PKLite bit).
Follow-ups filed: #300 (Properties.Ints[134] vs bitfield mirror gap), #301 (same
defect class for radar blip colour and radar behaviour), #302 (a pre-existing
PortalProjection allocation-assertion flake, 1 in 6, found while verifying this
gate), #303 (LiveEntityPvpBitfieldSync is App-resident but Runtime-owned-state).

Gates: complete Release solution 10,895 passed / 4 skipped / 0 failed (baseline
10,887 including #299). Adversarial + retail-conformance review PASS after one
FAIL round. Every new test discrimination-verified by reverting the fix.
Connected acceptance NOT run — needs a live two-client PKLite session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:59:01 +02:00
Erik
88348f6791 fix(physics): #299 — port retail's mover-side IsImpenetrable exemption branch
CollisionExemption checked only the TARGET's IsImpenetrable, and the class doc
asserted "retail's pseudo-C only checks the target's IsImpenetrable(); acdream
follows retail" while blaming ACE for checking both. That was backwards: ACE was
retail-faithful and acdream was missing half the check.

Retail short-circuits on EITHER the mover's own state & IS_IMPENETRABLE (0x80)
OR the target's IsImpenetrable(); either alone exempts. Verified at the byte
level rather than from the decompiler's rendering — Binary Ninja shows the mover
test as `int16_t state_1 ... if (state_1 < 0)`, which reads like a 0x8000 test,
but decoding the PDB-paired binary at the mapped offset gives:

    8b 43 04   mov  eax,[ebx+4]     ; mover object_info.state
    f6 c4 01   test ah,1            ; 0x100  IsPlayer
    84 c0      test al,al           ; sign bit of AL = state & 0x80
    78 3d      js   ...             ; -> collide

`test al, al; js` is a byte-level sign test on AL, i.e. 0x80, not 0x8000.
Corroborated downstream in the same block (`test ah,8` = 0x800 IsPK,
`test ah,0x10` = 0x1000 IsPKLite) and by OBJECTINFO::init @0x0050cf30 setting
state |= 0x80 from the object's own IsImpenetrable().

Also corrected: ACCWeenieObject::IsImpenetrable @0x0058c8c0 returns
(_bitfield >> 0x15) & 1 — retail genuinely conflates BF_FREE_PKSTATUS with
"impenetrable", so acdream's FromPwdBitfield decode was already right.

Both retail arms set collide, so ordering between them is semantically free and
a misreading here could only ever produce spurious collisions, never a
walk-through.

Found while investigating #297; not symptom-causing on its own. No divergence
row: this retires a missing port rather than introducing a deviation, and
nothing in the register or the collision digest's DO-NOT-RETRY tables covers it.

Gates: complete Release solution 10,887 passed / 4 skipped / 0 failed
(baseline 10,867/4/0). Adversarial + retail-conformance review PASS on this
change specifically. Both new tests discrimination-verified by reverting the
branch and confirming failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:13:05 +02:00
Erik
40f5721354 docs: C4 route 4 scoping — the stated budget failed, stop and re-plan
After route 2 I pinned a falsifiable bet: routes 4-7 reuse the seam route 2
built, so their marginal cost should be well under 400 production lines, and if
route 4 also cost ~900 the bet was dead. Scoping estimates 1,500-2,500 lines
plus ~1,700 lines of test re-modelling. Honouring the bet: no implementation
pass until the scope is re-planned.

The bet failed for an instructive reason. The seam generalises fine — the
begin/prepare/submit chain has no local-player precondition, the classifier's
remote branches are already retail-exact, and all remote physics state is
already in Runtime. Route 2 was simply not a representative unit: one entity vs
N, one disposition vs four, one execution path vs two (canonical SetPosition
AND the interpolation queue), no teleport hook, no constrain phase, two
duplicate authorities vs six. Picking the simplest route first and then
calibrating everything against it was the error.

Four findings that change the campaign plan, not just route 4:

- Route 4's Create half is already done (C3b/C3c). The remaining work is
  steady-state remote Position plus deletions; the route title misleads.
- AP-131 cannot be retired by route 4. Route 2 did not fix its FORCE_POSITION
  half, and its local ordinary-Apply half is owned by no route in the inventory.
- #277's safety bound breaks: it argues about Creates, while a steady-state
  Position can carry a remote out of the collision window with no Create at all.
  Needs a Position-time service-window guard on both hosts; the graphical host
  has no such predicate today.
- N3 (headless never calls RetryPending) stops being latent the moment route 4
  makes headless remotes produce placement receipts.

Also records three previously unfiled divergences found while scoping: the NPC
airborne hard-snap that ignores the wire IsGrounded bit, ConstrainTo armed
before the operation instead of after, and ConstrainTo never armed on the remote
teleport branch. Route 4 fixes all three by construction, which makes it a
behaviour change to every visible creature rather than a refactor.

Allocation is NOT the blocker the inventory feared: the steady state classifies
to Interpolate, which runs no SetPosition at all.

Recommends splitting route 4 into 4a (near/interpolate + airborne no-op — the
observable win, no park hazard) and 4b (teleport/far/cellless — where the parks,
the service-window guard, N3 and #277 live).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 19:46:01 +02:00