acdream/docs/research/2026-08-05-local-player-child-propagation.md
Erik 195667db94 research: file #319 — player-parented children never get a canonical cell, and route 7's gate cannot see it
Found by asking why route 7's connected gate stayed thin (one cause=propagate
across 5-6 equipped landblock crossings) instead of recording the thinness and
moving on.

EquippedChildRenderController.cs:134 hardcodes ParentInstanceSequence: 0 for a
parented CreateObject. Correct for creatures and statics, which are genuinely
sequence 0; wrong for players, whose ObjectInstance is Character.TotalLogins
(ACE Player_Networking.cs:37). The relation files under (playerGuid, 0) while
the record carries TotalLogins, so both route-7 write sites — D1's attach
re-cell and D2's propagation lookup — key on an incarnation that never matches.
TryCommitParent does not validate the sequence, so the attach succeeds and
prints normally.

This is a cd3129e9 (route 7) REGRESSION 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. Scope is wider than the local
player — every remote player's equipment is affected. Proven by class: every
probe-firing parent across both captured gate logs is 0x7/0x8 (sequence 0);
the sole 0x5 player parent is the sole failure.

User-visible consequence is NIL and that was verified rather than assumed —
rendering has an explicit fallback and attached children are structurally
excluded from spatial roots, physics worksets, collision retirement, radar and
picking.

THE FINDING THAT OUTRANKS THE DEFECT, and it is a flaw in my own gate design:
route 7's owed gate accepts a session "only if cause=propagate lines appear".
A zero-cell player child emits NO line, so the defect's signature is ABSENCE,
which that criterion reads as "not exercised" rather than "broken". Two
captured gate logs contain the defect and neither flags it. A gate that cannot
fail in the presence of its own target bug is worse than no gate — it
manufactures confidence. This is the same shape as route 3's round-2
regression, which I criticised at length in the closeout while shipping this.

The fix is deliberately NOT attempted here: it has more blast radius than the
bug. The player's canonical cell does not track the player during ordinary
movement, so correcting the key alone yields a stale cell rather than a right
one; and three sites are inert only because the cell is zero and would wake on
a fix (the hydration projectionCellId filter, RestoreShadow's broadphase row,
and the initial-create residence FullCellId refusal).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 09:59:35 +02:00

494 lines
24 KiB
Markdown

# The local player's equipped child never gets a canonical cell
**Date:** 2026-08-05
**Mode:** investigation, report-only. No production or test code changed.
**Worktree:** `.claude/worktrees/peaceful-visvesvaraya-e0a196`,
branch `claude/acdream-physics-divergence-5aa784`, HEAD `09911821`.
**Evidence logs:** `c5-gates.log`, `c4-gates.log` (both in the worktree root,
both captured with `ACDREAM_PROBE_CHILD_CELL=1`).
---
## Verdict up front
**No — the local player's equipped child never receives a canonical cell, and
it cannot follow the player across a cell boundary.** Its
`RuntimeEntityRecord.FullCellId` stays `0` for the whole attached lifetime.
**This is a C4 route 7 regression** (commit `cd3129e9`), triggered by a
**pre-existing latent bug** that route 7 removed the masking fallback for. The
latent bug is a hard-coded `ParentInstanceSequence: 0` at
`src/AcDream.App/Rendering/EquippedChildRenderController.cs:134`, which is wrong
for exactly one class of parent: **players**. It dates from `8dd99605`/`fe551496`,
long before route 7.
**Scope is wider than the local player.** Any child whose parent is a *player*
(guid `0x5xxxxxxx`) and whose relation arrives on a `CreateObject` — i.e. the
local player's own equipment at login, and every remote player's equipment as
they come into view — is affected. Children of creatures/NPCs/statics
(`0x7xxxxxxx`/`0x8xxxxxxx`) are unaffected, which is why 19 of the 20
`[child-cell]` lines in `c5-gates.log` look healthy.
**The observable consequence for the user is nil** — see §5. Rendering,
physics, picking, radar, VFX, and landblock teardown all either exclude
attached children structurally or have an explicit `entity.ParentCellId`
fallback. The real cost is diagnostic and architectural: route 7's own headline
invariant is silently false for the player, and — worse — **a zero-cell child
makes the still-owed connected acceptance gate unfalsifiable rather than
failing loudly**, because that gate's criterion is the *presence* of
`cause=propagate` probe lines.
---
## 1. The symptom, restated from the logs
`c5-gates.log` line 343 and `c4-gates.log` line 238 both show the App-side
attach for the local player's weapon:
```
equipment: attached child=0x800045EE parent=0x5000000A location=RightHand placement=RightHandCombat
```
with **no** `[child-cell]` line before it. Every other attach in both logs has
one, e.g. `c5-gates.log` 336-337:
```
[child-cell] parent=0x70007059 child=0x8000515D old=0x00000000 new=0x0007014B cause=attach
equipment: attached child=0x8000515D parent=0x70007059 location=RightHand placement=RightHandCombat
```
The probe was demonstrably live at the time — in `c4-gates.log` two probe lines
(210, 211) fire *before* the player's attach at 238.
Across 5-6 landblock crossings with the weapon equipped, zero `cause=propagate`
lines name `0x5000000A`.
### The pattern that names the cause
Sort every successful attach in both logs by parent guid prefix:
| parent prefix | ACE range (`references/ACE/Source/ACE.Entity/ObjectGuid.cs:21-34`) | probe fired? |
|---|---|---|
| `0x5xxxxxxx` | player (`PlayerMin 0x50000001` .. `PlayerMax 0x5FFFFFFF`) | **never** (1 case: `0x5000000A`) |
| `0x7xxxxxxx` | static landblock object (`StaticObjectMin 0x70000000`) | always |
| `0x8xxxxxxx` | dynamic (`DynamicMin 0x80000000`) | always |
Players are the only failing class. That is not a coincidence — see §2.
---
## 2. Root cause: the committed relation is filed under the wrong parent incarnation
### 2.1 ACE gives players a non-zero object-instance sequence
`references/ACE/Source/ACE.Server/WorldObjects/Player_Networking.cs:34-37`:
```csharp
Character.TotalLogins++;
CharacterChangesDetected = true;
Sequences.SetSequence(SequenceType.ObjectInstance, new UShortSequence((ushort)Character.TotalLogins));
```
**A player's `ObjectInstance` sequence is its lifetime login count.** For the
`+Acdream` test character that is a large number; for a fresh creature, item, or
landblock NPC it is `0`.
That value is written into the CreateObject physics-descriptor timestamp block
at `references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Networking.cs:419`
(`writer.Write(Sequences.GetCurrentSequence(SequenceType.ObjectInstance)); // 8`).
### 2.2 acdream parses it and stores it as the record's `Incarnation`
- `src/AcDream.Core.Net/Messages/CreateObject.cs:749` reads slot 8
(`instanceSeq`) out of the 9-sequence block.
- `src/AcDream.Core.Net/Messages/CreateObject.cs:1156-1158` passes it into
`Parsed.InstanceSequence`.
- `src/AcDream.Core.Net/WorldSession.cs:233` carries it onto
`EntitySpawn.InstanceSequence`.
- `src/AcDream.Runtime/Entities/RuntimeEntityRecord.cs:43`:
`public ushort Incarnation => Snapshot.InstanceSequence;`
So for the local player, `record.Incarnation == TotalLogins`, **not** `0`.
### 2.3 acdream hard-codes `0` when a CreateObject carries a parent
`src/AcDream.App/Rendering/EquippedChildRenderController.cs:129-135`:
```csharp
Relations.AcceptCreateObjectRelation(new ParentAttachmentRelation(
parentGuid,
spawn.Guid,
parentLocation,
placementId,
ParentInstanceSequence: 0,
spawn.PositionSequence));
```
This is structurally forced by the wire: the child's CreateObject physics
descriptor carries the parent's **guid and location only** — there is no field
for the parent's instance sequence. The correct value is available
(`_liveEntities.TryGetSnapshot(parentGuid).InstanceSequence`, which is exactly
what `ParentAttachmentState.Resolve` uses at
`src/AcDream.App/Rendering/EquippedChildRenderController.cs:834-836` for the
ParentEvent path) — it is simply not read here.
`ParentAttachmentState.Resolve` cannot correct it either: it early-returns on a
relation that is already staged
(`src/AcDream.Runtime/Entities/ParentAttachmentState.cs:425-426`), and
`AcceptCreateObjectRelation` stages directly
(`ParentAttachmentState.cs:391-394`).
Nor does the commit validate it: `RuntimeEntityObjectLifetime.TryCommitParent`
(`:1400-1412`) forwards guid, location, placement and the *child's* position
sequence — it never passes `relation.ParentInstanceSequence` to a gate. So the
attach succeeds and `equipment: attached` prints normally.
### 2.4 The relation is then filed under `(playerGuid, 0)`
`ParentAttachmentState.CommitProjection`
(`src/AcDream.Runtime/Entities/ParentAttachmentState.cs:571-597`):
```csharp
_lastAcceptedByChild[relation.ChildGuid] = relation;
var parent = new ParentIncarnation(
relation.ParentGuid,
relation.ParentInstanceSequence); // == 0
...
children.Add(relation.ChildGuid);
```
### 2.5 Both route-7 write sites read the parent's *real* incarnation, so both miss
**D1 (attach re-cell)**`RuntimeEntityObjectLifetime.CommitAcceptedParentCellless`,
`:1532-1549`:
```csharp
if (Entities.ParentAttachments.TryGetCommittedParent(
canonical.ServerGuid, out uint parentGuid, out ushort parentInstanceSequence)
&& Entities.TryGetActive(parentGuid, out RuntimeEntityRecord parent)
&& parent.Incarnation == parentInstanceSequence // TotalLogins == 0 -> FALSE
&& parent.FullCellId != 0u)
```
**D2 (crossing propagation)**`RuntimeEntityDirectory.PropagateFullCellToChildren`,
`:478-480`:
```csharp
IReadOnlyList<uint> children = ParentAttachments.ChildrenAttachedToParent(
current.ServerGuid,
current.Incarnation); // key (guid, TotalLogins)
```
`ChildrenAttachedToParent` (`ParentAttachmentState.cs:681-691`) looks up
`_committedChildrenByParent[(guid, TotalLogins)]`, but the child was filed under
`(guid, 0)`, so it returns `Array.Empty<uint>()`**forever**, for every cell
write the player ever makes.
One cause, both observed negatives. No other hypothesis explains the exact
player-vs-non-player split in the logs.
### 2.6 A counter-reading, recorded because it is the easy mistake
An independent read of this code concluded "the key pair matches by
construction; the lookup is not the failure mode", reasoning from
`ParentAttachmentState.Resolve`'s `parentInstance.Value ==
relation.ParentInstanceSequence` check (`:440-454`). **That is true for the
`ParentEvent` path and false for the `CreateObject` path**, because
`AcceptCreateObjectRelation` stages directly (`:391-394`) and `Resolve`
early-returns on anything already staged (`:425-426`). Anyone auditing this
should check `AcceptCreateObjectRelation`'s producer, not `Resolve`'s consumer.
The same read independently traced that the propagation *mechanism* would
otherwise fire for the local player on **every frame**`Project` ->
`RebucketLiveEntity` -> `CommitRebucket` -> `SetFullCell` ->
`PropagateFullCellToChildren`, unconditionally (`RuntimeEntityDirectory.cs:355`).
That agreement matters: the plumbing is correct and live; only the key is wrong.
---
## 3. Why this is a route-7 regression, not a pre-existing gap
Before `cd3129e9`, `EquippedChildRenderController.TickChild` wrote the child's
canonical cell every frame from the *parent's App-side* cell:
```diff
- _liveEntities.RebucketLiveEntity(child.ChildGuid, parentCellId);
+ EquippedChildPresentationRebucketDisposition disposition =
+ _liveEntities.RebucketEquippedChildPresentation(
+ child.ChildGuid, parentCellId);
```
The old `RebucketLiveEntity` reached `CommitRebucket`
(`src/AcDream.App/World/LiveEntityRuntime.cs:942-945`), and `CommitRebucket`
writes the canonical cell (`RuntimeEntityObjectLifetime.cs:1959-1963`
`Entities.SetFullCell(canonical, fullCellId, canonicalLandblockId)`). The
replacement, `RebucketLiveEntityPresentationOnly`, deliberately never calls
`CommitRebucket` (documented at `LiveEntityRuntime.cs:1026-1028`).
**The old path took the parent's cell from `parent.ParentCellId` on the
`WorldEntity` and keyed on nothing but the child guid.** It was structurally
immune to the incarnation bug, and it tracked the local player *exactly*,
because `LocalPlayerProjectionController.Project` writes
`entity.ParentCellId = movement.CellId` every frame
(`src/AcDream.App/Input/LocalPlayerProjectionController.cs:79`).
So route 7 replaced a correct-by-accident write with a correct-by-design write
whose design has a broken key. The register row AP-142 and the route-7 contract
both assume the propagation hook is reached; for a player parent it never is.
---
## 4. A second finding: even with the key fixed, the player's canonical cell does not track the player
This is independent of the incarnation bug and worth knowing before anyone
writes the fix.
The local player's canonical `FullCellId` is written in only three ways
(traced end-to-end):
| when | site |
|---|---|
| login activation (first non-zero) | `src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs:2741-2745` |
| accepted inbound Position / ForcePosition | `RuntimeEntityDirectory.RefreshSnapshot` -> `RuntimeEntityRecord.cs:234` |
| teleport / portal placement commit | `RuntimeSetPositionState.cs:5001-5007`; `LocalPlayerTeleportController.cs:255` |
**Ordinary WASD movement never writes it.**
`src/AcDream.App/Input/LocalPlayerProjectionController.cs:85-98` builds a
*landblock* id (low 16 bits forced to `0xFFFF` in both the indoor branch at `:90`
and the outdoor branch at `:97`) and passes it to `RebucketLiveEntity` at `:109`.
`LiveEntityRuntime.cs:935-938` then computes:
```csharp
uint committedFullCell =
(spatialCellOrLandblockId & 0xFFFFu) != 0xFFFFu
? spatialCellOrLandblockId
: record.FullCellId; // landblock id -> preserve the old cell
```
so `CommitRebucket` re-commits the *identical* cell and takes the
`previous == fullCellId` early-out (`RuntimeEntityObjectLifetime.cs:1965-1972`).
Nothing in `src/AcDream.Runtime/Gameplay/` reads or writes the canonical cell at
all (`grep -rn "FullCellId\|SetFullCell" src/AcDream.Runtime/Gameplay/` returns
zero hits), and the local player is explicitly excluded from the ordinary
physics updater before `CommitOrdinaryCell` can run
(`src/AcDream.App/Rendering/LiveEntityAnimationScheduler.cs:183-227`, returning
at `:227` ahead of the `_ordinaryPhysics.Tick` at `:376`).
**Consequence for the fix:** repairing the incarnation key alone would give the
player's weapon the *login/teleport* cell, not the player's current cell. Route
7's model ("the parent's canonical cell write is the propagation trigger")
is sound for remotes and creatures, whose cells are server-driven, but the
local player is client-authoritative and its canonical cell is a coarse,
mostly-frozen value. Whoever fixes this needs to decide which of the two is the
child's source of truth for a client-authoritative parent.
---
## 5. Does it matter? Observable consequence
**Rendering: no.** `LiveRenderProjectionJournal.Project`
(`src/AcDream.App/Rendering/Scene/LiveRenderProjectionJournal.cs:269-276`)
falls back explicitly:
```csharp
uint fullCellId = record.FullCellId != 0
? record.FullCellId
: entity.ParentCellId ?? 0;
```
and `TickChild` keeps `child.Entity.ParentCellId = parent.ParentCellId` current
every frame (`EquippedChildRenderController.cs:403`). The weapon draws, culls,
and moves with the hand exactly as before. The user should see nothing wrong.
**Liveness/residency predicates: no.** The two `FullCellId == 0` guards that
gate real behaviour —
`LiveEntityRuntime.GetRootObjectClockDisposition` (`:2602-2612`) and
`HasSpatialRuntimeProjection` (`:3340-3345`) — both also require
`ProjectionKind is LiveEntityProjectionKind.World`. An attached child is always
`Attached`, so it is excluded on the first clause regardless of its cell.
Route 7's own P4 note (`RuntimeEntityDirectory.cs:451-465`) says the same thing
for the physics/broadphase side: a committed child never becomes a spatial root
and never joins a workset or shadow list.
**Landblock retirement / teardown: no — checked from both ends.**
`CanonicalLandblockId` has exactly one production reader outside the record and
the propagation pair (`LiveRenderProjectionJournal.cs:273-274`), and that one
has a fallback. The App unload path,
`GpuWorldState.DetachLandblock` (`src/AcDream.App/Streaming/GpuWorldState.cs:1188-1317`),
never destroys live entities at all — non-persistent projections park in
`_pendingByLandblock` and merge back on reload (`:1228-1232`, `:1290-1294`) —
and it selects from the presentation buckets, which for an attached child were
set from `parent.ParentCellId`. The Runtime retirement sweep,
`RuntimeSetPositionState.IsAffectedCollisionResident` (`:3930-3946`), *is*
cell-keyed but is triple-gated against attached children: spatial-roots-only
iteration (`:3750`), `IsSpatialRoot` (`:3942`), and an explicit
`!ParentAttachments.HasCommittedParent(record.ServerGuid)` (`:3944-3945`). The
weapon is neither wrongly destroyed nor wrongly retained.
**Radar: no.** `ILiveEntityRadarSource`
(`src/AcDream.App/World/ILiveEntityRadarSource.cs:10-15`) is entirely
`WorldEntity`-based; it never reads a canonical cell.
**Picking / interaction: no.** `src/AcDream.Core/Selection/WorldPicker.cs`
contains zero cell references; picking is ray/bounds-based.
**VFX and scripts on the weapon: no.** `EntityEffectController`
(`src/AcDream.App/Rendering/Vfx/EntityEffectController.cs:445-479`) redirects an
attached child to its parent (`:450-451`, `:460-461`) and never consults the
child's own cell — retail-faithful, matching `update_object`'s parent early-out.
**Object clock: the zero is the correct answer anyway.** An attached child's
clock is deliberately suspended (`CommitAcceptedParentCellless` ->
`SuspendObjectClock`, `RuntimeEntityObjectLifetime.cs:1523`), and
`GetRootObjectClockDisposition` already returns `Suspend` on the `ProjectionKind
is not World` clause two conditions earlier.
**What DOES change:**
1. **The still-owed acceptance gate is unfalsifiable, not failing.** The commit
message's criterion is "a session counts only if `[child-cell]
cause=propagate` lines appear". A zero-cell player child produces *no* line —
the absence reads as "nothing happened" rather than "this is broken". Two
captured gate logs (`c4-gates.log`, `c5-gates.log`) contain the defect and
neither flags it. This is the thing to escalate.
2. **Route 7's headline invariant is false for every player-parented child.**
AP-142's model assumes the propagation hook is reached; for a player parent it
never is, so the register row describes behaviour the code does not have.
3. **Diagnostic reporting is wrong.** `RuntimeTraceRecorder.OnEntity`
(`src/AcDream.Runtime/GameRuntimeEvents.cs:246-253`) is the only non-stub
consumer of `delta.Entity.CellId`, and it records `0` for the player's weapon
from the `Withdrawn` delta at `RuntimeEntityObjectLifetime.cs:1551-1558`.
*Correction to an earlier reading of mine:* headless **bots** do not
misreport — all four `HeadlessBotPolicy.OnEntity` overloads are empty method
bodies (`src/AcDream.Headless/Policies/HeadlessBotPolicy.cs:48, 151, 281,
516`), and the three view queries there read the local player's own guid
only. The exposure is trace/diagnostic, not bot behaviour.
So: **not a user-visible defect today, but a real correctness defect in the
canonical layer**, and precisely the class of stale/zero-cell residue that
AP-142 clause (a) exists to reject.
### 5.1 The fix has more blast radius than the bug
Three call sites are currently *inert only because the child's cell is zero*.
Fixing the key will wake them, so they need checking before, not after:
1. `LiveEntityHydrationController.OnLandblockLoaded`
(`src/AcDream.App/World/LiveEntityHydrationController.cs:524-600`) computes
`projectionCellId = projection.ProjectionCellId ?? Snapshot.Position?.LandblockId ?? candidate.FullCellId`
(`:551-553`) and filters on `projectionCellId != 0` (`:554`). A cell-less
child is skipped — correct, since `EquippedChildRenderController` owns it.
With a non-zero cell the weapon becomes a candidate and takes the **full
legacy** `RebucketLiveEntity` branch (`:594-596`), which writes
`entity.ParentCellId = spatialCellOrLandblockId`
(`LiveEntityRuntime.cs:885-898`) — overwriting, for one frame, the value the
render tick maintains. Likely a transient (TickChild restores it next frame),
but it is a two-writer window route 7 exists to remove.
2. `LiveEntityPresentationController.RestoreShadow` (`:216-236`) no-ops today on
`record.FullCellId == 0` (`:220`). With a non-zero cell it would install a
collision-shadow row for an equipped weapon on a Hidden->Visible edge
(`:199`) — contradicting route 7's own P4 claim that "no child broadphase
registration exists to rebuild" (`RuntimeEntityDirectory.cs:450-465`). The
guard at `:218-220` checks neither `ProjectionKind` nor parentage.
3. `RuntimeInitialCreateResidenceState.Begin` refuses to open a lease when
`record.FullCellId != 0u` (`:583`), and `TryConvertToCellessRoute` refuses at
`:1041`. Whether a re-`CreateObject` for an equipped item reuses the same
record (in which case an inherited non-zero cell would block its residence)
was not established and should be checked.
---
## 6. Reconciling the passing headless test
`RuntimeLiveEntitySessionControllerTests
.DirectSink_D5_StandaloneParentEventCommitsChildToParentsExactCell`
(`tests/AcDream.Runtime.Tests/Session/RuntimeLiveEntitySessionControllerTests.cs:324-368`)
passes for two reasons, neither of which touches the defect:
1. **Its parent is not a player.** `const uint parentGuid = 0x70000020u`
(`:340`) — a static-range guid, spawned with `incarnation: 1` (`:342`).
2. **Its relation is a standalone `ParentEvent`, not a CreateObject.**
`sink.ParentUpdated(new ParentEvent.Parsed(..., ParentInstanceSequence: 1, ...))`
(`:358-364`) supplies the parent's real incarnation, and
`ParentAttachmentState.Resolve` (`:440-454`) validates it against
`resolveInstance(parentGuid)` before staging. Key match, propagation works.
The framing "a headless bot IS a local player" does not hold for this test: the
test drives a *remote-shaped* entity through the first-entry conductor, and
`RuntimeLiveEntitySessionController.ResolveAndCommitChildAttachment`
(`:387-424`) only ever handles `ParentEvent`-sourced relations, which always
carry the true sequence. **The headless drive has no
`AcceptCreateObjectRelation` equivalent at all** — so it cannot reproduce the
bug, and equally, headless never commits a CreateObject-carried equip.
The one path that would have caught this is the gate the route-7 contract
itself specified and the commit lists as STILL OWED
(`docs/research/2026-08-04-c4-route-7-contract.md:788-793`): "one headless
session where the local player equips (via the bot command surface) and crosses
a boundary".
---
## 7. Prediction worth testing before designing the fix
If the user **unequips and re-equips** the weapon mid-session, ACE sends a
`ParentEvent` (`GameMessageParentEvent.cs:16` writes the wielder's real
`ObjectInstance` sequence). That path goes through `Relations.Enqueue` ->
`Resolve`, which validates and preserves the true sequence, so the relation
would be filed under `(0x5000000A, TotalLogins)` and **both D1 and D2 should
start working for that weapon**, with `cause=attach` appearing immediately.
If that is observed, it confirms the diagnosis end-to-end with no code change.
It would also mean the bug is "login-equipped items only" in practice — which
is still every item on every character at every login.
---
## 8. Sketch of the fix (for approval, not applied)
Two independent pieces:
**(a) The key.** `EquippedChildRenderController.OnSpawn` should resolve the
parent's live incarnation instead of assuming `0`:
`_liveEntities.TryGetSnapshot(parentGuid, out spawn) ? spawn.InstanceSequence : ...`
— the same lookup `ResolveRelations` already passes to `Resolve` at `:834-836`.
The "parent not yet known" case must keep the relation unresolved (the deferred
path already exists) rather than committing under a guessed key. This alone
restores D1/D2 for player parents, and also fixes remote players' equipment.
Worth auditing at the same time: `ParentAttachmentRelation.ParentInstanceSequence`
is written by exactly two producers (`AcceptCreateObjectRelation` and
`Enqueue`), and only one of them is correct today. A commit-time assertion that
`relation.ParentInstanceSequence == parentRecord.Incarnation` would have made
this loud instead of silent.
**(b) The source of truth for a client-authoritative parent** (§4). Options:
either make the local player's canonical cell track its movement (an exact-cell
rebucket instead of the landblock-only one, which has knock-on effects on
`isOrdinaryRoot` and the animation-scheduler exclusion), or accept that the
child follows the parent's coarse cell and document the divergence. This is a
design call, not a bug fix, and should be decided before (a) lands — (a) alone
will start writing a stale-but-nonzero cell where there is currently a zero.
---
## 9. What this is NOT
- **NOT** an incarnation-drift bug. `record.Incarnation` is stable for the whole
session: inbound updates are gated by `IsCurrentInstance` and rejected on
mismatch, never merged (`InboundPhysicsStateController.cs:264, 1011`). The
mismatch is present from the very first commit.
- **NOT** a `parent.FullCellId == 0` timing race. The player's canonical cell is
non-zero throughout play — it has to be, or
`GetRootObjectClockDisposition` would return `Suspend` and
`RuntimeLocalPlayerFrameController.AdvanceBeforeNetwork:96-110` would never
call `controller.Update`, i.e. the player could not move at all.
- **NOT** a rendering or visibility bug. The draw path has an explicit
`entity.ParentCellId` fallback and the presentation rebucket route 7 kept
(`RebucketEquippedChildPresentation`) is keyed on the child guid alone and
works fine.
- **NOT** fixable by reverting only the `TickChild` half of route 7. That would
restore the masking write and re-create the two-writer problem route 7 exists
to remove.