diff --git a/.gitmodules b/.gitmodules index 0545be144b..dd8019b41b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -21,3 +21,6 @@ [submodule "tools/mipsmatch"] path = tools/mipsmatch url = https://github.com/ttkb-oss/mipsmatch +[submodule "tools/sotn_utils"] + path = tools/sotn_utils + url = https://github.com/ProjectOblivion/sotn_utils.git diff --git a/Makefile b/Makefile index d62e0dacda..619648f24a 100644 --- a/Makefile +++ b/Makefile @@ -230,6 +230,7 @@ format-tools: $(BLACK) tools/sotn_permuter/permuter_loader.py $(BLACK) diff_settings.py $(BLACK) tools/function_finder/*.py + $(BLACK) tools/extract_overlay/*.py .PHONY: format-symbols format-symbols-% format-symbols-us-sort: diff --git a/tools/.gitignore b/tools/.gitignore index ccd53a4311..2ed86e7e3a 100644 --- a/tools/.gitignore +++ b/tools/.gitignore @@ -6,6 +6,7 @@ out permute.me/ target/ !sotn-debugmodule/sotn-debugmodule.ld +extract_overlay_log.json # Dependency installation checkpoints *.make.chkpt diff --git a/tools/extract_overlay/e_init.c.mako b/tools/extract_overlay/e_init.c.mako new file mode 100644 index 0000000000..06c132901a --- /dev/null +++ b/tools/extract_overlay/e_init.c.mako @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +#include "${ovl_name}.h" + +% for function in entity_funcs: +void ${function}(Entity* self); +% endfor + +PfnEntityUpdate OVL_EXPORT(EntityUpdates)[] = { +% for function in entity_funcs: + ${function}, +% endfor +}; + +// clang-format off +// animSet, animCurFrame, unk5A, palette, enemyID +% for name, animSet, animCurFrame, unk5A, palette, enemyID in e_inits: +EInit ${name} = {${animSet}, ${animCurFrame}, ${unk5A}, ${palette}, ${enemyID}}; +% endfor +// clang-format on diff --git a/tools/extract_overlay/enemy_defs.yaml b/tools/extract_overlay/enemy_defs.yaml new file mode 100644 index 0000000000..4b0d7edc2f --- /dev/null +++ b/tools/extract_overlay/enemy_defs.yaml @@ -0,0 +1,172 @@ +# offset values taken from g_EnemyDefs which associate entities with EInits +BlueAxeKnight: 0x006 +SwordLord: 0x009 +Skelerang: 0x00B +BloodyZombie: 0x00D +FlyingZombieHalf1: 0x00E +FlyingZombieHalf2: 0x00F +Diplocephalus: 0x010 +OwlKnight: 0x014 +Owl: 0x016 +LesserDemon: 0x017 +MermanLvl2: 0x01B +MermanLvl3: 0x01D +Gorgon: 0x01F +ArmorLord: 0x022 +BlackPanther: 0x025 +DarkOctopus: 0x026 +FleaMan: 0x028 +FleaArmor: 0x029 +WhiteDragon: 0x02B +BoneArk: 0x02D +BoneArkSkeleton: 0x02E +BoneArkProjectile: 0x2F +FleaRider: 0x030 +Marionette: 0x031 +OlroxLvl25: 0x032 +OlroxLvl0: 0x037 +Wereskeleton: 0x03D +Bat: 0x040 +LargeSlime: 0x041 +Slime: 0x042 +PhantomSkull: 0x043 +FlailGuard: 0x044 +BloodSkeleton: 0x046 +HellfireBeast: 0x047 +Skeleton: 0x04B +DiscusLordLvl22: 0x04D +DiscusLordLvl0: 0x04E +FireDemon: 0x04F +SpittleBone: 0x051 +SkeletonApe: 0x053 +StoneRose: 0x055 +Ectoplasm: 0x058 +BonePillarLvl1: 0x05A +SpearGuard: 0x05D +PlateLord: 0x061 +FrozenShade: 0x063 +BoneMusket: 0x066 +DodoBird: 0x068 +BoneScimitar: 0x069 +Toad: 0x06A +Frog: 0x06B +BoneArcher: 0x06C +Zombie: 0x06E +GraveKeeper: 0x06F +Tombstone: 0x071 +BlueRaven: 0x072 +BlackCrow: 0x073 +JackOBones: 0x074 +BoneHalberd: 0x076 +Yorick: 0x078 +Skull: 0x079 +BladeMaster: 0x07A +BladeSoldier: 0x07C +NovaSkeleton: 0x07E +WingedGuard: 0x080 +SpectralSwordNO2: 0x081 +Poltergeist: 0x082 +Lossoth: 0x083 +ValhallaKnight: 0x085 +SpectralSwordDAI: 0x088 +SpectralSwordPuppetSword: 0x089 +SpectralSwordRDAI: 0x08A +Spear: 0x08B +Shield: 0x08C +Orobourous: 0x08D +Oruburos: 0x08E +OruburosRider: 0x08F +DragonRider1: 0x090 +DragonRider2: 0x091 +Dhuron: 0x092 +FireWarg: 0x094 +WargRider: 0x097 +CaveTroll: 0x099 +Ghost: 0x09C +Thornweed: 0x09D +CorpseweedUnused: 0x09E +Corpseweed: 0x09F +VenusWeedRoot: 0x0A1 +VenusWeedFlower: 0x0A2 +BombKnight: 0x0A5 +RockKnight: 0x0A7 +DraculaLvl0: 0x0A9 +GreaterDemon: 0x0AC +Warg: 0x0AF +Slinger: 0x0B2 +CornerGuard: 0x0B4 +Bitterfly: 0x0B6 +BonePillarSkull: 0x0B7 +BonePillarFireBreath: 0xB8 +BonePillarSpikedBall: 0x0B9 +Hammer: 0x0BA +Gurkha: 0x0BC +Blade: 0x0BE +OuijaTable: 0x0C1 +SniperofGoth: 0x0C3 +GalamothLvl50: 0x0C6 +GalamothLvl0: 0x0C7 +Minotaurus: 0x0CB +WerewolfARE: 0x0CE +Paranthropus: 0x0D3 +Mudman: 0x0D6 +GhostDancer: 0x0D8 +FrozenHalf: 0x0D9 +SalemWitch: 0x0DD +Azaghal: 0x0E0 +Gremlin: 0x0E1 +HuntingGirl: 0x0E3 +VandalSword: 0x0E4 +Salome: 0x0E5 +Ctulhu: 0x0E9 +Malachi: 0x0EC +Harpy: 0x0EF +Slogra: 0x0F3 +GreenAxeKnight: 0x0F6 +Spellbook: 0x0F7 +MagicTomeLvl8: 0x0F9 +MagicTomeLvl12: 0x0FB +Doppleganger10: 0x0FD +Gaibon: 0x0FE +SkullLord: 0x105 +Lion: 0x106 +Tinman: 0x108 +AkmodanII: 0x10B +Cloakedknight: 0x10F +DarkwingBat: 0x111 +Fishhead: 0x115 +Karasuman: 0x118 +Imp: 0x11C +Balloonpod: 0x11D +Scylla: 0x11F +Scyllawyrm: 0x126 +Granfaloon1: 0x127 +Granfaloon2: 0x128 +Hippogryph: 0x12C +MedusaHead1: 0x12F +MedusaHead2: 0x130 +Archer: 0x131 +RichterBelmont: 0x133 +Scarecrow: 0x142 +Schmoo: 0x143 +Beezelbub: 0x144 +FakeTrevor: 0x148 +FakeGrant: 0x14E +FakeSypha: 0x151 +Succubus: 0x156 +KillerFish: 0x15E +Shaft: 0x15F +Death1: 0x164 +Death2: 0x169 +Cerberos: 0x16B +Medusa: 0x16E +TheCreature: 0x172 +Doppleganger40: 0x174 +DraculaLvl98: 0x17B +StoneSkull: 0x180 +Minotaur: 0x182 +WerewolfRARE: 0x185 +BlueVenusWeed1: 0x188 +BlueVenusWeed2: 0x189 +Guardian: 0x18C + \ No newline at end of file diff --git a/tools/extract_overlay/extract_overlay.py b/tools/extract_overlay/extract_overlay.py new file mode 100755 index 0000000000..fca15721f6 --- /dev/null +++ b/tools/extract_overlay/extract_overlay.py @@ -0,0 +1,1694 @@ +#!/usr/bin/env python3 + +import os +import sys +import shutil +import argparse +import hashlib +import time +import re +import multiprocessing +from pathlib import Path +from subprocess import run +from types import SimpleNamespace +from collections import Counter +from mako.template import Template + +sys.path.insert(0, str(Path(__file__).parent.parent)) +import sotn_utils +from symbols import sort, extract_dynamic_symbols, Symbol + +""" +Handles many tasks for adding an overlay: +- Extracts the data necessary to generate an initial config +- Parses known data tables (psx header, EntityUpdates, psp export table) +- Compares newly extracted functions to existing asm files to identify functions +- Adds and renames identified symbols +- Cross references asm with existing asm and matches symbols from identical instruction sequences +- Creates the ovl.h file, header.c, and e_init.c files +- Imports the data for header.c and e_init.c for us version +- Cross-references header.c and e_init.c with existing file for non-us versions +- Splits source files into segments based on function boundaries +- Extracts and assigns .rodata segments to the correct files +- Parses and logs suggestions for segment splits from splat output +- Validates builds by comparing SHA1 hashes and updates check files + +Example usage: tools/extract_overlay.py lib -v us,pspeu + +Additional notes: +- If a segment has only one function, it is named as that function in snake case. If the function name starts with Entity, it replaces it with 'e'. + For example: A segment with the only function being EntityShuttingWindow would be named as e_shutting_window +""" + + +def build(targets=[], plan=True, dynamic_syms=False, build=True, version="us"): + logger = sotn_utils.get_logger() + cmds = {} + env = os.environ.copy() + # Ensure the correct VERSION is passed + if version: + env["VERSION"] = version + if dynamic_syms: + env.update({"FORCE_SYMBOLS": ""}) + if plan: + ret_key = None + Path(f"build/{version}/").mkdir(parents=True, exist_ok=True) + cmds[f"python3 tools/builds/gen.py build/{version}/build.ninja"] = "" + if build: + ret_key = ( + f"ninja -f build/{version}/build.ninja {' '.join(f"{x}" for x in targets)}" + ) + cmds[ret_key] = "" + + for cmd in cmds: + cmds[cmd] = run(cmd.split(), env=env, capture_output=True, text=True) + if cmds[cmd].returncode != 0: + logger.error( + f"Command '{cmd}' returned non-zero exit status {cmds[cmd].returncode}" + ) + if cmds[cmd].stdout: + logger.warning(cmds[cmd].stdout.rstrip("\n")) + if cmds[cmd].stderr: + logger.error(cmds[cmd].stderr.rstrip("\n")) + raise SystemExit + + if ret_key: + return cmds[ret_key] + + +def add_undefined_symbol(version, symbol, address): + symbol_line = f"{symbol} = 0x{address:08X};" + undefined_syms = Path(f"config/undefined_syms.{version}.txt") + undefined_syms_lines = undefined_syms.read_text().splitlines() + if symbol_line not in undefined_syms_lines: + undefined_syms_lines.append(symbol_line) + undefined_syms.write_text("\n".join(undefined_syms_lines) + "\n") + sort(undefined_syms) + + +def ovl_sort(name): + game = "dra ric maria " + stage = "are cat cen chi dai dre lib mad no0 no1 no2 no3 no4 np3 nz0 nz1 sel st0 top wrp " + rstage = ( + "rare rcat rcen rchi rdai rlib rno0 rno1 rno2 rno3 rno4 rnz0 rnz1 rtop rwrp " + ) + boss = "bo0 bo1 bo2 bo3 bo4 bo5 bo6 bo7 mar rbo0 rbo1 rbo2 rbo3 rbo4 rbo5 rbo6 rbo7 rbo8 " + servant = "tt_000 tt_001 tt_002 tt_003 tt_004 tt_005 tt_006 " + + name = Path(name).stem.lower() + basename = name.replace("f_", "") + if basename == "main": + group = 0 + elif basename in game and basename != "mar": + group = 1 + elif basename in stage: + group = 2 + elif basename in rstage: + group = 3 + elif basename in boss: + group = 4 + elif basename in boss and basename.startswith("r"): + group = 5 + elif name in servant: + group = 6 + elif "weapon" in name or "w0_" in name or "w1_" in name: + group = 7 + else: + group = 8 + + return (group, basename, name.startswith("f_")) + + +def add_sha1_hashes(ovl_config): + check_file_path = Path(f"config/check.{ovl_config.version}.sha") + check_file_lines = check_file_path.read_text().splitlines() + new_lines = check_file_lines.copy() + bin_line = ( + f"{ovl_config.sha1} build/{ovl_config.version}/{ovl_config.target_path.name}" + ) + if bin_line not in new_lines: + new_lines.append(bin_line) + fbin_path = ovl_config.target_path.with_name( + f"{"f" if ovl_config.platform == "psp" else "F"}_{ovl_config.target_path.name}" + ) + if fbin_path.exists(): + fbin_sha1 = hashlib.sha1(fbin_path.read_bytes()).hexdigest() + fbin_line = f"{fbin_sha1} build/{ovl_config.version}/{fbin_path.name}" + if fbin_line not in new_lines: + new_lines.append(fbin_line) + if new_lines != check_file_lines: + sorted_lines = sorted(new_lines, key=lambda x: ovl_sort(x.split()[-1])) + check_file_path.write_text(f"{"\n".join(sorted_lines)}\n") + + +def create_initial_files(ovl_config, spinner=SimpleNamespace(message="")): + spinner.message = f"creating initial files for overlay {ovl_config.name.upper()}" + ovl_config.write_config() + for symbol_path in ovl_config.symbol_addrs_path: + symbol_path.touch(exist_ok=True) + + ovl_include_path = ( + ovl_config.src_path_full.parent / ovl_config.name / f"{ovl_config.name}.h" + ) + create_ovl_include(None, ovl_config.name, ovl_config.ovl_type, ovl_include_path) + spinner.message = f"adding sha1 hashes to check file" + add_sha1_hashes(ovl_config) + spinner.message = f"performing initial split using config {ovl_config.config_path}" + sotn_utils.splat_split(ovl_config.config_path, ovl_config.disassemble_all) + spinner.message = f"adjusting initial include asm paths" + src_text = ovl_config.first_src_file.read_text() + adjusted_text = src_text.replace(f'("asm/{ovl_config.version}/', '("') + ovl_config.first_src_file.write_text(adjusted_text) + asm_path = ovl_config.asm_path.joinpath(ovl_config.nonmatchings_path) + if ovl_config.platform == "psp": + spinner.message = f"finding and parsing the psp {ovl_config.name.upper()} load function for symbols" + ovl_load_symbol, ovl_header_symbol, entity_updates, symexport_text = ( + parse_psp_ovl_load(ovl_config.name, ovl_config.path_prefix, asm_path) + ) + + if not ovl_config.symexport_path.exists(): + spinner.message = "creating symexport file" + ovl_config.symexport_path.write_text(symexport_text) + else: + ovl_load_symbol, ovl_header_symbol, entity_updates = None, None, {} + + spinner.message = f"generating new {ovl_config.version} build plan including {ovl_config.name.upper()}" + build(build=False, version=ovl_config.version) + spinner.message = ( + f"building initial {ovl_config.ld_script_path.with_suffix('.elf')}" + ) + build( + [f"{ovl_config.ld_script_path.with_suffix('.elf')}"], version=ovl_config.version + ) + + return ovl_load_symbol, ovl_header_symbol, entity_updates + + +def create_ovl_include(entity_updates, ovl_name, ovl_type, ovl_include_path): + entity_funcs = [] + if entity_updates: + camel_case_pattern = re.compile(r"([A-Za-z])([A-Z][a-z])") + for i, func in enumerate([symbol.name for symbol in entity_updates]): + if func == "EntityDummy": + entity_funcs.append((func, f"E_DUMMY_{i+1:X}")) + elif func.startswith("Entity") or func.startswith("OVL_EXPORT(Entity"): + entity_funcs.append( + ( + func, + camel_case_pattern.sub( + r"\1_\2", func.replace("OVL_EXPORT(", "").replace(")", "") + ) + .upper() + .replace("ENTITY", "E"), + ) + ) + elif func == "0x00000000": + entity_funcs.append((func, f"NULL")) + else: + entity_funcs.append((func, f"E_UNK_{i+1:X}")) + + template = Template((Path(__file__).parent / "ovl.h.mako").read_text()) + ovl_header_text = template.render( + ovl_name=ovl_name, + ovl_type=ovl_type, + entity_updates=entity_funcs, + ) + + if not ovl_include_path.exists(): + ovl_include_path.parent.mkdir(parents=True, exist_ok=True) + ovl_include_path.write_text(ovl_header_text) + elif entity_funcs and "Entities" not in ovl_include_path.read_text(): + ovl_include_path.write_text(ovl_header_text) + + +def build_reference_asm( + ovl_name, version, build_path, spinner=SimpleNamespace(message="") +): + ref_pattern = re.compile(r"splat\.\w+\.(?Pst|bo)(?P\w+)\.yaml") + ref_ovls = [] + for file in Path("config").glob(f"splat.{version}.*.yaml"): + if ovl_name not in file.name and (match := ref_pattern.match(file.name)): + prefix = match.group("prefix") or "" + ref_name = match.group("ref_ovl") + ld_path = build_path.joinpath(prefix + ref_name).with_suffix(".ld") + ref_ovls.append( + SimpleNamespace(prefix=prefix, name=ref_name, ld_path=ld_path) + ) + + if ref_ovls: + ref_lds = tuple(ovl.ld_path for ovl in ref_ovls) + found_elfs = tuple(build_path.glob("*.elf")) + missing_elfs = tuple( + ld.with_suffix(".elf") + for ld in ref_lds + if ld.with_suffix(".elf") not in found_elfs + ) + if missing_elfs: + spinner.message = ( + f"creating {len(missing_elfs)} missing reference .elf files" + ) + build(missing_elfs, plan=True, version=version) + + spinner.message = "extracting dynamic symbols" + for elf_file in (ld.with_suffix(".elf") for ld in ref_lds): + config_path = f"config/splat.{version}.{elf_file.stem}.yaml" + dyn_syms_path = f"build/{version}/config/dyn_syms.{elf_file.stem}.txt" + extract_dynamic_symbols(config_path, dyn_syms_path) + + [ld.unlink(missing_ok=True) for ld in ref_lds] + spinner.message = f"disassembling {len(ref_ovls)} reference overlays" + build(ref_lds, dynamic_syms=True, version=version) + + return ref_ovls + + +def find_files_to_compare(ref_ovls, ovl_name, version): + ref_files, check_files = [], [] + for dirpath, _, filenames in Path("asm").joinpath(version).walk(): + if any( + ovl.name in dirpath.parts or f"{ovl.name}_psp" in dirpath.parts + for ovl in ref_ovls + ): + ref_files.extend(dirpath / f for f in filenames) + if ovl_name in dirpath.parts or f"{ovl_name}_psp" in dirpath.parts: + check_files.extend( + dirpath / f + for f in filenames + if f.startswith(f"func_{version}_") or f.startswith(f"D_{version}_") + ) + return check_files, ref_files + + +# Validate logic and move to sotn-decomp +def parse_psp_ovl_load(ovl_name, path_prefix, asm_path): + first_address_pattern = re.compile(r"\s+/\*\s+[A-F0-9]{1,5}\s+([A-F0-9]{8})\s") + psp_entity_updates_pattern = r""" + \s+/\*\s[A-F0-9]{1,5}(?:\s[A-F0-9]{8}){2}\s\*/\s+lui\s+\$v1,\s+%hi\((?P[A-Za-z0-9_]+)\)\n + .*\n + \s+/\*\s[A-F0-9]{1,5}\s[A-F0-9]{8}\sC708023C\s\*/.*\n + \s+/\*\s[A-F0-9]{1,5}\s[A-F0-9]{8}\s30BC43AC\s\*/.*\n + """ + psp_ovl_header_pattern = r""" + \s+/\*\s[A-F0-9]{1,5}\s[A-F0-9]{8}\s1D09043C\s\*/.*\n + \s+/\*\s[A-F0-9]{1,5}\s[A-F0-9]{8}\s38F78424\s\*/.*\n + \s+/\*\s[A-F0-9]{1,5}(?:\s[A-F0-9]{8}){2}\s\*/\s+lui\s+\$a1,\s+%hi\((?P
[A-Za-z0-9_]+)\)\n + (?:.*\n){2} + \s+/\*\s[A-F0-9]{1,5}\s[A-F0-9]{8}\sE127240E\s\*/.*\n + """ + psp_ovl_header_entity_table_pattern = re.compile( + rf"{psp_entity_updates_pattern}(?:.*\n)+{psp_ovl_header_pattern}", + re.VERBOSE, + ) + ovl_load_name = None + ovl_load_symbol = None + ovl_header_symbol = None + entity_updates_name = None + for file in ( + dirpath / f + for dirpath, _, filenames in asm_path.walk() + for f in filenames + if ".data.s" not in f + ): + file_text = file.read_text() + # Todo: Clean up the condition checks + if ( + " 1D09043C " in file_text + and " 38F78424 " in file_text + and " E127240E " in file_text + and " C708023C " in file_text + and " 30BC43AC " in file_text + ): + if match := psp_ovl_header_entity_table_pattern.search(file_text): + if ovl_load_address := first_address_pattern.search(file_text): + ovl_load_symbol = Symbol( + f"{ovl_name.upper()}_Load", + int(ovl_load_address.group(1), 16), + None, + ) + ovl_load_name = file.stem + ovl_header_symbol = match.group("header") + entity_updates_name = match.group("entity") + + # build symexport lines, but only write if needed + template = Template((Path(__file__).parent / "symexport.txt.mako").read_text()) + symexport_text = template.render( + ovl_name=ovl_name, + path_prefix=f"{path_prefix}_" if path_prefix else "", + ovl_load_name=ovl_load_name, + ) + + return ( + ovl_load_symbol, + ovl_header_symbol, + {"name": entity_updates_name}, + symexport_text, + ) + + +def create_header_c(header_items, ovl_name, ovl_type, version, header_path): + header_syms = [ + ( + f"{symbol.name.replace(f'{ovl_name.upper()}_', 'OVL_EXPORT(')})" + if f"{ovl_name.upper()}_" in symbol.name + else "NULL" if not symbol.address else symbol.name + ) + for symbol in header_items + ] + common_syms = [ + "NULL", + "Update", + "HitDetection", + "UpdateRoomPosition", + "InitRoomEntities", + "OVL_EXPORT(rooms)", + "OVL_EXPORT(spriteBanks)", + "OVL_EXPORT(cluts)", + "OVL_EXPORT(pStObjLayoutHorizontal)", + "g_pStObjLayoutHorizontal", + "OVL_EXPORT(rooms_layers)", + "OVL_EXPORT(gfxBanks)", + "UpdateStageEntities", + ] + template = Template((Path(__file__).parent / "header.c.mako").read_text()) + new_header = template.render( + ovl_include_path=f"{ovl_name}.h", + ovl_type=ovl_type, + header_syms=header_syms, + common_syms=common_syms, + ) + if header_path.is_file(): + existing_header = header_path.read_text() + if new_header != existing_header: + new_lines = new_header.rstrip("\n").splitlines() + license = new_lines[0] + include = new_lines[1] + existing_lines = existing_header.rstrip("\n").splitlines() + existing_lines = existing_lines[2:] + ifdef = f"#ifdef VERSION_{'PSP' if version=='pspeu' else version.upper()}" + new_header = f"{license}\n{include}\n{ifdef}\n{"\n".join(new_lines[2:])}\n#else\n{'\n'.join(existing_lines)}\n#endif\n" + + header_path.write_text(new_header) + + +def create_e_init_c(entity_updates, e_inits, ovl_name, e_init_c_path): + if entity_updates: + entity_funcs = [ + ( + f"{symbol.name.replace(f'{ovl_name.upper()}_','OVL_EXPORT(')})" + if f"{ovl_name.upper()}_" in symbol.name + else symbol.name + ) + for symbol in entity_updates + ] + + template = Template((Path(__file__).parent / "e_init.c.mako").read_text()) + output = template.render( + ovl_name=ovl_name, + entity_funcs=entity_funcs, + e_inits=e_inits, + ) + e_init_c_path.write_text(output) + return True + else: + return False + + +def get_known_segments(ovl_name, segments_path): + segments_config = sotn_utils.yaml.safe_load(segments_path.read_text()) + known_segments = [] + # TODO: Simplify this logic + for label, values in segments_config.items(): + if not values or "functions" not in values: + continue + + if "ovl" not in values or ovl_name in values["ovl"]: + if "start" not in values: + starts = [values["functions"][0]] + elif isinstance(values["start"], str): + starts = [values["start"]] + elif isinstance(values["start"], list): + starts = values["start"] + else: + continue + + if "end" in values and isinstance(values["end"], str): + end = values["end"] + else: + end = "" + + functions = { + v.replace("${prefix}", ovl_name.upper()) + for v in values.get("functions", []) + } + known_segments.extend( + SimpleNamespace( + name=values.get("name", label).replace( + "${prefix}", ovl_name.upper() + ), + start=start.replace("${prefix}", ovl_name.upper()), + end=end.replace("${prefix}", ovl_name.upper()), + allow=set(starts) | functions, + ) + for start in starts + ) + # TODO: Check if this is an issue for multiple segments with the same start + return {x.start: x for x in known_segments} + + +def find_psx_entity_updates(first_data_text, pStObjLayoutHorizontal_address=None): + # we know that the EntityUpdates is always after the ovl header + end_of_header = first_data_text.find(".size") + # use the address of pStObjLayoutHorizontal if it was parsed from the header to reduce the amount of data we're searching through + if pStObjLayoutHorizontal_address: + start_index = first_data_text.find( + f"{pStObjLayoutHorizontal_address:08X}", end_of_header + ) + else: + sotn_utils.get_logger().warning( + "No address found for pStObjLayoutHorizontal, starting at end of header" + ) + start_index = end_of_header + + # the first entity referenced after the ovl header, which should be the first element of EntityUpdates + first_entity_index = first_data_text.find(" func_", start_index) + # the last glabel before the first function pointer should be the EntityUpdates symbol + entity_updates_index = first_data_text.rfind( + "glabel", start_index, first_entity_index + ) + # this is just a convoluted way of extracting the EntityUpdates symbol name + # get the second word of the first line, which should be the EntityUpdates symbol name + return { + "name": first_data_text[entity_updates_index:first_entity_index] + .splitlines()[0] + .split()[1] + } + + +def get_known_pairs(ovl_name, version): + # TODO: move to yaml file + known_pairs = [ + SimpleNamespace(first="func_801CC5A4", last="func_801CF438"), + SimpleNamespace(first="func_801CC90C", last="func_801CF6D8"), + SimpleNamespace( + first="GetAnglePointToEntityShifted", last="GetAnglePointToEntity" + ), + SimpleNamespace( + first="CreateEntityWhenInVerticalRange", + last="CreateEntityWhenInHorizontalRange", + ), + SimpleNamespace(first="FindFirstEntityToTheRight", last="FindFirstEntityAbove"), + SimpleNamespace(first="FindFirstEntityToTheLeft", last="FindFirstEntityBelow"), + SimpleNamespace(first="CreateEntitiesToTheRight", last="CreateEntitiesAbove"), + SimpleNamespace(first="CreateEntitiesToTheLeft", last="CreateEntitiesBelow"), + ] + # e_red_door typically comes before e_sealed door, but us rno2 and bo0 have e_sealed_door first + if version == "us" and ovl_name in ["rno2", "bo0"]: + known_pairs.append( + SimpleNamespace(first="SealedDoorIsNearPlayer", last="EntityIsNearPlayer") + ) + else: + known_pairs.append( + SimpleNamespace(first="EntityIsNearPlayer", last="SealedDoorIsNearPlayer") + ) + + return known_pairs + + +def parse_ovl_header(data_file_text, ovl_name, platform, header_symbol=None): + ovl_header = [ + "Update", + "HitDetection", + "UpdateRoomPosition", + "InitRoomEntities", + f"{ovl_name.upper()}_rooms", + f"{ovl_name.upper()}_spriteBanks", + f"{ovl_name.upper()}_cluts", + ( + "g_pStObjLayoutHorizontal" + if platform == "psp" + else f"{ovl_name.upper()}_pStObjLayoutHorizontal" + ), + f"{ovl_name.upper()}_rooms_layers", + f"{ovl_name.upper()}_gfxBanks", + "UpdateStageEntities", + "unk2C", # g_SpriteBank1 + "unk30", # g_SpriteBank2 + "unk34", + "unk38", + "StageEndCutScene", + ] + header_start = ( + data_file_text.find(f"glabel {header_symbol}") + if header_symbol + else data_file_text.find("glabel ") + ) + header_end = ( + data_file_text.find(f".size {header_symbol}") + if header_symbol + else data_file_text.find(".size ") + ) + if header_start != -1: + header = data_file_text[header_start:header_end] + header_address = int(header.splitlines()[0].split("_")[-1], 16) + else: + return {}, None + # Todo: Should this be findall or finditer? + matches = re.findall( + r"/\*\s[0-9A-F]{1,5}\s[0-9A-F]{8}\s(?P
[0-9A-F]{8})\s\*/\s+\.word\s+(?P\w+)", + header, + ) + if matches: + if len(matches) > 7: + pStObjLayoutHorizontal_address = int.from_bytes( + bytes.fromhex(matches[7][0]), "little" + ) + else: + pStObjLayoutHorizontal_address = None + + # Todo: Ensure this is doing a 1 for 1 line replacement, whether func, d_ or null + # Todo: Make the address parsing more straight forward, instead of capturing both address and name + header_items = tuple( + Symbol( + ( + address[1] + if name.startswith("unk") + or ( + not address[1].startswith("func_") + and not address[1].startswith("D_") + and not address[1].startswith("g_") + ) + else "NULL" if address[0] == "0x00000000" else name + ), + int.from_bytes(bytes.fromhex(address[0]), "little"), + None, + ) + # Todo: Does this need the filtering, or should it just overwrite the existing regardless? + for name, address in zip(ovl_header, matches) + ) + return { + "address": header_address, + "size_bytes": len(header_items) * 4, + "symbols": tuple(symbol for symbol in header_items if symbol.address), + "items": header_items, + }, pStObjLayoutHorizontal_address + else: + return {}, None + + +# TODO: Validate logic and move to sotn-decomp +def parse_init_room_entities(ovl_name, platform, init_room_entities_path, vram_start): + init_room_entities_map = { + f"{ovl_name.upper()}_pStObjLayoutHorizontal": 14 if platform == "psp" else 9, + f"{ovl_name.upper()}_pStObjLayoutVertical": 22 if platform == "psp" else 12, + "g_LayoutObjHorizontal": 18 if platform == "psp" else 17, + "g_LayoutObjVertical": 26 if platform == "psp" else 19, + "g_LayoutObjPosHorizontal": ( + 138 + if platform == "psp" and ovl_name == "rnz0" + else 121 if platform == "psp" else 81 + ), + "g_LayoutObjPosVertical": ( + 140 + if platform == "psp" and ovl_name == "rnz0" + else 123 if platform == "psp" else 83 + ), + } + init_room_entities_symbol_pattern = re.compile( + r"\s+/\*\s[0-9A-F]{1,5}\s[0-9A-F]{8}\s[0-9A-F]{8}\s\*/\s+[a-z]{1,5}[ \t]*\$\w+,\s%hi\(D_(?:\w+_)?(?P
[A-F0-9]{8})\)\s*" + ) + lines = init_room_entities_path.read_text().splitlines() + init_room_entities_symbols = { + Symbol( + name, + int( + init_room_entities_symbol_pattern.fullmatch(lines[i]).group("address"), + 16, + ), + None, + ) + for name, i in init_room_entities_map.items() + if "(D_" in lines[i] + } + + create_entity_bss_start = ( + min( + x.address + for x in init_room_entities_symbols + if x.name.startswith("g_Layout") + ) + - vram_start + ) + + return init_room_entities_symbols, create_entity_bss_start + + +def parse_entity_updates(data_file_text, ovl_name, entity_updates_symbol): + parsed_entity_updates = None + known_entity_updates = [ + f"{ovl_name.upper()}_EntityBreakable", + "EntityExplosion", + "EntityPrizeDrop", + "EntityDamageDisplay", + f"{ovl_name.upper()}_EntityRedDoor", + "EntityIntenseExplosion", + "EntitySoulStealOrb", + "EntityRoomForeground", + "EntityStageNamePopup", + "EntityEquipItemDrop", + "EntityRelicOrb", + "EntityHeartDrop", + "EntityEnemyBlood", + "EntityMessageBox", + "EntityDummy", + "EntityDummy", + f"{ovl_name.upper()}_EntityBackgroundBlock", + f"{ovl_name.upper()}_EntityLockCamera", + "EntityUnkId13", + "EntityExplosionVariants", + "EntityGreyPuff", + ] + entity_updates_start = data_file_text.find(f"glabel {entity_updates_symbol}") + entity_updates_end = data_file_text.find(f".size {entity_updates_symbol}") + if entity_updates_start != -1: + entity_updates_lines = data_file_text[ + entity_updates_start:entity_updates_end + ].splitlines() + + first_e_init_start = data_file_text.find("glabel ", entity_updates_end) + first_e_init_end = data_file_text.find("\n", first_e_init_start) + first_e_init = data_file_text[first_e_init_start:first_e_init_end].split()[1] + + # if the last item is a null address, then it is padding + if entity_updates_lines[-1].endswith("0x00000000"): + entity_updates_lines.pop() + + table_start, entity_updates_address = next( + ( + (i, int(line.split()[2], 16)) + for i, line in enumerate(entity_updates_lines) + if " func" in line or " Entity" in line + ), + (len(entity_updates_lines) - 1, None), + ) + entity_updates_lines = entity_updates_lines[table_start:] + if matches := re.findall( + r"/\*\s[0-9A-F]{1,5}\s[0-9A-F]{8}\s(?P
[0-9A-F]{8})\s\*/\s+\.word\s+(?P\w+)", + "\n".join(entity_updates_lines), + ): + entity_dummy_address = Counter([x[0] for x in matches]).most_common(1)[0][0] + entity_dummy_address = int.from_bytes( + bytes.fromhex(entity_dummy_address), "little" + ) + known_symbols = tuple( + Symbol( + address[1] if name == "skip" else name, + int.from_bytes(bytes.fromhex(address[0]), "little"), + None, + ) + for name, address in zip(known_entity_updates, matches) + ) + parsed_entity_updates = known_symbols + tuple( + Symbol( + name.split()[-1], + int.from_bytes(bytes.fromhex(address[0]), "little"), + None, + ) + for name, address in zip( + entity_updates_lines[len(known_symbols) :], + matches[len(known_symbols) :], + ) + ) + parsed_entity_updates = tuple( + Symbol( + ( + "EntityDummy" + if symbol.address == entity_dummy_address + else symbol.name + ), + symbol.address, + None, + ) + for symbol in parsed_entity_updates + ) + symbols = tuple( + symbol + for symbol in parsed_entity_updates + if symbol.name.split("_")[-1] != f"{symbol.address:08X}" + ) + else: + symbols = tuple() + # TODO: Why the weird + () ? + return { + "address": entity_updates_address, + "first_e_init": first_e_init, + "items": parsed_entity_updates, + "symbols": symbols + (), + } + + +def cross_reference_e_init_c( + check_entity_updates, check_e_inits, ref_e_init_path, ovl_name, map_path +): + if ref_e_init_path.is_file(): + symbols = [] + file_text = ref_e_init_path.read_text() + e_init_pattern = re.compile( + r""" + \nEInit\s+(?P(?:OVL_EXPORT\()?\w+\)?)\s*=\s*\{(?:\s*|\n?) + (?P(?:ANIMSET_(?:OVL|DRA)\()?(?:0x)?[0-9A-Fa-f]{1,4}\)?)\s* + ,\s*(?P(?:0x)?[0-9A-Fa-f]{1,4})\s* + ,\s*(?P(?:0x)?[0-9A-Fa-f]{1,4})\s* + ,\s*(?P(?:0x)?[0-9A-Fa-f]{1,4}|PAL_[A-Z0-9_]+)\s* + ,\s*(?P(?:0x)?[0-9A-Fa-f]{1,4})\}; + """, + re.VERBOSE, + ) + + if check_entity_updates: + entity_updates_start = file_text.find("EntityUpdates") + entity_updates_end = file_text.find("};", entity_updates_start) + ref_entity_updates = [ + item.strip().replace("OVL_EXPORT(", f"{ovl_name.upper()}_").rstrip(",)") + for item in file_text[ + entity_updates_start:entity_updates_end + ].splitlines()[1:] + if item + ] + if len(check_entity_updates) == len(ref_entity_updates): + symbols.extend( + Symbol(to_name, from_symbol.address, None) + for from_symbol, to_name in zip( + check_entity_updates, ref_entity_updates + ) + ) + + if check_e_inits: + ref_e_inits = [] + e_init_idx = file_text.find("EInit") + while e_init_idx != -1: + if e_init := e_init_pattern.match(file_text[e_init_idx - 1 :]): + name = ( + e_init.group("name") + .replace("OVL_EXPORT(", f"{ovl_name.upper()}_") + .rstrip(")") + ) + animSet = e_init.group("animSet") + animCurFrame = e_init.group("animCurFrame") + animCurFrame = int(animCurFrame, 16 if "0x" in animCurFrame else 10) + unk5A = e_init.group("unk5A") + unk5A = int(unk5A, 16 if "0x" in unk5A else 10) + palette = e_init.group("palette") + palette = ( + palette + if "PAL_" in palette + else int(palette, 16) if "0x" in palette else int(palette) + ) + enemyID = e_init.group("enemyID") + ref_e_inits.append( + (name, animSet, animCurFrame, unk5A, palette, enemyID) + ) + e_init_idx = file_text.find("EInit", e_init_idx + 1) + + not_matched = 0 + for i, ref_e_init in enumerate(ref_e_inits): + if i - not_matched < len(check_e_inits): + if ref_e_init[1:] != check_e_inits[i - not_matched][1:]: + not_matched += 1 + else: + symbols.append( + Symbol( + ref_e_init[0], + sotn_utils.get_symbol_address( + map_path, check_e_inits[i - not_matched][0] + ), + None, + ) + ) + + return symbols, len(ref_e_inits) == len(check_e_inits) + not_matched + return [], False + + +def parse_e_inits(data_file_text, first_e_init, ovl_name, platform, config_yaml_dir): + e_init_pattern = re.compile( + r""" + glabel\s+(?P\w+)\n + \s+/\*\s+(?P[0-9A-Fa-f]+)\s+(?P
[0-9A-Fa-f]{8})\s+[0-9A-Fa-f]{8}\s+\*/\s+\.word\s+0x(?P[0-9A-Fa-f]{4})(?P[0-9A-Fa-f]{4})\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+[0-9A-Fa-f]{8}\s+\*/\s+\.word\s+0x(?P[0-9A-Fa-f]{4})(?P[0-9A-Fa-f]{4})\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+[0-9A-Fa-f]{8}\s+\*/\s+\.word\s+0x0000(?P[0-9A-Fa-f]{4})\n + """ + + ( + r""" + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+00000000\s+\*/\s+\.word\s+0x00000000\n + """ + if platform == "psp" + else "" + ) + + r""" + (?P\.size\s+(?P=name),\s+\.\s+-\s+(?P=name)\n?)? + """, + re.VERBOSE, + ) + unused_e_init_pattern = r""" + \s+/\*\s+(?P[0-9A-Fa-f]+)\s+(?P
[0-9A-Fa-f]{8})\s+[0-9A-Fa-f]{8}\s+\*/\s+\.word\s+0x(?P[0-9A-Fa-f]{4})(?P[0-9A-Fa-f]{4})\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+[0-9A-Fa-f]{8}\s+\*/\s+\.word\s+0x(?P[0-9A-Fa-f]{4})(?P[0-9A-Fa-f]{4})\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+[0-9A-Fa-f]{8}\s+\*/\s+\.word\s+0x0000(?P[0-9A-Fa-f]{4})\n + """ + # when e_init[3] is referenced in code + split_e_init_pattern = re.compile( + r""" + glabel\s+(?P\w+)\n + \s+/\*\s+(?P[0-9A-Fa-f]+)\s+(?P
[0-9A-Fa-f]{8})\s+(?P[0-9A-Fa-f]{8})\s+\*/\s+\.word\s+0x(?P[0-9A-Fa-f]{4})(?P[0-9A-Fa-f]{4})\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+\*/\s+\.short\s+0x(?P[0-9A-Fa-f]{4})\n + \.size\s+(?P=name),\s+\.\s+-\s+(?P=name)\n + \n + glabel\s+(?P\w+)\n + \s+/\*\s+(?P[0-9A-Fa-f]+)\s+(?P[0-9A-Fa-f]{8})\s+\*/\s+\.short\s+0x(?P[0-9A-Fa-f]{4})\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+[0-9A-Fa-f]{8}\s+\*/\s+\.word\s+0x0000(?P[0-9A-Fa-f]{4})\n + """ + + ( + r""" + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+00000000\s+\*/\s+\.word\s+0x00000000\n + """ + if platform == "psp" + else "" + ) + + r""" + (?P\.size\s+(?P=pal_sym),\s+\.\s+-\s+(?P=pal_sym)\n?) + """, + re.VERBOSE, + ) + if platform == "psx": + # when e_init[3] and e_init[5] are referenced in code in us + short_e_init_pattern = re.compile( + r""" + glabel\s+(?P\w+)\n + \s+/\*\s+(?P[0-9A-Fa-f]+)\s+(?P
[0-9A-Fa-f]{8})\s+[0-9A-Fa-f]{8}\s+\*/\s+\.word\s+0x(?P[0-9A-Fa-f]{4})(?P[0-9A-Fa-f]{4})\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+\*/\s+\.short\s+0x(?P[0-9A-Fa-f]{4})\n + \.size\s+(?P=name),\s+\.\s+-\s+(?P=name)\n + \n + glabel\s+(?P\w+)\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+\*/\s+\.short\s+0x(?P[0-9A-Fa-f]{4})\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+\*/\s+\.short\s+0x(?P[0-9A-Fa-f]{4})\n + \.size\s+(?P=pal_sym),\s+\.\s+-\s+(?P=pal_sym)\n + \n + glabel\s+(?P\w+)\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+\*/\s+\.short\s+0x0000\n + (?P\.size\s+(?P=unk_sym),\s+\.\s+-\s+(?P=unk_sym)\n?) + """, + re.VERBOSE, + ) + if platform == "psp": + short_e_init_pattern = re.compile( + r""" + glabel\s+(?P\w+)\n + \s+/\*\s+(?P[0-9A-Fa-f]+)\s+(?P
[0-9A-Fa-f]{8})\s+\*/\s+\.short\s+0x(?P[0-9A-Fa-f]{4})\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+\*/\s+\.short\s+0x(?P[0-9A-Fa-f]{4})\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+\*/\s+\.short\s+0x(?P[0-9A-Fa-f]{4})\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+\*/\s+\.short\s+0x(?P[0-9A-Fa-f]{4})\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+\*/\s+\.short\s+0x(?P[0-9A-Fa-f]{4})\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+\*/\s+\.short\s+0x0000\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+\*/\s+\.short\s+0x0000\n + \s+/\*\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]{8}\s+\*/\s+\.short\s+0x0000\n + (?P\.size\s+(?P=name),\s+\.\s+-\s+(?P=name)\n?) + """, + re.VERBOSE, + ) + + known_e_inits = [ + f"{ovl_name.upper()}_EInitBreakable", + "g_EInitObtainable", + "g_EInitParticle", + "g_EInitSpawner", + "g_EInitInteractable", + "g_EInitUnkId13", + "g_EInitLockCamera", + "g_EInitCommon", + "g_EInitDamageNum", + ] + + text = data_file_text[data_file_text.find(f"glabel {first_e_init}") :] + parsed_e_inits = [] + while not parsed_e_inits or matches: + matches = ( + re.match(e_init_pattern, text) + or re.match(split_e_init_pattern, text) + or re.match(short_e_init_pattern, text) + ) + if platform != "psp" and matches and not matches.groupdict().get("size"): + size_name = matches.groupdict().get("name") + while not matches.groupdict().get("size"): + address = int(matches.group("address"), 16) + name = matches.groupdict().get("name") or f"g_EInitUnused{address:08X}" + animSet = int(matches.group("animSet"), 16) + parsed_e_inits.append( + ( + Symbol(name, address, None), + f"ANIMSET_{'OVL' if animSet & 0x8000 else 'DRA'}({animSet & ~0x8000})", + int(matches.group("animCurFrame"), 16), + int(matches.group("unk5A"), 16), + int(matches.group("palette"), 16), + int(matches.group("enemyID"), 16), + ) + ) + if matches.groupdict().get("size"): + break + text = text[matches.end() :] + unused_last_line_pattern = ( + rf"(?P\.size\s+{size_name},\s+\.\s+-\s+{size_name}\n?)?" + ) + matches = re.match( + unused_e_init_pattern + unused_last_line_pattern, text, re.VERBOSE + ) + + if matches: + address = int(matches.group("address"), 16) + name = matches.groupdict().get("name") or f"g_EInitUnused{address:08X}" + animSet = int(matches.group("animSet"), 16) + parsed_e_inits.append( + ( + Symbol(name, address, None), + f"ANIMSET_{'OVL' if animSet & 0x8000 else 'DRA'}({animSet & ~0x8000})", + int(matches.group("animCurFrame"), 16), + int(matches.group("unk5A"), 16), + int(matches.group("palette"), 16), + int(matches.group("enemyID"), 16), + ) + ) + text = text[matches.end() + 1 :] + + enemy_defs = { + v: k + for k, v in sotn_utils.yaml.safe_load( + (Path(config_yaml_dir) / "enemy_defs.yaml").read_text() + ).items() + } + + symbols = [ + Symbol(name, e_init[0].address, None) + for name, e_init in zip(known_e_inits, parsed_e_inits) + if platform != "psp" + ] + added_names = [] + for e_init in parsed_e_inits[len(symbols) :]: + if e_init[5] in enemy_defs: + name = f"g_EInit{enemy_defs[e_init[5]]}" + if name in added_names: + symbols.append( + Symbol(f"{name}{e_init[0].address:X}", e_init[0].address, None) + ) + else: + symbols.append(Symbol(name, e_init[0].address, None)) + added_names.append(name) + else: + symbols.append(Symbol(e_init[0].name, e_init[0].address, None)) + + e_inits = [ + ( + symbol.name if platform != "psp" else e_init[0].name, + e_init[1], + e_init[2], + e_init[3], + e_init[4], + f"0x{e_init[5]:03X}", + ) + for symbol, e_init in zip(symbols, parsed_e_inits) + ] + next_offset = re.match(r"glabel\s+\w+\n\s+/\*\s+(?P[0-9A-Fa-f]+)\s+", text) + return ( + e_inits, + int(next_offset.group("offset"), 16) if next_offset else None, + [x for x in symbols if not x.name.startswith("D_")], + ) + + +def add_initial_symbols( + ovl_config, + config_yaml_dir, + e_init_c_path, + ovl_header_symbol, + parsed_symbols=[], + entity_updates={}, + spinner=SimpleNamespace(message=""), +): + subsegments = ovl_config.subsegments.copy() + + ### group change ### + spinner.message = f"finding the first data file" + first_data_offset = next(subseg[0] for subseg in subsegments if "data" in subseg) + first_data_path = ovl_config.asm_path / "data" / f"{first_data_offset:X}.data.s" + if first_data_path.exists(): + first_data_text = first_data_path.read_text() + ### group change ### + spinner.message = f"parsing the overlay header for symbols" + ovl_header, pStObjLayoutHorizontal_address = parse_ovl_header( + first_data_text, + ovl_config.name, + ovl_config.platform, + ovl_header_symbol, + ) + # TODO: Add data segments for follow-on header symbols + if ovl_config.platform == "psx": + ### group change ### + spinner.message = f"finding the entity table" + entity_updates = find_psx_entity_updates( + first_data_text, pStObjLayoutHorizontal_address + ) + else: + first_data_text = None + ovl_header, pStObjLayoutHorizontal_address = {}, None + entity_updates = {} + + ### group change ### + spinner.message = "gathering initial symbols" + if entity_updates and first_data_text: + ### group change ### + spinner.message = "parsing EntityUpdates" + entity_updates.update( + parse_entity_updates( + first_data_text, ovl_config.name, entity_updates.get("name") + ) + ) + ### group change ### + spinner.message = "parsing EInits" + e_inits, next_offset, symbols = parse_e_inits( + first_data_text, + entity_updates.get("first_e_init"), + ovl_config.name, + ovl_config.platform, + config_yaml_dir, + ) + parsed_symbols.extend(symbols) + + ### group change ### + if ovl_config.version == "us": + e_init_success = True + else: + spinner.message = "cross-referencing e_init.c" + e_init_symbols, e_init_success = cross_reference_e_init_c( + entity_updates.get("items"), + e_inits, + e_init_c_path, + ovl_config.name, + ovl_config.ld_script_path.with_suffix(".map"), + ) + parsed_symbols.extend(e_init_symbols) + + entity_updates_offset = ( + entity_updates.get("address", 0) - ovl_config.vram + ovl_config.start + ) + if entity_updates_offset > 0: + e_init_subseg = [ + entity_updates_offset, + f"{'.' if e_init_success else ''}data", + ( + f"{ovl_config.name}/e_init" + if ovl_config.platform == "psp" + else "e_init" + ), + next_offset - entity_updates_offset, + ] + subsegments.append(e_init_subseg) + + if ovl_header.get("symbols") or entity_updates.get("symbols"): + parsed_symbols.extend( + ( + symbol + for symbols in ( + ovl_header.get("symbols"), + entity_updates.get("symbols"), + ) + if symbols is not None + for symbol in symbols + ) + ) + if entity_updates.get("address"): + parsed_symbols.append( + Symbol( + f"{ovl_config.name.upper()}_EntityUpdates", + entity_updates.get("address"), + None, + ) + ) + if ovl_header.get("address"): + parsed_symbols.append( + Symbol( + f"{ovl_config.name.upper()}_Overlay", ovl_header.get("address"), None + ) + ) + + return subsegments, parsed_symbols, ovl_header, entity_updates, e_inits + + +def validate_binary( + basename, + asm_path, + ld_script_path, + build_path, + target_path, + version, + sha1, + built_bin_path, +): + logger = sotn_utils.get_logger() + # TODO: Compare generated offsets to .elf segment offsets + run(["git", "clean", "-fdx", asm_path], capture_output=True) + ld_script_path.unlink(missing_ok=True) + build( + [ + f"{build_path}/config/splat.{version}.{basename}.yaml.dyn_syms", + f"{ld_script_path}", + f"{ld_script_path.with_suffix('.elf')}", + f"{build_path}/{target_path.name}", + ], + version=version, + ) + if built_bin_path.exists(): + built_sha1 = hashlib.sha1(built_bin_path.read_bytes()).hexdigest() + else: + logger.error(f"{built_bin_path} did not build properly") + raise SystemExit + + if built_sha1 != sha1: + logger.error(f"{built_bin_path} did not match {target_path}") + raise SystemExit + + +def can_extract(overlay): + stage = "are cat cen chi dai dre lib no0 no1 no2 no3 no4 np3 nz0 nz1 st0 top wrp " + rstage = ( + "rare rcat rcen rchi rdai rlib rno0 rno1 rno2 rno3 rno4 rnz0 rnz1 rtop rwrp " + ) + boss = "bo0 bo1 bo2 bo3 bo4 bo5 bo6 bo7 mar rbo0 rbo1 rbo2 rbo3 rbo4 rbo5 rbo6 rbo7 rbo8 " + return overlay in (stage + rstage + boss).split(" ") + + +def clean_artifacts(ovl_config, full_clean=False, spinner=SimpleNamespace(message="")): + if (asm_path := Path(f"asm/{ovl_config.version}")).exists(): + spinner.message = run(["git", "clean", "-fdx", asm_path], capture_output=True) + + spinner.message = f"Removing {ovl_config.ld_script_path}" + ovl_config.ld_script_path.unlink(missing_ok=True) + + spinner.message = f"Removing {ovl_config.ld_script_path.with_suffix(".elf")}" + ovl_config.ld_script_path.with_suffix(".elf").unlink(missing_ok=True) + + spinner.message = f"Removing {ovl_config.ld_script_path.with_suffix(".map")}" + ovl_config.ld_script_path.with_suffix(".map").unlink(missing_ok=True) + + spinner.message = ( + f"Removing {ovl_config.build_path.joinpath(f"{ovl_config.target_path.name}")}" + ) + ovl_config.build_path.joinpath(f"{ovl_config.target_path.name}").unlink( + missing_ok=True + ) + + if full_clean: + if ( + build_src_path := ovl_config.build_path / ovl_config.src_path_full + ).exists(): + spinner.message = run( + ["git", "clean", "-fdx", build_src_path], capture_output=True + ) + + spinner.message = f"Removing config/check.{ovl_config.version}.sha" + sha_check_path = Path(f"config/check.{ovl_config.version}.sha") + sha_check_lines = ( + line + for line in sha_check_path.read_text().splitlines() + if ovl_config.sha1 not in line + ) + fbin_path = ovl_config.target_path.with_name( + f"{"f" if ovl_config.platform == "psp" else "F"}_{ovl_config.target_path.name}" + ) + if fbin_path.exists(): + fbin_sha1 = hashlib.sha1(fbin_path.read_bytes()).hexdigest() + sha_check_lines = ( + line for line in sha_check_lines if fbin_sha1 not in line + ) + sha_check_path.write_text("\n".join(sha_check_lines) + "\n") + + spinner.message = f"Removing {ovl_config.ovl_symbol_addrs_path}" + ovl_config.ovl_symbol_addrs_path.unlink(missing_ok=True) + + if ovl_config.symexport_path: + spinner.message = f"Removing {ovl_config.symexport_path}" + ovl_config.symexport_path.unlink(missing_ok=True) + + if ovl_config.version != "hd" and ovl_config.src_path_full.exists(): + spinner.message = f"Removing {ovl_config.src_path_full}" + shutil.rmtree(ovl_config.src_path_full) + + spinner.message = f"Removing {ovl_config.config_path}" + ovl_config.config_path.unlink(missing_ok=True) + + spinner.message = f"cleaned {ovl_config.version} overlay {ovl_config.name.upper()} artifacts and configuration" + + +def remove_overlay(overlay, versions): + logger = sotn_utils.get_logger() + with sotn_utils.Spinner(message=f"starting overlay removal") as spinner: + for version in versions: + logger.info( + f"Removing {version} overlay {overlay.upper()} artifacts and configuration" + ) + spinner.message = f"removing {version} overlay {overlay.upper()} artifacts and configuration" + ovl_config = sotn_utils.SotnOverlayConfig(overlay, version) + clean_artifacts(ovl_config, True, spinner) + + +def extract(args, version): + logger = sotn_utils.get_logger() + start_time = time.perf_counter() + logger.info( + f"Starting config generation for {version} overlay {args.overlay.upper()}" + ) + with sotn_utils.Spinner( + message=f"generating config for {version} overlay {args.overlay.upper()}" + ) as spinner: + ovl_config = sotn_utils.SotnOverlayConfig(args.overlay, version) + if ovl_config.config_path.exists() and not args.clean: + logger.error( + f"Configuration {ovl_config.name} already exists. Use the --clean option to remove all existing overlay artifacts and re-extract the overlay." + ) + raise SystemExit + + clean_artifacts(ovl_config, args.clean, spinner) + ovl_load_symbol, ovl_header_symbol, entity_updates = create_initial_files( + ovl_config, spinner + ) + + with sotn_utils.Spinner(message="gathering initial symbols") as spinner: + e_init_c_path = ovl_config.src_path_full.with_name(ovl_config.name) / "e_init.c" + ovl_config.subsegments, parsed_symbols, ovl_header, entity_updates, e_inits = ( + add_initial_symbols( + ovl_config, + args.config_dir, + e_init_c_path, + ovl_header_symbol, + [ovl_load_symbol] if ovl_load_symbol else [], + entity_updates, + spinner, + ) + ) + if ovl_header.get("items"): + spinner.message = f"creating {ovl_config.name}/header.c" + create_header_c( + ovl_header.get("items"), + ovl_config.name, + ovl_config.ovl_type, + ovl_config.version, + ovl_config.src_path_full.parent / ovl_config.name / "header.c", + ) + spinner.message = f"adding header subsegment" + header_offset = ovl_header["address"] - ovl_config.vram + ovl_config.start + header_subseg = [ + header_offset, + ".data", + ( + f"{ovl_config.name}/header" + if ovl_config.platform == "psp" + else "header" + ), + ovl_header.get("size_bytes", 0), + ] + ovl_config.subsegments.append(header_subseg) + + if ovl_config.version == "us": + spinner.message = "creating e_init.c" + e_init_success = create_e_init_c( + entity_updates.get("items"), e_inits, ovl_config.name, e_init_c_path + ) + if parsed_symbols: + spinner.message = ( + f"adding {len(parsed_symbols)} parsed symbols and splitting again" + ) + sotn_utils.add_symbols( + ovl_config.ovl_symbol_addrs_path, + parsed_symbols, + ovl_config.name, + ovl_config.vram, + ovl_config.symbol_name_format.replace("$VRAM", ""), + ovl_config.src_path_full, + ovl_config.symexport_path, + ) + sotn_utils.shell(f"git clean -fdx {ovl_config.asm_path}") + sotn_utils.splat_split(ovl_config.config_path) + ovl_include_path = ( + ovl_config.src_path_full.parent / ovl_config.name / f"{ovl_config.name}.h" + ) + ovl_include_path = ( + ovl_config.src_path_full.parent / ovl_config.name / f"{ovl_config.name}.h" + ) + create_ovl_include( + entity_updates.get("items"), + ovl_config.name, + ovl_config.ovl_type, + ovl_include_path, + ) + + with sotn_utils.Spinner(message="gathering reference overlays") as spinner: + ref_ovls = build_reference_asm( + ovl_config.name, ovl_config.version, ovl_config.build_path, spinner + ) + if ref_ovls: + spinner.message = f"finding files to compare" + check_files, ref_files = find_files_to_compare( + ref_ovls, ovl_config.name, ovl_config.version + ) + spinner.message = f"parsing instructions from {len(check_files)} new files and {len(ref_files)} reference files" + parsed_check_files = sotn_utils.parse_asm_files(check_files) + parsed_ref_files = sotn_utils.parse_asm_files(ref_files) + else: + parsed_check_files, parsed_ref_files = None, None + spinner.message = f"found no reference overlays" + + if parsed_check_files and parsed_ref_files: + spinner.message = "searching for similar functions" + known_pairs = get_known_pairs(ovl_config.name, ovl_config.version) + ambiguous_renames, unhandled_renames = sotn_utils.rename_similar_functions( + ovl_config, parsed_check_files, parsed_ref_files, known_pairs, spinner + ) + spinner.message += ( + f" (compared {len(check_files)} new to {len(ref_files)} reference)" + ) + else: + ambiguous_renames, unhandled_renames = [], [] + + with sotn_utils.Spinner( + message="parsing symbols from InitRoomEntities.s" + ) as spinner: + nonmatchings_path = ( + f"{ovl_config.nonmatchings_path}/{ovl_config.name}_psp" + if ovl_config.platform == "psp" + else ovl_config.nonmatchings_path + ) + + init_room_entities_path = ( + ovl_config.asm_path + / nonmatchings_path + / f"first_{ovl_config.name}" + / f"InitRoomEntities.s" + ) + + if init_room_entities_path.exists(): + init_room_entities_symbols, create_entity_bss_start = ( + parse_init_room_entities( + ovl_config.name, + ovl_config.platform, + init_room_entities_path, + ovl_config.vram + ovl_config.start, + ) + ) + + create_entity_bss_end = create_entity_bss_start + ( + 0x18 if ovl_config.platform == "psp" else 0x10 + ) + + spinner.message = "parsing renamed functions for cross referencing" + parsed_check_files = sotn_utils.parse_asm_files( + dirpath / f + for dirpath, _, filenames in ovl_config.asm_path.walk() + for f in filenames + if ( + f.startswith(f"func_{ovl_config.version}_") + and f.split("_")[-2] == "from" + ) + or ( + not f.startswith(f"func_{ovl_config.version}_") + and not f.startswith("D_") + ) + ) + + spinner.message = f"cross referencing {len(parsed_check_files)} renamed functions against {len({x.path.name for x in parsed_ref_files})} existing functions" + cross_ref_symbols = sotn_utils.cross_reference_asm( + parsed_check_files, parsed_ref_files, ovl_config.name, ovl_config.version + ) + if init_room_entities_symbols or cross_ref_symbols: + spinner.message = f"adding {len(init_room_entities_symbols)} parsed symbols and {len(cross_ref_symbols)} cross referenced symbols and splitting again" + sotn_utils.add_symbols( + ovl_config.ovl_symbol_addrs_path, + init_room_entities_symbols | cross_ref_symbols, + ovl_config.name, + ovl_config.vram, + ovl_config.symbol_name_format.replace("$VRAM", ""), + ovl_config.src_path_full, + ovl_config.symexport_path, + ) + run(["git", "clean", "-fdx", ovl_config.asm_path], capture_output=True) + sotn_utils.splat_split(ovl_config.config_path, ovl_config.disassemble_all) + + with sotn_utils.Spinner( + message=f"creating {ovl_config.ld_script_path.with_suffix(".elf")}" + ) as spinner: + build( + [f'{ovl_config.ld_script_path.with_suffix(".elf")}'], + version=ovl_config.version, + ) + spinner.message = f"finding segments and splitting source files" + include_path = f"../{ovl_config.name}/" if ovl_config.platform == "psp" else "" + file_header = f'// SPDX-License-Identifier: AGPL-3.0-or-later\n#include "{include_path}{ovl_config.name}.h"\n\n' + + known_segments = get_known_segments( + ovl_config.name, Path(args.config_dir) / "segments.yaml" + ) + ovl_config.subsegments = sotn_utils.find_segments( + ovl_config, file_header, known_segments + ) + ovl_config.write_config() + + built_bin_path = ovl_config.build_path / ovl_config.target_path.name + spinner.message = f"building and validating {built_bin_path}" + validate_binary( + ovl_config.basename, + ovl_config.asm_path, + ovl_config.ld_script_path, + ovl_config.build_path, + ovl_config.target_path, + version, + ovl_config.sha1, + built_bin_path, + ) + + with sotn_utils.Spinner(message=f"cleaning up {ovl_config.name}.h") as spinner: + # just in case any function got renamed after the files were created + ovl_header_text = ovl_include_path.read_text() + ovl_header_text = re.sub( + rf"{ovl_config.name.upper()}_(\w\w+)\b", r"OVL_EXPORT(\1)", ovl_header_text + ) + entity_enum_pattern = re.compile( + r"\s+(?PE_[A-Z0-9_]+),\s+//\s+(?:OVL_EXPORT\()?(?PEntity\w+)\)?\b" + ) + camel_case_pattern = re.compile(r"([A-Za-z])([A-Z][a-z])") + ovl_header_lines = [ + ( + line.replace( + m.group("e_id"), + f"{camel_case_pattern.sub(r"\1_\2", m.group("func"))}".upper().replace( + "ENTITY", "E" + ), + ) + if (m := entity_enum_pattern.match(line)) and "DUMMY" not in line + else line + ) + for line in ovl_header_text.splitlines() + ] + e_id_max_length = max( + [ + len(line.split()[0]) + for line in ovl_header_lines + if " E_" in line and "//" in line + ] + or [""] + ) + ovl_header_lines = [ + ( + f" {line.split()[0]:<{e_id_max_length}} {' '.join(line.split()[1:])}" + if " E_" in line and "//" in line + else line + ) + for line in ovl_header_lines + ] + ovl_include_path.write_text("\n".join(ovl_header_lines) + "\n") + + spinner.message = "cleaning up e_init.c" + e_init_c_path.write_text( + re.sub( + rf"{ovl_config.name.upper()}_(\w+)\b", + r"OVL_EXPORT(\1)", + e_init_c_path.read_text(), + ) + ) + + spinner.message = "getting suggested segments" + suggested_segments = sotn_utils.get_suggested_segments(ovl_config.config_path) + + time_text = sotn_utils.get_run_time(start_time) + print(f"{sotn_utils.TTY.OK} Extracted {version} {args.overlay} ({time_text})") + if suggested_segments: + print( + f"{sotn_utils.TTY.INFO_CIRCLE} {len(suggested_segments)} additional segments were suggested by Splat:" + ) + for segment in suggested_segments: + print(f" - [{segment[0]}, {segment[1]}, {segment[2]}]") + logger.info(f"Additional segments suggested by splat: {suggested_segments}") + if ambiguous_renames: + print( + f"{sotn_utils.TTY.WARNING} Found {len(ambiguous_renames)} functions renamed with ambiguous matches:" + ) + from_width = max(len(x.old_names[0]) for x in ambiguous_renames) + 1 + to_width = max(len(x.new_names[0]) for x in ambiguous_renames) + 1 + print( + f" {'from':<{from_width}}{sotn_utils.TTY.BOLD}|{sotn_utils.TTY.RESET} {'to':<{to_width}}{sotn_utils.TTY.BOLD}|{sotn_utils.TTY.RESET} score {sotn_utils.TTY.BOLD}|{sotn_utils.TTY.RESET} other matches" + ) + for rename in ambiguous_renames: + print( + f" {rename.old_names[0]:<{from_width}}{sotn_utils.TTY.BOLD}|{sotn_utils.TTY.RESET} {rename.new_names[0]:<{to_width}}{sotn_utils.TTY.BOLD}|{sotn_utils.TTY.RESET} {round(rename.score, 2):<6}{sotn_utils.TTY.BOLD}|{sotn_utils.TTY.RESET} {', '.join(rename.all_matches[1:])}" + ) + if unhandled_renames: + print( + f"{sotn_utils.TTY.WARNING} Encountered {len(unhandled_renames)} unhandled matches:" + ) + from_width = max(len(", ".join(x.old_names)) for x in unhandled_renames) + 1 + to_width = max(len(", ".join(x.new_names)) for x in unhandled_renames) + 1 + print( + f" {'functions':<{from_width}}{sotn_utils.TTY.BOLD}|{sotn_utils.TTY.RESET} {'matches':<{to_width}}{sotn_utils.TTY.BOLD}|{sotn_utils.TTY.RESET} score" + ) + for rename in unhandled_renames: + print( + f" {', '.join(rename.old_names):<{from_width}}{sotn_utils.TTY.BOLD}|{sotn_utils.TTY.RESET} {', '.join(rename.new_names):{to_width}}{sotn_utils.TTY.BOLD}|{sotn_utils.TTY.RESET} {rename.score}" + ) + + # make clean removes new files in the config/ directory, so these need to be staged + precious_files = [ + f"{ovl_config.config_path}", + f"config/undefined_syms.{version}.txt", + f"config/check.{version}.sha", + ] + if isinstance(ovl_config.symbol_addrs_path, (list, tuple)): + precious_files.extend(f"{x}" for x in ovl_config.symbol_addrs_path) + else: + precious_files.append(f"{ovl_config.symbol_addrs_path}") + if ovl_config.platform == "psp": + precious_files.append(f"{ovl_config.symexport_path}") + run(["git", "add"] + precious_files, capture_output=True) + + # clean up certain files so the next build uses current symbols + ovl_config.ld_script_path.unlink(missing_ok=True) + ovl_config.ld_script_path.with_suffix(".map").unlink(missing_ok=True) + ovl_config.ld_script_path.with_suffix(".elf").unlink(missing_ok=True) + + if args.make_expected: + print(f"VERSION={ovl_config.version} make expected") + env = os.environ.copy() + env["VERSION"] = ovl_config.version + run(["make", "expected"], env=env, text=True) + + +if __name__ == "__main__": + # set global multiprocessing options + multiprocessing.log_to_stderr() + multiprocessing.set_start_method("spawn") + + parser = argparse.ArgumentParser( + description="Create initial configuration for new overlays" + ) + parser.add_argument( + "overlay", + help="Name of the overlay to create a configuration for", + ) + parser.add_argument( + "-v", + "--version", + required=False, + action="append", + default=[], + help="The version of the game to target (-v/--version can be passed multiple times or multiple comma separated versions: i.e. '-v us,pspeu' or '-v us -v pspeu'), use 'all' to extract us, pspeu, and hd", + ) + parser.add_argument( + "-c", + "--config-dir", + required=False, + default=f"{Path(__file__).parent}", + help=f"Specify a path to yaml config files (default is '{Path(__file__).parent.relative_to(Path.cwd())}')", + ) + parser.add_argument( + "-t", + "--templates", + required=False, + default=f"{Path(__file__).parent}", + help=f"Specify a directory where mako templates can be found (default is '{Path(__file__).parent.relative_to(Path.cwd())}')", + ) + parser.add_argument( + "-l", + "--log", + required=False, + default=f"{Path(__file__).parent / 'extract_overlay_log.json'}", + help=f"Specify a path for the log file (default is '{Path(__file__).parent.relative_to(Path.cwd()) / 'extract_overlay_log.json'}')", + ) + parser.add_argument( + "-e", + "--make-expected", + required=False, + action="store_true", + help="Run 'make expected' after successful extraction", + ) + parser.add_argument( + "--clean", + required=False, + action="store_true", + help="DESTRUCTIVE: Remove any existing overlay artifacts before re-extracting the overlay from the source binary", + ) + parser.add_argument( + "--remove", + required=False, + action="store_true", + help="DESTRUCTIVE: Remove any existing overlay artifacts without re-extracting the overlay", + ) + + args = parser.parse_args() + sotn_utils.init_logger(filename=args.log) + + if not args.version: + args.version.append(os.getenv("VERSION")) + else: + # split, flatten, and dedup version args + split_versions = [ + val.split(",") if "," in val else [val] for val in args.version + ] + args.version = { + version.strip() for versions in split_versions for version in versions + } + + if args.version and None not in args.version: + unsupported_versions = [ + ver for ver in args.version if ver not in ["us", "pspeu", "hd", "all"] + ] + if unsupported_versions: + parser.error( + f"argument -v/--version: invalid choice(s): {unsupported_versions} (choose from 'us', 'pspeu', 'hd', 'all')" + ) + else: + parser.error( + f"the following arguments are required: -v/--version (can be via VERSION env or cli)" + ) + + if "all" in args.version: + args.version = ["us", "pspeu", "hd"] + + if args.remove: + remove_overlay(args.overlay, args.version) + else: + # always build us first + if "us" in args.version: + # rchi has data values that get interpreted as global symbols, so those symbols need to be defined for the linker + if args.overlay == "rchi": + add_undefined_symbol("us", "PadRead", 0x80015288) + extract(args, "us") + if "pspeu" in args.version: + # bo4 and rbo5 have data values that get interpreted as global symbols, so those symbols need to be defined for the linker + if args.overlay == "bo4" or args.overlay == "rbo5": + add_undefined_symbol("pspeu", "g_Clut", 0x091F5DF8) + extract(args, "pspeu") + if "hd" in args.version: + extract(args, "hd") diff --git a/tools/extract_overlay/header.c.mako b/tools/extract_overlay/header.c.mako new file mode 100644 index 0000000000..e508981041 --- /dev/null +++ b/tools/extract_overlay/header.c.mako @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +#include "${ovl_include_path}" +% if header_syms[7] != "NULL": +% if ovl_type == "stage": +#include "../pfn_entity_update.h" +% elif ovl_type == "boss": +#include "../../st/pfn_entity_update.h" +% endif +% endif +<% +if len(header_syms) <= 12: + header_type = "AbbreviatedOverlay" +elif len(header_syms) == 13 or len(header_syms) == 14: + header_type = "AbbreviatedOverlay2" +else: + header_type = "Overlay" +%> +% if ovl_type == "stage" or ovl_type == "boss": +// common +extern RoomHeader OVL_EXPORT(rooms)[]; +extern SpriteParts* OVL_EXPORT(spriteBanks)[]; +extern u_long* OVL_EXPORT(cluts)[]; +extern RoomDef OVL_EXPORT(rooms_layers)[]; +extern GfxBank* OVL_EXPORT(gfxBanks)[]; +void UpdateStageEntities(void); +% for header_sym in set(header_syms): +% if header_sym not in common_syms: +// overlay +u8* ${header_sym}[]; +%endif +%endfor + +${header_type} OVL_EXPORT(Overlay) = { + .Update = ${header_syms[0]}, + .HitDetection = ${header_syms[1]}, + .UpdateRoomPosition = ${header_syms[2]}, + .InitRoomEntities = ${header_syms[3]}, + .rooms = ${header_syms[4]}, + .spriteBanks = ${header_syms[5]}, + .cluts = ${header_syms[6]}, +% if header_syms[7] == "NULL": + .objLayoutHorizontal = NULL, +% else: + .objLayoutHorizontal = &OBJ_LAYOUT_HORIZONTAL, +% endif + .tileLayers = ${header_syms[8]}, + .gfxBanks = ${header_syms[9]}, + .UpdateStageEntities = ${header_syms[10]}, +% if len(header_syms) > 12: + .unk2C = ${header_syms[11]}, + .unk30 = ${header_syms[12]}, +% endif +% if len(header_syms) > 15: + .unk34 = ${header_syms[13]}, + .unk38 = ${header_syms[14]}, + .StageEndCutScene = ${header_syms[15]}, +% endif +}; +% endif diff --git a/tools/extract_overlay/ovl.h.mako b/tools/extract_overlay/ovl.h.mako new file mode 100644 index 0000000000..35c11da419 --- /dev/null +++ b/tools/extract_overlay/ovl.h.mako @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +#ifndef ${ovl_name.upper()}_H +#define ${ovl_name.upper()}_H + +#include + +#define OVL_EXPORT(x) ${ovl_name.upper()}_##x +#define STAGE_IS_${ovl_name.upper()} + +enum Palettes { + PAL_NONE, +}; +% if entity_updates: +<% +maxlen = max(len(e_id) for function, e_id in entity_updates) + 1 +%> +enum Entities { + E_NONE, +% for function, e_id in entity_updates: + ${(e_id + ",").ljust(maxlen)} // ${function} +% endfor + NUM_ENTITIES, +}; +% endif + +#endif // ${ovl_name.upper()}_H \ No newline at end of file diff --git a/tools/extract_overlay/segments.yaml b/tools/extract_overlay/segments.yaml new file mode 100644 index 0000000000..b7f49a4ed9 --- /dev/null +++ b/tools/extract_overlay/segments.yaml @@ -0,0 +1,977 @@ +# The functions key is the only required key +# If no start functions are specified, the first item in functions will be used +# Start functions can be included in functions, but aren't required to be +# If an end function is specified, the segment will include the start, the end, and everything in between +# GetLang for pspeu is a special case where it starts the segment, but should not be listed as a start +# because it has the possibility of starting multiple different segments. +# It should only be included as a function in the segment and will be used as a start if it is found. +#example_segment_name: +# start: +# - Optional +# - Segment +# - Start +# - Functions +# functions: +# - All +# - Segment +# - Functions +# end: OptionalEndFunction +e_armor_lord_guardian: + functions: + - func_us_801D1184 + - func_us_801D1388 + - EntityArmorLordFireWave + - func_us_801D1A94 + - func_us_801D1A9C + - func_us_801D1DAC + - EntityArmorLord + - func_us_801D348C + - func_us_801D3700 +e_axe_knight: + functions: + - AxeKnightUnkFunc1 + - func_801C4198 + - func_801C4550 + - EntityAxeKnight + - EntityAxeKnightRotateAxe + - EntityAxeKnightThrowingAxe +e_background_bushes_trees: + functions: + - EntityBackgroundBushes + - EntityBackgroundTrees +e_background_clock_pendulum: + functions: + - func_us_801C2A34 # animation and sound + - func_us_801C2B24 # sound only +e_background_clouds: + functions: + - EntityBackgroundClouds +e_background_house: + functions: + - DrawFacade + - DrawSides + - DrawRoof + - Entity3DHouseSpawner + - Entity3DBackgroundHouse +e_background_lightning: + functions: + - EntityBackgroundLightning +e_background_pillars: + functions: + - func_us_801CC750 + - func_us_801CC8F8 + - func_us_801CC9B4 +e_bat: + functions: + - EntityBat +e_bell: + functions: + - EntityBell + - EntityBellHelper +e_blade: + functions: + - func_801D0A00 + - func_801D0B40 + - func_801D0B78 + - EntityBlade + - EntityBladeWeapon +e_blood_skeleton: + functions: + - EntityBloodSkeleton +e_bloody_zombie: + functions: + - EntityBloodSplatter + - func_801C53AC + - EntityBloodyZombie + - EntityBloodDrips +e_blue_axe_knight: + functions: + - AxeKnightUnkFunc1 + - func_us_801CA51C + - func_us_801C8D98 + - func_us_801CAD20 + - func_us_801C959C + - EntityAxeKnightBlue + - EntityAxeKnightRotateAxe + - EntityAxeKnightThrowingAxe + - func_us_801CBA30 +e_blue_venus_weed: + functions: + - SetupPrimsForEntitySpriteParts + - EntityVenusWeed + - EntityVenusWeedFlower + - func_us_801C5850 # rno3 + - EntityVenusWeedTendril + - func_us_801C5F40 # rno3 + - EntityVenusWeedDart + - func_us_801D7940 # rno4 + - EntityVenusWeedSpike + - func_us_801D8030 # rno4 + ovl: + - rno3 + - rno4 +e_bone_archer: + functions: + - EntityBoneArcher + - EntityBoneArcherArrow +e_bone_musket: + functions: + - EntityBoneMusket + - func_us_801CF298 +e_bone_scimitar: + functions: + - BoneScimitarAttackCheck + - EntityBoneScimitar + - EntityBoneScimitarParts + - EntityBoneHalberdParts +e_bossfight: + functions: + - EntityBossFightManager + - EntityBossRoomBlock +e_breakable: + functions: + - ${prefix}_EntityBreakable + - EntityBreakableHelper + - ${prefix}_EntityBreakableDebris + - EntityBreakableDebris + - func_us_801C123C +e_breakable_wall: + functions: + - EntityBreakableWallDebris + - EntityBreakableWall +e_candle_table: + functions: + - EntityCandleTable +e_castle_door: + functions: + - EntityCastleDoor +e_chair: + functions: + - ChairSitCheck + - func_us_801BEDD8 + - EntityChair + - func_us_801B81E8 +e_cloaked_knight: + functions: + - StepTowards + - EntityCloakedKnight + - EntityCloakedKnightCloak + - EntityCloakedKnightAura + - EntityCloakedKnightSword + ovl: + - nz1 + - rnz1 +e_clock_room: + start: + - func_us_801CCAAC + - EntityClockRoomController + functions: + - func_us_801CCAAC + - UpdateBirdcages + - UpdateClockHands + - EntityClockRoomController + - EntityClockHands + - EntityBirdcageDoor + - UpdateStatueTiles + - EntityStatue + - EntityStatueGear + - UpdateStoneDoorTiles + - EntityStoneDoor + - EntityClockRoomUnused +e_clock_tower: + functions: + - EntityClockTower3D +e_clouds: + functions: + - EntityClouds +e_collect: + start: + - func_us_0923C2F8 + - BlitChar + - PrizeDropFall + functions: + - func_us_0923C2F8 + - func_us_0923C390 + - BlitChar + - PrizeDropFall + - PrizeDropFall2 + - CollectHeart + - CollectGold + - CollectSubweapon + - CollectHeartVessel + - CollectLifeVessel + - CollectDummy + - func_801939C4 + - EntityPrizeDrop + - EntityExplosion + - BlinkItem + - Unreferenced_MAD_ST0_func + - EntityEquipItemDrop + - EntityRelicOrb + - EntityHeartDrop + - EntityMessageBox +e_ctulhu: + functions: + - EntityCtulhu + - EntityCtulhuFireball + - EntityCtulhuIceShockwave + - EntityCtulhuDeath +e_cutscene_actors: + start: + - func_us_801A8FC0 + - CutsceneCameraPan + functions: + - CutsceneCameraPan + - ${prefix}_EntityCutsceneStage + - ${prefix}_EntityCutsceneMaria + - func_us_801A8FC0 + - func_801961DC + - func_us_801A9084 + - func_us_801A9208 + - func_us_801A95F4 + - func_us_801A9944 +e_cutscene_dialogue: + start: + - DrawCutsceneActorName + - CutsceneUnk1 + functions: + - GetLang + - CutsceneUnk1 + - SetCutsceneScript + - CutsceneUnk3 + - CutsceneUnk4 + - DrawCutsceneActorName + - SetCutsceneEnd + - CutsceneRun + - CutsceneSkip + - ScaleCutsceneAvatar + - ${prefix}_EntityCutscene + - ${prefix}_EntityCutsceneDialogue + - func_us_801A7DC0 + - func_us_801A6EF8 + - func_us_801A7340 + - func_us_801A75B4 + - func_us_8018C90C +e_dai_no2_shortcut: + functions: + - EntityBlock + - EntityStatue +e_debug_cerberus_gate: + functions: + - EntityCerberusGateDebug +e_demon_switch_wall: + functions: + - UpdateFallingPebble + - EntityDemonSwitch + - EntityDemonSwitchWall +e_dracula: + functions: + - func_801ABBBC + - func_801AC458 + - EntityDracula + - EntityDraculaBody + - EntityDraculaFireball + - EntityDraculaMeteorball + - func_801AD838 + - EntityDraculaGlass +e_dracula_final: + functions: + - func_801ADAC8 + - func_801ADB10 + - EntityDraculaFinalForm + - EntityDraculaMegaFireball + - EntityDraculaRainAttack + - func_801AF380 + - func_801AF6D0 + - func_801AF774 +e_elevator: + start: + - func_us_801C1F98 # no0 + - func_us_8019FD4C # rcen + - func_us_801B6654 # rno0 + functions: + - func_us_801C1F98 + - func_us_8019FD4C + - func_us_801B6654 + - func_us_801C2044 + - func_us_801C2184 + - EntityUnkId1B + - func_us_801C27A4 # no0 +e_explosion_puff_opaque: + functions: + - CreateExplosionPuff + - EntityExplosionPuffOpaque +e_falling_stairs: + functions: + - UpdateDustParticles + - EntityFallingStairs + - EntityFallingStep +e_fire_warg: + functions: + - func_801CC5A4 + - func_801CC6F8 + - func_801CC820 + - func_801CC90C + - EntityFireWarg + - EntityUnkId30 + - EntityUnkId31 + - EntityExplosion3 + - func_801CE740 + - EntityFireWargWaveAttack + - EntityUnkId2F + - EntityFireWargDeathBeams +e_flea_armor: + functions: + - EntityFleaArmor + - EntityFleaArmorAttackHitbox +e_flea_man: + functions: + - CheckFieldCollisionY + - UpdateFacingDirection + - EntityFleaMan +e_gaibon: + functions: + - EntityGaibon + - EntityGaibonLeg + - func_801B69E8 + - EntitySmallGaibonProjectile + - EntityLargeGaibonProjectile +e_gorgon: + functions: + - func_801CD78C # rno0 func_801CD78C_801CEB40 + - func_us_801D2424 # are + - func_us_801CEEB4 # rno0 + - func_us_801CF08C # rno0 + - func_us_801CF24C # rno0 + - func_us_801CF380 # rno0 + - StepTowards + - func_us_801CF64C # rno0 + - func_us_801CF7D0 # rno0 +e_grave_keeper: + functions: + - SpawnDustParticles + - func_us_801D12E0 + - EntityGraveKeeper + - EntityGraveKeeperHitbox +e_gremlin: + functions: + - EntityGremlin + - EntityGremlinEffect + - EntityGremlinFire +e_gurkha: + functions: + - func_801CF778 + - func_801CF7A0 + - EntityGurkha + - EntityGurkhaWeapon +e_hammer: + start: + - func_801CE4CC + - EntityHammer + functions: + - func_801CE4CC + - EntityHammer + - EntityGurkhaBodyParts + - EntityHammerWeapon +e_harpy: + functions: + - EntityHarpy + - EntityHarpyDagger + - EntityHarpyFlame + - EntityHarpyKick + - EntityHarpyFeather +e_heartroom: + functions: + - EntityHeartRoomSwitch + - EntityHeartRoomGoldDoor +e_hunting_girl: + functions: + - HuntingGirlDrawAttack + - HuntingGirlDrawSpirit + - HuntingGirlTransitionToStep + - EntityHuntingGirl + - EntityHuntingGirlAttack +e_jack_o_bones: + functions: + - func_us_801C8100 # rno0 jack o move + - func_us_801C81A4 # rno0 EntityJackOBones + - func_us_801C86B4 # rno0 jack o death + - func_us_801C87B0 # rno0 jack o projectile +e_jewel_sword_puzzle: + functions: + - EntityMermanRockLeftSide + - EntityMermanRockRightSide + - EntityJewelSwordDoor + - EntityFallingRock2 +e_killer_fish: + functions: + - EntityKillerFish + - EntityKillerFishDeathPuff + - EntityWargExplosionPuffTransparent +e_lesser_demon: + start: + - func_us_801BB8DC + - func_801966B0 + functions: + - func_us_801BB8DC + - func_801966B0 + - func_us_801BBAB4 + - EntityLesserDemonSpit + - func_us_801BC28C + - func_us_801BC57C + - func_us_801BC814 + - func_us_801BCC10 + - func_us_801BCFD4 + - func_us_801BD268 + - func_us_801BDA34 + - EntityLesserDemon + - func_us_801BED48 + - func_us_801BF7B0 # rnz0 +e_life_up: + functions: + - EntityLifeUpSpawn +e_lock_camera: + start: + - PlayerIsWithinHitbox + - ${prefix}_EntityLockCamera + - EntityLockCamera + functions: + - PlayerIsWithinHitbox + - ${prefix}_EntityLockCamera + - EntityLockCamera +e_maria: + functions: + - func_801B8E0C + - EntityMariaNZ0 +e_medusa: + functions: + - func_us_80191438 + - EntityMedusa + - func_us_80192020 + - func_us_801922EC + - func_us_801923DC + - func_us_80192998 + - func_us_80192B38 +e_medusa_head: + functions: + - EntityMedusaHeadSpawner + - EntityMedusaHeadYellow + - EntityMedusaHeadBlue +e_merman: + functions: + - EntityMerman + - EntityMermanFireball + - EntityMermanJumpAir + - EntityMermanExplosion +e_merman2: + functions: + - CheckMermanEnteringWater + - EntityMerman2 + - EntityMermanFireSpit + - EntityMediumWaterSplash + - EntityMermanWaterSplash + - EntityMerman2JumpAir + - EntityHighWaterSplash + - EntityDeadMerman + - EntityMermanSpawner +e_misc: + functions: + - CheckColliderOffsets + - EntityUnkId13 + - EntityExplosionVariantsSpawner + - EntityGreyPuffSpawner + - EntityExplosionVariants + - EntityGreyPuff + - EntityOlroxDrool + - UnkCollisionFunc5 + - UnkCollisionFunc4 + - EntityIntenseExplosion + - InitializeUnkEntity + - MakeEntityFromId + - MakeExplosions + - EntityBigRedFireball + - UnkRecursivePrimFunc1 + - UnkRecursivePrimFunc2 + - ClutLerp + - PlaySfxPositional + - func_us_801C55AC +e_mist_door: + start: + - EntityMistDoor + functions: + - GetLang + - EntityMistDoor +e_nova_skeleton: + functions: + - func_us_801C8A74 # rno0 nova skeleton move + - func_us_801C8B14 # rno0 beam ring burst + - func_us_801C8D44 # rno0 EntityNovaSkeleton + - EntityBladeSoldierDeathParts + - func_us_801C9324 # rno0 beam attack + - func_us_801C97D4 # rno0 beam attack pulse +e_owl_knight: + functions: + - EntityOwl + - OwlKnightDeathAnim + - EntityOwlKnight + - EntityOwlKnightSword + - EntityOwlTarget +e_particles: + start: + - EntitySoulStealOrb + - func_us_0923AD68 + functions: + - EntitySoulStealOrb + - func_us_0923AD68 + - EntityEnemyBlood +e_plate_lord: + functions: + - func_801CD78C + - func_us_801D2424 + - func_us_801D26CC + - func_us_801D274C + - func_us_801D27C4 + - StepTowards + - EntityPlateLord + - func_us_801D4324 + - func_us_801D44A0 + - func_us_801D4AA4 + - func_us_801D4CAC + ovl: + - are + - no0 +e_red_door: + functions: + - EntityIsNearPlayer + - ${prefix}_EntityRedDoor +e_richter: + functions: + - func_us_801B4BD0 + - func_us_801B4EAC + - ${prefix}_CheckBladeDashInput + - ${prefix}_CheckHighJumpInput + - ${prefix}_RicMain + - func_us_801B5A14 + - RichterThinking + - func_us_801B6998 + - EntityRichter + - ${prefix}_RicStepStand + - ${prefix}_RicStepWalk + - ${prefix}_RicStepRun + - ${prefix}_RicStepJump + - ${prefix}_RicStepFall + - ${prefix}_RicStepCrouch + - ${prefix}_RicResetPose + - func_us_801B77D8 + - ${prefix}_RicStepHit + - ${prefix}_RicStepDead + - ${prefix}_RicStepStandInAir + - ${prefix}_RicStepEnableFlameWhip + - ${prefix}_RicStepHydrostorm + - ${prefix}_RicStepGenericSubwpnCrash + - ${prefix}_RicStepThrowDaggers + - ${prefix}_RicStepSlide + - ${prefix}_RicStepSlideKick + - ${prefix}_RicStepBladeDash + - func_us_801B8E80 + - ${prefix}_RicStepHighJump +e_richter_fight_trigger: + functions: + - func_us_801A9250 +e_room_bg: + functions: + - ${prefix}_EntityBackgroundBlock +e_room_fg: + functions: + - EntityRoomForeground +e_room_fg_2: + functions: + - EntityCastleWall1 + - EntityCastleWall2 + - EntityStaircase +e_salem_witch: + functions: + - SalemWitchTrySpawnShadow + - EntitySalemWitch + - EntitySalemWitchGlow + - EntitySalemWitchCurse + - EntitySalemWitchTriboltLaunch + - EntitySalemWitchTriboltProjectile + ovl: rnz0 +e_sealed_door: + start: + - SealedDoorIsNearPlayer + functions: + - GetLang + - EntitySealedDoor +e_shutting_window: + functions: + - EntityShuttingWindow +e_skelerang: + functions: + - EntitySkelerang + - EntitySkelerangBoomerang + - EntitySkelerangUnknown +e_skeleton: + functions: + - SkeletonAttackCheck + - EntitySkeleton + - EntitySkeletonPieces + - EntitySkeletonThrownBone + - UnusedSkeletonEntity + - ${prefix}_Unused +e_skeleton_ape: + functions: + - EntitySkeletonApe + - EntitySkeletonApePunch + - SkeletonApeBarrelHelper + - EntitySkeletonApeBarrel +e_skull_lord: + functions: + - EntitySkullLord + - EntitySkullLordOutline + - EntitySkullLordFlames + - EntitySkullLordPieces +e_sky_entities: + functions: + - EntityLightningThunder + - EntityLightningCloud +e_spear_guard: + functions: + - func_us_801D37A4 + - func_us_801D38E4 + - func_us_801D3918 + - EntitySpearGuard + - EntitySpearGuardBlock + - EntityThrownSpear +e_slogra: + start: + - EntitySlograSpecialCollision + - EntitySlogra + functions: + - EntitySlograSpecialCollision + - EntitySlogra + - EntitySlograSpear + - EntitySlograSpearProjectile +e_spectral_sword: + functions: + - StepTowards + - EntitySpectralSword + - func_us_801C0C44 # rdai + - EntitySpectralSwordAura + - func_us_801CEB08 + - EntitySpectralSwordWeapon + - func_us_801C17E8 # rdai + - EntityPoltergeist + ovl: rdai +e_spike_room: + functions: + - IncreaseBrightness + - DecreaseBrightness + - EntitySpikeRoomDarkness +e_spikes: + functions: + - EntitySpikesDust + - EntitySpikesParts + - SpikesBreak + - SpikesApplyDamage + - EntitySpikes + - EntitySpikesDamage +e_stage_name: + start: + - StageNamePopupDissolver + - LoadStageNameGraphics + - EntityStageNamePopup + - func_us_0923C0C0 + functions: + - StageNamePopupDissolver + - LoadStageNameGraphics + - EntityStageNamePopup + - func_us_0923C0C0 +e_stained_glass: + functions: + - StainedGlassBlendPalette + - StainedGlassRecurseDepth + - EntityStainedGlass + - EntityStainedGlassBackground +e_stairway: + functions: + - EntityStairwayPiece + - EntityFallingRock +e_stone_skull: + functions: + - func_us_801C7F24 # rno0 +e_subweapon_container: + functions: + - EntitySubWeaponContainer + - EntitySubWpnContGlass + - func_801C7654 + - func_801C77B8 + - func_801C7884 +e_succubus: + functions: + - EntitySuccubus + - EntitySuccubusPetal + - EntitySuccubusWingOverlay + - EntitySuccubusClone + - EntityPinkBallProjectile + - EntitySuccubusWingSpike + - EntitySuccubusWingSpikeTip +e_sword_lord: + start: + - func_us_801CF57C + - func_us_801BC9BC + functions: + - func_us_801CF57C + - func_us_801BC9BC + - EntitySwordLord + - func_us_801D04B8 + - EntitySwordLordAttackHitbox +e_thornweed_corpseweed: + functions: + - EntityThornweed + - EntityCorpseweed + - EntityCorpseweedProjectile +e_tilemap_shuffler: + functions: + - EntityTilemapShufflerUnused +e_tombstone: + functions: + - EntityTombstone +e_transparent_water: + functions: + - EntityTransparentWater +e_trapdoor: + functions: + - EntityTrapDoor +e_valhalla_knight: + functions: + - EntityValhallaKnight + - func_us_801C8954 + - func_us_801C8AAC +e_werewolf: + functions: + - func_us_801B7290 # rare EntityWerewolf + - EntityCornerGuardAttack # werewolf punch attack hitbox + - func_us_801B7FF8 # rare blue energy attack + - func_us_801B80E8 # rare somersault trail + - func_us_801B81CC # rare death fire pillar +e_wereskeleton: + functions: + - func_us_801CDDD8 + - EntityWereskeleton + - func_us_801CE958 + - func_us_801CEA2C +e_venus_weed: + functions: + - SetupPrimsForEntitySpriteParts + - EntityVenusWeed + - EntityVenusWeedFlower + - EntityVenusWeedTendril + - EntityVenusWeedDart + - EntityVenusWeedSpike + ovl: chi +e_zombie: + functions: + - EntityZombie + - EntityZombieSpawner +e_warg: + functions: + - func_801CF438 + - EntityWargExplosionPuffTransparent +e_warp_room: + start: + - EntityWarpRoom + - EntityRWarpRoom + functions: + - EntityWarpRoom + - EntityRWarpRoom + - EntityWarpSmallRocks +e_water_effects: + functions: + - func_801C4144 + - EntityAlucardWaterEffect + - EntitySplashWater + - EntitySurfacingWater + - EntitySideWaterSplash + - EntitySmallWaterDrop + - EntityWaterDrop +collision: + start: + - HitDetection + - ${prefix}_HitDetection + functions: + - HitDetection + - ${prefix}_HitDetection + - EntityDamageDisplay +create_entity: + functions: + - CreateEntityFromLayout + - CreateEntityWhenInVerticalRange + - CreateEntityWhenInHorizontalRange + - FindFirstEntityToTheRight + - FindFirstEntityToTheLeft + - CreateEntitiesToTheRight + - CreateEntitiesToTheLeft + - FindFirstEntityAbove + - FindFirstEntityBelow + - CreateEntitiesAbove + - CreateEntitiesBelow + - InitRoomEntities + - UpdateRoomPosition + - CreateEntityFromCurrentEntity + - CreateEntityFromEntity +doors: + functions: + - EntityUnkId18 + - EntityUnkId17 + - EntityUnkId1C + - EntityUnkId19 + - EntityUnkId1A + - func_us_801B5004 + - EntityUnkId1E +dop_anim: + functions: + - SetDopplegangerStep + - ${prefix}_InitPlayerAfterImage + - ${prefix}_DrawPlayerAfterImage + - ${prefix}_func_8010DA2C + - SetDopplegangerAnim + - ${prefix}_func_8010DA70 + - ${prefix}_UpdateUnarmedAnim + - ${prefix}_PlayAnimation + - ${prefix}_UpdateAnim +doppleganger: + start: + - func_us_801C1A38 + - func_us_801C096C + functions: + - func_us_801C1A38 + - func_us_801C096C + - ${prefix}_CheckStageCollision + - EntityDoppleganger10 + - EntityDoppleganger40 + - func_us_801C1DB0 + - func_us_801C1DC8 + - DopplegangerThinking + - EntityUnkId16 + - ${prefix}_CheckFloor + - ${prefix}_CheckCeiling + - ${prefix}_CheckWallRight + - ${prefix}_CheckWallLeft +entrance_weights: + functions: + - UpdateWeightChains + - EntityWeightsSwitch + - EntityPathBlockSmallWeight + - EntityPathBlockTallWeight + - func_us_801B2578 # rno3 +giantbro_helpers: + functions: + - func_801CD658 + - func_801CD734 + - func_801CD78C + - polarPlacePart + - func_801CD91C + - func_801CDA14 + - func_801CDA6C + - func_801CDAC8 + - func_801CDC80 + - func_801CDD00 + - func_801CDD80 + - func_801CDE10 + - polarPlacePartsWithAngvel + - func_801CDF1C + - func_801CDFD8 + - func_801CE04C + - func_801CE120 + - func_801CE1E8 + - func_801CE228 + - polarPlacePartsList + - func_801CE2CC + - func_801CE3FC +popup: + functions: + - BottomCornerText +prim_helpers: + functions: + - UnkPrimHelper + - UpdateAnimation + - FindFirstUnkPrim + - FindFirstUnkPrim2 + - PrimToggleVisibility + - PrimResetNext + - UnkPolyFunc2 + - UnkPolyFunc0 + - PrimDecreaseBrightness + - func_us_801ABD24 +prologue_scroll: + functions: + - SetGameState + - InitClutIndices + - ClearScreen + - SetDisplayBufferColorsToBlack + - SetStageCinematicBorders + - SetStageDisplayBuffer + - SetTitleDisplayBuffer + - IsSkipRequested + - PrologueScroll +servant_common: + functions: + - ServantUpdateAnim + - ServantUnk0 +st_common: + functions: + - DestroyEntity + - DestroyEntitiesFromIndex + - PreventEntityFromRespawning + - AnimateEntity + - UnkAnimFunc + - GetDistanceToPlayerX + - GetDistanceToPlayerY + - GetSideToPlayer + - MoveEntity + - FallEntity + - func_8019214C + - UnkCollisionFunc3 + - func_80192408 + - UnkCollisionFunc2 + - AllocEntity + - GetSineScaled + - GetSine + - SetEntityVelocityFromAngle + - Ratan2Shifted + - GetAngleBetweenEntitiesShifted + - GetAnglePointToEntityShifted + - AdjustValueWithinThreshold + - UnkEntityFunc0 + - Ratan2 + - GetAngleBetweenEntities + - GetAnglePointToEntity + - GetNormalizedAngle + - SetStep + - SetSubStep + - EntityExplosionSpawn + - InitializeEntity + - EntityDummy + - UnkCollisionFunc + - CheckFieldCollision + - GetPlayerCollisionWith + - ReplaceBreakableWithItemDrop +st_init: + start: + - GetLangAt + - InitEntityIds + functions: + - GetLangAt + - InitEntityIds + - ${prefix}_Load +st_update: + functions: + - Random + - Update + - UpdateStageEntities +title_card: + functions: + - EntityStageTitleFadeout + - EntityStageTitleCard diff --git a/tools/extract_overlay/splat.yaml.mako b/tools/extract_overlay/splat.yaml.mako new file mode 100644 index 0000000000..ad7ac0a0e8 --- /dev/null +++ b/tools/extract_overlay/splat.yaml.mako @@ -0,0 +1,57 @@ +# This template file is not yet used +options: + platform: ${platform} + basename: ${basename} + base_path: .. + build_path: ${build_path} + target_path: disks/pspeu/PSP_GAME/USRDIR/res/ps/PSPBIN/dai.bin + asm_path: asm/pspeu/st/dai_psp + asset_path: assets/st/dai + src_path: src/st + ld_script_path: build/pspeu/stdai.ld + compiler: GCC + symbol_addrs_path: + - config/symbols.pspeu.txt + - config/symbols.pspeu.stdai.txt + undefined_funcs_auto_path: config/undefined_funcs_auto.pspeu.stdai.txt + undefined_syms_auto_path: config/undefined_syms_auto.pspeu.stdai.txt + find_file_boundaries: true + use_legacy_include_asm: false + migrate_rodata_to_functions: true + asm_jtbl_label_macro: jlabel + symbol_name_format: pspeu_$VRAM + nonmatchings_path: nonmatchings + disassemble_all: true + section_order: + - .text + - .data + - .rodata + - .bss + ld_bss_is_noload: true + disasm_unknown: true + global_vram_start: 0x08000000 + ld_generate_symbol_per_data_segment: true + # psp only + asm_inc_header: | + .set noat /* allow manual use of $at */ + .set noreorder /* don't insert nops after branches */ +sha1: 91f0ea1eaff4cf7a3773ee63cf516ef167bf73e3 +segments: +# psp only + - [0x0, bin, mwo_header] + - name: stdai + type: code + start: 0x80 + vram: 0x09237700 + # Only present for psp, None will be passed for psx + bss_start_address: 0x09297000 + # Only present for psp, None will be passed for psx + bss_size: 0x900 + align: 128 + subalign: 8 + subsegments: + - [0x80, c, dai_psp/first_dai] + - [0x23780, data] + - [0x5F680, .rodata, dai_psp/first_dai] + - [0x5F980, bss] + - [0x60280] diff --git a/tools/extract_overlay/symexport.txt.mako b/tools/extract_overlay/symexport.txt.mako new file mode 100644 index 0000000000..5027413810 --- /dev/null +++ b/tools/extract_overlay/symexport.txt.mako @@ -0,0 +1,4 @@ +EXTERN(_binary_assets_${path_prefix}${ovl_name}_mwo_header_bin_start); +%if ovl_load_name: +EXTERN(${ovl_load_name}); +%endif diff --git a/tools/sotn_utils b/tools/sotn_utils new file mode 160000 index 0000000000..49cfad3b82 --- /dev/null +++ b/tools/sotn_utils @@ -0,0 +1 @@ +Subproject commit 49cfad3b82d756a4ea9278d0f117d4829ad18a46