git subrepo pull tools/splat

subrepo:
  subdir:   "tools/splat"
  merged:   "687885c4d"
upstream:
  origin:   "https://github.com/ethteck/splat.git"
  branch:   "master"
  commit:   "687885c4d"
git-subrepo:
  version:  "0.4.3"
  origin:   "https://github.com/ingydotnet/git-subrepo"
  commit:   "2f68596"
This commit is contained in:
Ethan Roseman 2021-01-25 00:19:56 +09:00
parent dc72859546
commit 15de829b58
29 changed files with 106 additions and 92 deletions

View File

@ -2,4 +2,4 @@
venv/
.vscode/
__pycache__/
util/Yay0decompress
util/n64/Yay0decompress

View File

@ -6,7 +6,7 @@
[subrepo]
remote = https://github.com/ethteck/splat.git
branch = master
commit = 7574db712ef19ca420904c82d3559e9ac4b8c5f5
parent = 86760369a5ab977c037c21aebf6f10484570642f
commit = 687885c4d8bcb86d399dbb7dcd660ad8548acdf7
parent = dc7285954620634e352b43bcae320ba1f8dd8de7
method = merge
cmdver = 0.4.3

12
tools/splat/CHANGELOG.md Normal file
View File

@ -0,0 +1,12 @@
# splat Release Notes
## 0.5
* n64splat name changed to splat
* Some refactoring was done to support other platforms besides n64 in the future
* New `platform` option, which defaults to `n64`
* This will cause breaking changes in custom segments, so please refer to one of the changes in one of the n64 base segments for details
* Support for custom artifact paths
* New `undefined_syms_auto_path` option
* New `undefined_funcs_auto_path` option
* New `cache_path` option
* (All path-like options' names now end with `_path`)

View File

@ -5,7 +5,7 @@ default: all
all: Yay0decompress
Yay0decompress:
gcc $(UTIL_DIR)/Yay0decompress.c -fPIC -shared -O3 -o $(UTIL_DIR)/Yay0decompress
gcc $(UTIL_DIR)/n64/Yay0decompress.c -fPIC -shared -O3 -o $(UTIL_DIR)/n64/Yay0decompress
clean:
rm -f $(UTIL_DIR)/Yay0decompress
rm -f $(UTIL_DIR)/n64/Yay0decompress

View File

@ -1,8 +1,10 @@
# n64splat
A n64 rom splitting tool to assist with decompilation and modding projects
# splat
A binary splitting tool to assist with decompilation and modding projects
For example usage, see https://github.com/ethteck/papermario
The Makefile `setup` target calls n64splat with a config file that you can use for reference. More documentation coming soon.
Currently, only N64 roms in the .z64 format are supported.
For example usage, see https://github.com/pmret/papermario
The Makefile `setup` target calls splat with a config file that you can use for reference. More documentation coming soon.
### Requirements
Python package requirements can be installed via `pip3 install -r requirements.txt`

View File

@ -1,13 +1,11 @@
#! /usr/bin/env python3
from capstone import *
from capstone.mips import *
import argparse
from util import rominfo
from util.n64 import rominfo
from util.n64 import find_code_length
from segtypes.code import N64SegCode
parser = argparse.ArgumentParser(description="Create a splat config from a rom")
parser = argparse.ArgumentParser(description="Create a splat config from a rom (currently only n64 .z64 roms supported)")
parser.add_argument("rom", help="path to a .z64 rom")
@ -25,12 +23,8 @@ options:
with open(rom_path, "rb") as f:
fbytes = f.read()
rom_addr = 0x1000
md = Cs(CS_ARCH_MIPS, CS_MODE_MIPS64 + CS_MODE_BIG_ENDIAN)
for insn in md.disasm(fbytes[rom_addr:], rom.entry_point):
rom_addr += 4
first_section_end = find_code_length.run(fbytes, 0x1000, rom.entry_point)
segments = \
"""segments:
@ -52,7 +46,7 @@ options:
- type: bin
start: 0x{:X}
- [0x{:X}]
""".format(rom.entry_point, rom_addr, rom.size)
""".format(rom.entry_point, first_section_end, rom.size)
outstr = header + segments

View File

@ -1,7 +1,6 @@
import os
from segtypes.segment import N64Segment
from pathlib import Path
from util import Yay0decompress
from segtypes.n64.segment import N64Segment
from util.n64 import Yay0decompress
class N64SegYay0(N64Segment):
def split(self, rom_bytes, base_path):

View File

View File

@ -1,5 +1,5 @@
import os
from segtypes.segment import N64Segment
from segtypes.n64.segment import N64Segment
from pathlib import Path

View File

@ -1,4 +1,4 @@
from segtypes.ci8 import N64SegCi8
from segtypes.n64.ci8 import N64SegCi8
class N64SegCi4(N64SegCi8):
def parse_image(self, data):

View File

@ -1,8 +1,7 @@
from segtypes.segment import N64Segment
from segtypes.rgba16 import N64SegRgba16
from segtypes.n64.rgba16 import N64SegRgba16
import png
import os
from util import Yay0decompress
from util.n64 import Yay0decompress
class N64SegCi8(N64SegRgba16):

View File

@ -3,7 +3,7 @@ from capstone import *
from capstone.mips import *
from collections import OrderedDict
from segtypes.segment import N64Segment, parse_segment_name
from segtypes.n64.segment import N64Segment
import os
from pathlib import Path, PurePath
from ranges import Range, RangeDict
@ -508,7 +508,7 @@ class N64SegCode(N64Segment):
if sym_type in ["float", "double"]:
if sym_type == "float":
float_str = floats.format_f32_imm(bits)
elif sym_type == "double":
else:
float_str = floats.format_f64_imm(bits)
# Fall back to .word if we see weird float values

View File

@ -1,7 +1,6 @@
import os
from segtypes.segment import N64Segment
from segtypes.n64.segment import N64Segment
from pathlib import Path
from util import rominfo
class N64SegHeader(N64Segment):
def should_run(self):

View File

@ -1,4 +1,4 @@
from segtypes.rgba16 import N64SegRgba16
from segtypes.n64.rgba16 import N64SegRgba16
import png
from math import ceil

View File

@ -1,4 +1,4 @@
from segtypes.i4 import N64SegI4
from segtypes.n64.i4 import N64SegI4
from math import ceil
class N64SegI8(N64SegI4):

View File

@ -1,4 +1,4 @@
from segtypes.ia4 import N64SegIa4
from segtypes.n64.ia4 import N64SegIa4
class N64SegIa8(N64SegIa4):
def parse_image(self, data):

View File

@ -1,5 +1,4 @@
import os
from segtypes.rgba16 import N64SegRgba16
from segtypes.n64.rgba16 import N64SegRgba16
import png
from math import ceil

View File

@ -1,5 +1,4 @@
from segtypes.ia4 import N64SegIa4
import png
from segtypes.n64.ia4 import N64SegIa4
from math import ceil
class N64SegIa8(N64SegIa4):

View File

@ -1,9 +1,9 @@
import os
from segtypes.segment import N64Segment
from segtypes.n64.segment import N64Segment
from util.color import unpack_color
from util.n64 import Yay0decompress
from util.iter import iter_in_groups
class N64SegPalette(N64Segment):
require_unique_name = False

View File

@ -1,7 +1,6 @@
import os
from segtypes.segment import N64Segment
from pathlib import Path
from util import Yay0decompress
from segtypes.n64.segment import N64Segment
from util.n64 import Yay0decompress
import png
from math import ceil
from util.color import unpack_color

View File

@ -1,4 +1,4 @@
from segtypes.rgba16 import N64SegRgba16
from segtypes.n64.rgba16 import N64SegRgba16
class N64SegRgba32(N64SegRgba16):
def parse_image(self, data):

View File

@ -0,0 +1,4 @@
from segtypes.segment import Segment
class N64Segment(Segment):
pass

View File

@ -1,7 +1,4 @@
import os
from pathlib import Path, PurePath
import re
import json
from util import log
default_subalign = 16
@ -43,7 +40,7 @@ def parse_segment_subalign(segment):
return default_subalign
class N64Segment:
class Segment:
require_unique_name = True
def __init__(self, segment, next_segment, options):
@ -52,8 +49,7 @@ class N64Segment:
self.type = parse_segment_type(segment)
self.name = parse_segment_name(segment, self.__class__)
self.vram_addr = parse_segment_vram(segment)
self.ld_name_override = segment.get(
"ld_name", None) if type(segment) is dict else None
self.ld_name_override = segment.get("ld_name", None) if type(segment) is dict else None
self.options = options
self.config = segment
self.subalign = parse_segment_subalign(segment)

View File

@ -5,16 +5,12 @@ import importlib
import importlib.util
import os
from ranges import Range, RangeDict
import re
from pathlib import Path
import segtypes
import sys
import yaml
import pickle
from colorama import Style, Fore
from collections import OrderedDict
from segtypes.segment import N64Segment, parse_segment_type
from segtypes.code import N64SegCode
from segtypes.segment import parse_segment_type
from segtypes.n64.code import N64SegCode
from util import log
parser = argparse.ArgumentParser(
@ -81,11 +77,23 @@ def parse_file_start(split_file):
def get_symbol_addrs_path(repo_path, options):
return os.path.join(repo_path, options.get("symbol_addrs", "symbol_addrs.txt"))
return os.path.join(repo_path, options.get("symbol_addrs_path", "symbol_addrs.txt"))
def get_undefined_syms_path(repo_path, options):
return os.path.join(repo_path, options.get("undefined_syms", "undefined_syms.txt"))
return os.path.join(repo_path, options.get("undefined_syms_path", "undefined_syms.txt"))
def get_undefined_syms_auto_path(repo_path, options):
return os.path.join(repo_path, options.get("undefined_syms_auto_path", "undefined_syms_auto.txt"))
def get_undefined_funcs_auto_path(repo_path, options):
return os.path.join(repo_path, options.get("undefined_funcs_auto_path", "undefined_funcs_auto.txt"))
def get_cache_path(repo_path, options):
return os.path.join(repo_path, options.get("cache_path", ".splat_cache"))
def gather_symbols(symbol_addrs_path, undefined_syms_path):
@ -156,13 +164,13 @@ def gather_c_variables(undefined_syms_path):
return vars
def get_base_segment_class(seg_type):
def get_base_segment_class(seg_type, platform):
try:
segmodule = importlib.import_module("segtypes." + seg_type)
segmodule = importlib.import_module(f"segtypes.{platform}.{seg_type}")
except ModuleNotFoundError:
return None
return getattr(segmodule, "N64Seg" + seg_type[0].upper() + seg_type[1:])
return getattr(segmodule, f"{platform.upper()}Seg{seg_type[0].upper()}{seg_type[1:]}")
def get_extension_dir(options, config_path):
@ -171,20 +179,20 @@ def get_extension_dir(options, config_path):
return os.path.join(Path(config_path).parent, options["extensions"])
def get_extension_class(options, config_path, seg_type):
def get_extension_class(options, config_path, seg_type, platform):
ext_dir = get_extension_dir(options, config_path)
if ext_dir == None:
return None
try:
ext_spec = importlib.util.spec_from_file_location(f"segtypes.{seg_type}", os.path.join(ext_dir, f"{seg_type}.py"))
ext_spec = importlib.util.spec_from_file_location(f"{platform}.segtypes.{seg_type}", os.path.join(ext_dir, f"{seg_type}.py"))
ext_mod = importlib.util.module_from_spec(ext_spec)
ext_spec.loader.exec_module(ext_mod)
except Exception as err:
log.write(err, status="error")
return None
return getattr(ext_mod, "N64Seg" + seg_type[0].upper() + seg_type[1:])
return getattr(ext_mod, f"{platform.upper()}Seg{seg_type[0].upper()}{seg_type[1:]}")
def fmt_size(size):
@ -196,7 +204,7 @@ def fmt_size(size):
return str(size) + " B"
def initialize_segments(options, config_path, config_segments):
def initialize_segments(options, config_path, config_segments, platform):
seen_segment_names = set()
ret = []
@ -207,24 +215,16 @@ def initialize_segments(options, config_path, config_segments):
seg_type = parse_segment_type(segment)
segment_class = get_base_segment_class(seg_type)
segment_class = get_base_segment_class(seg_type, platform)
if segment_class == None:
# Look in extensions
segment_class = get_extension_class(options, config_path, seg_type)
segment_class = get_extension_class(options, config_path, seg_type, platform)
if segment_class == None:
log.write(f"fatal error: could not load segment type '{seg_type}'\n(hint: confirm your extension directory is configured correctly)", status="error")
return 2
try:
segment = segment_class(segment, config_segments[i + 1], options)
except (IndexError, KeyError) as e:
try:
segment = N64Segment(segment, config_segments[i + 1], options)
segment.error(e)
except Exception as e:
log.write(f"fatal error (segment type = {seg_type}): " + str(e), status="error")
return 2
segment = segment_class(segment, config_segments[i + 1], options)
if segment_class.require_unique_name:
if segment.name in seen_segment_names:
@ -254,6 +254,7 @@ def main(rom_path, config_path, repo_path, modes, verbose, ignore_cache=False):
symbol_addrs_path = get_symbol_addrs_path(repo_path, options)
undefined_syms_path = get_undefined_syms_path(repo_path, options)
provided_symbols, c_func_labels_to_add, special_labels, ranges = gather_symbols(symbol_addrs_path, undefined_syms_path)
platform = options.get("platform", "n64")
processed_segments = []
ld_sections = []
@ -267,7 +268,7 @@ def main(rom_path, config_path, repo_path, modes, verbose, ignore_cache=False):
seg_cached = {}
# Load cache
cache_path = Path(repo_path) / ".splat_cache"
cache_path = get_cache_path(repo_path, options)
try:
with open(cache_path, "rb") as f:
cache = pickle.load(f)
@ -275,10 +276,10 @@ def main(rom_path, config_path, repo_path, modes, verbose, ignore_cache=False):
cache = {}
# Initialize segments
all_segments = initialize_segments(options, config_path, config["segments"])
all_segments = initialize_segments(options, config_path, config["segments"], platform)
for segment in all_segments:
if type(segment) == N64SegCode:
if platform == "n64" and type(segment) == N64SegCode: # remove special-case sometime
segment.all_functions = defined_funcs
segment.provided_symbols = provided_symbols
segment.special_labels = special_labels
@ -314,7 +315,7 @@ def main(rom_path, config_path, repo_path, modes, verbose, ignore_cache=False):
if len(segment.errors) == 0:
processed_segments.append(segment)
if type(segment) == N64SegCode:
if platform == "n64" and type(segment) == N64SegCode: # edge case
undefined_funcs |= segment.glabels_to_add
defined_funcs = {**defined_funcs, **segment.glabels_added}
undefined_syms |= segment.undefined_syms_to_add
@ -335,21 +336,23 @@ def main(rom_path, config_path, repo_path, modes, verbose, ignore_cache=False):
write_ldscript(config['basename'], repo_path, ld_sections, options)
# Write undefined_funcs_auto.txt
undefined_funcs_auto_path = get_undefined_funcs_auto_path(repo_path, options)
if verbose:
log.write(f"saving undefined_funcs_auto.txt")
log.write(f"saving {undefined_funcs_auto_path}")
c_predefined_funcs = set(provided_symbols.keys())
to_write = sorted(undefined_funcs - set(defined_funcs.values()) - c_predefined_funcs)
if len(to_write) > 0:
with open(os.path.join(repo_path, "undefined_funcs_auto.txt"), "w", newline="\n") as f:
with open(undefined_funcs_auto_path, "w", newline="\n") as f:
for line in to_write:
f.write(line + " = 0x" + line.split("_")[1][:8].upper() + ";\n")
# write undefined_syms_auto.txt
undefined_syms_auto_path = get_undefined_syms_auto_path(repo_path, options)
if verbose:
log.write(f"saving undefined_syms_auto.txt")
log.write(f"saving {undefined_syms_auto_path}")
to_write = sorted(undefined_syms, key=lambda x:x[0])
if len(to_write) > 0:
with open(os.path.join(repo_path, "undefined_syms_auto.txt"), "w", newline="\n") as f:
with open(undefined_syms_auto_path, "w", newline="\n") as f:
for sym in to_write:
f.write(f"{sym[0]} = 0x{sym[1]:X};\n")

View File

@ -4,9 +4,6 @@ from capstone import *
from capstone.mips import *
import argparse
import hashlib
import rominfo
import zlib
md = Cs(CS_ARCH_MIPS, CS_MODE_MIPS64 + CS_MODE_BIG_ENDIAN)
@ -33,7 +30,7 @@ def run(rom_bytes, start_offset, vram, end_offset=None):
def main():
args = parser.parse_args()
rom_bytes = rominfo.read_rom(args.rom)
rom_bytes = open(args.rom, "rb").read()
start = int(args.start, 0)
end = None
vram = int(args.vram, 0)

View File

@ -6,7 +6,7 @@ import zlib
parser = argparse.ArgumentParser(description='Gives information on n64 roms')
parser.add_argument('rom', help='path to a .z64 rom')
parser.add_argument('--encoding', help='Text encoding the game header is using, defaults to ASCII, see docs.python.org/2.4/lib/standard-encodings.html for valid encodings', default='ASCII')
parser.add_argument('--encoding', help='Text encoding the game header is using; see docs.python.org/3/library/codecs.html#standard-encodings for valid encodings', default='ASCII')
country_codes = {
0x37: "Beta",
@ -71,7 +71,7 @@ def get_info_bytes(rom_bytes, encoding):
try:
name = rom_bytes[0x20:0x34].decode(encoding).strip()
except:
print("n64splat could not decode the game name, try using a different encoding by passing the --encoding argument (see docs.python.org/2.4/lib/standard-encodings.html for valid encodings)")
print("splat could not decode the game name; try using a different encoding by passing the --encoding argument (see docs.python.org/3/library/codecs.html#standard-encodings for valid encodings)")
exit(1)
country_code = rom_bytes[0x3E]

View File

@ -0,0 +1,12 @@
class N64Symbol:
@staticmethod
def get_default_name(vram):
return f"D_{vram:X}"
def __init__(self, vram, rom=None, name=None, segment=None, length=4):
self.vram = vram
self.rom = rom
self.name = name if name else self.get_default_name(vram)
self.segment = segment
self.length = length