A player-parented child never received a canonical cell. Its FullCellId stayed 0 for its whole attached lifetime, so it could not follow the player across a boundary. Scope was wider than the local player: every REMOTE player's equipment too. ROOT CAUSE. EquippedChildRenderController hardcoded ParentInstanceSequence: 0 for a parented CreateObject. Correct for creatures and statics, which really are sequence 0; wrong for players, whose ObjectInstance is Character.TotalLogins (ACE Player_Networking.cs:37). The relation filed under (playerGuid, 0) while the record carried TotalLogins, so both route-7 write sites — D1's attach re-cell and D2's propagation lookup — keyed on an incarnation that never matched. TryCommitParent did not validate the sequence, so the attach succeeded and printed normally. Silent. A ROUTE 7 REGRESSION (cd3129e9) that un-masked a latent bug: the TickChild call route 7 deleted was keyed on the child guid alone and was structurally immune to a wrong parent key. THE FIX IS TO STOP TREATING PLAYERS DIFFERENTLY, not to special-case them. Retail's attach path is guid-only end to end — PhysicsDesc::get_parent_id @0x00558a18 -> CObjectMaint::GetObjectA @0x00558a2d -> set_parent @0x00558a3e, with SetChildren @0x00509370 hash-walking by guid — and neither set_parent overload (@0x00515A90, @0x00515B50) nor enter_cell @0x00510ED0 contains any player test or instance-sequence read. Our player/non-player split was purely an artifact of keying relations by (guid, incarnation) against a wire message that carries no parent incarnation. Late-binding to whoever currently holds the guid is retail's own semantics. Fixed at BOTH producers: OnSpawn and OnCreateParentAccepted, the second carrying the byte-identical defect and not named in the contract's scope line. THE INVARIANT IS EQUALITY, NOT FRESHNESS. The contract rejected both framings I offered: every one of the 45 FullCellId liveness predicates excludes a committed child on a NON-cell clause first, so the child inherits only the parent record's existing staleness, which is already present today with no symptom. The key fix alone restores child-equals-parent for every parent class. TWO SITES GATED, inert only because the cell was zero and would have woken wrongly: the hydration candidate loop (a nonzero-cell child would take the legacy RebucketLiveEntity -> CommitRebucket, a second canonical writer — route 7's exact defect class) and RestoreShadow (would install a broadphase row for the weapon, the #184 shape, contradicting route 7's P4). Retail anchor: update_object's parent != 0 early-out @0x00515D40 — children are never independently re-placed. THREE MAJORS WERE FIXED BY DELETION. The first pass added a deferral queue for an unaddressable parent, carrying a missing child-freshness gate (A2), a sentinel-0 collision with the generation filters (A3), and unbounded accumulation (A5). Both reviewers then proved the deferred branch unreachable for BOTH producers — RegisterEntityCore defers the entire CreateObject one layer above, reading the same ?? chain, and CreateParentUpdate is produced only inside AcceptCreateCore, after that gate passes. The machinery was deleted rather than repaired, and the diff SHRANK to 76 added / 13 removed from 91/24 while gaining the A1 fix. Retail confirmed the deletion does not diverge: acdream's real port of retail's per-guid replay (QueueBlobForObject) is a different, untouched layer, and the deleted queue was a third redundant one downstream of it. THE GUARD MUST NOT TEAR WHAT IT PROTECTS. The first pass threw InvalidOperationException AFTER the canonical half had committed, so the one time it fired it left the child parented with no committed relation and a staged one blocking Resolve — a torn transaction, the exact outcome the contract pinned against. Now a pure CanCommitIncarnation precondition checked BEFORE the commit at both sites, with a logged refusal instead of a throw. Route 3's N3 principle (do not make a transient fatal on a host that must survive 30 sessions x 2 hours) reinforces it, but the tearing argument stands alone. TEST QUALITY, the recurring lesson in its most refined form. The A1 test initially passed sabotage FOR THE WRONG REASON: a mismatched ChildPositionSequence meant TryCommitParent's own gate refused in either ordering, so the three assertions carrying A1's meaning passed both ways and only an incidental staging assertion failed. It failed on stranding, not tearing. Corrected, the sabotage now names line 925 — Assert.Null(snapshot.ParentGuid), with the parent's guid in it — proving the canonical mutation happened before the catch. "Fails under sabotage" is necessary, not sufficient; WHICH assertion fails is the real question. The dual parent-class matrix (player 0x5… incarnation > 1 vs creature 0x8… incarnation 0, identical outcomes, sabotage-verified in both directions) is the structural fix for how this survived a full dual review and two connected sessions: every prior test and both captured gate logs used sequence-0 parents. Register: AP-142 clause (f); AP-132 amended to distinguish the two producers; new row AP-146 for the local player's coarse canonical cell (retail writes it per tick at SetPositionInternal @0x00515330 — which, per the retail review, ALSO walks this->children writing each child's objcell_id @0x005153AE-@0x005153D8, so retail's per-tick child propagation lives in the same function). That divergence had no row at all, a standing rule-1 violation now corrected. Follow-up #320 filed for making the player's cell track ordinary movement — deliberately excluded here: it touches the landblock-preserve contract, the Rebucketed cadence, route-2/4b-3 classification inputs AP-136/AP-138 spent four review rounds pinning, and the portal-space frozen-source-cell race. Two dual review rounds; 6 architecture MAJORs and 2 retail MAJORs closed. Diagnostic refusals are latched per child guid and the latch clears on Clear()/RemoveChild, so a recycled guid's next incarnation still logs rather than being silently suppressed. Complete Release suite MEASURED at 11,112 passed / 4 skipped / 0 failed (baseline 11,090 at52175aa1, +22). Neither known flake fired. STILL OWED: the connected gate, with the CORRECTED positive criterion — assert the equipped child's FullCellId EQUALS the parent's after a crossing (a zero is a failure, not a silence), run with BOTH a player and a creature parent, plus the new step carrying an armed creature across a landblock unload/reload. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
674 lines
40 KiB
Markdown
674 lines
40 KiB
Markdown
# Issue #319 contract — the player-parented child's canonical cell (2026-08-05)
|
||
|
||
**Scope:** fix the hard-coded `ParentInstanceSequence: 0` that files every
|
||
CreateObject-carried parent relation under the wrong key for player parents
|
||
(`EquippedChildRenderController.OnSpawn`), restore route 7's D1/D2 propagation
|
||
for player-parented children (local AND remote players), structurally gate the
|
||
two call sites whose inertness today is an accident of the zero cell, and pin
|
||
the child's source of truth for a client-authoritative parent. **This slice
|
||
does NOT make the local player's canonical cell track ordinary movement** —
|
||
that is Decision 1's call, argued in §2, filed as its own follow-up.
|
||
|
||
Pinned at HEAD **`af828a8a`**, clean tree, branch
|
||
`claude/acdream-physics-divergence-5aa784`. (The dispatch named `2687d893`;
|
||
the branch advanced one docs commit — `af828a8a`, the gate-4 probe-label
|
||
closure — before this contract was written. Nothing in that commit changes
|
||
this contract's premises; it strengthens §6's probe rule.) **Line numbers are
|
||
as-of `af828a8a` and WILL go stale; every citation also names the symbol —
|
||
trust the symbol.**
|
||
|
||
Predecessor documents, binding where they still apply:
|
||
|
||
- [`2026-08-05-local-player-child-propagation.md`](2026-08-05-local-player-child-propagation.md)
|
||
— **the settling investigation. Its §§1–6 evidence is BINDING**; this
|
||
contract re-verified its load-bearing claims at HEAD and confirms them
|
||
(one nuance on §5.1 item 3, resolved in §3.3 below).
|
||
- [`2026-08-04-c4-route-7-contract.md`](2026-08-04-c4-route-7-contract.md)
|
||
— the regressing route's contract. Its §3 "must REMAIN true" invariants
|
||
all still bind and are re-asserted in §5; its D1/D2/D3/D4 designs are NOT
|
||
re-opened.
|
||
- [`2026-08-05-c4-closeout-handoff.md`](2026-08-05-c4-closeout-handoff.md)
|
||
— carries the CORRECTED route-7 gate criterion (positive equality
|
||
assertion, dual parent class). §7 of this contract instantiates it.
|
||
- [`2026-08-04-retail-parent-cell-propagation.md`](2026-08-04-retail-parent-cell-propagation.md)
|
||
— retail `set_parent`/`enter_cell` recursion, settled. Not re-derived.
|
||
- `docs/ISSUES.md` #319 — the defect record. This contract does not restate
|
||
its root-cause chain; it builds on it.
|
||
|
||
---
|
||
|
||
## 0. The defect, established — binding, do not re-derive
|
||
|
||
`EquippedChildRenderController.cs:134` (`OnSpawn`) hardcodes
|
||
`ParentInstanceSequence: 0` into `Relations.AcceptCreateObjectRelation`.
|
||
Correct for creatures/statics (genuinely sequence 0); wrong for players,
|
||
whose `ObjectInstance` is `Character.TotalLogins` (ACE
|
||
`Player_Networking.cs:34-37`), parsed into `RuntimeEntityRecord.Incarnation`
|
||
(`CreateObject.cs:749` → `WorldSession.cs:233` →
|
||
`RuntimeEntityRecord.cs:43`). The relation files under `(playerGuid, 0)`
|
||
while the record carries `TotalLogins`, so both route-7 write sites miss:
|
||
|
||
- **D1 attach re-cell** — `RuntimeEntityObjectLifetime.CommitAcceptedParentCellless`'s
|
||
`parent.Incarnation == parentInstanceSequence` gate (`:1537`) → false.
|
||
- **D2 crossing propagation** — `RuntimeEntityDirectory.PropagateFullCellToChildren`'s
|
||
`ChildrenAttachedToParent(guid, current.Incarnation)` (`:478-480`) →
|
||
`Array.Empty<uint>()`, forever.
|
||
|
||
`TryCommitParent` never validates the sequence; the attach succeeds
|
||
silently. Scope: the local player and every remote player, for every
|
||
CreateObject-carried equip (i.e. every login and every first-observe).
|
||
User-visible consequence NIL — verified in the investigation §5, not
|
||
assumed. `ParentEvent`-carried relations (mid-session equip) are unaffected:
|
||
`ParentAttachmentState.Enqueue`/`Resolve` carry and validate the wire's real
|
||
sequence (`ParentAttachmentState.cs:396-411`, `:440-454`).
|
||
|
||
---
|
||
|
||
## 1. Retail ground truth — does retail distinguish a player parent?
|
||
|
||
**No. The player/non-player split is entirely an acdream artifact of the
|
||
(guid, incarnation) relation key meeting a wire message that carries no
|
||
parent instance sequence.** Verified for this contract, not inherited:
|
||
|
||
| claim | anchor | status |
|
||
|---|---|---|
|
||
| The CreateObject attach path looks the parent up **by GUID alone** in the live object table and calls `set_parent` — no instance-sequence read anywhere in the attach | `ACCObjectMaint::CreateObject` @0x00558870: `PhysicsDesc::get_parent_id` @0x00558a18, `CObjectMaint::GetObjectA(this, parent_id)` @0x00558a2d, `CPhysicsObj::set_parent(result, arg2, location)` @0x00558a3e | ✓ read at the pseudo-C |
|
||
| The reverse direction (parent's CreateObject naming its children) is the same shape: per-child hash lookup by GUID, `GetNullObject` **placeholder** when the child is not yet constructed, then `set_parent` — again no instance gate | `CObjectMaint::SetChildren` @0x00509370: hash walk @0x005093b2-0x005093ca, `GetNullObject` @0x005093e6, `set_parent` @0x005093f8 | ✓ read at the pseudo-C |
|
||
| `set_parent` / `enter_cell` / `leave_cell` / `change_cell` contain no player test of any kind — they operate on `CPhysicsObj*` uniformly | `set_parent` @0x00515A90 (order enumerated in route-7 contract §2 row 3); `enter_cell` @0x00510ed0; `leave_cell` @0x00510f50; `change_cell` @0x00513390 | ✓ (route-7 contract §2, re-affirmed; no player branch exists in any of the four bodies) |
|
||
| Retail's `ObjectInstance` sequence lives in the physics-descriptor timestamp block and gates **message staleness**, never the parent relation — the relation is by live object pointer | timestamp block slot 8; AP-132's retail half ("retail's queue-by-GUID replay is pointer-only", @0x004535D0 ~92310-92326, `QueueBlobForObject` @0x005092D0) | ✓ (AP-132, register) |
|
||
| Retail's player cell is NEVER stale: `SetPositionInternal` writes the player's own cell on every physics tick, so "parent cell propagation from a stale parent cell" is unrepresentable in retail | `CPhysicsObj::SetPositionInternal` @0x00515330 (propagation research, binding) | ✓ |
|
||
| **CORRECTION (retail-conformance review, 2026-08-05): this row understated its own anchor.** `SetPositionInternal` @0x00515330 does not merely write the mover's own cell — in the same-cell branch it writes `this->m_position.objcell_id = objcell_id` @0x00515385, THEN walks `this->children` and writes each child's cell id directly (`*(uint32_t*)((char*)eax_2 + 0x4c) = objcell_id_1` @0x005153BD, `CPartArray::SetCellID` @0x005153CC, looping @0x005153AE–@0x005153D8); the cross-cell branch delegates to `change_cell` @0x00515372. Retail's D2 equivalent (per-tick child cell propagation) lives INSIDE THE SAME FUNCTION as the player's own per-tick cell write — a strengthening of the equality invariant and of AP-146, not a contradiction. (AP-142 clause (b) already cited this range; only this table's summary understated it.) | same, ranges above | ✓ |
|
||
|
||
Consequences, pinned:
|
||
|
||
1. **The retail-faithful semantic for a CreateObject-carried relation is
|
||
"attach to the current holder of the parent GUID"** — late-bound, no
|
||
wire incarnation to honor, because the wire supplies none. acdream's
|
||
mapping: the relation adopts the parent's **live incarnation at
|
||
resolve/commit time**. This is NOT a weakening of AP-132's incarnation
|
||
posture: AP-132 gates relations whose wire event **names** a specific
|
||
parent incarnation (`ParentEvent.ParentInstanceSequence`); a CreateObject
|
||
relation names none, so adopting the live value honors exactly what the
|
||
server sent — a GUID. AP-132's row gains a clarifying sentence in the
|
||
fix commit (§4 F4).
|
||
2. **The local player's coarse canonical cell is an acdream divergence with
|
||
no register row.** Retail writes the player's cell per tick; acdream's
|
||
canonical `FullCellId` for the local player is written only at login
|
||
activation (`RuntimeSetPositionState.cs:2741-2745`), accepted inbound
|
||
Position/ForcePosition (`RuntimeEntityDirectory.RefreshSnapshot` →
|
||
`RuntimeEntityRecord.cs:234`), and teleport/portal commit
|
||
(`RuntimeSetPositionState.cs:5001-5007`;
|
||
`LocalPlayerTeleportController.cs:255`). Ordinary WASD passes a
|
||
landblock id (`LocalPlayerProjectionController.Project`, `:85-98`, low
|
||
16 bits forced to `0xFFFF` in BOTH branches) that
|
||
`LiveEntityRuntime.RebucketLiveEntity` explicitly preserves the old cell
|
||
for (`:935-938`). Register rule 1 applies: **this divergence gets its
|
||
row in the fix commit regardless of Decision 1's direction** (§4 F4).
|
||
|
||
---
|
||
|
||
## 2. DECISION 1 — the child follows the parent's CANONICAL cell; the key fix alone is correct and complete for #319; the player-cell-tracking question is real, pre-existing, and files separately
|
||
|
||
**The call is (c)** — neither (a) nor (b) as framed. (b)'s framing
|
||
("staleness is harmless everywhere") is false without the §3 gates; (a)'s
|
||
framing ("stale is strictly worse than zero, so #319 must make the player's
|
||
cell track") rests on an argument that dissolves under the consumer
|
||
enumeration below. The pinned design:
|
||
|
||
> **The committed child's canonical `FullCellId` EQUALS its committed
|
||
> parent's canonical `FullCellId` at every stable observation point — no
|
||
> more, no less.** That is route 7's §3.1 headline invariant verbatim, and
|
||
> it is an EQUALITY invariant, not a freshness invariant. The parent's own
|
||
> canonical-cell freshness is a property of the PARENT's record, owned by
|
||
> the parent's own write paths — pre-existing, unchanged by this fix, and
|
||
> documented as its own divergence (§1 item 2). #319's fix makes the child
|
||
> inherit the parent's value exactly; it does not, and must not, invent a
|
||
> different cell authority for the child than the parent itself has.
|
||
|
||
### 2.1 Why (a)'s "stale defeats 45+ liveness sites" argument fails for the child
|
||
|
||
The 45+ `FullCellId != 0` predicates (route-7 contract §0 item 6) read a
|
||
**spatial root's** cell. A committed child is structurally excluded from
|
||
every one of them by a NON-cell clause, verified at HEAD:
|
||
|
||
- `LiveEntityRuntime.GetRootObjectClockDisposition` (`:2602-2612`) and
|
||
`HasSpatialRuntimeProjection` (`:3340-3345`): both require
|
||
`ProjectionKind is LiveEntityProjectionKind.World`; a committed child is
|
||
`Attached` — excluded on the first clause regardless of cell value.
|
||
- The collision-retirement sweep `RuntimeSetPositionState.IsAffectedCollisionResident`
|
||
(`:3930-3946`): triple-gated — spatial-roots-only iteration
|
||
(`ParkCollisionResidents` `:3749-3756`), `_physics.IsSpatialRoot`
|
||
(`:3942`), and an explicit
|
||
`!ParentAttachments.HasCommittedParent(record.ServerGuid)` (`:3944-3945`).
|
||
A committed child can never be parked by a retiring prefix, at any cell
|
||
value.
|
||
- Route 7's own P4 record (`RuntimeEntityDirectory.cs:451-465`): a committed
|
||
child is never a spatial root, joins no workset, has no shadow row.
|
||
|
||
What remains is exactly four consumers that can read a CHILD's cell, and
|
||
each is resolved individually:
|
||
|
||
1. **`LiveRenderProjectionJournal.Project` (`:270-277`)** — prefers
|
||
`record.FullCellId` over the `entity.ParentCellId` fallback when nonzero.
|
||
Traced to its consumers at HEAD: the dynamics **draw visibility** routes
|
||
read `record.Source.ParentCellId` (the fresh, TickChild-maintained
|
||
presentation field) — `RenderScenePViewFrameProduct.BuildDynamicLastRoute`
|
||
(`:1441-1481`, `ParentCell(in record)` for the indoor
|
||
`SphereVisibleInCell` test) and `BuildOutsideDynamicRoutes` (`:1356-1361`,
|
||
same source). `Residency.FullCellId` feeds only (i) the indoor per-cell
|
||
candidate index `_cellDynamics` (`ArchRenderScene.AddToIndices` `:639`),
|
||
consumed by the look-in candidate enumeration
|
||
(`BuildLookInRoutes` → `LoadCell` `includeDynamics: true`, `:1319-1322`),
|
||
and (ii) the `RenderSceneShadowRuntime` residency assertion (`:403`).
|
||
**Decisive fact: the LOCAL PLAYER's own record already sits in this exact
|
||
index under this exact staleness today** — the player's `FullCellId` is
|
||
nonzero-stale during ordinary play, and the player renders correctly
|
||
everywhere, because visibility is `Source.ParentCellId`-driven. Post-fix
|
||
the child is filed **beside its parent, under the same value** — equality
|
||
with the parent in the render residency index is the consistent state,
|
||
not a new staleness class. (P-R1 in §6 pins this with the shadow-runtime
|
||
assertion in mind.)
|
||
2. **The hydration filter** (`LiveEntityHydrationController.OnLandblockLoaded`
|
||
`:551-557`) — wakes undesirably; gated structurally in §3.1.
|
||
3. **`RestoreShadow`** (`LiveEntityPresentationController.cs:216-236`) —
|
||
wakes undesirably; gated structurally in §3.2.
|
||
4. **The unwield/drop Position classification**
|
||
(route-7 contract §11; `RuntimeAuthoritativePositionRouteClassifier`) —
|
||
wakes DESIRABLY: a committed child's pre-merge cell becomes
|
||
"deterministically the parent's (nonzero whenever the parent is
|
||
celled)", which is **route 7 §11's own stated intent and retail's own
|
||
predicate population** (retail's `unset_parent` does no cell work, so a
|
||
wielded child's unwield Position reaches `MoveOrTeleport` with the
|
||
parent's nonzero cell). #319 had silently re-created the pre-route-7
|
||
cell-less population for player-parented children; the fix restores the
|
||
§11 direction. Note the `af828a8a` finding: at a drop ACE also advances
|
||
TELEPORT_TS and the classifier is a short-circuit OR
|
||
(`RuntimeAuthoritativePositionRouteClassifier.cs:391`), so the observed
|
||
label stays `teleport-ts` either way. One test pins the stale-cell
|
||
flavor (§6 test 6).
|
||
|
||
Additionally, `RuntimeTraceRecorder.OnEntity`
|
||
(`GameRuntimeEvents.cs:246-253`) stops recording `0` for the player's
|
||
weapon — a diagnostic improvement, not a risk.
|
||
|
||
**Conclusion:** with the two §3 gates in place, no consumer of the CHILD's
|
||
cell behaves worse with stale-but-equal than with zero, several behave
|
||
better, and the route-7 invariant is restored exactly. The (a) framing's
|
||
"wrong answer looks right" applies to the PARENT's record — which this fix
|
||
does not touch and which is exactly as stale before and after.
|
||
|
||
### 2.2 Why the player-cell-tracking half does NOT ride in #319
|
||
|
||
Making `LocalPlayerProjectionController.Project` pass the exact
|
||
`movement.CellId` (available in both branches) instead of the coarsened
|
||
landblock would touch, at minimum:
|
||
|
||
- the deliberate landblock-preserve contract at
|
||
`LiveEntityRuntime.cs:935-938` (a generic rebucket rule, not
|
||
player-specific — changing its input population changes it for the one
|
||
caller that relies on it);
|
||
- `Rebucketed` delta publication cadence: today the player NEVER publishes
|
||
a `Rebucketed` entity delta during WASD (the preserve path early-outs at
|
||
`CommitRebucket`'s `previous == fullCellId`, `RuntimeEntityObjectLifetime.cs:1965-1972`);
|
||
exact-cell commits would publish per crossing (EnvCell crossings are the
|
||
high-frequency case) — a consumer enumeration in route 7's P8 class;
|
||
- the accepted-Position classification inputs for the LOCAL player (route
|
||
2 and the 4b-3 `PreMergeCommittedCellId` measurement at
|
||
`TryApplyPosition:1801-1814`): a fresh committed cell changes the
|
||
pre-merge population on live correction paths that AP-136/AP-138 spent
|
||
four review rounds pinning;
|
||
- the portal-space freeze interaction
|
||
(`LocalPlayerProjectionController.Project:100-103` — "the teleport owner
|
||
alone projects the destination while the local controller deliberately
|
||
retains its frozen source cell"): a canonical exact-cell writer must not
|
||
race the teleport owner;
|
||
- the `isOrdinaryRoot` family (`LiveEntityRuntime.cs:915-918`, `:3213`,
|
||
`:3323`) and the animation-scheduler local-player exclusion
|
||
(`LiveEntityAnimationScheduler.cs:183-227`).
|
||
|
||
That is a local-player movement-spine slice with its own contract, its own
|
||
review, and its own gate — calibrated against the campaign, comfortably
|
||
larger than route 7's 127 production lines once its verification surface is
|
||
counted, for zero behavioral need identified today. **It files as a
|
||
follow-up issue in the fix commit** (§4 F4), carrying one specific hazard
|
||
this contract identified and could NOT close (§9 item 2): whether the local
|
||
player is a `RuntimePhysicsState` spatial root, and if so whether a
|
||
stale-cell landblock retirement (a player WASD-ing beyond the streaming
|
||
radius from its last teleport, with no intervening teleport or inbound
|
||
Position) can sweep the player into `ParkCollisionResidents`. The child is
|
||
provably immune (the `HasCommittedParent` gate); the player is not obviously
|
||
so, and the connected routes that pass today teleport between stops, which
|
||
refreshes the cell and may be masking it.
|
||
|
||
---
|
||
|
||
## 3. DECISION 2 — the three inert-because-zero sites, one verdict each
|
||
|
||
### 3.1 Hydration filter — WAKES WRONGLY; add the structural gate in this slice
|
||
|
||
`LiveEntityHydrationController.OnLandblockLoaded` computes
|
||
`projectionCellId = projection?.ProjectionCellId ?? Snapshot.Position?.LandblockId ?? candidate.FullCellId`
|
||
(`:551-553`) and admits candidates on `projectionCellId != 0` + landblock
|
||
match + `SetupTableId` (`:554-557`). A committed child today falls through
|
||
all three sources to `0` (its `ProjectionCellId` is unset, its
|
||
`Snapshot.Position` is null per route-7 §0 item 10) and is skipped. With a
|
||
nonzero canonical cell it becomes a candidate whenever its (= the parent's)
|
||
canonical landblock loads — e.g. at every login, where the equipment's cell
|
||
names the activation landblock — and takes the full legacy
|
||
`RebucketLiveEntity` branch (`:594-596`), which writes
|
||
`entity.ParentCellId` (`LiveEntityRuntime.cs:885-898`) AND reaches
|
||
`CommitRebucket` — **a second canonical cell writer for a child, the exact
|
||
two-writer defect route 7 exists to remove**, plus a one-frame presentation
|
||
overwrite that TickChild then repairs.
|
||
|
||
**Waking is not desirable, and the current inertness is an accident of the
|
||
bug.** Pinned: the candidate loop excludes records with a committed parent
|
||
— gate on `ParentAttachments.HasCommittedParent(candidate.ServerGuid)` (or
|
||
the equivalent projection-kind test), the same structural exclusion the
|
||
retirement sweep already uses (`RuntimeSetPositionState.cs:3944-3945`).
|
||
Rationale is retail-anchored, not defensive: a retail child is never
|
||
independently re-placed by cell load — `update_object`'s `parent != 0`
|
||
early-out (@0x00515D40) means the parent's own propagation is the only
|
||
mechanism, and acdream's analog (D2) needs no hydration assist. The gate is
|
||
correct TODAY (it changes nothing for a zero-cell child) and required
|
||
post-fix; it lands in the same commit as the key fix, before it in
|
||
sequence.
|
||
|
||
**CORRECTION (retail review round 2, D1, 2026-08-05): the "falls through to 0"
|
||
and "correct TODAY (changes nothing for a zero-cell child)" claims above are
|
||
true only for a PLAYER-class parent — false for a creature/static-class
|
||
parent.** Pre-fix, `EquippedChildRenderController.OnSpawn`'s hardcoded
|
||
`ParentInstanceSequence: 0` genuinely MATCHES a creature-class parent's real
|
||
incarnation (creatures/statics are sequence 0), so D1's
|
||
`parent.Incarnation == parentInstanceSequence` gate already passes for that
|
||
class at HEAD, the relation commits, and the child already carries a nonzero
|
||
canonical `FullCellId` pre-fix — meaning a creature-parented child is ALREADY
|
||
a hydration candidate today and already takes the legacy `RebucketLiveEntity`
|
||
branch this gate removes. §3.2's twin premise received this same correction
|
||
(architecture review A4); this paragraph did not, until now. The gate's
|
||
direction is still correct (route 7's P4/D4), and it is still required in
|
||
this slice — but it is a live behaviour change for the creature class, not
|
||
merely a post-fix necessity for the player class. See the §7 gate's Half B
|
||
for the connected-gate watch step this requires.
|
||
|
||
### 3.2 `RestoreShadow` — WAKES WRONGLY; add the structural gate in this slice
|
||
|
||
`LiveEntityPresentationController.RestoreShadow` (`:216-236`) no-ops today
|
||
on `record.FullCellId == 0` (`:220`). Its other two guards do NOT protect a
|
||
committed child: `IsSpatiallyProjected` is set `true` by the
|
||
presentation-only rebucket the child takes every frame
|
||
(`LiveEntityRuntime.cs:1043`, inside `RebucketLiveEntityPresentationOnly`),
|
||
and `IsSpatiallyVisible` follows the child's ordinary visibility. On a
|
||
Hidden→Visible edge (`RetailHiddenTransition.BecameVisible`, `:187-204` —
|
||
reachable for a child both via its own transition and via the parent's
|
||
unhide cascading through `_setDirectChildrenNoDraw`), a nonzero cell would
|
||
install a `ShadowObjects` broadphase row for an equipped weapon at the
|
||
parent's canonical cell — contradicting route 7's P4 record ("a committed
|
||
child never joins a workset or shadow list",
|
||
`RuntimeEntityDirectory.cs:451-465`), AP-142's model, and retail (a child's
|
||
broadphase state is established at attach/unparent edges only; route-7
|
||
contract §0 trap 5a). An invisible-but-solid weapon row at a possibly-stale
|
||
cell is the #184 shape.
|
||
|
||
**Pinned:** `RestoreShadow` refuses records with a committed parent (same
|
||
predicate as §3.1; checking `ProjectionKind` alone is acceptable if the
|
||
implementer shows it is equivalent for every reachable record). Same
|
||
commit, same rationale: correct today, required post-fix.
|
||
|
||
### 3.3 `RuntimeInitialCreateResidenceState.Begin` — waking is a NON-EVENT; verify with a test, no production change
|
||
|
||
The investigation flagged `Begin`'s `record.FullCellId != 0u` refusal
|
||
(`:583`) and `TryConvertToCellessRoute`'s (`:1041`) as unknowns. Resolved
|
||
at HEAD: **the refusal is a defensive invariant, not a reachable gate.**
|
||
`Begin`'s only production caller,
|
||
`RuntimeEntityObjectLifetime.InitializeAcceptedCreateResidence`
|
||
(`:2736-2771`), ZEROES a nonzero cell first — `:2751-2752`,
|
||
`if (canonical.FullCellId != 0u) Entities.SetFullCell(canonical, 0u, 0u)`
|
||
— before calling `Begin`. So a re-CreateObject that opens a fresh residence
|
||
on a record inheriting a nonzero child cell zeroes it (flowing through the
|
||
D2 chokepoint, correctly zeroing any grandchildren per AP-142 clause (a)),
|
||
opens the `Parented` residence, and D1 re-cells at the attach commit —
|
||
the same sequence a first create runs. `TryConvertToCellessRoute`'s refusal
|
||
concerns an OPEN lease on a record with a committed cell — for a committed
|
||
child the lease was already forgotten at the attach commit's cancellation
|
||
prefix, so the arm is unreachable for the waking population.
|
||
|
||
**Pinned:** no production change at this site. One test (§6 test 5) drives
|
||
a re-CreateObject against an attached, non-zero-cell child (same
|
||
incarnation → the `TryApplyAttachedAppearance` path; new incarnation → the
|
||
replacement path with `InitializeAcceptedCreateResidence`) and asserts
|
||
byte-identical outcomes to today's, plus the zero-then-re-cell sequence
|
||
staying inside the transaction (no observable cell-less escape — route 7's
|
||
D1 atomicity clause).
|
||
|
||
---
|
||
|
||
## 4. The fix, pinned
|
||
|
||
### F1 — the key: CreateObject-carried relations late-bind to the parent's live incarnation
|
||
|
||
`EquippedChildRenderController.OnSpawn` stops writing the literal `0`:
|
||
|
||
- **Parent snapshot known at accept time** → stage the relation with the
|
||
parent's live `InstanceSequence` — the identical lookup
|
||
`ResolveRelations` already hands to `Resolve`
|
||
(`EquippedChildRenderController.cs:834-836`,
|
||
`_liveEntities.TryGetSnapshot`).
|
||
- **Parent not yet known** → do NOT stage under a guessed key. Route the
|
||
relation through the existing unresolved/deferred machinery (the
|
||
`RetryWaitingDescendants` / `WaitOwner.Parent` shape) so it resolves when
|
||
the parent arrives, adopting the parent's incarnation THEN. Retail
|
||
anchor for the late-bind semantic: `GetObjectA`/`GetNullObject` by GUID
|
||
(§1) — the wire named a GUID, not an incarnation; matching "the current
|
||
holder" is the faithful mapping. The implementer chooses the mechanism
|
||
(a wildcard-incarnation relation resolved in `Resolve`, or deferring the
|
||
`AcceptCreateObjectRelation` call itself); the pinned constraints:
|
||
**no relation is ever staged or committed with an incarnation that does
|
||
not equal the parent's live incarnation at that moment, and the
|
||
`Resolve` early-return on staged relations
|
||
(`ParentAttachmentState.cs:425-426`) must not strand a deferred
|
||
CreateObject relation.**
|
||
- **The commit-time tripwire** (the investigation's own recommendation,
|
||
adopted): the parent-relation commit path asserts
|
||
`relation.ParentInstanceSequence == parentRecord.Incarnation` whenever
|
||
the parent is active — a mismatch REFUSES the commit loudly (throw or
|
||
logged refusal per the codebase's commit-refusal idiom, implementer's
|
||
choice — pinned outcome: never a silent success under a mismatched key).
|
||
This is what converts any future producer regression from "silently
|
||
inert propagation" into a failing test/session.
|
||
|
||
This one change fixes the local player's login equipment AND every remote
|
||
player's observed equipment (same producer). `ParentEvent`-path behavior is
|
||
untouched.
|
||
|
||
### F2 — the two structural gates (§3.1, §3.2)
|
||
|
||
Both are committed-parent exclusions at sites that must never handle an
|
||
attached child. Both are behavior-preserving at HEAD (the excluded
|
||
population currently reaches neither site's active arm) and land in the
|
||
same commit so the key fix never ships without them. Sabotage rule: each
|
||
gate's test must fail with the gate deleted AND the key fix present (§6
|
||
tests 3–4).
|
||
|
||
### F3 — probe truthfulness
|
||
|
||
`ACDREAM_PROBE_CHILD_CELL` stays as-is (it is TEMPORARY, C5c strips it),
|
||
but the fix commit re-runs the §7 gate expectations against it. No probe
|
||
code change is required for this slice: the corrected gate criterion is a
|
||
positive read of the child's cell, not a probe-line count.
|
||
|
||
### F4 — bookkeeping, in the fix commit (register rules 1–2 binding)
|
||
|
||
- **AP-142 gains a clause** (or a new AP row if the reviewer prefers —
|
||
implementer's call, one of the two): the CreateObject-carried relation
|
||
late-binds to the parent's live incarnation because the wire carries no
|
||
parent instance sequence; retail's attach is GUID-only
|
||
(`GetObjectA` @0x00558a2d, `SetChildren` @0x005093f8, `GetNullObject`
|
||
placeholder @0x005093e6). AP-132's row gains the clarifying sentence
|
||
distinguishing the two producers (ParentEvent = wire-named incarnation,
|
||
gated; CreateObject = no wire incarnation, late-bound).
|
||
- **NEW register row — the local player's coarse canonical cell** (§1 item
|
||
2): intentional-architecture-or-stopgap classification to be argued in
|
||
the row itself; anchors `CPhysicsObj::SetPositionInternal` @0x00515330
|
||
vs the three acdream writers; risk column names the §2.2 park-sweep
|
||
question and the child-inherits-the-coarseness consequence (the child's
|
||
cell is stale-but-equal wherever the parent's is).
|
||
- **Follow-up issue filed**: local-player canonical cell tracking —
|
||
carrying §2.2's enumeration and §9 item 2's unresolved spatial-root
|
||
question as its first verification step.
|
||
- **ISSUES.md**: #319 → Recently closed with the commit SHA; the entry's
|
||
"do NOT rush the fix" block is answered by this contract's §2/§3.
|
||
- **Route-7 contract**: append a dated supersession note under its §7 gate
|
||
(the criterion correction is already recorded in the closeout handoff;
|
||
the note points here).
|
||
- **AP-142 clause (b)/(c) untouched**; no row deletions.
|
||
|
||
---
|
||
|
||
## 5. What must REMAIN true (route 7 §3, re-asserted for this slice)
|
||
|
||
1. Route 7 invariants 1–13 hold verbatim. Specifically re-tested here:
|
||
equality invariant (1), child never self-simulating (2), no placement
|
||
machinery on the route (3), **no `ConstrainTo`** (4), `TryCommitParent`
|
||
keeps zero `LeaveWorld` calls (6), presentation advances and is
|
||
asserted (7), **no per-crossing child shadow/cross-cell rebuild** (8),
|
||
ledger convergence (13).
|
||
2. **The removal path still propagates ZERO** (AP-142 clause (a)) — the
|
||
key fix must not perturb withdrawal/delete/EndGeneration edges; the §6
|
||
matrix re-runs them with a player-range parent.
|
||
3. **`RebucketLiveEntityPresentationOnly` does not become a canonical
|
||
writer again** — the D4 demotion stands; the presentation rebucket
|
||
stays keyed on the child guid alone.
|
||
4. **The `ParentEvent` path's incarnation gating (AP-132) is unchanged** —
|
||
a wire-named stale incarnation still discards/queues exactly as today.
|
||
5. **No new canonical cell writer is introduced.** The fix adds zero
|
||
`SetFullCell` call sites; it only makes the existing D1/D2 sites find
|
||
their children.
|
||
6. **The dormant-residence deferrals are untouched** (route 7 invariant 9);
|
||
the continuation executor's parent replay path benefits from F1
|
||
automatically (it routes through the same commit pair).
|
||
|
||
---
|
||
|
||
## 6. Test plan
|
||
|
||
Rules (route 5 §7 / route 7 §6, verbatim where they apply): assert the
|
||
layer that broke; positive facts, not absences; every new test fails
|
||
against a broken implementation; sabotage-verify both halves of dual-layer
|
||
assertions; never derive a test's workload from the constant under test.
|
||
|
||
**The structural rule this issue adds — MANDATORY: every scenario runs as a
|
||
dual-parent-class matrix.** One parent in the player range (`0x5…`, spawned
|
||
with a nonzero incarnation — use a value > 1 so an off-by-one cannot pass),
|
||
one in the creature/dynamic range (`0x8…`, incarnation 0), asserting
|
||
IDENTICAL outcomes. #319 exists precisely because every prior test and both
|
||
captured gate logs used sequence-0 parents; a matrix in which the two
|
||
classes can diverge silently is the defect's habitat. Where a fixture
|
||
helper spawns parents, the helper takes the incarnation as a parameter —
|
||
no baked-in zero.
|
||
|
||
Focused tests (`tests/AcDream.App.Tests` for the controller/producer,
|
||
`tests/AcDream.Runtime.Tests` for the propagation surface):
|
||
|
||
1. **The key itself (F1), CreateObject path, matrix.** A CreateObject-carried
|
||
equip against a live parent with incarnation N (N=0 creature, N=7
|
||
player): relation commits under `(parentGuid, N)`
|
||
(`TryGetCommittedParent` returns N), D1 re-cells the child to the
|
||
parent's exact cell at attach (`cause=attach` behavior — assert the cell
|
||
equality, not the probe), and a subsequent parent canonical-cell change
|
||
through each writer family reaches the child (D2). Sabotage: restore the
|
||
literal `0` and confirm the PLAYER row fails while the creature row
|
||
passes — the test must be able to see exactly #319.
|
||
2. **The deferred-parent flavor (F1), matrix.** Child CreateObject arrives
|
||
BEFORE the parent's: relation stays unresolved (never staged under a
|
||
guessed key — assert no committed relation exists), parent arrives with
|
||
incarnation N, relation resolves and commits under N, child celled.
|
||
Companion: parent arrives cell-less, later gains a cell → D2 catch-up
|
||
re-cells the child (route 7 §6 test 1's companion, now for the
|
||
CreateObject producer).
|
||
3. **Hydration gate (§3.1).** An attached child with nonzero canonical
|
||
cell; its landblock reloads; assert the child is NOT a hydration
|
||
candidate (no legacy rebucket, `entity.ParentCellId` untouched by the
|
||
hydration pass, no `CommitRebucket` invocation for the child). Sabotage:
|
||
delete the gate → test fails.
|
||
4. **Shadow gate (§3.2).** Attached child, nonzero cell, driven through a
|
||
Hidden→Visible edge: assert NO `ShadowObjects` row exists for the child
|
||
after the transition (assert against the engine's shadow table, not a
|
||
cache — the AP-145/#318 lesson). Sabotage: delete the gate → fails.
|
||
5. **Residence non-event (§3.3).** Re-CreateObject shapes against an
|
||
attached nonzero-cell child (same-incarnation refresh; new-incarnation
|
||
replacement): outcomes byte-identical to the zero-cell baseline; the
|
||
replacement path's zero-then-re-cell stays inside one transaction (no
|
||
observable cell-less window — reuse route 7 test 1's observation
|
||
technique).
|
||
6. **Unwield classification population (§2.1 item 4).** An attached child
|
||
whose parent cell is nonzero (and deliberately DIFFERENT from the wire
|
||
Position's cell, the stale flavor) receives an unparent Position:
|
||
classification takes the TELEPORT_TS/distance route (never the
|
||
cell-less arm), the drop commits at the WIRE position, and — the §11
|
||
guard — the 4b-3 synthetic `PreMergeCommittedCellId == 0` fixtures are
|
||
NOT relabeled: they remain synthetic.
|
||
7. **Withdrawal/delete matrix (invariant 2).** Pickup of the parent,
|
||
delete of the parent, `EndGeneration` — child (and a grandchild) go
|
||
cell-less, both parent classes.
|
||
8. **The commit-time tripwire (F1).** A relation carrying a wrong
|
||
incarnation reaching the commit path is REFUSED loudly; assert the
|
||
refusal is observable (exception or logged refusal + no committed
|
||
relation), not a silent success.
|
||
9. **Ledger convergence** (route 7 invariant 13): teardown/reset with a
|
||
player-parented committed child present.
|
||
|
||
The existing route-7 suites run unmodified — zero expectation changes
|
||
outside the new tests is the tripwire that F1/F2 changed no non-child
|
||
behavior.
|
||
|
||
---
|
||
|
||
## 7. The gate (connected, user-run) — the CORRECTED criterion, instantiated
|
||
|
||
Release build, `ACDREAM_RETAIL_UI=1`, `ACDREAM_PROBE_CHILD_CELL=1`, live
|
||
ACE, graceful close. Run BOTH halves; a session that runs only the
|
||
creature half is a not-run for this issue (process rule (g): the gate must
|
||
be able to see the defect — the defect is player-class-only).
|
||
|
||
**Half A — player parent (the local player, `0x5000000A`):**
|
||
|
||
1. Login with equipment. **PASS requires a `[child-cell]` line for the
|
||
player's child at attach-or-activation** (`cause=attach` if the player
|
||
was celled first, else the D2 catch-up line when login activation
|
||
commits the player's first cell) **AND the child's `FullCellId` read
|
||
equal to the player's, nonzero.** Today this line does not exist
|
||
(`c4-gates.log:238`, `c5-gates.log:343` — attach with no probe line);
|
||
its appearance is the direct #319 signal.
|
||
2. Carry across ≥2 landblock boundaries and back, plus one
|
||
EnvCell-to-EnvCell dungeon traversal. **PASS: child `FullCellId` EQUALS
|
||
the player's at every checkpoint — a zero is a FAILURE, not a silence.**
|
||
Expectation set honestly by §2: during WASD the player's canonical cell
|
||
does not change, so `cause=propagate` lines are NOT expected at these
|
||
crossings for the player half — equality (both records carrying the
|
||
same value) is the assertion. This expectation is itself part of the
|
||
gate record: if `propagate` lines DO appear here, something changed the
|
||
player's cell writers and the session gets investigated, not celebrated.
|
||
3. Portal recall / portal transit while equipped. **PASS: `cause=propagate`
|
||
fires for the player's child at the arrival commit** (the teleport
|
||
writer is the player's live cell-change edge), child equal to the
|
||
player's destination cell, equipment present and following.
|
||
4. Unequip/re-equip mid-session (the ParentEvent path): `cause=attach`
|
||
with the true incarnation — this was the investigation §7 prediction;
|
||
observing it closes that loop.
|
||
5. Reconnect with equipment: re-attach produces the same line set as step 1.
|
||
|
||
**Half B — creature parent (`0x7…`/`0x8…`, e.g. observe an armed NPC or
|
||
`@teleto` a wielding creature):** the route-7 recipe unchanged —
|
||
`cause=propagate` in double digits across several crossings, child equal
|
||
to parent at checkpoints. This half proves the fix did not perturb the
|
||
already-working class. **ADDED (architecture review A4, 2026-08-05):**
|
||
this half must ALSO watch the creature-parented weapon's shadow/collision
|
||
row across a Hidden→Visible edge (`@hide`/`@unhide` an armed NPC, or a
|
||
sit/stand-equivalent state toggle if available) — F2's `RestoreShadow`
|
||
gate (`LiveEntityPresentationController.cs`) is NOT behavior-preserving
|
||
at HEAD for this class the way the contract's §3.2 originally claimed:
|
||
route 7's D1 already gives a creature-parented child a nonzero cell, so
|
||
this gate is a live behavior change here (the weapon's broadphase row no
|
||
longer refreshes on that edge), not an inert one. Confirm no #184-shaped
|
||
regression (a stale-but-solid collider) and no crash; the change is
|
||
believed correct per route 7's P4 record but was never exercised by a
|
||
connected session before this note. **ADDED (retail review round 2, D1,
|
||
2026-08-05):** this half must ALSO carry an armed creature/NPC across a
|
||
landblock UNLOAD and RELOAD (walk it out of streaming range and back, or
|
||
force a landblock reload if the harness supports it) and confirm the
|
||
weapon is still attached and correctly placed afterward — F2's hydration
|
||
gate (`LiveEntityHydrationController.OnLandblockLoaded`) is likewise NOT
|
||
behavior-preserving at HEAD for the creature class (§3.1's correction,
|
||
same D1-already-nonzero fact): pre-fix a creature-parented child was
|
||
already a hydration candidate on landblock reload and took the legacy
|
||
`RebucketLiveEntity` path this gate now excludes. This is precisely route
|
||
7's "left behind at a boundary" regression shape if the gate's direction
|
||
were wrong; confirm it is not.
|
||
|
||
Secondary check only: probe-line volume. Primary: the equality reads.
|
||
Regressions to watch: route 7's list verbatim (weapon at origin/stale
|
||
ground position, invisible while equipped, left behind at a boundary,
|
||
invisible-but-solid, culled against parent) — none is expected, since the
|
||
presentation path is untouched.
|
||
|
||
---
|
||
|
||
## 8. Size estimate and the split call
|
||
|
||
Calibrated against the campaign (route 7: ~127 production lines; route 3:
|
||
~418):
|
||
|
||
| piece | non-comment production lines |
|
||
|---|---|
|
||
| F1 key + deferred late-bind + commit tripwire | 25–60 |
|
||
| F2 hydration gate | 4–12 |
|
||
| F2 shadow gate | 4–10 |
|
||
| F3 | 0 |
|
||
| **total** | **~35–80** |
|
||
|
||
Tests are the larger share (~250–450 lines, dominated by the matrix).
|
||
|
||
**ONE slice, no split.** F1 without F2 wakes the two sites; F2 without F1
|
||
is inert scaffolding; both are tiny. **If Decision 1 had gone to (a), the
|
||
answer would be different in kind, and it is worth stating for the record:
|
||
folding player-cell tracking in would put the slice at route-3 scale or
|
||
above** (the §2.2 enumeration: projection-controller change, rebucket
|
||
contract change, `Rebucketed` publication audit, route-2/4b-3
|
||
classification-input audit, portal-space race analysis, spatial-root/park
|
||
analysis, plus its own connected gate) **— and it would still be the wrong
|
||
bundling**, because the child fix is complete and gate-verifiable without
|
||
it, and the player-cell question deserves its own retail-conformance
|
||
argument rather than riding a key repair.
|
||
|
||
Stop and report rather than pushing through when:
|
||
|
||
1. F1's deferred path needs new relation-state machinery beyond a
|
||
wildcard/deferral flag (the unresolved-queue shape should suffice; if
|
||
`Resolve`'s staged early-return forces a redesign of
|
||
`ParentAttachmentState`'s state machine, that is a conversation, not an
|
||
ad-hoc build).
|
||
2. Any test in §6 requires touching a canonical cell writer.
|
||
3. The §3.2 gate turns out to mask a child shadow row that something DOES
|
||
legitimately create today (that would contradict route 7's P4 record —
|
||
file, do not fix inline).
|
||
4. The complete Release suite deviates from its measured baseline beyond
|
||
the two named flakes (#302, #308). Baseline at `e0f96a55` was 11,090 /
|
||
4 / 0 — MEASURE at the implementation HEAD, never inherit.
|
||
|
||
---
|
||
|
||
## 9. Open items — reported honestly, not smoothed
|
||
|
||
1. **The look-in render index nuance (§2.1 item 1) is argued from code,
|
||
not from a live A/B.** The claim "the local player already exhibits
|
||
this exact staleness in `_cellDynamics` with no observed symptom" is
|
||
solid at HEAD reading, but the `RenderSceneShadowRuntime:403`
|
||
residency assertion (`OwnerLandblockId != expected.LandblockId` throw)
|
||
was not chased to its trigger population. P-R1: the implementer runs
|
||
one indoor equipped session with the fix and confirms no shadow-runtime
|
||
assertion fires. If one does, that is a finding about the PLAYER's
|
||
record as much as the child's — report, do not patch the child.
|
||
2. **Whether the local player is a `RuntimePhysicsState` spatial root was
|
||
NOT established** (§2.2). If it is, the pre-existing stale player cell
|
||
can theoretically be swept by `ParkCollisionResidents` after a long
|
||
teleport-free WASD run — unexercised by the connected routes, which
|
||
teleport between stops. This rides in the follow-up issue as its first
|
||
verification step; it is NOT #319's blast radius (the fix does not
|
||
touch the player's record).
|
||
3. **The exact deferral mechanism for a parent-unknown CreateObject
|
||
relation is implementer's choice** within F1's pinned constraints; this
|
||
contract did not verify how `RetryWaitingDescendants` interacts with a
|
||
relation that was deliberately NOT staged (today it retries realize for
|
||
staged-but-unrealizable children). If the existing machinery cannot
|
||
carry it, stop condition 1 applies.
|
||
4. **Everything else in the investigation was confirmed at HEAD**: the
|
||
hardcoded 0 and its structural cause (the wire carries no parent
|
||
instance for CreateObject), both miss sites, the `Resolve` early-return,
|
||
the commit's missing validation, the three writers of the player's
|
||
canonical cell, the landblock-preserve, the hydration/shadow wake
|
||
mechanics (including `IsSpatiallyProjected = true` via the
|
||
presentation-only rebucket at `LiveEntityRuntime.cs:1043`), and the
|
||
render fallback. One investigation unknown was RESOLVED in the child's
|
||
favor: §5.1 item 3's residence question — the caller zeroes before
|
||
`Begin` (`RuntimeEntityObjectLifetime.cs:2751-2752`), §3.3.
|