acdream/docs/research/2026-08-04-c4-routes-6-7-scoping.md
Erik 11a8742884 docs: scope C4 routes 6 and 7 — route 6 needs zero production lines
Scoped together because both concern item/container placement; they turn out to
share no production file, but the shared inventory premise had to be corrected
once rather than twice.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 04:27:13 +02:00

672 lines
35 KiB
Markdown

# C4 routes 6 and 7 — scoping (2026-08-04)
Read-only research. Worktree
`C:\Users\erikn\source\repos\acdream\.claude\worktrees\peaceful-visvesvaraya-e0a196`,
branch `claude/acdream-physics-divergence-5aa784`, HEAD `d5bdc355`.
Scoped together deliberately: the campaign groups route 6 (drops +
split-recovery marking) and route 7 (residual pickup/parent/delete polish) as
adjacent item/container work. **They turn out not to share a single production
file.** The reason to keep them in one document is different from the reason
they were paired: both routes' premises in
[`2026-08-02-cutover-route-inventory.md`](2026-08-02-cutover-route-inventory.md)
were written **before C3c flipped both hosts' Create paths onto the residence
lease**, and both are now substantially or entirely satisfied by that flip.
Splitting the scoping would have produced two documents each re-deriving the
same C3c correction.
**Headline: route 6 requires no production change. Route 7 has exactly one
real defect, and it is a two-writer split on the child's canonical cell —
which is also the reason headless has no parent-realize sequence at all.**
---
## 0. The correction that governs both routes
The inventory's route-6 and route-7 sections both rest on this claim
(`2026-08-02-cutover-route-inventory.md:759-767`, repeated at `:806-824`):
> `RegisterEntityWithInitialResidence` is never called from any production
> path (confirmed by full-repo grep …). Both drop flavors equally bypass the
> canonical create-placement transaction.
**That is no longer true.** C3c (`529e0e9d`) flipped both hosts:
- Graphical: `src/AcDream.App/World/LiveEntityRuntime.cs:544-548`
`_entityObjects.RegisterEntityWithInitialResidence(incoming, isLocalPlayer,
RetirePriorProjection)`, with the C3c rationale comment at `:538-543`.
- Direct/headless: `src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs:118-123`
— the same call, except for the deliberately-excluded content-less host
(comment `:108-117`).
Route 7's inventory paragraph "these cancellation calls are structurally
present but functionally no-ops against always-empty state" is likewise
obsolete: `InitialCreateResidences` is populated in production now, so every
`ForgetInitialCreateResidence` in the pickup/parent/delete family is live.
Anything in the inventory's routes 6/7 sections that reasons from
"nothing upstream to cancel" must be re-derived, not cited.
---
# ROUTE 6 — drops and unparent-to-world
## 6.1 What the route actually is
**There is no route-6 classifier disposition and no route-6
`RuntimePositionEntityKind`.** A dropped item is an ordinary non-local
CreateObject:
- `RuntimePositionEntityKind.Remote`
(`src/AcDream.Runtime/Physics/RuntimeAuthoritativePositionRouteClassifier.cs:8-14`).
- `RuntimeCreateResidenceKind.TopLevel`, produced at
`src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs:603-606`
(`Parented` when the wire frame carries a parent, `PickedUp` when it has
neither parent nor position).
- `ClassifyCreate` (`RuntimeAuthoritativePositionRouteClassifier.cs:205-275`)
returns disposition `SetPosition` with
`InitialCreateFlags = Placement | Slide` (`:168-170`, applied `:254-258`).
That is byte-for-byte route 1's create classification. Route 6 is a *source*
of route-1 traffic, not a route of its own.
## 6.2 Both drop flavors already converge on the canonical transaction
**Whole-item drop.** `ItemInteractionController.ExecutePlacementActions`,
`DropToWorld` case
(`src/AcDream.App/UI/ItemInteractionController.cs:974-991`): optimistic
`MoveItemOptimistic(objectId, newContainerId: 0u, newSlot: -1)` then
`_sendDrop(objectId)`. No physics, no position. The server's CreateObject
comes back through the ordinary wire pump into
`LiveEntityHydrationController.OnCreate``_runtime.RegisterLiveEntity`
`RegisterEntityWithInitialResidence` (`LiveEntityRuntime.cs:544-548`).
**Split-to-world.** Same file `:992-1013` (`SplitToWorld` case) raises
`WorldDropDispatched`; `InventoryWorldDropProjectionController.OnWorldDropDispatched`
(`src/AcDream.App/World/InventoryWorldDropProjectionController.cs:81-98`)
records the pending identity; the new GUID's F748 Position is recovered by
`TryRecoverUnknownPosition` (`:54-68`), which calls **`_hydration.OnCreate(spawn)`
at `:66` — the identical entry point**. Same residence lease, same conductor,
same placement.
**Therefore the campaign handoff's route-6 requirement
(`2026-07-31-remaining-physics-campaign-handoff.md:357-362`) —
"`TryRecoverUnknownPosition` may create the logical object, but it must enter
the same canonical create-placement transaction" — is already met, by C3c, with
zero route-6-specific code.**
## 6.3 "Do not expose a stale source position" — satisfied
`PendingSplitToWorldProjection.BuildSpawn`
(`InventoryWorldDropProjectionController.cs:171-209`) overrides every
positional field from the arriving update:
| Field | Line |
|---|---|
| `Physics.Position` | `:179` |
| `Physics.Parent = null` | `:180` |
| `Physics.Velocity` | `:181` |
| `Physics.Timestamps.{Position,Teleport,ForcePosition,Instance}` | `:182-188` |
| DTO `Position` | `:194` |
| DTO `ContainerId = 0`, `WielderId = 0`, `CurrentWieldedLocation = 0` | `:197-199` |
| DTO `{Instance,Position}Sequence`, `ParentGuid = null`, `ParentLocation = null`, `PlacementId` | `:200-206` |
No source pose survives into the synthetic spawn.
## 6.4 "Do not replay create-time effects" — NOT ESTABLISHED as a defect
The inventory calls this "a genuine new capability, not merely dormant"
(`2026-08-02-cutover-route-inventory.md:768-775`). I could not find a reachable
defect behind it, and the retail decomp says there should not be one.
**acdream's only create-time effect replay is the F754/F755 network queue
drain, keyed by server GUID.** `LiveEntityReadyPublisher.Publish` calls
`effects.ReplayPendingForLiveEntity(record.ServerGuid)`
(`src/AcDream.App/World/LiveEntityHydrationPorts.cs:97`, body at
`src/AcDream.App/Rendering/Vfx/EntityEffectController.cs:199-205`). That queue is
written only by `HandleDirect`/`HandleTyped`
(`EntityEffectController.cs:92-126`) from inbound `PlayPhysicsScript` /
`PlayPhysicsScriptType` for that exact GUID. A fresh split GUID has nothing
queued unless ACE actually sent an effect for it — and draining it then is
retail's own behaviour, which the class documents and ports:
`SmartBox::HandlePlayScriptID @ 0x00452020` / `HandlePlayScriptType
@ 0x00452070` queue while the object is absent, and
`SmartBox::HandleCreateObject @ 0x00454C80` drains through
`ProcessObjectNetBlobs` (cited `EntityEffectController.cs:17-24, :37-45`).
**The one plausible mechanism — a cloned default script — does not fire at
create in either client.** `BuildSpawn` does clone `DefaultScriptType` /
`DefaultScriptIntensity` from the source (they are not in the override list;
fields at `src/AcDream.Core.Net/Messages/PhysicsSpawnData.cs:58-59`), and they
reach `EntityEffectProfile.RawDefaultScriptType`
(`src/AcDream.App/Rendering/Vfx/EntityEffectProfile.cs:67`). But acdream's
`PlayDefault` (`EntityEffectController.cs:409-418`) has exactly two callers,
both animation hooks: `DefaultScriptHook` and `DefaultScriptPartHook`
(`:428-435`). Retail matches: `CPhysicsObj::play_default_script @ 0x005132B0`
/ `@ 0x00513300` is reached only from `ACCWeenieObject::DoCollision
@ 0x0058C3A0` (call @0x0058C3B4) and the animation-hook dispatcher
(@0x00526C08, @0x00526C14). **Neither client plays a default script from
`set_description` or CreateObject.**
**Live evidence that would settle it if someone still doubts:** split a stack
of a WCID whose PhysicsDesc carries a nonzero `DefaultScriptType` and watch for
a script at the drop. Per the two call-site sets above there is none to
observe, so this is a confirmation, not an open question.
## 6.5 Retail truth for drops and for "split-recovery marking"
**Whole-item drop.** `ACCWeenieObject::UIAttemptPutIn3D @ 0x0058D700`: gate on
`IsPlayerReadyToMakeInventoryRequest` @0x0058D70F, send
`CM_Inventory::Event_DropItem` @0x0058D715, then record only
`prevRequest = IR_MOVE` (@0x0058D74F) or `IR_DROP` (@0x0058D731). **No
placement call, no split marker, no client-side position.** The dropped object
re-enters the world through the ordinary CreateObject path.
**Split.** `ACCWeenieObject::UIAttemptSplitTo3D @ 0x0058D850` sends
`Event_StackableSplitTo3D` @0x0058D86B and then records exactly three fields:
`splitStackSize = arg2` @0x0058D8A2, `splitClassID = this->pwd._wcid`
@0x0058D8A8, `splitTime = cur_time` @0x0058D8AE.
`ACCWeenieObject::UIAttemptSplitToContainer @ 0x0058D7D0` records the
**identical three fields** at @0x0058D82D / @0x0058D833 / @0x0058D838.
**The marker's consumer is `ACCWeenieObject::DeclareValid @ 0x0058E340`:**
```
splitClassID read @0x0058E44A
if (splitClassID != INVALID) {
if (splitClassID == this->pwd._wcid) @0x0058E462
stack = this->pwd._stackSize; if (0) 1 @0x0058E464-@0x0058E46E
if (splitStackSize == stack) { @0x0058E479
ACCWeenieObject::SetSelectedObject(this->id, 0); @0x0058E481
splitClassID = INVALID; return; @0x0058E490
}
if (cur_time - splitTime >= 10.0) @0x0058E49F-@0x0058E4B0
splitClassID = INVALID; @0x0058E4B2
}
```
**So retail's "split-recovery marking" is (i) a WCID + stack-size + 10-second
identity recorded for BOTH split flavors, and (ii) a recovery action that is a
SELECTION TRANSFER — `SetSelectedObject` — not effect suppression and not
anything placement-related.**
acdream implements (i) partially and (ii) not at all:
- The 10-second window is exact: `PendingSplitToWorldProjection.RetailRecognitionSeconds = 10.0`
(`InventoryWorldDropProjectionController.cs:113`), enforced `:146-153`.
- The match is **"any unknown GUID that is not the source"** (`:155-156`), not
WCID + stack size, because ACE's F748 carries neither. This is the
already-registered **AP-124**
(`docs/architecture/retail-divergence-register.md:274`), which cites both
retail symbols correctly.
- acdream records the marker only for `InventoryRequestKind.SplitToWorld`
(`:86`); retail records it for split-to-container too.
- No `SetSelectedObject` equivalent runs. The controller's constructor
(`:33-52`) takes interaction / objects / runtime / hydration / clock and has
no selection dependency at all.
## 6.6 Route 6 — what remains
**Nothing that C4 must do.** The three genuine residuals are:
| # | Item | Kind |
|---|---|---|
| R6-a | `DeclareValid`'s `SetSelectedObject(this->id, 0)` @0x0058E481 is not ported — the split result does not become the selected object, and the container-split flavor has no marker at all | UI/selection behaviour, **not placement**; file as an issue, out of C4 scope |
| R6-b | AP-124's WCID/count approximation | already registered; retiring it needs ACE to send CreateObject to the initiator, not a client change |
| R6-c | `BuildSpawn` clones `Children`, `Movement`, `AnimationFrame`, `SetupTableId` etc. wholesale from the source | **not established** as a defect: a stackable inventory item's `Children` should be empty and its `Movement` inert. Settled by dumping a source spawn's `PhysicsSpawnData` at a live split — one `ACDREAM_DUMP_*`-style probe, or an assertion in the new tests |
**Recommended route-6 landing: evidence + tests only, no production diff.**
Add the coverage the campaign handoff's route-6 test list names
(`2026-07-31-remaining-physics-campaign-handoff.md:363-365`) against the
now-flipped path: whole item, split stack, new GUID, second drop position,
unavailable destination, newer Position while waiting. Note that "attached
child becoming a world root" from that list is **not route 6's** — it is a
cell-less Position on an existing entity, owned by 4b-3.
## 6.7 Route 6 — line budget
- **Production: 0 lines** for the campaign mandate. If R6-a is adopted inside
C4 (I recommend against it — it is selection UX, and mixing it in makes the
landing un-reviewable against a placement contract), **40-80** lines.
- **Tests: 150-250 lines.** Route 4a was 364 production lines; this is an
order of magnitude below the threshold where splitting is a question.
## 6.8 Route 6 — connected gate
Cheap and directly visible.
1. Stand on open ground in Holtburg. Drop a whole item from inventory (drag to
3D or the drop action). It must appear **at your feet**, resting on the
ground, immediately, and be pickable.
2. Split a partial stack of a stackable (e.g. pyreals) to the ground. Same
result, with the correct quantity on the new pile and the remainder still
in inventory.
3. Drop a second item within ~1 m of the first. Both must remain visible and
separately pickable.
4. Repeat once indoors (an inn interior) and once immediately after a portal
recall.
5. Walk two landblocks away and back. Both piles must still be there and still
pickable.
**Regressions to watch for:** the item appears at the world origin or at your
*previous* position (stale pose); the item is invisible but blocks you
(invisible-but-solid, the #184 class); the item sinks into or floats above the
floor; the item cannot be picked up; the split pile never appears at all
(the recovery window failed); the second drop is swallowed by the first.
---
# ROUTE 7 — pickup, parent, and delete
## 7.1 What the route actually is
**Dispositions.** Route 7's classifier surface is
`RuntimeAuthoritativePositionRouteClassifier.ClassifyLeaveWorld`
(`:475-505`), taking a `RuntimeLeaveWorldRouteRequest` (`:141-145`) whose
`RuntimeLeaveWorldCause` is `Pickup` or `Parent` (`:31-36`). It returns
disposition `AwaitFreshPosition` with `LeaveWorld: true`,
`SetPositionFlags.None`, `ConstrainPhase.None`, and no teleport hook.
The create-side siblings are `RuntimeCreateResidenceKind.Parented` and
`PickedUp`, which `ClassifyCreate` routes to the same `AwaitFreshPosition` /
no-SetPosition branch (`:219-241`), with the comment at `:230-233` stating why
initial residence invents no withdrawal edge.
**`ClassifyLeaveWorld` has ZERO production callers.** Repo-wide grep returns
its own definition and exactly one test
(`tests/AcDream.Runtime.Tests/Physics/RuntimeAuthoritativePositionRouteClassifierTests.cs:341-342`),
plus a comment in `RuntimeInitialCreateContinuationExecutorTests.cs:1672`
noting that `ApplyPositionAction` does not call it. Every production pickup /
parent / delete performs its leave-world work by hard-coded sequence inside
`RuntimeEntityObjectLifetime`, never through the classifier.
**Entity kind** is whatever the object is — `Remote` for items, and the
classifier's `OperationKind(kind, initialCreate: false)` derives from it.
## 7.2 Retail truth: route 7 performs NO placement
This is the single most important fact for scoping route 7, and it inverts the
trap list that governed routes 2 / 4a / 4b.
**Pickup — `SmartBox::DoPickupEvent @ 0x00452240`:**
```
gate on obj->update_times[0] (the POSITION stamp) @0x0045224B-@0x00452274
obj->update_times[0] = incoming @0x00452278
CPhysicsObj::unset_parent(obj); @0x0045227F
CPhysicsObj::leave_world(obj); @0x00452286
```
No `SetPosition`, no `MoveOrTeleport`, no `ConstrainTo`, no placement flags.
**Parent — `SmartBox::DoParentEvent @ 0x00452290`:**
```
gate on child->update_times[0] @0x00452296-@0x004522C5
if (child had no parent && parentId != player)
parentWeenie->SetParentedState(1); @0x004522F4
CPhysicsObj::set_parent(child, parent, loc); @0x00452305
CPhysicsObj::SetPlacementFrame(child, arg5, 1); @0x00452313
```
**`CPhysicsObj::set_parent @ 0x00515A90` (3-arg):**
```
if (parent != 0 && add_child(parent, this, loc) != 0) { @0x00515A9D
unset_parent(this); @0x00515ABA
leave_world(this); @0x00515AC1
this->parent = parent; @0x00515AC6
if (parent->cell != 0) { @0x00515AD1
change_cell(this, parent->cell); @0x00515AD6
UpdateChild(parent, this, part_number, frame); @0x00515B0E
recalc_cross_cells(this); @0x00515B15
}
if (parent->state & NODRAW-bit) { @0x00515B26
this->state |= 0x20;
part_array->SetNoDrawInternal(1); @0x00515B38
}
return 1; @0x00515B45
}
return 0; @0x00515B4D
```
The 4-arg overload @0x00515B50 is the same shape, minus the `CHILDLIST`
index lookup and plus `m_bExaminationObject` inheritance @0x00515B76.
**`CPhysicsObj::unset_parent @ 0x00513470`:** `CHILDLIST::remove_child`
@0x00513484 → NoDraw restore @0x00513495-@0x005134A7`parent = null`
@0x005134AC`update_time = cur_time` @0x005134BF → tailcall
`clear_transient_states` @0x005134CE.
**Conclusion.** Route 7's entire mandate in this campaign is the one the
campaign handoff already states
(`2026-07-31-remaining-physics-campaign-handoff.md:367-373`): pickup / parent /
delete must **cancel** the exact active placement / lost-cell family first and
publish `Discard`/`Withdraw` before the later entity delta. There is no
placement to port, no leash, no distance threshold, no flags.
## 7.3 What is already canonical (verified, not assumed)
**Every cancellation choke point exists and is live.** Six sites in
`src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs`, each running the
identical `ForgetInitialCreateResidence``Physics.SetPosition.Forget`
`PreferCancellation` sequence and threading the receipt into
`AcknowledgeProjectionAndPublish`:
| Method | Cancellation lines |
|---|---|
| `CommitPositionChannelUpdate` | `:1074-1094` |
| `TryApplyPickup` | `:1226-1245` |
| `TryCommitParent` | `:1360-1374` (added at C0; rationale `:1344-1359`) |
| `CommitAcceptedParentCellless` | `:1390-1407` |
| `CommitWithdrawal` | `:1845-1860` (added at C0; rationale `:1837-1844`) |
| `TryAcceptDelete` | `:1939-1952` |
`ForgetInitialCreateResidence` is at `:2552-2569`; `PreferCancellation`
(initial wins over ordinary) at `:2571-2574`.
**The two C0 asymmetries the inventory flagged (`:826-837`) are FIXED.**
`TryCommitParent` had no choke point at all; `CommitWithdrawal` cancelled only
the residence family. Both now cancel both families, and both carry the
explanatory comment naming the C0 finding. `TryCommitParent`'s deliberate
omission of `Physics.CollisionReports.LeaveWorld` is documented `:1352-1359`
against `set_parent @ 0x00515A90`'s single gated `leave_world` @0x00515AC1
**do not "fix" this; the campaign contract already pins it.**
**Ordering is correct.** `AcknowledgeProjectionAndPublish` (`:2187-2213`) runs
`Physics.SetPosition.PublishCancellation(cancellation)` as its **first**
statement at `:2194`, before the currency re-check, before the host
acknowledgement callback, and before `PublishEntity`. That is exactly the
handoff's "publish `Discard`/`Withdraw` before the later entity/inventory
delta."
**Cancellation receipts ARE host-visible.**
`RuntimeSetPositionState.PublishCancellation` (`:5296-5309`) republishes the
retained `Discard`-kind `RuntimePlacementProjectionSnapshot` through
`PublishPlacement`, i.e. through the same one placement stream. Both hosts
consume the kind: `src/AcDream.App/World/RuntimePlacementPresentationSink.cs:102`,
`src/AcDream.App/World/LiveEntityRuntime.cs:1179`, and
`src/AcDream.Headless/Hosting/HeadlessRuntimePlacementProjectionSink.cs:27`.
**The C0 note "route-7 needs … host-visible cancellation receipts" is
satisfied.**
**Pickup/parent during a pending residence already defer.** `TryApplyPickup`
`:1189-1215` enqueues a dormant `RuntimeInitialCreateContinuationKind.Pickup`
continuation when `TryGetPendingInitialResidence` hits; `TryApplyParent` and
`TryApplyMotion` have the same shape.
**App and headless carry no duplicate placement authority for route 7.**
`LiveEntityDeletionController` (102 lines) is purely logical.
`LiveEntityHydrationController.OnPickup` (`:455-474`) calls
`_runtime.TryApplyPickup` then `_relationships.OnChildBecameUnparented`.
Headless `OnPickedUp` / `OnDeleted` / `OnParentUpdated`
(`RuntimeLiveEntitySessionController.cs:176-180, :155-174, :313-317`) are thin
pass-throughs. Grep for `SnapToCell` / `CommitRebucket` / `SuspendObjectClock`
/ `SetFullCell` across `src/AcDream.App` and `src/AcDream.Headless` returns no
pickup/parent/delete site.
## 7.4 The one real defect — the child's canonical cell has two writers
This is route 7's whole substance.
**Retail** puts the child in the parent's cell **inside `set_parent`**:
`change_cell(this, parent->cell)` @0x00515AD6 followed by
`recalc_cross_cells(this)` @0x00515B15, gated on `parent->cell != 0`
@0x00515AD1.
**acdream splits that in half across two assemblies:**
1. Runtime commits the child **cell-less, unconditionally**.
`RuntimeEntityObjectLifetime.CommitAcceptedParentCellless` (`:1377-1408`)
runs `Physics.CollisionReports.LeaveWorld` `:1396`,
`Entities.SuspendObjectClock` `:1397`, `Entities.SetFullCell(canonical, 0u,
0u)` `:1398`. Its App wrapper's doc comment
(`LiveEntityRuntime.cs:2288-2292`) claims it "commits retail
`CPhysicsObj::set_parent`'s cell-less edge" — accurate only for the
`parent->cell == 0` case; retail's `parent->cell != 0` case immediately
re-cells the child and acdream never performs that half in Runtime.
2. **App re-cells the child from a presentation tick.**
`EquippedChildRenderController.TickChild` `:405-408` calls
`_liveEntities.RebucketLiveEntity(child.ChildGuid, parentCellId)`, and the
public `LiveEntityRuntime.RebucketLiveEntity` (`:801-977`) is *not*
presentation-only — it writes the canonical cell through
`_entityObjects.CommitRebucket(record.Canonical, committedFullCell,
committedLandblock)` at `:904-907`.
So the canonical `FullCellId` of every equipped child is produced by a
per-frame render-composition tick, not by the parent commit. Two consequences,
and they are the same bug seen from two sides:
- **Headless leaves every parented child cell-less forever.**
`RuntimeLiveEntitySessionController.OnParentUpdated` (`:313-317`) calls only
`Entities.TryApplyParent`. Neither `TryCommitParent` nor
`CommitAcceptedParentCellless` has any headless caller — repo grep finds only
`EquippedChildRenderController.cs:869` and the App wrappers at
`LiveEntityRuntime.cs:2280-2312`. There is no headless
`EquippedChildRenderController`, so nothing ever supplies step 2. The
inventory called this a "headless-specific gap, pre-existing, adjacent"
(`:839-853`) — it is not adjacent, it is the *same* missing commit.
- **The two-store shape is exactly the failure class this campaign exists to
remove**, and it is the shape the trap list names ("two separate snapshot
stores for one piece of state").
**Fix shape:** move the parent-cell commit into `TryCommitParent` /
`CommitAcceptedParentCellless` (i.e. perform retail's `parent->cell != 0` half
in Runtime, gated the same way), and demote App's `TickChild` rebucket to
presentation-only — `RebucketLiveEntityPresentationOnly`
(`LiveEntityRuntime.cs:993-1050`) is the existing shape, added by C3c for
exactly this "Runtime already owns the canonical commit" case. Headless then
gets the correct child cell for free.
## 7.5 The second item — `ClassifyLeaveWorld` is dead
Either wire pickup/parent through it so route 7 stops being the one route with
hard-coded dispositions, or delete it and record why the hard-coded sequence is
the right shape. Leaving a classified disposition with one test and no
production caller is how routes 2 and 4 accumulated their divergences.
I lean **wire it**, because the cause discriminator (`Pickup` vs `Parent`)
is the thing that decides whether `EndChildProjection` runs, and today that
decision is implicit in *which method the caller picked*.
## 7.6 Route 7 — line budget
| Piece | Production lines |
|---|---|
| Runtime child-cell commit in `TryCommitParent` / `CommitAcceptedParentCellless` (retail's `change_cell` + `recalc_cross_cells` half, gated on parent cell) | 120-180 |
| Demote `EquippedChildRenderController.TickChild`'s rebucket to presentation-only + the follow path that replaces it | 60-100 |
| Headless parent-realize drive (`OnParentUpdated` → the same commit) | 60-100 |
| Wire or delete `ClassifyLeaveWorld` | 60-150 |
| **Total** | **300-490** |
Calibration: route 4a was 364 non-comment production lines, 4b-1 was 230 + a
57-line park fix. **Route 7 fits one slice and must not be split** — the child
cell commit and the App demotion are the two halves of one transfer and cannot
land separately without leaving the child cell-less in the interval. Test work
~400-700 lines.
If the implementation crosses **550** production lines, stop and report; the
most likely cause would be discovering that the per-tick follow (7.8, T5) needs
its own Runtime mechanism, which is a second slice.
## 7.7 Route 7 — connected gate
Two clients. `+Acdream` plus a second character observing.
1. **Equip / unequip cycle.** Wield a weapon, then a shield, then unwield both,
five times, while the observer watches. The weapon must appear in the hand,
at the hand, oriented with the hand, and disappear cleanly on unwield.
2. **Carry across a landblock boundary.** With the weapon equipped, run across
at least two landblock boundaries and back, both while the observer watches
and while observing the other character do it.
3. **Pickup.** Drop the weapon, pick it back up. It must leave the ground
(route 7's `leave_world`) and re-appear equipped or in inventory with no
ghost left on the ground.
4. **Delete under load.** Kill something and loot its equipped items.
5. **Reconnect.** Log out with equipment and back in; the equipment must
re-attach.
6. **Headless.** Run the headless bot with an equipped item and assert the
child's canonical `FullCellId` equals the parent's at a stable checkpoint —
this is the direct regression test for 7.4, and it fails **today**.
**Regressions to watch for:** the equipped weapon draws at the world origin or
at its last ground position; the weapon is invisible while equipped;
**invisible-but-solid** — the weapon's collision remains at the drop site after
being picked up; the weapon is left behind when the wearer crosses a landblock
boundary (this is the R1/#184 class and is the specific risk of demoting the
tick rebucket); the child is culled when the parent is visible or vice versa;
a picked-up item still blocks movement.
## 7.8 Traps
Mapped to the shapes that already bit this campaign, plus route-7-specific
ones.
**T1 — the inverse leash trap. Route 7 must NOT arm `ConstrainTo`.**
4b-2's contract correctly says "arm on refusal and rejection too" because
`MoveOrTeleport` returns 1 regardless and `HandleReceivedPosition` @0x00454254
arms @0x00454272. **Route 7 never reaches `HandleReceivedPosition` at all**
`DoPickupEvent` @0x00452240 and `DoParentEvent` @0x00452290 are separate wire
handlers. An implementer arriving from 4b-2 will carry the "always arm" rule
across. There is no leash on this route.
**T2 — deleting the only handler for a rejected classification.**
`ClassifyLeaveWorld` returns `RejectedAuthority` when
`Cause is RuntimeLeaveWorldCause.Unknown` or `ValidCreateAuthority` fails
(`:481-487`). Retail's `DoPickupEvent` has **only** a stale-timestamp no-op —
there is no "rejected" outcome that skips `unset_parent`/`leave_world` once the
stamp is accepted. If route 7 wires the classifier and performs the leave-world
only on `Accepted`, every rejected classification silently drops a pickup. This
is 4b-2's `null`/`Rejected*` trap, one level up: state the policy explicitly.
**T3 — `ValidCreateAuthority`'s teleport-equality predicate is the #307
shape.** `ClassifyLeaveWorld` gates on `ValidCreateAuthority` (`:507-512`),
which requires `PreviousTeleportSequence == AcceptedTeleportSequence`. That is
exactly the predicate #307 (`19d95094`) had to fix on the live Position path,
where `PreviousTeleport` was always 0 and every previously-teleported entity
failed it. Before wiring T2's classifier, **verify first-hand** that the
pickup/parent authority actually populates `PreviousTeleportSequence`, and that
retail's own gate is the *position* stamp (`update_times[0]` @0x0045224B), not
a teleport stamp at all — the two may simply not correspond.
**T4 — do not carry `MoveOrTeleport`'s guards onto this route.** AP-87's 4 m
`BodySnapThreshold` and `!willBeDrTicked` conditions and the 96 m
`MaxPhysicsDistance` are near/far *interpolation* decisions inside
`MoveOrTeleport @ 0x00516330`. Pickup and parent have no distance concept.
Importing any of them is the "carry a guard onto a branch that does not need
it" shape AP-87's own row and the 4b-2 contract both warn about.
**T5 — demoting the tick rebucket can reproduce #184 / route-4a's R1.**
`EquippedChildRenderController.TickChild`'s rebucket (`:405-408`) runs on every
recomposition, and `LiveEntityRuntime.RebucketLiveEntity` is what moves the
GPU draw bucket, sets `IsSpatiallyVisible`, calls `RefreshPresentation`, and
publishes `ProjectionVisibilityChanged` (`:844-953`). Route 4a's R1 finding was
precisely that gating this call left a creature body-and-shadow-correct but
draw-bucket-stale — **invisible but solid**. The replacement must keep the
child following the parent across cell boundaries.
**Retail-side status: PARTIALLY ESTABLISHED.** `CPhysicsObj::UpdateChild
@ 0x00512D50` composes the frame and calls `CPhysicsObj::set_frame(child, …)`
@0x00512D8D; `set_frame @ 0x00514090` writes `m_position.frame` @0x005140E9,
pushes it to the part array @0x00514101, and cascades
`UpdateChildrenInternal` @0x00514108 — **it never touches `objcell_id` or
`cell`.** So retail's per-move child update is frame-only, and the child's cell
is established once at `set_parent`'s `change_cell` @0x00515AD6. **Whether a
parent crossing a cell re-cells its children is NOT established** from
`UpdateChild`/`set_frame`/`UpdateChildrenInternal` alone. Settle it before
demoting: read `CPhysicsObj::change_cell` / `CPhysicsObj::set_cell`'s child
handling, or take a cdb trace with breakpoints on
`CPhysicsObj::change_cell` and `CPhysicsObj::UpdateChildrenInternal` while a
retail character carrying an equipped weapon walks across a landblock boundary.
**T6 — the pre-flight-guard trap probably does NOT apply, and saying so
matters.** 4b-1's finding was that a pre-flight service-window guard cannot see
conditions only Core can see (`AdjustToOutside` residency,
`ResultTouchesPrefix` over `QueriedCellIds`). Route 7's child-cell commit is
retail's `change_cell`**no sweep, no `AdjustPosition`, no placement** — so
there is no deferrable Core condition to miss. Do not import 4b-1's
service-window machinery here. If an implementer finds a reason a child cell
commit *can* defer, that is a new finding and needs its own stop-and-report.
**T7 — ordering divergence on pickup, significance NOT ESTABLISHED.** Retail
runs `unset_parent` @0x0045227F **before** `leave_world` @0x00452286. acdream
inverts it: `RuntimeEntityObjectLifetime.TryApplyPickup` runs
`Physics.CollisionReports.LeaveWorld` `:1229``SetFullCell(0,0)` `:1235`
`ParentAttachments.EndChildProjection` `:1236`, and at the App layer
`LiveEntityHydrationController.OnPickup` (`:460-473`) runs `TryApplyPickup`
first and `OnChildBecameUnparented` second. The two operations touch different
structures (collision reporting vs the relation table), so I could not
construct a case where the order is observable — but I did not prove it inert.
Decide deliberately and record the decision; do not silently preserve the
inversion because "tests are green".
**T8 — do not add a second `LeaveWorld` to `TryCommitParent`.** Already pinned
by the campaign plan and by the in-code comment at `:1352-1359`, but it is the
most natural-looking "asymmetry" a reviewer will flag, because every sibling
method calls `Physics.CollisionReports.LeaveWorld` and this one does not.
Retail's `set_parent` has exactly one `leave_world` @0x00515AC1, inside the
`add_child` success branch that acdream's staged/deferred realize sequence
represents.
---
## 8. Coupling, merge/split, and order
**Do they share files?** **No.** Route 6's surface is
`InventoryWorldDropProjectionController.cs`, `ItemInteractionController.cs`, and
(read-only) `LiveEntityHydrationController.OnCreate`. Route 7's surface is
`RuntimeEntityObjectLifetime.cs`, `EquippedChildRenderController.cs`,
`RuntimeLiveEntitySessionController.cs`, and
`RuntimeAuthoritativePositionRouteClassifier.cs`. The only shared *concept* is
that both funnel through `LiveEntityHydrationController`, and route 6 does not
modify it.
**Can 6 land without 7?** Yes, trivially — route 6 has no production diff.
**Should they be ONE slice?** **No, but not for the campaign's reason.** They
should be *two landings of very different kinds*:
- **Route 6 is a closure, not a slice.** No production change; a short evidence
note plus the drop/split coverage tests. Landing it as a "slice" with the
full contract → implementer → dual-review discipline would be ceremony over
a zero-line diff.
- **Route 7 is one genuine slice** of ~300-490 production lines, and its two
halves (Runtime child-cell commit; App demotion) **must not** be split from
each other.
**Recommended order: route 6 first, then route 7.** Not because of a
dependency — there is none — but because route 6's closure is what removes the
inventory's false "genuine new capability, not merely dormant" claim
(`:768-775`) from the campaign's live record. Leaving that claim standing means
route 7's implementer inherits a planning document that asserts an effect-replay
suppression signal must exist. Cost of doing 6 first: a few hours.
**One planning-record correction to fold in when route 6 closes:** the campaign
plan's own pre-cutover gap list
(`docs/plans/2026-08-02-placement-cutover.md:98-100`) still says
"Route-6 split-recovery creates need an effect-replay suppression signal;
route-7 needs `TryCommitParent`/`CommitWithdrawal` cancellation-symmetry fixes
and host-visible cancellation receipts". The first clause is unsubstantiated
(§6.4) and the second and third were both closed at C0 (§7.3). The list should
say what actually remains: the child-cell two-writer split and the headless
parent-realize gap.
---
## 9. What I could not establish
1. **Whether retail re-cells a child when its parent crosses a cell** (T5).
`UpdateChild` @0x00512D50`set_frame` @0x00514090 is frame-only and never
writes `objcell_id`. `change_cell`/`set_cell`'s child handling was not read.
This is load-bearing for demoting App's per-tick rebucket, and it is the one
thing route 7's contract must nail down before implementation.
2. **Whether the pickup ordering inversion is observable** (T7). Different data
structures; no constructed failure case; not proven inert.
3. **Whether `BuildSpawn`'s wholesale clone of `Children` / `Movement` /
`AnimationFrame` can carry anything harmful onto a split result** (R6-c).
Expected empty for a stackable inventory item; not measured.
4. **Whether `ClassifyLeaveWorld`'s `ValidCreateAuthority` teleport-equality
gate is even meaningful for pickup** (T3). Retail's gate is the position
stamp @0x0045224B; the classifier's is a teleport-sequence equality
inherited from the create path. The two may not correspond at all, in which
case wiring the classifier unchanged would reject valid pickups the way
#307 rejected valid force corrections.