acdream/docs/research/2026-08-04-c4-route-6-contract.md
Erik 1b484937b6 test(physics): C4 route 6 — drops/split-recovery closure, zero production lines
Route 6 needs no production change and this commit contains none: C3c
(529e0e9d) already flipped both hosts' Create paths onto the residence lease,
so a dropped item is byte-for-byte route 1's create classification
(RuntimePositionEntityKind.Remote, RuntimeCreateResidenceKind.TopLevel,
ClassifyCreate -> SetPosition with InitialCreateFlags = Placement | Slide).
Route 6 is a SOURCE of route-1 traffic, not a route of its own. Both drop
flavours converge on LiveEntityHydrationController.OnCreate ->
RegisterEntityWithInitialResidence — the whole-item drop through
ItemInteractionController's DropToWorld (no physics, no position; the server
decides), and split-to-world through TryRecoverUnknownPosition's call to the
identical entry point. Contract:
docs/research/2026-08-04-c4-route-6-contract.md.

Retires a FALSE PREMISE from the campaign plan (:97-100), which claimed
split-recovery creates "need an effect-replay suppression signal". Verified
against the decomp instead of assumed: play_default_script @0x005132B0 /
@0x00513300 has exactly three call sites in the entire pseudo-C dump —
DefaultScriptPartHook::Execute @0x00526c08, DefaultScriptHook::Execute
@0x00526c14, and ACCWeenieObject::DoCollision @0x0058c3b4 — and NONE from
set_description or CreateObject. Neither client plays a default script at
create, so there is nothing to suppress. acdream's only create-time replay is
the F754/F755 queue drain keyed by server GUID, which is retail's own
HandleCreateObject @0x00454C80 behaviour. The plan's other two clauses were
closed at C0 (TryCommitParent/CommitWithdrawal cancellation symmetry;
host-visible cancellation receipts); the list now states what actually
remains — route 7's child-cell two-writer split and the headless
parent-realize gap.

Retail split marking recorded for the record: UIAttemptSplitTo3D @0x0058D850
stores only splitStackSize/splitClassID/splitTime and performs no placement;
DeclareValid @0x0058E340's recovery action is SetSelectedObject @0x0058E481 —
a SELECTION transfer with a 10-second expiry, not effect suppression and not
placement. UIAttemptPutIn3D @0x0058D700 records no marker at all.

Seven tests over the now-flipped path (whole item, split stack, new-GUID
recovery, second drop, unavailable destination, newer Position after the
pending identity is consumed, plus the #314 repro), each sabotage-verified:
the production path was broken on purpose, the test was confirmed to fail,
and the sabotage reverted. R6-c is now settled by assertion rather than
argument — BuildSpawn's wholesale clone of Children/Movement/AnimationFrame/
SetupTableId is measured, not reasoned about.

FOUND WHILE TESTING — #314, filed not fixed (this route is zero-production by
contract). BuildSpawn resets top-level MovementSequence/ServerControlSequence
to 0 but its Timestamps `with` block overrides only Position/Teleport/
ForcePosition/Instance, leaving Physics.Timestamps.Movement and
.ServerControlledMove at the SOURCE item's values.
HasConsistentCreateIdentityAndParent requires the two projections to agree, so
a split whose source carries nonzero Movement timestamps — plausible for any
item dropped once, picked up, and split again — fails the predicate and throws
instead of completing the canonical transaction. Verified in source, not taken
on report. Note this is a crash in the exact mechanism the scoping cited as
EVIDENCE that drops already converge: code reading said the path converges,
driving it said it throws. Fixed in the immediately following commit.

Also filed: #313 (DeclareValid's SetSelectedObject port is missing and the
container-split flavour records no marker — selection UX, deliberately not
implemented inside a placement closure) and #315 (route 4b-3's per-packet
runTeleportHook Func<bool> closure at three RunRemoteArmTail call sites; the
network packet path, not Slice I's per-frame resolve path — filed now because
route 5 adds a fourth site). AP-124 stays open and registered.

Test lines are 410 against a 150-250 guidance, accepted: the excess is a real
ItemInteractionController harness plus the #314 repro, which is what found the
defect. A mock that proved nothing would have been shorter and worthless.

Complete Release suite MEASURED at 11,020 passed / 4 skipped / 0 failed
(baseline 11,013/4/0 at 6dc7ba51; +7 new). Neither known flake fired.

Connected gate (user-run) still owed: drop a whole item, split a stack to the
ground, drop a second within ~1 m, repeat indoors and after a portal recall,
then walk two landblocks away and back.

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

6.7 KiB

C4 route 6 — drops and split-recovery: pinned contract (2026-08-04)

Scoped in 2026-08-04-c4-routes-6-7-scoping.md §6. Pinned after route 4b-3 landed (6dc7ba51, gate passed 21cd6e9b).

Route 6 requires ZERO production lines. It is a closure, not a slice: C3c (529e0e9d) already flipped both hosts' Create paths onto the residence lease, and a dropped item is byte-for-byte route 1's create classification. The deliverable is evidence + coverage tests + one planning-record correction.

If implementing this contract requires a production change, STOP AND REPORT. A production diff here means the scoping's central finding is wrong, and that is a finding worth more than the slice.

Why there is nothing to build

A dropped item is an ordinary non-local CreateObject: RuntimePositionEntityKind.Remote, RuntimeCreateResidenceKind.TopLevel, ClassifyCreate → disposition SetPosition with InitialCreateFlags = Placement | Slide. Route 6 is a source of route-1 traffic, not a route of its own.

Both drop flavours already converge on the canonical transaction:

  • Whole-item dropItemInteractionController.ExecutePlacementActions's DropToWorld case sends the drop with no physics and no position; the server's CreateObject returns through LiveEntityHydrationController.OnCreateRegisterLiveEntityRegisterEntityWithInitialResidence.
  • Split-to-worldInventoryWorldDropProjectionController's TryRecoverUnknownPosition calls the identical _hydration.OnCreate entry point. Same residence lease, same conductor, same placement.

So the campaign handoff's route-6 requirement ("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 code.

The false premise this closure exists to retire

docs/plans/2026-08-02-placement-cutover.md:98-100 states route-6 split-recovery creates "need an effect-replay suppression signal". That is unsubstantiated and must be corrected in this landing.

acdream's only create-time effect replay is the F754/F755 queue drain keyed by server GUID (EntityEffectController.ReplayPendingForLiveEntity), written only by 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 (SmartBox::HandlePlayScriptID @0x00452020 / HandlePlayScriptType @0x00452070 queue while absent; HandleCreateObject @0x00454C80 drains).

The one plausible mechanism — a cloned DefaultScriptType surviving BuildSpawndoes not fire at create in either client. acdream's PlayDefault has exactly two callers, both animation hooks (DefaultScriptHook, DefaultScriptPartHook). 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.

Verify both retail claims yourself before relying on them.

Retail truth for split-recovery marking

ACCWeenieObject::UIAttemptSplitTo3D @0x0058D850 records exactly three fields: splitStackSize @0x0058D8A2, splitClassID @0x0058D8A8, splitTime @0x0058D8AE. UIAttemptSplitToContainer @0x0058D7D0 records the identical three. The consumer is ACCWeenieObject::DeclareValid @0x0058E340, whose recovery action is SetSelectedObject(this->id, 0) @0x0058E481 — a SELECTION transfer, with a 10-second expiry @0x0058E49F-@0x0058E4B2. Not effect suppression, and nothing placement-related.

ACCWeenieObject::UIAttemptPutIn3D @0x0058D700 (whole-item drop) records no marker at all and performs no placement.

Deliverables

  1. Coverage tests, 150-250 lines, against the now-flipped path — the list the campaign handoff names: whole item, split stack, new GUID, second drop position, unavailable destination, newer Position arriving while waiting. Note "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.
  2. R6-c settled by assertion, not by argument. BuildSpawn clones Children / Movement / AnimationFrame / SetupTableId wholesale from the source. Expected inert for a stackable inventory item but never measured. Assert it in the tests rather than reasoning about it.
  3. The plan correction at docs/plans/2026-08-02-placement-cutover.md:98-100. All three clauses are stale: the effect-replay premise is unsubstantiated (above), and route 7's TryCommitParent/CommitWithdrawal cancellation-symmetry and host-visible cancellation receipts were BOTH closed at C0. Replace with what actually remains: route 7's child-cell two-writer split and the headless parent-realize gap.
  4. R6-a filed as an issue, NOT implemented here. DeclareValid's SetSelectedObject is not ported and the container-split flavour has no marker. That is selection UX, not placement; mixing it into a placement closure makes the landing un-reviewable. Add to docs/ISSUES.md.

What must remain true

  • Zero production lines. No src/** change. See the stop condition above.
  • Tests must fail against broken behaviour. No source-text pins, no tautologies. The 4b-3 lesson (round-2 finding B1): a test asserting only what must NOT happen cannot detect a deleted write — assert the positive half too.
  • AP-124 (the WCID/count approximation in the recovery match) stays open and registered; retiring it needs ACE to send CreateObject to the initiator, not a client change.

Gates

  • Complete Release suite. Baseline 11,013 passed / 4 skipped / 0 failed at 6dc7ba51 — measure and record; do not inherit. Known flakes, do not chase and do not conflate: #302 (PortalProjectionTests, GC-allocation) and #308 (NakEmissionTests.LossSoak_…, wall-clock, full-suite load only).
  • Connected gate (user-run), cheap and directly visible: drop a whole item on open ground — it must land at your feet, resting, immediately pickable. Split a partial stack to the ground — correct quantity on the pile, remainder in inventory. Drop a second item within ~1 m — both remain visible and separately pickable. Repeat once indoors and once after a portal recall. Walk two landblocks away and back — both piles still there, still pickable. Regressions: item at world origin or your previous position (stale pose); invisible but blocking (#184 class); sunk into or floating above the floor; not pickable; the split pile never appears (recovery window failed); the second drop swallowed by the first.