Joystick handling refactored for precision

Changed 6 cvars for stick layout to 1; includes menu option
Axial deadzone replaced with radial one, 6 cvars to 2
Expo different for each thumbstick (1 cvar to 2); sliders included
Expo applied to full joystick vector magnitude, instead of per axis
Deleted "joystick_up" ("updown" function) and its sensitivity
This commit is contained in:
Jaime Moreira 2022-08-24 12:30:39 -04:00
parent 301a2ce735
commit f3fcf3fc24
4 changed files with 243 additions and 200 deletions

View file

@ -470,6 +470,22 @@ Set `0` by default.
`2` to use the Guide/Home/PS button. Requires a game restart
(or controller replug) when changed.
* **joy_layout**: Allows to select the stick layout of the gamepad.
- `0`: *Default*, left stick moves, right aims
- `1`: *Southpaw*, same as previous one with inverted sticks
- `2`: *Legacy*, left moves forward/backward and turns, right strafes
and looks up/down
- `3`: *Legacy Southpaw*, inverted sticks version of previous one
* **joy_left_deadzone** / **joy_right_deadzone**: Inner, circular
deadzone for each stick, where inputs below this radius will be
ignored. Default is `0.16` (16% of possible stick travel).
* **joy_left_expo** / **joy_right_expo**: Exponents on the response
curve on each stick. Increasing this will make small movements to
represent much smaller inputs, which helps precision with the sticks.
`1.0` is linear. Default `2.0` (quadratic curve).
* **gyro_mode**: Operation mode for the gyroscope sensor of the game
controller. Options are `0` = always off, `1` = off with the
`+gyroaction` bind to enable, `2` = on with `+gyroaction` to

View file

@ -294,6 +294,7 @@ extern cvar_t *cl_shownet;
extern cvar_t *cl_showmiss;
extern cvar_t *cl_showclamp;
extern cvar_t *lookstrafe;
extern cvar_t *joy_layout;
extern cvar_t *gyro_mode;
extern cvar_t *gyro_turning_axis;
extern cvar_t *m_pitch;

View file

@ -18,7 +18,10 @@
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
* 02111-1307, USA.
*
* Joystick threshold code is partially based on http://ioquake3.org code.
* Joystick reading and deadzone handling is based on:
* http://joshsutphin.com/2013/04/12/doing-thumbstick-dead-zones-right.html
* ...and implementation is partially based on code from:
* - http://quakespasm.sourceforge.net
*
* =======================================================================
*
@ -43,6 +46,19 @@
// ----
enum {
LAYOUT_DEFAULT = 0,
LAYOUT_SOUTHPAW,
LAYOUT_LEGACY,
LAYOUT_LEGACY_SOUTHPAW
};
typedef struct
{
float x;
float y;
} thumbstick_t;
typedef enum
{
REASON_NONE,
@ -57,9 +73,7 @@ typedef enum
// actual movement functions called at a later time.
static float mouse_x, mouse_y;
static int sdl_back_button = SDL_CONTROLLER_BUTTON_BACK;
static float joystick_yaw, joystick_pitch;
static float joystick_forwardmove, joystick_sidemove;
static float joystick_up;
static int joystick_left_x, joystick_left_y, joystick_right_x, joystick_right_y;
static float gyro_yaw, gyro_pitch;
static qboolean mlooking;
@ -118,24 +132,13 @@ static cvar_t *joy_yawsensitivity;
static cvar_t *joy_pitchsensitivity;
static cvar_t *joy_forwardsensitivity;
static cvar_t *joy_sidesensitivity;
static cvar_t *joy_upsensitivity;
static cvar_t *joy_expo;
// Joystick direction settings
static cvar_t *joy_axis_leftx;
static cvar_t *joy_axis_lefty;
static cvar_t *joy_axis_rightx;
static cvar_t *joy_axis_righty;
static cvar_t *joy_axis_triggerleft;
static cvar_t *joy_axis_triggerright;
// Joystick threshold settings
static cvar_t *joy_axis_leftx_threshold;
static cvar_t *joy_axis_lefty_threshold;
static cvar_t *joy_axis_rightx_threshold;
static cvar_t *joy_axis_righty_threshold;
static cvar_t *joy_axis_triggerleft_threshold;
static cvar_t *joy_axis_triggerright_threshold;
// Joystick's analog sticks configuration
cvar_t *joy_layout;
static cvar_t *joy_left_expo;
static cvar_t *joy_left_deadzone;
static cvar_t *joy_right_expo;
static cvar_t *joy_right_deadzone;
// Joystick haptic
static cvar_t *joy_haptic_magnitude;
@ -171,8 +174,7 @@ static unsigned short int updates_countdown = 30;
// Reason for the countdown
static updates_countdown_reasons countdown_reason = REASON_CONTROLLERINIT;
// Factors used to transform from SDL input to Q2 "view angle" change
#define NORMALIZE_SDL_AXIS (1.0f/32768.0f)
// Factor used to transform from SDL input to Q2 "view angle" change
static float normalize_sdl_gyro = 1.0f / M_PI; // can change depending on hardware
extern void CalibrationFinishedCallback(void);
@ -689,133 +691,51 @@ IN_Update(void)
case SDL_CONTROLLERAXISMOTION: /* Handle Controller Motion */
{
char *direction_type;
float threshold = 0;
float fix_value = 0;
int axis_value = event.caxis.value;
switch (event.caxis.axis)
{
/* left/right */
case SDL_CONTROLLER_AXIS_LEFTX:
direction_type = joy_axis_leftx->string;
threshold = joy_axis_leftx_threshold->value;
break;
/* top/bottom */
case SDL_CONTROLLER_AXIS_LEFTY:
direction_type = joy_axis_lefty->string;
threshold = joy_axis_lefty_threshold->value;
break;
/* second left/right */
case SDL_CONTROLLER_AXIS_RIGHTX:
direction_type = joy_axis_rightx->string;
threshold = joy_axis_rightx_threshold->value;
break;
/* second top/bottom */
case SDL_CONTROLLER_AXIS_RIGHTY:
direction_type = joy_axis_righty->string;
threshold = joy_axis_righty_threshold->value;
break;
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
direction_type = joy_axis_triggerleft->string;
threshold = joy_axis_triggerleft_threshold->value;
{
qboolean new_left_trigger = axis_value > 8192;
if (new_left_trigger != left_trigger)
{
left_trigger = new_left_trigger;
Key_Event(K_TRIG_LEFT, left_trigger, true);
}
break;
}
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
direction_type = joy_axis_triggerright->string;
threshold = joy_axis_triggerright_threshold->value;
{
qboolean new_right_trigger = axis_value > 8192;
if (new_right_trigger != right_trigger)
{
right_trigger = new_right_trigger;
Key_Event(K_TRIG_RIGHT, right_trigger, true);
}
break;
default:
direction_type = "none";
}
if (threshold > 0.9)
{
threshold = 0.9;
}
if (axis_value < 0 && (axis_value > (32768 * threshold)))
{
axis_value = 0;
}
else if (axis_value > 0 && (axis_value < (32768 * threshold)))
{
axis_value = 0;
}
// Smoothly ramp from dead zone to maximum value (from ioquake)
// https://github.com/ioquake/ioq3/blob/master/code/sdl/sdl_input.c
fix_value = ((float) abs(axis_value) / 32767.0f - threshold) / (1.0f - threshold);
if (fix_value < 0.0f)
{
fix_value = 0.0f;
}
// Apply expo
fix_value = pow(fix_value, joy_expo->value);
axis_value = (int) (32767 * ((axis_value < 0) ? -fix_value : fix_value));
if (cls.key_dest == key_game && (int) cl_paused->value == 0)
{
if (strcmp(direction_type, "sidemove") == 0)
{
joystick_sidemove = axis_value * joy_sidesensitivity->value;
// We need to be twice faster because with joystic we run...
joystick_sidemove *= cl_sidespeed->value * 2.0f;
}
else if (strcmp(direction_type, "forwardmove") == 0)
{
joystick_forwardmove = axis_value * joy_forwardsensitivity->value;
// We need to be twice faster because with joystic we run...
joystick_forwardmove *= cl_forwardspeed->value * 2.0f;
}
else if (strcmp(direction_type, "yaw") == 0)
{
joystick_yaw = axis_value * joy_yawsensitivity->value;
joystick_yaw *= cl_yawspeed->value;
}
else if (strcmp(direction_type, "pitch") == 0)
{
joystick_pitch = axis_value * joy_pitchsensitivity->value;
joystick_pitch *= cl_pitchspeed->value;
}
else if (strcmp(direction_type, "updown") == 0)
{
joystick_up = axis_value * joy_upsensitivity->value;
joystick_up *= cl_upspeed->value;
}
}
if (strcmp(direction_type, "triggerleft") == 0)
if (!cl_paused->value && cls.key_dest == key_game)
{
qboolean new_left_trigger = abs(axis_value) > (32767 / 4);
if (new_left_trigger != left_trigger)
switch (event.caxis.axis)
{
left_trigger = new_left_trigger;
Key_Event(K_TRIG_LEFT, left_trigger, true);
case SDL_CONTROLLER_AXIS_LEFTX:
joystick_left_x = axis_value;
break;
case SDL_CONTROLLER_AXIS_LEFTY:
joystick_left_y = axis_value;
break;
case SDL_CONTROLLER_AXIS_RIGHTX:
joystick_right_x = axis_value;
break;
case SDL_CONTROLLER_AXIS_RIGHTY:
joystick_right_y = axis_value;
break;
}
}
else if (strcmp(direction_type, "triggerright") == 0)
{
qboolean new_right_trigger = abs(axis_value) > (32767 / 4);
if (new_right_trigger != right_trigger)
{
right_trigger = new_right_trigger;
Key_Event(K_TRIG_RIGHT, right_trigger, true);
}
}
break;
}
@ -944,14 +864,68 @@ IN_Update(void)
}
}
/*
* Joystick vector magnitude
*/
static float
IN_StickMagnitude(thumbstick_t stick)
{
return sqrtf((stick.x * stick.x) + (stick.y * stick.y));
}
/*
* Radial deadzone based on github.com/jeremiah-sypult/Quakespasm-Rift
*/
static thumbstick_t
IN_RadialDeadzone(thumbstick_t stick, float deadzone)
{
thumbstick_t result = {0};
float magnitude = min(IN_StickMagnitude(stick), 1.0f);
deadzone = min( max(deadzone, 0.0f), 0.9f); // clamp to [0.0, 0.9]
if ( magnitude > deadzone )
{
const float scale = ((magnitude - deadzone) / (1.0 - deadzone)) / magnitude;
result.x = stick.x * scale;
result.y = stick.y * scale;
}
return result;
}
/*
* Exponent applied on stick magnitude
*/
static thumbstick_t
IN_ApplyExpo(thumbstick_t stick, float exponent)
{
thumbstick_t result = {0};
float magnitude = IN_StickMagnitude(stick);
if (magnitude == 0)
{
return result;
}
const float eased = powf(magnitude, exponent) / magnitude;
result.x = stick.x * eased;
result.y = stick.y * eased;
return result;
}
/*
* Move handling
*/
void
IN_Move(usercmd_t *cmd)
{
// Factor used to transform from SDL joystick input ([-32768, 32767]) to [-1, 1] range
static const float normalize_sdl_axis = 1.0f / 32768.0f;
static float old_mouse_x;
static float old_mouse_y;
static float joystick_yaw, joystick_pitch;
static float joystick_forwardmove, joystick_sidemove;
static thumbstick_t left_stick = {0}, right_stick = {0};
if (m_filter->value)
{
@ -1026,39 +1000,82 @@ IN_Move(usercmd_t *cmd)
mouse_x = mouse_y = 0;
}
// Joystick reading and processing
left_stick.x = joystick_left_x * normalize_sdl_axis;
left_stick.y = joystick_left_y * normalize_sdl_axis;
right_stick.x = joystick_right_x * normalize_sdl_axis;
right_stick.y = joystick_right_y * normalize_sdl_axis;
if (left_stick.x || left_stick.y)
{
left_stick = IN_RadialDeadzone(left_stick, joy_left_deadzone->value);
left_stick = IN_ApplyExpo(left_stick, joy_left_expo->value);
}
if (right_stick.x || right_stick.y)
{
right_stick = IN_RadialDeadzone(right_stick, joy_right_deadzone->value);
right_stick = IN_ApplyExpo(right_stick, joy_right_expo->value);
}
switch((int)joy_layout->value)
{
case LAYOUT_SOUTHPAW:
joystick_forwardmove = right_stick.y;
joystick_sidemove = right_stick.x;
joystick_yaw = left_stick.x;
joystick_pitch = left_stick.y;
break;
case LAYOUT_LEGACY:
joystick_forwardmove = left_stick.y;
joystick_sidemove = right_stick.x;
joystick_yaw = left_stick.x;
joystick_pitch = right_stick.y;
break;
case LAYOUT_LEGACY_SOUTHPAW:
joystick_forwardmove = right_stick.y;
joystick_sidemove = left_stick.x;
joystick_yaw = right_stick.x;
joystick_pitch = left_stick.y;
break;
default: // LAYOUT_DEFAULT
joystick_forwardmove = left_stick.y;
joystick_sidemove = left_stick.x;
joystick_yaw = right_stick.x;
joystick_pitch = right_stick.y;
}
// To make the the viewangles changes independent of framerate we need to scale
// with frametime (assuming the configured values are for 60hz)
//
// 1/32768 is to normalize the input values from SDL (they're between -32768 and
// 32768 and we want -1 to 1) for movement this is not needed, as those are
// absolute values independent of framerate
float frametime_ratio = cls.rframetime/0.01666f;
float joyViewFactor = NORMALIZE_SDL_AXIS * frametime_ratio;
float gyroViewFactor = normalize_sdl_gyro * frametime_ratio;
// For movement this is not needed, as those are absolute values independent of framerate
float joyViewFactor = cls.rframetime/0.01666f;
float gyroViewFactor = normalize_sdl_gyro * joyViewFactor;
if (joystick_yaw)
{
cl.viewangles[YAW] -= (m_yaw->value * joystick_yaw) * joyViewFactor;
cl.viewangles[YAW] -= (m_yaw->value * joy_yawsensitivity->value
* cl_yawspeed->value * joystick_yaw) * joyViewFactor;
}
if(joystick_pitch)
{
cl.viewangles[PITCH] += (m_pitch->value * joystick_pitch) * joyViewFactor;
cl.viewangles[PITCH] += (m_pitch->value * joy_pitchsensitivity->value
* cl_pitchspeed->value * joystick_pitch) * joyViewFactor;
}
if (joystick_forwardmove)
{
cmd->forwardmove -= (m_forward->value * joystick_forwardmove) / 32768;
// We need to be twice as fast because with joystick we run...
cmd->forwardmove -= m_forward->value * joy_forwardsensitivity->value
* cl_forwardspeed->value * 2.0f * joystick_forwardmove;
}
if (joystick_sidemove)
{
cmd->sidemove += (m_side->value * joystick_sidemove) / 32768;
}
if (joystick_up)
{
cmd->upmove -= (m_up->value * joystick_up) / 32768;
// We need to be twice as fast because with joystick we run...
cmd->sidemove += m_side->value * joy_sidesensitivity->value
* cl_sidespeed->value * 2.0f * joystick_sidemove;
}
if (gyro_yaw)
@ -1513,21 +1530,12 @@ IN_Controller_Init(qboolean notify_user)
controller = SDL_GameControllerOpen(i);
Com_Printf ("Controller settings: %s\n", SDL_GameControllerMapping(controller));
Com_Printf ("Controller axis: \n");
Com_Printf (" * leftx = %s\n", joy_axis_leftx->string);
Com_Printf (" * lefty = %s\n", joy_axis_lefty->string);
Com_Printf (" * rightx = %s\n", joy_axis_rightx->string);
Com_Printf (" * righty = %s\n", joy_axis_righty->string);
Com_Printf (" * triggerleft = %s\n", joy_axis_triggerleft->string);
Com_Printf (" * triggerright = %s\n", joy_axis_triggerright->string);
Com_Printf ("Controller thresholds: \n");
Com_Printf (" * leftx = %f\n", joy_axis_leftx_threshold->value);
Com_Printf (" * lefty = %f\n", joy_axis_lefty_threshold->value);
Com_Printf (" * rightx = %f\n", joy_axis_rightx_threshold->value);
Com_Printf (" * righty = %f\n", joy_axis_righty_threshold->value);
Com_Printf (" * triggerleft = %f\n", joy_axis_triggerleft_threshold->value);
Com_Printf (" * triggerright = %f\n", joy_axis_triggerright_threshold->value);
Com_Printf ("Left stick config:\n");
Com_Printf (" * response curve exponent = %.3f\n", joy_left_expo->value);
Com_Printf (" * inner deadzone = %.3f\n", joy_left_deadzone->value);
Com_Printf ("Right stick config:\n");
Com_Printf (" * response curve exponent = %.3f\n", joy_right_expo->value);
Com_Printf (" * inner deadzone = %.3f\n", joy_right_deadzone->value);
joystick_haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(controller));
@ -1583,7 +1591,7 @@ IN_Init(void)
Com_Printf("------- input initialization -------\n");
mouse_x = mouse_y = 0;
joystick_yaw = joystick_pitch = joystick_forwardmove = joystick_sidemove = 0;
joystick_left_x = joystick_left_y = joystick_right_x = joystick_right_y = 0;
gyro_yaw = gyro_pitch = 0;
exponential_speedup = Cvar_Get("exponential_speedup", "0", CVAR_ARCHIVE);
@ -1604,22 +1612,12 @@ IN_Init(void)
joy_pitchsensitivity = Cvar_Get("joy_pitchsensitivity", "1.0", CVAR_ARCHIVE);
joy_forwardsensitivity = Cvar_Get("joy_forwardsensitivity", "1.0", CVAR_ARCHIVE);
joy_sidesensitivity = Cvar_Get("joy_sidesensitivity", "1.0", CVAR_ARCHIVE);
joy_upsensitivity = Cvar_Get("joy_upsensitivity", "1.0", CVAR_ARCHIVE);
joy_expo = Cvar_Get("joy_expo", "2.0", CVAR_ARCHIVE);
joy_axis_leftx = Cvar_Get("joy_axis_leftx", "sidemove", CVAR_ARCHIVE);
joy_axis_lefty = Cvar_Get("joy_axis_lefty", "forwardmove", CVAR_ARCHIVE);
joy_axis_rightx = Cvar_Get("joy_axis_rightx", "yaw", CVAR_ARCHIVE);
joy_axis_righty = Cvar_Get("joy_axis_righty", "pitch", CVAR_ARCHIVE);
joy_axis_triggerleft = Cvar_Get("joy_axis_triggerleft", "triggerleft", CVAR_ARCHIVE);
joy_axis_triggerright = Cvar_Get("joy_axis_triggerright", "triggerright", CVAR_ARCHIVE);
joy_axis_leftx_threshold = Cvar_Get("joy_axis_leftx_threshold", "0.15", CVAR_ARCHIVE);
joy_axis_lefty_threshold = Cvar_Get("joy_axis_lefty_threshold", "0.15", CVAR_ARCHIVE);
joy_axis_rightx_threshold = Cvar_Get("joy_axis_rightx_threshold", "0.15", CVAR_ARCHIVE);
joy_axis_righty_threshold = Cvar_Get("joy_axis_righty_threshold", "0.15", CVAR_ARCHIVE);
joy_axis_triggerleft_threshold = Cvar_Get("joy_axis_triggerleft_threshold", "0.15", CVAR_ARCHIVE);
joy_axis_triggerright_threshold = Cvar_Get("joy_axis_triggerright_threshold", "0.15", CVAR_ARCHIVE);
joy_layout = Cvar_Get("joy_layout", "0", CVAR_ARCHIVE);
joy_left_expo = Cvar_Get("joy_left_expo", "2.0", CVAR_ARCHIVE);
joy_left_deadzone = Cvar_Get("joy_left_deadzone", "0.16", CVAR_ARCHIVE);
joy_right_expo = Cvar_Get("joy_right_expo", "2.0", CVAR_ARCHIVE);
joy_right_deadzone = Cvar_Get("joy_right_deadzone", "0.16", CVAR_ARCHIVE);
gyro_calibration_x = Cvar_Get("gyro_calibration_x", "0.0", CVAR_ARCHIVE);
gyro_calibration_y = Cvar_Get("gyro_calibration_y", "0.0", CVAR_ARCHIVE);
@ -1682,6 +1680,7 @@ IN_Controller_Shutdown(qboolean notify_user)
SDL_GameControllerClose(controller);
controller = NULL;
gyro_hardware = false;
joystick_left_x = joystick_left_y = joystick_right_x = joystick_right_y = 0;
gyro_yaw = gyro_pitch = 0;
normalize_sdl_gyro = 1.0f / M_PI;
}

View file

@ -1764,12 +1764,13 @@ M_Menu_Gyro_f(void)
/*
* JOY MENU
*/
static menuslider_s s_joy_expo_slider;
static menulist_s s_joy_layout_box;
static menuslider_s s_joy_yawsensitivity_slider;
static menuslider_s s_joy_pitchsensitivity_slider;
static menuslider_s s_joy_forwardsensitivity_slider;
static menuslider_s s_joy_sidesensitivity_slider;
static menuslider_s s_joy_upsensitivity_slider;
static menuslider_s s_joy_left_expo_slider;
static menuslider_s s_joy_right_expo_slider;
static menuslider_s s_joy_haptic_slider;
static menuaction_s s_joy_gyro_action;
static menuaction_s s_joy_customize_buttons_action;
@ -1793,10 +1794,26 @@ ConfigGyroFunc(void *unused)
M_Menu_Gyro_f();
}
static void
StickLayoutFunc(void *unused)
{
Cvar_SetValue("joy_layout", (int)s_joy_layout_box.curvalue);
}
static void
Joy_MenuInit(void)
{
extern qboolean show_haptic;
static const char *stick_layouts[] =
{
"default",
"southpaw",
"legacy",
"legacy southpaw",
0
};
int y = 0;
s_joy_menu.x = (int)(viddef.width * 0.50f);
@ -1846,27 +1863,25 @@ Joy_MenuInit(void)
y += 10;
s_joy_upsensitivity_slider.generic.type = MTYPE_SLIDER;
s_joy_upsensitivity_slider.generic.x = 0;
s_joy_upsensitivity_slider.generic.y = y;
s_joy_left_expo_slider.generic.type = MTYPE_SLIDER;
s_joy_left_expo_slider.generic.x = 0;
s_joy_left_expo_slider.generic.y = y;
y += 10;
s_joy_upsensitivity_slider.generic.name = "up sensitivity";
s_joy_upsensitivity_slider.cvar = "joy_upsensitivity";
s_joy_upsensitivity_slider.minvalue = 0.0f;
s_joy_upsensitivity_slider.maxvalue = 2.0f;
Menu_AddItem(&s_joy_menu, (void *)&s_joy_upsensitivity_slider);
s_joy_left_expo_slider.generic.name = "left expo";
s_joy_left_expo_slider.cvar = "joy_left_expo";
s_joy_left_expo_slider.minvalue = 1;
s_joy_left_expo_slider.maxvalue = 5;
Menu_AddItem(&s_joy_menu, (void *)&s_joy_left_expo_slider);
s_joy_right_expo_slider.generic.type = MTYPE_SLIDER;
s_joy_right_expo_slider.generic.x = 0;
s_joy_right_expo_slider.generic.y = y;
y += 10;
s_joy_expo_slider.generic.type = MTYPE_SLIDER;
s_joy_expo_slider.generic.x = 0;
s_joy_expo_slider.generic.y = y;
y += 10;
s_joy_expo_slider.generic.name = "expo";
s_joy_expo_slider.cvar = "joy_expo";
s_joy_expo_slider.minvalue = 1;
s_joy_expo_slider.maxvalue = 5;
Menu_AddItem(&s_joy_menu, (void *)&s_joy_expo_slider);
s_joy_right_expo_slider.generic.name = "right expo";
s_joy_right_expo_slider.cvar = "joy_right_expo";
s_joy_right_expo_slider.minvalue = 1;
s_joy_right_expo_slider.maxvalue = 5;
Menu_AddItem(&s_joy_menu, (void *)&s_joy_right_expo_slider);
if (show_haptic) {
y += 10;
@ -1882,6 +1897,18 @@ Joy_MenuInit(void)
Menu_AddItem(&s_joy_menu, (void *)&s_joy_haptic_slider);
}
y += 10;
s_joy_layout_box.generic.type = MTYPE_SPINCONTROL;
s_joy_layout_box.generic.x = 0;
s_joy_layout_box.generic.y = y;
y += 10;
s_joy_layout_box.generic.name = "stick layout";
s_joy_layout_box.generic.callback = StickLayoutFunc;
s_joy_layout_box.itemnames = stick_layouts;
s_joy_layout_box.curvalue = ClampCvar(0, 3, joy_layout->value);
Menu_AddItem(&s_joy_menu, (void *)&s_joy_layout_box);
if (gyro_hardware)
{
y += 10;