fix(p2): cellar-lip wedge — check_other_cells must use the LIVE sphere position

Root cause of the "blocked at the last cellar step" wedge (the primary,
ramp-climb family — 20/29 captured records). The prior session's pinned
"find_walkable is never called during the step-down" was a probe artifact:
a fresh [fc-dispatch]/[step-sphere-down] trace proves Path-3 StepSphereDown
IS reached for both the carried cell and the iterated other-cell.

The real divergence is in Transition.CheckOtherCells. Retail's
check_other_cells (acclient_2013_pseudo_c.txt:272735 → (*cell+0x88)(this))
re-collides the OTHER cells against the LIVE sphere_path.global_sphere — the
position AFTER the primary insert_into_cell ran. The primary collide can MOVE
the sphere: a Path-5 full-hit dispatches step_sphere_up, and a successful
step-up climbs the foot onto the cottage floor yet still returns OK. acdream
instead reused a footCenter snapshot captured BEFORE the primary collide, so
once the lip-riser step-up climbed the foot onto the floor, check_other_cells
still queried 0171 at the pre-climb (sunk ~0.25 m below the floor) position →
the foot spuriously near-missed the very floor it had climbed onto →
neg_step_up → a doomed second step_up vs the floor normal (0,0,1) whose
step_up_slide unwound the climb → validate_transition reverted → 0% advance.

Fix: re-read footCenter = sp.GlobalSphere[0].Origin at the top of
RunCheckOtherCellsAndAdvance (one line). Pre-fix 0/29 wedge records advanced;
post-fix 20/29 climb onto Z≈94.

No regression: full Core suite 1321 pass / 4 fail (the documented baseline:
Apparatus_Grounded_50cmOffCenter, 2× DoorBugTrajectoryReplay LiveCompare_*,
BSPStepUpTests.D4) / 1 skip. The 2 door LiveCompare divergences are
byte-identical with/without the fix (the door's step_up FAILS → sphere
restored → position unchanged → footCenter == live).

Tests: CellarLipWedgeTests.Fix_StaleFootCenter_RampRecordClimbsCottageFloor +
Fix_StaleFootCenter_MajorityOfWedgeRecordsAdvance (new, GREEN).
DocumentsResidualWedge_LiveFloorCp_SlidingNormalKillsPlusY documents the
remaining 9/29 (0,-1,0)-sliding-normal +Y-kill family (slide territory,
deferred to the visual gate).

Apparatus retained (gated on ACDREAM_PROBE_INDOOR_BSP): [fc-dispatch] in
BSPQuery.FindCollisions + [step-sphere-down] in BSPQuery.StepSphereDown +
CellarLipWedgeTests.Diagnostic_TraceRecordByIndex — strip once the residual
is resolved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-05 09:15:19 +02:00
parent bc1be26907
commit cc4590f9e5
4 changed files with 208 additions and 18 deletions

View file

@ -1,6 +1,54 @@
# P2 cellar-lip wedge — CANONICAL handoff (flat-floor contact-plane coin-flip) # P2 cellar-lip wedge — CANONICAL handoff (flat-floor contact-plane coin-flip)
## ▶ NEXT-SESSION KICKOFF (START HERE — supersedes everything below) ## ✅ PRIMARY ROOT CAUSE FOUND + FIXED 2026-06-05 (START HERE — supersedes UPDATE 2 below)
**The pinned "find_walkable is NEVER called during the step-down" (UPDATE 2) was a PROBE ARTIFACT.**
A clean `[fc-dispatch]`/`[step-sphere-down]` trace (TEMP probes, gated on `ACDREAM_PROBE_INDOOR_BSP`,
in `BSPQuery.FindCollisions` + `StepSphereDown`) proved `find_walkable` (Path 3 / `StepSphereDown`)
**IS** reached for both 0175 (primary) and 0171 (other-cell) during the step-down — UPDATE 2 mis-read
it (the `[fc-dispatch]` cell logs `path.CheckCellId` = the carried cell 0175 even while iterating
0171's BSP, because `CheckCellId` is the carried cell, not the iterated one).
**THE REAL ROOT CAUSE (ramp-climb family, 20/29 records):** `Transition.CheckOtherCells` collided the
OTHER cells against a **stale `footCenter`** snapshotted at `FindEnvCollisions` entry (TransitionTypes.cs
~L1959) — i.e. BEFORE the primary `insert_into_cell` ran. The primary collide can MOVE the sphere: a
Path-5 full-hit dispatches `step_sphere_up`, and a successful step-up **climbs the foot onto the cottage
floor yet still returns OK**. Retail's `check_other_cells` (`acclient_2013_pseudo_c.txt:272735`
`(*cell+0x88)(this)`) reads the **LIVE `sphere_path.global_sphere`** (post-insert). acdream used the
pre-climb snapshot, which is sunk ~0.25 m below the floor → the foot spuriously **near-misses the very
floor it just climbed onto** → `neg_step_up` → a doomed SECOND step_up against the floor normal (0,0,1)
whose `step_up_slide` unwinds the climb (it slides relative to `GlobalCurrCenter` = the step start, low Z)
`validate_transition` reverts the whole step → **0 % advance**.
**FIX (shipped):** in `Transition.RunCheckOtherCellsAndAdvance` re-read `footCenter =
sp.GlobalSphere[0].Origin` before iterating other cells. One line + comment. Pre-fix 0/29 records
advanced; post-fix **20/29 climb onto the cottage floor (Z≈94)**. **Zero regression** — full Core suite
1321 pass / 4 fail (the documented baseline 4: `Apparatus_Grounded_50cmOffCenter`,
2× `DoorBugTrajectoryReplay LiveCompare_*`, `BSPStepUpTests.D4`) / 1 skip. The 2 door `LiveCompare`
divergences are **byte-identical** with/without the fix (the door's step_up FAILS → sphere restored →
position unchanged → `footCenter` == live). Tests: `CellarLipWedgeTests.Fix_StaleFootCenter_*` (2 new,
GREEN).
**REMAINING RESIDUAL (9/29, OUT OF SCOPE this pass):** the `(0,-1,0)` sliding-normal **+Y-kill**
(`AdjustOffset` slide-crease projects the into-cottage +Y onto the floor×wall crease = world X and zeroes
it → only X survives → hits the slab X wall → step_up fails on the flat floor → revert). 7/29 records;
record #6 is the canonical one (`DocumentsResidualWedge_LiveFloorCp_SlidingNormalKillsPlusY`). This is
**slide-recovery territory** the kickoff said NOT to re-investigate, and is **suspected to be a
buggy-trajectory artifact** (the stale slide accumulated only because the player was already oscillating;
once the ramp-climb advances cleanly the player should not enter the south-wall-slide-into-doorway state).
**Let the VISUAL GATE decide** whether it needs a follow-up before touching the slide. (Record #21 moves
Y away from the cottage and is likely a legitimate non-advance.)
**VISUAL GATE (next):** run the client, walk up the Holtburg cottage cellar stairs — expect the last-step
wedge GONE (smooth ascent onto the floor). Also re-confirm the inn door BLOCKS and a generic step-up
climbs (the fix only changes `check_other_cells`'s position reference). If the ascent still intermittently
wedges, the `(0,-1,0)` +Y-kill is live → investigate `AdjustOffset` slide-crease / the sliding-normal seed
(with the visual evidence then justifying touching the slide). Apparatus: `[fc-dispatch]`/`[step-sphere-down]`
probes + `CellarLipWedgeTests.Diagnostic_TraceRecordByIndex` reproduce any record in <200 ms.
---
## ▶ NEXT-SESSION KICKOFF (historical — its "find_walkable never called" framing was disproven above)
**State:** M1.5 / P2 cellar-lip "blocked at the last step" wedge. A FAITHFUL deterministic reproduction now **State:** M1.5 / P2 cellar-lip "blocked at the last step" wedge. A FAITHFUL deterministic reproduction now
exists. The cause has been peeled through SIX evidence-disproven framings to one bounded question. No fix exists. The cause has been peeled through SIX evidence-disproven framings to one bounded question. No fix

View file

@ -1236,9 +1236,20 @@ public static class BSPQuery
ResolvedPolygon? polyHit = null; ResolvedPolygon? polyHit = null;
ushort _polyId = 0; // step-down doesn't need the id, but the signature requires it ushort _polyId = 0; // step-down doesn't need the id, but the signature requires it
// TEMP diagnostic (cellar-lip wedge dispatch trace, 2026-06-05): Path 3
// reached — log the step-down probe inputs + the walkable-finder result so
// we can see whether the cottage floor is tested + accepted. STRIP after fix.
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
Console.WriteLine(System.FormattableString.Invariant(
$"[step-sphere-down] ENTER cell=0x{path.CheckCellId:X8} stepDownAmt={path.StepDownAmt:F3} walkInterp={path.WalkInterp:F3} move=({movement.X:F3},{movement.Y:F3},{movement.Z:F3}) center=({checkPos.Center.X:F3},{checkPos.Center.Y:F3},{checkPos.Center.Z:F3}) r={checkPos.Radius:F3}"));
FindWalkableInternal(root, resolved, path, validPos, movement, up, FindWalkableInternal(root, resolved, path, validPos, movement, up,
ref polyHit, ref _polyId, ref changed); ref polyHit, ref _polyId, ref changed);
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
Console.WriteLine(System.FormattableString.Invariant(
$"[step-sphere-down] RESULT cell=0x{path.CheckCellId:X8} changed={changed} poly={(polyHit is null ? "n/a" : $"0x{polyHit.Id:X4} n=({polyHit.Plane.Normal.X:F3},{polyHit.Plane.Normal.Y:F3},{polyHit.Plane.Normal.Z:F3})")}"));
if (changed && polyHit is not null) if (changed && polyHit is not null)
{ {
// ACE: path.LocalSpacePos.LocalToGlobalVec(adjusted) * scale // ACE: path.LocalSpacePos.LocalToGlobalVec(adjusted) * scale
@ -1707,6 +1718,23 @@ public static class BSPQuery
returnState: -1); returnState: -1);
} }
// TEMP diagnostic (cellar-lip wedge dispatch trace, 2026-06-05): which of
// the 6 paths does this cell take? The path is flag-driven (BSP-independent),
// so the synthetic-leaf test reproduces it faithfully. Deduce the path from
// the dispatch order so a single line names path + every gating flag.
// Gated on ACDREAM_PROBE_INDOOR_BSP. STRIP once the wedge fix lands.
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
{
int _p = (path.InsertType == InsertType.Placement || obj.Ethereal) ? 1
: path.CheckWalkable ? 2
: path.StepDown ? 3
: path.Collide ? 4
: obj.State.HasFlag(ObjectInfoState.Contact) ? 5
: 6;
Console.WriteLine(System.FormattableString.Invariant(
$"[fc-dispatch] cell=0x{path.CheckCellId:X8} PATH={_p} stepUp={path.StepUp} stepDown={path.StepDown} chkWalk={path.CheckWalkable} insert={path.InsertType} collide={path.Collide} contact={obj.State.HasFlag(ObjectInfoState.Contact)} ethereal={obj.Ethereal} c0=({sphere0.Center.X:F3},{sphere0.Center.Y:F3},{sphere0.Center.Z:F3}) hasS1={sphere1 is not null}"));
}
// Helper: transform a local-space vector to world space. // Helper: transform a local-space vector to world space.
// ACE: path.LocalSpacePos.LocalToGlobalVec(v) // ACE: path.LocalSpacePos.LocalToGlobalVec(v)
Vector3 L2W(Vector3 v) => Vector3.Transform(v, localToWorld); Vector3 L2W(Vector3 v) => Vector3.Transform(v, localToWorld);

View file

@ -2161,6 +2161,23 @@ public sealed class Transition
var sp = SpherePath; var sp = SpherePath;
if (engine.DataCache is null) return TransitionState.OK; if (engine.DataCache is null) return TransitionState.OK;
// Retail check_other_cells (acclient_2013_pseudo_c.txt:272735) calls each
// other cell's find_collisions with `this`, so it reads the CURRENT
// this->sphere_path.global_sphere — i.e. the position AFTER the primary
// insert_into_cell ran. The primary collide can MOVE the sphere: a Path-5
// full-hit dispatches step_sphere_up, and a successful step_up climbs the
// foot sphere onto a higher surface yet still returns OK. The caller
// captured `footCenter` BEFORE that primary collide, so using it here
// queries the other cells at the PRE-climb position. At the cellar lip
// that pre-climb center is sunk ~0.25 m below the cottage floor, so the
// foot sphere spuriously near-misses the very floor it just climbed onto →
// neg_step_up → a doomed second step_up against the floor normal (0,0,1)
// whose step_up_slide unwinds the climb → validate_transition reverts the
// whole step → 0% advance (the P2 cellar-lip wedge). Re-read the live
// foot-sphere center so check_other_cells sees the post-climb (resting,
// tangent) position, where the floor no longer overlaps. (2026-06-05)
footCenter = sp.GlobalSphere[0].Origin;
uint containingCellId = CellTransit.FindCellSet( uint containingCellId = CellTransit.FindCellSet(
engine.DataCache, sp.GlobalSphere, sp.NumSphere, sp.CheckCellId, out var cellSet); engine.DataCache, sp.GlobalSphere, sp.NumSphere, sp.CheckCellId, out var cellSet);
LogIssue98CellSetSummary(engine, containingCellId, cellSet, footCenter, sphereRadius); LogIssue98CellSetSummary(engine, containingCellId, cellSet, footCenter, sphereRadius);

View file

@ -340,26 +340,123 @@ public class CellarLipWedgeTests
} }
/// <summary> /// <summary>
/// FAITHFUL documents-the-bug (passes while the wedge exists; FAILS when the /// TEMP diagnostic (2026-06-05): trace ONE record by index with full probes,
/// fix lands → flip to assert the climb). Replays a captured FLOOR-contact- /// to a per-index %TEMP%/lip-trace-{idx}.log. Used to compare a ramp record
/// plane wedge through the lip-cell engine; the player is STUCK (0% advance). /// (no sliding normal) against a floor record. STRIP after fix.
/// </summary>
[Theory]
[InlineData(6)] // STILL 0% post-footCenter-fix: flat floor, sliding normal (0,-1,0)
[InlineData(13)] // STILL 0% post-footCenter-fix: ramp, NO sliding normal, motion -X,+Y
[InlineData(0)] // STILL 0% post-footCenter-fix: ramp, sliding normal (0,-1,0)
[InlineData(21)] // STILL 0% post-footCenter-fix: ramp, NO slide, motion -X,-Y (away?)
public void Diagnostic_TraceRecordByIndex(int idx)
{
var rec = LoadWedgeRecords()[idx];
var saved = Console.Out;
var sw = new StringWriter();
PhysicsDiagnostics.ProbeIndoorBspEnabled = true;
PhysicsDiagnostics.ProbeStepWalkEnabled = true;
Environment.SetEnvironmentVariable("ACDREAM_DUMP_STEPUP", "1");
Console.SetOut(sw);
try
{
var (res, req, adv) = ReplayRecord(rec);
Console.SetOut(saved);
var bb = rec.BodyBefore!;
File.WriteAllText(Path.Combine(Path.GetTempPath(), $"lip-trace-{idx}.log"),
$"record #{idx} cur=({rec.Input.CurrentPos.X:F4},{rec.Input.CurrentPos.Y:F4},{rec.Input.CurrentPos.Z:F4}) " +
$"tgt=({rec.Input.TargetPos.X:F4},{rec.Input.TargetPos.Y:F4},{rec.Input.TargetPos.Z:F4}) " +
$"cp=({bb.ContactPlane.Normal.X:F2},{bb.ContactPlane.Normal.Y:F2},{bb.ContactPlane.Normal.Z:F2}) " +
$"slide=({bb.SlidingNormal.X:F2},{bb.SlidingNormal.Y:F2},{bb.SlidingNormal.Z:F2}) ts=0x{bb.TransientState:X2} " +
$"req={req:F3} adv={adv:F3} res=({res.X:F4},{res.Y:F4},{res.Z:F4})\n\n" + sw.ToString());
Assert.True(true);
}
finally
{
Console.SetOut(saved);
Environment.SetEnvironmentVariable("ACDREAM_DUMP_STEPUP", null);
PhysicsDiagnostics.ProbeIndoorBspEnabled = false;
PhysicsDiagnostics.ProbeStepWalkEnabled = false;
}
}
/// <summary>
/// FIX VALIDATION (2026-06-05) — the stale-footCenter fix in
/// <c>RunCheckOtherCellsAndAdvance</c>. Retail's <c>check_other_cells</c>
/// (acclient_2013_pseudo_c.txt:272735) re-collides the OTHER cells against the
/// LIVE <c>sphere_path.global_sphere</c> — i.e. AFTER the primary cell's
/// <c>insert_into_cell</c> may have moved the sphere via a successful
/// <c>step_sphere_up</c>. acdream captured the foot-sphere center BEFORE the
/// primary collide and reused that stale snapshot, so once the lip-riser
/// step_up climbed the foot onto the cottage floor, <c>check_other_cells</c>
/// still queried 0171 at the pre-climb (sunk, penetrating) position → the foot
/// spuriously near-missed the very floor it had climbed onto → a doomed second
/// step_up against the floor normal whose slide unwound the climb →
/// validate_transition reverted → 0% advance.
/// ///
/// <para> /// <para>
/// <b>Root cause (traced via <c>Diagnostic_ReplayFloorCpRecord_StepUpProbes</c>):</b> /// Pre-fix: 0/29 captured wedge records advanced. Post-fix: the ramp-climb
/// the player is at the doorway EDGE of the cottage floor (0171, poly 0x0023, /// family (≈20/29) advances onto the cottage floor (Z≈94). This asserts a
/// Z=94). The step-up's step-down finds that floor and the 0.48 sphere /// representative ramp record (#9, cp Z=0.78, no sliding normal) now climbs.
/// OVERLAPS it (0.085 m below), but acdream's walkable check REJECTS it
/// because the sphere center projects outside the floor poly's edge
/// (<c>insideEdges=False</c>, <c>gap=0.395</c>) → no contact plane → step-up
/// fails → StepUpSlide=Collided. Retail accepts the floor at its edge and
/// crosses (0175 never blocks). Fix is in the walkable-edge acceptance
/// (WalkableHitsSphere / PolygonHitsSpherePrecise / CheckWalkable edge math)
/// — compare retail CPolygon::walkable_hits_sphere + check_walkable. DOOR
/// REGRESSION RISK: walkable changes are global; visual-gate + door tests.
/// </para> /// </para>
/// </summary> /// </summary>
[Fact] [Fact]
public void DocumentsWedge_LiveFloorCp_PlayerStuckAtCottageFloorEdge() public void Fix_StaleFootCenter_RampRecordClimbsCottageFloor()
{
var rec = LoadWedgeRecords()[9];
var (res, requested, advance) = ReplayRecord(rec);
Assert.True(advance > 0.25f * requested && res.Z >= CottageFloorZ - 0.05f,
$"Expected ramp record #9 to climb onto the cottage floor after the " +
$"stale-footCenter fix. advance={advance:F3} (req={requested:F3}), " +
$"res=({res.X:F3},{res.Y:F3},{res.Z:F3}); want advance>0.25·req and Z≥{CottageFloorZ - 0.05f:F2}.");
}
/// <summary>
/// FIX REGRESSION GUARD (2026-06-05): the majority of captured wedge records
/// advance after the stale-footCenter fix. Pre-fix 0/29 → post-fix ≈20/29.
/// A drop here means <c>check_other_cells</c> is again querying other cells at
/// a stale pre-step_up position (the cellar-lip wedge regressed).
/// </summary>
[Fact]
public void Fix_StaleFootCenter_MajorityOfWedgeRecordsAdvance()
{
var recs = LoadWedgeRecords();
int advanced = 0, total = 0;
foreach (var rec in recs)
{
if (rec.BodyBefore is null) continue;
total++;
var (res, req, adv) = ReplayRecord(rec);
if (adv > 0.25f * req) advanced++;
}
Assert.True(advanced >= 18,
$"Expected ≥18 of {total} captured wedge records to advance >0.25·req " +
$"after the stale-footCenter fix; got {advanced}.");
}
/// <summary>
/// DOCUMENTS-THE-BUG (passes while a RESIDUAL wedge exists; flip when fixed).
/// Record #6 is a FLOOR-contact-plane record that ALSO carries a stale
/// <c>(0,-1,0)</c> sliding normal (the cottage south wall). The stale-footCenter
/// fix does NOT clear it: <c>AdjustOffset</c>'s slide-crease projects the
/// into-cottage +Y motion onto the floor×wall crease (the world X axis) and
/// ZEROES it before the sphere moves, so only the X residual survives → it
/// full-hits the slab's X wall → a step_up that fails on the flat floor (no CP)
/// → Collided → revert → 0% advance.
///
/// <para>
/// This is the SEPARATE "(0,-1,0) sliding-normal +Y-kill" family (7/29 records).
/// It is slide-recovery territory — explicitly OUT OF SCOPE for this pass per
/// the kickoff ("do not re-investigate ... slide") — and is suspected to be a
/// buggy-trajectory artifact (the stale slide accumulated only because the
/// player was already oscillating; once the ramp-climb advances cleanly the
/// player should not enter the south-wall-slide-into-doorway state). The visual
/// gate decides whether it needs a follow-up. If the residual is fixed, flip
/// this to require advance &gt; 0.25·requested.
/// </para>
/// </summary>
[Fact]
public void DocumentsResidualWedge_LiveFloorCp_SlidingNormalKillsPlusY()
{ {
var recs = LoadWedgeRecords(); var recs = LoadWedgeRecords();
var rec = recs.First(r => r.BodyBefore is not null var rec = recs.First(r => r.BodyBefore is not null
@ -367,10 +464,10 @@ public class CellarLipWedgeTests
var (res, requested, advance) = ReplayRecord(rec); var (res, requested, advance) = ReplayRecord(rec);
var c = rec.Input.CurrentPos; var t = rec.Input.TargetPos; var c = rec.Input.CurrentPos; var t = rec.Input.TargetPos;
Assert.True(advance < 0.1f * requested, Assert.True(advance < 0.1f * requested,
$"DOCUMENTS-THE-BUG: expected the player STUCK at the cottage-floor edge. " + $"DOCUMENTS-RESIDUAL: expected the player STUCK (sliding-normal +Y-kill). " +
$"Instead it advanced: cur=({c.X:F3},{c.Y:F3},{c.Z:F3}) tgt=({t.X:F3},{t.Y:F3},{t.Z:F3}) " + $"Instead it advanced: cur=({c.X:F3},{c.Y:F3},{c.Z:F3}) tgt=({t.X:F3},{t.Y:F3},{t.Z:F3}) " +
$"res=({res.X:F3},{res.Y:F3},{res.Z:F3}) requested={requested:F3} advance={advance:F3}. " + $"res=({res.X:F3},{res.Y:F3},{res.Z:F3}) requested={requested:F3} advance={advance:F3}. " +
$"If the walkable-edge fix landed, FLIP this to require advance>0.25·requested."); $"If the slide +Y-kill residual is fixed, FLIP this to require advance>0.25·requested.");
} }
private static PhysicsEngine BuildEngineWithLipFixtures() private static PhysicsEngine BuildEngineWithLipFixtures()