mirror of
https://github.com/zeldaret/ph
synced 2026-07-05 12:53:41 -04:00
ActorShopItem 98% (#151)
* ActorShopItem 93% * Fix build * Add missing symbols to usa * Document BMG message ID functions * Create bmg.py for inspecting BMG files * ActorShopItem 98% * Match func_ov031_0217dfec * Port reloc changes to usa * Make `ModelRender::GetLcdcAddress` non-const
This commit is contained in:
+115
@@ -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()
|
||||
@@ -1,3 +1,4 @@
|
||||
ndspy
|
||||
pre-commit
|
||||
pyperclip
|
||||
requests
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user