Last active
February 3, 2024 10:50
-
-
Save anisse/c6e4101236708890381414f48804201b to your computer and use it in GitHub Desktop.
Sonic The Hedgehog (Game Gear) romhack: level select, modify level name, set number of lives
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import sys | |
import pathlib | |
space = 0xEB | |
# fmt: off | |
alphabet_0_8 = [ | |
*range(0x34, 0x38), # ABCD | |
*range(0x44, 0x48), # EFGH | |
*range(0x40, 0x44), # IJKL | |
0x50, 0x51, 0x52, # MNO | |
0x60, 0x61, 0x62, # POR | |
0x70, # S | |
0x80, 0x81, # TU | |
0x54, # V | |
*range(0x3C, 0x40) # WXYZ | |
] | |
alphabet_0_8_others = { | |
"©": 0xCF, | |
" ": space, | |
} | |
# fmt: on | |
alphabet_9_17_base = 0x1E # A | |
alphabet_9_17 = [ | |
alphabet_9_17_base + row + col for row in range(0, 193, 16) for col in range(2) | |
] | |
alphabet_9_17_others = { | |
'"': 0xE9, | |
"©": 0xAB, | |
"-": 0xE8, | |
" ": space, | |
} | |
message_base_offset = 0x118B # Green Hill | |
message_next_len = 15 | |
lives_offset = 0x137E | |
def main(): | |
if len(sys.argv) != 4: | |
print(f"usage: {sys.argv[0]} <string 0-12 chars> <level 0-18> <lives 0-99>") | |
return 1 | |
message = sys.argv[1] | |
level = int(sys.argv[2]) | |
lives = int(sys.argv[3]) | |
assert level <= 18 and level >= 0 | |
assert lives < 100 and lives >= 0 | |
rom = bytearray(pathlib.Path("./sonic1.gg").read_bytes()) | |
base = message_base_offset + message_next_len * (level // 3) | |
written = patch_level_name(rom, base, message, tile_alphabet(level)) | |
patch_level_selection(rom, level) | |
rom[lives_offset] = lives | |
pathlib.Path(f"./sonic-lvl{level}-{lives}lives-{written}.gg").write_bytes(rom) | |
def patch_level_name(rom, base, message, chars): | |
i = 0 | |
written = "" | |
for offset in range(12): | |
if i == len(message): | |
rom[base + offset] = space | |
while i < len(message): | |
c = message[i].upper() | |
i += 1 | |
if c in chars: | |
written += c | |
rom[base + offset] = chars[c] | |
break | |
print(f"Ignored character {c} not in alphabet") | |
if i < len(message): | |
print(f"{len(message)-i} characters ignored: {message[i:]}") | |
return written | |
def tile_alphabet(level): | |
alphabet = {} | |
source = alphabet_0_8 | |
if level > 8: | |
source = alphabet_9_17 | |
alphabet.update(alphabet_9_17_others) | |
else: | |
alphabet.update(alphabet_0_8_others) | |
for i in range(26): | |
alphabet[chr(i + ord("A"))] = source[i] | |
return alphabet | |
def patch_level_selection(rom, level): | |
assert level >= 0 and level < 256 | |
patch = { | |
# fmt: off | |
0x138C: [ | |
0xCD, 0xE0, 0x7F, # CALL $7FE0 | |
0x00, # NOP | |
], | |
0x7FE0: [ | |
0x3E, level, # LD A, $level | |
0x32, 0x38, 0xD2, # LD ($D238), A | |
0xAF, # XOR A | |
0xC9, # RET | |
], | |
# fmt: on | |
} | |
for k, v in patch.items(): | |
rom[k : k + len(v)] = v | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment