diff --git a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md index 177a2003..4a034cb4 100644 --- a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md +++ b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md @@ -360,24 +360,35 @@ registration transaction. Every consumer borrows it; the App-owned render index, visual-AABB fallbacks, origin-cell fallbacks, and duplicate cell buckets are deleted. -**Chunks:** -1. Read-only graph snapshot beside existing owners from the same accepted - publication; equality test against source publication; zero pixel change. -2. Static `AddPartsShadow` parity: ordered membership vector per visual part; - old index retained as comparator only inside this chunk; cut render lookup - after the installed-DAT comparator is zero-mismatch. Fixtures: cathedral +**Chunks (revised 2026-09-02 from the ownership trace in +`s2-membership-ownership-map.md`):** +1. Retail CELLARRAY from the whole part array (per part: physics sphere else + drawing sphere, part box) computed in `ShadowObjectRegistry` at the same + registration call as today's collision flood, retained as a new product + beside `_entityToCells`, plus per-cell part entries (entity, part, GfxObj, + cell frame, clip-planes-required) in registration order. Nothing consumes + it yet; a comparator over installed-DAT fixtures reports retail vs current + collision vs current render cells. Zero pixel change. +2. Render statics cut over to the registry's per-cell part entries; delete + `ResolveStaticRenderCells`, `ComputeStaticRenderCells`, the fingerprint + cache, and App's `FromStaticRenderParts` use. Fixtures: cathedral ramp/stairs/walls, Facility stairs, a 24 m cell-edge crosser, a landblock-edge object, a non-colliding decoration, a multi-part Setup whose parts cross different cells. -3. Dynamic/child parity: `add_shadows_to_cells`, removal, movement update, - child inheritance as one transaction keyed by exact incarnation. Fixtures: - local/remote player, NPC, projectile, spell world object, equipped child, - contained child, indoor portal crossing, outdoor crossing, teardown/ - reconnect. -4. Consumer cutover in order: indoor static render lookup, outdoor static - render lookup, dynamic render lookup, physics broadphase projection, - particle owner-cell lookup, point-light owner-cell lookup, directional-shadow - reachability. Delete the superseded builders. +3. Collision cut over: `_entityToCells`/`_cells` from the same CELLARRAY. + Physics suites, placement/collision replay fixtures, connected nine-stop + route. +4. Dynamics and children: `add_shadows_to_cells` recursion at registration and + movement; particles own-cell; delete the render-side parent-chain walk. + Fixtures: local/remote player, NPC, projectile, spell world object, + equipped child, contained child, indoor portal crossing, outdoor crossing, + teardown/reconnect. +5. Consumer cutover and deletion: remaining `WalkProductionWorldData` buckets + become borrowed views, particle owner union reads the same, duplicate + dictionaries and origin-cell/visual-AABB fallbacks are deleted. Lights and + directional shadows stay for S5. Topology triplication (`LoadedCell` / + `WalkCell` / flat asset) is consolidated with S3's `WalkPView` state, not + here. **Review:** architecture (one authority; retry/recenter/reset safe; no per-frame rebuild) and retail (crossed cells and insertion order match the diff --git a/docs/research/2026-09-01-overhaul/s2-membership-ownership-map.md b/docs/research/2026-09-01-overhaul/s2-membership-ownership-map.md new file mode 100644 index 00000000..61901d79 --- /dev/null +++ b/docs/research/2026-09-01-overhaul/s2-membership-ownership-map.md @@ -0,0 +1,113 @@ +# S2 plan packet — cell-membership ownership map and chunk contracts + +Date: 2026-09-02. Campaign OVERHAUL S2 (world graph: one membership owner). +Retail contract: `oh1-construction-landscape-contract.md` Contracts A and B. +Current-state evidence: read-only code trace (file:line) taken 2026-09-02 after +S1 closed at `8c6563ca`. + +## 1. Retail truth this slice ports + +One transaction per object (`CPhysicsObj::calc_cross_cells_static` +`0x00515160`, then `remove_shadows_from_cells` `0x00511230` + +`add_shadows_to_cells` `0x00514ae0`): + +1. **CELLARRAY** is computed from the whole part array. Cylsphere route when + `(state & 0x10000) == 0 && part_array != null && GetNumCylsphere() != 0`; + otherwise the bbox route (`find_bbox_cell_list` `0x00510fc0`): seed the + object's cell, then for every array cell call + `CEnvCell::find_transit_cells` `0x0052cae0`, which per portal and per + VISUAL part uses `gfxobj->physics_sphere` else `drawing_sphere` for the + plane reject, the part bounding box (`CPhysicsPart::GetBoundingBox`) for + `Plane::intersect_box` and `box_intersects_cell`, and + `add_all_outside_cells` once for an `0xFFFFFFFF` portal. Insertion order + is retained; nothing prunes outside cells afterwards. +2. **Two products of that one array, in the same order:** per cell a + `CShadowObj` (collision row, `add_shadow_object`) and, when a part array + exists, `CPartArray::AddPartsShadow` `0x00517e40`, which adds EVERY part to + that cell's `shadow_part_list` with `planes = num_shadow_objects > 1 ? + cell->clip_planes : null` and the cell frame. Per-part visibility is NOT a + membership subset; it is decided at draw time by the portal view test and + the drawn stamp (S3). +3. **Children** receive the root's same CELLARRAY recursively in child-list + order. **Particles** (`state & 0x1000`) register only in their own cell. +4. **Removal** is the inverse transaction at the same owner. + +## 2. What acdream does today (trace, 2026-09-02) + +| Owner | Inputs | Output | Runs | Consumers | +|---|---|---|---|---| +| `LandblockPhysicsPublisher.PublishStaticEntity` → `ShadowObjectRegistry.RegisterMultiPart` (`LandblockPhysicsPublisher.cs:930-1079`, `ShadowObjectRegistry.cs:493-584`) | BSP parts only (`ShadowShapeBuilder.FromLandblockBspParts`) or Setup cyl/sphere fallback; `CellTransit.BuildShadowCellSetFromParts` | `_entityToCells[id]` (ordered, the CELLARRAY analog) + `_cells[cell]` `ShadowEntry` rows | once per static at publication | physics broadphase `TransitionTypes.FindObjCollisionsInCell` (`TransitionTypes.cs:3755-3779`); outdoor static render lookup reads it verbatim | +| `WalkProductionWorldData.ResolveStaticRenderCells` → `ShadowObjectRegistry.ComputeStaticRenderCells` (`WalkProductionWorldData.cs:285-366`, `ShadowObjectRegistry.cs:371-394`) | ALL visual parts (`ShadowShapeBuilder.FromStaticRenderParts`: physics sphere else visual-AABB circumsphere; exact visual box); same `BuildShadowCellSetFromParts` | per-object cell list, cached by fingerprint; whole `RenderProjectionRecord` bucketed into every cell | every `BeginFrame` sweep of `RenderSceneIndex.IndoorCellStatic` | `WalkFrameDriver.GetCellStatics`; `RetailPViewRenderer` particle owner union | +| `LiveEntityCollisionBuilder.Register` / `ShadowObjectRegistry.UpdatePosition` / `CommitSetPosition` (`LiveEntityCollisionBuilder.cs:149-280`, `ShadowObjectRegistry.cs:881-982`) | `ShadowShapeBuilder.FromSetup` (BSP parts, else cylspheres, else spheres) | `_entityToCells` + `_cells` | spawn and every accepted move/appearance change | physics broadphase; dynamic render lookup reads it verbatim | +| `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` | +| `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` | + +Findings that shape the chunks: + +- The two static floods are the SAME `CellTransit` walk with DIFFERENT part + inputs (BSP-only vs all visual parts). Retail uses all parts once. The + render-side flood already carries retail's inputs; the collision side does + not. +- 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. +- `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 + direction; it is re-verified, not re-litigated, in chunk 3. +- Topology triplication is real but the walk is its consumer; consolidating + it belongs with S3's `WalkPView` state, not S2. S2 owns membership only. + +## 3. Chunks + +**Chunk 1 — the retail CELLARRAY and the per-part render product, beside +the existing owners (zero pixel change).** Core `ShadowObjectRegistry` gains +a registration input for the whole part array (per part: GfxObj id, part +frame, physics sphere else drawing sphere, part box) and computes retail's +CELLARRAY from it as a new retained `RetailCellArray(entityId)`; it also +records per-cell part entries `(entityId, partIndex, gfxObjId, cell frame, +clipPlanes-required)` in registration order. Existing `_entityToCells`, +`_cells`, and every consumer are untouched. App (`LandblockPhysicsPublisher`) +and Runtime (`LiveEntityCollisionBuilder`) supply the part array at the same +call. A comparator test over installed-DAT fixtures (Facility stair Setup +`0x02000623` in `0x8A02015F`, cathedral ramp `0x020009A2`, the #334 Neftet +formation, an outdoor landblock-edge static, a multi-part Setup) reports the +three answers side by side: retail CELLARRAY, current collision cells, +current render cells. Differences are evidence for chunks 2/3, not failures. + +**Chunk 2 — render statics cut over.** `WalkProductionWorldData` indoor and +outdoor static buckets are filled from the registry's per-cell part entries +(grouped back to records for the existing draw path; per-part draw +admission is S3). `ResolveStaticRenderCells`, `ComputeStaticRenderCells`, +the fingerprint cache, and App's use of `FromStaticRenderParts` are deleted. +The Facility stair pin moves to the registry. + +**Chunk 3 — collision cut over.** `_entityToCells`/`_cells` are populated +from the retail CELLARRAY (collision shapes into every array cell). Physics +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 5 — consumer cutover and deletion.** Remaining `WalkProductionWorldData` +buckets become borrowed views; particle owner-cell union reads the same; +duplicate dictionaries and fallbacks are deleted; architecture text updated. +Lights and directional shadows stay as they are (S5). + +Owner gate G2 after chunk 5 (plan §9 S2). + +## 4. Tests that pin current behavior (must be consciously replaced, not +silently broken) + +`WalkProductionWorldDataTests` (bucketing), `Issue177StairDescentCameraFloodTests. +FacilityStairAssembly_RegisterAcross015FTo015EWithoutCollisionRows`, the +`CellTransit*` and `BuildShadowCellSetTests` families, `ShadowObjectRegistry*` +and `ShadowShapeBuilder*`, `LiveEntityCollisionBuilderTests`, +`LandblockPhysicsPublisherTests`, `Issue334*`, the flood replay fixtures, and +the Runtime placement/physics suites (full list in the 2026-09-02 trace).