multitap enhancements

This commit is contained in:
Alex Vanderveen
2026-08-07 10:19:12 -04:00
parent 33c9901497
commit 4cdccf58c1
9 changed files with 334 additions and 33 deletions
+20 -3
View File
@@ -103,9 +103,26 @@ Guests rearrange among seats **1..N−1**.
| Library / lobby / UI ceiling | **8** seats (`RNET_MAX_SLOTS`, dual SCPH-1070) |
| Per-title cap | `game.toml` `players` / `PSX_MAX_PLAYERS` (e.g. MotK=2, Bomberman Party=5) |
Rollback and delay-sync both carry multitap pad bytes. Offline and netplay
share the same multitap arming rules (game-started gate, `multitap_port` in
`game.toml`). Empty tap seats are fine — not every slot needs a device.
**Offline:** recomp-ui exposes a Multitap checkbox on PSX titles with
`num_players > 4`. Off hides controller seats beyond Player 4 and caps
`g_offline_pad_count` at 4 (`settings.toml` `[controller] multitap`,
default on). Multitap still arms at game-start when three or more offline
seats are live (`multitap_port` from `game.toml`).
**Multitap analog (hack):** tap seats are plain digital by default. Opt in
with `game.toml` / `settings.toml` `[controller] multitap_analog = true`
(or the Controllers / Lobby Settings checkbox). When on, tap seats may
report DualShock (`0x73` + sticks) in multitap bulk status. Hosts publish
`match_caps.multitap_analog` so every peer applies the same setting at
launch. Not reliable across titles — leave off unless the game needs it.
**Netplay (psxrecomp only):** lobbies with more than **2** seats always
force SCPH-1070 multitap on (`force_session_pads_connected` /
session start when `slot_count >= 3`). The offline Multitap toggle does
not opt out of that. Empty tap seats are fine — not every slot needs a
device.
Rollback and delay-sync both carry multitap pad bytes.
---
+120 -1
View File
@@ -6,9 +6,11 @@
#include <cctype>
#include <cstdio>
#include <fstream>
#include <sstream>
#include <set>
#include <stdexcept>
#include <string>
#include <vector>
#include "bios_rom_alias.h"
#include "fmt/format.h"
@@ -696,6 +698,10 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
rt.multitap_port = static_cast<int>(n);
rt.has_multitap_port = true;
}
if (ct.contains("multitap_analog")) {
rt.multitap_analog = toml::find<bool>(ct, "multitap_analog");
rt.has_multitap_analog = true;
}
// Prefer string key; accept legacy bool alias.
if (ct.contains("legacy_pad_config")) {
rt.legacy_pad_config = toml::find<bool>(ct, "legacy_pad_config");
@@ -2314,6 +2320,14 @@ UserSettings load_user_settings(const fs::path& path) {
}
});
}
if (ct.contains("multitap")) try_get([&]{
s.multitap_enabled = toml::find<bool>(ct, "multitap");
s.has_multitap_enabled = true;
});
if (ct.contains("multitap_analog")) try_get([&]{
s.multitap_analog = toml::find<bool>(ct, "multitap_analog");
s.has_multitap_analog = true;
});
if (ct.contains("deadzone")) try_get([&]{
const auto n = toml::find<int64_t>(ct, "deadzone");
if (n >= 0 && n <= 32767) {
@@ -2429,7 +2443,8 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
}
{
bool any_ctrl = s.has_deadzone;
bool any_ctrl = s.has_deadzone || s.has_multitap_enabled ||
s.has_multitap_analog;
for (int i = 0; i < UserSettings::kMaxControllerPlayers; ++i) {
if (s.has_p_device[i] || s.has_p_mode[i] || s.has_p_deadzone[i])
any_ctrl = true;
@@ -2452,6 +2467,12 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
if (s.has_p_deadzone[i])
f << kDzKeys[i] << " = " << s.p_deadzone[i] << "\n";
}
if (s.has_multitap_enabled)
f << "multitap = " << (s.multitap_enabled ? "true" : "false")
<< "\n";
if (s.has_multitap_analog)
f << "multitap_analog = "
<< (s.multitap_analog ? "true" : "false") << "\n";
/* Keep a global deadzone= for older readers (mirrors P1). */
if (s.has_deadzone || s.has_p_deadzone[0])
f << "deadzone = "
@@ -2478,4 +2499,102 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
return f.good();
}
bool upsert_game_toml_controller_bool(const std::filesystem::path& path,
const char* key, bool value)
{
if (!key || !key[0]) return false;
std::string text;
{
std::ifstream in(path);
if (in) {
std::ostringstream ss;
ss << in.rdbuf();
text = ss.str();
}
}
const std::string val = value ? "true" : "false";
const std::string assign = std::string(key) + " = " + val;
auto is_section = [](const std::string& line) {
size_t i = 0;
while (i < line.size() && (line[i] == ' ' || line[i] == '\t')) ++i;
return i < line.size() && line[i] == '[';
};
auto section_name = [](const std::string& line) -> std::string {
size_t a = line.find('[');
size_t b = line.find(']');
if (a == std::string::npos || b == std::string::npos || b <= a + 1)
return {};
return line.substr(a + 1, b - a - 1);
};
auto key_prefix = [&](const std::string& line) {
size_t i = 0;
while (i < line.size() && (line[i] == ' ' || line[i] == '\t')) ++i;
if (i < line.size() && line[i] == '#') return false;
const std::string rest = line.substr(i);
return rest.rfind(std::string(key) + " =", 0) == 0 ||
rest.rfind(std::string(key) + "=", 0) == 0;
};
std::istringstream ls(text);
std::vector<std::string> lines;
std::string line;
while (std::getline(ls, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
lines.push_back(line);
}
int ctrl_start = -1, ctrl_end = -1;
for (int i = 0; i < (int)lines.size(); ++i) {
if (is_section(lines[i]) && section_name(lines[i]) == "controller") {
ctrl_start = i;
ctrl_end = (int)lines.size();
for (int j = i + 1; j < (int)lines.size(); ++j) {
if (is_section(lines[j])) {
ctrl_end = j;
break;
}
}
break;
}
}
if (ctrl_start < 0) {
if (!text.empty() && text.back() != '\n') text.push_back('\n');
text += "\n[controller]\n";
text += assign;
text += "\n";
} else {
int replace_at = -1;
for (int i = ctrl_start + 1; i < ctrl_end; ++i) {
if (key_prefix(lines[i])) {
replace_at = i;
break;
}
}
if (replace_at >= 0) {
lines[replace_at] = assign;
} else {
int insert_at = ctrl_end;
while (insert_at > ctrl_start + 1 &&
lines[insert_at - 1].find_first_not_of(" \t") ==
std::string::npos)
--insert_at;
lines.insert(lines.begin() + insert_at, assign);
}
std::ostringstream out;
for (size_t i = 0; i < lines.size(); ++i) {
out << lines[i];
if (i + 1 < lines.size() || (!text.empty() && text.back() == '\n'))
out << '\n';
}
text = out.str();
}
std::ofstream f(path, std::ios::binary | std::ios::trunc);
if (!f) return false;
f << text;
return f.good();
}
} // namespace PSXRecompV4
+19
View File
@@ -511,6 +511,12 @@ struct RuntimeConfig {
bool has_multitap_port = false;
int multitap_port = 1;
// multitap_analog: opt-in DualShock-on-tap hack (default false). When true,
// multitap bulk seats may report 0x73 + stick bytes; when false (faithful),
// tap seats stay plain digital. Overridable by settings.toml / match_caps.
bool has_multitap_analog = false;
bool multitap_analog = false;
// legacy_pad_config: per-game pad-protocol compatibility opt-in. false (default)
// = the modern DualShock config state machine (proper 0x43 enter/exit, config id
// 0xF3 only while in config) — required by MMX6 and the correct default for every
@@ -1115,6 +1121,13 @@ struct UserSettings {
// Legacy global deadzone (settings.toml `deadzone=`). Applied to slots
// that do not have an explicit pN_deadzone. Default 10%.
bool has_deadzone = false; int deadzone = 3277;
// SCPH-1070 multitap for offline seats beyond Player 4 (settings.toml
// [controller] multitap). Default ON when unset. Netplay lobbies with
// more than 2 seats always arm multitap in the runtime regardless.
bool has_multitap_enabled = false; bool multitap_enabled = true;
// DualShock-on-tap hack (settings.toml [controller] multitap_analog).
// Default off when unset; game.toml [controller] multitap_analog seeds it.
bool has_multitap_analog = false; bool multitap_analog = false;
// Localization: the launcher's chosen language code (feeds RuntimeConfig
// .language / g_lang). "off"/"jp"/"" = untranslated native game. Persisted to
// settings.toml [localization].language.
@@ -1167,6 +1180,12 @@ UserSettings load_user_settings(const std::filesystem::path& path);
// Write settings.toml deterministically. Returns false on I/O failure.
bool save_user_settings(const std::filesystem::path& path, const UserSettings& s);
// Surgical upsert of `key = true|false` under [controller] in game.toml.
// Preserves comments and unrelated keys. Creates [controller] if missing.
// Used to persist launcher multitap_analog into the title game config.
bool upsert_game_toml_controller_bool(const std::filesystem::path& path,
const char* key, bool value);
// Locate the project root by walking upward from `config_path` until a
// directory containing `.gitignore`, `.git`, or `CMakeLists.txt` is found.
// Throws std::runtime_error if not found within 8 levels.
+2
View File
@@ -62,6 +62,8 @@ typedef struct PsxLobbyMatchCaps {
int force_input_relay; /* 0/1 — server input relay (vs P2P) */
int force_turn; /* 0/1 — ICE relay-only (Force TURN for UDP) */
int rollback; /* 0/1 — invent/rollback netplay (default on) */
/* DualShock-on-multitap-tap hack (0/1). Host-authoritative for the match. */
int multitap_analog;
char language[PSX_LOBBY_LANG_LEN];
} PsxLobbyMatchCaps;
+8 -2
View File
@@ -84,9 +84,15 @@ void sio_set_legacy_cfg(int enabled);
int sio_get_legacy_cfg(void);
/* 1 when multitap is armed and `logical_slot` is a tap pad (not the lone
* pad on the opposite console port). SCPH-1070 taps are treated as plain
* digital controllers (0x41) — DualShock/analog on a tap is not reliable
* across titles, so host input and SIO type requests are forced digital. */
* digital controllers (0x41) by default — DualShock/analog on a tap is not
* reliable across titles. Opt in with sio_set_multitap_analog(1) (hack). */
int sio_pad_on_multitap(int logical_slot);
/* Opt-in DualShock-on-tap hack. Off by default. When on, tap seats may
* report 0x73 + stick bytes in multitap bulk status (and host/netplay
* sampling stops forcing digital). game.toml [controller] multitap_analog /
* settings.toml / lobby match_caps may enable this. */
void sio_set_multitap_analog(int enabled);
int sio_get_multitap_analog(void);
/* Update pad button state. Buttons use PS1 convention: 0=pressed, 1=released.
Bit layout: SELECT, L3, R3, START, UP, RIGHT, DOWN, LEFT,
+134 -16
View File
@@ -329,6 +329,17 @@ struct PlayerInput {
static PlayerInput g_players[PSX_MAX_PLAYERS];
/* Offline SIO sample loop bound (from game.toml players; clamped). */
static int g_offline_pad_count = 2;
/* Offline seat ceiling from game.toml players, optionally capped at 4 when
* the launcher Multitap toggle is off (seats 5+). Netplay ignores this and
* arms multitap whenever session slot_count > 2. */
static void apply_offline_pad_count(int game_players, bool multitap_enabled)
{
int n = game_players > 0 ? game_players : 1;
if (n > PSX_MAX_PLAYERS) n = PSX_MAX_PLAYERS;
if (game_players > 4 && !multitap_enabled && n > 4)
n = 4;
g_offline_pad_count = n;
}
/* ARGB8888 staging buffer. Sized for the active internal resolution:
* 640*scale x 512*scale. Allocated once the supersampling scale is known
* (sized for the native 640x512 when supersampling is off). */
@@ -3416,10 +3427,10 @@ static int effective_player_mode(const PlayerInput& p) {
return p.mode;
}
/* Pad mode for the SIO seat this sample will drive. Multitap taps are always
* plain digital (0x41) — DualShock on a SCPH-1070 tap is not reliable. */
/* Pad mode for the SIO seat this sample will drive. Multitap taps stay digital
* unless the DualShock-on-tap hack (sio_get_multitap_analog) is armed. */
static int effective_player_mode_for_sio(const PlayerInput& p, int sio_slot) {
if (sio_pad_on_multitap(sio_slot))
if (sio_pad_on_multitap(sio_slot) && !sio_get_multitap_analog())
return (int)PSXRecompV4::PAD_MODE_DIGITAL;
return effective_player_mode(p);
}
@@ -3869,10 +3880,10 @@ static int capture_pad_slot(int s, PsxNetPad* out) {
* analog axes below. An assigned device keeps its configured mode (a
* launcher-selected analog DualShock stays analog, so its input path / SIO
* handshake cadence is preserved exactly). Keyboard is always digital.
* Multitap taps are forced digital. A P1 with no assigned device but
* dev-any-input on presents as HYBRID. */
* Multitap taps are forced digital unless multitap_analog hack is on.
* A P1 with no assigned device but dev-any-input on presents as HYBRID. */
int mode;
if (sio_pad_on_multitap(s))
if (sio_pad_on_multitap(s) && !sio_get_multitap_analog())
mode = (int)PSXRecompV4::PAD_MODE_DIGITAL;
else if (p.kind != 0) mode = effective_player_mode(p);
else if (dev_here) mode = (int)PSXRecompV4::PAD_MODE_HYBRID;
@@ -3978,7 +3989,7 @@ static int capture_pad_slot_exclusive(int s, PsxNetPad* out, int present_sio_slo
}
static void apply_pad_slot_to_sio(int s, const PsxNetPad& pad) {
if (sio_pad_on_multitap(s))
if (sio_pad_on_multitap(s) && !sio_get_multitap_analog())
sio_set_pad_config_capable(s, 0);
sio_set_pad_state_slot(s, pad.buttons);
sio_set_pad_sticks(s, pad.lx, pad.ly, pad.rx, pad.ry);
@@ -4974,16 +4985,19 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
if (g_offline_pad_count >= 3 && fntrace_is_game_started() &&
!sio_get_multitap()) {
sio_set_multitap(1);
/* Tap seats drop to plain digital as soon as the tap is live. */
/* Tap seats drop to plain digital unless multitap_analog is on. */
for (int s = 0; s < PSX_MAX_PLAYERS; ++s) {
if (!sio_pad_on_multitap(s)) continue;
if (sio_get_multitap_analog()) continue;
sio_set_pad_config_capable(s, 0);
sio_set_pad_analog(s, 0, 0x80, 0x80, 0x80, 0x80);
}
std::fprintf(stdout,
"psxrecomp: multitap armed (console Port %d; "
"tap pads forced digital)\n",
sio_get_multitap_port() + 1);
"tap pads %s)\n",
sio_get_multitap_port() + 1,
sio_get_multitap_analog() ? "analog hack on"
: "forced digital");
}
/* Solo resim self-check replay republishes the recorded rows itself —
* live sampling must not touch SIO during the replay window. */
@@ -5958,6 +5972,7 @@ namespace {
int g_lnch_force_turn = 0;
/* Lobby default on; host “Disable Rollback” clears this → delay_sync. */
int g_lnch_rollback = 1;
int g_lnch_multitap_analog = 0;
int g_lnch_host_max_slots = 2;
/* Delay-sync READY/START waits for every seat in slot_count. Use seated
@@ -7090,6 +7105,8 @@ namespace {
caps.force_input_relay = g_lnch_force_input_relay != 0;
caps.force_turn = g_lnch_force_turn != 0;
caps.rollback = g_lnch_rollback != 0;
caps.multitap_analog = g_lnch_multitap_analog != 0;
if (s) caps.multitap_analog = s->multitap_analog != 0;
return caps;
}
@@ -7109,6 +7126,8 @@ namespace {
caps.force_input_relay = g_lnch_force_input_relay != 0;
caps.force_turn = g_lnch_force_turn != 0;
caps.rollback = g_lnch_rollback != 0;
caps.multitap_analog = g_lnch_multitap_analog != 0;
if (settings) caps.multitap_analog = settings->multitap_analog != 0;
(void)psx_lobby_set_match_caps(&caps);
}
@@ -7174,6 +7193,19 @@ namespace {
ae_np_push_match_caps(nullptr);
return 0;
}
int ae_np_multitap_analog_get(void*) {
if (!g_lnch_hosting_lan && !g_lnch_joined_lan) {
const PsxLobbyMatchCaps* caps = psx_lobby_match_caps();
if (caps && caps->valid)
return caps->multitap_analog ? 1 : 0;
}
return g_lnch_multitap_analog;
}
int ae_np_multitap_analog_set(void*, int enable) {
g_lnch_multitap_analog = enable ? 1 : 0;
ae_np_push_match_caps(nullptr);
return 0;
}
int ae_np_input_prediction_get(void*) {
if (!g_lnch_hosting_lan && !g_lnch_joined_lan) {
const PsxLobbyMatchCaps* caps = psx_lobby_match_caps();
@@ -8473,6 +8505,8 @@ namespace {
ae_np_rollback_set,
ae_np_input_prediction_get,
ae_np_input_prediction_set,
ae_np_multitap_analog_get,
ae_np_multitap_analog_set,
};
} // namespace
#endif
@@ -8637,6 +8671,8 @@ int main(int argc, char** argv) {
std::filesystem::path memcard2_path; /* explicit slot-2 .mcd (empty => dir/card2.mcd) */
bool memcard1_enabled = true;
bool memcard2_enabled = true;
bool multitap_enabled = true; /* [controller] multitap; seats 5+ offline */
bool multitap_analog = false; /* DualShock-on-tap hack; game.toml/settings */
/* [controller] device routing (defaults: P1 keyboard/digital, P2 none). */
/* Dev builds default Player 1 to the first connected controller ("auto").
* PSX_DEV_INPUT=1 additionally merges every other controller and the
@@ -8717,9 +8753,7 @@ int main(int argc, char** argv) {
game_id = gc.id;
game_region = gc.region;
game_players = gc.players;
g_offline_pad_count = game_players > 0 ? game_players : 1;
if (g_offline_pad_count > PSX_MAX_PLAYERS)
g_offline_pad_count = PSX_MAX_PLAYERS;
apply_offline_pad_count(game_players, multitap_enabled);
game_has_disc_crc = gc.has_disc_crc;
game_disc_crc = gc.disc_crc;
g_netplay_disc_expect.require_cue = gc.netplay_require_cue;
@@ -8919,6 +8953,17 @@ int main(int argc, char** argv) {
"psxrecomp: multitap on console Port %d\n",
gc.runtime.multitap_port);
}
if (gc.runtime.has_multitap_analog) {
multitap_analog = gc.runtime.multitap_analog;
sio_set_multitap_analog(multitap_analog ? 1 : 0);
#if defined(RECOMP_LAUNCHER) && defined(PSX_HAS_LOBBY_CLIENT)
g_lnch_multitap_analog = multitap_analog ? 1 : 0;
#endif
if (multitap_analog)
std::fprintf(stdout,
"psxrecomp: multitap_analog hack ON "
"(DualShock on tap seats)\n");
}
/* LEGACY per-game pad-config opt-in (default modern). Only Tomba sets
* it, so its launcher Hybrid mode's analog<->digital flip doesn't make
* libpad manufacture a 1-frame "pad unplugged". sio_init() does not
@@ -9082,6 +9127,14 @@ int main(int argc, char** argv) {
if (us.has_memcard2_path) memcard2_path = us.memcard2_path;
if (us.has_memcard1_enabled) memcard1_enabled = us.memcard1_enabled;
if (us.has_memcard2_enabled) memcard2_enabled = us.memcard2_enabled;
if (us.has_multitap_enabled) multitap_enabled = us.multitap_enabled;
if (us.has_multitap_analog) {
multitap_analog = us.multitap_analog;
sio_set_multitap_analog(multitap_analog ? 1 : 0);
#if defined(RECOMP_LAUNCHER) && defined(PSX_HAS_LOBBY_CLIENT)
g_lnch_multitap_analog = multitap_analog ? 1 : 0;
#endif
}
if (us.has_language) resolved_language = us.language;
{
const int n = std::min(PSX_MAX_PLAYERS,
@@ -9093,6 +9146,7 @@ int main(int argc, char** argv) {
}
if (us.has_deadzone) resolved_deadzone = us.deadzone;
}
apply_offline_pad_count(game_players, multitap_enabled);
if (us.has_low_latency_input) g_low_latency_input = us.low_latency_input ? 1 : 0;
if (us.has_vsync) g_video_vsync = us.vsync;
if (us.has_frame_interpolation)
@@ -9489,6 +9543,8 @@ int main(int argc, char** argv) {
seed.memcard_dir = memcard_dir; seed.has_memcard_dir = true;
seed.memcard1_enabled = memcard1_enabled; seed.has_memcard1_enabled = true;
seed.memcard2_enabled = memcard2_enabled; seed.has_memcard2_enabled = true;
seed.multitap_enabled = multitap_enabled; seed.has_multitap_enabled = true;
seed.multitap_analog = multitap_analog; seed.has_multitap_analog = true;
if (!memcard1_path.empty()) { seed.memcard1_path = memcard1_path; seed.has_memcard1_path = true; }
if (!memcard2_path.empty()) { seed.memcard2_path = memcard2_path; seed.has_memcard2_path = true; }
seed.language = resolved_language; seed.has_language = true;
@@ -9613,6 +9669,13 @@ int main(int argc, char** argv) {
}
ls.memcard_enabled[0] = seed.memcard1_enabled ? 1 : 0;
ls.memcard_enabled[1] = seed.memcard2_enabled ? 1 : 0;
#if defined(RECOMP_LAUNCHER_HAS_MULTITAP_ENABLED)
ls.multitap_enabled = seed.multitap_enabled ? 1 : 0;
#endif
#if defined(RECOMP_LAUNCHER_HAS_MULTITAP_ANALOG)
ls.multitap_analog = seed.multitap_analog ? 1 : 0;
g_lnch_multitap_analog = seed.multitap_analog ? 1 : 0;
#endif
/* Region badge: game.toml [game] region wins verbatim; otherwise derive
* from the game_id serial prefix (SCUS/SLUS/LSP -> USA, SCES/SLES ->
@@ -9749,9 +9812,7 @@ int main(int argc, char** argv) {
#if defined(PSX_HAS_RECOMP_NET) && defined(PSX_HAS_LOBBY_CLIENT)
g_lnch_netplay_game_name = game_name.empty() ? "PSX" : game_name;
g_lnch_game_players = game_players;
g_offline_pad_count = game_players > 0 ? game_players : 1;
if (g_offline_pad_count > PSX_MAX_PLAYERS)
g_offline_pad_count = PSX_MAX_PLAYERS;
apply_offline_pad_count(game_players, multitap_enabled);
psx_lobby_set_max_slots(game_players);
gi.netplay_supported =
(game_players >= 2 && game_players <= PSX_MAX_PLAYERS) ? 1 : 0;
@@ -9867,6 +9928,22 @@ int main(int argc, char** argv) {
/* Memory-card slots: enable flags + any Browse/New paths. */
seed.memcard1_enabled = ls.memcard_enabled[0] != 0; seed.has_memcard1_enabled = true;
seed.memcard2_enabled = ls.memcard_enabled[1] != 0; seed.has_memcard2_enabled = true;
#if defined(RECOMP_LAUNCHER_HAS_MULTITAP_ENABLED)
seed.multitap_enabled = ls.multitap_enabled != 0;
seed.has_multitap_enabled = true;
multitap_enabled = seed.multitap_enabled;
apply_offline_pad_count(game_players, multitap_enabled);
#endif
#if defined(RECOMP_LAUNCHER_HAS_MULTITAP_ANALOG)
seed.multitap_analog = ls.multitap_analog != 0;
seed.has_multitap_analog = true;
multitap_analog = seed.multitap_analog;
sio_set_multitap_analog(multitap_analog ? 1 : 0);
if (game_config_path && game_config_path[0]) {
(void)PSXRecompV4::upsert_game_toml_controller_bool(
game_config_path, "multitap_analog", multitap_analog);
}
#endif
if (ls.memcard_path[0][0]) { seed.memcard1_path = ls.memcard_path[0]; seed.has_memcard1_path = true; }
if (ls.memcard_path[1][0]) { seed.memcard2_path = ls.memcard_path[1]; seed.has_memcard2_path = true; }
@@ -9882,6 +9959,11 @@ int main(int argc, char** argv) {
if (ls.netplay_launch.enabled) {
const PsxLobbyMatchCaps* caps = psx_lobby_match_caps();
if (caps && caps->valid) {
/* Host-authoritative DualShock-on-tap for the match. */
multitap_analog = caps->multitap_analog != 0;
seed.multitap_analog = multitap_analog;
seed.has_multitap_analog = true;
sio_set_multitap_analog(multitap_analog ? 1 : 0);
seed.aspect_num = caps->aspect_num ? caps->aspect_num : seed.aspect_num;
seed.aspect_den = caps->aspect_den ? caps->aspect_den : seed.aspect_den;
seed.has_aspect_ratio = true;
@@ -11114,6 +11196,13 @@ soft_return_lobby:
std::snprintf(ls.netplay_player_name, sizeof(ls.netplay_player_name), "%s",
psx_lobby_display_name());
std::snprintf(ls.bios_path, sizeof(ls.bios_path), "%s", bios_path_str.c_str());
#if defined(RECOMP_LAUNCHER_HAS_MULTITAP_ENABLED)
ls.multitap_enabled = multitap_enabled ? 1 : 0;
#endif
#if defined(RECOMP_LAUNCHER_HAS_MULTITAP_ANALOG)
ls.multitap_analog = multitap_analog ? 1 : 0;
g_lnch_multitap_analog = multitap_analog ? 1 : 0;
#endif
/* Preserve input devices across soft-return. A zeroed ls left every
* player_src at None, so the rematch UI (and a subsequent writeback)
* looked like netplay had wiped the pad assignment. */
@@ -11196,6 +11285,18 @@ soft_return_lobby:
disc_path_str = resolved_disc.string();
}
if (ls.netplay_launch.enabled) {
{
const PsxLobbyMatchCaps* caps = psx_lobby_match_caps();
if (caps && caps->valid) {
multitap_analog = caps->multitap_analog != 0;
g_lnch_multitap_analog = multitap_analog ? 1 : 0;
sio_set_multitap_analog(multitap_analog ? 1 : 0);
} else {
multitap_analog = ls.multitap_analog != 0;
g_lnch_multitap_analog = multitap_analog ? 1 : 0;
sio_set_multitap_analog(multitap_analog ? 1 : 0);
}
}
net_cfg = {};
net_cfg.enabled = 1;
net_cfg.local_slot = ls.netplay_launch.local_slot;
@@ -11264,6 +11365,23 @@ soft_return_lobby:
}
us.deadzone = player_deadzone[0];
us.has_deadzone = true;
#if defined(RECOMP_LAUNCHER_HAS_MULTITAP_ENABLED)
multitap_enabled = ls.multitap_enabled != 0;
us.multitap_enabled = multitap_enabled;
us.has_multitap_enabled = true;
apply_offline_pad_count(game_players, multitap_enabled);
#endif
#if defined(RECOMP_LAUNCHER_HAS_MULTITAP_ANALOG)
multitap_analog = ls.multitap_analog != 0;
us.multitap_analog = multitap_analog;
us.has_multitap_analog = true;
g_lnch_multitap_analog = multitap_analog ? 1 : 0;
sio_set_multitap_analog(multitap_analog ? 1 : 0);
if (game_config_path && game_config_path[0]) {
(void)PSXRecompV4::upsert_game_toml_controller_bool(
game_config_path, "multitap_analog", multitap_analog);
}
#endif
us.renderer = ls.renderer;
us.has_renderer = true;
us.supersampling = ls.supersampling;
+3 -1
View File
@@ -1276,6 +1276,7 @@ static void parse_match_caps_object(const char *obj, PsxLobbyMatchCaps *out)
out->force_turn = json_get_bool(obj, "force_turn", 0);
/* Absent field → delay-sync (older hosts). New hosts always publish explicit. */
out->rollback = json_get_bool(obj, "rollback", 0);
out->multitap_analog = json_get_bool(obj, "multitap_analog", 0);
json_get_str(obj, "language", out->language, sizeof(out->language));
out->valid = 1;
}
@@ -1306,7 +1307,7 @@ static int append_match_caps_json(char *dst, size_t dst_cap, const PsxLobbyMatch
"\"turbo_loads\":%s,\"bios_hle\":%s,\"fast_boot\":%s,"
"\"auto_skip_fmv\":%s,\"input_delay\":%d,\"input_prediction\":%d,"
"\"force_input_relay\":%s,\"force_turn\":%s,\"rollback\":%s,"
"\"language\":\"%s\"}",
"\"multitap_analog\":%s,\"language\":\"%s\"}",
caps->aspect_num, caps->aspect_den,
caps->turbo_loads ? "true" : "false",
caps->bios_hle ? "true" : "false",
@@ -1317,6 +1318,7 @@ static int append_match_caps_json(char *dst, size_t dst_cap, const PsxLobbyMatch
caps->force_input_relay ? "true" : "false",
caps->force_turn ? "true" : "false",
caps->rollback ? "true" : "false",
caps->multitap_analog ? "true" : "false",
lang);
}
+8 -5
View File
@@ -164,8 +164,10 @@ static void force_session_pads_connected(int slot_count)
sio_set_multitap(0);
for (i = 0; i < slot_count; ++i) {
sio_connect_pad(i);
/* Multitap taps are plain digital (sio clamps); lone port pad may be DS. */
sio_set_pad_config_capable(i, sio_pad_on_multitap(i) ? 0 : 1);
/* Tap seats are digital unless multitap_analog hack is armed. */
const int force_dig =
sio_pad_on_multitap(i) && !sio_get_multitap_analog();
sio_set_pad_config_capable(i, force_dig ? 0 : 1);
}
}
@@ -2099,14 +2101,15 @@ static void apply_pad_slot(int slot, const PsxNetPad *pad)
g_sio_pad_prev[slot] = pad->buttons;
g_sio_pad_have[slot] = 1u;
}
const int force_dig = on_tap && !sio_get_multitap_analog();
sio_set_pad_connected(slot, 1);
sio_set_pad_config_capable(slot, on_tap ? 0 : 1);
sio_set_pad_config_capable(slot, force_dig ? 0 : 1);
sio_set_pad_state_slot(slot, pad->buttons);
if (on_tap)
if (force_dig)
sio_set_pad_sticks(slot, 0x80, 0x80, 0x80, 0x80);
else
sio_set_pad_sticks(slot, pad->lx, pad->ly, pad->rx, pad->ry);
sio_request_pad_type(slot, (!on_tap && pad->analog) ? 1 : 0);
sio_request_pad_type(slot, (!force_dig && pad->analog) ? 1 : 0);
if (psx_start_consumer_enabled()) {
uint32_t sim = g_np.session ? rnet_session_sim_tick(g_np.session) : 0u;
psx_start_consumer_note(slot, sim, pad->buttons);
+20 -5
View File
@@ -69,6 +69,8 @@ static uint8_t pad_connected = 0;
static int sio_multitap_enabled = 0;
/* Physical SIO port hosting the tap: 0 = console Port 1, 1 = Port 2. */
static int sio_multitap_port = 0;
/* Opt-in DualShock on tap seats (default off — faithful digital taps). */
static int sio_multitap_analog_hack = 0;
/* Pad communication state machine */
typedef enum {
@@ -794,12 +796,20 @@ int sio_get_multitap_port(void) {
#endif
}
void sio_set_multitap_analog(int enabled) {
sio_multitap_analog_hack = enabled ? 1 : 0;
}
int sio_get_multitap_analog(void) {
return sio_multitap_analog_hack;
}
int sio_pad_on_multitap(int logical_slot) {
#if PSX_MAX_PLAYERS >= 5
if (!sio_multitap_active()) return 0;
if (logical_slot < 0 || logical_slot >= PSX_MAX_PLAYERS) return 0;
/* Dual tap: every seat is a tap pad (digital). Single tap: opposite-port
* lone pad may stay DualShock; every tap slot is digital. */
/* Dual tap: every seat is a tap pad. Single tap: opposite-port lone pad
* may stay DualShock; every other slot is on the tap. */
if (sio_dual_multitap()) return 1;
return (logical_slot == mtap_standalone_logical()) ? 0 : 1;
#else
@@ -808,6 +818,11 @@ int sio_pad_on_multitap(int logical_slot) {
#endif
}
/* Tap seat that must stay digital unless the analog hack is armed. */
static int sio_tap_force_digital(int logical_slot) {
return sio_pad_on_multitap(logical_slot) && !sio_multitap_analog_hack;
}
void sio_connect_pad(int slot) {
if (slot >= 0 && slot < PSX_MAX_PLAYERS)
pad_connected |= (uint8_t)(1u << slot);
@@ -821,7 +836,7 @@ void sio_set_pad_connected(int slot, int connected) {
void sio_set_pad_config_capable(int slot, int capable) {
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
if (sio_pad_on_multitap(slot)) capable = 0;
if (sio_tap_force_digital(slot)) capable = 0;
pad_supports_config[slot] = capable ? 1 : 0;
/* A plain digital pad can never be in config mode; clear any stale latch so
* the next poll reports the digital id (0x41), not the config id (0xF3). */
@@ -844,7 +859,7 @@ void sio_set_pad_state_slot(int slot, uint16_t buttons) {
void sio_set_pad_analog(int slot, int enabled,
uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry) {
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
if (sio_pad_on_multitap(slot)) {
if (sio_tap_force_digital(slot)) {
enabled = 0;
lx = ly = rx = ry = 0x80;
}
@@ -866,7 +881,7 @@ void sio_set_pad_sticks(int slot, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry
* can never split a poll or a config handshake. A no-op if already that type. */
void sio_request_pad_type(int slot, int analog) {
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
if (sio_pad_on_multitap(slot)) analog = 0;
if (sio_tap_force_digital(slot)) analog = 0;
int want = analog ? 1 : 0;
pad_type_req[slot] = (pad_analog[slot] == want) ? -1 : (int8_t)want;
}