mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 18:25:35 -04:00
bump
This commit is contained in:
@@ -74,6 +74,14 @@ multi-track cue. The runtime mounts the resolved path, fingerprints the TOC
|
||||
| `required_tracks` | `0` | Exact `iso_track_count` when > 0 (e.g. MotK Redump = `17`) |
|
||||
| `required_leadout_lba` | unset | Exact lead-out LBA when set |
|
||||
| `required_disc_fp` | `""` | Exact lowercase hex SHA-256 TOC fingerprint when non-empty |
|
||||
| `required_disc_fps` | unset | Multi-disc: array parallel to `[game] discs`, the TOC fingerprint of each disc. Each disc of a set has its own TOC, so a set gated only on `required_disc_fp` (the boot disc's) refuses online play on every other disc the launcher lets the player select. A disc with no entry falls back to `required_disc_fp`. |
|
||||
|
||||
The whole `[netplay]` policy is resolved **per mounted disc**, not once per
|
||||
build — `required_tracks` and `required_leadout_lba` are per-disc facts too (a
|
||||
set may mix a CD-DA disc with a data-only one, and the lead-out LBA is the
|
||||
disc's size). Only `required_disc_fps` carries per-disc data today; the
|
||||
resolution point is `netplay_expect_for_disc()` in `runtime/src/main.cpp`, and
|
||||
that is where the others go when a title needs them.
|
||||
|
||||
Offline Play may still launch with a TOC warning; first-run setup Finish and
|
||||
online Create/Join require `netplay_ok` (and online also a clean verify +
|
||||
@@ -107,6 +115,25 @@ respective files.
|
||||
| `stack_base` | game | hex string, initial `$sp` value for the game |
|
||||
| `disc` | game (single-disc) | path to .cue, relative to project root |
|
||||
| `discs` | game (multi-disc) | array of .cue paths; `disc` is sugar for `discs = [disc]` |
|
||||
| `disc_serials` | game (multi-disc, optional) | array parallel to `discs`: the serial each disc carries (`["SCUS-94163", "SCUS-94164", "SCUS-94165"]`). Without it every disc is checked against `[game] id` — the BOOT disc's serial — so selecting disc 2 reports "wrong disc". A disc with no entry here is not serial-gated; the ISO-header check still applies. |
|
||||
|
||||
### Multi-disc selection
|
||||
|
||||
A build whose `discs` array has more than one entry grows a **Disc Selection**
|
||||
dropdown in the launcher, above the Serial/Region/ISO-header checklist. The
|
||||
choice is persisted in `settings.toml`:
|
||||
|
||||
```toml
|
||||
[disc]
|
||||
path = "/abs/path/Game (Disc 2).bin" # the image actually mounted
|
||||
selected = 2 # 1-based index into [game] discs
|
||||
```
|
||||
|
||||
`selected` names the disc and `path` only survives when it *is* that disc
|
||||
(same file-name stem), so writing `selected` alone — from an external launcher
|
||||
or by hand — switches discs even though `path` still points at the previous
|
||||
one. That is what makes disc choice manageable like any other setting. Both
|
||||
keys are written only for multi-disc titles.
|
||||
|
||||
## Recompiler block
|
||||
|
||||
|
||||
@@ -1223,6 +1223,10 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
const auto& d = toml::find<std::string>(game, "disc");
|
||||
discs.push_back(fs::absolute(root / d));
|
||||
}
|
||||
/* Per-disc serials, parallel to `discs`. Absent => no per-disc gate. */
|
||||
std::vector<std::string> disc_serials;
|
||||
if (game.contains("disc_serials"))
|
||||
disc_serials = toml::find<std::vector<std::string>>(game, "disc_serials");
|
||||
|
||||
// Optional expected disc identity (launcher verification badge).
|
||||
bool has_disc_crc = false;
|
||||
@@ -1244,6 +1248,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
bool has_netplay_required_leadout = false;
|
||||
uint32_t netplay_required_leadout_lba = 0;
|
||||
std::string netplay_required_disc_fp;
|
||||
std::vector<std::string> netplay_required_disc_fps;
|
||||
std::string netplay_local_viewport;
|
||||
std::string netplay_local_viewport_aspect;
|
||||
if (cfg.contains("netplay")) {
|
||||
@@ -1262,6 +1267,14 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
for (char& c : netplay_required_disc_fp)
|
||||
c = (char)std::tolower((unsigned char)c);
|
||||
}
|
||||
/* Per-disc fingerprints for a multi-disc set, parallel to [game]
|
||||
* discs. Lowercased here so the runtime compares like for like. */
|
||||
if (np.contains("required_disc_fps")) {
|
||||
netplay_required_disc_fps =
|
||||
toml::find<std::vector<std::string>>(np, "required_disc_fps");
|
||||
for (std::string& fp : netplay_required_disc_fps)
|
||||
for (char& c : fp) c = (char)std::tolower((unsigned char)c);
|
||||
}
|
||||
if (np.contains("local_viewport")) {
|
||||
netplay_local_viewport = toml::find<std::string>(np, "local_viewport");
|
||||
for (char& c : netplay_local_viewport)
|
||||
@@ -2073,6 +2086,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
/*text_size*/ text_size,
|
||||
/*stack_base*/ stack_base,
|
||||
/*discs*/ discs,
|
||||
/*disc_serials*/ disc_serials,
|
||||
/*has_disc_crc*/ has_disc_crc,
|
||||
/*disc_crc*/ disc_crc,
|
||||
/*disc_sha1*/ disc_sha1,
|
||||
@@ -2081,6 +2095,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
/*has_netplay_required_leadout*/ has_netplay_required_leadout,
|
||||
/*netplay_required_leadout_lba*/ netplay_required_leadout_lba,
|
||||
/*netplay_required_disc_fp*/ netplay_required_disc_fp,
|
||||
/*netplay_required_disc_fps*/ netplay_required_disc_fps,
|
||||
/*netplay_local_viewport*/ netplay_local_viewport,
|
||||
/*netplay_local_viewport_aspect*/ netplay_local_viewport_aspect,
|
||||
/*seeds_path*/ seeds_path,
|
||||
@@ -2423,6 +2438,15 @@ UserSettings load_user_settings(const fs::path& path) {
|
||||
const auto p = toml::find<std::string>(d, "path");
|
||||
if (!p.empty()) { s.disc_path = fs::path(p); s.has_disc_path = true; }
|
||||
});
|
||||
/* Multi-disc selection, 1-based. Anything below 1 is a malformed
|
||||
* hand-edit, not a request to boot disc 0 -- clamp up and keep going
|
||||
* rather than silently mounting nothing. The upper bound is the disc
|
||||
* roster's size, which only the runtime knows, so it clamps there. */
|
||||
if (d.contains("selected")) try_get([&]{
|
||||
const auto n = toml::find<int>(d, "selected");
|
||||
s.disc_index = n < 1 ? 1 : n;
|
||||
s.has_disc_index = true;
|
||||
});
|
||||
}
|
||||
if (doc.contains("memcard")) {
|
||||
const toml::value& m = toml::find(doc, "memcard");
|
||||
@@ -2640,8 +2664,15 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
|
||||
}
|
||||
if (s.has_bios_path)
|
||||
f << "\n[bios]\npath = \"" << fwd(s.bios_path) << "\"\n";
|
||||
if (s.has_disc_path)
|
||||
f << "\n[disc]\npath = \"" << fwd(s.disc_path) << "\"\n";
|
||||
if (s.has_disc_path || s.has_disc_index) {
|
||||
f << "\n[disc]\n";
|
||||
if (s.has_disc_path)
|
||||
f << "path = \"" << fwd(s.disc_path) << "\"\n";
|
||||
/* Only meaningful for a multi-disc title; harmless (and informative)
|
||||
* for a single-disc one, where it is always 1. */
|
||||
if (s.has_disc_index)
|
||||
f << "selected = " << s.disc_index << "\n";
|
||||
}
|
||||
if (s.has_memcard_dir || s.has_memcard1_path || s.has_memcard2_path ||
|
||||
s.has_memcard1_enabled || s.has_memcard2_enabled) {
|
||||
f << "\n[memcard]\n";
|
||||
|
||||
@@ -741,6 +741,17 @@ struct GameConfig {
|
||||
// the union here).
|
||||
std::vector<std::filesystem::path> discs;
|
||||
|
||||
// Optional per-disc serials, PARALLEL to `discs` ([game] disc_serials).
|
||||
// Each disc of a set carries its own serial (FF7 US: SCUS-94163 / -64 /
|
||||
// -65), so a set verified against the BOOT disc's serial alone would
|
||||
// report every other disc as "wrong disc" the moment the player selects
|
||||
// it. May be shorter than `discs` (or absent entirely, which every
|
||||
// single-disc title and every port written before this is): a disc with
|
||||
// no serial here is simply not serial-gated -- the ISO header check
|
||||
// still applies. Written by tools/new_project_layout from the same probe
|
||||
// that produced disc_set.json.
|
||||
std::vector<std::string> disc_serials;
|
||||
|
||||
// Optional expected disc identity, for the launcher's "Disc verified" badge.
|
||||
// disc_crc: full-file CRC32 (IEEE) of the data track. disc_sha1: lowercase
|
||||
// hex SHA-1. Either may be absent (has_disc_crc / disc_sha1.empty()).
|
||||
@@ -757,6 +768,16 @@ struct GameConfig {
|
||||
bool has_netplay_required_leadout = false;
|
||||
uint32_t netplay_required_leadout_lba = 0;
|
||||
std::string netplay_required_disc_fp; // lowercase hex SHA-256
|
||||
// Per-disc TOC fingerprints for a MULTI-DISC set, PARALLEL to [game]
|
||||
// discs ([netplay] required_disc_fps). Every disc of a set has its own
|
||||
// TOC, so a set gated on one fingerprint refuses online play on every
|
||||
// disc but that one -- and the flat required_disc_fp above can only ever
|
||||
// name the boot disc. May be shorter than `discs`, or absent (every
|
||||
// single-disc title): a disc with no fingerprint here falls back to the
|
||||
// flat value, and a set that declares neither is simply not fingerprint-
|
||||
// gated. Values come from the same verify_disc_set.py probe that writes
|
||||
// disc_set.json.
|
||||
std::vector<std::string> netplay_required_disc_fps;
|
||||
// local_viewport = "vertical_split": while real netplay is active, crop
|
||||
// presentation to this peer's left/right split-screen half. This is a
|
||||
// presentation-only helper for titles that still render native split-screen
|
||||
@@ -1231,6 +1252,14 @@ struct UserSettings {
|
||||
// [bios] / [disc] / [memcard]
|
||||
bool has_bios_path = false; std::filesystem::path bios_path;
|
||||
bool has_disc_path = false; std::filesystem::path disc_path;
|
||||
// [disc] selected: which image of a MULTI-DISC set ([game] discs in
|
||||
// game.toml) the player last chose, 1-based. Stored as an ordinary
|
||||
// setting so the launcher's Disc Selection dropdown persists across
|
||||
// sessions and an external launcher can drive it like any other row.
|
||||
// Ignored by single-disc titles. When it disagrees with disc_path, the
|
||||
// rule is in resolve_selected_disc() (runtime/src/main.cpp): the index
|
||||
// names the disc, disc_path only survives when it is that same disc.
|
||||
bool has_disc_index = false; int disc_index = 1;
|
||||
bool has_memcard_dir = false; std::filesystem::path memcard_dir;
|
||||
// Per-slot memory-card overrides. An explicit card path overrides the
|
||||
// <dir>/card<N>.mcd default; the enable flag inserts/removes the card.
|
||||
|
||||
+215
-9
@@ -2204,6 +2204,40 @@ static bool pick_runtime_file(const char* title, const char* filter,
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Per-disc expected serials for a multi-disc set, keyed by the disc image's
|
||||
* UPPERCASED file-name stem (game.toml [game] disc_serials, in the order of
|
||||
* [game] discs). Every disc of a set carries its own serial, so checking disc
|
||||
* 2 against the BOOT disc's serial reports the player's correct disc as the
|
||||
* wrong game -- which is what the launcher's disc dropdown would trip on
|
||||
* every time, and what the launch-time check warned about even when the
|
||||
* player picked exactly the right image. A disc absent from this map is not
|
||||
* serial-gated at all; the ISO-header check still applies. Keyed by stem
|
||||
* rather than full path because a .cue and the .bin it owns are one disc and
|
||||
* either may be what the player picked. Populated from game.toml; empty for
|
||||
* every single-disc title, which therefore behaves exactly as before. */
|
||||
static std::unordered_map<std::string, std::string> g_disc_serials;
|
||||
|
||||
/* Per-disc netplay TOC fingerprints, same keying and same reason as
|
||||
* g_disc_serials: every disc of a set has its own TOC, so a set gated on the
|
||||
* BOOT disc's fingerprint refuses online play on every other disc the player
|
||||
* can now select. Populated from game.toml [netplay] required_disc_fps; empty
|
||||
* for every single-disc title and every port that predates the key, which
|
||||
* therefore keep the flat [netplay] required_disc_fp behaviour exactly. */
|
||||
static std::unordered_map<std::string, std::string> g_disc_netplay_fps;
|
||||
|
||||
static std::string uppercase_ascii(std::string s);
|
||||
|
||||
/* The serial THIS image is expected to carry: the set's per-disc value when
|
||||
* the game declared one, the game's own id otherwise. An image that belongs
|
||||
* to a declared set but has no serial listed is returned ungated (""), never
|
||||
* gated against another disc's number. */
|
||||
static std::string expected_serial_for_disc(const std::filesystem::path& disc,
|
||||
const std::string& fallback) {
|
||||
if (g_disc_serials.empty()) return fallback;
|
||||
const auto it = g_disc_serials.find(uppercase_ascii(disc.stem().string()));
|
||||
return it != g_disc_serials.end() ? it->second : std::string();
|
||||
}
|
||||
|
||||
static std::string uppercase_ascii(std::string s) {
|
||||
for (char& c : s) {
|
||||
c = (char)std::toupper((unsigned char)c);
|
||||
@@ -2247,17 +2281,20 @@ static DiscValidation validate_disc_image(const std::filesystem::path& selected_
|
||||
// the launcher badge can never drift apart. No CRC here — the launch check
|
||||
// only cares about openability / header / serial; the launcher does the
|
||||
// (slower) CRC pass when an expected CRC is configured.
|
||||
/* A multi-disc set is checked against the SELECTED disc's serial, not the
|
||||
* boot disc's -- see expected_serial_for_disc(). */
|
||||
const std::string expect = expected_serial_for_disc(selected_path, game_id);
|
||||
const PSXRecompV4::DiscIdentity id =
|
||||
PSXRecompV4::identify_disc(selected_path, game_id, /*expected_crc*/0,
|
||||
PSXRecompV4::identify_disc(selected_path, expect, /*expected_crc*/0,
|
||||
/*has_expected_crc*/false, /*compute_crc*/false);
|
||||
DiscValidation v;
|
||||
v.opened = id.opened;
|
||||
v.has_header = id.has_header;
|
||||
v.id_matches = game_id.empty() ? true : id.serial_matches;
|
||||
v.id_matches = expect.empty() ? true : id.serial_matches;
|
||||
v.detail = id.detail;
|
||||
if (id.opened && id.has_header && !v.id_matches && v.detail.empty()) {
|
||||
v.detail = "The disc header is readable, but it does not contain the expected game ID " +
|
||||
uppercase_ascii(game_id) + " in the early disc metadata.";
|
||||
uppercase_ascii(expect) + " in the early disc metadata.";
|
||||
}
|
||||
return v;
|
||||
}
|
||||
@@ -2300,6 +2337,35 @@ static std::filesystem::path normalize_disc_path_for_launch(const std::filesyste
|
||||
return p;
|
||||
}
|
||||
|
||||
/* Which image of a MULTI-DISC set to mount, given the roster this build was
|
||||
* made from (game.toml [game] discs), the player's persisted [disc] selected
|
||||
* index, and the [disc] path the launcher last wrote.
|
||||
*
|
||||
* The index names the disc; the path only survives when it IS that disc.
|
||||
* That ordering is the whole point of storing the index: an external launcher
|
||||
* (or a hand edit) changes discs by writing one integer, and it takes effect
|
||||
* even though settings.toml still carries the previous disc's path. A player
|
||||
* who browsed for their own copy of the selected disc is still honoured,
|
||||
* because a relocated or container-swapped image keeps its stem -- a Redump
|
||||
* dump names the disc in the file name and only the extension moves
|
||||
* (".. (Disc 2).cue" -> ".. (Disc 2).bin", which is exactly the substitution
|
||||
* normalize_disc_path_for_launch performs).
|
||||
*
|
||||
* Single-disc titles (roster of 0 or 1) are returned unchanged: the persisted
|
||||
* path wins, exactly as it did before any of this existed. */
|
||||
static std::filesystem::path resolve_selected_disc(
|
||||
const std::vector<std::filesystem::path>& roster, int selected_1based,
|
||||
const std::filesystem::path& persisted) {
|
||||
if (roster.size() < 2) return persisted;
|
||||
const int idx = selected_1based - 1;
|
||||
if (idx < 0 || idx >= (int)roster.size()) return persisted;
|
||||
if (!persisted.empty() &&
|
||||
uppercase_ascii(persisted.stem().string()) ==
|
||||
uppercase_ascii(roster[(size_t)idx].stem().string()))
|
||||
return persisted;
|
||||
return roster[(size_t)idx];
|
||||
}
|
||||
|
||||
/* BIOS selection state (docs/BIOS_SELECTION.md). s_openbios_allowed is the
|
||||
* game's [runtime] openbios; s_bundled_bios_rel is where the shipped
|
||||
* redistributable image lives relative to the executable. */
|
||||
@@ -7309,6 +7375,27 @@ static void sdl_vblank_present(void) {
|
||||
/* game.toml [netplay] + last TOC fingerprint — shared by launcher verify and
|
||||
* the pre-psx_netplay_start gate (works with or without RECOMP_LAUNCHER). */
|
||||
static PSXRecompV4::NetplayDiscExpect g_netplay_disc_expect{};
|
||||
|
||||
/* The netplay mount policy for ONE image. The policy is per-disc, not
|
||||
* per-build: the TOC fingerprint is the disc's own, and the same is true in
|
||||
* principle of required_tracks / required_leadout_lba (a set may mix a
|
||||
* CD-DA disc with a data-only one, and the lead-out LBA is literally the
|
||||
* disc's size). Resolving the whole struct per image -- rather than reaching
|
||||
* for the one global -- is what keeps those honest as soon as a title needs
|
||||
* them; today only the fingerprint has per-disc data to apply.
|
||||
*
|
||||
* A disc with no per-disc fingerprint keeps the flat [netplay]
|
||||
* required_disc_fp, so single-disc titles are unaffected. */
|
||||
static PSXRecompV4::NetplayDiscExpect netplay_expect_for_disc(
|
||||
const std::filesystem::path& disc) {
|
||||
PSXRecompV4::NetplayDiscExpect e = g_netplay_disc_expect;
|
||||
if (!g_disc_netplay_fps.empty()) {
|
||||
const auto it =
|
||||
g_disc_netplay_fps.find(uppercase_ascii(disc.stem().string()));
|
||||
if (it != g_disc_netplay_fps.end()) e.required_disc_fp = it->second;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
static std::string g_session_disc_fp;
|
||||
static bool g_session_netplay_disc_ok = false;
|
||||
|
||||
@@ -7494,12 +7581,24 @@ namespace {
|
||||
int ae_disc_verify(const char* disc_path, RecompLauncherCDiscVerify* out) {
|
||||
if (!disc_path || !disc_path[0] || !out) return 0;
|
||||
std::memset(out, 0, sizeof(*out));
|
||||
/* Which serial THIS disc should carry -- the set's per-disc value, or
|
||||
* the game's own for a single-disc title. The expected CRC is disc
|
||||
* 1's and is only consulted when the game supplied one, so a set that
|
||||
* ships a CRC and lets the player pick disc 2 lands on the "warn"
|
||||
* verdict rather than "bad". */
|
||||
const std::string expect_serial =
|
||||
expected_serial_for_disc(std::filesystem::path(disc_path),
|
||||
g_lnch_expected_serial);
|
||||
/* The netplay gate is the SELECTED disc's, for the same reason the
|
||||
* serial is -- see netplay_expect_for_disc(). */
|
||||
const PSXRecompV4::NetplayDiscExpect np_expect =
|
||||
netplay_expect_for_disc(std::filesystem::path(disc_path));
|
||||
PSXRecompV4::DiscIdentity id = PSXRecompV4::identify_disc(
|
||||
disc_path, g_lnch_expected_serial, g_lnch_expected_crc,
|
||||
disc_path, expect_serial, g_lnch_expected_crc,
|
||||
g_lnch_has_crc, /*compute_crc*/ g_lnch_has_crc,
|
||||
g_lnch_netplay_available ? &g_netplay_disc_expect : nullptr);
|
||||
g_lnch_netplay_available ? &np_expect : nullptr);
|
||||
const std::string& serial = !id.detected_serial.empty()
|
||||
? id.detected_serial : g_lnch_expected_serial;
|
||||
? id.detected_serial : expect_serial;
|
||||
std::snprintf(out->serial, sizeof(out->serial), "%s", serial.c_str());
|
||||
std::snprintf(out->region, sizeof(out->region), "%s", id.region.c_str());
|
||||
out->iso_ok = id.has_header ? 1 : 0;
|
||||
@@ -10901,6 +11000,15 @@ int main(int argc, char** argv) {
|
||||
std::string resolved_language = "en";
|
||||
std::vector<PSXRecompV4::RuntimeConfig::LanguageOption> lang_menu_options;
|
||||
std::filesystem::path resolved_disc;
|
||||
/* game.toml [game] discs, in disc order -- the images this build was made
|
||||
* from. Single-disc titles leave one entry (or none). This is the roster
|
||||
* the launcher's Disc Selection dropdown offers and the list [disc]
|
||||
* selected indexes into; it is NOT a scan of the player's folder, so a
|
||||
* player who moved an image still browses for it. */
|
||||
std::vector<std::filesystem::path> game_discs;
|
||||
/* 1-based selected disc, from settings.toml [disc] selected. Kept at main
|
||||
* scope because it round-trips through both launcher entry points. */
|
||||
int selected_disc_index = 1;
|
||||
std::string window_title = PSX_WINDOW_TITLE;
|
||||
uint16_t debug_port = (uint16_t)DEFAULT_DEBUG_PORT;
|
||||
std::string game_name;
|
||||
@@ -10964,6 +11072,26 @@ int main(int argc, char** argv) {
|
||||
(gc.netplay_local_viewport_aspect == "16:9") ? 1 :
|
||||
(gc.netplay_local_viewport_aspect == "21:9") ? 2 :
|
||||
(gc.netplay_local_viewport_aspect == "adaptive") ? 3 : 0;
|
||||
game_discs = gc.discs;
|
||||
/* Per-disc serial gate, shared by the launch-time disc check and
|
||||
* the launcher's disc verdict. Keyed by the image's uppercased
|
||||
* stem so a .cue and its .bin agree. */
|
||||
g_disc_serials.clear();
|
||||
for (size_t i = 0;
|
||||
i < gc.discs.size() && i < gc.disc_serials.size(); ++i) {
|
||||
if (gc.disc_serials[i].empty()) continue;
|
||||
g_disc_serials[uppercase_ascii(gc.discs[i].stem().string())] =
|
||||
gc.disc_serials[i];
|
||||
}
|
||||
/* Same keying for the per-disc netplay TOC fingerprints. */
|
||||
g_disc_netplay_fps.clear();
|
||||
for (size_t i = 0;
|
||||
i < gc.discs.size() && i < gc.netplay_required_disc_fps.size();
|
||||
++i) {
|
||||
if (gc.netplay_required_disc_fps[i].empty()) continue;
|
||||
g_disc_netplay_fps[uppercase_ascii(gc.discs[i].stem().string())] =
|
||||
gc.netplay_required_disc_fps[i];
|
||||
}
|
||||
if (!gc.discs.empty()) resolved_disc = gc.discs.front();
|
||||
if (gc.runtime.has_memcard_dir) memcard_dir = gc.runtime.memcard_dir;
|
||||
if (gc.runtime.has_window_title) window_title = gc.runtime.window_title;
|
||||
@@ -11464,6 +11592,22 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
if (us.has_disc_path && !disc_override_path)
|
||||
resolved_disc = normalize_disc_path_for_launch(us.disc_path);
|
||||
/* Multi-disc: the remembered disc NUMBER decides which image boots,
|
||||
* so a skip-launcher boot resumes on the disc the player left off on
|
||||
* and an external launcher can move them by writing one field. */
|
||||
if (us.has_disc_index) selected_disc_index = us.disc_index;
|
||||
if (!game_discs.empty())
|
||||
selected_disc_index =
|
||||
std::min(std::max(selected_disc_index, 1), (int)game_discs.size());
|
||||
if (!disc_override_path && game_discs.size() > 1) {
|
||||
const auto sel = resolve_selected_disc(game_discs,
|
||||
selected_disc_index,
|
||||
resolved_disc);
|
||||
/* Never normalize an empty path -- fs::absolute("") is the cwd,
|
||||
* which would turn "no disc yet" into a bogus mount. */
|
||||
if (!sel.empty())
|
||||
resolved_disc = normalize_disc_path_for_launch(sel);
|
||||
}
|
||||
if (us.has_memcard_dir) memcard_dir = us.memcard_dir;
|
||||
if (us.has_memcard1_path) memcard1_path = us.memcard1_path;
|
||||
if (us.has_memcard2_path) memcard2_path = us.memcard2_path;
|
||||
@@ -12114,6 +12258,9 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
ls.memcard_enabled[0] = seed.memcard1_enabled ? 1 : 0;
|
||||
ls.memcard_enabled[1] = seed.memcard2_enabled ? 1 : 0;
|
||||
/* Which disc the dropdown opens on (1-based; ignored when the
|
||||
* game is single-disc and gi.discs is empty). */
|
||||
ls.disc_index = selected_disc_index;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_MULTITAP_ENABLED)
|
||||
ls.multitap_enabled = seed.multitap_enabled ? 1 : 0;
|
||||
#endif
|
||||
@@ -12144,6 +12291,27 @@ int main(int argc, char** argv) {
|
||||
g_lnch_has_crc = game_has_disc_crc;
|
||||
g_lnch_argv0 = argv[0];
|
||||
|
||||
/* Multi-disc roster for the launcher's Disc Selection dropdown.
|
||||
* The ABI BORROWS every pointer, so this storage has to outlive
|
||||
* recomp_launcher_run_window below -- hence plain locals in this
|
||||
* scope rather than a temporary. Paths go through the same
|
||||
* normalization as every other mount so a roster entry and the
|
||||
* initial disc are spelled identically. Labels are left null:
|
||||
* recomp-ui formats "Disc N" from the number itself, and one
|
||||
* source of that string beats two. */
|
||||
std::vector<std::string> rui_disc_paths;
|
||||
std::vector<RecompLauncherCDisc> rui_discs;
|
||||
if (game_discs.size() > 1) {
|
||||
rui_disc_paths.reserve(game_discs.size());
|
||||
for (const auto& d : game_discs)
|
||||
rui_disc_paths.push_back(
|
||||
normalize_disc_path_for_launch(d).string());
|
||||
rui_discs.reserve(rui_disc_paths.size());
|
||||
for (size_t i = 0; i < rui_disc_paths.size(); ++i)
|
||||
rui_discs.push_back(RecompLauncherCDisc{
|
||||
(int)i + 1, nullptr, rui_disc_paths[i].c_str()});
|
||||
}
|
||||
|
||||
RecompLauncherCGameInfo gi{};
|
||||
ae_fill_psx_launcher_game_info(
|
||||
&gi,
|
||||
@@ -12161,6 +12329,8 @@ int main(int argc, char** argv) {
|
||||
rui_lang_labels.empty() ? nullptr : rui_lang_labels.data(),
|
||||
(int)rui_lang_labels.size(),
|
||||
/*resume_netplay_room=*/0);
|
||||
gi.discs = rui_discs.empty() ? nullptr : rui_discs.data();
|
||||
gi.num_discs = (int)rui_discs.size();
|
||||
#if defined(PSX_HAS_SETUP_WIZARD)
|
||||
/* MotK ships tools/prepare_disc.py (2448→2352). Offer it in the
|
||||
* first-run wizard so players need not run the script by hand. */
|
||||
@@ -12246,6 +12416,16 @@ int main(int argc, char** argv) {
|
||||
seed.disc_path = rui_out_disc;
|
||||
seed.has_disc_path = true;
|
||||
}
|
||||
/* Persist the Disc Selection choice next to the disc path, so
|
||||
* the next session opens on the same disc and an external
|
||||
* launcher sees it as an ordinary settings row. Written for
|
||||
* multi-disc titles only -- a single-disc game has nothing to
|
||||
* select and should not grow a meaningless key. */
|
||||
if (game_discs.size() > 1 && ls.disc_index > 0) {
|
||||
selected_disc_index = ls.disc_index;
|
||||
seed.disc_index = ls.disc_index;
|
||||
seed.has_disc_index = true;
|
||||
}
|
||||
seed.fullscreen = ls.fullscreen; seed.has_fullscreen = true;
|
||||
seed.skip_launcher = ls.skip_launcher != 0; seed.has_skip_launcher = true;
|
||||
/* aspect_index round-trips 0/1/2 -> 4:3 / 16:9 / 21:9, superseding the
|
||||
@@ -13431,17 +13611,21 @@ session_reboot:
|
||||
const std::filesystem::path disc_check = resolved_disc;
|
||||
if (!disc_check.empty()) {
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
const std::string& expect_serial = g_lnch_expected_serial;
|
||||
const std::string expect_serial =
|
||||
expected_serial_for_disc(disc_check, g_lnch_expected_serial);
|
||||
const uint32_t expect_crc = g_lnch_expected_crc;
|
||||
const bool has_crc = g_lnch_has_crc;
|
||||
#else
|
||||
const std::string expect_serial = game_id;
|
||||
const std::string expect_serial =
|
||||
expected_serial_for_disc(disc_check, game_id);
|
||||
const uint32_t expect_crc = game_disc_crc;
|
||||
const bool has_crc = game_has_disc_crc;
|
||||
#endif
|
||||
const PSXRecompV4::NetplayDiscExpect np_expect =
|
||||
netplay_expect_for_disc(disc_check);
|
||||
PSXRecompV4::DiscIdentity nid = PSXRecompV4::identify_disc(
|
||||
disc_check, expect_serial, expect_crc, has_crc,
|
||||
/*compute_crc*/ false, &g_netplay_disc_expect);
|
||||
/*compute_crc*/ false, &np_expect);
|
||||
g_session_disc_fp = nid.disc_fp;
|
||||
g_session_netplay_disc_ok = nid.netplay_ok && !nid.disc_fp.empty();
|
||||
psx_lobby_set_disc_fp(nid.disc_fp.c_str());
|
||||
@@ -13954,6 +14138,7 @@ soft_return_lobby:
|
||||
RecompLauncherCSettings ls{};
|
||||
ls.output_method = 2;
|
||||
ls.window_scale = std::max(1, std::min(4, g_video_win_w / 320));
|
||||
ls.disc_index = selected_disc_index;
|
||||
ls.fullscreen = g_fullscreen ? 1 : 0;
|
||||
ls.ignore_aspect = 0;
|
||||
ls.linear_filter = (g_video_texfilter != 0) ? 1 : 0;
|
||||
@@ -14058,6 +14243,20 @@ soft_return_lobby:
|
||||
for (const auto& lo : lang_menu_options)
|
||||
rui_lang_labels.push_back(lo.label.c_str());
|
||||
|
||||
/* Same borrowed-roster contract as the first-open launcher above. */
|
||||
std::vector<std::string> rui_disc_paths;
|
||||
std::vector<RecompLauncherCDisc> rui_discs;
|
||||
if (game_discs.size() > 1) {
|
||||
rui_disc_paths.reserve(game_discs.size());
|
||||
for (const auto& d : game_discs)
|
||||
rui_disc_paths.push_back(
|
||||
normalize_disc_path_for_launch(d).string());
|
||||
rui_discs.reserve(rui_disc_paths.size());
|
||||
for (size_t i = 0; i < rui_disc_paths.size(); ++i)
|
||||
rui_discs.push_back(RecompLauncherCDisc{
|
||||
(int)i + 1, nullptr, rui_disc_paths[i].c_str()});
|
||||
}
|
||||
|
||||
RecompLauncherCGameInfo gi{};
|
||||
ae_fill_psx_launcher_game_info(
|
||||
&gi,
|
||||
@@ -14075,6 +14274,8 @@ soft_return_lobby:
|
||||
rui_lang_labels.empty() ? nullptr : rui_lang_labels.data(),
|
||||
(int)rui_lang_labels.size(),
|
||||
/*resume_netplay_room=*/1);
|
||||
gi.discs = rui_discs.empty() ? nullptr : rui_discs.data();
|
||||
gi.num_discs = (int)rui_discs.size();
|
||||
#if defined(PSX_HAS_SETUP_WIZARD) && defined(PSX_HAS_GAME_CODEGEN)
|
||||
psx_game_codegen_setup_apply(&gi);
|
||||
#endif
|
||||
@@ -14198,6 +14399,11 @@ soft_return_lobby:
|
||||
}
|
||||
us.deadzone = player_deadzone[0];
|
||||
us.has_deadzone = true;
|
||||
if (game_discs.size() > 1 && ls.disc_index > 0) {
|
||||
selected_disc_index = ls.disc_index;
|
||||
us.disc_index = ls.disc_index;
|
||||
us.has_disc_index = true;
|
||||
}
|
||||
#if defined(RECOMP_LAUNCHER_HAS_MULTITAP_ENABLED)
|
||||
multitap_enabled = ls.multitap_enabled != 0;
|
||||
us.multitap_enabled = multitap_enabled;
|
||||
|
||||
Reference in New Issue
Block a user