Tooling to work with asset files from the 1995 video game Dungeon Master II: Legend of Skullkeep
  • Rust 95.4%
  • Python 1.7%
  • Kaitai Struct 1.1%
  • Shell 0.5%
  • GLSL 0.5%
  • Other 0.7%
Find a file
Johann Queuniet a583b10dd0
All checks were successful
CI / msrv (1.90) (pull_request) Successful in 42s
CI / audit (pull_request) Successful in 19s
CI / stable (pull_request) Successful in 1m42s
CI / python bindings (3.11) (pull_request) Successful in 46s
CI / msrv (1.90) (push) Successful in 40s
CI / audit (push) Successful in 19s
CI / stable (push) Successful in 1m42s
CI / python bindings (3.11) (push) Successful in 44s
chore(release): automate the version and changelog surgery with cargo-release
The mechanical half of a release was four hand edits with a documented trap in
each: bump `[workspace.package]`, bump nine inter-crate requirements, bump
`bindings/python/pyproject.toml` — which `cargo set-version` cannot reach, and
whose omission shipped a 0.2.0 wheel disagreeing with its own library — and
then retitle the changelog section, open a fresh one and rewrite two link
references. `cargo release <version>` now does all of it, dry-run by default.

It is configured not to tag, push or publish. Tagging triggers the workflow
that publishes to three registries, so it stays a separate act after the
prepare commit has been reviewed.

Two details the dry run turned up rather than the documentation:

`pre-release-replacements` is inherited by every workspace member and its
paths resolve against that member's directory, so a workspace-level entry goes
looking for `crates/dm2-binrw/CHANGELOG.md`. The replacements therefore sit on
dm2-cli with `../../` paths, and run once.

`{{tag_name}}` is not available to replacements. Used there it is written into
the file literally, behind a warning that is easy to miss in the output — so
the link references use `v{{version}}`.

Verified by running it for real against a throwaway clone: the commit touches
exactly Cargo.toml, Cargo.lock, pyproject.toml and CHANGELOG.md; all ten
version references move; no tag and no push; and the resulting changelog
matches what was written by hand for 0.2.1.
2026-09-05 10:38:26 +02:00
.claude refactor(dm2-saves): remove committed spike; document gdat-mask assumption; assert no width-change 2026-06-27 16:23:54 +02:00
.forgejo feat(release): publish to crates.io, npm and PyPI from the tag 2026-09-05 10:23:38 +02:00
.idea chore(ide): map the GitHub Workflow schema onto the Forgejo workflows 2026-09-04 22:26:48 +02:00
bindings/python fix(python): lead the README with the PyPI install 2026-09-05 08:59:57 +02:00
crates chore(release): automate the version and changelog surgery with cargo-release 2026-09-05 10:38:26 +02:00
docs chore(release): automate the version and changelog surgery with cargo-release 2026-09-05 10:38:26 +02:00
fixtures test(fixtures): add DM2 save-file fixtures (88 files) 2026-07-20 06:54:41 +02:00
formats docs: fix low-severity doc/code inconsistencies 2026-07-22 07:22:34 +02:00
fuzz build: consume opl-audio and sms-audio from crates.io 2026-07-23 10:08:32 +02:00
scripts feat(saves): decode DOS Beta v0.9 gameplay (party + champions) 2026-06-30 20:28:05 +02:00
tooling docs: remove scratch references; promote stranded tooling and a mask correction 2026-07-23 18:51:00 +02:00
.gitignore chore(python): prepare the bindings for wheel distribution 2026-09-04 22:24:12 +02:00
Cargo.lock chore: prepare for v0.2.1 release 2026-09-05 08:25:16 +02:00
Cargo.toml chore(release): automate the version and changelog surgery with cargo-release 2026-09-05 10:38:26 +02:00
CHANGELOG.md chore(release): automate the version and changelog surgery with cargo-release 2026-09-05 10:38:26 +02:00
deny.toml ci(deny): trim unmatched license allowances to comments 2026-07-23 13:59:33 +02:00
LICENSE-APACHE Initial commit 2026-04-26 23:09:03 +02:00
LICENSE-MIT Initial commit 2026-04-26 23:09:03 +02:00
README.md docs: retire ROADMAP.md in favour of Forgejo issues/milestones 2026-07-23 21:50:22 +02:00

dm2-tools

A Cargo workspace of Rust libraries and a CLI for working with Dungeon Master II asset files (the 1995 Legend of Skullkeep).

The goal is to provide reusable, well-tested parsers for the formats the game ships in — graphics.dat, dungeon.dat, music data, savegames — and a single dm2 binary that uses them to extract, convert, and repack assets into the layout downstream consumers expect (notably the unofficial skproject C++ port).

This is tooling only. The runtime engine is out of scope here; if you want to play the game, point the converted output at an engine that knows how to load it.

Layout

crates/
├── dm2-graphics/   library: graphics.dat parser/writer (formats 4/5, LE+BE) + 107 SFX decoder
├── dm2-dungeon/    library: dungeon.dat parser/writer (LE+BE)
├── dm2-music/      library: HMP (DOS), MOD (Amiga), SMF + 'snd ' (Mac resource fork)
├── sms-audio/      library: SoundMusicSys (Mac) song renderer — SMF + Sound Manager sample bank to PCM
├── opl-audio/      library: pure-Rust OPL2 (YM3812) FM renderer — timed MIDI events + AdLib bank to PCM
├── mod-audio/      library: ProTracker (Amiga MOD) renderer, wraps xmrsplayer with Paula-faithful hard panning
├── dm2-saves/      library: SkSave#.dat / .bak full decode/encode (header, gameplay sections, record stream phases 17; DOS + Mac, bit-exact field re-encode)
├── dm2-ftl/        library: FTL 68k module container parser (Sega CD / Amiga) — decompressors + all four checksums
├── dm2-wav/        workspace-internal: shared mono 8-bit WAV writer (consumed by dm2-graphics + dm2-music)
├── dm2-binrw/      workspace-internal: binrw glue (parse/write_to/to_bytes helpers)
├── dm2-test-fixtures/ workspace-internal: fixture-path macros
└── dm2-cli/        binary `dm2`: info, extract, dump, convert, repack, verify, save, render

Editing savegames from the CLI is documented in docs/cli-save-editing.md.

Library crates accept &[u8] or impl Read + Seek; only the CLI does path I/O. This keeps parsers cheap to test and fuzz.

Build

cargo build --workspace
cargo test  --workspace
cargo run -p dm2-cli -- --help

Format specs (Kaitai Struct)

formats/ holds machine-readable .ksy specifications for the DM2 formats we have firm byte-level knowledge of — graphics.dat (format 5, LE+BE), dungeon.dat header (LE+BE), Mac md.dat, DOS songlist.dat, sksave#.dat header. The specs are verified end-to-end against fixtures via the Python kaitaistruct runtime and serve as the canonical declarative reference.

The Rust crates parse via binrw rather than codegen — see formats/README.md for the rationale and the hand-sync convention between .ksy and #[binrw] structs.

Project status

Phases 16 have landed (every format decodes and round-trips; the CLI, tooling, CI and the first crates.io release are done). Remaining and future work is tracked as issues and milestones on the Forgejo project, not in this repo — thematic milestones (cinematics, Amiga deep-dive, bindings, the GUI editors, …) sliced by area/, type/, and port/ labels, with a Kanban board for status. Per-format and per-port reference lives under docs/ (start at docs/sources/overview.md).

Contributing

docs/development.md collects the working conventions: the branch/commit workflow, the pre-push gate that mirrors CI, markdown/doc house style, the source-of-truth hierarchy for reverse-engineering claims, and the Forgejo CI runner quirks.

Format documentation

Format notes live under docs/formats/:

  • graphics-dat.md — the asset archive
  • dungeon-dat.md — level data
  • music.md — DOS HMP, Amiga MOD (Player 4.1A packed), Mac US SMF-via-SoundMusicSys, Sega CD / Mac JP Red Book CD-DA, and the Mac US md.dat / DOS songlist.dat / Amiga CD.DAT music-routing tables
  • saves.mdSkSave#.dat savegames; see its capability matrix for per-port support and conversion
  • qstrat-dat.md — Hint Oracle data (Mac US + Amiga; DOS embeds it inside graphics.dat)
  • mac-resources.md — Mac resource fork reference (the format the entire Mac US soundtrack is delivered in; Mac JP uses Red Book CD-DA instead)

Per-source notes live under docs/sources/; see docs/sources/overview.md for the cross-port doc index, the 16-fixture attribute matrix, and the asset-family / DUNGEON.DAT lineage clusters. Each port has its own extraction-recipe doc next to the overview.

Operational tooling notes — which external CLI tools (binwalk, hexyl, rizin, sox, fluidsynth, kaitai-struct-compiler, …) help with which task — live in docs/tooling.md. None are workspace build dependencies; they are agent-driveable companions for asset extraction and verification work.

External references are collected in docs/external-references.md; format docs cite them by short name. Currently registered: [DMWEB], [DMFORUM], [GREATSTONE], [DM2EDIT], [SKPROJECT], [KKDF2], [IM], [RFC1740], [HMI], [SMF], [SMS], [SCUMMVM].

Test fixtures

No game data is committed. See fixtures/README.md for the layout you can drop your own legitimately-owned copies into so integration tests run against real data.

License

Dual-licensed under MIT or Apache-2.0 at your option. See LICENSE-MIT and LICENSE-APACHE.