Skip to main content

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

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

py_marathon_utils-0.1.0.tar.gz (94.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

py_marathon_utils-0.1.0-py3-none-any.whl (85.0 kB view details)

Uploaded Python 3

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

Hashes for py_marathon_utils-0.1.0.tar.gz
Algorithm Hash digest
SHA256 aba7c418c6fc7fbd3660c069cf24ba9aa1dfdd42d5c348862d0808411658059a
MD5 c6a4c0e652a44fe761f937f17a191e17
BLAKE2b-256 8ee6622271a797f0ec551bc25ae2c4421200ad04a0754549788405726591a574

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file py_marathon_utils-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for py_marathon_utils-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7b285089c55c0427d0d2094d7242771034256aefed73ed1b37605474c05cea34
MD5 c175bfa6bc8e4af65a361da644422c09
BLAKE2b-256 54b278c0d1a5c69def74cd478e62d1baf23ddbe44f79d5d8faf7afdabd0d702a

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page