From 96b4bab947eeb4cf1d6e98cbeba8efc6da3e0820 Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sun, 27 Sep 2026 15:00:07 -0400 Subject: [PATCH] runner: a Transfer Pak on every seat (--tpak1-rom .. --tpak4-rom) The runner bound a Transfer Pak on seat 0 only. The ABI already carries a seat per accessory binding, and n64lle's core accepts n64.transfer_pak on seats 0-3, declaring tpakN / tpakN.rtc for each. - runner: --tpakN-rom / -save / -rtc for N = 1-4, in headless and link mode, one n64.transfer_pak binding per seat with a cartridge (transfer_pak_bindings); save files tpakN, tpakN.rtc. - --version reports transfer_pak_seats 4; probe_runner reads it (1 when absent: a runner from before takes seat 1 only). - corelink: LaunchSpec::tpak_roms[4] replaces tpak_rom; the link test takes --tpakN-*. - docs: CORE_RUNNER, CORE_LINK, RELEASES; OVERLAY/CORE_LINK note the hub's rebindable shortcuts (Function combos, L3 + R3, no Guide). Checked: 60 headless frames of Pokemon Stadium with Red in seat 2 and Gold in seat 4 (tpak2, tpak4, tpak4.rtc declared and written), and 60 frames through the link with Blue in seat 3 (saved by the host); ctest 18/18. Co-Authored-By: Claude Opus 5.5 --- corelink/core_link.cpp | 7 ++--- corelink/core_link.hpp | 7 ++++- corelink/link_test_main.cpp | 12 ++++++--- corelink/runner_probe.cpp | 3 +++ corelink/runner_probe.hpp | 3 +++ docs/CORE_LINK.md | 13 ++++++--- docs/CORE_RUNNER.md | 10 ++++--- docs/OVERLAY.md | 8 +++--- docs/RELEASES.md | 1 + runner/runner_link.cpp | 26 ++++++++++++------ runner/runner_link.hpp | 16 ++++++++++- runner/runner_main.cpp | 53 ++++++++++++++++++++++--------------- 12 files changed, 110 insertions(+), 49 deletions(-) diff --git a/corelink/core_link.cpp b/corelink/core_link.cpp index d7d7ee1..1309ff8 100644 --- a/corelink/core_link.cpp +++ b/corelink/core_link.cpp @@ -64,9 +64,10 @@ bool CoreLink::start(const LaunchSpec& spec, std::string* error) { args.push_back("--load-state"); args.push_back(path_utf8(*spec_.load_state)); } - if (!spec_.tpak_rom.empty()) { - args.push_back("--tpak1-rom"); - args.push_back(spec_.tpak_rom); + for (std::size_t seat = 0; seat < spec_.tpak_roms.size(); ++seat) { + if (spec_.tpak_roms[seat].empty()) continue; + args.push_back("--tpak" + std::to_string(seat + 1) + "-rom"); + args.push_back(spec_.tpak_roms[seat]); } sp.env = spec_.env; sp.log = runner_log(); diff --git a/corelink/core_link.hpp b/corelink/core_link.hpp index 06b0ace..75c7d50 100644 --- a/corelink/core_link.hpp +++ b/corelink/core_link.hpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -32,7 +33,11 @@ struct LaunchSpec { bool strict = false; std::map options; std::optional load_state; - std::string tpak_rom; + // Seat N's Transfer Pak cartridge (argv --tpakN-rom), N = 1-4; empty for + // a seat without one. The runner must report transfer_pak_seats >= N + // (runner_probe.hpp); an older runner takes seat 1 only. Its battery + // save is save_files["tpakN"], the MBC3 clock save_files["tpakN.rtc"]. + std::array tpak_roms; std::map save_files; // region id -> file // Regions not named above land here as .sav. Empty = only the named // ones persist. (Region ids are only known once the core has loaded.) diff --git a/corelink/link_test_main.cpp b/corelink/link_test_main.cpp index 3c7b41a..9239913 100644 --- a/corelink/link_test_main.cpp +++ b/corelink/link_test_main.cpp @@ -108,9 +108,15 @@ int main(int argc, char** argv) { else if (a == "--out") spec.session_dir = utf8_path(val()); else if (a == "--frames") frames = std::strtoull(val().c_str(), nullptr, 10); else if (a == "--load-state") spec.load_state = utf8_path(val()); - else if (a == "--tpak1-rom") spec.tpak_rom = val(); - else if (a == "--tpak1-save") spec.save_files["tpak1"] = utf8_path(val()); - else if (a == "--tpak1-rtc") spec.save_files["tpak1.rtc"] = utf8_path(val()); + else if (a.size() >= 11 && a.compare(0, 6, "--tpak") == 0 && a[6] >= '1' && a[6] <= '4') { + const std::size_t seat = static_cast(a[6] - '1'); + const std::string id = "tpak" + std::string(1, a[6]); + const std::string what = a.substr(7); + if (what == "-rom") spec.tpak_roms[seat] = val(); + else if (what == "-save") spec.save_files[id] = utf8_path(val()); + else if (what == "-rtc") spec.save_files[id + ".rtc"] = utf8_path(val()); + else die("unknown argument " + a); + } else if (a == "--save") { // = const std::string kv = val(); const auto eq = kv.find('='); diff --git a/corelink/runner_probe.cpp b/corelink/runner_probe.cpp index ca6df50..5eb1a9b 100644 --- a/corelink/runner_probe.cpp +++ b/corelink/runner_probe.cpp @@ -70,6 +70,9 @@ bool probe_runner(const fs::path& runner, RunnerVersion& out, std::string* error if (fields.count("describe") && !num("describe", v.describe)) { return fail("--version printed an unreadable describe line"); } + if (fields.count("transfer_pak_seats") && !num("transfer_pak_seats", v.transfer_pak_seats)) { + return fail("--version printed an unreadable transfer_pak_seats line"); + } const std::string link = fields["link_protocol"]; const auto dot = link.find('.'); bool ok = !v.version.empty() && dot != std::string::npos && num("rcore_abi_major", v.abi_major) && diff --git a/corelink/runner_probe.hpp b/corelink/runner_probe.hpp index 6e6507a..c30eff4 100644 --- a/corelink/runner_probe.hpp +++ b/corelink/runner_probe.hpp @@ -22,6 +22,9 @@ struct RunnerVersion { // 1 when the runner takes --package (GAME_PACKAGE cores); 0 for a runner // from before it did, which prints no game_package line. std::uint32_t game_package = 0; + // Seats that take a Transfer Pak (--tpak1-rom .. --tpakN-rom): 4, or 1 + // for a runner from before seats 2-4, which prints no transfer_pak_seats. + std::uint32_t transfer_pak_seats = 1; // 1 when the runner answers --describe (a core's declared options and // inputs, no ROM; docs/CORE_RUNNER.md); 0 for a runner from before it. std::uint32_t describe = 0; diff --git a/docs/CORE_LINK.md b/docs/CORE_LINK.md index 9c33ec5..6738bd7 100644 --- a/docs/CORE_LINK.md +++ b/docs/CORE_LINK.md @@ -132,7 +132,7 @@ states neither falls back to a fixed 60 Hz. ## Direct mode `retro-hub --run-core [--package ] --rom [--title-dir D] -[--tpak1-rom GB --tpak1-save SAV] [--opt key=value ...] [--no-gl]` boots +[--tpakN-rom GB --tpakN-save SAV (N = 1-4)] [--opt key=value ...] [--no-gl]` boots straight into the core and exits when the player closes it. This is the shape a standalone release takes (`HOST_LIFECYCLE.md` §3). `` is a per-title core (`_core.so`) or a generic one with the `game_package` capability (such as @@ -150,10 +150,15 @@ package; `--title-dir` then defaults to the shim's directory. source is logged. With no usable runner, or `--package` and a runner without `game_package 1`, the window shows the error and the hub exits 1. A background runtime update applies from the next launch, never mid-session. -- **Menu:** the guide button, Esc or F1 open the paused quick menu (Resume, +- **Menu:** Esc or F1, or L3 + R3 on a pad, open the paused quick menu (Resume, Save states, Show FPS, Volume, Close game). F11 toggles fullscreen. -- **Overlay** (`OVERLAY.md`): F3 FPS, Tab (held) turbo, +/- volume, F7 or - SELECT + R1 save states. +- **Overlay** (`OVERLAY.md`): F3 FPS, Tab (held) turbo, +/- volume, F7 save + states by default. Since 2026-09-27 the hub's hotkeys are the player's to + bind (Retro-Launcher's settings page, `play.ini` `[keys]` / `[combos]`), and + its pad shortcuts are `Function + <button>`, Function being an input the + player assigns per controller (Back by default: some USB pads report Select + oddly). Save states are Function + R1; L3 + R3 also opens the menu. The hub + keeps SELECT out of the browser's own chord. - **Input:** - Gamepads fill seats 0–3 in the order SDL lists them. - With none attached, the keyboard is port 1: arrows = D-pad, X/Z/C/S = the diff --git a/docs/CORE_RUNNER.md b/docs/CORE_RUNNER.md index 251b1b2..e406b8b 100644 --- a/docs/CORE_RUNNER.md +++ b/docs/CORE_RUNNER.md @@ -51,7 +51,10 @@ This page covers what exists and how it is checked. compares. Its command line and outputs match n64lle's `rcore_probe` (`--core --package --rom --title-dir --out --frames --load-state --tpak1-rom --tpak1-save --tpak1-rtc --gl --strict --no-seats --replay-at --opt - --input-script --list-options`). + --input-script --list-options`). The Transfer Pak flags go up to seat 4 + (`--tpakN-rom`, `--tpakN-save`, `--tpakN-rtc`, N = 1-4; 2026-09-27): each + seat with a cartridge gets an `n64.transfer_pak` binding at slot 0, and its + save regions are `tpakN` and `tpakN.rtc`. 5. **Savestates** (`runner/state_keeper.*`, 2026-09-26). The runner writes and checks the savestate envelope (`OVERLAY.md`, `CORE_ABI.md` "Savestates"). It holds every identity the load rule compares: the core's @@ -73,8 +76,9 @@ This page covers what exists and how it is checked. `--version` prints the release version, commit, link protocol and rcore ABI compiled in, whether `--gl` is available, `game_package 1` (this runner -takes `--package`) and `describe 1` (this runner answers `--describe`), and -exits 0 (`RELEASES.md`). +takes `--package`), `describe 1` (this runner answers `--describe`) and +`transfer_pak_seats 4` (it takes `--tpak1-rom` to `--tpak4-rom`; a runner +without the line takes seat 1 only), and exits 0 (`RELEASES.md`). Exit codes: diff --git a/docs/OVERLAY.md b/docs/OVERLAY.md index 703201e..c5183a5 100644 --- a/docs/OVERLAY.md +++ b/docs/OVERLAY.md @@ -147,14 +147,14 @@ These are the hub's choices, listed here because every core gets them: | F3 | show / hide FPS (also a setting: "Show FPS", saved in `<data_dir>/play.ini`) | | Tab (held) | turbo; sound is dropped while it runs | | + / - (`=` `-` or keypad) | volume, 10% steps, meter on the right (saved in `play.ini`) | -| F7, or SELECT + R1 | save states | -| Esc, F1, Guide | the pause menu, which also has Save states, Show FPS and Volume | +| F7, or SELECT + R1 (Retro-Launcher's hub: Function + R1, rebindable) | save states | +| Esc, F1 (Retro-Launcher's hub: also L3 + R3 or Function + Start; never Guide, which Steam keeps) | the pause menu, which also has Save states, Show FPS and Volume | - The recomp-ui family binds FPS to F. In the hub F drives C-Left (the TFGH right stick), so FPS is F3. - Turbo holds on key events and drops when the window loses focus. -- Nothing here is bound on a pad except the browser's chord and the existing - Guide. +- Nothing here is bound on a pad except the browser's chord; the hub adds its + own pad shortcuts (Function combos, L3 + R3) and leaves Guide alone. ## How it was checked (2026-09-26) diff --git a/docs/RELEASES.md b/docs/RELEASES.md index 2eab33e..06bcd14 100644 --- a/docs/RELEASES.md +++ b/docs/RELEASES.md @@ -115,6 +115,7 @@ rcore_draft_revision 5 gl 1 game_package 1 describe 1 +transfer_pak_seats 4 ``` A build outside a release says `version dev`. `game_package 1` (2026-09-26) diff --git a/runner/runner_link.cpp b/runner/runner_link.cpp index 8095016..28f1093 100644 --- a/runner/runner_link.cpp +++ b/runner/runner_link.cpp @@ -12,6 +12,23 @@ namespace retro::runner { +std::vector<rcore_accessory_binding> transfer_pak_bindings( + const std::array<std::string, kTransferPakSeats>& roms) { + std::vector<rcore_accessory_binding> out; + for (std::size_t seat = 0; seat < roms.size(); ++seat) { + if (roms[seat].empty()) continue; + rcore_accessory_binding b{}; + b.struct_size = sizeof b; + b.seat = static_cast<std::uint32_t>(seat); + b.slot = 0; + b.type_id = "n64.transfer_pak"; + b.content_path = roms[seat].c_str(); + b.content_sha256 = nullptr; + out.push_back(b); + } + return out; +} + using namespace retro::corelink; namespace { @@ -206,14 +223,7 @@ int run_link_mode(const LoadedCore& core, const CoreManifest& manifest, const Li return exiting(sock, 2, "init -> " + std::to_string(rc)); } - std::vector<rcore_accessory_binding> bindings; - if (!a.tpak_rom.empty()) { - rcore_accessory_binding b{}; - b.struct_size = sizeof b; - b.type_id = "n64.transfer_pak"; - b.content_path = a.tpak_rom.c_str(); - bindings.push_back(b); - } + std::vector<rcore_accessory_binding> bindings = transfer_pak_bindings(a.tpak_roms); rcore_load_params lp{}; lp.struct_size = sizeof lp; lp.content_path = a.rom.c_str(); diff --git a/runner/runner_link.hpp b/runner/runner_link.hpp index 7f76912..be8a213 100644 --- a/runner/runner_link.hpp +++ b/runner/runner_link.hpp @@ -9,10 +9,22 @@ #include <map> #include <optional> +#include <array> +#include <cstddef> #include <string> +#include <vector> namespace retro::runner { +// Seats that can carry a Transfer Pak: the N64's four controller ports +// (`transfer_pak_seats` in --version; --tpak1-rom .. --tpak4-rom). +constexpr std::size_t kTransferPakSeats = 4; + +// One n64.transfer_pak binding per seat that has a cartridge (slot 0: one pak +// per controller, rcore.h / docs/CORE_ABI.md). The strings must outlive them. +std::vector<rcore_accessory_binding> transfer_pak_bindings( + const std::array<std::string, kTransferPakSeats>& roms); + struct LinkArgs { std::string rom; std::string package; // --package; empty for a core without game_package @@ -23,7 +35,9 @@ struct LinkArgs { bool strict = false; std::map<std::string, std::string> overrides; std::optional<fs::path> load_state; - std::string tpak_rom; + // --tpakN-rom: the Game Boy cartridge in seat N's Transfer Pak (N = 1-4), + // empty for a seat without one. + std::array<std::string, kTransferPakSeats> tpak_roms; std::string link_handles; // --link-handles (Windows) }; diff --git a/runner/runner_main.cpp b/runner/runner_main.cpp index 47960f2..ebebac1 100644 --- a/runner/runner_main.cpp +++ b/runner/runner_main.cpp @@ -55,6 +55,7 @@ #include "state_keeper.hpp" #include "transport.hpp" +#include <array> #include <cinttypes> #include <cstdio> #include <cstdlib> @@ -261,10 +262,11 @@ void print_version() { "rcore_draft_revision %u\n" "gl %d\n" "game_package 1\n" - "describe 1\n", + "describe 1\n" + "transfer_pak_seats %zu\n", RETRO_RUNTIME_VERSION, RETRO_RUNTIME_VERSION, RETRO_RUNTIME_COMMIT, retro::corelink::kProtocolMajor, retro::corelink::kProtocolMinor, - RCORE_ABI_MAJOR, RCORE_DRAFT_REVISION, gl); + RCORE_ABI_MAJOR, RCORE_DRAFT_REVISION, gl, retro::runner::kTransferPakSeats); } // One --describe field: \ TAB LF CR escaped, so a record is one line; a NULL @@ -341,8 +343,12 @@ int main(int argc, char** argv) { std::string core_path, rom, package, title_dir = "."; fs::path out = "."; std::uint64_t frames = 60; - std::optional<fs::path> load_state, tpak_save, tpak_rtc; - std::string tpak_rom, link_handles; + std::optional<fs::path> load_state; + // --tpakN-rom / -save / -rtc, N = 1-4: seat N's Transfer Pak cartridge, + // its battery save and its MBC3 clock. + std::array<std::string, retro::runner::kTransferPakSeats> tpak_roms; + std::array<std::optional<fs::path>, retro::runner::kTransferPakSeats> tpak_saves, tpak_rtcs; + std::string link_handles; bool gl = false, strict = false, seat0 = true, list_options = false, link = false; bool describe = false; std::optional<std::uint64_t> replay_at; @@ -371,9 +377,18 @@ int main(int argc, char** argv) { else if (a == "--out") out = retro::corelink::utf8_path(val()); else if (a == "--frames") frames = num(val()); else if (a == "--load-state") load_state = retro::corelink::utf8_path(val()); - else if (a == "--tpak1-rom") tpak_rom = val(); - else if (a == "--tpak1-save") tpak_save = retro::corelink::utf8_path(val()); - else if (a == "--tpak1-rtc") tpak_rtc = retro::corelink::utf8_path(val()); + else if (a.size() == 11 && a.compare(0, 6, "--tpak") == 0 && a[6] >= '1' && + a[6] < static_cast<char>('1' + retro::runner::kTransferPakSeats) && + a.compare(7, 4, "-rom") == 0) + tpak_roms[static_cast<std::size_t>(a[6] - '1')] = val(); + else if (a.size() == 12 && a.compare(0, 6, "--tpak") == 0 && a[6] >= '1' && + a[6] < static_cast<char>('1' + retro::runner::kTransferPakSeats) && + a.compare(7, 5, "-save") == 0) + tpak_saves[static_cast<std::size_t>(a[6] - '1')] = retro::corelink::utf8_path(val()); + else if (a.size() == 11 && a.compare(0, 6, "--tpak") == 0 && a[6] >= '1' && + a[6] < static_cast<char>('1' + retro::runner::kTransferPakSeats) && + a.compare(7, 4, "-rtc") == 0) + tpak_rtcs[static_cast<std::size_t>(a[6] - '1')] = retro::corelink::utf8_path(val()); else if (a == "--gl") gl = true; else if (a == "--link") link = true; else if (a == "--link-handles") link_handles = val(); @@ -495,7 +510,7 @@ int main(int argc, char** argv) { la.strict = strict; la.overrides = overrides; la.load_state = load_state; - la.tpak_rom = tpak_rom; + la.tpak_roms = tpak_roms; la.link_handles = link_handles; std::fflush(stdout); return run_link_mode(core, manifest, la, lend); @@ -528,18 +543,8 @@ int main(int argc, char** argv) { die("init -> " + std::to_string(rc)); } - // ---- load: content, the Transfer Pak binding, host-owned saves ------- - std::vector<rcore_accessory_binding> bindings; - if (!tpak_rom.empty()) { - rcore_accessory_binding b{}; - b.struct_size = sizeof b; - b.seat = 0; - b.slot = 0; - b.type_id = "n64.transfer_pak"; - b.content_path = tpak_rom.c_str(); - b.content_sha256 = nullptr; - bindings.push_back(b); - } + // ---- load: content, the Transfer Pak bindings, host-owned saves ------ + std::vector<rcore_accessory_binding> bindings = retro::runner::transfer_pak_bindings(tpak_roms); rcore_load_params lp{}; lp.struct_size = sizeof lp; lp.content_path = rom.c_str(); @@ -554,8 +559,12 @@ int main(int argc, char** argv) { die("load -> " + std::to_string(rc)); } std::map<std::string, fs::path> save_files; - if (tpak_save) save_files["tpak1"] = *tpak_save; - if (tpak_rtc) save_files["tpak1.rtc"] = *tpak_rtc; + // The core names seat N's regions tpakN and tpakN.rtc (docs/CORE_ABI.md). + for (std::size_t seat = 0; seat < retro::runner::kTransferPakSeats; ++seat) { + const std::string id = "tpak" + std::to_string(seat + 1); + if (tpak_saves[seat]) save_files[id] = *tpak_saves[seat]; + if (tpak_rtcs[seat]) save_files[id + ".rtc"] = *tpak_rtcs[seat]; + } session.adopt_save_regions(regs, nregs, save_files); for (const SaveRegion& r : session.save_regions()) { std::printf("save region %s: kind=%u seat=%u size=%zu from %s\n", r.id.c_str(), r.kind,