diff --git a/src/spice2x/cfg/analog.cpp b/src/spice2x/cfg/analog.cpp index e8c6ba9..2c3815f 100644 --- a/src/spice2x/cfg/analog.cpp +++ b/src/spice2x/cfg/analog.cpp @@ -207,22 +207,38 @@ float Analog::applyMultiplier(float value) { } float Analog::normalizeAnalogValue(float value) { - // effectively the same as fmodf(value, 1.f) - // for small values, this is MUCH faster than fmodf. - float new_value = value; - while (new_value > 1.f) { - new_value -= 1.f; + if (getType() == GameAPI::Analogs::AnalogType::Circular) { + // effectively the same as fmodf(value, 1.f) + // for small values, this is MUCH faster than fmodf. + float new_value = value; + while (new_value > 1.f) { + new_value -= 1.f; + } + while (new_value < 0.f) { + new_value += 1.f; + } + return new_value; + + } else { + // clamp to [0, 1] range + return std::clamp(value, 0.f, 1.f); } - while (new_value < 0.f) { - new_value += 1.f; - } - return new_value; } float Analog::applyDeadzone(float raw_value) { float value = raw_value; - const auto deadzone = this->getDeadzone(); - if (deadzone > 0) { + auto deadzone = this->getDeadzone(); + + // in the past, positive deadzone applied in the center, negative deadzone applied to 0 + // after each analog value received a type (circular/linear) this has been simpliifed to + // positive values only since we can figure out where the rest value is + // for back compat, treat negative value as positive + if (deadzone < 0.f) { + deadzone = -deadzone; + } + + // relative mode assumes that user is using a stick, so center is neutral regardless of analog type + if (getType() != GameAPI::Analogs::AnalogType::LinearPositive || isRelativeMode()) { // calculate values const auto delta = value - 0.5f; @@ -255,7 +271,7 @@ float Analog::applyDeadzone(float raw_value) { } } - } else if (deadzone < 0) { + } else { // invert for mirror if (this->getDeadzoneMirror()) { @@ -263,8 +279,8 @@ float Analog::applyDeadzone(float raw_value) { } // deadzone from minimum value - if (deadzone > -1 && value > -deadzone) { - value = std::min(1.f, (value + deadzone) / (1.f + deadzone)); + if (deadzone < 1.f && deadzone < value) { + value = std::max(0.f, (value - deadzone) / (1.f - deadzone)); } else { value = 0.f; } diff --git a/src/spice2x/cfg/analog.h b/src/spice2x/cfg/analog.h index b3f4738..4900b4a 100644 --- a/src/spice2x/cfg/analog.h +++ b/src/spice2x/cfg/analog.h @@ -13,6 +13,21 @@ namespace rawinput { class RawInputManager; } +namespace GameAPI::Analogs { + enum class AnalogType { + // default; values wrap around (below 0 turns into 1, over 1 is 0) + // knobs, turntables + Circular = 0, + + // typical joystick that rests at the center and caps at [0, 1] + LinearCentered = 1, + + // one-directional value (sliders and instruments like piano/drum velocity) + // starts at 0 and goes up to 1 + LinearPositive = 2, + }; +} + struct AnalogMovingAverage { double time_in_ms; float sine; @@ -31,10 +46,11 @@ private: float last_state = 0.5f; bool sensitivity_set = false; bool deadzone_set = false; + GameAPI::Analogs::AnalogType type = GameAPI::Analogs::AnalogType::Circular; // smoothing function bool smoothing = false; - std::array vector_history; + std::array vector_history = {}; int vector_history_index = 0; float smoothed_last_state = 0.f; @@ -66,7 +82,8 @@ public: float override_state = 0.5f; explicit Analog(std::string name) : name(std::move(name)) { - vector_history.fill({0.0, 0.f, 0.f}); + }; + explicit Analog(std::string name, GameAPI::Analogs::AnalogType type) : name(std::move(name)), type(type) { }; std::string getDisplayString(rawinput::RawInputManager* manager); @@ -214,4 +231,12 @@ public: inline std::queue &getDelayBuffer() { return this->delay_buffer; } + + inline GameAPI::Analogs::AnalogType getType() const { + return this->type; + } + + inline void setType(GameAPI::Analogs::AnalogType type) { + this->type = type; + } }; diff --git a/src/spice2x/cfg/api.cpp b/src/spice2x/cfg/api.cpp index 0961f2e..6399f9a 100644 --- a/src/spice2x/cfg/api.cpp +++ b/src/spice2x/cfg/api.cpp @@ -679,7 +679,9 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D } // deadzone - if (analog.isDeadzoneSet()) { + // do not apply deadzone to circular analogs since it doesn't make sense (except in relative mode) + if (analog.isDeadzoneSet() && + (analog.getType() != AnalogType::Circular || analog.isRelativeMode())) { value = analog.applyDeadzone(value); } @@ -704,7 +706,7 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D // translate relative movement to absolute value value = analog.getAbsoluteValue(relative_delta); - } else { + } else if (analog.getType() == AnalogType::Circular) { // integer multiplier value = analog.applyMultiplier(value); @@ -732,6 +734,53 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D // apply to value value = rads * (float) M_1_TAU; } + } else { + // sensitivity + if (analog.isSensitivitySet()) { + // adjust curve + // values < 1.f : less sensitive around neutral + // values > 1.f : more sensitive around neutral + float curve = analog.getSensitivity(); + if (curve <= 0.f) { + curve = 0.01f; + } + curve = 1.f / curve; + + if (analog.getType() == AnalogType::LinearCentered) { + // convert 0.0..1.0 to -1.0..+1.0 + float signed_raw = (value - 0.5f) * 2.0f; + // apply curve + float sign = signed_raw < 0.0f ? -1.0f : 1.0f; + float magnitude = fabsf(signed_raw); + float curved = sign * powf(magnitude, curve); + // convert back to 0.0..1.0 + value = curved * 0.5f + 0.5f; + } else { + value = powf(value, curve); + } + + value = std::clamp(value, 0.f, 1.f); + } + + // multiplier / divisor + if (analog.getMultiplier() < -1) { + if (analog.getType() == AnalogType::LinearCentered) { + value = (value - 0.5f) / (-analog.getMultiplier()) + 0.5f; + } else { + value /= -analog.getMultiplier(); + } + + value = std::clamp(value, 0.f, 1.f); + + } else if (analog.getMultiplier() > 1) { + if (analog.getType() == AnalogType::LinearCentered) { + value = (value - 0.5f) * analog.getMultiplier() + 0.5f; + } else { + value *= analog.getMultiplier(); + } + + value = std::clamp(value, 0.f, 1.f); + } } // delay @@ -839,6 +888,42 @@ std::vector GameAPI::Analogs::sortAnalogs( return sorted; } +static std::vector sortAnalogsWithTypeInternal( + std::vector &analogs, + const std::initializer_list list) { + + std::vector sorted; + + bool analog_found; + for (auto &a : list) { + analog_found = false; + + for (auto &analog : analogs) { + if (a.name == analog.getName()) { + analog_found = true; + analog.setType(a.type); + sorted.push_back(analog); + break; + } + } + + if (!analog_found) { + sorted.emplace_back(a.name, a.type); + } + } + + return sorted; +} + +void GameAPI::Analogs::sortAnalogsWithType( + std::vector *analogs, + const std::initializer_list list) { + + if (analogs) { + *analogs = sortAnalogsWithTypeInternal(*analogs, list); + } +} + float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, Analog &analog) { // check override diff --git a/src/spice2x/cfg/api.h b/src/spice2x/cfg/api.h index e8ed32f..ea3960f 100644 --- a/src/spice2x/cfg/api.h +++ b/src/spice2x/cfg/api.h @@ -85,14 +85,15 @@ namespace GameAPI { const std::vector &analogs, const std::vector &analog_names); - template - void sortAnalogs(std::vector *analogs, T t) { - const std::vector analog_names { t }; + struct AnalogWithType { + const std::string name; + const AnalogType type; - if (analogs) { - *analogs = GameAPI::Analogs::sortAnalogs(*analogs, analog_names); - } - } + AnalogWithType(std::string name, AnalogType type) : + name(std::move(name)), type(type) {} + }; + + void sortAnalogsWithType(std::vector *analogs, const std::initializer_list list); template void sortAnalogs(std::vector *analogs, T t, Rest... rest) { diff --git a/src/spice2x/games/bc/io.cpp b/src/spice2x/games/bc/io.cpp index 2843009..ffbf6d0 100644 --- a/src/spice2x/games/bc/io.cpp +++ b/src/spice2x/games/bc/io.cpp @@ -38,11 +38,12 @@ std::vector &games::bc::get_analogs() { if (analogs.empty()) { analogs = GameAPI::Analogs::getAnalogs("Busou Shinki: Armored Princess Battle Conductor"); - GameAPI::Analogs::sortAnalogs( - &analogs, - "Stick X", - "Stick Y" - ); + using GameAPI::Analogs::AnalogType; + + GameAPI::Analogs::sortAnalogsWithType(&analogs, { + { "Stick X", AnalogType::LinearCentered }, + { "Stick Y", AnalogType::LinearCentered } + }); } return analogs; diff --git a/src/spice2x/games/ccj/io.cpp b/src/spice2x/games/ccj/io.cpp index d3e4abb..18dd715 100644 --- a/src/spice2x/games/ccj/io.cpp +++ b/src/spice2x/games/ccj/io.cpp @@ -45,13 +45,14 @@ std::vector &games::ccj::get_analogs() { if (analogs.empty()) { analogs = GameAPI::Analogs::getAnalogs("Chase Chase Jokers"); - GameAPI::Analogs::sortAnalogs( - &analogs, - "Joystick X", - "Joystick Y", - "Trackball DX", - "Trackball DY" - ); + using GameAPI::Analogs::AnalogType; + + GameAPI::Analogs::sortAnalogsWithType(&analogs, { + { "Joystick X", AnalogType::LinearCentered }, + { "Joystick Y", AnalogType::LinearCentered }, + { "Trackball DX", AnalogType::LinearCentered }, + { "Trackball DY", AnalogType::LinearCentered } + }); } return analogs; diff --git a/src/spice2x/games/ddr/io.cpp b/src/spice2x/games/ddr/io.cpp index 4450497..2bb1e5f 100644 --- a/src/spice2x/games/ddr/io.cpp +++ b/src/spice2x/games/ddr/io.cpp @@ -55,13 +55,14 @@ std::vector &games::ddr::get_analogs() { if (analogs.empty()) { analogs = GameAPI::Analogs::getAnalogs("Dance Dance Revolution"); - GameAPI::Analogs::sortAnalogs( - &analogs, - "P1 Left-Right (Axis Fix)", - "P1 Up-Down (Axis Fix)", - "P2 Left-Right (Axis Fix)", - "P2 Up-Down (Axis Fix)" - ); + using namespace GameAPI::Analogs; + + GameAPI::Analogs::sortAnalogsWithType(&analogs, { + { "P1 Left-Right (Axis Fix)", AnalogType::LinearCentered }, + { "P1 Up-Down (Axis Fix)", AnalogType::LinearCentered }, + { "P2 Left-Right (Axis Fix)", AnalogType::LinearCentered }, + { "P2 Up-Down (Axis Fix)", AnalogType::LinearCentered } + }); } return analogs; } diff --git a/src/spice2x/games/ftt/io.cpp b/src/spice2x/games/ftt/io.cpp index e9af3c9..51dcada 100644 --- a/src/spice2x/games/ftt/io.cpp +++ b/src/spice2x/games/ftt/io.cpp @@ -35,13 +35,14 @@ std::vector &games::ftt::get_analogs() { if (analogs.empty()) { analogs = GameAPI::Analogs::getAnalogs("FutureTomTom"); - GameAPI::Analogs::sortAnalogs( - &analogs, - "Pad 1", - "Pad 2", - "Pad 3", - "Pad 4" - ); + using GameAPI::Analogs::AnalogType; + + GameAPI::Analogs::sortAnalogsWithType(&analogs, { + { "Pad 1", AnalogType::LinearPositive }, + { "Pad 2", AnalogType::LinearPositive }, + { "Pad 3", AnalogType::LinearPositive }, + { "Pad 4", AnalogType::LinearPositive } + }); } return analogs; diff --git a/src/spice2x/games/gitadora/io.cpp b/src/spice2x/games/gitadora/io.cpp index 252b7f8..67a196d 100644 --- a/src/spice2x/games/gitadora/io.cpp +++ b/src/spice2x/games/gitadora/io.cpp @@ -86,19 +86,21 @@ std::vector