fix(physics): R1 membership — current-cell-first hysteresis in find_cell_list pick

The flap R1 exposed is a cell-membership ping-pong: the find_cell_list containing-
cell pick (CellTransit.BuildCellSetAndPickContaining) iterated an UNORDERED HashSet
and returned the first interior cell whose BSP contains the sphere center, with no
preference for the current cell. Retail CObjCell::find_cell_list adds the current
cell at index 0 (add_cell, pc:308766) and iterates current-first with interior-wins-
break (pc:308791-308819) — you STAY in your current cell until the center genuinely
leaves it. acdream's HashSet dropped that ordering; once the candidate set churns at
a boundary the enumeration can surface a neighbour before the current cell → the
ping-pong. Restore the explicit, deterministic current-cell-first test (retail's
index-0 hysteresis). + a two-direction regression guard (current cell wins the
straddle).

Diagnosed from the existing [cell-transit] walk log (no new probing): room flips are
the pick non-determinism; stairs flips additionally show the foot Z oscillating
~0.2m/tick (a separate stairs-physics residual, #98 family, to verify after this).

The 2 DoorBugTrajectoryReplay failures are PRE-EXISTING (verified: they fail without
this change too) — 2 of the handoff's '3 door-collision apparatus / A6.P5'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-02 21:51:25 +02:00
parent 58822fed96
commit 5ca2f448d4
2 changed files with 63 additions and 0 deletions

View file

@ -519,6 +519,25 @@ public static class CellTransit
PhysicsDiagnostics.LogCellSetBuild(currentCellId, worldSphereCenter, candidates);
}
// Retail CObjCell::find_cell_list checks the CURRENT cell FIRST — it adds it at index 0
// (add_cell, pseudo_c:308766) and the pick loop iterates from index 0 with interior-wins-break
// (pseudo_c:308791-308819). So if the sphere center is still inside the current cell, it wins
// and the search stops: you STAY in your current cell until the center genuinely leaves it,
// never flipping to an overlapping neighbour. acdream's unordered HashSet candidate set dropped
// that ordering — once the set churns at a boundary the enumeration can surface a neighbour
// before the current cell, producing the membership ping-pong (the R1 flap). Restore the
// explicit, deterministic current-cell-first test (the retail hysteresis):
if (currentLow >= 0x0100u)
{
var curCell = cache.GetCellStruct(currentCellId);
if (curCell?.CellBSP?.Root is not null)
{
var localCur = Vector3.Transform(worldSphereCenter, curCell.InverseWorldTransform);
if (BSPQuery.PointInsideCellBsp(curCell.CellBSP.Root, localCur))
return currentCellId; // still inside the current cell → stay (retail index-0)
}
}
// Retail CObjCell::find_cell_list containing-cell pick (pseudo_c:308788-308819):
// INTERIOR-WINS — the first EnvCell whose point_in_cell (BSP) contains the sphere center
// wins and stops the search. Only if no interior cell contains it do we fall to the