acdream/docs/research/2026-05-23-a6-stepwalkadjust-findings.md
Erik 8daf7e7e4d research(phys): A6.P3 #98 — [step-walk-adjust] capture + findings
The diagnostic-first capture revealed the failure mode the plan's
four-branch decision tree (A/B/C/D) did not anticipate. AdjustOffset
is CORRECT: 145/146 calls use the into-plane branch, mean zGain
+0.045 m per call, sphere world Z climbs 90.95 -> 92.80 monotonically.

The climb caps at world Z 92.80 (cottage floor at 94.00 is still
1.20 m above). At the cap, the per-step CP reset at TransitionTypes.cs
723-725 clears ContactPlaneValid as designed; TransitionalInsert
should re-establish CP at the proposed position. Step-up logic fires
because the offset has +Z; step-up calls DoStepDown(stepDownHeight=
0.6, runPlacement=true). The downward probe finds NO walkable surface
within 0.6 m below the proposed position (cottage floor is ABOVE,
not below) -- 101 stepdown-reject hits in this capture vs 1 acceptance.

Conclusion: Target E (new). Three candidate fix shapes named in the
findings note. Each one researched against retail named-decomp before
any code lands. Test baseline 1167 + 8 maintained.

Findings:  docs/research/2026-05-23-a6-stepwalkadjust-findings.md
Capture:   docs/research/2026-05-23-a6-captures/stepwalkadjust/acdream.log

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

11 KiB
Raw Blame History

A6.P3 #98 — [step-walk-adjust] capture analysis (2026-05-23)

Capture: docs/research/2026-05-23-a6-captures/stepwalkadjust/acdream.log (1.3 MB, 6,467 lines)

Plan ref: docs/superpowers/plans/2026-05-23-a6-p3-issue98-cellar-up-fix.md

Probe commit: 8a232a3 — added [step-walk-adjust] site inside Transition.AdjustOffset (branch token + zGain per call).


TL;DR

The fix plan's four-branch decision tree (A / B / C / D) does not match what the data shows. The diagnostic conclusively proves:

  1. AdjustOffset is correct. branch=into-plane for 145 of 146 calls; zGain = +0.052 ± 0.001 per call when sphere offset points into the ramp normal (0, 0.719, 0.695). Cumulative theoretical zGain across the climb portion: roughly +5 m, far more than the ~2 m the sphere actually climbed.

  2. Z gain accumulates correctly while mid-ramp. Sphere world Z went 90.95 → 92.80 monotonically across the climb portion.

  3. The climb caps at world Z ≈ 92.80 with the sphere frozen at cur=(141.5054, 7.1684, 92.7968). X drifts by ~0.006/tick from sliding; Y and Z are nailed. The cottage floor at world Z=94 is still 1.20 m above.

  4. At the freeze, the per-step rollback mechanism takes the +Z out. The sequence:

    • find-start — winterp=1.0, walkPoly=True, CP=ramp ✓
    • [step-walk-adjust] — input=(0.006,-0.105,0), output=(0.006,-0.051,+0.052), branch=into-plane ✓
    • after-adjust — adj=(0.006,-0.051,+0.052), CP=ramp ✓
    • CP cleared by the per-step reset at TransitionTypes.cs:723-725.
    • before-insert — check advanced to (141.5117, 7.1179, 92.8491), CP=n/a
    • Inside TransitionalInsert(3): step-up branch fires (stepUp=True), step-down probes by 0.6m downward.
    • Step-down finds no walkable below the proposed position (cottage floor is ABOVE, not below).
    • Two stepdown-reject fires inside the insert.
    • after-insert — check rolled back to (141.5117, **7.1684, 92.7968**). Only X advanced by 0.006. walkPoly=False, winterp=-0.0000.
    • find-end — same state, walkPoly=False.
  5. This is a NEW fix target — call it "Target E." The plan's decision tree didn't anticipate this mode. AdjustOffset's slope projection works perfectly. The failure is in the step-up validation logic at the top of the ramp, where the next walkable surface (cottage floor) is ABOVE the proposed position, not below. The step-down probe inside step-up scans downward and finds nothing → rejects → rollback.


Branch histogram (across the entire capture)

Branch Count %
into-plane 145 99.3%
no-cp 1 0.7%
All others (away-plane, slide-crease, slide-degenerate, no-cp-slide, +safety-push) 0 0%

No safety-push annotations. No slide planes ever installed. No CP-cleared mid-climb (except by the deliberate per-step reset).

zGain summary

  • 146 calls total.
  • Total zGain: +6.63 m.
  • Mean per call: +0.045 m.
  • Cellar-floor calls (CP normal (0,0,1), d=-90.95): zGain=0 (expected — flat floor doesn't tilt motion).
  • Ramp calls (CP normal (0, 0.719, 0.695), d=-69.50): zGain ≈ +0.052 to +0.055 per call (very tight distribution).
  • Math verified: collisionAngle = dot(input, normal) ≈ -0.076 → result -= normal × collisionAngle → +Z component matches log exactly.

cur Z trajectory (from [step-walk] site=after-adjust)

Phase World Z Notes
start 90.9500 Walking flat across cellar floor (cell 0xA9B40147 floor)
climb begins 90.9500 → 91.013 → 91.068 → ... Sphere reaches ramp foot
climb proceeds rises by ~0.05/tick Y decreasing as Z increasing — climbing -Y direction
cap 92.7968 Sphere locks here; X drifts only
end-of-capture 92.7968 Sphere never escapes

Max Z reached: 92.7968. Cottage floor: 94.00. Gap: 1.20 m. Sphere top (center+radius): 93.28 — still 0.72 m below cottage floor.

stepdown probe-site counts (across whole capture)

Site Count
stepdown-enter 236
stepdown-after-insert 236
stepdown-after-offset 134
stepdown-reject 101
stepdown-after-placement 1

101 rejections vs 1 acceptance + 134 offset-only outcomes. Step-down is failing far more often than succeeding. This is the failure-frequency signature.


At the freeze: which validation rejects?

Reading TransitionTypes.cs:2848-2850:

if (transitState == TransitionState.OK
    && CollisionInfo.ContactPlaneValid
    && CollisionInfo.ContactPlane.Normal.Z >= walkableZ)

The accept condition needs ALL three. At the freeze moment:

  • transitState == OK — TRUE (per log).
  • CollisionInfo.ContactPlaneValidFALSE (per log: cp=n/a at stepdown-after-insert, stepdown-reject).
  • ContactPlane.Normal.Z >= walkableZ — moot since CP is invalid.

So ContactPlaneValid is the false condition.

Why is ContactPlaneValid false after TransitionalInsert(5) (called by DoStepDown at line 2825)?

The CP was set to (0, 0.719, 0.695) at find-start. Then per-step reset at line 724 cleared it before TransitionalInsert(3) ran. Inside that insert, step-up logic fired. Step-up internally calls DoStepDown(stepDownHeight=0.6, walkableZ=0.6642, runPlacement=true). That nested DoStepDown runs TransitionalInsert(5) again, and inside THAT, the sphere checks for walkable polys. None found below the proposed step-up position → CP stays unset → accept condition fails → stepdown-reject.

The retail behavior (from the cdb capture, retail.decoded.log):

  • Retail's BPE writes ContactPlane to (0,0,1) d=-93.9998 (cottage floor at world Z=94) DIRECTLY from (0,0,1) d=-90.9500 (cellar floor) with no intermediate.
  • Retail's BPE writes never set CP to the cellar ramp normal.
  • Retail's sphere DOES climb across the ramp, but the CP stays on the flat-floor planes the whole time.

So retail's mechanism: the sphere climbs the ramp by step-up SUCCEEDING and landing on cottage floor as the next walkable surface. The ramp itself isn't used as a ContactPlane in retail.

In acdream: the ramp is treated as a walkable surface. When the sphere reaches the top of the ramp, the next required walkable surface (cottage floor) is too far above the proposed position to be acceptable to the step-down probe.


Conclusion: Fix target is "Target E" (new)

The previous decision tree (A / B / C / D) was based on the divergence comparison doc's framing of "no altitude gain." The data shows the climb DOES gain altitude (correctly). The bug is at the top of the ramp, in the step-up + step-down validation, NOT in AdjustOffset.

Target E definition

Name: Step-up validation rejects ramp-climb advances when the next walkable surface (cottage floor) is too high above the proposed step-up position to be acceptable to the downward step-down probe.

Failure mechanic: At the top of the cellar ramp:

  1. Sphere proposes to advance up the ramp by ~0.10 m horizontal + 0.05 m vertical.
  2. The advance puts the sphere bottom AT world Z ≈ 92.37 (still 1.63 m below cottage floor at world Z=94).
  3. Step-up logic fires (because there's a +Z component in the offset).
  4. Step-up calls DoStepDown with stepDownHeight=0.6 m to find a walkable surface within reach.
  5. Step-down probes the sphere downward by 0.6 m to world Z ≈ 91.77, but no walkable polygon exists at that altitude in any of the overlapping cells (0x0147, 0x0143, 0x0146).
  6. step-down rejects → step-up rejects → rollback restores sphere Y and Z, advances X by sliding amount.
  7. Sphere is now in IDENTICAL state next tick → infinite loop.

Two candidate fix shapes (TO RESEARCH — DO NOT CODE YET)

Shape 1 — keep ramp as ContactPlane during the climb. Match retail's behavior of NOT clearing ContactPlane between AdjustOffset calls when the player is mid-ramp. Retail's BPE shows CP is "sticky" on the cellar floor, then suddenly transitions to cottage floor. Our per-step reset at TransitionTypes.cs:721-725 clears CP every step; this is the documented "ACE order" but may not match retail.

Shape 2 — fix step-up to look UPWARD for cottage floor. When step-up fails to find a walkable directly below the proposed position, probe UPWARD by stepUpHeight looking for a walkable that the sphere can land on after a vertical lift. This is the natural "climb up a ledge" behavior. The current step-up only probes downward (via DoStepDown).

Shape 3 — preserve walkPoly across rollback. When step-up rejects, the rollback should preserve walkPoly=True if the PREVIOUS frame had it (the sphere was on a valid walkable). Currently walkPoly=False after rollback, which then poisons the next tick's OnWalkable check.

These three shapes are NOT mutually exclusive. The fix may need shape 1 + 3, or shape 2 alone, or some combination.


What this rules out

Hypothesis Status
AdjustOffset projection broken (decision-tree Branch A / B / C / D) RULED OUT — projection works correctly, +zGain per call is consistent and matches the math.
WalkInterp depletion gating forward motion RULED OUT — winterp=1.0 at find-start of every freeze tick. Only DEPLETED winterp=-0.0000 appears AFTER stepdown-reject, which is a consequence not a cause.
Cell-resolver ping-pong between cellar and cottage RULED OUT — every tick has cell=0xA9B40147→0xA9B40147 (no transition); slice-3 stickiness fix held.
Step-down rejected because no walkable found above sphere NOT TESTABLE BY THIS PROBE — this probe is inside AdjustOffset, not inside DoStepDown's accept-condition check. A follow-up probe inside the accept-condition check would prove which of the three accept clauses fails. We CAN see it indirectly: cp=n/a at stepdown-after-insert tells us ContactPlaneValid is false at the moment of the check.

Pickup prompt for the fix plan

A6.P3 issue #98 — [step-walk-adjust] capture analysis complete.

Read FIRST:
  docs/research/2026-05-23-a6-stepwalkadjust-findings.md
  docs/research/2026-05-23-a6-captures/stepwalkadjust/acdream.log
    (search for "stepdown-reject" and the freeze tick at line ~3891)

Conclusion: Fix target is "Target E" (new) — step-up validation
rejects ramp-climb advances at the top of the cellar ramp because
the cottage floor is too far ABOVE the proposed step-up position to
be found by the downward step-down probe.

Three candidate fix shapes:
  1. Keep ramp ContactPlane sticky across per-step resets (match retail).
  2. Make step-up probe UPWARD for the next walkable (climb-up behavior).
  3. Preserve walkPoly across rollback to avoid OnWalkable being poisoned.

Next: research which shape matches retail's named decomp at
acclient_2013_pseudo_c.txt (search step_sphere_up, step_sphere_down,
find_walkable). Retail's BPE writes ONLY ever set CP to flat floors
(cellar Z=90.95 then cottage Z=94) — never to the ramp.

The replay harness (Issue98CellarUpReplayTests, <200ms) is the inner
test loop. The cdb capture in cellar_up_capture_1/ is the ground-truth
oracle. The fix MUST flip the failing-frame assertions in the replay
tests — that's the contract.

Test baseline: 1167 + 8. CLAUDE.md rules apply. No workarounds.