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

35 KiB

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 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.RegisterLiveEntityRegisterEntityWithInitialResidence (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 ForgetInitialCreateResidencePhysics.SetPosition.ForgetPreferCancellation 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 allDoPickupEvent @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_cellno 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 :1229SetFullCell(0,0) :1235ParentAttachments.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.