Metadata-Version: 2.5
Name: dm2-tools
Version: 0.5.0
Summary: Python bindings for the Dungeon Master II asset parsers (over the dm2-ffi C ABI)
Project-URL: Homepage, https://git.lordran.net/jqueuniet/dm2-tools
Project-URL: Repository, https://git.lordran.net/jqueuniet/dm2-tools
Project-URL: Changelog, https://git.lordran.net/jqueuniet/dm2-tools/src/branch/main/CHANGELOG.md
Project-URL: Issues, https://git.lordran.net/jqueuniet/dm2-tools/issues
License-Expression: MIT OR Apache-2.0
License-File: LICENSE-APACHE
License-File: LICENSE-MIT
Keywords: bindings,dungeon-master,gamedev,parser
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Games/Entertainment
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Provides-Extra: test
Requires-Dist: pytest; extra == 'test'
Description-Content-Type: text/markdown

# dm2-tools (Python bindings)

Pythonic bindings for the *Dungeon Master II: The Legend of Skullkeep* asset
parsers, layered over the `dm2-ffi` C ABI via `ctypes`. Every function takes
`bytes` in and returns `bytes` / `dict` / `str` / `bool` out; failures raise
`dm2_tools.Dm2Error`.

## Install

```bash
pip install dm2-tools
```

Python 3.11 or newer. Wheels are published for x86-64 Linux (manylinux 2.28)
and x86-64 Windows; each carries the native library, so no Rust toolchain is
needed, and one wheel per platform covers every supported Python — the library
is loaded through `ctypes` rather than linked against a specific interpreter.

There is no source distribution. On macOS and other architectures, install
from a checkout of [the repository](https://git.lordran.net/jqueuniet/dm2-tools)
instead:

```bash
pip install ./bindings/python
```

That runs a hatchling build hook which invokes
`cargo build -p dm2-ffi --release` against the workspace and bundles the
resulting cdylib (`libdm2_ffi.dylib` / `libdm2_ffi.so` / `dm2_ffi.dll`) into
the wheel under `dm2_tools/_lib/`. **A Rust toolchain must be installed and on
`PATH`** for that path.

### Development / override

For local iteration without reinstalling, the module also looks for the
native library at:

1. `dm2_tools/_lib/<lib>` (bundled, present after a real install)
2. the path in the `DM2_FFI_LIB` environment variable, if set
3. `<repo-root>/target/release/<lib>` (the plain `cargo build` output — used
   automatically when running the package straight out of a checkout)

Set `DM2_FFI_LIB=/path/to/libdm2_ffi.so` (or `.dylib` / `.dll`) to point at a
specific build.

## Usage

```python
import dm2_tools

dump = dm2_tools.save_to_json(save_bytes)   # -> dict
dump["gameplay"]["champions"][0]["cur_hp"] = 999
edited = dm2_tools.save_from_json(dump)      # -> bytes
```

JSON-returning functions parse the C ABI's UTF-8 JSON payload with
`json.loads` and return a plain `dict`; JSON-consuming functions accept a
plain `dict` and serialize it with `json.dumps` before calling in.

## API

| Function | Signature | Notes |
|---|---|---|
| `version()` | `() -> str` | Native library version (matches this package's version). |
| `save_to_json(data)` | `bytes -> dict` | Self-contained lossless savegame dump. |
| `save_from_json(dump)` | `dict -> bytes` | Rebuild savegame bytes from a `save_to_json` dump. |
| `save_info_json(data)` | `bytes -> dict` | Lossy, human-readable gameplay-only savegame view. |
| `save_to_json_full(data, dungeon, gdat)` | `(bytes, bytes, bytes) -> dict` | Like `save_to_json`, also decoding the record stream (tile chains, inventories) against the save's `dungeon.dat` and `graphics.dat`. |
| `save_from_json_full(dump, dungeon, gdat)` | `(dict, bytes, bytes) -> bytes` | Rebuild savegame bytes from a `save_to_json_full` dump, edits included. |
| `save_inventory_json(dump, gdat, language="en")` | `(dict, bytes, str) -> dict` | Every champion's held items and the cursor item, named from `gdat` (`en`, `de`, `fr`, `jp`), each with the slots the game lets it go in (`fits`) and the icons the game draws for it there (`frames`, `frame_ms`). |
| `save_inventory_move(dump, src, dst)` | `(dict, str, str) -> dict` | Move a held item; returns the edited dump. Locations are `cursor` or `<hero>:<slot>` (`0:backpack.3`). |
| `save_inventory_swap(dump, a, b)` | `(dict, str, str) -> dict` | Exchange the items at two occupied held locations, under the rules of a move of each. |
| `save_inventory_copy(dump, src, dst)` | `(dict, str, str) -> dict` | Like `save_inventory_move`, leaving the source in place. |
| `save_inventory_clear(dump, loc)` | `(dict, str) -> dict` | Empty a held-item location. |
| `save_container_json(dump, gdat, loc, language="en")` | `(dict, bytes, str, str) -> list[dict]` | Items inside the container held at `loc`, or nested in it (`cursor/in:0`: item 0 of the cursor's container); the index is the one `save_container_take` takes. |
| `save_container_put(dump, gdat, src, container)` | `(dict, bytes, str, str) -> dict` | Move the held item at `src` into a container, held or nested, under the game's rules. |
| `save_container_take(dump, gdat, container, index, dst)` | `(dict, bytes, str, int, str) -> dict` | Move a container item to the empty `dst`; a money box coin stack gives one coin. |
| `save_item_create(dump, gdat, dst, item)` | `(dict, bytes, str, dict) -> dict` | Create `item` (`{"category": "potion", "fields": {"potion_type": 6, "power": 40}}`) at `dst`: an empty held location, or `<container>/in` (`cursor/in:0/in`) to append it to a container. |
| `save_item_preview(dump, gdat, dst, item, language="en")` | `(dict, bytes, str, dict, str) -> dict` | The item `save_item_create` would make at `dst`, shaped like a `save_inventory_json` item. |
| `item_catalog(gdat, language="en")` | `(bytes, str) -> list[dict]` | Item kinds `save_item_create` accepts, with names, icons and settable field ranges. |
| `save_field_limits(variant)` | `str -> dict` | Range, step and display scale of each editable champion field for a dump's `variant`. |
| `skill_level(experience, skill)` | `(list[int], int) -> int` | Level of skill `skill` (0–19) from a champion's 20 experience values. |
| `skill_experience_for_level(experience, skill, level)` | `(list[int], int, int) -> int \| None` | Least storable experience giving `skill` exactly `level`, or `None`. |
| `skill_title(gdat, skill, level, language="en")` | `(bytes, int, int, str) -> dict \| None` | Inventory panel title `{"name", "glyph_name"}` of base skill `skill` at `level`; `None` at level 1. |
| `dungeon_to_json(data)` | `bytes -> dict` | Self-contained lossless `dungeon.dat` dump. |
| `dungeon_from_json(dump)` | `dict -> bytes` | Rebuild `dungeon.dat` bytes from a `dungeon_to_json` dump. |
| `dungeon_skproject_json(data)` | `bytes -> dict` | Lossy skproject-compatible `dungeon.dat` dump. |
| `graphics_header_json(data)` | `bytes -> dict` | Small header summary of a `graphics.dat` (format version, byte order, entry count). |
| `graphics_decode_png(data, cls1, cls2, cls4)` | `(bytes, int, int, int) -> bytes` | Decode the image at class tuple `(cls1, cls2, 0x01, cls4)` to PNG bytes. |
| `graphics_decode_png_transparent(data, cls1, cls2, cls4)` | `(bytes, int, int, int) -> bytes` | Same, as an indexed PNG with the transparent colour in `tRNS`. |
| `graphics_decode_icon_png(data, frames, frame_ms, transparent=True)` | `(bytes, list, int, bool) -> bytes` | An item's `frames` as one PNG; several frames as a looping APNG of `frame_ms` each. |
| `music_detect(data)` | `bytes -> str` | Sniff a standalone music file's container format. |
| `music_hmp_to_smf(data)` | `bytes -> bytes` | Convert a DOS HMI/HMP stream to a Standard MIDI File. |
| `music_render_mod_wav(data)` | `bytes -> bytes` | Render an Amiga ProTracker module to a stereo WAV. |
| `music_snd_to_wav(data)` | `bytes -> bytes` | Decode a Mac `'snd '` resource payload to a mono WAV. |
| `ftl_info_json(data)` | `bytes -> dict` | Small summary of an FTL 68k module container. |
| `ftl_roundtrip_ok(data)` | `bytes -> bool` | `True` if the FTL module re-emits byte-exact. |
| `resfork_list(data)` | `bytes -> list[dict]` | List a Mac resource fork's resources (`type_code`, `id`, `name`, `payload_len`). |
| `resfork_extract(data, type_code, id)` | `(bytes, bytes\|str, int) -> bytes` | Extract one resource's payload by `(type_code, id)`. `type_code` may be 4 raw bytes or a 4-character `str` (encoded as Mac Roman). |

All exceptions raised by the above are `dm2_tools.Dm2Error`.

## Testing

```bash
python -m pytest -q
```

Tests that need workspace fixtures look for them at `<repo-root>/fixtures/`
(override with `DM2_FIXTURES_DIR`); anything whose fixture isn't present
skips cleanly rather than failing.

## Scope

Out of scope for this package, intentionally deferred:

- Publishing to PyPI.
- Prebuilt / multi-platform wheels (CI-built manylinux, macOS universal2,
  Windows, …). Installing always compiles the cdylib locally via Cargo.
