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:
parent
58822fed96
commit
5ca2f448d4
2 changed files with 63 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -158,4 +158,48 @@ public class CellTransitFindCellSetTests
|
|||
Assert.Contains(0xA9B40100u, cellSet);
|
||||
Assert.Contains(0xA9B40001u, cellSet);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// Membership hysteresis — the R1-flap root cause.
|
||||
// Retail CObjCell::find_cell_list adds the CURRENT cell at index 0
|
||||
// (add_cell, pc:308766) and the pick loop iterates from index 0 with
|
||||
// interior-wins-break (pc:308791-308819) — so when two cells' BSPs both
|
||||
// contain the sphere center at a boundary, the CURRENT cell wins and the
|
||||
// membership does NOT flip. acdream's unordered-HashSet pick dropped that
|
||||
// ordering, producing the stair/room/doorway ping-pong (cell flips every
|
||||
// tick while straddling). This guards the current-cell-first behaviour.
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData(0xA9B40100u, 0xA9B40101u)]
|
||||
[InlineData(0xA9B40101u, 0xA9B40100u)]
|
||||
public void TwoOverlappingCells_CurrentCellWinsTheStraddle(uint currentCellId, uint otherCellId)
|
||||
{
|
||||
// Both cells use a Leaf BSP (contains any point) and are reciprocally portalled at the same
|
||||
// plane (local x=2.5), so the candidate set is {both} from EITHER seed and the sphere center
|
||||
// is inside BOTH cells' BSPs. The pick must return the CURRENT cell (hysteresis), not flip to
|
||||
// the neighbour. Testing both seed directions guarantees a deterministic failure on the
|
||||
// unordered pick (it returns the same enumeration-first cell regardless of which is current).
|
||||
ushort currentLow = (ushort)(currentCellId & 0xFFFF);
|
||||
ushort otherLow = (ushort)(otherCellId & 0xFFFF);
|
||||
|
||||
var current = MakeCellWithPortalAtRightWall(Matrix4x4.Identity, otherLow, flags: 0);
|
||||
var other = MakeCellWithPortalAtRightWall(Matrix4x4.Identity, currentLow, flags: 0);
|
||||
|
||||
var cache = new PhysicsDataCache();
|
||||
cache.RegisterCellStructForTest(currentCellId, current);
|
||||
cache.RegisterCellStructForTest(otherCellId, other);
|
||||
|
||||
// Center straddles the portal plane at local x=2.5 (both cells), inside both Leaf BSPs.
|
||||
var sphereCenter = new Vector3(2.0f, 0f, 2.5f);
|
||||
|
||||
uint containing = CellTransit.FindCellSet(
|
||||
cache, sphereCenter, sphereRadius: 0.5f,
|
||||
currentCellId: currentCellId,
|
||||
out var cellSet);
|
||||
|
||||
Assert.Contains(currentCellId, cellSet);
|
||||
Assert.Contains(otherCellId, cellSet);
|
||||
Assert.Equal(currentCellId, containing); // retail current-cell-first hysteresis
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue