Reworks Campaign CH slice CH2 per the REJECT-review findings doc (docs/research/2026-08-09-ch2-review-findings.md). BLOCKER 1 — SpewBoxController never rendered a line and leaked its pending queue. LinesProvider only ran through UiText.OnDraw, which gates on Visible — and the box started invisible, so the provider (the sole caller of SpewBoxState.Tick) never ran. Gave the controller an explicit per-frame Tick(now) driven by UiRoot's global-message-3 broadcast (a zero-size GlobalTimeSink child, the same pattern VendorUiController.DragOverGlobalTimeSink already uses), matching retail's gmSpewBoxUI::Update. LinesProvider now only returns the cache. Tests rewritten to drive root.Tick(...) instead of calling the provider directly, plus new coverage for visibility-without-a-draw, queue-drain-without-a-draw, and bounded-queue-across-many-ticks. BLOCKER 2 — re-derived the HandleFailureEvent routing table from the PDB-paired binary instead of the pseudo-C's ~33-char string previews. tools/pdb-extract/sweep_weenie_strings.py sweeps every push imm32 in VA 0x571990-0x575480, dereferences into .rdata/.data, and decodes the full UTF-16LE literal. Added the 5 ids dispatched via else-if (missed by case-label enumeration), resolved 0x4F8 (previously excluded), fixed 18 wrong strings (16 the review flagged + 2 more — 0x4E9 and 0x518 — an automated diff between every swept literal and the landed table found). Every changed row cross-checked against ACE's WeenieError/WeenieErrorWithString enum doc comments; both oracles agreed on every row, including a case where the review's own proposed text for the new 0x4E8 row was itself wrong (it was 0x4E9's text) — corrected via the else-if block's own instruction address plus the ACE cross-check. Pinned table count: 344 (338 + 5 + 0x4F8). SHOULD-FIX 1 — RuntimeCommunicationState.ResetSpewBox was dead code; folded into the ChatIdentity generation-reset stage (same lifetime boundary), with a reset assertion added to the existing populated-reset test. SHOULD-FIX 2 — AddText trimmed only the trailing end and invented an empty-string early return; retail's AddTextToScroll trims both ends (trim(&str, 1, 1, ws)) and has no empty guard. Both retired. SHOULD-FIX 3 — ShowWeenieError bypassed the AddText chokepoint via ChatLog.OnWeenieError (hardcoded LogTextType 0x00); routed through Communication.AddText(Resolve(code, param)) instead, and ChatLog.OnWeenieError is deleted — GameEventWiring's legacy no-router fallback now resolves + calls OnSystemMessage directly. SHOULD-FIX 4 — retail's HandleFailureEvent switch has no default case; an unmapped id now resolves to a null Text (silence toward the player) instead of the invented "WeenieError 0xNNNN" hex fallback, with a diagnostics-only console log line for the id. NITs — AP-TBD placeholders corrected to their real register rows (AP-178, not the unrelated AP-177 lifetime row); filed AP-180 for the windowId dual-destination gap and corrected three stale "lands with CH2" comments; extended SpewBoxLayoutDumpDiagnostic from dats.Portal to dats.Local and found the SpewBox element for real — LayoutDesc 0x21000011, element 0x10000048, size 450x72, MaxConcurrentItems (ListBox property 0x10000028) = 4, not retail's code default of 1. AP-178 narrowed accordingly; SpewBoxState.MaxConcurrentItems and SpewBoxController's extent/anchor/OneLine are now authored rather than placeholder (absolute screen position and colour remain open); fixed the "19 ids... lists 18" miscount by retiring the stale paragraph in the class doc rewrite; aligned the UseDone handler's silent-status check with the other two WeenieError handlers. Full Release suite: 11,914 passed / 4 skipped / 0 failed (build 0 errors). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| check_exe_pdb.py | ||
| check_function_map.py | ||
| dump_pdb_info.py | ||
| pdb_extract.py | ||
| README.md | ||
| sweep_weenie_strings.py | ||
pdb-extract — pure-Python MSF 7.00 PDB extractor
Reads refs/acclient.pdb (Sept 2013 EoR build, 28 MB) and writes two
grep-friendly JSON sidecars to docs/research/named-retail/:
symbols.json— 18,366 named public function symbols from the PDB's S_PUB32 records. Each entry hasaddress(image VA),name(MSVC-demangledClass::Methodform), andmangled(raw C++ ABI symbol for callers that need exact mangling).types.json— 5,371 unique named struct/class type records from the TPI stream (LF_CLASS / LF_STRUCTURE). Each entry hasname,size(bytes), andkind(classorstruct).
Usage
py tools\pdb-extract\pdb_extract.py refs\acclient.pdb
Runs in <1 second. No external dependencies — uses Python stdlib only.
Schema
symbols.json:
[
{
"address": "0x00594570",
"name": "CEnchantmentRegistry::EnchantAttribute",
"mangled": "?EnchantAttribute@CEnchantmentRegistry@@QBEHKAAK@Z"
},
...
]
types.json:
[
{
"name": "CEnchantmentRegistry",
"size": 32,
"kind": "class"
},
...
]
Workflow integration
The committed JSON sidecars are the named-retail counterpart to the
acclient_2013_pseudo_c.txt text dump. Pseudo-C is for reading
function bodies; symbols.json is for programmatic lookups. Use
jq to query:
# Find a function by exact name
cat docs/research/named-retail/symbols.json | jq '.[] | select(.name == "CEnchantmentRegistry::EnchantAttribute")'
# Find all functions on a class
cat docs/research/named-retail/symbols.json | jq '.[] | select(.name | startswith("CACQualities::"))'
# Reverse lookup by address (e.g. mid-body fix-up)
cat docs/research/named-retail/symbols.json | jq '.[] | select(.address == "0x00594570")'
# Find a type by name
cat docs/research/named-retail/types.json | jq '.[] | select(.name == "Enchantment")'
Address mapping caveat
The PDB is from the Sept 2013 EoR build. Addresses generally match the
binary used to produce our docs/research/decompiled/ Ghidra chunks
within ~0xC00 bytes (different build runs of the same source revision).
When using symbols.json to correct entries in
acclient_function_map.md, match by name, not by raw address.
Implementation notes
The script is a self-contained MSF 7.00 reader. References used:
- LLVM PDB documentation (https://llvm.org/docs/PDB/) — file format spec
- Microsoft
pdbparse(community) — implementation cross-check
Streams consumed:
- 3 (DBI) — parses the header to extract the symbol-record stream index + the optional debug-header sub-stream's section-headers index.
- 9 (section headers) — parses
IMAGE_SECTION_HEADERentries to build a section-base table for VA computation. - 8 (sym record stream) — iterates records, picks
S_PUB32with thePUBSYM_FLAG_CODEbit set, computesVA = section_base + offset. - 2 (TPI) — iterates type records, picks
LF_CLASS/LF_STRUCTUREthat aren't forward-declared, parses size leaf + name.
The MSVC name demangler (_demangle) is best-effort: handles the
common ?Method@Class@Outer@@<sig> patterns, constructors (??0),
and destructors (??1). Returns the mangled string unchanged for
operator overloads (??2, ??3), vtables (??_), and other forms
where a partial demangle would be misleading. Both name (demangled)
and mangled (raw) are emitted in symbols.json so consumers can
choose.
When to regenerate
- Whenever
refs/acclient.pdbis updated (rare). - Whenever
pdb_extract.pyis changed (e.g. better demangler, more type info recovery).
The output JSONs are committed because they're stable + small (~3 MB combined) and grep-faster than re-parsing the PDB on every session.
Future work (out of scope here)
The current types.json only carries name + size. A more ambitious
version would walk LF_FIELDLIST records to recover field names +
offsets + types — giving us a JSON-encoded acclient.h. Not done yet
because acclient.h already exists committed at
docs/research/named-retail/acclient.h. Consider this if a future
panel needs offsetof() at runtime.