summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/bmg.py115
-rw-r--r--tools/requirements.txt1
-rw-r--r--tools/vtable_sym.py129
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()