From dd64bc661139e030caa5e53622f1fdb5f4e09a5e Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 2 Oct 2026 16:19:30 -0700 Subject: [PATCH] TCP input injection for port 2 (set_input/press/clear_input port=2) Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 66cc81fe63dcd0da8602ad5e3705dcb9271dc2b5) --- runtime/include/debug_server.h | 2 ++ runtime/src/debug_server.c | 35 +++++++++++++++++++++++++++++++++- runtime/src/main.cpp | 16 ++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/runtime/include/debug_server.h b/runtime/include/debug_server.h index 1c78ed0c..832f9d6d 100644 --- a/runtime/include/debug_server.h +++ b/runtime/include/debug_server.h @@ -198,6 +198,8 @@ void debug_server_check_watchpoints(void); /* Returns >= 0 if the debug server wants to override pad input, * -1 if no override is active. Value is PS1 16-bit button mask. */ int debug_server_get_input_override(void); +/* Port-2 override word (set_input/press "port":2), or -1. */ +int debug_server_get_input_override_port2(void); /* Optional analog-stick override set alongside set_input (lx/ly/rx/ry, * 0..255, 0x80 = centre). Returns 1 and fills st[lx,ly,rx,ry] when armed, diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index a09352e8..b54297c3 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -286,6 +286,10 @@ static uint64_t s_dirty_break_hits = 0; /* ---- Input override ---- */ static int s_input_override = -1; static int s_input_frames = 0; +/* Port 2 (set_input / press / clear_input "port":2): digital buttons only, + * for driving two-player modes headless. */ +static int s_input_override_p2 = -1; +static int s_input_frames_p2 = 0; /* Optional analog-stick override (set_input lx/ly/rx/ry, 0..255, 0x80 = * centre). Lets injected input drive analog-mode movement; consumed by the * pad sampler alongside the button word. */ @@ -8004,6 +8008,12 @@ static void handle_set_input(int id, const char *json) if (pad_type < -1 || pad_type > 2) { send_err(id, "pad_type must be -1 (automatic), 0, 1, or 2"); return; } + if (json_get_int(json, "port", 1) == 2) { + s_input_override_p2 = (int)(hex_to_u32(val_str) & 0xFFFFu); + s_input_frames_p2 = 0; + send_ok(id); + return; + } s_pad_type_override = pad_type; s_input_override = (int)hex_to_u32(val_str); s_input_frames = 0; @@ -8044,6 +8054,12 @@ static void handle_press(int id, const char *json) if (pad_type < -1 || pad_type > 2) { send_err(id, "pad_type must be -1 (automatic), 0, 1, or 2"); return; } + if (json_get_int(json, "port", 1) == 2) { + s_input_override_p2 = buttons & 0xFFFF; + s_input_frames_p2 = frames; + send_ok(id); + return; + } s_pad_type_override = pad_type; s_input_override = buttons; s_input_frames = frames; @@ -8095,6 +8111,7 @@ 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_p2\":%d,\"override_p2_frames\":%d," "\"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", @@ -8108,6 +8125,7 @@ static void handle_pad_status(int id, const char *json) sticks1[0], sticks1[1], sticks1[2], sticks1[3], 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_input_override_p2, s_input_frames_p2, s_axis_st[0], s_axis_st[1], s_axis_st[2], s_axis_st[3], s_axis_override ? "true" : "false", s_host_layer ? "true" : "false", s_host_buttons, @@ -8117,7 +8135,12 @@ static void handle_pad_status(int id, const char *json) static void handle_clear_input(int id, const char *json) { - (void)json; + const int port = json_get_int(json, "port", 0); /* 0 = both */ + if (port == 0 || port == 2) { + s_input_override_p2 = -1; + s_input_frames_p2 = 0; + } + if (port == 2) { send_ok(id); return; } s_input_route_active = 0; s_input_route_index = 0; s_input_route_remaining = 0; @@ -16718,6 +16741,16 @@ int debug_server_get_input_override(void) return current; } +int debug_server_get_input_override_port2(void) +{ + int current = s_input_override_p2; + if (s_input_override_p2 >= 0 && s_input_frames_p2 > 0) { + if (--s_input_frames_p2 == 0) + s_input_override_p2 = -1; + } + return current; +} + int debug_server_get_axis_override(unsigned char st[4]) { if (!s_axis_override) return 0; diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index de089dab..817d83d5 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -6878,6 +6878,18 @@ static PadExtHooks pad_ext_main_hooks(void) { return h; } +/* TCP port-2 injection (set_input/press "port":2): a digital pad plugged + * into port 2 for as long as a test drives it, so headless runs can reach and + * play two-player modes. Applied after the normal sampling so it wins. */ +static void apply_input_override_port2(int override_word) { + if (override_word < 0) return; + if (!sio_get_pad_connected(1)) { + sio_set_pad_connected(1, 1); + sio_set_pad_analog(1, 0, 0x80, 0x80, 0x80, 0x80); + } + sio_set_pad_state_slot(1, (uint16_t)override_word); +} + static void sample_pad_into_sio(int override) { /* Selfcheck fighter mash owns P1 when enabled (headless-safe). */ if (override < 0) { @@ -8098,12 +8110,14 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { /* Check debug server input override. */ int override = debug_server_get_input_override(); + int override_p2 = debug_server_get_input_override_port2(); #else /* Production: skip debug server. Still need to advance frame counter * locally so anything else that reads it continues to work. */ extern uint64_t s_frame_count; s_frame_count++; int override = -1; + int override_p2 = -1; #endif psx_local_mouse_begin(sdl_window, local_mouse_live(override)); @@ -8299,6 +8313,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { sample_headless_pad_into_sio(override); else sample_pad_into_sio(override); + apply_input_override_port2(override_p2); } /* Offline vblank boundary: record/replay/compare (PSX_RB_SELFCHECK). * Defer opening a window while multitap arming is still pending — @@ -8629,6 +8644,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { SDL_PumpEvents(); // retain native timing when no policy exists } sample_pad_into_sio(override); + apply_input_override_port2(override_p2); latency_ring_restamp_input(); } }