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>
9.7 KiB
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 == trueANDabs(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'scheck_other_cellsloop.nearest.containsFootXY == trueANDnearest.dz > 0.5 mANDnearest.dz < 5 m→ H2 confirmed. Floor poly XY-overlaps the foot but sits below the 0.5 m probe distance. Fix: increaseINDOOR_WALKABLE_PROBE_DISTANCE(with the retail-faithful constant pulled fromacclient_2013_pseudo_c.txtor the OBJECTINFO step-down height).nearest.containsFootXY == trueANDnearest.dz <= 0.5 mANDnearest.normalZ >= FloorZ→ H3 candidate. Poly is XY-aligned, within the probe, walkable-slope — should hit but doesn't. Next step: cdb attach to retail atBSPLEAF::find_walkableto compare poly iter with ours.nearest.containsFootXY == falseANDlandcell.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:
- Walk
cellPhysics.Resolvedfor walkable-eligible polys (wherepoly.Plane.Normal.Z >= PhysicsGlobals.FloorZ). - 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: smallestdzacross all polys). - Call
engine.SampleTerrainWalkable(foot.X, foot.Y)for the LandCell probe. - 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
polyinresolvedwhereNormal.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 (mirrorsPhysicsDiagnosticsTests).WalkMissDiagnostic_SelectsNearestWalkablePoly_WhenFootXYInsideBbox— given aCellPhysicswith two walkable polys at different Z, the per-MISS aggregator picks the one closest bydz. Uses the sameIndoorWalkablePlaneTestsfixture style.WalkMissDiagnostic_FallsBackToNearestPolyByDz_WhenFootXYOutsideAllBboxes— given a foot XY outside every floor-poly bbox, the aggregator reportscontainsFootXY=falseand returns the nearest bydzfor 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
dotnet buildgreen.dotnet testgreen (existing tests untouched + 3 new tests).- 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.
- When
ACDREAM_PROBE_WALK_MISSis 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]).