The end-to-end process for authoring one fix, from "this bug exists" to "this bug is gone in the shipped patch."
This is the doc to read before sitting down to fix a specific bug. Skim once. Refer back as needed.
- Fedora dev environment set up: see
build-environment.md. - Both games extracted to
.games/ds1/and.games/ds2/. - DOSBox-Staging configured to run them.
- A clean save before the bug-trigger point (build a save library per game; reuse across fixes).
Don't fix what you can't reproduce.
- Run the game in DOSBox-Staging on a clean install.
- Take the save state to the bug-trigger point.
- Trigger the bug. Capture screen + audio if relevant
(DOSBox
capturekeybind, orobs-studio). - Note exact party composition, inventory state, and recent actions. Some bugs only fire under specific preconditions.
- Save the captured material to
scratch/<bug-id>/repro/(scratch/is gitignored).
If you can't reproduce reliably, stop here. Document what you
saw in known-bugs.md as "not yet reproducible"
and move on.
Quickly decide: GPL data fix or DSUN.EXE binary fix?
Heuristics:
- Quest-progression bug, dialog bug, flag bug, item-give bug, trigger-fires-twice bug → GPL, almost certainly.
- Combat AI bug, rendering bug, input bug, save/load corruption, audio glitch → DSUN.EXE, almost certainly.
- Item-state bug (charged weapon vanishing) → could be either; start with GPL.
If wrong, you'll know within an hour of investigation. Switch surfaces and try again.
Workflow detail in gpl-bytecode.md.
Quick version:
gpl-disasm .games/dsN/GPLDATA.GFF > scratch/<bug-id>/dump.gpl.s- Locate the chunk responsible (search for dialog strings, item names, NPC names: they tend to be embedded in or adjacent to the relevant chunk).
- Read the chunk's disassembly in
nvimore. - Find the bug. Often a wrong jump target, missing flag clear, or off-by-one.
- Compute the byte-level edit. Most fixes are 1–3 bytes.
Workflow detail in binary-patching.md.
Quick version:
r2 -A .games/dsN/DSUN.EXE/r <symptom-string>to find the function.- Read the function with
pdforVV. - Set a DOSBox-debugger breakpoint, trigger the bug, watch state.
- Compute the byte-level edit. Most fixes are 1–3 bytes.
Two artifacts per fix:
# fix.dsN.<short-id>
**Bug**: One-line statement of the symptom.
**Repro**: How to make the bug fire on a clean install.
**Cause**: What's wrong, mechanically.
**Fix**: What we change.
**Surface**: GPL / DSUN.EXE
**Verified on**: GOG 1.10 (DS1 / DS2)
**Default**: on / off
## Details
Long-form analysis. Include:
- The disassembly snippet (before / after) for GPL fixes
- The r2 listing (before / after) for binary fixes
- Why this fix is correct (not just "it makes the bug stop")
- Any edge cases the fix doesn't handlePython 3, stdlib-only. One fix per module; idempotent by
construction (the applier's journal and AlreadyApplied
bookkeeping mean running twice does not double-apply).
The authoritative skeleton and contract live in
fix-format.md (one canonical copy, so it
cannot drift): a fix module declares ID, TARGET,
SOURCE_SHA256, EDITS (fingerprint-checked, in-place byte
edits), and an apply(source_path, dest_path) function the
applier loads. ds1-patch/fixes/000-noop.py is the live
example; the engine behind it is darkfix.patcher
(ds1-patch/scripts/darkfix/: byte edits, GFF chunk
replacement, backup, journal) and the umbrella applier
ds1-patch/scripts/apply.py (verify, backup, apply,
--unapply). GPL data fixes use apply_gff_chunk from the
same module: extract via gff-edit, edit via gpl-asm's
verified --patch output, reinsert.
Three layers:
The hash gate is the applier's selftest: its real-install cycle
applies the shipped fix set to copies of every [target.files]
entry, asserts the patched-file hash recorded in the fix writeup
(any EDITS drift fails the run), verifies, and unapplies
byte-identically:
python3 ds1-patch/scripts/apply.py --selftest(The fix scripts define an apply() function for the applier to
load, not a CLI, so they are not run directly.) The recorded
value lives in the fix's writeup under fixes/; add it there
when you author the fix.
- Apply the patch to a fresh DOSBox install.
- Load the bug-trigger save.
- Run through the trigger. The bug should not fire.
- Run through any nearby triggers (the same NPC, the same region, the same item) to verify nothing else broke.
Periodically: at minor-version boundaries: do a longer playthrough with all enabled fixes on. Log anything unusual. This is the catch-net for "fix A interacts badly with fix B."
- Add the writeup and script to
dsN-patch/fixes/. - Update
dsN-patch/manifest.tomlwith the new fix entry. - Update
known-bugs.md: mark the bug "fixed in vN.M". - Update
patchnotes.mdunder the current "Unreleased" section. - Commit. Suggested message:
dsN: fix.dsN.<short-id>: one-line summary.
(Per house rule, see CLAUDE.md under "Git habits": never push
without explicit approval, and show the commit message first.)
When the next minor version is ready:
- Bump
dsN-patch/VERSION(the single source of truth;manifest.tomlnever carries the version, per spec.md §4). - Move "Unreleased" content in
patchnotes.mdunder a new version heading. - Build the distribution zip:
tools/build-release.sh dsN <version>. The script assembles the flattened zip spec.md §4 defines (manifest.toml, VERSION, apply.py, darkfix/, fixes/, the player README), gates it through the fix-contract checks, a compile pass, and the staged applier's--selftest, then writesdarkfix-dsN-v<version>.zip(default outputscratch/releases/, override with--out; rebuilding an unchanged tree is byte-identical). - Tag the release commit (annotated; the message is that
release's full patchnotes entry, not a summary): extract the
entry verbatim into a file, then
git tag -a darkfix-dsN-vMAJOR.MINOR.PATCH --cleanup=verbatim -F <file> <release-commit>. - Push. Create a GitHub release and attach the zip. From the
next darkfix tag onward the
release zipworkflow does this automatically on the tag push (and on a manual dispatch with a tag input, for a release that already exists).
- Ask in the dsoageofheroes Discord: https://discord.gg/W942xHN72S
- Re-read
upstream-projects.md; the answer is often already in libgff or soloscuro-archive's source. - Sleep on it. Half of the genuinely-hard bugs in projects like this resolve overnight, in the shower, on a walk.