From 3e140cfe711552fbde666abdb711fdf4d4b8e634 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 22 May 2026 12:04:51 +0200 Subject: [PATCH] =?UTF-8?q?fix(phys):=20A6.P3=20slice=203=20v2=20=E2=80=94?= =?UTF-8?q?=20point-in=20stickiness=20(was=20sphere-overlap)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 3 v1 (8898166) used SphereIntersectsCellBsp for the stickiness check. User verification showed: ping-pong WAS closed (3 cell-transit events vs 20+ pre-fix) but user still couldn't walk up out of cellar because the stickiness was OVER-CORRECTED — the sphere still partially overlapped the cellar cell at the top of stairs, so stickiness held the player in the cellar even when the center had transitioned to the cottage main floor cell. Fix: switch the stickiness check from SphereIntersectsCellBsp (sphere overlap) to PointInsideCellBsp (center-in). Matches FindCellList's own semantics for "which cell are you in." Player stays in fallback only while center is still inside fallback's BSP volume. Trade-off: - More permissive transitions (good — cellar-up works) - Less aggressive stickiness, so some boundary ping-ponging may return IF the sphere center oscillates across the boundary (rare; would require sub-mm Z drift across the boundary line) If the trade-off bites (ping-pong returns somewhere), the fix is a small geometric margin around the point-in check — but verify before adding. Test suite: 1148 pass + 8 pre-existing fail (baseline maintained). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.Core/Physics/PhysicsEngine.cs | 44 ++++++++++++----------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index be3f76c..f8eefb4 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -264,39 +264,43 @@ public sealed class PhysicsEngine // ── 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). + // CellId still validly contains the sphere CENTER (point-in). + // If yes, prefer it over any FindCellList result that might + // pick a different cell whose BSP also contains the point — + // fixes issue #98 (cellar-up stuck at last step due to CellId + // ping-ponging between adjacent cells in iteration-order races). // - // 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. + // Mechanism: when the sphere is on a cell boundary where the + // CENTER is in multiple cells geometrically (overlapping BSPs), + // FindCellList's candidate-iteration order (HashSet, + // implementation-defined) determines which cell wins. That + // order may shift tick-to-tick → ping-pong. Stickiness keeps + // the CURRENT cell as long as the sphere center is still + // inside it, only switching when the center has moved out. + // + // Uses POINT-IN (not sphere-overlap). Sphere-overlap stickiness + // (the first slice 3 attempt) over-corrected — held the player + // in the fallback cell even when the center had transitioned to + // an adjacent cell, blocking legitimate cell transitions at + // stair tops + portal exits. Point-in matches FindCellList's + // own semantics for "which cell are you in." // // 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. + // acclient_2013_pseudo_c.txt:308742-308783. // // 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) + // - #90 (sphere-overlap workaround below) — superseded; + // can be removed after visual verification (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. + if (BSPQuery.PointInsideCellBsp(fallbackCell.CellBSP.Root, fallbackLocal)) + return fallbackCellId; // center still inside; stick. } // Fallback cell no longer valid → re-resolve via portal-graph BFS.