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>
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):
RegisterEntityWithInitialResidenceis 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 atsrc/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs:603-606(Parentedwhen the wire frame carries a parent,PickedUpwhen it has neither parent nor position).ClassifyCreate(RuntimeAuthoritativePositionRouteClassifier.cs:205-275) returns dispositionSetPositionwithInitialCreateFlags = 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
SetSelectedObjectequivalent 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.
- 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.
- 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.
- Drop a second item within ~1 m of the first. Both must remain visible and separately pickable.
- Repeat once indoors (an inn interior) and once immediately after a portal recall.
- 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:
- Runtime commits the child cell-less, unconditionally.
RuntimeEntityObjectLifetime.CommitAcceptedParentCellless(:1377-1408) runsPhysics.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 retailCPhysicsObj::set_parent's cell-less edge" — accurate only for theparent->cell == 0case; retail'sparent->cell != 0case immediately re-cells the child and acdream never performs that half in Runtime. - App re-cells the child from a presentation tick.
EquippedChildRenderController.TickChild:405-408calls_liveEntities.RebucketLiveEntity(child.ChildGuid, parentCellId), and the publicLiveEntityRuntime.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 onlyEntities.TryApplyParent. NeitherTryCommitParentnorCommitAcceptedParentCelllesshas any headless caller — repo grep finds onlyEquippedChildRenderController.cs:869and the App wrappers atLiveEntityRuntime.cs:2280-2312. There is no headlessEquippedChildRenderController, 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.
- 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.
- 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.
- 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. - Delete under load. Kill something and loot its equipped items.
- Reconnect. Log out with equipment and back in; the equipment must re-attach.
- Headless. Run the headless bot with an equipped item and assert the
child's canonical
FullCellIdequals 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
- Whether retail re-cells a child when its parent crosses a cell (T5).
UpdateChild@0x00512D50 →set_frame@0x00514090 is frame-only and never writesobjcell_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. - Whether the pickup ordering inversion is observable (T7). Different data structures; no constructed failure case; not proven inert.
- Whether
BuildSpawn's wholesale clone ofChildren/Movement/AnimationFramecan carry anything harmful onto a split result (R6-c). Expected empty for a stackable inventory item; not measured. - Whether
ClassifyLeaveWorld'sValidCreateAuthorityteleport-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.