rawinput: distinguish between circular and linear analog input (#665)

## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Currently, all analogs are treated as circular values that wrap around.
This works for knobs and turntables, but can get weird for linear values
(like tilt sensors and sliders) especially once analog options are
enabled, such as sensitivity.

This PR groups analogs into 3 buckets: circular, linear (centered), and
linear (positive). Linear types get different algorithms applied to it
when options are set (e.g., sensitivity caps out at 0, 1 instead of
wrapping around).

In linear mode:
* Sensitivity value applies an exponential response curve (power curve)
to the original value.
* Multiplier/divisor serves as a linear cut-off.
* Relative mode works as you expect, but of course, values don't wrap
around.
* Invert and deadzone work as you expect (since they applied to the
controller input anyway)
* Smoothing is removed from the UI and has no effect.
* Delay is unaffected.

Also, fix a small bug with -/+ button not working for analog `Delay`
option.

## Testing
WIP
This commit is contained in:
bicarus
2026-05-01 19:02:32 -07:00
committed by GitHub
parent d9d5823fdb
commit af8d8dae9f
18 changed files with 324 additions and 166 deletions
+30 -14
View File
@@ -207,22 +207,38 @@ float Analog::applyMultiplier(float value) {
} }
float Analog::normalizeAnalogValue(float value) { float Analog::normalizeAnalogValue(float value) {
// effectively the same as fmodf(value, 1.f) if (getType() == GameAPI::Analogs::AnalogType::Circular) {
// for small values, this is MUCH faster than fmodf. // effectively the same as fmodf(value, 1.f)
float new_value = value; // for small values, this is MUCH faster than fmodf.
while (new_value > 1.f) { float new_value = value;
new_value -= 1.f; 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 Analog::applyDeadzone(float raw_value) {
float value = raw_value; float value = raw_value;
const auto deadzone = this->getDeadzone(); auto deadzone = this->getDeadzone();
if (deadzone > 0) {
// 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 // calculate values
const auto delta = value - 0.5f; const auto delta = value - 0.5f;
@@ -255,7 +271,7 @@ float Analog::applyDeadzone(float raw_value) {
} }
} }
} else if (deadzone < 0) { } else {
// invert for mirror // invert for mirror
if (this->getDeadzoneMirror()) { if (this->getDeadzoneMirror()) {
@@ -263,8 +279,8 @@ float Analog::applyDeadzone(float raw_value) {
} }
// deadzone from minimum value // deadzone from minimum value
if (deadzone > -1 && value > -deadzone) { if (deadzone < 1.f && deadzone < value) {
value = std::min(1.f, (value + deadzone) / (1.f + deadzone)); value = std::max(0.f, (value - deadzone) / (1.f - deadzone));
} else { } else {
value = 0.f; value = 0.f;
} }
+27 -2
View File
@@ -13,6 +13,21 @@ namespace rawinput {
class RawInputManager; 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 { struct AnalogMovingAverage {
double time_in_ms; double time_in_ms;
float sine; float sine;
@@ -31,10 +46,11 @@ private:
float last_state = 0.5f; float last_state = 0.5f;
bool sensitivity_set = false; bool sensitivity_set = false;
bool deadzone_set = false; bool deadzone_set = false;
GameAPI::Analogs::AnalogType type = GameAPI::Analogs::AnalogType::Circular;
// smoothing function // smoothing function
bool smoothing = false; bool smoothing = false;
std::array<AnalogMovingAverage, ANALOG_HISTORY_CNT> vector_history; std::array<AnalogMovingAverage, ANALOG_HISTORY_CNT> vector_history = {};
int vector_history_index = 0; int vector_history_index = 0;
float smoothed_last_state = 0.f; float smoothed_last_state = 0.f;
@@ -66,7 +82,8 @@ public:
float override_state = 0.5f; float override_state = 0.5f;
explicit Analog(std::string name) : name(std::move(name)) { 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); std::string getDisplayString(rawinput::RawInputManager* manager);
@@ -214,4 +231,12 @@ public:
inline std::queue<float> &getDelayBuffer() { inline std::queue<float> &getDelayBuffer() {
return this->delay_buffer; return this->delay_buffer;
} }
inline GameAPI::Analogs::AnalogType getType() const {
return this->type;
}
inline void setType(GameAPI::Analogs::AnalogType type) {
this->type = type;
}
}; };
+87 -2
View File
@@ -679,7 +679,9 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D
} }
// deadzone // 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); value = analog.applyDeadzone(value);
} }
@@ -704,7 +706,7 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D
// translate relative movement to absolute value // translate relative movement to absolute value
value = analog.getAbsoluteValue(relative_delta); value = analog.getAbsoluteValue(relative_delta);
} else { } else if (analog.getType() == AnalogType::Circular) {
// integer multiplier // integer multiplier
value = analog.applyMultiplier(value); value = analog.applyMultiplier(value);
@@ -732,6 +734,53 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D
// apply to value // apply to value
value = rads * (float) M_1_TAU; 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 // delay
@@ -839,6 +888,42 @@ std::vector<Analog> GameAPI::Analogs::sortAnalogs(
return sorted; return sorted;
} }
static std::vector<Analog> sortAnalogsWithTypeInternal(
std::vector<Analog> &analogs,
const std::initializer_list<GameAPI::Analogs::AnalogWithType> list) {
std::vector<Analog> 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<Analog> *analogs,
const std::initializer_list<AnalogWithType> list) {
if (analogs) {
*analogs = sortAnalogsWithTypeInternal(*analogs, list);
}
}
float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, Analog &analog) { float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, Analog &analog) {
// check override // check override
+8 -7
View File
@@ -85,14 +85,15 @@ namespace GameAPI {
const std::vector<Analog> &analogs, const std::vector<Analog> &analogs,
const std::vector<std::string> &analog_names); const std::vector<std::string> &analog_names);
template<typename T> struct AnalogWithType {
void sortAnalogs(std::vector<Analog> *analogs, T t) { const std::string name;
const std::vector<std::string> analog_names { t }; const AnalogType type;
if (analogs) { AnalogWithType(std::string name, AnalogType type) :
*analogs = GameAPI::Analogs::sortAnalogs(*analogs, analog_names); name(std::move(name)), type(type) {}
} };
}
void sortAnalogsWithType(std::vector<Analog> *analogs, const std::initializer_list<AnalogWithType> list);
template<typename T, typename... Rest> template<typename T, typename... Rest>
void sortAnalogs(std::vector<Analog> *analogs, T t, Rest... rest) { void sortAnalogs(std::vector<Analog> *analogs, T t, Rest... rest) {
+6 -5
View File
@@ -38,11 +38,12 @@ std::vector<Analog> &games::bc::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Busou Shinki: Armored Princess Battle Conductor"); analogs = GameAPI::Analogs::getAnalogs("Busou Shinki: Armored Princess Battle Conductor");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Stick X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Stick Y" { "Stick X", AnalogType::LinearCentered },
); { "Stick Y", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+8 -7
View File
@@ -45,13 +45,14 @@ std::vector<Analog> &games::ccj::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Chase Chase Jokers"); analogs = GameAPI::Analogs::getAnalogs("Chase Chase Jokers");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Joystick X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Joystick Y", { "Joystick X", AnalogType::LinearCentered },
"Trackball DX", { "Joystick Y", AnalogType::LinearCentered },
"Trackball DY" { "Trackball DX", AnalogType::LinearCentered },
); { "Trackball DY", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+8 -7
View File
@@ -55,13 +55,14 @@ std::vector<Analog> &games::ddr::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Dance Dance Revolution"); analogs = GameAPI::Analogs::getAnalogs("Dance Dance Revolution");
GameAPI::Analogs::sortAnalogs( using namespace GameAPI::Analogs;
&analogs,
"P1 Left-Right (Axis Fix)", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"P1 Up-Down (Axis Fix)", { "P1 Left-Right (Axis Fix)", AnalogType::LinearCentered },
"P2 Left-Right (Axis Fix)", { "P1 Up-Down (Axis Fix)", AnalogType::LinearCentered },
"P2 Up-Down (Axis Fix)" { "P2 Left-Right (Axis Fix)", AnalogType::LinearCentered },
); { "P2 Up-Down (Axis Fix)", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
} }
+8 -7
View File
@@ -35,13 +35,14 @@ std::vector<Analog> &games::ftt::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("FutureTomTom"); analogs = GameAPI::Analogs::getAnalogs("FutureTomTom");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Pad 1", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Pad 2", { "Pad 1", AnalogType::LinearPositive },
"Pad 3", { "Pad 2", AnalogType::LinearPositive },
"Pad 4" { "Pad 3", AnalogType::LinearPositive },
); { "Pad 4", AnalogType::LinearPositive }
});
} }
return analogs; return analogs;
+12 -10
View File
@@ -86,19 +86,21 @@ std::vector<Button> &games::gitadora::get_buttons() {
std::vector<Analog> &games::gitadora::get_analogs() { std::vector<Analog> &games::gitadora::get_analogs() {
static std::vector<Analog> analogs; static std::vector<Analog> analogs;
using namespace GameAPI::Analogs;
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("GitaDora"); analogs = GameAPI::Analogs::getAnalogs("GitaDora");
GameAPI::Analogs::sortAnalogs(&analogs, GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Guitar P1 Wail X", {"Guitar P1 Wail X", AnalogType::LinearCentered},
"Guitar P1 Wail Y", {"Guitar P1 Wail Y", AnalogType::LinearCentered},
"Guitar P1 Wail Z", {"Guitar P1 Wail Z", AnalogType::LinearCentered},
"Guitar P1 Knob", {"Guitar P1 Knob", AnalogType::Circular},
"Guitar P2 Wail X", {"Guitar P2 Wail X", AnalogType::LinearCentered},
"Guitar P2 Wail Y", {"Guitar P2 Wail Y", AnalogType::LinearCentered},
"Guitar P2 Wail Z", {"Guitar P2 Wail Z", AnalogType::LinearCentered},
"Guitar P2 Knob" {"Guitar P2 Knob", AnalogType::Circular}
); });
} }
return analogs; return analogs;
+11 -10
View File
@@ -63,16 +63,17 @@ std::vector<Analog> &games::iidx::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Beatmania IIDX"); analogs = GameAPI::Analogs::getAnalogs("Beatmania IIDX");
GameAPI::Analogs::sortAnalogs( using namespace GameAPI::Analogs;
&analogs,
"Turntable P1", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Turntable P2", { "Turntable P1", AnalogType::Circular },
"VEFX", { "Turntable P2", AnalogType::Circular },
"Low-EQ", { "VEFX", AnalogType::LinearPositive },
"Hi-EQ", { "Low-EQ", AnalogType::LinearPositive },
"Filter", { "Hi-EQ", AnalogType::LinearPositive },
"Play Volume" { "Filter", AnalogType::LinearPositive },
); { "Play Volume", AnalogType::LinearPositive }
});
} }
return analogs; return analogs;
} }
+5 -5
View File
@@ -36,11 +36,11 @@ std::vector<Analog> &games::mga::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Metal Gear"); analogs = GameAPI::Analogs::getAnalogs("Metal Gear");
GameAPI::Analogs::sortAnalogs( using namespace GameAPI::Analogs;
&analogs, GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Joy X", { "Joy X", AnalogType::LinearCentered },
"Joy Y" { "Joy Y", AnalogType::LinearCentered }
); });
} }
return analogs; return analogs;
+32 -31
View File
@@ -165,37 +165,38 @@ std::vector<Analog> &games::nost::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Nostalgia"); analogs = GameAPI::Analogs::getAnalogs("Nostalgia");
GameAPI::Analogs::sortAnalogs( using namespace GameAPI::Analogs;
&analogs,
"Key 1", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Key 2", { "Key 1", AnalogType::LinearPositive },
"Key 3", { "Key 2", AnalogType::LinearPositive },
"Key 4", { "Key 3", AnalogType::LinearPositive },
"Key 5", { "Key 4", AnalogType::LinearPositive },
"Key 6", { "Key 5", AnalogType::LinearPositive },
"Key 7", { "Key 6", AnalogType::LinearPositive },
"Key 8", { "Key 7", AnalogType::LinearPositive },
"Key 9", { "Key 8", AnalogType::LinearPositive },
"Key 10", { "Key 9", AnalogType::LinearPositive },
"Key 11", { "Key 10", AnalogType::LinearPositive },
"Key 12", { "Key 11", AnalogType::LinearPositive },
"Key 13", { "Key 12", AnalogType::LinearPositive },
"Key 14", { "Key 13", AnalogType::LinearPositive },
"Key 15", { "Key 14", AnalogType::LinearPositive },
"Key 16", { "Key 15", AnalogType::LinearPositive },
"Key 17", { "Key 16", AnalogType::LinearPositive },
"Key 18", { "Key 17", AnalogType::LinearPositive },
"Key 19", { "Key 18", AnalogType::LinearPositive },
"Key 20", { "Key 19", AnalogType::LinearPositive },
"Key 21", { "Key 20", AnalogType::LinearPositive },
"Key 22", { "Key 21", AnalogType::LinearPositive },
"Key 23", { "Key 22", AnalogType::LinearPositive },
"Key 24", { "Key 23", AnalogType::LinearPositive },
"Key 25", { "Key 24", AnalogType::LinearPositive },
"Key 26", { "Key 25", AnalogType::LinearPositive },
"Key 27", { "Key 26", AnalogType::LinearPositive },
"Key 28" { "Key 27", AnalogType::LinearPositive },
); { "Key 28", AnalogType::LinearPositive }
});
} }
return analogs; return analogs;
} }
+6 -5
View File
@@ -51,11 +51,12 @@ std::vector<Analog> &games::pc::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Polaris Chord"); analogs = GameAPI::Analogs::getAnalogs("Polaris Chord");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Fader-L", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Fader-R" { "Fader-L", AnalogType::LinearCentered },
); { "Fader-R", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+7 -6
View File
@@ -35,12 +35,13 @@ std::vector<Analog> &games::rf3d::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Road Fighters 3D"); analogs = GameAPI::Analogs::getAnalogs("Road Fighters 3D");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Wheel", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Accelerate", { "Wheel", AnalogType::LinearCentered },
"Brake" { "Accelerate", AnalogType::LinearPositive },
); { "Brake", AnalogType::LinearPositive }
});
} }
return analogs; return analogs;
+8 -7
View File
@@ -31,13 +31,14 @@ std::vector<Analog> &games::sc::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Steel Chronicle"); analogs = GameAPI::Analogs::getAnalogs("Steel Chronicle");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Left Stick X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Left Stick Y", { "Left Stick X", AnalogType::LinearCentered },
"Right Stick X", { "Left Stick Y", AnalogType::LinearCentered },
"Right Stick Y" { "Right Stick X", AnalogType::LinearCentered },
); { "Right Stick Y", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+6 -5
View File
@@ -31,11 +31,12 @@ std::vector<Analog> &games::silentscope::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Silent Scope: Bone Eater"); analogs = GameAPI::Analogs::getAnalogs("Silent Scope: Bone Eater");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Gun X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Gun Y" { "Gun X", AnalogType::LinearCentered },
); { "Gun Y", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+8 -7
View File
@@ -50,13 +50,14 @@ std::vector<Analog> &games::we::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Winning Eleven"); analogs = GameAPI::Analogs::getAnalogs("Winning Eleven");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Pad Stick Left X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Pad Stick Left Y", { "Pad Stick Left X", AnalogType::LinearCentered },
"Pad Stick Right X", { "Pad Stick Left Y", AnalogType::LinearCentered },
"Pad Stick Right Y" { "Pad Stick Right X", AnalogType::LinearCentered },
); { "Pad Stick Right Y", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+47 -29
View File
@@ -2506,31 +2506,45 @@ namespace overlay::windows {
const bool value_changed = const bool value_changed =
ImGui::SliderFloat("Sensitivity", &sensitivity, 0.f, 2.f, "%.3f"); ImGui::SliderFloat("Sensitivity", &sensitivity, 0.f, 2.f, "%.3f");
ImGui::SameLine(); ImGui::SameLine();
ImGui::HelpMarker( if (device->type == rawinput::HID && analog.getType() != GameAPI::Analogs::AnalogType::Circular) {
"Adjust floating point multiplier to relative movement.\n\n" ImGui::HelpMarker(
"Value is squared before being multiplied (e.g., 1.44 is 2x sensitivity, 2.00 is 4x).\n\n" "Adjust the analog sensitivity curve; "
"Dependent on how often the game polls for input. Intended for angular input (knobs, turntables)"); "values <1.0 are less sensitive around neutral, "
"values >1.0 are more sensitive.");
} else {
ImGui::HelpMarker(
"Adjust floating point multiplier to relative movement.\n\n"
"Value is squared before being multiplied (e.g., 1.44 is 2x sensitivity, 2.00 is 4x).");
}
if (value_changed) { if (value_changed) {
analog.setSensitivity(sensitivity * sensitivity); analog.setSensitivity(sensitivity * sensitivity);
} }
} }
if (device->type == rawinput::HID || device->type == rawinput::MIDI) {
// hide deadzone for circular analog since it doesn't make any sense (unless in relative mode)
if ((device->type == rawinput::HID || device->type == rawinput::MIDI) &&
((analog.getType() != GameAPI::Analogs::AnalogType::Circular) || analog.isRelativeMode())) {
auto deadzone = analog.getDeadzone(); auto deadzone = analog.getDeadzone();
// for back compat (before each analog had a type)
if (deadzone < 0.f) {
deadzone = -deadzone;
analog.setDeadzone(deadzone);
}
const bool value_changed = const bool value_changed =
ImGui::SliderFloat("Deadzone", &deadzone, -0.999f, 0.999f, "%.3f"); ImGui::SliderFloat("Deadzone", &deadzone, 0.f, 0.999f, "%.3f");
if (value_changed) { if (value_changed) {
analog.setDeadzone(deadzone); analog.setDeadzone(deadzone);
} }
ImGui::SameLine(); ImGui::SameLine();
ImGui::HelpMarker("Positive values specify a deadzone around the middle.\n" ImGui::HelpMarker("Specify the deadzone that gets applied to at-rest (neutral) value.");
"Negative values specify a deadzone from the minimum value.");
// deadzone mirror // deadzone mirror
bool deadzone_mirror = analog.getDeadzoneMirror(); bool deadzone_mirror = analog.getDeadzoneMirror();
ImGui::Checkbox("Deadzone Mirror", &deadzone_mirror); ImGui::Checkbox("Deadzone Mirror", &deadzone_mirror);
ImGui::SameLine(); ImGui::SameLine();
ImGui::HelpMarker("Positive deadzone values cut off at edges instead.\n" ImGui::HelpMarker("Apply deadzone to extreme value(s) instead of neutral.");
"Negative deadzone values cut off at maximum value instead.");
if (deadzone_mirror != analog.getDeadzoneMirror()) { if (deadzone_mirror != analog.getDeadzoneMirror()) {
analog.setDeadzoneMirror(deadzone_mirror); analog.setDeadzoneMirror(deadzone_mirror);
} }
@@ -2549,23 +2563,26 @@ namespace overlay::windows {
if (this->analogs_devices_selected >= 0) { if (this->analogs_devices_selected >= 0) {
const auto device = this->analogs_devices.at(this->analogs_devices_selected); const auto device = this->analogs_devices.at(this->analogs_devices_selected);
if (device->type == rawinput::HID) { if (device->type == rawinput::HID) {
// smoothing
bool smoothing = analog.getSmoothing(); if (analog.getType() == GameAPI::Analogs::AnalogType::Circular) {
ImGui::BeginDisabled(analog.isRelativeMode()); // smoothing
ImGui::Checkbox("Smooth Axis (adds latency)", &smoothing); bool smoothing = analog.getSmoothing();
ImGui::SameLine(); ImGui::BeginDisabled(analog.isRelativeMode());
ImGui::HelpMarker( ImGui::Checkbox("Smooth Axis (adds latency)", &smoothing);
"Apply a moving average algorithm; intended for angular input (knobs, turntables). " ImGui::SameLine();
"Adds a slight bit of latency to input as the algorithm averages out recent input. " ImGui::HelpMarker(
"Only use in dire situations where the input is too jittery for the game."); "Apply a moving average algorithm; intended for angular input (knobs, turntables). "
ImGui::EndDisabled(); "Adds a slight bit of latency to input as the algorithm averages out recent input. "
if (smoothing != analog.getSmoothing()) { "Only use in dire situations where the input is too jittery for the game.");
analog.setSmoothing(smoothing); ImGui::EndDisabled();
if (smoothing != analog.getSmoothing()) {
analog.setSmoothing(smoothing);
}
} }
// relative input mode // relative input mode
bool relative_analog = analog.isRelativeMode(); bool relative_analog = analog.isRelativeMode();
ImGui::Checkbox("Relative Axis (experimental)", &relative_analog); ImGui::Checkbox("Relative Axis", &relative_analog);
ImGui::SameLine(); ImGui::SameLine();
ImGui::HelpMarker( ImGui::HelpMarker(
"Use relative directional input instead of positional values.\n\n" "Use relative directional input instead of positional values.\n\n"
@@ -2579,8 +2596,7 @@ namespace overlay::windows {
// delay buffer // delay buffer
int delay = analog.getDelayBufferDepth(); int delay = analog.getDelayBufferDepth();
ImGui::InputInt("Delay (experimental)", &delay, 1, 10); if (ImGui::InputInt("Delay", &delay, 1, 10)) {
if (ImGui::IsItemDeactivatedAfterEdit()) {
delay = CLAMP(delay, 0, 256); delay = CLAMP(delay, 0, 256);
analog.setDelayBufferDepth(delay); analog.setDelayBufferDepth(delay);
} }
@@ -2600,10 +2616,12 @@ namespace overlay::windows {
ImGui::ProgressBar(value); ImGui::ProgressBar(value);
// centered knob preview // centered knob preview
const float knob_size = 64.f; if (analog.getType() == GameAPI::Analogs::AnalogType::Circular) {
auto width = ImGui::GetContentRegionAvail().x - knob_size; const float knob_size = 64.f;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (width / 2)); auto width = ImGui::GetContentRegionAvail().x - knob_size;
ImGui::Knob(value, knob_size); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (width / 2));
ImGui::Knob(value, knob_size);
}
// update analog // update analog
if (analogs_devices_selected >= 0 && analogs_devices_selected < (int) analogs_devices.size()) { if (analogs_devices_selected >= 0 && analogs_devices_selected < (int) analogs_devices.size()) {