mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-10 09:11:52 -04:00
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>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
// The single vocabulary shared by everything that has to turn a Config.toml
|
||||
// controller name into a real button: the F10 settings bar, the macro engine,
|
||||
// and the startup mapping pass. Keeping one table here means a name that the
|
||||
// settings bar offers is always a name the config parser accepts, and vice
|
||||
// versa; the two used to drift because each side carried its own copy.
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
namespace ControllerNames {
|
||||
|
||||
// A GameCube button as the game sees it, with the Config.toml key that selects
|
||||
// it. Order matches RuntimeConfigFile::kControllerButtonKeys.
|
||||
struct GameCubeButtonItem {
|
||||
const char* configKey;
|
||||
const char* label;
|
||||
PADButton padButton;
|
||||
};
|
||||
|
||||
inline constexpr std::array<GameCubeButtonItem, PAD_BUTTON_COUNT> kGameCubeButtons = {{
|
||||
{"a", "A", PAD_BUTTON_A},
|
||||
{"b", "B", PAD_BUTTON_B},
|
||||
{"x", "X", PAD_BUTTON_X},
|
||||
{"y", "Y", PAD_BUTTON_Y},
|
||||
{"start", "Start", PAD_BUTTON_START},
|
||||
{"z", "Z", PAD_TRIGGER_Z},
|
||||
{"l", "L", PAD_TRIGGER_L},
|
||||
{"r", "R", PAD_TRIGGER_R},
|
||||
{"up", "D-pad Up", PAD_BUTTON_UP},
|
||||
{"down", "D-pad Down", PAD_BUTTON_DOWN},
|
||||
{"left", "D-pad Left", PAD_BUTTON_LEFT},
|
||||
{"right", "D-pad Right", PAD_BUTTON_RIGHT},
|
||||
}};
|
||||
|
||||
// A physical button on the host pad. Names are positional (south/east/...)
|
||||
// rather than Xbox-labelled so one config reads the same on any hardware.
|
||||
struct NativeButtonItem {
|
||||
const char* configName;
|
||||
const char* label;
|
||||
uint32_t nativeButton;
|
||||
};
|
||||
|
||||
inline constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNativeButtons = {{
|
||||
{"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID},
|
||||
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
|
||||
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
|
||||
{"north", "North (Y / Triangle)", SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{"back", "Back / Select / Create", SDL_GAMEPAD_BUTTON_BACK},
|
||||
{"guide", "Guide / Home / PS", SDL_GAMEPAD_BUTTON_GUIDE},
|
||||
{"start", "Start / Options", SDL_GAMEPAD_BUTTON_START},
|
||||
{"left_stick", "Left stick click (L3)", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
{"right_stick", "Right stick click (R3)", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||
{"left_shoulder", "Left bumper (LB / L1)", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||
{"right_shoulder", "Right bumper (RB / R1)", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||
{"dpad_up", "D-pad Up", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
||||
{"dpad_down", "D-pad Down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||
{"dpad_left", "D-pad Left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
||||
{"dpad_right", "D-pad Right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||
{"misc1", "Misc 1 / Share / Mic", SDL_GAMEPAD_BUTTON_MISC1},
|
||||
{"right_paddle1", "Right paddle 1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
||||
{"left_paddle1", "Left paddle 1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
||||
{"right_paddle2", "Right paddle 2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
||||
{"left_paddle2", "Left paddle 2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
||||
{"touchpad", "Touchpad click", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
||||
{"misc2", "Misc 2", SDL_GAMEPAD_BUTTON_MISC2},
|
||||
{"misc3", "Misc 3 / GC L click", SDL_GAMEPAD_BUTTON_MISC3},
|
||||
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
|
||||
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
|
||||
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
|
||||
}};
|
||||
|
||||
inline std::string TrimToken(std::string_view token) {
|
||||
const size_t begin = token.find_first_not_of(" \t");
|
||||
if (begin == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
const size_t end = token.find_last_not_of(" \t");
|
||||
return std::string(token.substr(begin, end - begin + 1));
|
||||
}
|
||||
|
||||
inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
|
||||
const std::string name = TrimToken(configName);
|
||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
|
||||
[&](const NativeButtonItem& item) { return name == item.configName; });
|
||||
return it == kNativeButtons.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
// Falls back to the "unmapped" entry so callers always have a label to draw.
|
||||
inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
|
||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
|
||||
[&](const NativeButtonItem& item) { return nativeButton == item.nativeButton; });
|
||||
return it == kNativeButtons.end() ? kNativeButtons.front() : *it;
|
||||
}
|
||||
|
||||
inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) {
|
||||
const std::string key = TrimToken(configKey);
|
||||
const auto it = std::find_if(kGameCubeButtons.begin(), kGameCubeButtons.end(),
|
||||
[&](const GameCubeButtonItem& item) { return key == item.configKey; });
|
||||
return it == kGameCubeButtons.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
// "up" or "up,a" -> the OR of those GC button bits. Unknown names are skipped so
|
||||
// a typo costs one button instead of the whole macro.
|
||||
inline uint16_t GameCubeMaskFromKeys(std::string_view keys) {
|
||||
uint16_t mask = 0;
|
||||
size_t begin = 0;
|
||||
while (begin <= keys.size()) {
|
||||
const size_t comma = keys.find(',', begin);
|
||||
const std::string_view token =
|
||||
keys.substr(begin, comma == std::string_view::npos ? std::string_view::npos : comma - begin);
|
||||
if (const GameCubeButtonItem* item = FindGameCubeButton(token)) {
|
||||
mask |= static_cast<uint16_t>(item->padButton);
|
||||
}
|
||||
if (comma == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
begin = comma + 1;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
inline std::string GameCubeKeysFromMask(uint16_t mask) {
|
||||
std::string keys;
|
||||
for (const auto& item : kGameCubeButtons) {
|
||||
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!keys.empty()) {
|
||||
keys += ',';
|
||||
}
|
||||
keys += item.configKey;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
inline std::string GameCubeLabelsFromMask(uint16_t mask) {
|
||||
std::string labels;
|
||||
for (const auto& item : kGameCubeButtons) {
|
||||
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!labels.empty()) {
|
||||
labels += " + ";
|
||||
}
|
||||
labels += item.label;
|
||||
}
|
||||
return labels.empty() ? std::string("None") : labels;
|
||||
}
|
||||
|
||||
} // namespace ControllerNames
|
||||
@@ -0,0 +1,67 @@
|
||||
#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
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
// Dolphin-compatible input expressions.
|
||||
//
|
||||
// Values are doubles in Dolphin's ControlState style; a control counts as
|
||||
// pressed above kConditionThreshold. Timing matches Dolphin: wall-clock
|
||||
// seconds on a steady clock, so an expression copied from GCPadNew.ini
|
||||
// behaves the same here as it does there.
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace InputExpr {
|
||||
|
||||
inline constexpr double kConditionThreshold = 0.5;
|
||||
|
||||
// Resolves a backtick-quoted input name to its current value.
|
||||
using InputSource = std::function<double(const std::string&)>;
|
||||
|
||||
struct Node;
|
||||
|
||||
class Expression {
|
||||
public:
|
||||
Expression();
|
||||
~Expression();
|
||||
Expression(Expression&&) noexcept;
|
||||
Expression& operator=(Expression&&) noexcept;
|
||||
|
||||
// Returns false and fills error on a syntax problem.
|
||||
static bool Parse(const std::string& text, Expression& out, std::string& error);
|
||||
|
||||
bool Empty() const { return m_root == nullptr; }
|
||||
double Evaluate(const InputSource& source) const;
|
||||
|
||||
// Input names the expression references, for diagnostics.
|
||||
std::vector<std::string> ReferencedInputs() const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Node> m_root;
|
||||
};
|
||||
|
||||
// Parses a Dolphin GCPadNew.ini and returns the expression text for each
|
||||
// control of the requested pad, keyed by Dolphin's own control names
|
||||
// ("Buttons/A", "D-Pad/Up", "Triggers/L", ...). Returns false if the file
|
||||
// cannot be read or the section is missing.
|
||||
bool ReadDolphinConfig(const std::filesystem::path& path, int padIndex,
|
||||
std::vector<std::pair<std::string, std::string>>& controls,
|
||||
std::string& deviceName, std::string& error);
|
||||
|
||||
} // namespace InputExpr
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -89,6 +90,8 @@ struct RuntimeUserConfig {
|
||||
// comma-separated SDL-style physical button names ("south", or
|
||||
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
|
||||
std::array<std::optional<std::string>, 12> controllerButtons;
|
||||
std::optional<bool> rumbleEnabled;
|
||||
std::map<std::string, std::string> controllerExpressions;
|
||||
};
|
||||
|
||||
namespace RuntimeConfigFile {
|
||||
@@ -407,6 +410,17 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
|
||||
}
|
||||
|
||||
config.rumbleEnabled = FindConfigValue<bool>(document, "controller", "rumble");
|
||||
|
||||
if (const auto* section = document.contains("controller") ? &document.at("controller") : nullptr;
|
||||
section != nullptr && section->is_table()) {
|
||||
for (const auto& [key, value] : section->as_table()) {
|
||||
if (key.rfind("expr_", 0) == 0 && value.is_string()) {
|
||||
config.controllerExpressions[key] = value.as_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
|
||||
config.windowPosX = FindConfigInt(document, "video", "window_x");
|
||||
config.windowPosY = FindConfigInt(document, "video", "window_y");
|
||||
@@ -677,6 +691,25 @@ inline bool SetControllerButton(size_t index, std::string value) {
|
||||
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
|
||||
}
|
||||
|
||||
inline std::string ControllerExpression(const std::string& key) {
|
||||
const auto it = Get().controllerExpressions.find(key);
|
||||
return it == Get().controllerExpressions.end() ? std::string() : it->second;
|
||||
}
|
||||
|
||||
inline bool SetControllerExpression(const std::string& key, const std::string& value) {
|
||||
Mutable().controllerExpressions[key] = value;
|
||||
return WriteSetting("controller", key, FormatString(value));
|
||||
}
|
||||
|
||||
inline bool RumbleEnabled(bool fallback = true) {
|
||||
return Get().rumbleEnabled.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool SetRumbleEnabled(bool value) {
|
||||
Mutable().rumbleEnabled = value;
|
||||
return WriteSetting("controller", "rumble", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetAudioVolume(float value) {
|
||||
value = std::clamp(value, 0.0f, 1.0f);
|
||||
Mutable().audioVolume = value;
|
||||
|
||||
Reference in New Issue
Block a user