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 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-05 13:11:51 -07:00
parent 3358f81e25
commit f3801ba10b
5 changed files with 93 additions and 0 deletions
+8
View File
@@ -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)
+8
View File
@@ -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.
+44
View File
@@ -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) {
+17
View File
@@ -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
@@ -0,0 +1,16 @@
#include "mod_local_input_policy.h"
#include <stdio.h>
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;
}