From f3801ba10b2f821166f8a75a3d07f8ea0d6beedf Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:11:51 -0700 Subject: [PATCH] mods: read local host sticks for presentation-only plugins psx_mod_read_local_pad_sticks(player, out[4]) returns the mapped local stick axes (lx, ly, rx, ry; 0..255, centre 128) without touching the SIO sample, so a presentation plugin (a look-around camera) can read the right stick even when the game selected a digital pad. It returns 0 and centred axes for an invalid or disconnected player, in netplay, and during rollback resimulation; peer input is never presentation input. The debug-server axis override feeds it for deterministic tests. Co-Authored-By: Claude Opus 5.5 --- runtime/CMakeLists.txt | 8 ++++ runtime/include/mod_plugins.h | 8 ++++ runtime/src/main.cpp | 44 ++++++++++++++++++++++ runtime/src/mod_local_input_policy.h | 17 +++++++++ runtime/tests/test_mod_local_input_scope.c | 16 ++++++++ 5 files changed, 93 insertions(+) create mode 100644 runtime/src/mod_local_input_policy.h create mode 100644 runtime/tests/test_mod_local_input_scope.c diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index f057641c..6bccba11 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -207,6 +207,14 @@ if(BUILD_TESTING) add_test(NAME launcher_pad_mode_resolve_test COMMAND launcher_pad_mode_resolve_test) + # Presentation-only mod input must not be sourced from disconnected, + # peer-synchronized, or rollback-replayed pad state. + add_executable(mod_local_input_scope_test + tests/test_mod_local_input_scope.c) + target_include_directories(mod_local_input_scope_test PRIVATE src) + add_test(NAME mod_local_input_scope_test + COMMAND mod_local_input_scope_test) + # COP0.CAUSE IP2 must mirror the INTC line combinationally, never latch. add_executable(cause_ip2_combinational_test tests/test_cause_ip2_combinational.c) diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 948ae4c9..3adff5ce 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -627,6 +627,14 @@ typedef struct PSXModControllerInput { } PSXModControllerInput; typedef uint32_t (*PSXModControllerPresentationCallback)( const PSXModControllerInput* input); +/* Read-only snapshot of the local host stick state for an emulation-thread + * presentation callback. Values are ordered lx, ly, rx, ry and use the + * DualShock byte range (0..255, centered at 128). This reads mapped local + * controller axes independently of the guest's current digital/analog SIO + * mode and never changes the simulation's controller sample. Returns 0 and + * centers the output for invalid/disconnected input, netplay, or rollback + * resimulation; presentation state must never follow synchronized peer input. */ +int psx_mod_read_local_pad_sticks(uint32_t player, uint8_t out[4]); /* * Override one player's resolved controller presentation mode for this launch. * This is intentionally a trusted-plugin API, not a generic launcher setting. diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index ef8ed927..d183bc53 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -94,6 +94,7 @@ extern "C" void psx_event_step_conservative_env_init(void); #include "launcher_device.h" #include "game_options.h" #include "mod_plugins.h" +#include "mod_local_input_policy.h" #include "mod_session_baseline.h" #include "mod_runtime.h" #include "mod_packages.h" @@ -5646,6 +5647,49 @@ static void apply_input_override_to_sio(int override_word) { (uint8_t)(eff_analog ? 1 : 0)); } +/* Local host sticks for presentation-only mods (mod_plugins.h). */ +extern "C" int psx_mod_read_local_pad_sticks(uint32_t player, + uint8_t out[4]) { + if (!out) return 0; + out[0] = out[1] = out[2] = out[3] = 0x80; + + const bool player_valid = player < PSX_MAX_PLAYERS; + const bool netplay = psx_netplay_active() != 0; + const bool resimulating = psx_netplay_is_resimulating() != 0; + if (!player_valid || netplay || resimulating) return 0; + + PlayerInput& p = g_players[player]; + const bool dev_here = player == 0 && dev_any_input_enabled(); + const bool attached = p.kind == 1 || (p.kind == 2 && p.handle); + if (!psx_mod_local_input_available(player_valid, attached || dev_here, + netplay, resimulating)) + return 0; + +#ifndef PSX_NO_DEBUG_TOOLS + /* The local debug injector supplies a deterministic host-side sample for + * camera/input tests. It is never sourced from rollback or a peer. */ + if (player == 0) { + uint8_t injected[4]; + if (debug_server_get_axis_override(injected)) { + std::memcpy(out, injected, sizeof(injected)); + return 1; + } + } +#endif + + if (attached) { + /* Read the host pad mapping directly. The SIO-facing mode can be + * digital because the game selected a digital pad; presentation mods + * still need local axes without changing that simulation state. */ + pad_sticks_for(p, (int)player + 1, out); + return 1; + } + const Uint8* keys = SDL_GetKeyboardState(NULL); + psx_keybinds_sticks(keys, (int)player + 1, out); + dev_any_controller_sticks(out); + return 1; +} + /* Capture one SIO slot's host pad into a netplay/local blob. Returns 1 if a * device (or dev-any P1) is present; 0 leaves *out as released/disconnected. */ static int capture_pad_slot(int s, PsxNetPad* out) { diff --git a/runtime/src/mod_local_input_policy.h b/runtime/src/mod_local_input_policy.h new file mode 100644 index 00000000..63b7a991 --- /dev/null +++ b/runtime/src/mod_local_input_policy.h @@ -0,0 +1,17 @@ +#ifndef PSXRECOMP_MOD_LOCAL_INPUT_POLICY_H +#define PSXRECOMP_MOD_LOCAL_INPUT_POLICY_H + +/* Local presentation plugins may inspect live host input only while its + * controller is available during ordinary offline execution. Guest pad mode + * is deliberately absent: a game selecting digital SIO does not make its + * locally attached analog stick unavailable to presentation code. Peer input + * and rollback replay remain simulation state, never presentation input. */ +static inline int psx_mod_local_input_available(int player_valid, + int connected, + int netplay_active, + int resimulating) { + return player_valid && connected && + !netplay_active && !resimulating; +} + +#endif diff --git a/runtime/tests/test_mod_local_input_scope.c b/runtime/tests/test_mod_local_input_scope.c new file mode 100644 index 00000000..5677bcb5 --- /dev/null +++ b/runtime/tests/test_mod_local_input_scope.c @@ -0,0 +1,16 @@ +#include "mod_local_input_policy.h" + +#include + +static int failures; +#define CHECK(cond) do { if (!(cond)) { fprintf(stderr, "FAIL: %s\n", #cond); failures++; } } while (0) + +int main(void) { + /* Digital guest pad mode does not gate local presentation input. */ + CHECK(psx_mod_local_input_available(1, 1, 0, 0)); + CHECK(!psx_mod_local_input_available(0, 1, 0, 0)); + CHECK(!psx_mod_local_input_available(1, 0, 0, 0)); + CHECK(!psx_mod_local_input_available(1, 1, 1, 0)); + CHECK(!psx_mod_local_input_available(1, 1, 0, 1)); + return failures ? 1 : 0; +}