{"id":50719054,"url":"https://github.com/dmang-dev/py-marathon-utils","last_synced_at":"2026-06-09T22:01:16.676Z","repository":{"id":361097078,"uuid":"1253066322","full_name":"dmang-dev/py-marathon-utils","owner":"dmang-dev","description":"Read Bungie Marathon / Aleph One data files (maps, shapes, sounds, terminals, physics) in pure Python — M1, M2, Infinity.","archived":false,"fork":false,"pushed_at":"2026-05-29T06:11:05.000Z","size":283,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-29T08:15:58.460Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dmang-dev.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-29T06:00:23.000Z","updated_at":"2026-05-29T06:11:09.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dmang-dev/py-marathon-utils","commit_stats":null,"previous_names":["dmang-dev/py-marathon-utils"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/dmang-dev/py-marathon-utils","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmang-dev%2Fpy-marathon-utils","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmang-dev%2Fpy-marathon-utils/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmang-dev%2Fpy-marathon-utils/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmang-dev%2Fpy-marathon-utils/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmang-dev","download_url":"https://codeload.github.com/dmang-dev/py-marathon-utils/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmang-dev%2Fpy-marathon-utils/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34127345,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-09T22:01:15.584Z","updated_at":"2026-06-09T22:01:16.671Z","avatar_url":"https://github.com/dmang-dev.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# py-marathon-utils\n\n[![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)\n[![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)\n[![Ruff](https://img.shields.io/badge/lint-ruff-261230)](https://github.com/astral-sh/ruff)\n[![Checked with mypy](https://img.shields.io/badge/mypy-checked-2a6db2)](https://mypy-lang.org/)\n\nRead Bungie Marathon / Aleph One data files (maps, sprites, sounds) in pure Python.\n\nA clean-room Python port of the byte-level parsing in\n[Hopper262/marathon-utils](https://github.com/Hopper262/marathon-utils), plus\nsome extension for formats that upstream doesn't cover. Supports all three\nMarathon games shipped by Aleph One:\n\n| Game | Map | Shapes | Sounds | Levels |\n|---|---|---|---|---|\n| **Marathon 1** (M1A1) | ✅ | ✅ | ✅ | 37 |\n| **Marathon 2: Durandal** | ✅ | ✅ | ✅ | 41 |\n| **Marathon Infinity** | ✅ | ✅ | ✅ | 57 |\n\nUseful if you're modding, porting Marathon to a different engine, building a\nlevel previewer, or just poking at the data files for fun.\n\nMap parsing for M1 is cross-validated against the reference Perl\nimplementation: **bit-exact** on all 7,595 polygons across 37 levels\n(see [tests/test_perl_parity.py](tests/test_perl_parity.py)).\n\n## Install\n\n```bash\npip install py-marathon-utils                  # core (stdlib only)\npip install \"py-marathon-utils[images]\"        # adds shape/terminal/image PNG output (Pillow)\npip install \"py-marathon-utils[dev]\"           # dev: pytest, ruff, mypy\n```\n\nPython 3.8+. The image-producing features (shapes, terminals, images, marines,\nvisualize) need the `[images]` extra; everything else is stdlib-only.\n\n### Build from source\n\n```bash\npip install build\npython -m build           # produces dist/*.whl and *.tar.gz\n```\n\nThe terminal renderer's bitmap fonts are bundled in the wheel (generated from\nthe SIL-OFL Courier Prime font — see `scripts/generate_fonts.py`), so terminal\nrendering works out of the box with no external font files.\n\n## Quick start\n\n### CLI\n\n```bash\n# Marathon 1\nmarathon-utils extract maps    Map.scen    out/Maps      # per-level JSON\nmarathon-utils extract sounds  Sounds.sndz out/Sounds    # WAV files\nmarathon-utils extract shapes  Shapes.shps out/Shapes    # sprite/texture PNGs\nmarathon-utils visualize       Map.scen    out/PNG       # top-down level images\n\n# Marathon 2 / Infinity (same commands, different file extensions)\nmarathon-utils extract maps    Map.sceA    out/Maps\nmarathon-utils extract sounds  Sounds.sndA out/Sounds\nmarathon-utils extract shapes  Shapes.shpA out/Shapes\nmarathon-utils visualize       Map.sceA    out/PNG\n\n# Physics models (M2/Infinity Standard.phyA — fully decoded per-record JSON)\nmarathon-utils extract physics \"Standard.phyA\"  out/Physics\n\n# Strings + terminal lore (Marathon.appl resource fork)\nmarathon-utils extract strings  Marathon.appl   out/Strings\n\n# Anvil shape patches (community mod packs)\nmarathon-utils extract patches  some_pack.patch out/Patches\n\n# Terminal screens — renders each terminal page as a PNG.\n# Auto-detects M1 (compiles Marathon.appl scripts) vs M2/Infinity (map chunks).\nmarathon-utils extract terminals Map.sceA      out/Terminals\nmarathon-utils extract terminals Marathon.appl out/Terminals_M1\n\n# Marine player sprites (Samsara Doom-mod helper) — composited torso+leg PNGs\nmarathon-utils marines Shapes.shpA out/Marines              # one per view\nmarathon-utils marines Shapes.shpA out/Marines --full-animation   # ~23k frames\n\n# Chapter screens / title art (M2/Infinity Images.imgA) — decodes PICTs to PNG\nmarathon-utils extract images Images.imgA out/Images\n```\n\nFormat auto-detection means you don't need to tell the CLI which Marathon\nversion a file is from — it figures it out from the bytes.\n\n### Library\n\n```python\nfrom marathon_utils import macbinary, wad, maps, sounds\n\n# Unwrap MacBinary II\ndata, rsrc, meta = macbinary.unwrap_file(\"Map.scen\")\n\n# Walk the WAD\nheader = wad.read_header(data)\nprint(f\"M1 WAD v{header['version']}: {header['wad_count']} levels named {header['name']!r}\")\n\nfor entry in wad.read_directory(data, header):\n    for tag, payload in wad.read_chunks(data, entry, header['entry_header_size']):\n        print(entry['index'], wad.tag_str(tag), len(payload))\n\n# High-level extractors\nresult = maps.extract(\"Map.scen\", \"out/Maps\")\nfor lev in result['levels'][:3]:\n    print(f\"{lev['index']:\u003e2} {lev['name']!r}  \"\n          f\"polygons={lev['polygon_count']} lights={len(lev.get('LITE') or [])}\")\n```\n\n## What it can do\n\n| File (M1 / M2-MI) | Reader | Output | Status |\n|---|---|---|---|\n| `Map.scen` / `Map.sceA` | `marathon_utils.maps` | per-level JSON (geometry, lights, objects, terminal text) | ✅ M1 + M2 + Infinity |\n| `Sounds.sndz` / `Sounds.sndA` | `marathon_utils.sounds` | 16-bit WAV files | ✅ M1 (Mac rsrc) + M2/Infinity (snd2) |\n| `Shapes.shps` / `Shapes.shpA` | `marathon_utils.shapes` | per-collection palette + per-shape PNG | ✅ M1 (RLE) + M2/Infinity (sparse) |\n| `Standard.phyA` / `Physics.phys` | `marathon_utils.physics` | per-record JSON (monsters, weapons, projectiles, effects, player physics) | ✅ M1 (mons/effe/proj/phys/weap) + M2 + Infinity |\n| Anvil patches (community mod packs) | `marathon_utils.patches` | parsed override records, `apply()` overlay, and `write()` round-trip | ✅ |\n| Terminal screens (all 3 games) | `marathon_utils.terminals` | per-page PNGs in the classic green-on-black look | ✅ M1 (compiled scripts) + M2 + Infinity |\n| `Marathon.appl` (resource fork) | `marathon_utils.strings` | STR / STR# / TEXT / M1 terminal scripts + `clut`/`nrct`/`finf` → MML | ✅ |\n| Shapes writer | `marathon_utils.shapes.write_m2` | round-trip parsed collections back to a binary `.shpA` (8-bit + 16-bit banks) | ✅ M2 / Infinity |\n| Marine player sprites | `marathon_utils.samsara` | composited torso+leg PNGs (Samsara Doom-mod helper) | ✅ M2 / Infinity |\n| `Images.imgA` chapter art | `marathon_utils.images` | title/chapter screens → PNG (QuickDraw PICT v2 decoder) | ✅ M2 / Infinity |\n| any WAD | `marathon_utils.wad` | walk chunks programmatically | ✅ M1 v0 + M2 v2 + Infinity v4 |\n| MacBinary II | `marathon_utils.macbinary` | unwrap to data+rsrc forks | ✅ |\n| Mac OS resource fork | `marathon_utils.macrsrc` | typed `{resource_type: [{id, name, data}, ...]}` | ✅ |\n\nPlus a top-down map visualizer (`marathon_utils.visualize`) that renders each\nlevel as a PNG, a terminal **location** finder and **HTML preview** generator\n(`terminals.terminal_locations` / `terminals.generate_html_preview`), and an\nM1 terminal-script **compiler** (`terminals.compile_m1_script`).\n\n### Version-specific notes\n\n- **M1 maps** use 32-byte LITE records and use the `plat` (lowercase) chunk\n  for platforms.\n- **M2 / Infinity maps** use 100-byte LITE records with six function blocks\n  (primary/secondary/becoming-active and -inactive states), use the `PLAT`\n  (uppercase) chunk format, and add `medi` / `ambi` / `bonk` chunks for media,\n  ambient sound images, and random sound images. Directory entries embed the\n  64-byte level name (no need to read the `NAME` chunk).\n- **Infinity** adds **per-level embedded physics chunks** (`MNpx`, `FXpx`,\n  `PRpx`, `PXpx`, `WPpx`) that let each level customize monster/projectile/\n  weapon/physics constants. They're preserved as raw bytes in the JSON for now.\n- **Shape files**: M1 stores collections as `.256` Mac resources with row/\n  column int16-opcode RLE bitmaps; M2+ uses a flat 32-entry collection table\n  with column-major sparse `(first_row, last_row, pixels)` bitmaps. Each M2+\n  table slot can hold an 8-bit bank and a 16-bit bank (~5 collections ship the\n  higher-color 16-bit art); both are read, rendered, and round-tripped.\n- **Sound files**: M1 uses classic Mac `snd ` resources; M2/Infinity use a\n  custom `snd2` container with per-sound permutations. Both support stdSH\n  (8-bit unsigned), extSH (multi-channel/16-bit), and cmpSH \"twos\" (signed\n  8-bit) headers.\n\n## What it doesn't do\n\nThe full marathon-utils \"wishlist\" (everything that touches M1/M2/Infinity +\nAleph One) is ported. The remaining upstream scripts are deliberately out of\nscope:\n\n- **Historical prerelease formats** — Marathon 2 Preview Shapes\n  (`prevshapes2xml.pl`) and the M1 Alpha/January/May/June beta shape variants\n  (`betas/*.pl`). Marathon archaeology for snapshots almost nobody has.\n- **Marathon: Durandal XBLA assets** (`cma2wavs.pl`, `cmt2dds.pl`,\n  `live2dir.pl`, `mark2dir.pl`) — a separate codebase for a separate game.\n\n## Format reference\n\nByte-level layouts for all supported formats are documented in\n[`docs/format-reference.md`](docs/format-reference.md). If you're writing a\nparser in another language, that doc is the easiest read.\n\n## Cross-validation\n\n`tests/test_perl_parity.py` runs the upstream `map2xml.pl` (if Perl is on PATH)\nand compares its XML output to ours. Currently bit-exact for all M1 maps.\n\n```bash\npytest tests/test_perl_parity.py -v\n```\n\nThe Anvil patches module is additionally validated against a real community\npatch from Simplici7y (the CTF Flag Shapes Patch by Juice — 67×148 flag\nsprites at items[14] and items[15]). Run this once to fetch it:\n\n```bash\npython scripts/fetch_sample_patches.py\npytest tests/test_patches.py::test_real_world_ctf_flag_patch -v\n```\n\n## License\n\n[MIT](LICENSE). Use it for whatever — modding, ports, ROM-archaeology, your\nside project.\n\n## Acknowledgements\n\n- **[Hopper262](https://github.com/Hopper262/marathon-utils)** — the Perl\n  scripts whose byte-layout decoders this port is based on. These are the\n  reference implementation; this library is a clean-room idiomatic Python\n  translation of the format knowledge.\n- **[Aleph One](https://github.com/Aleph-One-Marathon/alephone)** — the open\n  source Marathon engine, source of truth for any format ambiguity.\n- **[Bungie](https://www.bungie.net/)** for making Marathon and later\n  releasing the source.\n\n## Disclaimer\n\nThis is a third-party tool. Marathon and its assets are property of Bungie.\nAleph One's free distribution license for the game data does not transfer\nto derivative projects; if you extract assets with this library, treat them\nas Bungie IP for redistribution purposes.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmang-dev%2Fpy-marathon-utils","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmang-dev%2Fpy-marathon-utils","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmang-dev%2Fpy-marathon-utils/lists"}