An open API service indexing awesome lists of open source software.

https://github.com/dmang-dev/py-marathon-utils

Read Bungie Marathon / Aleph One data files (maps, shapes, sounds, terminals, physics) in pure Python — M1, M2, Infinity.
https://github.com/dmang-dev/py-marathon-utils

Last synced: 3 days ago
JSON representation

Read Bungie Marathon / Aleph One data files (maps, shapes, sounds, terminals, physics) in pure Python — M1, M2, Infinity.

Awesome Lists containing this project

README

          

# 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.