mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-10 09:11:52 -04:00
be153e0fa0
* 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>
220 lines
8.2 KiB
C++
220 lines
8.2 KiB
C++
// Verifies the expression engine against Dolphin's documented semantics,
|
|
// including the exact line from the user's GCPadNew.ini.
|
|
|
|
#include "input_expr.h"
|
|
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <map>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
static int g_failures = 0;
|
|
static std::map<std::string, double> g_inputs;
|
|
|
|
static InputExpr::InputSource Source() {
|
|
return [](const std::string& name) {
|
|
const auto it = g_inputs.find(name);
|
|
return it == g_inputs.end() ? 0.0 : it->second;
|
|
};
|
|
}
|
|
|
|
static void Check(bool ok, const std::string& what) {
|
|
if (!ok) {
|
|
std::printf(" FAIL: %s\n", what.c_str());
|
|
++g_failures;
|
|
}
|
|
}
|
|
|
|
static InputExpr::Expression Compile(const std::string& text) {
|
|
InputExpr::Expression expr;
|
|
std::string error;
|
|
if (!InputExpr::Expression::Parse(text, expr, error)) {
|
|
std::printf(" FAIL: parse '%s': %s\n", text.c_str(), error.c_str());
|
|
++g_failures;
|
|
}
|
|
return expr;
|
|
}
|
|
|
|
static bool Pressed(const InputExpr::Expression& e) {
|
|
return e.Evaluate(Source()) > InputExpr::kConditionThreshold;
|
|
}
|
|
|
|
static void Sleep(int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); }
|
|
|
|
int main() {
|
|
std::printf("Dolphin expression engine\n");
|
|
|
|
// Operators: & is min, | is max, ! is 1-x, matching Dolphin.
|
|
g_inputs["A"] = 1.0;
|
|
g_inputs["B"] = 0.0;
|
|
Check(Pressed(Compile("`A`")), "bare input");
|
|
Check(!Pressed(Compile("!`A`")), "not");
|
|
Check(!Pressed(Compile("`A` & `B`")), "and is min");
|
|
Check(Pressed(Compile("`A` | `B`")), "or is max");
|
|
Check(Pressed(Compile("`A` ^ `B`")), "xor");
|
|
Check(!Pressed(Compile("`A` ^ `A`")), "xor of equal inputs is false");
|
|
|
|
// Precedence: & binds tighter than |, so this is A | (B & A).
|
|
g_inputs["B"] = 0.0;
|
|
Check(Pressed(Compile("`A` | `B` & `A`")), "& binds tighter than |");
|
|
|
|
// Parens and numeric literals.
|
|
Check(Pressed(Compile("(`B` | 1)")), "literal");
|
|
Check(Pressed(Compile("min(1, `A`)")), "min");
|
|
Check(!Pressed(Compile("min(0, `A`)")), "min with zero");
|
|
Check(Pressed(Compile("if(`A`, 1, 0)")), "if");
|
|
Check(Pressed(Compile("clamp(5, 0, 1)")), "clamp");
|
|
|
|
// toggle flips on each rising edge and holds between them.
|
|
auto toggle = Compile("toggle(`T`)");
|
|
g_inputs["T"] = 0.0;
|
|
toggle.Evaluate(Source());
|
|
g_inputs["T"] = 1.0;
|
|
Check(Pressed(toggle), "toggle on after first press");
|
|
g_inputs["T"] = 0.0;
|
|
Check(Pressed(toggle), "toggle stays on after release");
|
|
g_inputs["T"] = 1.0;
|
|
Check(!Pressed(toggle), "toggle off on second press");
|
|
|
|
// hold requires the input to be down for the full duration.
|
|
auto hold = Compile("hold(`H`, 0.05)");
|
|
g_inputs["H"] = 1.0;
|
|
Check(!Pressed(hold), "hold not satisfied immediately");
|
|
Sleep(70);
|
|
Check(Pressed(hold), "hold satisfied after the interval");
|
|
g_inputs["H"] = 0.0;
|
|
Check(!Pressed(hold), "hold clears on release");
|
|
|
|
// pulse fires for the given duration after a rising edge.
|
|
auto pulse = Compile("pulse(`P`, 0.05)");
|
|
g_inputs["P"] = 0.0;
|
|
pulse.Evaluate(Source());
|
|
g_inputs["P"] = 1.0;
|
|
Check(Pressed(pulse), "pulse fires on rising edge");
|
|
Sleep(80);
|
|
Check(!Pressed(pulse), "pulse expires");
|
|
|
|
// The timing-window idiom seen in shared Dolphin configs.
|
|
auto window = Compile("!pulse(`W`, 0.05) & pulse(`W`, 0.15)");
|
|
g_inputs["W"] = 0.0;
|
|
window.Evaluate(Source());
|
|
g_inputs["W"] = 1.0;
|
|
Check(!Pressed(window), "window closed before its start");
|
|
Sleep(90);
|
|
Check(Pressed(window), "window open between the two pulses");
|
|
Sleep(90);
|
|
Check(!Pressed(window), "window closed after its end");
|
|
|
|
// timer ramps 0..1 and wraps, so a threshold turns it into a square wave.
|
|
auto timer = Compile("`X` & timer(0.1)");
|
|
g_inputs["X"] = 1.0;
|
|
int high = 0;
|
|
int low = 0;
|
|
for (int i = 0; i < 40; ++i) {
|
|
(Pressed(timer) ? high : low)++;
|
|
Sleep(5);
|
|
}
|
|
Check(high > 5 && low > 5, "timer alternates high and low");
|
|
|
|
// The exact D-Pad/Up line from the user's GCPadNew.ini.
|
|
auto dolphinLine = Compile("`Hat 0 N` | `Button 4` & timer(0.01)");
|
|
g_inputs["Hat 0 N"] = 0.0;
|
|
g_inputs["Button 4"] = 0.0;
|
|
Check(!Pressed(dolphinLine), "idle with nothing held");
|
|
g_inputs["Hat 0 N"] = 1.0;
|
|
Check(Pressed(dolphinLine), "hat alone presses");
|
|
g_inputs["Hat 0 N"] = 0.0;
|
|
g_inputs["Button 4"] = 1.0;
|
|
high = low = 0;
|
|
for (int i = 0; i < 60; ++i) {
|
|
(Pressed(dolphinLine) ? high : low)++;
|
|
Sleep(2);
|
|
}
|
|
Check(high > 5 && low > 5, "LB alternates via timer(0.01)");
|
|
|
|
// Regression tests for the CodeRabbit findings on PR #89.
|
|
g_inputs["A"] = 1.0;
|
|
// clamp with reversed bounds: std::clamp is UB when lo > hi.
|
|
Check(Compile("clamp(0.5, 1, 0)").Evaluate(Source()) == 0.5, "clamp tolerates reversed bounds");
|
|
// deadzone(v, 1) would divide by zero.
|
|
{
|
|
const double v = Compile("deadzone(`A`, 1)").Evaluate(Source());
|
|
Check(std::isfinite(v), "deadzone with dz=1 stays finite");
|
|
}
|
|
// timer with a zero or negative period would produce inf or NaN.
|
|
for (const char* text : {"timer(0)", "timer(-1)"}) {
|
|
const double v = Compile(text).Evaluate(Source());
|
|
Check(std::isfinite(v), std::string(text) + " stays finite");
|
|
}
|
|
// Any non-finite result is squashed before it can reach the uint8_t cast.
|
|
for (const char* text : {"sqrt(0 - 1)", "pow(10, 10000)", "tan(1.5707963267948966)"}) {
|
|
const double v = Compile(text).Evaluate(Source());
|
|
Check(std::isfinite(v), std::string(text) + " is sanitised at the boundary");
|
|
}
|
|
|
|
// tap count is user authored; negative, huge and non-finite must not reach
|
|
// the unsigned conversion.
|
|
for (const char* text : {"tap(`A`, 0.2, -1)", "tap(`A`, 0.2, 999999999)", "tap(`A`, 0.2, 0)"}) {
|
|
InputExpr::Expression e;
|
|
std::string err;
|
|
Check(InputExpr::Expression::Parse(text, e, err), std::string("parse ") + text);
|
|
const double v = e.Evaluate(Source());
|
|
Check(std::isfinite(v), std::string(text) + " evaluates without UB");
|
|
}
|
|
|
|
// Exponent notation is not part of the number syntax, matching Dolphin's
|
|
// lexer; it is rejected rather than silently misparsed.
|
|
{
|
|
InputExpr::Expression e;
|
|
std::string err;
|
|
Check(!InputExpr::Expression::Parse("tap(`A`, 0.2, 1e30)", e, err), "exponent notation rejected");
|
|
}
|
|
|
|
// A zero divisor must not skip the left subtree: stateful functions there
|
|
// still need their per-frame update.
|
|
{
|
|
auto divToggle = Compile("toggle(`D`) / `Z`");
|
|
g_inputs["Z"] = 0.0;
|
|
g_inputs["D"] = 0.0;
|
|
divToggle.Evaluate(Source());
|
|
g_inputs["D"] = 1.0;
|
|
divToggle.Evaluate(Source()); // rising edge seen even though rhs is 0
|
|
g_inputs["D"] = 0.0;
|
|
g_inputs["Z"] = 1.0;
|
|
Check(divToggle.Evaluate(Source()) > InputExpr::kConditionThreshold,
|
|
"toggle still latched while the divisor was zero");
|
|
}
|
|
|
|
// smooth with a zero rate divides 0 by 0; NaN must not stick in the node.
|
|
{
|
|
auto sm = Compile("smooth(`A`, 0)");
|
|
g_inputs["A"] = 1.0;
|
|
sm.Evaluate(Source());
|
|
Sleep(5);
|
|
Check(std::isfinite(sm.Evaluate(Source())), "smooth with a zero rate stays finite");
|
|
}
|
|
|
|
// Referenced inputs, used for diagnostics in the UI.
|
|
const auto refs = dolphinLine.ReferencedInputs();
|
|
Check(refs.size() == 2, "two referenced inputs");
|
|
|
|
// Errors are reported, not silently swallowed.
|
|
InputExpr::Expression bad;
|
|
std::string error;
|
|
Check(!InputExpr::Expression::Parse("`A` & ", bad, error), "trailing operator rejected");
|
|
Check(!InputExpr::Expression::Parse("nope(1)", bad, error), "unknown function rejected");
|
|
Check(!InputExpr::Expression::Parse("(`A`", bad, error), "missing paren rejected");
|
|
Check(!InputExpr::Expression::Parse("`A", bad, error), "unterminated backtick rejected");
|
|
Check(InputExpr::Expression::Parse("", bad, error) && bad.Empty(), "empty parses to empty");
|
|
Check(!InputExpr::Expression::Parse("hold(`A`)", bad, error), "wrong arg count rejected");
|
|
|
|
if (g_failures == 0) {
|
|
std::printf("all checks passed\n");
|
|
return 0;
|
|
}
|
|
std::printf("%d check(s) failed\n", g_failures);
|
|
return 1;
|
|
}
|