added ntscu-u support
This commit is contained in:
@@ -48,9 +48,15 @@ while _a:
|
||||
LV = "%02d" % LEVEL if LEVEL is not None else None
|
||||
SFX = "_l%s" % LV if LEVEL is not None else ""
|
||||
HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
# the region (build.py --region): its host sources (tools/region.py writes them) and constants
|
||||
REGION = os.environ.get("UYA_REGION", "pal")
|
||||
HOSTDIR = os.environ.get("UYA_HOST") or os.path.join(HERE, "host")
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import region as _region
|
||||
RC = _region.consts(REGION)
|
||||
PLAT = os.environ.get("RAC_PLATFORM", os.environ.get("UYA_PLATFORM", "linux")) # linux | windows (COFF i386 adds a leading _)
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
overrides = {os.path.basename(p)[:-2] for p in glob.glob(os.path.join(HERE, "host", "overrides", "func_*.c"))}
|
||||
overrides = {os.path.basename(p)[:-2] for p in glob.glob(os.path.join(HOSTDIR, "overrides", "func_*.c"))}
|
||||
ASM_MACRO = re.compile(r"^\s*(INCLUDE_ASM|ASM_FUNC|LINKER_REMNANT|INCLUDE_RODATA|TEXT_PADDING)\s*\(([^()]*)\)\s*;?\s*(?:/\*.*?\*/|//.*)?\s*$", re.M)
|
||||
|
||||
|
||||
@@ -82,7 +88,7 @@ def strip_comments_keep_len(s):
|
||||
|
||||
host_defined = set()
|
||||
STATIC = os.environ.get("UYA_STATIC", "1") != "0" # RAC1_STATICRECOMP: translate the whole game from its bytes
|
||||
for p in glob.glob(os.path.join(HERE, "host", "src", "**", "*.c"), recursive=True):
|
||||
for p in glob.glob(os.path.join(HOSTDIR, "src", "**", "*.c"), recursive=True):
|
||||
host_defined |= set(re.findall(r"^[\w \t\*]*\b(func_(?:L\d\d_)?[0-9A-Fa-f]{6,8})\s*\([^;{]*\)\s*\{", strip_comments_keep_len(open(p).read()), re.M))
|
||||
|
||||
|
||||
@@ -171,8 +177,8 @@ def rewrite_types(s):
|
||||
DECL = re.compile(r"^[ \t]*(?:extern[ \t]+)?(?P<t>[A-Za-z_][\w \t\*]*?)[ \t\*]*\b(?P<n>(?:func|D)_(?:L\d\d_)?[0-9A-Fa-f]{6,8}\w*)[ \t]*(?P<s>\((?:[^;{}]*)\)|\[[^;{}]*\])?[ \t]*;", re.M)
|
||||
|
||||
|
||||
MAIN_TEXT = (0x001E9080, 0x0023E740) # the executable's game program (.text of the main segment)
|
||||
RESIDENT = 0x0015F000 # below: core segment and core_lit, the same in every program
|
||||
MAIN_TEXT = RC["main_text"] # the executable's game program (.text of the main segment); PAL 0x001E9080-0x0023E740
|
||||
RESIDENT = RC["resident"] # below: core segment and core_lit, the same in every program; PAL 0x0015F000
|
||||
PLACES = {} # level mode: catalogue name -> its addresses in this level
|
||||
CAT_KIND = {}
|
||||
CAT_SIZE = {}
|
||||
@@ -333,7 +339,7 @@ if LEVEL is None: os.makedirs(INC, exist_ok=True)
|
||||
for h in (glob.glob(os.path.join(REPO, "include", "*.h")) if LEVEL is None else []):
|
||||
bn = os.path.basename(h)
|
||||
if bn == "common.h":
|
||||
t = open(os.path.join(HERE, "host", "include", "rac1_common.h")).read()
|
||||
t = open(os.path.join(HOSTDIR, "include", "rac1_common.h")).read()
|
||||
elif bn == "include_asm.h":
|
||||
t = "/* host: assembly inclusion has no meaning on the PC */\n"
|
||||
else:
|
||||
@@ -344,7 +350,7 @@ for h in (glob.glob(os.path.join(REPO, "include", "*.h")) if LEVEL is None else
|
||||
|
||||
stubbed, defined, asm_entries, report = [], set(), [], {}
|
||||
gen_texts = {}
|
||||
drafts = {os.path.basename(p)[:-2]: p for p in glob.glob(os.path.join(HERE, "host", "drafts", "func_*.c"))}
|
||||
drafts = {os.path.basename(p)[:-2]: p for p in glob.glob(os.path.join(HOSTDIR, "drafts", "func_*.c"))}
|
||||
used_drafts = set()
|
||||
files = sorted(os.path.relpath(p, REPO) for p in glob.glob(os.path.join(REPO, "src", "game", "*.c")))
|
||||
files += sorted(os.path.relpath(p, REPO) for p in glob.glob(os.path.join(REPO, "src", "game", "movie", "*.c")))
|
||||
@@ -359,6 +365,9 @@ dropped = [] # level mode: functions of other levels' programs
|
||||
# the EE ABI but not for x86 (each entry: file, old text, new text)
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from source_patches import PATCHES
|
||||
if REGION != "pal": # (written against the PAL sources: in the region's names, as tools/region.py rewrote them)
|
||||
_rw = _region.Rewriter(_region.RegionMap(_region.REGIONS[REGION]["map"]))
|
||||
PATCHES = [(pf, _rw.text(old, "patch:" + pf), _rw.text(new, "patch:" + pf)) for pf, old, new in PATCHES]
|
||||
patches_applied = 0
|
||||
# names.h: readable names are macros for the address names; expand them in
|
||||
# the C (outside string literals) so every later pass sees func_/D_ names.
|
||||
@@ -453,7 +462,8 @@ for rel in files:
|
||||
# UYA_FORCE_ASM="" compiles all the C; or a list: file substrings, re:regex,
|
||||
# !file (keep that file's C), e.g. "help_0020CDF0,help_00232560" (the old default).
|
||||
_fa = os.environ.get("UYA_FORCE_ASM", "re:.")
|
||||
if LEVEL is not None and _fa:
|
||||
# (another region: the executable's C too, so all of it comes from the region's own bytes)
|
||||
if (LEVEL is not None or REGION != "pal") and _fa:
|
||||
_ex = [t[1:] for t in _fa.split(",") if t.startswith("!")]
|
||||
if any(x and x in rel for x in _ex): _fa = "" # "!file-substring": that file keeps its C
|
||||
for tok in _fa.split(","):
|
||||
@@ -483,7 +493,9 @@ for rel in files:
|
||||
seg = "/* host: top-level asm removed */"
|
||||
else:
|
||||
name, bpos = extra
|
||||
if LEVEL is not None and base_func(name) == name and (name.startswith("func_L") or is_main_func(name)) and name not in PLACES:
|
||||
# (another region: func_palonly_X, a PAL function with no counterpart found in this region,
|
||||
# is not in any of its programs either; its C would be defined once per level)
|
||||
if LEVEL is not None and (name.startswith("func_palonly_") or base_func(name) == name and (name.startswith("func_L") or is_main_func(name)) and name not in PLACES):
|
||||
dropped.append(name)
|
||||
out.append("/* host: %s is not in level %s's program */" % (name, LV)); pos = e
|
||||
continue
|
||||
@@ -602,7 +614,7 @@ for fn, t in gen_texts.items():
|
||||
rets[q.group(1)] = ret_type(t[q.start():q.start(1) + len(q.group(1))], q.group(1))
|
||||
# host functions (host/src, host/overrides) too: a decomp call that leaves out
|
||||
# arguments the EE would still pass in registers gets them filled in
|
||||
for hp in glob.glob(os.path.join(HERE, "host", "src", "**", "*.c"), recursive=True) + glob.glob(os.path.join(HERE, "host", "overrides", "*.c")):
|
||||
for hp in glob.glob(os.path.join(HOSTDIR, "src", "**", "*.c"), recursive=True) + glob.glob(os.path.join(HOSTDIR, "overrides", "*.c")):
|
||||
ht = open(hp, errors="replace").read()
|
||||
hm = strip_comments_keep_len(ht)
|
||||
for q in HDR.finditer(hm):
|
||||
@@ -808,7 +820,7 @@ for n, p in defs.items():
|
||||
a = ps2_addr(n)
|
||||
if a is not None and re.fullmatch(r"func_(?:L\d\d_)?[0-9A-Fa-f]{8}", n) and not any(x[0] == "..." for x in p):
|
||||
fdefs[a] = p
|
||||
for op_ in glob.glob(os.path.join(HERE, "host", "overrides", "func_*.c")):
|
||||
for op_ in glob.glob(os.path.join(HOSTDIR, "overrides", "func_*.c")):
|
||||
ot = open(op_, errors="replace").read()
|
||||
for q in HDR.finditer(strip_comments_keep_len(ot)):
|
||||
n = q.group(1); a = ps2_addr(n)
|
||||
@@ -1224,7 +1236,7 @@ for fn_, t_ in gen_texts.items():
|
||||
# value in an int (`int mode = GetEffectTex(1);` then passed on as the texture),
|
||||
# gets 64 bits: on the EE the register keeps all of them whatever the C says.
|
||||
_r64 = sorted(n for n, s in sigs.items() if s.get("src") == "decl" and s["ret"] == "l" and re.fullmatch(r"func_(?:L\d\d_)?[0-9A-Fa-f]{8}", n)
|
||||
and (s.get("addr") or 0) >= 0x0015F000) # game code (the core's library calls are the host's)
|
||||
and (s.get("addr") or 0) >= RESIDENT) # game code (the core's library calls are the host's)
|
||||
if _r64:
|
||||
_names = "|".join(re.escape(n) for n in _r64)
|
||||
_INT = r"(?:int|s32|u32|unsigned int|unsigned|signed int|signed)"
|
||||
|
||||
@@ -43,6 +43,11 @@ rabbitizer.config.pseudos_enablePseudos = False
|
||||
|
||||
GEN, REPO = sys.argv[1], sys.argv[2]
|
||||
HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
REGION = os.environ.get("UYA_REGION", "pal")
|
||||
HOSTDIR = os.environ.get("UYA_HOST") or os.path.join(HERE, "host")
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import region as _region
|
||||
RC = _region.consts(REGION)
|
||||
LEVEL = DUMP = EXEGEN = None
|
||||
_a = sys.argv[3:]
|
||||
while _a:
|
||||
@@ -53,14 +58,15 @@ while _a:
|
||||
else: sys.exit("recomp.py: unknown option " + _o)
|
||||
LV = "%02d" % LEVEL if LEVEL is not None else None
|
||||
P = "rc_" if LEVEL is None else "rc%s_" % LV # prefix of the generated symbols
|
||||
RESIDENT = 0x0015F000
|
||||
RESIDENT = RC["resident"] # PAL 0x0015F000
|
||||
|
||||
# ---- inputs -----------------------------------------------------------------
|
||||
# testing: UYA_RC_TRACE="func_A,func_B" (build time) logs every call of those translated functions
|
||||
TRACE_FUNCS = set(x for x in os.environ.get("UYA_RC_TRACE", "").split(",") if x)
|
||||
EXE = os.environ.get("UYA_EXE") or os.path.join(REPO, "baserom", "SCES_509.16") # build.py: the copy read from the disc image
|
||||
EXE = os.environ.get("UYA_EXE") or os.path.join(REPO, "baserom", RC["exe"]) # build.py: the copy read from the disc image
|
||||
img = open(EXE, "rb").read()
|
||||
TEXT_LO, TEXT_HI = 0x00100080, 0x00100080 + 0x13E6B0
|
||||
TEXT_LO = 0x00100080 # the executable's one loadable segment (PAL size 0x13E6B0)
|
||||
TEXT_HI = TEXT_LO + struct.unpack_from("<I", img, struct.unpack_from("<I", img, 28)[0] + 16)[0]
|
||||
LTEXT_LO = LTEXT_HI = 0
|
||||
if LEVEL is not None:
|
||||
_ld = os.path.join(DUMP, "level_%s" % LV)
|
||||
@@ -86,16 +92,15 @@ for u in report["units"]:
|
||||
fsize[f["name"]] = (int(md["virtual_address"]), int(f["size"]))
|
||||
sigs = json.load(open(os.path.join(GEN, "sigs.json")))
|
||||
entries = json.load(open(os.path.join(GEN, "asm_entries.json")))
|
||||
overrides = {os.path.basename(p)[:-2] for p in os.listdir(os.path.join(HERE, "host", "overrides")) if p.endswith(".c")} if os.path.isdir(os.path.join(HERE, "host", "overrides")) else set()
|
||||
overrides = {os.path.basename(p)[:-2] for p in os.listdir(os.path.join(HOSTDIR, "overrides")) if p.endswith(".c")} if os.path.isdir(os.path.join(HOSTDIR, "overrides")) else set()
|
||||
|
||||
todo = [n for n in entries if n in fsize and n not in overrides and re.fullmatch(r"func_[0-9A-F]{8}", n)
|
||||
and not (n in sigs and sigs[n].get("src") == "def")
|
||||
and TEXT_LO <= fsize[n][0] < TEXT_HI]
|
||||
# Sony libraries that are pure computation are translated too (libvu0: the
|
||||
# sceVu0 matrix and vector routines, core_text 0x1252A0-0x125630)
|
||||
EXTRA_RANGES = [(0x001252A0, 0x00125630),
|
||||
# libgcc (64-bit integer and soft-float helpers; level code calls some the executable's C does not)
|
||||
(0x0011DF10, 0x001207B8)]
|
||||
EXTRA_RANGES = RC["extra_ranges"] # PAL 0x001252A0-0x00125630, and libgcc 0x0011DF10-0x001207B8
|
||||
# (libgcc: 64-bit integer and soft-float helpers; level code calls some the executable's C does not)
|
||||
todo += [n for n, (a, sz) in fsize.items() if any(lo <= a < hi for lo, hi in EXTRA_RANGES)
|
||||
and re.fullmatch(r"func_[0-9A-F]{8}", n) and not (n in sigs and sigs[n].get("src") == "def") and n not in overrides]
|
||||
base_of = {}
|
||||
@@ -259,7 +264,7 @@ else:
|
||||
_is_start_x = {a for a, _ in _starts_x}
|
||||
_abs = os.path.join(GEN, "abs_syms.s")
|
||||
_cand = {int(x, 16) for x in re.findall(r"\.set\s+_?D_([0-9A-F]{8})\w*,", open(_abs).read())} if os.path.exists(_abs) else set()
|
||||
_cand = {v for v in _cand if 0x001E9080 <= v < 0x0023E740}
|
||||
_cand = {v for v in _cand if RC["main_text"][0] <= v < RC["main_text"][1]}
|
||||
# and functions the C (the executable's and the levels') calls by an address
|
||||
# inside an assembly-only function: func_001252C0 is 0x20 into libvu0's
|
||||
# func_001252A0 (the levels' hero code calls it)
|
||||
@@ -277,7 +282,7 @@ else:
|
||||
_w = struct.unpack_from("<I", _d, _i)[0]
|
||||
if _w >> 26 == 3:
|
||||
_t = (_w & 0x3FFFFFF) << 2
|
||||
if TEXT_LO <= _t < 0x0015F000: _cand.add(_t)
|
||||
if TEXT_LO <= _t < RESIDENT: _cand.add(_t)
|
||||
for _v in sorted(_cand):
|
||||
if not (TEXT_LO <= _v < TEXT_HI) or _v % 4 or _v in _is_start_x: continue
|
||||
_own = [(a, e) for a, e in _starts_x if a < _v < e]
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env python3
|
||||
"""region.py: build another region's version of the game from the PAL sources.
|
||||
|
||||
python tools/region.py us DECOMP_REPO OUT_DIR
|
||||
|
||||
The decomp, the host sources and every tool here work in the PAL executable's
|
||||
addresses (SCES-50916), and they name functions and data by address
|
||||
(func_0012F4A8, D_0013A548). region/us.json (made by tools/region_map.py from both
|
||||
executables; addresses only) maps every PAL address to the US one (SCUS-97199).
|
||||
This writes, under OUT_DIR:
|
||||
|
||||
repo/ the decomp as the tools read it, in US addresses: its catalogues
|
||||
(progress/report.json, config/*, config/overlays/functions.tsv), headers and
|
||||
sources with every func_/D_ name and address literal rewritten
|
||||
host/ the host sources (host/src, host/include, host/overrides) the same way
|
||||
region_report.txt every literal rewritten, and what could not be mapped
|
||||
|
||||
A PAL function the US build does not have is renamed func_palonly_X, so nothing is
|
||||
ever placed at its PAL address; a US function the PAL build does not have is added to
|
||||
the catalogue (and translated from the bytes like the rest). The build then runs the
|
||||
usual tools on these trees (build.py --region us)."""
|
||||
import bisect, json, os, re, shutil, sys
|
||||
|
||||
HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
REGIONS = {"us": {"map": os.path.join(HERE, "region", "us.json"), "exe": "SCUS_971.99", "pal_exe": "SCES_509.16",
|
||||
"video": "ntsc"}}
|
||||
RESIDENT_PAL = 0x0015F000
|
||||
CORE_TEXT = (0x00112380, 0x0012F580) # PAL core.text: the Sony libraries
|
||||
|
||||
|
||||
class RegionMap:
|
||||
def __init__(self, path):
|
||||
m = json.load(open(path))
|
||||
self.m = m
|
||||
self.exe = {int(k, 16): v for k, v in m["exe_funcs"].items()}
|
||||
self.exe_starts = sorted(self.exe)
|
||||
self.data = sorted(m["data"])
|
||||
self.lv = {lv: {int(k, 16): v for k, v in d["funcs"].items()} for lv, d in m["levels"].items()}
|
||||
self.lv_starts = {lv: sorted(d) for lv, d in self.lv.items()}
|
||||
self.lv_data = {lv: sorted(r) for lv, r in m["level_data"].items()}
|
||||
self.sections = m["sections"]
|
||||
self.us_only = m["exe_us_only"]
|
||||
self.lv_us_only = {lv: d["us_only"] for lv, d in m["levels"].items()}
|
||||
|
||||
@staticmethod
|
||||
def _func(table, starts, a, interior=True):
|
||||
if a in table: return table[a][0]
|
||||
if not interior: return None
|
||||
k = bisect.bisect_right(starts, a) - 1
|
||||
if k >= 0:
|
||||
s = starts[k]; ua, sz, method = table[s]
|
||||
if s < a < s + sz and method == "fingerprint": return ua + (a - s) # same instructions: same offsets
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _runs(runs, a):
|
||||
for r in runs:
|
||||
if r[0] <= a < r[1]: return a + r[2]
|
||||
return None
|
||||
|
||||
def section_edge(self, a):
|
||||
"""a PAL section's first address or its end: the US section's"""
|
||||
if a == 0x0023E740: # the end of PAL's game program as the tools give it (consts)
|
||||
t = self.sections[".text"]; return (t[2] + t[3] + 15) & ~15
|
||||
for n, (pa, ps, ua, us_) in self.sections.items():
|
||||
if not pa: continue
|
||||
if a == pa: return ua
|
||||
if a == pa + ps: return ua + us_
|
||||
if a == (pa + ps + 15) & ~15: return (ua + us_ + 15) & ~15 # (an end rounded up to 16)
|
||||
return None
|
||||
|
||||
def exe_addr(self, a, interior=True):
|
||||
"""the US address of a PAL executable address, or None"""
|
||||
if 0x00100000 <= a < 0x00112380: return a # .reginfo, .vutext: the same in both
|
||||
f = self._func(self.exe, self.exe_starts, a, interior)
|
||||
if f is not None: return f
|
||||
return self._runs(self.data, a)
|
||||
|
||||
def level_addr(self, lv, a, interior=True):
|
||||
if a < RESIDENT_PAL: return self.exe_addr(a, interior) # the core: shared by every program
|
||||
f = self._func(self.lv.get(lv, {}), self.lv_starts.get(lv, []), a, interior)
|
||||
if f is not None: return f
|
||||
return self._runs(self.lv_data.get(lv, []), a)
|
||||
|
||||
def exe_func(self, a): return self.exe[a][0] if a in self.exe else None
|
||||
def level_func(self, lv, a): return self.lv[lv][a][0] if lv in self.lv and a in self.lv[lv] else None
|
||||
|
||||
|
||||
def consts(region):
|
||||
"""what the tools need to know about the executable, for this region"""
|
||||
pal = {"exe": "SCES_509.16", "main_text": (0x001E9080, 0x0023E740), "resident": 0x0015F000,
|
||||
"core_text": CORE_TEXT, "extra_ranges": [(0x001252A0, 0x00125630), (0x0011DF10, 0x001207B8)]}
|
||||
if region in (None, "", "pal"): return pal
|
||||
rm = RegionMap(REGIONS[region]["map"])
|
||||
t = rm.sections[".text"]; lit = rm.sections[".lit"]; ct = rm.sections["core.text"]
|
||||
return {"exe": REGIONS[region]["exe"],
|
||||
"main_text": (t[2], (t[2] + t[3] + 15) & ~15),
|
||||
"resident": lit[2],
|
||||
"core_text": (ct[2], ct[2] + ct[3]),
|
||||
"extra_ranges": [(rm.exe_addr(lo), rm.exe_addr(hi) if rm.exe_addr(hi) else rm.exe_addr(hi - 4) + 4) for lo, hi in pal["extra_ranges"]]}
|
||||
|
||||
|
||||
# ---- rewriting ---------------------------------------------------------------------------------------
|
||||
NAME = re.compile(r"\b(func|D)_(?:L(\d\d)_)?([0-9A-Fa-f]{6,8})(?![0-9A-Fa-f])")
|
||||
LIT = re.compile(r"\b0x(00[1-9][0-9A-Fa-f]{5}|0[1-9][0-9A-Fa-f]{5}|[1-9][0-9A-Fa-f]{5})(?=[uUlL]*\b)")
|
||||
# a line marked region:keep holds values that only look like addresses (IOP addresses, handles, lui values)
|
||||
|
||||
|
||||
class Rewriter:
|
||||
def __init__(self, rm):
|
||||
self.rm = rm
|
||||
self.log, self.unmapped = [], []
|
||||
self.taken = {}
|
||||
|
||||
def name(self, kind, lv_name, hexpart, lv_ctx, where):
|
||||
a = int(hexpart, 16)
|
||||
up = hexpart.isupper() or not any(c.isalpha() for c in hexpart)
|
||||
fmt = "%08X" if len(hexpart) == 8 else ("%06X" if up else "%06x")
|
||||
if kind == "func":
|
||||
if lv_name is not None: # a level catalogue name: its address in that level
|
||||
ua = self.rm.level_func(lv_name, a)
|
||||
if ua is None: self.unmapped.append((where, "func_L%s_%s" % (lv_name, hexpart))); return "func_palonly_L%s_%s" % (lv_name, hexpart)
|
||||
return "func_L%s_%s" % (lv_name, fmt % ua)
|
||||
ua = self.rm.exe_func(a)
|
||||
if ua is None and CORE_TEXT[0] <= a < CORE_TEXT[1]: ua = self.rm.exe_addr(a) # a library entry inside a split unit
|
||||
if ua is None:
|
||||
# the executable's main-segment functions are also catalogue names of level copies
|
||||
self.unmapped.append((where, "func_" + hexpart)); return "func_palonly_" + hexpart
|
||||
return "func_" + (fmt % ua)
|
||||
ctx = lv_name or lv_ctx # D_LNN_X: level NN's own data
|
||||
pre = "D_L%s_" % lv_name if lv_name else "D_"
|
||||
ua = self.rm.level_addr(ctx, a) if ctx else self.rm.exe_addr(a)
|
||||
if ua is None: self.unmapped.append((where, pre + hexpart)); return pre.replace("D_", "D_palonly_") + hexpart
|
||||
# one to one: a second PAL address landing on a US address already taken (two neighbours either
|
||||
# side of a shift boundary) is not trusted; its uses then fail to build instead of reading other data
|
||||
key = (ctx, bool(lv_name), ua)
|
||||
if self.taken.setdefault(key, a) != a:
|
||||
self.unmapped.append((where, "%s%s (clashes with PAL %08X)" % (pre, hexpart, self.taken[key]))); return pre.replace("D_", "D_palonly_") + hexpart
|
||||
return pre + (fmt % ua)
|
||||
|
||||
def text(self, s, where, lv_ctx=None, literals=True):
|
||||
def nm(q):
|
||||
r = self.name(q.group(1), q.group(2), q.group(3), lv_ctx, where)
|
||||
return r
|
||||
s = NAME.sub(nm, s)
|
||||
if literals:
|
||||
def lit(q):
|
||||
h = q.group(1); a = int(h, 16)
|
||||
if not (0x00100000 <= a < 0x02000000): return q.group(0)
|
||||
# only what is surely an address: a section's edge, a function's first instruction, data
|
||||
# (a constant inside a function's range, such as a size of 0x200000, stays as it is)
|
||||
ua = self.rm.section_edge(a)
|
||||
if ua is None:
|
||||
core = CORE_TEXT[0] <= a < CORE_TEXT[1] # the Sony libraries: entries inside split units, same code in both
|
||||
ua = self.rm.level_addr(lv_ctx, a, interior=core) if lv_ctx else self.rm.exe_addr(a, interior=core)
|
||||
if ua is None: self.unmapped.append((where, "0x" + h)); return q.group(0)
|
||||
if ua != a: self.log.append((where, "0x%s -> 0x%08X" % (h, ua)))
|
||||
return "0x%0*X" % (len(h), ua)
|
||||
s = "".join(l if "region:keep" in l else LIT.sub(lit, l) for l in s.splitlines(True))
|
||||
return s
|
||||
|
||||
|
||||
def level_of(rel):
|
||||
q = re.search(r"overlays[/\\]l(\d\d)_", rel)
|
||||
return q.group(1) if q else None
|
||||
|
||||
|
||||
def write_repo(rm, rw, repo, out):
|
||||
"""the decomp, as the tools read it, in US addresses"""
|
||||
for sub in ("include", "src", "nonmatching", "tools"):
|
||||
top = os.path.join(repo, sub)
|
||||
for root, _, files in os.walk(top):
|
||||
for f in files:
|
||||
p = os.path.join(root, f); rel = os.path.relpath(p, repo); d = os.path.join(out, rel)
|
||||
os.makedirs(os.path.dirname(d), exist_ok=True)
|
||||
if sub == "tools" or not f.endswith((".c", ".h", ".inc", ".s")):
|
||||
shutil.copyfile(p, d); continue
|
||||
d_name = os.path.join(os.path.dirname(d), rw.text(f, rel, literals=False))
|
||||
open(d_name, "w").write(rw.text(open(p, errors="replace").read(), rel, level_of(rel)))
|
||||
os.makedirs(os.path.join(out, "config", "overlays"), exist_ok=True)
|
||||
os.makedirs(os.path.join(out, "progress"), exist_ok=True)
|
||||
# symbols
|
||||
for fn in ("config/symbol_addrs.txt", "config/port_symbols.txt"):
|
||||
p = os.path.join(repo, fn)
|
||||
if not os.path.exists(p): continue
|
||||
lines = []
|
||||
for l in open(p, errors="replace"):
|
||||
q = re.match(r"^(\s*)(\w+)(\s*=\s*)0x([0-9A-Fa-f]+)(.*)$", l.rstrip("\n"))
|
||||
if q:
|
||||
a = int(q.group(4), 16); ua = rm.exe_addr(a)
|
||||
if ua is None:
|
||||
rw.unmapped.append((fn, q.group(2))); continue
|
||||
l = "%s%s%s0x%08X%s" % (q.group(1), rw.text(q.group(2), fn, literals=False), q.group(3), ua, q.group(5))
|
||||
else:
|
||||
l = rw.text(l.rstrip("\n"), fn)
|
||||
lines.append(l)
|
||||
open(os.path.join(out, fn), "w").write("\n".join(lines) + "\n")
|
||||
for fn in ("config/names.tsv", "config/symbol_names.txt", "config/file_cflags.txt", "config/linker_remnants.txt", "config/handwritten_asm.txt"):
|
||||
p = os.path.join(repo, fn)
|
||||
if os.path.exists(p): open(os.path.join(out, fn), "w").write(rw.text(open(p, errors="replace").read(), fn, literals=False))
|
||||
# the executable's functions (progress/report.json): US addresses and sizes
|
||||
rep = json.load(open(os.path.join(repo, "progress", "report.json")))
|
||||
seen = set()
|
||||
for u in rep.get("units", []):
|
||||
fl = []
|
||||
for f in u.get("functions", []):
|
||||
md = f.get("metadata") or {}
|
||||
if "virtual_address" not in md: fl.append(f); continue
|
||||
a = int(md["virtual_address"])
|
||||
if a in rm.exe:
|
||||
ua, sz, _ = rm.exe[a]
|
||||
f = dict(f); f["name"] = rw.text(f["name"], "report.json", literals=False); f["size"] = str(sz)
|
||||
md = dict(md); md["virtual_address"] = str(ua); f["metadata"] = md
|
||||
fl.append(f); seen.add(ua)
|
||||
# (PAL-only: dropped)
|
||||
u["functions"] = fl
|
||||
extra = [{"name": "func_%08X" % ua, "size": str(sz), "metadata": {"virtual_address": str(ua)}}
|
||||
for ua, sz in rm.us_only if ua not in seen and sz >= 8]
|
||||
rep.setdefault("units", []).append({"name": "us_only", "functions": extra})
|
||||
json.dump(rep, open(os.path.join(out, "progress", "report.json"), "w"))
|
||||
# the US-only functions: assembly entries in a source file of their own
|
||||
with open(os.path.join(out, "src", "game", "us_only.c"), "w") as f:
|
||||
f.write('/* region.py: functions the US executable has and PAL does not (translated from the bytes) */\n#include "common.h"\n\n')
|
||||
for e in extra: f.write('INCLUDE_ASM("asm/us_only", %s);\n' % e["name"])
|
||||
# the level catalogue: each function's places in every level, in US addresses
|
||||
rows = []
|
||||
for l in open(os.path.join(repo, "config", "overlays", "functions.tsv")):
|
||||
if l.startswith("#") or not l.strip(): rows.append(l.rstrip("\n")); continue
|
||||
f = l.rstrip("\n").split("\t")
|
||||
pl, size = [], None
|
||||
for x in f[5].split(","):
|
||||
if ":" not in x: continue
|
||||
lv, a = x.split(":"); a = int(a, 16)
|
||||
ua = rm.level_func(lv, a)
|
||||
if ua is None: continue
|
||||
pl.append("%s:%08X" % (lv, ua))
|
||||
if size is None: size = rm.lv[lv][a][1]
|
||||
if not pl: continue
|
||||
name = rw.text(f[0], "functions.tsv", literals=False)
|
||||
if name.startswith(("func_palonly", "D_palonly")): # (named after a level copy the US build lacks: name it after one it has)
|
||||
lv0, a0 = pl[0].split(":"); name = "func_L%s_%s" % (lv0, a0)
|
||||
f[0], f[2], f[4], f[5] = name, str(size or f[2]), str(len({p.split(':')[0] for p in pl})), ",".join(pl)
|
||||
rows.append("\t".join(f))
|
||||
open(os.path.join(out, "config", "overlays", "functions.tsv"), "w").write("\n".join(rows) + "\n")
|
||||
for fn in ("config/overlays/names.tsv",):
|
||||
p = os.path.join(repo, fn)
|
||||
if os.path.exists(p): open(os.path.join(out, fn), "w").write(rw.text(open(p, errors="replace").read(), fn, literals=False))
|
||||
os.makedirs(os.path.join(out, "baserom"), exist_ok=True)
|
||||
|
||||
|
||||
# host sources that use the game's addresses (the rest: the platform, the GS, VU and movie code)
|
||||
HOST_GAME = ("src/rac1/", "src/sdk/", "overrides/", "src/main.c", "src/runtime.c", "include/")
|
||||
|
||||
|
||||
def write_host(rm, rw, src_host, out):
|
||||
for root, dirs, files in os.walk(src_host):
|
||||
dirs[:] = [d for d in dirs if d not in ("uya_only", "drafts")]
|
||||
for f in files:
|
||||
p = os.path.join(root, f); rel = os.path.relpath(p, src_host).replace(os.sep, "/")
|
||||
if rel.startswith("region/"):
|
||||
# host/region/<region>/: code only that region has, already in its addresses
|
||||
if rel.startswith("region/%s/" % REGION):
|
||||
d = os.path.join(out, "src", "region", rel[len("region/%s/" % REGION):])
|
||||
os.makedirs(os.path.dirname(d), exist_ok=True); shutil.copyfile(p, d)
|
||||
continue
|
||||
d = os.path.join(out, os.path.dirname(rel), rw.text(f, "host/" + rel, literals=False))
|
||||
os.makedirs(os.path.dirname(d), exist_ok=True)
|
||||
if not f.endswith((".c", ".h")): shutil.copyfile(p, d); continue
|
||||
s = open(p, errors="replace").read()
|
||||
s = s.replace("SCES_509.16", REGIONS[REGION]["exe"]) # the executable's file name on the disc
|
||||
open(d, "w").write(rw.text(s, "host/" + rel, literals=rel.startswith(HOST_GAME)) if rel.startswith(HOST_GAME) else s)
|
||||
|
||||
|
||||
REGION = None
|
||||
|
||||
|
||||
def main():
|
||||
global REGION
|
||||
region, repo, out = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
REGION = region
|
||||
cfg = REGIONS[region]
|
||||
rm = RegionMap(cfg["map"])
|
||||
rw = Rewriter(rm)
|
||||
shutil.rmtree(os.path.join(out, "repo"), ignore_errors=True); shutil.rmtree(os.path.join(out, "host"), ignore_errors=True)
|
||||
write_repo(rm, rw, repo, os.path.join(out, "repo"))
|
||||
write_host(rm, rw, os.path.join(HERE, "host"), os.path.join(out, "host"))
|
||||
with open(os.path.join(out, "region_report.txt"), "w") as f:
|
||||
f.write("region %s: %d literals rewritten, %d names and literals not mapped\n\n== not mapped\n" % (region, len(rw.log), len(rw.unmapped)))
|
||||
for w, x in sorted(set(rw.unmapped)): f.write("%s\t%s\n" % (w, x))
|
||||
f.write("\n== literals rewritten\n")
|
||||
for w, x in rw.log: f.write("%s\t%s\n" % (w, x))
|
||||
hosts = [(w, x) for w, x in set(rw.unmapped) if w.startswith("host/")]
|
||||
print("region %s: %d literals rewritten; not mapped: %d (host sources: %d) -> %s" % (
|
||||
region, len(rw.log), len(set(rw.unmapped)), len(hosts), os.path.join(out, "region_report.txt")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user