tools(cdb): A6.P3 #98 Step 4 — retail find_walkable capture script
Step 4 of the apparatus plan. Adds the cdb script + runner that pairs
with Issue98CellarUpReplayTests to compare retail's walkable-query
behavior against acdream's during the Holtburg cottage cellar ascent.
Breakpoints (all symbols verified against refs/acclient.pdb via grep
docs/research/named-retail/symbols.json):
- BPA: BSPLEAF::find_walkable — leaf-level walkable query
- BPB: CPolygon::walkable_hits_sphere — per-polygon overlap test
- BPC: CPolygon::find_crossed_edge — per-polygon edge containment
- BPD: CTransition::check_other_cells — outer dispatcher
- BPE: COLLISIONINFO::set_contact_plane — GOLD signal: retail accepted
this plane
- BPF: CPolygon::adjust_sphere_to_plane — per-polygon projection
Output format: 32-bit hex bits for all floats via dwo() + %08X (cdb's
%f handling is broken for dwo reads; see a6-probe.cdb v3→v4 history).
Decoder: tools/cdb/decode_retail_hex.py already handles _h=0x... fields.
Auto-detach threshold: 50000 hits across BPA/B/C/D/F. BPE is unbounded
(contact plane writes are rare, ~18 per ascent per slice 5 capture).
Runner: tools/cdb/issue98-runner.ps1
.\tools\cdb\issue98-runner.ps1 -ScenarioTag "cellar_up_attempt_1"
Prereqs (per CLAUDE.md retail debugger toolchain section):
- Retail acclient.exe v11.4186 running and in-world
- ACE running on 127.0.0.1:9000
- Character at the BOTTOM of a Holtburg cottage cellar stair
- cdb.exe present at the Windows Kits 10 path
Output:
docs\research\2026-05-23-a6-captures\<ScenarioTag>\retail.log
Reading the log:
- [BPE] lines tell you which plane retail accepted (the answer we need).
- Cross-reference [BPE]'s normal/d against the cell fixtures in
tests/AcDream.Core.Tests/Fixtures/issue98/*.json to identify which
cell + polyId retail picked.
- The divergence between retail's accepted polygon and our replay test's
"no walkable accepted" result IS the fix target.
The capture itself is a user action (cdb requires a live retail
process); this commit only ships the protocol. Step 5 (comparison doc)
follows after the capture lands.
This commit is contained in:
parent
856aa78ec1
commit
6f666c14da
2 changed files with 168 additions and 0 deletions
88
tools/cdb/issue98-cellar-up-find-walkable.cdb
Normal file
88
tools/cdb/issue98-cellar-up-find-walkable.cdb
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
$$
|
||||
$$ A6.P3 issue #98 cdb capture — 2026-05-23
|
||||
$$
|
||||
$$ Captures retail's walkable-query behavior during the Holtburg cottage
|
||||
$$ cellar ascent. Pairs with tests/AcDream.Core.Tests/Physics/
|
||||
$$ Issue98CellarUpReplayTests for an evidence-based comparison: the
|
||||
$$ acdream replay says "no walkable accepted" at the failing-frame
|
||||
$$ sphere; retail succeeds. The cdb log tells us which polygon retail
|
||||
$$ accepts, in which cell, at what sphere position. The divergence is
|
||||
$$ then the input to the fix plan.
|
||||
$$
|
||||
$$ Breakpoints (snake_case names from refs/acclient.pdb):
|
||||
$$ BPA: BSPLEAF::find_walkable — per-leaf walkable query. Logs sphere
|
||||
$$ center + outPoly pointer at entry. (outPoly is OUTPUT, only
|
||||
$$ populated post-call; we capture inputs only here.)
|
||||
$$ BPB: CPolygon::walkable_hits_sphere — per-polygon overlap test.
|
||||
$$ Logs polygon vtable + sphere center.
|
||||
$$ BPC: CPolygon::find_crossed_edge — per-polygon edge containment.
|
||||
$$ Logs polygon vtable + sphere center.
|
||||
$$ BPD: CTransition::check_other_cells — outer dispatcher. Logs
|
||||
$$ otherCell pointer.
|
||||
$$ BPE: COLLISIONINFO::set_contact_plane — gold-standard accept
|
||||
$$ signal. Logs plane normal + d. (Mirrors BP7 in a6-probe.cdb.)
|
||||
$$ BPF: CPolygon::adjust_sphere_to_plane — per-polygon sphere
|
||||
$$ projection. Mirrors BP5 in a6-probe.cdb.
|
||||
$$
|
||||
$$ All floats output as 32-bit hex via dwo() + %08X. Decode with
|
||||
$$ struct.unpack('<f', bytes.fromhex(...)) on the analysis side.
|
||||
$$
|
||||
$$ Calling convention: __thiscall. ecx = this; args at [esp+4],
|
||||
$$ [esp+8], [esp+0xC], [esp+0x10], [esp+0x14], [esp+0x18].
|
||||
$$
|
||||
$$ Field offsets (from refs/acclient.pdb dt-dump, same as a6-probe.cdb):
|
||||
$$ CSphere: center.x +0x00, .y +0x04, .z +0x08, radius +0x0c
|
||||
$$ CPolygon: plane +0x20 (N.x +0x20, N.y +0x24, N.z +0x28, d +0x2c)
|
||||
$$ SPHEREPATH: check_cell* +0x024 (CObjCell* — first arg to BSPLEAF
|
||||
$$ is the SPHEREPATH; the cell can be read off it)
|
||||
$$
|
||||
$$ Threshold: 50000 hits across BPs A+B+C+D+F. BPE has its own
|
||||
$$ counter so contact-plane writes always log.
|
||||
$$
|
||||
$$ Auto-detach via qd when threshold is reached. Do NOT raise the
|
||||
$$ threshold blindly — too many hits make retail feel sluggish and may
|
||||
$$ trigger an ACE keepalive timeout.
|
||||
|
||||
.logopen /t a6-issue98-${ARG_LOG_TAG}.log
|
||||
.sympath C:\Users\erikn\source\repos\acdream\refs
|
||||
.symopt+ 0x40
|
||||
.reload /f acclient.exe
|
||||
|
||||
r $t0 = 0
|
||||
r $t1 = 0
|
||||
r $t2 = 0
|
||||
r $t3 = 0
|
||||
r $t4 = 0
|
||||
r $t5 = 0
|
||||
r $t6 = 0
|
||||
|
||||
$$ BPA: BSPLEAF::find_walkable(this, SPHEREPATH* sp, CSphere* validPos, CPolygon** outPoly, Vector3* movement, Vector3* up, int* changed)
|
||||
$$ sp = [esp+4]
|
||||
$$ validPos = [esp+8] → x +0x00, y +0x04, z +0x08, r +0x0c
|
||||
$$ outPoly = [esp+0xC]
|
||||
$$ up = [esp+0x14]
|
||||
bp acclient!BSPLEAF::find_walkable "r $t1 = @$t1 + 1; r $t0 = @$t0 + 1; .printf /D \"[BPA] find_walkable hit#%d leaf=0x%08X sp=0x%08X cx_h=0x%08X cy_h=0x%08X cz_h=0x%08X r_h=0x%08X upx_h=0x%08X upy_h=0x%08X upz_h=0x%08X\\n\", @$t1, @ecx, dwo(@esp+4), dwo(poi(@esp+8)+0), dwo(poi(@esp+8)+4), dwo(poi(@esp+8)+8), dwo(poi(@esp+8)+0xc), dwo(poi(@esp+0x14)+0), dwo(poi(@esp+0x14)+4), dwo(poi(@esp+0x14)+8); .if (@$t0 >= 50000) { qd } .else { gc }"
|
||||
|
||||
$$ BPB: CPolygon::walkable_hits_sphere(this, SPHEREPATH* sp, CSphere* sphere, Vector3* up)
|
||||
$$ this = polygon (ecx) → plane @ +0x20
|
||||
$$ sphere = [esp+8]
|
||||
bp acclient!CPolygon::walkable_hits_sphere "r $t2 = @$t2 + 1; r $t0 = @$t0 + 1; .printf /D \"[BPB] walkable_hits hit#%d poly=0x%08X Nx_h=0x%08X Ny_h=0x%08X Nz_h=0x%08X d_h=0x%08X cx_h=0x%08X cy_h=0x%08X cz_h=0x%08X r_h=0x%08X\\n\", @$t2, @ecx, dwo(@ecx+0x20), dwo(@ecx+0x24), dwo(@ecx+0x28), dwo(@ecx+0x2c), dwo(poi(@esp+8)+0), dwo(poi(@esp+8)+4), dwo(poi(@esp+8)+8), dwo(poi(@esp+8)+0xc); .if (@$t0 >= 50000) { qd } .else { gc }"
|
||||
|
||||
$$ BPC: CPolygon::find_crossed_edge(this, CSphere* sphere, Vector3* up, Vector3* out)
|
||||
bp acclient!CPolygon::find_crossed_edge "r $t3 = @$t3 + 1; r $t0 = @$t0 + 1; .printf /D \"[BPC] find_crossed_edge hit#%d poly=0x%08X Nx_h=0x%08X Ny_h=0x%08X Nz_h=0x%08X d_h=0x%08X cx_h=0x%08X cy_h=0x%08X cz_h=0x%08X r_h=0x%08X\\n\", @$t3, @ecx, dwo(@ecx+0x20), dwo(@ecx+0x24), dwo(@ecx+0x28), dwo(@ecx+0x2c), dwo(poi(@esp+4)+0), dwo(poi(@esp+4)+4), dwo(poi(@esp+4)+8), dwo(poi(@esp+4)+0xc); .if (@$t0 >= 50000) { qd } .else { gc }"
|
||||
|
||||
$$ BPD: CTransition::check_other_cells(this, CObjCell* otherCell)
|
||||
bp acclient!CTransition::check_other_cells "r $t4 = @$t4 + 1; r $t0 = @$t0 + 1; .printf /D \"[BPD] check_other_cells hit#%d trans=0x%08X otherCell=0x%08X\\n\", @$t4, @ecx, dwo(@esp+4); .if (@$t0 >= 50000) { qd } .else { gc }"
|
||||
|
||||
$$ BPE: COLLISIONINFO::set_contact_plane(this, plane, is_water) — GOLD signal
|
||||
$$ This fires when retail commits to a contact plane. The plane args
|
||||
$$ are the polygon retail accepted. NO threshold gate — every contact
|
||||
$$ plane write logs.
|
||||
bp acclient!COLLISIONINFO::set_contact_plane "r $t5 = @$t5 + 1; .printf /D \"[BPE] set_contact_plane hit#%d Nx_h=0x%08X Ny_h=0x%08X Nz_h=0x%08X d_h=0x%08X isWater=%d\\n\", @$t5, dwo(poi(@esp+4)+0), dwo(poi(@esp+4)+4), dwo(poi(@esp+4)+8), dwo(poi(@esp+4)+0xc), dwo(@esp+8); gc"
|
||||
|
||||
$$ BPF: CPolygon::adjust_sphere_to_plane(this, sphere_path, sphere, movement)
|
||||
bp acclient!CPolygon::adjust_sphere_to_plane "r $t6 = @$t6 + 1; r $t0 = @$t0 + 1; .printf /D \"[BPF] adjust_sphere hit#%d poly=0x%08X Nx_h=0x%08X Ny_h=0x%08X Nz_h=0x%08X d_h=0x%08X cx_h=0x%08X cy_h=0x%08X cz_h=0x%08X r_h=0x%08X winterp_h=0x%08X\\n\", @$t6, @ecx, dwo(@ecx+0x20), dwo(@ecx+0x24), dwo(@ecx+0x28), dwo(@ecx+0x2c), dwo(poi(@esp+8)+0), dwo(poi(@esp+8)+4), dwo(poi(@esp+8)+8), dwo(poi(@esp+8)+0xc), dwo(poi(@esp+4)+0x1bc); .if (@$t0 >= 50000) { qd } .else { gc }"
|
||||
|
||||
.printf "issue98-find-walkable cdb armed: BPA-BPF, 50000-hit threshold (BPE unbounded)\\n"
|
||||
|
||||
g
|
||||
80
tools/cdb/issue98-runner.ps1
Normal file
80
tools/cdb/issue98-runner.ps1
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# A6.P3 issue #98 cdb capture runner — 2026-05-23
|
||||
#
|
||||
# Attaches cdb to a live retail acclient.exe with
|
||||
# issue98-cellar-up-find-walkable.cdb. Pairs with
|
||||
# tests/AcDream.Core.Tests/Physics/Issue98CellarUpReplayTests.cs to
|
||||
# answer: which polygon does retail accept during cellar ascent, in
|
||||
# which cell, at what sphere position?
|
||||
#
|
||||
# Per-capture usage:
|
||||
# .\tools\cdb\issue98-runner.ps1 -ScenarioTag "cellar_up_attempt_1"
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Retail acclient.exe v11.4186 running and in-world (matches
|
||||
# refs/acclient.pdb). Verify with:
|
||||
# py tools\pdb-extract\check_exe_pdb.py "C:\Turbine\Asheron's Call\acclient.exe"
|
||||
# 2. ACE running locally on 127.0.0.1:9000.
|
||||
# 3. Retail character at the BOTTOM of a Holtburg cottage cellar
|
||||
# stair. Take a long breath; the next walk-forward is the capture
|
||||
# window. cdb auto-detaches at 50,000 BP hits (~5-20s of motion).
|
||||
#
|
||||
# Output:
|
||||
# docs\research\2026-05-23-a6-captures\<ScenarioTag>\retail.log
|
||||
#
|
||||
# Reading the log:
|
||||
# The relevant signal is [BPE] set_contact_plane lines — these tell
|
||||
# you which plane retail accepted. Then trace back through [BPA]
|
||||
# find_walkable + [BPF] adjust_sphere entries to see the sphere
|
||||
# position + polygon when that acceptance happened. Cross-reference
|
||||
# the polygon plane normal/d against the cell fixtures in
|
||||
# tests/AcDream.Core.Tests/Fixtures/issue98/0x*.json to identify which
|
||||
# cell + polyId retail picked.
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$ScenarioTag
|
||||
)
|
||||
|
||||
$cdbExe = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe"
|
||||
if (-not (Test-Path $cdbExe)) {
|
||||
Write-Error "cdb.exe not found at $cdbExe. Install Microsoft Store WinDbg (~50 MB)."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$scriptPath = Join-Path $PSScriptRoot "issue98-cellar-up-find-walkable.cdb"
|
||||
if (-not (Test-Path $scriptPath)) {
|
||||
Write-Error "issue98-cellar-up-find-walkable.cdb not found at $scriptPath."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$captureDir = Join-Path $PSScriptRoot "..\..\docs\research\2026-05-23-a6-captures\$ScenarioTag"
|
||||
if (-not (Test-Path $captureDir)) {
|
||||
New-Item -ItemType Directory -Path $captureDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$logPath = Join-Path $captureDir "retail.log"
|
||||
|
||||
# Substitute ${ARG_LOG_TAG} in the .cdb script.
|
||||
$scriptContent = Get-Content $scriptPath -Raw
|
||||
$patchedScript = $scriptContent -replace '\$\{ARG_LOG_TAG\}', $ScenarioTag
|
||||
|
||||
$tempScript = Join-Path $env:TEMP "issue98-$ScenarioTag.cdb"
|
||||
Set-Content -Path $tempScript -Value $patchedScript -Encoding ASCII
|
||||
|
||||
Write-Host "Attaching cdb to acclient.exe with scenario tag '$ScenarioTag'..."
|
||||
Write-Host "Log: $logPath"
|
||||
Write-Host "Script: $tempScript"
|
||||
Write-Host "(cdb auto-detaches at 50K total hits across BPA/B/C/D/F; BPE unbounded.)"
|
||||
Write-Host ""
|
||||
Write-Host "TRIGGER: walk forward from the bottom of the cellar stair NOW."
|
||||
Write-Host ""
|
||||
|
||||
# ASCII output so subsequent grep does not have to deal with UTF-16 BOM.
|
||||
& $cdbExe -pn acclient.exe -cf $tempScript 2>&1 | Out-File -FilePath $logPath -Encoding ASCII
|
||||
|
||||
Remove-Item $tempScript -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Capture complete. Log saved to $logPath"
|
||||
Write-Host "Decode floats: dwo() hex bits → struct.unpack('<f', bytes.fromhex(...))"
|
||||
Write-Host "Decode helper: tools/cdb/decode_retail_hex.py"
|
||||
Loading…
Add table
Add a link
Reference in a new issue