Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions corelink/core_link.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
7 changes: 6 additions & 1 deletion corelink/core_link.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include <cstdint>
#include <filesystem>
#include <array>
#include <map>
#include <optional>
#include <string>
Expand All @@ -32,7 +33,11 @@ struct LaunchSpec {
bool strict = false;
std::map<std::string, std::string> options;
std::optional<fs::path> 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<std::string, 4> tpak_roms;
std::map<std::string, fs::path> save_files; // region id -> file
// Regions not named above land here as <id>.sav. Empty = only the named
// ones persist. (Region ids are only known once the core has loaded.)
Expand Down
12 changes: 9 additions & 3 deletions corelink/link_test_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::size_t>(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") { // <region id>=<file>
const std::string kv = val();
const auto eq = kv.find('=');
Expand Down
3 changes: 3 additions & 0 deletions corelink/runner_probe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) &&
Expand Down
3 changes: 3 additions & 0 deletions corelink/runner_probe.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 9 additions & 4 deletions docs/CORE_LINK.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ states neither falls back to a fixed 60 Hz.
## Direct mode

`retro-hub --run-core <core> [--package <shim>] --rom <image> [--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). `<core>` is a per-title core
(`<title>_core.so`) or a generic one with the `game_package` capability (such as
Expand All @@ -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
Expand Down
10 changes: 7 additions & 3 deletions docs/CORE_RUNNER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

Expand Down
8 changes: 4 additions & 4 deletions docs/OVERLAY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions docs/RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 18 additions & 8 deletions runner/runner_link.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down
16 changes: 15 additions & 1 deletion runner/runner_link.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
};

Expand Down
53 changes: 31 additions & 22 deletions runner/runner_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
#include "state_keeper.hpp"
#include "transport.hpp"

#include <array>
#include <cinttypes>
#include <cstdio>
#include <cstdlib>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -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,
Expand Down
Loading