mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 18:25:35 -04:00
controller: hybrid pad mode (auto analog/digital) + 3-way launcher selector
Replaces the analog on/off boolean with a per-player PadMode {hybrid, analog,
digital}. Hybrid (default) auto-switches the reported pad from the most-recent
input: nudge the stick -> DualShock (0x73, variable sticks); press the D-pad ->
digital pad (0x41) so the game runs its own digital path at true d-pad
sensitivity -- mirroring a DualShock's analog LED and Tomba Special Edition's
auto-detect. analog pins 0x73; digital pins 0x41.
config_loader parses [controller] default_mode / p1_mode / p2_mode (legacy
default_analog / p1_analog / p2_analog booleans still accepted); the launcher
exposes a Hybrid|Analog|D-Pad segmented selector per player with a tooltip;
settings persist as p1_mode / p2_mode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,27 @@ namespace PSXRecompV4 {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Pad mode <-> string. Accepts "hybrid"/"analog"/"digital" (case-insensitive);
|
||||
// returns `fallback` for anything unrecognised so a typo never silently flips
|
||||
// the pad type.
|
||||
int pad_mode_from_string(const std::string& s, int fallback) {
|
||||
std::string l;
|
||||
l.reserve(s.size());
|
||||
for (char c : s) l.push_back((char)std::tolower((unsigned char)c));
|
||||
if (l == "hybrid") return PAD_MODE_HYBRID;
|
||||
if (l == "analog") return PAD_MODE_ANALOG;
|
||||
if (l == "digital") return PAD_MODE_DIGITAL;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const char* pad_mode_to_string(int mode) {
|
||||
switch (mode) {
|
||||
case PAD_MODE_ANALOG: return "analog";
|
||||
case PAD_MODE_DIGITAL: return "digital";
|
||||
default: return "hybrid";
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a hex string ("0x...") to uint32_t. Throws on malformed input.
|
||||
static uint32_t parse_hex(const std::string& s, const std::string& field) {
|
||||
try {
|
||||
@@ -176,19 +197,40 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
// Optional [controller] block — game-declared input defaults.
|
||||
if (cfg.contains("controller")) {
|
||||
const toml::value& ct = toml::find(cfg, "controller");
|
||||
// Legacy boolean form (true->analog, false->digital), read first so the
|
||||
// new string `*_mode` keys win when both are present.
|
||||
if (ct.contains("default_analog")) {
|
||||
const bool a = toml::find<bool>(ct, "default_analog");
|
||||
rt.default_p1_analog = a;
|
||||
rt.default_p2_analog = a;
|
||||
rt.has_default_analog = true;
|
||||
const int m = toml::find<bool>(ct, "default_analog")
|
||||
? PAD_MODE_ANALOG : PAD_MODE_DIGITAL;
|
||||
rt.default_p1_mode = rt.default_p2_mode = m;
|
||||
rt.has_default_mode = true;
|
||||
}
|
||||
if (ct.contains("p1_analog")) {
|
||||
rt.default_p1_analog = toml::find<bool>(ct, "p1_analog");
|
||||
rt.has_default_analog = true;
|
||||
rt.default_p1_mode = toml::find<bool>(ct, "p1_analog")
|
||||
? PAD_MODE_ANALOG : PAD_MODE_DIGITAL;
|
||||
rt.has_default_mode = true;
|
||||
}
|
||||
if (ct.contains("p2_analog")) {
|
||||
rt.default_p2_analog = toml::find<bool>(ct, "p2_analog");
|
||||
rt.has_default_analog = true;
|
||||
rt.default_p2_mode = toml::find<bool>(ct, "p2_analog")
|
||||
? PAD_MODE_ANALOG : PAD_MODE_DIGITAL;
|
||||
rt.has_default_mode = true;
|
||||
}
|
||||
// Preferred string form.
|
||||
if (ct.contains("default_mode")) {
|
||||
const int m = pad_mode_from_string(
|
||||
toml::find<std::string>(ct, "default_mode"), PAD_MODE_HYBRID);
|
||||
rt.default_p1_mode = rt.default_p2_mode = m;
|
||||
rt.has_default_mode = true;
|
||||
}
|
||||
if (ct.contains("p1_mode")) {
|
||||
rt.default_p1_mode = pad_mode_from_string(
|
||||
toml::find<std::string>(ct, "p1_mode"), PAD_MODE_HYBRID);
|
||||
rt.has_default_mode = true;
|
||||
}
|
||||
if (ct.contains("p2_mode")) {
|
||||
rt.default_p2_mode = pad_mode_from_string(
|
||||
toml::find<std::string>(ct, "p2_mode"), PAD_MODE_HYBRID);
|
||||
rt.has_default_mode = true;
|
||||
}
|
||||
if (ct.contains("deadzone")) {
|
||||
const auto n = toml::find<int64_t>(ct, "deadzone");
|
||||
@@ -741,11 +783,27 @@ UserSettings load_user_settings(const fs::path& path) {
|
||||
const auto v = toml::find<std::string>(ct, "p2_device");
|
||||
if (!v.empty()) { s.p2_device = v; s.has_p2_device = true; }
|
||||
});
|
||||
// Legacy boolean form first (true->analog, false->digital); the new
|
||||
// string `*_mode` keys override when present.
|
||||
if (ct.contains("p1_analog")) try_get([&]{
|
||||
s.p1_analog = toml::find<bool>(ct, "p1_analog"); s.has_p1_analog = true;
|
||||
s.p1_mode = toml::find<bool>(ct, "p1_analog")
|
||||
? PAD_MODE_ANALOG : PAD_MODE_DIGITAL;
|
||||
s.has_p1_mode = true;
|
||||
});
|
||||
if (ct.contains("p2_analog")) try_get([&]{
|
||||
s.p2_analog = toml::find<bool>(ct, "p2_analog"); s.has_p2_analog = true;
|
||||
s.p2_mode = toml::find<bool>(ct, "p2_analog")
|
||||
? PAD_MODE_ANALOG : PAD_MODE_DIGITAL;
|
||||
s.has_p2_mode = true;
|
||||
});
|
||||
if (ct.contains("p1_mode")) try_get([&]{
|
||||
s.p1_mode = pad_mode_from_string(
|
||||
toml::find<std::string>(ct, "p1_mode"), PAD_MODE_HYBRID);
|
||||
s.has_p1_mode = true;
|
||||
});
|
||||
if (ct.contains("p2_mode")) try_get([&]{
|
||||
s.p2_mode = pad_mode_from_string(
|
||||
toml::find<std::string>(ct, "p2_mode"), PAD_MODE_HYBRID);
|
||||
s.has_p2_mode = true;
|
||||
});
|
||||
if (ct.contains("deadzone")) try_get([&]{
|
||||
const auto n = toml::find<int64_t>(ct, "deadzone");
|
||||
@@ -817,19 +875,19 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
|
||||
f << "enable2 = " << (s.memcard2_enabled ? "true" : "false") << "\n";
|
||||
}
|
||||
|
||||
if (s.has_p1_device || s.has_p2_device || s.has_p1_analog || s.has_p2_analog ||
|
||||
if (s.has_p1_device || s.has_p2_device || s.has_p1_mode || s.has_p2_mode ||
|
||||
s.has_deadzone) {
|
||||
f << "\n[controller]\n";
|
||||
if (s.has_p1_device)
|
||||
f << "p1_device = \"" << s.p1_device << "\"\n";
|
||||
if (s.has_p1_analog)
|
||||
f << "p1_analog = " << (s.p1_analog ? "true" : "false") << "\n";
|
||||
if (s.has_p1_mode)
|
||||
f << "p1_mode = \"" << pad_mode_to_string(s.p1_mode) << "\"\n";
|
||||
if (s.has_p2_device)
|
||||
f << "p2_device = \"" << s.p2_device << "\"\n";
|
||||
if (s.has_p2_analog)
|
||||
f << "p2_analog = " << (s.p2_analog ? "true" : "false") << "\n";
|
||||
if (s.has_p2_mode)
|
||||
f << "p2_mode = \"" << pad_mode_to_string(s.p2_mode) << "\"\n";
|
||||
if (s.has_deadzone)
|
||||
f << "deadzone = " << s.deadzone << "\n";
|
||||
f << "deadzone = " << s.deadzone << "\n";
|
||||
}
|
||||
|
||||
return f.good();
|
||||
|
||||
@@ -25,6 +25,23 @@
|
||||
|
||||
namespace PSXRecompV4 {
|
||||
|
||||
// Pad input mode (per player). Replaces the old analog on/off boolean.
|
||||
// hybrid — auto-switch DualShock(analog)/digital per the most-recent input:
|
||||
// nudge the stick -> report DualShock (0x73, variable sticks);
|
||||
// press the D-pad -> report a digital pad (0x41) so the game runs
|
||||
// its OWN d-pad path at true digital sensitivity. Mirrors a
|
||||
// DualShock's analog LED toggling on/off (and Tomba Special
|
||||
// Edition's auto-detect). Default.
|
||||
// analog — always present a DualShock/analog pad (id 0x73). The D-pad is
|
||||
// folded onto the stick at full deflection so it still moves you.
|
||||
// digital — always present a digital pad (id 0x41); sticks disabled.
|
||||
enum PadMode { PAD_MODE_HYBRID = 0, PAD_MODE_ANALOG = 1, PAD_MODE_DIGITAL = 2 };
|
||||
|
||||
// Parse/format a pad mode. pad_mode_from_string accepts "hybrid"/"analog"/
|
||||
// "digital" (case-insensitive) and returns `fallback` for anything else.
|
||||
int pad_mode_from_string(const std::string& s, int fallback);
|
||||
const char* pad_mode_to_string(int mode);
|
||||
|
||||
// [runtime] block — consumed by runtime/src/main.cpp. All fields optional;
|
||||
// callers that need them check has_* flags or use the supplied defaults.
|
||||
struct RuntimeConfig {
|
||||
@@ -138,15 +155,18 @@ struct RuntimeConfig {
|
||||
bool audio_spu_hq = false;
|
||||
|
||||
// ---- [controller] block — game-declared input defaults ----
|
||||
// default_analog: present a DualShock/analog pad (id 0x73) by default for
|
||||
// this game, so a stick-capable title (e.g. Tomba, which gives variable run
|
||||
// speed through the analog stick) feels right out of the box without the
|
||||
// user toggling DualShock in the launcher. Per-install settings.toml
|
||||
// [controller] p1_analog/p2_analog still override. Default off (digital 0x41).
|
||||
// `default_analog = true` sets both ports; `p1_analog`/`p2_analog` set one.
|
||||
bool has_default_analog = false;
|
||||
bool default_p1_analog = false;
|
||||
bool default_p2_analog = false;
|
||||
// default_mode: the pad input mode this game ships with (see PadMode):
|
||||
// "hybrid" (default) auto-switches DualShock/digital from the player's
|
||||
// input, "analog" pins DualShock (0x73), "digital" pins a digital pad
|
||||
// (0x41). A stick-capable title (e.g. Tomba) ships "hybrid" so the stick
|
||||
// gives variable run speed yet the D-pad keeps its classic digital feel,
|
||||
// with no launcher toggling. Per-install settings.toml [controller]
|
||||
// p1_mode/p2_mode still override. `default_mode` sets both ports;
|
||||
// `p1_mode`/`p2_mode` set one. Legacy `default_analog`/`p1_analog`/
|
||||
// `p2_analog` booleans are still accepted (true->analog, false->digital).
|
||||
bool has_default_mode = false;
|
||||
int default_p1_mode = PAD_MODE_HYBRID;
|
||||
int default_p2_mode = PAD_MODE_HYBRID;
|
||||
|
||||
// deadzone: default analog-stick deadzone in raw SDL axis units (0..32767).
|
||||
// Applied both to the stick->d-pad press threshold and the analog-axis centre
|
||||
@@ -348,12 +368,15 @@ struct UserSettings {
|
||||
// "none" — no pad in this port (port not connected)
|
||||
// "keyboard" — driven by the keyboard map (input.ini)
|
||||
// "<GUID>" — an SDL game-controller GUID (SDL_JoystickGetGUIDString)
|
||||
// analog selects the emulated pad type: false = digital (0x41), true =
|
||||
// DualShock/analog (0x73). Defaults: P1 keyboard/digital, P2 none/digital.
|
||||
// p1_mode/p2_mode select the emulated pad behaviour (see PadMode):
|
||||
// hybrid (default) / analog / digital. Defaults: P1 keyboard, P2 none.
|
||||
bool has_p1_device = false; std::string p1_device = "keyboard";
|
||||
bool has_p2_device = false; std::string p2_device = "none";
|
||||
bool has_p1_analog = false; bool p1_analog = false;
|
||||
bool has_p2_analog = false; bool p2_analog = false;
|
||||
// Pad input mode per player (see PadMode): hybrid (default) / analog /
|
||||
// digital. Persisted as p1_mode/p2_mode strings. Legacy p1_analog/p2_analog
|
||||
// booleans are still read for back-compat (true->analog, false->digital).
|
||||
bool has_p1_mode = false; int p1_mode = PAD_MODE_HYBRID;
|
||||
bool has_p2_mode = false; int p2_mode = PAD_MODE_HYBRID;
|
||||
// Analog-stick deadzone, raw SDL axis units (0..32767). The launcher edits
|
||||
// this as 0-100% (raw = pct*32767/100), mirroring snesrecomp's GamepadDeadzone.
|
||||
bool has_deadzone = false; int deadzone = 12000;
|
||||
|
||||
@@ -101,6 +101,18 @@ button:hover { background-color: #232d3c; }
|
||||
.knob { display: block; width: 16dp; height: 16dp; border-radius: 8dp; background-color: #e6e9ef; margin: 3dp 0dp 0dp 3dp; }
|
||||
.switch.on .knob { margin-left: 23dp; }
|
||||
|
||||
/* Pad-mode 3-way segmented selector (Hybrid | Analog | D-Pad). */
|
||||
.seg { display: flex; flex-direction: row; align-items: center; position: relative;
|
||||
background-color: #161d28; border: 1dp #2b3543; border-radius: 7dp; }
|
||||
.seg-opt { display: block; padding: 5dp 11dp; color: #8b949e; font-size: 12dp; border-radius: 6dp; }
|
||||
.seg-opt:hover { color: #cdd5df; }
|
||||
.seg-opt.on { background-color: #2563eb; color: #ffffff; }
|
||||
/* Hybrid carries the explanatory tooltip, revealed on hover. */
|
||||
.seg-tip { display: none; position: absolute; top: 34dp; right: 0dp; width: 250dp; z-index: 80;
|
||||
background-color: #0d1219; border: 1dp #3b82f6; border-radius: 7dp;
|
||||
padding: 10dp 12dp; color: #cdd5df; font-size: 11dp; line-height: 16dp; }
|
||||
.seg-opt.hybrid:hover .seg-tip { display: block; }
|
||||
|
||||
.card-body { display: flex; flex-direction: row; align-items: center; }
|
||||
.card-body.disabled { opacity: 0.4; }
|
||||
/* Player card body is a fixed height so toggling the pad type never reflows. */
|
||||
@@ -219,11 +231,14 @@ button:hover { background-color: #232d3c; }
|
||||
<div class="card left">
|
||||
<div class="card-head">
|
||||
<span class="card-name">PLAYER 1</span>
|
||||
<span class="enable-lbl">DualShock</span>
|
||||
<span class="switch" data-class-on="p1_analog" data-event-click="toggle_ds_p1"><span class="knob"></span></span>
|
||||
<div class="seg">
|
||||
<span class="seg-opt hybrid" data-class-on="p1_mode == 0" data-event-click="set_mode_p1(0)">Hybrid<span class="seg-tip">Mirrors Tomba Special Edition: nudge the analog stick for smooth analog movement, or use the D-pad for classic digital control. It switches automatically based on what you touch.</span></span>
|
||||
<span class="seg-opt" data-class-on="p1_mode == 1" data-event-click="set_mode_p1(1)">Analog</span>
|
||||
<span class="seg-opt" data-class-on="p1_mode == 2" data-event-click="set_mode_p1(2)">D-Pad</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body player">
|
||||
<div class="pad-art" data-class-analog="p1_analog" data-class-digital="p1_analog == false"></div>
|
||||
<div class="pad-art" data-class-analog="p1_mode != 2" data-class-digital="p1_mode == 2"></div>
|
||||
<div class="card-col">
|
||||
<div class="field-lbl">Device</div>
|
||||
<div class="dropdown-wrap">
|
||||
@@ -240,11 +255,14 @@ button:hover { background-color: #232d3c; }
|
||||
<div class="card right">
|
||||
<div class="card-head">
|
||||
<span class="card-name">PLAYER 2</span>
|
||||
<span class="enable-lbl">DualShock</span>
|
||||
<span class="switch" data-class-on="p2_analog" data-event-click="toggle_ds_p2"><span class="knob"></span></span>
|
||||
<div class="seg">
|
||||
<span class="seg-opt hybrid" data-class-on="p2_mode == 0" data-event-click="set_mode_p2(0)">Hybrid<span class="seg-tip">Mirrors Tomba Special Edition: nudge the analog stick for smooth analog movement, or use the D-pad for classic digital control. It switches automatically based on what you touch.</span></span>
|
||||
<span class="seg-opt" data-class-on="p2_mode == 1" data-event-click="set_mode_p2(1)">Analog</span>
|
||||
<span class="seg-opt" data-class-on="p2_mode == 2" data-event-click="set_mode_p2(2)">D-Pad</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body player">
|
||||
<div class="pad-art" data-class-analog="p2_analog" data-class-digital="p2_analog == false"></div>
|
||||
<div class="pad-art" data-class-analog="p2_mode != 2" data-class-digital="p2_mode == 2"></div>
|
||||
<div class="card-col">
|
||||
<div class="field-lbl">Device</div>
|
||||
<div class="dropdown-wrap">
|
||||
|
||||
@@ -133,8 +133,10 @@ struct LauncherModel {
|
||||
// Keyboard / a plugged-in SDL controller) and a pad type (DualShock=analog).
|
||||
int p1_dev_index = 1; // index into the shared device option list
|
||||
int p2_dev_index = 0;
|
||||
bool p1_analog = false; // DualShock (analog) vs digital pad
|
||||
bool p2_analog = false;
|
||||
// Pad input mode (PSXRecompV4::PadMode): 0=hybrid (default), 1=analog,
|
||||
// 2=digital. Bound to the segmented 3-way selector in each player card.
|
||||
int p1_mode = 0;
|
||||
int p2_mode = 0;
|
||||
int deadzone_pct = 37; // analog-stick deadzone 0-100% (raw = pct*32767/100)
|
||||
Rml::String p1_dev_label = "Keyboard";
|
||||
Rml::String p2_dev_label = "None";
|
||||
@@ -316,14 +318,16 @@ void refresh_player(LauncherModel& m, int player, const std::vector<DeviceOption
|
||||
Rml::String& status = player == 0 ? m.p1_status : m.p2_status;
|
||||
Rml::String& dot = player == 0 ? m.p1_dot : m.p2_dot;
|
||||
Rml::String& options= player == 0 ? m.p1_options : m.p2_options;
|
||||
const bool analog = player == 0 ? m.p1_analog : m.p2_analog;
|
||||
const int mode = player == 0 ? m.p1_mode : m.p2_mode;
|
||||
|
||||
if (idx < 0 || idx >= (int)opts.size()) idx = 0;
|
||||
const DeviceOption& o = opts[idx];
|
||||
label = o.label;
|
||||
options = build_options_rml(player, opts);
|
||||
|
||||
const char* type = analog ? "DualShock (analog)" : "digital pad";
|
||||
const char* type = mode == 1 ? "DualShock (analog)"
|
||||
: mode == 2 ? "digital pad"
|
||||
: "hybrid (auto analog/d-pad)";
|
||||
if (o.kind == 0) { status = "No device — port empty"; dot = "off"; }
|
||||
else if (o.kind == 1) { status = Rml::String("Keyboard \xE2\x80\x94 ") + type; dot = ""; }
|
||||
else { status = o.label + Rml::String(" \xE2\x80\x94 ") + type; dot = ""; }
|
||||
@@ -615,8 +619,8 @@ Result run(SDL_Window* window, void* gl_context,
|
||||
|
||||
// ---- Seed the controller slots: enumerate devices, resolve selections ----
|
||||
std::vector<DeviceOption> dev_opts = enumerate_devices();
|
||||
m.p1_analog = io.has_p1_analog ? io.p1_analog : false;
|
||||
m.p2_analog = io.has_p2_analog ? io.p2_analog : false;
|
||||
m.p1_mode = io.has_p1_mode ? io.p1_mode : PSXRecompV4::PAD_MODE_HYBRID;
|
||||
m.p2_mode = io.has_p2_mode ? io.p2_mode : PSXRecompV4::PAD_MODE_HYBRID;
|
||||
m.deadzone_pct = io.has_deadzone ? (io.deadzone * 100 / 32767) : 37;
|
||||
if (io.has_p1_device) {
|
||||
m.p1_dev_index = find_or_add_device_index(dev_opts, io.p1_device);
|
||||
@@ -660,8 +664,8 @@ Result run(SDL_Window* window, void* gl_context,
|
||||
c.Bind("verdict_detail", &m.verdict_detail);
|
||||
c.Bind("verdict_state", &m.verdict_state);
|
||||
c.Bind("view", &m.view);
|
||||
c.Bind("p1_analog", &m.p1_analog);
|
||||
c.Bind("p2_analog", &m.p2_analog);
|
||||
c.Bind("p1_mode", &m.p1_mode);
|
||||
c.Bind("p2_mode", &m.p2_mode);
|
||||
c.Bind("deadzone_pct", &m.deadzone_pct);
|
||||
c.Bind("p1_dev_label", &m.p1_dev_label);
|
||||
c.Bind("p2_dev_label", &m.p2_dev_label);
|
||||
@@ -787,10 +791,10 @@ Result run(SDL_Window* window, void* gl_context,
|
||||
[&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
|
||||
m.view = "dashboard"; handle.DirtyVariable("view");
|
||||
});
|
||||
// ---- controller: device dropdown + DualShock toggle ----
|
||||
// ---- controller: device dropdown + pad-mode segmented selector ----
|
||||
auto dirty_player = [handle](int player) mutable {
|
||||
const char* v0[] = {"p1_dev_label","p1_status","p1_dot","p1_options","p1_analog"};
|
||||
const char* v1[] = {"p2_dev_label","p2_status","p2_dot","p2_options","p2_analog"};
|
||||
const char* v0[] = {"p1_dev_label","p1_status","p1_dot","p1_options","p1_mode"};
|
||||
const char* v1[] = {"p2_dev_label","p2_status","p2_dot","p2_options","p2_mode"};
|
||||
for (const char* v : (player == 0 ? v0 : v1)) handle.DirtyVariable(v);
|
||||
};
|
||||
// dev_opts is captured by value: the device list is fixed for the launcher
|
||||
@@ -817,13 +821,17 @@ Result run(SDL_Window* window, void* gl_context,
|
||||
dirty_player(player);
|
||||
handle.DirtyVariable("dd_open");
|
||||
});
|
||||
c.BindEventCallback("toggle_ds_p1",
|
||||
[&m, handle, dev_opts, dirty_player](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
|
||||
m.p1_analog = !m.p1_analog; refresh_player(m, 0, dev_opts); dirty_player(0);
|
||||
// Pad-mode segmented selector: each segment passes its mode (0=hybrid,
|
||||
// 1=analog, 2=digital) so any mode is one click away.
|
||||
c.BindEventCallback("set_mode_p1",
|
||||
[&m, handle, dev_opts, dirty_player](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& args) mutable {
|
||||
if (args.empty()) return;
|
||||
m.p1_mode = (int)args[0].Get<int>(); refresh_player(m, 0, dev_opts); dirty_player(0);
|
||||
});
|
||||
c.BindEventCallback("toggle_ds_p2",
|
||||
[&m, handle, dev_opts, dirty_player](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable {
|
||||
m.p2_analog = !m.p2_analog; refresh_player(m, 1, dev_opts); dirty_player(1);
|
||||
c.BindEventCallback("set_mode_p2",
|
||||
[&m, handle, dev_opts, dirty_player](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& args) mutable {
|
||||
if (args.empty()) return;
|
||||
m.p2_mode = (int)args[0].Get<int>(); refresh_player(m, 1, dev_opts); dirty_player(1);
|
||||
});
|
||||
/* Analog-stick deadzone, stepped 0..50% (wraps). Applies to both the
|
||||
* stick->d-pad threshold and the analog centre dead-band. */
|
||||
@@ -952,8 +960,8 @@ Result run(SDL_Window* window, void* gl_context,
|
||||
const int i2 = (m.p2_dev_index >= 0 && m.p2_dev_index < (int)dev_opts.size()) ? m.p2_dev_index : 0;
|
||||
io.p1_device = device_string(dev_opts[i1]); io.has_p1_device = true;
|
||||
io.p2_device = device_string(dev_opts[i2]); io.has_p2_device = true;
|
||||
io.p1_analog = m.p1_analog; io.has_p1_analog = true;
|
||||
io.p2_analog = m.p2_analog; io.has_p2_analog = true;
|
||||
io.p1_mode = m.p1_mode; io.has_p1_mode = true;
|
||||
io.p2_mode = m.p2_mode; io.has_p2_mode = true;
|
||||
io.deadzone = m.deadzone_pct * 32767 / 100; io.has_deadzone = true;
|
||||
}
|
||||
|
||||
|
||||
+97
-35
@@ -105,7 +105,12 @@ static SDL_Texture* sdl_texture;
|
||||
struct PlayerInput {
|
||||
int kind = 0; /* 0=none, 1=keyboard, 2=controller */
|
||||
char guid[40] = {0}; /* SDL joystick GUID string when kind==controller */
|
||||
bool analog = false; /* DualShock (analog) vs digital pad */
|
||||
/* Pad input mode (PSXRecompV4::PadMode): 0=hybrid (default), 1=analog,
|
||||
* 2=digital. hybrid_analog is the per-frame auto-switch latch used only in
|
||||
* hybrid mode: true => currently presenting DualShock (stick was the last
|
||||
* input), false => currently presenting a digital pad (D-pad was last). */
|
||||
int mode = PSXRecompV4::PAD_MODE_HYBRID;
|
||||
bool hybrid_analog = false;
|
||||
SDL_GameController* handle = nullptr;
|
||||
SDL_JoystickID instance = -1;
|
||||
};
|
||||
@@ -963,6 +968,13 @@ static void open_player(PlayerInput& p, const PlayerInput& other) {
|
||||
}
|
||||
}
|
||||
|
||||
/* The pad type a mode reports before any input has been sampled (boot /
|
||||
* hotplug). analog pins DualShock; digital and hybrid both start as a digital
|
||||
* pad (hybrid only flips to DualShock once the player nudges the stick). */
|
||||
static int pad_mode_boot_analog(int mode) {
|
||||
return mode == PSXRecompV4::PAD_MODE_ANALOG ? 1 : 0;
|
||||
}
|
||||
|
||||
/* Open/close SDL handles so they match g_players, and (re)assert each slot's
|
||||
* PSX connection + pad type. Safe to call repeatedly (hotplug, boot). */
|
||||
static void refresh_player_devices(void) {
|
||||
@@ -971,14 +983,15 @@ static void refresh_player_devices(void) {
|
||||
if (p.kind != 2) close_player(p); /* keyboard/none: no handle */
|
||||
else open_player(p, g_players[s ^ 1]);
|
||||
sio_set_pad_connected(s, p.kind != 0 ? 1 : 0);
|
||||
sio_set_pad_analog(s, p.analog ? 1 : 0, 0x80, 0x80, 0x80, 0x80);
|
||||
sio_set_pad_analog(s, pad_mode_boot_analog(p.mode), 0x80, 0x80, 0x80, 0x80);
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse a [controller] device string into a player slot:
|
||||
* "none" -> no pad; "keyboard" -> keyboard map; otherwise an SDL GUID. */
|
||||
static void set_player_device(PlayerInput& p, const std::string& dev, bool analog) {
|
||||
p.analog = analog;
|
||||
static void set_player_device(PlayerInput& p, const std::string& dev, int mode) {
|
||||
p.mode = mode;
|
||||
p.hybrid_analog = false; /* hybrid starts digital until the stick moves */
|
||||
p.guid[0] = '\0';
|
||||
std::string d = lower_copy(trim_copy(dev));
|
||||
if (d.empty() || d == "none") { p.kind = 0; }
|
||||
@@ -1078,15 +1091,18 @@ static uint16_t pad_buttons_for(const PlayerInput& p) {
|
||||
|
||||
/* Analog stick bytes (lx,ly,rx,ry) for a player; centred if no live source.
|
||||
*
|
||||
* The host left stick maps to the LEFT analog axes (variable). The physical
|
||||
* D-pad (and, for a keyboard player, the arrow keys) is ALSO folded onto the
|
||||
* left axes at full deflection, so an analog-mode game whose movement reads only
|
||||
* the stick magnitude (e.g. Tomba) still responds to the D-pad/keyboard. This is
|
||||
* the faithful "seamless" behaviour the Special Edition exposes: the stick gives
|
||||
* variable speed, the D-pad gives full speed, and both work at once. Only the
|
||||
* PHYSICAL D-pad is folded in (not the stick->d-pad button mapping), so the
|
||||
* stick keeps its true variable magnitude rather than snapping to full. */
|
||||
static void pad_sticks_for(const PlayerInput& p, uint8_t out[4]) {
|
||||
* The host left stick maps to the LEFT analog axes (variable). When fold_dpad
|
||||
* is set, the physical D-pad (and, for a keyboard player, the arrow keys) is
|
||||
* ALSO folded onto the left axes at full deflection, so an analog-mode game
|
||||
* whose movement reads only the stick magnitude (e.g. Tomba) still responds to
|
||||
* the D-pad/keyboard — the faithful "seamless" behaviour: the stick gives
|
||||
* variable speed, the D-pad gives full speed, both at once. Only the PHYSICAL
|
||||
* D-pad is folded in (not the stick->d-pad button mapping), so the stick keeps
|
||||
* its true variable magnitude. fold_dpad is false in HYBRID mode (there the
|
||||
* D-pad instead flips the pad to digital, so the game runs its own d-pad path)
|
||||
* and true in pinned-ANALOG mode. The keyboard branch always folds the arrows
|
||||
* — they are that player's only stick source. */
|
||||
static void pad_sticks_for(const PlayerInput& p, uint8_t out[4], bool fold_dpad) {
|
||||
out[0] = out[1] = out[2] = out[3] = 0x80;
|
||||
if (p.kind == 1) {
|
||||
const Uint8* keys = SDL_GetKeyboardState(NULL);
|
||||
@@ -1101,13 +1117,44 @@ static void pad_sticks_for(const PlayerInput& p, uint8_t out[4]) {
|
||||
out[1] = axis_to_pad_byte(SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_LEFTY));
|
||||
out[2] = axis_to_pad_byte(SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_RIGHTX));
|
||||
out[3] = axis_to_pad_byte(SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_RIGHTY));
|
||||
if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) out[0] = 0x00;
|
||||
if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) out[0] = 0xFF;
|
||||
if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_UP)) out[1] = 0x00;
|
||||
if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) out[1] = 0xFF;
|
||||
if (fold_dpad) {
|
||||
if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) out[0] = 0x00;
|
||||
if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) out[0] = 0xFF;
|
||||
if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_UP)) out[1] = 0x00;
|
||||
if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) out[1] = 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* HYBRID-mode auto-switch detectors (mirror the DualShock analog LED toggling).
|
||||
* hybrid_stick_active: the LEFT stick is deflected past the deadzone — the
|
||||
* player has reached for analog. hybrid_dpad_active: any D-pad direction (or,
|
||||
* for the keyboard, an arrow key) is held — the player wants classic digital.
|
||||
* The keyboard has no analog stick, so a keyboard player stays digital. */
|
||||
static bool hybrid_stick_active(const PlayerInput& p) {
|
||||
if (p.kind != 2 || !p.handle) return false;
|
||||
const int dz = controller_deadzone;
|
||||
int lx = SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_LEFTX);
|
||||
int ly = SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_LEFTY);
|
||||
return lx > dz || lx < -dz || ly > dz || ly < -dz;
|
||||
}
|
||||
static bool hybrid_dpad_active(const PlayerInput& p, bool kb_always) {
|
||||
if (p.kind == 2 && p.handle) {
|
||||
if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_LEFT) ||
|
||||
SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_RIGHT) ||
|
||||
SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_UP) ||
|
||||
SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_DOWN))
|
||||
return true;
|
||||
}
|
||||
if (p.kind == 1 || kb_always) {
|
||||
const Uint8* keys = SDL_GetKeyboardState(NULL);
|
||||
if (keys[SDL_SCANCODE_LEFT] || keys[SDL_SCANCODE_RIGHT] ||
|
||||
keys[SDL_SCANCODE_UP] || keys[SDL_SCANCODE_DOWN])
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* PSX native vblank cadence: NTSC ≈ 59.94 Hz. Wall-clock target keeps
|
||||
* audio sample generation (735 samples/vblank * 60 = 44100/sec) matched
|
||||
* to the SDL audio device drain rate, eliminating queue overflow drops
|
||||
@@ -1181,14 +1228,29 @@ static void sdl_vblank_present(void) {
|
||||
sio_set_pad_state_slot(0, (uint16_t)override);
|
||||
} else {
|
||||
for (int s = 0; s < 2; s++) {
|
||||
const PlayerInput& p = g_players[s];
|
||||
PlayerInput& p = g_players[s];
|
||||
if (p.kind == 0) continue; /* no device in this port */
|
||||
sio_set_pad_state_slot(s, pad_buttons_for(p));
|
||||
if (p.analog) {
|
||||
uint8_t st[4];
|
||||
pad_sticks_for(p, st);
|
||||
sio_set_pad_analog(s, 1, st[0], st[1], st[2], st[3]);
|
||||
|
||||
/* Resolve the pad type this frame from the player's mode. HYBRID
|
||||
* auto-switches DualShock<->digital from the most-recent input
|
||||
* (stick -> analog, D-pad -> digital), mirroring the Special
|
||||
* Edition's analog-LED auto-detect, so the game runs its own analog
|
||||
* or digital input path exactly as on hardware. */
|
||||
int eff_analog;
|
||||
uint8_t st[4] = { 0x80, 0x80, 0x80, 0x80 };
|
||||
if (p.mode == PSXRecompV4::PAD_MODE_DIGITAL) {
|
||||
eff_analog = 0;
|
||||
} else if (p.mode == PSXRecompV4::PAD_MODE_ANALOG) {
|
||||
eff_analog = 1;
|
||||
pad_sticks_for(p, st, /*fold_dpad=*/true);
|
||||
} else { /* HYBRID */
|
||||
if (hybrid_stick_active(p)) p.hybrid_analog = true;
|
||||
else if (hybrid_dpad_active(p, false)) p.hybrid_analog = false;
|
||||
eff_analog = p.hybrid_analog ? 1 : 0;
|
||||
if (eff_analog) pad_sticks_for(p, st, /*fold_dpad=*/false);
|
||||
}
|
||||
sio_set_pad_analog(s, eff_analog, st[0], st[1], st[2], st[3]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1550,8 +1612,8 @@ int main(int argc, char** argv) {
|
||||
/* [controller] device routing (defaults: P1 keyboard/digital, P2 none). */
|
||||
std::string p1_device = "keyboard";
|
||||
std::string p2_device = "none";
|
||||
bool p1_analog = false;
|
||||
bool p2_analog = false;
|
||||
int p1_mode = PSXRecompV4::PAD_MODE_HYBRID;
|
||||
int p2_mode = PSXRecompV4::PAD_MODE_HYBRID;
|
||||
int resolved_deadzone = -1; /* <0 => keep input.ini/runtime default (12000) */
|
||||
std::filesystem::path resolved_disc;
|
||||
std::string window_title = PSX_WINDOW_TITLE;
|
||||
@@ -1603,9 +1665,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.has_default_analog) {
|
||||
p1_analog = gc.runtime.default_p1_analog;
|
||||
p2_analog = gc.runtime.default_p2_analog;
|
||||
if (gc.runtime.has_default_mode) {
|
||||
p1_mode = gc.runtime.default_p1_mode;
|
||||
p2_mode = gc.runtime.default_p2_mode;
|
||||
}
|
||||
if (gc.runtime.has_deadzone) resolved_deadzone = gc.runtime.deadzone;
|
||||
{ const char *e = std::getenv("PSX_GL_FORCE_CPU_PRESENT");
|
||||
@@ -1699,8 +1761,8 @@ int main(int argc, char** argv) {
|
||||
if (us.has_memcard2_enabled) memcard2_enabled = us.memcard2_enabled;
|
||||
if (us.has_p1_device) p1_device = us.p1_device;
|
||||
if (us.has_p2_device) p2_device = us.p2_device;
|
||||
if (us.has_p1_analog) p1_analog = us.p1_analog;
|
||||
if (us.has_p2_analog) p2_analog = us.p2_analog;
|
||||
if (us.has_p1_mode) p1_mode = us.p1_mode;
|
||||
if (us.has_p2_mode) p2_mode = us.p2_mode;
|
||||
if (us.has_deadzone) resolved_deadzone = us.deadzone;
|
||||
}
|
||||
|
||||
@@ -1777,8 +1839,8 @@ int main(int argc, char** argv) {
|
||||
if (!memcard2_path.empty()) { seed.memcard2_path = memcard2_path; seed.has_memcard2_path = true; }
|
||||
seed.p1_device = p1_device; seed.has_p1_device = true;
|
||||
seed.p2_device = p2_device; seed.has_p2_device = true;
|
||||
seed.p1_analog = p1_analog; seed.has_p1_analog = true;
|
||||
seed.p2_analog = p2_analog; seed.has_p2_analog = true;
|
||||
seed.p1_mode = p1_mode; seed.has_p1_mode = true;
|
||||
seed.p2_mode = p2_mode; seed.has_p2_mode = true;
|
||||
seed.deadzone = resolved_deadzone >= 0 ? resolved_deadzone : 12000;
|
||||
seed.has_deadzone = true;
|
||||
seed.window_width = g_video_win_w; seed.has_window_width = true;
|
||||
@@ -1845,7 +1907,7 @@ int main(int argc, char** argv) {
|
||||
if (seed.has_memcard1_path) memcard1_path = seed.memcard1_path;
|
||||
if (seed.has_memcard2_path) memcard2_path = seed.memcard2_path;
|
||||
p1_device = seed.p1_device; p2_device = seed.p2_device;
|
||||
p1_analog = seed.p1_analog; p2_analog = seed.p2_analog;
|
||||
p1_mode = seed.p1_mode; p2_mode = seed.p2_mode;
|
||||
if (seed.has_deadzone) resolved_deadzone = seed.deadzone;
|
||||
g_video_win_w = seed.window_width;
|
||||
/* Persist the user's choices next to the exe. */
|
||||
@@ -1929,11 +1991,11 @@ int main(int argc, char** argv) {
|
||||
* SDL controller handles are opened later (after SDL_Init); here we only
|
||||
* set the PSX-visible connection + pad type so the BIOS sees the right
|
||||
* ports during early boot. */
|
||||
set_player_device(g_players[0], p1_device, p1_analog);
|
||||
set_player_device(g_players[1], p2_device, p2_analog);
|
||||
set_player_device(g_players[0], p1_device, p1_mode);
|
||||
set_player_device(g_players[1], p2_device, p2_mode);
|
||||
for (int s = 0; s < 2; s++) {
|
||||
sio_set_pad_connected(s, g_players[s].kind != 0 ? 1 : 0);
|
||||
sio_set_pad_analog(s, g_players[s].analog ? 1 : 0, 0x80, 0x80, 0x80, 0x80);
|
||||
sio_set_pad_analog(s, pad_mode_boot_analog(g_players[s].mode), 0x80, 0x80, 0x80, 0x80);
|
||||
}
|
||||
/* SPU float-shadow gate must be set before spu_init() (which runs
|
||||
* spu_shadow_reset()). Default OFF; PSX_AUDIO_SHADOW env overrides. */
|
||||
|
||||
Reference in New Issue
Block a user