Merge remote-tracking branch 'origin/feat/title-rewind-block' into feat/netplay-input-transform

# Conflicts:
#	generated/OpenBIOS.emitter.sha
#	runtime/src/debug_server.c
This commit is contained in:
Shokunin
2026-10-06 19:37:46 -07:00
14 changed files with 662 additions and 49 deletions
+5 -3
View File
@@ -72,8 +72,9 @@ Columns: **N** = native, **D** = DuckStation oracle.
| `cdrom_sector_history_clear` | ✓ | | — | Reset the CD-ROM sector history ring |
| `watch` | ✓ | ✓ | `addr` | Set byte-level memory watchpoint (fires per-frame on change) |
| `unwatch` | ✓ | ✓ | `addr` | Remove memory watchpoint |
| `set_input` | ✓ | ✓ | `buttons`, optional `frames`, optional `lx`, `ly`, `rx`, `ry`, `pad_type` | Override pad1 buttons and optional analog axes (PS1 inverted bitmask, 0 = pressed; axes 0-255). Debug builds also accept `pad_type` (0 digital, 1 DualShock, 2 JogCon) to test an emulated device identity. Holds until `clear_input` on both backends; pass `frames=N` (beetle) to auto-release after N frames |
| `set_input` | ✓ | ✓ | `buttons`, optional `frames`, optional `lx`, `ly`, `rx`, `ry`, `pad_type` | Override pad1 buttons and optional analog axes (PS1 inverted bitmask, 0 = pressed; axes 0-255). Debug builds also accept `pad_type` (0 digital, 1 DualShock, 2 JogCon) to test an emulated device identity. Holds until `clear_input` on both backends; pass `frames=N` (beetle) to auto-release after N frames. Runtime: `layer="host"` instead arms a virtual P1 gamepad (`buttons`, `lx`..`ry`, `lt`/`rt` 0-255) that feeds the normal offline input path (controller source, title pad transform, trigger values) and host shortcut polling, also headless; a plain override still wins while armed; `clear_input` disarms it |
| `clear_input` | ✓ | ✓ | — | Remove input and analog axis overrides |
| `rewind_status` | ✓ | | — | Local Rewind: `enabled`, `open`, `title_blocked` (`psx_mod_set_rewind_blocked`) and `snaps` held in the ring |
| `turbo` | ✓ | | `enabled` | Enable/disable TCP-controlled frontend turbo for fast-forward validation |
| `turbo_state` | ✓ | | — | Query TCP-controlled turbo state |
| `pause` | ✓ | | — | **REMOVED** — still registered, but always returns an error. Query a ring buffer (`fn_entry_tail`, `wtrace_dump`, `gpu_frame_dump`) instead of synthesizing a snapshot |
@@ -431,9 +432,9 @@ The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` i
## Complete command index (generated)
**344 commands registered** — 331 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
**345 commands registered** — 332 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
63 of 344 have prose above; **281 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
64 of 345 have prose above; **281 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
@@ -672,6 +673,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `restore_trace` | ✓ | | |
| `restore_trace_clear` | ✓ | | |
| `restore_trace_window` | ✓ | | |
| `rewind_status` | ✓ | | ✓ |
| `rtrace_arm` | ✓ | ✓ | |
| `rtrace_clear` | ✓ | | |
| `rtrace_disarm` | | ✓ | |
+9
View File
@@ -690,6 +690,15 @@ ignores all of these.
Netplay rollback is a separate subsystem with its own ring and is unaffected by
this setting; rewind is in fact suppressed while a netplay session is active.
A title can also block Rewind while it is in a mode that must not be rewound,
such as local split-screen multiplayer: a trusted mod plugin calls
`psx_mod_set_rewind_blocked(1)` when the mode starts and `0` when it ends
(`runtime/include/mod_plugins.h`). While blocked, opening Rewind is refused
with an OSD note, no snapshots are captured (the ring keeps what it already
has), and a title-allowed direct Rewind button (`psx_mod_allow_direct_shortcut`)
is not claimed, so it reaches the game as if Rewind were off. The block clears
at every mod/session reset; nothing changes for a title that never calls it.
Bezel artwork is intentionally not a `[video]` key. It is exposed as the
disabled-by-default `psx.presentation.bezel` mod package, which draws a
user-selected image resource behind the game image in OpenGL letterbox or
+16
View File
@@ -908,6 +908,22 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
if (ct.contains("legacy_pad_config")) {
rt.legacy_pad_config = toml::find<bool>(ct, "legacy_pad_config");
}
if (ct.contains("direct_shortcut")) {
rt.direct_shortcut = toml::find<std::string>(ct, "direct_shortcut");
if (rt.direct_shortcut != "rewind" &&
rt.direct_shortcut != "save_state_menu" &&
rt.direct_shortcut != "fast_forward" &&
rt.direct_shortcut != "fast_forward_toggle")
throw std::runtime_error(fmt::format(
"[controller] direct_shortcut must be rewind, "
"save_state_menu, fast_forward or fast_forward_toggle, "
"got '{}'", rt.direct_shortcut));
if (!ct.contains("direct_shortcut_button"))
throw std::runtime_error(
"[controller] direct_shortcut needs direct_shortcut_button");
rt.direct_shortcut_button =
toml::find<std::string>(ct, "direct_shortcut_button");
}
if (ct.contains("anti_deadzone")) {
const auto n = toml::find<int64_t>(ct, "anti_deadzone");
if (n < 0 || n > 32767)
+9
View File
@@ -706,6 +706,15 @@ struct RuntimeConfig {
bool has_multitap_port = false;
int multitap_port = 1;
// direct_shortcut / direct_shortcut_button: one host shortcut ("rewind",
// "save_state_menu", "fast_forward", "fast_forward_toggle") the launcher
// captures as a single controller button, with that SDL GameController
// button name (e.g. "y") as its default binding. At run time a
// one-button binding acts alone only while the title's mod allows it
// (psx_mod_allow_direct_shortcut); otherwise it means Select + button.
std::string direct_shortcut;
std::string direct_shortcut_button;
// multitap_analog: DualShock-on-tap hack (default true). When true,
// multitap bulk seats may report 0x73 + stick bytes; when false (faithful),
// tap seats stay plain digital. Overridable by settings.toml / match_caps.
+3
View File
@@ -471,6 +471,9 @@ if(BUILD_TESTING)
tests/test_mod_pad_transform.c src/mod_pad_transform.c)
target_include_directories(mod_pad_transform_test PRIVATE include)
add_test(NAME mod_pad_transform_test COMMAND mod_pad_transform_test)
add_executable(psx_hotkey_pad_test tests/test_psx_hotkey_pad.c)
target_include_directories(psx_hotkey_pad_test PRIVATE include)
add_test(NAME psx_hotkey_pad_test COMMAND psx_hotkey_pad_test)
add_executable(psx_trigger_values_test tests/test_psx_trigger_values.c)
target_include_directories(psx_trigger_values_test PRIVATE include)
add_test(NAME psx_trigger_values_test COMMAND psx_trigger_values_test)
+7
View File
@@ -209,6 +209,13 @@ int debug_server_get_axis_override(unsigned char st[4]);
* 2=JogCon. */
int debug_server_get_pad_type_override(void);
/* Host-pad layer (set_input {"layer":"host", buttons, lx..ry, lt, rt}): a
* virtual P1 gamepad feeding the normal offline input path and host shortcut
* polling. Returns 1 while armed (until clear_input) and fills the PSX
* active-low word, sticks and trigger values (0..255). */
int debug_server_get_host_pad(uint16_t *buttons, uint8_t st[4], uint8_t *lt,
uint8_t *rt);
/* TCP-controlled turbo mode. When enabled the frontend skips presentation and
* wall-clock pacing at vblank, matching the keyboard TAB turbo path. */
int debug_server_turbo_enabled(void);
+26
View File
@@ -902,6 +902,32 @@ typedef struct PSXModPadTransform {
} PSXModPadTransform;
int psx_mod_set_pad_transform(uint32_t player, const PSXModPadTransform *transform);
/* Let a host shortcut bound to ONE controller button act on that button
* alone. Without this, a one-button binding means Select + button (the
* legacy rule), so a title's default one-button binding is safe whether or
* not its mod is enabled. While allowed (and, for Rewind, while Rewind is
* enabled) the runtime claims that host button: it is removed from P1's
* guest pad, and a claim made while it is held lasts until it is released.
* Call at mod activation; cleared at every mod/session reset. Returns 0 for
* an unknown shortcut. Multi-button bindings are unaffected. */
enum {
PSX_MOD_SHORTCUT_REWIND = 0,
PSX_MOD_SHORTCUT_SAVE_STATE_MENU = 1,
PSX_MOD_SHORTCUT_FAST_FORWARD = 2,
PSX_MOD_SHORTCUT_FAST_FORWARD_TOGGLE = 3
};
int psx_mod_allow_direct_shortcut(uint32_t shortcut);
/* Refuse local Rewind while the title is in a mode it must not rewind (e.g.
* local split-screen multiplayer). While blocked, opening Rewind is refused
* with an OSD note, no history is captured (snapshots already in the ring are
* kept), and an allowed direct Rewind shortcut is not in force: its button is
* not claimed and reaches the guest pad / title transform as if Rewind were
* disabled. Netplay already refuses Rewind on its own. Opt-in: call with 1
* when the mode starts and 0 when it ends (setting it every frame from the
* title's state is fine). Cleared at every mod/session reset. */
void psx_mod_set_rewind_blocked(int blocked);
/* Local P1 mouse policy. The runtime delivers ordered events on the SDL owner
* (main) thread, owns relative capture and folds the resulting right-stick
* bytes after native input/presentation (and after any controller source),
+84
View File
@@ -0,0 +1,84 @@
#pragma once
/* Host controller shortcut bindings (launcher "assist" binds), pure logic.
*
* Encodings (shared with recomp-ui): 0 unbound; 1..99 one button (code+1),
* always chorded with Select (legacy); 100..999 an axis direction, chorded
* with Select; >= 1000 a button combination (1000 + mask of SDL
* GameController button codes), every button held.
*
* A ONE-button combination is a direct shortcut only while the running title
* allows it for that shortcut (psx_mod_allow_direct_shortcut); otherwise it
* means Select + that button, like the legacy one-button encoding. A direct
* shortcut claims its host button: the button is removed from the guest pad
* while claimed and, once claimed while held, until it is released. */
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define PSX_HOTKEY_PAD_COMBO_BASE 1000
typedef int (*PsxHotkeyButtonDown)(void *ctx, int sdl_button);
typedef int (*PsxHotkeyAxisValue)(void *ctx, int sdl_axis);
/* SDL button code of a one-button combination, else -1. */
static inline int psx_hotkey_pad_single_button(int binding) {
if (binding < PSX_HOTKEY_PAD_COMBO_BASE) return -1;
const uint32_t mask = (uint32_t)(binding - PSX_HOTKEY_PAD_COMBO_BASE);
if (!mask || (mask & (mask - 1u))) return -1;
int code = 0;
while (!(mask & (1u << code))) code++;
return code;
}
/* Whether `binding` is held. `back` is the Select (Back) button code. */
static inline int psx_hotkey_pad_down(int binding, int direct_allowed, int back,
PsxHotkeyButtonDown button_down,
PsxHotkeyAxisValue axis_value, void *ctx) {
if (binding <= 0) return 0;
if (binding >= PSX_HOTKEY_PAD_COMBO_BASE) {
const uint32_t mask = (uint32_t)(binding - PSX_HOTKEY_PAD_COMBO_BASE);
if (!mask) return 0;
if (psx_hotkey_pad_single_button(binding) >= 0 && !direct_allowed &&
!button_down(ctx, back))
return 0;
for (int code = 0; code < 32; ++code)
if ((mask & (1u << code)) && !button_down(ctx, code)) return 0;
return 1;
}
if (!button_down(ctx, back)) return 0;
if (binding < 100) return button_down(ctx, binding - 1);
{
const int axis = (binding - 100) / 2;
const int v = axis_value ? axis_value(ctx, axis) : 0;
return ((binding - 100) & 1) ? (v > 16000) : (v < -16000);
}
}
/* Whether the direct allowance for `shortcut` is in force: allowed by the
* title and, for Rewind (`is_rewind`), Rewind enabled and not blocked by the
* title (psx_mod_set_rewind_blocked). When not in force the button is not
* claimed and a one-button binding falls back to Select + button. */
static inline int psx_hotkey_direct_active(uint32_t allowed_mask, int shortcut,
int is_rewind, int rewind_enabled,
int rewind_blocked) {
if (shortcut < 0 || shortcut >= 32 || !(allowed_mask & (1u << shortcut)))
return 0;
if (is_rewind && (!rewind_enabled || rewind_blocked)) return 0;
return 1;
}
/* Per-frame claim of direct-shortcut host buttons. `claimed` is the mask of
* buttons claimed this frame, `held` the buttons physically held. Returns
* the mask to remove from the guest pad; *latched carries a claim through
* release after the shortcut stops claiming the button. */
static inline uint32_t psx_hotkey_claim_update(uint32_t *latched,
uint32_t claimed, uint32_t held) {
*latched = (*latched | claimed) & held;
return claimed | *latched;
}
#ifdef __cplusplus
}
#endif
+7
View File
@@ -40,6 +40,13 @@ void psx_rewind_configure(uint32_t bios_checksum, uint32_t entry_pc);
void psx_rewind_shutdown(void);
int psx_rewind_enabled(void);
/* Title block (mod API psx_mod_set_rewind_blocked): while set, Rewind refuses
* to open and captures no history (the ring keeps what it has). Cleared at
* every mod session start. */
void psx_rewind_set_title_blocked(int blocked);
int psx_rewind_title_blocked(void);
/* Snapshots in the ring (debug/status). */
uint32_t psx_rewind_snap_count(void);
int psx_rewind_is_open(void);
/* 1 while open or slide animation still visible. */
int psx_rewind_needs_present(void);
+67 -2
View File
@@ -61,6 +61,7 @@
#include "guest_tty.h"
#include "frame_fingerprint.h"
#include "psx_segment_miss.h"
#include "psx_rewind.h"
#include <stdio.h>
#include <stdlib.h>
@@ -7828,10 +7829,40 @@ static void handle_unwatch(int id, const char *json)
static int s_trigger_override;
static uint32_t s_trigger_lt, s_trigger_rt;
/* Host-pad layer (set_input {"layer":"host"}): a virtual P1 gamepad that
* feeds stage 1 of the normal offline input path (and host shortcut
* polling) instead of replacing it, so title transforms, direct shortcuts
* and trigger values see it like a physical pad. Persistent until
* clear_input. A plain override still wins while armed. */
static int s_host_layer = 0;
static uint16_t s_host_buttons = 0xFFFF;
static uint8_t s_host_axes[4] = { 0x80, 0x80, 0x80, 0x80 };
static uint8_t s_host_lt = 0, s_host_rt = 0;
static uint8_t clamp_u8_arg(int v, uint8_t dflt) {
if (v < 0) return dflt;
return (uint8_t)(v > 255 ? 255 : v);
}
static void handle_set_input(int id, const char *json)
{
char val_str[32];
char layer[16];
const int pad_type = json_get_int(json, "pad_type", -1);
if (json_get_str(json, "layer", layer, sizeof(layer)) &&
strcmp(layer, "host") == 0) {
s_host_buttons = json_get_str(json, "buttons", val_str, sizeof(val_str))
? (uint16_t)hex_to_u32(val_str) : (uint16_t)0xFFFF;
s_host_axes[0] = clamp_u8_arg(json_get_int(json, "lx", -1), 0x80);
s_host_axes[1] = clamp_u8_arg(json_get_int(json, "ly", -1), 0x80);
s_host_axes[2] = clamp_u8_arg(json_get_int(json, "rx", -1), 0x80);
s_host_axes[3] = clamp_u8_arg(json_get_int(json, "ry", -1), 0x80);
s_host_lt = clamp_u8_arg(json_get_int(json, "lt", -1), 0);
s_host_rt = clamp_u8_arg(json_get_int(json, "rt", -1), 0);
s_host_layer = 1;
send_ok(id);
return;
}
if (!json_get_str(json, "buttons", val_str, sizeof(val_str))) {
send_err(id, "missing buttons"); return;
}
@@ -7900,6 +7931,19 @@ extern int sio_get_pad_analog(int slot);
extern void sio_get_pad_sticks(int slot, uint8_t out[4]);
extern void sio_get_pad_negcon(int slot, uint8_t out[3]);
extern int sio_get_pad_mode_locked(int slot);
/* Local Rewind: enabled, panel open, title block (psx_mod_set_rewind_blocked)
* and the snapshots held, so scripted runs can check capture and opening. */
static void handle_rewind_status(int id, const char *json)
{
(void)json;
send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%s,\"open\":%s,"
"\"title_blocked\":%s,\"snaps\":%u}\n",
id, psx_rewind_enabled() ? "true" : "false",
psx_rewind_is_open() ? "true" : "false",
psx_rewind_title_blocked() ? "true" : "false",
(unsigned)psx_rewind_snap_count());
}
static void handle_pad_status(int id, const char *json)
{
(void)json;
@@ -7916,7 +7960,9 @@ static void handle_pad_status(int id, const char *json)
"\"slot1\":{\"buttons\":\"0x%04X\",\"connected\":%s,\"analog\":%s,\"type\":%d,\"sticks\":[%u,%u,%u,%u],"
"\"negcon\":[%u,%u,%u],\"mode_locked\":%s},"
"\"override\":%d,\"override_frames\":%d,\"override_pad_type\":%d,"
"\"override_axes\":[%u,%u,%u,%u],\"override_axes_valid\":%s}\n",
"\"override_axes\":[%u,%u,%u,%u],\"override_axes_valid\":%s,"
"\"host_layer\":%s,\"host_buttons\":\"0x%04X\",\"host_axes\":[%u,%u,%u,%u],"
"\"host_lt\":%u,\"host_rt\":%u}\n",
id, pad0,
pad0, sio_get_pad_connected(0) ? "true" : "false", sio_get_pad_analog(0) ? "true" : "false",
sio_get_pad_analog(0),
@@ -7928,7 +7974,10 @@ static void handle_pad_status(int id, const char *json)
neg1[0], neg1[1], neg1[2], sio_get_pad_mode_locked(1) ? "true" : "false",
s_input_override, s_input_frames, s_pad_type_override,
s_axis_st[0], s_axis_st[1], s_axis_st[2], s_axis_st[3],
s_axis_override ? "true" : "false");
s_axis_override ? "true" : "false",
s_host_layer ? "true" : "false", s_host_buttons,
s_host_axes[0], s_host_axes[1], s_host_axes[2], s_host_axes[3],
s_host_lt, s_host_rt);
}
static void handle_clear_input(int id, const char *json)
@@ -7942,6 +7991,10 @@ static void handle_clear_input(int id, const char *json)
s_axis_override = 0;
s_pad_type_override = -1;
s_axis_st[0] = s_axis_st[1] = s_axis_st[2] = s_axis_st[3] = 0x80;
s_host_layer = 0;
s_host_buttons = 0xFFFF;
s_host_axes[0] = s_host_axes[1] = s_host_axes[2] = s_host_axes[3] = 0x80;
s_host_lt = s_host_rt = 0;
send_ok(id);
}
@@ -15128,6 +15181,7 @@ static const CmdEntry s_commands[] = {
{ "set_input", handle_set_input },
{ "press", handle_press },
{ "pad_status", handle_pad_status },
{ "rewind_status", handle_rewind_status },
{ "clear_input", handle_clear_input },
{ "input_route_clear", handle_input_route_clear },
{ "input_route_append",handle_input_route_append },
@@ -16134,6 +16188,17 @@ int debug_server_get_axis_override(unsigned char st[4])
return 1;
}
int debug_server_get_host_pad(uint16_t *buttons, uint8_t st[4], uint8_t *lt,
uint8_t *rt)
{
if (!s_host_layer) return 0;
if (buttons) *buttons = s_host_buttons;
if (st) memcpy(st, s_host_axes, 4);
if (lt) *lt = s_host_lt;
if (rt) *rt = s_host_rt;
return 1;
}
int debug_server_get_pad_type_override(void)
{
return s_pad_type_override;
+291 -38
View File
@@ -69,6 +69,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
#include "psx_stick.h" /* radial SDL-stick -> DualShock response transform */
#include "psx_trigger.h" /* continuous SDL trigger -> 0..255 magnitude */
#include "mod_pad_transform.h"
#include "psx_hotkey_pad.h" /* host shortcut bindings, direct claims */
#include "psx_controller_type.h" /* mapped wheel-name classification */
#include "psx_netplay_rb.h"
#include "psx_selfcheck.h"
@@ -1339,6 +1340,27 @@ static int g_hotkey_pad_rewind = 1272; /* select + r3 */
static int g_hotkey_pad_save_state_menu = 2040;/* select + r1 */
static int g_hotkey_pad_fast_forward = 1528; /* select + l1 (hold) */
static int g_hotkey_pad_fast_forward_toggle = 0; /* unbound: latch fast-forward */
/* Direct host shortcuts the running title allows (bit per PSX_MOD_SHORTCUT_*,
* psx_mod_allow_direct_shortcut); cleared at every mod session start. */
static uint32_t g_direct_shortcut_allowed = 0;
/* SDL buttons of P1's gamepad (or the debug host layer) currently claimed by
* a direct shortcut and removed from the guest pad; see psx_hotkey_pad.h. */
static uint32_t g_p1_claimed_buttons = 0;
static uint32_t g_p1_claim_latched = 0;
/* Title-declared direct shortcut ([controller] direct_shortcut / _button):
* the launcher captures that action as one button and defaults it there. */
static int g_title_direct_shortcut = -1;
static int g_title_direct_button = -1;
extern "C" int psx_mod_allow_direct_shortcut(uint32_t shortcut) {
if (shortcut > PSX_MOD_SHORTCUT_FAST_FORWARD_TOGGLE) return 0;
g_direct_shortcut_allowed |= 1u << shortcut;
return 1;
}
extern "C" void psx_mod_set_rewind_blocked(int blocked) {
psx_rewind_set_title_blocked(blocked);
}
static uint32_t g_savestate_input_guard_min_until = 0;
static uint32_t g_savestate_input_guard_max_until = 0;
static int g_headless = 0; /* debug/CI frontend: no SDL window/audio */
@@ -5225,12 +5247,20 @@ static bool source_is_stick_axis(const ControllerSource& s) {
static uint16_t controller_pad_buttons(const ControllerMap& map,
SDL_GameController* h,
bool suppress_stick_axes,
int deadzone_raw) {
int deadzone_raw,
uint32_t claimed_buttons = 0) {
uint16_t buttons = 0xFFFF; /* all released */
if (!h) return buttons;
for (const auto& entry : map) {
for (const auto& source : entry.sources) {
if (suppress_stick_axes && source_is_stick_axis(source)) continue;
/* A host button claimed by a direct shortcut never reaches the
* guest (psx_hotkey_pad.h). */
if (claimed_buttons &&
source.kind == ControllerSource::Kind::Button &&
source.id >= 0 && source.id < 32 &&
(claimed_buttons & (1u << source.id)))
continue;
if (!controller_source_pressed_h(h, source, deadzone_raw)) continue;
if (entry.bit)
buttons &= (uint16_t)~entry.bit;
@@ -5268,7 +5298,8 @@ static uint16_t pad_buttons_for(const PlayerInput& p, int player, bool suppress_
if (p.kind == 1) return pad_from_keyboard(player);
if (p.kind == 2)
return controller_pad_buttons(controller_map_for(p), p.handle,
suppress_stick_axes, p.deadzone);
suppress_stick_axes, p.deadzone,
player == 1 ? g_p1_claimed_buttons : 0u);
return 0xFFFF;
}
@@ -5760,6 +5791,70 @@ static void apply_input_override_to_sio(int override_word) {
* disconnected. `guarded` (savestate input guard) delivers neutral buttons and
* sticks but still resolves presence/type. No mouse or source side effects:
* those are layered on top by pad_ext_resolve(). */
/* Debug host-pad layer (set_input {"layer":"host"}): a virtual P1 gamepad. */
static int host_pad_layer(uint16_t* buttons, uint8_t st[4], uint8_t* lt,
uint8_t* rt) {
#ifndef PSX_NO_DEBUG_TOOLS
return debug_server_get_host_pad(buttons, st, lt, rt);
#else
(void)buttons; (void)st; (void)lt; (void)rt;
return 0;
#endif
}
/* The host layer's PSX word expressed as the Xbox-layout SDL buttons a
* default-mapped gamepad would press (L2/R2 are trigger axes). */
static const struct { int sdl; uint16_t psx; } kHostLayerButtons[] = {
{ SDL_CONTROLLER_BUTTON_A, PAD_CROSS },
{ SDL_CONTROLLER_BUTTON_B, PAD_CIRCLE },
{ SDL_CONTROLLER_BUTTON_X, PAD_SQUARE },
{ SDL_CONTROLLER_BUTTON_Y, PAD_TRIANGLE },
{ SDL_CONTROLLER_BUTTON_BACK, PAD_SELECT },
{ SDL_CONTROLLER_BUTTON_START, PAD_START },
{ SDL_CONTROLLER_BUTTON_LEFTSTICK, PAD_L3 },
{ SDL_CONTROLLER_BUTTON_RIGHTSTICK, PAD_R3 },
{ SDL_CONTROLLER_BUTTON_LEFTSHOULDER, PAD_L1 },
{ SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, PAD_R1 },
{ SDL_CONTROLLER_BUTTON_DPAD_UP, PAD_UP },
{ SDL_CONTROLLER_BUTTON_DPAD_DOWN, PAD_DOWN },
{ SDL_CONTROLLER_BUTTON_DPAD_LEFT, PAD_LEFT },
{ SDL_CONTROLLER_BUTTON_DPAD_RIGHT, PAD_RIGHT },
};
static uint32_t host_layer_sdl_held(uint16_t word) {
uint32_t held = 0;
for (const auto& b : kHostLayerButtons)
if ((word & b.psx) == 0) held |= 1u << b.sdl;
return held;
}
static uint16_t host_layer_release_claimed(uint16_t word, uint32_t claimed) {
for (const auto& b : kHostLayerButtons)
if (claimed & (1u << b.sdl)) word |= b.psx;
return word;
}
/* Stage 1 for P1 while the host layer is armed: buttons less any claimed
* shortcut button; DualShock with the layer's sticks unless P1 is digital
* (host_view, the transform's host pad: the sticks in either mode). */
static int capture_host_layer_pad(PsxNetPad* out, uint16_t word,
const uint8_t st[4], bool guarded,
bool host_view) {
const int mode = effective_player_mode_for_sio(g_players[0], 0);
const bool analog = mode != PSXRecompV4::PAD_MODE_DIGITAL;
const bool sticks = analog || host_view;
out->buttons = host_layer_release_claimed(word, g_p1_claimed_buttons);
out->lx = sticks ? st[0] : 0x80; out->ly = sticks ? st[1] : 0x80;
out->rx = sticks ? st[2] : 0x80; out->ry = sticks ? st[3] : 0x80;
out->analog = analog ? SIO_PAD_DUALSHOCK : SIO_PAD_DIGITAL;
out->connected = 1;
if (guarded) {
out->buttons = 0xFFFFu;
out->lx = out->ly = out->rx = out->ry = 0x80u;
}
return 1;
}
/* host_view: the port's host pad before presentation, for a title pad
* transform (PadExtHooks.host_pad): real sticks and buttons without the
* digital stick->D-pad fold, whatever the configured mode; type unchanged. */
@@ -5771,6 +5866,14 @@ static int capture_pad_slot_view(int s, PsxNetPad* out, bool guarded,
out->analog = 0;
out->connected = 0;
if (s == 0) {
uint16_t host_word;
uint8_t host_st[4], host_lt, host_rt;
if (host_pad_layer(&host_word, host_st, &host_lt, &host_rt))
return capture_host_layer_pad(out, host_word, host_st, guarded,
host_view);
}
PlayerInput& p = g_players[s];
const int player = s + 1; /* keybinds.ini section (1..5) */
/* Opt-in dev merge: P1 is driven by the keyboard AND every connected
@@ -6532,6 +6635,16 @@ static void pad_ext_host_extras(void*, int s, uint32_t* flags, uint32_t* lt,
uint32_t* rt) {
*flags = *lt = *rt = 0;
if (s < 0 || s >= PSX_MAX_PLAYERS) return;
if (s == 0) {
uint16_t word;
uint8_t st[4], hl, hr;
if (host_pad_layer(&word, st, &hl, &hr)) {
*flags = PSX_MOD_PAD_HOST_GAMEPAD | PSX_MOD_PAD_HOST_LT |
PSX_MOD_PAD_HOST_RT;
*lt = hl; *rt = hr;
return;
}
}
SDL_GameController* handle = g_players[s].handle;
if (g_players[s].kind != 2 || !handle) return;
*flags = PSX_MOD_PAD_HOST_GAMEPAD;
@@ -6629,6 +6742,16 @@ static void sample_headless_pad_into_sio(int override) {
apply_input_override_to_sio(override);
return;
}
if (host_pad_layer(nullptr, nullptr, nullptr, nullptr)) {
/* Debug host layer: P1 goes through the full offline resolution
* (source, title transform) exactly as a windowed gamepad would. */
PadExtHooks hooks = pad_ext_main_hooks();
PsxNetPad pad;
if (pad_ext_resolve(&hooks, 0, &pad))
apply_pad_slot_to_sio(0, pad);
sio_set_pad_state_slot(1, 0xFFFFu);
return;
}
#ifdef PSX_COSIM
/* Input-driven cosim (EXPERIMENTAL): the coordinator sets a HELD pad state via the
* `setpad` TCP command; the headless sampler applies it every frame so the same
@@ -7015,42 +7138,119 @@ static int normalize_hotkey_pad_binding(int binding, int fallback) {
return fallback;
}
static int hotkey_pad_binding_down(int binding) {
SDL_GameController *h = g_players[0].handle;
if (!h || binding == 0)
return 0;
if (PSX_HOTKEY_PAD_IS_BUTTON_COMBO(binding)) {
uint32_t mask = (uint32_t)PSX_HOTKEY_PAD_BUTTON_COMBO_MASK(binding);
if (!mask)
return 0;
for (int code = 0; code < SDL_CONTROLLER_BUTTON_MAX && code < 32; ++code) {
if ((mask & ((uint32_t)1u << code)) == 0)
continue;
if (!SDL_GameControllerGetButton(
h, (SDL_GameControllerButton)code))
return 0;
/* P1 host controller state for shortcut polling: the debug host layer while
* armed, else P1's assigned SDL gamepad. */
static int p1_host_button_down(void*, int code) {
uint16_t word;
uint8_t st[4], lt, rt;
if (host_pad_layer(&word, st, &lt, &rt))
return (host_layer_sdl_held(word) >> code) & 1u;
SDL_GameController* h = g_players[0].handle;
if (!h || code < 0 || code >= SDL_CONTROLLER_BUTTON_MAX) return 0;
return SDL_GameControllerGetButton(h, (SDL_GameControllerButton)code) != 0;
}
static int p1_host_axis_value(void*, int axis) {
uint16_t word;
uint8_t st[4], lt, rt;
if (host_pad_layer(&word, st, &lt, &rt)) {
switch (axis) {
case SDL_CONTROLLER_AXIS_LEFTX: return ((int)st[0] - 0x80) * 256;
case SDL_CONTROLLER_AXIS_LEFTY: return ((int)st[1] - 0x80) * 256;
case SDL_CONTROLLER_AXIS_RIGHTX: return ((int)st[2] - 0x80) * 256;
case SDL_CONTROLLER_AXIS_RIGHTY: return ((int)st[3] - 0x80) * 256;
case SDL_CONTROLLER_AXIS_TRIGGERLEFT: return (int)lt * 128;
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: return (int)rt * 128;
default: return 0;
}
return 1;
}
if (!SDL_GameControllerGetButton(h, SDL_CONTROLLER_BUTTON_BACK))
SDL_GameController* h = g_players[0].handle;
if (!h || axis < 0 || axis >= SDL_CONTROLLER_AXIS_MAX) return 0;
return SDL_GameControllerGetAxis(h, (SDL_GameControllerAxis)axis);
}
static int p1_host_present(void) {
uint16_t word;
uint8_t st[4], lt, rt;
return host_pad_layer(&word, st, &lt, &rt) || g_players[0].handle;
}
/* Whether the title's direct allowance for `shortcut` is in force. Rewind's
* needs Rewind enabled and not blocked by the title; otherwise its button is
* neither claimed nor a one-button shortcut and reaches the guest pad. */
static int direct_shortcut_active(int shortcut) {
if (shortcut < 0 || shortcut >= 32) return 0;
return psx_hotkey_direct_active(
g_direct_shortcut_allowed, shortcut,
shortcut == PSX_ASSIST_BIND_REWIND, g_rewind_enabled,
psx_rewind_title_blocked());
}
/* Whether host shortcut `shortcut` (PSX_ASSIST_BIND_*) bound to `binding` is
* held. A one-button combination is direct only while the title allows that
* shortcut; otherwise it is Select + button, like the legacy encoding. */
static int hotkey_shortcut_down(int shortcut, int binding) {
if (!p1_host_present() || binding == 0)
return 0;
if (PSX_HOTKEY_PAD_IS_BUTTON(binding)) {
int code = PSX_HOTKEY_PAD_BUTTON_CODE(binding);
if (code < 0 || code >= SDL_CONTROLLER_BUTTON_MAX)
return 0;
return SDL_GameControllerGetButton(h, (SDL_GameControllerButton)code) != 0;
const int direct = direct_shortcut_active(shortcut);
return psx_hotkey_pad_down(binding, direct, SDL_CONTROLLER_BUTTON_BACK,
p1_host_button_down, p1_host_axis_value,
nullptr);
}
/* Per frame, before P1 is sampled: the SDL buttons claimed by allowed direct
* shortcuts leave the guest pad (claimed, and through release once claimed
* while held). Rewind claims its button only while Rewind is enabled and the
* title has not blocked it. */
static void direct_shortcut_claim_update(void) {
const int bindings[PSX_ASSIST_BIND_COUNT] = {
g_hotkey_pad_rewind, g_hotkey_pad_save_state_menu,
g_hotkey_pad_fast_forward, g_hotkey_pad_fast_forward_toggle,
};
uint32_t claimed = 0, held = 0;
for (int i = 0; i < PSX_ASSIST_BIND_COUNT; ++i) {
if (!direct_shortcut_active(i)) continue;
const int b = psx_hotkey_pad_single_button(bindings[i]);
if (b >= 0) claimed |= 1u << b;
}
if (PSX_HOTKEY_PAD_IS_AXIS(binding)) {
int code = PSX_HOTKEY_PAD_AXIS_CODE(binding);
Sint16 v;
if (code < 0 || code >= SDL_CONTROLLER_AXIS_MAX)
return 0;
v = SDL_GameControllerGetAxis(h, (SDL_GameControllerAxis)code);
return PSX_HOTKEY_PAD_AXIS_POSITIVE(binding)
? (v > 16000)
: (v < -16000);
for (int code = 0; code < SDL_CONTROLLER_BUTTON_MAX && code < 32; ++code)
if (p1_host_button_down(nullptr, code)) held |= 1u << code;
g_p1_claimed_buttons =
psx_hotkey_claim_update(&g_p1_claim_latched, claimed, held);
}
static int* hotkey_pad_binding_slot(int shortcut) {
switch (shortcut) {
case PSX_ASSIST_BIND_REWIND: return &g_hotkey_pad_rewind;
case PSX_ASSIST_BIND_SAVE_STATE_MENU: return &g_hotkey_pad_save_state_menu;
case PSX_ASSIST_BIND_FAST_FORWARD: return &g_hotkey_pad_fast_forward;
case PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE: return &g_hotkey_pad_fast_forward_toggle;
default: return nullptr;
}
return 0;
}
/* game.toml [controller] direct_shortcut(_button): that shortcut's default
* becomes the one-button combination (saved settings still win) and the
* launcher captures it as one button. */
static void apply_title_direct_shortcut(const std::string& shortcut,
const std::string& button) {
static const char* const kNames[PSX_ASSIST_BIND_COUNT] = {
"rewind", "save_state_menu", "fast_forward", "fast_forward_toggle",
};
int index = -1;
for (int i = 0; i < PSX_ASSIST_BIND_COUNT; ++i)
if (shortcut == kNames[i]) index = i;
const int code = (int)SDL_GameControllerGetButtonFromString(button.c_str());
if (index < 0 || code < 0 || code >= SDL_CONTROLLER_BUTTON_MAX || code >= 32) {
std::fprintf(stderr,
"psxrecomp: [controller] direct_shortcut '%s' / button '%s' "
"not recognised; ignored\n", shortcut.c_str(), button.c_str());
return;
}
g_title_direct_shortcut = index;
g_title_direct_button = code;
*hotkey_pad_binding_slot(index) =
PSX_HOTKEY_PAD_BUTTON_COMBO((uint32_t)1u << code);
}
static int savestate_menu_open = 0;
@@ -7192,7 +7392,7 @@ static void savestate_menu_poll_nav(uint32_t now_ms) {
cancel = 1;
}
const int toggle = hotkey_pad_binding_down(g_hotkey_pad_save_state_menu);
const int toggle = hotkey_shortcut_down(PSX_ASSIST_BIND_SAVE_STATE_MENU, g_hotkey_pad_save_state_menu);
if (savestate_menu_ignore_toggle_release) {
if (!toggle)
savestate_menu_ignore_toggle_release = 0;
@@ -7227,7 +7427,7 @@ static void savestate_menu_poll_nav(uint32_t now_ms) {
}
static int rewind_toggle_buttons_down(void) {
return hotkey_pad_binding_down(g_hotkey_pad_rewind);
return hotkey_shortcut_down(PSX_ASSIST_BIND_REWIND, g_hotkey_pad_rewind);
}
static void rewind_poll_toggle_buttons(void) {
@@ -7242,7 +7442,7 @@ static void rewind_poll_toggle_buttons(void) {
static void savestate_menu_poll_toggle_buttons(void) {
static int was_down;
int down = hotkey_pad_binding_down(g_hotkey_pad_save_state_menu);
int down = hotkey_shortcut_down(PSX_ASSIST_BIND_SAVE_STATE_MENU, g_hotkey_pad_save_state_menu);
if (down && !was_down && !psx_rewind_is_open())
savestate_menu_toggle(0);
was_down = down;
@@ -7271,7 +7471,7 @@ static void fast_forward_toggle_flip(void) {
static void fast_forward_toggle_poll_buttons(void) {
static int was_down;
int down = hotkey_pad_binding_down(g_hotkey_pad_fast_forward_toggle);
int down = hotkey_shortcut_down(PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE, g_hotkey_pad_fast_forward_toggle);
if (down && !was_down)
fast_forward_toggle_flip();
was_down = down;
@@ -7289,6 +7489,12 @@ static void rewind_poll_nav(uint32_t now_ms) {
/* Honor remapped Cross/Circle (and Select/R3) via the same pad path as
* gameplay — GameController A/B alone miss keyboard-as-pad and remaps. */
uint16_t btn = pad_buttons_for(g_players[0], 1, true);
{
uint16_t host_word;
uint8_t host_st[4], host_lt, host_rt;
if (host_pad_layer(&host_word, host_st, &host_lt, &host_rt))
btn = host_word;
}
if ((btn & PAD_LEFT) == 0)
left = 1;
if ((btn & PAD_RIGHT) == 0)
@@ -7345,6 +7551,17 @@ static void rewind_host_pause_loop(void) {
psx_local_mouse_reset();
freeze_heartbeat_set_paused(1);
while (psx_rewind_is_open()) {
#ifndef PSX_NO_DEBUG_TOOLS
/* The guest is frozen; keep the debug endpoint live so injected
* input, screenshots and status reads can drive the filmstrip. */
debug_server_poll();
#endif
if (g_headless) {
rewind_poll_nav((uint32_t)SDL_GetTicks());
starvation_watchdog_heartbeat();
SDL_Delay(1);
continue;
}
SDL_Event ev;
while (SDL_PollEvent(&ev)) {
(void)psx_local_mouse_event(ev);
@@ -7794,6 +8011,9 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
&g_runtime_perf.provider_poll_ticks);
}
/* Direct-shortcut claims are settled before any shortcut poll or pad
* sample this frame (no-op unless a title allowed a direct shortcut). */
direct_shortcut_claim_update();
if (!g_headless) {
/* Pump SDL events to prevent window freeze. */
psx_local_mouse_begin(sdl_window, local_mouse_live(override));
@@ -7807,6 +8027,15 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
savestate_menu_host_pause_loop();
if (psx_rewind_is_open())
rewind_host_pause_loop();
} else if (host_pad_layer(nullptr, nullptr, nullptr, nullptr)) {
/* Headless with the debug host layer: the host shortcuts that need no
* window (Rewind capture/open, fast-forward latch) poll as they would
* windowed, so scripted runs can drive them. */
rewind_poll_toggle_buttons();
fast_forward_toggle_poll_buttons();
psx_rewind_note_frame();
if (psx_rewind_is_open())
rewind_host_pause_loop();
}
/* Sample each player's device and feed the matching SIO pad slot.
@@ -8042,7 +8271,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
const bool kb_turbo = host_hotkey_input_focused() &&
host_keymap_down(HOST_KEYMAP_TURBO, host_keys, (int)SDL_GetModState());
if (kb_turbo || g_manual_turbo_latched ||
hotkey_pad_binding_down(g_hotkey_pad_fast_forward)) {
hotkey_shortcut_down(PSX_ASSIST_BIND_FAST_FORWARD, g_hotkey_pad_fast_forward)) {
const int mult = manual_fast_forward_multiplier();
const int present_every = (mult < 0) ? 4 : (mult <= 4 ? 2 : 4);
manual_turbo_active = true;
@@ -13797,6 +14026,24 @@ namespace {
gi->settings_bindings = 1;
gi->assist_binding_labels = kPsxHostShortcutLabels;
gi->assist_binding_count = PSX_ASSIST_BIND_COUNT;
#if defined(RECOMP_LAUNCHER_HAS_DIRECT_ASSIST_BIND)
/* Title-declared direct shortcut: captured as one button, defaulted
* to the title's button; the other shortcuts keep their defaults. */
if (g_title_direct_shortcut >= 0) {
/* recomp-ui copies RECOMP_LAUNCHER_MAX_ASSIST_BINDINGS entries. */
static int pad_defaults[RECOMP_LAUNCHER_MAX_ASSIST_BINDINGS];
static const int key_defaults[RECOMP_LAUNCHER_MAX_ASSIST_BINDINGS] = {};
pad_defaults[PSX_ASSIST_BIND_REWIND] = PSX_HOTKEY_PAD_SELECT_R3;
pad_defaults[PSX_ASSIST_BIND_SAVE_STATE_MENU] = PSX_HOTKEY_PAD_SELECT_R1;
pad_defaults[PSX_ASSIST_BIND_FAST_FORWARD] = PSX_HOTKEY_PAD_SELECT_L1;
pad_defaults[PSX_ASSIST_BIND_FAST_FORWARD_TOGGLE] = 0;
pad_defaults[g_title_direct_shortcut] = PSX_HOTKEY_PAD_BUTTON_COMBO(
(uint32_t)1u << g_title_direct_button);
gi->assist_default_pad_bind = pad_defaults;
gi->assist_default_key_bind = key_defaults;
gi->assist_direct_pad_bind_action = g_title_direct_shortcut + 1;
}
#endif
gi->has_skip_fmv = skip_fmv_offered_b ? 1 : 0;
gi->has_turbo_loads = turbo_loads_offered_b ? 1 : 0;
/* The Perspective textures row. Hidden for a title that ships PGXP
@@ -14615,6 +14862,9 @@ int main(int argc, char** argv) {
g_auto_skip_fmv = gc.runtime.video_auto_skip_fmv ? 1 : 0;
/* [controller] game-declared input defaults (settings.toml/launcher
* still override below). */
if (!gc.runtime.direct_shortcut.empty())
apply_title_direct_shortcut(gc.runtime.direct_shortcut,
gc.runtime.direct_shortcut_button);
if (gc.runtime.has_default_mode) {
for (int i = 0; i < PSX_MAX_PLAYERS; ++i) {
player_mode[i] = (i == 0) ? gc.runtime.default_p1_mode
@@ -16349,6 +16599,9 @@ int main(int argc, char** argv) {
g_mod_controller_mode_override.fill(-1);
mod_controller_source_reset();
mod_pad_transform_reset();
g_direct_shortcut_allowed = 0;
g_p1_claimed_buttons = 0;
psx_rewind_set_title_blocked(0);
for (auto& policy : g_mod_controller_policy)
policy = ModControllerPresentationPolicy{};
g_mod_load_wall_multiplier = -1;
+34 -2
View File
@@ -100,8 +100,19 @@ static const uint8_t FONT8[59][8] = {
};
#endif
/* Title block (psx_mod_set_rewind_blocked): set by the running title while
* a mode it cannot rewind is live, cleared at every mod session start. */
static int s_title_blocked;
int psx_rewind_title_blocked(void)
{
return s_title_blocked;
}
#if !defined(PSX_HAS_RBENGINE_SNAP)
void psx_rewind_set_title_blocked(int blocked) { s_title_blocked = blocked ? 1 : 0; }
uint32_t psx_rewind_snap_count(void) { return 0; }
void psx_rewind_set_depth(uint32_t depth) { (void)depth; }
void psx_rewind_set_interval(uint32_t interval) { (void)interval; }
void psx_rewind_set_enabled(int enabled) { (void)enabled; }
@@ -462,6 +473,11 @@ int psx_rewind_is_open(void)
return s_open;
}
uint32_t psx_rewind_snap_count(void)
{
return s_count;
}
int psx_rewind_needs_present(void)
{
return s_open || s_slide > 0.01f || s_anim_dir != 0;
@@ -470,7 +486,8 @@ int psx_rewind_needs_present(void)
void psx_rewind_note_frame(void)
{
uint32_t iv;
if (!psx_rewind_enabled() || s_open || psx_netplay_active())
if (!psx_rewind_enabled() || s_open || psx_netplay_active() ||
s_title_blocked)
return;
s_frame++;
iv = rewind_capture_interval();
@@ -552,7 +569,7 @@ void psx_rewind_poll(CPUState *cpu, uint32_t resume_pc)
(void)do_load(cpu, tick);
return;
}
if (s_capture_due && !s_open && !psx_netplay_active())
if (s_capture_due && !s_open && !psx_netplay_active() && !s_title_blocked)
(void)do_capture(cpu, resume_pc);
}
@@ -569,6 +586,10 @@ int psx_rewind_toggle(void)
host_osd_push("Rewind off during netplay", 1500);
return 0;
}
if (s_title_blocked && !s_open) {
host_osd_push("Rewind is off in this mode", 1500);
return 0;
}
if (s_open) {
s_open = 0;
s_anim_dir = -1;
@@ -588,6 +609,17 @@ int psx_rewind_toggle(void)
return 1;
}
void psx_rewind_set_title_blocked(int blocked)
{
s_title_blocked = blocked ? 1 : 0;
if (!s_title_blocked)
return;
/* Captures stop here; one already scheduled is dropped so nothing from
* the blocked mode lands in history. An open panel closes unloaded. */
s_capture_due = 0;
(void)psx_rewind_cancel();
}
int psx_rewind_cancel(void)
{
if (!s_open)
+88
View File
@@ -0,0 +1,88 @@
/* Host shortcut binding semantics: legacy Select chords, multi-button combos,
* one-button combos that are direct only when the title allows it, and the
* claim-through-release mask for direct shortcut buttons. */
#include <stdio.h>
#include "psx_hotkey_pad.h"
#define CHECK(expr) do { if (!(expr)) { \
fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #expr); return 1; } } while (0)
enum { A = 0, B = 1, BACK = 4, Y = 3, RS = 8, LEFTX = 0 };
static uint32_t held;
static int axis;
static int down(void *c, int code) { (void)c; return (held >> code) & 1u; }
static int axis_value(void *c, int a) { (void)c; return a == LEFTX ? axis : 0; }
static int combo(uint32_t mask) { return PSX_HOTKEY_PAD_COMBO_BASE + (int)mask; }
static int pressed(int binding, int direct) {
return psx_hotkey_pad_down(binding, direct, BACK, down, axis_value, NULL);
}
int main(void) {
CHECK(psx_hotkey_pad_single_button(combo(1u << Y)) == Y);
CHECK(psx_hotkey_pad_single_button(combo((1u << BACK) | (1u << RS))) == -1);
CHECK(psx_hotkey_pad_single_button(1 + Y) == -1); /* legacy encoding */
CHECK(psx_hotkey_pad_single_button(combo(0)) == -1);
CHECK(psx_hotkey_pad_single_button(0) == -1);
/* One-button combination: Select + Y unless direct is allowed. */
held = 1u << Y;
CHECK(!pressed(combo(1u << Y), 0));
CHECK(pressed(combo(1u << Y), 1));
held = (1u << Y) | (1u << BACK);
CHECK(pressed(combo(1u << Y), 0));
/* Multi-button combination: all held, direct flag irrelevant. */
held = (1u << BACK) | (1u << RS);
CHECK(pressed(combo((1u << BACK) | (1u << RS)), 0));
held = 1u << RS;
CHECK(!pressed(combo((1u << BACK) | (1u << RS)), 1));
/* Legacy one-button and axis encodings always chord with Select. */
held = 1u << Y;
CHECK(!pressed(1 + Y, 1));
held |= 1u << BACK;
CHECK(pressed(1 + Y, 0));
axis = -20000;
CHECK(pressed(100 + LEFTX * 2 + 0, 0));
CHECK(!pressed(100 + LEFTX * 2 + 1, 0));
held = 0;
CHECK(!pressed(100 + LEFTX * 2 + 0, 1));
CHECK(!pressed(0, 1));
/* Claims: a claimed button is suppressed; once claimed while held it
* stays suppressed through release even after the claim ends. */
{
uint32_t latched = 0;
CHECK(psx_hotkey_claim_update(&latched, 1u << Y, 0) == (1u << Y));
CHECK(psx_hotkey_claim_update(&latched, 1u << Y, 1u << Y) == (1u << Y));
CHECK(psx_hotkey_claim_update(&latched, 0, 1u << Y) == (1u << Y));
CHECK(psx_hotkey_claim_update(&latched, 0, 1u << Y) == (1u << Y));
CHECK(psx_hotkey_claim_update(&latched, 0, 0) == 0);
CHECK(psx_hotkey_claim_update(&latched, 0, 1u << Y) == 0); /* fresh press */
CHECK(psx_hotkey_claim_update(&latched, 0, 0) == 0 && latched == 0);
}
/* Direct allowance in force: title-allowed, and for Rewind only while
* Rewind is enabled and the title has not blocked it. */
{
const uint32_t allowed = 1u << 0; /* shortcut 0 (Rewind) */
CHECK(psx_hotkey_direct_active(allowed, 0, 1, 1, 0));
CHECK(!psx_hotkey_direct_active(allowed, 0, 1, 0, 0)); /* disabled */
CHECK(!psx_hotkey_direct_active(allowed, 0, 1, 1, 1)); /* blocked */
CHECK(!psx_hotkey_direct_active(0, 0, 1, 1, 0)); /* not allowed */
CHECK(psx_hotkey_direct_active(1u << 2, 2, 0, 0, 1)); /* not Rewind */
CHECK(!psx_hotkey_direct_active(~0u, -1, 0, 1, 0));
CHECK(!psx_hotkey_direct_active(~0u, 32, 0, 1, 0));
/* Blocked mid-press: the claim stops but the held button stays
* suppressed until release, then reaches the guest. */
uint32_t latched = 0;
CHECK(psx_hotkey_claim_update(&latched, 1u << Y, 1u << Y) == (1u << Y));
CHECK(psx_hotkey_claim_update(&latched, 0, 1u << Y) == (1u << Y));
CHECK(psx_hotkey_claim_update(&latched, 0, 0) == 0);
CHECK(psx_hotkey_claim_update(&latched, 0, 1u << Y) == 0);
/* Not in force: a one-button binding needs Select, like legacy. */
held = 1u << Y;
CHECK(!pressed(combo(1u << Y), psx_hotkey_direct_active(allowed, 0, 1, 1, 1)));
}
fprintf(stderr, "psx_hotkey_pad: passed\n");
return 0;
}
+16 -4
View File
@@ -11,9 +11,9 @@ assert "static int g_hotkey_pad_rewind = 1272;" in MAIN
assert "static int g_hotkey_pad_save_state_menu = 2040;" in MAIN
assert "PSX_HOTKEY_PAD_IS_BUTTON_COMBO" in MAIN
assert "PSX_HOTKEY_PAD_SELECT_R3" in MAIN
assert "static int hotkey_pad_binding_down(int binding)" in MAIN
assert "SDL_GameControllerGetButton(h, SDL_CONTROLLER_BUTTON_BACK)" in MAIN
assert "return hotkey_pad_binding_down(g_hotkey_pad_rewind);" in MAIN
assert "static int hotkey_shortcut_down(int shortcut, int binding)" in MAIN
assert "psx_hotkey_pad_down(binding, direct, SDL_CONTROLLER_BUTTON_BACK," in MAIN
assert "return hotkey_shortcut_down(PSX_ASSIST_BIND_REWIND, g_hotkey_pad_rewind);" in MAIN
assert "(btn & PAD_SELECT) == 0 && (btn & PAD_L3) == 0" not in MAIN
assert (
"SDL_GameControllerGetButton(h, SDL_CONTROLLER_BUTTON_BACK) &&\n"
@@ -47,7 +47,7 @@ assert "g_frame_period_ms / (double)mult" in MAIN
assert "static int g_hotkey_pad_fast_forward = 1528;" in MAIN
assert "PSX_HOTKEY_PAD_SELECT_L1" in MAIN
assert "PSX_ASSIST_BIND_FAST_FORWARD" in MAIN
assert " hotkey_pad_binding_down(g_hotkey_pad_fast_forward)) {" in MAIN
assert "hotkey_shortcut_down(PSX_ASSIST_BIND_FAST_FORWARD, g_hotkey_pad_fast_forward)) {" in MAIN
assert MAIN.count("ls.assist_pad_bind[PSX_ASSIST_BIND_FAST_FORWARD]") == \
MAIN.count("ls.assist_pad_bind[PSX_ASSIST_BIND_SAVE_STATE_MENU]")
assert '"Fast-forward",' in MAIN
@@ -83,3 +83,15 @@ assert "s_actions[HOST_KEYMAP_TURBO].count == 0" not in KEYMAP_C
assert "if (want_default(HOST_KEYMAP_TURBO))" in KEYMAP_C
print("host shortcut guard passed")
# Title Rewind block (psx_mod_set_rewind_blocked): refuses opening, stops
# capture, drops the direct Rewind claim, and clears at every mod session.
REW = (ROOT / "runtime" / "src" / "psx_rewind.c").read_text(encoding="utf-8")
assert 'extern "C" void psx_mod_set_rewind_blocked(int blocked) {' in MAIN
assert " psx_rewind_set_title_blocked(0);\n" in MAIN
assert "if (!direct_shortcut_active(i)) continue;" in MAIN
assert "const int direct = direct_shortcut_active(shortcut);" in MAIN
assert "psx_rewind_title_blocked());" in MAIN
assert " if (!psx_rewind_enabled() || s_open || psx_netplay_active() ||\n s_title_blocked)\n return;" in REW
assert "!psx_netplay_active() && !s_title_blocked)\n (void)do_capture" in REW
assert 'host_osd_push("Rewind is off in this mode", 1500);' in REW