fix(physics): fall through to outdoor cell when indoor BSP doesn't contain player (A1.7)

ISSUES #83 Phase A1.7. CellTransit.FindCellList returns currentCellId
when no candidate cell's CellBSP contains the sphere center — but
this also fires when the player has walked OUTSIDE the entire
portal-connected indoor graph (e.g., breached a missing wall poly,
walked through a doorway gap). The player's CellId stays stuck on
the old indoor cell whose BSP is now far away, NodeIntersects fails
at the BSP root for every collision query, and no walls block in
their actual location.

Probe evidence (launch-stairs.utf8.log, A1.6 verify):
- Cell 0xA9B40164: 646 indoor-bsp queries, 644 returned OK (99.7%).
  No walls firing.
- Player local positions in cell 0xA9B40164 ranged X[-0.66..33.18],
  Y[10.68..63.53] — a 34x53m envelope. The player was geometrically
  ~62m from the cell's world origin while CellId never updated.

Compare to adjacent cells where collision works:
- Cell 0xA9B4015A: 230 queries, 32% hit rate
- Cell 0xA9B40157: 131 queries, 38% hit rate

Fix: after CellTransit.FindCellList returns, verify the resolved
cell's CellBSP actually contains the sphere center via
BSPQuery.PointInsideCellBsp. If not, fall through to the existing
outdoor cell resolution branch (terrain grid + CheckBuildingTransit
for re-entry into a different building).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-20 14:25:20 +02:00
parent 700abad94c
commit 4679134d66

View file

@ -261,7 +261,32 @@ public sealed class PhysicsEngine
// Indoor branch needs DataCache to look up cells; outdoor uses
// _landblocks (no DataCache dependency).
if (DataCache is null) return fallbackCellId;
return CellTransit.FindCellList(DataCache, worldPos, sphereRadius, fallbackCellId);
uint indoorResult = CellTransit.FindCellList(DataCache, worldPos, sphereRadius, fallbackCellId);
// ISSUES #83 / Phase A1.7 (2026-05-21): verify the indoor result
// actually contains the player. CellTransit.FindCellList falls back
// to currentCellId when no candidate cell's CellBSP contains the
// sphere center — but this happens even when the player has walked
// OUTSIDE the entire portal-connected indoor cell graph (e.g.,
// exited through an unblocked wall or doorway gap). In that state
// the player's CellId is stuck on an indoor cell whose BSP is
// far away, every indoor-bsp query returns OK (NodeIntersects
// fails at root), and no walls block.
//
// If the resolved indoor cell's BSP does NOT contain the sphere
// center, fall through to the outdoor cell resolution below — it
// will compute the correct landcell from the terrain grid and
// optionally re-enter an indoor cell via CheckBuildingTransit.
var indoorCell = DataCache.GetCellStruct(indoorResult);
if (indoorCell?.CellBSP?.Root is null)
return indoorResult; // Can't verify (no CellBSP); trust FindCellList.
var localCenter = Vector3.Transform(worldPos, indoorCell.InverseWorldTransform);
if (BSPQuery.PointInsideCellBsp(indoorCell.CellBSP.Root, localCenter))
return indoorResult;
// Fall through to outdoor resolution: player has left the indoor
// portal-connected graph entirely.
}
// Outdoor seed: use terrain grid to compute the prefixed cell id.