Files
Nicholas Bly be153e0fa0 Add Dolphin-compatible input expressions and GCPadNew.ini import, DualSense L/R remapping, vibration toggle (#89)
* Add Dolphin-compatible input expressions and GCPadNew.ini import

Rebased onto current main; addresses both CodeRabbit reviews on #89.

- Expression engine matching Dolphin's semantics: doubles rather than
  booleans, 0.5 press threshold, & as min, | as max, and the functions if,
  min, max, clamp, abs, sqrt, pow, sin, cos, tan, deadzone, timer, toggle,
  hold, tap, pulse and smooth. Timing uses a steady clock in seconds, as
  Dolphin does, so a copied expression behaves identically.

- Expressions bind to the GameCube buttons and triggers, combined with the
  existing button mapping rather than replacing it, and are skipped while
  the settings overlay holds input.

- Import reads [GCPadN] from the Dolphin config directory or from
  GCPadNew.ini beside the executable. Stick axes are not expression driven
  and keep their normal mapping.

- Fixes #74: a digital button bound to L or R now reports a fully pulled
  analog trigger, plus a PlayStation preset and a vibration toggle.

Review fixes: config paths round-trip through RuntimeConfigFile::PathToUtf8
and PathFromUtf8 so non-ASCII paths open correctly on Windows, and the
duplicated exists branch is gone; the tap count is clamped before the
unsigned conversion; the expression editor uses resizable storage via
ImGuiInputTextFlags_CallbackResize so a long expression cannot be saved
truncated; clamp bounds are ordered before std::clamp; <cstdlib> is included
for std::strtod; non-finite values are rejected at the evaluator boundary as
well as at the deadzone and timer divisions; and InputBindings::Reload() runs
from InitializeRuntimeSettings rather than the vibration handler.

runtime/tests/test_expr.cpp covers operator precedence, each stateful
function and every case raised in review.

Third review round: smooth() guards NaN as well as infinity so a zero rate
cannot latch a non-finite value in node state; division evaluates both operands
so stateful functions in the left subtree still update when the divisor is zero;
the expression editor clears stale errors when the port changes; and
runtime/tests/test_expr.cpp is registered with CTest as mkw_input_expr_tests,
following the existing test targets.

* Update runtime/src/input_expr.cpp

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update runtime/src/input_expr.cpp

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update runtime/src/input_expr.cpp

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-09-05 10:07:05 +02:00

68 lines
2.5 KiB
C++

#pragma once
// Per-port expression bindings for the GameCube controls, plus import of a
// Dolphin GCPadNew.ini.
#include <array>
#include <cstdint>
#include <string>
#include <dolphin/pad.h>
namespace InputBindings {
// The controls an expression can drive, in Dolphin's own naming so an
// imported config maps across without translation.
struct ControlInfo {
const char* dolphinName;
const char* label;
uint16_t padButton; // 0 for the analog-only controls below
int analog; // 0 none, 1 trigger L, 2 trigger R
};
inline constexpr std::array<ControlInfo, 14> kControls = {{
{"Buttons/A", "A", PAD_BUTTON_A, 0},
{"Buttons/B", "B", PAD_BUTTON_B, 0},
{"Buttons/X", "X", PAD_BUTTON_X, 0},
{"Buttons/Y", "Y", PAD_BUTTON_Y, 0},
{"Buttons/Z", "Z", PAD_TRIGGER_Z, 0},
{"Buttons/Start", "Start", PAD_BUTTON_START, 0},
{"D-Pad/Up", "D-pad Up", PAD_BUTTON_UP, 0},
{"D-Pad/Down", "D-pad Down", PAD_BUTTON_DOWN, 0},
{"D-Pad/Left", "D-pad Left", PAD_BUTTON_LEFT, 0},
{"D-Pad/Right", "D-pad Right", PAD_BUTTON_RIGHT, 0},
{"Triggers/L", "L", PAD_TRIGGER_L, 1},
{"Triggers/R", "R", PAD_TRIGGER_R, 2},
{"Triggers/L-Analog", "L analog", 0, 1},
{"Triggers/R-Analog", "R analog", 0, 2},
}};
void Reload() noexcept;
// The pad library has PADBlockInput but no matching query, so the settings
// overlay reports its own state here.
void SetInputBlocked(bool blocked) noexcept;
bool InputBlocked() noexcept;
// Mix expression output into a freshly read status set. Call once per guest
// PADRead, after every other input source has been merged.
void Apply(PADStatus* statuses) noexcept;
std::string GetExpression(uint32_t port, size_t control) noexcept;
// Returns false and fills error if the text does not parse; the binding is
// left unchanged in that case.
bool SetExpression(uint32_t port, size_t control, const std::string& text, std::string& error) noexcept;
// True while the control's expression is above the press threshold.
bool IsActive(uint32_t port, size_t control) noexcept;
// The default Dolphin config location on Windows, then next to the executable.
std::string DefaultDolphinConfigPath() noexcept;
// Imports [GCPad<padIndex>] into the given port. Returns the number of controls
// imported, or -1 on failure with error filled.
int ImportDolphinConfig(const std::string& path, int padIndex, uint32_t port,
std::string& summary, std::string& error) noexcept;
} // namespace InputBindings