From b030e445d083ac00ba728ceeaff83a51741c3a1d Mon Sep 17 00:00:00 2001 From: Gabriel Scarcella Date: Sat, 24 Oct 2026 15:33:21 -0300 Subject: [PATCH] Input: a second pad on port 1 from the window (input.player2) A setting, input.player2 ("off" by default, "first" or "second": that gamepad by connection order), makes one gamepad player 2: it drives the PS1's port 1 with the gamepad map, port 1 is connected only while that gamepad is there, and its inputs never reach a hotkey. The keyboard and every other gamepad stay player 1's, so a one-pad setup, every log and every record are unchanged. Whoever owns the pad sets both ports every vsync (a script leaves port 1 empty), since the shim's "connected" is part of a save state. The frame log and the record trace port 1 when it is used ("I port 1 ..." lines, inputs entries with "port": 1); the crash report names pad 2 when one is connected. --input-test gains a players' part: a second virtual gamepad, inputs on each pad and on both, player 2's gamepad hotkey chords (reach pad 2, fire nothing), and the unplugs. hello tints its gradient from pad 2; tests/hello_test.py --menu runs the self-test with "second" and "first". The runtime's players are PORT_PADS arrays and the setting is player, so a multitap can add players later. Part of #101 (phase 1 of 3: the runtime; the launcher's Controls screen and the script/debug channel's port 1 follow). Co-Authored-By: Claude Opus 5.5 --- docs/DECISIONS.md | 11 + docs/LAUNCHER.md | 9 +- docs/PORT.md | 12 +- docs/RUNTIME.md | 16 +- examples/hello/hello.c | 6 +- include/psxstack/port_runtime.h | 12 +- psyq/libpad.c | 12 +- psyq/psyq.h | 1 + runtime/crash.c | 4 + runtime/debug.c | 8 +- runtime/framelog.c | 41 ++- runtime/input.c | 439 ++++++++++++++++++++++++++++---- runtime/script.c | 7 +- runtime/settings.c | 3 +- tests/hello_test.py | 29 ++- 15 files changed, 525 insertions(+), 85 deletions(-) diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 5df6521..8e7e503 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -281,3 +281,14 @@ every change waits for Steam to be closed; the launcher asks, it never stops Ste launcher (the AppImage path the self-update keeps), so an update needs no change in Steam. The artwork is the game's choice (game.json `launcher.steam_art`, optional): embedded in the launcher at build time, so a release stays the launcher and the game with nothing beside them, and written as the files Steam's own grid folder holds per appid. + +## Player 2: a gamepad by connection order, off by default, never a hotkey (2026-10-24) + +A second pad (issue #101) is one gamepad chosen by `input.player2` (`"first"` or `"second"`, by SDL's instance ids, +which grow with each connection), not by SDL's player index: Linux's evdev and uinput devices, the virtual pads a +streaming host (Sunshine) creates among them, usually have none. The default stays `"off"`, so a one-pad setup and +every existing record are unchanged and port 1 is connected only while player 2's gamepad is there. Player 2 shares +the gamepad map and never reaches a hotkey: the host's player owns pause, the menu and the save states. Whoever owns +the pad (the window, a script) sets both ports every vsync, since the shim's "connected" is part of a save state. The +runtime's players are arrays of `PORT_PADS` and the setting is `player`, so a multitap can add players without a +new format. diff --git a/docs/LAUNCHER.md b/docs/LAUNCHER.md index fe3dd2a..a5da06a 100644 --- a/docs/LAUNCHER.md +++ b/docs/LAUNCHER.md @@ -114,7 +114,7 @@ and absolute paths and exits 0, or exits 64 naming the bad key. ### Input bindings -`input` has three members; `--print-settings` prints it back as it was given. +`input` has four members; `--print-settings` prints it back as it was given. - **`keyboard`**: PS1 button -> one key name or a list of them. **`gamepad`**: PS1 button -> one gamepad input name or a list. A button named replaces all of that button's entries of that kind; a button that is absent keeps its @@ -126,7 +126,12 @@ and absolute paths and exits 0, or exits 64 naming the bad key. - **Gamepad input names are ours** (SDL's positional buttons): `south east west north back guide start leftstick rightstick leftshoulder rightshoulder dpup dpdown dpleft dpright misc1 paddle1 paddle2 paddle3 paddle4 touchpad`, and the axes past half way: `lefttrigger righttrigger leftx- leftx+ lefty- lefty+ rightx- rightx+ righty- righty+` - (`lefty-` is the left stick up). Every connected gamepad is ORed into pad 1. + (`lefty-` is the left stick up). Every connected gamepad is ORed into pad 1, unless it is player 2's. +- **`player2`**: `"off"` (the default), `"first"` or `"second"`: the gamepad, by connection order, that is player 2. + It drives pad 2 (the PS1's port 1) with the `gamepad` map; port 1 has a pad in it only while that gamepad is there + (a game that reads port 0 alone is unaffected); the keyboard and every other gamepad stay player 1's, and player 2's inputs never + fire a hotkey. `"first"` suits a player 1 on the keyboard. A gamepad unplugged and plugged in again counts as the + newest. Anything else ends the game with status 64. - **Default map** (the runtime's `input.c`): arrows the D-pad, `X` cross, `C` circle, `Z` square, `S` triangle, `Return` and `Keypad Enter` START, `Backspace` and `Right Shift` SELECT, `Q`/`E` L1/R1, `1`/`3` L2/R2; gamepads: south cross, east circle, west square, north triangle, back SELECT, start START, the shoulders L1/R1, the triggers diff --git a/docs/PORT.md b/docs/PORT.md index 3f7ac06..3ee7269 100644 --- a/docs/PORT.md +++ b/docs/PORT.md @@ -256,7 +256,7 @@ bare binary, the replays and the goldens are unchanged. The ops: boundary (the pump's pause, shared with the window's pause key). `step frames`: exactly N vsyncs, then pause. - `wait`: run until a host or PS1 read, the stage or the map equals a value, or a timeout in frames; leaves the game paused. `pad buttons frames release [sync]`: the channel owns pad 1 (`psyq_pad_set` each vsync) until `pad_free`; - a script keeps precedence. + a script keeps precedence; the window keeps pad 2. - `peek` / `poke`: raw host memory, any range `/proc/self/maps` says is mapped (a bad address never faults the game). `peek_ps1` / `poke_ps1`: a PS1 address, through the adapter's state map first (`game_state_host`, 1/2/4 bytes: the host objects behind PS1 addresses, an overlay's globals in the slots among them), else the arena (from the first @@ -343,7 +343,7 @@ and the shim lacks stops the run with `port_unimplemented(name)` (status 3) and | LIBPRESS | MDEC movie decoding (`libpress.c`, `mdec.c`) | | LIBSND | The sequencer and voices over the SPU core (`libsnd*.c`; `docs/SOUND.md` in the first game): SEPs and SEQs, ticked by the game (`SS_NOTICK`) or by `SsStart` once per vsync | | LIBSPU | The voice and common attributes and the reverb work area a game sets itself (`libspu.c`) | -| LIBPAD | A digital pad on port 0; actuator calls accepted and ignored | +| LIBPAD | A digital pad on port 0, and on port 1 while the runtime connects one (`input.player2`); actuator calls accepted and ignored | | LIBMCRD | Real, over `.mcd` images (`libmcrd.c`), whose card store LIBCARD shares | | LIBAPI | Events, root counter 3 (the vblank: its event's handler runs inside the vsync tick and may preempt a fiber), critical sections (`libapi.c`); the BIOS's file calls on the memory cards (`bu00:`, `bu10:`) with LIBCARD (`libcard.c`). `open`, `read`, `write`, `lseek`, `close` and `EnterCriticalSection`/`ExitCriticalSection` are also the host's names: the game's units get `include/psxstack/psyq_names.h` forced in, which renames them to the shim's `psyq_api_*` (and LIBC2's `rand`/`srand` to `psyq_c2_*`), so the runtime and libc keep the host's | | LIBCARD | `InitCARD`, `StartCARD`, `_bu_init`, `_card_info`/`_load`/`_clear`/`_format`: real over the `.mcd` images, completing on the vsync tick with SwCARD/HwCARD events (`libcard.c`) | @@ -498,7 +498,9 @@ same with it on or off. ## Input - **Window input** (`runtime/input.c`): the keyboard and every gamepad SDL sees are ORed into pad 1, a digital pad, - once per vsync. Bindings are by key position and SDL's positional (PlayStation-layout) gamepad buttons, rebindable + once per vsync; with `input.player2` one gamepad (by connection order) drives pad 2 instead, port 1 connected while + it is there, and never a hotkey. Whoever owns the pad sets both ports every vsync (a script leaves port 1 empty), so + a save state never carries a second pad into a run that has none. Bindings are by key position and SDL's positional (PlayStation-layout) gamepad buttons, rebindable in the settings file; hotkeys (fullscreen, pause, the quick state's save and load, the in-game menu, the mods') never reach the pad. The mapping and defaults are in `docs/RUNTIME.md` "The window". - **The in-game menu** (`include/psxstack/menu.h`, `launcher/ui/menu.cpp`; `docs/LAUNCHER.md` "In-game menu"): the @@ -581,8 +583,8 @@ main thread and reports its registers and stack. The `.dmp` opens in WinDbg or V stack overflow gets the text (the filter runs on what the guard page leaves) and perhaps no dump. ## Known limitations -- **Pads:** one digital pad on port 0; no analog mode, no rumble (`PadSetAct` is accepted and ignored), no second - port or multitap. +- **Pads:** digital pads on port 0 and, from the window's input only, port 1; no analog mode, no rumble (`PadSetAct` is + accepted and ignored), no multitap. The script and the debug channel drive port 0 only. - **No reset key** in the window (the console reset exists only as a script step and a debug-channel op). - **Timing stand-ins:** the CD seek times (3 ticks + 1 per 8192 sectors, at most 40) and `StGetNext`'s 5000 polls per vsync are estimates, not measurements. diff --git a/docs/RUNTIME.md b/docs/RUNTIME.md index 222f6fd..56086d4 100644 --- a/docs/RUNTIME.md +++ b/docs/RUNTIME.md @@ -133,21 +133,25 @@ time between vsyncs changes: the log and the record of a window run are the head window nothing is paced. **Input** (`runtime/input.c`): the keyboard and every gamepad SDL sees are ORed into pad 1, a digital pad (`psyq_pad_set`), -once per vsync. Keys (by position): arrows the D-pad; X cross, C circle, Z square, S triangle; Enter (or keypad Enter) +once per vsync. With `input.player2` (`"first"` or `"second"`: that gamepad by connection order; docs/LAUNCHER.md +"Input bindings") one gamepad is player 2 instead: it drives pad 2 (port 1) with the same gamepad map, port 1 is +connected while it is there, and it never fires a hotkey (player 2's Back+Start reaches the game). The default, `"off"`, +leaves port 1 empty, as a one-pad console. Keys (by position): arrows the D-pad; X cross, C circle, Z square, S triangle; Enter (or keypad Enter) START; Backspace (or right Shift) SELECT; Q L1, E R1, 1 L2, 3 R2; F11 fullscreen, P pause. Gamepads (SDL's positional buttons, the PlayStation layout): south cross, east circle, west square, north triangle, back SELECT, start START, shoulders L1/R1, triggers L2/R2, the D-pad and the left stick the D-pad. All of it is rebindable in the settings (`input.keyboard`, `input.gamepad`, `input.hotkeys`; docs/LAUNCHER.md "Input bindings"), and the mods add hotkeys. A hotkey never reaches the pad: a completed trigger masks its inputs until they are all released. **The pause** stops the game between two vsyncs (the window keeps presenting the last image, the audio device pauses) until the key again. -**The quick state**: F5 saves the whole game to the quick slot, F9 goes on from it ("Save states" below). Every change of the pad goes to the log's `I` lines and the +**The quick state**: F5 saves the whole game to the quick slot, F9 goes on from it ("Save states" below). Every change of a pad goes to the log's `I` lines and the record's `inputs`, as a script's do. With `--script` the script owns the pad: the window's input never reaches it. Closing the window (or SIGINT/SIGTERM, which SDL turns into a quit) ends the run: `port_exit(0, "window closed")`, the log and the record written. `--input-test` checks the path: it injects every key of the map (`SDL_PushEvent`), then attaches a virtual SDL gamepad and presses each of its buttons, triggers and stick directions, and a chord, one per frame with its release in the next, then every hotkey trigger, and requires the buttons sent to `psyq_pad_set` to be the map's (none for a hotkey's, which must fire; with `--script`: none sent, and the script's log stays byte-identical to a -run without the test), then (without a script) the pause's round trip, the quick slot's, and the in-game menu's +run without the test), then the players: a second virtual gamepad, a gamepad input on each and on both, the gamepad-only +hotkey triggers on the second, then each unplugged, with both ports as `input.player2` says after every poll, then (without a script) the pause's round trip, the quick slot's, and the in-game menu's (a build with `PSXSTACK_MENU`; docs/LAUNCHER.md "In-game menu"); exit 0 when all the checks pass, 6 otherwise. With `--config` it tests the settings' map. The first game's CI runs it on SDL's offscreen driver, with the defaults and with a rebound settings file. **The mods** (`runtime/mods.c`; docs/LAUNCHER.md "Mod manifest", "Mod runtime"): the registry (the stack's @@ -394,7 +398,8 @@ far (an event between two ticks carries the last tick's number, as an emulator's | `M map 0x` | A map-sequence entry: the map changed (frame 1 always) | | `L tier file 0x word0 0x<8 hex> size 0x` | A file copied into a slot (`port_overlay_load`): the overlay's name, or `(data)`; its first word; its size | | `C stage map 0x rnd sha1 <40 hex> stable <40 hex>` | A checkpoint; `... rnd image false` for a checkpoint step with `"image": false` (no hashes) | -| `I buttons 0x<4 hex>` | The script's pad changed (`port_framelog_input`; PS1 bit order, active high) | +| `I buttons 0x<4 hex>` | Pad 1 changed (the script's, the window's or the debug channel's: `port_framelog_input`; PS1 bit order, active high) | +| `I port 1 buttons 0x<4 hex>`, `I port 1 none` | Pad 2 changed, or was unplugged (`none`); a run that never connects one has no such line | | `R reset` | The console's reset (the script's `reset` step): the next frame runs the game from `main()` again | | `X status ` | The exit (`port_exit`) | @@ -402,7 +407,8 @@ far (an event between two ticks carries the last tick's number, as an emulator's JSON written at exit, with the keys of the first game's emulator records (its `replay.py` reads it as it is): `runner` (`"port"`), `status` (the exit status), `reason`, `frames`, `checkpoints` (`name`, `frame`, `stage`, `map`, `random_index`, `gamestate_sha1`, `gamestate_sha1_stable`, as the emulator runner records them; a checkpoint whose script step says `"image": false` has `"image": false` instead of the two hashes, and a record without `image` means true), `overlay_sequence` -(`{frame, stage, file}`), `map_sequence` (`{frame, map}`), and `inputs` (`{frame, buttons: [names]}`, the names sorted) once the script has called `port_framelog_input`. The sequences follow the emulator runner's vsync +(`{frame, stage, file}`), `map_sequence` (`{frame, map}`), and `inputs` (`{frame, buttons: [names]}`, the names sorted) once the script has called `port_framelog_input`; pad 2's +entries add `"port": 1`, and `"connected": false` (with no buttons) when it was unplugged. The sequences follow the emulator runner's vsync listener: an entry at every change, the first frame always (`{1, 0, 0}`, map 0). ## The replay runners (`tools/replay/`) diff --git a/examples/hello/hello.c b/examples/hello/hello.c index 2d91798..d5a39bf 100644 --- a/examples/hello/hello.c +++ b/examples/hello/hello.c @@ -1,7 +1,8 @@ /* examples/hello: a disc-free Psy-Q program on psxstack, the stack's own smoke test (tests/hello_test.py). It needs * no game and no disc: the stack's own Psy-Q declarations (include/psxstack/psyq/), a common.h that provides the * type names and the host hooks, and this file. The build renames main() to game_main (the runtime - * owns main). It draws two polygons that change with the frame and the pad, double-buffered as a PS1 program would, + * owns main). It draws two polygons that change with the frame and the pads (the square with pad 1, the gradient with + * pad 2 while one is connected: byte 0 of its buffer 0), double-buffered as a PS1 program would, * initialises the sound library and sets the master volume (a key-on needs a VAB, which the example has none of). */ #include "common.h" #include "psxstack/psyq/libetc.h" @@ -37,6 +38,7 @@ int main(void) { for (;;) { u32 *ot = hello_ot[db]; u16 buttons = (u16)~(((u16)hello_pad0[2] << 8) | hello_pad0[3]); /* active high: a pressed button tints */ + u16 buttons2 = hello_pad1[0] == 0 ? (u16)~(((u16)hello_pad1[2] << 8) | hello_pad1[3]) : 0; POLY_F4 *q = &hello_quad[db]; POLY_G4 *g = &hello_grad[db]; @@ -48,7 +50,7 @@ int main(void) { setPolyG4(g); setRGB0(g, 255, 0, 0); setRGB1(g, 0, 255, 0); - setRGB2(g, 0, 0, 255); + setRGB2(g, (u8)(buttons2 & 0xFF), (u8)(buttons2 >> 8), 255); setRGB3(g, (u8)(frame * 5), 255, 255); setXY4(g, 190, 50, 300, 70, 180, 180, 290, 200); addPrim(ot + 2, g); diff --git a/include/psxstack/port_runtime.h b/include/psxstack/port_runtime.h index 1e196a8..789641d 100644 --- a/include/psxstack/port_runtime.h +++ b/include/psxstack/port_runtime.h @@ -66,10 +66,14 @@ void port_state_sha1(char full[41], char stable[41]); /* ---- The per-frame log's events (framelog.c), from the runtime and the script */ /* A file copied into a slot (port_overlay_load): `name` is the overlay's, or NULL for a data file. */ void port_framelog_overlay_load(int tier, s32 file, const char *name, u32 word0, u32 size); -/* The pad the script holds this frame (psyq_pad_set's bits, active high): a change goes to the log and to the record's - * `inputs` ({frame, buttons: [names]}, as run.lua's apply_pad); the record has `inputs` once this has been called. */ -void port_framelog_input(u16 buttons); -u16 port_framelog_last_input(void); /* the buttons last given (0 before any): the debug channel's status */ +/* The pads the runtime drives: port 0 and port 1 (psyq_pad_set's ports; a multitap would add more). */ +#define PORT_PADS 2 +/* A port's pad this frame, as given to psyq_pad_set (`connected`, the buttons: active high; port 0 is always + * connected): a change goes to the log and to the record's `inputs` ({frame, buttons: [names]}, as run.lua's + * apply_pad; port 1's entries say "port": 1, and "connected": false while nothing is there); the record has `inputs` + * once this has been called. Port 1 starts unconnected, so a run that never connects it logs nothing for it. */ +void port_framelog_input(int port, int connected, u16 buttons); +u16 port_framelog_last_input(int port); /* the buttons last given (0 before any): the debug channel's status */ /* ---- The interrupt pump (pump.c) */ extern long port_max_frames; /* --max-frames: port_wait() exits 0 after this many vsync ticks (0: no cap) */ diff --git a/psyq/libpad.c b/psyq/libpad.c index aca90dc..92f5d24 100644 --- a/psyq/libpad.c +++ b/psyq/libpad.c @@ -1,7 +1,7 @@ -/* psyq/libpad.c: LIBPAD. One digital pad on port 0 (pad.c reads `buffers[port]`: byte 0 the status, 0 = ok, - * 0xFF = nothing connected; byte 1 the id, 0x41 = digital pad, 0x80 = multitap; bytes 2-3 the buttons, active - * low). The port's input script sets the buttons with psyq_pad_set; every vsync tick makes them "new data" for - * PadChkVsync. +/* psyq/libpad.c: LIBPAD. A digital pad on port 0, and one on port 1 while the runtime connects one there (pad.c reads + * `buffers[port]`: byte 0 the status, 0 = ok, 0xFF = nothing connected; byte 1 the id, 0x41 = digital pad, 0x80 = + * multitap; bytes 2-3 the buttons, active low). The runtime (the window's input, the script, the debug channel) sets + * the buttons with psyq_pad_set; every vsync tick makes them "new data" for PadChkVsync. * * Assumption to verify (M3, against the emulator): the id/mode numbers (PadInfoMode term 2 = the current mode id, * 4 for a digital pad, 7 for a DualShock in analog mode; term 3 = the id table, 0 here), PadGetState 6 = stable, @@ -58,6 +58,10 @@ u16 psyq_pad_get(int port) { return (port == 0 || port == 1) ? psyq_pad_buttons[port] : 0; } +int psyq_pad_present(int port) { + return (port == 0 || port == 1) && psyq_pad_connected[port]; +} + /* The console's reset (psyq.c psyq_reset): no buffers, not started, the default pads (port 0 connected with nothing * pressed, port 1 empty). */ void psyq_pad_reset(void) { diff --git a/psyq/psyq.h b/psyq/psyq.h index 113f138..56edac6 100644 --- a/psyq/psyq.h +++ b/psyq/psyq.h @@ -70,6 +70,7 @@ void psyq_cd_set_reader(int (*read)(unsigned lba, u8 *sector)); * `connected` 0 reports no controller. Default: port 0 connected with nothing pressed, port 1 empty. */ void psyq_pad_set(int port, int connected, u16 buttons); u16 psyq_pad_get(int port); /* the buttons last set (0 when none, or the port is not 0 or 1): the crash report */ +int psyq_pad_present(int port); /* 1 when a pad is connected there (port 0 or 1), else 0 */ /* The primitive stream recorder (LIBGPU): the FNV-1a hash of every primitive DrawOTag/ContinueDraw walked since * the previous call (and the number of them in *count, if not NULL), then resets both. */ diff --git a/runtime/crash.c b/runtime/crash.c index 92d75ad..0eb8150 100644 --- a/runtime/crash.c +++ b/runtime/crash.c @@ -210,6 +210,10 @@ static void crash_write_context(const char *kind, int status) { cw_dec(game_state_map()); cw_str("\npad: "); cw_hex(psyq_pad_get(0), 4); + if (psyq_pad_present(1)) { + cw_str("\npad 2: "); + cw_hex(psyq_pad_get(1), 4); + } cw_str("\nscript: "); cw_str(port_script_active ? "yes" : "no"); cw_str("\n\nlog (last "); diff --git a/runtime/debug.c b/runtime/debug.c index 61a8913..7e12d12 100644 --- a/runtime/debug.c +++ b/runtime/debug.c @@ -179,7 +179,7 @@ static void debug_disconnect(const char *why) { if (port_debug_pad_owned) { port_debug_pad_owned = 0; psyq_pad_set(0, 1, 0); - port_framelog_input(0); + port_framelog_input(0, 1, 0); } port_pump_resume_request(); /* a dead tool never leaves the game held */ } @@ -474,7 +474,7 @@ static void debug_pad_apply(void) { debug_pad_release--; } psyq_pad_set(0, 1, v); - port_framelog_input(v); + port_framelog_input(0, 1, v); debug_pad_applied_frame = port_frames; } @@ -516,7 +516,7 @@ static void debug_op_status(const DebugReq *req) { "\"pace\": %ld, \"rate\": %ld, \"window\": %d, \"script\": %d, \"pad_owner\": \"%s\", \"pad\": %u", port_frames, game_state_stage(), game_state_file(), (u32)game_state_map(), port_state_slot1_word0(), port_pump_paused(), port_pace_get(), port_rate, port_window != 0, port_script_active != 0, - debug_pad_owner(), port_framelog_last_input()); + debug_pad_owner(), port_framelog_last_input(0)); } static void debug_op_step(const DebugReq *req, const PortJson *obj) { @@ -603,7 +603,7 @@ static void debug_op_pad_free(const DebugReq *req) { if (port_debug_pad_owned) { port_debug_pad_owned = 0; psyq_pad_set(0, 1, 0); - port_framelog_input(0); + port_framelog_input(0, 1, 0); } debug_ok(req, NULL); } diff --git a/runtime/framelog.c b/runtime/framelog.c index 48c8794..0ca2e29 100644 --- a/runtime/framelog.c +++ b/runtime/framelog.c @@ -38,6 +38,8 @@ typedef struct PortCheckpoint { typedef struct PortInput { long frame; u16 buttons; + u8 port; /* 0, or 1: the second pad */ + u8 connected; /* port 1: 0 while nothing is there (no buttons) */ } PortInput; static FILE *port_log_file; @@ -51,7 +53,8 @@ static size_t port_overlay_cap, port_map_cap, port_checkpoint_cap, port_input_ca static int port_sampled, port_inputs_used; static s32 port_last_stage, port_last_file; static u32 port_last_map; -static u16 port_last_buttons; +static u16 port_last_buttons[PORT_PADS]; +static u8 port_last_connected[PORT_PADS] = { 1, 0 }; /* The PS1 pad's button names by bit (PCSX.CONSTS.PAD.BUTTON, as run.lua records them). */ static const char *const port_button_names[16] = { @@ -217,19 +220,33 @@ void port_framelog_reset(void) { port_logf("R %ld reset", port_frames); } -void port_framelog_input(u16 buttons) { +void port_framelog_input(int port, int connected, u16 buttons) { + if (port < 0 || port >= PORT_PADS) { + return; + } port_inputs_used = 1; - if (buttons == port_last_buttons) { + connected = port == 0 || connected != 0; + if (!connected) { + buttons = 0; + } + if (buttons == port_last_buttons[port] && connected == port_last_connected[port]) { return; } *(PortInput *)port_append(&port_inputs, &port_input_n, &port_input_cap, sizeof(*port_inputs)) = - (PortInput){ port_frames, buttons }; - port_last_buttons = buttons; - port_logf("I %ld buttons 0x%04X", port_frames, buttons); + (PortInput){ port_frames, buttons, (u8)port, (u8)connected }; + port_last_buttons[port] = buttons; + port_last_connected[port] = (u8)connected; + if (port == 0) { + port_logf("I %ld buttons 0x%04X", port_frames, buttons); + } else if (connected) { + port_logf("I %ld port %d buttons 0x%04X", port_frames, port, buttons); + } else { + port_logf("I %ld port %d none", port_frames, port); + } } -u16 port_framelog_last_input(void) { - return port_last_buttons; +u16 port_framelog_last_input(int port) { + return port >= 0 && port < PORT_PADS ? port_last_buttons[port] : 0; } /* ---- The record (JSON) */ @@ -307,7 +324,12 @@ static void port_write_record(int status, const char *reason) { if (port_inputs_used) { fputs(",\n \"inputs\": [", f); for (i = 0; i < port_input_n; i++) { - fprintf(f, "%s\n {\"frame\": %ld, \"buttons\": ", i ? "," : "", port_inputs[i].frame); + fprintf(f, "%s\n {\"frame\": %ld, ", i ? "," : "", port_inputs[i].frame); + if (port_inputs[i].port != 0) { + fprintf(f, "\"port\": %d, %s", port_inputs[i].port, + port_inputs[i].connected ? "" : "\"connected\": false, "); + } + fputs("\"buttons\": ", f); port_json_buttons(f, port_inputs[i].buttons); fputc('}', f); } @@ -379,4 +401,5 @@ void port_framelog_state(PortState *s) { PORT_STATE_VAR(s, port_last_file); PORT_STATE_VAR(s, port_last_map); PORT_STATE_VAR(s, port_last_buttons); + PORT_STATE_VAR(s, port_last_connected); } diff --git a/runtime/input.c b/runtime/input.c index b037686..dfbad38 100644 --- a/runtime/input.c +++ b/runtime/input.c @@ -1,8 +1,12 @@ /* The window's input (M2; docs/PORT.md "Input"; rebindable: docs/LAUNCHER.md "Input bindings"): the * keyboard and every SDL gamepad, ORed into pad 1 (a digital pad: SELECT, START, the D-pad, L1/L2/R1/R2 and the four * face buttons; psyq_pad_set, psyq.h's bit order), once per vsync from pump.c; and the hotkey actions (pause, - * fullscreen, the mods' bindings). Only in a build configured with -DPSXSTACK_SDL=ON (PSXSTACK_SDL); the headless build - * has no input but the script's (its actions never fire; the settings' names are checked for their shape only). + * fullscreen, the mods' bindings). With `input.player2` ("first" or "second": that gamepad by connection order, SDL's + * instance ids growing with each connection) one gamepad is player 2 instead: it drives pad 2 (port 1, connected + * while that gamepad is there, with the same gamepad map) and never a hotkey; player 1 keeps the keyboard and every + * other gamepad. "off" (the default): port 1 stays empty, as on a console with one pad. Only in a build configured + * with -DPSXSTACK_SDL=ON (PSXSTACK_SDL); the headless build has no input but the script's (its actions never fire; the + * settings' names are checked for their shape only). * * Inputs: every keyboard key (an SDL scancode: the key's place, whatever the layout) and the gamepad inputs of * input_pad_names (SDL's positional buttons, then the triggers and the stick directions past half way, all gamepads @@ -29,11 +33,15 @@ * * `--input-test` (a self-test of the mapping, for CI and after a change here): from frame 2 on, it injects the key * events of every keyboard input of the pad map with SDL_PushEvent (a press in one frame, the release in the next), - * then attaches a virtual SDL gamepad and does the same with each gamepad input of the map, then one chord of two keys - * and a gamepad input, then every hotkey trigger; after each step's poll the buttons sent to psyq_pad_set must be the - * map's bits for the inputs held (none for a hotkey's: masked; with a script: nothing sent at all) and a hotkey step - * must have fired its action (the actions do nothing during the test). Without --config it tests the defaults; with - * one, the settings' map and hotkeys. Then (without a script) the pause: its key pressed, released and pressed again + * then attaches a virtual SDL gamepad (player 1's: with `input.player2` "first", a second one is attached before it) + * and does the same with each gamepad input of the map, then one chord of two keys and a gamepad input, then every + * hotkey trigger; after each step's poll the buttons sent to psyq_pad_set must be the map's bits for the inputs held + * (none for a hotkey's: masked; with a script: nothing sent at all) and a hotkey step must have fired its action (the + * actions do nothing during the test). Without --config it tests the defaults; with one, the settings' map and + * hotkeys. Then the players: a second virtual gamepad, a gamepad input pressed on each pad and on both, every hotkey + * trigger made of gamepad inputs on the second (player 2: it reaches pad 2 and fires nothing), then the second + * detached; after each poll both ports must be what `input.player2` says (with "off" everything is pad 1's and port 1 + * stays empty). Then (without a script) the pause: its key pressed, released and pressed again * while paused; the pause must poll without a vsync and give back the game; the quick slot's round trip; then the * menu's (a build with PSXSTACK_MENU, menu.h): its hotkey pressed at a vsync, the menu must be shown in the pause's * polls (the second present with it up is the _PORT_MENU_SHOT picture), the hotkey again gives back the game. @@ -124,6 +132,11 @@ int port_action_pause = -1, port_action_fullscreen = -1, port_action_save_state int port_action_menu = -1; static int input_test; /* --input-test running */ static int input_test_mute; /* the actions do nothing for their consumers (the test's steps) */ +/* input.player (n >= 2; a multitap would add players): the gamepad that player has, by connection order (1 the + * first, 2 the second; 0 none: that player's port stays empty). Player 1 has the keyboard and every other gamepad. */ +static int input_player_gamepad[PORT_PADS]; +static const char *const input_player_names[] = { "off", "first", "second" }; +#define INPUT_PLAYER_NAMES ((int)(sizeof(input_player_names) / sizeof(input_player_names[0]))) /* ---- The names (the settings' grammar; docs/LAUNCHER.md "Input bindings") */ @@ -345,12 +358,37 @@ static void input_map_section(const PortJson *obj, int pad) { } } +/* input.player2: "off", "first" or "second". */ +static void input_players_section(const PortJson *input) { + int p, k; + for (p = 1; p < PORT_PADS; p++) { + char key[16], where[32]; + const PortJson *v; + snprintf(key, sizeof(key), "player%d", p + 1); + snprintf(where, sizeof(where), "input.%s", key); + v = port_json_get(input, key); + if (v == NULL) { + continue; + } + for (k = 0; k < INPUT_PLAYER_NAMES; k++) { + if (v->type == PORT_JSON_STRING && strcmp(v->string, input_player_names[k]) == 0) { + break; + } + } + if (k == INPUT_PLAYER_NAMES) { + port_settings_fail(where, "\"off\", \"first\" or \"second\" (the gamepad by connection order)"); + } + input_player_gamepad[p] = k; + } +} + void port_input_settings(const PortJson *input) { - static const char *const known[] = { "keyboard", "gamepad", "hotkeys", NULL }; + static const char *const known[] = { "keyboard", "gamepad", "hotkeys", "player2", NULL }; const PortJson *hotkeys = NULL; size_t i; input_map_defaults(); input_action_count = 0; + memset(input_player_gamepad, 0, sizeof(input_player_gamepad)); if (input != NULL) { for (i = 0; i < input->count; i++) { const char *const *k; @@ -362,6 +400,7 @@ void port_input_settings(const PortJson *input) { } input_map_section(port_json_get(input, "keyboard"), 0); input_map_section(port_json_get(input, "gamepad"), 1); + input_players_section(input); hotkeys = port_json_get(input, "hotkeys"); if (hotkeys != NULL && hotkeys->type != PORT_JSON_OBJECT) { port_settings_fail("input.hotkeys", "an object (an action -> a binding)"); @@ -394,10 +433,12 @@ void port_input_settings(const PortJson *input) { #define INPUT_MAX_GAMEPADS 8 static u8 input_key_down[INPUT_KEYS]; /* from the key events */ -static u8 input_down[INPUT_IDS]; /* every input, at the last poll */ -static u8 input_masked[INPUT_IDS]; /* inputs of a latched trigger */ +static u8 input_down[PORT_PADS][INPUT_IDS]; /* every input of each player, at the last poll (the keys: player 1's) */ +static u8 input_masked[INPUT_IDS]; /* player 1's inputs of a latched trigger */ static SDL_Gamepad *input_gamepads[INPUT_MAX_GAMEPADS]; -static u16 input_applied; /* the buttons last sent to psyq_pad_set */ +static SDL_JoystickID input_player_id[PORT_PADS]; /* the gamepad of player 2.. (0: none); player 1 has the others */ +static u16 input_applied[PORT_PADS]; /* the buttons last sent to psyq_pad_set, per port */ +static int input_applied_connected[PORT_PADS]; /* and whether it said connected */ static int input_applied_any; /* psyq_pad_set was called by this file */ /* ---- --input-test */ @@ -405,8 +446,10 @@ static int input_test_step; /* the current step (input_test_steps: done) */ static int input_test_phase; /* 0: press now, 1: release now */ static int input_test_checks, input_test_failures; static long input_test_wait; /* frames spent waiting for the virtual gamepad */ -static SDL_JoystickID input_test_pad; /* the virtual gamepad (0: not attached) */ +static SDL_JoystickID input_test_pad; /* the virtual gamepad, player 1's (0: not attached) */ static SDL_Joystick *input_test_joy; +static SDL_JoystickID input_test_pad2; /* the second (the players' part; with player2 "first" attached before it) */ +static SDL_Joystick *input_test_joy2; /* The steps: each a set of inputs pressed together, and the action it must fire (-1: none). */ #define TEST_MAX_STEPS 192 static struct { @@ -449,7 +492,42 @@ static int input_gamepad_open(SDL_JoystickID id) { return 0; } -/* Every input's state: the keys from their events, the gamepads read now (all of them ORed). */ +/* Who has which gamepad (input.player): player n the n-th setting's gamepad by connection order (SDL's instance ids + * grow with each connection, so sorting the open ones gives it), player 1 every other gamepad. */ +static void input_assign(void) { + SDL_JoystickID ids[INPUT_MAX_GAMEPADS]; + int n = 0, i, j, p; + for (i = 0; i < INPUT_MAX_GAMEPADS; i++) { + if (input_gamepads[i] != NULL) { + SDL_JoystickID id = SDL_GetGamepadID(input_gamepads[i]); + for (j = n++; j > 0 && ids[j - 1] > id; j--) { + ids[j] = ids[j - 1]; + } + ids[j] = id; + } + } + for (p = 1; p < PORT_PADS; p++) { + int k = input_player_gamepad[p]; + input_player_id[p] = k > 0 && k <= n ? ids[k - 1] : 0; + } +} + +static int input_gamepad_player(SDL_JoystickID id) { + int p; + for (p = 1; p < PORT_PADS; p++) { + if (input_player_id[p] == id) { + return p; + } + } + return 0; +} + +/* A player's port has a pad in it: player 1's always, another's while its gamepad is there. */ +static int input_connected(int player) { + return player == 0 || input_player_id[player] != 0; +} + +/* Every input's state: the keys from their events (player 1's), the gamepads read now (each ORed into its player's). */ static void input_read(void) { static const SDL_GamepadAxis axes[] = { SDL_GAMEPAD_AXIS_LEFT_TRIGGER, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTX, @@ -457,26 +535,30 @@ static void input_read(void) { SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, SDL_GAMEPAD_AXIS_RIGHTY }; int i, j; - memcpy(input_down, input_key_down, INPUT_KEYS); - memset(input_down + INPUT_KEYS, 0, INPUT_PAD_INPUTS); + input_assign(); + memset(input_down, 0, sizeof(input_down)); + memcpy(input_down[0], input_key_down, INPUT_KEYS); for (i = 0; i < INPUT_MAX_GAMEPADS; i++) { SDL_Gamepad *g = input_gamepads[i]; + u8 *down; if (g == NULL) { continue; } + down = input_down[input_gamepad_player(SDL_GetGamepadID(g))]; for (j = 0; j < INPUT_PAD_BUTTONS; j++) { - input_down[INPUT_PAD(j)] |= SDL_GetGamepadButton(g, (SDL_GamepadButton)j) ? 1 : 0; + down[INPUT_PAD(j)] |= SDL_GetGamepadButton(g, (SDL_GamepadButton)j) ? 1 : 0; } for (j = 0; j < INPUT_PAD_INPUTS - INPUT_PAD_BUTTONS; j++) { Sint16 v = SDL_GetGamepadAxis(g, axes[j]); /* the triggers and the + directions: above half way; the - directions: below minus half way */ int on = (j < 2 || (j & 1)) ? v > INPUT_AXIS_PRESSED : v < -INPUT_AXIS_PRESSED; - input_down[INPUT_PAD(INPUT_PAD_BUTTONS + j)] |= on ? 1 : 0; + down[INPUT_PAD(INPUT_PAD_BUTTONS + j)] |= on ? 1 : 0; } } } -/* The actions from the inputs: triggers completed (an action pressed), held, latched; the masked inputs. */ +/* The actions from player 1's inputs (no other player's reach a hotkey): triggers completed (an action pressed), + * held, latched; the masked inputs. */ static void input_actions_update(void) { int a, t, k; memset(input_masked, 0, sizeof(input_masked)); @@ -486,8 +568,8 @@ static void input_actions_update(void) { InputTrigger *tr = &input_actions[a].triggers[t]; int all = 1, any = 0; for (k = 0; k < tr->n; k++) { - all &= input_down[tr->ids[k]]; - any |= input_down[tr->ids[k]]; + all &= input_down[0][tr->ids[k]]; + any |= input_down[0][tr->ids[k]]; } if (all && !tr->complete) { input_actions[a].pressed = 1; @@ -507,11 +589,12 @@ static void input_actions_update(void) { } } -static u16 input_buttons(void) { +/* A player's pad from the map (player 1's without the inputs a hotkey masks). */ +static u16 input_buttons(int player) { u16 bits = 0; int i; for (i = 0; i < input_map_count; i++) { - if (input_down[input_map[i].id] && !input_masked[input_map[i].id]) { + if (input_down[player][input_map[i].id] && !(player == 0 && input_masked[input_map[i].id])) { bits |= input_map[i].bits; } } @@ -530,26 +613,43 @@ static void input_push_key(SDL_Scancode key, int down) { } } -/* The virtual gamepad: every button in SDL_GamepadButton order (south .. touchpad) and 6 axes in SDL_GamepadAxis +/* A virtual gamepad: every button in SDL_GamepadButton order (south .. touchpad) and 6 axes in SDL_GamepadAxis * order (SDL's virtual driver maps them one to one when the masks are 0). */ -static void input_test_attach(void) { +static void input_test_attach_one(const char *name, SDL_JoystickID *pad, SDL_Joystick **joy) { SDL_VirtualJoystickDesc desc; SDL_INIT_INTERFACE(&desc); desc.type = SDL_JOYSTICK_TYPE_GAMEPAD; desc.nbuttons = INPUT_PAD_BUTTONS; desc.naxes = SDL_GAMEPAD_AXIS_COUNT; - desc.name = PSXSTACK_GAME_ID " input test"; - input_test_pad = SDL_AttachVirtualJoystick(&desc); - if (input_test_pad == 0) { + desc.name = name; + *pad = SDL_AttachVirtualJoystick(&desc); + if (*pad == 0) { port_fatal("input test: SDL_AttachVirtualJoystick: %s", SDL_GetError()); } - input_test_joy = SDL_OpenJoystick(input_test_pad); - if (input_test_joy == NULL) { + *joy = SDL_OpenJoystick(*pad); + if (*joy == NULL) { port_fatal("input test: SDL_OpenJoystick: %s", SDL_GetError()); } } -static void input_test_set(int id, int down) { +static void input_test_detach_one(SDL_JoystickID *pad, SDL_Joystick **joy) { + if (*pad != 0) { + SDL_CloseJoystick(*joy); + SDL_DetachVirtualJoystick(*pad); + *pad = 0; + *joy = NULL; + } +} + +/* Player 1's virtual gamepad; with input.player2 "first" the second one is attached before it (it is player 2's). */ +static void input_test_attach(void) { + if (input_player_gamepad[1] == 1 && input_test_pad2 == 0) { + input_test_attach_one(PSXSTACK_GAME_ID " input test 2", &input_test_pad2, &input_test_joy2); + } + input_test_attach_one(PSXSTACK_GAME_ID " input test", &input_test_pad, &input_test_joy); +} + +static void input_test_set_on(SDL_Joystick *joy, int id, int down) { static const SDL_GamepadAxis axes[] = { SDL_GAMEPAD_AXIS_LEFT_TRIGGER, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, SDL_GAMEPAD_AXIS_LEFTY, @@ -562,14 +662,18 @@ static void input_test_set(int id, int down) { } p = id - INPUT_KEYS; if (p < INPUT_PAD_BUTTONS) { - SDL_SetJoystickVirtualButton(input_test_joy, p, down != 0); + SDL_SetJoystickVirtualButton(joy, p, down != 0); } else { int j = p - INPUT_PAD_BUTTONS; Sint16 rest = j < 2 ? -32768 : 0, on = (j < 2 || (j & 1)) ? 32767 : -32768; - SDL_SetJoystickVirtualAxis(input_test_joy, axes[j], down ? on : rest); + SDL_SetJoystickVirtualAxis(joy, axes[j], down ? on : rest); } } +static void input_test_set(int id, int down) { + input_test_set_on(input_test_joy, id, down); +} + static int input_test_has(int id) { int i; for (i = 0; i < input_test_count; i++) { @@ -625,6 +729,19 @@ static void input_test_plan(void) { } static u16 input_test_expected; +static int input_test_players; /* the players' part: 1 plan, 2 both gamepads open, 3 the steps, 4..7 the unplugs */ +#define TEST_MAX_PSTEPS 16 +static struct { + int pads; /* 1 the first virtual gamepad, 2 the second, 3 both */ + int n; + int ids[INPUT_CHORD]; +} input_test_psteps[TEST_MAX_PSTEPS]; +static int input_test_pcount, input_test_photkeys, input_test_pstep, input_test_pphase, input_test_ppending; +static int input_test_pchecks, input_test_pfailures; +static u16 input_test_pexpected[PORT_PADS]; +static int input_test_pconnected[PORT_PADS]; +static int input_test_players_before(void); +static void input_test_players_after(void); static int input_test_pending; /* a step's input was applied this frame: check after the poll */ static int input_test_pause; /* the pause's round trip: 1 press now, 2 paused (then released) */ static int input_test_pause_polls; @@ -658,7 +775,8 @@ static int input_test_needs_pad(int step) { /* Before the poll: this frame's step (a press or its release). */ static void input_test_before(void) { int k, i, masked; - if (input_test_pause_before() || input_test_state_before() || input_test_menu_before()) { + if (input_test_players_before() || input_test_pause_before() || input_test_state_before() || + input_test_menu_before()) { return; } if (port_frames < 2 || input_test_step >= input_test_count) { @@ -699,6 +817,10 @@ static void input_test_before(void) { /* After the poll: the pad must show the step's bits (nothing at all with a script), a hotkey must have fired. */ static void input_test_after(void) { int ok, action; + if (input_test_players != 0) { + input_test_players_after(); + return; + } if (!input_test_pending) { return; } @@ -706,9 +828,9 @@ static void input_test_after(void) { input_test_checks++; action = input_test_steps[input_test_step].action; if (port_script_active) { - ok = !input_applied_any && input_buttons() == input_test_expected; + ok = !input_applied_any && input_buttons(0) == input_test_expected; } else { - ok = input_applied == input_test_expected; + ok = input_applied[0] == input_test_expected; } if (action >= 0 && input_test_phase == 0) { ok &= input_actions[action].pressed; @@ -716,7 +838,7 @@ static void input_test_after(void) { if (!ok) { input_test_failures++; port_log("input test: frame %ld step %d %s: pad 0x%04X (computed 0x%04X), expected 0x%04X%s%s%s", port_frames, - input_test_step, input_test_phase == 0 ? "press" : "release", input_applied, input_buttons(), + input_test_step, input_test_phase == 0 ? "press" : "release", input_applied[0], input_buttons(0), input_test_expected, port_script_active ? " and nothing sent (a script owns the pad)" : "", action >= 0 ? ", the action " : "", action >= 0 ? input_actions[action].name : ""); } @@ -731,13 +853,16 @@ static void input_test_after(void) { port_log("input test: %d of %d checks passed (%d keys, %d gamepad inputs, 1 chord, %d hotkey triggers)%s", input_test_checks - input_test_failures, input_test_checks, input_test_keys, input_test_pads, input_test_hotkeys, port_script_active ? "; the script owned the pad: nothing was sent" : ""); - if (input_test_pad != 0) { - SDL_CloseJoystick(input_test_joy); - SDL_DetachVirtualJoystick(input_test_pad); - } if (input_test_failures != 0) { port_exit(6, "input test failed"); } + input_test_players = 1; /* then the players' part */ +} + +/* After the players' part: the virtual gamepads go, then (without a script) the pause's round trip. */ +static void input_test_players_done(void) { + input_test_detach_one(&input_test_pad2, &input_test_joy2); + input_test_detach_one(&input_test_pad, &input_test_joy); if (!port_script_active && input_actions[port_action_pause].count > 0) { input_test_pause = 1; /* then the pause's round trip */ return; @@ -748,6 +873,223 @@ static void input_test_after(void) { } } +/* The players' part (after the steps): `input.player2` decides where each virtual gamepad goes. The first (player 1's + * by construction: with "first" the second was attached before it) always drives port 0; the second drives port 1 + * with "first" or "second", else port 0 too, and port 1 is connected exactly then. Steps: a gamepad input of the map + * (one no single-input hotkey takes) on the first, on the second, on both; with a player 2, each hotkey trigger made of + * gamepad inputs only, on the second (it must reach pad 2 and fire nothing). Each a press in one frame, the release in + * the next; then the second detached (port 1 then has the first with "first", nothing otherwise), then the first + * (nothing at all). Skipped when the map has no such gamepad input, or other gamepads are open. */ +static void input_test_players_add(int pads, int n, const int *ids) { + if (input_test_pcount == TEST_MAX_PSTEPS) { + port_fatal("input test: more than %d players' steps", TEST_MAX_PSTEPS); + } + input_test_psteps[input_test_pcount].pads = pads; + input_test_psteps[input_test_pcount].n = n; + memcpy(input_test_psteps[input_test_pcount].ids, ids, sizeof(int) * (size_t)n); + input_test_pcount++; +} + +static int input_test_single_hotkey(int id) { + int a, t; + for (a = 0; a < input_action_count; a++) { + for (t = 0; t < input_actions[a].count; t++) { + if (input_actions[a].triggers[t].n == 1 && input_actions[a].triggers[t].ids[0] == id) { + return 1; + } + } + } + return 0; +} + +static void input_test_players_plan(void) { + int i, a, t, k, g = -1; + for (i = 0; i < input_map_count && g < 0; i++) { + if (input_map[i].id >= INPUT_KEYS && !input_test_single_hotkey(input_map[i].id)) { + g = input_map[i].id; + } + } + input_test_pcount = 0; + if (g < 0) { + return; + } + input_test_players_add(1, 1, &g); + input_test_players_add(2, 1, &g); + input_test_players_add(3, 1, &g); + for (a = 0; a < input_action_count && input_player_gamepad[1] != 0; a++) { + for (t = 0; t < input_actions[a].count; t++) { + const InputTrigger *tr = &input_actions[a].triggers[t]; + int pads_only = 1; + for (k = 0; k < tr->n; k++) { + pads_only &= tr->ids[k] >= INPUT_KEYS; + } + if (pads_only) { + input_test_players_add(2, tr->n, tr->ids); + input_test_photkeys++; + } + } + } +} + +static int input_test_gamepads_open(void) { + int i, n = 0; + for (i = 0; i < INPUT_MAX_GAMEPADS; i++) { + n += input_gamepads[i] != NULL; + } + return n; +} + +/* Before the poll. Returns 1 while the players' part runs (the other parts wait). */ +static int input_test_players_before(void) { + int k, q, pad; + switch (input_test_players) { + case 1: /* the plan and the second gamepad */ + input_test_players_plan(); + if (input_test_pcount == 0) { + port_log("input test: the players: skipped (no gamepad input in the map that no hotkey takes alone)"); + input_test_players = 0; + input_test_players_done(); + return 1; + } + if (input_test_pad == 0) { + input_test_attach(); + } + if (input_test_pad2 == 0) { + input_test_attach_one(PSXSTACK_GAME_ID " input test 2", &input_test_pad2, &input_test_joy2); + } + input_test_wait = 0; + input_test_players = 2; + return 1; + case 2: /* both open (their SDL_EVENT_GAMEPAD_ADDED come with a poll) */ + if (!input_gamepad_open(input_test_pad) || !input_gamepad_open(input_test_pad2)) { + if (++input_test_wait > 10) { + port_fatal("input test: the second virtual gamepad was not opened after 10 frames"); + } + return 1; + } + if (input_test_gamepads_open() != 2) { + port_log("input test: the players: skipped (%d other gamepads are open)", input_test_gamepads_open() - 2); + input_test_players = 0; + input_test_players_done(); + return 1; + } + input_test_pstep = input_test_pphase = 0; + input_test_players = 3; /* the first step at once */ + /* fallthrough */ + case 3: + memset(input_test_pexpected, 0, sizeof(input_test_pexpected)); + input_test_pconnected[0] = 1; + input_test_pconnected[1] = input_player_gamepad[1] != 0; + for (pad = 1; pad <= 2; pad++) { + if (!(input_test_psteps[input_test_pstep].pads & pad)) { + continue; + } + q = pad == 2 && input_player_gamepad[1] != 0 ? 1 : 0; + for (k = 0; k < input_test_psteps[input_test_pstep].n; k++) { + int id = input_test_psteps[input_test_pstep].ids[k], i; + input_test_set_on(pad == 1 ? input_test_joy : input_test_joy2, id, input_test_pphase == 0); + for (i = 0; i < input_map_count && input_test_pphase == 0; i++) { + if (input_map[i].id == id) { + input_test_pexpected[q] |= input_map[i].bits; + } + } + } + } + input_test_ppending = 1; + return 1; + case 4: /* unplug the second, then the first */ + case 6: + if (input_test_players == 4) { + input_test_detach_one(&input_test_pad2, &input_test_joy2); + } else { + input_test_detach_one(&input_test_pad, &input_test_joy); + } + input_test_wait = 0; + input_test_players++; + return 1; + case 5: + case 7: + return 1; + default: + return 0; + } +} + +/* After the poll: both ports as expected, no hotkey fired; the unplugs once their gamepad is closed. */ +static void input_test_players_after(void) { + const char *mode = input_player_names[input_player_gamepad[1]], *fired = NULL; + int ok = 1, p, a; + if (input_test_players == 5 || input_test_players == 7) { + if (input_gamepad_open(input_test_players == 5 ? input_test_pad2 : input_test_pad)) { + /* not yet: SDL_EVENT_GAMEPAD_REMOVED comes with a poll */ + if (++input_test_wait > 10) { + port_fatal("input test: a detached virtual gamepad was still open after 10 frames"); + } + return; + } + memset(input_test_pexpected, 0, sizeof(input_test_pexpected)); + input_test_pconnected[0] = 1; + /* the first is now the only gamepad: player 2's with "first" */ + input_test_pconnected[1] = input_test_players == 5 && input_player_gamepad[1] == 1; + } else if (input_test_players != 3 || !input_test_ppending) { + return; + } + input_test_ppending = 0; + input_test_pchecks++; + for (p = 0; p < PORT_PADS; p++) { + u16 sent = port_script_active ? input_buttons(p) : input_applied[p]; + int connected = port_script_active ? input_connected(p) : input_applied_connected[p]; + ok &= sent == input_test_pexpected[p] && connected == input_test_pconnected[p]; + } + if (port_script_active) { + ok &= !input_applied_any; + } + for (a = 0; a < input_action_count; a++) { + if (input_actions[a].pressed) { + fired = input_actions[a].name; + ok = 0; + } + } + if (!ok) { + input_test_pfailures++; + port_log("input test: frame %ld the players (input.player2 \"%s\"), %s: port 0 0x%04X, port 1 0x%04X%s; " + "expected 0x%04X, 0x%04X%s%s%s%s", + port_frames, mode, + input_test_players == 3 ? (input_test_pphase == 0 ? "press" : "release") + : input_test_players == 5 ? "the second unplugged" : "the first unplugged", + port_script_active ? input_buttons(0) : input_applied[0], + port_script_active ? input_buttons(1) : input_applied[1], + (port_script_active ? input_connected(1) : input_applied_connected[1]) ? "" : " (none)", + input_test_pexpected[0], input_test_pexpected[1], input_test_pconnected[1] ? "" : " (none)", + port_script_active ? " and nothing sent (a script owns the pad)" : "", + fired != NULL ? ", and no hotkey, but this fired: " : "", fired != NULL ? fired : ""); + } + if (input_test_players == 5 || input_test_players == 7) { + input_test_players++; + } else if (input_test_pphase == 0) { + input_test_pphase = 1; + return; + } else { + input_test_pphase = 0; + if (++input_test_pstep == input_test_pcount) { + input_test_players = 4; + } + return; + } + if (input_test_players != 8) { + return; + } + port_log("input test: the players (input.player2 \"%s\"): %d of %d checks passed (%d presses, %d hotkey triggers " + "on the second gamepad, 2 unplugs)%s", + mode, input_test_pchecks - input_test_pfailures, input_test_pchecks, + input_test_pcount - input_test_photkeys, input_test_photkeys, port_script_active ? "; the script owned the pad: nothing was sent" : ""); + input_test_players = 0; + if (input_test_pfailures != 0) { + port_exit(6, "input test failed"); + } + input_test_players_done(); +} + /* The pause's round trip (after the steps, without a script): the pause key's first trigger pressed at a vsync (the * pump pauses at its end), released at the first poll of the pause and pressed again at the third (the pump goes * on), released at the next vsync: the pause must have polled three times without a vsync. */ @@ -1053,12 +1395,17 @@ void port_input_frame(void) { input_test_before(); } input_poll(); - if (!port_script_active && !port_debug_pad_owned) { - u16 bits = input_buttons(); - psyq_pad_set(0, 1, bits); - port_framelog_input(bits); - input_applied = bits; - input_applied_any = 1; + if (!port_script_active) { + int p; + for (p = port_debug_pad_owned ? 1 : 0; p < PORT_PADS; p++) { /* the debug channel drives port 0 */ + int connected = input_connected(p); + u16 bits = connected ? input_buttons(p) : 0; + psyq_pad_set(p, connected, bits); + port_framelog_input(p, connected, bits); + input_applied[p] = bits; + input_applied_connected[p] = connected; + input_applied_any = 1; + } } if (input_test) { input_test_after(); diff --git a/runtime/script.c b/runtime/script.c index 2d0e98e..b44a50d 100644 --- a/runtime/script.c +++ b/runtime/script.c @@ -690,9 +690,12 @@ void port_script_frame(void) { break; } } - /* run.lua apply_pad: the held buttons on pad 1, every frame; the record's input trace gets the changes */ + /* run.lua apply_pad: the held buttons on pad 1, every frame; the record's input trace gets the changes. The script + * owns both ports: port 1 has nothing connected (so a state saved with a second pad in goes on without it) */ psyq_pad_set(0, 1, script_held); - port_framelog_input(script_held); + port_framelog_input(0, 1, script_held); + psyq_pad_set(1, 0, 0); + port_framelog_input(1, 0, 0); if (script_index >= script_step_count) { port_exit(0, "script complete"); } diff --git a/runtime/settings.c b/runtime/settings.c index 38ee270..b939930 100644 --- a/runtime/settings.c +++ b/runtime/settings.c @@ -12,7 +12,8 @@ * "audio": { "mute": false }, * "memcard1": "card1.mcd", "memcard2": "card2.mcd", null: no card in that slot * "watchdog": 0, seconds; 0 off - * "input": { "keyboard": {...}, "gamepad": {...}, "hotkeys": {...} }, input.c (port_input_settings) + * "input": { "keyboard": {...}, "gamepad": {...}, "hotkeys": {...}, + * "player2": "off" }, input.c (port_input_settings) * "mods": { "": { "enabled": false, "