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

218 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 m`** → **H1
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 m`** → **H2 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 >= FloorZ`** → **H3 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 == false`**
**H1+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-272798`
`CTransition::check_other_cells` (H1 candidate).
- `:323725-323939``BSPTREE::find_collisions` (H3 candidate).
- `:323006-323028``CPolygon::walkable_hits_sphere` (H3 candidate).
- Code anchors:
- `src/AcDream.Core/Physics/TransitionTypes.cs:1294`
`TryFindIndoorWalkablePlane`.
- `src/AcDream.Core/Physics/TransitionTypes.cs:1538` — MISS log site.
- `src/AcDream.Core/Physics/PhysicsDiagnostics.cs:223`
`ProbeCellCacheEnabled` (template for new flag).
- `src/AcDream.Core/Physics/PhysicsDataCache.cs:182``[cell-cache]`
pattern (template for `[floor-polys]`).