Files
dockipelago/worlds/cvlod/patcher.py
Jonathan Tinney 7971961166
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

1499 lines
95 KiB
Python

import logging
import zlib
import struct
from typing import Collection, TypedDict, NotRequired
from .data.enums import Scenes, Objects, ObjectExecutionFlags, ActorSpawnFlags, Items, Pickups, PickupFlags, \
DoorFlags
from .cvlod_text import cvlod_string_to_bytearray, cvlod_strings_to_pool, cvlod_bytes_to_string, \
CVLOD_STRING_END_CHARACTER, CVLOD_TEXT_POOL_END_CHARACTER
N64_RDRAM_START = 0x80000000
CRC1_START = 0x10
CRC2_START = 0x14
CRC_ROM_START = 0x1000
CRC_DATA_LENGTH = 0x100000
CIC_6102_7101_INITIAL_VALUE = ((0x5D588B65 * 0x3F) & 0xFFFFFFFF) + 0x1
COMMON_SEGMENT_RDRAM_START = 0x141870 | N64_RDRAM_START
SCENE_OVERLAY_RDRAM_START = 0x2E3B70 | N64_RDRAM_START
COMMON_SEGMENT_ROM_START = 0xC2120 # 80141870
SCENE_OVERLAY_RDRAM_ADDRS_START = 0xB3858 # 800B2C58
SCENE_OVERLAY_ROM_ADDRS_START = 0xB39E8 # 800B2DE8
SCENE_ACTOR_PTRS_START = 0x10D150 # 8018C8A0
SCENE_1HB_PTRS_START = 0x11222C # 8019197C
SCENE_3HB_PTRS_START = 0x1122F4 # 80191A44
SCENE_ENEMY_PILLARS_PTRS_START = 0x10DBB4 # 8018D304
SCENE_DOOR_PTRS_START = 0x1108C0 # 80190010
SCENE_LOADING_ZONE_PTRS_START = 0x110D38 # 80190488
SCENE_TEXT_PTRS_START = 0xB8DE0 # 800B81E0
SCENE_SPAWN_COORDS_PTRS_START = 0x1107F8 # 8018FF48
NORMAL_ACTOR_ENTRY_LENGTH = 0x20
ONE_HIT_BREAKABLE_ENTRY_LENGTH = 0xC
THREE_HIT_BREAKABLE_ENTRY_LENGTH = 0xC
ENEMY_PILLAR_ENTRY_LENGTH = 0x10
ENEMY_PILLAR_ACTOR_ENTRY_LENGTH = 0x18
LOADING_ZONE_ENTRY_LENGTH = 0x14
DOOR_ENTRY_LENGTH = 0x2C
SCENE_SPAWN_LENGTH = 0x16
EXTENDED_DATA_ACTORS: dict[int, str] = {Objects.ONE_HIT_BREAKABLE: "1hb",
Objects.THREE_HIT_BREAKABLE: "3hb",
Objects.FOGGY_LAKE_ABOVE_DECKS_BARREL: "1hb special",
Objects.FOGGY_LAKE_BELOW_DECKS_BARREL: "1hb special",
Objects.SORCERY_BLUE_DIAMOND: "1hb special",
Objects.ENEMY_GENERATOR_PILLAR: "pillar",
Objects.DOOR: "door",
Objects.LOADING_ZONE: "load"}
SPECIAL_1HB_HARDCODED_PTRS_ADDRS: dict[int, int] = {Scenes.FOGGY_LAKE_ABOVE_DECKS: 0xC4, # 0x802E3C34
Scenes.FOGGY_LAKE_BELOW_DECKS: 0xC4, # 0x802E3C34
Scenes.TOWER_OF_SORCERY: 0x3FA4} # 0x802E7B14
class CVLoDSceneDataEntry(TypedDict):
"""Base class that all CVLoD scene data entries inherit from."""
start_addr: NotRequired[int] # Where in the RDRAM the data entry starts, if it's vanilla. For easier debugging.
delete: NotRequired[bool] # Whether the entry should be deleted when it comes time to reinsert the list.
class CVLoDNormalActorEntry(CVLoDSceneDataEntry):
"""An entry from any regular actor list in any scene in the game."""
spawn_flags: int # 16-bit bitfield comprising flags that affect the condition under which the actor spawns.
status_flags: int # 16-bit bitfield comprising flags used to tell the actor's current spawned state. Normally 0.
x_pos: float # How far east/west from the center of the scene the actor is located. 32-bit float.
y_pos: float # How far up/down from the center of the scene the actor is located. 32-bit float.
z_pos: float # Hor far north/south from the center of the scene the actor is located. 32-bit float.
execution_flags: int # Flags related to execution functionality with the actor. Upper 5 bits in the object ID bytes.
object_id: int # The game's ID for what object the actor is (breakable, enemy, pickup, etc.). Lower 11 bits in the
# object ID bytes.
flag_id: int # For normal: Int16 ID for the flag value the actor checks to see if it should or shouldn't spawn, if
# spawn flags 0x0020 or 0x0080 are set on it.
# For pillar: 32-bit struct for a flag to check to see if the pillar should spawn the actor. If the
# 80000000 bitflag is set, check the flag ID being set. If 40000000 is set, check it being un-set.
var_a: int # Extra int16 parameters that apply to the actor (exactly what these mean vary per actor).
var_b: int
var_c: int
var_d: int
extra_condition_ptr: int # Int32 pointer to an extra spawn check function to run, if spawn flag 0x0008 is enabled.
class CVLoDPillarActorEntry(CVLoDSceneDataEntry):
"""An entry from the enemy generator pillar actor list found exclusively in Tower of Execution (Central Tower)."""
x_pos: int # How far east/west from the center of the scene the actor is located. Signed 16-bit int, unlike the
# regular actor lists which use floats.
y_pos: int # How far up/down from the center of the scene the actor is located. Signed 16-bit int.
z_pos: int # Hor far north/south from the center of the scene the actor is located. Signed 16-bit int.
execution_flags: int # Flags related to execution functionality with the actor. Upper 5 bits in the object ID bytes.
object_id: int # The game's ID for what object the actor is (breakable, enemy, pickup, etc.). Lower 11 bits in the
# object ID bytes.
flag_id: int # For normal: Int16 ID for the flag value the actor checks to see if it should or shouldn't spawn, if
# spawn flags 0x0020 or 0x0080 are set on it.
# For pillar: 32-bit struct for a flag to check to see if the pillar should spawn the actor. If the
# 80000000 bitflag is set, check the flag ID being set. If 40000000 is set, check it being un-set.
var_c: int # Extra int16 parameters that apply to the actor (exactly what these mean vary per actor).
var_a: int # (Yes, these are out of order in the actual game's structs).
var_b: int
var_d: int
class CVLoD1HitBreakableEntry(CVLoDSceneDataEntry):
"""An entry from the scene's list of 1-hit breakable datas."""
appearance_id: int # Int16 ID for what appearance the 1HB takes. 0 = normal floor candle, 1 = wall candle, etc.
pickup_id: int # Int16 ID for what pickup to drop upon breaking the 1HB if the event flag isn't set.
flag_id: int # Int32 ID for what event flag the dropped pickup checks to see if it should spawn and sets upon being
# picked up.
pickup_flags: int # 16-bit bitfield comprising bitflags that affect the dropped pickup's behavior.
class CVLoD3HitBreakableEntry(CVLoDSceneDataEntry):
"""An entry from the scene's list of 3-hit breakable datas."""
appearance_id: int # Int16 ID for what breakable's appearance the 3HB takes.
pickup_count: int # How many item IDs from the item array start the 3HB drops. Int16.
pickup_array_start: int # 32-bit address in RAM for the first pickup ID the 3HB drops. The remaining pickup IDs are
# always located right after the first one.
flag_id: int # Int32 ID for what event flag the 3HB checks to see if it should break and sets upon breaking. In
# this implementation, it is instead the first flag ID to set on the first dropped pickup, with the
# subsequent pickups getting +1 every time (the 3HBs break every time no matter what).
class CVLoDEnemyPillarEntry(CVLoDSceneDataEntry):
"""An entry from the scene's list of enemy generator pillar datas (exclusive to Tower of Execution (Central))."""
actor_list_start: int # 32-bit address in RAM for the first actor the pillar spawns when broken. The remaining
# actors are always located right after the first one.
actor_count: int # How many actors from the actor list start the pillar spawns. Int16.
dissolve_flags: int # 16-bit bitfield comprising flags that affect the dissolve effect when the pillar breaks.
rotation: int # What angle direction the pillar is facing on the Y axis. Signed int16.
flag_id: int # Int32 ID for what event flag the pillar checks to see if it should break and sets upon breaking.
class CVLoDLoadingZoneEntry(CVLoDSceneDataEntry):
"""An entry from the scene's list of loading zone datas."""
heal_player: bool # Whether the loading zone should heal the player. 16 bits.
scene_id: int # Int8 ID for which scene in the game the loading zone will send the player to.
spawn_id: int # Int8 ID for which entrance in the destination scene the player will spawn at.
fade_settings_id: int # Int8 ID for which settings in the game's fade settings table will be used.
cutscene_settings_id: int # Int16 ID for which settings in the game's loading zone cutscene settings table will be
# used. 0 if no cutscene should play.
min_x_pos: int # How far east/west the scene the zone's bounding box min is located. Signed int16.
min_y_pos: int # How far up/down the zone's bounding box min is located. Signed int16.
min_z_pos: int # How far north/south the zone's bounding box min is located. Signed int16.
max_x_pos: int # How far east/west the zone's bounding box max is located. Signed int16.
max_y_pos: int # How far up/down the zone's bounding box max is located. Signed int16.
max_z_pos: int # How far north/south the zone's bounding box max is located. Signed int16.
class CVLoDDoorEntry(CVLoDSceneDataEntry):
"""An entry from the scene's list of door datas."""
dlist_addr: int # Int32 DisplayList address for the door's model in the scene file.
texture_id: int # Int8 ID for what texture to apply to the door. FF if not applicable.
palette_id: int # Int8 ID for what palette to apply to the door's texture. FF if not applicable.
byte_6: int # Something???
door_flags: int # 16-bit bitfield comprising flags that affect the door's opening conditions.
extra_condition_ptr: int # Int32 pointer to a custom door opening check function to run, if door flag 0x0200 is
# enabled. NOTE: This function is called INSTEAD of the regular door flags check one, so
# the custom function must call the regular one as part of it if that behavior's desired.
flag_id: int # Int16 ID for what event flag to check if certain door flags are set on this door.
item_id: int # Int32 ID for what item is used to open the door if door flag 0x0100 is enabled.
front_room_id: int # Int8 ID for which room to load when opening the door from the back. 80 if not applicable.
back_room_id: int # Int8 ID for which room to load when opening the door from the front. 80 if not applicable.
flag_locked_text_id: int # Int8 ID for what text in the scene's text pool to display if the player tries opening
# the door when they can't due to the door flags preventing it. 80 if nothing.
unlocked_text_id: int # Int8 ID for what text in the scene's text pool to display if the player opens the door via
# their unlocking animation. 80 if nothing.
enemy_locked_text_id: int # Int8 ID for what text in the scene's text pool to display if the player tries opening
# the door when they can't due to there being enemies around that lock all doors until
# defeated (like the Castle Center vampires). 80 if not applicable.
opening_sound_id: int # Int16 ID for what sound to play when the door starts opening.
half_20: int
closing_sound_id: int # Int16 ID for what sound to play when the door starts closing.
half_24: int
shut_sound_id: int # Int16 ID for what sound to play when the door slams shut.
half_28: int
half_2a: int
class CVLoDSpawnEntranceEntry(CVLoDSceneDataEntry):
"""An entry from the scene's list of spawn entrance coordinate datas."""
room_id: int # Int16 ID for which room to load initially if it's a room-based scene.
player_x_pos: int # How far east/west from the center of the scene the player spawns. Signed int16.
player_y_pos: int # How far up/down from the center of the scene the player spawns. Signed int16.
player_z_pos: int # How far north/south from the center of the scene the player spawns. Signed int16.
player_rotation: int # What angle direction the player is facing upon spawning in here. Signed int16.
camera_x_pos: int # How far east/west from the center of the scene the camera spawns. Signed int16.
camera_y_pos: int # How far up/down from the center of the scene the camera spawns. Signed int16.
camera_z_pos: int # How far north/south from the center of the scene the camera spawns. Signed int16.
focus_x_pos: int # How far e/w from the center of the scene the camera's initial point of focus is. Signed int16.
focus_y_pos: int # How far u/d from the center of the scene the camera's initial point of focus is. Signed int16.
focus_z_pos: int # How far n/s from the center of the scene the camera's initial point of focus is. Signed int16.
class CVLoDSceneTextEntry(CVLoDSceneDataEntry):
"""An entry from the scene's text pool."""
text: str
class CVLoDScene:
overlay: bytearray | None
spawn_spots: list[CVLoDSpawnEntranceEntry] # Spawn entrances in the scene.
enemy_pillars: list[CVLoDEnemyPillarEntry] # 3HB enemy pillars in the scene.
one_hit_breakables: list[CVLoD1HitBreakableEntry] # 1-hit breakables in the scene.
one_hit_special_breakables: list[CVLoD1HitBreakableEntry] # Special 1HBs that aren't the regular 1HB actor, like
# the Foggy Lake barrels and Sorcery diamonds. Always
# scene-specific with hardcoded ptrs to their datatables.
three_hit_drop_ids: list[int] # Array of item IDs that the 3HBs on the scene can drop.
three_hit_drops_start: int # Spot in RDRAM where the scene's 3HB drop IDs array begins.
three_hit_drops_orig_len: int # Original length of the 3HB drop IDs array.
three_hit_breakables: list[CVLoD3HitBreakableEntry] # 3-hit breakables in the scene.
scene_text: list[CVLoDSceneTextEntry] # All textbox texts associated with the scene.
scene_text_orig_size: int # Original size of the scene's text pool.
doors: list[CVLoDDoorEntry] # Doors in the scene.
loading_zones: list[CVLoDLoadingZoneEntry] # Loading zones in the scene.
# Dict of the normal init/proxy/room actor lists in the scene mapped to which one it is.
actor_lists: dict[str, list[CVLoDNormalActorEntry | CVLoDPillarActorEntry]]
highest_ids: dict[str, int] # Highest IDs for specific data like 1-hit breakables, determined from the actors.
start_addr: int | None # Where in RDRAM the scene's data starts when loaded, if it's vanilla. For easier debugging.
name: str | None # The name of the scene. To more easily see which one it is while debugging.
# space_available: dict[int, int] # Start and end addresses of space in the overlay freed up from moving a data
# # from one spot to another.
def __init__(self, start_addr: int | None=None, name: str | None=None) -> None:
self.start_addr = start_addr
self.name = name
self.highest_ids = {extended_data: -1 for object_id, extended_data in EXTENDED_DATA_ACTORS.items()}
self.spawn_spots = []
self.enemy_pillars = []
self.one_hit_breakables = []
self.one_hit_special_breakables = []
self.three_hit_drop_ids = []
self.three_hit_breakables = []
self.scene_text = []
self.doors = []
self.loading_zones = []
self.actor_lists = {}
self.space_available = {}
self.three_hit_drops_orig_len = 0
self.scene_text_orig_size = 0
def read_ovl_byte(self, offset: int) -> int | None:
"""Return a byte at a specified address in the scene's overlay."""
if self.overlay:
return self.overlay[offset]
logging.error(f"Scene {self.name} has no overlay associated with it. Fix the code and Try, Try Again!")
def read_ovl_bytes(self, start_address: int, length: int, return_as_int: bool = False) -> bytearray | int | None:
"""Return a string of bytes of a specified length beginning at a specified address in the scene's overlay."""
if not self.overlay:
logging.error(f"Scene {self.name} has no overlay associated with it. Fix the code and Never Let Up!")
return None
if return_as_int:
return int.from_bytes(self.overlay[start_address:start_address + length], "big")
return self.overlay[start_address:start_address + length]
def write_ovl_byte(self, address: int, value: int) -> None:
self.overlay[address] = value
def write_ovl_bytes(self, start_address: int, values: Collection[int]) -> None:
self.overlay[start_address:start_address + len(values)] = values
def write_ovl_int16(self, address: int, value: int) -> None:
value = value & 0xFFFF
self.write_ovl_bytes(address, [(value >> 8) & 0xFF, value & 0xFF])
def write_ovl_int16s(self, start_address: int, values: list[int]) -> None:
for i, value in enumerate(values):
self.write_ovl_int16(start_address + (i * 2), value)
def write_ovl_int24(self, address: int, value: int) -> None:
value = value & 0xFFFFFF
self.write_ovl_bytes(address, [(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF])
def write_ovl_int24s(self, start_address: int, values: list[int]) -> None:
for i, value in enumerate(values):
self.write_ovl_int24(start_address + (i * 3), value)
def write_ovl_int32(self, address: int, value: int) -> None:
value = value & 0xFFFFFFFF
self.write_ovl_bytes(address, [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF])
def write_ovl_int32s(self, start_address: int, values: list[int]) -> None:
for i, value in enumerate(values):
self.write_ovl_int32(start_address + (i * 4), value)
class CVLoDRomPatcher:
rom: bytearray
decompressed_files: dict[int, bytearray]
compressed_files: dict[int, bytearray]
scenes: list[CVLoDScene]
ni_table_start: int
ni_file_buffers_start: int
decomp_file_sizes_table_start: int
number_of_ni_files: int
current_write_file_addr: int
scene_overlays_end: int
def __init__(self, input_rom: bytearray) -> None:
self.rom = input_rom
self.decompressed_files = {}
self.compressed_files = {}
# Seek the "Nisitenma-Ichigo" string in the ROM indicating where the table containing the compressed file start
# and end offsets begins.
nisitenma_ichigo_start = self.rom.find("Nisitenma-Ichigo".encode("utf-8"))
# If the "Nisitenma-Ichigo" string is somehow nowhere to be found, raise an exception.
if nisitenma_ichigo_start == -1:
raise Exception("Nisitenma-Ichigo string not found.")
# The actual table begins 16 bytes after where the Nisitenma-Ichigo string begins.
self.ni_table_start = nisitenma_ichigo_start + 16
# Save the file start address of the first entry in the table to get the start of ALL the NI files in the ROM
# as a whole.
self.ni_file_buffers_start = int.from_bytes(self.rom[self.ni_table_start: self.ni_table_start + 4],
"big") & 0xFFFFFFF
# Figure out how many Nisitenma-Ichigo files there are alongside grabbing them out of the ROM.
self.number_of_ni_files = 1
while True:
# Each entry in the NI table is 8 bytes long. The first four are the ROM start address of a file, and the
# last four are the end address.
ni_table_entry_start = self.ni_table_start + ((self.number_of_ni_files - 1) * 8)
file_start_addr = self.read_bytes(ni_table_entry_start, 4, return_as_int=True) & 0xFFFFFFF
# If the file start address is 00000000, we've reached the end of the NI table. In which case, end the loop.
if not file_start_addr:
break
# Calculate the compressed file's size based on the start and end addresses and use that to read said file
# out of the ROM.
file_size = (int.from_bytes(self.read_bytes(ni_table_entry_start + 4, 4), "big") & 0xFFFFFFF) - \
file_start_addr
self.compressed_files[self.number_of_ni_files] = self.read_bytes(file_start_addr, file_size)
self.number_of_ni_files += 1
# Figure out where the decompressed file sizes table starts by going backwards the number of NI files from the
# start of "Nisitenma-Ichigo". Each entry in this table is 8 bytes long, with the 2nd-4th bytes being the size.
self.decomp_file_sizes_table_start = nisitenma_ichigo_start - 4 - (8 * self.number_of_ni_files)
# Figure out where the first scene overlay begins and the last scene overlay ends. This will be the first
# free-range that we write files into.
self.current_write_file_addr = self.read_bytes(SCENE_OVERLAY_ROM_ADDRS_START, 4, return_as_int=True)
self.scene_overlays_end = self.read_bytes(SCENE_OVERLAY_ROM_ADDRS_START + (Scenes.TEST_GRID * 8) + 4, 4,
return_as_int=True)
# Read out the important common scene data structs into a format that is easy to edit and then reinsert
# (actor lists, breakable datas, scene text, etc.).
self.scenes = []
for scene_id in range(len(Scenes) - 1):
self.scenes.append(CVLoDScene(name=Scenes(scene_id).name))
# Check to see if the scene has an associated overlay. If it doesn't (because the scene has no start address
# in the game's hardcoded list of scene overlay ROM starts), skip it.
scene_overlay_start = self.read_bytes(SCENE_OVERLAY_ROM_ADDRS_START + (scene_id * 8), 4,
return_as_int=True)
scene_overlay_end = self.read_bytes(SCENE_OVERLAY_ROM_ADDRS_START + (scene_id * 8) + 4, 4,
return_as_int=True)
if not scene_overlay_start:
self.scenes[scene_id].overlay = None
continue
# Otherwise, save its vanilla overlay data and continue.
self.scenes[scene_id].overlay = self.rom[scene_overlay_start: scene_overlay_end]
# Extract the scene's init actor list (the one that spawns all of its contents upon the scene initializing
# and keeps them spawned regardless of both the player's proximity to them and what room they're in). Take
# the third pointer in the scene's entry in the game's table of loaded scene actor list starts and determine
# how far in it is relative to the start of the overlay in RDRAM.
init_actors_start = self.read_bytes(SCENE_ACTOR_PTRS_START + 8 + (scene_id * 0x10), 4,
return_as_int=True) - SCENE_OVERLAY_RDRAM_START
self.scenes[scene_id].actor_lists["init"] = self.extract_normal_scene_actor_list(scene_id,
init_actors_start)
# Extract the scene's proxy actor list (the one that will spawn its things ONLY while the player is
# within a certain proximity from them and is also not tied to any room). This is the second pointer in the
# scene's entry in the above-mentioned table.
proxy_actors_start = self.read_bytes(SCENE_ACTOR_PTRS_START + 4 + (scene_id * 0x10),
4, return_as_int=True) - SCENE_OVERLAY_RDRAM_START
self.scenes[scene_id].actor_lists["proxy"] = self.extract_normal_scene_actor_list(scene_id,
proxy_actors_start)
# Extract the scene's room actor lists (the ones that will spawn their things only while their designated
# rooms are loaded and, much like the init list, don't care about the player's proximity). Not every scene
# has these; in fact, most don't. To see if the current scene we're looking at has one, we will check if the
# fourth pointer in the scene's table entry is not 00000000.
room_actor_ptrs_start = self.read_bytes(SCENE_ACTOR_PTRS_START + 0xC + (scene_id * 0x10), 4,
return_as_int=True)
# While we are at it, also take the first pointer, which points to not an actor list-related thing but the
# start of the scene's decoration data list. This is where the room actor pointers list ends.
scene_decorations_data_start = self.read_bytes(SCENE_ACTOR_PTRS_START + (scene_id * 0x10), 4,
return_as_int=True)
# If room actor lists exist, extract them.
if room_actor_ptrs_start != 0:
room_actor_ptrs_start -= SCENE_OVERLAY_RDRAM_START
scene_decorations_data_start -= SCENE_OVERLAY_RDRAM_START
# Grab the list of pointers to said lists. To determine how many pointers the list has, take the
# difference between the scene decorations data start address and the room actor pointers start address;
# the latter structs always begin immediately after the former.
room_list_ptrs = [self.scenes[scene_id].read_ovl_bytes(room_actor_ptrs_start + (room_id * 4), 4,
return_as_int=True) - SCENE_OVERLAY_RDRAM_START
for room_id in range((scene_decorations_data_start - room_actor_ptrs_start) // 4)]
# Extract the actor lists at the pointers we just extracted.
for room_id in range(len(room_list_ptrs)):
self.scenes[scene_id].actor_lists[f"room {room_id}"] = \
self.extract_normal_scene_actor_list(scene_id, room_list_ptrs[room_id])
# Extract the enemy pillar data if enemy pillar data exists (highest found enemy pillar ID is 0 or higher).
# This should only occur for the Tower of Execution (Central Tower) map.
self.scenes[scene_id].enemy_pillars = []
if self.scenes[scene_id].highest_ids["pillar"] > -1:
# Get the start address of the map's enemy pillar data in the overlay.
enemy_pillars_start = self.read_bytes(SCENE_ENEMY_PILLARS_PTRS_START + (scene_id * 4), 4,
return_as_int=True) - SCENE_OVERLAY_RDRAM_START
# Loop over every enemy pillar data that is known to exist based on how high the highest ID is.
enemy_pillar_list = []
lowest_pillar_actor_list_start = 0xFFFFFFFF
highest_pillar_actor_list_end = 0x00000000
for enemy_pillar_id in range(self.scenes[scene_id].highest_ids["pillar"] + 1):
current_enemy_pillar_start = enemy_pillars_start + (enemy_pillar_id * ENEMY_PILLAR_ENTRY_LENGTH)
enemy_pillar_data = self.scenes[scene_id].read_ovl_bytes(current_enemy_pillar_start,
ENEMY_PILLAR_ENTRY_LENGTH)
enemy_pillar = CVLoDEnemyPillarEntry(
actor_list_start=int.from_bytes(enemy_pillar_data[0x00:0x04], "big"),
actor_count=int.from_bytes(enemy_pillar_data[0x04:0x06], "big"),
dissolve_flags=int.from_bytes(enemy_pillar_data[0x06:0x08], "big"),
rotation=struct.unpack('>h', enemy_pillar_data[0x0A:0x0C])[0],
flag_id=int.from_bytes(enemy_pillar_data[0x0C:], "big"),
start_addr=current_enemy_pillar_start + SCENE_OVERLAY_RDRAM_START,
)
# If the pillar has a lower actor list start address than the lowest one we've found, save the new
# one.
if enemy_pillar["actor_list_start"] < lowest_pillar_actor_list_start:
lowest_pillar_actor_list_start = enemy_pillar["actor_list_start"]
# If the pillar has a higher actor list end address than the lowest one we've found, save the new
# one.
pillar_actor_list_end = enemy_pillar["actor_list_start"] + \
(enemy_pillar["actor_count"] * ENEMY_PILLAR_ACTOR_ENTRY_LENGTH)
if pillar_actor_list_end > highest_pillar_actor_list_end:
highest_pillar_actor_list_end = pillar_actor_list_end
# Append the enemy pillar to the end of the list.
enemy_pillar_list.append(enemy_pillar)
# Save the complete enemy pillar list.
self.scenes[scene_id].enemy_pillars = enemy_pillar_list
# Extract the map's enemy pillar actor list using the lowest and highest pillar actor list addresses
# that we gleamed from the regular enemy pillar data.
pillar_actor_list = []
for pillar_actor_list_id in range((highest_pillar_actor_list_end - lowest_pillar_actor_list_start)
// ENEMY_PILLAR_ACTOR_ENTRY_LENGTH):
current_enemy_pillar_actor_start = lowest_pillar_actor_list_start - SCENE_OVERLAY_RDRAM_START + \
(pillar_actor_list_id * ENEMY_PILLAR_ACTOR_ENTRY_LENGTH)
pillar_actor_data = self.scenes[scene_id].read_ovl_bytes(current_enemy_pillar_actor_start,
ENEMY_PILLAR_ACTOR_ENTRY_LENGTH)
object_id = int.from_bytes(pillar_actor_data[0x06:0x08], "big")
pillar_actor = CVLoDPillarActorEntry(
x_pos=struct.unpack('>h', pillar_actor_data[0x00:0x02])[0],
y_pos=struct.unpack('>h', pillar_actor_data[0x02:0x04])[0],
z_pos=struct.unpack('>h', pillar_actor_data[0x04:0x06])[0],
execution_flags=object_id >> 0xB,
object_id=object_id & 0x7FF,
var_c=struct.unpack('>H', pillar_actor_data[0x08:0x0A])[0],
var_a=struct.unpack('>H', pillar_actor_data[0x0A:0x0C])[0],
var_b=struct.unpack('>H', pillar_actor_data[0x0C:0x0E])[0],
var_d=struct.unpack('>H', pillar_actor_data[0x0E:0x10])[0],
flag_id=int.from_bytes(pillar_actor_data[0x14:], "big"),
start_addr=current_enemy_pillar_actor_start + SCENE_OVERLAY_RDRAM_START,
)
# Check if the actor has a documented entry in the actors enum.
if pillar_actor["object_id"] in Objects:
pillar_actor["object_id"] = Objects(pillar_actor["object_id"])
# Check if the actor is an item pickup actor, and if it is, does it have has an entry in the
# pickups enum.
if pillar_actor["object_id"] == Objects.INTERACTABLE and pillar_actor["var_c"] in Pickups:
pillar_actor["var_c"] = Pickups(pillar_actor["var_c"])
# Loop over every pickup flag in Var B and see if we have any of them set and documented.
for flag_index in range(0x16):
flag_to_check = pillar_actor["var_b"] & (1 << flag_index)
if flag_to_check in PickupFlags:
pillar_actor["var_b"] |= PickupFlags(flag_to_check)
# Loop over every execution flag and see if we have any of them set and documented.
for flag_index in range(0x5):
flag_to_check = pillar_actor["execution_flags"] & (1 << flag_index)
if flag_to_check in ObjectExecutionFlags:
pillar_actor["execution_flags"] |= ObjectExecutionFlags(flag_to_check)
# If the actor is in the dict of actors with extended data, check the data ID in param 3 against the
# highest one we've found so far for that actor on this map. If it's higher, then consider it the
# new highest.
if pillar_actor["object_id"] in EXTENDED_DATA_ACTORS.keys():
extended_data_type = EXTENDED_DATA_ACTORS[pillar_actor["object_id"]]
if pillar_actor["var_c"] > self.scenes[scene_id].highest_ids[extended_data_type]:
self.scenes[scene_id].highest_ids[extended_data_type] = pillar_actor["var_c"]
# Append the actor to the end of the list.
pillar_actor_list.append(pillar_actor)
# Save the complete pillar actor list.
self.scenes[scene_id].actor_lists["pillars"] = pillar_actor_list
# Extract the 1-hit breakables data if 1HB data exists (highest found 1HB ID is 0 or higher).
if self.scenes[scene_id].highest_ids["1hb"] > -1:
# Get the start address of the scene's 1HB data in the overlay.
one_hit_breakables_start = self.read_bytes(SCENE_1HB_PTRS_START + (scene_id * 4), 4,
return_as_int=True) - SCENE_OVERLAY_RDRAM_START
# Loop over every 1HB data that is known to exist based on how high the highest ID is.
one_hit_list = []
for one_hit_id in range(self.scenes[scene_id].highest_ids["1hb"] + 1):
current_one_hit_start = one_hit_breakables_start + (one_hit_id * ONE_HIT_BREAKABLE_ENTRY_LENGTH)
one_hit_data = self.scenes[scene_id].read_ovl_bytes(current_one_hit_start,
ONE_HIT_BREAKABLE_ENTRY_LENGTH)
one_hit = CVLoD1HitBreakableEntry(
appearance_id=int.from_bytes(one_hit_data[0x00:0x02], "big"),
pickup_id=int.from_bytes(one_hit_data[0x02:0x04], "big"),
flag_id=int.from_bytes(one_hit_data[0x04:0x08], "big"),
pickup_flags=int.from_bytes(one_hit_data[0x08:0x0A], "big"),
start_addr=current_one_hit_start + SCENE_OVERLAY_RDRAM_START,
)
# Check if the pickup ID has an entry in the pickups enum.
if one_hit["pickup_id"] in Pickups:
one_hit["pickup_id"] = Pickups(one_hit["pickup_id"])
# Loop over every pickup flag and see if we have any of them set and documented.
for flag_index in range(0x16):
flag_to_check = one_hit["pickup_flags"] & (1 << flag_index)
if flag_to_check in PickupFlags:
one_hit["pickup_flags"] |= PickupFlags(flag_to_check)
# Append the 1HB to the end of the list.
one_hit_list.append(one_hit)
# Save the complete 1HB list.
self.scenes[scene_id].one_hit_breakables = one_hit_list
# Extract the special 1-hit breakables data if special 1HB data exists (highest found 1HB special ID is 0
# or higher).
if self.scenes[scene_id].highest_ids["1hb special"] > -1:
# Get the start address of the scene's 1HB special data in the overlay. This is hardcoded in the overlay
# of every map that has one, so we will need to read the lower halves of two separate instructions and
# put them together (it's an LUI followed by an ADDIU into the same register in every case).
upper_addr_half = self.scenes[scene_id].read_ovl_bytes(SPECIAL_1HB_HARDCODED_PTRS_ADDRS[scene_id] + 2,
2, return_as_int= True)
lower_addr_half = self.scenes[scene_id].read_ovl_bytes(SPECIAL_1HB_HARDCODED_PTRS_ADDRS[scene_id] + 6,
2, return_as_int= True)
# If the lower half is 0x8000 or higher, subtract 1 from the upper half. ADDIU-ing a signed negative
# decrements the upper half of the number being added into by 1 normally.
if lower_addr_half >= 0x8000:
upper_addr_half -= 1
# Put the two halves together to form the complete address.
special_one_hit_breakables_start = (upper_addr_half << 0x10) + lower_addr_half \
- SCENE_OVERLAY_RDRAM_START
# Loop over every special 1HB data that is known to exist based on how high the highest ID is. It is
# formatted exactly the same way as regular 1HB data in every case.
special_one_hit_list = []
for special_one_hit_id in range(self.scenes[scene_id].highest_ids["1hb special"] + 1):
current_special_one_hit_start = special_one_hit_breakables_start + (special_one_hit_id *
ONE_HIT_BREAKABLE_ENTRY_LENGTH)
special_one_hit_data = self.scenes[scene_id].read_ovl_bytes(current_special_one_hit_start,
ONE_HIT_BREAKABLE_ENTRY_LENGTH)
special_one_hit = CVLoD1HitBreakableEntry(
appearance_id=int.from_bytes(special_one_hit_data[0x00:0x02], "big"),
pickup_id=int.from_bytes(special_one_hit_data[0x02:0x04], "big"),
flag_id=int.from_bytes(special_one_hit_data[0x04:0x08], "big"),
pickup_flags=int.from_bytes(special_one_hit_data[0x08:0x0A], "big"),
start_addr=current_special_one_hit_start + SCENE_OVERLAY_RDRAM_START,
)
# Check if the pickup ID has an entry in the pickups enum.
if special_one_hit["pickup_id"] in Pickups:
special_one_hit["pickup_id"] = Pickups(special_one_hit["pickup_id"])
# Loop over every pickup flag and see if we have any of them set and documented.
for flag_index in range(0x16):
flag_to_check = special_one_hit["pickup_flags"] & (1 << flag_index)
if flag_to_check in PickupFlags:
special_one_hit["pickup_flags"] |= PickupFlags(flag_to_check)
# Append the special 1HB to the end of the list.
special_one_hit_list.append(special_one_hit)
# Save the complete special 1HB list.
self.scenes[scene_id].one_hit_special_breakables = special_one_hit_list
# Extract the 3-hit breakables data if 3HB data exists (highest found 3HB ID is 0 or higher).
if self.scenes[scene_id].highest_ids["3hb"] > -1:
# Get the start address of the scene's 3HB data in the overlay.
three_hit_breakables_start = self.read_bytes(SCENE_3HB_PTRS_START + (scene_id * 4), 4,
return_as_int=True) - SCENE_OVERLAY_RDRAM_START
# Loop over every 3HB data that is known to exist based on how high the highest ID is.
three_hit_list = []
lowest_3hb_pickup_array_start = 0xFFFFFFFF
highest_3hb_pickup_array_end = 0x00000000
for three_hit_id in range(self.scenes[scene_id].highest_ids["3hb"] + 1):
current_three_hit_start = three_hit_breakables_start + (three_hit_id *
THREE_HIT_BREAKABLE_ENTRY_LENGTH)
three_hit_data = self.scenes[scene_id].read_ovl_bytes(current_three_hit_start,
THREE_HIT_BREAKABLE_ENTRY_LENGTH)
three_hit = CVLoD3HitBreakableEntry(
appearance_id=int.from_bytes(three_hit_data[0x00:0x02], "big"),
pickup_count=int.from_bytes(three_hit_data[0x02:0x04], "big"),
pickup_array_start=int.from_bytes(three_hit_data[0x04:0x08], "big"),
flag_id=int.from_bytes(three_hit_data[0x08:], "big"),
start_addr=current_three_hit_start + SCENE_OVERLAY_RDRAM_START,
)
# If the 3HB has a lower pickup array start address than the lowest one we've found, save the new
# one.
if three_hit["pickup_array_start"] < lowest_3hb_pickup_array_start:
lowest_3hb_pickup_array_start = three_hit["pickup_array_start"]
# If the 3HB has a higher pickup array end address than the lowest one we've found, save the new
# one.
pickup_array_end = three_hit["pickup_array_start"] + (three_hit["pickup_count"] * 2)
if pickup_array_end > highest_3hb_pickup_array_end:
highest_3hb_pickup_array_end = pickup_array_end
# Append the 3HB to the end of the list.
three_hit_list.append(three_hit)
# Save the complete 3HB list and the start address of its drop IDs array.
self.scenes[scene_id].three_hit_breakables = three_hit_list
self.scenes[scene_id].three_hit_drops_start = lowest_3hb_pickup_array_start
# Extract the scene's array of 3HB pickup IDs using the lowest and highest 3HB drop array addresses that
# we gleamed from the regular 3HB data.
three_hit_pickups_list = []
for three_hit_pickup_array_id in range((highest_3hb_pickup_array_end -
lowest_3hb_pickup_array_start) // 2):
three_hit_pickup_id = self.scenes[scene_id].read_ovl_bytes(
lowest_3hb_pickup_array_start - SCENE_OVERLAY_RDRAM_START + (three_hit_pickup_array_id * 2), 2,
return_as_int=True)
# Check if the pickup ID is in the pickups enum and then add it to the list.
if three_hit_pickup_id in Pickups:
three_hit_pickup_id = Pickups(three_hit_pickup_id)
three_hit_pickups_list.append(three_hit_pickup_id)
# Save the complete 3HB pickups list and its original length.
self.scenes[scene_id].three_hit_drop_ids = three_hit_pickups_list
self.scenes[scene_id].three_hit_drops_orig_len = len(three_hit_pickups_list)
# Extract the door data if door data exists (highest found door ID is 0 or higher).
if self.scenes[scene_id].highest_ids["door"] > -1:
# Get the start address of the scene's door data in the overlay.
doors_start = self.read_bytes(SCENE_DOOR_PTRS_START + (scene_id * 4), 4, return_as_int=True) \
- SCENE_OVERLAY_RDRAM_START
# Loop over every door data that is known to exist based on how high the highest ID is.
door_list = []
for door_id in range(self.scenes[scene_id].highest_ids["door"] + 1):
current_door_start = doors_start + (door_id * DOOR_ENTRY_LENGTH)
door_data = self.scenes[scene_id].read_ovl_bytes(current_door_start, DOOR_ENTRY_LENGTH)
door = CVLoDDoorEntry(
dlist_addr=int.from_bytes(door_data[0x00:0x04], "big"),
texture_id=door_data[0x04],
palette_id=door_data[0x05],
byte_6=door_data[0x06],
door_flags=int.from_bytes(door_data[0x08:0x0A], "big"),
extra_condition_ptr=int.from_bytes(door_data[0x0C:0x10], "big"),
flag_id=int.from_bytes(door_data[0x10:0x12], "big"),
item_id=int.from_bytes(door_data[0x14:0x18], "big"),
front_room_id=door_data[0x18],
back_room_id=door_data[0x19],
flag_locked_text_id=door_data[0x1A],
unlocked_text_id=door_data[0x1B],
enemy_locked_text_id=door_data[0x1C],
opening_sound_id=int.from_bytes(door_data[0x1E:0x20], "big"),
half_20=int.from_bytes(door_data[0x20:0x22], "big"),
closing_sound_id=int.from_bytes(door_data[0x22:0x24], "big"),
half_24=int.from_bytes(door_data[0x24:0x26], "big"),
shut_sound_id=int.from_bytes(door_data[0x26:0x28], "big"),
half_28=int.from_bytes(door_data[0x28:0x2A], "big"),
half_2a=int.from_bytes(door_data[0x2A:], "big"),
start_addr=current_door_start + SCENE_OVERLAY_RDRAM_START,
)
# Check if the door has an item ID with an entry in the items enum.
if door["item_id"] in Items:
door["item_id"] = Items(door["item_id"])
# Loop over every door flag and see if we have any of them set and documented.
for flag_index in range(0x16):
flag_to_check = door["door_flags"] & (1 << flag_index)
if flag_to_check in DoorFlags:
door["door_flags"] |= DoorFlags(flag_to_check)
# Append the 3HB to the end of the list.
door_list.append(door)
# Save the complete door list.
self.scenes[scene_id].doors = door_list
# Extract the loading zone data if door data exists (highest found loading zone ID is 0 or higher).
if self.scenes[scene_id].highest_ids["load"] > -1:
# Get the start address of the scene's loading zone data in the overlay.
loading_zones_start = self.read_bytes(SCENE_LOADING_ZONE_PTRS_START + (scene_id * 4), 4,
return_as_int=True) - SCENE_OVERLAY_RDRAM_START
# Loop over every loading zone data that is known to exist based on how high the highest ID is.
loading_zone_list = []
for loading_zone_id in range(self.scenes[scene_id].highest_ids["load"] + 1):
current_loading_zone_start = loading_zones_start + (loading_zone_id * LOADING_ZONE_ENTRY_LENGTH)
loading_zone_data = self.scenes[scene_id].read_ovl_bytes(current_loading_zone_start,
LOADING_ZONE_ENTRY_LENGTH)
loading_zone = CVLoDLoadingZoneEntry(
heal_player=bool.from_bytes(loading_zone_data[0x00:0x02], "big"),
scene_id=loading_zone_data[0x02],
spawn_id=loading_zone_data[0x03],
fade_settings_id=loading_zone_data[0x04],
cutscene_settings_id=int.from_bytes(loading_zone_data[0x06:0x08], "big"),
min_x_pos=struct.unpack('>h',loading_zone_data[0x08:0x0A])[0],
min_y_pos=struct.unpack('>h',loading_zone_data[0x0A:0x0C])[0],
min_z_pos=struct.unpack('>h',loading_zone_data[0x0C:0x0E])[0],
max_x_pos=struct.unpack('>h',loading_zone_data[0x0E:0x10])[0],
max_y_pos=struct.unpack('>h',loading_zone_data[0x10:0x12])[0],
max_z_pos=struct.unpack('>h',loading_zone_data[0x12:])[0],
start_addr=current_loading_zone_start + SCENE_OVERLAY_RDRAM_START,
)
# Check if the zone's destination scene has a scene ID with an entry in the scenes enum.
if loading_zone["scene_id"] in Scenes:
loading_zone["scene_id"] = Scenes(loading_zone["scene_id"])
# Append the 3HB to the end of the list.
loading_zone_list.append(loading_zone)
# Save the complete loading zone list.
self.scenes[scene_id].loading_zones = loading_zone_list
# Extract the scene's text pool if it has one (scene ID is The Outer Wall's or lower).
if scene_id <= Scenes.THE_OUTER_WALL:
current_string_start = self.read_bytes(SCENE_TEXT_PTRS_START + (scene_id * 4), 4,
return_as_int=True) - SCENE_OVERLAY_RDRAM_START
# Loop over every character in the scene text pool.
current_scene_text_char_start = current_string_start
raw_scene_text = bytearray(0)
while True:
scene_text_char = self.scenes[scene_id].read_ovl_bytes(current_scene_text_char_start, 2)
# Increment the current text character start for the next loop.
current_scene_text_char_start += 2
# If we found the character indicating the end of the entire pool, terminate the loop.
if scene_text_char == CVLOD_TEXT_POOL_END_CHARACTER:
break
# If we found the character indicating the end of a string in the text pool, convert what we
# extracted of the current string now.
if scene_text_char == CVLOD_STRING_END_CHARACTER:
converted_text = CVLoDSceneTextEntry(text=cvlod_bytes_to_string(raw_scene_text),
start_addr=current_string_start + \
SCENE_OVERLAY_RDRAM_START)
# Add the string's length to the total original size of the text pool + 1 for the end char.
self.scenes[scene_id].scene_text_orig_size += len(converted_text["text"]) + 1
# Save the text entry.
self.scenes[scene_id].scene_text.append(converted_text)
# Reset the raw scene text back to nothing and then continue to the next iteration.
raw_scene_text = bytearray(0)
current_string_start = current_scene_text_char_start
continue
raw_scene_text += scene_text_char
# Extract the scene's spawn entrance coordinates list. All of this is actually in the common segment,
# not an overlay, so to find this in the ROM we will need to take the pointer to it, subtract the start
# of the common segment in RDRAM, and add the start in ROM.
spawn_entrances_start = \
self.read_bytes(SCENE_SPAWN_COORDS_PTRS_START + (scene_id * 4), 4, return_as_int=True) \
- COMMON_SEGMENT_RDRAM_START + COMMON_SEGMENT_ROM_START
# If the current scene is the last scene (the Test Grid), consider the spawn entrances end to be where their
# pointers start.
if scene_id == Scenes.TEST_GRID:
spawn_entrances_end = SCENE_SPAWN_COORDS_PTRS_START
# If it's Dracula Ultimate's arena, consider there to only be one spawn entrance. The next two entrance
# datas after this are very likely to have been meant to be where the two nonexistent scenes were meant to
# have their spawn data begin at, based on the data patterns suggesting the Dracula Ultimate map's spawn
# data was copied for them as a placeholder.
elif scene_id == Scenes.CASTLE_KEEP_VOID:
spawn_entrances_end = \
self.read_bytes(SCENE_SPAWN_COORDS_PTRS_START + (Scenes.CASTLE_KEEP_VOID * 4) + SCENE_SPAWN_LENGTH,
4, return_as_int=True) - COMMON_SEGMENT_RDRAM_START + COMMON_SEGMENT_ROM_START
# Otherwise, the spawn entrances end for the current map is where the entrances for the next map begin.
else:
spawn_entrances_end = \
self.read_bytes(SCENE_SPAWN_COORDS_PTRS_START + ((scene_id + 1) * 4), 4, return_as_int=True) - \
COMMON_SEGMENT_RDRAM_START + COMMON_SEGMENT_ROM_START
# Loop over every spawn entrance data and extract it.
spawn_entrances_list = []
for spawn_id in range((spawn_entrances_end - spawn_entrances_start) // SCENE_SPAWN_LENGTH):
spawn_entrances_data = self.read_bytes(spawn_entrances_start + (spawn_id * SCENE_SPAWN_LENGTH),
SCENE_SPAWN_LENGTH)
spawn_entrance = CVLoDSpawnEntranceEntry(
room_id=int.from_bytes(spawn_entrances_data[0x00:0x02], "big"),
player_x_pos=struct.unpack('>h', spawn_entrances_data[0x02:0x04])[0],
player_y_pos=struct.unpack('>h', spawn_entrances_data[0x04:0x06])[0],
player_z_pos=struct.unpack('>h', spawn_entrances_data[0x06:0x08])[0],
player_rotation = struct.unpack('>h', spawn_entrances_data[0x08:0x0A])[0],
camera_x_pos=struct.unpack('>h', spawn_entrances_data[0x0A:0x0C])[0],
camera_y_pos=struct.unpack('>h', spawn_entrances_data[0x0C:0x0E])[0],
camera_z_pos=struct.unpack('>h', spawn_entrances_data[0x0E:0x10])[0],
focus_x_pos=struct.unpack('>h', spawn_entrances_data[0x10:0x12])[0],
focus_y_pos=struct.unpack('>h', spawn_entrances_data[0x12:0x14])[0],
focus_z_pos=struct.unpack('>h', spawn_entrances_data[0x14:])[0],
start_addr=(spawn_entrances_start + (spawn_id * SCENE_SPAWN_LENGTH)) \
- COMMON_SEGMENT_ROM_START + COMMON_SEGMENT_RDRAM_START
)
# Append the spawn entrance to the end of the list.
spawn_entrances_list.append(spawn_entrance)
# Save the complete spawn entrances list.
self.scenes[scene_id].spawn_spots = spawn_entrances_list
def read_byte(self, address: int, file_num: int = 0) -> int:
"""Return a byte at a specified address in a specified file."""
if file_num == 0:
return self.rom[address]
if file_num not in self.decompressed_files:
self.decompress_file(file_num)
return self.decompressed_files[file_num][address]
def read_bytes(self, start_address: int, length: int, return_as_int: bool = False,
file_num: int = 0) -> bytearray | int:
"""Return a string of bytes of a specified length beginning at a specified address in a specified file."""
if file_num == 0:
values = self.rom[start_address:start_address + length]
else:
if file_num not in self.decompressed_files:
self.decompress_file(file_num)
values = self.decompressed_files[file_num][start_address:start_address + length]
if return_as_int:
return int.from_bytes(values, "big")
return values
def write_byte(self, address: int, value: int, file_num: int = 0) -> None:
if file_num == 0:
self.rom[address] = value
else:
if file_num not in self.decompressed_files:
self.decompress_file(file_num)
self.decompressed_files[file_num][address] = value
def write_bytes(self, start_address: int, values: Collection[int], file_num: int = 0) -> None:
if file_num == 0:
self.rom[start_address:start_address + len(values)] = values
else:
if file_num not in self.decompressed_files:
self.decompress_file(file_num)
self.decompressed_files[file_num][start_address:start_address + len(values)] = values
def write_int16(self, address: int, value: int, file_num: int = 0) -> None:
value = value & 0xFFFF
self.write_bytes(address, [(value >> 8) & 0xFF, value & 0xFF], file_num)
def write_int16s(self, start_address: int, values: list[int], file_num: int = 0) -> None:
for i, value in enumerate(values):
self.write_int16(start_address + (i * 2), value, file_num)
def write_int24(self, address: int, value: int, file_num: int = 0) -> None:
value = value & 0xFFFFFF
self.write_bytes(address, [(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF], file_num)
def write_int24s(self, start_address: int, values: list[int], file_num: int = 0) -> None:
for i, value in enumerate(values):
self.write_int24(start_address + (i * 3), value, file_num)
def write_int32(self, address: int, value: int, file_num: int = 0) -> None:
value = value & 0xFFFFFFFF
self.write_bytes(address, [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF],
file_num)
def write_int32s(self, start_address: int, values: list[int], file_num: int = 0) -> None:
for i, value in enumerate(values):
self.write_int32(start_address + (i * 4), value, file_num)
def extract_normal_scene_actor_list(self, scene_id: int, start_addr: int) -> list[CVLoDNormalActorEntry]:
"""Extracts normal actor list data out of a given scene ID's overlay starting at a given address."""
# Loop over each actor list entry and read it out. Continue looping until we reach the end of the list.
actor_list = []
curr_addr = start_addr
while True:
actor_data = self.scenes[scene_id].read_ovl_bytes(curr_addr, NORMAL_ACTOR_ENTRY_LENGTH)
# The execution flags (the upper 5 bits) need to be split out of the object ID (the lower 11 bits).
object_id = int.from_bytes(actor_data[0x10:0x12], "big")
actor = CVLoDNormalActorEntry(
spawn_flags=int.from_bytes(actor_data[0x00:0x02], "big"),
status_flags=int.from_bytes(actor_data[0x02:0x04], "big"),
x_pos=struct.unpack('>f', actor_data[0x04:0x08])[0],
y_pos=struct.unpack('>f', actor_data[0x08:0x0C])[0],
z_pos=struct.unpack('>f', actor_data[0x0C:0x10])[0],
execution_flags=object_id >> 0xB,
object_id=object_id & 0x7FF,
flag_id=int.from_bytes(actor_data[0x12:0x14], "big"),
var_a=struct.unpack('>H', actor_data[0x14:0x16])[0],
var_b=struct.unpack('>H', actor_data[0x16:0x18])[0],
var_c=struct.unpack('>H', actor_data[0x18:0x1A])[0],
var_d=struct.unpack('>H', actor_data[0x1A:0x1C])[0],
extra_condition_ptr=int.from_bytes(actor_data[0x1C:], "big"),
start_addr=curr_addr + SCENE_OVERLAY_RDRAM_START,
)
# Check if the actor has a documented entry in the actors enum.
if actor["object_id"] in Objects:
actor["object_id"] = Objects(actor["object_id"])
# Check if the actor is an item pickup actor, and if it is, does it have has an entry in the pickups enum.
if actor["object_id"] == Objects.INTERACTABLE and actor["var_c"] in Pickups:
actor["var_c"] = Pickups(actor["var_c"])
# Loop over every pickup flag in Var B and see if we have any of them set and documented.
for flag_index in range(0x16):
flag_to_check = actor["var_b"] & (1 << flag_index)
if flag_to_check in PickupFlags:
actor["var_b"] |= PickupFlags(flag_to_check)
# Loop over every spawn flag and see if we have any of them set and documented.
for flag_index in range(0x16):
flag_to_check = actor["spawn_flags"] & (1 << flag_index)
if flag_to_check in ActorSpawnFlags:
actor["spawn_flags"] |= ActorSpawnFlags(flag_to_check)
# Loop over every execution flag and see if we have any of them set and documented.
for flag_index in range(0x5):
flag_to_check = actor["execution_flags"] & (1 << flag_index)
if flag_to_check in ObjectExecutionFlags:
actor["execution_flags"] |= ObjectExecutionFlags(flag_to_check)
# If the actor we extracted has an object ID of 0x7FF, then we have reached the actor entry signifying
# the end of the list. In which case, return the actor list as-is (don't append the end-of-list entry).
if actor["object_id"] == Objects.END_OF_ACTOR_LIST:
return actor_list
# If the actor is in the dict of actors with extended data, check the data ID in Var C against the highest
# one we've found so far for that actor on this scene. If it's higher, then consider it the new highest.
if actor["object_id"] in EXTENDED_DATA_ACTORS.keys():
extended_data_type = EXTENDED_DATA_ACTORS[actor["object_id"]]
if actor["var_c"] > self.scenes[scene_id].highest_ids[extended_data_type]:
self.scenes[scene_id].highest_ids[extended_data_type] = actor["var_c"]
# Append the actor to the end of the list.
actor_list.append(actor)
# Increment the current main actor sotart address so we will be at the start of the next one on the
# next loop.
curr_addr += NORMAL_ACTOR_ENTRY_LENGTH
def decompress_file(self, file_num: int) -> None:
self.decompressed_files[file_num] = bytearray(zlib.decompress(self.compressed_files[file_num][4:]))
def compress_file(self, file_num: int) -> None:
compressed_file = bytearray(zlib.compress(self.decompressed_files[file_num], level=zlib.Z_BEST_COMPRESSION))
# Pad the buffer to 0x02.
if len(compressed_file) % 2:
compressed_file.append(0x00)
# Add the length header at the beginning.
self.compressed_files[file_num] = bytearray((0x80000004 + len(compressed_file)).to_bytes(4, "big")) \
+ bytearray(compressed_file)
def get_decompressed_file_size(self, file_num: int) -> int:
"""Gets the current size of a Nisitenma-Ichigo file when decompressed. The file will be decompressed if it isn't already.
Otherwise, the size of the file (with all current modifications) will be returned as-is."""
if file_num not in self.decompressed_files:
self.decompress_file(file_num)
return len(self.decompressed_files[file_num])
def find_space_and_write_file(self, file: bytearray) -> int:
"""Finds a space in the ROM to write a given file and writes it, returning the start address the file was written to.
Files will be written one after the other every time this method is called, from where the map overlays normally are first,
and then starting from where the compressed Nistienma-Ichigo files are."""
# If the current write address is before the Nisitenma-Ichigo files start, and either trying to write the
# current file would have said file overflow past where the map overlay files normally end or the current write
# address is exactly the end of the scene overlays, start writing files in the NI file range instead.
if self.current_write_file_addr < self.ni_file_buffers_start and \
(self.current_write_file_addr + len(file) > self.scene_overlays_end or
self.current_write_file_addr == self.scene_overlays_end):
self.current_write_file_addr = self.ni_file_buffers_start
# Write the file and update the current write address to be where the file ends.
write_spot = self.current_write_file_addr
self.write_bytes(self.current_write_file_addr, file)
self.current_write_file_addr += len(file)
# Return the start address of the spot we just wrote the file to.
return write_spot
def recalculate_crc(self) -> None:
"""After all changes to the ROM have been made, the last step is to recalculate and replace the checksum (CRC) values in the header.
CVLoD's CIC chip is the very standard NTSC 6102/PAL 7101, so this implements the formula for that one."""
# Initialize all our CRC calculation values to the initial CIC 6102/7101 value.
# These will all persist across iterations on the CRC data.
crc_value_1a = CIC_6102_7101_INITIAL_VALUE # CRC1
crc_value_1b = CIC_6102_7101_INITIAL_VALUE
crc_value_1c = CIC_6102_7101_INITIAL_VALUE
crc_value_2a = CIC_6102_7101_INITIAL_VALUE # CRC2
crc_value_2b = CIC_6102_7101_INITIAL_VALUE
crc_value_2c = CIC_6102_7101_INITIAL_VALUE
# Iterate over every 4-byte word in the first megabyte of the ROM starting at offset 0x1000.
for crc_word_offset in range(CRC_ROM_START, CRC_ROM_START + CRC_DATA_LENGTH, 4):
crc_word = self.read_bytes(crc_word_offset, 4, return_as_int=True)
# Add the current CRC word with CRC Value 1A, keeping only the lowest 4 bytes.
# This will be used for CRC Values 1A and 1B.
value_1a_plus_word = (crc_value_1a + crc_word) & 0xFFFFFFFF
# For CRC Value 1B, if the result of the above is less than what 1A currently is, increment it by 1.
# Otherwise, don't touch it.
if value_1a_plus_word < crc_value_1a:
crc_value_1b += 1
# For CRC Value 1A, overwrite it with the result of the above addition operation.
crc_value_1a = value_1a_plus_word
# For CRC Value 1C, XOR the CRC word into it.
crc_value_1c ^= crc_word
# Calculate the "banana split" number (I can't think of a better name to call this, LOL!), which will be
# used for CRC Values 2A and 2B. The way we do this is by: left-shifting the base CRC word by its lowest
# 5 bits, right-shifting the base CRC word by 0x20 minus the same 5-bit value, and then OR-ing the two
# results together, keeping only the lowest 4 bytes.
banana_split = ((crc_word << (crc_word & 0x1F)) | (crc_word >> (0x20 - (crc_word & 0x1F)))) & 0xFFFFFFFF
# For CRC Value 2A, add the banana split into it, keeping only the lowest 4 bytes.
crc_value_2a = (crc_value_2a + banana_split) & 0xFFFFFFFF
# For CRC Value 2B, if it's less than the CRC word, XOR the CRC word XOR'd with Value 1A into it.
if crc_value_2b < crc_word:
crc_value_2b ^= crc_word ^ crc_value_1a
# Otherwise, XOR the banana split into it.
else:
crc_value_2b ^= banana_split
# For CRC Value 2C, add into it the CRC word XOR'd with Value 2A, keeping the lowest 4 bytes.
crc_value_2c = (crc_value_2c + (crc_word ^ crc_value_2a)) & 0xFFFFFFFF
# To get our final CRC1 value, XOR the final CRC Values 1A, 1B, and 1C together.
# Write the result at 0x10 in the ROM header.
self.write_int32(CRC1_START, crc_value_1a ^ crc_value_1b ^ crc_value_1c)
# To get our final CRC2 value, XOR the final CRC Values 2A, 2B, and 2C together.
# Write the result at 0x14 in the ROM header.
self.write_int32(CRC2_START, crc_value_2a ^ crc_value_2b ^ crc_value_2c)
def get_output_rom(self) -> bytes:
# Rebuild and reinsert all modified scene data.
for scene_id in range(len(self.scenes)):
# If we're looking at an empty scene with no overlay, skip it entirely.
if not self.scenes[scene_id].overlay:
continue
# # # ACTOR LISTS # # #
# Create the final actor lists with all entries primed for deletion removed.
new_actor_lists = {list_name: [actor_entry for actor_entry in actor_list
if "delete" not in actor_entry]
for list_name, actor_list in self.scenes[scene_id].actor_lists.items()}
# Loop over each actor list, convert each one to binary data, and insert them back in the map overlay.
for list_name, actor_list in new_actor_lists.items():
# If it's a pillar actor list, get the actor list data in pillar list format.
list_data = bytearray(0)
if list_name == "pillars":
for entry in actor_list:
list_data += struct.pack(">h", entry["x_pos"]) + \
struct.pack(">h", entry["y_pos"]) + \
struct.pack(">h", entry["z_pos"]) + \
int.to_bytes(entry["object_id"] | (entry["execution_flags"] << 0xB), 2, "big") + \
struct.pack(">H", entry["var_c"]) + \
struct.pack(">H", entry["var_a"]) + \
struct.pack(">H", entry["var_b"]) + \
struct.pack(">H", entry["var_d"]) + b'\x00\x00\x00\x00' + \
int.to_bytes(entry["flag_id"], 4, "big")
# Otherwise, get it in the normal actor list format.
else:
for entry in actor_list:
list_data += int.to_bytes(entry["spawn_flags"], 2, "big") +\
int.to_bytes(entry["status_flags"], 2, "big") +\
struct.pack(">f", entry["x_pos"]) + \
struct.pack(">f", entry["y_pos"]) + \
struct.pack(">f", entry["z_pos"]) + \
int.to_bytes(entry["object_id"] | (entry["execution_flags"] << 0xB), 2, "big") + \
int.to_bytes(entry["flag_id"], 2, "big") + \
struct.pack(">H", entry["var_a"]) + \
struct.pack(">H", entry["var_b"]) + \
struct.pack(">H", entry["var_c"]) + \
struct.pack(">H", entry["var_d"]) + \
int.to_bytes(entry["extra_condition_ptr"], 4, "big")
# Append the list end entry for all normal actor lists.
list_data += (b'\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\xFF\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
# Get the size of the original actor list by counting the number of entries in the list without entries
# deleted that have a defined start address. If the new actor data is the same size or smaller
# than it was before, write it back where it was originally (if we even have a list to begin with).
if len(actor_list) <= len([orig_entry for orig_entry in self.scenes[scene_id].actor_lists[list_name]
if "start_addr" in orig_entry]):
if actor_list:
self.scenes[scene_id].write_ovl_bytes(self.scenes[scene_id].actor_lists[
list_name][0]["start_addr"] \
- SCENE_OVERLAY_RDRAM_START, list_data)
# If it's larger, however, put it on the end of the overlay and update the pointer(s) to it.
else:
new_actor_list_addr = len(self.scenes[scene_id].overlay) + SCENE_OVERLAY_RDRAM_START
self.scenes[scene_id].overlay += list_data
# If it's an init list, the pointer to update is the third pointer in the scene's entry in the
# game's table of loaded scene actor list starts and determine how far in it is relative to the
# start of the overlay in RDRAM.
if list_name == "init":
self.write_int32(SCENE_ACTOR_PTRS_START + 8 + (scene_id * 0x10), new_actor_list_addr)
# If it's a proxy list, the pointer to update is the second pointer in the above-mentioned table.
elif list_name == "proxy":
self.write_int32(SCENE_ACTOR_PTRS_START + 4 + (scene_id * 0x10), new_actor_list_addr)
# If it's a room list, take the fourth pointer in the table to arrive at the scene's list of room
# actor list pointers, and then offset into it by the room number.
elif "room" in list_name:
room_list_ptrs_start = self.read_bytes(SCENE_ACTOR_PTRS_START + 12 + (scene_id * 0x10), 4,
return_as_int=True) - SCENE_OVERLAY_RDRAM_START
self.scenes[scene_id].write_ovl_int32(room_list_ptrs_start + (int(list_name[5:]) * 4),
new_actor_list_addr)
# Otherwise, if it's a 3HB pillar list, loop through every pillar data and update its actor pointer
# there.
else:
old_actor_list_addr = self.scenes[scene_id].actor_lists["pillars"][0]["start_addr"]
for pillar_data in self.scenes[scene_id].enemy_pillars:
pillar_data["actor_list_start"] = new_actor_list_addr + (pillar_data["actor_list_start"] -
old_actor_list_addr)
# # # ENEMY PILLARS LIST # # #
# Create the final enemy pillar list with all entries primed for deletion removed.
new_pillar_list = [pillar_entry for pillar_entry in self.scenes[scene_id].enemy_pillars
if "delete" not in pillar_entry]
# Loop over each enemy pillar, convert each one to binary data, and insert them back in the map overlay.
list_data = bytearray(0)
for pillar in new_pillar_list:
list_data += int.to_bytes(pillar["actor_list_start"], 4, "big") + \
int.to_bytes(pillar["actor_count"], 2, "big") + \
int.to_bytes(pillar["dissolve_flags"], 2, "big") + b'\x00\x00' + \
struct.pack(">h", pillar["rotation"]) + \
int.to_bytes(pillar["flag_id"], 4, "big")
# If the new pillar data is the same size or smaller than it was before, write it back where it was
# originally (if we even have a list to begin with).
if len(new_pillar_list) <= len([orig_entry for orig_entry in self.scenes[scene_id].enemy_pillars
if "start_addr" in orig_entry]):
if new_pillar_list:
self.scenes[scene_id].write_ovl_bytes(self.scenes[scene_id].enemy_pillars[0]["start_addr"] \
- SCENE_OVERLAY_RDRAM_START, list_data)
# If it's larger, however, put it on the end of the overlay and update the pointer to it
# (if it's not higher than Outer Wall's as that's where the pointer table ends).
elif scene_id <= Scenes.THE_OUTER_WALL:
new_pillar_list_addr = len(self.scenes[scene_id].overlay) + SCENE_OVERLAY_RDRAM_START
self.scenes[scene_id].overlay += list_data
self.write_int32(SCENE_ENEMY_PILLARS_PTRS_START + (scene_id * 4), new_pillar_list_addr)
# # # 1-HIT BREAKABLES LIST # # #
# Create the final 1-hit breakables list with all entries primed for deletion removed.
new_1hb_list = [one_hit_entry for one_hit_entry in self.scenes[scene_id].one_hit_breakables
if "delete" not in one_hit_entry]
# Loop over each 1HB, convert each one to binary data, and insert them back in the map overlay.
list_data = bytearray(0)
for one_hit in new_1hb_list:
list_data += int.to_bytes(one_hit["appearance_id"], 2, "big") + \
int.to_bytes(one_hit["pickup_id"], 2, "big") + \
int.to_bytes(one_hit["flag_id"], 4, "big") + \
int.to_bytes(one_hit["pickup_flags"], 2, "big") + b'\x00\x00'
# If the new 1HB data is the same size or smaller than it was before, write it back where it was originally
# (if we even have a list to begin with).
if len(new_1hb_list) <= len([orig_entry for orig_entry in self.scenes[scene_id].one_hit_breakables
if "start_addr" in orig_entry]):
if new_1hb_list:
self.scenes[scene_id].write_ovl_bytes(self.scenes[scene_id].one_hit_breakables[0]["start_addr"] \
- SCENE_OVERLAY_RDRAM_START, list_data)
# If it's larger, however, put it on the end of the overlay and update the pointer to it.
else:
new_1hb_list_addr = len(self.scenes[scene_id].overlay) + SCENE_OVERLAY_RDRAM_START
self.scenes[scene_id].overlay += list_data
self.write_int32(SCENE_1HB_PTRS_START + (scene_id * 4), new_1hb_list_addr)
# # # SPECIAL 1-HIT BREAKABLES LIST # # #
# Create the final special 1-hit breakables list with all entries primed for deletion removed.
new_special_1hb_list = [special_one_hit_entry for special_one_hit_entry in
self.scenes[scene_id].one_hit_special_breakables
if "delete" not in special_one_hit_entry]
# Loop over each special 1HB, convert each one to binary data, and insert them back in the map overlay.
list_data = bytearray(0)
for special_one_hit in new_special_1hb_list:
list_data += int.to_bytes(special_one_hit["appearance_id"], 2, "big") + \
int.to_bytes(special_one_hit["pickup_id"], 2, "big") + \
int.to_bytes(special_one_hit["flag_id"], 4, "big") + \
int.to_bytes(special_one_hit["pickup_flags"], 2, "big") + b'\x00\x00'
# If the new special 1HB data is the same size or smaller than it was before, write it back where it was
# originally (if we even have a list to begin with).
if len(new_special_1hb_list) <= len([orig_entry for orig_entry in
self.scenes[scene_id].one_hit_special_breakables
if "start_addr" in orig_entry]):
if new_special_1hb_list:
self.scenes[scene_id].write_ovl_bytes(
self.scenes[scene_id].one_hit_special_breakables[0]["start_addr"] \
- SCENE_OVERLAY_RDRAM_START, list_data)
# If it's larger, however, put it on the end of the overlay and update the pointer to it.
else:
new_special_1hb_list_addr = len(self.scenes[scene_id].overlay) + SCENE_OVERLAY_RDRAM_START
self.scenes[scene_id].overlay += list_data
# Split the pointer up into an upper and lower half and write them separately onto the hardcoded
# instructions for that scene.
upper_addr_half = new_special_1hb_list_addr >> 0x10
lower_addr_half = new_special_1hb_list_addr & 0xFFFF
# Increment the upper half if the lower is 0x8000 or higher.
if lower_addr_half >= 0x8000:
upper_addr_half += 1
self.scenes[scene_id].write_ovl_int16(SPECIAL_1HB_HARDCODED_PTRS_ADDRS[scene_id] + 2, upper_addr_half)
self.scenes[scene_id].write_ovl_int16(SPECIAL_1HB_HARDCODED_PTRS_ADDRS[scene_id] + 6, lower_addr_half)
# # # 3HB DROP IDS LIST # # #
# Convert the 3HB drops array back into binary data.
list_data = bytearray(0)
for three_hit_drop in self.scenes[scene_id].three_hit_drop_ids:
list_data += int.to_bytes(three_hit_drop, 2, "big")
# Pad the list data to be 4-aligned.
if len(list_data) % 4:
list_data += b'\x00\x00'
# If the new array is the same size or smaller than it was before, write it back where it was originally
# (if we even have an array to begin with).
if len(self.scenes[scene_id].three_hit_drop_ids) <= self.scenes[scene_id].three_hit_drops_orig_len:
if self.scenes[scene_id].three_hit_drop_ids:
self.scenes[scene_id].write_ovl_bytes(self.scenes[scene_id].three_hit_drops_start \
- SCENE_OVERLAY_RDRAM_START, list_data)
# If it's larger, however, put it on the end of the overlay and update the pointers to it.
else:
new_3hb_drop_list_addr = len(self.scenes[scene_id].overlay) + SCENE_OVERLAY_RDRAM_START
self.scenes[scene_id].overlay += list_data
for three_hit_data in self.scenes[scene_id].three_hit_breakables:
three_hit_data["pickup_array_start"] = new_3hb_drop_list_addr + \
(three_hit_data["pickup_array_start"] -
self.scenes[scene_id].three_hit_drops_start)
# # # 3-HIT BREAKABLES LIST # # #
# Create the final 3-hit breakables list with all entries primed for deletion removed.
new_3hb_list = [three_hit_entry for three_hit_entry in self.scenes[scene_id].three_hit_breakables
if "delete" not in three_hit_entry]
# Loop over each 3HB, convert each one to binary data, and insert them back in the map overlay.
list_data = bytearray(0)
for three_hit in new_3hb_list:
list_data += int.to_bytes(three_hit["appearance_id"], 2, "big") + \
int.to_bytes(three_hit["pickup_count"], 2, "big") + \
int.to_bytes(three_hit["pickup_array_start"], 4, "big") + \
int.to_bytes(three_hit["flag_id"], 4, "big")
# If the new 3HB data is the same size or smaller than it was before, write it back where it was
# originally (if we even have a list to begin with).
if len(new_3hb_list) <= len([orig_entry for orig_entry in self.scenes[scene_id].three_hit_breakables
if "start_addr" in orig_entry]):
if new_3hb_list:
self.scenes[scene_id].write_ovl_bytes(self.scenes[scene_id].three_hit_breakables[0]["start_addr"] \
- SCENE_OVERLAY_RDRAM_START, list_data)
# If it's larger, however, put it on the end of the overlay and update the pointer to it.
else:
new_3hb_list_addr = len(self.scenes[scene_id].overlay) + SCENE_OVERLAY_RDRAM_START
self.scenes[scene_id].overlay += list_data
self.write_int32(SCENE_3HB_PTRS_START + (scene_id * 4), new_3hb_list_addr)
# # # DOORS LIST # # #
# Create the final doors list with all entries primed for deletion removed.
new_door_list = [door_entry for door_entry in self.scenes[scene_id].doors
if "delete" not in door_entry]
# Loop over each door, convert each one to binary data, and insert them back in the map overlay.
door_data = bytearray(0)
for door in new_door_list:
door_data += (int.to_bytes(door["dlist_addr"], 4, "big") +
int.to_bytes(door["texture_id"], 1, "big") +
int.to_bytes(door["palette_id"], 1, "big") +
int.to_bytes(door["byte_6"], 1, "big") + b'\x00' +
int.to_bytes(door["door_flags"], 2, "big") + b'\x00\00' +
int.to_bytes(door["extra_condition_ptr"], 4, "big") +
int.to_bytes(door["flag_id"], 2, "big") + b'\x00\00' +
int.to_bytes(door["item_id"], 4, "big") +
int.to_bytes(door["front_room_id"], 1, "big") +
int.to_bytes(door["back_room_id"], 1, "big") +
int.to_bytes(door["flag_locked_text_id"], 1, "big") +
int.to_bytes(door["unlocked_text_id"], 1, "big") +
int.to_bytes(door["enemy_locked_text_id"], 1, "big") + b'\x00' +
int.to_bytes(door["opening_sound_id"], 2, "big") +
int.to_bytes(door["half_20"], 2, "big") +
int.to_bytes(door["closing_sound_id"], 2, "big") +
int.to_bytes(door["half_24"], 2, "big") +
int.to_bytes(door["shut_sound_id"], 2, "big") +
int.to_bytes(door["half_28"], 2, "big") +
int.to_bytes(door["half_2a"], 2, "big"))
# If the new door data is the same size or smaller than it was before, write it back where it was
# originally (if we even have a list to begin with).
if len(new_door_list) <= len([orig_entry for orig_entry in self.scenes[scene_id].doors
if "start_addr" in orig_entry]):
if new_door_list:
self.scenes[scene_id].write_ovl_bytes(self.scenes[scene_id].doors[0]["start_addr"] \
- SCENE_OVERLAY_RDRAM_START, door_data)
# If it's larger, however, put it on the end of the overlay and update the pointer to it.
else:
new_door_list_addr = len(self.scenes[scene_id].overlay) + SCENE_OVERLAY_RDRAM_START
self.scenes[scene_id].overlay += door_data
self.write_int32(SCENE_DOOR_PTRS_START + (scene_id * 4), new_door_list_addr)
# # # LOADING ZONES LIST # # #
# Create the final loading zones list with all entries primed for deletion removed.
new_loading_zone_list = [loading_zone_entry for loading_zone_entry in self.scenes[scene_id].loading_zones
if "delete" not in loading_zone_entry]
# Loop over each loading zone, convert each one to binary data, and insert them back in the map overlay.
loading_zone_data = bytearray(0)
for loading_zone in new_loading_zone_list:
loading_zone_data += (int.to_bytes(loading_zone["heal_player"], 2, "big") +
int.to_bytes(loading_zone["scene_id"], 1, "big") +
int.to_bytes(loading_zone["spawn_id"], 1, "big") +
int.to_bytes(loading_zone["fade_settings_id"], 1, "big") + b'\x00' +
int.to_bytes(loading_zone["cutscene_settings_id"], 2, "big") +
struct.pack(">h", loading_zone["min_x_pos"]) +
struct.pack(">h", loading_zone["min_y_pos"]) +
struct.pack(">h", loading_zone["min_z_pos"]) +
struct.pack(">h", loading_zone["max_x_pos"]) +
struct.pack(">h", loading_zone["max_y_pos"]) +
struct.pack(">h", loading_zone["max_z_pos"]))
# If the new loading zone data is the same size or smaller than it was before, write it back where it was
# originally (if we even have a list to begin with).
if len(new_loading_zone_list) <= len([orig_entry for orig_entry in self.scenes[scene_id].loading_zones
if "start_addr" in orig_entry]):
if new_loading_zone_list:
self.scenes[scene_id].write_ovl_bytes(self.scenes[scene_id].loading_zones[0]["start_addr"] \
- SCENE_OVERLAY_RDRAM_START, loading_zone_data)
# If it's larger, however, put it on the end of the overlay and update the pointer to it.
else:
new_loading_zone_list_ptr = len(self.scenes[scene_id].overlay) + SCENE_OVERLAY_RDRAM_START
self.scenes[scene_id].overlay += loading_zone_data
self.write_int32(SCENE_LOADING_ZONE_PTRS_START + (scene_id * 4), new_loading_zone_list_ptr)
# # # TEXT # # #
# Create the final text list with all entries primed for deletion removed.
new_text_list = [text_entry["text"] for text_entry in self.scenes[scene_id].scene_text
if "delete" not in text_entry]
# Calculate the text's new size while it's in Python string form and build the final list of texts to
# convert into a text pool that the game recognizes.
new_text_size = 0
for scene_text in new_text_list:
new_text_size += len(scene_text) + 1
# Convert the final text pool back into a bytearray to be inserted back in the scene overlay.
text_data = cvlod_strings_to_pool(new_text_list, wrap=False)
# If the new text data is the same size or smaller than it was before, write it back where it was originally
# (if we even have a list to begin with).
if new_text_size <= self.scenes[scene_id].scene_text_orig_size:
if new_text_list:
self.scenes[scene_id].write_ovl_bytes(self.scenes[scene_id].scene_text[0]["start_addr"] \
- SCENE_OVERLAY_RDRAM_START, text_data)
# If it's larger, however, put it on the end of the overlay and update the pointer to it
# (if it's not higher than Outer Wall's as that's where the pointer table ends).
elif scene_id <= Scenes.THE_OUTER_WALL:
new_text_pool_ptr = len(self.scenes[scene_id].overlay) + SCENE_OVERLAY_RDRAM_START
self.scenes[scene_id].overlay += text_data
self.write_int32(SCENE_TEXT_PTRS_START + (scene_id * 4), new_text_pool_ptr)
# # # SPAWN SPOTS # # #
# Create the final spawn spots list with all entries primed for deletion removed.
new_spawn_list = [spawn_entry for spawn_entry in self.scenes[scene_id].spawn_spots
if "delete" not in spawn_entry]
# Loop over each spawn spot, convert each one to binary data, and insert them back in the map overlay.
spawn_data = bytearray(0)
for spawn in new_spawn_list:
spawn_data += (int.to_bytes(spawn["room_id"], 2, "big") +
struct.pack(">h", spawn["player_x_pos"]) +
struct.pack(">h", spawn["player_y_pos"]) +
struct.pack(">h", spawn["player_z_pos"]) +
struct.pack(">h", spawn["player_rotation"]) +
struct.pack(">h", spawn["camera_x_pos"]) +
struct.pack(">h", spawn["camera_y_pos"]) +
struct.pack(">h", spawn["camera_z_pos"]) +
struct.pack(">h", spawn["focus_x_pos"]) +
struct.pack(">h", spawn["focus_y_pos"]) +
struct.pack(">h", spawn["focus_z_pos"]))
# Pad the list data to be 4-aligned.
if len(spawn_data) % 4:
spawn_data += b'\x00\x00'
# If the new spawn spot data is the same size or smaller than it was before, write it back where it was
# in the common segment originally (if we even have a list to begin with).
if len(new_spawn_list) <= len([orig_entry for orig_entry in self.scenes[scene_id].spawn_spots
if "start_addr" in orig_entry]):
if new_spawn_list:
self.write_bytes(self.scenes[scene_id].spawn_spots[0]["start_addr"] \
- COMMON_SEGMENT_RDRAM_START + COMMON_SEGMENT_ROM_START,
spawn_data)
# If it's larger, however, put it on the end of the overlay and update the pointer to it.
else:
new_spawn_list_ptr = len(self.scenes[scene_id].overlay) + SCENE_OVERLAY_RDRAM_START
self.scenes[scene_id].overlay += spawn_data
self.write_int32(SCENE_SPAWN_COORDS_PTRS_START + (scene_id * 4), new_spawn_list_ptr)
# Pad the overlay to 0x10 if it isn't.
while len(self.scenes[scene_id].overlay) % 0x10:
self.scenes[scene_id].overlay += b'\x00'
# Write the final scene overlay back into the ROM.
new_overlay_start = self.find_space_and_write_file(self.scenes[scene_id].overlay)
# Update the pointers to the start and end of the overlay in both the ROM and RDRAM scene overlay pointers
# table.
self.write_int32(SCENE_OVERLAY_ROM_ADDRS_START + (scene_id * 8), new_overlay_start)
self.write_int32((SCENE_OVERLAY_ROM_ADDRS_START + (scene_id * 8)) + 4,
new_overlay_start + len(self.scenes[scene_id].overlay))
self.write_int32(SCENE_OVERLAY_RDRAM_ADDRS_START + (scene_id * 8) + 4,
SCENE_OVERLAY_RDRAM_START + len(self.scenes[scene_id].overlay))
# Reinsert all Nisitenma-Ichigo files, both modified and unmodified, in the order they should be.
for i in range(1, self.number_of_ni_files):
# Re-compress the file if decompressed and update the game's decompressed sizes table.
if i in self.decompressed_files:
self.compress_file(i)
self.write_int24(self.decomp_file_sizes_table_start + (i * 8) + 5, len(self.decompressed_files[i]))
new_overlay_start = self.find_space_and_write_file(self.compressed_files[i])
# Update the Nisitenma-Ichigo table with the NI file's start and end addresses.
self.write_int24(self.ni_table_start + ((i - 1) * 8) + 1, new_overlay_start)
self.write_int24(self.ni_table_start + ((i - 1) * 8) + 5, new_overlay_start + len(self.compressed_files[i]))
# Recalculate the CRC numbers.
self.recalculate_crc()
# NOP out the CRC BNEs.
#self.write_int32(0x66C, 0x00000000)
#self.write_int32(0x678, 0x00000000)
# Return the final output ROM.
return bytes(self.rom)