mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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` | | ✓ | |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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, <, &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, <, &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, <, &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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user