Recon script, three capture templates (walk / parts / alpha-depth), runner, and operator README under tools/walk-oracle/oh/. FW0 line formats are kept byte-identical; new EC/OC/PD/DM/AM/FL/PM/PC line kinds are documented. Auto-detach uses the fall-through + top-level qd recipe; qd never appears inside a breakpoint action. Lead-resolved: DBObj::m_DID is +0x28 (header layout, first base of CGfxObj and CObjCell, matches the proven FW0 cell reads); the three D3DPolyRender alpha functions are static cdecl with stack arguments, uint8 arguments read with by(). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| oh-capture-alpha-depth.cdb.template | ||
| oh-capture-parts.cdb.template | ||
| oh-capture-walk.cdb.template | ||
| oh-run-capture.ps1 | ||
| README.md | ||
OH1 walk-oracle capture kit (2026-09-02)
Draft cdb capture scripts for Campaign OVERHAUL's OH1 evidence gap: the four
OH1 contract docs
(docs/research/2026-09-01-overhaul/oh1-retail-world-contract.md,
oh1-built-mesh-view-contract.md, oh1-alpha-list-contract.md,
oh1-depth-lifecycle.md) establish retail's built-mesh/view/stamp, alpha
FIFO, and depth-lifecycle contracts from the named decomp plus live Ghidra
arbitration, but they explicitly say (oh1-retail-world-contract.md s10):
"Existing FW0 captures cover only frame/pose/DrawInside/DrawCells/
landscape/building entry. Full semantic equality requires a bounded richer
retail capture before the owning later slices close." This kit is that
richer capture: it extends the proven FW0 F/P/LS/BLD/DI/DC per-frame shape
with three new families (cell-shell/object-cell stamps, part/mesh
admission, and the alpha-FIFO/depth-counter state machine) without changing
any existing line format.
These scripts are DRAFTS pending the recon pass below. Do not treat any
offset as final until oh-recon.cdb's dt output confirms it against a
live attach. See "Open questions" at the end before running a real
session.
Files
| File | Purpose |
|---|---|
oh-recon.cdb |
No breakpoints. dt/x/uf/dd dumps that confirm every offset the three capture scripts assume. Run this FIRST, once per session. |
oh-capture-walk.cdb.template |
FW0's F/P/LS/BLD/DI/DC lines, byte-identical, plus new EC/OC lines (cell-shell and object-cell draw-stamp events). 5 frames. |
oh-capture-parts.cdb.template |
F/P lines plus new PD/DM lines (per-part and per-mesh admission/GfxObj identity). The high-hit-rate script. 3 frames. |
oh-capture-alpha-depth.cdb.template |
F/P lines plus new AM/FL/PM/PC lines (the two alpha FIFO lists and the persistent portal-mask depth counter). 5 frames. |
oh-run-capture.ps1 |
Runner: -Script -Log -Frames. Verifies the live acclient.exe pairs with refs/acclient.pdb (aborts on mismatch), substitutes <LOG>/<FRAMES>, launches cdb -pn acclient.exe -cf <script> in the background. |
Prerequisites
C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exeinstalled (Microsoft Store WinDbg).- The retail client running the 2013 v11.4186 EoR build (PDB GUID
9e847e2f-777c-4bd9-886c-22256bb87f32), in-world, connected to local ACE on127.0.0.1:9000. The runner checks this for you and aborts on mismatch, but confirm before starting — a mismatched attach silently corrupts every offset in these scripts. docs/research/named-retail/acclient.handacclient_2013_pseudo_c.txtavailable (already in the repo) if you need to cross-check a line number cited in a script's comments.
The four poses
Reused verbatim from docs/research/2026-08-30-fw-walk-oracle/README.md's
trace table — these are the exact in-world spots already used for the FW0
oracle fixtures, so a human who captured those poses before can return to
the same spots from memory; there is no additional GPS/coordinate record
beyond these descriptions in this repo.
| Pose | Description | Root shape |
|---|---|---|
holtburg-doorway-still |
Standing in a cottage doorway, still | INTERIOR (DI a9b4013f) |
terrace-edge |
Waterfall ledge (the #456 acceptance pose), still | OUTDOOR |
cathedral-arrival |
Cathedral portal-in arrival, still | INTERIOR (DI f4180106) |
foundry-deep |
Bottom of the Holtburg foundry, still | INTERIOR (DI a9b40176) |
All four are still poses (no movement during capture) — this kit only targets the still-fixture family, matching FW0's own "still fixtures gate first" precedent (pose-application timing sensitivity in moving fixtures is a separate, harder problem this kit does not attempt).
Operator procedure
Budget: one hour total for all four poses. Each pose's three scripts (walk, parts, alpha-depth) run back to back and take well under a minute of actual capture time each; the time goes to repositioning, verifying detach, and the occasional retry.
0. Once per session
Run recon first, before touching any pose:
cd tools\walk-oracle\oh
.\oh-run-capture.ps1 -Script oh-recon.cdb -Log C:\path\to\oh-recon.log
Wait for it to finish (recon has no breakpoints, so it runs to completion
in well under a second once attached — watch for the oh-recon.log.console
file to stop growing, or just wait ~5 seconds). Read oh-recon.log and:
- confirm every
dtoffset matches what the capture scripts' comments assume (see each template's own "OFFSETS (VERIFY-IN-RECON)" block); - read the
dt acclient!DBObjoutput and confirmm_DIDis at+0x28(the lead derived this from the header on 2026-09-02 and the parts template is pre-filled with it; ifdtdisagrees, fix the two0x28reads inoh-capture-parts.cdb.templatebefore running it); - glance at the three
ufdisassembly dumps (AddMeshToAlphaList/DrawPortalPolyInternal/FlushAlphaList): the named pseudo-C declares all three as plain static functions with no receiver and no non-default convention (acclient_2013_pseudo_c.txt424920, 424490, 425941), i.e.__cdeclwith arguments at[esp+4]upward; the templates readuint8arguments withby()(one byte) so the untouched upper bytes of a pushed byte cannot pollute the value. Only if the prologue showsecx/edxbeing consumed as arguments does the template need changing.
If either open question resolves DIFFERENTLY than assumed, fix the template before capturing — do not capture first and "adjust the parser later"; a wrong stack offset produces plausible-looking garbage, not an obvious crash.
1. Per pose
For each of the four poses, in order (doorway-still, terrace-edge, cathedral-arrival, foundry-deep — matches the FW0 capture order, no particular reason to change it):
- Position the retail character/camera at the pose (see table above) and hold still.
- Run the walk capture:
.\oh-run-capture.ps1 -Script oh-capture-walk.cdb.template ` -Log C:\path\to\<pose>.walk.log -Frames 5 - Wait for detach before moving or running the next script. The
runner returns immediately (it launches cdb in the background), so
watch for completion instead of assuming it's done:
or simply tail the log and watch it stop growing atwhile (Get-Process cdb -ErrorAction SilentlyContinue) { Start-Sleep -Milliseconds 500 }F 5:Get-Content C:\path\to\<pose>.walk.log -Tail 20 -Wait - Run the parts capture (stay in the same pose):
Wait for detach the same way..\oh-run-capture.ps1 -Script oh-capture-parts.cdb.template ` -Log C:\path\to\<pose>.parts.log -Frames 3 - Run the alpha-depth capture (stay in the same pose):
Wait for detach..\oh-run-capture.ps1 -Script oh-capture-alpha-depth.cdb.template ` -Log C:\path\to\<pose>.alphadepth.log -Frames 5 - Move to the next pose and repeat.
Do not run two of these scripts against the same live process
simultaneously — cdb's -pn attach is exclusive, and the second launch
will simply fail to attach rather than queue.
Expected line counts
For oh-capture-walk.cdb.template's F/P/LS/BLD/DI/DC lines, real per-frame
counts from the ORIGINAL FW0 40-frame captures of these exact four poses
give a concrete baseline (frame 40 in every FW0 log is truncated by the
auto-detach race, so these are per-COMPLETE-frame averages over frames
1-39; this kit's own 5-frame captures will end at a different, cleaner
boundary since they use the fall-through recipe instead of FW0's
non-firing qd, but the per-frame magnitude should still hold):
| Pose | DI/frame | DC/frame | LS/frame | BLD/frame |
|---|---|---|---|---|
holtburg-doorway-still |
1 | ~6 | 1 | ~13 |
terrace-edge |
0 (outdoor root) | ~2 | 1 | ~14 |
cathedral-arrival |
1 | ~3 | 1 | ~10 |
foundry-deep |
1 | 1 | 1 | ~12 |
EC/OC (new in the walk script) have no prior baseline — this is
their first capture. Expect a similar order of magnitude to DC/BLD
(shell/object-cell draws roughly track the cell/building roster each frame
walks); a flat zero EC or OC count for an interior pose is the real
red flag, not an exact mismatch against the table above.
PD/DM (parts script) and AM/FL/PM/PC (alpha-depth script) also
have no prior baseline:
PD/DMare per-resident-part, not per-cell/building — expect hundreds to low thousands of lines across 3 frames in a town pose (doorway-still, terrace-edge, cathedral-arrival) and noticeably fewer in the sparser foundry-deep interior. This is exactly why the parts script defaults to 3 frames instead of 5.FLis bounded by known call sites (oh1-alpha-list-contract.mds7): expect roughly (buildings-in-view-this-frame) + 1 (top-levelSmartBox::RenderNormalModeflush) + (0 or 1, depending on whether that frame'sPView::DrawCellshad an outside view) per frame — so order 10-16 for the town poses, fewer for foundry-deep.AMandPMdepend entirely on how much transparent/portal content is in view at that specific pose (falls, torches, doorway apertures) and could legitimately be zero for a pose with no transparent surfaces in frame — a zero count here is not automatically a bug.
If retail lags or ACE disconnects
The parts script is the risky one: it traps on every part draw (hundreds
per frame), so expect retail to freeze for a few seconds during its three
frames. Run it last for each pose, and if ACE drops the session once, use
-Frames 2 on the retry rather than abandoning the pose.
Per claude-memory/project_retail_debugger.md's "High hit rates lag the
game" watchout: PD/DM in the parts script are the highest-risk
breakpoints in this kit (fired once per resident part draw, potentially
thousands of times per frame in a dense town pose). If you observe visible
stutter, input lag, or an ACE disconnect during a capture:
- Let the current capture finish or auto-detach if possible — do not
force-kill cdb (see CLAUDE.md:
Stop-Process -Forceon cdb takes the attached retail client down with it viaTerminateProcess). - Re-run with a SHORTER frame count for the offending script:
oh-capture-walk.cdb.template: try-Frames 3instead of 5.oh-capture-parts.cdb.template: try-Frames 1instead of 3 — a single frame is still enough to sanity-check thePD/DMline format and the<GFXOBJ_DID_OFF>offset.oh-capture-alpha-depth.cdb.template: try-Frames 3instead of 5.
- If ACE actually disconnected, follow CLAUDE.md's "Logout-before- reconnect" guidance before relaunching retail — a hard-killed session can stay logged in on ACE's side for several minutes.
- If a specific pose keeps lagging even at the reduced frame count, capture that pose's walk/alpha-depth scripts (cheap) and skip its parts capture (expensive) rather than spending the whole hour on one pose.
New line-kind prefixes (glossary)
All existing FW0 prefixes (F, P, LS, BLD, DI, DC) are
byte-identical to
docs/research/2026-08-30-fw-walk-oracle/README.md's "Trace format"
section — not repeated here.
| Prefix | Script | Source breakpoint | Meaning |
|---|---|---|---|
EC |
walk | RenderDeviceD3D::DrawEnvCell @0x0059F170 |
A built EnvCell shell's draw-stamp fired this frame; <cellid> = the cell's m_DID. |
OC |
walk | RenderDeviceD3D::DrawObjCellForDummies @0x005A0760 |
An object-cell's shadow-part list was walked this frame; <cellid> = the cell's m_DID. |
PD |
parts | CPhysicsPart::Draw @0x0050D7A0 |
A physics part's draw call, with its degrade-resolved GfxObj identity and (best-effort) containing cell. |
DM |
parts | RenderDeviceD3D::DrawMeshInternal @0x0059F360 |
The actual admission/stamp/submit call for one GfxObj part, with its GfxObj identity directly from the argument (no degrade-level resolution needed). |
AM |
alpha-depth | D3DPolyRender::AddMeshToAlphaList @0x0059C230 |
One subset appended to the CLIP or ALPHA FIFO list. |
FL |
alpha-depth | D3DPolyRender::FlushAlphaList @0x0059D2E0 |
A flush-barrier call, with the return address to identify the caller site. |
PM |
alpha-depth | D3DPolyRender::DrawPortalPolyInternal @0x0059BC90 |
A far-Z building punch or true-depth exit-seal attempt, with the counter value before this call's own possible increment. |
PC |
alpha-depth | PView::DrawCells @0x005A4840 |
The persistent portalsDrawnCount/forceClear state sampled at this eligible call's entry. |
Open questions a human must settle before a real session
- GfxObj DataID offset — RESOLVED 2026-09-02 (lead).
DBObj : Interfacelays outvfptr@0,m_dataCategory@4,m_bLoaded@8, the 8-bytem_timeStamp@0x10,m_pNext@0x18,m_pLast@0x1C,m_pMaintainer@0x20,m_numLinks@0x24,m_DID@0x28.DBObjis the first base of bothCGfxObjandCObjCell(viaSerializeUsingPackDBObj), and+0x28is exactly them_DIDoffset every FW0 capture already read on cells. The parts template is pre-filled with0x28; recon'sdt acclient!DBObjre-confirms it. - Calling convention — RESOLVED 2026-09-02 (lead) from the named
pseudo-C signatures.
AddMeshToAlphaList(MeshBuffer*, int32, CSurface*, uint8, uint8, uint8)(424920),DrawPortalPolyInternal(CPolygon*, uint8)(424490), andFlushAlphaList(float)(425941) are static with the default convention: no receiver, arguments at[esp+4]upward. TheAMline now also prints theCSurface*at[esp+0xC], and everyuint8argument is read withby(). The reconufdumps remain as a sanity check. CPhysicsPart+0x14(deg_level). Derived only fromacclient.h:31151-31173's field order (confirmed consistent with its neighborsdraw_state+0x1Candgfxobj+0x20, both of which OH1 independently Ghidra-cited), but never independently Ghidra-checked itself anywhere in the OH1 evidence docs. Low risk, still worth a glance atoh-recon.cdb'sdt acclient!CPhysicsPartoutput.DrawObjCellvs.DrawObjCellForDummiescell-argument location. The task that produced this kit flagged this exact question as an example of the class of ambiguity to watch for. It turned out NOT to be ambiguous once the pseudo-C signatures were read directly (acclient_2013_pseudo_c.txt:429177,429198): both take the cell identity as a plain stack argument at[esp+4], andoh-capture-walk.cdb.templateusesDrawObjCellForDummies(the functionPView::DrawCellsstep 10 actually calls, peroh1-depth-lifecycle.md's exact frame ordering) rather than the innerDrawObjCell/DrawPartCellhelpers. Recorded here as a "resolved, but worth a second pair of eyes" item rather than a true open question, since it was resolved by reading text rather than by a livedt/uf.- The
PD/DMlines'cell=field can be stale. It is set by a silentRenderDeviceD3D::DrawPartCellhook and simply carries forward whatever cell it last saw — it can be wrong for the first part drawn after a building's forced portal-only pass (DrawBuilding'sDraw(part, 1)call) and before that frame's first ordinary object-cell walk. Treat it as advisory, not authoritative; a real port needs the actual call-stack correlation this draft does not attempt. oh-capture-alpha-depth.cdb.templatedoes not instrument the exact mid-PView::DrawCellsPCs (0x5A4885stamp increment,0x5A488Ccounter read,0x5A489Ereset,0x5A48A9conditional clear) thatoh1-depth-lifecycle.md's own Ghidra arbitration table cites. It samples the globalportalsDrawnCount/forceClearmemory cells atPView::DrawCells' function ENTRY instead (thePCline), which is sufficient to reconstruct before/after values across consecutive eligible calls (per that same doc's "Consecutive-call example" table) without guessing which register holds the value at an arbitrary mid-function instruction — but it means this kit does not directly observe the mid-frame stamp increment itself. If a later slice needs that specific event, it needs its own recon pass over those four PCs.