Files
dockipelago/worlds/dk64/randomizer/Patching/Library/Assets.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

551 lines
20 KiB
Python

"""Library functions for pointer tables (Heavily WIP)."""
import js
import zlib
import gzip
from typing import Dict, List, Tuple, Union
from enum import IntEnum, auto
from randomizer.Patching.Patcher import ROM, LocalROM
from randomizer.Patching.Library.DataTypes import float_to_hex
class TableNames(IntEnum):
"""Pointer Table Enum."""
MusicMIDI = 0
MapGeometry = auto()
MapWalls = auto()
MapFloors = auto()
ModelTwoGeometry = auto()
ActorGeometry = auto()
Unknown6 = auto()
TexturesUncompressed = auto()
Cutscenes = auto()
Setups = auto()
InstanceScripts = auto()
Animations = auto()
Text = auto()
Unknown13 = auto()
TexturesHUD = auto()
Paths = auto()
Spawners = auto()
DKTVInputs = auto()
Triggers = auto()
Unknown19 = auto()
Unknown20 = auto()
Autowalks = auto()
Unknown22 = auto()
Exits = auto()
RaceCheckpoints = auto()
TexturesGeometry = auto()
UncompressedFileSizes = auto()
Unknown27 = auto()
Unknown28 = auto()
Unknown29 = auto()
Unknown30 = auto()
Unknown31 = auto()
class TableData:
"""Class to store information regarding a pointer table."""
def __init__(
self,
vanilla_compressed: bool,
rando_compressed: bool,
decode_function=None,
encode_function=None,
):
"""Initialize with given variables."""
self.vanilla_compressed = vanilla_compressed
self.rando_compressed = rando_compressed
self.decode_function = decode_function
self.encode_function = encode_function
def decoder_exits(data: bytes) -> dict:
"""Decode the exit data."""
exit_count = int(len(data) / 10)
ret = {"exits": []}
for exit_index in range(exit_count):
exit_data = {}
ret["exits"].append(exit_data)
return ret
table_functions = {
TableNames.MusicMIDI: TableData(False, False, None, None),
TableNames.MapGeometry: TableData(False, False, None, None),
TableNames.MapWalls: TableData(False, False, None, None),
TableNames.MapFloors: TableData(False, False, None, None),
TableNames.ModelTwoGeometry: TableData(False, False, None, None),
TableNames.ActorGeometry: TableData(False, False, None, None),
TableNames.Unknown6: TableData(False, False, None, None),
TableNames.TexturesUncompressed: TableData(False, False, None, None),
TableNames.Cutscenes: TableData(False, False, None, None),
TableNames.Setups: TableData(False, False, None, None),
TableNames.InstanceScripts: TableData(False, False, None, None),
TableNames.Animations: TableData(False, False, None, None),
TableNames.Text: TableData(False, False, None, None),
TableNames.Unknown13: TableData(False, False, None, None),
TableNames.TexturesHUD: TableData(False, False, None, None),
TableNames.Paths: TableData(False, False, None, None),
TableNames.Spawners: TableData(False, False, None, None),
TableNames.DKTVInputs: TableData(False, False, None, None),
TableNames.Triggers: TableData(False, False, None, None),
TableNames.Unknown19: TableData(False, False, None, None),
TableNames.Unknown20: TableData(False, False, None, None),
TableNames.Autowalks: TableData(False, False, None, None),
TableNames.Unknown22: TableData(False, False, None, None),
TableNames.Exits: TableData(False, False, None, None),
TableNames.RaceCheckpoints: TableData(False, False, None, None),
TableNames.TexturesGeometry: TableData(False, False, None, None),
TableNames.UncompressedFileSizes: TableData(False, False, None, None),
TableNames.Unknown27: TableData(False, False, None, None),
TableNames.Unknown28: TableData(False, False, None, None),
TableNames.Unknown29: TableData(False, False, None, None),
TableNames.Unknown30: TableData(False, False, None, None),
TableNames.Unknown31: TableData(False, False, None, None),
}
class PointerTableFile:
"""Class to store information about a pointer table file."""
def __init__(self, table: TableNames, start: int, end: int, is_compressed: bool = None):
"""Initialize with given parameters."""
self.start = start
self.end = end
self.size = end - start
self.is_compressed = is_compressed
if is_compressed is None:
self.is_compressed = table_functions[table].rando_compressed
def getPointerLocation(table: TableNames, file_index: int) -> int:
"""Get the address of a pointer table file."""
return js.pointer_addresses[table]["entries"][file_index]["pointing_to"]
def getPointerFile(table: TableNames, file_index: int, is_compressed: bool = None) -> PointerTableFile:
"""Get pointer table file information."""
start = getPointerLocation(table, file_index)
if "compressed_size" in js.pointer_addresses[table]["entries"][file_index]:
file_size = js.pointer_addresses[table]["entries"][file_index]["compressed_size"]
else:
file_end = getPointerLocation(table, file_index + 1)
file_size = file_end - start
end = start + file_size
return PointerTableFile(table, start, end, is_compressed)
def getPointerData(ROM_COPY: LocalROM | ROM, table: TableNames, file_index: int) -> bytes:
"""Get the data inside a pointer table file."""
ref_data = getPointerFile(table, file_index)
ROM_COPY.seek(ref_data.start)
data = ROM_COPY.readBytes(ref_data.size)
if ref_data.is_compressed:
return zlib.decompress(data, (15 + 32))
return data
def writePointerFile(ROM_COPY: LocalROM | ROM, table: TableNames, file_index: int, data: bytes, is_compressed: bool = False):
"""Write data to a pointer file."""
ref_file = getPointerFile(table, file_index)
if is_compressed:
data = gzip.compress(data, compresslevel=9)
if len(data) > ref_file.size:
raise Exception(f"Attempted to write data to a file slot which isn't big enough.\n- Table: {table}\n- File {file_index}\n- Attempt size {hex(len(data))}\n- Capacity: {hex(ref_file.size)}")
ROM_COPY.seek(ref_file.start)
ROM_COPY.writeBytes(data)
def decodeFile(ROM_COPY: LocalROM | ROM, table: TableNames, file_index: int) -> dict:
"""Decode a pointer table file."""
function_data = table_functions.get(table, (None, None))
file_data = getPointerData(ROM_COPY, table, file_index)
if function_data.decode_function is None:
return {
"data": file_data,
}
return function_data.decode_function(file_data)
def encodeFile(ROM_COPY: LocalROM | ROM, table: TableNames, file_index: int, data: dict):
"""Encode a pointer table file."""
function_data = table_functions.get(table, (None, None))
ref_data = getPointerFile(table, file_index)
if function_data.encode_function is None:
writePointerFile(ROM_COPY, table, file_index, data["data"], ref_data.is_compressed)
return
output_data = function_data.encode_function(data)
writePointerFile(ROM_COPY, table, file_index, output_data, ref_data.is_compressed)
def getRawFile(ROM_COPY: Union[ROM, LocalROM], table_index: TableNames, file_index: int, compressed: bool):
"""Get raw file from ROM."""
file_start = getPointerLocation(table_index, file_index)
if "compressed_size" in js.pointer_addresses[table_index]["entries"][file_index]:
file_size = js.pointer_addresses[table_index]["entries"][file_index]["compressed_size"]
if file_size is None:
return bytes(bytearray([]))
else:
file_end = getPointerLocation(table_index, file_index + 1)
file_size = file_end - file_start
ROM_COPY.seek(file_start)
data = ROM_COPY.readBytes(file_size)
if compressed:
data = zlib.decompress(data, (15 + 32))
return data
def writeRawFile(table_index: TableNames, file_index: int, compressed: bool, data: bytearray, ROM_COPY):
"""Write raw file from ROM."""
file_start = getPointerLocation(table_index, file_index)
if "compressed_size" in js.pointer_addresses[table_index]["entries"][file_index]:
file_size = js.pointer_addresses[table_index]["entries"][file_index]["compressed_size"]
else:
file_end = getPointerLocation(table_index, file_index + 1)
file_size = file_end - file_start
write_data = bytes(data)
if compressed:
write_data = gzip.compress(bytes(data), compresslevel=9)
if len(write_data) > file_size:
raise Exception(f"Cannot write file {file_index} in table {table_index} to ROM as it's too big.")
ROM_COPY.seek(file_start)
ROM_COPY.writeBytes(write_data)
icon_db = {
0x0: "waterfall_tall",
0x1: "waterfall_short",
0x2: "water",
0x3: "lava",
0x4: "sparkles",
0x5: "pop_explosion",
0x6: "lava_explosion",
0x7: "green_leaf?",
0x8: "brown_smoke_explosion",
0x9: "small_explosion",
0xA: "solar_flare?",
0xB: "splash",
0xC: "bubble",
0xD: "purple_sparkle",
0xE: "yellow_sparkle",
0xF: "green_sparkle",
0x10: "purple_sparkle",
0x11: "yellow_sparkle",
0x12: "green_sparkle",
0x13: "large_smoke_explosion",
0x14: "pink_implosion",
0x15: "brown_horizontal_spinning_plank",
0x16: "birch_horizontal_spinning_plank",
0x17: "brown_vertical_spinning_plank",
0x18: "star_water_ripple",
0x19: "circle_water_ripple",
0x1A: "small_smoke_explosion",
0x1B: "static_star",
0x1C: "static_z",
0x1D: "white_flare?",
0x1E: "static_rain?",
0x1F: "medium_smoke_explosion",
0x20: "bouncing_melon",
0x21: "vertical_rolling_melon",
0x22: "red_flare?",
0x23: "sparks",
0x24: "peanut",
0x25: "star_flare?",
0x26: "peanut_shell",
0x27: "small_explosion",
0x28: "large_smoke_implosion",
0x29: "blue_lazer",
0x2A: "pineapple",
0x2B: "fireball",
0x2C: "orange",
0x2D: "grape",
0x2E: "grape_splatter",
0x2F: "tnt_sparkle",
0x30: "fire_explosion",
0x31: "small_fireball",
0x32: "diddy_coin",
0x33: "chunky_coin",
0x34: "lanky_coin",
0x35: "dk_coin",
0x36: "tiny_coin",
0x37: "dk_coloured_banana",
0x38: "film",
0x39: "bouncing_orange",
0x3A: "crystal_coconut",
0x3B: "gb",
0x3C: "banana_medal",
0x3D: "diddy_coloured_banana",
0x3E: "chunky_coloured_banana",
0x3F: "lanky_coloured_banana",
0x40: "dk_coloured_banana",
0x41: "tiny_coloured_banana",
0x42: "exploded_krash_barrel_enemy",
0x43: "white_explosion_thing",
0x44: "coconut",
0x45: "coconut_shell",
0x46: "spinning_watermelon_slice",
0x47: "tooth",
0x48: "ammo_crate",
0x49: "race_coin",
0x4A: "lanky_bp",
0x4B: "cannonball",
0x4C: "crystal_coconut",
0x4D: "feather",
0x4E: "guitar_gazump",
0x4F: "bongo_blast",
0x50: "saxophone",
0x51: "triangle",
0x52: "trombone",
0x53: "waving_yellow_double_eighth_note",
0x54: "waving_yellow_single_eighth_note",
0x55: "waving_green_single_eighth_note",
0x56: "waving_purple_double_eighth_note",
0x57: "waving_red_double_eighth_note",
0x58: "waving_red_single_eighth_note",
0x59: "waving_white_double_eighth_note",
0x5A: "diddy_bp",
0x5B: "chunky_bp",
0x5C: "dk_bp",
0x5D: "tiny_bp",
0x5E: "spinning_sparkle",
0x5F: "static_rain?",
0x60: "translucent_water",
0x61: "unk61",
0x62: "black_screen",
0x63: "white_cloud",
0x64: "thin_lazer",
0x65: "blue_bubble",
0x66: "white_faded_circle",
0x67: "white_circle",
0x68: "grape_particle?",
0x69: "spinning_blue_sparkle",
0x6A: "white_smoke_explosion",
0x6B: "l-r_joystick",
0x6C: "fire_wall",
0x6D: "static_rain_bubble",
0x6E: "a_button",
0x6F: "b_button",
0x70: "z_button",
0x71: "c_down_button",
0x72: "c_up_button",
0x73: "c_left_button",
0x74: "acid",
0x75: "acid_explosion",
0x76: "race_hoop",
0x77: "acid_goop?",
0x78: "unk78",
0x79: "broken_bridge?",
0x7A: "white_pole?",
0x7B: "bridge_chip?",
0x7C: "wooden_beam_with_rivets",
0x7D: "chunky_bunch",
0x7E: "diddy_bunch",
0x7F: "lanky_bunch",
0x80: "dk_bunch",
0x81: "tiny_bunch",
0x82: "chunky_balloon",
0x83: "diddy_balloon",
0x84: "dk_balloon",
0x85: "lanky_balloon",
0x86: "tiny_balloon",
0x87: "r_button",
0x88: "l_button",
0x89: "fairy",
0x8A: "boss_key",
0x8B: "crown",
0x8C: "rareware_coin",
0x8D: "nintendo_coin",
0x8E: "no_symbol",
0x8F: "headphones",
0x90: "opaque_blue_water",
0x91: "start_button",
0x92: "white_question_mark",
0x93: "candy_face",
0x94: "cranky_face",
0x95: "snide_face",
0x96: "funky_face",
0x97: "left_arrow",
0x98: "white_spark?",
0x99: "black_boulder_chunk",
0x9A: "green_boulder_chunk",
0x9B: "wood_chip",
0x9C: "snowflake/dandelion",
0x9D: "static_water?",
0x9E: "spinning_leaf",
0x9F: "flashing_water?",
0xA0: "rainbow_coin",
0xA1: "shockwave_orange_particle",
0xA2: "implosion?",
0xA3: "rareware_employee_face",
0xA4: "smoke",
0xA5: "static_smoke?",
0xA6: "barrel_bottom_chunk",
0xA7: "scoff_face",
0xA8: "multicoloured_bunch",
0xA9: "dk_face",
0xAA: "diddy_face",
0xAB: "lanky_face",
0xAC: "tiny_face",
0xAD: "chunky_face",
0xAE: "fairy_tick",
0xAF: "wrinkly",
}
def grabText(ROM_COPY: Union[ROM, LocalROM], file_index: int) -> List[List[Dict[str, List[str]]]]:
"""Pull text from ROM with a particular file index."""
file_start = getPointerLocation(TableNames.Text, file_index)
ROM_COPY.seek(file_start + 0)
count = int.from_bytes(ROM_COPY.readBytes(1), "big")
text = []
text_data = []
text_start = (count * 0xF) + 3
data_start = 1
for i in range(count):
ROM_COPY.seek(file_start + data_start)
section_1_count = int.from_bytes(ROM_COPY.readBytes(1), "big")
section_2_count = int.from_bytes(ROM_COPY.readBytes(1), "big")
section_3_count = int.from_bytes(ROM_COPY.readBytes(1), "big")
ROM_COPY.seek(file_start + data_start + 5)
start = int.from_bytes(ROM_COPY.readBytes(2), "big")
int.from_bytes(ROM_COPY.readBytes(2), "big")
block_start = 1
blocks = []
for k in range(section_1_count):
ROM_COPY.seek(file_start + data_start + block_start)
sec2ct = int.from_bytes(ROM_COPY.readBytes(1), "big")
offset = 0
if (sec2ct & 4) != 0:
offset += 4
text_blocks = []
if (sec2ct & 1) == 0:
if (sec2ct & 2) != 0:
ROM_COPY.seek(file_start + data_start + block_start + offset + 1)
sec3ct = int.from_bytes(ROM_COPY.readBytes(1), "big")
for j in range(sec3ct):
_block = block_start + 2 + offset + (4 * j) - 1
ROM_COPY.seek(file_start + data_start + _block)
_pos = int.from_bytes(ROM_COPY.readBytes(2), "big")
ROM_COPY.seek(file_start + data_start + _block)
_dat = int.from_bytes(ROM_COPY.readBytes(4), "big")
text_blocks.append(
{
"type": "sprite",
"position": _pos,
"data": hex(_dat),
"sprite": icon_db[(_dat >> 8) & 0xFF],
}
)
added = block_start + 2 + offset + (4 * sec3ct) + 4
else:
ROM_COPY.seek(file_start + data_start + block_start + offset + 1)
sec3ct = int.from_bytes(ROM_COPY.readBytes(1), "big")
for j in range(sec3ct):
_block = block_start + 2 + offset + (8 * j) - 1
ROM_COPY.seek(file_start + data_start + _block + 3)
_start = int.from_bytes(ROM_COPY.readBytes(2), "big")
ROM_COPY.seek(file_start + data_start + _block + 5)
_size = int.from_bytes(ROM_COPY.readBytes(2), "big")
text_blocks.append({"type": "normal", "start": _start, "size": _size})
added = block_start + 2 + offset + (8 * sec3ct) + 4
# print(f"File {file_index}, Textbox {i}, section {k}")
blocks.append(
{
"block_start": hex(block_start + data_start),
"section2count": sec2ct,
"section3count": sec3ct,
"offset": offset,
"text": text_blocks,
}
)
block_start = added
ROM_COPY.seek(file_start + data_start)
if added < data_start:
info = b""
else:
info = ROM_COPY.readBytes(added - data_start)
text_data.append(
{
"arr": info,
"text": blocks,
"section1count": section_1_count,
"section2count": section_2_count,
"section3count": section_3_count,
"data_start": hex(data_start),
}
)
text_start += added - data_start
data_start += block_start
for item in text_data:
text_block = []
# print(item)
for item2 in item["text"]:
# print(item2)
temp = []
for item3 in item2["text"]:
if item3["type"] == "normal":
start = item3["start"] + data_start + 2
# print(hex(start))
start + item3["size"]
ROM_COPY.seek(file_start + start)
temp.append(ROM_COPY.readBytes(item3["size"]).decode())
elif item3["type"] == "sprite":
temp.append(item3["sprite"])
# print(fh.read(item3["size"]))
text_block.append(temp)
text.append(text_block)
formatted_text = []
for t in text:
y = []
for x in t:
y.append({"text": x})
formatted_text.append(y)
return formatted_text
def writeText(ROM_COPY: Union[ROM, LocalROM], file_index: int, text: List[Union[List[Dict[str, List[str]]], Tuple[Dict[str, List[str]]]]]) -> None:
"""Write the text to ROM."""
text_start = getPointerLocation(TableNames.Text, file_index)
ROM_COPY.seek(text_start)
ROM_COPY.writeBytes(bytearray([len(text)]))
position = 0
for textbox in text:
ROM_COPY.writeBytes(len(textbox).to_bytes(1, "big"))
for block in textbox:
# Get Icon State
icon_id = -1
for string in block["text"]:
if string in icon_db.values():
for icon in icon_db:
if icon_db[icon] == string:
icon_id = icon
if icon_id > -1:
ROM_COPY.writeBytes(bytearray([2, 1]))
ROM_COPY.writeBytes(icon_id.to_bytes(2, "big"))
ROM_COPY.writeBytes(bytearray([0, 0]))
else:
ROM_COPY.writeBytes(bytearray([1, len(block["text"])]))
for string in block["text"]:
ROM_COPY.writeBytes(position.to_bytes(4, "big"))
ROM_COPY.writeBytes(len(string).to_bytes(2, "big"))
ROM_COPY.writeBytes(bytearray([0, 0]))
position += len(string)
unk0 = 0
if "unk0" in block:
unk0 = block["unk0"]
ROM_COPY.writeBytes(int(float_to_hex(unk0), 16).to_bytes(4, "big"))
ROM_COPY.writeBytes(bytearray(position.to_bytes(2, "big")))
for textbox in text:
for block in textbox:
is_icon = False
for string in block["text"]:
if string in icon_db.values():
is_icon = True
if not is_icon:
for string in block["text"]:
ROM_COPY.writeBytes(string.encode("ascii"))