Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion code/cmdline/cmdline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ cmdline_parm stretch_menu("-stretch_menu", nullptr, AT_NONE); // Cmdline_stretch
cmdline_parm capture_mouse("-capture_mouse", nullptr, AT_NONE); // Cmdline_capture_mouse
cmdline_parm vr("-vr", nullptr, AT_NONE);
cmdline_parm deadzone("-deadzone",
"Sets the joystick deadzone. Integer value from 0 to 100 as a percentage of the joystick's range (100% would make the stick do nothing). Disables deadzone slider in the in-game Options menu.", AT_INT); //Cmdline_deadzone
"Sets the joystick deadzone. Integer value from 0 to 100 as a percentage of the joystick's range (100% would make the stick do nothing). Disables deadzone slider in the in-game Options menu; a per-axis deadzone set there still applies to its axis.", AT_INT); //Cmdline_deadzone

int Cmdline_autopilot_interruptable = 1;
bool Cmdline_stretch_menu = false;
Expand Down
19 changes: 13 additions & 6 deletions code/controlconfig/controlsconfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -454,14 +454,21 @@ static int joy_get_unscaled_reading(int raw)
return (int) ((std::uint64_t) raw * (std::uint64_t) F1_0 / (std::uint64_t) rng); // convert to 0 - F1_0 range, 64bit ints used to avoid uint overflow
}

int joy_get_scaled_reading(int raw)
int joy_get_scaled_reading(int raw, int action)
{
int x, d, dead_zone, rng;
float percent, sensitivity_percent, non_sensitivity_percent;

const bool per_axis = action >= 0 && action < Action::NUM_VALUES;
const int axis_dead_zone = per_axis ? Joy_axis_dead_zone_size[action] : -1;
const int sensitivity =
(per_axis && Joy_axis_sensitivity[action] >= 0) ? Joy_axis_sensitivity[action] : Joy_sensitivity;

raw -= JOY_AXIS_CENTER;

if (Cmdline_deadzone >= 0) {
if (axis_dead_zone >= 0) {
dead_zone = (JOY_AXIS_MAX - JOY_AXIS_MIN) * axis_dead_zone / 100;
} else if (Cmdline_deadzone >= 0) {
dead_zone = (JOY_AXIS_MAX - JOY_AXIS_MIN) * Cmdline_deadzone / 200;
//Making this div by 200 is what allows us to have the granularity of 0 to 100 in the cmdline.
//This allows a larger deadzone than the original maximum, all the way to the stick's full range.
Expand All @@ -481,11 +488,11 @@ int joy_get_scaled_reading(int raw)
if (d > rng)
d = rng;

Assert(Joy_sensitivity >= 0 && Joy_sensitivity <= 9);
Assert(sensitivity >= 0 && sensitivity <= 9);

// compute percentages as a range between 0 and 1
sensitivity_percent = (float) Joy_sensitivity / 9.0f;
non_sensitivity_percent = (float) (9 - Joy_sensitivity) / 9.0f;
sensitivity_percent = (float) sensitivity / 9.0f;
non_sensitivity_percent = (float) (9 - sensitivity) / 9.0f;

// find percent of max axis is at
percent = (float) d / (float) rng;
Expand Down Expand Up @@ -3055,7 +3062,7 @@ void scale_invert(const CC_bind &bind,
case CC_TYPE_AXIS_BTN_NEG:
case CC_TYPE_AXIS_BTN_POS:
default:
dx = joy_get_scaled_reading(axis_in[cid][btn]);
dx = joy_get_scaled_reading(axis_in[cid][btn], action);
break;
}

Expand Down
8 changes: 6 additions & 2 deletions code/controlconfig/controlsconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,8 @@ extern int Failed_key_index;

extern int Joy_dead_zone_size;
extern int Joy_sensitivity;
extern int Joy_axis_dead_zone_size[Action::NUM_VALUES];
extern int Joy_axis_sensitivity[Action::NUM_VALUES];

extern int Control_config_overlay_id;

Expand Down Expand Up @@ -1037,11 +1039,13 @@ void control_config_clear_used_status();
/**
* @brief Applies sensitivity multiplier and deadzone histerisis to the raw axis value
*
* @param[in] raw The raw axis value to transform
* @param[in] raw The raw axis value to transform
* @param[in] action The axis action (Action::HEADING and so on) whose own deadzone and sensitivity apply when set,
* or -1 for the global ones
*
* @return The transformed value
*/
int joy_get_scaled_reading(int raw);
int joy_get_scaled_reading(int raw, int action = -1);

/**
* Lookups the given stringified enum to find its value
Expand Down
88 changes: 86 additions & 2 deletions code/controlconfig/controlsconfigcommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ auto DeadZoneOption = options::OptionBuilder<int>("Input.JoystickDeadZone",
.level(options::ExpertLevel::Beginner)
.default_val(10)
.bind_to(&Joy_dead_zone_size)
.importance(1)
.importance(11)
.flags({options::OptionFlags::RetailBuiltinOption})
.finish();

Expand All @@ -69,10 +69,94 @@ auto SensitivityOption = options::OptionBuilder<int>("Input.JoystickSensitivity"
.level(options::ExpertLevel::Beginner)
.default_val(9)
.bind_to(&Joy_sensitivity)
.importance(2)
.importance(12)
.flags({options::OptionFlags::RetailBuiltinOption})
.finish();

// -1 means "use the global setting above"
static_assert(Action::NUM_VALUES == 5, "give every axis action a -1 entry below");
int Joy_axis_dead_zone_size[Action::NUM_VALUES] = {-1, -1, -1, -1, -1};
int Joy_axis_sensitivity[Action::NUM_VALUES] = {-1, -1, -1, -1, -1};

static SCP_string axis_setting_display(const int& val)
{
return val < 0 ? SCP_string("Use Global") : std::to_string(val);
}

static std::shared_ptr<const options::Option<int>>
make_axis_option(const char* key, const char* title, const char* description, int max, int* dest, int importance)
{
return options::OptionBuilder<int>(key,
std::pair<const char*, int>{title, -1},
std::pair<const char*, int>{description, -1})
.category(std::make_pair("Input", 1827))
.range(-1, max)
.level(options::ExpertLevel::Advanced)
.default_val(-1)
.display(axis_setting_display)
.flags({options::OptionFlags::RangeTypeInteger})
.bind_to(dest)
.importance(importance)
.finish();
}

// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
static auto HeadingDeadZoneOption __UNUSED = make_axis_option("Input.HeadingDeadZone",
"Heading Deadzone",
"The deadzone for the axis bound to heading; Use Global follows Deadzone",
45,
&Joy_axis_dead_zone_size[Action::HEADING],
10);
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
static auto PitchDeadZoneOption __UNUSED = make_axis_option("Input.PitchDeadZone",
"Pitch Deadzone",
"The deadzone for the axis bound to pitch; Use Global follows Deadzone",
45,
&Joy_axis_dead_zone_size[Action::PITCH],
9);
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
static auto BankDeadZoneOption __UNUSED = make_axis_option("Input.BankDeadZone",
"Bank Deadzone",
"The deadzone for the axis bound to bank; Use Global follows Deadzone",
45,
&Joy_axis_dead_zone_size[Action::BANK],
8);
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
static auto RelThrottleDeadZoneOption __UNUSED = make_axis_option("Input.RelThrottleDeadZone",
"Relative Throttle Deadzone",
"The deadzone for the axis bound to relative throttle; Use Global follows Deadzone",
45,
&Joy_axis_dead_zone_size[Action::REL_THROTTLE],
7);
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
static auto HeadingSensitivityOption __UNUSED = make_axis_option("Input.HeadingSensitivity",
"Heading Sensitivity",
"The sensitivity for the axis bound to heading; Use Global follows Sensitivity",
9,
&Joy_axis_sensitivity[Action::HEADING],
6);
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
static auto PitchSensitivityOption __UNUSED = make_axis_option("Input.PitchSensitivity",
"Pitch Sensitivity",
"The sensitivity for the axis bound to pitch; Use Global follows Sensitivity",
9,
&Joy_axis_sensitivity[Action::PITCH],
5);
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
static auto BankSensitivityOption __UNUSED = make_axis_option("Input.BankSensitivity",
"Bank Sensitivity",
"The sensitivity for the axis bound to bank; Use Global follows Sensitivity",
9,
&Joy_axis_sensitivity[Action::BANK],
4);
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
static auto RelThrottleSensitivityOption __UNUSED = make_axis_option("Input.RelThrottleSensitivity",
"Relative Throttle Sensitivity",
"The sensitivity for the axis bound to relative throttle; Use Global follows Sensitivity",
9,
&Joy_axis_sensitivity[Action::REL_THROTTLE],
3);

//! arrays which hold the key mappings. The array index represents a key-independent action.
//! please use SPACES for aligning the fields of this array
//! When adding new controls, order that they show up is dependant on their location in IoActionId, not on their hardcoded locations here
Expand Down
177 changes: 177 additions & 0 deletions test/src/controlconfig/test_joy_scaling.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#include "cmdline/cmdline.h"
#include "controlconfig/controlsconfig.h"
#include "io/joy.h"
#include "options/Option.h"
#include "options/OptionsManager.h"

#include <gtest/gtest.h>

// not in a header; defined in controlsconfig.cpp
void scale_invert(const CC_bind& bind,
int action,
CC_type type,
float frame_time,
int (&axis_in)[CID_JOY_MAX + 1][JOY_NUM_AXES],
int* axis_out);

class JoyScalingTest : public ::testing::Test {
protected:
void SetUp() override
{
saved_dead_zone = Joy_dead_zone_size;
saved_sensitivity = Joy_sensitivity;
saved_cmdline_dead_zone = Cmdline_deadzone;
for (int i = 0; i < Action::NUM_VALUES; ++i) {
saved_axis_dead_zone[i] = Joy_axis_dead_zone_size[i];
saved_axis_sensitivity[i] = Joy_axis_sensitivity[i];
Joy_axis_dead_zone_size[i] = -1;
Joy_axis_sensitivity[i] = -1;
}
Joy_dead_zone_size = 10;
Joy_sensitivity = 9;
Cmdline_deadzone = -1;
}

void TearDown() override
{
// also here, so a failed assertion can't leave an override behind for later tests
options::OptionsManager::instance()->clearOverrides();
Joy_dead_zone_size = saved_dead_zone;
Joy_sensitivity = saved_sensitivity;
Cmdline_deadzone = saved_cmdline_dead_zone;
for (int i = 0; i < Action::NUM_VALUES; ++i) {
Joy_axis_dead_zone_size[i] = saved_axis_dead_zone[i];
Joy_axis_sensitivity[i] = saved_axis_sensitivity[i];
}
}

int saved_dead_zone = 0;
int saved_sensitivity = 0;
int saved_cmdline_dead_zone = 0;
int saved_axis_dead_zone[Action::NUM_VALUES] = {};
int saved_axis_sensitivity[Action::NUM_VALUES] = {};
};

// 10% of the axis range is 6553 units, so 5000 off center sits inside the global deadzone and 5% (3276) is not.
TEST_F(JoyScalingTest, AxisDeadZoneOverridesGlobal)
{
const int raw = JOY_AXIS_CENTER + 5000;
ASSERT_EQ(joy_get_scaled_reading(raw), 0);
ASSERT_EQ(joy_get_scaled_reading(raw, Action::BANK), 0);

Joy_axis_dead_zone_size[Action::BANK] = 5;
ASSERT_GT(joy_get_scaled_reading(raw, Action::BANK), 0);
ASSERT_EQ(joy_get_scaled_reading(raw, Action::HEADING), 0);
ASSERT_EQ(joy_get_scaled_reading(raw), 0);

Joy_axis_dead_zone_size[Action::HEADING] = 0;
ASSERT_GT(joy_get_scaled_reading(JOY_AXIS_CENTER + 1, Action::HEADING), 0);
}

TEST_F(JoyScalingTest, AxisDeadZoneWinsOverCommandLine)
{
const int raw = JOY_AXIS_CENTER + 5000;
Cmdline_deadzone = 0;
ASSERT_GT(joy_get_scaled_reading(raw, Action::PITCH), 0);

Joy_axis_dead_zone_size[Action::PITCH] = 20;
ASSERT_EQ(joy_get_scaled_reading(raw, Action::PITCH), 0);
}

TEST_F(JoyScalingTest, AxisSensitivityOverridesGlobal)
{
const int raw = JOY_AXIS_CENTER + JOY_AXIS_RANGE / 2;
const int global = joy_get_scaled_reading(raw, Action::PITCH);

Joy_axis_sensitivity[Action::PITCH] = 0;
const int low = joy_get_scaled_reading(raw, Action::PITCH);
ASSERT_LT(low, global);
ASSERT_EQ(joy_get_scaled_reading(raw, Action::HEADING), global);

Joy_sensitivity = 0;
ASSERT_EQ(joy_get_scaled_reading(raw), low);
Joy_axis_sensitivity[Action::PITCH] = 9;
ASSERT_EQ(joy_get_scaled_reading(raw, Action::PITCH), global);
}

TEST_F(JoyScalingTest, NegativeDeflectionIsSymmetric)
{
Joy_axis_dead_zone_size[Action::BANK] = 5;
Joy_axis_sensitivity[Action::BANK] = 4;
const int offset = JOY_AXIS_RANGE / 3;
ASSERT_EQ(joy_get_scaled_reading(JOY_AXIS_CENTER - offset, Action::BANK),
-joy_get_scaled_reading(JOY_AXIS_CENTER + offset, Action::BANK));
}

TEST_F(JoyScalingTest, AxisDeadZoneUsesTheGlobalScale)
{
for (int dz : {0, 10, 20, 45}) {
for (int offset : {1000, 5000, 9000, 15000, 30000}) {
Joy_dead_zone_size = dz;
const int global = joy_get_scaled_reading(JOY_AXIS_CENTER + offset);
Joy_dead_zone_size = 10;
Joy_axis_dead_zone_size[Action::HEADING] = dz;
ASSERT_EQ(joy_get_scaled_reading(JOY_AXIS_CENTER + offset, Action::HEADING), global)
<< "deadzone " << dz << ", offset " << offset;
Joy_axis_dead_zone_size[Action::HEADING] = -1;
}
}
}

TEST_F(JoyScalingTest, ActionsOutsideTheRangeUseTheGlobalSettings)
{
for (int i = 0; i < Action::NUM_VALUES; ++i) {
Joy_axis_dead_zone_size[i] = 0;
Joy_axis_sensitivity[i] = 0;
}
const int raw = JOY_AXIS_CENTER + 5000;
ASSERT_EQ(joy_get_scaled_reading(raw, -1), 0);
ASSERT_EQ(joy_get_scaled_reading(raw, Action::NUM_VALUES), 0);
ASSERT_GT(joy_get_scaled_reading(raw, Action::NUM_VALUES - 1), 0);
}

TEST_F(JoyScalingTest, BoundAxisUsesItsActionSettings)
{
int axis_in[CID_JOY_MAX + 1][JOY_NUM_AXES] = {};
int axis_out[Action::NUM_VALUES] = {};
axis_in[CID_JOY1][JOY_X_AXIS] = JOY_AXIS_CENTER + 5000;
const CC_bind bind(CID_JOY1, JOY_X_AXIS);

scale_invert(bind, Action::BANK, CC_TYPE_AXIS_REL, 0.016f, axis_in, axis_out);
ASSERT_EQ(axis_out[Action::BANK], 0);

Joy_axis_dead_zone_size[Action::BANK] = 5;
scale_invert(bind, Action::BANK, CC_TYPE_AXIS_REL, 0.016f, axis_in, axis_out);
ASSERT_GT(axis_out[Action::BANK], 0);
}

TEST_F(JoyScalingTest, OptionsBindToTheirOwnAction)
{
const struct {
const char* key;
int* dest;
} options_to_check[] = {
{"Input.HeadingDeadZone", &Joy_axis_dead_zone_size[Action::HEADING]},
{"Input.PitchDeadZone", &Joy_axis_dead_zone_size[Action::PITCH]},
{"Input.BankDeadZone", &Joy_axis_dead_zone_size[Action::BANK]},
{"Input.RelThrottleDeadZone", &Joy_axis_dead_zone_size[Action::REL_THROTTLE]},
{"Input.HeadingSensitivity", &Joy_axis_sensitivity[Action::HEADING]},
{"Input.PitchSensitivity", &Joy_axis_sensitivity[Action::PITCH]},
{"Input.BankSensitivity", &Joy_axis_sensitivity[Action::BANK]},
{"Input.RelThrottleSensitivity", &Joy_axis_sensitivity[Action::REL_THROTTLE]},
};
auto* mgr = options::OptionsManager::instance();
for (const auto& entry : options_to_check) {
auto* opt = mgr->getOptionByKey(entry.key);
ASSERT_NE(opt, nullptr) << entry.key;
ASSERT_TRUE(opt->getFlags()[options::OptionFlags::RangeTypeInteger]) << entry.key;

mgr->setOverride(entry.key, "7", "test");
opt->loadInitial();
for (const auto& other : options_to_check) {
ASSERT_EQ(*other.dest, other.dest == entry.dest ? 7 : -1) << entry.key << " set " << other.key;
}
mgr->clearOverrides();
*entry.dest = -1;
}
}
4 changes: 4 additions & 0 deletions test/src/source_groups.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ add_file_folder("CFile"
cfile/cfile.cpp
)

add_file_folder("ControlConfig"
controlconfig/test_joy_scaling.cpp
)

add_file_folder("Globalincs"
globalincs/test_flagset.cpp
globalincs/test_safe_strings.cpp
Expand Down
Loading