mirror of
https://github.com/zeldaret/ph
synced 2026-05-23 15:01:37 -04:00
30ca45f694
Fix incorrect progress displayed, bytes didn't add up to the correct total Add option to display progress for each overlay/module
206 lines
7.5 KiB
Python
206 lines
7.5 KiB
Python
import sys
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
import argparse
|
|
|
|
|
|
def eprint(*args, **kwargs):
|
|
print(*args, file=sys.stderr, **kwargs)
|
|
|
|
|
|
def remove_whitespace(s: str):
|
|
return ''.join(s.split())
|
|
|
|
|
|
def get_nonmatching_funcs(source: str):
|
|
# Example:
|
|
# ARM bool NONMATCH(ItemManager::func_ov00_020ad790)(unk32 param1)
|
|
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
funcs = re.findall(r'NONMATCH\W*\(\W*([^\)]+)', source)
|
|
return [remove_whitespace(func) for func in funcs]
|
|
|
|
|
|
def get_all_nonmatching_funcs():
|
|
funcs = []
|
|
for root, dirs, files in os.walk('src'):
|
|
for file in files:
|
|
name, ext = file.rsplit('.', 1)
|
|
if ext not in ['c', 'cpp']: continue
|
|
with open(os.path.join(root, file), 'r') as f:
|
|
source = f.read()
|
|
funcs.extend(get_nonmatching_funcs(source))
|
|
return set(funcs)
|
|
|
|
|
|
class Section:
|
|
def __init__(self, name: str) -> None:
|
|
self.name = name
|
|
# Number of bytes from assembly, C/C++ and nonmatching code
|
|
# In total, they amount to the entire code
|
|
self.bytes_asm = 0
|
|
self.bytes_c_cpp = 0
|
|
self.bytes_nonmatch = 0
|
|
|
|
def total(self):
|
|
return self.bytes_asm + self.bytes_c_cpp + self.bytes_nonmatch
|
|
|
|
def add(self, other):
|
|
self.bytes_asm += other.bytes_asm
|
|
self.bytes_c_cpp += other.bytes_c_cpp
|
|
self.bytes_nonmatch += other.bytes_nonmatch
|
|
|
|
def print(self, indent=0):
|
|
total = self.total()
|
|
percent_asm = 100 * (self.bytes_asm / total)
|
|
percent_c_cpp = 100 * (self.bytes_c_cpp / total)
|
|
percent_nonmatch = 100 * (self.bytes_nonmatch / total)
|
|
indent = ' ' * indent
|
|
|
|
print(f'{indent}{self.name}:')
|
|
print(f'{indent} asm = {self.bytes_asm:7} or {self.bytes_asm:6X}h bytes ({percent_asm:7.3f}%)')
|
|
print(f'{indent} C/C++ = {self.bytes_c_cpp:7} or {self.bytes_c_cpp:6X}h bytes ({percent_c_cpp:7.3f}%)')
|
|
print(f'{indent} nonmatch = {self.bytes_nonmatch:7} or {self.bytes_nonmatch:6X}h bytes ({percent_nonmatch:7.3f}%)')
|
|
print()
|
|
|
|
|
|
section_names = ['.text', '.rodata', '.data', '.bss', '.init']
|
|
class Module:
|
|
def __init__(self, name: str) -> None:
|
|
self.name = name
|
|
self.sections = {name: Section(name) for name in section_names}
|
|
|
|
def total(self):
|
|
total = Section('TOTAL')
|
|
total.bytes_asm = sum(section.bytes_asm for section in self.sections.values())
|
|
total.bytes_c_cpp = sum(section.bytes_c_cpp for section in self.sections.values())
|
|
total.bytes_nonmatch = sum(section.bytes_nonmatch for section in self.sections.values())
|
|
return total
|
|
|
|
def main(args: argparse.Namespace):
|
|
nonmatching_funcs = get_all_nonmatching_funcs()
|
|
|
|
modules: dict[str, Module] = {}
|
|
current_module = None
|
|
|
|
map_path = None
|
|
regions = ['eur', 'usa']
|
|
for region in regions:
|
|
map_path = Path(f'build/{region}/main.bin.xMAP')
|
|
if map_path.is_file(): break
|
|
if map_path is None:
|
|
eprint('You must build the ROM first!')
|
|
exit(1)
|
|
|
|
with open(f'build/{region}/main.bin.xMAP', 'r') as f:
|
|
line = '\n'
|
|
while len(line) > 0:
|
|
line = f.readline()
|
|
if line.startswith('# .'):
|
|
module_name = line[2:].strip()
|
|
current_module = Module(module_name)
|
|
modules[module_name] = current_module
|
|
|
|
# 020AD090 00000028 .text ItemManager::Create() (ItemManager.cpp.o)
|
|
words = line.split()
|
|
if len(words) < 5: continue
|
|
|
|
# 020AD090
|
|
addr = words[0]
|
|
try: addr = int(addr, base=16)
|
|
except: continue
|
|
|
|
# 00000028
|
|
size = words[1]
|
|
try: size = int(size, base=16)
|
|
except: continue
|
|
|
|
# .text
|
|
section = words[2]
|
|
# Much of .sbss hasn't been defined yet, so group it together with .bss
|
|
if section == '.sbss': section = '.bss'
|
|
# .ctor and .init are coupled together
|
|
if section == '.ctor': section = '.init'
|
|
section = current_module.sections[section]
|
|
|
|
# ItemManager::Create()
|
|
func = words[3]
|
|
is_section = func.startswith('.')
|
|
# ItemManager::Create
|
|
func = func.split('(', 1)[0]
|
|
is_nonmatch = func in nonmatching_funcs
|
|
if is_nonmatch: nonmatching_funcs.remove(func)
|
|
|
|
# (ItemManager.cpp.o)
|
|
file = words[-1]
|
|
if not file.startswith('('): continue
|
|
if not file.endswith(')'): continue
|
|
# ItemManager.cpp.o
|
|
file = file[1:-1]
|
|
|
|
is_asm = file.endswith('.s.o')
|
|
is_c_cpp = file.endswith('.c.o') or file.endswith('.cpp.o')
|
|
if not is_asm and not is_c_cpp:
|
|
eprint(f"Unknown file extension for '{file}'")
|
|
continue
|
|
|
|
if is_asm and is_section: section.bytes_asm += size
|
|
if is_c_cpp and not is_section:
|
|
if is_nonmatch: section.bytes_nonmatch += size
|
|
else: section.bytes_c_cpp += size
|
|
|
|
# Remove empty sections
|
|
for module in modules.values():
|
|
module.sections = {name: section for name, section in module.sections.items() if section.total() > 0}
|
|
# Remove empty modules
|
|
modules = {name: module for name, module in modules.items() if len(module.sections) > 0}
|
|
|
|
if len(nonmatching_funcs) > 0:
|
|
eprint('Some nonmatching functions were not found in the map file:')
|
|
eprint('\n'.join(list(nonmatching_funcs)))
|
|
eprint()
|
|
|
|
if args.all:
|
|
if args.sections:
|
|
sections = {name: Section(name) for name in section_names}
|
|
for module in modules.values():
|
|
for name, section in module.sections.items():
|
|
sections[name].add(section)
|
|
for section in sections.values():
|
|
section.print()
|
|
if args.total:
|
|
module_totals = [module.total() for module in modules.values()]
|
|
total = Section('TOTAL')
|
|
total.bytes_asm = sum(module_total.bytes_asm for module_total in module_totals)
|
|
total.bytes_c_cpp = sum(module_total.bytes_c_cpp for module_total in module_totals)
|
|
total.bytes_nonmatch = sum(module_total.bytes_nonmatch for module_total in module_totals)
|
|
total.print()
|
|
|
|
if args.modules:
|
|
for name, module in modules.items():
|
|
print(f'Module {name}:')
|
|
if args.sections:
|
|
for section in module.sections.values():
|
|
section.print(indent=2)
|
|
if args.total:
|
|
module.total().print(indent=2)
|
|
|
|
parser = argparse.ArgumentParser(description='Parses an xMAP file to calculate and display decompilation progress')
|
|
parser.add_argument('--modules', help='display progress for each module', action='store_true')
|
|
parser.add_argument('--all', help='display progress for all modules combined', action='store_true')
|
|
parser.add_argument('--sections', help='display progress for each section', action='store_true')
|
|
parser.add_argument('--total', help='display progress for all section combined', action='store_true')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not args.modules and not args.all:
|
|
parser.print_help()
|
|
eprint("\nYou must specify at least one of --modules and --all, see above")
|
|
exit(1)
|
|
if not args.sections and not args.total:
|
|
parser.print_help()
|
|
eprint("\nYou must specify at least one of --sections and --total, see above")
|
|
exit(1)
|
|
|
|
main(args)
|