mirror of
https://github.com/BrianPugh/game-and-watch-patch.git
synced 2025-12-16 07:16:26 +01:00
741 lines
28 KiB
Python
741 lines
28 KiB
Python
from pathlib import Path
|
|
|
|
from PIL import Image
|
|
|
|
import patches
|
|
|
|
from .compression import lzma_compress
|
|
from .exception import BadImageError, InvalidStockRomError
|
|
from .firmware import Device, ExtFirmware, Firmware, IntFirmware
|
|
from .tileset import bytes_to_tilemap, decode_backdrop, tilemap_to_bytes
|
|
from .utils import (
|
|
fds_remove_crc_gaps,
|
|
printd,
|
|
printe,
|
|
printi,
|
|
round_down_word,
|
|
round_up_page,
|
|
seconds_to_frames,
|
|
)
|
|
|
|
build_dir = Path("build") # TODO: expose this properly or put in better location
|
|
|
|
|
|
class MarioGnW(Device, name="mario"):
|
|
class Int(IntFirmware):
|
|
STOCK_ROM_SHA1_HASH = "efa04c387ad7b40549e15799b471a6e1cd234c76"
|
|
|
|
# Note: this isn't the ACTUAL Stock ROM end, this is actually
|
|
# pointing to where some rwdata is, but this data will be relocated
|
|
# and compressed. This variable is used in the linker scripts as to
|
|
# where to start putting novel code.
|
|
STOCK_ROM_END = 0x18100
|
|
KEY_OFFSET = 0x106F4
|
|
NONCE_OFFSET = 0x106E4
|
|
RWDATA_OFFSET = 0x180A4
|
|
RWDATA_LEN = 36
|
|
RWDATA_ITCM_IDX = 0
|
|
RWDATA_DTCM_IDX = 1
|
|
|
|
class Ext(ExtFirmware):
|
|
STOCK_ROM_SHA1_HASH = "eea70bb171afece163fb4b293c5364ddb90637ae"
|
|
ENC_END = 0xF_E000
|
|
|
|
def _verify(self):
|
|
h = self.hash(self[:-8192])
|
|
if h != self.STOCK_ROM_SHA1_HASH:
|
|
raise InvalidStockRomError
|
|
|
|
class FreeMemory(Firmware):
|
|
FLASH_BASE = 0x240F2124
|
|
FLASH_LEN = 0x24100000 - FLASH_BASE
|
|
|
|
def argparse(self, parser):
|
|
group = parser.add_argument_group("Timeout patches")
|
|
|
|
mgroup = group.add_mutually_exclusive_group()
|
|
mgroup.add_argument(
|
|
"--disable-sleep", action="store_true", help="Disables sleep timer"
|
|
)
|
|
mgroup.add_argument(
|
|
"--sleep-time",
|
|
type=float,
|
|
default=None,
|
|
help="Go to sleep after this many seconds of inactivity.. "
|
|
"Valid range: [1, 1092]",
|
|
)
|
|
|
|
group.add_argument(
|
|
"--hard-reset-time",
|
|
type=float,
|
|
default=None,
|
|
help="Hold power button for this many seconds to perform hard reset.",
|
|
)
|
|
group.add_argument(
|
|
"--mario-song-time",
|
|
type=float,
|
|
default=None,
|
|
help="Hold the A button for this many seconds on the time "
|
|
"screen to launch the mario drawing song easter egg.",
|
|
)
|
|
|
|
group = parser.add_argument_group("ROM Hacks and Graphical Mods")
|
|
group.add_argument(
|
|
"--smb1",
|
|
type=Path,
|
|
default="build/smb1.nes",
|
|
help="Override SMB1 ROM with your own file.",
|
|
)
|
|
mgroup = group.add_mutually_exclusive_group()
|
|
mgroup.add_argument(
|
|
"--smb1-graphics",
|
|
nargs="*",
|
|
default=[],
|
|
type=Path,
|
|
help="ROM hacks where just the graphical assets will be used.",
|
|
)
|
|
mgroup.add_argument(
|
|
"--smb1-graphics-glob",
|
|
action="store_true",
|
|
help='Add all IPS files from the "ips/" folder',
|
|
)
|
|
|
|
mgroup = group.add_mutually_exclusive_group()
|
|
mgroup.add_argument(
|
|
"--clock-tileset",
|
|
type=Path,
|
|
default=None,
|
|
help="Override the clock tileset",
|
|
)
|
|
mgroup.add_argument(
|
|
"--clock-tileset-index",
|
|
type=Path,
|
|
default=None,
|
|
help="Override the clock tileset",
|
|
)
|
|
# group.add_argument(
|
|
# "--iconset",
|
|
# type=Path,
|
|
# default=Path("build/iconset.png"),
|
|
# help="Override the iconset",
|
|
# )
|
|
|
|
group = parser.add_argument_group("Low level flash savings flags")
|
|
group.add_argument(
|
|
"--no-save",
|
|
action="store_true",
|
|
help="Don't use up 2 pages (8192 bytes) of extflash for non-volatile saves. "
|
|
"High scores and brightness/volume configurations will NOT survive homebrew launches.",
|
|
)
|
|
group.add_argument("--no-smb2", action="store_true", help="Remove SMB2 rom.")
|
|
group.add_argument(
|
|
"--no-mario-song",
|
|
action="store_true",
|
|
help="Remove the mario song easter egg.",
|
|
)
|
|
group.add_argument(
|
|
"--no-sleep-images",
|
|
action="store_true",
|
|
help="Remove the 5 sleeping images.",
|
|
)
|
|
|
|
group = parser.add_argument_group("High level flash savings flags")
|
|
group.add_argument(
|
|
"--slim",
|
|
action="store_true",
|
|
help="Remove mario song and sleeping images from extflash.",
|
|
)
|
|
group.add_argument(
|
|
"--clock-only",
|
|
action="store_true",
|
|
help="Everything in --slim plus remove SMB2.",
|
|
)
|
|
group.add_argument(
|
|
"--internal-only",
|
|
action="store_true",
|
|
help="Configuration so no external flash is used.",
|
|
)
|
|
|
|
self.args = parser.parse_args()
|
|
|
|
############
|
|
# Validate #
|
|
############
|
|
if self.args.sleep_time and (
|
|
self.args.sleep_time < 1 or self.args.sleep_time > 1092
|
|
):
|
|
parser.error("--sleep-time must be in range [1, 1092]")
|
|
if self.args.mario_song_time and (
|
|
self.args.mario_song_time < 1 or self.args.mario_song_time > 1092
|
|
):
|
|
parser.error("--mario_song-time must be in range [1, 1092]")
|
|
|
|
if len(self.args.smb1_graphics) > 8:
|
|
parser.error("A maximum of 8 SMB1 graphics mods can be specified.")
|
|
|
|
if self.args.smb1_graphics_glob:
|
|
ips_folder = Path("ips")
|
|
self.args.smb1_graphics = list(ips_folder.glob("*.ips"))
|
|
self.args.smb1_graphics.extend(list(ips_folder.glob("*.IPS")))
|
|
|
|
if self.args.internal_only:
|
|
self.args.slim = True
|
|
self.args.extended = True
|
|
self.args.no_save = True
|
|
if self.args.sd_bootloader:
|
|
self.args.no_smb2 = True
|
|
|
|
if self.args.clock_only:
|
|
self.args.slim = True
|
|
self.args.no_smb2 = True
|
|
|
|
if self.args.slim:
|
|
self.args.no_mario_song = True
|
|
self.args.no_sleep_images = True
|
|
|
|
return self.args
|
|
|
|
def patch(self):
|
|
printi("Invoke custom bootloader prior to calling stock Reset_Handler.")
|
|
self.internal.replace(0x4, "bootloader")
|
|
|
|
printi("Intercept button presses for macros.")
|
|
self.internal.bl(0x6B52, "read_buttons")
|
|
|
|
printi("Mute clock audio on first boot.")
|
|
self.internal.asm(0x49E0, "mov.w r1, #0x00000")
|
|
|
|
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")
|
|
|
|
if self.args.hard_reset_time:
|
|
hard_reset_time_ms = int(round(self.args.hard_reset_time * 1000))
|
|
printi(
|
|
f"Hold power button for {hard_reset_time_ms} milliseconds to perform hard reset."
|
|
)
|
|
self.internal.asm(0x9CEE, f"movw r1, #{hard_reset_time_ms}")
|
|
|
|
if self.args.sleep_time:
|
|
printi(f"Setting sleep time to {self.args.sleep_time} seconds.")
|
|
sleep_time_frames = seconds_to_frames(self.args.sleep_time)
|
|
self.internal.asm(0x6C3C, f"movw r2, #{sleep_time_frames}")
|
|
|
|
if self.args.disable_sleep:
|
|
printi("Disable sleep timer")
|
|
self.internal.replace(0x6C40, 0x91, size=1)
|
|
|
|
if self.args.mario_song_time:
|
|
printi(f"Setting Mario Song time to {self.args.mario_song_time} seconds.")
|
|
mario_song_frames = seconds_to_frames(self.args.mario_song_time)
|
|
self.internal.asm(0x6FC4, f"cmp.w r0, #{mario_song_frames}")
|
|
|
|
if not self.args.encrypt:
|
|
# Disable OTFDEC
|
|
self.internal.nop(0x10688, 2)
|
|
self.internal.nop(0x1068E, 1)
|
|
|
|
# Dump the tileset
|
|
tileset_addr, tileset_size = 0x9_8B84, 0x1_0000
|
|
palette_addr = 0xB_EC68
|
|
palette = self.external[palette_addr : palette_addr + 320]
|
|
tileset_bytes = self.external[tileset_addr : tileset_addr + tileset_size]
|
|
tileset = bytes_to_tilemap(tileset_bytes, palette=palette)
|
|
tileset.save(build_dir / "tileset.png")
|
|
tileset_index = bytes_to_tilemap(tileset_bytes)
|
|
tileset_index.save(build_dir / "tileset_index.png")
|
|
|
|
# Override tileset
|
|
if self.args.clock_tileset:
|
|
with Image.open(self.args.clock_tileset) as tileset:
|
|
if tileset.height != 256 or tileset.width != 256:
|
|
raise BadImageError(
|
|
"Clock tileset image must have height=256, width=256"
|
|
)
|
|
tileset = tileset.convert("RGB")
|
|
if tileset.getpixel((255, 255))[:3] != (95, 115, 255):
|
|
raise BadImageError(
|
|
"Clock tileset image color is corrupt. Possibly due to some gamma issue."
|
|
)
|
|
self.external[
|
|
tileset_addr : tileset_addr + tileset_size
|
|
] = tilemap_to_bytes(tileset, palette)
|
|
|
|
# Dump the iconset
|
|
iconset_addr, iconset_size = 0xAACE4, 0x3F00
|
|
palette_addr = 0xB_EC68
|
|
palette = self.external[palette_addr : palette_addr + 320]
|
|
iconset = bytes_to_tilemap(
|
|
self.external[iconset_addr : iconset_addr + iconset_size],
|
|
palette=palette,
|
|
bpp=4,
|
|
)
|
|
iconset.save(build_dir / "iconset.png")
|
|
|
|
# Override iconset
|
|
# with Image.open(self.args.iconset) as iconset:
|
|
# if iconset.height != 128 or iconset.width !=256:
|
|
# raise BadImageError("Iconset image must have height=128, width=256")
|
|
# iconset = iconset.convert("RGB")
|
|
# if iconset.getpixel((255, 127))[:3] != (95, 115, 255):
|
|
# raise BadImageError("Iconset image color is corrupt. Possibly due to some gamma issue.")
|
|
# self.external[iconset_addr : iconset_addr + iconset_size] = \
|
|
# tilemap_to_bytes(iconset, palette, bpp=4)[:iconset_size]
|
|
|
|
# Dump BALL logo
|
|
# ball_logo_addr, ball_logo_size = 0x1_13CC, 768
|
|
# palette_addr = 0xB_EC68
|
|
# palette = self.external[palette_addr : palette_addr + 320]
|
|
# ball_logo = bytes_to_tilemap(
|
|
# self.external[ball_logo_addr : ball_logo_addr + ball_logo_size],
|
|
# palette=palette,
|
|
# width=128,
|
|
# bpp=2,
|
|
# )
|
|
# ball_logo.save(build_dir / "ball_logo.png")
|
|
|
|
if self.args.smb1_graphics:
|
|
printi("Intercept prepare_clock_rom")
|
|
self.internal.bl(0x690E, "prepare_clock_rom")
|
|
self.internal.nop(0x1_0EF0, 2)
|
|
|
|
table = self.internal.address("SMB1_GRAPHIC_MODS", sub_base=True)
|
|
for file_path in self.args.smb1_graphics:
|
|
if file_path.suffix.lower() == ".nes":
|
|
rom = file_path.read_bytes()
|
|
if len(rom) == 40976:
|
|
# Remove the NES header
|
|
rom = rom[16:]
|
|
assert len(rom) == 40960
|
|
graphics = rom[0x8000:0x9EC0]
|
|
graphics_compressed = lzma_compress(graphics)
|
|
loc = self.move_to_int(
|
|
graphics_compressed, len(graphics_compressed), None
|
|
)
|
|
loc += self.internal.FLASH_BASE
|
|
elif file_path.suffix.lower() == ".ips":
|
|
patch = file_path.read_bytes()
|
|
patch = patches.ips.strip_header(patch)
|
|
loc = self.move_to_int(patch, len(patch), None)
|
|
loc += self.internal.FLASH_BASE
|
|
else:
|
|
raise ValueError(
|
|
f"Don't know how to handle extension for {file_path}."
|
|
)
|
|
# Update the SMB1_GRAPHIC_MODS table
|
|
self.internal.replace(table, loc, size=4)
|
|
table += 4
|
|
|
|
printd("Compressing and moving stuff stuff to internal firmware.")
|
|
compressed_len = self.external.compress(
|
|
0x0, 7772
|
|
) # Dst expects only 7772 bytes, not 7776
|
|
self.internal.bl(0x665C, "memcpy_inflate")
|
|
self.move_ext(0x0, compressed_len, 0x7204)
|
|
# Note: the 4 bytes between 7772 and 7776 is padding.
|
|
self.ext_offset -= 7776 - round_down_word(compressed_len)
|
|
|
|
# SMB1 ROM (plus loading custom ROM)
|
|
printd("Compressing and moving SMB1 ROM to compressed_memory.")
|
|
smb1_addr, smb1_size = 0x1E60, 40960
|
|
# Adding the header for patching convenience.
|
|
(build_dir / "smb1.nes").write_bytes(
|
|
b"NES\x1a\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
+ self.external[smb1_addr : smb1_addr + smb1_size]
|
|
)
|
|
smb1 = self.args.smb1.read_bytes()
|
|
if len(smb1) == 40976:
|
|
# Remove the NES header
|
|
smb1 = smb1[16:]
|
|
if len(smb1) != smb1_size:
|
|
raise ValueError(f"Unknown length {len(smb1)} of file {self.args.smb1}")
|
|
self.external[smb1_addr : smb1_addr + smb1_size] = smb1
|
|
patch_smb1_refr = self.internal.address("SMB1_ROM", sub_base=True)
|
|
self.move_to_compressed_memory(
|
|
smb1_addr, smb1_size, [0x7368, 0x10954, 0x7218, patch_smb1_refr]
|
|
)
|
|
|
|
# I think these are all scenes for the clock, but not 100% sure.
|
|
# The giant lookup table references all these
|
|
self.move_to_compressed_memory(0xBE60, 11620, None)
|
|
|
|
# Starting here are BALL references
|
|
self.move_to_compressed_memory(0xEBC4, 528, 0x4154)
|
|
self.rwdata_lookup(0xEBC4, 528)
|
|
|
|
self.move_to_compressed_memory(0xEDD4, 100, 0x4570)
|
|
|
|
references = {
|
|
0xEE38: 0x4514,
|
|
0xEE78: 0x4518,
|
|
0xEEB8: 0x4520,
|
|
0xEEF8: 0x4524,
|
|
}
|
|
for external, internal in references.items():
|
|
self.move_to_compressed_memory(external, 64, internal)
|
|
|
|
references = [
|
|
0x2AC,
|
|
0x2B0,
|
|
0x2B4,
|
|
0x2B8,
|
|
0x2BC,
|
|
0x2C0,
|
|
0x2C4,
|
|
0x2C8,
|
|
0x2CC,
|
|
0x2D0,
|
|
]
|
|
self.move_to_compressed_memory(0xEF38, 128 * 10, references)
|
|
|
|
self.move_to_compressed_memory(0xF438, 96, 0x456C)
|
|
self.move_to_compressed_memory(0xF498, 180, 0x43F8)
|
|
|
|
# This is the first thing passed into the drawing engine.
|
|
self.move_to_compressed_memory(0xF54C, 1100, 0x43FC)
|
|
self.move_to_compressed_memory(0xF998, 180, 0x4400)
|
|
self.move_to_compressed_memory(0xFA4C, 1136, 0x4404)
|
|
self.move_to_compressed_memory(0xFEBC, 864, 0x450C)
|
|
self.move_to_compressed_memory(0x1_021C, 384, 0x4510)
|
|
self.move_to_compressed_memory(0x1_039C, 384, 0x451C)
|
|
self.move_to_compressed_memory(0x1_051C, 384, 0x4410)
|
|
self.move_to_compressed_memory(0x1_069C, 384, 0x44F8)
|
|
self.move_to_compressed_memory(0x1_081C, 384, 0x4500)
|
|
self.move_to_compressed_memory(0x1_099C, 384, 0x4414)
|
|
self.move_to_compressed_memory(0x1_0B1C, 384, 0x44FC)
|
|
self.move_to_compressed_memory(0x1_0C9C, 384, 0x4504)
|
|
self.move_to_compressed_memory(0x1_0E1C, 384, 0x440C)
|
|
self.move_to_compressed_memory(0x1_0F9C, 384, 0x4408)
|
|
self.move_to_compressed_memory(0x1_111C, 192, 0x44F4)
|
|
self.move_to_compressed_memory(0x1_11DC, 192, 0x4508)
|
|
self.move_to_compressed_memory(0x1_129C, 304, 0x458C)
|
|
self.move_to_compressed_memory(
|
|
0x1_13CC, 768, 0x4584
|
|
) # BALL logo tile idx tight
|
|
self.move_to_compressed_memory(0x1_16CC, 1144, 0x4588)
|
|
self.move_to_compressed_memory(0x1_1B44, 768, 0x4534)
|
|
self.move_to_compressed_memory(0x1_1E44, 32, 0x455C)
|
|
self.move_to_compressed_memory(0x1_1E64, 32, 0x4558)
|
|
self.move_to_compressed_memory(0x1_1E84, 32, 0x4554)
|
|
self.move_to_compressed_memory(0x1_1EA4, 32, 0x4560)
|
|
self.move_to_compressed_memory(0x1_1EC4, 32, 0x4564)
|
|
self.move_to_compressed_memory(0x1_1EE4, 64, 0x453C)
|
|
self.move_to_compressed_memory(0x1_1F24, 64, 0x4530)
|
|
self.move_to_compressed_memory(0x1_1F64, 64, 0x4540)
|
|
self.move_to_compressed_memory(0x1_1FA4, 64, 0x4544)
|
|
self.move_to_compressed_memory(0x1_1FE4, 64, 0x4548)
|
|
self.move_to_compressed_memory(0x1_2024, 64, 0x454C)
|
|
self.move_to_compressed_memory(0x1_2064, 64, 0x452C)
|
|
self.move_to_compressed_memory(0x1_20A4, 64, 0x4550)
|
|
|
|
self.move_to_compressed_memory(0x1_20E4, 21 * 96, 0x4574)
|
|
self.move_to_compressed_memory(0x1_28C4, 192, 0x4578)
|
|
self.move_to_compressed_memory(0x1_2984, 640, 0x457C)
|
|
|
|
# This is a 320 byte palette used for BALL, but the last 160 bytes are empty
|
|
self.move_to_compressed_memory(0x1_2C04, 320, 0x4538)
|
|
|
|
mario_song_len = 0x85E40 # 548,416 bytes
|
|
if self.args.no_mario_song:
|
|
# This isn't really necessary, but we keep it here because its more explicit.
|
|
printe("Erasing Mario Song")
|
|
self.external.replace(0x1_2D44, b"\x00" * mario_song_len)
|
|
self.rwdata_erase(0x1_2D44, mario_song_len)
|
|
self.ext_offset -= mario_song_len
|
|
|
|
self.internal.asm(0x6FC8, "b 0x1c")
|
|
else:
|
|
references = [
|
|
# Banners
|
|
0x11A00,
|
|
0x11A00 + 4,
|
|
0x11A00 + 8,
|
|
0x11A00 + 12,
|
|
0x11A00 + 16,
|
|
0x11A00 + 20,
|
|
0x11A00 + 24,
|
|
# Audio
|
|
0x1199C,
|
|
]
|
|
self.move_ext(0x1_2D44, mario_song_len, references)
|
|
self.rwdata_lookup(0x1_2D44, mario_song_len)
|
|
|
|
# Each tile is 16x16 pixels, stored as 256 bytes in row-major form.
|
|
# These index into one of the palettes starting at 0xbec68.
|
|
printe("Compressing clock graphics")
|
|
compressed_len = self.external.compress(0x9_8B84, 0x1_0000)
|
|
self.internal.bl(0x678E, "memcpy_inflate")
|
|
|
|
printe("Moving clock graphics")
|
|
self.move_ext(0x9_8B84, compressed_len, 0x7350)
|
|
self.ext_offset -= 0x1_0000 - round_down_word(compressed_len)
|
|
|
|
# Note: the clock uses a different palette; this palette only applies
|
|
# to ingame Super Mario Bros 1 & 2
|
|
printe("Moving NES emulator palette.")
|
|
self.move_to_compressed_memory(0xA_8B84, 192, 0xB720)
|
|
|
|
# Note: UNKNOWN* represents a block of data that i haven't decoded
|
|
# yet. If you know what the block of data is, please let me know!
|
|
self.move_to_compressed_memory(0xA_8C44, 8352, 0xBC44)
|
|
|
|
printe("Moving iconset.")
|
|
# MODIFY THESE IF WE WANT CUSTOM GAME ICONS
|
|
self.move_to_compressed_memory(0xA_ACE4, 16128, [0xCEA8, 0xD2F8])
|
|
|
|
printe("Moving menu stuff (icons? meta?)")
|
|
references = [
|
|
0x0_D010,
|
|
0x0_D004,
|
|
0x0_D2D8,
|
|
0x0_D2DC,
|
|
0x0_D2F4,
|
|
0x0_D2F0,
|
|
]
|
|
self.move_to_compressed_memory(0xA_EBE4, 116, references)
|
|
|
|
# Dump a playable version of SMB2
|
|
smb2_addr, smb2_size = 0xA_EC58, 0x1_0000
|
|
smb2_end = smb2_addr + smb2_size
|
|
smb2 = self.external[smb2_addr:smb2_end].copy()
|
|
smb2 = fds_remove_crc_gaps(smb2)
|
|
(build_dir / "smb2.fds").write_bytes(smb2)
|
|
|
|
if self.args.no_smb2:
|
|
printe("Erasing SMB2 ROM")
|
|
self.external.replace(
|
|
smb2_addr,
|
|
b"\x00" * smb2_size,
|
|
)
|
|
self.ext_offset -= smb2_size
|
|
|
|
# Replace conditional-branch with unconditional
|
|
# TODO: this prevents hardlocking when selecting SMB2 from the menu,
|
|
# but it's not quite right and is glitchy.
|
|
self.internal.b(0x69FC, 0x6A8C)
|
|
else:
|
|
printe("Compressing and moving SMB2 ROM.")
|
|
compressed_len = self.external.compress(smb2_addr, smb2_size)
|
|
self.internal.bl(0x6A12, "memcpy_inflate")
|
|
self.move_to_compressed_memory(smb2_addr, compressed_len, 0x7374)
|
|
self.ext_offset -= smb2_size - round_down_word(
|
|
compressed_len
|
|
) # Move by the space savings.
|
|
|
|
# Round to nearest page so that the length can be used as an imm
|
|
compressed_len = round_up_page(compressed_len)
|
|
|
|
# Update the length of the compressed data (doesn't matter if its too large)
|
|
self.internal.asm(0x6A0A, f"mov.w r2, #{compressed_len}")
|
|
self.internal.asm(0x6A1E, f"mov.w r3, #{compressed_len}")
|
|
|
|
# Not sure what this data is
|
|
self.move_to_compressed_memory(0xBEC58, 8 * 2, 0x10964)
|
|
|
|
printe("Moving Palettes")
|
|
# There are 80 colors, each in BGRA format, where A is always 0
|
|
# These are referenced by the scene table.
|
|
self.move_to_compressed_memory(0xBEC68, 320, None) # Day palette [0600, 1700]
|
|
self.move_to_compressed_memory(0xBEDA8, 320, None) # Night palette [1800, 0400)
|
|
self.move_to_compressed_memory(
|
|
0xBEEE8, 320, None
|
|
) # Underwater palette (between 1200 and 2400 at XX:30)
|
|
self.move_to_compressed_memory(
|
|
0xBF028, 320, None
|
|
) # Unknown palette. Maybe bowser castle? need to check...
|
|
self.move_to_compressed_memory(0xBF168, 320, None) # Dawn palette [0500, 0600)
|
|
|
|
# These are scene headers, each containing 2x uint32_t's.
|
|
# They are MOSTLY [0x36, 0xF], but there are a few like [0x30, 0xF] and [0x20, 0xF],
|
|
# Referenced by the scene table
|
|
self.move_to_compressed_memory(0xBF2A8, 45 * 8, None)
|
|
|
|
# IDK what this is.
|
|
self.move_to_compressed_memory(0xBF410, 144, 0x1658C)
|
|
|
|
# SCENE TABLE
|
|
# Goes in chunks of 20 bytes (5 addresses)
|
|
# Each scene is represented by 5 pointers:
|
|
# 1. Pointer to a 2x uint32_t header (I think it's total tile (w, h) )
|
|
# The H is always 15, which would be 240 pixels tall.
|
|
# The W is usually 54, which would be 864 pixels (probably the flag pole?)
|
|
# 2. RLE something. Usually 32 bytes.
|
|
# 3. RLE something
|
|
# 4. RLE something
|
|
# 5. Palette
|
|
#
|
|
# The RLE encoded data could be background tilemap, animation routine, etc.
|
|
lookup_table_start = 0xB_F4A0
|
|
lookup_table_end = 0xB_F838
|
|
lookup_table_len = lookup_table_end - lookup_table_start # 46 * 5 * 4 = 920
|
|
for addr in range(lookup_table_start, lookup_table_end, 4):
|
|
self.external.lookup(addr)
|
|
|
|
# Now move the table
|
|
self.move_to_compressed_memory(lookup_table_start, lookup_table_len, 0xDF88)
|
|
|
|
# Not sure what this is
|
|
references = [
|
|
0xE8F8,
|
|
0xF4EC,
|
|
0xF4F8,
|
|
0x10098,
|
|
0x105B0,
|
|
]
|
|
self.move_to_compressed_memory(0xBF838, 280, references)
|
|
|
|
self.move_to_compressed_memory(0xBF950, 180, [0xE2E4, 0xF4FC])
|
|
self.move_to_compressed_memory(0xBFA04, 8, 0x1_6590)
|
|
self.move_to_compressed_memory(0xBFA0C, 784, 0x1_0F9C)
|
|
|
|
# MOVE EXTERNAL FUNCTIONS
|
|
new_loc = self.move_ext(0xB_FD1C, 14244, None)
|
|
references = [ # internal references to external functions
|
|
0x00D330,
|
|
0x00D310,
|
|
0x00D308,
|
|
0x00D338,
|
|
0x00D348,
|
|
0x00D360,
|
|
0x00D368,
|
|
0x00D388,
|
|
0x00D358,
|
|
0x00D320,
|
|
0x00D350,
|
|
0x00D380,
|
|
0x00D378,
|
|
0x00D318,
|
|
0x00D390,
|
|
0x00D370,
|
|
0x00D340,
|
|
0x00D398,
|
|
0x00D328,
|
|
]
|
|
for reference in references:
|
|
self.internal.lookup(reference)
|
|
|
|
references = [ # external references to external functions
|
|
0xC_1174,
|
|
0xC_313C,
|
|
0xC_049C,
|
|
0xC_1178,
|
|
0xC_220C,
|
|
0xC_3490,
|
|
0xC_3498,
|
|
]
|
|
for reference in references:
|
|
reference = reference - 0xB_FD1C + new_loc
|
|
try:
|
|
self.internal.lookup(reference)
|
|
except (IndexError, KeyError):
|
|
self.external.lookup(reference)
|
|
|
|
# BALL sound samples.
|
|
self.move_to_compressed_memory(0xC34C0, 6168, 0x43EC)
|
|
self.rwdata_lookup(0xC34C0, 6168)
|
|
self.move_to_compressed_memory(0xC4CD8, 2984, 0x459C)
|
|
self.move_to_compressed_memory(0xC5880, 120, 0x4594)
|
|
|
|
total_image_length = 193_568
|
|
references = [
|
|
0x1097C,
|
|
0x1097C + 4,
|
|
0x1097C + 8,
|
|
0x1097C + 12,
|
|
0x1097C + 16,
|
|
]
|
|
for name, index in [
|
|
("mario_sleeping", 0xC_58F8),
|
|
("mario_juggling", 0xC_D858),
|
|
("bowser_sleeping", 0xD_6C78),
|
|
("pizza", 0xE_16F8),
|
|
("minions_sleeping", 0xE_C318),
|
|
]:
|
|
img, _ = decode_backdrop(self.external[index:])
|
|
img.save(build_dir / f"backdrop_{name}.png")
|
|
|
|
if self.args.no_sleep_images:
|
|
# Images Notes:
|
|
# * In-between images are just zeros.
|
|
#
|
|
# start: 0x900C_58F8 end: 0x900C_D83F mario sleeping
|
|
# start: 0x900C_D858 end: 0x900D_6C65 mario juggling
|
|
# start: 0x900D_6C78 end: 0x900E_16E2 bowser sleeping
|
|
# start: 0x900E_16F8 end: 0x900E_C301 mario and luigi eating pizza
|
|
# start: 0x900E_C318 end: 0x900F_4D04 minions sleeping
|
|
# zero_padded_end: 0x900f_4d18
|
|
# Total Image Length: 193_568 bytes
|
|
printe("Deleting sleeping images.")
|
|
self.external.replace(0xC58F8, b"\x00" * total_image_length)
|
|
for reference in references:
|
|
self.internal.replace(reference, b"\x00" * 4) # Erase image references
|
|
self.ext_offset -= total_image_length
|
|
else:
|
|
self.move_ext(0xC58F8, total_image_length, references)
|
|
|
|
# Definitely at least contains part of the TIME graphic on startup screen.
|
|
self.move_to_compressed_memory(0xF4D18, 2880, 0x10960)
|
|
|
|
# What is this data?
|
|
# The memcpy to this address is all zero, so i guess its not used?
|
|
self.external.replace(0xF5858, b"\x00" * 34728) # refence at internal 0x7210
|
|
self.ext_offset -= 34728
|
|
|
|
if self.compressed_memory_pos:
|
|
# Compress and copy over compressed_memory
|
|
self.internal.rwdata.append(
|
|
self.compressed_memory[: self.compressed_memory_pos].copy(),
|
|
self.compressed_memory.FLASH_BASE,
|
|
)
|
|
|
|
# Compress, insert, and reference the modified rwdata
|
|
self.int_pos += self.internal.rwdata.write_table_and_data(
|
|
0x17DB4, data_offset=self.int_pos
|
|
)
|
|
|
|
# Shorten the external firmware
|
|
# This rounds the negative self.ext_offset towards zero.
|
|
self.ext_offset = round_up_page(self.ext_offset)
|
|
|
|
if self.args.no_save:
|
|
# Disable nvram loading
|
|
for nop in [0x495E, 0x49A6, 0x49B2]:
|
|
self.internal.nop(nop, 2)
|
|
# self.internal.b(0x4988, 0x49be) # If you still want the first-startup "Press TIME Button" screen
|
|
self.internal.b(0x4988, 0x49C0) # Skips Press TIME Button screen
|
|
|
|
# Disable nvram saving
|
|
# This just skips the body of the nvram_write_bank function
|
|
self.internal.b(0x48BE, 0x4912)
|
|
|
|
self.ext_offset -= 8192
|
|
else:
|
|
printi("Update NVRAM read addresses")
|
|
self.internal.asm(
|
|
0x4856,
|
|
"ite ne; "
|
|
f"movne.w r4, #{hex(0xff000 + self.ext_offset)}; "
|
|
f"moveq.w r4, #{hex(0xfe000 + self.ext_offset)}",
|
|
)
|
|
printi("Update NVRAM write addresses")
|
|
self.internal.asm(
|
|
0x48C0,
|
|
"ite ne; "
|
|
f"movne.w r4, #{hex(0xff000 + self.ext_offset)}; "
|
|
f"moveq.w r4, #{hex(0xfe000 + self.ext_offset)}",
|
|
)
|
|
|
|
# Finally, shorten the firmware
|
|
printi("Updating end of OTFDEC pointer")
|
|
self.internal.add(0x1_06EC, self.ext_offset)
|
|
self.external.shorten(self.ext_offset)
|
|
|
|
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
|