diff --git a/src/mars_patcher/mf/connections.py b/src/mars_patcher/mf/connections.py index d4d3fcb..e17f8cc 100644 --- a/src/mars_patcher/mf/connections.py +++ b/src/mars_patcher/mf/connections.py @@ -75,15 +75,14 @@ def __init__(self, rom: Rom): self.area_conns_count = gd.area_connections_count(rom) def set_elevator_connections(self, data: MarsschemamfElevatorconnections) -> None: - # Repoint area connections data + # Reserve space for 8 more area connections and repoint size = self.area_conns_count * 3 - # Reserve space for 8 more area connections - new_size = size + 8 * 3 - ac_addr = self.rom.reserve_free_space(new_size) - self.rom.copy_bytes(self.area_conns_addr, ac_addr, size) - # TODO: Move constant - self.rom.write_ptr(0x6945C, ac_addr) - self.area_conns_addr = ac_addr + ac_data = self.rom.read_bytes(self.area_conns_addr, size) + ac_data += bytearray(8 * 3) + # TODO: Move pointer constant + self.area_conns_addr = self.rom.write_repointable_data( + self.area_conns_addr, size, ac_data, [0x6945C] + ) # Connect tops to bottoms pairs_top = data["ElevatorTops"] diff --git a/src/mars_patcher/mf/door_locks.py b/src/mars_patcher/mf/door_locks.py index 36d3f9d..77df437 100644 --- a/src/mars_patcher/mf/door_locks.py +++ b/src/mars_patcher/mf/door_locks.py @@ -285,7 +285,14 @@ def change_minimap_tiles( for (x, y, room), tile_changes in area_map.items(): tile_id, palette, h_flip, v_flip = minimap.get_tile_value(x, y) - tile_data = ALL_DOOR_TILES[tile_id] + try: + tile_data = ALL_DOOR_TILES[tile_id] + except KeyError: + logging.warning( + f"Minimap tile 0x{tile_id:X} in area {area} " + + f"at 0x{x:X}, 0x{y:X} was expected to have a door" + ) + continue # Account for h_flip before changing edges left = tile_changes.get("left") diff --git a/src/mars_patcher/mf/item_patcher.py b/src/mars_patcher/mf/item_patcher.py index ea6cf0e..6abbf21 100644 --- a/src/mars_patcher/mf/item_patcher.py +++ b/src/mars_patcher/mf/item_patcher.py @@ -225,9 +225,8 @@ def write_custom_message( ), centered=messages.centered, ) - message_addr = rom.reserve_free_space(len(encoded_text) * 2) - rom.write_ptr(message_table_addrs[lang] + (4 * custom_message_id), message_addr) - rom.write_16_list(message_addr, encoded_text) + message_ptr = message_table_addrs[lang] + (4 * custom_message_id) + rom.write_data_with_pointers(encoded_text, [message_ptr]) # TODO: Move these? diff --git a/src/mars_patcher/mf/navigation_text.py b/src/mars_patcher/mf/navigation_text.py index fbad6f1..60d928f 100644 --- a/src/mars_patcher/mf/navigation_text.py +++ b/src/mars_patcher/mf/navigation_text.py @@ -108,18 +108,14 @@ def write(self, rom: Rom) -> None: # Info Text for info_place, text in lang_texts["ShipText"].items(): encoded_text = encode_text(rom, MessageType.CONTINUOUS, text) - text_addr = rom.reserve_free_space(len(encoded_text) * 2) - rom.write_ptr(base_text_address + info_place.value * 4, text_addr) - rom.write_ptr(base_text_address + info_place.value * 4 + 4, text_addr) - rom.write_16_list(text_addr, encoded_text) + text_ptr = base_text_address + info_place.value * 4 + rom.write_data_with_pointers(encoded_text, [text_ptr, text_ptr + 4]) # Navigation Text for nav_room, text in lang_texts["NavigationTerminals"].items(): encoded_text = encode_text(rom, MessageType.CONTINUOUS, text) - text_addr = rom.reserve_free_space(len(encoded_text) * 2) - rom.write_ptr(base_text_address + nav_room.value * 8, text_addr) - rom.write_ptr(base_text_address + nav_room.value * 8 + 4, text_addr) - rom.write_16_list(text_addr, encoded_text) + text_ptr = base_text_address + nav_room.value * 8 + rom.write_data_with_pointers(encoded_text, [text_ptr, text_ptr + 4]) @classmethod def apply_hint_security( diff --git a/src/mars_patcher/mf/room_names.py b/src/mars_patcher/mf/room_names.py index 1d92cf9..0b4d7d9 100644 --- a/src/mars_patcher/mf/room_names.py +++ b/src/mars_patcher/mf/room_names.py @@ -23,13 +23,11 @@ def write_room_names(rom: Rom, data: list[MarsschemamfRoomnamesItem]) -> None: seen_rooms.add((area_id, room_id)) # Find room name table by indexing at ROOM_NAMES_TABLE_ADDR - area_addr = rom.read_ptr(ROOM_NAMES_TABLE_ADDR) + (area_id * 4) - area_room_name_addr = rom.read_ptr(area_addr) + area_ptr = rom.read_ptr(ROOM_NAMES_TABLE_ADDR) + (area_id * 4) + area_room_name_ptrs = rom.read_ptr(area_ptr) # Find specific room by indexing by the room_id - room_name_addr = area_room_name_addr + (room_id * 4) + room_name_ptr = area_room_name_ptrs + (room_id * 4) encoded_text = encode_text(rom, MessageType.TWO_LINE, room_name) - message_addr = rom.reserve_free_space(len(encoded_text) * 2) - rom.write_ptr(room_name_addr, message_addr) - rom.write_16_list(message_addr, encoded_text) + rom.write_data_with_pointers(encoded_text, [room_name_ptr]) diff --git a/src/mars_patcher/minimap.py b/src/mars_patcher/minimap.py index 14cdc94..c10b97c 100644 --- a/src/mars_patcher/minimap.py +++ b/src/mars_patcher/minimap.py @@ -21,7 +21,7 @@ def __init__(self, rom: Rom, id: MinimapId): self.rom = rom self.pointer = minimap_ptrs(rom) + (id * 4) addr = rom.read_ptr(self.pointer) - self.tile_data, self.comp_len = decomp_lz77(rom.data, addr) + self.tile_data, self.comp_size = decomp_lz77(rom.data, addr) def __enter__(self) -> Minimap: # We don't need to do anything @@ -62,15 +62,9 @@ def set_tile_value( def write(self) -> None: comp_data = comp_lz77(self.tile_data) - comp_len = len(comp_data) - if comp_len > self.comp_len: - # Repoint data - addr = self.rom.reserve_free_space(comp_len) - self.rom.write_ptr(self.pointer, addr) - else: - addr = self.rom.read_ptr(self.pointer) - self.rom.write_bytes(addr, comp_data) - self.comp_len = comp_len + addr = self.rom.read_ptr(self.pointer) + self.rom.write_repointable_data(addr, self.comp_size, comp_data, [self.pointer]) + self.comp_size = len(comp_data) def apply_minimap_edits(rom: Rom, edit_dict: dict) -> None: diff --git a/src/mars_patcher/rom.py b/src/mars_patcher/rom.py index 4de3a6f..880678b 100644 --- a/src/mars_patcher/rom.py +++ b/src/mars_patcher/rom.py @@ -1,3 +1,4 @@ +from collections.abc import Sequence from enum import Enum from os import PathLike @@ -111,6 +112,8 @@ def __init__(self, path: str | PathLike[str]): self.free_space_addr = ReservedConstantsMF.PATCHER_FREE_SPACE_ADDR elif self.is_zm(): self.free_space_addr = ReservedConstantsZM.PATCHER_FREE_SPACE_ADDR + # Track all spaces freed when data is repointed. Keys are addresses, values are sizes + self.free_spaces: dict[int, int] = {} def is_mf(self) -> bool: """Returns true when the currently loaded game is Metroid Fusion.""" @@ -212,37 +215,87 @@ def write_bytes( val_end = val_addr + size self.data[data_addr:data_end] = vals[val_addr:val_end] - def write_16_list(self, addr: int, vals: list[int]) -> int: - """Writes a list of numbers as 16-bit integers. Does not check if the - values are within 16-bit range. Returns the ending address.""" - for val in vals: - self.write_16(addr, val) - addr += 2 - return addr - def copy_bytes(self, src_addr: int, dst_addr: int, size: int) -> None: """Copies a specified amount of bytes from the source address to the destination address.""" self.write_bytes(dst_addr, self.data, src_addr, size) - def reserve_free_space(self, size: int) -> int: + @staticmethod + def align_4_bytes(num: int) -> int: + remain = num % 4 + if remain != 0: + num += 4 - remain + return num + + def reserve_free_space(self, data_size: int) -> int: """ - Returns an address that is able to fit in a specified size. - Alignment is always 4. + Returns an address that is able to fit data with the specified size. The alignment is + always 4. """ - remain = self.free_space_addr % 4 - if remain != 0: - self.free_space_addr += 4 - remain - addr = self.free_space_addr - self.free_space_addr += size - # Check if past end of reserved space - if self.is_mf(): - free_space_end = ReservedConstantsMF.PATCHER_FREE_SPACE_END - elif self.is_zm(): - free_space_end = ReservedConstantsZM.PATCHER_FREE_SPACE_END + # Check for existing free space that can fit the specified size + data_addr: int | None = None + for free_addr, free_size in self.free_spaces.items(): + if data_size <= free_size: + data_addr = free_addr + break + if data_addr is not None: + # Remove found entry + free_size = self.free_spaces.pop(data_addr) + # Add new entry if there's still space remaining (align to 4 bytes first) + free_addr = self.align_4_bytes(data_addr + data_size) + free_size -= free_addr - data_addr + if free_size >= 4: + self.free_spaces[free_addr] = free_size else: - raise ValueError(self.game) - if self.free_space_addr > free_space_end: - raise RuntimeError("Ran out of reserved free space") + # No existing free space found, use end of ROM + self.free_space_addr = self.align_4_bytes(self.free_space_addr) + data_addr = self.free_space_addr + self.free_space_addr += data_size + # Check if past end of reserved space + if self.is_mf(): + free_space_end = ReservedConstantsMF.PATCHER_FREE_SPACE_END + elif self.is_zm(): + free_space_end = ReservedConstantsZM.PATCHER_FREE_SPACE_END + else: + raise ValueError(self.game) + if self.free_space_addr > free_space_end: + raise RuntimeError("Ran out of reserved free space") + return data_addr + + def write_repointable_data( + self, addr: int, prev_size: int, vals: BytesLike, pointers: Sequence[int] + ) -> int: + """ + Writes data that may have changed size. If bigger than its previous size, new space will + be allocated and the provided pointers will be repointed. The provided address and + previous size are used to mark the original location as free space. Returns the address + where the data was written. + """ + # Ensure each pointer points to the provided address + for ptr in pointers: + if self.read_ptr(ptr) != addr: + raise ValueError(f"Expected pointer at 0x{ptr:X} to be 0x{addr:X}") + write_addr = addr + if len(vals) > prev_size: + # Data is bigger, so reserve new space and repoint pointers + write_addr = self.reserve_free_space(len(vals)) + for ptr in pointers: + self.write_ptr(ptr, write_addr) + # Mark the original location as free space (align to 4 bytes first) + free_addr = self.align_4_bytes(addr) + free_size = prev_size - (free_addr - addr) + self.free_spaces[free_addr] = free_size + self.write_bytes(write_addr, vals) + return write_addr + + def write_data_with_pointers(self, vals: BytesLike, pointers: Sequence[int]) -> int: + """ + Writes data by allocating new space and writes the address to the provided pointers. + Returns the address where the data was written. + """ + addr = self.reserve_free_space(len(vals)) + for ptr in pointers: + self.write_ptr(ptr, addr) + self.write_bytes(addr, vals) return addr def save(self, path: str | PathLike[str]) -> None: diff --git a/src/mars_patcher/room_entry.py b/src/mars_patcher/room_entry.py index e518cf2..b7e8eda 100644 --- a/src/mars_patcher/room_entry.py +++ b/src/mars_patcher/room_entry.py @@ -80,7 +80,8 @@ def __init__(self, rom: Rom, ptr: int): self.pointer = ptr self.width = rom.read_8(addr) self.height = rom.read_8(addr + 1) - self.block_data, self.comp_len = decomp_rle(rom.data, addr + 2) + self.block_data, comp_size = decomp_rle(rom.data, addr + 2) + self.data_size = comp_size + 2 def get_block_value(self, x: int, y: int) -> int: idx = (y * self.width + x) * 2 @@ -102,15 +103,7 @@ def set_block_value(self, x: int, y: int, value: int) -> None: self.block_data[idx + 1] = value >> 8 def write(self) -> None: - comp_data = comp_rle(self.block_data) - comp_len = len(comp_data) - if comp_len > self.comp_len: - # Repoint data - addr = self.rom.reserve_free_space(comp_len + 2) - self.rom.write_ptr(self.pointer, addr) - else: - addr = self.rom.read_ptr(self.pointer) - self.rom.write_8(addr, self.width) - self.rom.write_8(addr + 1, self.height) - self.rom.write_bytes(addr + 2, comp_data) - self.comp_len = comp_len + data = bytearray([self.width, self.height]) + comp_rle(self.block_data) + addr = self.rom.read_ptr(self.pointer) + self.rom.write_repointable_data(addr, self.data_size, data, [self.pointer]) + self.data_size = len(data) diff --git a/src/mars_patcher/text.py b/src/mars_patcher/text.py index 42600fa..fbdbfe2 100644 --- a/src/mars_patcher/text.py +++ b/src/mars_patcher/text.py @@ -117,7 +117,7 @@ def encode_text( string: str, max_width: int = MAX_LINE_WIDTH, centered: bool = False, -) -> list[int]: +) -> bytes: char_map = get_char_map(rom.region) char_widths_addr = character_widths(rom) text: list[int] = [] @@ -236,7 +236,12 @@ def encode_text( text.append(NEWLINE) text.append(END) - return text + + text_bytes = bytearray() + for val in text: + text_bytes.append(val & 0xFF) + text_bytes.append(val >> 8) + return bytes(text_bytes) def write_seed_hash(rom: Rom, seed_hash: str) -> None: