Metadata-Version: 2.4
Name: py-marathon-utils
Version: 0.1.0
Summary: Read Bungie Marathon / Aleph One data files (maps, shapes, sounds) in pure Python.
Author: dmang-dev
License: MIT License
        
        Copyright (c) 2026 dmang-dev
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in
        all copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
        THE SOFTWARE.
        
Project-URL: Homepage, https://github.com/dmang-dev/py-marathon-utils
Project-URL: Repository, https://github.com/dmang-dev/py-marathon-utils
Project-URL: Issues, https://github.com/dmang-dev/py-marathon-utils/issues
Project-URL: Reference, https://github.com/Aleph-One-Marathon/alephone
Keywords: marathon,bungie,aleph-one,wad,macbinary,retro-gaming,modding
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Games/Entertainment :: First Person Shooters
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Archiving
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: images
Requires-Dist: Pillow>=9.1; extra == "images"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: Pillow>=9.1; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: mypy>=1.5; extra == "dev"
Dynamic: license-file

# py-marathon-utils

[![CI](https://github.com/dmang-dev/py-marathon-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/dmang-dev/py-marathon-utils/actions/workflows/ci.yml)
[![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![Ruff](https://img.shields.io/badge/lint-ruff-261230)](https://github.com/astral-sh/ruff)
[![Checked with mypy](https://img.shields.io/badge/mypy-checked-2a6db2)](https://mypy-lang.org/)

Read Bungie Marathon / Aleph One data files (maps, sprites, sounds) in pure Python.

A clean-room Python port of the byte-level parsing in
[Hopper262/marathon-utils](https://github.com/Hopper262/marathon-utils), plus
some extension for formats that upstream doesn't cover. Supports all three
Marathon games shipped by Aleph One:

| Game | Map | Shapes | Sounds | Levels |
|---|---|---|---|---|
| **Marathon 1** (M1A1) | ✅ | ✅ | ✅ | 37 |
| **Marathon 2: Durandal** | ✅ | ✅ | ✅ | 41 |
| **Marathon Infinity** | ✅ | ✅ | ✅ | 57 |

Useful if you're modding, porting Marathon to a different engine, building a
level previewer, or just poking at the data files for fun.

Map parsing for M1 is cross-validated against the reference Perl
implementation: **bit-exact** on all 7,595 polygons across 37 levels
(see [tests/test_perl_parity.py](tests/test_perl_parity.py)).

## Install

```bash
pip install py-marathon-utils                  # core (stdlib only)
pip install "py-marathon-utils[images]"        # adds shape/terminal/image PNG output (Pillow)
pip install "py-marathon-utils[dev]"           # dev: pytest, ruff, mypy
```

Python 3.8+. The image-producing features (shapes, terminals, images, marines,
visualize) need the `[images]` extra; everything else is stdlib-only.

### Build from source

```bash
pip install build
python -m build           # produces dist/*.whl and *.tar.gz
```

The terminal renderer's bitmap fonts are bundled in the wheel (generated from
the SIL-OFL Courier Prime font — see `scripts/generate_fonts.py`), so terminal
rendering works out of the box with no external font files.

## Quick start

### CLI

```bash
# Marathon 1
marathon-utils extract maps    Map.scen    out/Maps      # per-level JSON
marathon-utils extract sounds  Sounds.sndz out/Sounds    # WAV files
marathon-utils extract shapes  Shapes.shps out/Shapes    # sprite/texture PNGs
marathon-utils visualize       Map.scen    out/PNG       # top-down level images

# Marathon 2 / Infinity (same commands, different file extensions)
marathon-utils extract maps    Map.sceA    out/Maps
marathon-utils extract sounds  Sounds.sndA out/Sounds
marathon-utils extract shapes  Shapes.shpA out/Shapes
marathon-utils visualize       Map.sceA    out/PNG

# Physics models (M2/Infinity Standard.phyA — fully decoded per-record JSON)
marathon-utils extract physics "Standard.phyA"  out/Physics

# Strings + terminal lore (Marathon.appl resource fork)
marathon-utils extract strings  Marathon.appl   out/Strings

# Anvil shape patches (community mod packs)
marathon-utils extract patches  some_pack.patch out/Patches

# Terminal screens — renders each terminal page as a PNG.
# Auto-detects M1 (compiles Marathon.appl scripts) vs M2/Infinity (map chunks).
marathon-utils extract terminals Map.sceA      out/Terminals
marathon-utils extract terminals Marathon.appl out/Terminals_M1

# Marine player sprites (Samsara Doom-mod helper) — composited torso+leg PNGs
marathon-utils marines Shapes.shpA out/Marines              # one per view
marathon-utils marines Shapes.shpA out/Marines --full-animation   # ~23k frames

# Chapter screens / title art (M2/Infinity Images.imgA) — decodes PICTs to PNG
marathon-utils extract images Images.imgA out/Images
```

Format auto-detection means you don't need to tell the CLI which Marathon
version a file is from — it figures it out from the bytes.

### Library

```python
from marathon_utils import macbinary, wad, maps, sounds

# Unwrap MacBinary II
data, rsrc, meta = macbinary.unwrap_file("Map.scen")

# Walk the WAD
header = wad.read_header(data)
print(f"M1 WAD v{header['version']}: {header['wad_count']} levels named {header['name']!r}")

for entry in wad.read_directory(data, header):
    for tag, payload in wad.read_chunks(data, entry, header['entry_header_size']):
        print(entry['index'], wad.tag_str(tag), len(payload))

# High-level extractors
result = maps.extract("Map.scen", "out/Maps")
for lev in result['levels'][:3]:
    print(f"{lev['index']:>2} {lev['name']!r}  "
          f"polygons={lev['polygon_count']} lights={len(lev.get('LITE') or [])}")
```

## What it can do

| File (M1 / M2-MI) | Reader | Output | Status |
|---|---|---|---|
| `Map.scen` / `Map.sceA` | `marathon_utils.maps` | per-level JSON (geometry, lights, objects, terminal text) | ✅ M1 + M2 + Infinity |
| `Sounds.sndz` / `Sounds.sndA` | `marathon_utils.sounds` | 16-bit WAV files | ✅ M1 (Mac rsrc) + M2/Infinity (snd2) |
| `Shapes.shps` / `Shapes.shpA` | `marathon_utils.shapes` | per-collection palette + per-shape PNG | ✅ M1 (RLE) + M2/Infinity (sparse) |
| `Standard.phyA` / `Physics.phys` | `marathon_utils.physics` | per-record JSON (monsters, weapons, projectiles, effects, player physics) | ✅ M1 (mons/effe/proj/phys/weap) + M2 + Infinity |
| Anvil patches (community mod packs) | `marathon_utils.patches` | parsed override records, `apply()` overlay, and `write()` round-trip | ✅ |
| Terminal screens (all 3 games) | `marathon_utils.terminals` | per-page PNGs in the classic green-on-black look | ✅ M1 (compiled scripts) + M2 + Infinity |
| `Marathon.appl` (resource fork) | `marathon_utils.strings` | STR / STR# / TEXT / M1 terminal scripts + `clut`/`nrct`/`finf` → MML | ✅ |
| Shapes writer | `marathon_utils.shapes.write_m2` | round-trip parsed collections back to a binary `.shpA` (8-bit + 16-bit banks) | ✅ M2 / Infinity |
| Marine player sprites | `marathon_utils.samsara` | composited torso+leg PNGs (Samsara Doom-mod helper) | ✅ M2 / Infinity |
| `Images.imgA` chapter art | `marathon_utils.images` | title/chapter screens → PNG (QuickDraw PICT v2 decoder) | ✅ M2 / Infinity |
| any WAD | `marathon_utils.wad` | walk chunks programmatically | ✅ M1 v0 + M2 v2 + Infinity v4 |
| MacBinary II | `marathon_utils.macbinary` | unwrap to data+rsrc forks | ✅ |
| Mac OS resource fork | `marathon_utils.macrsrc` | typed `{resource_type: [{id, name, data}, ...]}` | ✅ |

Plus a top-down map visualizer (`marathon_utils.visualize`) that renders each
level as a PNG, a terminal **location** finder and **HTML preview** generator
(`terminals.terminal_locations` / `terminals.generate_html_preview`), and an
M1 terminal-script **compiler** (`terminals.compile_m1_script`).

### Version-specific notes

- **M1 maps** use 32-byte LITE records and use the `plat` (lowercase) chunk
  for platforms.
- **M2 / Infinity maps** use 100-byte LITE records with six function blocks
  (primary/secondary/becoming-active and -inactive states), use the `PLAT`
  (uppercase) chunk format, and add `medi` / `ambi` / `bonk` chunks for media,
  ambient sound images, and random sound images. Directory entries embed the
  64-byte level name (no need to read the `NAME` chunk).
- **Infinity** adds **per-level embedded physics chunks** (`MNpx`, `FXpx`,
  `PRpx`, `PXpx`, `WPpx`) that let each level customize monster/projectile/
  weapon/physics constants. They're preserved as raw bytes in the JSON for now.
- **Shape files**: M1 stores collections as `.256` Mac resources with row/
  column int16-opcode RLE bitmaps; M2+ uses a flat 32-entry collection table
  with column-major sparse `(first_row, last_row, pixels)` bitmaps. Each M2+
  table slot can hold an 8-bit bank and a 16-bit bank (~5 collections ship the
  higher-color 16-bit art); both are read, rendered, and round-tripped.
- **Sound files**: M1 uses classic Mac `snd ` resources; M2/Infinity use a
  custom `snd2` container with per-sound permutations. Both support stdSH
  (8-bit unsigned), extSH (multi-channel/16-bit), and cmpSH "twos" (signed
  8-bit) headers.

## What it doesn't do

The full marathon-utils "wishlist" (everything that touches M1/M2/Infinity +
Aleph One) is ported. The remaining upstream scripts are deliberately out of
scope:

- **Historical prerelease formats** — Marathon 2 Preview Shapes
  (`prevshapes2xml.pl`) and the M1 Alpha/January/May/June beta shape variants
  (`betas/*.pl`). Marathon archaeology for snapshots almost nobody has.
- **Marathon: Durandal XBLA assets** (`cma2wavs.pl`, `cmt2dds.pl`,
  `live2dir.pl`, `mark2dir.pl`) — a separate codebase for a separate game.

## Format reference

Byte-level layouts for all supported formats are documented in
[`docs/format-reference.md`](docs/format-reference.md). If you're writing a
parser in another language, that doc is the easiest read.

## Cross-validation

`tests/test_perl_parity.py` runs the upstream `map2xml.pl` (if Perl is on PATH)
and compares its XML output to ours. Currently bit-exact for all M1 maps.

```bash
pytest tests/test_perl_parity.py -v
```

The Anvil patches module is additionally validated against a real community
patch from Simplici7y (the CTF Flag Shapes Patch by Juice — 67×148 flag
sprites at items[14] and items[15]). Run this once to fetch it:

```bash
python scripts/fetch_sample_patches.py
pytest tests/test_patches.py::test_real_world_ctf_flag_patch -v
```

## License

[MIT](LICENSE). Use it for whatever — modding, ports, ROM-archaeology, your
side project.

## Acknowledgements

- **[Hopper262](https://github.com/Hopper262/marathon-utils)** — the Perl
  scripts whose byte-layout decoders this port is based on. These are the
  reference implementation; this library is a clean-room idiomatic Python
  translation of the format knowledge.
- **[Aleph One](https://github.com/Aleph-One-Marathon/alephone)** — the open
  source Marathon engine, source of truth for any format ambiguity.
- **[Bungie](https://www.bungie.net/)** for making Marathon and later
  releasing the source.

## Disclaimer

This is a third-party tool. Marathon and its assets are property of Bungie.
Aleph One's free distribution license for the game data does not transfer
to derivative projects; if you extract assets with this library, treat them
as Bungie IP for redistribution purposes.
