Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions src/mars_patcher/mf/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
9 changes: 8 additions & 1 deletion src/mars_patcher/mf/door_locks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
5 changes: 2 additions & 3 deletions src/mars_patcher/mf/item_patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
12 changes: 4 additions & 8 deletions src/mars_patcher/mf/navigation_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 4 additions & 6 deletions src/mars_patcher/mf/room_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
14 changes: 4 additions & 10 deletions src/mars_patcher/minimap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
101 changes: 77 additions & 24 deletions src/mars_patcher/rom.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections.abc import Sequence
from enum import Enum
from os import PathLike

Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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:
Expand Down
19 changes: 6 additions & 13 deletions src/mars_patcher/room_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
9 changes: 7 additions & 2 deletions src/mars_patcher/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand Down Expand Up @@ -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:
Expand Down