mirror of
https://github.com/BrianPugh/game-and-watch-patch.git
synced 2025-12-16 07:16:26 +01:00
422 lines
14 KiB
Python
422 lines
14 KiB
Python
"""
|
|
Start End Description
|
|
---------- ---------- --------------------------------
|
|
0x00000 0x00090 If cleared, factory start. Stores Vermin score.
|
|
0x01000 0x01090 ^ second bank
|
|
|
|
0x02000 0x02B20 LA JP Save1
|
|
0x03000 0x03B20 LA JP Save2
|
|
0x04000 0x04B20 LA EN Save1
|
|
0x05000 0x05B20 LA EN Save2
|
|
0x06000 0x06B20 LA FR Save1
|
|
0x07000 0x07B20 LA FR Save2
|
|
0x08000 0x08B20 LA DE Save1
|
|
0x09000 0x09B20 LA DE Save2
|
|
|
|
0x0A000 0x0A560 LoZ1 EN Save1
|
|
0x0B000 0x0B560 LoZ1 EN Save2
|
|
0x0C000 0x0C540 LoZ1 JP Save1
|
|
0x0D000 0x0D540 LoZ1 JP Save2
|
|
|
|
0x0E000 0x0E360 LoZ2 EN Save1
|
|
0x0F000 0x0F360 LoZ2 EN Save2
|
|
0x10000 0x10360 LoZ2 JP Save1
|
|
0x11000 0x11360 LoZ2 JP Save2
|
|
|
|
0x12000 0x13000 Factory Test Scratch Pad
|
|
|
|
0x13000 0z20000 Empty
|
|
|
|
0x20000 0x30000 Sprites?
|
|
|
|
0x30000 0x50000 LoZ1 EN ROM
|
|
0x50000 0x70000 LoZ1 JP ROM
|
|
|
|
0x70000 0xB0000 LoZ2 EN ROM
|
|
0xB0000 0xD0000 LoZ2 JP ROM
|
|
|
|
0xD0000 0xD2000 LoZ2 Timer stuff?
|
|
|
|
0xD2000 0x1F4C00 LA ROMs (1,190,912 bytes)
|
|
|
|
0x1f4c00 0x288120 The 11 Backdrop Images (603,424 bytes)
|
|
|
|
0x288120 0x325490 External FW Data (643,952 bytes)
|
|
|
|
0x325490 0x3E0000 Empty (764,784 bytes)
|
|
|
|
0x3E0000 0x3E8000 Cleared when you reset the GW
|
|
0x3E8000 0x3F0000 Launched LA, didn't save. Generic GB stuff?
|
|
0x3F0000 0x400000 Empty
|
|
"""
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
from .exception import InvalidStockRomError
|
|
from .firmware import Device, ExtFirmware, Firmware, IntFirmware
|
|
from .tileset import decode_backdrop
|
|
from .utils import fds_remove_crc_gaps, printd, printi
|
|
|
|
build_dir = Path("build") # TODO: expose this properly or put in better location
|
|
|
|
|
|
class ZeldaGnW(Device, name="zelda"):
|
|
class Int(IntFirmware):
|
|
STOCK_ROM_SHA1_HASH = "ac14bcea6e4ff68c88fd2302c021025a2fb47940"
|
|
STOCK_ROM_END = 0x1B3E0 # Used for generating linker script.
|
|
KEY_OFFSET = 0x165A4
|
|
NONCE_OFFSET = 0x16590
|
|
RWDATA_OFFSET = 0x1B390
|
|
RWDATA_LEN = 20
|
|
RWDATA_DTCM_IDX = 0 # decompresses to 0x2000_A800
|
|
|
|
class Ext(ExtFirmware):
|
|
STOCK_ROM_SHA1_HASH = "1c1c0ed66d07324e560dcd9e86a322ec5e4c1e96"
|
|
ENC_START = 0x20000
|
|
ENC_END = 0x3254A0
|
|
|
|
def _verify(self):
|
|
h = self.hash(self[self.ENC_START : self.ENC_END])
|
|
if h != self.STOCK_ROM_SHA1_HASH:
|
|
raise InvalidStockRomError
|
|
|
|
class FreeMemory(Firmware):
|
|
FLASH_BASE = 0x240F2124
|
|
FLASH_LEN = 0 # 0x24100000 - FLASH_BASE
|
|
|
|
def argparse(self, parser):
|
|
group = parser.add_argument_group("Low level flash savings flags")
|
|
group.add_argument(
|
|
"--no-la",
|
|
action="store_true",
|
|
help="Remove Link's Awakening rom (all languages).",
|
|
)
|
|
group.add_argument(
|
|
"--no-sleep-images",
|
|
action="store_true",
|
|
help="Remove the 5 sleeping images.",
|
|
)
|
|
group.add_argument(
|
|
"--loz1",
|
|
type=Path,
|
|
default=None,
|
|
help="Override LoZ1 ROM with your own file.",
|
|
)
|
|
group.add_argument(
|
|
"--loz1j",
|
|
type=Path,
|
|
default=None,
|
|
help="Override Japanese LoZ1 (FDS) ROM with your own file.",
|
|
)
|
|
group.add_argument(
|
|
"--loz2",
|
|
type=Path,
|
|
default=None,
|
|
help="Override LoZ2 ROM with your own file.",
|
|
)
|
|
|
|
group.add_argument(
|
|
"--no-second-beep",
|
|
action="store_true",
|
|
help="Remove the second beep in TIME/CLOCK.",
|
|
)
|
|
group.add_argument(
|
|
"--no-hour-tune",
|
|
action="store_true",
|
|
help="Remove the hour tune in TIME/CLOCK.",
|
|
)
|
|
self.args = parser.parse_args()
|
|
return self.args
|
|
|
|
def _flash_roms(self):
|
|
# English Zelda 1
|
|
if self.args.loz1:
|
|
loz1_addr, loz1_size = 0x3_0000, 0x2_0000
|
|
loz1 = self.args.loz1.read_bytes()
|
|
# Remove the NES header
|
|
if loz1[0] == 0x4E:
|
|
loz1 = loz1[16:]
|
|
self.external[loz1_addr : loz1_addr + loz1_size] = loz1
|
|
|
|
# Japanese Zelda 1 (FDS)
|
|
if self.args.loz1j:
|
|
loz1j_addr, loz1j_size = 0x5_0000, 0x2_0000
|
|
loz1j = self.args.loz1j.read_bytes()
|
|
# Remove the NES header
|
|
if loz1j[0] == 0x46:
|
|
loz1j = loz1j[16:]
|
|
self.external[loz1j_addr : loz1j_addr + loz1j_size] = loz1j
|
|
|
|
# English Zelda 2
|
|
if self.args.loz2:
|
|
loz2_addr, loz2_size = 0x7_0000, 0x4_0000
|
|
loz2 = self.args.loz2.read_bytes()
|
|
# Remove the NES header
|
|
if loz2[0] == 0x4E:
|
|
loz2 = loz2[16:]
|
|
self.external[loz2_addr : loz2_addr + loz2_size] = loz2
|
|
|
|
def _dump_roms(self):
|
|
# English Zelda 1
|
|
rom_addr = 0x3_0000
|
|
rom_size = 0x2_0000
|
|
(build_dir / "Legend of Zelda, The (USA).nes").write_bytes(
|
|
b"NES\x1a\x08\x00\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
+ self.external[rom_addr : rom_addr + rom_size]
|
|
)
|
|
|
|
# Japanse Zelda 1
|
|
# This rom doesn't work :(
|
|
rom_addr = 0x5_0000
|
|
rom_size = 0x1_0000
|
|
rom1 = bytearray(self.external[rom_addr : rom_addr + rom_size])
|
|
# bios = self.external[0x5_E000:0x6_0000]
|
|
rom1 = fds_remove_crc_gaps(rom1)
|
|
rom_addr = 0x6_0000
|
|
rom_size = 0x1_0000
|
|
rom2 = fds_remove_crc_gaps(self.external[rom_addr : rom_addr + rom_size])
|
|
(build_dir / "Zelda no Densetsu: The Hyrule Fantasy (J).fds").write_bytes(
|
|
rom1 + rom2
|
|
)
|
|
|
|
# English Zelda 2
|
|
rom_addr = 0x7_0000
|
|
rom_size = 0x4_0000
|
|
(build_dir / "Zelda II - Adventure of Link (USA).nes").write_bytes(
|
|
b"NES\x1a\x08\x10\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
+ self.external[rom_addr : rom_addr + rom_size]
|
|
)
|
|
|
|
# Japanse Zelda 2
|
|
# This rom doesn't work :(
|
|
rom_addr = 0xB_0000
|
|
rom_size = 0x1_0000
|
|
rom1 = bytearray(self.external[rom_addr : rom_addr + rom_size])
|
|
# bios = self.external[0xB_E000:0xC_0000]
|
|
rom1 = fds_remove_crc_gaps(rom1)
|
|
rom_addr = 0xC_0000
|
|
rom_size = 0x1_0000
|
|
rom2 = fds_remove_crc_gaps(self.external[rom_addr : rom_addr + rom_size])
|
|
(build_dir / "Link no Bouken - The Legend of Zelda 2 (J).fds").write_bytes(
|
|
rom1 + rom2
|
|
)
|
|
|
|
# I Believe 0xD_0000 ~ 0xD_2000 are LoZ2-JP tweaks... or maybe just the timer?
|
|
|
|
# English Link's Awakening
|
|
# This rom doesn't work :(
|
|
rom_addr = 0xD_2000
|
|
rom_size = 0x8_0000
|
|
(build_dir / "Legend of Zelda, The - Link's Awakening (en).gb").write_bytes(
|
|
self.external[rom_addr : rom_addr + rom_size]
|
|
)
|
|
|
|
def _erase_roms(self):
|
|
"""Temporary for debugging, just seeing which roms impact the clock."""
|
|
|
|
if False:
|
|
# loz1-en is critical to clock
|
|
rom_addr = 0x3_0000
|
|
rom_size = 0x2_0000
|
|
self.external.clear_range(rom_addr, rom_addr + rom_size)
|
|
|
|
if True:
|
|
# loz1-jp is not critical
|
|
rom_addr = 0x5_0000
|
|
rom_size = 0x2_0000
|
|
self.external.clear_range(rom_addr, rom_addr + rom_size)
|
|
|
|
if True:
|
|
# loz2-en is not critical
|
|
rom_addr = 0x7_0000
|
|
rom_size = 0x4_0000
|
|
self.external.clear_range(rom_addr, rom_addr + rom_size)
|
|
|
|
if True:
|
|
# loz2-jp is critical to timer; only crashes if timer is started.
|
|
rom_addr = 0xB_0000
|
|
rom_size = 0x2_0000
|
|
self.external.clear_range(rom_addr, rom_addr + rom_size)
|
|
|
|
if True:
|
|
# Links Awakening (various)
|
|
# 1,190,912 bytes
|
|
rom_start = 0xD_2000
|
|
rom_end = 0x1F_4C00
|
|
self.external.clear_range(rom_start, rom_end)
|
|
|
|
# self.external.ENC_END (0x32_54A0) to the true encryped end 0x3E_0000
|
|
# is all unused space. Extra 764,768 bytes free
|
|
# More data at 0x3e_8000...
|
|
|
|
def _dump_backdrops(self):
|
|
"""Dump the 11 backdrop images.
|
|
|
|
Overall length: 603,424 bytes
|
|
|
|
Start End
|
|
-------- --------
|
|
0x1F4C00 0x205a7d
|
|
0x205A80 0x211913
|
|
0x211920 0x213840
|
|
0x213840 0x222500
|
|
0x222500 0x234128
|
|
0x234140 0x24247e
|
|
0x242480 0x253949
|
|
0x253960 0x25cf1f
|
|
0x25CF20 0x26aaf8
|
|
0x26AB00 0x279f98
|
|
0x279FA0 0x28811d
|
|
"""
|
|
bytes_starts = [
|
|
("0", 0x1F4C00),
|
|
("1", 0x205A80),
|
|
("2", 0x211920),
|
|
("3", 0x213840),
|
|
("4", 0x222500),
|
|
("5", 0x234140),
|
|
("6", 0x242480),
|
|
("7", 0x253960),
|
|
("8", 0x25CF20),
|
|
("9", 0x26AB00),
|
|
("10", 0x279FA0),
|
|
]
|
|
for name, start in bytes_starts:
|
|
img, consumed = decode_backdrop(self.external[start:])
|
|
img.save(build_dir / f"backdrop_{name}.png")
|
|
# print(hex(start + consumed))
|
|
|
|
def _disable_save_encryption(self):
|
|
# Skip ingame save encryption
|
|
self.internal.nop(0xF222, 1)
|
|
self.internal.asm(0xF228, "add.w r2,r1,#0x10")
|
|
self.internal.asm(0xF22C, "sub.w r1,r8,#0x10")
|
|
|
|
# Skip LA save state encryption
|
|
self.internal.b(0x13ED8, 0x13F06)
|
|
|
|
# Skip NVRAM (system settings and vermin save) encryption
|
|
self.internal.asm(0xB5C4, "mov r1,r2")
|
|
self.internal.nop(0xB5C6, 1)
|
|
self.internal.nop(0xB5CC, 1)
|
|
|
|
# Skip ingame save decryption
|
|
self.internal.asm(0xF12C, "add.w r7,r0,#0x10")
|
|
self.internal.asm(0xF130, "mov r5,r1")
|
|
self.internal.asm(0xF132, "sub.w r6,r2,#0x10")
|
|
self.internal.asm(0xF136, "sub sp,#0x10")
|
|
self.internal.asm(0xF138, "mov r1,r6")
|
|
self.internal.asm(0xF13A, "mov r0,r7")
|
|
self.internal.replace(0xF13C, b"\xf4\xf7\xbc\xfc")
|
|
self.internal.asm(0xF140, "mov r2,r7")
|
|
self.internal.asm(0xF142, "mov r1,r6")
|
|
self.internal.asm(0xF144, "mov r0,r5")
|
|
self.internal.replace(0xF146, b"\xfc\xf7\x29\xfc")
|
|
self.internal.b(0xF14A, 0xF172)
|
|
|
|
# Skip LA save state decryption
|
|
self.internal.b(0x13F52, 0x13F94)
|
|
|
|
# Skip NVRAM (system settings and vermin save) decryption
|
|
self.internal.asm(0xB528, "mov r7,r0")
|
|
self.internal.nop(0xB52A, 1)
|
|
self.internal.replace(0xB54C, b"\xc0\xb1")
|
|
|
|
def _erase_savedata(self):
|
|
self.external.set_range(0x0000, 0x12000, b"\xFF")
|
|
self.external.set_range(0x3E_8000, 0x3F_0000, b"\xFF")
|
|
|
|
def patch(self):
|
|
b_w_memcpy_inflate_asm = "b.w #" + hex(
|
|
0xFFFFFFFE & self.internal.address("memcpy_inflate")
|
|
)
|
|
|
|
self._dump_roms()
|
|
self._dump_backdrops()
|
|
|
|
if False:
|
|
self._erase_roms()
|
|
|
|
self._flash_roms()
|
|
|
|
self._erase_savedata()
|
|
|
|
if self.args.debug:
|
|
# Override fault handlers for easier debugging via gdb.
|
|
printi("Overriding handlers for debugging.")
|
|
self.internal.replace(0x8, "NMI_Handler")
|
|
self.internal.replace(0xC, "HardFault_Handler")
|
|
|
|
from .tileset import bytes_to_tilemap
|
|
|
|
_ = bytes_to_tilemap(self.external[0x20000:0x30000])
|
|
|
|
self._disable_save_encryption()
|
|
|
|
printi("Invoke custom bootloader prior to calling stock Reset_Handler.")
|
|
self.internal.replace(0x4, "bootloader")
|
|
|
|
printi("Intercept button presses for macros.")
|
|
self.internal.bl(0xFE54, "read_buttons")
|
|
|
|
if not self.args.encrypt:
|
|
# Disable OTFDEC
|
|
self.internal.nop(0x16536, 2)
|
|
self.internal.nop(0x1653A, 1)
|
|
self.internal.nop(0x1653C, 1)
|
|
|
|
if self.args.no_hour_tune:
|
|
# Disable TIME/CLOCK hour tune
|
|
# Change 'bne' to 'b'. Will replace the 'hour tune' with a 'second beep'
|
|
self.external[0x320025] = 0xE0
|
|
|
|
if self.args.no_second_beep:
|
|
# Disable TIME/CLOCK second beep
|
|
self.external.nop(0x32002E, 1)
|
|
|
|
if False:
|
|
# This doesn't quite work yet
|
|
# I think RWData stuff probably needs to be updated
|
|
printd("Compressing and moving LoZ2 JP ROM data to int")
|
|
compressed_len = self.external.compress(0xB_0000, 0x1E000)
|
|
self.internal.asm(0xF702, b_w_memcpy_inflate_asm)
|
|
self.move_to_int(0xB_0000, compressed_len, 0xFD1C)
|
|
|
|
printd("Compressing and moving LoZ2 TIMER data to int")
|
|
compressed_len = self.external.compress(0xD_0000, 0x2000)
|
|
self.internal.asm(0xF430, b_w_memcpy_inflate_asm)
|
|
self.move_to_int(0xD_0000, compressed_len, 0xFCF8)
|
|
|
|
if self.args.no_la:
|
|
printi("Removing Link's Awakening (All Languages)")
|
|
self.external.clear_range(0xD2000, 0x1F4C00)
|
|
self.external[0x315B54] = 0x00 # Ignore LA EN menu selection
|
|
self.external[0x315B58] = 0x00 # Ignore LA FR menu selection
|
|
self.external[0x315B5C] = 0x00 # Ignore LA DE menu selection
|
|
self.external[0x315B60] = 0x00 # Ignore LA JP menu selection
|
|
# TODO: make this work with moving stuff around, currently just
|
|
# removing to free up an island of space.
|
|
|
|
if self.args.no_sleep_images:
|
|
self.external.clear_range(0x1F4C00, 0x288120)
|
|
|
|
# setting this to NULL doesn't just display a black image, I
|
|
# don't think the drawing code has a NULL check.
|
|
# self.rwdata_erase(0x1f4c00, 0x288120 - 0x1f4c00)
|
|
|
|
# TODO: make this work with moving stuff around, currently just
|
|
# removing to free up an island of space.
|
|
|
|
# Compress, insert, and reference the modified rwdata
|
|
self.int_pos += self.internal.rwdata.write_table_and_data(
|
|
0x1B070, data_offset=self.int_pos
|
|
)
|
|
|
|
internal_remaining_free = len(self.internal) - self.int_pos
|
|
compressed_memory_free = (
|
|
len(self.compressed_memory) - self.compressed_memory_pos
|
|
)
|
|
|
|
return internal_remaining_free, compressed_memory_free
|