Skip to main content

py-marathon-utils

CI Python License: MIT Ruff Checked with mypy

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

Source distribution for py-marathon-utils 0.1.0
File Size Uploaded
py_marathon_utils-0.1.0.tar.gz 94.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for py-marathon-utils 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page