Read Bungie Marathon / Aleph One data files (maps, shapes, sounds) in pure Python.
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file py_marathon_utils-0.1.0.tar.gz.
File metadata
- Download URL: py_marathon_utils-0.1.0.tar.gz
- Upload date:
- Size: 94.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aba7c418c6fc7fbd3660c069cf24ba9aa1dfdd42d5c348862d0808411658059a
|
|
| MD5 |
c6a4c0e652a44fe761f937f17a191e17
|
|
| BLAKE2b-256 |
8ee6622271a797f0ec551bc25ae2c4421200ad04a0754549788405726591a574
|
Provenance
The following attestation bundles were made for py_marathon_utils-0.1.0.tar.gz:
Publisher:
release.yml on dmang-dev/py-marathon-utils
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
py_marathon_utils-0.1.0.tar.gz -
Subject digest:
aba7c418c6fc7fbd3660c069cf24ba9aa1dfdd42d5c348862d0808411658059a - Sigstore transparency entry: 1666018590
- Sigstore integration time:
-
Permalink:
dmang-dev/py-marathon-utils@684b6536a8a2a20d5cb9e579b85898ef9ac5aad1 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/dmang-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@684b6536a8a2a20d5cb9e579b85898ef9ac5aad1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file py_marathon_utils-0.1.0-py3-none-any.whl.
File metadata
- Download URL: py_marathon_utils-0.1.0-py3-none-any.whl
- Upload date:
- Size: 85.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7b285089c55c0427d0d2094d7242771034256aefed73ed1b37605474c05cea34
|
|
| MD5 |
c175bfa6bc8e4af65a361da644422c09
|
|
| BLAKE2b-256 |
54b278c0d1a5c69def74cd478e62d1baf23ddbe44f79d5d8faf7afdabd0d702a
|
Provenance
The following attestation bundles were made for py_marathon_utils-0.1.0-py3-none-any.whl:
Publisher:
release.yml on dmang-dev/py-marathon-utils
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
py_marathon_utils-0.1.0-py3-none-any.whl -
Subject digest:
7b285089c55c0427d0d2094d7242771034256aefed73ed1b37605474c05cea34 - Sigstore transparency entry: 1666018750
- Sigstore integration time:
-
Permalink:
dmang-dev/py-marathon-utils@684b6536a8a2a20d5cb9e579b85898ef9ac5aad1 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/dmang-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@684b6536a8a2a20d5cb9e579b85898ef9ac5aad1 -
Trigger Event:
push
-
Statement type: