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:
parent
706fc49397
commit
75ea269d35
14 changed files with 1072 additions and 179 deletions
|
|
@ -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 1–3; bake-tool 4
|
||||
appended typed GfxObj,
|
||||
|
|
|
|||
|
|
@ -38,9 +38,10 @@ One transaction per object (`CPhysicsObj::calc_cross_cells_static`
|
|||
|---|---|---|---|---|
|
||||
| **Chunk 3 (2026-09-02).** `LandblockPhysicsPublisher.PublishStaticEntity` → `ShadowObjectRegistry.RegisterMultiPart` (`LandblockPhysicsPublisher.cs:930-1079`, `ShadowObjectRegistry.cs`, `RegisterMultiPart`/`Register`/`ComputeContractACellArray`/`PublishRetailCellArray`) | the WHOLE visual part array (`ShadowShapeBuilder.FromStaticRenderParts`, chunk 1b) drives ONE Contract A flood (cylsphere route from the collision shapes' cylinders, else bbox route over the part array); a caller with no part array (every unit-test fixture, `partArray ?? shapes`) keeps the pre-chunk-3 legacy dispatch byte-for-byte | `_entityToCells[id]`/`_cells[cell]` (collision) AND `_retailCellArrays`/`_retailPartEntriesByCell` (render) from the SAME array — the second (redundant) flood chunk 1 left running is deleted | once per static at publication | physics broadphase `TransitionTypes.FindObjCollisionsInCell` (`TransitionTypes.cs:3755-3779`); `WalkProductionWorldData` (below) |
|
||||
| **DELETED (chunk 2).** Was `WalkProductionWorldData.ResolveStaticRenderCells` → `ShadowObjectRegistry.ComputeStaticRenderCells`. Now: `WalkProductionWorldData.ResolveIndoorStaticRenderCells`/`ResolveOutdoorStaticRenderCells` → `ShadowObjectRegistry.TryGetRetailCellArray` (`WalkProductionWorldData.cs`, `ShadowObjectRegistry.cs:634-663`) | none (borrowed) | chunk 1's retained per-entity retail CELLARRAY, now the SAME array chunk 3 also uses for collision | every `BeginFrame` sweep of `RenderSceneIndex.IndoorCellStatic`/`OutdoorStatic` (a borrow, not a recompute) | `WalkFrameDriver.GetCellStatics`; `RetailPViewRenderer` particle owner union; unregistered fallback (indoor: authored `ParentCellId`; outdoor: `GetOwnerCells`) counted by `WalkProductionWorldData.UnregisteredStaticRenderFallbackCount` |
|
||||
| **Chunk 3 (2026-09-02).** `LiveEntityCollisionBuilder.Register` / `ShadowObjectRegistry.UpdatePosition` / `CommitSetPosition` (`LiveEntityCollisionBuilder.cs:149-280`, `ShadowObjectRegistry.cs`) | `ShadowShapeBuilder.FromSetup` (collision dispatch) + `FromSetupRenderParts` (chunk 1b's part array) → the same ONE Contract A flood as above at registration; movement (`UpdatePosition`/`CommitSetPosition`'s `Recalculate` action) forwards to the same registration entry points, so it inherits the fix. `CommitSetPosition`'s `None`/`Preserve`/`Replace` actions (`RefreshPositionRows`/`ReplacePositionRows`) are UNCHANGED — they consume an externally-decided collision cell list and separately recompute the retail product via the same Contract A primitive on every move (chunk 1's design, not a second registration flood) | `_entityToCells` + `_cells`, and (when a part array is retained) `_retailCellArrays`/`_retailPartEntriesByCell` | spawn and every accepted move/appearance change | physics broadphase; `WalkProductionWorldData` dynamic render lookup |
|
||||
| **Chunk 3 (2026-09-02).** The staged `ShadowObjectRegistry.TryPrepareSetPosition`/`TryApplySetPosition` pipeline | `PreparedShadowOwnerState` now also carries `RetailPartArray`/`RetailCellIds`/`RetailRoute`/`RetailRows`; `TryCaptureOwnerState`/`InstallOwnerState` seed them so the staging registry's own `RecomputeRetailCellArrayIfPresent` call (reached through `CommitSetPosition`) no longer silently no-ops; `PrepareRetailPartEntryReplacements` diffs the staged retail rows the same way `PrepareCellReplacements` diffs collision rows | `_retailPartEntriesByCell` publishes alongside `_cells` on `TryApplySetPosition` | every live entity moving through the transactional SetPosition publication tail (Runtime) | same as direct `CommitSetPosition`, now consistent with it |
|
||||
| `WalkProductionWorldData.ResolveDynamicRenderCells` (`WalkProductionWorldData.cs:247-283`) | `GetOwnerCells`; for an `EquippedChild` with no cells, a 64-hop parent-chain walk | whole-record buckets | every `BeginFrame` | `WalkFrameDriver.GetCellDynamics` |
|
||||
| **Chunk 4 (2026-09-02).** `LiveEntityCollisionBuilder.Register` / `ShadowObjectRegistry.UpdatePosition` / `CommitSetPosition` (`LiveEntityCollisionBuilder.cs:149-280`, `ShadowObjectRegistry.cs`) | `ShadowShapeBuilder.FromSetup` (collision dispatch) + `FromSetupRenderParts` (chunk 1b's part array) → the same ONE Contract A flood as above at registration; movement (`UpdatePosition`/`CommitSetPosition`'s `Recalculate` action) forwards to the same registration entry points, so it inherits the fix. **RESOLVED (chunk 4):** `CommitSetPosition`'s `None`/`Preserve`/`Replace` actions (`RefreshPositionRows`/`ReplacePositionRows`) now publish the retail render product from the SAME exact collision cell list the move just used (`PublishRetailProductFromExactCells`) — the independent Contract A recompute chunk 1/3 ran on every move (`RecomputeRetailCellArrayIfPresent`) is deleted. Mirrors retail `SetPositionInternal`'s `add_shadows_to_cells(this, &arg2->cell_array)` (pc:283536-283537), which consumes the transition's own array for both products in one call; the `num_cells > 0` keep-when-empty gate (pc:283540) now covers both products identically | `_entityToCells` + `_cells`, and (when a part array is retained) `_retailCellArrays`/`_retailPartEntriesByCell`, both from the SAME exact cell list on every move | spawn and every accepted move/appearance change | physics broadphase; `WalkProductionWorldData` dynamic render lookup (now a direct `TryGetRetailCellArray` read, see below) |
|
||||
| **Chunk 4 (2026-09-02).** The staged `ShadowObjectRegistry.TryPrepareSetPosition`/`TryApplySetPosition` pipeline | `PreparedShadowOwnerState` carries `RetailPartArray`/`RetailCellIds`/`RetailRoute`/`RetailRows`; `TryCaptureOwnerState`/`InstallOwnerState` seed them so the staging registry's own `CommitSetPosition` call inherits chunk 4's one-array-two-products move path for free; `PrepareRetailPartEntryReplacements` diffs the staged retail rows the same way `PrepareCellReplacements` diffs collision rows; `TryApplySetPosition` additionally calls `RepublishAttachedChildren` on the LIVE registry after publishing, since the staging registry carries none of the live attach-chain state | `_retailPartEntriesByCell` publishes alongside `_cells` on `TryApplySetPosition`; every attached child of the moved root republishes too | every live entity moving through the transactional SetPosition publication tail (Runtime) | same as direct `CommitSetPosition`, now consistent with it |
|
||||
| **Chunk 4 (2026-09-02).** `ShadowObjectRegistry.AttachChild`/`DetachChild` (`ShadowObjectRegistry.cs`) — retail's `add_shadows_to_cells` child-inheritance recursion (Contract B) | `EquippedChildRenderController.TryRealize` calls `AttachChild(childEntityId, rootEntityId, childPartArray)` at the exact moment the attached projection's `WorldEntity` is registered against its accepted parent, using `ShadowShapeBuilder.FromSetupRenderParts` over the SAME `PhysicsDataCache` resolvers `LiveEntityCollisionBuilder` uses; `CommitProjectionRemoval` (the single withdrawal/unparent/teardown funnel) calls `DetachChild`. A nested attachment (child of a child) resolves to the ultimate root by walking the existing chain, bounded and cycle-safe | new `_childParent`/`_parentChildren`/`_childPartArrays` dictionaries (own `CollisionWorldState` fields, reset with everything else); PART ENTRIES only for the child — never a collision row, since acdream's attached projections own no independent collision shapes | attach/detach, and every time the root's array republishes (registration, move, staged apply, `ReplaceMultiPartPayload`) | `WalkProductionWorldData.ResolveDynamicRenderCells` (a plain `TryGetRetailCellArray` read on the CHILD's own id — no parent walk) |
|
||||
| **RESOLVED (chunk 4).** `WalkProductionWorldData.ResolveDynamicRenderCells` (`WalkProductionWorldData.cs`) | `ShadowObjectRegistry.TryGetRetailCellArray` directly, for every dynamic record including equipped children (their array is published at attach time by the registry, above); unregistered fallback is `GetOwnerCells`, counted by the SAME `UnregisteredStaticRenderFallbackCount` the static buckets use — the render-side 64-hop parent-chain walk and its `FindParentLocalId` plumbing into this class are deleted | whole-record buckets | every `BeginFrame` | `WalkFrameDriver.GetCellDynamics` |
|
||||
| `LightManager.SelectForCell` (`LightManager.cs:594-650`) | flat `_all` list, sphere overlap | per-cell light subset | per cell drawn | lighting (S5 owns; retail selects from the current EnvCell + stab list) |
|
||||
| `DirectionalShadowCasterFrame` (`DirectionalShadowCasterFrame.cs:229-233`) | flat outdoor indices, no cell filter | caster list | per frame | shadow pass (S5) |
|
||||
| Topology: `EnvCellLandblockBuildBuilder.BuildVisibilityCell` vs `WalkCellFactory.FromParsed` vs `FlatEnvCellTopology` | same DAT / pak | three distinct portal graphs (`LoadedCell`, `WalkCell`, flat asset) | per landblock build | render, walk, `CellTransit` |
|
||||
|
|
@ -54,8 +55,12 @@ Findings that shape the chunks:
|
|||
registration that carries a real part array.
|
||||
- Render membership is whole-object per cell; retail's is every part per
|
||||
cell (identity retained per part, with cell frame and clip planes).
|
||||
- Children are inherited only on the render side, at bucket time; retail
|
||||
inherits at registration.
|
||||
- **RESOLVED (chunk 4).** Children were inherited only on the render side, at
|
||||
bucket time (a 64-hop parent-chain walk); retail inherits at registration
|
||||
(`add_shadows_to_cells`'s recursion through `children`). Now
|
||||
`ShadowObjectRegistry.AttachChild` owns the inheritance at the registry, at
|
||||
attach time, and re-propagates on every root array change; the render-side
|
||||
walk is deleted.
|
||||
- `b3b7d922` removed the indoor-seed stab-list prune inside the shared
|
||||
`BuildShadowCellSetFromParts`, affecting every caller. Retail has no such
|
||||
prune in the bbox route (Contract A), so the removal is the retail
|
||||
|
|
@ -106,9 +111,11 @@ gates: Core physics suites, the placement/collision replay fixtures, and the
|
|||
connected nine-stop route; `CellTransit`'s prune removal is confirmed
|
||||
against Contract A.
|
||||
|
||||
**Chunk 4 — dynamics and children.** `add_shadows_to_cells` recursion:
|
||||
children register with the root's CELLARRAY at registration and movement;
|
||||
particles own-cell only; the render-side parent-chain walk is deleted.
|
||||
**Chunk 4 (landed 2026-09-02) — dynamics and children.** `add_shadows_to_cells`
|
||||
recursion: children register with the root's CELLARRAY at registration and
|
||||
movement; particles own-cell only (still no production path reaches this
|
||||
registry — chunk 3's finding stands, deferred); the render-side parent-chain
|
||||
walk is deleted. See §7.
|
||||
|
||||
**Chunk 5 — consumer cutover and deletion.** Remaining `WalkProductionWorldData`
|
||||
buckets become borrowed views; particle owner-cell union reads the same;
|
||||
|
|
@ -328,3 +335,147 @@ branch to implement in this registry today; the retail
|
|||
`add_particle_shadow_to_cell` own-cell-only rule remains chunk 4 (or later)
|
||||
territory, to be implemented if/when particle emitters are ever routed
|
||||
through this registry.
|
||||
|
||||
## 7. Chunk 4 evidence (2026-09-02)
|
||||
|
||||
**Move-path truth confirmed in the pseudo-C (item A).** `CPhysicsObj::SetPositionInternal`
|
||||
`0x00515330` (`docs/research/named-retail/acclient_2013_pseudo_c.txt` lines
|
||||
283399-283543) proves the movement product is retail's SAME
|
||||
`add_shadows_to_cells` call the registration path uses — not a second,
|
||||
independently-flooded array:
|
||||
|
||||
```text
|
||||
if (this->cell != 0)
|
||||
{
|
||||
if ((this->state & 0x10000) != 0)
|
||||
{
|
||||
CPhysicsObj::calc_cross_cells(this); // full recompute (pc:283530)
|
||||
return 1;
|
||||
}
|
||||
if (arg2->cell_array.num_cells > 0) // keep-when-empty gate (pc:283534/283540)
|
||||
{
|
||||
CPhysicsObj::remove_shadows_from_cells(this);
|
||||
CPhysicsObj::add_shadows_to_cells(this, &arg2->cell_array); // pc:283536-283537
|
||||
}
|
||||
// else: neither product is touched — both stay exactly as they were.
|
||||
}
|
||||
```
|
||||
|
||||
`arg2` is the `CTransition` the sphere-path walk already produced; its
|
||||
`cell_array` is the SAME array retail's collision system computed during the
|
||||
move — not Contract A's `calc_cross_cells_static` dispatch (that only runs at
|
||||
registration, or via the `state & 0x10000` full-recompute branch, which
|
||||
acdream already maps to `PhysicsShadowCommitAction.Recalculate` →
|
||||
`UpdatePosition`, unaffected by this chunk). `CPhysicsObj::calc_cross_cells`
|
||||
`0x00515230` (lines 283332-283395) confirms the full-recompute branch is a
|
||||
DIFFERENT primitive (its own cylsphere/bbox dispatch, ending in the same
|
||||
`remove_shadows_from_cells` + `add_shadows_to_cells` pair) — chunk 4 leaves it
|
||||
untouched.
|
||||
|
||||
**Chunk 4 changes, exactly as the pseudo-C requires:**
|
||||
|
||||
- `ShadowObjectRegistry.ReplacePositionRows` now calls the new
|
||||
`PublishRetailProductFromExactCells(entityId, exactCells)` — the SAME
|
||||
`exactCells` list collision just republished into — instead of
|
||||
`RecomputeRetailCellArrayIfPresent`'s independent Contract A flood.
|
||||
`PublishRetailProductFromExactCells` is a thin wrapper over the existing
|
||||
`PublishRetailCellArray`, reusing the entity's LAST recorded dispatch route
|
||||
(registration never re-runs on a move, matching the pseudo-C: no
|
||||
`calc_cross_cells_static` call anywhere in `SetPositionInternal`'s
|
||||
non-full-recompute branch).
|
||||
- `ShadowObjectRegistry.RefreshPositionRows`'s "nothing retained at all"
|
||||
branch (both `_entityToCells` and `_suspendedEntityCells` empty) no longer
|
||||
calls anything — true keep-when-empty, matching the pseudo-C's `else`
|
||||
(neither product touched).
|
||||
- `RecomputeRetailCellArrayIfPresent` and the now-unreachable
|
||||
`RecomputeRetailCellArray` are deleted outright (S2 plan rule "no
|
||||
competing production owner").
|
||||
- **Scope-decision reversal from chunk 3's §6 open question:** chunk 3 left
|
||||
the move path as two legitimately-different computations (transition
|
||||
cross-cells vs. Contract A's part-array flood) and flagged it for chunk 4's
|
||||
retail review. The pseudo-C resolves it: retail's OWN move path never runs
|
||||
Contract A at all; it reuses the transition's array for both products. The
|
||||
"two computations" framing was itself the deviation from retail — chunk 4
|
||||
removes it, not preserves it.
|
||||
- **Test consequence:** `ShadowObjectRegistryRetailCellArrayTests.CommitSetPosition_WithRetainedPartArray_RecomputesRetailCellArray`
|
||||
encoded exactly the two-source behavior this chunk removes (a `.None`
|
||||
commit whose retail array independently flooded to the entity's NEW world
|
||||
position while collision stayed at the OLD retained cell). It is replaced
|
||||
by `CommitSetPosition_NoneAction_PublishesRetailProductFromTheRetainedCells`
|
||||
(retail array == `GetOwnerCells`, both at the retained cell) plus two new
|
||||
fixtures covering `.Replace` (both move to the transition's cross cells)
|
||||
and the direct-path keep-when-empty case. The new invariant, pinned across
|
||||
every `CommitSetPosition` variant and the staged apply: **retail array ==
|
||||
`GetOwnerCells` after every move.**
|
||||
|
||||
**Children (item B/C), landed exactly as designed.** `ShadowObjectRegistry`
|
||||
gains `AttachChild(childEntityId, rootEntityId, childPartArray)` /
|
||||
`DetachChild(childEntityId)`, backed by three new `CollisionWorldState`
|
||||
dictionaries (`ShadowChildParent`, `ShadowParentChildren`,
|
||||
`ShadowChildPartArrays` — reset by `AttachCollisionWorld`'s state swap and by
|
||||
`Clear()` exactly like every other per-entity table). `AttachChild` publishes
|
||||
PART ENTRIES only (never a collision row — acdream's attached projections own
|
||||
no independent collision shapes) into every cell of the root's CURRENT
|
||||
`_retailCellArrays` entry; a nested attachment (child of a child) resolves to
|
||||
the ultimate root by walking the existing `_childParent` chain, bounded at 64
|
||||
hops and rejecting a cycle before any state mutates. `RepublishAttachedChildren`
|
||||
is called from every site that publishes a root's retail cell array —
|
||||
`PublishRetailCellArray` (covers registration and, via
|
||||
`PublishRetailProductFromExactCells`, movement), `TryApplySetPosition`
|
||||
(staged apply, since the staging registry that computed the prepared commit
|
||||
carries none of the live registry's attach-chain state), and
|
||||
`ReplaceMultiPartPayload` (defensive completeness; the root's cell SET never
|
||||
actually changes there, so this is a structural no-op in practice) — and
|
||||
recurses depth-first through `_parentChildren` in attach order, matching
|
||||
`add_shadows_to_cells`'s exact recursion through `children`. `Deregister`
|
||||
cascades a genuine teardown through every attached child (guarded so the
|
||||
INTERNAL "clear then re-register" idiom `Register`/`RegisterMultiPart` use
|
||||
ahead of their own flood — `DeregisterCore(..., publishMutation: false)` —
|
||||
never detaches children it should be re-publishing to instead).
|
||||
|
||||
**App wiring (item C).** `EquippedChildRenderController.TryRealize` — the one
|
||||
seam that both `OnCreateParentAccepted` and `OnParentEvent` funnel through to
|
||||
materialize an attached projection's `WorldEntity` — calls `AttachChild`
|
||||
right after `_attachedByChild[childKey] = attached`, using a new
|
||||
`BuildChildRenderParts` helper: `ShadowShapeBuilder.FromSetupRenderParts`
|
||||
over the child's `Setup` + its post-`AnimPartChanged` `template` (already
|
||||
built by `BuildPartTemplate` for the pose composition) + the controller's own
|
||||
new `PhysicsDataCache` field — the SAME resolver pair
|
||||
`LiveEntityCollisionBuilder.Build` uses, wired from the SAME composition
|
||||
point (`d.PhysicsDataCache`, `LivePresentationComposition.cs`) that already
|
||||
supplies `LiveEntityCollisionBuilder`. `CommitProjectionRemoval` — the single
|
||||
funnel every withdrawal/unparent/teardown path in the file commits removal
|
||||
through — calls `DetachChild(child.Entity.Id)`. No Core→App dependency: both
|
||||
`ShadowObjectRegistry` and `PhysicsDataCache` are Core types the App layer
|
||||
already depends on; the controller (App) references Core, never the reverse.
|
||||
|
||||
`WalkProductionWorldData.ResolveDynamicRenderCells` is now a direct
|
||||
`TryGetRetailCellArray` read (the same recipe `ResolveOutdoorStaticRenderCells`
|
||||
already used for statics) for EVERY dynamic record, children included — no
|
||||
special-casing on `RenderCasterIdentityKind.EquippedChild` any more, since the
|
||||
registry now answers correctly for a child on its own id. The render-side
|
||||
64-hop parent-chain walk, its `_findParentLocalId` field, and the
|
||||
`findParentLocalId` constructor parameter threaded through
|
||||
`WalkProductionWorldData` → `RetailPViewRenderer` → `FrameRootComposition`
|
||||
are deleted; `EquippedChildRenderController.FindParentLocalId` itself is
|
||||
UNCHANGED and keeps serving its other caller (`EntityEffectController`'s pose
|
||||
composition, `LivePresentationComposition.cs`). An entity with no retail
|
||||
array yet falls back to `GetOwnerCells`, counted by the SAME
|
||||
`UnregisteredStaticRenderFallbackCount` the static buckets already use (item
|
||||
C's explicit requirement — one fallback counter, not a second one for
|
||||
dynamics).
|
||||
|
||||
**Particles (item D).** Unchanged from chunk 3's finding: no production code
|
||||
path sets the particle-emitter state bit or routes an emitter through
|
||||
`ShadowObjectRegistry` at all (`AcDream.App/Rendering/ParticleRenderer.cs`
|
||||
owns emitter placement independently via a GPU mesh-reference table). Left as
|
||||
a documented chunk-5-or-later item; this chunk invents no path for it.
|
||||
|
||||
**Automated evidence.** Core suite `Lane!=Timing&Lane!=Linux`: 4,970/4,970 (was
|
||||
4,961; +9 new `ShadowObjectRegistryRetailCellArrayTests` fixtures — 2 replacing
|
||||
the retargeted move-path test, 7 covering `AttachChild`/`DetachChild`,
|
||||
nesting, and cycle rejection). App suite (hermetic lanes): 6,761/6,761 (was
|
||||
6,760; net +1 — the old `ResolveDynamicRenderCells_EquippedDescendantInheritsRootCellArray`
|
||||
parent-chain-walk test replaced by two direct-registry tests). Runtime suite:
|
||||
1,884/1,884 (unchanged — chunk 4 touched no Runtime source). Full solution
|
||||
Release build: 0 warnings, 0 errors.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue