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

@ -82,8 +82,8 @@ public sealed class PhysicsDataCache
/// <summary>
/// The unified cell graph (UCG): the active id-&gt;cell resolver and registry.
/// Populated unconditionally in <see cref="CacheCellStruct"/> so BSP-less
/// authored cells are registered too, and
/// Populated by <see cref="CacheCellStruct"/> for cells with valid
/// containment (including cells with no physics root), and
/// consumed across the engine: the player render/lighting root
/// (<c>CellGraph.CurrCell</c>, written at the player chokepoint
/// <c>PhysicsEngine.UpdatePlayerCurrCell</c> and read by the renderer), the
@ -364,11 +364,10 @@ public sealed class PhysicsDataCache
}
/// <summary>
/// Extract and cache the authored CellStruct payload (indoor room
/// geometry), including cells whose physics or containment BSP has a null
/// root. Retail keeps those loaded cells distinct from an unavailable
/// visible-cell lookup; the null containment root is universally inside.
/// No-ops only when the id is already cached.
/// Extract and cache an authored CellStruct payload (indoor room geometry).
/// A missing physics root is valid (the cell can still own containment and
/// portals); a missing containment root is not a loadable CEnvCell and is
/// rejected before either the graph or collision record is published.
/// </summary>
public void CacheCellStruct(
uint envCellId,
@ -398,11 +397,6 @@ public sealed class PhysicsDataCache
!_flatEnvCell.ContainsKey(envCellId))
throw MissingPreparedCollision("EnvCell topology", envCellId);
if (preparedStructure is not null)
_flatCellStruct.TryAdd(envCellId, preparedStructure);
if (preparedTopology is not null)
_flatEnvCell.TryAdd(envCellId, preparedTopology);
if (_requirePreparedCollision)
{
CachePreparedCellStruct(
@ -416,7 +410,27 @@ public sealed class PhysicsDataCache
return;
}
// UCG Stage 1: register in the unified graph for every authored cell.
// CCellStruct::point_in_cell dereferences cell_bsp->root_node before
// entering BSPNODE::point_inside_cell_bsp. A null ROOT is therefore
// not the recursive missing-positive-child "inside" sentinel. The
// installed 2013 catalog contains zero such payloads; quarantine one
// rather than publishing a cell that claims the whole world.
if (cellStruct.CellBSP?.Root is null)
return;
// A malformed optional prepared shadow must not attach to an otherwise
// valid raw cell. Production takes the prepared-only overload below.
if (preparedStructure?.ContainmentBsp.RootIndex < 0)
{
preparedStructure = null;
preparedTopology = null;
}
if (preparedStructure is not null)
_flatCellStruct.TryAdd(envCellId, preparedStructure);
if (preparedTopology is not null)
_flatEnvCell.TryAdd(envCellId, preparedTopology);
// UCG Stage 1: register only a loadable authored cell.
if (!CellGraph.Contains(envCellId))
{
CellGraph.Add(UcgEnvCell.FromDat(
@ -618,6 +632,13 @@ public sealed class PhysicsDataCache
FlatCellStructureCollisionAsset preparedStructure,
FlatEnvCellTopology preparedTopology)
{
// Same invariant as the raw loader. RootIndex -1 is the flattened
// encoding of a missing ROOT, not a recursive positive-child sentinel.
// Reject it atomically so graph, collision, and prepared caches agree
// that this cell is unavailable and a later valid hydration may retry.
if (preparedStructure.ContainmentBsp.RootIndex < 0)
return;
_flatCellStruct.TryAdd(envCellId, preparedStructure);
_flatEnvCell.TryAdd(envCellId, preparedTopology);
@ -630,9 +651,8 @@ public sealed class PhysicsDataCache
preparedTopology));
}
// The prepared structure itself is the loaded CellStruct payload.
// Empty physics and containment roots remain meaningful authored
// values; neither means that the cell is unavailable.
// Physics may be rootless even though the cell's containment and
// topology are valid; preserve that loaded, non-colliding cell.
if (_cellStruct.ContainsKey(envCellId))
return;
@ -1023,9 +1043,9 @@ public sealed class CellPhysics
/// (point-in-cell tests). Separate tree from <see cref="BSP"/>
/// (collision) and from the renderer's drawing-BSP.
/// Source: <c>cellStruct.CellBSP</c> at cache time.
/// A nullable root is an authored, universally-inside containment tree.
/// Cell availability is represented by presence of this
/// <see cref="CellPhysics"/> record, not by root presence.
/// Root presence is required for a published cell. Missing positive
/// children inside a valid tree are the retail inside base case; a missing
/// root is rejected by <see cref="PhysicsDataCache.CacheCellStruct"/>.
/// </summary>
public DatReaderWriter.Types.CellBSPTree? CellBSP { get; init; }