fix(phys): A6.P3 slice 3 — cell-resolver stickiness for ping-pong fix

Closes A6.P2 Finding 3 (cell-resolver instability) + issue #98 (cellar
ascent stuck at last step) + likely closes #97 (phantom collisions +
fall-through on 2nd floor; same instability family).

Adds a cell-stickiness check at the top of ResolveCellId's indoor
branch: before re-resolving via FindCellList, check if the fallback
(previous-tick) CellId's BSP still validly contains the sphere. If
yes, return fallbackCellId immediately — preserves cell membership
when the sphere is at a boundary where multiple cells overlap.

The bug: at cell boundaries (cellar last step, indoor doorways,
between two adjacent indoor cells), the sphere overlaps multiple
cells geometrically. FindCellList's candidate-iteration order
(HashSet, implementation-defined) determines which cell wins. That
order may shift tick-to-tick → CellId ping-pong → AdjustOffset
operates against a different cell's geometry each tick → player
can't accumulate forward motion → stuck.

Evidence: scen3_inn_2nd_floor_slice2v2 capture shows the ping-pong
chain at the cellar boundary:
  0xA9B4014B → 0xA9B4014A → 0xA9B4013F → 0xA9B4014A → 0xA9B4014B
  (Z stable ~96.4; CellId oscillates every tick; reason=resolver)

Retail oracle: cell-array hysteresis pattern from
CObjCell::find_cell_list Position-variant at
acclient_2013_pseudo_c.txt:308742-308783. Retail preserves cell
membership when sphere is close to (but slightly past) cell
boundaries.

Implementation: 9 lines added (sphere-overlap check against
fallbackCellId's CellBSP before falling through to FindCellList).
Existing #90 workaround at line 299-300 (post-FindCellList sphere-
overlap check) is now redundant in the common case but kept for
safety; deferred to A6.P4 removal after visual verification.

Test suite: 1148 pass + 8 pre-existing fail (baseline maintained).
Visual verification: pending — user happy-test will confirm cellar-
up walk succeeds + no ping-pong in cell-transit log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-22 12:01:28 +02:00
parent d868946537
commit 88981669fe

View file

@ -261,6 +261,45 @@ public sealed class PhysicsEngine
// Indoor branch needs DataCache to look up cells; outdoor uses // Indoor branch needs DataCache to look up cells; outdoor uses
// _landblocks (no DataCache dependency). // _landblocks (no DataCache dependency).
if (DataCache is null) return fallbackCellId; if (DataCache is null) return fallbackCellId;
// ── Cell-stickiness check (A6.P3 slice 3, 2026-05-22) ──
// Before re-resolving via FindCellList, check if the fallback
// CellId still validly contains the sphere. If yes, prefer it
// over any FindCellList result that might pick a different
// overlapping cell — fixes issue #98 (cellar-up stuck at last
// step due to CellId ping-ponging between adjacent cells whose
// BSPs all overlap the sphere).
//
// Mechanism: when the sphere is on a cell boundary where it
// overlaps multiple cells, FindCellList's candidate-iteration
// order (HashSet, implementation-defined) determines which
// cell wins. That order may shift tick-to-tick → ping-pong.
// Stickiness ensures we keep the CURRENT cell as long as the
// sphere still validly overlaps it, only switching when the
// sphere has truly left.
//
// Retail oracle: cell-array hysteresis pattern from
// CObjCell::find_cell_list Position-variant at
// acclient_2013_pseudo_c.txt:308742-308783. Retail's cell
// resolution preserves cell membership when the sphere is
// close to (but slightly past) cell boundaries.
//
// Likely closes/obsoletes:
// - #98 (cellar ascent stuck at last step) — direct target
// - #97 (phantom collisions + fall-through on 2nd floor) —
// same instability family hypothesized
// - #90 (sphere-overlap stickiness workaround in this same
// function below) — superseded; can be removed after
// visual verification (deferred to A6.P4)
var fallbackCell = DataCache.GetCellStruct(fallbackCellId);
if (fallbackCell?.CellBSP?.Root is not null)
{
var fallbackLocal = Vector3.Transform(worldPos, fallbackCell.InverseWorldTransform);
if (BSPQuery.SphereIntersectsCellBsp(fallbackCell.CellBSP.Root, fallbackLocal, sphereRadius))
return fallbackCellId; // sphere still overlaps; stick.
}
// Fallback cell no longer valid → re-resolve via portal-graph BFS.
uint indoorResult = 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 // ISSUES #83 / Phase A1.7 (2026-05-21): verify the indoor result