diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/bmg.py | 115 | ||||
| -rw-r--r-- | tools/requirements.txt | 1 | ||||
| -rw-r--r-- | tools/vtable_sym.py | 129 |
3 files changed, 245 insertions, 0 deletions
diff --git a/tools/bmg.py b/tools/bmg.py new file mode 100644 index 00000000..d3046ed1 --- /dev/null +++ b/tools/bmg.py @@ -0,0 +1,115 @@ +from pathlib import Path +from argparse import ArgumentParser + +import ndspy.bmg + +def main(): + parser = ArgumentParser(description="View strings in BMG files") + parser.add_argument("--file", help="Path to the BMG file. If not provided, the file will be derived from the message ID.") + parser.add_argument("--language", help="Language of the BMG file. Does nothing if --file is provided.") + parser.add_argument("--version", help="Game version to use. Does nothing if --file is provided.") + parser.add_argument("id", help="Index of the BMG entry") + args = parser.parse_args() + + if args.id.startswith("0x"): + msg_id = int(args.id, 16) + else: + msg_id = int(args.id) + bmg_file = get_bmg_file(args.file, msg_id, args.language, args.version) + + with bmg_file.open("rb") as f: + data = f.read() + bmg = ndspy.bmg.BMG(data) + message = bmg.messages[msg_id & 0xffff] + + for part in message.stringParts: + print(part, end="") + print() + +BMG_FILENAMES = [ + "system", + "regular", + "battle", + "test", + "default", + "sea", + "kaitei", + "main_isl", + "brave", + "flame", + "wind", + "frost", + "power", + "wisdom", + "ghost", + "hidari", + "sennin", + "ship", + "collect", + "mainselect", + "field", + "wisdom_dngn", + "demo", + "battleCommon", + "bossLast1", + "bossLast3", + "torii", + "myou", + "kojima1", + "kojima2", + "kojima5", + "kojima3", + "staff", + "kaitei_F", +] + +LANGUAGES = [ + "English", + "French", + "German", + "Italian", + "Spanish", + "Japanese", +] + +def get_bmg_file(file: str | None, msg_id: int, language: str | None, version: str | None) -> Path: + if file is not None: + return Path(file) + + versions = find_available_versions() + if len(versions) == 0: + print("You must extract the game files before using this tool") + exit(1) + if version is None: + version = versions[0] + if version not in versions: + print(f"Version {version} not found in the extract directory") + exit(1) + + files_dir = Path(__file__).parent.parent / "extract" / version / "files" + if language is None: + for lang in LANGUAGES: + lang_dir = files_dir / lang + if lang_dir.exists(): + language = lang + break + if language is None: + print("No language directories found in the extracted assets") + exit(1) + + file_index = msg_id >> 16 + if file_index >= len(BMG_FILENAMES): + print(f"Message ID {msg_id} is out of range") + exit(1) + filename = BMG_FILENAMES[file_index] + ".bmg" + + return files_dir / language / "Message" / filename + + +def find_available_versions() -> list[str]: + extract_path = Path(__file__).parent.parent / "extract" + return [d.name for d in extract_path.iterdir() if d.is_dir()] + + +if __name__ == "__main__": + main() diff --git a/tools/requirements.txt b/tools/requirements.txt index 4682190e..b0f58dc2 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,3 +1,4 @@ +ndspy pre-commit pyperclip requests diff --git a/tools/vtable_sym.py b/tools/vtable_sym.py new file mode 100644 index 00000000..826cd610 --- /dev/null +++ b/tools/vtable_sym.py @@ -0,0 +1,129 @@ +import argparse +from pathlib import Path +import re + +def main(): + parser = argparse.ArgumentParser(description='Define vtable symbols and update relocations') + parser.add_argument('old_name', help='The old name of the vtable symbol') + parser.add_argument('new_name', help='The new name of the vtable symbol') + parser.add_argument('--dry', action='store_true', help='Print the changes without writing to files') + args = parser.parse_args() + + old_name: str = args.old_name + new_name: str = args.new_name + dry_run: bool = args.dry + + file_write_buffer: list[tuple[Path, list[str]]] = [] + manual_changes: list[str] = [] + + current_path = Path(__file__).parent + root_path = current_path.parent + base_config_path = root_path / "config" + for config_path in base_config_path.iterdir(): + if config_path.is_file(): + continue + + old_address = None + new_address = None + dest_module = None + for symbol_file in config_path.glob("**/symbols.txt"): + with symbol_file.open("r") as f: + lines = f.readlines() + + for row, line in enumerate(lines): + if not line.startswith(old_name): + continue + print(f"Updating symbol {old_name} in {symbol_file}:{row + 1}") + address = get_attr_value(line, "addr") + if address is None: + print(f"Error: Could not find symbol address at {symbol_file}:{row + 1}") + exit(1) + address = int(address, 16) + old_address = address + new_address = address - 8 + line = line.replace(old_name, new_name, 1) + line = set_attr_value(line, "addr", f"0x{new_address:08x}") + print(f"-> {line}") + lines[row] = line + + if old_address is None or new_address is None: + # Try next symbols.txt file + continue + + file_name = str(symbol_file.relative_to(config_path)) + if file_name.endswith("dtcm/symbols.txt"): + dest_module = ("dtcm", 0) + elif file_name.endswith("itcm/symbols.txt"): + dest_module = ("itcm", 0) + elif file_name.endswith("arm9/symbols.txt"): + dest_module = ("main", 0) + else: + overlay_id = re.search(r"ov(\d+)/symbols.txt", file_name) + if overlay_id is None: + print(f"Error: Could not determine module for {symbol_file}") + exit(1) + dest_module = ("overlay", int(overlay_id.group(1))) + + file_write_buffer.append((symbol_file, lines)) + break + + if old_address is None or new_address is None or dest_module is None: + print(f"Error: Could not find symbol {old_name} in any symbols.txt file in {config_path}") + exit(1) + + for relocs_file in config_path.glob("**/relocs.txt"): + with relocs_file.open("r") as f: + lines = f.readlines() + + any_change = False + for row, line in enumerate(lines): + to_addr = get_attr_value(line, "to") + if to_addr is None: + continue + to_addr = int(to_addr, 16) + if to_addr != old_address: + continue + reloc_module = get_attr_value(line, "module") + if reloc_module is None: + continue + if dest_module[0] == "overlay" and reloc_module.startswith("overlays"): + print(f"Warning: Found ambiguous relocation for {old_name} in {relocs_file}, it will require manual review.") + manual_changes.append(f"{relocs_file}:{row + 1}") + if not reloc_module.startswith(dest_module[0]): + continue + + print(f"Updating relocation for {old_name} in {relocs_file}:{row + 1}") + line = set_attr_value(line, "to", f"0x{new_address:08x}") + line = set_attr_value(line, "add", "0x8") + print(f"-> {line}") + lines[row] = line + any_change = True + + if any_change: + file_write_buffer.append((relocs_file, lines)) + + if not dry_run: + for symbol_file, lines in file_write_buffer: + with symbol_file.open("w") as f: + f.writelines(lines) + print(f"Changes written to {len(file_write_buffer)} files:") + else: + print(f"Dry run complete. {len(file_write_buffer)} files would be updated:") + for symbol_file, _ in file_write_buffer: + print(f"- {symbol_file}") + +def get_attr_value(line: str, attr: str) -> str | None: + match = re.search(rf"{attr}:(\S+)", line) + if match is None: + return None + return match.group(1) + +def set_attr_value(line: str, attr: str, value: str) -> str: + pattern = rf"{attr}:\S+" + if not re.search(pattern, line): + line = line.strip() + f" {attr}:{value}\n" + return line + return re.sub(rf"{attr}:\S+", f"{attr}:{value}", line, count=1) + +if __name__ == "__main__": + main() |
