fix(physics): validate retail cell containment roots

This commit is contained in:
Erik 2026-07-31 14:48:26 +02:00
parent 7716c2ee89
commit 3e0f3b6206
23 changed files with 429 additions and 197 deletions

View file

@ -1,26 +1,41 @@
# Retail cell availability and null-root containment — 2026-07-31
# Retail cell availability and containment-root validation — 2026-07-31
## Scope
This note closes divergence rows AD-3 and AD-4. It does not begin AD-6's
atomic streaming-generation work.
The bug was one collapsed state. acdream treated all three of these as
“containment unavailable”:
The corrected port distinguishes these states:
1. no visible cell payload is loaded;
2. a loaded CellStruct has a null containment root;
3. a loaded CellStruct has an authored containment root.
2. a malformed raw/prepared payload has no containment root;
3. a loaded CellStruct has a valid authored containment root (its physics root
may independently be absent).
Retail distinguishes (1) from (2). A failed visible-cell lookup is
unavailable. A loaded CellStruct remains a real cell even when its BSPTREE
root is null, and the containment query's null-node base case is inside.
Only (3) is published. State (1) remains unavailable and retryable. State (2)
is quarantined atomically so a later valid hydration can retry; it must not
become a world-wide containing cell.
## Installed-data audit
The complete installed EoR catalog and matching prepared package were audited
before choosing this invariant:
- enumerated EnvCells: **729,888**;
- raw: 0 missing EnvCells, 0 missing Environments, 0 missing CellStructs,
0 null `CellBSP` objects, **0 null `CellBSP.Root`**, 729,888 valid roots;
- prepared `acdream.pak`: 0 missing aliases, 0 corrupt payloads,
**0 `ContainmentBsp.RootIndex < 0`**, 729,888 valid roots;
- 6,940 raw EnvCells have zero portals, so the retail portal-pointer guard is
a real catalog path rather than dead defensive code.
There are therefore no root-null record IDs to preserve in either source.
## Retail oracle
`CObjCell::find_cell_list @ 0x0052B4E0` in
`docs/research/named-retail/acclient_2013_pseudo_c.txt:308742` establishes the
availability gate:
availability gates:
- `CEnvCell::GetVisible` / `CLandCell::GetVisible` resolves the active seed at
`0x0052B50C..0x0052B515`;
@ -31,32 +46,47 @@ availability gate:
- each later candidate is independently skipped when its stored cell pointer
is null at `0x0052B58E`.
`CCellStruct::point_in_cell @ 0x005338F0` delegates directly to
`BSPTREE::point_inside_cell_bsp @ 0x005398C0`. The already-ported graph and
flat BSP queries preserve the retail null-root base case: a negative/null root
returns true. Root presence is therefore not an availability predicate.
`CEnvCell::point_in_cell @ 0x0052C300` first returns false when
`this->portals == 0`, then transforms the point and calls
`CCellStruct::point_in_cell`.
`CCellStruct::point_in_cell @ 0x005338F0` calls
`BSPTREE::point_inside_cell_bsp @ 0x005398C0`, which immediately invokes
`BSPNODE::point_inside_cell_bsp(this->root_node, ...)`. The BSP node method at
`0x0053C1F0` dereferences `this` before walking positive children. Only a
missing **positive child below a valid root** is the inside terminal case. A
missing root is not.
## Ported behavior
- `PhysicsDataCache` now publishes a `CellPhysics` record whenever an authored
raw or prepared CellStruct payload exists, even if its physics BSP and/or
containment BSP root is absent.
- `CollisionTraversal.HasCellContainment` tests representation payload
availability, not `Root` / `RootIndex`. `PointInsideCell` then lets the
graph or flat query return true for the null-root base case.
- `PhysicsDataCache` publishes graph, collision, and prepared records only
after a valid raw/prepared containment root is present. A missing physics
root is retained as a valid non-colliding cell. Invalid containment
publication changes no cache, so later hydration can retry.
- `CollisionTraversal.HasCellContainment` tests `Root` / `RootIndex`.
- Both raw and prepared `EnvCell.PointInCell` paths apply the zero-portals
guard before containment. `CellTransit` applies the same guard to its
`CellPhysics` representation.
- `CellTransit.BuildShadowCellSet` still seeds all overlapped outdoor cells,
but skips the transit walk when the active outdoor seed cannot be resolved
from `CellGraph`. A separately cached building can no longer promote an
object through an unavailable landcell.
from `CellGraph`. Every later outdoor candidate independently resolves via
`GetVisible` before building transit, so a stale building cannot promote an
object through an unavailable adjacent landcell.
- The existing reflood lifecycle remains the recovery mechanism. Once terrain
or an indoor CellStruct publishes, the next reflood walks the same authored
or a valid indoor CellStruct publishes, the next reflood walks the authored
portal/building relationships without reconstructing a different rule.
## Gates
Focused tests cover raw graph and prepared flat cache publication, absent
versus loaded-null-root containment, indoor and outdoor seeds, preservation of
outside-cell seeding, suppression of spurious building promotion, and
hydration/reflood recovery. Final Release gates passed: Core 4,162 / 1 skipped,
Runtime 440 / 0 skipped, App 4,002 / 3 skipped, plus the complete solution
build with zero errors.
Focused tests cover raw/prepared rootless quarantine and valid retry, valid
containment with missing physics, raw/prepared zero-portal parity, indoor and
outdoor seeds, preservation of outside-cell seeding, per-candidate adjacent
landcell availability, suppression of stale-building promotion, and
hydration/reflood recovery. The corrective checkpoint passes:
- focused cell-availability suite: **54/54**;
- Core Release: **4,165 passed / 1 skipped**;
- Runtime Release: **440/440**;
- App Release: **4,002 passed / 3 skipped**;
- complete Release solution: **10,122 passed / 4 skipped**;
- `dotnet build AcDream.slnx -c Release`: **0 warnings / 0 errors**.