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>
This commit is contained in:
Erik 2026-05-20 11:00:11 +02:00
parent a2e7a87c25
commit bb1e919ef2
3 changed files with 1157 additions and 0 deletions

View file

@ -0,0 +1,218 @@
# 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]`).