acdream/docs/research/2026-08-04-c4-route-4b-3-contract.md
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

734 lines
48 KiB
Markdown

# C4 route 4b-3 — remote teleport + cell-less: pinned contract (2026-08-04)
Split rationale:
[`2026-08-04-c4-route-4b-scoping-and-split.md`](2026-08-04-c4-route-4b-scoping-and-split.md).
4b-1 (infrastructure) landed at `2e8e09ac`; 4b-2 (far snap) landed at
`7f1c1f5a` after four fix rounds; the shared-core park restore it forced is at
`634bc551` and inside 4b-2's rounds 3/4. Read the whole findings chain before
implementing — every defect class it names reappears here at larger scale:
[round 1](2026-08-04-c4-route-4b-2-review-findings.md) →
[round 2 (delta)](2026-08-04-c4-route-4b-2-delta-review-findings.md) →
[round 3](2026-08-04-c4-route-4b-2-round3-correction.md) →
[round 4](2026-08-04-c4-route-4b-2-round4-correction.md).
**4b-3 flips the LAST remote classification on: `SetPosition` — teleport
(TELEPORT_TS advanced) and cell-less (the body has no committed cell) — through
4b-1's `RuntimeRemotePlacementDriveController`, runs retail's `teleport_hook`
before the placement, and deletes the legacy remote-teleport machinery:
`RemoteTeleportController` (605 lines), `RemoteTeleportPlacement` (85),
`RemoteShadowPlacementSynchronizer` (49, two classes), the
`remotePlacementRequired` predicate, the `TeleportHookRequired` timestamp
plumbing, the legacy pre-operation `ConstrainTo` fallback, and the player arm's
legacy `!update.IsGrounded` fallback — plus 1,709 lines of their tests.**
This retires AP-137's cell-less enqueue-vs-place delta. AP-135 does NOT retire
(its two writes sit physically inside the method this slice rewrites — see
"Must remain true" item 8). AP-131 does not retire. #276 does not close.
## Retail ground truth — verified in `acclient_2013_pseudo_c.txt`, verify again yourself
`CPhysicsObj::MoveOrTeleport` @0x00516330, teleport/cell-less branch:
```
00516375 eax_8 = CPhysicsObj::newer_event(this_1, TELEPORT_TS, arg3);
00516386 if ((eax_8 != 0 || this_1->cell == 0)) {
005163ef CPhysicsObj::teleport_hook(this_1, edx_2);
005163f8 SetPositionStruct::SetPositionStruct(&var_64);
00516406 SetPositionStruct::SetPosition(&var_64, arg2);
00516414 SetPositionStruct::SetFlags(&var_64, 0x1012);
00516420 CPhysicsObj::SetPosition(this_1, &var_64);
00516438 return 1;
00516386 }
0051638e if (arg4 != 0) { ... near @0x005163AF / far @0x005163C1-E8 ... }
0051636d return 0;
```
Five facts in that listing decide this slice:
1. **The teleport branch is decided BEFORE the contact test** (`arg4` is only
read @0x0051638E, after the branch). A teleport/cell-less packet places
unconditionally — airborne wire bit, airborne body, any distance. acdream's
routing must therefore decide the teleport arm AHEAD of every
airborne/landing carve-out (see design decision D5).
2. **`this_1->cell == 0` is the BODY's current cell** — "this object has no
resolved cell right now" — not the wire destination's cell. See D1: the
classifier's current input implements a different (and dead) predicate.
3. **`teleport_hook` @0x00514ED0 runs BEFORE the placement** and is, complete
(each guarded on the manager existing):
`MovementManager::CancelMoveTo(0x3C)` @0x00514EDF,
`PositionManager::UnStick` @0x00514EEE,
`PositionManager::StopInterpolating` @0x00514EFD,
`PositionManager::UnConstrain` @0x00514F0C,
`TargetManager::ClearTarget` @0x00514F1B +
`TargetManager::NotifyVoyeurOfEvent(Teleported_TargetStatus)` @0x00514F28,
`CPhysicsObj::report_collision_end(this, 1)` @0x00514F31.
4. **`SetFlags(0x1012)`** (`Teleport|Slide|SendPositionEvent`) @0x00516414
acdream's analog is the classifier's `AuthoritativeTeleportFlags`, carried on
`route.SetPositionFlags` into
`RuntimeSetPositionState.TryPrepareAndSubmitAuthoredPlacement`'s `flags`
parameter. (There is NO separate "teleport hook phase support" inside
`RuntimeSetPositionState` — the phase lives on the route,
`RuntimeTeleportHookPhase.BeforePositionOperation`, emitted at the
classifier's remote teleport/cell-less branch. The handoff's phrasing
implied otherwise; this is the correction.)
5. **The branch returns 1 and discards `SetPosition`'s error**, so
`SmartBox::HandleReceivedPosition` @0x00453FD0 arms `ConstrainTo`
@0x00454272 — the remote arm's ONLY arming site, shared by all three
nonzero-returning branches, anchored `&arg2->m_position` read live
(post-move). Retail arms even when the placement failed. So: hook
`UnConstrain` first, place, then ONE post-operation re-arm — never a second
arming site, and arm on non-commit outcomes too (the 4b-2 B8/R5 lesson).
## What must REMAIN true (process rule 1 — the contract causes the defect)
For every path this slice adds or rewrites, including every refusal,
contention, rejection, and short-circuit:
1. **The pose still advances.** A teleport-classified packet whose canonical
placement never reached the engine (`Refused` / `Contention` /
`RejectedPreparation` / `NotApplicable`) still commits the accepted
destination pose to the canonical body — the same
`StoresAcceptedDestination` partition and the same
`StoreAcceptedDestinationPose` the far arm uses, which is retail's
`store_position` @0x00515CE2 on the no-transition branch. `Deferred` and
`RejectedByPlacement` do NOT store, for the reasons already pinned in the
enum's own doc (`ParkDeferred` already snapped; the engine ran and
refused / a settled pose must survive). Do not re-litigate the partition —
extend it to the teleport arm unchanged.
2. **The render entity still advances.** The teleport arm's tail syncs
`WorldEntity` (position, `ParentCellId`, rotation) from the RESOLVED body
and publishes the collision shadow, exactly as both existing grounded arm
tails do (`LiveEntityShadowPublisher.TryPublishRemote`). A teleport must
never leave the rendered pose a packet behind the body. #312's lesson:
presentation state is where this family breaks; tests must assert it
(see Test plan).
3. **The object clock keeps running and the entity stays in the world** on
every non-commit outcome: `body.InWorld`, `TransientStateFlags.Active`,
`record.ObjectClock` active, `FullCellId != 0`, spatial projection intact,
`IsSpatiallyVisible` unchanged. No park survives the controller
(`CancelToken` with `restoreCancelledPark: true`); the pre-flight
(`CanAttemptDestination`) stays an OPTIMISATION, never the reason a remote
stops tracking or becomes invisible.
4. **The interpolation queue is empty after the teleport arm runs**
cleared by the hook's `StopInterpolating` (@0x00514EFD), NOT by the route
flag: the classifier's teleport branch deliberately carries
`StopInterpolating: false` because retail's clear lives inside the hook.
The far arm's route-flag-driven clear is untouched.
5. **The leash is armed exactly once per accepted packet**, post-operation,
anchored post-move, by the classification partition — never zero times
(the current shipped hole: `remotePlacementRequired` returns ahead of every
arming site while `RemoteTeleportHook` has already `UnConstrain`ed, so a
remote that teleports and stands still is leash-less until the next
packet), and never twice. See D4 for the complete partition.
6. **The per-packet prologue keeps running for every classification**:
`TryApplyGenericRemoteRenderPose` (see D6 for the gate), the
`RebucketLiveEntity(update.Guid, p.LandblockId)` spatial-bucket
transaction, the velocity install
(`TryCommitAuthoritativeVelocity` — retail's PositionPack `set_velocity`,
upstream of `MoveOrTeleport`), and the incarnation re-validation
chain. Retail's remote teleport never writes velocity itself
(`ZeroVelocity` is the LOCAL player's `SmartBox::TeleportPlayer`
@0x004541B4 only); the teleport arm must not add a velocity write.
7. **`AcceptedPositionSource`/authority validation is unchanged.** The
teleport arm executes only an already-classified route from
`ClassifyRemoteAcceptedPosition` — the shared builder
(`RuntimeAcceptedPositionRouteRequests`), never a re-derivation.
8. **AP-135's two writes stay**, on both arms' airborne no-op
neighbourhoods: the server-cell adopt (`rmState.CellId = p.LandblockId`)
and the `LastServerPos`/`LastServerPosTime` sample. They are 4a-owned
acdream-only bookkeeping for the free-fall sweep gate
(`RuntimeRemotePhysicsUpdater`'s `rm.CellId != 0` gate) and the
first-grounded-packet velocity synthesis. This slice rewrites the method
they sit in; they do not go. The register row does not retire.
9. **`ParkCollisionResidents`'s overlap throw stays unreachable** — see
"Proof obligations".
10. **`ArmLostFamilyDeadlines`' reaper gains no production caller.**
`TickLostCellDeadlines` and `TryDequeueExpiredLostCell`
(`RuntimeSetPositionState`) have zero production callers today; this
slice must not add one. `ParkDeferred` arming the family (its
`ArmLostFamilyDeadlines` call) is pre-existing and stays inert.
11. **Route 1's classification inputs are unchanged.**
`RuntimeInitialCreateContinuationExecutor`'s calls into the shared
builder keep their current semantics (D1 adds an overload for the remote
PositionEvent path; the builder's own doc mandates "an overload here,
never a third copy").
12. **The 4a dispositions are untouched**: `NoPositionOperation` writes
nothing (plus item 8's bookkeeping), `Interpolate` enqueues, the landing
block still hard-snaps a wire-grounded packet for a not-in-contact body
on 4a-owned classifications, AP-87's snap conditions and AP-139's landing
clear are unchanged.
13. **The far arm (4b-2) is untouched** except where a shared gate widens to
include the teleport arm (D6, D7) — widening must not change the far
arm's own behaviour. Everything round 2's/round 4's "do not churn" lists
verified stays: the `StoresAcceptedDestination` partition, the
`IsRestorableQuiescencePark` relocation, the pose-parity composition, the
two-arm guard/arm ordering.
## Design decisions — pinned, not open for redesign
### D1 — the classifier's cell-less input becomes the PRE-merge committed cell (resolves trap T1 and undetermined item 3)
**Finding (undetermined item 3, now determined by reading the code):** the
merge cannot zero a previously-nonzero `FullCellId` — it does the opposite.
`RuntimeEntityObjectLifetime.TryApplyPosition` calls
`Entities.RefreshSnapshot(canonical, snapshot, refreshPosition: acceptedPosition)`
for every accepted Position, and `RuntimeEntityRecord.RefreshDerivedState`
then executes `SetFullCell(position.LandblockId, …)` from the just-merged
snapshot. So at classification time (OnPosition classifies after the
authority gate's merge, before the prologue rebucket), `canonical.FullCellId`
IS the accepted wire cell. A wire cell of 0 fails
`LandDefs.InboundValidCellId` inside `PositionFrameValidation.IsValid`, which
the classifier's `ValidPosition` check turns into `RejectedData` BEFORE the
cell-less test. **Consequence: the classifier's remote `cellless` predicate,
as fed today (`CommittedCellId: canonical.FullCellId` in
`RuntimeAcceptedPositionRouteRequests.Build`), is unreachable for a remote
PositionEvent — the remote `SetPosition` classification currently fires on
`TeleportAdvanced` alone.** It is not a subset relationship with
`remotePlacementRequired`; it is a dead predicate. (The classifier's own
comment above the test — "only a later Runtime SetPosition or simulation
commit may change FullCellId" — is falsified by `RefreshDerivedState` and
must be corrected in this slice; process rule 6.)
Retail's predicate is the BODY's current cell (`this_1->cell == 0` read at
`MoveOrTeleport` entry, before any placement) — "this object is not resident
anywhere right now", e.g. the first Position after an unwield-to-3D
(`SetFullCell(0,0)` at the pickup/parent leave-world sites) or after any
canonical withdrawal. The acdream value with exactly that meaning is the
committed cell BEFORE this packet's merge — the `wasCellless`/`beforeCell`
pair `TryApplyPosition` already measures.
**Pinned:** thread the pre-merge committed cell out of the merge and into the
remote classification's `CommittedCellId` input, via an overload of the shared
builder (never a third copy; never a change to the existing overloads' route-1
semantics). Plumbing shape is the implementer's choice — the natural carrier
is `AcceptedPhysicsTimestamps` (whose `TeleportHookRequired` field this slice
deletes; replacing a policy tri-OR with a data field is strictly better) or
`AcceptedPositionNetworkUpdate`. Constraints: the value is the pre-merge
`FullCellId` measured by the SAME `TryApplyPosition` call that merged the
packet (never re-read after the merge); no fabrication (a known record always
has an honest value — 0 means "was celless", not "unknown").
**What is deliberately NOT adopted from `remotePlacementRequired`:** the
graphical `projectionRequiresTeleportHook` arm
(`LiveEntityRuntime.TryApplyPosition`: pre-merge `FullCellId == 0` OR
`!IsSpatiallyProjected` OR `!IsSpatiallyVisible`). Its visibility half is a
presentation predicate with NO retail analogue — it made "not currently
rendered" fire the whole teleport machinery on a routine hot path (trap T1's
warning). After this slice the teleport classification is retail's exact pair
(`TeleportAdvanced || wasCellless`); a not-visible remote's Position
classifies by distance like any other, the canonical placement decides
placeability, and visibility remains presentation-only. This behavioural
change is recorded in the AP-137 rewrite (D8). The whole
`projectionRequiresTeleportHook` computation, the lifetime parameter, the
headless `false` at `RuntimeLiveEntitySessionController`, and the
`TeleportHookRequired` field + its `timestamps with {…}` write are deleted.
### D2 — classifier-null and `Rejected*` keep 4b-2's stated policy (resolves trap T2); the legacy airborne fallback is replaced by the retail return-0 shape
The scoping doc's T2 described the pre-4b-2 world; 4b-2 already deleted the
legacy near/far blocks and routed `null` / `RejectedAuthority` /
`RejectedData` to `UnroutedCatchUp`
(`RuntimeRemoteFarSnapPosition.ResolveArm`, AP-137). **4b-3 keeps that policy
unchanged** — during the login window (null `_playerController`
classification refuses; every remote packet) remotes keep tracking through
AP-87's catch-up, exactly as today. One consequence to state in the register
rewrite: a TELEPORT_TS-advancing packet that arrives while classification is
null consumes its teleport sequence in the timestamp gate but runs no hook —
benign in the only producing window (fresh session: no moveto, stick, leash,
or target exists yet to tear down), stated rather than discovered later.
What T2 actually leaves 4b-3 is the player arm's legacy
`!update.IsGrounded` fallback (the block whose comment says "4b deletes this
fallback" — a comment this slice at last makes true), which handled
wire-airborne packets for classifications 4a does not own. After D1 the
unowned set shrinks to null and `Rejected*`. **Pinned replacement:** a
wire-airborne packet with a null/`Rejected*` classification takes the retail
return-0 shape applied to the acdream-only states — AP-135's two bookkeeping
writes, no body write, no queue write, no render write, no leash arm, return.
This deletes the legacy block's entity-revert quirk
(`entity.SetPosition(rmState.Body.Position)`) and unifies the player and NPC
arms on one leftover-airborne behaviour. Recorded in the AP-137 rewrite.
### D3 — one teleport-hook implementation, triggered by the route, run inside the teleport arm before the placement
The hook trigger moves from `timestamps.TeleportHookRequired` (deleted) to
the classification: `route.TeleportHookPhase ==
RuntimeTeleportHookPhase.BeforePositionOperation`, which the classifier
already emits on exactly the remote teleport/cell-less branch. The hook runs
inside the new teleport arm, immediately before
`TryExecuteAcceptedRemotePosition` — retail's order (hook @0x005163EF before
`SetPosition` @0x00516420), and it runs REGARDLESS of what the placement then
yields (retail runs it before knowing the outcome).
There must be exactly ONE hook implementation. The existing
`RemoteTeleportHook.Execute` sequence — the six actions in retail order with
a currency re-check between every step — is the port and must be preserved
verbatim; whether the file moves into `AcDream.Runtime` (every action is
expressible there: `remote.Movement.CancelMoveTo`,
`rmState.Host.PositionManager.UnStick/UnConstrain`, `remote.Interp.Clear()`,
`host.NotifyTeleported()`, `Physics.Engine.ShadowObjects.Suspend(localId)`)
or stays an App bundle invoked from the arm is the implementer's choice.
Runtime residence is preferred (it is where the sibling arm logic lives and
what a future headless remote-motion consumer needs), but not at the cost of
inventing a second hook path. `RemoteTeleportHookTests` (38 lines) moves or
adapts with it — not silently dropped.
**Undetermined item 2 is RESOLVED — yes, `EntityPhysicsHost.NotifyTeleported()`
covers retail's TargetManager pair.** Verified on both sides:
`NotifyTeleported` executes `_targetManager.ClearTarget()` then
`_targetManager.NotifyVoyeurOfEvent(TargetStatus.Teleported)`
(`src/AcDream.Runtime/Physics/EntityPhysicsHost.cs`), and retail's
`teleport_hook` executes `TargetManager::ClearTarget` @0x00514F1B then
`NotifyVoyeurOfEvent(Teleported_TargetStatus)` @0x00514F28 under one
`target_manager != 0` guard. One-to-one; no open question remains here.
### D4 — the single `ConstrainTo` arm, and its complete partition
The legacy pre-operation arming call (the `OwnsAfterOperationConstraint`-gated
fallback in the player/NPC shared section, whose comment already says "4b-3
deletes it") is DELETED. The post-operation site —
`RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation`, called once
per arm after routing — becomes the only arm, matching retail's single site
@0x00454272. Its predicate widens from
`OwnsSteadyState || OwnsFarSnap` to the full retail partition:
| classification (routed arm) | wire contact | retail return | arm? |
|---|---|---|---|
| teleport / cell-less (`SetPosition`, new arm) | any | 1 | **yes** — after the operation, on every placement outcome (retail discards the error), post-move anchor |
| `Interpolate` (near) | grounded | 1 | yes (unchanged, 4a) |
| `SetPositionSimple` (far) | grounded | 1 | yes (unchanged, 4b-2) |
| `NoPositionOperation` (airborne no-op) | airborne | 0 | **no** (unchanged, 4a) |
| null / `Rejected*``UnroutedCatchUp` | grounded | (no retail state; analog: nonzero) | yes — via the same post-operation site, which must therefore accept a null route for this case |
| null / `Rejected*`, wire-airborne (D2 shape) | airborne | (analog: 0) | **no** — the current legacy pre-op arm DOES arm these; that was a divergence and it retires with the site |
The one-packet unarmed residual on a superseded incarnation (the currency
guard returning before arming) is AP-138(3) and extends to the teleport arm
unchanged. This partition closes the shipped hole named in scoping
correction 2: a remote hard teleport currently arms the leash NOWHERE
(`remotePlacementRequired` returns ahead of every arming site after the
hook's `UnConstrain`); after this slice the hook `UnConstrain`s and the
single post-operation site re-arms — retail's exact sequence.
### D5 — routing order: the teleport arm precedes every contact carve-out; sticky does not suppress it
Retail decides the teleport branch before reading `arg4`. Therefore:
- `RuntimeRemoteFarSnapPosition.ResolveArm` (or its successor) returns the
new teleport arm ahead of everything else, and
`ApplyRemoteContactRouting` dispatches it BEFORE the `!remote.Body.InContact`
free-flight carve-out — an airborne-body teleport packet places, it does
not `AirborneSnap`.
- The player arm's landing block (`!rmState.Body.InContact` hard-snap +
return) must not claim a teleport-classified packet: the teleport
classification is routed before it (or the block is gated to classifications
it owns — implementer's choice, pinned outcome: a teleport-classified
packet always reaches the teleport arm regardless of wire or body contact).
- The 4a `IsAirborneNoOperation` early returns are classification-gated
already and cannot claim a `SetPosition` route — unchanged.
- **The NPC arm's TS-44 sticky suppression (`snapSuppressedByStick`) does not
suppress the teleport arm.** Retail's sticky cannot survive a teleport —
`UnStick` is the hook's second action. The suppression remains exactly as
it is for the near/far/leftover arms (its register row describes an
NPC-only steady-state gate, which stays true).
### D6 — the two per-packet gates widen to the teleport arm, same rule as the far arm
- `TryApplyGenericRemoteRenderPose`: the gate stays `OwnsSteadyState` — the
teleport arm (like the far arm) takes the early wire-pose write, and its
tail re-syncs the render entity from the resolved body (invariant 2). This
resolves the standing "route 4b-3 revisits the gate" comment: the answer is
"unchanged, now stated"; delete the forward reference.
- `TryAdoptWireCellAfterRouting`: the suppression (currently
`arm is FarSnapPlacement`) widens to the teleport arm, for the same reason —
after a canonical placement the placement is the cell authority; retail
resolves the destination cell through `AdjustPosition`/`set_cell` and
nothing writes the wire cell over it.
### D7 — the post-placement currency guard covers the teleport arm; consolidation is sanctioned
Both arms' re-validation ("the far arm is re-entrant — re-validate position
ownership on EVERY placement status before writing anything else, arming
included") widens to `Arm is FarSnapPlacement or <TeleportArm>`. Round 2's
"do not churn" explicitly named for 4b-3 that this guard "belongs behind a
testable Runtime seam rather than duplicated at two App call sites" — moving
the duplicated guard into the Runtime seam is sanctioned in this slice, but
only if the two arms' observable ordering (guard before arm, both arms
identical — the R5 invariant) is preserved and pinned by test.
### D8 — register bookkeeping, in the implementation commit
- **AP-137 is REWRITTEN, not deleted.** Its cell-less enqueue-vs-place delta
(part R2) retires — that is this slice's headline. But the row also records
the two surviving acdream-only states (null classification during the login
window; `RejectedData`/`RejectedAuthority` applied through
`UnroutedCatchUp`), which have no retail mechanism and therefore keep a
row. Rewrite the row to exactly the survivors plus D1's visibility-arm
deletion and D2's wire-airborne leftover shape. (The handoff says "row
deletion"; a deletion that silently dropped the surviving divergences would
violate register rule 1 — this contract overrides that wording, and the
summary reports the contradiction.)
- **AD-42 must be updated**: it cites
`src/AcDream.App/Physics/RemoteTeleportController.cs (ResolvePlacement)` as
a surviving two-call enter-world split path. That citation dies with the
class; the headless portal-arrival resync and
`PhysicsEngine.ResolvePlacement` citations remain.
- **AP-136 / AP-138 (D5 scoping text)**: both name "`RemoteTeleportController`'s
rollback" as the shipped writer that can rebucket `record.FullCellId` to a
third landblock under a retained retry. That writer is deleted; the
surviving non-Position rebucket writers are the projection materializer
(`DatLiveEntityProjectionMaterializer`) and the equipped-child renderer
(`EquippedChildRenderController.TickChild`). Update both rows and the same
claim inside `RuntimeSetPositionState`'s `CurrentCellId` doc and
`RuntimeRemotePlacementDriveController.CanAttemptDestination`'s doc
(process rule 6).
- **AP-138's Risk column gains the teleport arm as a second producer** of the
visible-without-collision residual: a remote that teleports into a
non-published landblock and stands still is exactly the AP-136/AP-138
shape, now reachable through this arm. No new machinery — the row's
retirement path is already #309.
- **AP-135 is untouched.**
## Deletion inventory — every file, wiring site, and test
Files deleted (739 production lines):
| file | lines |
|---|---|
| `src/AcDream.App/Physics/RemoteTeleportController.cs` | 605 |
| `src/AcDream.App/Physics/RemoteTeleportPlacement.cs` | 85 |
| `src/AcDream.App/Physics/RemoteShadowPlacementSynchronizer.cs` (contains BOTH `RemoteShadowPlacementSynchronizer` and `RemoteTeleportPlacementPresentation`) | 49 |
`src/AcDream.App/Physics/RemoteTeleportHook.cs` (57) is NOT deleted — it is
the retail `teleport_hook` port and moves/re-wires per D3.
Tests deleted (1,709 lines):
| file | lines |
|---|---|
| `tests/AcDream.App.Tests/Physics/RemoteTeleportControllerTests.cs` | 1,515 |
| `tests/AcDream.App.Tests/Physics/RemoteTeleportPlacementTests.cs` | 194 |
`tests/AcDream.App.Tests/Physics/RemoteTeleportHookTests.cs` (38) moves with
the hook.
Wiring sites (the handoff's list of eight was a raw grep; the true set is
below — two sites the handoff missed, and three of its entries are
comment-only):
| site | what happens |
|---|---|
| `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` | The heart of the slice. Delete: ctor param + `_remoteTeleportController` field; `remoteHardTeleport` / `remotePlacementRequired`; `RunRemoteTeleportHook` (moves per D3); the `BeginPlacement` call; the whole `remotePlacementRequired` placement block (the `TryApply` call and its `Applied`/`Superseded` tails); the classification gate's `&& !remotePlacementRequired`; the legacy `!update.IsGrounded` fallback (D2); the legacy pre-operation `ConstrainTo` call (D4). Add: the teleport arm dispatch (D5), widened guards (D6/D7). |
| `src/AcDream.App/Composition/LivePresentationComposition.cs` | Delete the `RemoteShadowPlacementSynchronizer` + `RemoteTeleportPlacementPresentation` constructions, the `remoteTeleportLease` acquisition, the `RemoteTeleport` record member, and the lease parameter threading. |
| `src/AcDream.App/Composition/SessionPlayerComposition.cs` | Three `live.RemoteTeleport` pass-throughs (network-update controller ctor, teardown controller ctor, and the third composition site) — replaced by nothing; the drive controller is already threaded. **Missing from the handoff's list.** |
| `src/AcDream.App/Net/LiveSessionRuntimeFactory.cs` | Delete the `RemoteTeleport` record member and the reset-plan binding `RemoteTeleport = _world.RemoteTeleport.Clear`. |
| `src/AcDream.App/Net/LiveSessionResetManifest.cs` | Delete the `required Action RemoteTeleport` member and its `new("remote teleport", …)` stage. **Missing from the handoff's list.** Reset coverage is not lost: the drive controller's teardown is `DetachRoute` (cancels every live operation) and its ledger convergence is already asserted. |
| `src/AcDream.App/Rendering/GameWindow.cs` | Delete the `_remoteTeleportController` field and its assignment from the composition result. |
| `src/AcDream.App/Rendering/GameWindowLifetime.cs` | Delete the `RemoteTeleportController? RemoteTeleport` shutdown-root member and the `Hard("remote teleport", …)` stage. |
| `src/AcDream.App/World/LiveEntityRuntimeTeardownController.cs` | Delete the ctor param, field, and the `_remoteTeleport.Forget(record)` cleanup entry. Per-entity teardown coverage is not lost: the drive controller self-heals on `IsPlacementCurrent` and `Forget`-on-accepted-Position retires operations. |
| `src/AcDream.Runtime/Physics/RuntimeRemotePhysicsUpdater.cs` | **Comment-only** (two doc comments citing `RemoteTeleportPlacement.Apply` — the isCurrent-delegate note and the five-writer `Airborne` list, which becomes a four-writer list; the teleport path's `Airborne` derivation is now the canonical placement commit's, already on the list). Correct both (process rule 6). |
| `src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs` | **Comment-only** (the `CurrentCellId` retained-retry doc naming `RemoteTeleportController`'s rollback — D8). No code change; there is no teleport-hook machinery in this file to touch. |
| `src/AcDream.Core/Physics/EntityCollisionFlags.cs` | **Comment-only** (TS-23 history note naming the pre-P3 inlined call sites). Historical statement — rewrite to past tense or leave verifiably historical; do not let it read as a live citation. |
| `src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs` | Widen `OwnsPlacement`'s scope comment (it ALREADY matches `SetPosition` + `Teleport` flag — no predicate change needed for the teleport disposition); add the teleport-arm entry point (D3); correct the `Advance()`/`CanAttemptDestination` docs' writer list (D8). |
| `src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs` + `src/AcDream.App/World/LiveEntityRuntime.cs` + `src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs` + `src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs` | Delete the `TeleportHookRequired` field, its `timestamps with {…}` computation, the `projectionRequiresTeleportHook` parameter/computation/`false` argument, and thread the pre-merge cell instead (D1). One doc comment in `InboundPhysicsStateController` cites `TeleportHookRequired`-adjacent bookkeeping — reword. |
| `src/AcDream.App/World/LiveEntityPresentationController.cs` | `BeginAuthoritativePlacement` and `CompleteAuthoritativePlacement(deferShadowRestore: false)` lose their only production callers (`DeferShadowRestore`, `HasActivePlacement`, `HasDeferredShadowRestore` already have none), leaving `_activePlacementOwners` write-never while `IsPlacementActive` still reads it in the visibility suspend/restore gates. Delete the dead half in this commit — do not leave a zombie set that silently gates nothing — but trace the `IsPlacementActive` consumers first and state in the commit what each gate degenerates to. If tracing shows a live non-teleport dependency, STOP and report rather than deleting blind. |
Test files updated (not deleted) — each references the deleted machinery
incidentally: `UpdateFrameOrchestratorTests` (a `typeof(RemoteTeleportPlacementPresentation)`
row), `RuntimeEntityOwnershipTests` (two `typeof(RemoteTeleportController)`
exact-key assertions), `GameWindowLiveEntityCompositionTests`
(`[InlineData("RunRemoteTeleportHook")]`), `LiveEntityLifecycleStressTests`
(constructs the controller and calls `TryApply` — its scenario must be
re-expressed against the canonical teleport arm, not dropped),
`LiveSessionResetPlanTests` (the "remote teleport" stage),
`CurrentGameRuntimeAdapterTests` (a noop binding),
`RuntimeInitialCreateResidenceStateTests`
(`RemoteTeleportSuffixIsQueuedBehindInitialAdmission` — verify what it pins;
it is about suffix ordering under initial admission and likely survives with
a rename), plus the eight files matching `TeleportHookRequired`.
Stale comments this slice must make true (process rule 6 — verify each
against the code beside it, and prefer symbol references): the
`// 4b deletes this fallback` line (D2 deletes the fallback); the
`// 4b-3 deletes it` on the legacy arm site (D4); the `remotePlacementRequired
guarantees the classifier's teleport disposition never reaches here` route
comment; `ClassifyRemoteAcceptedPosition`'s caller-doc sentence "whose
remotePlacementRequired gate is already false"; `SeedRemoteSpawnPlacement`'s
"Mirrors `RemoteTeleportPlacement`'s commit"; the AP-140 comment's citation of
`RemoteTeleportPlacementTests.Apply_PendingGroundToSteepContact_…` as an
`Airborne`-definition dependent (the test dies; the dependency list shrinks);
the classifier's "only a later Runtime SetPosition or simulation commit may
change FullCellId" (D1 shows it false); `TryApplyGenericRemoteRenderPose`'s
"Route 4b-3 revisits the gate" (D6 resolves it).
## Proof obligations (must prove, not assume)
1. **`ParkCollisionResidents`'s overlap throw stays unreachable.** The
argument is 4b-1/4b-2's, extended: every operation this route begins goes
through `TryBeginExclusiveAuthoredPlacement` (one live operation per key —
the Begin refuses a second), `DeferredCell` outcomes are cancelled
synchronously (no park survives the controller), and retained entries are
preparation retries bounded by the pre-flight re-check and the `Advance()`
window-drop. The teleport arm adds packets to the same machinery, not a
new operation shape. State this in the contract-conformance section of the
implementation commit and keep the honest caveat 4b-1's B2 established:
the guarded property is `TryAcquireCollisionPrefixMutationPermission`'s
`HasOldPrefixPlacementDebt` refusal (a stall, not a throw), and the
ledger convergence tests are the floor under it.
2. **Ledger convergence with teleports in flight**: teardown, session reset,
and generation change converge `RemotePlacementDrivePendingCount` (both
registrations) to zero with retained teleport retries present — the same
suite shape 4b-1 built, driven through the new arm.
3. **The one-arm partition (D4)**: a test that counts arming calls per packet
across the partition table's rows — exactly one for every "yes" row,
exactly zero for every "no" row. The 4b-2 lesson (R7): assert the
observable (the arm count / the anchor), not the code shape.
## What this slice does NOT do
- **AP-131** (shared merge call) — C5.
- **AP-135** — stays, writes preserved (invariant 8).
- **#276** — `SeedRemoteSpawnPlacement` is still not classification-gated;
the AD-61 settle is untouched.
- **#309 / AP-136 / AP-138 residuals** — the lost-cell/hidden-until-cell-load
behaviour is not built here; the teleport arm inherits the far arm's
store-and-stay-visible residual and the register rows say so (D8).
- **Route 5 (projectile)** — `OwnsPlacement` keeps excluding
`ProjectileAuthoritative`; no widening here.
- **No headless remote consumer** — `RuntimeLiveEntitySessionController`
still returns early for non-local GUIDs; the vacuous-satisfaction statement
stays in the interface doc and the AP-137 rewrite.
- **No changes to route 2's local-player ForcePosition/teleport paths**, the
local-player teleport transit (`LocalPlayerTeleportController` keeps its own
`NotifyTeleported` call), or route 1's executor.
- **The recorded-not-consumed route facts stay recorded-not-consumed**:
`UnparentBeforeRouting` / `ApplyPlacementFrameBeforeRouting` have no reader
today (the unparent edge is owned by the merge's `EndChildProjection` and
the hydration recovery); this slice adds no reader and does not delete the
facts.
## Test plan
Tests must assert the layer that broke historically (process rule 4 —
presentation/visibility, not only `InWorld`/clock/residency), and every new
test must fail against a broken implementation (no source-text pins, no
tautologies).
Focused Runtime tests (`tests/AcDream.Runtime.Tests`):
1. Teleport-classified commit: body at resolved destination, `FullCellId` =
resolved cell, hook ran (moveto cancelled, stick released, interp queue
empty, leash re-armed post-operation at the post-move anchor), clock
active, `InWorld`.
2. Teleport refused (destination outside the service window): pose STILL
advances to the accepted destination (invariant 1), no park opened, no
operation retained, entity `InWorld` + clock active + spatial root intact,
AND the hook still ran (retail runs it before the placement decision).
3. Teleport `RejectedByPlacement` (engine refused): pose does NOT move;
`Cancelled`-after-commit: settled pose survives. (The far arm's tests
exist; these drive the teleport arm through the same partition.)
4. Cell-less classification now fires: a record whose PRE-merge committed
cell is 0 (unwield-to-3D shape) classifies `SetPosition` and places
unconditionally — the AP-137-retiring behaviour. Companion: the same
packet with a nonzero pre-merge cell and no TELEPORT_TS advance does NOT
classify `SetPosition` (proves D1's input is the pre-merge value, not the
post-merge wire cell — this is the test that discriminates the fix from
the shipped dead predicate).
5. D4 partition: arm-count table test (proof obligation 3), including the
wire-airborne null/`Rejected*` no-arm rows and the
teleport-arm-on-every-outcome row.
6. D5 ordering: an airborne-body teleport packet places (does not
`AirborneSnap`, does not take the landing block); a stuck NPC's teleport
packet runs the hook (`UnStick`) and places despite TS-44's suppression.
7. Currency: teleport arm's synchronous receipt deletes/replaces the
incarnation → nothing further written for the packet (guard before arm,
both arms — the R5 shape, now for the teleport arm).
8. Ledger/teardown: proof obligation 2.
App-layer tests (`tests/AcDream.App.Tests`):
9. **The presentation assertion (#312's layer):** after a remote teleport
commit, the render `WorldEntity` pose equals the resolved body pose,
`ParentCellId` equals the resolved cell, the entity is spatially visible,
and the collision shadow was published. After a refused teleport, the
render pose tracks the stored destination and the entity REMAINS visible.
10. D2's leftover-airborne shape: wire-airborne null-classified packet writes
exactly AP-135's two fields and nothing else (body, entity, queue, leash
all untouched).
11. AP-135 preservation on the rewritten arms (both airborne no-op paths).
12. The generic render-pose + wire-cell-adoption gates: teleport arm takes
the early write and suppresses the post-routing wire-cell adopt (D6) —
asserted through the existing extracted entry points, not restated
logic.
Live-execution proof (process rule 5): a `[remote-teleport]` probe line —
`PhysicsDiagnostics`-owned, `ACDREAM_PROBE_REMOTE_TELEPORT=1`, one line per
routed teleport arm with guid, cause (`teleport-ts` vs `cellless`), hook-ran,
and placement status, marked TEMPORARY with the existing probe family. The
connected gate below is recorded as a pass ONLY if the probe line shows the
new arm executed (a clean-looking session with zero probe lines is a
not-run, exactly like #309's park probe).
## Gates
- Focused Runtime + App tests above.
- Complete Release suite: `$env:ACDREAM_PAK_PATH` set,
`dotnet test AcDream.slnx -c Release -m:1`. **Baseline 11,027 passed / 4
skipped / 0 failed** at `2eb39a02`. The net count will move (1,709 test
lines deleted, new tests added) — measure and record the new figure; do not
inherit 11,027 as the expectation. Two known flakes, do not chase and do
NOT conflate: **#302** (`PortalProjectionTests.ClipToRegion_FrameOwnedStore_…`,
GC-allocation assertion, App.Tests) and **#308**
(`NakEmissionTests.LossSoak_…`, wall-clock deadline, Core.Net.Tests,
full-suite load only). If either appears, re-run and say which.
- **Two-client connected teleport gate (user-run).** **CORRECTION (2026-08-04
fix round, both independent Opus reviews):** the recipe below originally
specified "a second character" as the teleport target. That is WRONG — a
player-character target cannot exercise this arm's NPC/creature code path
at all, and two of the three MAJOR defects the fix round found (A1's
zero-arm leash regression, A2/R3's synthesized-velocity/run-cycle defect)
are BOTH on the NPC-guid branch only; `RemoteServerControlledVelocityCycle.Apply`
itself early-returns for any `0x50xxxxxx` guid, so a player-target run
would report a clean pass while both defects shipped underneath it. **The
target MUST be an NPC/creature** (an ACE admin teleport — `@teleto` /
`@teleloc` — applied to a drudge/mosswart/etc., NOT a second player
character) for the gate to see what it is supposed to see. Recipe: acdream
stands as observer; the creature target is teleported with an ACE admin
teleport (`@teleto` / `@teleloc`) — those route through `Teleport()` /
`SendUpdatePosition(true)` and advance **ObjectTeleport** (TELEPORT_TS),
which is exactly this arm's trigger on the observer (`@pklite` is route
2's ForcePosition lever, NOT this gate — see
[`2026-08-03-c4-route-2-visual-gate.md`](2026-08-03-c4-route-2-visual-gate.md)).
Run with `ACDREAM_PROBE_REMOTE_TELEPORT=1`. Correct: the observed creature
vanishes from the old spot and appears at the destination in one step (no
glide, no interpolated streak), **stands STILL with correct idle
animation — NOT sprinting/running in place** (A2/R3's specific symptom:
a synthesized teleport-distance velocity planning a RunForward cycle at
the destination), and moves normally afterward (leash re-armed: no
rubber-band, no tether — A1's specific symptom is the ABSENCE of a
rubber-band/re-anchor where one should exist, since a creature that was
knocked airborne on its first accepted Position after this teleport would
otherwise never arm at all). Also teleport the remote OUT of view and
back: it must re-appear correctly. Regressions to watch: a remote gliding
across the map at teleport (queue not cleared), freezing at the old spot
(the 4b-2 round-1 freeze class), invisible-but-audible at the destination
(#312 class), invisible-but-solid (#184 class), a rubber-band after
arrival, or the creature sprinting/running in place at the destination
(A2/R3). Confirm at least one `[remote-teleport]` line per teleport, with
the expected cause. Graceful close (ACE session-clear) per the standing
rule.
## Budget
**~400-700 non-comment production lines** (net LOC strongly negative — the
deletions are 739 production + 1,709 test lines). For calibration: 4a was
364, 4b-1 was 230+57, 4b-2 landed within its 350-500. Exceed 700 new lines
and STOP and report rather than pushing through.
## Open questions routed to the retail-conformance reviewer
1. **D1's evidence chain** (the merge stamps `FullCellId` with the wire cell
before classification, making the shipped cell-less predicate dead) rests
on the reading `TryApplyPosition``RefreshSnapshot(refreshPosition:
acceptedPosition)``RefreshDerivedState``SetFullCell(wire cell)`,
with classification after the gate and before the prologue rebucket.
Verify independently — it is the load-bearing claim of this contract, and
if it is wrong the D1 plumbing is unnecessary churn.
2. **`report_collision_end(this, 1)``ShadowObjects.Suspend(localEntityId)`**:
the existing hook maps retail's collision-end report to a broadphase
shadow suspension. This mapping predates 4b-3 and is carried, not
re-derived. Confirm against @0x00514F31's callee that the "1" argument
(report-to-partners) has no unported half, or file the delta on the
AP-137 successor row.
3. **The `LiveEntityPresentationController._activePlacementOwners` deletion**
(deletion-inventory last row): confirm by reading `IsPlacementActive`'s
two consumers that removing the write-never set cannot change a
Hidden/UnHide or visibility-edge restore for non-teleport entities.
4. **`RemoteTeleportSuffixIsQueuedBehindInitialAdmission`**
(`RuntimeInitialCreateResidenceStateTests`): confirm what it pins and
that it survives (renamed) rather than being deleted as collateral.
## Contradictions with the handoff/scoping docs — reported, not smoothed over
- **The handoff's "AP-137 row deletion belongs in the implementation
commit"** conflicts with the row's own content: null and `Rejected*` are
acdream-only divergences that survive this slice and must keep a row.
Pinned as rewrite-in-place (D8). If the reviewer prefers
delete-and-refile-narrow, either satisfies register rule 1; silent whole-row
deletion does not.
- **The handoff's wiring-site list (8 sites) is a raw grep, not a wiring
list**: `SessionPlayerComposition.cs` and `LiveSessionResetManifest.cs` are
real wiring sites it missed; `RuntimeRemotePhysicsUpdater.cs`,
`RuntimeSetPositionState.cs`, and `EntityCollisionFlags.cs` are
comment-only.
- **The handoff's "`RuntimeSetPositionState.cs` — the teleport-hook phase
support and SetFlags analog"**: there is no teleport-hook phase support in
that file. The phase lives on the classifier route
(`RuntimeTeleportHookPhase`), and the SetFlags analog is the `flags`
parameter of `TryPrepareAndSubmitAuthoredPlacement` fed from
`route.SetPositionFlags`.
- **The scoping doc's T1 ("they disagree in both directions")** understates
the finding: as fed today the classifier's cell-less predicate is not
merely different — it is unreachable for remote PositionEvents (D1). The
design decision is therefore not "reconcile two predicates" but "feed the
classifier retail's predicate at all".
- **The scoping doc's T2** describes the pre-4b-2 tree; 4b-2 already
established the null/`Rejected*` policy. What 4b-3 actually decides is the
wire-airborne leftover shape (D2), which neither doc names.
---
## Connected gate RESULT — PASSED 2026-08-04 (user-run, user-accepted)
Run against the exact `6dc7ba51` Release binary with the retail UI
(`ACDREAM_RETAIL_UI=1`) and `ACDREAM_PROBE_REMOTE_TELEPORT=1`, live ACE at
`127.0.0.1:9000`. Log: `4b3-gate-retailui.log` (440 lines, graceful exit 0).
**User verdict: "all works"** — no glide/streak on arrival, correct standing
animation, normal movement afterward, no rubber-band or tether.
**Probe evidence (this is what makes it a pass rather than a clean-looking
session):** 16 `[remote-teleport]` lines across 7 distinct creatures, every one
`hookRan=True placement=Committed`. Every guid is in the `0x8xxxxxxx` creature
range — NOT `0x50xxxxxx` — so the run exercised the NPC-guid branch, which is
where all three of the fix round's NPC-arm MAJORs lived (A1's zero-arm leash
regression, R1's missing D2 shape, R3/A2's synthesized run-cycle velocity).
The corrected creature-target recipe is therefore validated in practice, not
just in argument.
**HONEST GAP — the cell-less half is NOT live-verified.** All 16 lines are
`cause=teleport-ts`; `cause=cellless` was never observed. So the arm's
TELEPORT_TS trigger is proven live and its cell-less trigger (retail's
`this_1->cell == 0` @0x00516386 — a body with no committed cell at all, e.g.
after an unwield-to-3D `SetFullCell(0,0)` or any canonical withdrawal) is
covered by tests only. This is deliberately recorded rather than folded into a
blanket "gate passed", because it is exactly 4b-2's #309 shape: that session's
11 park probes were all one cause, the other cause went unexercised, and
without the probe the session would have been recorded as full coverage.
**Superseded 2026-08-04 by C4 route 7 (see
`docs/research/2026-08-04-c4-route-7-contract.md` §11).** The recipe below —
"the unwield-to-3D path is the cheapest reachable trigger" — no longer fires.
Route 7 ported retail's parent-cell propagation (`docs/research/2026-08-04-retail-parent-cell-propagation.md`):
retail's `unset_parent` does no cell work, so a wielded child's unwield
Position reaches `MoveOrTeleport` with `this->cell` = the parent's cell —
NON-zero — and retail's cell-less branch never fires for unwield either. A
committed child's canonical `FullCellId` is now deterministically the
parent's (nonzero whenever the parent is celled), so an unwield Position
classifies by TELEPORT_TS/distance like any other packet, exactly matching
retail's predicate population. This is a correctness fix, not a regression:
the OLD recipe only worked because a parented child's canonical cell was
whatever the render-tick writer last produced — zero headless and zero in
any pre-first-tick window — which was itself the #184-class defect route 7
closed.
**To close the honest gap now:** provoke a Position on a body that is
genuinely withdrawn/never-celled at merge time — a body between
`CommitWithdrawal`/`CommitAcceptedParentCellless`'s cell-less edge and its
next accepted Position, or a body whose initial Create never resolved a
cell. Whether ACE ever emits an UpdatePosition in that exact window is
UNESTABLISHED; this needs its own investigation before a live recipe can be
written down. Test 4 in this contract's plan (`4b-3`, "unwield-to-3D shape
classifies `SetPosition`") remains a valid Runtime-level fixture — it
directly constructs a record with `PreMergeCommittedCellId == 0` — but it is
a SYNTHETIC pre-merge-cell-0 fixture, not a live unwield behavior, and must
not be re-labeled as one.