diff --git a/host/psxrecomp_codegen_host.c b/host/psxrecomp_codegen_host.c index 23aff86d..b28e518d 100644 --- a/host/psxrecomp_codegen_host.c +++ b/host/psxrecomp_codegen_host.c @@ -1323,6 +1323,108 @@ static int resolve_bios_arg(char* out, size_t cap) { } /* bios_path NULL = leave bios.cfg untouched; "" = clear (OpenBIOS); else write. */ +/* ---- multi-disc roster (game.toml [game] discs) -------------------------- + * + * The RUNTIME launcher has published a disc roster for a while, built from + * game.toml [game] discs. The SETUP host never did -- so on a first run the + * wizard could not know a title was a 3-disc set and asked for a single image. + * The other discs were then discovered missing much later, mid-game, at the + * first swap. + * + * Mirrors the runtime's rule: a roster is published only when there is more + * than one image, so single-disc titles see no behavioural change at all. + * Matches LNG_MAX_DISCS in recomp-ui; a longer list is clamped, exactly as the + * launcher clamps it. */ +#define PSX_HOST_MAX_DISCS 8 + +static char g_disc_paths[PSX_HOST_MAX_DISCS][1024]; +static RecompLauncherCDisc g_disc_roster[PSX_HOST_MAX_DISCS]; +static int g_num_discs; + +static int host_path_is_absolute(const char* p) { + if (!p || !p[0]) return 0; + if (p[0] == '/' || p[0] == '\\') return 1; + return (p[1] == ':' && ((p[0] >= 'A' && p[0] <= 'Z') || + (p[0] >= 'a' && p[0] <= 'z'))); +} + +/* Minimal reader for one known key rather than a TOML library: this host shells + * out to Python for every real parse, and pulling in a parser to read a single + * array of strings would be a dependency for one line of config. + * + * Only [game] discs is recognised, and only string entries -- anything else in + * the file is skipped rather than guessed at. */ +static void load_game_toml_disc_roster(void) { + FILE* f; + char line[1600]; + int in_game = 0, in_array = 0, i; + + g_num_discs = 0; + if (!g_game_toml[0]) return; + f = fopen(g_game_toml, "rb"); + if (!f) return; + + while (fgets(line, sizeof(line), f)) { + const char* p = line; + while (*p == ' ' || *p == '\t') ++p; + if (!in_array) { + if (*p == '[') { /* section header */ + in_game = (strncmp(p, "[game]", 6) == 0); + continue; + } + if (*p == '#' || !*p) continue; + if (!in_game) continue; + if (strncmp(p, "discs", 5) != 0) continue; + p += 5; + while (*p == ' ' || *p == '\t') ++p; + /* Guard against a longer key that merely starts with "discs". */ + if (*p != '=') continue; + ++p; + in_array = 1; + } + for (; *p; ++p) { + if (*p == ']') { in_array = 0; break; } + if (*p == '#') break; /* trailing comment */ + if (*p != '"') continue; + { + const char* q = ++p; + size_t n; + while (*p && *p != '"') ++p; + if (!*p) break; /* unterminated -- give up */ + n = (size_t)(p - q); + if (g_num_discs < PSX_HOST_MAX_DISCS && + n < sizeof(g_disc_paths[0])) { + memcpy(g_disc_paths[g_num_discs], q, n); + g_disc_paths[g_num_discs][n] = '\0'; + ++g_num_discs; + } + } + } + if (!in_array) break; + } + fclose(f); + + /* game.toml may hold repo-relative paths. The launcher existence-checks + * what we publish, and its cwd is not necessarily the project root, so a + * relative entry would render as "missing" on a perfectly good install. */ + for (i = 0; i < g_num_discs; ++i) { + char abs[1024]; + if (host_path_is_absolute(g_disc_paths[i])) continue; + if (!g_project_root[0]) continue; + if (join_path(abs, sizeof(abs), g_project_root, g_disc_paths[i])) + snprintf(g_disc_paths[i], sizeof(g_disc_paths[i]), "%s", abs); + } + + /* One image is not a set: leave GameInfo dark so nothing changes for + * single-disc titles. */ + if (g_num_discs < 2) { g_num_discs = 0; return; } + for (i = 0; i < g_num_discs; ++i) { + g_disc_roster[i].number = i + 1; + g_disc_roster[i].label = NULL; /* launcher formats "Disc N" */ + g_disc_roster[i].path = g_disc_paths[i]; + } +} + static int host_persist_setup(void* ctx, const char* rom_path, const char* bios_path) { char path[1200]; @@ -1368,6 +1470,61 @@ static int host_persist_setup(void* ctx, const char* rom_path, return 0; } +/* Multi-disc flush (RecompLauncherCGameInfo.persist_setup_discs). + * + * disc.cfg becomes one path per line, in disc order. Readers that take only + * the first line keep working and get disc 1 -- which is what they got before + * this existed -- so the format change is additive in the direction that + * matters. A slot the player has not located is written as an EMPTY line + * rather than skipped, so line N is always disc N and filling one in later + * cannot renumber the others. + * + * The BIOS half is delegated to host_persist_setup: passing rom_path NULL runs + * its bios.cfg logic and skips its single-path disc write, so the two entry + * points cannot drift on BIOS handling. */ +static int host_persist_setup_discs(void* ctx, const char* const* disc_paths, + int disc_count, const char* bios_path) { + char body[PSX_HOST_MAX_DISCS * 1030]; + char path[1200]; + size_t o = 0; + int i, any = 0; + + host_persist_setup(ctx, NULL, bios_path); + + if (!disc_paths || disc_count <= 0) + return 0; + if (disc_count > PSX_HOST_MAX_DISCS) + disc_count = PSX_HOST_MAX_DISCS; + + body[0] = '\0'; + for (i = 0; i < disc_count; ++i) { + const char* d = disc_paths[i] ? disc_paths[i] : ""; + size_t n = strlen(d); + if (o + n + 2 >= sizeof(body)) + return 0; + if (i) body[o++] = '\n'; + if (n) { memcpy(body + o, d, n); o += n; any = 1; } + body[o] = '\0'; + } + /* Nothing located: leave any existing disc.cfg alone rather than replacing + * it with a file of blank lines. */ + if (!any) + return 0; + + if (g_project_root[0] && + join_path(path, sizeof(path), g_project_root, "disc.cfg")) + write_line_file(path, body); + write_line_file("disc.cfg", body); + if (g_exe_path[0]) + write_sidecar_near_exe(g_exe_path, "disc.cfg", body); + if (g_build_dir[0]) { + char build_exe[1200]; + if (join_path(build_exe, sizeof(build_exe), g_build_dir, g_exe_basename)) + write_sidecar_near_exe(build_exe, "disc.cfg", body); + } + return 0; +} + static void persist_relaunch_sidecars(const char* near_exe, const char* disc_path) { char bios_line[1100]; @@ -3956,6 +4113,15 @@ void psxrecomp_codegen_host_apply(RecompLauncherCGameInfo* gi, (void)find_python(g_python, sizeof(g_python)); gi->persist_setup = host_persist_setup; gi->persist_setup_ctx = NULL; + /* Multi-disc titles only: load_game_toml_disc_roster leaves g_num_discs 0 + * for a single image, so the wizard keeps its one-image layout and the + * launcher keeps taking the single-path flush. */ + load_game_toml_disc_roster(); + if (g_num_discs > 1) { + gi->discs = g_disc_roster; + gi->num_discs = g_num_discs; + gi->persist_setup_discs = host_persist_setup_discs; + } gi->prepare_with_progress = host_prepare_generate; gi->prepare_use_selected_rom = 1; /* Number prefix is applied in the setup UI (BIOS adds a step). */