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 atcff52c44, +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>
48 KiB
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.
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 →
round 2 (delta) →
round 3 →
round 4.
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:
- The teleport branch is decided BEFORE the contact test (
arg4is 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). this_1->cell == 0is 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.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.SetFlags(0x1012)(Teleport|Slide|SendPositionEvent) @0x00516414 — acdream's analog is the classifier'sAuthoritativeTeleportFlags, carried onroute.SetPositionFlagsintoRuntimeSetPositionState.TryPrepareAndSubmitAuthoredPlacement'sflagsparameter. (There is NO separate "teleport hook phase support" insideRuntimeSetPositionState— 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.)- The branch returns 1 and discards
SetPosition's error, soSmartBox::HandleReceivedPosition@0x00453FD0 armsConstrainTo@0x00454272 — the remote arm's ONLY arming site, shared by all three nonzero-returning branches, anchored&arg2->m_positionread live (post-move). Retail arms even when the placement failed. So: hookUnConstrainfirst, 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:
- 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 sameStoresAcceptedDestinationpartition and the sameStoreAcceptedDestinationPosethe far arm uses, which is retail'sstore_position@0x00515CE2 on the no-transition branch.DeferredandRejectedByPlacementdo NOT store, for the reasons already pinned in the enum's own doc (ParkDeferredalready snapped; the engine ran and refused / a settled pose must survive). Do not re-litigate the partition — extend it to the teleport arm unchanged. - 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). - The object clock keeps running and the entity stays in the world on
every non-commit outcome:
body.InWorld,TransientStateFlags.Active,record.ObjectClockactive,FullCellId != 0, spatial projection intact,IsSpatiallyVisibleunchanged. No park survives the controller (CancelTokenwithrestoreCancelledPark: true); the pre-flight (CanAttemptDestination) stays an OPTIMISATION, never the reason a remote stops tracking or becomes invisible. - 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 carriesStopInterpolating: falsebecause retail's clear lives inside the hook. The far arm's route-flag-driven clear is untouched. - 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:
remotePlacementRequiredreturns ahead of every arming site whileRemoteTeleportHookhas alreadyUnConstrained, so a remote that teleports and stands still is leash-less until the next packet), and never twice. See D4 for the complete partition. - The per-packet prologue keeps running for every classification:
TryApplyGenericRemoteRenderPose(see D6 for the gate), theRebucketLiveEntity(update.Guid, p.LandblockId)spatial-bucket transaction, the velocity install (TryCommitAuthoritativeVelocity— retail's PositionPackset_velocity, upstream ofMoveOrTeleport), and the incarnation re-validation chain. Retail's remote teleport never writes velocity itself (ZeroVelocityis the LOCAL player'sSmartBox::TeleportPlayer@0x004541B4 only); the teleport arm must not add a velocity write. AcceptedPositionSource/authority validation is unchanged. The teleport arm executes only an already-classified route fromClassifyRemoteAcceptedPosition— the shared builder (RuntimeAcceptedPositionRouteRequests), never a re-derivation.- AP-135's two writes stay, on both arms' airborne no-op
neighbourhoods: the server-cell adopt (
rmState.CellId = p.LandblockId) and theLastServerPos/LastServerPosTimesample. They are 4a-owned acdream-only bookkeeping for the free-fall sweep gate (RuntimeRemotePhysicsUpdater'srm.CellId != 0gate) 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. ParkCollisionResidents's overlap throw stays unreachable — see "Proof obligations".ArmLostFamilyDeadlines' reaper gains no production caller.TickLostCellDeadlinesandTryDequeueExpiredLostCell(RuntimeSetPositionState) have zero production callers today; this slice must not add one.ParkDeferredarming the family (itsArmLostFamilyDeadlinescall) is pre-existing and stays inert.- 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"). - The 4a dispositions are untouched:
NoPositionOperationwrites nothing (plus item 8's bookkeeping),Interpolateenqueues, 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. - 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
StoresAcceptedDestinationpartition, theIsRestorableQuiescenceParkrelocation, 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 UnConstrains 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, andApplyRemoteContactRoutingdispatches it BEFORE the!remote.Body.InContactfree-flight carve-out — an airborne-body teleport packet places, it does notAirborneSnap.- The player arm's landing block (
!rmState.Body.InContacthard-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
IsAirborneNoOperationearly returns are classification-gated already and cannot claim aSetPositionroute — unchanged. - The NPC arm's TS-44 sticky suppression (
snapSuppressedByStick) does not suppress the teleport arm. Retail's sticky cannot survive a teleport —UnStickis 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 staysOwnsSteadyState— 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 (currentlyarm 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 throughAdjustPosition/set_celland 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/RejectedAuthorityapplied throughUnroutedCatchUp), 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 andPhysicsEngine.ResolvePlacementcitations remain. - AP-136 / AP-138 (D5 scoping text): both name "
RemoteTeleportController's rollback" as the shipped writer that can rebucketrecord.FullCellIdto 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 insideRuntimeSetPositionState'sCurrentCellIddoc andRuntimeRemotePlacementDriveController.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)
ParkCollisionResidents's overlap throw stays unreachable. The argument is 4b-1/4b-2's, extended: every operation this route begins goes throughTryBeginExclusiveAuthoredPlacement(one live operation per key — the Begin refuses a second),DeferredCelloutcomes are cancelled synchronously (no park survives the controller), and retained entries are preparation retries bounded by the pre-flight re-check and theAdvance()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 isTryAcquireCollisionPrefixMutationPermission'sHasOldPrefixPlacementDebtrefusal (a stall, not a throw), and the ledger convergence tests are the floor under it.- 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. - 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 —
SeedRemoteSpawnPlacementis 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) —
OwnsPlacementkeeps excludingProjectileAuthoritative; no widening here. - No headless remote consumer —
RuntimeLiveEntitySessionControllerstill 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 (
LocalPlayerTeleportControllerkeeps its ownNotifyTeleportedcall), or route 1's executor. - The recorded-not-consumed route facts stay recorded-not-consumed:
UnparentBeforeRouting/ApplyPlacementFrameBeforeRoutinghave no reader today (the unparent edge is owned by the merge'sEndChildProjectionand 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):
- 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. - 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). - 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.) - Cell-less classification now fires: a record whose PRE-merge committed
cell is 0 (unwield-to-3D shape) classifies
SetPositionand places unconditionally — the AP-137-retiring behaviour. Companion: the same packet with a nonzero pre-merge cell and no TELEPORT_TS advance does NOT classifySetPosition(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). - 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. - 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. - 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).
- Ledger/teardown: proof obligation 2.
App-layer tests (tests/AcDream.App.Tests):
- The presentation assertion (#312's layer): after a remote teleport
commit, the render
WorldEntitypose equals the resolved body pose,ParentCellIdequals 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. - 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).
- AP-135 preservation on the rewritten arms (both airborne no-op paths).
- 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_PATHset,dotnet test AcDream.slnx -c Release -m:1. Baseline 11,027 passed / 4 skipped / 0 failed at2eb39a02. 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.Applyitself early-returns for any0x50xxxxxxguid, 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 throughTeleport()/SendUpdatePosition(true)and advance ObjectTeleport (TELEPORT_TS), which is exactly this arm's trigger on the observer (@pkliteis route 2's ForcePosition lever, NOT this gate — see2026-08-03-c4-route-2-visual-gate.md). Run withACDREAM_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
- D1's evidence chain (the merge stamps
FullCellIdwith the wire cell before classification, making the shipped cell-less predicate dead) rests on the readingTryApplyPosition→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. 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.- The
LiveEntityPresentationController._activePlacementOwnersdeletion (deletion-inventory last row): confirm by readingIsPlacementActive's two consumers that removing the write-never set cannot change a Hidden/UnHide or visibility-edge restore for non-teleport entities. 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.csandLiveSessionResetManifest.csare real wiring sites it missed;RuntimeRemotePhysicsUpdater.cs,RuntimeSetPositionState.cs, andEntityCollisionFlags.csare 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 theflagsparameter ofTryPrepareAndSubmitAuthoredPlacementfed fromroute.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.