acdream/docs/superpowers/specs/2026-05-21-indoor-walk-miss-probe-design.md
Erik bb1e919ef2 docs(physics): spec + plan + findings for ISSUES #83 walk-miss probe spike
Three docs from the indoor walk-miss probe spike landed in commits
27c7284..a2e7a87:

- Spec: design of the [walk-miss] + [floor-polys] diagnostic emissions
  with the H1/H2/H3 disambiguation matrix.
- Plan: 3-task TDD implementation plan (flag, aggregator, emissions).
- Findings: live-capture analysis showing H3 (walkable_hits_sphere /
  adjust_sphere_to_plane synthesis rejection) is the dominant defect.
  817 of 876 ground-contact misses (93%) cluster at dz~0.48 m, while
  the 7 HITs all sit at dz~0.46 m — a 2 cm boundary between working
  and broken that points at the sphere-overlap math, not the probe
  distance. H1 (multi-cell iteration missing) is real but only 3%
  of misses, secondary. H2 (probe distance) ruled out.

Next step: line-by-line decomp comparison of FindWalkableInternal /
walkable_hits_sphere / adjust_sphere_to_plane against retail at
acclient_2013_pseudo_c.txt:322032 / :323006 / :326793, then design
the fix in a follow-up session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:00:11 +02:00

9.7 KiB
Raw Blame History

Indoor walk-miss probe (ISSUES #83 H-disambiguation spike)

Date: 2026-05-21 Status: Spec — awaiting user review before plan-writing. Phase: Indoor walking, ISSUES #83 next step. Spike-only — no behavior changes. Author: Claude Opus 4.7.

Summary

Indoor walking glitches at floor-poly edges and doorway thresholds (stuck-falling animation). The previous session's investigation (docs/research/2026-05-20-indoor-walking-bug-a-handoff.md) narrowed the open question to: when TryFindIndoorWalkablePlane returns MISS, why? Three live hypotheses (H1 multi-cell iteration missing, H2 probe distance too short, H3 floor poly absent / walkable_hits_sphere rejection). A single composite probe answers all three in one capture session. No physics behavior changes. The fix is designed in a follow-up session after the probe data points at the right hypothesis.

Goal

Add a diagnostic probe set that, at a single Holtburg test run covering doorway crossing + 2nd-floor edge + cellar descent, produces enough per-MISS evidence to pick between H1/H2/H3 without attaching cdb to retail.

What the probe must capture

For each [indoor-walkable] ... result=MISS event, emit a paired [walk-miss] line with:

Field Purpose
cellId Cross-reference with the cell-load dump.
foot.W (world XY,Z) Where the foot sphere is.
foot.L (cell-local XY,Z) Cell-local position used by the BSP.
floorPolyCount How many walkable-eligible polys this cell holds.
nearest.polyId Poly id of the closest walkable poly (XY-overlapping foot, then nearest by dz).
nearest.containsFootXY true if foot XY is inside the poly's local-XY bounding box.
nearest.dz Signed vertical distance from foot to the poly plane (positive = foot above poly).
nearest.normalZ Normal Z of the poly (high = horizontal floor, low = steep ramp).
landcell.hasTerrain Does engine.SampleTerrainWalkable(foot.X, foot.Y) return non-null?
landcell.terrainZ If yes: the terrain plane Z at that XY (compute as -D/normal.Z).
landcell.dz foot.Z - landcell.terrainZ (positive = foot above terrain).

Once-per-cell, when CellPhysics is cached, emit a [floor-polys] dump:

Field Purpose
cellId The envCell.
walkableCount Polys with Plane.Normal.Z >= FloorZ.
Per walkable poly: id, normalZ, bboxLocalXY, planeZAtBboxCenter Lets us reconstruct floor coverage offline.

Both gated on a single new ProbeWalkMissEnabled flag.

Disambiguation matrix (after the run)

Aggregate the [walk-miss] lines from the capture, then:

  • landcell.hasTerrain == true AND abs(landcell.dz) < 0.2 mH1 confirmed. Multi-cell iteration would have grounded the player on the outdoor LandCell's terrain at the threshold. Fix: port retail's check_other_cells loop.
  • nearest.containsFootXY == true AND nearest.dz > 0.5 m AND nearest.dz < 5 mH2 confirmed. Floor poly XY-overlaps the foot but sits below the 0.5 m probe distance. Fix: increase INDOOR_WALKABLE_PROBE_DISTANCE (with the retail-faithful constant pulled from acclient_2013_pseudo_c.txt or the OBJECTINFO step-down height).
  • nearest.containsFootXY == true AND nearest.dz <= 0.5 m AND nearest.normalZ >= FloorZH3 candidate. Poly is XY-aligned, within the probe, walkable-slope — should hit but doesn't. Next step: cdb attach to retail at BSPLEAF::find_walkable to compare poly iter with ours.
  • nearest.containsFootXY == false AND landcell.hasTerrain == falseH1+H3 combination. Both indoor and outdoor have no floor here; retail must do something we haven't found. cdb attach required.

Components

1. PhysicsDiagnostics.ProbeWalkMissEnabled

New static property in src/AcDream.Core/Physics/PhysicsDiagnostics.cs, matching the existing pattern (ProbeIndoorBspEnabled, etc):

  • Env var: ACDREAM_PROBE_WALK_MISS=1.
  • Runtime-toggleable via property setter.
  • No DebugPanel mirror — one-shot diagnostic, not a persistent toggle.

2. Per-MISS [walk-miss] log line

In Transition.FindEnvCollisions (TransitionTypes.cs:1538 — the existing MISS branch of TryFindIndoorWalkablePlane's caller). When ProbeWalkMissEnabled:

  1. Walk cellPhysics.Resolved for walkable-eligible polys (where poly.Plane.Normal.Z >= PhysicsGlobals.FloorZ).
  2. For each: compute the XY bbox from poly.Vertices, compute the plane Z at the foot's local XY (Z = (-D - normal.X*X - normal.Y*Y) / normal.Z), track the one with smallest |dz| where foot XY ∈ bbox (tiebreaker: smallest dz across all polys).
  3. Call engine.SampleTerrainWalkable(foot.X, foot.Y) for the LandCell probe.
  4. Emit the line.

Zero cost when flag is off (single bool check guards the whole block).

3. One-shot [floor-polys] cell-load dump

In PhysicsDataCache.CacheCellStruct (immediately after the existing [cell-cache] block at PhysicsDataCache.cs:182). When ProbeWalkMissEnabled:

  • For each poly in resolved where Normal.Z >= FloorZ: log id, normalZ, bbox, planeZAtCenter.
  • Aggregate into one line if walkableCount ≤ 6, else multi-line.

Fires once per cell — _cellStruct is keyed by id with ConcurrentDictionary so a second cache call is a no-op (existing path).

4. Test coverage

tests/AcDream.Core.Tests/Physics/IndoorWalkMissProbeTests.cs:

  • ProbeWalkMiss_StaticApi_Roundtrip — flag get/set roundtrip with finally-restore (mirrors PhysicsDiagnosticsTests).
  • WalkMissDiagnostic_SelectsNearestWalkablePoly_WhenFootXYInsideBbox — given a CellPhysics with two walkable polys at different Z, the per-MISS aggregator picks the one closest by dz. Uses the same IndoorWalkablePlaneTests fixture style.
  • WalkMissDiagnostic_FallsBackToNearestPolyByDz_WhenFootXYOutsideAllBboxes — given a foot XY outside every floor-poly bbox, the aggregator reports containsFootXY=false and returns the nearest by dz for context (or null if no walkable polys exist).

The actual log-format roundtrip (capturing stdout) is not unit-tested — that's verified by the live capture per acceptance criterion below.

Acceptance criteria

  1. dotnet build green.
  2. dotnet test green (existing tests untouched + 3 new tests).
  3. A live capture at Holtburg with the env var on produces:
    • [floor-polys] lines for every loaded indoor cell.
    • At least one [walk-miss] line at the cottage doorway threshold.
    • At least one [walk-miss] line at an inn 2nd-floor edge.
    • Aggregated counts let us classify each MISS into H1/H2/H3 per the disambiguation matrix.
  4. When ACDREAM_PROBE_WALK_MISS is unset, normal play produces zero [walk-miss] / [floor-polys] lines (zero-cost gate).

Out of scope

  • No fix to the underlying glitch — that's a follow-up session after the probe data lands.
  • No changes to TryFindIndoorWalkablePlane, FindWalkableSphere, BSPQuery.FindCollisions, or any physics behavior.
  • No retail cdb attach — that's the H3-only fallback path.
  • No new DebugPanel checkbox — keep the probe lean.
  • No removal of the existing [indoor-bsp] / [indoor-walkable] / [cp-write] probes — they're complementary.

Risks

  • R1: [floor-polys] dump blows up the log. Holtburg has hundreds of loaded cells. Per cell, typical walkable poly count is 1-4 (cottages) up to ~10 (inn ground floor). Worst-case dump volume ~3 KB per cell × 200 cells = ~600 KB. Acceptable. Mitigation: none needed at this scale.

  • R2: Per-MISS line at 99.87% miss rate floods the log. A 60-second capture at 30 Hz with one MISS per tick = ~1800 lines. At ~300 bytes each ≈ 540 KB. Acceptable. The user already produced 51k-line cp-write logs without issue. Mitigation: none needed.

  • R3: The probe doesn't disambiguate — the data falls into the "H1+H3 combination" cell. Then we attach cdb to retail and gather ground truth. This is the planned fallback per CLAUDE.md "Retail debugger toolchain" section. Mitigation: the disambiguation matrix has an entry for this case.

  • R4: The probe finds the answer is also in a code path we haven't read yet (e.g. precipice_slide, cliff_slide, step_up_slide). Then the next session's design exercises a new decomp read. Mitigation: unavoidable — the probe is the cheapest first move.

Why this is the right next step

  • Falsifiable in one capture — no iterative back-and-forth.
  • No behavior change — can't regress the M1 demo paths (door open, pickup, NPC click).
  • Cheap implementation — ~80 lines of code, 3 tests, one commit.
  • The previous session's premature design lesson (Bug A) explicitly flagged "probe-first, design-second" as the rule. This is that rule applied.

References

  • docs/research/2026-05-20-indoor-walking-bug-a-handoff.md — the comprehensive previous-session handoff that motivated this spec.
  • docs/research/2026-05-21-indoor-walking-doorway-investigation-prompt.md — the pickup brief.
  • docs/superpowers/specs/2026-05-20-indoor-bsp-worldorigin-fix-design.md — Bug B (shipped), the immediately-prior spec in the series.
  • Retail decomp anchors:
    • acclient_2013_pseudo_c.txt:272717-272798CTransition::check_other_cells (H1 candidate).
    • :323725-323939BSPTREE::find_collisions (H3 candidate).
    • :323006-323028CPolygon::walkable_hits_sphere (H3 candidate).
  • Code anchors:
    • src/AcDream.Core/Physics/TransitionTypes.cs:1294TryFindIndoorWalkablePlane.
    • src/AcDream.Core/Physics/TransitionTypes.cs:1538 — MISS log site.
    • src/AcDream.Core/Physics/PhysicsDiagnostics.cs:223ProbeCellCacheEnabled (template for new flag).
    • src/AcDream.Core/Physics/PhysicsDataCache.cs:182[cell-cache] pattern (template for [floor-polys]).