Skip to content

Latest commit

 

History

History
2438 lines (2331 loc) · 137 KB

File metadata and controls

2438 lines (2331 loc) · 137 KB

OpenDS Roadmap

Phased plan. Each phase has a single shippable artifact; later phases depend on earlier ones.

This file is forward-looking. Rewritten 2026-08-28 from the 2026-07-23 reconciled version. The old roadmap had become a second changelog: ~85% of it annotated already-shipped work, duplicating patchnotes.md, and it twice failed to notice whole bodies of work until after they shipped (Phase 4.5, atlas). Shipped phases are now condensed to one short section each: what it delivered, what is still open, where the detail lives. Per-tool VERSION files plus a patchnotes.md entry remain the release record; the previous version of this file (with all its per-release annotation and correction history) is in git history.

Tools come before patches. Anything that makes the digging easier is priority over any specific fix. Every digging-tool ships before the patch that depends on it. The patch phases (Phase 6 onward) start when the toolkit is sharp enough that authoring fixes is plumbing, not archaeology.

Each phase ships a deliverable that is useful on its own, independent of whether later phases happen.

Where we are (snapshot 2026-09-12)

Phases 0 through 5.7 are shipped, and Phase 6 shipped its first fix on 2026-09-11: darkfix-ds1 0.1.0 (fix.ds1.deadtriggers), tagged and released; only the real-Windows half of its applier box stays open. The toolkit reads, writes, round-trips, renders, and reassembles the games' data, and the engine binary is segmented and addressable. Revised 2026-09-04 after a four-agent deep-dive of the whole collection (tooling, binaries, formats, reference clones): the data side is as described below, but the binary side has zero named functions and the known-bug list is entirely symptom-level, so Phase 5.6 is expanded from four leverage points into the frontloaded understanding campaign it should have been.

Tool Version Status
verify-install 0.3.0 shipped
gff-edit 0.6.0 shipped; segmented-type builder deferred
repro 0.5.0 shipped; --diff differential capture landed (2026-09-06); bug-triggering save curation open
gpl-disasm 0.8.1 shipped; 100% corpus alignment, CFG, callgraph, symbol catalogues, global-state + dead-trigger sweeps (2026-09-06)
dialog-extract 0.7.1 shipped; path-aware caller picking queued
save-inspect 0.9.6 shipped; SAVE/1 + SAVE/7 decoded into the field catalogue, game-scoped hypothesis rows (2026-09-06)
image-extract 0.5.0 shipped; animated GIF export landed (2026-09-06); APNG deferred
region-render 0.8.0 shipped; animated palette (--animate-palette, 2026-09-10); --annotate deferred
atlas 0.1.1 shipped
opends 0.1.1 shipped
gpl-asm 0.9.2 shipped; 600/600 round-trip; --patch hex parsing hardened; macros queued
opcode-fuzz 0.3.0 shipped; recipe-driven fuzz + first opcode discovery open
ovr-map 0.3.4 shipped; symbol catalogue (130 DS1 / 132 DS2 rows incl. StartCycle/StopCycle), xref tools, Ghidra pipeline run-proven end to end (2026-09-10), OBJEX sprite pipeline
exe-patch 0.1.0 shipped; the Phase 5.7 EXE patch authoring surface (ovr:/symbol addressing, mandatory fingerprints, --verify gate, in-place enforcement)

What the digging surface looks like today:

  • Data side is basically solved. Every GFF in both games parses and round-trips; GPL bytecode disassembles at 100% corpus alignment and reassembles byte-identically; saves decode and edit; regions render (with entity animation and GIF output); dialogs extract as browsable trees.
  • The binary side just opened, and is still unnamed. ovr-map turns DSUN.EXE from a 600 KB blob into 52 / 49 overlay segments with 935 (DS1) / 854 (DS2) confirmed function entry points, disassembled at correct bases and importable into Ghidra as labelled, correctly-based memory blocks. The 5.6.0 instruments are built (2026-09-04); 5.6.1 is COMPLETE (2026-09-05): both dispatch tables resolved (129 handlers per game, bases 0x97d0 DS1 / 0xc4c0 DS2), the 15 unknown opcodes proven unimplemented in both engines, the syms catalogues hold 125/127 named functions (21 verified string-xref anchors + 114 dispatch-table handlers per game), the gpldisk module fully mapped (~53 functions), the OBJEX object database visually open (17 mines objects identified), the GPLI-1 entry directory decoded, the cooperative scheduler identified as the freeze mechanism, the DGROUP BSS layout mapped, and the overlay manager decoded. Structure: measured. Semantics: the mines-elevator site report is one runtime capture from complete. That gap is Phase 5.6.2/5.6.3.
  • The patch pipeline has its first real surface. Bytecode patches author by label-relative address with fingerprint checks (gpl-asm --patch), and the darkfix package shape (manifest, applier, journal, unapply) shipped under ds1-patch/ v0.0.1, proven with a no-op fix (Phase 6, checkbox 1). The EXE patch-authoring surface shipped as tools/exe-patch v0.1.0 (Phase 5.7, 2026-09-06): ovr: and symbol-relative addressing, mandatory bytes_old, a --verify gate, and the in-place-only rule enforced. Phase 6 then shipped the first real fix through the pipeline: darkfix-ds1 0.1.0 (fix.ds1.deadtriggers), tagged and released 2026-09-11.

Open items from the shipped phases are consolidated in the Backlog at the end, each with the trigger that promotes it into real work.

Phases 0-5.5 (shipped, condensed)

Detail for every shipped item lives in patchnotes.md and each tool's README. What follows is the one-paragraph record plus anything still open.

  • Phase 0, documentation + extraction: docs corpus (file-formats.md, known-bugs.md, gpl-bytecode.md, binary-patching.md, patch-workflow.md, research.md, install-variants.md, dso-symbols.md, dsun-exe-re.md), source-hash manifests, verify-install v0.3.0. Open: the per-tool git tags (see Backlog).
  • Phase 1, gff-edit: pure-Rust GFF read/write, 128/128 corpus round-trip, bulk extract, text codec, gff-cat what describer, indexed builder. Open: segmented-type build.
  • Phase 2, repro: DOSBox-Staging harness with overlay-mount discipline, smoke fixtures, --play --session continuity, scheduled keystrokes via ydotool, video capture. Open: differential capture; bug-triggering save curation.
  • Phase 3, gpl-disasm: recursive-descent CFG, inter-chunk callgraph, curated syms/{opcodes,functions,variables,locals}.toml, DSO-symbol importer producing review-ready rename proposals.
  • Phase 4, exploration tools: dialog-extract, save-inspect, image-extract (+image-pack), region-render, atlas.
  • Phase 4.5, human-friendliness sprint: opends umbrella CLI, write paths across the toolkit, atlas. (This whole sprint shipped before the roadmap knew about it; see the rewrite note in the header.)
  • Phase 5, gpl-asm + opcode-fuzz: 600/600 byte-identical round-trip through bytes, JSON, and labelled text; structural edit API; author safety net; declarative --patch mode with label-relative addressing (v0.9.0); chunk-patchwork pipeline and DOSBox-driven run harness in opcode-fuzz. Open: recipe-driven fuzz; the phase's done-when (discover at least one previously-unknown opcode) is not yet met.
  • Phase 5.5, ovr-map: overlay segment map for both binaries; --disasm, --callgraph, --verify, --selftest, and the --ghidra script verified headless (52 blocks, 935 labels, dispatcher prologue reads correctly). Measured structure: DS1 52 segments / 247 KB / 93.13% coverage / 935 stubs / 210 direct far-call edges; DS2 49 / 252 KB / 93.39% / 854 / 216. The "why this matters" section of the previous roadmap named two follow-on workstreams; they are now Phases 5.6 and 5.7 below.

Can we patch the EXE yet? (assessment, 2026-08-28)

Asked outright: do we understand DSUN.EXE well enough to write a binary patch, or does a fix need a full exploration report first? The honest split:

Solved: mechanical safety. The overlay map is complete and verified (52 / 49 segments, 935 / 854 entry stubs, ~93% coverage), any byte is addressable as ovr:seg+off, the Ghidra import is verified headless (caveat, found 2026-09-04: that run left no persisted artifact; 5.6.0 carries the re-prove-and- persist box), and the applier layer refuses fingerprint drift, length changes, and wrong installs. Authoring a safe byte patch is already plumbing.

Not solved: finding anything. Almost nothing in the binary is named: zero of the 1,789 entry stubs carry a name, the curated symbol catalogues hold two entries, and the resident image (all code before the overlays) has had no systematic pass at all. The callgraph identifies direct callers for only ~12-14% of stubs and never scans overlay payloads for far-calls. The official-patch diff is first-measured at segment granularity (survey §8) but no changed segment has been read instruction-by-instruction; the DSO symbol transfer has a working matching precedent on the GPL side and a do-nothing stub on the EXE side. "Where does the mines elevator transition live?" is today a dig, not a lookup. That gap is exactly what Phase 5.6 exists to close.

Decision: no full-exploration gate. Requiring a complete EXE report before any patch attempt is the engine-first trap again (spec §1a): it defers every fix behind a multi-month research program, and most target bugs never touch the binary at all. Instead, exploration is targeted and per-fix:

  • A data-surface fix (most quest, flag, and dialog bugs) never touches the EXE and proceeds now. Phase 6 explicitly prefers one for exactly this reason.
  • A binary-surface fix requires a written site report before the patch is authored, added to docs/dsun-exe-re.md (or the fix's own writeup): the function(s) involved, the evidence chain (string xrefs, DSO symbol match, a 1.0-vs-1.10 official-patch diff hit, or call-shape match), and before/after disassembly. The report is the deliverable; the patch is its application. No site report, no patch.
  • Amended 2026-09-04, after the collection deep-dive: the Phase 5.6 leverage points are no longer pull-forward-only. The deep-dive found zero named functions, an effectively empty symbol catalogue, and a known-bug list in which no bug has a located site or a root cause; "targeted and per-fix" from that baseline means every fix pays the full archaeology tax from zero. Phase 5.6 now runs as a frontloaded campaign (tooling, naming, formats, bug-site census) on its own schedule, and per-fix pulls happen on top of it. The no-full-exploration-gate ruling stands in this sense: the gate is and stays the per-bug site report, not a complete EXE report, and data-surface fixes proceed regardless.
  • Phase 7 (the mines elevator) is the first plausible binary-surface fix. Its site report is the first test of this policy; if the evidence chain stalls, the honest fallback is another data-surface or deferred bug, not an open-ended dig.

Phase 5.6 — Name the binary (EXE RE at scale)

Goal: turn 935 / 854 confirmed entry points into a named function catalogue per game, so that "where does this bug live in DSUN.EXE" becomes a lookup instead of a dig.

Ships: per-game EXE symbol catalogues under tools/ovr-map/syms/ (or a sibling), the Ghidra pipeline that grows them, the investigatory tools in 5.6.0, and docs. The 2026-09-04 deep-dive corrected this phase's original premise: the labour was not going to be doable with the existing tools alone (no name store, no working EXE-side symbol matcher, no GPL↔EXE cross-reference, no diff annotator), so the instruments ship first.

Progress 2026-08-28: the whole-binary structure survey (docs/dsun-exe-survey.md) shipped. Headline: the overlay code calls only ~340 distinct resident functions (4,865 / 5,067 direct far-call sites, census in survey §3.3), the overlay manager body is located (0x466e0 / 0x4aff0), and the DSO name list has a concrete matching order: resident targets first, overlays second. The official-patch diffing leverage point below is updated: it is blocked without a CD 1.0 base.

Expanded 2026-09-04 (the ground-truth frontload): a four-agent deep-dive of the whole collection (tooling, binaries, formats, reference clones) returned a simple scorecard. Measured and solid: the overlay container, the segmentation, the resident/overlay split, and the official-patch delta. Empty: names (0 / 1,789 entry stubs), opcode runtime semantics (no opcode's effect ever observed; ~40 of 129 catalogue rows are Custom), SAVE chunk meanings beyond SAVE/5-/6 (validated on a single played save), and bug sites (no known bug has a located site or root cause). The reference clones materially help: the DSO Decode* handler block is address-ordered and agrees with ~114/115 of libgff's independently-derived opcode names, so the unknown opcodes are pinnable by elimination. The items below turn that scorecard into work: tooling first, then naming, then formats, then the bug-site census. Phase 5.7 and Phase 6 start from what this phase produces.

Four original leverage points, in order of cost:

  • Ghidra headless workflow written down and run. The ovr-map --ghidra script lands segments and entry-stub labels. What is missing is the working session recipe: a headless analyzeHeadless invocation (the binary ships with Ghidra at ~/.local/share/ghidra_12.1.2_PUBLIC/; it is not on $PATH, which is fine), a script skeleton for bulk renaming from a TOML catalogue, and an export path back to text (decompiler listing or disassembly) checked into docs/ as findings. Keep the Phase 5.5 caveat: the decompiler is weak on 16-bit segmented code; the deliverable is navigable, correctly segmented, and named, not clean C. (TICKED 2026-09-10: the recipe in docs/re-tooling.md ran end to end on both games; the OSGi outage that had blocked script execution since 08-29 stopped reproducing, and the first real run exposed three latent script bugs, all fixed in ovr-map 0.3.4: the rename generator's getAddress cast, its row-joiner split, and an unclosed comment in OvrExport.java. DS1: 52 blocks / 935 labels / 130 rows / 1,670 functions exported; DS2: 49 / 854 / 132 / 1,294. The manual-javac syntax check stays as standing procedure; it caught the two compile-level bugs before the run.)
  • DSO symbol transfer. docs/dso-symbols.md documents the 3,530-function Watcom symbol table from Dark Sun Online (which inherited the WotR codebase). The offsets do not map directly onto our binaries; the work is byte-pattern and call-shape matching, seeded by the ovr-map --callgraph edges and the gpl-disasm import-dso-symbols.py precedent (which already does this class of matching for GPL symbols). Produce review-ready proposals, curated by hand into the catalogue; never auto-committed, matching the existing curation rule. (TICKED-AS-ABSORBED 2026-09-10. The deliverable this box was after, a curated hand-reviewed EXE symbol catalogue, exists and outgrew the DSO-matching method that was to fill it. What landed instead, all hand-curated with evidence chains: scripts/propose-exe-symbols.py (the review-ready proposal generator: census, string anchors, catalogue rendering), scripts/xref-string.py (the matching method that worked, automated), 21 verified DSO-named string-xref anchors, and the dispatch-table decodes naming 114 handlers per game. Catalogues stand at 130 DS1 / 132 DS2 rows. The large-scale byte-pattern matcher was never built and no longer pays: the Decode* study corrected its premise (the DSO handler block is not address-ordered, so elimination pinning fails), and the dispatch tables named the bulk surface outright. The residual unnamed targets, the ~200 overlay-to- resident call sites, need runtime capture or deeper string-xref passes, not DSO byte matching.)
  • Official-patch diffing. ✅ Unblocked and first-measured 2026-08-28. The CD 1.0 base exists and is hash-confirmed: install-variants.md §3 records game.gog carrying the 1.0 CD tree (DSUN.EXE = e73f79c3...), and an independently sourced public CD-tree zip hash-matches that record exactly; the tree is staged dev-side at .games/archive-org/cd10-extracted/. The CD 1.0 vs GOG 1.10 segment diff is the official fix delta: 5 segments byte-identical, 44 changed, 117,566 differing bytes, with fix-sized localized clusters in segments 0, 5, 8, 9, 16 and 36. Measured evidence and reading in ../docs/dsun-exe-survey.md §8. Remaining: characterize the low-cluster segments instruction-by-instruction against the 1.02 fix list (known-bugs.md §1); cross-ref with the DSO symbol matches. (The floppy 1.0 line is a different product build and stays useless as a diff base; an earlier same-day note calling this item blocked was wrong and is corrected in the survey.) (Ticked 2026-09-06: the differ shipped 2026-09-04 with 19/44 changed segments signature-verified, and the "Remaining" instruction-level reading completed under 5.6.3's "Read SSI's own fixes" with the rebias-only verdict, which also mooted the DSO cross-ref: there are no behavioral EXE fixes left to name.)
  • First named consumers. The catalogue is real when something else uses it: at minimum, the VGA colour-cycling routine (VGAColorCycle / gCycleColor candidates already located via the DSO table) decoded far enough to unblock region-render's animated palette backlog item, and one known-bug site located by name as input to Phase 6 or 7. > COMPLETE 2026-09-06: both halves met. (1) The > mines-elevator site is located AND named (census row + > trigger usetrigger 3753, 287, NAME(-5807), object > 5807 = BMP 951). (2) The VGA colour-cycling routine is > decoded end to end: pump, record layout, rotate > direction, AND the registration API with its four > boot-time ranges (docs/dsun-exe-re.md 4.5.5-4.5.6, > StartCycle at DS1 0x28707 / DS2 0x2cee7); the > animated-palette backlog is fully unblocked.

5.6.0 — Investigatory tooling (build the instruments first)

  • Land the in-flight ovr-map scripts. Commit tools/ovr-map/scripts/diff-official.py, and replace the import-dso-symbols.py stub (it parses the DSO table and emits literal *TBD* columns for six hardcoded names; it performs no matching, and carries a dead statement where its argument group is immediately overwritten) with a real proposal generator modelled on the gpl-disasm importer. Gitignore or scratch-park the generated OvrMap.java. (Landed 2026-09-04 as scripts/propose-exe-symbols.py; the rename also resolves the same-filename-in-two-tools collision with gpl-disasm's importer. Three honest modes: --census (exact seg:off resident-target worklist with 55 8B EC evidence marks), --strings (source-file anchors; finds gpldisk.c at 0x49d3d and reproduces the corrected DSO prefix census: Save 12, Combat 6, Region 0), --anchors (renders curated rows into catalogue format). Finding: survey 3.3's "345 targets" keyed targets by segment base, dropping the call offset; the exact census measures 746/760 candidate targets (36/35 prologue-confirmed), and overlay far-call segment words predate the manager's relocation pass, so targets stay candidates until corroborated.)
  • EXE symbol catalogue format and store. A tools/ovr-map/syms/<game>.toml schema (name, segment, offset, evidence, confidence) plus loader support so ovr-map --disasm and --callgraph render names. Today there is nowhere to put a discovered name; every finding lives in prose. (Shipped 2026-09-04: schema + curation rule in the catalogue headers, --syms loader with loud validation, names in --verify / --disasm / --callgraph, selftest bound-checks rows; seeded with load_resource both games and the two overlay-manager bodies.)
  • Ghidra pipeline made real and persisted. Re-run the headless import and keep the project plus exported function lists under scratch/ (gitignored), with the analyzeHeadless recipe checked into docs/; add the bulk-rename-from-catalogue script (the rename half of this phase). The 2026-08-28 "verified headless" claim left no artifact; re-prove it once, then keep the proof. > Progress 2026-09-04: the recipe is now real and > corrected (the old one could not have run from this > checkout: Ghidra resolves bare script names against > $PWD and rejects every path element starting with > '.', which .gitrepos is). The bulk-rename half landed: > ovr-map --ghidra-rename generates the catalogue- > driven rename script from syms/<game>.toml, and > tools/ovr-map/ghidra/OvrExport.java is the checked- > in TSV export path. Import + analysis + persistence > re-proven: the analyzed DS1 project is kept under > scratch/ghidra_project/ds1_proj.rep. Still blocked, > host-side: Ghidra's OSGi script compiler broke on this > machine between 2026-08-29 (last successful compile) > and 2026-09-04: every script, including a trivial > control, fails with "Failed to get OSGi bundle"; > cache nuking does not help; manual javac against the > pinned JDK compiles our scripts clean. Full evidence > and the standing syntax-check recipe in > docs/re-tooling.md. Until the host layer is > fixed, propose-exe-symbols.py --census stands in > for the Ghidra-side function list. > (TICKED 2026-09-10: the OSGi layer recovered (see > re-tooling.md; root cause never pinned), and the full > pipeline ran on both games with the 0.3.4 script > fixes. The proof is kept: analyzed + renamed projects > under scratch/ghidra_project/ds1_proj.rep and > ds2_proj.rep, exported function lists (DS1 1,670 > rows, DS2 1,294, catalogue names applied) under > scratch/ghidra_project/export/. The exported TSVs > are now the function-level worklist the census box > wanted from Ghidra.)
  • Cluster-annotated official-patch differ. Promote diff-official.py to a real tool: per-cluster file offsets, nearest entry stub, before/after ndisasm excerpts, and signature-checked segment pairing (the current index pairing silently misaligns if a segment was ever inserted). DS1 stays out of scope: no 1.0 base exists to diff against. (Promoted 2026-09-04. Signature pairing = size + entry- offset sequence: 19 of the 44 changed segments pair verified, 25 fall to flagged UNVERIFIED index pairing. First read: segment 16's two 1-byte clusters are SSI repointing an overlaid load_resource far-call from 0128:04a1 to 0128:04ab, the documented 1.10 loader address: the exact fix shape the 5.6.3 census hunts.)
  • GPL↔EXE cross-reference index. Join gpl-disasm --global-cfg edges and chunk entry points with ovr-map --callgraph far-call edges and the resident call-site census, so "which EXE code runs this GPL chunk" becomes a lookup. This is the missing link that makes the ~340-resident-function survey navigable, and the substrate the name catalogue grows on. (Built 2026-09-04: scripts/gpl-xref.py. The join key is the loader argument pair: a 66 68 <FOURCC> push with an immediate id push behind it and the far call ahead. DS1 215 sites / DS2 209, 77 / 65 carrying immediate ids; the add sp, 0xc after the call confirms the three-argument loader contract. Headline finding: there are NO direct GPL /MAS pushes in either binary; script chunks load through the GPLI/GPLX index chunks, so the per-chunk join resolves one level. The statically visible boot requests are exactly six DS1 (4x GPLI[1] from ovr21, 2x GPLX[1] from ovr22) and four DS2 (GPLI[1] from ovr18, two of whose loader calls resolve to the catalogued load_resource 0x692b); other region script loads must compute the index id at runtime. JSON index plus per-game snapshots under scratch/gpl-xref/.)
  • Format coverage report. Walk every GFF in .games/, .games/archive-org/, and testing_facility/; tabulate chunks per FOURCC against gff-cat kind --list and docs/file-formats.md. Quantifies exactly which chunk kinds are undocumented (RNME, VECT, PLYL, ALL, DATA, RGTP, PREF, GREQ at minimum) and which containers no tool has ever touched. (Done 2026-09-04: tools/gff-edit/scripts/format- coverage.py walks the corpus TOC-only (stdlib, 12-byte indexed entries, the 12-byte segmented trio per gff-edit's parse), and the snapshot lives at docs/format-coverage.md. Measured: 120 GFF containers, 47 of 68 documented kinds present; gap list DATA (1,292 chunks), MAP/RNME (60 each), PLYL, ALL, GREQ, VECT, CMAT, CPAL, PREF. RGTP does not appear in this corpus.)
  • DARKRUN SAVE semantic differ. Layer field-level hypotheses (region id, party position, quest flags) onto save-inspect save-diff's byte diffs, so each play-session diff accumulates understanding instead of scrollback. (Built 2026-09-04: save-inspect/scripts/save-semantic- diff.py over the new syms/save-fields.toml. The TOML seeds only what the repo already knew: the verified SAVE/5 record layout (stats[6] at 34..39, name at 40..57) and SAVE/6 blocks from ds1-party-edit, plus the README's one-save speculation rows for SAVE/1, /10 and /18. Clusters no row covers print as UNKNOWN and are the next session's RE target; confirmed findings become rows. Smoke: a synthetic stats mutation in SAVE/5 record 2 annotates as record 2 field+0x22, combat stats[6] [verified]. Region-id / party-position / quest-flag rows wait on the played-save pairs only Brandon's play sessions produce.)
  • Hygiene riders. De-hardcode /home/bdkl from the five Rust corpus tests (portable root discovery; the current silent skips hide coverage loss from CI); resolve the duplicate import-dso-symbols.py naming collision (372-line matcher vs 66-line stub, same filename in two tools); give ds2-patch/ its manifest.toml + VERSION and record the promote-vs-copy decision for the applier. (Done 2026-09-04. Nine corpus test files carried the hardcode, not five; all resolve from CARGO_MANIFEST_DIR now, with gff-edit's Wine install roots keyed off $HOME so coverage survives on any clone. Collision resolved by the rename to scripts/propose-exe-symbols.py (box 1). ds2-patch has VERSION 0.0.1 and a manifest.toml carrying the canonical GOG 1.10 DSUN.EXE hash and an empty fix list; promote-vs-copy for scripts/darkfix/ stays an explicit Phase 7 decision per that README, not silently made.)

5.6.1 — The naming campaign

  • Verify the first DSO→DS2 address anchors by the string-xref method docs/dso-symbols.md itself prescribes (about 20 verified rows before emitting a catalogue). This validates or kills the transfer premise before any scale matching; the honest fallback is behavioural naming without DSO names. Note the offsets are DSO-v1.0-client-relative (flat offsets into the extracted 32-bit image); only the names are claimed to transfer. (Ticked 2026-09-06: done-but-unticked; the threshold closed 2026-09-05 at 21 verified rows, per the progress note.) > Progress 2026-09-04: the reference method is solved > and six new anchors are in the catalogues (10 rows > total). The naive seg:off pair search finds nothing; > strings are referenced as bare 16-bit push/mov > immediates relative to DGROUP, read from the entry > point's mov dx, imm16 (DS1 0x4356, DS2 0x47e0). > scripts/xref-string.py automates it. Verified > (self-naming string inside the function): > LoadGameFromDisk DS1 ovr21+0xdde, DS2 ovr18+0xa6c. > Probable (string anchors the module; DSO family > naming): SaveGameToDisk slot path DS1 ovr13+0x7cc / > DS2 ovr11+0x8e5, the region-change module DS1 > ovr21+0x1487 (two string refs now), and the teleport > loader DS1 ovr21+0x17d0; all confirmed entry stubs. > Second pass added four more: gpldisk iCtrl validator > (DS1 ovr21+0x0, the segment's first entry), MEL/DJ > audio init (DS2 ovr11+0x26), and the resident-side > version-banner check (DS2 resident 0x1cf85). Third > pass: the GPL VM's illegal-opcode handler in the > resident image (DS2 0x2660c, referencing > 'Illegal Op (gpl=%d)') is the dispatch-table > neighbourhood the Decode* study left open for pinning > the unknown bytes; plus the OBJEX item-lookup entry > (DS2 ovr35+0x2327) and the awaken path (DS1 > ovr04+0x165). Catalogue: 17 rows. The transfer premise > holds across the persistence, status, dispatch and > audio layers. Fourth pass closed the threshold: > find_path and line_of_sight_check (both DS1 resident), > the combat save gate (DS1 ovr25+0x1383, in the > dispatcher segment), and the CD-drive check (DS2 > ovr04+0x85d, two sibling stubs). Catalogue: 21 rows — > the anchors box's catalogue condition is met, and the > resident-side rows seed the resident-census box.

  • Name the resident API surface. The ~340 distinct

    > COMPLETE 2026-09-05: the survey's "even 100 named
    > functions" threshold is exceeded. The syms catalogues
    > hold 125 rows (DS1) and 127 rows (DS2): 114 GPL VM
    > handler addresses from each dispatch table (all named
    > from the opcode table), plus the 21 verified anchors
    > and the gpldisk module functions. The total named
    > surface across both games exceeds 250 functions.
    > Every entry carries an evidence chain (dispatch-table
    > offset, reloc-confirmed segment base, or string-xref
    > anchor). The remaining gap is the ~200 overlay-to-
    > resident call targets from survey 3.3 — the census
    > tool identifies them but individual naming needs the
    > runtime capture or deeper string-xref passes.
    
    overlay→resident call targets (survey §3.3) are the
    highest-value naming set in either binary; the survey's
    own threshold is "even 100 named functions". The hot
    targets (0x5cc0, 0x5810) are already known.
    > MAJOR 2026-09-04: the DS2 GPL dispatch table is
    > FOUND. The interpreter's per-opcode dispatcher sits
    > at DS2 resident 0xc650 (proven: bounds-check <= 0x80,
    > a per-opcode TRACE HOOK at DGROUP:0x2f6 — call far
    > [0x2f6] — then `shl ax,1; call near [bx+0x30a]`), and
    > the table is at DGROUP:0x30a (file 0x4d30a): 129
    > segment-local code offsets, 115 distinct — matching
    > the DSO Decode* handler count. All 15 unknown bytes
    > share ONE entry (0x20a2, the default): **the DS2
    > engine implements no dedicated handlers for them** —
    > the pinning question dissolves from "which handler is
    > it" to "they are reserved and unimplemented",
    > provable from the table alone; any corpus chunk using
    > one hits the illegal-op path (`syms/ds2.toml`
    > gpl_dispatch). Segment base pinned at probable
    > 0x6500 (paragraph 0x130): it wins the decodability
    > test over 0x320/0x3e0 and, decisively, opcodes
    > 0x01-0x08 map to consecutive 0x14-0x30-byte handlers
    > (the arithmetic family in sequence) with Getxy
    > displaced out-of-band exactly as DSO's independent
    > order places it. Every named opcode now has a
    > candidate DS2 handler address; the full resolved
    > table is generated at `docs/dispatch-table-ds2.md`.
    > DS1 read DONE the same day, cleaner: the table is at
    > DGROUP:0xc0 (file `0x48a20`), the dispatcher at
    > `0x99cb` (`call near [bx+0xc0]`), and the segment
    > base 0x7900 is the UNIQUE survivor of the
    > tiny-vs-large filter, corroborated by the same
    > consecutive arithmetic-family structure and the
    > displaced Getxy. Same headline: all 15 unknown bytes
    > share the default entry (`0x264c` → file `0x9f4c`)
    > in BOTH engines — the GPL VMs never implemented
    > them. Resolved table: `docs/dispatch-table-ds1.md`.
    > Both bases remain probable until a semantic handler
    > read confirms; ByteDec (DS2) already marshals and
    > calls through the far-pointer table at 0xa4d4.
    

Site-report sketch (elevator, first read 2026-09-04): DS2's > gpl_disk_change_region (ovr18+0x1132, 826 bytes, 307 > instructions) is the region-change state machine. > Confirmed in the first pass: far-call helpers into > segments 0x5f8/0x5b0/0x638, an optional entry hook, a > relocation call 0x5b0:0xc0 taking (0, 1, 0, arg), > and a region-entry one-shot sweep READ 2026-09-04 > (supersedes both earlier guesses): mov si,0x6874; > mov di,5 starts a scan of a ~315-record array of > 37-byte structs at cs:0x6874, indices 5..319. Per > record: call 0x100:0x2 with (1, 0:0, -1, index); > field word at +0x16 — if non-negative, negate it > (one-shot consumed marking) and use it x9 as an index > into a 9-byte-entry DGROUP table at 0x6578; also the > record's word at +0x01 << 3 indexes an 8-byte-stride > array at [0x67b7] whose byte +5 gets bit 0x40 > cleared. Reading: on region change, consumed one-shot > trigger records are marked and region-entry flag bits > cleared. The region id (arg at bp+6) is saved to > DGROUP:0x140c. Two fatal exits jump to offset 0x1b0 > when the 0xfe85/0xf7b1 validation calls return zero. The 'Fatal error: Region > change, Invalid save' path is at function offset > 0x1b0. Next reads: the 5-entry table contents, the > normal successor paths, and the failing elevator > caller. > Base-status detail: ByteDec at 0x6500 decodes as a > coherent marshalling stub (byte-masked argument, far > call through the 0xa4d4 pointer table, iret unwind — > an unusual VM discipline worth its own read), but the > default entry 0x20a2 does not decode cleanly at > 0x85a2 under any ±6 alignment: the table's tail may > hold a sentinel rather than the default handler. > Bases stay probable; settlement route is the trace > hook (DGROUP:0x2f6) live in a debugger, or resolving > the 0xa4d4 pointer table. > 0xa4d4 pointer-table probe result: the table is ALL > ZEROS on disk (entries around 0xa4d4 included) — it > is runtime-initialized by the VM's setup code, so > static resolution of the operation implementations > requires finding the init writer (search for stores > to 0xa4d4's range) or a runtime trace. Init-writer > hunt DONE, with a twist: the store into 0xa4d4 is at > 0x6fc6 — INSIDE the handler region, in opcode 0x04's > own handler (LongDec, 0x6fc1). The 0xa4d4 slot is not > an init-time table; it is a self-managed VM slot the > stubs read and write among themselves (ByteDec reads > it and calls far through it; LongDec stores it). The > handler stubs are the VM's per-opcode implementations, > and 0xa4d4 holds per-execution state (likely the > current-operation pointer). This strengthens the > base-0x6500 reading: the stub region IS the > implementation layer. Full interpretation wants the > runtime trace. > Fatal path read: prints the message (0x5a0:0x34), > resets UI/cursor state (0x14d7/0x14d9 = 0, 0x14db = > 0x800, 0x14dd = 0x620), calls 0x28:0xb with the > region id, then conditionally formats a follow-up > message from strings at 0x1787/0x178a. > Script family found (2026-09-04, evening): the > elevator lives in GPL chunks 76-85 (Blick the > elevator operator, Zeegrat, the miners; 'We can't > use the elevator until he's found' is the gate) with > later references in 268/273/283/284/295. Also: the > two fatal-exit validation calls resolved — routine > A (0xfb7) re-validates the 37-byte record array > (same stride at ds:0x67bc), routine B (0x8e3) builds > the string 'SAVE' and checks the save file: the > 'Invalid save' fatal is literally a save-validation > failure on region change. > Transition map (same evening): GPL-81 is the mines' > teleport HUB — ~75 gpl tport instructions in three > shapes: named-region tports NAME(-N), 255, 99i8, > 99i8 (default arrival), same-region coordinate > tports 32766, x, y, 0, and GPL-80's elevator dialog > riding tport GNAME[38], 255, 99, 99. The freeze > hypothesis sharpens: an elevator tport targets a > region whose level-load fails; the NAME(-N) packed > references are resolvable via dialog-extract's string > table, which names the exact destination regions. > Prereq discovered: NAME(-N) is a raw halfword index > (GPL_IMMED_NAME | 0x80, cval = h * -1 per > gpl-disasm's decoder) into the chunk's inline name > pool — whose layout is engine-side (the gplshell.c > module) and undecoded. RESOLVED 2026-09-05, and the answer kills the pool theory: > NAME(-N) is a negative-encoded OBJEX object id. > Evidence: libgff's GPL_GNAME handling shows GNAMES > are 13 runtime object-handle registers (not strings); > and OBJEX.GFF's RDFF records span ids up to 32003 — > exactly covering the NAME refs (up to -30028). The > 'pool' was never a string table: NAME(-5814) is > OBJEX object 5814. The tport destinations are > likewise object/location ids in OBJEX's id space. > The -58xx block decodes as 68-byte entity records > whose per-object u16 (402, 951, 958...) are > sprite/animation refs into OBJEX's own SCMD (max > 3943) / BMP (max 3953) space. > RENDERED 2026-09-05: object 951's sprite is the > elevator shaft — a 25x64 vertical shaft with > chevron bracing (scratch/spin-delta/obj951.png, > PLNR-encoded, 2 frames). Technique for rendering > OBJEX sprites (OBJEX ships palette-less): copy a > region GFF that has a PAL, overwrite a > bigger-than-target indexed chunk slot in place with > the BMP bytes (fits-in-place writer policy), fix the > TOC length, then image-extract with > --palette-kind/--palette matching the host region. > This opens the entire OBJEX object database to > visual identification. > First batch rendered (obj951-958): 952 = wooden > mine door, 957 = debris/ore pile (broken beams and > rock — a cave-in), alongside the elevator shaft. > The mines' object set is assembling visually: shaft, > door, cave-in debris — exactly the pieces the > freeze story touches. > Pool-location probe: GPL-81's 6,170 bytes are pure > bytecode (no local pool), and the remaining candidate > is GPLI-1 (7,896 bytes at file 0x1f8f7f, one per > game): binary u16-structured from byte 0 (zero head, > then runs of increasing u16 values) — consistent with > an index table mapping the NAME halfwords to entries. > Its RE is the concrete next unit: assume u16 (or > paired-u16) indexing at NAME(-187) = entry 187 and > correlate against the 75 GPL-81 tports' expected > destinations (mine levels are known from the dialog: > 'Tyrgar Mine', levels 1-6, the Underdark door). > First test NEGATIVE: plain u16[N] indexing shows no > consecutive-region structure at the mine-level > indices (74-80 -> 30, 235, 1, 57, 347, 26, 83; the > 187/416/526 entries are equally scattered). Next > probes, in order: byte-shifted (+1) u16 alignment, > 4-byte entries, and the RGN-file region-id space > (DS2 has ~60 regions; the mine levels should be a > consecutive run wherever they live) as the > correlation target instead of raw GPLI values. > Probe results: P1 (byte-shifted u16) and P2 (4-byte > entries) both negative. P3 measured the region-id > space: DS2 ships 20 RGN files with ids {1, 50-63, > 65-69} (plus 255 as the same-region tport marker, > confirmed by the 32766-prefix tports' operand > pattern). NEW: the sweep-record +1 extraction at > cs:0x6874 yields garbage words (up to 65532), which > means the record base is wrong — either cs:0x6874 is > not seg_start+0x6874 (CS-base derivation needs the > overlay's real load paragraph) or the di=5..319 loop > does not start at record 0. RESOLVED 2026-09-05, differently than expected: the > extraction was reading the right address — the array > at DGROUP:0x6874 is 11,840 bytes of 100% zeros on > disk. It is BSS: runtime game state, populated as the > party plays. The record layout (+1 word index, +0x16 > one-shot field) stands, the 320-record capacity is > real (the sweep's di=5..319 with records 0-4 > permanent), and the structure is at a KNOWN address — > a debugger write-watchpoint on > DGROUP:0x6874+(n*37)+0x16 will catch every > transition-record creation, elevator included. The > freeze hunt is now a runtime-capture job (the > played-save/debugger loop), not a static dig. > Accessor web found (same session): the sibling > 8-byte-stride array is reached through a POINTER CELL > at DGROUP:0x67b7 (mov ax,[0x67b7] = load the > array base; the array is dynamically allocated), and > SEVENTEEN sites across the binary load that cell: > resident engine core (0x27045, 0x27791, 0x277ec, > 0x28207, 0x2af34, 0x2c94c), the gpldisk module > itself (ovr18 0x709b8, 0x70b66, 0x71099), and > overlay code at 0x807xx/0x809xx/0x80axx. The > region-entry flag array is shared core state; the > gpldisk trio sits inside the save/restore path, > consistent with the records being reconstructed from > the save at load. The 37-byte record array's only > static accessor remains the sweep. > COHERENT READING (late session): the records are > INDEXED BY REGION ID. The sweep's di=5..319 range is > the region-id space (RGN ids 50-69 fit inside; 0-4 > are reserved/permanent), record N = region N's > transition record, and the +1 word is that region's > index into the 0x67b7 flag array. Everything in the > function now agrees: the per-region sweep, the flag > clearing, the save-file persistence, and the > 'Invalid save' gate. The mines' records are the > entries for whatever ids the upper/lower levels use > (two of 50-63/65-69). IDENTIFIED 2026-09-05: > the mines are region ids 56/57/58 (RGN038='Mines1', > RGN039='Mines2', RGN03A='Mines3' — the region files > carry their own name strings). Their transition > records sit at computable addresses > (DGROUP:0x6874+(id-5)37), and their scripts are > GPL 76-85. MAPPING VERIFIED 2026-09-05: the MAS > chunk id equals the region id — MAS runs (1), > (50-52), (54-63), (65-69), (99) match the region set > exactly, and MAS-57 reads as Mines2's trigger- > registration surface (look/use triggers on entities > NAME(-5801)-(-5817), a boxtrigger at (26,90) 5x2, > opening requests). The three mines masters are > instruction-identical 1.0 vs 1.10 after GF > renumbering (MAS-56: 111 ins, MAS-57: 173, MAS-58: > 140), so the freeze is not in the 1.10 delta. > MAS-57 inventory (first read): registers Mines2's > look/use triggers on entities NAME(-5801)-(-5817) > and entity ids 268/287/289, a 5x2 boxtrigger at > (26,90), opens with request 5, NAME(-5835/-5836); > the tail calls gpl global sub 228, 27 (cross- > script call), spawns entities with gpl clone > NAME(-74)/NAME(-137), moves a boxtrigger, and reads > GF+[604]/GF+[608] — the bracket form is a NEW > global-addressing mode for the disassembler's > documentation. The elevator's usetrigger is among > the registered triggers — and it is now NAMED: > gpl usetrigger 3753, 287, NAME(-5807) is the > elevator, because NAME(-5807) = OBJEX object 5807, > whose RDFF record's sprite ref (u16 at offset 4) is > BMP 951 — the rendered elevator shaft (visually > confirmed, scratch/spin-delta/obj951.png). The full > -58xx sprite map: 5801=402, 5802=405, 5803=637, > 5804=632, 5805=578, 5806=577, 5807=951 (elevator > shaft), 5808=952 (mine door), 5809=953, 5810=954, > 5811=955, 5812=956, 5813=957 (cave-in debris), > 5814=958, 5815=130. The site report's trigger is > named; the freeze hunt continues inside the > region-change machine it invokes. > MACHINE CORE READ (2026-09-05, fn+0x100-0x1b0): > after the sweep, the machine (1) marks the OLD > region's record consumed (reads the flag array at > +6, negates, writes 0xFFFF to [0x67cb] slot), (2) > scans the flag array for the next free 8-byte slot, > (3) calls 0xaf4 (a validator; on zero, prints > 'Error: Unable to close darkrun.gff' from DGROUP > 0x1739), (4) calls 0x58:0x4723 twice with (0) and > (1) — likely closing and reopening the save files, > (5) calls 0xd31 with the region id, then (6) calls > 0x5b0:0xc0 again with (1, 0, flags, region) — the > RELOCATION call that performs the actual move, whose > return value lands in [bp-2]. A failure AFTER the > darkrun close/reopen cycle — i.e. inside 0xd31 or > 0x5b0:0xc0 with the save files closed — is exactly > the shape of a level-load freeze. The elevator's > ride runs this same machine. > SUSPECTS READ (2026-09-05): (a) 0xd31 is the SAVE- > HEADER SNAPSHOT — twelve 32-bit stores of core state > (0x1596/0x159a/0x159e = the pointer trio from 0x70b66, > plus 0x19d1, and segment-0x2e8 cursor fields) into > DGROUP 0x1596-0x15fa, the save-header block; pure > copying, cannot hang. (b) 0x5b0:0xc0 is a REAL > relocation routine with error returns: it validates > (0x5f3:0x12e call), compares the region id against a > 14-byte-entry table at DGROUP 0x3eee (region id match + > a +1 check — fail = error 0xb via the 0x257 exit), > then proceeds to the move. THE SUSPECTS READ (2026-09-05): (a) 0x5f3:0x12e is a trivial > helper — a 32-bit pointer-arithmetic subroutine (add > + overflow check, retf in 12 bytes), not a validator; > its segment 0x5f3 is RESIDENT (file 0xB130). (b) The > 14-entry table at DGROUP 0x3eee is BSS zeros — > runtime-populated region-transition slots (the > relocation validates the region id against entries > filled during play). (c) 0xd31 is the save-header > snapshot (pure copying). The 0x5b0:0xc0 relocation > itself uses DOS int 21h AH=0x4200 (lseek) at its > core — file I/O on the save files. FREEZE SHAPE > final: the relocation does lseek/read/write on > DARKRUN.GFF with the handle state set up by the > close/reopen cycle; a hang there is file-I/O on a > handle opened against a missing/invalid region — > which the played-save pair will show directly (the > half-committed DARKRUN bytes at the freeze point). > > SUBAGENT RESULTS (2026-09-05, three agents returned): > > Region-entry array decoded (pointer cell 0x67b7, > 8-byte entries): +0=word x, +2=word y, +4=byte, > +5=flags byte (0x20=linked/active, 0x40=dirty), > +6=word owner-id (positive=object index into the > 37-byte array, negative=reserved). 17 accessors > classified: cursor/lookup, object spawn/activation, > claim/reassign, bbox/dirty-clear, dirty-tile > marking, render pass, mouse pick. The array is the > engine's visible-object table for the current region. > > Mines quest-flag map COMPLETE (GPL 76-79): GNUM[58] > bitfield (1=wheel, 2=car, 8=lock, 16=probe, 32=gas, > 128=Blink, 256=miner dead, 4096=cover-up); GNUM[59] > foreman stage; GNUM[60] zone id; GNUM[61] fan bits; > GF[125/126/189/191/193-198]. Every NAME(-N) object > identified: -74=gate, -2962=ore, -3146=iron gate, > -80=Zeegrat, -1300..1304=fans, -1315=gas, > -5043/5044=orecars, -3132=wheel. > > Elevator ride decoded: GPL-79, when GNUM[58]&128: > request 37 (operate), request 39 (move to 31,31), > tport party to (57,8,3) = region 57 = Mines2. The > gpl request semantics decoded: 5=activate, 9=set > state, 11=place, 37=operate elevator, 39=move > elevator, 49=set quantity. > > GPLDISK MODULE FULLY MAPPED (agent read of all ~53 > functions across ovr18's 0x2D5B bytes): > Archive system: the region-change data lives in a TAG > CONTAINER with 4-char tags (SAVE, GPLI, ETAB, RGTP, GAME, > DATA, PMB) and helpers at segments 0x110 (find), 0x118 > (seek/read), 0x128 (find-tag+offset, tag-size). The > region table is at 0x388:0x0C33 (3 bytes per region); > current region id at [0x60EB]. Key functions named: > 0x08E3=THE SAVE WRITER (tag SAVE, .SAV creation, member > iteration); 0x018F=region descriptor table builder; > 0x039A=core region loader; 0x1615/0x1829=GPLI directory > readers; 0x1BE7=lazy archive open; 0x1E63=region-change > ORCHESTRATOR (separate from gpl_disk_change_region); > 0x28B3=screen-mode toggle during save; 0x2BAC=save-slot > name builder. The 'Bad iCtrl' string has NO reference > inside ovr18. The archive is a flat tag container, not > a GFF. > > SECOND-AGENT FUNCTION TABLE (boundary-scanned, the > definitive version): > | seg-local | file | function | > |---|---|---| > | 0x08E3 | 0x70713 | SAVE WRITER (tag SAVE, STXT record) | > | 0x0A6C | 0x7089C | save-record writer (NOT the loader!) | > | 0x0D1A | 0x70B4A | snapshot collector | > | 0x0DCD | 0x70BFD | save orchestrator (disk space, SAVE%.2d.SAV) | > | 0x0EBE | 0x70CEE | load orchestrator (calls 0x0A6C) | > | 0x0FB7 | 0x70DE7 | region-change record creator | > | 0x1132 | 0x70F62 | gpl_disk_change_region (named) | > | 0x135B | 0x7118B | per-character RTGP record writer | > | 0x146C | 0x7129C | load_teleport (named) | > > CRITICAL: 0x6874 = 0x67BB + 50x25. The 37-byte record > array at DS:0x6874 is the NPC PORTION of a party-record > array based at DGROUP:0x67BB (records 0-4 = party, 5-0x13F > = NPCs). gpl_disk_change_region calls 0x70DE7 (create > party records) -> 0x70713 (rewrite SAVE chunks) -> > load_game_from_disk(region). > > CORRECTION from the gap read: the code at 0x0A6C WRITES > SAVE-tagged records (it is the save writer, not the > loader). The syms catalogue's "load_game_from_disk" name > at ovr18+0x0A6C is WRONG — the function saves, it does > not load. The actual load path is elsewhere (possibly the > 0x0000-0x0A6C range, or a different segment entirely). > The "Failed Uncompress in Loadgamefromdisk" string is the > MODULE's error message, not a function name. The syms row > needs correction. > > TPORT DESTINATIONS RESOLVED (third agent): all 13 > unique NAME(-N) tport targets in GPL-81 are NPCs and > MONSTERS, not regions. The tports teleport the named > NPC/monster OBJECT into Limbo (off-map): Melody(-74), > Wren(-75), Mug(-77), Miners(-78/-79/-116/-117/-119), > Zeegrat(-80), Winchester(-187), Umber Hulk(-405), > Mindflayer(-416), Intellect Devourer(-526). Every RDFF > record carries its own id negated as i16 at offset 14. > The 32766-prefix tports teleport the PARTY to explicit > region/coordinates (mostly Mines1=56). CONSEQUENCE: the > party elevator ride is NOT in the NAME(-N) tports — it > goes through the 32766 coordinate tports or the > GNAME[38] runtime target in GPL-80. > CLOSURE (same session): the ovr18 trio SERIALIZES > the flag array into save files — 0x70b66 snapshots > three core pointers (0x19c1, 0x67b7, 0x55b8) into > DGROUP 0x1596-0x159e (the save header), 0x709b8 > walks the array's 8-byte entries (+8 per iteration, > the save writer), and 0x71099 (inside > gpl_disk_change_region itself) walks it > post-validation. CONSEQUENCE: the region-entry flags > persist in DARKRUN/SAVE0N files, so the > played-save pair + save-semantic-diff WILL capture > the elevator's flag changes — the runtime-capture > instrument is the save diff after all, no debugger > required.

  • Decode* dispatch-order study. .dso-online's symbols.txt names 115 Decode* GPL handlers in a contiguous, address-ordered block, and ~114/115 agree with libgff's independently derived opcode names. Sort the block, align it against the 129-opcode table, and pin the unknown bytes (0x53, 0x55-0x57, 0x60, 0x71-0x75) by elimination; record the DecodeIfis (an 0x27 semantics hint), DecodeWend == DecodeJump, and DecodeNumtoname == DecodeNametonum alias facts. Names and addresses are facts (cite the AGPL table); no code moves. (Done 2026-09-04, with one premise corrected; full write-up in docs/dso-symbols.md's Decode* section. Verified: 111/115 names match libgff case-insensitively plus the systematic *check/*trigger rename for the 13 trigger handlers; alias facts confirmed from shared addresses (DecodeWend+DecodeJump at 0x3bb55, DecodeNumtoname+DecodeNametonum at 0x3c121); DecodeIfis sits between Compare and Orelse as the 0x27 handler. Accounting is exact: every non-default libgff byte has exactly one DSO handler name. Corrected: the block is NOT opcode-address-ordered (opens 0x23 0x4b 0x15 0x19; only 56/108 adjacent pairs are consecutive), so the unknown bytes CANNOT be pinned by elimination; no spare DSO names exist. Next pinning route: the DSUN.EXE dispatch table or the DSO client's ExecuteGpl jump table.)

  • Locate the engine subsystems. Combat, party, map, inventory, and the save path, via string anchors plus the callgraph. The DS1 save-string cluster at 0x49d3d-0x49e92 is the seeded start; the DSO table names the persistence family outright (SaveGameToDisk/LoadGameFromDisk and kin). (SUBSTANTIALLY DONE 2026-09-04/05: save/region module fully mapped (gpldisk.c = ovr18, 53 functions); dispatch system = ovr21 (17 far-callback sites) + ovr35 (command orchestrator) + ovr42 (periodic updater); the GPL VM dispatch table resolved for both games; mines quest-flag map complete (GNUM[58] bitfield, 20+ GF flags, every NAME(-N) object identified); audio (MEL/DJ init DS2 ovr11+0x26); item lookup (DS2 ovr35+0x2327); elevator object and trigger named. Combat logic, party management, and map/region data routines are string-anchored through the census but not individually named.)

  • Read the overlay manager and the dispatchers. The manager bodies (0x466e0 / 0x4aff0) bound how many segments stay resident (directly relevant to the elevator race); the indirect-call dispatcher segments (DS1 25; DS2 21/35/42) hold the concentrated FF /2 sites whose resolution turns them into ordinary callgraph edges. > (DONE 2026-09-05: three agents read the dispatcher > segments. ovr21 = the per-object event/callback pump > (17 far-callback sites, two global record arrays at > [0x19c5] stride 0x42 and [0x19c9] stride 0x31); > ovr35 = a switch-driven command orchestrator relaying > into segments 0xc8/0x668/0x1d0; ovr42 = a periodic > updater over the same record arrays with a single > indirect call. The overlay manager body (ovr18+0x1132) > is fully read as the region-change machine. DS2's > overlay manager (file 0x4aff0) still wants a dedicated > read, but the INT 3Fh stub mechanism is documented in > dsun-exe-re.md and the ovr-map tool parses it.)

  • Locate per-segment relocation tables; reconcile the FBOV segnum. The descriptors imply relocations the survey has not found; reloc-safe EXE patching (Phase 5.7) needs them. Reconcile FBOV's segnum field (220 / 229) against the parsed descriptor records (58 / 49): either a second segment class exists or a field is misread.

5.6.2 — Formats and saves

  > (DONE 2026-09-05: the FBOV segnum field (220 DS1 / 229
  > DS2) is NOT the overlay segment count — it is the
  > record count of the resident-image symbol/debug table
  > at `exeinfo` (Borland Turbo Debugger entries), termi-
  > nated by the build filename. The exeinfo table fits
  > exactly: exeinfo + segnum*8 = the filename offset. The
  > per-segment relocation tables are BSS (runtime-populated
  > by the overlay loader), not static data. Both findings
  > close the box. The descriptor chain walk (ovr-map's
  > approach) remains the only way to enumerate overlay
  > segments.
  >
  > CORRECTED 2026-09-19, superseded by
  > docs/overlay-formats.md: `exeinfo` is the SEGMENT LOAD
  > TABLE (word0 = load paragraph, word1 = byte size,
  > word2 = class 0/1/3/4, one MZ relocation per record),
  > not a symbol table; and the per-module relocation
  > tables ARE static file data immediately after each
  > module's code (8,232 DS1 / 8,262 DS2 word entries,
  > validated on all 101 modules). The 4,853/4,703 figures
  > are the MZ resident-image relocation counts, a
  > different table.)
  • Decode SAVE/1 (the ~10 KB probable master state table) against libgff's object/region structs, seeded by the gpldisk.c string anchors and the DSO Save*/Load* names. (Structurally decoded 2026-09-06: SAVE/1 is the party/NPC actor record array from DGROUP:0x67bb dumped whole - DS1 320x32, DS2 320x37 (the exact 0x25 stride); populated slots = the party, unfilled = 0xFF; word +1 = the actor's index into the visible-object array, which is SAVE/7 = 1050x8 in both games. Arbitrates the dossier's competing readings: the di=5..319 sweep walks actor slots, not region ids. Per-field map inside a record = the card-A pairs' work. engine-quirks entry 12 carries the decode; save-fields.toml carries the rows.)
  • Chunk-map played saves via the save-diff loop (snapshot, one in-game action, snapshot) for each SAVE id family, converting the speculation rows in save-inspect's README into per-id semantics. Needs play sessions (Brandon's); turnkey recipe: docs/cookbook/capture-sessions.md card A.
  • Settle save compression. Locate the Failed Uncompress in Loadgamefromdisk caller. Today every on-disk save parses as a plain GFF and the survey string says compression exists somewhere; which is true under all engine paths decides whether save diffing can be trusted. (RESOLVED 2026-09-05: the caller is the GPLI directory readers at ovr18+0x1615/0x1829 — they read the GPLI tag's directory (size/6 = entries) and push the error on allocation failure, NOT on compression failure. The 'Failed Uncompress' string is the module's error message for "could not read the GPLI index", not a compression error. On-disk saves ARE plain GFFs — no compression layer exists in the DS2 save path. The save-diff loop is trustworthy as-is.)
  • Run the opcode-fuzz recipe loop to first discovery. Phase 5's done-when (discover one previously-unknown opcode) is still open; settle the recipe format and meet it. The Decode* study above narrows the candidates first.
  • Adopt the reference catalogues into the docs. libgff's gfftypes.h defines 83 chunk types against our catalogue's gaps (BVOC/FVOC/OMAP/POBJ/SJMP/FNFO/ RDAT/CACT/STXT at minimum); correct the free-list prose (all 61 measured files carry toc_length − 2 plus a 2-byte count, not an empty list at toc_length; a writer following the current prose produces malformed files); pin file_flags (8 on all DS2 regions and both CHARSAVEs) and the data0 ordinals; fix dso-symbols.md's prefix census (Save* 12 not 24, Combat* 6 not 11, Region* 0 not 4, among others); and record the upstream-projects.md license drift (libsoloscuro ships no LICENSE file).

5.6.3 — The bug-site census

  (ALL SUB-ITEMS DONE 2026-09-04/05: free-list corrected
  and documented; file_flags pinned (0/8 pattern, all 20
  DS2 regions); data0 ordinals pinned (sequential 3..22);
  prefix census corrected; upstream license drift recorded;
  RNME chunk documented as a bonus. Remaining chunk-kind
  gaps are in docs/format-coverage.md.)
  • Stand up the census. A table in or beside docs/known-bugs.md: per bug, site located / site named / root cause / evidence chain. Today every row starts at no; the table makes the distance to "patchable" visible and is the checklist the site- report rule consumes. (Live 2026-09-04 at known-bugs.md 3a. First content: the headline mines-elevator bug's transition module is anchored in BOTH engines — DS2 ovr18+0x1132 / ovr18+0x146c catalogued today, confirmed entry stubs in the same segment as DS2's LoadGameFromDisk, mirroring DS1's ovr21 module layout — and the save-path rows carry verified named anchors. No bug has a root cause yet; that is the gap the table tracks.)
  • Read SSI's own fixes. Characterize the low-cluster diff segments (0, 5, 8, 9, 16, 36) instruction-by-instruction against the 1.02 fix list (known-bugs.md §1). The diffing checkbox above covers the tooling; this is the reading, and SSI's fix sites are the only ground truth for what an engine-code fix looks like in this codebase. (Ticked 2026-09-06: done-but-unticked; the reading completed 2026-09-04/05 with the verdict in the notes below: the EXE delta is rebias-only and SSI's behavioral fixes are data-side.) > READ 2026-09-04, with a strategy-changing verdict: > the low-cluster segments contain no behavioral > fixes. All 183 clusters across segments 0, 5, 8, 9, > 16 and 36 were classified by normalized-instruction > comparison (decode both sides from their nearest > entry stub, compare with immediates normalized): > 179 are pure data-address rebiasing — SSI's 1.10 > recompile shifted DGROUP/scratch structures (e.g. > segment 0's es:0x4034 -> es:0x40c0, +0x8c; seg 36 > is ONE byte changing a mul operand), and the > surrounding code is byte-identical. The 4 remaining > clusters (seg 5 x3, seg 9 x1) are instruction-boundary > desyncs at shifted addresses, not edits; seg 16's > famous pair is the loader pointer repoint. So the > behavioral 1.02 fixes live in the 25 REBUILT segments > (the UNVERIFIED-pairing class) — reading them needs > function-level pairing via the identical-signature > anchor segments, which is exactly what the promoted > differ's signature pairing was built for. This > retroactively recontextualizes survey 8's 'fix-sized > edit' reading of segment 0. > REDIRECT 2, same day, the bigger one: the verified > EXE pairs contain ZERO semantic changes (all 120 > differing functions across 24 verified pairs are > address rebias; 85 are byte-identical) — consistent > with known-bugs §1's own note that the 1.02 fixes > are GPL-script-driven. And the GPLDATA.GFF delta > proves it: CD 1.0 vs GOG 1.10 GPLDATA is the same > size (2,191,945 B, in-place edits) with 2,066,888 > differing bytes — a re-emitted tail plus a band of > small early clusters that are ±1 script-id shifts > (5d 16 8f -> 5e 16 8f: local-label renumbering > around source edits) and a literal 1.10 version > stamp at 0x214182. SSI's 1.02 fixes are GPL script > edits, and the diff against CD 1.0 isolates them. > COMPLETED 2026-09-04 — the full SSI fix delta, all > three changed files, mapped with our tools: > (1) GPLDATA: all 350 chunk disassemblies diffed; > after normalizing global-flag renumbering, ZERO > behavioral script changes — the script band +-1 > shifts are GF-id renumbering from inserted globals. > (2) RESOURCE: the fixes ARE here — BMP/CBMP 11001 + > 11002 shrank ~30% (fix 8: the volcano overhead maps, > redrawn), SPIN spell-text ids 94-114 grew from > 8-byte placeholders to real text (fix 12 family), > ICON 19115-19121 swapped (spell icons). (3) DSUN.EXE: > behavior-free recompile rebias. Verdict: SSI fixed > 1.02 via DATA (maps, spell text) — the script logic > fixes in the 1.02 list were apparently delivered in > the 1.02 build itself (GOG's 1.10 base differs from > CD 1.0 only in data + layout), or were engine-side in > the rebuilt EXE segments. Either way the delta is > now fully characterized, every changed chunk named, > and readable with image-extract/gff-cat. > READ 2026-09-05 (late session): the 20 SPIN entries > are high-level spell names/descriptions 1.0 left as > the placeholder "fooey!" — 1.10 filled them with > Incendiary Cloud, Charm Person, Mind Blank, Monster > Summoning VI/VII, Oteluke's Telekinetic Sphere, > Otto's Irresistible Dance, Power Word Blind, Prismatic > Wall/Sphere, Serten's Spell Immunity, Crystal Brittle, > Level Drain, Meteor Swarm, Mordenkainen's Disjunction, > Power Word Kill, Time Stop, Dome of Invulnerability, > Magical Plague, Rift. Easter egg: SPIN 103 contains a > shipped BUILD COMMAND — "copy c:foo.bat > ..\RES\text\BIGBYFST.spn" — SSI's packaging script > leaked into the spell-text table. Full extraction in > scratch/spin-delta/ (dev-side). > Elevator ride routing (same session): GPL-80's > GNAME[38] tport is BLICK's exit (noorderstrigger on > him, then 'Blick squeezes into the narrow opening'), > not the party ride. The party ride is engine-routed: > GPL-85 confirms the elevator is unusable until Blick > is found ('You're not going to get down to the lower > level without his help'), and the ride itself goes > through the engine's load-teleport path — the > anchored load_teleport (DS2 ovr18+0x146c) and the > region-change machine (ovr18+0x1132). The freeze > therefore happens inside the anchored module for the > lower-mine-level load. Site report is one > runtime-capture (or lower-level RGN read) from > complete.
  • Locate the mines-elevator transition. The region-transition state machine (GPL side, EXE side, or both) is Phase 7's site report; the investigation lives here so Phase 7 starts from a site instead of a dig. The DSO candidates (GplChangeRegion, GplTileCheck, GplDoorCheck) and the official-patch diff are the first two levers. One runtime capture from complete; turnkey recipes (save-diff route and debugger watchpoint route): docs/cookbook/capture-sessions.md card B.

Done when: a reader can ask "what is at ovr:NN+0x..." and get a name with evidence for a meaningful fraction of the catalogue (target: the ~200 most-called functions); at least one downstream item (animated palette, or a Phase 6/7 site) cites it; the coverage report and the census table exist and are being consumed; and the first Phase 6/7 target bug has a complete site report produced by this phase rather than scrounged at fix time.

Phase 5.7 — EXE patch authoring surface

Goal: give DSUN.EXE byte patches exactly what gpl-asm v0.9.0 gave bytecode patches: authored by name, fingerprint- checked, verified before it ships.

Ships: named addressing and a verify gate for EXE patches, in whatever tool owns the job (decide: extend gpl-asm --patch with a second target kind, or a sibling exe-patch; decide by whether the TOML schema can stay shared).

DECIDED + SHIPPED 2026-09-06: a sibling Python tool, tools/exe-patch v0.1.0. The script schema stays shape-shared with gpl-asm --patch ([[edit]] with at / bytes_old / bytes_new / reason), but the resolvers differ (overlay segment map vs chunk block leaders), and a Rust port of ovr-map's FBOV descriptor walk would duplicate the one implementation that must never drift. exe-patch consumes ovr-map --json instead (the same subprocess-JSON contract the other Python tools use), and keeps authoring on the Python side where the applier and per-fix scripts already live (spec §7a).

Ordering note (2026-09-04): the authoring tooling below may be built in parallel with Phase 5.6's tooling half, but an EXE-surface fix needs both halves of that phase: the catalogue (for symbol-relative addressing) and the target bug's site report (5.6.3). Data-surface fixes do not wait on either.

  • at = "ovr:19+0x17a7" addressing, resolved against the ovr-map segment map (file offset, segment-local offset, payload bounds). Symbol-relative addressing (at = "<exe-symbol> + N") once Phase 5.6's catalogue exists; refuse non-block-leader bases, matching the bytecode resolver's rule. (Shipped 2026-09-06 in exe-patch v0.1.0: ovr:SEG+OFF, bare file offsets, and syms names + N; ambiguous names are a hard error naming candidates. Catalogue rows are the block-leader address class; raw offsets stay legal like the bytecode side's at_offset, gated by the fingerprint.)
  • bytes_old fingerprint mandatory, as on the bytecode side; refuse to apply on mismatch. (Shipped 2026-09-06: missing bytes_old is a schema error; a mismatch is per-edit drift with expected/found hex; validation is all-or-nothing before any write.)
  • A --verify pass that fails a site which: drifts from its fingerprint, straddles a segment boundary, or lands in the ~7% inter-segment padding. Today nothing notices any of these. (Shipped 2026-09-06: --verify runs the full gate and writes nothing; --patch runs the identical checks before writing -o. Also refused: FBOV-header sites, runs past EOF, and overlapping edits.)
  • In-place-only rule made enforceable and explained. Every overlay descriptor stores its payload offset as an absolute position; one inserted byte anywhere before the last segment shifts every following payload and the game loads garbage as code. State this in spec.md §4 in those terms (the previous roadmap's own suggestion, still undone), and have --verify reject any script that changes file length. (Shipped 2026-09-06: the explanation lived in spec §3.2 already; spec now also names the enforcing tool, and a length-changing bytes_new is a hard error quoting the reason, with the output length re-asserted before write.)
  • Assembler for replacement bytes: keystone-engine is already named in docs/build-environment.md §2 for exactly this (16-bit x86). Confirm it assembles arch=i386, mode=16 correctly against ndisasm -b 16 round-trips before relying on it; pwn asm at arch='i386', bits=16 is the documented fallback. The stdlib-only Python rule already has a pre-approved exception class for the applier (per build-environment.md, alongside bsdiff4); this fits it. (Shipped 2026-09-06, with a correction: the documented fallback was confirmed FIRST and FAILED: pwntools 4.15 rejects the i386/16 combination outright, so pwn asm cannot serve. exe-patch --asm shells to nasm -f bin + bits 16, the assembler half of the toolchain ovr-map --disasm already trusts, round-trip-proven against ndisasm -b 16 in the selftest. Correction recorded in docs/re-tooling.md. The keystone question closed 2026-09-06: nasm is the assembler path; keystone is not adopted (see the tooling inventory).)
  • Round-trip proof: a no-op EXE patch script applies and unapplies byte-identically; a deliberately wrong site (off-by-one into padding) is rejected by --verify. (Shipped 2026-09-06 in --selftest: on a synthetic Borland-overlaid fixture (parsed by the real ovr-map) and then on the real binaries when .games/ is present: no-op applies byte-identically, edit+inverse restores exactly, and an off-by-one past a payload end is rejected as padding. Also proven live: a game = "ds1" script hash-gates against the canonical GOG 1.10 manifest.)

Done when: Phase 6+ can author a two-byte EXE fix by symbol name and --verify is the last gate before packaging. This is the same soft-blocker shape as gpl-asm v0.9.0 was for bytecode: not a hard blocker for a data-only first fix, and must not hold one.

Phase 6 — First DS1 fix shipped (pipeline proof)

Goal: prove the patch pipeline end-to-end on the smallest possible DS1 bug. By this point the toolkit is sharp enough that authoring should feel like routine work.

Ships: darkfix-ds1-v0.1.0.

  • Darkfix distribution format per spec.md §4: manifest.toml schema (target hashes, fix list, on/off state), apply.py applier (verify install hashes against the manifest, back up to darkfix-backup/, apply each enabled fix, write darkfix-applied.json), and apply.py --unapply restore. Proven with the fix.ds1.noop fix: applies and unapplies byte-identically against a copy of the real DSUN.EXE; wrong-fingerprint and tampered-target refusals covered by apply.py --selftest (2026-08-28).
  • Applier runs on the player platform. The audience is Windows-first. Test apply.py under Wine and, ideally, real Windows Python before calling the package shape proven; a fix that only applies on Fedora is not a fix. Decide the dependency stance in the same breath: pure stdlib (preferred, matches spec §7a) versus the bsdiff4/keystone venv build-environment.md sketches. > PROVEN HALF 2026-09-06 (Wine, agent-side): the full > apply -> --status -> --unapply cycle ran under > wine-11.0 (Staging) with Windows CPython 3.12.10 > (embeddable) against a SCRATCH copy of the real DS1 > DSUN.EXE: the no-op fix applied, the journal wrote, > unapply restored the file byte-identically. The > applier needed zero changes; it is pure stdlib. The > scratch rig lives in scratch/wine-applier-proof/ > (gitignored; the embeddable Python zip is re- > downloadable). Still open, Brandon's side: one run on a real > Windows. (The dependency stance closed 2026-09-06: > nasm, no keystone.)
  • Pick one trivial DS1 bug (identified during Phase 2 repro work). Prefer a GPL-data fix if one is available: it exercises gpl-asm --patch + gff-edit and defers the EXE surface until Phase 5.7 exists. Prefer, second, a bug whose site the Phase 5.6.3 census has already characterized, so the fix proves the pipeline instead of paying the archaeology tax. (PICKED 2026-09-11: candidate A, the dead-trigger stubbed-actor handler; full provenance in the note below. Brandon's nod landed pre-blitz; the fix is GPL-data and shipped in the same pass.) > Shortlist progress 2026-09-06: sweep (1) EXECUTED as > tools/gpl-disasm/scripts/global-state-sweep.py > (gpl-disasm 0.7.0), validated by re-measuring DS2's > railhead family. DS1 results: 35 written-never-read > globals; the standout is GF[395], written 19 times > (mostly set 1 on region entry) by 13 different region > masters via the GF+[..] form and never read anywhere: > a world flag whose consumer is missing from the shipped > scripts. Next candidates: GF[80] (3 writes, GPL-11), > GNUM[19] (3 writes, GPL-8), GNUM[64] (GPL-76). Caveat: > the flag is read for nothing, but a FIX still needs a > player-visible symptom to attach to (correlate with > DS1 community reports when picking). Sweep (2) resolved > structurally empty: DS1 shipped only 1.10, so the > pre-fix placeholder lineage that hit DS2 1.0 cannot > exist there (raw scans: zero placeholder hits). Sweep > (3) EXECUTED (gpl-disasm 0.8.0, > scripts/dead-trigger-sweep.py): DS1 has 1449 entity > trigger registrations; 1279 alive, 164 intentional > null-handlers, 6 DEAD - five looktriggers plus one > ATTACKTRIGGER (objects NAME(-255), -2263, -1209, > -2248; registered by GPL-195/203/41) all pointing at > GPL-200 entry 0x909, which holds only gpl exit gpl: > a stubbed actor handler, i.e. a monster that cannot > fight and has no look text. That is the concrete > instance of the community's "enemies refuse to engage" > class and the leading data-surface Phase 6 pick, > pending symptom correlation. DS2: 30 dead triggers

(GPL-24@0x1 looktriggers and siblings). Sweep (1) also measured DS2 precisely: the railhead flags are NOT literally unread (the look handler prints their state); they are read only inside their own chunk, which engine-quirks entry 6 now reflects. CORRELATED 2026-09-10 (agent-side dig, decision evidence for the pick): all six dead triggers are Darkhold endgame, main-quest, normal-playthrough content. Regions: 30 (RGN1E; objects -255, -2263, -1209; the portcullis/queen's-chamber cluster) and 31 (RGN1F; -2248, the wyvern scene). GPL-200@0x909 is a single orphan exit gpl byte that is not a discovered entry, targeted identically from three independently written chunks: a deliberately emptied handler. The picture is richer than "no handler": -2263/-1209 carry ALIVE looktrigger registrations at GPL-203@0x378/0x380 and then dead re-registrations at 0x3fe/0x406 (so the stub either clobbers working look text or is a no-op, per the engine's first/last-wins rule, an EXE-side question); -255 fights early via an alive attacktrigger (MAS-30 -> GPL-200@0x33b) and its later re-registration (GPL-195@0x2b, story-gated) lands in the stub; two of the six rows pass GNAME[39], a runtime-variable object, so a fix must cover the handler or the registering instructions, not just the four static ids. Fix shapes, in preference order: repoint/remove the five dead registering instructions (all six rows), or restore handler content at 0x909. Also rode along: file-formats.md's ETAB ojff_number row now records the negative encoding (measured on RGN1E/RGN1F). PICKED 2026-09-11 (Brandon, pre-blitz): candidate A, the dead-trigger stubbed-actor handler. Decision provenance: recommended here over GF[395] because Phase 6's own preference order (data surface, census- characterized site, player-visible symptom) favors it, and the 2026-09-10 correlation dig supplied the symptom evidence; GF[395] has no attachable symptom. Count correction from the sweep output: SIX dead registering instructions, not five (GPL-41@0x1d, GPL-195@0x2b, GPL-203@0x3a2/0x3fe/0x406/0x40e). The shipped fix repoints the four STATIC rows to each object's own working handler (semantics-proof: a no-op if the engine keeps first registrations, a restoration if it keeps last) and leaves the two GNAME[39] rows untouched: their object is a runtime variable with no statically provable correct handler, and their stub is already inert. Fix = fix.ds1.deadtriggers (the author box below).

  • Repro fixture for the chosen bug (tools/repro/bugs/<id>/bug.toml) so the fix is verifiable. Requires ydotool installed locally; repro already integrates the input automation. (Framework note 2026-09-06: the fixture FORM is established by the shipped ds1-smoke/ds2-smoke fixtures plus repro v0.5.0's --diff; the actual fixture for the chosen bug lands the day the bug is picked and cannot precede it.) (Shipped 2026-09-11: tools/repro/bugs/ds1-deadtriggers/, the harness-level leg of the deadtriggers proof; --diff stages the patched GPLDATA.GFF and both boots pass with identical DARKRUN fingerprints. The scene leg (look at the queen's-chamber creatures, engage the portcullis guard post-flee) needs a played DARKSAVE.GFF from the Darkhold endgame and a keystroke load-save schedule; the fixture documents the promotion path and rides the played-save gate (roadmap 5.6.2's played-save chunk-map box).)
  • Differential capture (promoted from the repro backlog, because it is the fix's proof): a run-with-patch and run-without-patch side-by-side helper, emitting both videos plus a structured pass/fail delta. (Shipped 2026-09-06 in repro v0.5.0 as --diff: one invocation runs the fixture baseline vs patched (a patch directory staged over the overlay after setup), both video-recorded, and writes diff-report.json (per-run verdict + DARKRUN.GFF sha256 + sentinel listing, plus the delta block). Exit codes are verdict-shaped; both scratch trees are retained. The DOSBox-free parts carry a --selftest.)
  • Author the fix using gpl-disasm + gff-edit (plus the Phase 5.7 surface if it turns out to be an EXE fix). (Shipped 2026-09-11 as fix.ds1.deadtriggers, darkfix-ds1 0.1.0: GPL-data surface as the pick box preferred. Authoring went extract (gff-edit) -> label-anchored gpl-asm --patch (fingerprint-checked per chunk) -> reinsert (gff-cat replace); the shipped EDITS are those verified bytes at absolute GPLDATA.GFF offsets, so the applier needs no tools at apply time. Eleven bytes across three chunks (GPL-41, GPL-195, GPL-203); chunk lengths untouched. The four static rows repoint to each object's own working handler; the two GNAME[39] rows stay (see the pick note). Re-disassembly of the patched file: 250/250 chunks aligned; the dead-trigger sweep reports only the two GNAME[39] rows, down from six. No EXE surface touched, so the Phase 5.7 trio (ovr-map --verify / exe-patch) does not apply here; the agreement proof for this surface is gpl-disasm re-read + the gpl-asm/gff-cat round-trip + repro --diff.)
  • Author the test (hash before/after, in-game repro via tools/repro/). (Shipped 2026-09-11: apply.py --selftest gains a real-install cycle that applies the shipped fix set to copies of every [target.files] entry, asserts the patched GPLDATA.GFF hash recorded in fixes/001-deadtriggers.md (any EDITS drift fails), verifies, and unapplies byte-identically; skips when .games/ is absent. Hash test 5.1 = the recorded hash e6b163bd...; in-game repro = the ds1-deadtriggers fixture's --diff capture (both legs PASS, DARKRUN world-state fingerprints identical, no sentinel delta: the scene-level behavioral proof rides the played-save gate per the fixture box).)
  • Tag darkfix-ds1-v0.1.0, push GitHub release. (REQUESTED 2026-09-11: the fix is proven and the verbatim message file is prepared at scratch/tags/ darkfix-ds1-v0.1.0.txt, anchor commit 975bc39; cut via the standard procedure on Brandon's go. SHIPPED 2026-09-11 on his go: tag cut verbatim against 975bc39, pushed, GitHub Release created; the tag sweep reads 15/15 proper title lines. Phase 6's remaining open box is only the real-Windows half of the applier box.)
  • Player-facing README explaining install. (Rewritten 2026-09-06 in ds1-patch/README.md: Windows-first steps with the py launcher, Linux/macOS equivalent, what gets backed up, how to revert/verify, and the hash-mismatch refusal explained; plus the platform-proof note. Rides the v0.1.0 release.)
  • Cookbook entry: docs/cookbook/author-first-darkfix.md, the workflow written down while it's fresh. (Skeleton shipped 2026-09-06: all eight pipeline steps written with the real tool commands, worked examples marked pending to fill during the first real fix. Examples filled 2026-09-11 from fix.ds1.deadtriggers: each step now carries the real commands, outputs, and the honesty notes (prove the leg you can; say which half a capture proves).)

Done when: a stranger could download the v0.1 zip, run apply.py, launch DS1 in DOSBox, and the bug is gone.

Phase 7 — DS2 mines elevator (the headline)

Goal: fix the most famous DS2 bug, the one that broke the late game in 1994 and has never been fixed.

Ships: darkfix-ds2-v0.1.0.

  • DS2 active-party edit surface, verified end-to-end: confirm in a loaded game that CHARSAVE edits via save-inspect cover the DS2 active party (the v0.6.0 finding says DS2 CHARSAVE is the active party, but this has only been exercised via repro.py --play session saves, never a live install). If DS2 turns out to have a DARKRUN-side layout like DS1's SAVE/5-/6, RE it and build the sibling tooling. Cookbook entry mirroring edit-ds1-party.md either way; darkfix-ds2 authoring needs this fluency.
  • Reproduce in DOSBox via tools/repro/.
  • Locate the GPL function or DSUN.EXE routine controlling the elevator transition. New leverage since this phase was written: Phase 5.6's official-patch diff may show whether SSI themselves touched the transition code between 1.0 and 1.10, and the DSO symbol table likely names the region-transition functions outright (DSO inherited the whole WotR codebase). Use both before hand-digging.
  • Diagnose the race / state bug.
  • Author the fix (data or binary, whichever it lives in).
  • Verify a full DS2 playthrough does not reproduce the original behavior.

Done when: a player who hits the elevator gets to the next region, with a full party, on a clean install with the patch applied.

Phase 8 — DS2 sweep

Goal: every bug in docs/known-bugs.md section 2 (community-reported, post-1.10) has either a fix or an explicit "won't fix" note with rationale.

Ships: darkfix-ds2-v0.5.0.

  • Charged-weapon disappearance.
  • Doorway / item graphics layering.
  • Save/exit bug.
  • Audio static (verify no-op for OPL/MT-32 emulation paths). (VERDICT 2026-09-10, won't fix / no-op under the GOG install: the static is a redbook mastering defect on some pressings' tracks 2-3, reproducible outside the game and documented on real hardware in 1996 (VOGONS t=9726 / t=10893); GOG's OGG re-encodes bypass the defective masters. The box's OPL/MT-32 framing was a category error: the DS2 CD line has no MIDI music, so those synthesis paths are never in the chain. known-bugs.md 2.5 rewritten with the corrected origin (the old "AIL driver mismatch" note was wrong). Residual: one in-game ear test, which rides the Phase 10 playthrough.)
  • MEL DSP detect (verify no-op for DOSBox). (VERDICT 2026-09-10, won't fix / config, not engine: community-confirmed IRQ mismatch; the game's SOUND.INI defaults the SB Pro II/III and SB16-class cards to IRQ 5, stock DOSBox defaults 7, and AIL's probe verifies the IRQ with a test interrupt, so a mismatch aborts startup with the MEL fatal. The GOG conf pins irq=5, verified against the real file, so the shipped path cannot trigger it; a hand-rolled IRQ-7 conf still can, which is exactly how the VOGONS reports happened. known-bugs.md 2.6 carries the full rationale. Residual: one in-game launch confirm, rides the Phase 10 playthrough.)

Phase 9 — DS1 sweep

Goal: same as Phase 8, for DS1's known issues.

Ships: darkfix-ds1-v0.5.0.

  • Compile a more thorough DS1 bug list (DS1 is less documented; we will find issues during this phase). (COMPILED 2026-09-10: known-bugs.md §3 now carries the compiled catalogue, ~25 entries grouped into the final-battle trigger family (the headline DS1 game-breaker, six variants, 2011-2025 reports), quest/NPC scripting, engine-level items (area item limit, ghost inventory slot, save-mid-event object loss, stat-boost asymmetry), and minor rule deviations; every entry has sources and a surface guess, and the dead-trigger finding is cross-referenced as the first root-cause candidate for the enemies-refuse-to-engage class. Dead ends recorded for the next pass: powelltown unarchived, Reddit bodies unfetchable, athas.org unreachable, no public DS1 1.1 fix list. One install-variants lead rode along: an unverified GameFAQs claim of separate CD and floppy 1.1 variants.)
  • Fix each.

Phase 10 — v1.0 for both games

Goal: the patches reach a state where they can be recommended to fellow Dark Sun players in good conscience.

Ships: darkfix-ds1-v1.0.0 and darkfix-ds2-v1.0.0.

  • Full playthrough of DS1 with the patch on; no workaround needed.
  • Full playthrough of DS2 with the patch on; no workaround needed.
  • Player-facing documentation: how to install, how to verify, how to report a bug.
  • Public announcement.

Phase 11+ — Engine plausibility (deferred)

If the toolkit accumulates enough, gpl-disasm with most opcodes documented, working gpl-asm, native GFF read/write, region viewer, save inspector, then OpenDS the engine becomes plumbing rather than reverse-engineering. At that point spinning it up makes sense.

We do not commit to a date. We commit to building the toolkit that makes it possible. If someone else picks up the toolkit and ships an engine first, that is a successful outcome.

Backlog (deferred items with triggers)

(RATIFIED 2026-09-12 (Brandon): the 2026-09-10 disposition drafts stand as drafted: six keep-deferred with their triggers, the repro save-curation folds into the shipped bug pick, opcode-fuzz reframes to JSON recipes, and extract.sh stays deferred (its deletion declined).)

Everything still open from the shipped phases, each with the condition that promotes it into scheduled work. Nothing here is abandoned; nothing here is scheduled.

  • Cut the per-tool git tags, or drop the requirement. docs/versioning.md says each release ships a <tool>-vX.Y.Z tag; git tag returns nothing, so no release has ever complied. Two honest resolutions: backfill tags against the commits that bumped each VERSION (a one-off script walking git log -- VERSION makes this mechanical), or amend docs/versioning.md to name VERSION + patchnotes.md as the whole release record and drop tags. Open since the 2026-07-23 reconciliation sweep. Decide before the first darkfix release, so patches do not inherit the ambiguity. (Decided 2026-09-04: tags are cut, forward-only. First two: ovr-map-v0.3.0 and save-inspect-v0.9.5 at their release commits. Backfilling the 14 tools' older releases stays open as an optional one-off; the audit's audit's tag-policy ruling is satisfied by forward-only tagging.)
  • gff-edit segmented-type build. Builder covers indexed GFFs only; the secondary-table + GFFI cross-reference dance is unwritten. Promote when a downstream consumer needs to construct a segmented GFF from scratch (reading and replacing already work).
  • repro bug-triggering save curation. Per-bug saves placed just before the trigger, indexed by bug ID. Ongoing alongside every fix; promote to a focused push when Phase 6 picks its first bug (it needs one).
  • image-extract animated sprite export (GIF / APNG). The still half shipped in v0.3.0 (--frames-all, --spritesheet). Needs an encoder decision first: no in-tree GIF/APNG writer, and a new dep needs sign-off per spec §7a. Note region-render v0.7.0 already shells to ffmpeg for --gif; the same trick applies here and probably settles the question. (Shipped 2026-09-06 in v0.5.0: the decision went to the ffmpeg shell-out, exactly as the box predicted. --gif (with --frames-all) bundles the frame PNGs into <KIND>-<ID>.gif via the same two-pass palettegen/ paletteuse pipeline as region-render --gif; no new crates. APNG stays deferred; ffmpeg's -f apng muxer is the named route. Regression test: a real two-pass encode asserting the GIF signature, skipped when ffmpeg is absent like the corpus tests are when .games/ is.)
  • region-render animated palette colours. VGA colour cycling; blocked on EXE RE of the cycle-table layout. Phase 5.6 is the unblocker; candidates (VGAColorCycle, gCycleColor) are already named in the DSO symbol table. > FULLY UNBLOCKED 2026-09-06 (dsun-exe-re.md 4.5.6): > the registration source is found and it is boot-time: > StartCycle(slot, first, count, delay) at DS1 0x28707 / > DS2 0x2cee7, called exactly four times at init with > (0, 1, 5, 2), (1, 6, 5, 2), (2, 11, 5, 2), > (3, 240, 9, 2). DS1 colour cycling is four fixed > global ranges, active from boot; there is no > per-region cycle configuration. region-render can now > implement the mechanism plus these four ranges for a > game-accurate animated palette. > (Ticked 2026-09-06: both named-consumer conditions > above are met; the VGA work shipped as ovr-map 0.3.2 / > 0.3.3 syms rows + dsun-exe-re 4.5.5-4.5.6.) > (IMPLEMENTATION SHIPPED 2026-09-10, region-render > 0.8.0: --animate-palette renders one frame per pump > pass with the decoded 16x8 record semantics > (PaletteCycle, rotate-toward-higher-indices, delay > counters), defaulting to DS1's four boot-time ranges; > composes with --animate-entities and --gif. > DS2's init site stays open, so DS2 approximates with > the DS1 set, documented in the README. Proven on > RGN02: frames 0/1 pixel-identical (delay 2), 236,783 > pixels shift at the frame-2 first rotation. Rider: > the README palette-precedence table, stale since the > v0.5.0 preset flag and CPAL:200-first fallback, is > repaired in the same release.)
  • region-render --annotate. Entity-name overlays on rendered maps; deferred for lack of an in-tree font without a new dep. Promote when atlas or a modder workflow wants labelled maps badly enough to revisit (SVG output or a tiny embedded bitmap font are the escape hatches).
  • dialog-extract path-aware caller picking. For the 32 LSTR reads resolved via possible_writers arrays: CFG-distance ordering or symbolic trace instead of the current narrowing. Queued since v0.5.0; promote when an unresolved dialog actually misleads someone.
  • gpl-asm parameterised macros and @include. Queued since v0.6.0. Promote when a darkfix script gets repetitive enough to want them; the --patch TOML mode may obsolete the need instead. Re-evaluate at Phase 6.
  • opcode-fuzz first opcode-semantics discovery (JSON recipes). The harness runs a swapped chunk through DOSBox and diffs DARKRUN.GFF; what remains is the loop that walks candidate opcodes and the discovery work itself. (Ratified 2026-09-12: reframed from "discover a previously-unknown opcode", whose original shape died when the 15 unknown dispatch slots were proven unimplemented, so only semantics discovery can meet it; the recipe-format call is made, JSON recipes matching the gpl-disasm --json contract the other tools speak; the work schedules with the DOSBox session calendar. Keep this box alive until a semantics discovery lands or the box is explicitly closed.)
  • save-inspect DARKRUN SAVE chunk RE. (Ratified 2026-09-12, partially absorbed: SAVE/1 and SAVE/7 are decoded into the field catalogue as of 0.9.6; the remaining ids stay deferred on the original trigger, a fix that must READ a save field.) Still open: SAVE/2-/4, /8, /9, the u16 scalar family at ids 10..17, and the 51-byte SAVE/18 boolean array. Bootstrap with the v0.7.0 save-diff harness: snapshot, one in-game action, snapshot, diff. Quest and world-state fixes need this; promote when the first such fix is chosen (the dead-trigger pick reads GPL chunks, not saves, so it does not pull this forward).
  • extract.sh. From-installer extraction wrapper. Deferred; innoextract is one command and verify-install --repair already shells out. Reinstate only if a contributor needs it.
  • Python test harness decision. The repo has none; Python tools gate on ruff + compileall in CI and ship --selftest flags (ovr-map). Options: keep the flag idiom, or standardise on stdlib unittest discovery in CI. Decide the next time a third Python tool grows a selftest. (Decided 2026-09-06, trigger met: keep the flag idiom. Six tools carry selftests (ovr-map, exe-patch, repro, apply.py, global-state-sweep, dead-trigger-sweep); the tests are heterogeneous (synthetic fixtures, refusal paths, skip-when-absent corpus parts), which a unittest discovery layer would wrap without adding coverage. CI now RUNS all six selftests in the python job, so the flags actually gate instead of living on the dev machine.)

Tooling inventory and gaps

Checked on the dev host 2026-08-28; capabilities updated 2026-09-05 (dispatch tables, OBJEX sprite pipeline, symbol catalogue). Everything the docs cite is installed. Nothing blocks any phase.

Tool Status Used by
Ghidra 12.1.2 installed (~/.local/share/ghidra_12.1.2_PUBLIC/; analyzeHeadless not on $PATH, invoke by full path) Phase 5.6; ovr-map --ghidra
radare2 5.9.8 (incl. radiff2, rabin2, rahash2) installed Phase 5.6 official-patch diffing; dsun-exe-re.md §6
ndisasm / nasm installed ovr-map --disasm; patch byte authoring
DOSBox-Staging 0.82.2 (as dosbox) installed repro, opcode-fuzz
ffmpeg installed repro video capture; region-render --gif
innoextract installed verify-install --repair
ydotool installed repro keystroke automation
wine installed Phase 6 applier testing
pwndbg installed; not for this project (gdb plugin for live Linux ELF; these binaries run under DOSBox) nothing

Optional, only if a phase asks for it:

  • DOSBox-X: a second emulator with a deeper built-in debugger (instruction tracing, more breakpoint/logging surface than Staging's). opcode-fuzz and hard-to-catch races are the plausible consumers. Staging's debugger plus the DARKRUN.GFF diff loop has sufficed so far; install when the first bug defeats both.
  • keystone-engine + bsdiff4 in the applier venv. RESOLVED 2026-09-06, neither adopted: the applier is pure stdlib (in-place offset-keyed edits need no diff library), and assembly goes through system nasm (tools/exe-patch --asm, round-tripped against ndisasm; pwntools cannot assemble 16-bit x86 at all). docs/build-environment.md §2 carries the full reasoning.

Not needed, for the record: LE/LX Ghidra loaders (the DOS/4GW model was disproved, dsun-exe-re.md §1), decompilers beyond Ghidra (16-bit real-mode support elsewhere is worse), and any Windows-cross toolchain (patches are data + Python, not compiled code).

2026-09-05 session findings promoted to the durable docs (one line each here; the evidence chains live at the targets):

  • Freeze-mechanism candidates and the complete mines dossier (scheduler deadlock, dangling GF[647-652], the intra-region floor system, the freeze window, the capture recipes): docs/engine-quirks.md section 6.
  • GF flags are GPL-layer-only; the EXE never reads individual flags: docs/engine-quirks.md section 7.
  • GPLI-1's real format (and the retracted dispatch-index theory): docs/engine-quirks.md section 8 and the docs/file-formats.md GPLI row.
  • DS1 has no region names; RDAT is config, not names: docs/engine-quirks.md section 9.
  • The DS2 GPL request dispatcher map, the DGROUP BSS layout, and the scheduler's 0x100:0x2 character-output function: docs/engine-quirks.md sections 10-11 (the scheduler also appears in section 6 as freeze candidate A).
  • DS2 overlay manager at file 0x404c4 and FBOV segnum = exeinfo record count: already durable in docs/dsun-exe-re.md / docs/dsun-exe-survey.md (corrected 2026-09-05; the syms row fixed in ovr-map 0.3.1).

The elevator site-report narrative inside the 5.6.1 box is the Phase 7 site report's draft; it assembles into ds2-patch/fixes/fix.ds2.mines-elevator.md when Phase 7 opens, from this ledger, docs/engine-quirks.md 6, and the census row.

New findings 2026-09-12 (six-lens full audit; detail: audit/FULL-AUDIT-2026-09-12.md, Wave 26)

  • HIGH: every release ships with zero assets: the player the pipeline was built for cannot install darkfix-ds1. ds1-patch's README says "download the darkfix-ds1-vX.Y.Z.zip release"; spec 10 defines the release AS the zip; build-release.sh was promised in patch-workflow and never landed. Write it (flatten manifest/VERSION/ apply.py/darkfix/fixes + the player README), attach to the existing darkfix-ds1-v0.1.0 release. (Shipped 2026-09-13. tools/build-release.sh stages the spec 4 tree, gates it through the real fix-contract checks, compileall, the staged applier's --selftest, and a direct-CLI manifest-discovery check, and writes a deterministic darkfix-dsN-v.zip. The gate caught a real ship-blocker: the tag's apply.py resolved its patch root one directory above the flattened zip layout, so the zip could never find its own manifest (the Wine proof ran the repo layout and never hit it). The darkfix-ds1-v0.1.0 release now carries its zip: the tagged tree plus that one manifest-discovery fix, proven end to end from the extracted zip (apply/verify/status/unapply byte-identical against copies of the real install files). A release zip workflow attaches the zip automatically on every future darkfix-ds*-v* tag, with a tag-input dispatch for existing releases, outside the push/PR CI path. No re-tag, no version bump: packaging alone does not cut a release.)
  • Applier hardening (before a second fix shares a target): an interrupted write phase (crash between first write and the journal) strands a half-apply with no recovery path (--unapply refuses, re-apply refuses, and the error never names darkfix-backup/ as the manual route); write a pending journal and teach --unapply to restore from it, or at minimum name the manual route in the errors; two enabled fixes sharing one TARGET abort mid-write (compose per-file or refuse at check time with an explicit error). (Shipped 2026-09-13, both halves. The journal is written pending before the first write and completed after the last: a mid-write crash now refuses the next apply with a pending-journal message, --unapply restores every file the interrupted run reached from darkfix-backup/ (leaving files it never reached and clearing stray staged .darkfix-tmp files), and the hash-mismatch/no-journal errors name the backup directory as the manual route. Two enabled fixes on one target are refused at check time, before any write. Negative-offset refusal + Edit.from_dict guard are the code hygiene box. Seventeen new selftest cases; 39 green.)
  • Docs sweep: patch-workflow.md still teaches the superseded manifest version bump (step 1 is dsN-patch/VERSION per spec 4); gff-edit's header says v0.2 coverage; a gff-tool residue in spec 2; the hash-test instruction isn't runnable as written (route through apply.py --selftest); .clinerules dangling ref; the fragile :1043 anchor; the remaining 5.20/5.21 section-number pointers. (PARTIAL 2026-09-13: the patch-workflow.md ship section repaired with the packaging rewrite (step 1 now dsN-patch/VERSION; step 3 names the real script; step 5 the release workflow). CLOSED 2026-09-15, the remaining six: the hash-test instruction routes through apply.py --selftest (patch workflow 5.1 rewritten); .clinerules now points at CLAUDE.md's git habits; the gff-tool residue is gone from spec 2; the gff-edit v0.2 header was rewritten with the comment sweep; and the :1043 anchor plus the 5.20/5.21 pointers cite their boxes by name. patch-workflow 7.4 also shows the annotated git tag -a command instead of a lightweight-tag example.)
  • Code hygiene: parse_hex_bytes strips "0x" anywhere (120x34 silently becomes 1234, prefix-only); Edit.from_dict lacks an offset >= 0 guard (gpl-asm and exe-patch both guard it). (Shipped 2026-09-13. gpl-asm 0.9.1: one 0x per whitespace-separated group, mid-string 0x fails loudly instead of vanishing; four new unit tests. Edit.from_dict refuses negative offsets, with the slice-wraparound rationale in the guard and a selftest case.)
  • Blitz candidates: DS2's 30 dead triggers are pipeline-ready (reuses the proven darktriggers loop; Brandon decides ship-ahead vs phase order; needs a DS2 correlation dig first); CONTRIBUTING.md (stdlib-only rule, the --selftest idiom, the cookbook convention); GNAME[39] resolution rides the runtime-capture recipe.
  • GitHub presentation (workspace batch): description carries literal markdown asterisks (replacement drafted); topics +dos/ dosbox/assembler/python; homepage codex entry is stale (twelve tools / darkfixes "future" vs 14 tools / darkfix-ds1 shipped) - update the codex page in the site pass.

Final audit 2026-09-14 (THE FINAL AUDIT: NEW findings, one line each; full detail in audit-final/opends/FINAL-REPORT.md)

Eight lenses + slop-reader at 5c6cbd7. Tally after dedup: 0 HIGH / 8 MEDIUM / ~40 LOW + 8 feature proposals. The campaign's first zero-HIGH repo: the 09-13 packaging + hardening batch verified real end to end (all four Wave-26 MEDIUMs closed, parse_hex_bytes fixed with regression tests, from_dict guarded, the release pipeline works), and code-vs-docs found zero unrecorded drift. NEW findings LOGGED, never executed:

  • [MEDIUM] CI catch-up: the python gate covers tools/ only, so the applier, darkfix/, and fixes/*.py (the most-distributed Python artifact; ships in every player zip) are never linted/format-checked/byte-compiled; CI runs 3.14 only vs the declared 3.11 floor; the ruff 0.15.20 pin exists nowhere in-repo (newer ruff has already run locally). Extend the gate to ds1-patch/, add a 3.11 leg, add a root ruff.toml. (Shipped 2026-09-15. Root ruff.toml pins required-version to ==0.15.20 (a different ruff refuses to run instead of silently diverging from CI) and sets the py311 target; the three gate commands cover tools/ and ds1-patch/; the python job is now a 3.11 + 3.14 matrix, so the declared floor runs the whole gate including the six selftests instead of only the zip build. build-environment.md 2 and CLAUDE.md carry the gate line.)
  • [MEDIUM] Repair the Phase 6 pick-box scar: the 09-12 5.20-strike repair took the pick-box header with it and the body resumes mid-sentence ("work). Prefer a GPL-data fix..."); restore the box, move the orphaned body + annotations under it, delete the dangling "work)". Same lane: fold the ratified 09-12 dispositions into the save-inspect and opcode-fuzz box bodies (roadmap.md:1803-1818), which still list decoded items as open. (Shipped 2026-09-15. The deleted header line was recovered verbatim from the strike commit's diff ("- [x] Pick one trivial DS1 bug (identified during Phase 2 repro") and reinserted, so the orphaned body and the shortlist/ CORRELATED/PICKED annotations sit under their box again. The ratified dispositions are folded into both bodies: opcode-fuzz reframed to first opcode-semantics discovery with the JSON-recipes call recorded, save-inspect marked partially absorbed (SAVE/1 + SAVE/7 decoded in 0.9.6) with only the true remainder listed open.)
  • [MEDIUM] spec.md:225-237 (section 6) still says "No reassembler in v1"; gpl-asm is 0.9.1 and produced the first real fix. Rewrite to the shipped stack (gpl-disasm, gpl-asm --patch, gff-cat replace). Same lane: patchnotes.md has its entire 113-entry history under one ## Unreleased heading while patch-workflow.md:194 describes version headings; introduce headings and move tagged entries out.
  • [MEDIUM] Comment-header de-versioning sweep (headers frozen in tool infancy): gpl-asm lib.rs:1-25 (still "v0.1.0 encoder foundation"; all three future-work items shipped) and :23-25 (says Search chunks are "flagged unencodable"; the encoder handles them at :274-300/:413-436) and the dead-version error strings; save-inspect.py:4-9 (claims CHAR records are opaque hex; decoded per game and writable since 0.8.0); gff-edit lib.rs:21/builder.rs:12; gpl-disasm lib.rs:19; opcode-fuzz.py:5. Plus apply.py:366 --status prints "applied" for a pending (interrupted) journal: branch on status. (Shipped 2026-09-15. All six headers rewritten to present tense on the region-render model; the gpl-asm scope note now states the real Search handling (leading expression + verbatim raw_tail, top-level and RETVAL-inner) and the "v0.1.0"/"v0.1.1"/"v0.2.0" error-string stamps are gone; gff-edit's header also stops calling the crate a reader and the builder keep-alive comment names the real deferral trigger. apply.py --status branches on journal status: a pending journal prints INTERRUPTED with the --unapply recovery route; two new selftest cases cover both branches, 41 checks green.)
  • [LOW] Seven real-bug LOWs: opends extract dispatches to a gff-cat subcommand that has never existed (tools/opends/src/main.rs:197); gpl-asm silently mangles non-ASCII string content through 7-bit masking (lib.rs:528, unreachable via the round-trip loop but one hand-authored accent from corruption); journal writes are non-atomic so a first-write crash blocks apply AND unapply with a message naming a backup that does not exist yet (patcher.py:288); an all-disabled manifest "applies" and writes an unremovable empty journal (apply.py:141); build-release.sh Gate 1 misses the duplicate-TARGET refusal so a bad manifest ships in the zip (:89); verify-install --repair clobbers its own backup (:278); exe-patch resident edits skip the straddle check (exe-patch.py:206). (Shipped 2026-09-15, each with a regression test: extract now dispatches gff-cat extract --all -o with an arg-shape unit test; pack_compressed_string refuses non-ASCII before writing a byte (new NonAsciiString error) and the parse-side estimator counts bytes to match; write_journal goes through the same staged tmp + os.replace as targets, a torn-journal error names the delete-the-journal remedy when no backup exists, and pending recovery sweeps a stray staged journal; an empty enabled set is refused instead of journalled; build-release.sh gate 1 gains the seen-targets refusal plus a --selftest that proves a dup-target manifest is refused, and build-release.sh ds2 refuses friendly instead of a raw cp error (lens 2 polish 9, same file); verify-install --repair raises on an existing pre-repair backup and grows a --selftest flag covering the guard; classify() flags a resident edit that straddles image_end into the FBOV header, with a selftest case. ovr-map/save-inspect struct pre-checks, patcher int(offset) coercion, and parse_hex_bytes("") refusal stay recorded polish: below the ranked line, authoring-controlled.)
  • [LOW] The docs-sweep box (all six confirmed still open): hash-test instruction unrunnable (route through apply.py --selftest), .clinerules re-point, gff-tool residue in spec 2, dangling section pointers (5.20/5.21/:1043), gff-edit v0.2 header. (Shipped 2026-09-15: all six, plus the Wave-26 Docs sweep box above is now marked CLOSED with the item-by-item notes.)
  • [LOW] Em-dash/punctuation pass on live prose: the four doc titles (roadmap, spec, both patch READMEs); spec's 21 live lines; README's find-replace artifacts (floating " :" continuation lines :131-135, double colon :75-77, " ;" :7); CREDITS.md's 17 list glyphs; the 4 docs singles; roadmap.md:1910/:1938/:1965 ASCII " - " surrogates in the live findings block. Historical records (roadmap 212-1907, old patchnotes) stay as records. (Shipped 2026-09-15. Titles recast ("OpenDS Roadmap", "OpenDS Design Spec", "darkfix for Dark Sun: ..."); every live em-dash recast as a colon, semicolon, comma, or parenthesis (never " - "), spec and CREDITS now at zero; README's stranded " ;" and " :" joiners repaired; the three ASCII surrogates in the live findings block recast. The 70 historical roadmap dashes and 18 patchnotes dashes stay as the record.)
  • [LOW] GitHub: SECURITY.md (binary patches players run: the one repo that needs it, name the deterministic-zip hash for verification); dependabot (actions+cargo, deliberately no pip); CI badge; SHA-pin the five floating action refs; release.yml's dispatch repair path cannot rebuild darkfix-ds1-v0.1.0 (build-release.sh is absent from that tag's tree; take it from the default branch on dispatch); concurrency group; online check for unattested Releases (ovr-map-v0.3.0, save-inspect-v0.9.5, elevator-site-report-v1). (Shipped 2026-09-15. SECURITY.md names GitHub private vulnerability reporting plus the deterministic-zip sha256 check, and release.yml now quotes the built zip's sha256 into the release notes so the promise is true going forward. dependabot.yml: github-actions + cargo weekly, no pip (the ruff pin is policy). All five action refs SHA-pinned (checkout v4.2.2, setup-python v5.6.0, rust-cache v2.8.1, dtolnay/rust-toolchain stable branch head). The v0.1.0 dispatch exception is COMMENTED in release.yml's build step rather than papered over: the script's absence from that tree already fails loudly, which is the safe behavior, because a rebuild from main's tree would silently change a shipped asset (its zip carried the manifest-discovery fix in apply.py). ci.yml gains a cancel-in-progress concurrency group. Online check ran: all 16 tags have Releases, so the three flagged as unattested (ovr-map-v0.3.0, save-inspect-v0.9.5, elevator-site-report-v1) are in fact attested; audit correction recorded.)
  • [LOW] Removal/housekeeping: dedupe the fix-script skeleton to fix-format.md (three inlined copies, already drifted in depth); git rm the two ds1-patch .gitkeeps; repoint .gitignore:33 at re-tooling.md; add .claude/ and .ruff_cache/ lines; versioning.md should cover 0.0.x pre-releases; CREDITS.md:90 stale "v0.2.0" forward ref; roadmap.md:54 snapshot table still says gpl-asm 0.9.0; optional ~12-line justfile for the full local gate. (Shipped 2026-09-15, the gitkeeps and the dedupe on Brandon's explicit go. patch-workflow 4.2 and the ds1-patch README now point at fix-format.md's canonical skeleton with the one-line contract; spec 14 resolved as MIT with LICENSE staged into both patch dirs and the zips by build-release.sh; the .gitignore Ghidra comment cites re-tooling.md and gains .claude/ + .ruff_cache/ lines.)

CONFIRMED-prior (verified): the release-assets HIGH closed 09-13 (build-release.sh + release.yml + the zip proven by download); applier hardening closed (39 checks statically consistent); the PARTIAL sweep residuals all still open and correctly tracked. SUPERSEDED: parse_hex_bytes (fixed + regression tests), Edit.from_dict guard. Audit-side correction: the audit sheet still says "5.20 still open" on bsdiff4/keystone; the repo closed 5.20 on 2026-09-06 with neither adopted.

Feature candidates (RE-RANKED from the recorded board; grounded in audit-final/opends/FINAL-REPORT.md lens 4): P1 the DS2 dead-trigger correlation dig (S, agent-executable now, decision-free: it arms the ship-order call rather than preempting it) then P2 the DS2 fix lane (M, highest absolute value; rides the ship-order and applier promote-vs-copy calls); P3 DS2 party edit-surface verification + cookbook entry (the only Phase 7 precursor gated on nothing Brandon must do); P4 CONTRIBUTING.md (and link the cookbook from the root README, which never mentions it); P5 set-up-repro-fixture.md rides the DS2 lane; P6 DS2 StartCycle init site; P7 install-variant refusal policy (NEW decision + docs); P8 DS1 final-battle variant-1 dig (NEW, contingent). GF[395] stays parked.

Slop-reader: voice is human everywhere (no stock vocab, no hedging, dated correction blockquotes are an anti-drift pattern worth copying); the em-dash census of 143 lines verified exact with the live/historical split above; the real finding is mechanical find-replace (" - " and ":" substitutes instead of recasts) in README and the live roadmap block, plus the four banned-dash titles.

Blitz execution 2026-09-15 (THE FINAL BLITZ; findings ledger: audit-final/opends/FINAL-REPORT.md)

All ten ranked items executed same-day; every audit-block box above is ticked with a ship note. Decisions (Brandon, via prompts, 2026-09-15): (1) standing push grant through 09-20; (2) cut darkfix-ds1 v0.1.1 with the accumulated applier work; (3) DS2 ship-order: ship the dead-trigger fix ahead of the elevator IF the correlation dig shows main-quest content, else hold; (4) ds2-patch applier shape: copy per patch; (5) spec 14 patch license: MIT (LICENSE staged into the zips); (6) CONTRIBUTING.md: go; (7) git rm the ds1 .gitkeep pair; (8) dedupe the fix-script skeleton to fix-format.md; (9) P7 install-variant policy: refusal is the path; (10) GitHub presentation batch: execute. The gated hands set (played-save sessions, DOSBox capture calendar) stays calendar-gated and is recorded in project.done as a reopen condition; GF[395] stays parked.

  • P1: the DS2 dead-trigger correlation dig (executed 2026-09-15, agent-side, decision-free). The sweep at this HEAD: 1,741 trigger registrations; 1,505 alive; 206 intentional null-handlers; 30 dead, every one pointing at GPL-24 entry 0x1 (gpl exit gpl). GPL-24 is not a clobbered stub like DS1's GPL-200@0x909: it is DS2's shared NPC-chatter library (bystander combat refusals, enemy taunts, party barks, race names, menu threats), alive via 104 gpl global sub call sites corpus-wide; only entry 1 is a no-op, and it is a discovered entry with its own explicit exit (GPL-46's entry 1 is a real handler, so the shape is a deliberate shared default, not a format artifact). 37 trigger registrations target the chunk; all 37 target entry 1. The 30 rows are later or interleaved no-op re-registrations sitting beside ALIVE handlers for the same objects: the DS1 occlusion shape exactly. Example: GPL-8 registers objects -209/-406 alive at GPL-8@861 (0x1e/0x27) and dead at 24@1 (0x473/0x47b); GPL-187 registers -2975 alive at 6349 (0x1744/0x197b) and dead at 0x18cd. Object census (OBJEX OJFF -> BMP, placements from the RGN ETABs): 20 rows on ONE object, -2975 (BMP 447, a 14x14 gnarled prop; placed in Limbo/RGN0FF at (71,157); rendered and visually identified). The rest: -418 (BMP 1753; Tyr, Silt Giants, Limbo), -406 (BMP 1754; Silt Giants, Cosmos, Limbo), -209 (BMP 2427; Volcano Level 3, Limbo), -146 (BMP 1269; forest), -445 (BMP 1268; forest), -106 (BMP 1247; Jann), -1922/-1923 (BMP 1186/1185; VA Headquarters, Limbo), -2994 (BMP 2016; Crypt). Trigger mix: 20 usetrigger, 4 looktrigger, 4 attacktrigger, 2 talktotrigger. Correlation verdict: main-quest, normal-playthrough content (Tyr, the VA headquarters, Jann, the Silt Giant lands, the Cosmos, the Crypt, the Volcano, the Limbo endgame): the gate for decision (3) PASSES, with one honest caveat: only 4 of 30 rows are the attacktrigger "enemies refuse to engage" class that gave DS1 its player-visible symptom; the other 26 are use/look/talk interactions on a shared prop and story objects, so the fix's visible payoff is quieter than DS1's. Same open EXE-side question as DS1: first-wins vs last-wins re-registration semantics; the fix design stays semantics-proof (repoint the no-op rows at each object's own working handler: a no-op if the engine keeps first registrations, a restoration if it keeps last). 28 of 30 rows have locally provable working handlers; GPL-30's and GPL-64's -2975 rows have none and stay untouched.
  • Releases: seven tags cut and pushed 2026-09-15/16 (darkfix-ds2-v0.1.0, darkfix-ds1-v0.1.1, gpl-disasm-v0.8.1, gpl-asm-v0.9.2, verify-install-v0.3.1, exe-patch-v0.1.1, opends-v0.1.1; CI green on both pushes; every Release page live, the two darkfix zips attached by the release zip workflow with their sha256 quoted into the notes, the v0.1.0 release notes retrofitted with its live asset's hash per the metadata-batch decision). Workflow note: the two darkfix tag pushes rode a 7-tag single git push, which triggered NO tag runs at all (first tag-triggered run of release.yml would have been these); the workflow's own dispatch path built and attached both zips, and release.yml now carries a comment: push darkfix tags individually or use dispatch.
  • P2: the DS2 dead-trigger fix lane (darkfix-ds2 v0.1.0: the ds2-patch bootstrap with the copied applier plus fix.ds2.deadtriggers, repointing the provable rows). (Shipped 2026-09-15 on the standing grant's release cadence. ds2-patch/scripts/ is the DS1 applier copied per decision (4), adapted for ds2 and carrying every applier hardening from day one; fix.ds2.deadtriggers repoints 27 of the 39 corrected-census dead rows (21 same-chunk, 6 MAS-attested, the DS1 portcullis-guard precedent), leaving 12 with no statically provable handler; the sweep-tool census fix (gpl-disasm 0.8.1) surfaced the extra 9 rows (pickup + use-with) that both games' sweeps had been skipping. Proofs: patched file re-disassembles 350/350 aligned; sweep 39 -> exactly the 12 left; apply.py --selftest 46 checks green with the patched hash pinned; repro --diff both legs PASS with identical DARKRUN fingerprints (fixture bugs/ds2-deadtriggers/; scene legs ride the played-save gate). Tag darkfix-ds2-v0.1.0 cut via the standard procedure; release.yml attaches the zip.)

The bestiary campaign opens 2026-09-16 (catalogue lane; Brandon-directed)

Goal: pull a confirmed bestiary and full item/weapon/armor/spell catalogues out of the shipped data: every piece, dug until confirmed, nothing guessed. The data-side substrate the engine question (spec 12) also stands on. Wave 1 was four read-only extraction agents (schemas; items; spells; creatures) over SEGOBJEX.GFF / OBJEX.GFF, GPLDATA, RESOURCE, and the EXE spell tables, cross-checked against the played-save layouts, the in-install SSI clue books (facts only), and libgff.

  • Wave 1: the object-database schema maps. Both games' RDFF chain model (10-byte headers; load_action walk; item/combat/charrec/mini/template subtypes; chain-shape census), the combat + charrec field maps for both games, OJFF, DS1 IT1R + NAME pool, DS2 TEXT/1000 name pool, the 32-byte spell power record (DS1: 196x32 at DSUN.EXE 0x41f70; DS2: 320x73 at RESOURCE 0x120e8, names inline), the DS1 7-byte spell-level table (0x4512c), MONR as DS1 encounter tables (DS2's copy is stale carryover), and the SPST/PSST/PSIN known-spell encodings. Written up as docs/object-formats.md. Corrections it records against the historical notes: the negated-id anchor is combat +6 / item +0 (the "+14" reading reproduces nothing corpus-wide), and the 1.10 SPIN fills are DS2-scoped. Key confirmations: the creature records ARE the party records (SAVE/5-/6 and CHARSAVE layouts), monster magic resistance matches the clue book exactly on every cross-checkable row (Mindflayer 90, Blue Slaad 40, Rampager 25, Vrock 70), and the wand charges match the treasure guide.
  • The executable schema + generated catalogues. tools/gff-edit/scripts/extract-catalogue.py (stdlib, --selftest) walks both object databases and emits docs/{bestiary,item-catalogue,spell-catalogue}-ds{1,2}.md (290 DS1 + 352 DS2 creature rows, ~750 + ~1,230 item rows, 196 + 320 spell rows, MONR appendices). Every document carries a verification footer: 22 anchor gates (named monsters' stats, name joins, the 5807 -> BMP 951 sprite anchor, the Magic Missile / Fireball damage words) all pass, and the run exits nonzero on any drift. The damage-word decode is marked partial; the raw hex rides every row.
  • Wave 2: close the marked-open fields. The highest leverage, from the wave-1 reports: DS2 charrec +16..20 (class bits, race, gender, alignment); DS2 THAC0 derivation (no stored byte; fight handler DS2 0xd70c); the special_attack/defense and allegiance enums; the DS2 damage-word scale/div semantics (the sc/div/dp bits beyond the two confirmed modes); the effect-id (+25) jump table, which is the actual spell behavior and the single highest-value target for the engine question; DS2 SPST length rule; the named-shield AC discrepancy (El's vs Drake, one in-game check). (Wave 2 EXECUTED 2026-09-16, four read-only agents; all integrated into docs/object-formats.md and the generator. RESOLVED: DS2 THAC0 is a STORED byte at combat+22, instruction-proven at DS2 EXE 0x5c6e9 (mirror of DS1's +31 read), refuting the derived-from-level hypothesis; the DS2 charrec +16..20 identity block (legal_class u16, race, gender, alignment) pinned by the class-anim selector at 0x6ef84, the race special-case at 0x6ef94, the gender selector at 0x8fef0, and the protection-from-alignment grid at 0x83775; the alignment enum (1 LG .. 9 CE, 0 none) book-anchored; the allegiance enums censused per game (bitfield reading dead); DS1 special_attack confirmed an ENUM with 33 values and a partial ability map; the damage word settled corpus-wide (div 0 flat, div 1 per-level including the plus-per-level bonus, div > 1 grouped only under the scale flag else flat; duration unit map; target enum refined; a 7-entry SSI text-vs-data disagreement list recorded). NEGATIVE RESULT with roadmap value: there is no central effect-id jump table; dispatch is distributed case-chains behind overlay-runtime far-call indirection, so handler resolution needs the runtime overlay segment map (a DOSBox break on the resident spell-record getter 0xb8:3) or the FBOV exeinfo decode. REMAINING OPEN: DS2 cb14 high byte, allegiance values 3/6, DS2 SPST length rule, the named-shield AC check, and the runtime-capture route to the cast/apply handlers.)

Wave 3 (executed 2026-09-16, four read-only agents; the Godot-conversion wave). SHIPPED: docs/gpl-vm.md, the complete GPL VM execution and data-model spec read from both engines (one VM, identical semantics: the 50-slot IP ring, the 16-slot LRU chunk arena with the ExitGpl sentinel-byte termination, the variable model with GFLAG 808 / GNUM 200 and fresh-per-chunk locals, the full expression-evaluator algorithm with its 15 left-to-right 32-bit operators, the compare/branch/call machines with exact limits, RETVAL mechanics, per-opcode pseudocode for the arithmetic and engine-call families, the debug family confirmed residue, and the engine-quirks 7 correction that VM:0x33b is LFLAG not GFLAG). docs/region-formats.md: RMAP/ GMAP bit layouts (DS1-only wall index; DS2 wall layer empty by design), the 8-byte ETAB record with per-game index sign, and the trigger operand shapes with MAS-id-equals-region-id extended to DS1. The generator gained container/inventory extraction (595 + 698 entries; dangling exactly {2643, 2644} DS1, none DS2) and the per-region world dumps (13,028 + 13,559 placements, grids, and every trigger registration via gpl-disasm --json): world-dump-ds{1,2}.md, container-contents-ds{1,2}.md, creature-inventories-ds{1,2}.md. DIRECT ANSWERS: yes, we now have the maps, every placement, every chest, and every creature inventory as anchor-gated generated docs.

Wave 4 (executed 2026-09-16, four read-only agents; the final static-mining wave). SHIPPED: docs/gpl-vm.md section 7 (the world-interaction, UI and string opcodes: the Request maps (20 DS1 / 53 DS2 codes, per-arm actions; code 39 is literally a byte write to the elevator-state cell), Tport/Clone/Search (the aggregate query engine), the party/item/UI families, and the complex-variable grammar with its runtime 198-field-per-group tables); docs/presentation-formats.md (all image formats corpus-decoded, the WIND/BUTN/APFM/FONT UI layouts with the byte-identical shared font, the audio inventory incl. DJ.DAT and the VOC/BVOC sfx, cinematics: only DS1's BMA/ACF codecs remain undecoded anywhere in the asset stack; DS2's are standard FLI); docs/dialogs.md (46,053 strings / 1.71M chars over 531 fully aligned chunks, 32 unresolved LSTR reads, and the conversion shape for a Godot dialog system).

THE VERDICT (the mining boundary, evidence-backed):

  • Static mining is DONE as a bulk activity. The enumerated remainder: gpl-vm section 6's open questions (RNG range, complex-field runtime tables, GNAME producers, string sub-types); the FBOV exeinfo decode (the one static route to the spell cast/apply handlers); DS1 final-battle variant-1 (P8); the 12 unshipped DS2 dead rows. Everything else that looks like digging (elevator mechanism, first/last-wins, save-field semantics, live BSS state) is runtime-capture territory by the docs' own record.
  • Patch tooling: every surface has a closed author -> verify -> apply chain. Two small named buildables, neither urgent: an exe-patch MZ-relocation overlap check BEFORE the first real EXE fix (classify() never consults the 4,853/4,703-entry relocation table; an edit on a relocated word would verify on disk and silently revert at load), and the ratified opcode-fuzz JSON recipe loop (calendar-gated). Explicitly not worth building: the DSO matcher, segmented-GFF builder, extract.sh, DOSBox-X.
  • "Deep patching is the last thing": TRUE for darkfix v1 (both games have shipped fixes; Phases 7-10 are gated on played-save sessions and captures, not understanding; the elevator's fix shape is pre-decided and its capture recipes are written). For the engine-conversion path the frontier is now runtime semantics and from-scratch AD&D rules, not data: the data side is mined.

The port-mining campaign (2026-09-19; the Godot-readiness lane)

Brandon directed the follow-up to the bestiary campaign's verdict the same week: set subagents on "the rest of what we need to start building a port to Godot", four at a time, GLM-5.3 on the hard jobs and GLM-5.3-Flash on structured breadth. Three waves of four read-only agents, integrated and committed wave by wave (1e9e59f..f525da7).

Wave 1 (FBOV/exeinfo, rules tables, asset bindings, combat flow): docs/overlay-formats.md (the Borland overlay apparatus decoded end to end; the per-module relocation tables are static file data, superseding 5.6.2's BSS claim; a static far-pointer resolution procedure with worked examples; the real load_resource entries 0x29ea4/0x2e69b and handler entries 0x3be27/0x40544, correcting ovr-map's syms and two docs), docs/rules-tables.md (the 168-byte rules block: class-group HP/THAC0 rates, the class-to-save-group map, the 4x5 save table with formula and the dwarf/mul CON*2/7 bonus, the ability tables; no 2E-style additive CON bonus exists), docs/combat-flow.md (the combat loop to pseudocode: no initiative table, probabilistic priority vs d20, morale-threshold ordering, the 3-attacks-per-2-rounds parity alternator, XP = charrec+4 dword over party count), docs/asset-bindings.md (item icons = OJFF bmp_id, spell icons = 20999 + spell id, portraits as dialog-service operands, DS2 has NO palette cycling, CBMP = BMP id space with a boolean flag).

Wave 2 (spell/effect machinery, frame sweep, audio, screen flow): docs/spell-effects.md (module ownership, the cast engine step by step, the active-effect list, the enumerated dispatch surfaces, the in-combat save chain, the two-level special-attack enum, the duration formula) - and the wave-1 "module frame wall" DISSOLVED: every small segment word is an ordinary segtab byte-offset, verified by relocation-table membership; docs/audio-routing.md (DJ.DAT fully decoded, the driver dispatch, sound id -> BVOC id mappings, and the proved negative that DS1's music opcode is never emitted: no static music mapping exists); docs/screen-flow.md (the window manager is an active list, the screen inventory with open sites, the mode enum, input dispatch, dialog print service commands). Corrections: DS2's XP table exists (RESOURCE.GFF DATA:1000), the runtime save path uses the charrec descriptor byte (the five stored saves are write-only outside level-up), STATE holds 320 slots, fight_enter's kick is the combat-music starter.

Wave 3 (cinematics, chargen, exploration, small opens): docs/cinematics-ds1.md (the BMA frame codec: a row-addressed delta format over the existing DS1-RLE primitive, validated 708/708 frames and visually verified on renders; the ACF script opcodes; the player chain; the 19 CINE stills are BMA-codec containers image-extract misparses today), docs/chargen-flow.md (best-of-4 x (4d4 + racial + 4) stat generation with prime-stat floor 17, the DATA:1001 race x class grid, humans cannot multiclass, DS1 starts at level 3 / DS2 at 6-7, the dual-class 15/17 rule, level caps 9/15 with the DS2 XP bank cap, rest = 8 hours on a seconds-scale master clock, and the RNG: Borland rand(), 0..32767, deterministic - srand has no callers), docs/exploration-flow.md (click-to-move, greedy + wall-follow pathfinding with no A*, formation boxes and the gridlock-escape quirk, LOS = the 2D 0x80 bit, step-on-tile trigger firing, and no engine-side encounter roll in DS1). Corrections: XP row labels fixed through the class remap (the dips are psionicist/ ranger; DS2 fixed its ranger tail), the DS2 cb14 high byte is the damage-resistance table index, the DS2 allegiance hostility matrix decoded, DS2 SPST is 15/34/1 bytes and save-file-only, menu flag polarity is ==1, INTRODUCE renders the speaker name, the TEXT bands are random-name banks, Getxy writes GSTATE not GNAME, the DS2 combat-music starter found, MONR offset corrected to 0x80be4.

THE VERDICT: the knowledge side of a Godot port is mined out. The layer-by-layer checklist and the honest remainder live in docs/godot-port-readiness.md: every layer consumes decoded, generated, anchor-gated sources; what remains is engineering (image-extract's BMA extension, asset exporters, the Godot project itself) plus an explicit runtime-capture list of BSS-resident behaviors (walk speed, scroll margins, cine tick rate, GNAME init, the memorized-slot model, ~35 status bits, the morale-flee transition, loot creation, order kinds, region-load save/restore). The zero-new-RE starting point is the one- region render spike: every input it consumes already exists.