fix(physics): Cluster A #84 + #85 — indoor cell tracking

ResolveOutdoorCellId only resolved outdoor terrain landcells. A player
geometrically inside an EnvCell stayed in outdoor-landcell range, so
FindEnvCollisions' indoor cell-BSP branch (gated on cellLow >= 0x0100)
never fired. Both #84 (blocked by air indoors) and #85 (pass through
walls outside→in) are downstream of this — without indoor cell-BSP
collision the player gets stuck against outdoor-stab back-faces of the
building shell, and walls only block from one side.

Adds an indoor-cell-containment check via PhysicsDataCache: at
CacheCellStruct time, compute each cell's local AABB from its resolved
polygon vertices; at ResolveOutdoorCellId time, transform the world
position into each cached cell's local space and return the matched
cell's full id when contained. Falls through to the existing outdoor
terrain logic when no EnvCell contains the position.

Also fixes a pre-existing prefix-preservation bug in the outdoor branch:
the function now always applies the matched landblock's high-16 prefix
even when the input fallbackCellId arrived bare-low-byte (the L.2e
finding from CLAUDE.md). Updated two existing PhysicsEngineTests that
encoded the old bare-low-byte output.

Evidence: launch-cluster-a-capture.log @ 2026-05-19 — player at
worldPos (155.376, 14.010, 94.000) geometrically inside cottage cell
0xA9B40172, but sp.CheckCellId stuck at 0x00000031 (outdoor landcell)
across 454 [resolve] lines; zero [indoor-bsp] lines because the gate
never opened.

Closes #84.
Closes #85.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-19 15:20:36 +02:00
parent 4e308d567a
commit c19d6fb321
4 changed files with 292 additions and 10 deletions

View file

@ -230,20 +230,48 @@ public sealed class PhysicsEngine
}
/// <summary>
/// Resolve the outdoor cell id that owns a world-space position.
/// Indoor ids are preserved because EnvCell ownership still comes from
/// portal/cell BSP state; outdoor ids are derived from the registered
/// landblock that currently contains the point.
/// Resolve a position's CellId. Tries indoor EnvCell containment first
/// (via <see cref="PhysicsDataCache.TryFindContainingCell"/>); falls back
/// to outdoor terrain landcell resolution.
///
/// <para>
/// Indoor walking Phase D (2026-05-19) extended this to fix #84 + #85:
/// previously the function only resolved outdoor cells, so a player
/// geometrically inside an EnvCell stayed in outdoor-landcell range and
/// the indoor cell-BSP collision branch never fired. The indoor
/// containment check promotes the player's CellId to the matched
/// EnvCell, which lets <see cref="Transition.FindEnvCollisions"/>'s
/// indoor branch (gated on cellLow &gt;= 0x0100) take effect.
/// </para>
///
/// <para>
/// Also fixes a pre-existing prefix-preservation bug: the outdoor branch
/// now always applies the matched landblock's high-16 prefix even when
/// the input <paramref name="fallbackCellId"/> arrived bare-low-byte
/// (the L.2e finding from CLAUDE.md).
/// </para>
/// </summary>
internal uint ResolveOutdoorCellId(Vector3 worldPos, uint fallbackCellId)
{
if (fallbackCellId == 0)
return 0;
// Phase D: indoor-cell-containment check. If the player's worldPos
// is geometrically inside a cached EnvCell, return that cell's full
// id — overrides any prior outdoor CellId the caller passed in.
if (DataCache is not null && DataCache.TryFindContainingCell(worldPos, out var indoorId))
return indoorId;
// Pre-existing: if the caller already passes an indoor CellId AND
// the player isn't in any cached EnvCell, trust the caller. This
// preserves behaviour for indoor cells whose physics hasn't been
// cached yet (rare; should be impossible in steady state).
uint fallbackLow = fallbackCellId & 0xFFFFu;
if (fallbackLow >= 0x0100u)
return fallbackCellId;
// Outdoor terrain resolution. Always applies the matched landblock's
// prefix — fixes the bare-low-byte preservation bug (L.2e).
foreach (var kvp in _landblocks)
{
var lb = kvp.Value;
@ -252,9 +280,7 @@ public sealed class PhysicsEngine
if (localX >= 0f && localX < 192f && localY >= 0f && localY < 192f)
{
uint lowCellId = lb.Terrain.ComputeOutdoorCellId(localX, localY);
return (fallbackCellId & 0xFFFF0000u) == 0
? lowCellId
: (kvp.Key & 0xFFFF0000u) | lowCellId;
return (kvp.Key & 0xFFFF0000u) | lowCellId;
}
}