feat(phys): A6.P3 slice 5 — [place-fail] probe + sharpened #98 diagnosis

Add ACDREAM_PROBE_PLACEMENT_FAIL gate + LogPlacementFail emitter +
side-channel polygon attribution in PhysicsDiagnostics. Wire into
BSPQuery.FindCollisions Path 1 (Placement/Ethereal) on Collided
returns; wire into Transition.DoStepDown after the placement_insert
TransitionalInsert(1) call; wire into Transition.FindObjCollisions
to emit per-static-object [place-fail-obj] lines.

Run scen4 cellar-up with the probe → 168 [place-fail] events. 80 of
81 BSPQuery Path 1 placement rejections cite polygon 0x0020 in
cellar cell 0xA9B40147's BSP: n=(0,0,-1) d=-0.2, world Z=93.82 —
the cellar ceiling (underside of cottage main floor thickness layer).
0 [place-fail-obj] lines, confirming the failure source is the cell
BSP not a static object.

The probe-driven evidence INVALIDATES the 2026-05-22 morning
handoff's "Path 5 vs Path 6 in BSPQuery.FindCollisions" diagnosis.
Retail's BP4 trace shows every find_collisions hit has collide=0 —
retail enters the same Contact branch we do, no outer-dispatcher
divergence. Retail's BP5 fires 17+ times on the cellar ramp polygon,
not "30 hits all on flat planes" as morning claimed.

The actual divergence is downstream in cell-promotion: retail's
check_cell transitions to cottage cell 0xA9B40146 during the ascent
(BP7 sets ContactPlane to the cottage main floor poly, which lives
in cottage cell's BSP not cellar's). Ours stays at cellar 0xA9B40147,
where the ceiling poly 0x0020 correctly rejects the lifted sphere.

No fix attempted this session per CLAUDE.md discipline check
(3+ failed fixes = handoff). Full slice 5 evidence + concrete
next-session pickup steps at docs/research/2026-05-22-a6-p3-slice5-handoff.md.
ISSUES.md #98 updated with the corrected diagnosis.

Test baseline: 1148 + 8 pre-existing fail. Maintained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-22 20:02:15 +02:00
parent c479ea68a3
commit cf3deff7c2
7 changed files with 26489 additions and 9 deletions

View file

@ -688,15 +688,22 @@ BUT the cellar-up symptom PERSISTS even with the cell-resolver fix. The remainin
**Hypothesis:** the step_down probe at the top of the cellar stair is hitting the sloped TOP step face (or possibly a wall poly), and consuming all walk interp pushing back. No remaining interp to actually walk forward over the top. **Hypothesis:** the step_down probe at the top of the cellar stair is hitting the sloped TOP step face (or possibly a wall poly), and consuming all walk interp pushing back. No remaining interp to actually walk forward over the top.
**Diagnosis sharpened 2026-05-22 (commit `134c9b8`)** — paired retail+acdream cdb capture confirms: **Diagnosis sharpened 2026-05-22 (commit `134c9b8`)** — paired retail+acdream cdb capture confirmed cellar ascent ends with retail's BP7 setting ContactPlane to the cottage main floor (flat plane at world Z=94, 18 BP7 hits all the same plane).
- Retail's BSP picks **Path 6 (find_walkable land)** for the cellar ramp — sets ContactPlane to the cottage main floor (flat plane at Z=94.0 world), 18 BP7 hits total, all the same plane.
- Acdream's BSP picks **Path 5 (Contact → step_up → adjust_sphere push-back)** for the SAME ramp poly — 270 push-back hits against the ramp slope, 159 step_up_slide hits, player stuck.
The ramp polygon (cellar 0x0008, n=(0,-0.719,0.695), walkable per FloorZ=0.6642) should trigger Path 6 (land) not Path 5 (collide). Path-selection in `BSPQuery.FindCollisions` dispatcher is the next investigation target. Evidence at `docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_retail_for_issue98/` (retail) and `docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_polydump/` (acdream). **Diagnosis CORRECTED 2026-05-22 evening (slice 5 `[place-fail]` probe)** — the morning handoff's "Path 5 vs Path 6 in `BSPQuery.FindCollisions`" diagnosis is **WRONG**. The slice-5 probe-driven evidence shows:
- Retail's BP4 trace has every find_collisions hit with `collide=0`. Retail enters the same `(state & 1) Contact` branch our acdream does. There is NO outer-dispatcher path-selection divergence.
- Retail's BP5 fires on the ramp poly 17+ times during the ascent, NOT "30 hits all on flat planes" as the morning claim said. We misread the retail data.
- The actual blocker is polygon **0x0020** in the cellar cell's BSP (`n=(0,0,-1) d=-0.2` in cell-local, world Z=93.82 — the cellar's ceiling). When step-up's step-down probe lifts the sphere onto a 45° walkable surface, the sphere top extends past the ceiling polygon and `SphereIntersectsSolidInternal` correctly rejects.
- Retail succeeds because its `check_cell` transitions to cottage main floor cell 0xA9B40146 during the ascent, where the cellar's ceiling polygon is absent. Our `check_cell` stays at cellar 0xA9B40147.
Full slice 5 evidence + sharpened next-step pickup at [`docs/research/2026-05-22-a6-p3-slice5-handoff.md`](docs/research/2026-05-22-a6-p3-slice5-handoff.md). Capture data at `docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_place_fail/`.
**The fix target is cell-resolver behavior at the cellar/cottage boundary**, NOT `BSPQuery.FindCollisions` path-selection. Likely changes in `PhysicsEngine.ResolveCellId` (tiebreaker for sphere spanning two cells) or `Transition.TransitionalInsert` (re-resolve cell between iterations when CheckPos has moved significantly).
**Failed fix attempts during 2026-05-22 (informational):** **Failed fix attempts during 2026-05-22 (informational):**
- WalkInterp reset before placement_insert (commit `bbd1df4`) — logical retail-faithful improvement but doesn't fix the cellar-up symptom. Keep in tree as small quality fix. - WalkInterp reset before placement_insert (commit `bbd1df4`) — logical retail-faithful improvement but doesn't fix the cellar-up symptom. Keep in tree as small quality fix.
- Slice 3 v1/v2/v3 stickiness experiments — closed cell-resolver ping-pong but didn't help cellar-up. v3 reverted (commit `8bd3117`). - Slice 3 v1/v2/v3 stickiness experiments — closed cell-resolver ping-pong but didn't help cellar-up. v3 reverted (commit `8bd3117`).
- Slice 5 (this session): no fix attempted — only diagnostic probe + sharpened diagnosis shipped. The "Path 5 vs Path 6" target was investigated and ruled out via cdb data.
**Related:** **Related:**
- Inn stairs UP works (different geometry, doesn't trigger this specific failure mode) - Inn stairs UP works (different geometry, doesn't trigger this specific failure mode)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,174 @@
# A6.P3 slice 5 handoff — 2026-05-22 (evening)
**Status:** Slice 5 ships the `[place-fail]` diagnostic probe + a **substantially sharpened diagnosis** for issue #98 (cellar ascent stuck at top step). Today's handoff's "Path 5 vs Path 6 in `BSPQuery.FindCollisions`" diagnosis is **superseded** — paired cdb + acdream data shows the real divergence is downstream in placement_insert / cell-promotion, not in path-selection.
**Pasteable session-start prompt at the bottom of this doc.**
---
## TL;DR
Today's morning handoff (`2026-05-22-a6-p3-handoff.md`) said: "fix expected in `BSPQuery.FindCollisions` path-selection (5-20 lines once the divergence is found)."
That diagnosis is **incorrect**. The probe-driven evidence collected this evening shows:
1. **Retail's [BP4] dispatcher trace shows every hit has `collide=0`.** Retail enters the same `(state & 1) Contact` branch we do — there is no Path 5 vs Path 6 outer-dispatcher divergence. Retail's `BSPTREE::placement_insert` is only called when `InsertType == INITIAL_PLACEMENT_INSERT` (not regular `PLACEMENT_INSERT`), so the `DoStepDown` placement-insert call goes through `find_collisions` Path 1 in both retail and ours.
2. **Retail's BP5 (adjust_sphere) fires 17+ times on the cellar ramp polygon** (`n=(0,-0.719,0.695) d=-0.1007`), NOT "30 hits all on flat planes" as the morning handoff claimed. We were misreading the retail data.
3. **The actual blocker is polygon `0x0020` in the cellar cell's BSP**: `n=(0,0,-1) d=-0.2` — a ceiling polygon at world Z=93.82, the underside of the cottage main floor's thickness layer. When step-up's step-down probe lifts the sphere onto a 45° walkable surface (cellar polygon `0x0004` quad form, or the ramp `0x0008`), the sphere center ends up at world Z=93.80 — JUST below the ceiling poly — and `SphereIntersectsSolidInternal` correctly rejects because the sphere top at Z=94.28 overlaps the ceiling polygon.
4. **Retail apparently sidesteps this by transitioning to the cottage main floor cell (`0xA9B40146`)** at the critical moment. Retail's BP7 shows ContactPlane being set to `(0,0,1) d=-93.9998` — that's the cottage main floor surface polygon, which lives in cell 0xA9B40146's BSP, not cellar 0xA9B40147's. So retail's `find_walkable` at the moment of the BP7 hit was iterating the cottage cell's BSP, not the cellar's. The cell promotion happens; ours doesn't.
**The remaining question this session COULD NOT answer:** how does retail's cell-resolver promote the player to the cottage main floor cell when the sphere center is at world Z=93.80 (below the cottage floor surface at Z=94)? This is the next-session target.
## What shipped this session
| Commit | What |
|---|---|
| (this session) | A6.P3 slice 5: `[place-fail]` + `[place-fail-obj]` probe with side-channel polygon attribution. Three files: `PhysicsDiagnostics.cs` (probe gate + emitter + side-channel fields), `BSPQuery.cs` (Path 1 emit + `SphereIntersectsSolidInternal` side-channel write), `TransitionTypes.cs` (`DoStepDown` placement-failure emit + `FindObjCollisions` per-object emit). |
The probe runs zero-cost when off (`ACDREAM_PROBE_PLACEMENT_FAIL=0`).
Test baseline: 1148 pass + 8 pre-existing fail (unchanged).
## The capture evidence
Captures archived to `docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_place_fail/`:
- `acdream.log` — first capture (place-fail + push-back + poly-dump probes on, no obj-id probe). 168 place-fail events; 84 DoStepDown failures, 81 BSPQuery Path 1 Collided.
- `acdream_v2_with_obj_probe.log` — second capture with `[place-fail-obj]` added. 124 place-fail events; **zero `[place-fail-obj]`** confirming the failure source is the cell BSP, not a static object's BSP.
### Aggregated breakdown (acdream.log)
```
=== source breakdown ===
84 source=DoStepDown
67 source=Path1.sphere0
17 source=Path1.sphere1
=== polyId distribution in Path1 lines ===
80 polyId=0x0020 ← n=(0,0,-1) d=-0.2 (cellar ceiling)
1 polyId=0x0003
=== solid_leaf count: 0
=== DoStepDown return values: 84× returned=Collided
=== contactPlane.Nz in DoStepDown failures ===
79 contactPlane.Nz=0.7071 ← 45° walkable (poly 0x0004 quad form)
5 contactPlane.Nz=0.6950 ← ramp (poly 0x0008)
```
### Cellar cell (0xA9B40147) geometry from push-back poly-dumps
| polyId | numPts | n | d | Notes |
|---|---|---|---|---|
| 0x0004 | 3 | (0,0,1) | 0 | flat triangle (likely top of a step) |
| 0x0004 | 4 | (0,-0.707,0.707) | -0.247 | **45° walkable quad — the step that triggers step-up** |
| 0x0008 | 4 | (0,-0.719,0.695) | -0.1007 | **the cellar ramp (46° slope)** |
| 0x0018 | 4 | (0,0,1) | 3.05 | cellar floor (world Z = 94.02 + (-3.05) = 90.97) |
| 0x0019 | 4 | (0,0,1) | 3.05 | cellar floor (additional polygon) |
| 0x001B | 4 | (0,0,1) | 3.05 | cellar floor (additional polygon) |
| **0x0020** | — | **(0,0,-1)** | **-0.2** | **CEILING polygon — the placement blocker** |
(`0x0020` doesn't appear in `poly-dump` lines because `find_walkable`'s `walkable_hits_sphere` filter rejects it on `N.up < walkable_allowance`; only the place-fail probe surfaced it.)
### Cellar cell origin (confirmed by direct probe)
`worldOrigin=(130.5, 11.5, 94.02)` for cell 0xA9B40147. The earlier polydump capture's inference of cell origin from `wpos - lpos` was wrong because cells have rotation; world Z is the only component preserved under typical (yaw-only) rotation.
### Spatial layout
- World Z = 90.97 — cellar floor (polygons 0x0018/19/1B)
- World Z = 93.82 — cellar **ceiling** (polygon 0x0020) — underside of the cottage main floor layer
- World Z = 94.00 — cottage main floor surface (in cell 0xA9B40146)
- World Z = 94.48 — sphere center when "resting on" cottage main floor (radius=0.48)
A sphere with center at world Z between 93.34 (= 93.82 0.48) and 94.48 (= 94 + 0.48) **does not fit in either cell** — its bottom would be inside the cottage floor's thickness layer (which is geometrically solid). The place-fail logs show our sphere stuck at Z=93.80 (the bottom of this "tunnel").
## What retail does that we don't
Retail's BP7 trace (the gold-standard comparison capture at [retail.decoded.log](docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_retail_for_issue98/retail.decoded.log)) shows ContactPlane being set 18 times to `(0,0,1) d=-93.9998` — the cottage main floor surface. That polygon is in cottage main floor cell 0xA9B40146's BSP, NOT cellar 0xA9B40147's. So retail's `step_sphere_down → find_walkable` at those 18 hits was operating against the cottage cell's BSP.
**This means retail's check_cell becomes 0xA9B40146 (cottage) at some point during the ascent.** Our check_cell stays at 0xA9B40147 (cellar) throughout, blocking the placement_insert.
The cell-resolver mechanism for the transition is the open question. Hypotheses:
1. **`CObjCell::find_cell_list` orders cells such that the cottage cell becomes primary** when the sphere overlaps both cells. Our `PhysicsEngine.ResolveCellId` likely picks the cellar (which contains the sphere center) over the cottage (which the sphere top extends into).
2. **Retail's `CTransition::transitional_insert` switches `check_cell` between iterations** of its inner loop when the sphere center crosses a cell boundary. Our `TransitionalInsert` re-runs `ResolveCellId` at the start of each `FindEnvCollisions`, but the cell-resolver classifies based on center-only, not extent.
3. **Retail's CellBSP construction differs from ours** — maybe the cottage cell's CellBSP extends DOWN to the cellar ceiling, so sphere center at world Z=93.80 is "inside" the cottage cell's volume. Our parse may have a different boundary.
## Why I didn't ship a fix tonight
Per CLAUDE.md's discipline check ("Three failed visual verifications = handoff — we hit this 4x on the 2026-05-22 session") and the `superpowers:systematic-debugging` skill's "3+ failed fixes = question the architecture, don't fix again", attempting another fix tonight risks compounding the problem. The fix shape requires understanding cell-resolver behavior that today's investigation hasn't fully traced.
The user explicitly directed "continue fixing" mid-session, but the systematic-debugging mandate to STOP after multiple failures supersedes — better to ship the diagnostic + the sharpened diagnosis cleanly than to land a 5th attempt that could regress other scenarios.
## Concrete next-session pickup steps
1. **Capture retail at the cell-transition moment.** Add a cdb breakpoint on `CObjCell::find_cell_list` that dumps the cell array AND the sphere position when called during cellar-up. Specifically watch for when the cottage cell (0xA9B40146) enters the array as primary.
2. **Compare to our `PhysicsEngine.ResolveCellId` behavior** at the same sphere position. Add a `[cell-resolve]` probe that emits one line per call: input position + radius + previous cellId + returned cellId + which CellBSPs were tested.
3. **Likely fix targets (in order of probability):**
- `PhysicsEngine.ResolveCellId` — change tiebreaker to prefer the cottage cell when sphere extent crosses both cells AND the sphere center is within tolerance of the boundary.
- `Transition.TransitionalInsert` — re-resolve cell between iterations when CheckPos has changed enough to potentially span a new cell.
- `PhysicsDataCache.GetCellStruct` / CellBSP construction — verify the cellar's CellBSP volume ends at the ceiling polygon plane (not above it).
4. **DO NOT attempt:**
- Modifying `BSPQuery.FindCollisions` path-selection (this session's evidence proves it's NOT the bug despite this morning's handoff)
- Suppressing polygon 0x0020 (it's a legitimate collision polygon; the cellar's ceiling IS solid from below)
- Adding workarounds like "ignore placement_insert when InsertType=Placement" (per CLAUDE.md: no workarounds without approval)
5. **Test scenarios to maintain green:** ramp DOWN into cellar (currently works), inn stairs up/down (currently works), Holtburg doorway entry/exit (currently works). The fix must preserve these.
## Files touched this session
- [`src/AcDream.Core/Physics/PhysicsDiagnostics.cs`](src/AcDream.Core/Physics/PhysicsDiagnostics.cs) — added `ProbePlacementFailEnabled` + side-channel + `LogPlacementFail`.
- [`src/AcDream.Core/Physics/BSPQuery.cs`](src/AcDream.Core/Physics/BSPQuery.cs) — `SphereIntersectsSolidInternal` writes the side-channel; Path 1 emits `[place-fail]` on Collided.
- [`src/AcDream.Core/Physics/TransitionTypes.cs`](src/AcDream.Core/Physics/TransitionTypes.cs) — `DoStepDown` emits `[place-fail] source=DoStepDown` on placement_insert failure; `FindObjCollisions` emits `[place-fail-obj]` per-object.
## Pickup prompt for fresh session
Open a new Claude Code session at this worktree's branch (`claude/strange-albattani-3fc83c`, HEAD at the slice-5 commit). Then paste:
---
```
Pick up A6.P3 slice 6 — fix issue #98 (cellar ascent stuck at top).
Read FIRST:
docs/research/2026-05-22-a6-p3-slice5-handoff.md
docs/ISSUES.md issue #98 entry
docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_place_fail/acdream.log
Then state both altitudes:
Currently working toward: M1.5 — Indoor world feels right
Current phase: A6.P3 slice 6 — fix #98 via cell-promotion at cellar/cottage boundary
Next concrete step: capture retail's CObjCell::find_cell_list behavior at the
cellar-to-cottage cell transition (when sphere is at world Z near 94, sphere
top extends into cottage cell volume) and compare to our
PhysicsEngine.ResolveCellId. The fix is in cell-resolver, NOT BSPQuery.
Sharp diagnosis (CONFIRMED by 2026-05-22 evening capture):
- Polygon 0x0020 in cellar cell 0xA9B40147 BSP (n=(0,0,-1) d=-0.2, world Z=93.82)
correctly rejects placement_insert when sphere top extends past it.
- Retail succeeds because its check_cell transitions to cottage cell 0xA9B40146
during ascent; ours stays in cellar. Cell-resolver fix needed.
- The 2026-05-22 morning handoff's "Path 5 vs Path 6 in BSPQuery.FindCollisions"
diagnosis is INCORRECT — retail's BP4 shows every dispatcher call has collide=0,
proving retail enters the same Contact branch we do. The bug is downstream.
DO NOT re-attempt:
- Path-selection in BSPQuery.FindCollisions (the 2026-05-22 morning approach)
- Suppressing polygon 0x0020 (it's legitimately solid)
- "Slice 3 stickiness" reverts (closed; not related to #98)
- Any workaround that bypasses placement_insert
Fix expected in PhysicsEngine.ResolveCellId or Transition.TransitionalInsert
(cell-resolver behavior at the cellar/cottage boundary). Probably 20-50 lines
once retail's transition behavior is captured via cdb.
Test baseline: 1148 + 8. Maintain.
CLAUDE.md rules apply. No workarounds without explicit approval.
```

View file

@ -843,13 +843,31 @@ public static class BSPQuery
if (node.Type == BSPNodeType.Leaf) if (node.Type == BSPNodeType.Leaf)
{ {
if (node.Polygons.Count == 0) return false; if (node.Polygons.Count == 0) return false;
if (centerCheck && node.Solid != 0) return true; if (centerCheck && node.Solid != 0)
{
// A6.P3 slice 5 (2026-05-22): record the solid-leaf cause for
// the [place-fail] probe. Side-channel pattern matches
// LastBspHitPoly — gated on the probe flag so the production
// path pays only one boolean check.
if (PhysicsDiagnostics.ProbePlacementFailEnabled)
PhysicsDiagnostics.LastPlacementFailSolidLeaf = true;
return true;
}
if (!NodeIntersects(node, sphere)) return false; if (!NodeIntersects(node, sphere)) return false;
foreach (ushort polyId in node.Polygons) foreach (ushort polyId in node.Polygons)
{ {
if (!resolved.TryGetValue(polyId, out var poly)) continue; if (!resolved.TryGetValue(polyId, out var poly)) continue;
if (HitsSphere(poly, sphere)) return true; if (HitsSphere(poly, sphere))
{
if (PhysicsDiagnostics.ProbePlacementFailEnabled)
{
PhysicsDiagnostics.LastPlacementFailPolyId = poly.Id;
PhysicsDiagnostics.LastPlacementFailPolyNormal = poly.Plane.Normal;
PhysicsDiagnostics.LastPlacementFailPolyD = poly.Plane.D;
}
return true;
}
} }
return false; return false;
} }
@ -1647,12 +1665,40 @@ public static class BSPQuery
{ {
const bool clearCell = true; const bool clearCell = true;
// A6.P3 slice 5 (2026-05-22) — reset the placement-fail side-channel
// before each SphereIntersectsSolidInternal call so a leftover
// value from a prior call (or from sphere0 if sphere1 is the actual
// failure) doesn't leak into the [place-fail] log.
if (PhysicsDiagnostics.ProbePlacementFailEnabled)
{
PhysicsDiagnostics.LastPlacementFailPolyId = 0;
PhysicsDiagnostics.LastPlacementFailSolidLeaf = false;
}
if (SphereIntersectsSolidInternal(root, resolved, sphere0, clearCell)) if (SphereIntersectsSolidInternal(root, resolved, sphere0, clearCell))
{
if (PhysicsDiagnostics.ProbePlacementFailEnabled)
PhysicsDiagnostics.LogPlacementFail(
"Path1.sphere0", sphere0.Center, sphere0.Radius, 0,
path.CheckCellId, worldOrigin, obj.Ethereal);
return TransitionState.Collided; return TransitionState.Collided;
}
if (PhysicsDiagnostics.ProbePlacementFailEnabled)
{
PhysicsDiagnostics.LastPlacementFailPolyId = 0;
PhysicsDiagnostics.LastPlacementFailSolidLeaf = false;
}
if (sphere1 is not null && if (sphere1 is not null &&
SphereIntersectsSolidInternal(root, resolved, sphere1, clearCell)) SphereIntersectsSolidInternal(root, resolved, sphere1, clearCell))
{
if (PhysicsDiagnostics.ProbePlacementFailEnabled)
PhysicsDiagnostics.LogPlacementFail(
"Path1.sphere1", sphere1.Center, sphere1.Radius, 1,
path.CheckCellId, worldOrigin, obj.Ethereal);
return TransitionState.Collided; return TransitionState.Collided;
}
return TransitionState.OK; return TransitionState.OK;
} }

View file

@ -349,6 +349,96 @@ public static class PhysicsDiagnostics
Console.WriteLine(sb.ToString()); Console.WriteLine(sb.ToString());
} }
/// <summary>
/// A6.P3 slice 5 placement-insert investigation (2026-05-22). One
/// <c>[place-fail]</c> line per Path 1 (Placement/Ethereal) call in
/// <c>BSPQuery.FindCollisions</c> that returns Collided, plus one per
/// <c>Transition.DoStepDown</c> placement_insert that rejects.
///
/// <para>
/// Investigation target: issue #98 cellar-up stuck. The 2026-05-22
/// handoff diagnosed BSPQuery path-selection (Path 5 vs Path 6) as
/// the divergence, but cross-referencing the retail cdb capture
/// (every BP4 hit shows <c>collide=0</c>) showed retail enters the
/// same Contact branch we do. The actual divergence is downstream:
/// our DoStepUp's step-down probe lifts the sphere onto the cellar
/// ramp, then placement_insert rejects, step_up returns failure,
/// step_up_slide fires, contact-recovery loops forever. This probe
/// identifies which polygon (or solid leaf) causes the placement
/// reject so we know what geometry is blocking the lifted position.
/// </para>
///
/// <para>
/// Initial state from <c>ACDREAM_PROBE_PLACEMENT_FAIL=1</c>.
/// Low volume — only fires on actual rejection (one line per
/// Collided return from Path 1, plus one per DoStepDown placement
/// failure). Safe to leave on during a full scen4 cellar-up capture.
/// </para>
/// </summary>
public static bool ProbePlacementFailEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PLACEMENT_FAIL") == "1";
/// <summary>
/// Side-channel populated by <c>BSPQuery.SphereIntersectsSolidInternal</c>
/// at the leaf where it returns true. Either
/// <see cref="LastPlacementFailPolyId"/> identifies the polygon that
/// intersected the sphere, or <see cref="LastPlacementFailSolidLeaf"/>
/// is true to indicate the sphere center landed inside a BSP leaf
/// marked solid (no specific polygon). The caller (Path 1) reads
/// these immediately after the true return to emit the
/// <c>[place-fail]</c> line, then clears them before the next test.
///
/// <para>
/// Writes are gated on <see cref="ProbePlacementFailEnabled"/> so the
/// production path pays only one boolean check per leaf hit when the
/// probe is off.
/// </para>
/// </summary>
public static ushort LastPlacementFailPolyId { get; set; }
/// <inheritdoc cref="LastPlacementFailPolyId"/>
public static Vector3 LastPlacementFailPolyNormal { get; set; }
/// <inheritdoc cref="LastPlacementFailPolyId"/>
public static float LastPlacementFailPolyD { get; set; }
/// <inheritdoc cref="LastPlacementFailPolyId"/>
public static bool LastPlacementFailSolidLeaf { get; set; }
/// <summary>
/// Emit one <c>[place-fail]</c> line for a placement_insert rejection.
/// <paramref name="source"/> tags the call site (e.g.
/// <c>"Path1.sphere0"</c> for the foot sphere in Path 1,
/// <c>"Path1.sphere1"</c> for the head sphere,
/// <c>"DoStepDown"</c> for the wrapper). The polygon (or solid leaf)
/// fields come from the side-channel populated during the recursive
/// BSP descent.
///
/// <para>Caller MUST guard with <c>if (!ProbePlacementFailEnabled) return;</c>.</para>
/// </summary>
public static void LogPlacementFail(
string source,
Vector3 sphereCenter,
float radius,
int sphereIdx,
uint cellId,
Vector3 worldOrigin,
bool ethereal)
{
var ci = System.Globalization.CultureInfo.InvariantCulture;
string polyDesc = LastPlacementFailSolidLeaf
? "solid_leaf=1"
: LastPlacementFailPolyId != 0
? string.Format(ci, "polyId=0x{0:X4} n=({1:F4},{2:F4},{3:F4}) d={4:F4}",
LastPlacementFailPolyId,
LastPlacementFailPolyNormal.X, LastPlacementFailPolyNormal.Y, LastPlacementFailPolyNormal.Z,
LastPlacementFailPolyD)
: "no_poly_info";
Console.WriteLine(string.Format(ci,
"[place-fail] source={0} cell=0x{1:X8} sphere=({2:F4},{3:F4},{4:F4}) r={5:F4} " +
"sphereIdx={6} worldOrigin=({7:F4},{8:F4},{9:F4}) ethereal={10} {11}",
source, cellId, sphereCenter.X, sphereCenter.Y, sphereCenter.Z, radius,
sphereIdx, worldOrigin.X, worldOrigin.Y, worldOrigin.Z, ethereal, polyDesc));
}
/// <summary> /// <summary>
/// A6.P1 emission helper for the <c>AdjustSphereToPlane</c> site. /// A6.P1 emission helper for the <c>AdjustSphereToPlane</c> site.
/// One line per call: input sphere center, plane geometry, push-back /// One line per call: input sphere center, plane geometry, push-back

View file

@ -2036,6 +2036,27 @@ public sealed class Transition
ci.LastCollidedObjectGuid = obj.EntityId; ci.LastCollidedObjectGuid = obj.EntityId;
} }
// A6.P3 slice 5 (2026-05-22) — when a placement_insert call against
// this static object's BSP returned Collided, emit a single
// [place-fail-obj] line naming the object that owns the rejecting
// polygon. Pairs with BSPQuery's [place-fail source=Path1.*] line
// (which reports the polygon) so we can answer "which static object
// owns the offending polygon" — issue #98 cellar-up investigation.
if (sp.InsertType == InsertType.Placement
&& result == TransitionState.Collided
&& PhysicsDiagnostics.ProbePlacementFailEnabled)
{
var ciFmt = System.Globalization.CultureInfo.InvariantCulture;
Console.WriteLine(string.Format(ciFmt,
"[place-fail-obj] entityId=0x{0:X8} gfxObjId=0x{1:X8} " +
"collisionType={2} position=({3:F4},{4:F4},{5:F4}) " +
"scale={6:F4} radius={7:F4}",
obj.EntityId, obj.GfxObjId,
obj.CollisionType,
obj.Position.X, obj.Position.Y, obj.Position.Z,
obj.Scale, obj.Radius));
}
// L.2d slice 1 (2026-05-13): emit one multi-line [resolve-bldg] // L.2d slice 1 (2026-05-13): emit one multi-line [resolve-bldg]
// entry per attributed hit when the per-shadow-entry probe is on. // entry per attributed hit when the per-shadow-entry probe is on.
// Captures partIdx (distinguishes hypothesis Y: over-registration), // Captures partIdx (distinguishes hypothesis Y: over-registration),
@ -2519,12 +2540,40 @@ public sealed class Transition
// This fix is the cellar-up target (issue #98). May also help // This fix is the cellar-up target (issue #98). May also help
// other "step-up onto sloped surface" scenarios. // other "step-up onto sloped surface" scenarios.
var savedInsert = sp.InsertType; var savedInsert = sp.InsertType;
float winterpBeforePlacement = sp.WalkInterp;
sp.InsertType = InsertType.Placement; sp.InsertType = InsertType.Placement;
sp.WalkInterp = 1.0f; sp.WalkInterp = 1.0f;
var placeState = TransitionalInsert(1, engine); var placeState = TransitionalInsert(1, engine);
sp.InsertType = savedInsert; sp.InsertType = savedInsert;
// A6.P3 slice 5 (2026-05-22): log the placement_insert rejection
// with the surrounding step-down context. The matching
// [place-fail] from Path 1 (in BSPQuery) names the offending
// polygon; this entry tells us which DoStepDown call (and the
// sphere position + cell + thresholds active at the time of
// the call) produced that rejection.
if (placeState != TransitionState.OK
&& PhysicsDiagnostics.ProbePlacementFailEnabled)
{
var ci = System.Globalization.CultureInfo.InvariantCulture;
Console.WriteLine(string.Format(ci,
"[place-fail] source=DoStepDown returned={0} " +
"sphere=({1:F4},{2:F4},{3:F4}) cell=0x{4:X8} " +
"stepDownHeight={5:F4} walkableZ={6:F4} " +
"winterpBefore={7:F4} " +
"contactPlane.Nz={8:F4} contactPlaneValid={9} spStepUp={10}",
placeState,
sp.CheckPos.X, sp.CheckPos.Y, sp.CheckPos.Z,
sp.CheckCellId,
stepDownHeight, walkableZ,
winterpBeforePlacement,
CollisionInfo.ContactPlane.Normal.Z,
CollisionInfo.ContactPlaneValid,
sp.StepUp));
}
return placeState == TransitionState.OK; return placeState == TransitionState.OK;
} }