added ntscu-u support
This commit is contained in:
@@ -23,6 +23,8 @@ python3 project-solana build ~/Games/ratchet.iso # once: about 95 MB of downlo
|
|||||||
./project-solana desktop # adds it to the applications menu; from there, Steam > Add a Non-Steam Game
|
./project-solana desktop # adds it to the applications menu; from there, Steam > Add a Non-Steam Game
|
||||||
```
|
```
|
||||||
|
|
||||||
|
With both the PAL and the NTSC-U version built, `play` starts the one built or played last; `play --game pal` or `play --game us` picks one (`log` and `report` take `--game` too).
|
||||||
|
|
||||||
It needs `python3` and the **32-bit** SDL2 and OpenGL libraries. On SteamOS (Steam Deck, Steam Machine) nothing needs installing: the launcher uses the copy of SDL2 that comes with Steam. Elsewhere, `./project-solana status` says what to install for your distribution. Everything is kept in `~/.local/share/project-solana`. On a Steam Deck, run the build once in Desktop Mode (Konsole), then add the game to Steam with `desktop`; it starts full screen there (and on any SteamOS device), with the Deck's controls as the controller.
|
It needs `python3` and the **32-bit** SDL2 and OpenGL libraries. On SteamOS (Steam Deck, Steam Machine) nothing needs installing: the launcher uses the copy of SDL2 that comes with Steam. Elsewhere, `./project-solana status` says what to install for your distribution. Everything is kept in `~/.local/share/project-solana`. On a Steam Deck, run the build once in Desktop Mode (Konsole), then add the game to Steam with `desktop`; it starts full screen there (and on any SteamOS device), with the Deck's controls as the controller.
|
||||||
|
|
||||||
### Supported games
|
### Supported games
|
||||||
@@ -30,7 +32,8 @@ It needs `python3` and the **32-bit** SDL2 and OpenGL libraries. On SteamOS (Ste
|
|||||||
| Game | Region | Disc executable | Status |
|
| Game | Region | Disc executable | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Ratchet & Clank | PAL (v2.00) | `SCES_509.16`, MD5 `d86429e2eca4e1da7d7f1cc656af2574` | Playable start to finish |
|
| Ratchet & Clank | PAL (v2.00) | `SCES_509.16`, MD5 `d86429e2eca4e1da7d7f1cc656af2574` | Playable start to finish |
|
||||||
| Ratchet & Clank | NTSC-U, NTSC-J | `SCUS_971.99`, `SCPS_150.37` | Recognised, not supported yet |
|
| Ratchet & Clank | NTSC-U | `SCUS_971.99`, MD5 `0c081d4e0353b419d95e5f95eb0ebacb` | New: boots, menus and all 19 levels load; less tested than PAL |
|
||||||
|
| Ratchet & Clank | NTSC-J | `SCPS_150.37` | Recognised, not supported yet |
|
||||||
| Going Commando, Up Your Arsenal, Deadlocked | all | | Recognised, not supported yet |
|
| Going Commando, Up Your Arsenal, Deadlocked | all | | Recognised, not supported yet |
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
@@ -74,7 +77,9 @@ The launcher fetches from the release named after the game (`releases/download/r
|
|||||||
| `toolchain-linux64.zip` | the same for Linux x86-64 (the Linux launcher's) |
|
| `toolchain-linux64.zip` | the same for Linux x86-64 (the Linux launcher's) |
|
||||||
| `rac1-build.zip` | `games/rac1` plus the decomp's source as `decomp/` (no `baserom/`) |
|
| `rac1-build.zip` | `games/rac1` plus the decomp's source as `decomp/` (no `baserom/`) |
|
||||||
|
|
||||||
It checks both zips against their SHA-256s, unpacks them next to itself (`toolchain\`, `games\rac1\`), runs `build.py --platform windows --iso <your disc>`, and then deletes the intermediate files (about 1.3 GB) and the bytes it read from the disc, keeping `games\rac1\build\windows` (the game, its saves and logs).
|
It checks both zips against their SHA-256s, unpacks them next to itself (`toolchain\`, `games\rac1\`), runs `build.py --platform windows --region pal|us --iso <your disc>`, and then deletes the intermediate files (about 1.3 GB) and the bytes it read from the disc, keeping `games\rac1\build\windows` (PAL) or `games\rac1\build\windows_us` (NTSC-U): the game, its saves and logs. Both versions come from the same kit.
|
||||||
|
|
||||||
|
The NTSC-U build is made from the same sources: `tools/region.py` rewrites the decomp and the host engine from PAL addresses into NTSC-U ones with the address map `region/us.json` (functions paired by their code, data by the code that points at it; made by `dev/region_map.py`), and the code itself is translated from the NTSC-U executable and levels. `host/region/us/` holds the little the NTSC-U game needs that PAL's does not.
|
||||||
|
|
||||||
`solana.ini` `[build]` `release_url=` (a URL or a local folder laid out like the release) and `levels=` override where it fetches from and which levels it builds, for testing.
|
`solana.ini` `[build]` `release_url=` (a URL or a local folder laid out like the release) and `levels=` override where it fetches from and which levels it builds, for testing.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Ratchet & Clank (PAL v2.00): build kit
|
# Ratchet & Clank (PAL v2.00, NTSC-U): build kit
|
||||||
|
|
||||||
A static recompilation of Ratchet & Clank (`SCES_509.16`) for Windows (and Linux, for development).
|
A static recompilation of Ratchet & Clank (PAL `SCES_509.16`, and NTSC-U `SCUS_971.99` with `--region us`) for Windows and Linux.
|
||||||
|
|
||||||
`build.py` reads the game's executable and level programs from your disc image, generates C from them (the decomp's own C where it has it, everything else translated instruction by instruction by `tools/recomp.py`, the VU microcode by `tools/vu_recomp.py`), and compiles it with the host engine in `host/` into `build/windows/rac1_game.dll` and `rac1_wrapper.exe`.
|
`build.py` reads the game's executable and level programs from your disc image, generates C from them (the decomp's own C where it has it, everything else translated instruction by instruction by `tools/recomp.py`, the VU microcode by `tools/vu_recomp.py`), and compiles it with the host engine in `host/` into `build/windows/rac1_game.dll` and `rac1_wrapper.exe`.
|
||||||
|
|
||||||
@@ -8,6 +8,8 @@ A static recompilation of Ratchet & Clank (`SCES_509.16`) for Windows (and Linux
|
|||||||
python build.py --platform windows --iso "Ratchet & Clank (Europe).iso" --repo ../rac1-decomp --levels 0,1,...,18
|
python build.py --platform windows --iso "Ratchet & Clank (Europe).iso" --repo ../rac1-decomp --levels 0,1,...,18
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`--region us` builds the NTSC-U version into `build/<platform>_us` from the same sources: `tools/region.py` first writes the decomp and `host/` in NTSC-U addresses (`build/<platform>_us/region`, with `region_report.txt` listing what it changed and what had no counterpart), using the address map `region/us.json` (made by `dev/region_map.py` from both executables and both sets of levels). Code only the NTSC-U game has goes in `host/region/us/`, already in its addresses.
|
||||||
|
|
||||||
Needs Python 3, `pip install rabbitizer ziglang`. The Project Solana launcher runs this for players with its own copies of both.
|
Needs Python 3, `pip install rabbitizer ziglang`. The Project Solana launcher runs this for players with its own copies of both.
|
||||||
|
|
||||||
| Path | What |
|
| Path | What |
|
||||||
@@ -16,6 +18,7 @@ Needs Python 3, `pip install rabbitizer ziglang`. The Project Solana launcher ru
|
|||||||
| `tools/recomp.py` | MIPS (R5900) to C, the executable and each level |
|
| `tools/recomp.py` | MIPS (R5900) to C, the executable and each level |
|
||||||
| `tools/vu_recomp.py` | VU0/VU1 microprograms to C |
|
| `tools/vu_recomp.py` | VU0/VU1 microprograms to C |
|
||||||
| `tools/level_dump.py` | a level's program from the disc image |
|
| `tools/level_dump.py` | a level's program from the disc image |
|
||||||
|
| `tools/region.py`, `region/us.json` | the NTSC-U build: the sources rewritten in its addresses |
|
||||||
| `host/src/gs/` | the Graphics Synthesizer: software and OpenGL 3.3 (upscaling: `UYA_SCALE`) |
|
| `host/src/gs/` | the Graphics Synthesizer: software and OpenGL 3.3 (upscaling: `UYA_SCALE`) |
|
||||||
| `host/src/gfx/` | VIF, GIF and the VU runtime |
|
| `host/src/gfx/` | VIF, GIF and the VU runtime |
|
||||||
| `host/src/sdk/`, `host/src/rac1/` | Sony library entry points, 989snd and the SPU2, levels, boot |
|
| `host/src/sdk/`, `host/src/rac1/` | Sony library entry points, 989snd and the SPU2, levels, boot |
|
||||||
|
|||||||
+25
-11
@@ -25,13 +25,19 @@ ap.add_argument("-j", type=int, default=os.cpu_count() or 2)
|
|||||||
ap.add_argument("-O", default="2")
|
ap.add_argument("-O", default="2")
|
||||||
ap.add_argument("--iso", default=None)
|
ap.add_argument("--iso", default=None)
|
||||||
ap.add_argument("--levels", default="00")
|
ap.add_argument("--levels", default="00")
|
||||||
|
ap.add_argument("--region", default="pal", choices=("pal", "us"),
|
||||||
|
help="pal: SCES-50916 (the decomp's own); us: SCUS-97199, made from the same sources by tools/region.py")
|
||||||
a = ap.parse_args()
|
a = ap.parse_args()
|
||||||
|
sys.path.insert(0, os.path.join(HERE, "tools"))
|
||||||
|
import region as _region
|
||||||
|
RC = _region.consts(a.region)
|
||||||
|
EXE_NAME = RC["exe"]
|
||||||
|
|
||||||
zig = [shutil.which("zig")] if shutil.which("zig") else [sys.executable, "-m", "ziglang"]
|
zig = [shutil.which("zig")] if shutil.which("zig") else [sys.executable, "-m", "ziglang"]
|
||||||
# Linux: 32-bit glibc 2.17 or newer (any distribution since 2013), linked dynamically so it can load
|
# Linux: 32-bit glibc 2.17 or newer (any distribution since 2013), linked dynamically so it can load
|
||||||
# the system's SDL2 and OpenGL driver (32-bit: i386 libraries)
|
# the system's SDL2 and OpenGL driver (32-bit: i386 libraries)
|
||||||
target = {"linux": "x86-linux-gnu.2.17", "windows": "x86-windows-gnu"}[a.platform]
|
target = {"linux": "x86-linux-gnu.2.17", "windows": "x86-windows-gnu"}[a.platform]
|
||||||
out = os.path.join(HERE, "build", a.platform)
|
out = os.path.join(HERE, "build", a.platform if a.region == "pal" else "%s_%s" % (a.platform, a.region))
|
||||||
gen = os.path.join(out, "gen")
|
gen = os.path.join(out, "gen")
|
||||||
stage = os.path.join(out, "gen.new")
|
stage = os.path.join(out, "gen.new")
|
||||||
obj = os.path.join(out, "obj")
|
obj = os.path.join(out, "obj")
|
||||||
@@ -90,14 +96,22 @@ def iso_file(iso, name):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
env = dict(os.environ, UYA_PLATFORM=a.platform, RAC_PLATFORM=a.platform)
|
env = dict(os.environ, UYA_PLATFORM=a.platform, RAC_PLATFORM=a.platform, UYA_REGION=a.region)
|
||||||
|
# another region: the decomp and the host sources rewritten in its addresses (tools/region.py)
|
||||||
|
HOST = os.path.join(HERE, "host")
|
||||||
|
if a.region != "pal":
|
||||||
|
rdir = os.path.join(out, "region")
|
||||||
|
subprocess.run([sys.executable, os.path.join(HERE, "tools", "region.py"), a.region, a.repo, rdir], check=True)
|
||||||
|
a.repo = os.path.join(rdir, "repo")
|
||||||
|
HOST = os.path.join(rdir, "host")
|
||||||
|
env["UYA_HOST"] = HOST
|
||||||
# the static recompilation is built from the game itself: the executable comes
|
# the static recompilation is built from the game itself: the executable comes
|
||||||
# from the disc image (the decomp repo only lends its symbol and function lists)
|
# from the disc image (the decomp repo only lends its symbol and function lists)
|
||||||
if find_iso():
|
if find_iso():
|
||||||
_exe = iso_file(find_iso(), "SCES_509.16")
|
_exe = iso_file(find_iso(), EXE_NAME)
|
||||||
if _exe:
|
if _exe:
|
||||||
os.makedirs(os.path.join(HERE, "build", "disc"), exist_ok=True)
|
os.makedirs(os.path.join(HERE, "build", "disc"), exist_ok=True)
|
||||||
_p = os.path.join(HERE, "build", "disc", "SCES_509.16")
|
_p = os.path.join(HERE, "build", "disc", EXE_NAME)
|
||||||
if not os.path.exists(_p) or open(_p, "rb").read() != _exe: open(_p, "wb").write(_exe)
|
if not os.path.exists(_p) or open(_p, "rb").read() != _exe: open(_p, "wb").write(_exe)
|
||||||
env["UYA_EXE"] = _p
|
env["UYA_EXE"] = _p
|
||||||
print("executable read from the disc image:", find_iso())
|
print("executable read from the disc image:", find_iso())
|
||||||
@@ -108,7 +122,7 @@ def progress(phase, i, n):
|
|||||||
print("[progress] %s %d %d" % (phase, i, n), flush=True)
|
print("[progress] %s %d %d" % (phase, i, n), flush=True)
|
||||||
progress("prepare", 0, 3)
|
progress("prepare", 0, 3)
|
||||||
subprocess.run([py, os.path.join(HERE, "tools", "gen_host.py"), a.repo, stage], check=True, env=env)
|
subprocess.run([py, os.path.join(HERE, "tools", "gen_host.py"), a.repo, stage], check=True, env=env)
|
||||||
dumps = os.path.join(HERE, "build", "levels")
|
dumps = os.path.join(HERE, "build", "levels" if a.region == "pal" else "levels_" + a.region)
|
||||||
# the level programs first: the executable's translation looks at the core
|
# the level programs first: the executable's translation looks at the core
|
||||||
# addresses their code calls (tools/recomp.py)
|
# addresses their code calls (tools/recomp.py)
|
||||||
for lv in levels:
|
for lv in levels:
|
||||||
@@ -146,7 +160,7 @@ with open(os.path.join(stage, "level_table.c"), "w") as f:
|
|||||||
sync(stage, gen)
|
sync(stage, gen)
|
||||||
|
|
||||||
common = ["-target", target, "-O" + a.O, "-g", "-march=nehalem", "-mfpmath=sse", "-malign-double", "-fno-strict-aliasing", "-fwrapv",
|
common = ["-target", target, "-O" + a.O, "-g", "-march=nehalem", "-mfpmath=sse", "-malign-double", "-fno-strict-aliasing", "-fwrapv",
|
||||||
"-fno-PIE", "-fno-stack-protector", "-I", os.path.join(gen, "include"), "-I", os.path.join(HERE, "host", "include")]
|
"-fno-PIE", "-fno-stack-protector", "-I", os.path.join(gen, "include"), "-I", os.path.join(HOST, "include")]
|
||||||
if a.platform == "linux": common += ["-D_FILE_OFFSET_BITS=64"] # (32-bit glibc: disc images are over 2 GB)
|
if a.platform == "linux": common += ["-D_FILE_OFFSET_BITS=64"] # (32-bit glibc: disc images are over 2 GB)
|
||||||
# frontbin code: keep it permissive, it was written for gcc 2.95
|
# frontbin code: keep it permissive, it was written for gcc 2.95
|
||||||
menu_flags = ["-w", "-Wno-error=incompatible-pointer-types", "-Wno-error=int-conversion",
|
menu_flags = ["-w", "-Wno-error=incompatible-pointer-types", "-Wno-error=int-conversion",
|
||||||
@@ -168,9 +182,9 @@ jobs = [] # (source, extra flags, object name, forced header)
|
|||||||
for c in sorted(glob.glob(os.path.join(gen, "*.c"))):
|
for c in sorted(glob.glob(os.path.join(gen, "*.c"))):
|
||||||
jobs.append((c, menu_flags if os.path.basename(c) not in ("tramp_table.c", "level_table.c") else [], None, None))
|
jobs.append((c, menu_flags if os.path.basename(c) not in ("tramp_table.c", "level_table.c") else [], None, None))
|
||||||
jobs.append((os.path.join(gen, "abs_syms.s"), [], None, None))
|
jobs.append((os.path.join(gen, "abs_syms.s"), [], None, None))
|
||||||
for c in sorted(glob.glob(os.path.join(HERE, "host", "overrides", "*.c"))):
|
for c in sorted(glob.glob(os.path.join(HOST, "overrides", "*.c"))):
|
||||||
jobs.append((c, menu_flags, None, None))
|
jobs.append((c, menu_flags, None, None))
|
||||||
for c in sorted(glob.glob(os.path.join(HERE, "host", "src", "**", "*.c"), recursive=True)):
|
for c in sorted(glob.glob(os.path.join(HOST, "src", "**", "*.c"), recursive=True)):
|
||||||
jobs.append((c, ["-Wall", "-Wno-unused"], None, None))
|
jobs.append((c, ["-Wall", "-Wno-unused"], None, None))
|
||||||
for LV, *_ in level_info:
|
for LV, *_ in level_info:
|
||||||
lg = os.path.join(gen, "l" + LV)
|
lg = os.path.join(gen, "l" + LV)
|
||||||
@@ -181,7 +195,7 @@ for LV, *_ in level_info:
|
|||||||
jobs.append((os.path.join(lg, "abs_syms.s"), [], os.path.join("l" + LV, "abs_syms.s.o"), None))
|
jobs.append((os.path.join(lg, "abs_syms.s"), [], os.path.join("l" + LV, "abs_syms.s.o"), None))
|
||||||
# the executable's main-segment overrides that the level has: the level's copies
|
# the executable's main-segment overrides that the level has: the level's copies
|
||||||
for nm in open(os.path.join(lg, "overrides.txt")).read().split():
|
for nm in open(os.path.join(lg, "overrides.txt")).read().split():
|
||||||
c = os.path.join(HERE, "host", "overrides", nm + ".c")
|
c = os.path.join(HOST, "overrides", nm + ".c")
|
||||||
jobs.append((c, menu_flags + ["-include", hdr], os.path.join("l" + LV, "override_" + nm + ".c.o"), hdr))
|
jobs.append((c, menu_flags + ["-include", hdr], os.path.join("l" + LV, "override_" + nm + ".c.o"), hdr))
|
||||||
|
|
||||||
|
|
||||||
@@ -194,13 +208,13 @@ if not os.path.exists(_stamp) or open(_stamp).read() != _flags:
|
|||||||
flags_time = os.path.getmtime(_stamp)
|
flags_time = os.path.getmtime(_stamp)
|
||||||
|
|
||||||
# headers every generated file sees: a change rebuilds them all
|
# headers every generated file sees: a change rebuilds them all
|
||||||
headers_time = max([flags_time] + [os.path.getmtime(h) for h in glob.glob(os.path.join(gen, "include", "*.h")) + glob.glob(os.path.join(HERE, "host", "include", "*.h"))])
|
headers_time = max([flags_time] + [os.path.getmtime(h) for h in glob.glob(os.path.join(gen, "include", "*.h")) + glob.glob(os.path.join(HOST, "include", "*.h"))])
|
||||||
|
|
||||||
|
|
||||||
def cc(job):
|
def cc(job):
|
||||||
src, extra, oname, hdr = job
|
src, extra, oname, hdr = job
|
||||||
o = os.path.join(obj, oname or (os.path.basename(src) + ".o"))
|
o = os.path.join(obj, oname or (os.path.basename(src) + ".o"))
|
||||||
if os.path.exists(o) and src.endswith(".c") and os.path.join(HERE, "host") not in src:
|
if os.path.exists(o) and src.endswith(".c") and HOST not in src:
|
||||||
t = os.path.getmtime(o)
|
t = os.path.getmtime(o)
|
||||||
if t > os.path.getmtime(src) and t > headers_time and (not hdr or t > os.path.getmtime(hdr)):
|
if t > os.path.getmtime(src) and t > headers_time and (not hdr or t > os.path.getmtime(hdr)):
|
||||||
return o, None
|
return o, None
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""region_map.py DECOMP_REPO PAL_EXE US_EXE PAL_LEVELS US_LEVELS OUT_JSON
|
||||||
|
|
||||||
|
Developer tool (needs both executables): the PAL-to-US address map that lets the
|
||||||
|
build make the US version (SCUS-97199) from the same sources as PAL (SCES-50916).
|
||||||
|
The output is addresses only (no game bytes) and ships in the build kit as
|
||||||
|
region/us.json; tools/region.py reads it.
|
||||||
|
|
||||||
|
functions the executable's: OpenRAC's config/overlays/us_map.tsv (code alone),
|
||||||
|
PAL name -> US address and size, with how it was paired; and each
|
||||||
|
level's, from the same table and the catalogue's places
|
||||||
|
(config/overlays/functions.tsv), PAL place -> US place
|
||||||
|
data the executable's data sections: PAL and US words aligned (an address
|
||||||
|
in either one counts as equal: the pointers moved), giving runs of
|
||||||
|
PAL addresses that share one shift. Sections whose size did not
|
||||||
|
change move as one. Checked against the data addresses the paired
|
||||||
|
functions form with lui/addiu (both builds load the same data).
|
||||||
|
levels each level's data records (lit, bss, data, vtbl, camvtbl, sndvtbl)
|
||||||
|
aligned the same way (PAL_LEVELS and US_LEVELS: level_dump.py output)
|
||||||
|
"""
|
||||||
|
import json, os, re, struct, sys
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
|
||||||
|
REPO, PAL_EXE, US_EXE, PAL_LV, US_LV, OUT = sys.argv[1:7]
|
||||||
|
pal, us = open(PAL_EXE, "rb").read(), open(US_EXE, "rb").read()
|
||||||
|
|
||||||
|
|
||||||
|
def sections(e):
|
||||||
|
shoff, _, shnum, shstr = struct.unpack_from("<I", e, 32)[0], *struct.unpack_from("<HHH", e, 46)
|
||||||
|
S = [struct.unpack_from("<10I", e, shoff + i * 40) for i in range(shnum)]
|
||||||
|
names = e[S[shstr][4]:S[shstr][4] + S[shstr][5]]
|
||||||
|
out = {}
|
||||||
|
for s in S:
|
||||||
|
n = names[s[0]:names.index(b"\0", s[0])].decode()
|
||||||
|
if s[3]: out[n] = {"addr": s[3], "off": s[4], "size": s[5], "nobits": s[1] == 8}
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
SP, SU = sections(pal), sections(us)
|
||||||
|
|
||||||
|
|
||||||
|
def seg_bytes(e, a, n):
|
||||||
|
phoff, phnum = struct.unpack_from("<I", e, 28)[0], struct.unpack_from("<H", e, 44)[0]
|
||||||
|
for i in range(phnum):
|
||||||
|
t, off, va, pa, fs = struct.unpack_from("<5I", e, phoff + 32 * i)
|
||||||
|
if va <= a and a + n <= va + fs: return e[off + a - va: off + a - va + n]
|
||||||
|
|
||||||
|
|
||||||
|
# ---- functions -----------------------------------------------------------------------------------
|
||||||
|
fsize = {}
|
||||||
|
for u in json.load(open(os.path.join(REPO, "progress", "report.json")))["units"]:
|
||||||
|
for f in u.get("functions", []):
|
||||||
|
md = f.get("metadata") or {}
|
||||||
|
if "virtual_address" in md: fsize[f["name"]] = (int(md["virtual_address"]), int(f["size"]))
|
||||||
|
places = {} # catalogue name -> {level: [PAL addresses]}
|
||||||
|
for l in open(os.path.join(REPO, "config", "overlays", "functions.tsv")):
|
||||||
|
if l.startswith("#") or not l.strip(): continue
|
||||||
|
f = l.rstrip("\n").split("\t")
|
||||||
|
d = defaultdict(list)
|
||||||
|
for x in f[5].split(","):
|
||||||
|
if ":" in x: d[x.split(":")[0]].append(int(x.split(":")[1], 16))
|
||||||
|
places[f[0]] = d
|
||||||
|
|
||||||
|
exe_funcs, exe_us_only = {}, []
|
||||||
|
lv_rows = defaultdict(list)
|
||||||
|
for l in open(os.path.join(REPO, "config", "overlays", "us_map.tsv")):
|
||||||
|
if l.startswith("#") or not l.strip(): continue
|
||||||
|
prog, ua, sz, name, method, sim = l.rstrip("\n").split("\t")[:6]
|
||||||
|
ua, sz = int(ua, 16), int(sz)
|
||||||
|
if prog == "boot":
|
||||||
|
q = re.fullmatch(r"func_([0-9A-F]{8})", name)
|
||||||
|
if q: exe_funcs["%08X" % int(q.group(1), 16)] = [ua, sz, method]
|
||||||
|
else: exe_us_only.append([ua, sz])
|
||||||
|
else:
|
||||||
|
lv_rows[prog[6:]].append((ua, sz, name, method))
|
||||||
|
levels = {}
|
||||||
|
for lv, rows in sorted(lv_rows.items()):
|
||||||
|
by_name = defaultdict(list)
|
||||||
|
only = []
|
||||||
|
for ua, sz, name, method in sorted(rows):
|
||||||
|
if name in places and places[name].get(lv): by_name[name].append((ua, sz, method))
|
||||||
|
else: only.append([ua, sz])
|
||||||
|
m = {}
|
||||||
|
for name, us_pl in by_name.items():
|
||||||
|
for pa, (ua, sz, method) in zip(sorted(places[name][lv]), us_pl): # several copies: paired in address order
|
||||||
|
m["%08X" % pa] = [ua, sz, method]
|
||||||
|
levels[lv] = {"funcs": m, "us_only": only}
|
||||||
|
|
||||||
|
# ---- data ----------------------------------------------------------------------------------------
|
||||||
|
def words(b): return list(struct.unpack("<%dI" % (len(b) // 4), b[:len(b) // 4 * 4]))
|
||||||
|
def tok(w): return -1 if 0x00100000 <= w < 0x02000000 else w
|
||||||
|
|
||||||
|
|
||||||
|
def align(a_words, b_words, a_base, b_base, W=8):
|
||||||
|
"""runs [pal_lo, pal_hi, shift] (bytes) of a PAL range onto a US range"""
|
||||||
|
a, b = [tok(x) for x in a_words], [tok(x) for x in b_words]
|
||||||
|
d, runs, i = 0, [], 0
|
||||||
|
while i < len(a):
|
||||||
|
if 0 <= i + d < len(b) and a[i] == b[i + d]:
|
||||||
|
if runs and runs[-1][2] == d and runs[-1][1] == i: runs[-1][1] = i + 1
|
||||||
|
else: runs.append([i, i + 1, d])
|
||||||
|
i += 1; continue
|
||||||
|
best = None
|
||||||
|
for k in range(0, 1024):
|
||||||
|
for dd in (d - k, d + k):
|
||||||
|
if 0 <= i + dd and i + dd + W <= len(b) and a[i:i + W] == b[i + dd:i + dd + W] and \
|
||||||
|
sum(1 for x in a[i:i + W] if x not in (0, -1)) >= 3:
|
||||||
|
best = dd; break
|
||||||
|
if best is not None: break
|
||||||
|
if best is None: i += 1; continue
|
||||||
|
d = best
|
||||||
|
# runs of a few words that only match zeros or pointers are noise: drop the short ones
|
||||||
|
# between two runs of one shift
|
||||||
|
out = []
|
||||||
|
for s, e, dd in runs:
|
||||||
|
sh = (b_base + 4 * (s + dd)) - (a_base + 4 * s)
|
||||||
|
if out and out[-1][2] == sh: out[-1][1] = a_base + 4 * e
|
||||||
|
else: out.append([a_base + 4 * s, a_base + 4 * e, sh])
|
||||||
|
clean = []
|
||||||
|
for k, r in enumerate(out):
|
||||||
|
if 0 < k < len(out) - 1 and r[1] - r[0] <= 16 and out[k - 1][2] == out[k + 1][2] != r[2]:
|
||||||
|
continue
|
||||||
|
if clean and clean[-1][2] == r[2]: clean[-1][1] = r[1]
|
||||||
|
else: clean.append(r)
|
||||||
|
# close the gaps: a gap takes the shift of the run before it
|
||||||
|
for k in range(len(clean) - 1): clean[k][1] = clean[k + 1][0]
|
||||||
|
if clean: clean[0][0], clean[-1][1] = a_base, a_base + 4 * len(a_words)
|
||||||
|
return clean
|
||||||
|
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for name in ("core.data", "core.rdata", "core.bss", "core.lit", ".lit", ".bss", ".data", "lvl.vtbl", "lvl.camvtbl", "lvl.sndvtbl"):
|
||||||
|
p, u = SP[name], SU[name]
|
||||||
|
if p["nobits"]: # (no bytes to compare: it moves as one; checked against the code below)
|
||||||
|
data.append([p["addr"], p["addr"] + p["size"], u["addr"] - p["addr"], name])
|
||||||
|
continue
|
||||||
|
for lo, hi, sh in align(words(pal[p["off"]:p["off"] + p["size"]]), words(us[u["off"]:u["off"] + u["size"]]), p["addr"], u["addr"]):
|
||||||
|
data.append([lo, hi, sh, name])
|
||||||
|
|
||||||
|
|
||||||
|
def data_shift(a):
|
||||||
|
for lo, hi, sh, _ in data:
|
||||||
|
if lo <= a < hi: return sh
|
||||||
|
|
||||||
|
|
||||||
|
# the data addresses paired functions build (lui + addiu/ori/load/store): both builds load the
|
||||||
|
# same data, so each is a PAL -> US pair. The executable's functions and every level's (level code
|
||||||
|
# uses the executable's data a lot).
|
||||||
|
def refs(b):
|
||||||
|
w = struct.unpack("<%dI" % (len(b) // 4), b); hi = {}; out = {}
|
||||||
|
for i, x in enumerate(w):
|
||||||
|
op, rs, rt, imm = x >> 26, (x >> 21) & 31, (x >> 16) & 31, x & 0xFFFF
|
||||||
|
if op == 0x0F: hi[rt] = imm << 16; continue
|
||||||
|
if rs in hi and (op == 0x09 or 0x20 <= op <= 0x3F or op in (0x1A, 0x1B, 0x1E, 0x1F)):
|
||||||
|
out[i] = (hi[rs] + (imm - 0x10000 if imm & 0x8000 else imm)) & 0xFFFFFFFF
|
||||||
|
if op in (0x09, 0x0D, 0x0F) or 0x20 <= op <= 0x27: hi.pop(rt, None)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
anchor_votes = defaultdict(Counter)
|
||||||
|
RESIDENT = 0x0015F000 # PAL: from here up, each level program has its own lit/bss/data/tables
|
||||||
|
def collect(b1, b2, top):
|
||||||
|
if not b1 or not b2: return
|
||||||
|
r1, r2 = refs(b1), refs(b2)
|
||||||
|
for i, x in r1.items():
|
||||||
|
if i in r2 and SP["core.data"]["addr"] <= x < top:
|
||||||
|
anchor_votes[x][r2[i] - x] += 1
|
||||||
|
|
||||||
|
|
||||||
|
for pa_s, (ua, sz, method) in exe_funcs.items():
|
||||||
|
if method == "fingerprint":
|
||||||
|
pa = int(pa_s, 16); collect(seg_bytes(pal, pa, sz), seg_bytes(us, ua, sz), SP[".text"]["addr"])
|
||||||
|
lv_text = {}
|
||||||
|
for lv in levels:
|
||||||
|
try:
|
||||||
|
tp = [r for r in json.load(open(os.path.join(PAL_LV, "level_" + lv, "manifest.json")))["records"] if r["name"] == "text"][0]
|
||||||
|
tu = [r for r in json.load(open(os.path.join(US_LV, "level_" + lv, "manifest.json")))["records"] if r["name"] == "text"][0]
|
||||||
|
except (OSError, IndexError):
|
||||||
|
continue
|
||||||
|
bp = open(os.path.join(PAL_LV, "level_" + lv, "text.bin"), "rb").read(); bu = open(os.path.join(US_LV, "level_" + lv, "text.bin"), "rb").read()
|
||||||
|
lv_text[lv] = (tp["address"], bp, tu["address"], bu)
|
||||||
|
for pa_s, (ua, sz, method) in levels[lv]["funcs"].items():
|
||||||
|
if method != "fingerprint": continue
|
||||||
|
pa = int(pa_s, 16)
|
||||||
|
collect(bp[pa - tp["address"]:pa - tp["address"] + sz], bu[ua - tu["address"]:ua - tu["address"] + sz], RESIDENT)
|
||||||
|
|
||||||
|
# data sections move down (by 0x100 to 0x400): a "shift" outside that is a constant, not an address
|
||||||
|
def align_shift(a):
|
||||||
|
for lo, hi, sh, _ in data:
|
||||||
|
if lo <= a < hi: return sh
|
||||||
|
|
||||||
|
|
||||||
|
anchors = {}
|
||||||
|
for x, c in anchor_votes.items():
|
||||||
|
sh, n = c.most_common(1)[0]
|
||||||
|
if not (-0x600 <= sh <= -0x40 and sh % 4 == 0): continue
|
||||||
|
# trusted when several references agree and none disagrees, or when the bytes agree
|
||||||
|
# (a lone or contested pair is often base + a constant the compiler folded differently)
|
||||||
|
if (n >= 2 and len(c) == 1) or sh == align_shift(x): anchors[x] = sh
|
||||||
|
|
||||||
|
|
||||||
|
def build(sec_runs):
|
||||||
|
"""the final runs for one section: anchors where the code has them; between two anchors of
|
||||||
|
one shift, that shift; between anchors of different shifts, the alignment's boundary if it
|
||||||
|
has one there, else halfway"""
|
||||||
|
lo, hi = sec_runs[0][0], sec_runs[-1][1]
|
||||||
|
pts = sorted((a, s) for a, s in anchors.items() if lo <= a < hi)
|
||||||
|
if not pts: return [r[:3] for r in sec_runs]
|
||||||
|
def align_at(a):
|
||||||
|
for r in sec_runs:
|
||||||
|
if r[0] <= a < r[1]: return r[2]
|
||||||
|
out = []
|
||||||
|
def put(a0, a1, sh):
|
||||||
|
if a0 >= a1: return
|
||||||
|
if out and out[-1][2] == sh and out[-1][1] == a0: out[-1][1] = a1
|
||||||
|
else: out.append([a0, a1, sh])
|
||||||
|
# before the first anchor: the alignment, as long as it agrees with that anchor, else the anchor's
|
||||||
|
put(lo, pts[0][0], pts[0][1])
|
||||||
|
for (a, s), (b, t) in zip(pts, pts[1:]):
|
||||||
|
if s == t: put(a, b, s); continue
|
||||||
|
# the boundary: the first address from a where the alignment says t
|
||||||
|
cut = None
|
||||||
|
for r in sec_runs:
|
||||||
|
if r[2] == t and a < r[0] <= b: cut = r[0]; break
|
||||||
|
if cut is None: cut = (a + (b - a) // 2) & ~3
|
||||||
|
put(a, cut, s); put(cut, b, t)
|
||||||
|
put(pts[-1][0], hi, pts[-1][1])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
final = []
|
||||||
|
for name in ("core.data", "core.rdata", "core.bss", "core.lit", ".lit", ".bss", ".data", "lvl.vtbl", "lvl.camvtbl", "lvl.sndvtbl"):
|
||||||
|
rs = [r for r in data if r[3] == name]
|
||||||
|
for r in build(rs): final.append(r + [name])
|
||||||
|
data = final
|
||||||
|
|
||||||
|
|
||||||
|
def data_shift(a):
|
||||||
|
for lo, hi, sh, _ in data:
|
||||||
|
if lo <= a < hi: return sh
|
||||||
|
|
||||||
|
|
||||||
|
agree = sum(1 for x, s in anchors.items() if data_shift(x) == s)
|
||||||
|
print("data map: %d runs; %d code anchors (%d agree with the map)" % (len(data), len(anchors), agree))
|
||||||
|
for lo, hi, sh, n in data[:80]: print(" %-11s %08X-%08X %+X" % (n, lo, hi, sh))
|
||||||
|
|
||||||
|
# ---- level data ----------------------------------------------------------------------------------
|
||||||
|
level_data = {}
|
||||||
|
for lv in sorted(levels):
|
||||||
|
pm = os.path.join(PAL_LV, "level_" + lv, "manifest.json"); um = os.path.join(US_LV, "level_" + lv, "manifest.json")
|
||||||
|
if not (os.path.exists(pm) and os.path.exists(um)): continue
|
||||||
|
P = {r["name"]: r for r in json.load(open(pm))["records"]}; U = {r["name"]: r for r in json.load(open(um))["records"]}
|
||||||
|
runs = []
|
||||||
|
for rec in ("lit", "bss", "data", "vtbl", "camvtbl", "sndvtbl"):
|
||||||
|
p, u = P[rec], U[rec]
|
||||||
|
if p["bytes"] == u["bytes"] or rec == "bss":
|
||||||
|
runs.append([p["address"], p["address"] + p["bytes"], u["address"] - p["address"], rec]); continue
|
||||||
|
pb = open(os.path.join(PAL_LV, "level_" + lv, rec + ".bin"), "rb").read()
|
||||||
|
ub = open(os.path.join(US_LV, "level_" + lv, rec + ".bin"), "rb").read()
|
||||||
|
for lo, hi, sh in align(words(pb), words(ub), p["address"], u["address"]): runs.append([lo, hi, sh, rec])
|
||||||
|
level_data[lv] = runs
|
||||||
|
|
||||||
|
json.dump({"region": "us", "exe": "SCUS_971.99", "pal_exe": "SCES_509.16",
|
||||||
|
"sections": {n: [SP[n]["addr"], SP[n]["size"], SU[n]["addr"], SU[n]["size"]] for n in SP if n in SU},
|
||||||
|
"exe_funcs": exe_funcs, "exe_us_only": exe_us_only, "data": data,
|
||||||
|
"levels": levels, "level_data": level_data}, open(OUT, "w"), separators=(",", ":"))
|
||||||
|
print("wrote %s: %d executable functions (%d US-only), %d levels" % (OUT, len(exe_funcs), len(exe_us_only), len(levels)))
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/* NTSC-U (SCUS_971.99) only: library functions the PAL game does not have.
|
||||||
|
* Written in US addresses; tools/region.py copies this folder into the US
|
||||||
|
* host tree as it is (no rewriting) and the PAL build never sees it. */
|
||||||
|
#include "uya_host.h"
|
||||||
|
#include "uya_runtime.h"
|
||||||
|
|
||||||
|
s32 rac1_mc_chdir(s32 port, s32 slot, const char *dir, char *prev);
|
||||||
|
|
||||||
|
/* 0x001240C0: sceMcChdir(port, slot, newDir, curDir), RPC 0x0C (its sync
|
||||||
|
* callback 0x00124038 copies the previous directory into curDir) */
|
||||||
|
s32 func_001240C0(s32 port, s32 slot, char *dir, char *cur) { return rac1_mc_chdir(port, slot, dir, cur); }
|
||||||
@@ -107,7 +107,7 @@ s32 func_00118E70(s32 reg) { (void)reg; return 0x00070000; } /* sceSifGe
|
|||||||
* game sends the data with sceSifSetDma (above). Without it every stash
|
* game sends the data with sceSifSetDma (above). Without it every stash
|
||||||
* slot was refused (size 0) and the pause menu unpacked the weapon data
|
* slot was refused (size 0) and the pause menu unpacked the weapon data
|
||||||
* from a scrubbed buffer (a crash reading 0xDEE6BEEF). ---------- */
|
* from a scrubbed buffer (a crash reading 0xDEE6BEEF). ---------- */
|
||||||
#define STASH_BASE 0x00160000u
|
#define STASH_BASE 0x00160000u /* an IOP address (region:keep) */
|
||||||
#define STASH_SIZE 0x00094000u
|
#define STASH_SIZE 0x00094000u
|
||||||
void func_0011AE20(s32 mode) { (void)mode; } /* sceSifInitRpc */
|
void func_0011AE20(s32 mode) { (void)mode; } /* sceSifInitRpc */
|
||||||
s32 func_0011B2F8(void *cd, u32 server, s32 mode) /* sceSifBindRpc */
|
s32 func_0011B2F8(void *cd, u32 server, s32 mode) /* sceSifBindRpc */
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ const char *rc_stack_scan(void)
|
|||||||
t[0] = 0;
|
t[0] = 0;
|
||||||
for (int i = 0; sp && i < 1024 && k < 12; i += 2) {
|
for (int i = 0; sp && i < 1024 && k < 12; i += 2) {
|
||||||
u32 v = sp[i];
|
u32 v = sp[i];
|
||||||
if (v >= 0x00112380u && v < 0x00300000u && !(v & 3) && (v & 0xFFFF) && v != 0x00160000u && v != 0x00170000u) {
|
if (v >= 0x00112380u && v < 0x00300000u && !(v & 3) && (v & 0xFFFF) && v != 0x00160000u && v != 0x00170000u) { /* region:keep */
|
||||||
n += snprintf(t + n, sizeof t - n, " %08X@%d", v, i * 4); k++;
|
n += snprintf(t + n, sizeof t - n, " %08X@%d", v, i * 4); k++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ static u32 bank_add(const u8 *src, u32 size)
|
|||||||
b->nsounds = (s16)rd16(b->sblk + 0x16);
|
b->nsounds = (s16)rd16(b->sblk + 0x16);
|
||||||
b->first_sound = rd32(b->sblk + 0x1C);
|
b->first_sound = rd32(b->sblk + 0x1C);
|
||||||
b->first_grain = rd32(b->sblk + 0x20);
|
b->first_grain = rd32(b->sblk + 0x20);
|
||||||
b->handle = 0x00120000u + (u32)i * 0x1000u + ((++bank_serial) & 0xFFF);
|
b->handle = 0x00120000u + (u32)i * 0x1000u + ((++bank_serial) & 0xFFF); /* region:keep */
|
||||||
if (rd32(b->sblk + 4) != 1) uya_log("989snd: SBlk version %u (only 1 is expected)", rd32(b->sblk + 4));
|
if (rd32(b->sblk + 4) != 1) uya_log("989snd: SBlk version %u (only 1 is expected)", rd32(b->sblk + 4));
|
||||||
uya_log("989snd: bank %08X loaded: %d sounds, %u KB of samples", b->handle, b->nsounds, b->smp_size / 1024);
|
uya_log("989snd: bank %08X loaded: %d sounds, %u KB of samples", b->handle, b->nsounds, b->smp_size / 1024);
|
||||||
if (snd_log > 0 && getenv("UYA_DUMP")) { /* for checking: the bank as loaded */
|
if (snd_log > 0 && getenv("UYA_DUMP")) { /* for checking: the bank as loaded */
|
||||||
|
|||||||
@@ -96,11 +96,15 @@ static int glob_match(const char *p, const char *s)
|
|||||||
/* Card path ("/DIR/file" or "DIR/file") to a host path. Wildcards in the
|
/* Card path ("/DIR/file" or "DIR/file") to a host path. Wildcards in the
|
||||||
* directory part pick the first directory that matches. Returns 0 if a
|
* directory part pick the first directory that matches. Returns 0 if a
|
||||||
* wildcard directory had no match. */
|
* wildcard directory had no match. */
|
||||||
|
static char mc_cwd[2][512] = {"/", "/"}; /* sceMcChdir's current directory (the NTSC-U game sets it) */
|
||||||
static int host_path(int port, const char *name, char *out, size_t n, char *last, size_t nlast)
|
static int host_path(int port, const char *name, char *out, size_t n, char *last, size_t nlast)
|
||||||
{
|
{
|
||||||
char tmp[512], cur[1024];
|
char tmp[512], cur[1024];
|
||||||
snprintf(cur, sizeof cur, "%s", slot_dir(port));
|
snprintf(cur, sizeof cur, "%s", slot_dir(port));
|
||||||
snprintf(tmp, sizeof tmp, "%s", name ? name : "");
|
if (name && name[0] != '/' && strcmp(mc_cwd[port & 1], "/"))
|
||||||
|
snprintf(tmp, sizeof tmp, "%s/%s", mc_cwd[port & 1], name); /* relative to the current directory */
|
||||||
|
else
|
||||||
|
snprintf(tmp, sizeof tmp, "%s", name ? name : "");
|
||||||
char *parts[16]; int np = 0;
|
char *parts[16]; int np = 0;
|
||||||
for (char *t = strtok(tmp, "/"); t && np < 16; t = strtok(0, "/")) parts[np++] = t;
|
for (char *t = strtok(tmp, "/"); t && np < 16; t = strtok(0, "/")) parts[np++] = t;
|
||||||
for (int i = 0; i < np - 1; i++) {
|
for (int i = 0; i < np - 1; i++) {
|
||||||
@@ -177,6 +181,32 @@ s32 uyafn_12E400(s32 port, s32 slot, s32 *type, s32 *free_kb, s32 *format)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* sceMcChdir(port, slot, new, cur*) (RPC 0x0C): the card's current directory
|
||||||
|
* for relative names; cur, if given, gets the previous one. PAL's game never
|
||||||
|
* calls it; NTSC-U's sets "/" before listing (host/region/us maps it). */
|
||||||
|
s32 rac1_mc_chdir(s32 port, s32 slot, const char *dir, char *prev)
|
||||||
|
{
|
||||||
|
(void)slot;
|
||||||
|
if (mclog()) uya_log("memory card: chdir %s", dir ? dir : "(null)");
|
||||||
|
if (!card_present(port)) { finish(0xC, -10); return 0; }
|
||||||
|
if (prev) snprintf(prev, 1024, "%s", mc_cwd[port & 1]);
|
||||||
|
char want[512];
|
||||||
|
if (!dir || !*dir) { finish(0xC, 0); return 0; }
|
||||||
|
if (dir[0] == '/') snprintf(want, sizeof want, "%s", dir);
|
||||||
|
else if (!strcmp(dir, "..")) {
|
||||||
|
snprintf(want, sizeof want, "%s", mc_cwd[port & 1]);
|
||||||
|
char *sl = strrchr(want, '/'); if (sl && sl != want) *sl = 0; else snprintf(want, sizeof want, "/");
|
||||||
|
} else snprintf(want, sizeof want, "%s%s%s", mc_cwd[port & 1], strcmp(mc_cwd[port & 1], "/") ? "/" : "", dir);
|
||||||
|
size_t l = strlen(want);
|
||||||
|
while (l > 1 && want[l - 1] == '/') want[--l] = 0;
|
||||||
|
char hp[1024];
|
||||||
|
snprintf(hp, sizeof hp, "%s%s", slot_dir(port), want);
|
||||||
|
if (!is_dir(hp)) { finish(0xC, -4); return 0; } /* no such directory */
|
||||||
|
snprintf(mc_cwd[port & 1], sizeof mc_cwd[0], "%s", want);
|
||||||
|
finish(0xC, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* sceMcGetDir table entry (sceMcTblGetDir), 0x40 bytes */
|
/* sceMcGetDir table entry (sceMcTblGetDir), 0x40 bytes */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
u8 create[8], modify[8];
|
u8 create[8], modify[8];
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -48,9 +48,15 @@ while _a:
|
|||||||
LV = "%02d" % LEVEL if LEVEL is not None else None
|
LV = "%02d" % LEVEL if LEVEL is not None else None
|
||||||
SFX = "_l%s" % LV if LEVEL is not None else ""
|
SFX = "_l%s" % LV if LEVEL is not None else ""
|
||||||
HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
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 _)
|
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)
|
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)
|
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()
|
host_defined = set()
|
||||||
STATIC = os.environ.get("UYA_STATIC", "1") != "0" # RAC1_STATICRECOMP: translate the whole game from its bytes
|
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))
|
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)
|
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)
|
MAIN_TEXT = RC["main_text"] # the executable's game program (.text of the main segment); PAL 0x001E9080-0x0023E740
|
||||||
RESIDENT = 0x0015F000 # below: core segment and core_lit, the same in every program
|
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
|
PLACES = {} # level mode: catalogue name -> its addresses in this level
|
||||||
CAT_KIND = {}
|
CAT_KIND = {}
|
||||||
CAT_SIZE = {}
|
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 []):
|
for h in (glob.glob(os.path.join(REPO, "include", "*.h")) if LEVEL is None else []):
|
||||||
bn = os.path.basename(h)
|
bn = os.path.basename(h)
|
||||||
if bn == "common.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":
|
elif bn == "include_asm.h":
|
||||||
t = "/* host: assembly inclusion has no meaning on the PC */\n"
|
t = "/* host: assembly inclusion has no meaning on the PC */\n"
|
||||||
else:
|
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(), [], {}
|
stubbed, defined, asm_entries, report = [], set(), [], {}
|
||||||
gen_texts = {}
|
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()
|
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", "*.c")))
|
||||||
files += sorted(os.path.relpath(p, REPO) for p in glob.glob(os.path.join(REPO, "src", "game", "movie", "*.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)
|
# 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__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
from source_patches import PATCHES
|
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
|
patches_applied = 0
|
||||||
# names.h: readable names are macros for the address names; expand them in
|
# 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.
|
# 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,
|
# 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).
|
# !file (keep that file's C), e.g. "help_0020CDF0,help_00232560" (the old default).
|
||||||
_fa = os.environ.get("UYA_FORCE_ASM", "re:.")
|
_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("!")]
|
_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
|
if any(x and x in rel for x in _ex): _fa = "" # "!file-substring": that file keeps its C
|
||||||
for tok in _fa.split(","):
|
for tok in _fa.split(","):
|
||||||
@@ -483,7 +493,9 @@ for rel in files:
|
|||||||
seg = "/* host: top-level asm removed */"
|
seg = "/* host: top-level asm removed */"
|
||||||
else:
|
else:
|
||||||
name, bpos = extra
|
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)
|
dropped.append(name)
|
||||||
out.append("/* host: %s is not in level %s's program */" % (name, LV)); pos = e
|
out.append("/* host: %s is not in level %s's program */" % (name, LV)); pos = e
|
||||||
continue
|
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))
|
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
|
# 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
|
# 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()
|
ht = open(hp, errors="replace").read()
|
||||||
hm = strip_comments_keep_len(ht)
|
hm = strip_comments_keep_len(ht)
|
||||||
for q in HDR.finditer(hm):
|
for q in HDR.finditer(hm):
|
||||||
@@ -808,7 +820,7 @@ for n, p in defs.items():
|
|||||||
a = ps2_addr(n)
|
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):
|
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
|
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()
|
ot = open(op_, errors="replace").read()
|
||||||
for q in HDR.finditer(strip_comments_keep_len(ot)):
|
for q in HDR.finditer(strip_comments_keep_len(ot)):
|
||||||
n = q.group(1); a = ps2_addr(n)
|
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),
|
# 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.
|
# 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)
|
_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:
|
if _r64:
|
||||||
_names = "|".join(re.escape(n) for n in _r64)
|
_names = "|".join(re.escape(n) for n in _r64)
|
||||||
_INT = r"(?:int|s32|u32|unsigned int|unsigned|signed int|signed)"
|
_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]
|
GEN, REPO = sys.argv[1], sys.argv[2]
|
||||||
HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
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
|
LEVEL = DUMP = EXEGEN = None
|
||||||
_a = sys.argv[3:]
|
_a = sys.argv[3:]
|
||||||
while _a:
|
while _a:
|
||||||
@@ -53,14 +58,15 @@ while _a:
|
|||||||
else: sys.exit("recomp.py: unknown option " + _o)
|
else: sys.exit("recomp.py: unknown option " + _o)
|
||||||
LV = "%02d" % LEVEL if LEVEL is not None else None
|
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
|
P = "rc_" if LEVEL is None else "rc%s_" % LV # prefix of the generated symbols
|
||||||
RESIDENT = 0x0015F000
|
RESIDENT = RC["resident"] # PAL 0x0015F000
|
||||||
|
|
||||||
# ---- inputs -----------------------------------------------------------------
|
# ---- inputs -----------------------------------------------------------------
|
||||||
# testing: UYA_RC_TRACE="func_A,func_B" (build time) logs every call of those translated functions
|
# 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)
|
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()
|
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
|
LTEXT_LO = LTEXT_HI = 0
|
||||||
if LEVEL is not None:
|
if LEVEL is not None:
|
||||||
_ld = os.path.join(DUMP, "level_%s" % LV)
|
_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"]))
|
fsize[f["name"]] = (int(md["virtual_address"]), int(f["size"]))
|
||||||
sigs = json.load(open(os.path.join(GEN, "sigs.json")))
|
sigs = json.load(open(os.path.join(GEN, "sigs.json")))
|
||||||
entries = json.load(open(os.path.join(GEN, "asm_entries.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)
|
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 not (n in sigs and sigs[n].get("src") == "def")
|
||||||
and TEXT_LO <= fsize[n][0] < TEXT_HI]
|
and TEXT_LO <= fsize[n][0] < TEXT_HI]
|
||||||
# Sony libraries that are pure computation are translated too (libvu0: the
|
# Sony libraries that are pure computation are translated too (libvu0: the
|
||||||
# sceVu0 matrix and vector routines, core_text 0x1252A0-0x125630)
|
# sceVu0 matrix and vector routines, core_text 0x1252A0-0x125630)
|
||||||
EXTRA_RANGES = [(0x001252A0, 0x00125630),
|
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)
|
# (libgcc: 64-bit integer and soft-float helpers; level code calls some the executable's C does not)
|
||||||
(0x0011DF10, 0x001207B8)]
|
|
||||||
todo += [n for n, (a, sz) in fsize.items() if any(lo <= a < hi for lo, hi in EXTRA_RANGES)
|
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]
|
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 = {}
|
base_of = {}
|
||||||
@@ -259,7 +264,7 @@ else:
|
|||||||
_is_start_x = {a for a, _ in _starts_x}
|
_is_start_x = {a for a, _ in _starts_x}
|
||||||
_abs = os.path.join(GEN, "abs_syms.s")
|
_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 = {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
|
# 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
|
# inside an assembly-only function: func_001252C0 is 0x20 into libvu0's
|
||||||
# func_001252A0 (the levels' hero code calls it)
|
# func_001252A0 (the levels' hero code calls it)
|
||||||
@@ -277,7 +282,7 @@ else:
|
|||||||
_w = struct.unpack_from("<I", _d, _i)[0]
|
_w = struct.unpack_from("<I", _d, _i)[0]
|
||||||
if _w >> 26 == 3:
|
if _w >> 26 == 3:
|
||||||
_t = (_w & 0x3FFFFFF) << 2
|
_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):
|
for _v in sorted(_cand):
|
||||||
if not (TEXT_LO <= _v < TEXT_HI) or _v % 4 or _v in _is_start_x: continue
|
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]
|
_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()
|
||||||
@@ -19,14 +19,16 @@ import argparse, configparser, hashlib, json, os, shutil, struct, subprocess, sy
|
|||||||
|
|
||||||
RELEASE_URL = "https://github.com/OpenRAC/project-solana/releases/download/"
|
RELEASE_URL = "https://github.com/OpenRAC/project-solana/releases/download/"
|
||||||
GAMES = { # serial (BOOT2 in SYSTEM.CNF): what this version is
|
GAMES = { # serial (BOOT2 in SYSTEM.CNF): what this version is
|
||||||
"SCES_509.16": {"title": "Ratchet & Clank (PAL)", "kit": "rac1", "md5": "d86429e2eca4e1da7d7f1cc656af2574", "exe": "rac1_wrapper"},
|
"SCES_509.16": {"title": "Ratchet & Clank (PAL)", "id": "rac1", "region": "pal", "kit": "rac1", "md5": "d86429e2eca4e1da7d7f1cc656af2574", "exe": "rac1_wrapper"},
|
||||||
|
"SCUS_971.99": {"title": "Ratchet & Clank (NTSC-U)", "id": "rac1us", "region": "us", "kit": "rac1", "md5": "0c081d4e0353b419d95e5f95eb0ebacb", "exe": "rac1_wrapper"},
|
||||||
}
|
}
|
||||||
OTHER = {"SCUS_971.99": "Ratchet & Clank (NTSC-U)", "SCPS_150.37": "Ratchet & Clank (NTSC-J)",
|
BY_ID = {g["id"]: g for g in GAMES.values()}
|
||||||
|
OTHER = {"SCPS_150.37": "Ratchet & Clank (NTSC-J)",
|
||||||
"SCUS_972.68": "Ratchet & Clank: Going Commando (NTSC-U)", "SCES_516.07": "Ratchet & Clank 2 (PAL)",
|
"SCUS_972.68": "Ratchet & Clank: Going Commando (NTSC-U)", "SCES_516.07": "Ratchet & Clank 2 (PAL)",
|
||||||
"SCUS_973.53": "Ratchet & Clank: Up Your Arsenal (NTSC-U)", "SCES_524.56": "Ratchet & Clank 3 (PAL)",
|
"SCUS_973.53": "Ratchet & Clank: Up Your Arsenal (NTSC-U)", "SCES_524.56": "Ratchet & Clank 3 (PAL)",
|
||||||
"SCUS_974.65": "Ratchet: Deadlocked (NTSC-U)", "SCES_532.85": "Ratchet: Gladiator (PAL)"}
|
"SCUS_974.65": "Ratchet: Deadlocked (NTSC-U)", "SCES_532.85": "Ratchet: Gladiator (PAL)"}
|
||||||
|
|
||||||
VERSION = "1.5"
|
VERSION = "1.6"
|
||||||
HOME = os.path.join(os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share"), "project-solana")
|
HOME = os.path.join(os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share"), "project-solana")
|
||||||
INI = os.path.join(HOME, "solana.ini")
|
INI = os.path.join(HOME, "solana.ini")
|
||||||
LAUNCHER_LOG = os.path.join(HOME, "launcher_log.txt")
|
LAUNCHER_LOG = os.path.join(HOME, "launcher_log.txt")
|
||||||
@@ -65,6 +67,20 @@ def die(msg):
|
|||||||
sys.exit("project-solana: " + msg)
|
sys.exit("project-solana: " + msg)
|
||||||
|
|
||||||
|
|
||||||
|
def out_dir(g):
|
||||||
|
"""the game's build output (and its saves): build/linux, build/linux_us"""
|
||||||
|
return os.path.join(HOME, "games", g["kit"], "build", "linux" if g["region"] == "pal" else "linux_" + g["region"])
|
||||||
|
|
||||||
|
|
||||||
|
def chosen(c, want=None):
|
||||||
|
"""the game to play: --game, else the one built last (else PAL)"""
|
||||||
|
if want:
|
||||||
|
g = BY_ID.get(want) or BY_ID.get("rac1" + want) or (BY_ID["rac1"] if want == "pal" else None)
|
||||||
|
if not g: die("no game %s: pal or us" % want)
|
||||||
|
return g
|
||||||
|
return BY_ID.get(c.get("play", "game", fallback="rac1"), BY_ID["rac1"])
|
||||||
|
|
||||||
|
|
||||||
def settings():
|
def settings():
|
||||||
c = configparser.ConfigParser()
|
c = configparser.ConfigParser()
|
||||||
c.read(INI)
|
c.read(INI)
|
||||||
@@ -343,7 +359,8 @@ def build(args):
|
|||||||
if os.path.isdir(gdir): # everything but the build output (and its saves); a new kit version builds from scratch
|
if os.path.isdir(gdir): # everything but the build output (and its saves); a new kit version builds from scratch
|
||||||
for e in os.listdir(gdir):
|
for e in os.listdir(gdir):
|
||||||
if e != "build": rmtree(os.path.join(gdir, e))
|
if e != "build": rmtree(os.path.join(gdir, e))
|
||||||
rmtree(os.path.join(gdir, "build", "linux", "gen")); rmtree(os.path.join(gdir, "build", "linux", "obj"))
|
for b in ("linux", "linux_us"):
|
||||||
|
rmtree(os.path.join(gdir, "build", b, "gen")); rmtree(os.path.join(gdir, "build", b, "obj"))
|
||||||
unzip(zp, gdir, "build kit"); os.remove(zp)
|
unzip(zp, gdir, "build kit"); os.remove(zp)
|
||||||
open(kv, "w").write(man["version"]) # (unpacked: an interrupted build resumes with what it compiled)
|
open(kv, "w").write(man["version"]) # (unpacked: an interrupted build resumes with what it compiled)
|
||||||
|
|
||||||
@@ -356,7 +373,7 @@ def build(args):
|
|||||||
env = dict(os.environ, PATH=os.path.join(tc_dir, "zig") + os.pathsep + os.environ.get("PATH", ""), PYTHONUTF8="1",
|
env = dict(os.environ, PATH=os.path.join(tc_dir, "zig") + os.pathsep + os.environ.get("PATH", ""), PYTHONUTF8="1",
|
||||||
ZIG_GLOBAL_CACHE_DIR=os.path.join(gdir, "zig-cache"), ZIG_LOCAL_CACHE_DIR=os.path.join(gdir, "zig-cache"))
|
ZIG_GLOBAL_CACHE_DIR=os.path.join(gdir, "zig-cache"), ZIG_LOCAL_CACHE_DIR=os.path.join(gdir, "zig-cache"))
|
||||||
py = os.path.join(tc_dir, "python", "bin", "python3")
|
py = os.path.join(tc_dir, "python", "bin", "python3")
|
||||||
cmd = [py, "-u", "build.py", "--platform", "linux", "--levels", levels, "--iso", iso, "--repo", "decomp", "-j", str(jobs)]
|
cmd = [py, "-u", "build.py", "--platform", "linux", "--region", g["region"], "--levels", levels, "--iso", iso, "--repo", "decomp", "-j", str(jobs)]
|
||||||
log = open(os.path.join(gdir, "build_log.txt"), "w")
|
log = open(os.path.join(gdir, "build_log.txt"), "w")
|
||||||
raise_open_files()
|
raise_open_files()
|
||||||
p = subprocess.Popen(cmd, cwd=gdir, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
|
p = subprocess.Popen(cmd, cwd=gdir, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
|
||||||
@@ -365,22 +382,25 @@ def build(args):
|
|||||||
if not progress_line(line) and ("error" in line.lower() or "failed" in line.lower()): print("\n " + line.rstrip())
|
if not progress_line(line) and ("error" in line.lower() or "failed" in line.lower()): print("\n " + line.rstrip())
|
||||||
p.wait(); log.close(); print()
|
p.wait(); log.close(); print()
|
||||||
if p.returncode: die("the build stopped (see %s)" % os.path.join(gdir, "build_log.txt"))
|
if p.returncode: die("the build stopped (see %s)" % os.path.join(gdir, "build_log.txt"))
|
||||||
for j in ("build/linux/gen", "build/linux/gen.new", "build/linux/obj", "build/levels", "build/disc", "zig-cache"): rmtree(os.path.join(gdir, j))
|
bd = os.path.relpath(out_dir(g), gdir)
|
||||||
c.set("disc", kit, iso); c.set("build", "built_" + kit, man["version"]); save(c)
|
for j in (bd + "/gen", bd + "/gen.new", bd + "/obj", bd + "/region", "build/levels", "build/levels_us", "build/disc", "zig-cache"): rmtree(os.path.join(gdir, j))
|
||||||
say("Built. Ready to play: project-solana play")
|
c.set("disc", g["id"], iso); c.set("build", "built_" + g["id"], man["version"]); c.set("play", "game", g["id"]); save(c)
|
||||||
|
other = [x for x in BY_ID.values() if x is not g and c.get("build", "built_" + x["id"], fallback="")]
|
||||||
|
say("Built. Ready to play: project-solana play" + (" (the other version: project-solana play --game %s)" % other[0]["region"] if other else ""))
|
||||||
|
|
||||||
|
|
||||||
# ---- play ----
|
# ---- play ----
|
||||||
def play(args):
|
def play(args):
|
||||||
c = settings()
|
c = settings()
|
||||||
kit = "rac1"
|
g = chosen(c, args.game)
|
||||||
iso = c.get("disc", kit, fallback="")
|
iso = c.get("disc", g["id"], fallback="")
|
||||||
exe = os.path.join(HOME, "games", kit, "build", "linux", GAMES["SCES_509.16"]["exe"])
|
exe = os.path.join(out_dir(g), g["exe"])
|
||||||
if not os.path.exists(exe) or not iso: die("not built yet: run project-solana build /path/to/your/disc.iso")
|
if not os.path.exists(exe) or not iso: die("not built yet: run project-solana build /path/to/your/disc.iso")
|
||||||
if not os.path.exists(iso): die("the disc image has moved (%s): build again with its new path" % iso)
|
if not os.path.exists(iso): die("the disc image has moved (%s): build again with its new path" % iso)
|
||||||
pc = c["play"]
|
pc = c["play"]
|
||||||
for k, v in (("resolution", args.resolution), ("fullscreen", args.fullscreen), ("vsync", args.vsync)):
|
for k, v in (("resolution", args.resolution), ("fullscreen", args.fullscreen), ("vsync", args.vsync)):
|
||||||
if v is not None: pc[k] = str(v)
|
if v is not None: pc[k] = str(v)
|
||||||
|
pc["game"] = g["id"]
|
||||||
save(c)
|
save(c)
|
||||||
env = dict(os.environ)
|
env = dict(os.environ)
|
||||||
deck = os.environ.get("SteamDeck") == "1" or is_steamos() # (Steam Deck, Steam Machine: full screen by default)
|
deck = os.environ.get("SteamDeck") == "1" or is_steamos() # (Steam Deck, Steam Machine: full screen by default)
|
||||||
@@ -399,7 +419,7 @@ def play(args):
|
|||||||
gdir = os.path.dirname(exe)
|
gdir = os.path.dirname(exe)
|
||||||
out = os.path.join(gdir, "game_output.txt") # what SDL, the graphics driver and the game print (kept: one run back)
|
out = os.path.join(gdir, "game_output.txt") # what SDL, the graphics driver and the game print (kept: one run back)
|
||||||
if os.path.exists(out): os.replace(out, out + ".prev")
|
if os.path.exists(out): os.replace(out, out + ".prev")
|
||||||
log("play: %s (UYA_SCALE=%s, fullscreen %s, vsync %s)" % (iso, env.get("UYA_SCALE", "1"), full, pc.get("vsync", "0")))
|
log("play: %s, %s (UYA_SCALE=%s, fullscreen %s, vsync %s)" % (g["title"], iso, env.get("UYA_SCALE", "1"), full, pc.get("vsync", "0")))
|
||||||
with open(out, "w") as o:
|
with open(out, "w") as o:
|
||||||
try:
|
try:
|
||||||
p = subprocess.Popen([exe, "-", iso], cwd=gdir, env=env, stdout=o, stderr=subprocess.STDOUT)
|
p = subprocess.Popen([exe, "-", iso], cwd=gdir, env=env, stdout=o, stderr=subprocess.STDOUT)
|
||||||
@@ -418,9 +438,9 @@ def status(args):
|
|||||||
say("Data folder: %s" % HOME)
|
say("Data folder: %s" % HOME)
|
||||||
tv = os.path.join(HOME, "toolchain", "version.txt")
|
tv = os.path.join(HOME, "toolchain", "version.txt")
|
||||||
say("Build tools: %s" % (open(tv).read().strip() if os.path.exists(tv) else "not downloaded"))
|
say("Build tools: %s" % (open(tv).read().strip() if os.path.exists(tv) else "not downloaded"))
|
||||||
for kit in ("rac1",):
|
for g in BY_ID.values():
|
||||||
b = c.get("build", "built_" + kit, fallback="")
|
b = c.get("build", "built_" + g["id"], fallback="")
|
||||||
say("%s: %s, disc %s" % (kit, ("built from kit " + b) if b else "not built", c.get("disc", kit, fallback="(none)")))
|
say("%s: %s, disc %s" % (g["title"], ("built from kit " + b) if b else "not built", c.get("disc", g["id"], fallback="(none)")))
|
||||||
say("32-bit SDL2 and OpenGL: %s" % ("found" if check_libs(quiet=True) else "missing"))
|
say("32-bit SDL2 and OpenGL: %s" % ("found" if check_libs(quiet=True) else "missing"))
|
||||||
if not check_libs(quiet=True): check_libs()
|
if not check_libs(quiet=True): check_libs()
|
||||||
|
|
||||||
@@ -429,16 +449,20 @@ def desktop(args):
|
|||||||
me = os.path.abspath(sys.argv[0])
|
me = os.path.abspath(sys.argv[0])
|
||||||
d = os.path.join(os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share"), "applications")
|
d = os.path.join(os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share"), "applications")
|
||||||
os.makedirs(d, exist_ok=True)
|
os.makedirs(d, exist_ok=True)
|
||||||
p = os.path.join(d, "project-solana-rac1.desktop")
|
c = settings()
|
||||||
with open(p, "w") as f:
|
built = [g for g in BY_ID.values() if c.get("build", "built_" + g["id"], fallback="")] or [BY_ID["rac1"]]
|
||||||
f.write("[Desktop Entry]\nType=Application\nName=Ratchet & Clank (Project Solana)\nComment=Built from your own disc\n"
|
for g in built:
|
||||||
"Exec=python3 \"%s\" play\nTerminal=false\nCategories=Game;\n" % me) # (python3: works without the executable bit)
|
name = "Ratchet & Clank (Project Solana)" if len(built) == 1 else "%s (Project Solana)" % g["title"]
|
||||||
say("Added %s" % p)
|
p = os.path.join(d, "project-solana-%s.desktop" % g["id"])
|
||||||
say("Steam: Games > Add a Non-Steam Game to My Library, and pick \"Ratchet & Clank (Project Solana)\".")
|
with open(p, "w") as f:
|
||||||
|
f.write("[Desktop Entry]\nType=Application\nName=%s\nComment=Built from your own disc\n"
|
||||||
|
"Exec=python3 \"%s\" play --game %s\nTerminal=false\nCategories=Game;\n" % (name, me, g["region"])) # (python3: works without the executable bit)
|
||||||
|
say("Added %s" % p)
|
||||||
|
say("Steam: Games > Add a Non-Steam Game to My Library, and pick \"%s\"." % name)
|
||||||
|
|
||||||
|
|
||||||
def show_log(args):
|
def show_log(args):
|
||||||
p = os.path.join(HOME, "games", "rac1", "build", "linux", "rac1_log.txt")
|
p = os.path.join(out_dir(chosen(settings(), args.game)), "rac1_log.txt")
|
||||||
if not os.path.exists(p): die("no log yet: it is written when the game runs")
|
if not os.path.exists(p): die("no log yet: it is written when the game runs")
|
||||||
sys.stdout.write(open(p, errors="replace").read())
|
sys.stdout.write(open(p, errors="replace").read())
|
||||||
|
|
||||||
@@ -454,8 +478,9 @@ def tail(path, n):
|
|||||||
def report(args):
|
def report(args):
|
||||||
"""one text file with what a bug report needs; nothing from the game, and no file of the user's"""
|
"""one text file with what a bug report needs; nothing from the game, and no file of the user's"""
|
||||||
c = settings()
|
c = settings()
|
||||||
gdir = os.path.join(HOME, "games", "rac1")
|
g = chosen(c, args.game)
|
||||||
bdir = os.path.join(gdir, "build", "linux")
|
gdir = os.path.join(HOME, "games", g["kit"])
|
||||||
|
bdir = out_dir(g)
|
||||||
out = []
|
out = []
|
||||||
w = out.append
|
w = out.append
|
||||||
w("Project Solana bug report (Linux launcher %s), %s\n" % (VERSION, time.strftime("%Y-%m-%d %H:%M:%S %z")))
|
w("Project Solana bug report (Linux launcher %s), %s\n" % (VERSION, time.strftime("%Y-%m-%d %H:%M:%S %z")))
|
||||||
@@ -491,8 +516,9 @@ def report(args):
|
|||||||
w("build tools: %s\n" % (open(tv).read().strip() if os.path.exists(tv) else "not downloaded"))
|
w("build tools: %s\n" % (open(tv).read().strip() if os.path.exists(tv) else "not downloaded"))
|
||||||
kv = os.path.join(gdir, "kit_version.txt")
|
kv = os.path.join(gdir, "kit_version.txt")
|
||||||
w("build kit: %s\n" % (open(kv).read().strip() if os.path.exists(kv) else "none"))
|
w("build kit: %s\n" % (open(kv).read().strip() if os.path.exists(kv) else "none"))
|
||||||
|
w("game: %s\n" % g["title"])
|
||||||
w("game built: %s\n" % ("yes" if os.path.exists(os.path.join(bdir, "rac1_wrapper")) else "no"))
|
w("game built: %s\n" % ("yes" if os.path.exists(os.path.join(bdir, "rac1_wrapper")) else "no"))
|
||||||
iso = c.get("disc", "rac1", fallback="")
|
iso = c.get("disc", g["id"], fallback="")
|
||||||
w("disc image: %s\n" % ((("found, %.2f GB" % (os.path.getsize(iso) / 1e9)) if os.path.exists(iso) else "MISSING (moved?)") if iso else "none"))
|
w("disc image: %s\n" % ((("found, %.2f GB" % (os.path.getsize(iso) / 1e9)) if os.path.exists(iso) else "MISSING (moved?)") if iso else "none"))
|
||||||
w("settings:\n")
|
w("settings:\n")
|
||||||
for s in c.sections():
|
for s in c.sections():
|
||||||
@@ -520,6 +546,7 @@ def main():
|
|||||||
b.add_argument("--clean", action="store_true", help="fetch the build kit again and build from scratch")
|
b.add_argument("--clean", action="store_true", help="fetch the build kit again and build from scratch")
|
||||||
b.add_argument("--release-url", default="", help=argparse.SUPPRESS)
|
b.add_argument("--release-url", default="", help=argparse.SUPPRESS)
|
||||||
p = sp.add_parser("play", help="play (options are remembered)")
|
p = sp.add_parser("play", help="play (options are remembered)")
|
||||||
|
p.add_argument("--game", default=None, help="pal or us, when both are built (default: the last one built or played)")
|
||||||
g = p.add_mutually_exclusive_group()
|
g = p.add_mutually_exclusive_group()
|
||||||
g.add_argument("--upscaled", dest="resolution", action="store_const", const="upscaled", help="render at about your screen's resolution (default)")
|
g.add_argument("--upscaled", dest="resolution", action="store_const", const="upscaled", help="render at about your screen's resolution (default)")
|
||||||
g.add_argument("--native", dest="resolution", action="store_const", const="native", help="the PS2's own resolution")
|
g.add_argument("--native", dest="resolution", action="store_const", const="native", help="the PS2's own resolution")
|
||||||
@@ -530,8 +557,8 @@ def main():
|
|||||||
v.add_argument("--vsync", dest="vsync", action="store_const", const=1)
|
v.add_argument("--vsync", dest="vsync", action="store_const", const=1)
|
||||||
v.add_argument("--no-vsync", dest="vsync", action="store_const", const=0)
|
v.add_argument("--no-vsync", dest="vsync", action="store_const", const=0)
|
||||||
sp.add_parser("status", help="what is installed"); sp.add_parser("desktop", help="add it to the applications menu")
|
sp.add_parser("status", help="what is installed"); sp.add_parser("desktop", help="add it to the applications menu")
|
||||||
sp.add_parser("log", help="print the last game log")
|
for n, h in (("log", "print the last game log"), ("report", "write one file with everything a bug report needs")):
|
||||||
sp.add_parser("report", help="write one file with everything a bug report needs")
|
sp.add_parser(n, help=h).add_argument("--game", default=None, help="pal or us")
|
||||||
a = ap.parse_args()
|
a = ap.parse_args()
|
||||||
if sys.platform != "linux": die("this is the Linux launcher: on Windows, use Project Solana.exe")
|
if sys.platform != "linux": die("this is the Linux launcher: on Windows, use Project Solana.exe")
|
||||||
log("project-solana %s: %s" % (VERSION, " ".join(sys.argv[1:])))
|
log("project-solana %s: %s" % (VERSION, " ".join(sys.argv[1:])))
|
||||||
|
|||||||
+2
-1
@@ -36,6 +36,7 @@ typedef struct {
|
|||||||
const wchar_t *exe; /* its wrapper in that folder (0: not supported yet) */
|
const wchar_t *exe; /* its wrapper in that folder (0: not supported yet) */
|
||||||
const wchar_t *kit; /* its GitHub release tag and folder under games\ (the Build button) */
|
const wchar_t *kit; /* its GitHub release tag and folder under games\ (the Build button) */
|
||||||
const wchar_t *const *levels;
|
const wchar_t *const *levels;
|
||||||
|
const wchar_t *rgn; /* build.py --region (0: pal); its output is build\windows_<rgn> */
|
||||||
} game_t;
|
} game_t;
|
||||||
|
|
||||||
static const wchar_t *const rac1_levels[] = {
|
static const wchar_t *const rac1_levels[] = {
|
||||||
@@ -45,7 +46,7 @@ static const wchar_t *const rac1_levels[] = {
|
|||||||
|
|
||||||
static const game_t games[] = {
|
static const game_t games[] = {
|
||||||
{ "SCES_509.16", L"Ratchet & Clank", L"PAL", "d86429e2eca4e1da7d7f1cc656af2574", L"games\\rac1\\build\\windows", L"rac1_wrapper.exe", L"rac1", rac1_levels },
|
{ "SCES_509.16", L"Ratchet & Clank", L"PAL", "d86429e2eca4e1da7d7f1cc656af2574", L"games\\rac1\\build\\windows", L"rac1_wrapper.exe", L"rac1", rac1_levels },
|
||||||
{ "SCUS_971.99", L"Ratchet & Clank", L"NTSC-U", 0, 0, 0, 0, 0 },
|
{ "SCUS_971.99", L"Ratchet & Clank", L"NTSC-U", "0c081d4e0353b419d95e5f95eb0ebacb", L"games\\rac1\\build\\windows_us", L"rac1_wrapper.exe", L"rac1", rac1_levels, L"us" },
|
||||||
{ "SCPS_150.37", L"Ratchet & Clank", L"NTSC-J", 0, 0, 0, 0, 0 },
|
{ "SCPS_150.37", L"Ratchet & Clank", L"NTSC-J", 0, 0, 0, 0, 0 },
|
||||||
{ "SCES_516.07", L"Ratchet & Clank 2", L"PAL", 0, 0, 0, 0, 0 },
|
{ "SCES_516.07", L"Ratchet & Clank 2", L"PAL", 0, 0, 0, 0, 0 },
|
||||||
{ "SCUS_972.68", L"Ratchet & Clank: Going Commando", L"NTSC-U", 0, 0, 0, 0, 0 },
|
{ "SCUS_972.68", L"Ratchet & Clank: Going Commando", L"NTSC-U", 0, 0, 0, 0, 0 },
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[settings]
|
||||||
|
upscaled=1
|
||||||
|
vsync=0
|
||||||
|
no_gs_thread=0
|
||||||
|
no_gl_thread=0
|
||||||
|
fps_log=0
|
||||||
|
capture_seconds=3
|
||||||
|
start_level=-1
|
||||||
|
minimize=1
|
||||||
|
[library]
|
||||||
|
count=2
|
||||||
|
disc0=C:\Users\Nathan\Desktop\folder folder\Roms\ps2\Ratchet & Clank (Europe) (En,Fr,De,Es,It) (v2.00).iso
|
||||||
|
disc1=C:\Users\Nathan\Desktop\folder folder\Roms\ps2\Ratchet & Clank.iso
|
||||||
|
selected=1
|
||||||
+12
-8
@@ -363,7 +363,7 @@ static void build_line(const char *l, double f0, double f1)
|
|||||||
else if (!strcmp(ph, "compile")) bld_status(f, L"%ls (%d of %d files)", what, a, b);
|
else if (!strcmp(ph, "compile")) bld_status(f, L"%ls (%d of %d files)", what, a, b);
|
||||||
else bld_status(f, L"%ls", what);
|
else bld_status(f, L"%ls", what);
|
||||||
}
|
}
|
||||||
static int run_build(const wchar_t *gdir, const wchar_t *iso, const wchar_t *levels, double f0, double f1)
|
static int run_build(const wchar_t *gdir, const wchar_t *iso, const wchar_t *levels, const wchar_t *rgn, double f0, double f1)
|
||||||
{
|
{
|
||||||
wchar_t py[MAX_PATH], zig[MAX_PATH], tc[MAX_PATH], cmd[4096], logp[MAX_PATH];
|
wchar_t py[MAX_PATH], zig[MAX_PATH], tc[MAX_PATH], cmd[4096], logp[MAX_PATH];
|
||||||
path_join(tc, base_dir, L"toolchain");
|
path_join(tc, base_dir, L"toolchain");
|
||||||
@@ -375,7 +375,7 @@ static int run_build(const wchar_t *gdir, const wchar_t *iso, const wchar_t *lev
|
|||||||
int j = (int)si.dwNumberOfProcessors, jm = (int)(ms.ullTotalPhys / (1536ull << 20));
|
int j = (int)si.dwNumberOfProcessors, jm = (int)(ms.ullTotalPhys / (1536ull << 20));
|
||||||
if (j > jm) j = jm;
|
if (j > jm) j = jm;
|
||||||
if (j < 1) j = 1;
|
if (j < 1) j = 1;
|
||||||
swprintf(cmd, 4096, L"\"%ls\" -u build.py --platform windows --levels %ls --iso \"%ls\" --repo decomp -j %d", py, levels, iso, j);
|
swprintf(cmd, 4096, L"\"%ls\" -u build.py --platform windows --region %ls --levels %ls --iso \"%ls\" --repo decomp -j %d", py, rgn, levels, iso, j);
|
||||||
static wchar_t path[8192]; static int have_path; /* (the PATH this program started with) */
|
static wchar_t path[8192]; static int have_path; /* (the PATH this program started with) */
|
||||||
if (!have_path) { DWORD k = GetEnvironmentVariableW(L"PATH", path, 8192); if (k >= 8192) path[0] = 0; have_path = 1; }
|
if (!have_path) { DWORD k = GetEnvironmentVariableW(L"PATH", path, 8192); if (k >= 8192) path[0] = 0; have_path = 1; }
|
||||||
{ /* the toolchain first on PATH; Python in UTF-8 mode (the sources are UTF-8) */
|
{ /* the toolchain first on PATH; Python in UTF-8 mode (the sources are UTF-8) */
|
||||||
@@ -496,7 +496,8 @@ static DWORD WINAPI build_thread(LPVOID unused)
|
|||||||
GetPrivateProfileStringW(g->kit, L"kit_version", L"", kv, 64, ini_path);
|
GetPrivateProfileStringW(g->kit, L"kit_version", L"", kv, 64, ini_path);
|
||||||
MultiByteToWideChar(CP_UTF8, 0, version, -1, wv, 64);
|
MultiByteToWideChar(CP_UTF8, 0, version, -1, wv, 64);
|
||||||
if (kv[0] && wcscmp(kv, wv)) { /* (none recorded: a first build, or one from before this was kept) */
|
if (kv[0] && wcscmp(kv, wv)) { /* (none recorded: a first build, or one from before this was kept) */
|
||||||
static const wchar_t *old[] = { L"build\\windows\\gen", L"build\\windows\\gen.new", L"build\\windows\\obj", L"zig-cache" };
|
static const wchar_t *old[] = { L"build\\windows\\gen", L"build\\windows\\gen.new", L"build\\windows\\obj",
|
||||||
|
L"build\\windows_us\\gen", L"build\\windows_us\\gen.new", L"build\\windows_us\\obj", L"zig-cache" };
|
||||||
for (unsigned i = 0; i < sizeof old / sizeof old[0]; i++) { path_join(p, gdir, old[i]); rm_tree(p); }
|
for (unsigned i = 0; i < sizeof old / sizeof old[0]; i++) { path_join(p, gdir, old[i]); rm_tree(p); }
|
||||||
}
|
}
|
||||||
WritePrivateProfileStringW(g->kit, L"kit_version", wv, ini_path);
|
WritePrivateProfileStringW(g->kit, L"kit_version", wv, ini_path);
|
||||||
@@ -519,17 +520,20 @@ static DWORD WINAPI build_thread(LPVOID unused)
|
|||||||
GetPrivateProfileStringW(L"build", L"levels", L"", levels, 256, ini_path);
|
GetPrivateProfileStringW(L"build", L"levels", L"", levels, 256, ini_path);
|
||||||
if (!levels[0]) MultiByteToWideChar(CP_UTF8, 0, lv[0] ? lv : "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18", -1, levels, 256);
|
if (!levels[0]) MultiByteToWideChar(CP_UTF8, 0, lv[0] ? lv : "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18", -1, levels, 256);
|
||||||
bld_status(0.32, L"Starting the build");
|
bld_status(0.32, L"Starting the build");
|
||||||
if (!run_build(gdir, d->path, levels, 0.32, 0.99)) goto out;
|
if (!run_build(gdir, d->path, levels, g->rgn ? g->rgn : L"pal", 0.32, 0.99)) goto out;
|
||||||
}
|
}
|
||||||
/* 5. keep what the game needs: the intermediate files (about 1.3 GB) and the bytes read from the disc go */
|
/* 5. keep what the game needs: the intermediate files (about 1.3 GB) and the bytes read from the disc go */
|
||||||
bld_status(0.99, L"Cleaning up");
|
bld_status(0.99, L"Cleaning up");
|
||||||
{
|
{
|
||||||
static const wchar_t *junk[] = { L"build\\windows\\gen", L"build\\windows\\gen.new", L"build\\windows\\obj", L"build\\levels", L"build\\disc",
|
static const wchar_t *junk[] = { L"gen", L"gen.new", L"obj", L"region", L"rac1_game.pdb", L"rac1_wrapper.pdb", L"asm_stubs.c.lib" }; /* (in the build's own folder) */
|
||||||
L"build\\windows\\rac1_game.pdb", L"build\\windows\\rac1_wrapper.pdb", L"build\\windows\\asm_stubs.c.lib", L"zig-cache" };
|
static const wchar_t *junk2[] = { L"build\\levels", L"build\\levels_us", L"build\\disc", L"zig-cache" };
|
||||||
for (unsigned i = 0; i < sizeof junk / sizeof junk[0]; i++) { path_join(p, gdir, junk[i]); rm_tree(p); }
|
const wchar_t *bd = g->rgn && wcscmp(g->rgn, L"pal") ? L"build\\windows_" : L"build\\windows";
|
||||||
|
for (unsigned i = 0; i < sizeof junk / sizeof junk[0]; i++) {
|
||||||
|
swprintf(p, MAX_PATH, L"%ls\\%ls%ls\\%ls", gdir, bd, g->rgn && wcscmp(g->rgn, L"pal") ? g->rgn : L"", junk[i]); rm_tree(p); }
|
||||||
|
for (unsigned i = 0; i < sizeof junk2 / sizeof junk2[0]; i++) { path_join(p, gdir, junk2[i]); rm_tree(p); }
|
||||||
}
|
}
|
||||||
{ wchar_t wv[64]; MultiByteToWideChar(CP_UTF8, 0, version, -1, wv, 64);
|
{ wchar_t wv[64]; MultiByteToWideChar(CP_UTF8, 0, version, -1, wv, 64);
|
||||||
WritePrivateProfileStringW(g->kit, L"built_version", wv, ini_path); }
|
WritePrivateProfileStringW(g->kit, g->rgn ? L"built_version_us" : L"built_version", wv, ini_path); }
|
||||||
bld_status(1.0, L"Built");
|
bld_status(1.0, L"Built");
|
||||||
out:
|
out:
|
||||||
free(js);
|
free(js);
|
||||||
|
|||||||
Reference in New Issue
Block a user