Skip to content

Instantly share code, notes, and snippets.

@anisse
Last active February 3, 2024 10:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anisse/c6e4101236708890381414f48804201b to your computer and use it in GitHub Desktop.
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
#!/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