fix(physics): Stage 1 — collide-then-pick (remove pre-pick fork) — the flap engine
The ordered pick alone did not stop the cottage doorway flap: the live [cell-transit] log showed the cell faithfully following a position that cleanly oscillated between two values at constant Z — the signature of a bistable membership<->collision feedback loop (user's §4.4 #3, the forked collision). Root cause: FindEnvCollisions RE-PICKED the cell from the TARGET position (old line 1958) BEFORE running the primary collision, so the collision geometry (which cell BSP / terrain) swapped the instant the pick flipped -> position shifts -> pick flips back. Retail does NOT do this. CEnvCell::find_env_collisions (acclient_2013_pseudo_c.txt:309573) collides against the cell it was called ON (sphere_path.check_cell, the carried seed) and picks the NEW containing cell AFTER, in CTransition::check_other_cells (272717->272761: check_cell = var_4c). Collide-then-pick. This commit ports that order: - remove the pre-pick (production); collide against the carried cell (indoor BSP block / terrain block unchanged); - new shared RunCheckOtherCellsAndAdvance() runs the ordered FindCellSet pick + multi-valued CheckOtherCells + the carried-cell advance AFTER the primary collision, for BOTH indoor and outdoor seeds; - the outdoor-seed post-step replaces the removed pre-pick's outdoor->indoor re-entry promotion (CheckBuildingTransit interior cell + its wall collision on the entry frame). Cache-null unit-test fallback (ResolveCellId) kept. Full Core suite: 1293 pass / 5 fail = the documented §10 baseline exactly (2 step-up + 3 door-collision), zero new breakage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
22a184ca68
commit
e5457f9552
1 changed files with 85 additions and 76 deletions
|
|
@ -1944,23 +1944,22 @@ public sealed class Transition
|
|||
Vector3 footCenter = sp.GlobalSphere[0].Origin;
|
||||
float sphereRadius = sp.GlobalSphere[0].Radius;
|
||||
|
||||
// Phase W: cell membership comes from the SWEPT find_cell_list pick (retail
|
||||
// CObjCell::find_cell_list), not a static re-derive. FindCellSet builds candidates anchored
|
||||
// to the current cell (interior neighbors + outside cells via the exit portal + building
|
||||
// re-entry cells) and picks the containing cell interior-wins. The commit to sp.CurCellId
|
||||
// is gated by ValidateTransition (accept-on-move), so a push-back can't flip the cell.
|
||||
// Retail does NOT re-pick the cell before colliding. CEnvCell::find_env_collisions
|
||||
// (acclient_2013_pseudo_c.txt:309573) collides against the cell it was called ON —
|
||||
// sphere_path.check_cell, the carried-forward (seeded) cell — and the NEW containing cell
|
||||
// is picked AFTER, in check_other_cells (272717 → 272761 sets check_cell = var_4c).
|
||||
//
|
||||
// DataCache-null fallback: PhysicsEngineTests use engines without a DataCache (no cell
|
||||
// registry). FindCellSet requires a cache, so we fall back to the old ResolveCellId
|
||||
// outdoor re-derive in that case. In production DataCache is always set.
|
||||
if (engine.DataCache is not null)
|
||||
{
|
||||
uint sweptCellId = CellTransit.FindCellSet(
|
||||
engine.DataCache, sp.GlobalSphere, sp.NumSphere, sp.CheckCellId, out _);
|
||||
if (sweptCellId != sp.CheckCellId)
|
||||
sp.SetCheckPos(sp.CheckPos, sweptCellId);
|
||||
}
|
||||
else
|
||||
// The previous pre-pick here re-derived the cell from the TARGET position BEFORE the
|
||||
// primary collision, so the collision geometry (which BSP / which terrain) swapped the
|
||||
// instant the pick flipped → a bistable membership↔collision feedback loop = the cottage
|
||||
// doorway "flap" (2026-06-03 evidence: cell oscillated 0170↔0031↔0171 at constant Z while
|
||||
// the foot Y bounced cleanly between two values). Removed: collide against the carried cell
|
||||
// first (the indoor BSP block / the terrain block below), then advance the cell only in the
|
||||
// post-collision step (RunCheckOtherCellsAndAdvance) — retail's collide-then-pick order.
|
||||
//
|
||||
// Cache-null fallback: PhysicsEngineTests use engines without a DataCache (no cell registry,
|
||||
// so FindCellSet is unavailable). Keep the old outdoor re-derive for them only.
|
||||
if (engine.DataCache is null)
|
||||
{
|
||||
uint resolvedOutdoorCellId = engine.ResolveCellId(sp.GlobalSphere[0].Origin, sphereRadius, sp.CheckCellId);
|
||||
if (resolvedOutdoorCellId != sp.CheckCellId)
|
||||
|
|
@ -2071,52 +2070,16 @@ public sealed class Transition
|
|||
return cellState;
|
||||
}
|
||||
|
||||
// ── Phase A4 (2026-05-20): query every other cell ──────────
|
||||
// Retail oracle: CTransition::check_other_cells at
|
||||
// acclient_2013_pseudo_c.txt:272717-272798. The vestibule
|
||||
// walls bug (cell 0xA9B40164 has only 4 polys; adjacent
|
||||
// 0xA9B40157 has the actual walls) closes here.
|
||||
//
|
||||
uint containingCellId = CellTransit.FindCellSet(
|
||||
engine.DataCache, sp.GlobalSphere, sp.NumSphere,
|
||||
sp.CheckCellId, out var cellSet);
|
||||
LogIssue98CellSetSummary(engine, containingCellId, cellSet, footCenter, sphereRadius);
|
||||
var otherCellsState = CheckOtherCells(engine, footCenter, sphereRadius, cellSet);
|
||||
if (otherCellsState != TransitionState.OK)
|
||||
return otherCellsState;
|
||||
|
||||
// Retail CTransition::check_other_cells retargets
|
||||
// sphere_path.check_cell to var_4c after the other-cell
|
||||
// loop succeeds, then calls SPHEREPATH::adjust_check_pos.
|
||||
// For indoor cells our SetCheckPos mirrors that id swap and
|
||||
// refreshes the cached global sphere centers without moving.
|
||||
if (containingCellId != sp.CheckCellId)
|
||||
sp.SetCheckPos(sp.CheckPos, containingCellId);
|
||||
// ──────────────────────────────────────────────────────────
|
||||
|
||||
// ── Indoor walkable handling — A6.P3 slice 1 (2026-05-22) ─
|
||||
// Retail's CEnvCell::find_env_collisions (decomp
|
||||
// acclient_2013_pseudo_c.txt:309573) returns OK after
|
||||
// BSPTREE::find_collisions returns OK — NO call to
|
||||
// set_contact_plane or any synthesis. ContactPlane is
|
||||
// either:
|
||||
// - Already valid from a previous frame's Path-6 land
|
||||
// write inside BSPQuery.FindCollisions (Mechanism A).
|
||||
// - Restored from LKCP by the per-transition Mechanism B
|
||||
// in Transition.ValidateTransition (added in 5aba071,
|
||||
// Task 4 of this slice).
|
||||
//
|
||||
// The old TryFindIndoorWalkablePlane synthesis path is
|
||||
// removed here; the function definition is retained for
|
||||
// now and is deleted in A6.P4 along with the #90
|
||||
// workaround.
|
||||
//
|
||||
// If subsequent visual verification shows first-frame
|
||||
// fall-through (LKCP invalid AND no Path-6 land happens
|
||||
// for a flat-walk-only scenario), A6.P3 slice 2 adds
|
||||
// Mechanism C (retail's frames_stationary_fall flat-CP
|
||||
// synthesis at acclient_2013_pseudo_c.txt:272622+).
|
||||
return TransitionState.OK;
|
||||
// ── check_other_cells (retail collide-then-pick) ───────────
|
||||
// The primary indoor BSP collision above ran against the carried
|
||||
// cell (sp.CheckCellId). NOW pick the new containing cell, collide
|
||||
// every OTHER cell the sphere overlaps, and advance the carried
|
||||
// cell. Retail CTransition::check_other_cells (pseudo_c:272717).
|
||||
// (Indoor walkable: retail CEnvCell::find_env_collisions returns OK
|
||||
// after the BSP find_collisions — no set_contact_plane synthesis;
|
||||
// ContactPlane comes from a prior Path-6 land or the per-transition
|
||||
// LKCP restore in ValidateTransition.)
|
||||
return RunCheckOtherCellsAndAdvance(engine, footCenter, sphereRadius);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2135,22 +2098,68 @@ public sealed class Transition
|
|||
// find_terrain_poly and uses walkable.Plane — the actual triangle's
|
||||
// plane, not a reconstructed flat one. SampleTerrainWalkable returns
|
||||
// that plane plus the triangle vertices needed by precipice slide.
|
||||
// ── Outdoor PRIMARY collision: terrain (retail CLandCell::find_env_collisions) ──
|
||||
// Runs against the carried (outdoor) cell. The post-collision pick below then promotes
|
||||
// to an interior cell if the sphere has re-entered a building (replacing the removed
|
||||
// pre-pick's outdoor→indoor promotion).
|
||||
var terrainWalkable = engine.SampleTerrainWalkable(footCenter.X, footCenter.Y);
|
||||
if (terrainWalkable is null)
|
||||
return TransitionState.OK; // no terrain loaded here — allow pass-through
|
||||
if (terrainWalkable is not null)
|
||||
{
|
||||
// Per-point water depth: 0.9 on fully water cells, 0.45 on partial-
|
||||
// water near a water corner, 0.1 on partial-water near a dry corner,
|
||||
// 0 on dry cells. ValidateWalkable adds this to its signed-distance
|
||||
// check so the character is allowed to sink this far below the
|
||||
// contact plane before the push-up fires. In retail, this is what
|
||||
// makes characters appear submerged in water — there is NO separate
|
||||
// water surface mesh; the character just sits lower than terrain.
|
||||
var terrainState = ValidateWalkable(footCenter, sphereRadius, terrainWalkable.Value.Plane,
|
||||
terrainWalkable.Value.IsWater,
|
||||
terrainWalkable.Value.WaterDepth,
|
||||
cellId: terrainWalkable.Value.CellId,
|
||||
walkableVertices: terrainWalkable.Value.Vertices);
|
||||
if (terrainState != TransitionState.OK)
|
||||
return terrainState;
|
||||
}
|
||||
// else: no terrain loaded here — allow pass-through, but STILL run the post-collision
|
||||
// pick so an outdoor-seeded sphere re-entering a building is promoted to the interior.
|
||||
|
||||
// Per-point water depth: 0.9 on fully water cells, 0.45 on partial-
|
||||
// water near a water corner, 0.1 on partial-water near a dry corner,
|
||||
// 0 on dry cells. ValidateWalkable adds this to its signed-distance
|
||||
// check so the character is allowed to sink this far below the
|
||||
// contact plane before the push-up fires. In retail, this is what
|
||||
// makes characters appear submerged in water — there is NO separate
|
||||
// water surface mesh; the character just sits lower than terrain.
|
||||
return ValidateWalkable(footCenter, sphereRadius, terrainWalkable.Value.Plane,
|
||||
terrainWalkable.Value.IsWater,
|
||||
terrainWalkable.Value.WaterDepth,
|
||||
cellId: terrainWalkable.Value.CellId,
|
||||
walkableVertices: terrainWalkable.Value.Vertices);
|
||||
// ── check_other_cells (retail collide-then-pick) ──
|
||||
// Pick the new containing cell + collide every OTHER cell the sphere overlaps + advance
|
||||
// the carried cell. For an outdoor seed this is the outdoor→indoor re-entry path (the
|
||||
// ordered pick promotes to the interior cell via CheckBuildingTransit and collides its
|
||||
// walls on the entry frame). Retail CTransition::check_other_cells (pseudo_c:272717).
|
||||
return RunCheckOtherCellsAndAdvance(engine, footCenter, sphereRadius);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CTransition::check_other_cells</c> (acclient_2013_pseudo_c.txt:272717): the
|
||||
/// post-collision step. After the primary cell's <see cref="FindEnvCollisions"/> ran against
|
||||
/// the carried cell (<c>sphere_path.check_cell</c>), build the ordered cell array (retail
|
||||
/// <c>find_cell_list</c>), collide every OTHER cell the sphere overlaps (multi-valued
|
||||
/// collision), then retarget the carried cell to the ordered-pick containing cell
|
||||
/// (<c>var_4c</c> @272761). This is the collide-THEN-pick order — membership advances ONLY
|
||||
/// here, never before the primary collision (the pre-pick that caused the doorway flap).
|
||||
/// </summary>
|
||||
private TransitionState RunCheckOtherCellsAndAdvance(
|
||||
PhysicsEngine engine, Vector3 footCenter, float sphereRadius)
|
||||
{
|
||||
var sp = SpherePath;
|
||||
if (engine.DataCache is null) return TransitionState.OK;
|
||||
|
||||
uint containingCellId = CellTransit.FindCellSet(
|
||||
engine.DataCache, sp.GlobalSphere, sp.NumSphere, sp.CheckCellId, out var cellSet);
|
||||
LogIssue98CellSetSummary(engine, containingCellId, cellSet, footCenter, sphereRadius);
|
||||
|
||||
var otherCellsState = CheckOtherCells(engine, footCenter, sphereRadius, cellSet);
|
||||
if (otherCellsState != TransitionState.OK)
|
||||
return otherCellsState;
|
||||
|
||||
// Retarget the carried cell to the ordered-pick containing cell (retail check_cell =
|
||||
// var_4c, then adjust_check_pos). SetCheckPos mirrors that id swap + global-sphere
|
||||
// refresh without moving the sphere.
|
||||
if (containingCellId != sp.CheckCellId)
|
||||
sp.SetCheckPos(sp.CheckPos, containingCellId);
|
||||
return TransitionState.OK;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue