Skip to content

Instantly share code, notes, and snippets.

@yuriks
Created November 30, 2024 15:45
Show Gist options
  • Save yuriks/020fba929f0030d9d8f0f7844d29394d to your computer and use it in GitHub Desktop.
Save yuriks/020fba929f0030d9d8f0f7844d29394d to your computer and use it in GitHub Desktop.
Super Metroid ROM data splitter
#!/usr/bin/env python3
#
# Copyright (c) 2022 yuriks
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import argparse
from pathlib import Path
import hashlib
import sys
SM_NTSC_CRC32 = "D63ED5F8"
SM_NTSC_SHA256 = "12b77c4bc9c1832cee8881244659065ee1d84c70c3d29e6eaf92e6798cc2ca72"
def make_argument_parser():
parser = argparse.ArgumentParser(
description="Splits out data from an SM rom into separate data files for use with the disassembly.")
parser.add_argument('rom_file', type=argparse.FileType('rb'),
help="Path to input ROM file. Must be an unheadered SM NTSC ROM.")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--output', '-o', type=Path,
help="Directory to output split files to. Will be created if it doesn't exist.")
group.add_argument('--check', '-c', action='store_true',
help="Simply check validity of the file instead of splitting anything.")
return parser
def addr_to_lorom_offset(addr):
bank = addr >> 16
bank_offset = (addr & 0xFFFF) - 0x8000
if bank < 0x80 or bank_offset < 0:
raise ValueError(f"{addr:X} is not a valid LoROM address")
return (bank - 0x80) * 0x8000 + bank_offset
def rom_valid(rom_data):
if len(rom_data) != 0x300000: # 3MB
return False
if hashlib.sha256(rom_data).hexdigest() != SM_NTSC_SHA256:
return False
return True
def write_file(path, data):
try:
print(f"Writing {path}...", file=sys.stderr)
path.write_bytes(data)
except FileNotFoundError:
# Try to create parent directory and try again
print(f"Creating {path.parent}...")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
def dump_rom(rom_data, output_path):
def dump_range(lorom_addr_start, lorom_addr_end, filename):
offset_start = addr_to_lorom_offset(lorom_addr_start)
offset_end = addr_to_lorom_offset(lorom_addr_end)
if offset_start >= len(rom_data) or offset_end >= len(rom_data):
raise ValueError("File offset out of bounds")
data_to_dump = rom_data[offset_start:offset_end + 1]
write_file(output_path / filename, data_to_dump)
def dump(lorom_addr, length, filename):
dump_range(lorom_addr, lorom_addr + length - 1, filename)
dump(0x80_8000, 0x8000, "bank_80.bin") # System routines
dump(0x81_8000, 0x8000, "bank_81.bin") # SRAM, spritemap processing & menus
dump(0x82_8000, 0x8000, "bank_82.bin") # Top level main game routines
dump(0x83_8000, 0x8000, "bank_83.bin") # FX and door definitions
dump(0x84_8000, 0x8000, "bank_84.bin") # PLMs
dump(0x85_8000, 0x8000, "bank_85.bin") # Message boxes
dump(0x86_8000, 0x8000, "bank_86.bin") # Enemy projectiles
dump(0x87_8000, 0x8000, "bank_87.bin") # Animated tiles
dump(0x88_8000, 0x8000, "bank_88.bin") # HDMA
dump(0x89_8000, 0x8000, "bank_89.bin") # Item PLM graphics, FX loader
dump(0x8A_8000, 0x8000, "bank_8a.bin") # FX tilemaps
dump(0x8B_8000, 0x8000, "bank_8b.bin") # Non gameplay routines
dump(0x8C_8000, 0x8000, "bank_8c.bin") # Title sequence and intro
dump(0x8D_8000, 0x8000, "bank_8d.bin") # Enemy projectile spritemaps, palette FX objects
dump(0x8E_8000, 0x8000, "bank_8e.bin") # Menu tiles
dump(0x8F_8000, 0x8000, "bank_8f.bin") # Rooms definitions
dump(0x90_8000, 0x8000, "bank_90.bin") # Samus
dump(0x91_8000, 0x8000, "bank_91.bin") # Aran
dump(0x92_8000, 0x8000, "bank_92.bin") # Samus animations
dump(0x93_8000, 0x8000, "bank_93.bin") # Projectiles
dump(0x94_8000, 0x8000, "bank_94.bin") # Block properties, some cutscene graphics
dump(0x95_8000, 0x8000, "bank_95.bin") # Cutscene graphics {
dump(0x96_8000, 0x8000, "bank_96.bin")
dump(0x97_8000, 0x8000, "bank_97.bin")
dump(0x98_8000, 0x8000, "bank_98.bin")
dump(0x99_8000, 0x8000, "bank_99.bin") # }
dump(0x9A_8000, 0x8000, "bank_9a.bin") # Projectile and map graphics
dump(0x9B_8000, 0x8000, "bank_9b.bin") # Grapple beam and Samus graphics
dump(0x9C_8000, 0x8000, "bank_9c.bin") # Samus graphics {
dump(0x9D_8000, 0x8000, "bank_9d.bin")
dump(0x9E_8000, 0x8000, "bank_9e.bin")
dump(0x9F_8000, 0x8000, "bank_9f.bin") # }
dump(0xA0_8000, 0x8000, "bank_a0.bin") # Enemies
dump(0xA1_8000, 0x8000, "bank_a1.bin") # Enemy population
dump(0xA2_8000, 0x8000, "bank_a2.bin") # Enemy AI - inc. gunship & shutters
dump(0xA3_8000, 0x8000, "bank_a3.bin") # Enemy AI - inc. elevator & metroid
dump(0xA4_8000, 0x8000, "bank_a4.bin") # Enemy AI - Crocomire
dump(0xA5_8000, 0x8000, "bank_a5.bin") # Enemy AI - Draygon & Spore Spawn
dump(0xA6_8000, 0x8000, "bank_a6.bin") # Enemy AI - inc. Ridley & zebetites
dump(0xA7_8000, 0x8000, "bank_a7.bin") # Enemy AI - inc. Kraid & Phantoon
dump(0xA8_8000, 0x8000, "bank_a8.bin") # Enemy AI - inc. ki-hunter
dump(0xA9_8000, 0x8000, "bank_a9.bin") # Enemy AI - Mother Brain, Shitroid & dead monsters
dump(0xAA_8000, 0x8000, "bank_aa.bin") # Enemy AI - inc. Torizo & Tourian statue
dump(0xAB_8000, 0x8000, "bank_ab.bin") # Enemy graphics - inc. Kraid
dump(0xAC_8000, 0x8000, "bank_ac.bin") # Enemy graphics - inc. Spore Spawn & Phantoon
dump(0xAD_8000, 0x8000, "bank_ad.bin") # Enemy graphics - Crocomire, gunship & space pirates; extra Mother Brain code
dump(0xAE_8000, 0x8000, "bank_ae.bin") # Enemy graphics
dump(0xAF_8000, 0x8000, "bank_af.bin") # Enemy graphics - inc. Torizo
dump(0xB0_8000, 0x8000, "bank_b0.bin") # Enemy graphics - inc. Ridley, Draygon & Mother Brain
dump(0xB1_8000, 0x8000, "bank_b1.bin") # Enemy graphics - inc. Shitroid & ki-hunter
dump(0xB2_8000, 0x8000, "bank_b2.bin") # Enemy AI - space pirates
dump(0xB3_8000, 0x8000, "bank_b3.bin") # Enemy AI - inc. Botwoon
dump(0xB4_8000, 0x8000, "bank_b4.bin") # Enemy instructions, sets, drop chances, resistances
dump(0xB5_8000, 0x8000, "bank_b5.bin") # Region maps
dump(0xB6_8000, 0x8000, "bank_b6.bin") # Pause screen graphics
dump(0xB7_8000, 0x8000, "bank_b7.bin") # Enemy graphics - inc. dead enemies, Botwoon & Mother Brain
# Bank B8 is unused (apparently extra dev RAM)
# Banks B9..CE: CRE, background images, tile graphics, tile tables, palettes, level data
dump_range(0xB9_8000, 0xCE_FFFF, "bank_data.bin")
# Banks CF..DF: Music
dump_range(0xCF_8000, 0xDF_FFFF, "bank_music.bin")
def main():
args = make_argument_parser().parse_args()
with args.rom_file as inf:
rom_data = inf.read()
if not rom_valid(rom_data):
print("Invalid ROM. Ensure it's unheadered and the NTSC version:\n"
"Expected size: 3145728 (3 MB)\n"
"CRC32:", SM_NTSC_CRC32, "\n"
"SHA256:", SM_NTSC_SHA256,
file=sys.stderr)
sys.exit(1)
if not args.check:
dump_rom(rom_data, args.output)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment