added ntscu-u support
This commit is contained in:
@@ -19,14 +19,16 @@ import argparse, configparser, hashlib, json, os, shutil, struct, subprocess, sy
|
||||
|
||||
RELEASE_URL = "https://github.com/OpenRAC/project-solana/releases/download/"
|
||||
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_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)"}
|
||||
|
||||
VERSION = "1.5"
|
||||
VERSION = "1.6"
|
||||
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")
|
||||
LAUNCHER_LOG = os.path.join(HOME, "launcher_log.txt")
|
||||
@@ -65,6 +67,20 @@ def die(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():
|
||||
c = configparser.ConfigParser()
|
||||
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
|
||||
for e in os.listdir(gdir):
|
||||
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)
|
||||
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",
|
||||
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")
|
||||
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")
|
||||
raise_open_files()
|
||||
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())
|
||||
p.wait(); log.close(); print()
|
||||
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))
|
||||
c.set("disc", kit, iso); c.set("build", "built_" + kit, man["version"]); save(c)
|
||||
say("Built. Ready to play: project-solana play")
|
||||
bd = os.path.relpath(out_dir(g), gdir)
|
||||
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))
|
||||
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 ----
|
||||
def play(args):
|
||||
c = settings()
|
||||
kit = "rac1"
|
||||
iso = c.get("disc", kit, fallback="")
|
||||
exe = os.path.join(HOME, "games", kit, "build", "linux", GAMES["SCES_509.16"]["exe"])
|
||||
g = chosen(c, args.game)
|
||||
iso = c.get("disc", g["id"], fallback="")
|
||||
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(iso): die("the disc image has moved (%s): build again with its new path" % iso)
|
||||
pc = c["play"]
|
||||
for k, v in (("resolution", args.resolution), ("fullscreen", args.fullscreen), ("vsync", args.vsync)):
|
||||
if v is not None: pc[k] = str(v)
|
||||
pc["game"] = g["id"]
|
||||
save(c)
|
||||
env = dict(os.environ)
|
||||
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)
|
||||
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")
|
||||
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:
|
||||
try:
|
||||
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)
|
||||
tv = os.path.join(HOME, "toolchain", "version.txt")
|
||||
say("Build tools: %s" % (open(tv).read().strip() if os.path.exists(tv) else "not downloaded"))
|
||||
for kit in ("rac1",):
|
||||
b = c.get("build", "built_" + kit, fallback="")
|
||||
say("%s: %s, disc %s" % (kit, ("built from kit " + b) if b else "not built", c.get("disc", kit, fallback="(none)")))
|
||||
for g in BY_ID.values():
|
||||
b = c.get("build", "built_" + g["id"], fallback="")
|
||||
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"))
|
||||
if not check_libs(quiet=True): check_libs()
|
||||
|
||||
@@ -429,16 +449,20 @@ def desktop(args):
|
||||
me = os.path.abspath(sys.argv[0])
|
||||
d = os.path.join(os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share"), "applications")
|
||||
os.makedirs(d, exist_ok=True)
|
||||
p = os.path.join(d, "project-solana-rac1.desktop")
|
||||
with open(p, "w") as f:
|
||||
f.write("[Desktop Entry]\nType=Application\nName=Ratchet & Clank (Project Solana)\nComment=Built from your own disc\n"
|
||||
"Exec=python3 \"%s\" play\nTerminal=false\nCategories=Game;\n" % me) # (python3: works without the executable bit)
|
||||
say("Added %s" % p)
|
||||
say("Steam: Games > Add a Non-Steam Game to My Library, and pick \"Ratchet & Clank (Project Solana)\".")
|
||||
c = settings()
|
||||
built = [g for g in BY_ID.values() if c.get("build", "built_" + g["id"], fallback="")] or [BY_ID["rac1"]]
|
||||
for g in built:
|
||||
name = "Ratchet & Clank (Project Solana)" if len(built) == 1 else "%s (Project Solana)" % g["title"]
|
||||
p = os.path.join(d, "project-solana-%s.desktop" % g["id"])
|
||||
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):
|
||||
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")
|
||||
sys.stdout.write(open(p, errors="replace").read())
|
||||
|
||||
@@ -454,8 +478,9 @@ def tail(path, n):
|
||||
def report(args):
|
||||
"""one text file with what a bug report needs; nothing from the game, and no file of the user's"""
|
||||
c = settings()
|
||||
gdir = os.path.join(HOME, "games", "rac1")
|
||||
bdir = os.path.join(gdir, "build", "linux")
|
||||
g = chosen(c, args.game)
|
||||
gdir = os.path.join(HOME, "games", g["kit"])
|
||||
bdir = out_dir(g)
|
||||
out = []
|
||||
w = out.append
|
||||
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"))
|
||||
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("game: %s\n" % g["title"])
|
||||
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("settings:\n")
|
||||
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("--release-url", default="", help=argparse.SUPPRESS)
|
||||
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.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")
|
||||
@@ -530,8 +557,8 @@ def main():
|
||||
v.add_argument("--vsync", dest="vsync", action="store_const", const=1)
|
||||
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("log", help="print the last game log")
|
||||
sp.add_parser("report", help="write one file with everything a bug report needs")
|
||||
for n, h in (("log", "print the last game log"), ("report", "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()
|
||||
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:])))
|
||||
|
||||
+2
-1
@@ -36,6 +36,7 @@ typedef struct {
|
||||
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 *const *levels;
|
||||
const wchar_t *rgn; /* build.py --region (0: pal); its output is build\windows_<rgn> */
|
||||
} game_t;
|
||||
|
||||
static const wchar_t *const rac1_levels[] = {
|
||||
@@ -45,7 +46,7 @@ static const wchar_t *const rac1_levels[] = {
|
||||
|
||||
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 },
|
||||
{ "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 },
|
||||
{ "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 },
|
||||
|
||||
@@ -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 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];
|
||||
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));
|
||||
if (j > jm) j = jm;
|
||||
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) */
|
||||
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) */
|
||||
@@ -496,7 +496,8 @@ static DWORD WINAPI build_thread(LPVOID unused)
|
||||
GetPrivateProfileStringW(g->kit, L"kit_version", L"", kv, 64, ini_path);
|
||||
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) */
|
||||
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); }
|
||||
}
|
||||
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);
|
||||
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");
|
||||
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 */
|
||||
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",
|
||||
L"build\\windows\\rac1_game.pdb", L"build\\windows\\rac1_wrapper.pdb", L"build\\windows\\asm_stubs.c.lib", L"zig-cache" };
|
||||
for (unsigned i = 0; i < sizeof junk / sizeof junk[0]; i++) { path_join(p, gdir, junk[i]); rm_tree(p); }
|
||||
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) */
|
||||
static const wchar_t *junk2[] = { L"build\\levels", L"build\\levels_us", L"build\\disc", L"zig-cache" };
|
||||
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);
|
||||
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");
|
||||
out:
|
||||
free(js);
|
||||
|
||||
Reference in New Issue
Block a user