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
6 changes: 3 additions & 3 deletions docs/LAUNCHER.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ over the generated header). It finds the settings directory, edits `settings.jso
edit, unknown ones included), and starts the game. Screens: **Disc** (file dialog, drag-and-drop, or a typed path; only
the described discs' SHA-1s are accepted), **Play** (starts the game; on an error shows the exit status, the crash
report and the last lines of output, "Crash report"), **Settings** (scale, fullscreen, the rate when the game offers
two, renderer, mute, memory cards, the update check when the game has one: "Updates", Add to Steam: "Steam"), **Controls** (keyboard, gamepad,
hotkeys), **Mods** (an on/off switch per mod and a page generated from its manifest; the data mods in the settings
two, renderer, mute, memory cards, the update check when the game has one: "Updates", Add to Steam: "Steam"), **Controls** (keyboard, gamepad
with the second player's gamepad, hotkeys), **Mods** (an on/off switch per mod and a page generated from its manifest; the data mods in the settings
directory's `mods/` in priority order, "Data mods"). Build, run, details and the self-test: `launcher/README.md`.

## Updates
Expand Down Expand Up @@ -505,7 +505,7 @@ at a time), where leaving the game for the launcher's window is a chore.
polls, the hotkey again, the next vsync follows the paused one), and `<PREFIX>_PORT_MENU_SHOT=PATH` writes the
window's output with the menu up as a PNG (the software renderer; the hardware renderer's present is not read
back with the menu in it), on the page `<PREFIX>_PORT_MENU_SCREEN=game|settings|controls|mods` names (`mods:<id>`:
with that mod chosen), with the window first sized to `<PREFIX>_PORT_MENU_WINDOW=WxH` when it is set (SDL's
with that mod chosen; `controls:keyboard|gamepad|hotkeys`: on that tab), with the window first sized to `<PREFIX>_PORT_MENU_WINDOW=WxH` when it is set (SDL's
software renderer follows it: `SDL_RENDER_DRIVER=software`). The shot's log line names the windows whose content
reaches past their right edge ("nothing wider than its window" when none does).
The round trip also re-applies the settings live at its second poll, as the menu does after a save, and with
Expand Down
2 changes: 1 addition & 1 deletion docs/RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ cmake -S port -B build/port-sdl -G Ninja -DPSXSTACK_SDL=ON -DPSXSTACK_MENU=ON
links as C++ (`launcher/ui/`, Dear ImGui from `tools/imgui`); without the option nothing of it is in the binary. A
change made in the menu is written to the settings file and applied live where the runtime can (`runtime/live.c`).
`<PREFIX>_PORT_MENU_SHOT=PATH` writes the window with the menu up as a PNG (the software renderer), on the page
`<PREFIX>_PORT_MENU_SCREEN` names (`mods:<id>` with a mod chosen), at the size `<PREFIX>_PORT_MENU_WINDOW=WxH` gives
`<PREFIX>_PORT_MENU_SCREEN` names (`mods:<id>` with a mod chosen, `controls:<tab>` on a tab of Controls), at the size `<PREFIX>_PORT_MENU_WINDOW=WxH` gives
the window (with `SDL_RENDER_DRIVER=software`).
SDL's backends follow the `-dev` headers present when `setup.sh sdl3` runs: missing optional ones are turned off one by
one (logged), and with no X11/Wayland headers at all only the `offscreen` and `dummy` video drivers are built (enough
Expand Down
43 changes: 24 additions & 19 deletions launcher/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ half way (Gamepad), or, for a hotkey, any of them or a chord (everything held to
most 4 inputs, and 8 triggers a binding: the game's limits). Escape alone, or Cancel, closes it. The prompt takes its
events before ImGui, and gamepad navigation pauses until the gamepad is released, so the press that was bound does
not also click a button. An input used by two buttons (or a button and a single-input hotkey) is marked, with a
tooltip naming the other use. A mod's own bindings are its options (Mods, below).
tooltip naming the other use. A mod's own bindings are its options (Mods, below). The **Gamepad** tab starts with
**Player 2** (`input.player2`: Off, First gamepad, Second gamepad, by connection order; docs/LAUNCHER.md "Input
bindings"), a line on what the choice does, and the gamepads connected now with the player each is (in the in-game
menu the game's own count; in the launcher the same connection order the game will count). Both players' gamepads use
the tab's map. The tables wrap their chips, so every tab fits the in-game menu over a 640x480 window.

## Mods

Expand Down Expand Up @@ -193,32 +197,33 @@ drops `SDL_VIDEO_DRIVER` from the program's environment.)

No disc and no display needed (CI runs it). It replaces `DIR/launcher-self-test/` and checks: the path helpers (Windows
forms too), the JSON writer, the lookup order, the settings file's round trips (the documented example with its unknown
members kept, invalid values, a broken file, a newer schema), the binding grammar and the input prompt (keys, gamepad
buttons, stick directions, chords, Escape), the `.cue` reader and the SHA-1 check (against the described discs), the
launch path with **the launcher itself as the game's stand-in** (`<PREFIX>_LAUNCHER_FAKE_GAME=mode`: a game with and
without `--config`, one that rejects the file, one that crashes, one that fails after 250 lines), then opens the
window, walks every screen with injected key events and a virtual gamepad, plays with the stand-in (a file of the first
described disc's size stands for the verified disc), rebinds a key through the prompt and checks the file, drops a
wrong file on the window, renders the mods screen over the fixture manifests (`tests/fixtures/mods/`: the first game's
`fast_forward` as shipped, the documented `skip_dialogues` example, one with every option type, one with presets and a
slider whose toggle makes it a typed number; three unusable ones written at test time) and rebinds a mod's key through
the prompt, and saves a picture of each screen in `DIR/launcher-self-test/screens/`. The update check never touches the
network: `<PREFIX>_LAUNCHER_FAKE_CURL` points at **the launcher itself as curl's stand-in** (it answers with a file, in
pieces for a download, or with curl's errors; `<PREFIX>_SELFTEST_CURL_REPLY` says which), and every check of the
self-test goes to it. It checks the version rules (only an exact `X.Y.Z` is offered an update), GitHub's answer
members kept, invalid values, a broken file, a newer schema), the binding grammar, `input.player2`, and the input prompt
(keys, gamepad buttons, stick directions, chords, Escape), the `.cue` reader and the SHA-1 check (against the described
discs), the launch path with **the launcher itself as the game's stand-in** (`<PREFIX>_LAUNCHER_FAKE_GAME=mode`: a game
with and without `--config`, one that rejects the file, one that crashes, one that fails after 250 lines), then opens
the window, walks every screen with injected key events and a virtual gamepad, plays with the stand-in (a file of the
first described disc's size stands for the verified disc), rebinds a key through the prompt and checks the file, shows
the Gamepad tab with player 2 set and two virtual gamepads connected, drops a wrong file on the window, renders the mods
screen over the fixture manifests (`tests/fixtures/mods/`: the first game's `fast_forward` as shipped, the documented
`skip_dialogues` example, one with every option type, one with presets and a slider whose toggle makes it a typed
number; three unusable ones written at test time) and rebinds a mod's key through the prompt, and saves a picture of
each screen in `DIR/launcher-self-test/screens/`. The update check never touches the network:
`<PREFIX>_LAUNCHER_FAKE_CURL` points at **the launcher itself as curl's stand-in** (it answers with a file, in pieces
for a download, or with curl's errors; `<PREFIX>_SELFTEST_CURL_REPLY` says which), and every check of the self-test goes
to it. It checks the version rules (only an exact `X.Y.Z` is offered an update), GitHub's answer
(`tests/fixtures/updates/`: the tag, the page, this platform's package with its size and SHA-256; a tag that is not a
version, a cut answer), `launcher.updates` in the file, `src/http.cpp` (a GET, curl's own errors, curl missing, a
download with its progress, a cancel, a wrong size), then in the window: the prompt over Play (a picture), Escape as
Later, the Release page, Skip and a newer release after it, the setting off with Check now, `--no-update-check`, the
prompt waiting for a running game, curl missing, a cut answer, a development build and a description without a
repository (pictures of the Settings screen's part). Then **Update now** in the window, the stand-in "downloading"
fixture packages (`<PREFIX>_SELFTEST_CURL_DOWNLOAD` is its answer for a download, the release's JSON being the
check's): the line instead of the button when the plan is not possible (no `$APPIMAGE`), the progress and Cancel on a
slow download (no check started meanwhile, the file removed, the install unchanged), a wrong digest and a missing one
fixture packages (`<PREFIX>_SELFTEST_CURL_DOWNLOAD` is its answer for a download, the release's JSON being the check's):
the line instead of the button when the plan is not possible (no `$APPIMAGE`), the progress and Cancel on a slow
download (no check started meanwhile, the file removed, the install unchanged), a wrong digest and a missing one
(nothing applied, the message, Release page still works), the whole flow into a fake AppImage install (`$APPIMAGE` at a
copy of the launcher; Enter presses Update now) and into a fake Windows folder (a copy of the launcher, the release's
zip made with the self-update's tar), each ending with the new launcher (a marked copy) started with the arguments
given and this one quitting, and the relaunched stamp not prompted again (pictures of the prompt's steps).
zip made with the self-update's tar), each ending with the new launcher (a marked copy) started with the arguments given
and this one quitting, and the relaunched stamp not prompted again (pictures of the prompt's steps).

The Steam shortcut (`selftest_steam.cpp`, in `DIR/launcher-self-test/steam/`; never this system's Steam): the fixture
`shortcuts.vdf` read and written back byte for byte, the launcher's shortcut added (the bytes of
Expand Down
54 changes: 54 additions & 0 deletions launcher/src/selftest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,28 @@ static void test_input(const std::string &root) {
h.values.keyboard["circle"] = { "B" };
check(h.save(&err) && Json::parse(read(h.path()), nullptr).find("input")->find("future") != nullptr,
"unknown members of input are kept");
// input.player2 (the second pad): written once set, read back; kept once in the file, as video.menu_size; a bad
// value is warned about and reads as "off"; a file without it says "off" and gets none.
check(h.values.player2 == "off", "input.player2: off by default");
h.values.player2 = "second";
check(h.save(&err) &&
*Json::parse(read(h.path()), nullptr).find("input")->find("player2") == Json::string("second"),
"input.player2 is written");
SettingsFile p2;
p2.load(dir);
check(p2.values.player2 == "second" && p2.messages().empty(), "input.player2 reads back");
p2.values.player2 = "off";
check(p2.save(&err) && *Json::parse(read(p2.path()), nullptr).find("input")->find("player2") == Json::string("off"),
"input.player2 set back to off stays in the file");
write(p2.path(), R"({"schema": 1, "input": {"player2": "third"}})");
p2.load(dir);
check(p2.values.player2 == "off" && p2.messages().size() == 1 &&
p2.messages()[0].find("input.player2") != std::string::npos,
"a bad input.player2: a warning and off");
write(p2.path(), R"({"schema": 1})");
p2.load(dir);
check(p2.save(&err) && Json::parse(read(p2.path()), nullptr).find("input") == nullptr,
"no input.player2 written for a file without one");

// The capture, fed events.
InputCapture c;
Expand Down Expand Up @@ -1022,6 +1044,7 @@ static void test_game(const std::string &root) {
r.values.gamepad["select"] = {};
r.values.hotkeys["pause"] = { { "pad:guide", "pad:start" }, { "P" } };
r.values.hotkeys["fullscreen"] = {};
r.values.player2 = "second";
// Every mod beside the game turned on, every option off its default (the game's own checks of mods.<id>).
int mods_set = 0, mods_found = 0;
ModValues mv(&r.doc);
Expand Down Expand Up @@ -1254,6 +1277,37 @@ static void test_play(SDL_Window *window, const std::string &root) {
push_key(window, SDL_SCANCODE_ESCAPE, SDL_KMOD_NONE, false);
pump(app, 2);
check(!app.capture().active() && app.settings().values.hotkeys.count("pause") == 0, "Escape leaves the hotkey");
// The second pad (input.player2): the Gamepad tab with two virtual gamepads connected, the second one player 2's
// (a picture); then back to off and the Keyboard tab.
{
SDL_JoystickID pads[2] = { 0, 0 };
for (int i = 0; i < 2; i++) {
SDL_VirtualJoystickDesc desc;
SDL_INIT_INTERFACE(&desc);
desc.type = SDL_JOYSTICK_TYPE_GAMEPAD;
desc.nbuttons = SDL_GAMEPAD_BUTTON_DPAD_RIGHT + 1;
desc.naxes = SDL_GAMEPAD_AXIS_COUNT;
desc.name = i == 0 ? "psxstack self-test pad A" : "psxstack self-test pad B";
pads[i] = SDL_AttachVirtualJoystick(&desc);
check(pads[i] != 0, std::string("a virtual gamepad: ") + SDL_GetError());
}
app.settings().values.player2 = "second";
app.screens().show_controls_tab("gamepad");
pump(app, 4);
int listed = 0;
SDL_free(SDL_GetGamepads(&listed));
check(listed >= 2, "the two virtual gamepads are connected (" + std::to_string(listed) + ")");
png = path_join(shots, "8b-Controls-gamepad.png");
app.frame(png.c_str());
for (SDL_JoystickID id : pads) {
if (id != 0) {
SDL_DetachVirtualJoystick(id);
}
}
app.settings().values.player2 = "off";
app.screens().show_controls_tab("keyboard");
pump(app, 2);
}
app.set_screen(Screen::Settings);
pump(app, 2);
png = path_join(shots, "9-Settings.png");
Expand Down
10 changes: 9 additions & 1 deletion launcher/src/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ static void read_input(const Json &doc, Settings *s, std::vector<std::string> *w
(pad ? s->gamepad : s->keyboard)[b.id] = names;
}
}
read_string(input, "input", "player2", &s->player2, w);
if (std::find(std::begin(PLAYER2_NAMES), std::end(PLAYER2_NAMES), s->player2) == std::end(PLAYER2_NAMES)) {
warn(w, "input.player2: expected \"off\", \"first\" or \"second\"; using \"off\"");
s->player2 = "off";
}
if (const Json *hotkeys = input->find("hotkeys")) {
if (!hotkeys->is_object()) {
warn(w, "input.hotkeys: expected an object; using the defaults");
Expand All @@ -204,7 +209,7 @@ static void read_input(const Json &doc, Settings *s, std::vector<std::string> *w
// removed (a file never rebound has no input at all).
static void write_input(const Settings &s, Json *doc) {
Json *input = doc->find("input");
if (input == nullptr && s.keyboard.empty() && s.gamepad.empty() && s.hotkeys.empty()) {
if (input == nullptr && s.keyboard.empty() && s.gamepad.empty() && s.hotkeys.empty() && s.player2 == "off") {
return;
}
if (input == nullptr || !input->is_object()) {
Expand Down Expand Up @@ -252,6 +257,9 @@ static void write_input(const Settings &s, Json *doc) {
input->erase("hotkeys");
}
}
if (s.player2 != "off" || input->find("player2") != nullptr) { // as video.menu_size
input->set("player2", Json::string(s.player2));
}
if (input->members().empty()) {
doc->erase("input");
}
Expand Down
3 changes: 3 additions & 0 deletions launcher/src/settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ struct MemoryCard {
inline const char *const FILTER_NAMES[] = { "none", "sharp", "scanlines", "crt", "smooth" };
// video.menu_size's values (the in-game menu's text, launcher/ui/menu.cpp), in the Settings screen's order.
inline const char *const MENU_SIZE_NAMES[] = { "smaller", "auto", "larger" };
// input.player2's values (the game's runtime/input.c): none, or the gamepad by connection order that is player 2.
inline const char *const PLAYER2_NAMES[] = { "off", "first", "second" };

// launcher.steam (docs/LAUNCHER.md "Steam"): what Add to Steam did, so Remove and Update shortcut undo exactly that
// without looking for Steam again. appid 0: not in Steam.
Expand Down Expand Up @@ -102,6 +104,7 @@ struct Settings {
std::map<std::string, std::vector<std::string>> keyboard; // input.keyboard: button -> key names
std::map<std::string, std::vector<std::string>> gamepad; // input.gamepad: button -> gamepad input names
std::map<std::string, Binding> hotkeys; // input.hotkeys: action -> binding
std::string player2 = "off"; // input.player2: one of PLAYER2_NAMES

// The effective values (the file's, else the defaults).
std::vector<std::string> keys_for(const std::string &button) const;
Expand Down
12 changes: 8 additions & 4 deletions launcher/ui/menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
// <PREFIX>_PORT_MENU_SHOT=PATH writes the window's output with the menu up, once, as a PNG: the second present after
// it is shown (the software renderer: the input self-test's picture; the hardware renderer's present cannot be read
// back with the menu in it, logged). <PREFIX>_PORT_MENU_SCREEN=game|settings|controls|mods picks the page it opens on
// (the picture of a page; default game).
// (the picture of a page; default game), `mods:<id>` with that mod chosen, `controls:<tab>` on that tab (keyboard,
// gamepad, hotkeys).
#include "menu.h"

#include <algorithm>
Expand Down Expand Up @@ -83,11 +84,11 @@ bool menu_open_common(SDL_Window *window) {
mn.shot = shot;
}
mn.from_launcher = SDL_getenv(env_name("_LAUNCHER").c_str()) != nullptr;
std::string start_mod;
std::string start_mod, start_tab;
if (const char *page = SDL_getenv(env_name("_PORT_MENU_SCREEN").c_str())) {
std::string name = page;
if (size_t colon = name.find(':'); colon != std::string::npos) { // mods:<id>
start_mod = name.substr(colon + 1);
if (size_t colon = name.find(':'); colon != std::string::npos) { // mods:<id>, controls:<tab>
(SDL_strncasecmp(page, "controls:", 9) == 0 ? start_tab : start_mod) = name.substr(colon + 1);
name.resize(colon);
}
for (int i = 0; i < PAGE_COUNT; i++) {
Expand Down Expand Up @@ -123,6 +124,9 @@ bool menu_open_common(SDL_Window *window) {
if (!start_mod.empty()) {
mn.screens.select_mod(start_mod);
}
if (!start_tab.empty()) {
mn.screens.show_controls_tab(start_tab);
}
if (mn.has_settings) {
port_log("menu: settings %s, mods beside %s%s%s", port_settings.file, exe_dir.c_str(),
user_mods != nullptr ? ", data mods in " : "", user_mods != nullptr ? user_mods : "");
Expand Down
Loading
Loading