feat(physics): S2 chunk 4 - movement publishes from the transition's cells; children inherit at registration

Movement: CommitSetPosition's RefreshPositionRows/ReplacePositionRows and
the staged apply publish the retail render product from the exact cell
list collision just used, the transition's cell_array retail feeds
add_shadows_to_cells in CPhysicsObj::SetPositionInternal @0x00515330
(pseudo-C 283526-283539); the separate move-path bbox recompute is deleted.
calc_cross_cells @0x00515230 stays the distinct full-recompute path
(PhysicsShadowCommitAction.Recalculate).

Children (Contract B recursion): ShadowObjectRegistry.AttachChild/DetachChild
give an attached object the root's current cells as part entries only,
republished whenever the root's array changes, detached at withdrawal and
cascaded from the root's Deregister; nested attachment resolves to the root
with a bounded, cycle-safe chain. EquippedChildRenderController attaches at
realization (FromSetupRenderParts over the child's Setup) and detaches at
its single removal funnel. WalkProductionWorldData's dynamic sweep reads
TryGetRetailCellArray directly; the 64-hop parent-chain walk and its
FindParentLocalId plumbing are deleted. CollisionWorldState.Clear now
also clears the retail products.

Gates (implementer's isolated worktree at identical content): Release
build 0/0; Core 4,970/4,970; App hermetic 6,761/6,761; targeted
walk/child/live-entity/placement/comparator 166/166; Runtime 1,884/1,884.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-02 23:37:34 +02:00
parent 706fc49397
commit 75ea269d35
14 changed files with 1072 additions and 179 deletions

View file

@ -212,27 +212,58 @@ walk. The Facility Hub stair Setup `0x02000623` is the installed-DAT
regression: its authored parent is `0x8A02015F`, while its visual parts also
register in `0x8A02015E` and the adjoining vertical cells.
**Moving multipart objects and equipped children (S2 chunk 4, 2026-09-02).**
Moving multipart objects follow the sibling retail route rather than their
authored/feet cell alone: `CPhysicsObj::add_shadows_to_cells` installs every
`CPartArray` part in every cell of the object's retained `CELLARRAY`, and
`CPhysicsPart::Draw` stamps each part only after that part passes a cell's
portal test. `WalkProductionWorldData` therefore buckets live projections by
all `ShadowObjectRegistry.GetOwnerCells` rows, while the walk classifier owns
a projection+part drawn-pass stamp. Retail advances that stamp after
portal test. `WalkProductionWorldData.ResolveDynamicRenderCells` therefore
reads `ShadowObjectRegistry.TryGetRetailCellArray` directly for every dynamic
record — the SAME borrow-not-recompute recipe the static sweeps already use
— falling back to `GetOwnerCells` (counted by the shared
`UnregisteredStaticRenderFallbackCount`) only when the registry has not
registered an array yet, while the walk classifier owns a separate
projection+part drawn-pass stamp. Retail advances that stamp after
`LScape::draw` + `FlushAlphaList` and before the interior depth clear
(`PView::DrawCells` @0x005A4886), so a part may draw once in the landscape
pass and once again in the post-clear interior-cell pass; acdream re-arms the
classifier at that exact walk boundary. `add_shadows_to_cells` recursively
passes the root's same `CELLARRAY` to every attached child, so equipped-child render
projections resolve their accepted parent chain to the root's owner cells;
they do not fall back to one authored cell. Never replace the per-part stamp
with a whole-projection drawn-once gate: at a stair portal the torso may pass
in the first cell while head/hair fails and must retry through another crossed
classifier at that exact walk boundary. Never replace the per-part stamp with
a whole-projection drawn-once gate: at a stair portal the torso may pass in
the first cell while head/hair fails and must retry through another crossed
cell. One binding exception is the local player: retail
`RenderDeviceD3D::DrawMeshInternal` bypasses `Get/SetDrawnThisFrame` when
`CPhysicsPart::IsPartOfPlayerObj` is true, repainting the player's parts at
every crossed-cell turn so later wall/depth ordering remains correct.
Retail's move-path product is the SAME `add_shadows_to_cells` call the
registration path uses, not a second independent flood:
`CPhysicsObj::SetPositionInternal` (0x00515330) takes the transition's own
`cell_array` on a successful move and calls
`remove_shadows_from_cells`/`add_shadows_to_cells` with it directly — the
`calc_cross_cells_static` cylsphere/bbox dispatch never re-runs on a move.
`ShadowObjectRegistry.ReplacePositionRows` mirrors this exactly:
`PublishRetailProductFromExactCells` publishes the retail render product from
the SAME exact cell list collision just republished into, replacing the
independent Contract A recompute an earlier chunk ran on every move; retail's
`num_cells > 0` keep-when-empty gate covers both products identically, so a
move with no resolvable transition array leaves both untouched.
`add_shadows_to_cells` also recursively passes the root's same `CELLARRAY` to
every object in `children` (Contract B); acdream's attached projections
(equipped weapons/shields/ammunition) own no independent collision shapes, so
`ShadowObjectRegistry.AttachChild`/`DetachChild` publish PART ENTRIES only —
never a collision row — into every cell of the resolved root's current retail
CELLARRAY, re-publishing automatically whenever that array changes
(registration, move, staged apply, `ReplaceMultiPartPayload`).
`EquippedChildRenderController.TryRealize` calls `AttachChild` at the exact
moment an attached projection's `WorldEntity` is registered against its
accepted parent; `CommitProjectionRemoval` (the withdrawal/unparent/teardown
funnel) calls `DetachChild`. A nested attachment (a child of a child)
resolves to the ultimate root by walking the existing attach chain at the
registry, bounded and cycle-safe — `WalkProductionWorldData` no longer walks
an accepted-parent chain of its own; it reads the child's own id straight off
`TryGetRetailCellArray`, identically to any other dynamic record.
**Slice I3 prepared collision extension (2026-07-25).** At its introduction,
the package remained format 1 and retained mesh type values 13; bake-tool 4
appended typed GfxObj,