py-marathon-utils
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, 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).
Install
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
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
# 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
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 addmedi/ambi/bonkchunks for media, ambient sound images, and random sound images. Directory entries embed the 64-byte level name (no need to read theNAMEchunk). - 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
.256Mac 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
sndresources; M2/Infinity use a customsnd2container 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. 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.
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:
python scripts/fetch_sample_patches.py
pytest tests/test_patches.py::test_real_world_ctf_flag_patch -v
License
MIT. Use it for whatever — modding, ports, ROM-archaeology, your side project.
Acknowledgements
- Hopper262 — 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 — the open source Marathon engine, source of truth for any format ambiguity.
- Bungie 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.
Metadata
Release files for py-marathon-utils 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| py_marathon_utils-0.1.0.tar.gz | 94.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| py_marathon_utils-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 179.7 kB
Release files / py_marathon_utils-0.1.0.tar.gz
| Download URL | py_marathon_utils-0.1.0.tar.gz |
|---|---|
| Size | 94.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
aba7c418c6fc7fbd3660c069cf24ba9aa1dfdd42d5c348862d0808411658059a
|
|
BLAKE2b-256 checksum How to use checksums |
8ee6622271a797f0ec551bc25ae2c4421200ad04a0754549788405726591a574
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 29, 2026.
Transparency logRelease files / py_marathon_utils-0.1.0-py3-none-any.whl
| Download URL | py_marathon_utils-0.1.0-py3-none-any.whl |
|---|---|
| Size | 85.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7b285089c55c0427d0d2094d7242771034256aefed73ed1b37605474c05cea34
|
|
BLAKE2b-256 checksum How to use checksums |
54b278c0d1a5c69def74cd478e62d1baf23ddbe44f79d5d8faf7afdabd0d702a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 29, 2026.
Transparency log