From ab53e9aa1340465049850a96bd6d7f0776cc3a6f Mon Sep 17 00:00:00 2001
From: Daniel Gibson <metalcaedes@gmail.com>
Date: Sun, 7 Jan 2024 01:26:45 +0100
Subject: [PATCH 01/14] Merge GameController support from Quadrilateral Cowboy

https://github.com/blendogames/quadrilateralcowboy

pretty much as it is there, with only minimal changes required to work
with dhewm3
---
 neo/framework/Common.cpp     |   2 +-
 neo/framework/KeyInput.cpp   |  35 +++--
 neo/framework/KeyInput.h     |  41 +++--
 neo/framework/UsercmdGen.cpp | 291 +++++++++++++++++++++++++++++++----
 neo/sys/events.cpp           | 228 +++++++++++++++++++++++++++
 neo/sys/sys_public.h         |  73 ++++++++-
 neo/ui/Window.cpp            |   4 +-
 7 files changed, 604 insertions(+), 70 deletions(-)

diff --git a/neo/framework/Common.cpp b/neo/framework/Common.cpp
index bfc69f93..9289a833 100644
--- a/neo/framework/Common.cpp
+++ b/neo/framework/Common.cpp
@@ -2919,7 +2919,7 @@ void idCommonLocal::Init( int argc, char **argv ) {
 #endif
 #endif
 
-	if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO | SDL_INIT_JOYSTICK)) // init joystick to work around SDL 2.0.9 bug #4391
+	if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER)) // init joystick to work around SDL 2.0.9 bug #4391
 		Sys_Error("Error while initializing SDL: %s", SDL_GetError());
 
 	Sys_InitThreads();
diff --git a/neo/framework/KeyInput.cpp b/neo/framework/KeyInput.cpp
index bc5c2e17..b239613e 100644
--- a/neo/framework/KeyInput.cpp
+++ b/neo/framework/KeyInput.cpp
@@ -124,23 +124,24 @@ static const keyname_t keynames[] =
 	{"JOY13",			K_JOY13,			"#str_07074"},
 	{"JOY14",			K_JOY14,			"#str_07075"},
 	{"JOY15",			K_JOY15,			"#str_07076"},
-	{"JOY16",			K_JOY16,			"#str_07077"},
-	{"JOY17",			K_JOY17,			"#str_07078"},
-	{"JOY18",			K_JOY18,			"#str_07079"},
-	{"JOY19",			K_JOY19,			"#str_07080"},
-	{"JOY20",			K_JOY20,			"#str_07081"},
-	{"JOY21",			K_JOY21,			"#str_07082"},
-	{"JOY22",			K_JOY22,			"#str_07083"},
-	{"JOY23",			K_JOY23,			"#str_07084"},
-	{"JOY24",			K_JOY24,			"#str_07085"},
-	{"JOY25",			K_JOY25,			"#str_07086"},
-	{"JOY26",			K_JOY26,			"#str_07087"},
-	{"JOY27",			K_JOY27,			"#str_07088"},
-	{"JOY28",			K_JOY28,			"#str_07089"},
-	{"JOY29",			K_JOY29,			"#str_07090"},
-	{"JOY30",			K_JOY30,			"#str_07091"},
-	{"JOY31",			K_JOY31,			"#str_07092"},
-	{"JOY32",			K_JOY32,			"#str_07093"},
+
+	{"JOY_STICK1_UP", 		K_JOY_STICK1_UP, 		"JOY_STICK1_UP"},
+	{"JOY_STICK1_DOWN", 	K_JOY_STICK1_DOWN, 		"JOY_STICK1_DOWN"},
+	{"JOY_STICK1_LEFT", 	K_JOY_STICK1_LEFT, 		"JOY_STICK1_LEFT"},
+	{"JOY_STICK1_RIGHT", 	K_JOY_STICK1_RIGHT, 	"JOY_STICK1_RIGHT"},
+
+	{"JOY_STICK2_UP", 		K_JOY_STICK2_UP, 		"JOY_STICK2_UP"},
+	{"JOY_STICK2_DOWN", 	K_JOY_STICK2_DOWN, 		"JOY_STICK2_DOWN"},
+	{"JOY_STICK2_LEFT", 	K_JOY_STICK2_LEFT, 		"JOY_STICK2_LEFT"},
+	{"JOY_STICK2_RIGHT", 	K_JOY_STICK2_RIGHT, 	"JOY_STICK2_RIGHT"},
+
+	{"JOY_TRIGGER1", 		K_JOY_TRIGGER1, 		"JOY_TRIGGER1"},
+	{"JOY_TRIGGER2", 		K_JOY_TRIGGER2, 		"JOY_TRIGGER2"},
+
+	{"JOY_DPAD_UP", 		K_JOY_DPAD_UP, 			"JOY_DPAD_UP"},
+	{"JOY_DPAD_DOWN", 		K_JOY_DPAD_DOWN, 		"JOY_DPAD_DOWN"},
+	{"JOY_DPAD_LEFT", 		K_JOY_DPAD_LEFT, 		"JOY_DPAD_LEFT"},
+	{"JOY_DPAD_RIGHT", 		K_JOY_DPAD_RIGHT, 		"JOY_DPAD_RIGHT"},
 
 	{"AUX1",			K_AUX1,				"#str_07094"},
 	{"AUX2",			K_AUX2,				"#str_07095"},
diff --git a/neo/framework/KeyInput.h b/neo/framework/KeyInput.h
index 77179d35..e959a1f7 100644
--- a/neo/framework/KeyInput.h
+++ b/neo/framework/KeyInput.h
@@ -138,6 +138,10 @@ typedef enum {
 	K_MWHEELDOWN = 195,
 	K_MWHEELUP,
 
+	//------------------------
+	// K_JOY codes must be contiguous, too
+	//------------------------
+
 	K_JOY1 = 197,
 	K_JOY2,
 	K_JOY3,
@@ -154,23 +158,26 @@ typedef enum {
 	K_JOY14,
 	K_JOY15,
 	K_JOY16,
-	K_JOY17,
-	K_JOY18,
-	K_JOY19,
-	K_JOY20,
-	K_JOY21,
-	K_JOY22,
-	K_JOY23,
-	K_JOY24,
-	K_JOY25,
-	K_JOY26,
-	K_JOY27,
-	K_GRAVE_A = 224,	// lowercase a with grave accent
-	K_JOY28,
-	K_JOY29,
-	K_JOY30,
-	K_JOY31,
-	K_JOY32,
+
+	K_JOY_STICK1_UP,
+	K_JOY_STICK1_DOWN,
+	K_JOY_STICK1_LEFT,
+	K_JOY_STICK1_RIGHT,
+
+	K_JOY_STICK2_UP,
+	K_JOY_STICK2_DOWN,
+	K_JOY_STICK2_LEFT,
+	K_JOY_STICK2_RIGHT,
+
+	K_JOY_TRIGGER1,
+	K_JOY_TRIGGER2,
+
+	K_JOY_DPAD_UP,
+	K_JOY_DPAD_DOWN,
+	K_JOY_DPAD_LEFT,
+	K_JOY_DPAD_RIGHT,
+
+	K_GRAVE_A = 229,	// lowercase a with grave accent FIXME: used to be 224; this probably isn't used anyway
 
 	K_AUX1 = 230,
 	K_CEDILLA_C = 231,	// lowercase c with Cedilla
diff --git a/neo/framework/UsercmdGen.cpp b/neo/framework/UsercmdGen.cpp
index f1b213f7..6fd46169 100644
--- a/neo/framework/UsercmdGen.cpp
+++ b/neo/framework/UsercmdGen.cpp
@@ -350,6 +350,8 @@ private:
 	bool			Inhibited( void );
 	void			AdjustAngles( void );
 	void			KeyMove( void );
+	void			CircleToSquare( float & axis_x, float & axis_y ) const;
+	void			HandleJoystickAxis( int keyNum, float unclampedValue, float threshold, bool positive );
 	void			JoystickMove( void );
 	void			MouseMove( void );
 	void			CmdButtons( void );
@@ -384,7 +386,14 @@ private:
 	bool			mouseDown;
 
 	int				mouseDx, mouseDy;	// added to by mouse events
-	int				joystickAxis[MAX_JOYSTICK_AXIS];	// set by joystick events
+	float			joystickAxis[MAX_JOYSTICK_AXIS];	// set by joystick events
+
+	int				pollTime;
+	int				lastPollTime;
+	float			lastLookValuePitch;
+	float			lastLookValueYaw;
+
+	bool			heldJump; // TODO: ???
 
 	static idCVar	in_yawSpeed;
 	static idCVar	in_pitchSpeed;
@@ -419,6 +428,24 @@ idCVar idUsercmdGenLocal::m_smooth( "m_smooth", "1", CVAR_SYSTEM | CVAR_ARCHIVE
 idCVar idUsercmdGenLocal::m_strafeSmooth( "m_strafeSmooth", "4", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_INTEGER, "number of samples blended for mouse moving", 1, 8, idCmdSystem::ArgCompletion_Integer<1,8> );
 idCVar idUsercmdGenLocal::m_showMouseRate( "m_showMouseRate", "0", CVAR_SYSTEM | CVAR_BOOL, "shows mouse movement" );
 
+idCVar joy_mergedThreshold( "joy_mergedThreshold", "1", CVAR_BOOL | CVAR_ARCHIVE, "If the thresholds aren't merged, you drift more off center" );
+idCVar joy_newCode( "joy_newCode", "0", CVAR_BOOL | CVAR_ARCHIVE, "Use the new codepath" );
+idCVar joy_triggerThreshold( "joy_triggerThreshold", "0.05", CVAR_FLOAT | CVAR_ARCHIVE, "how far the joystick triggers have to be pressed before they register as down" );
+idCVar joy_deadZone( "joy_deadZone", "0.4", CVAR_FLOAT | CVAR_ARCHIVE, "specifies how large the dead-zone is on the joystick" );
+idCVar joy_range( "joy_range", "1.0", CVAR_FLOAT | CVAR_ARCHIVE, "allow full range to be mapped to a smaller offset" );
+idCVar joy_gammaLook( "joy_gammaLook", "1", CVAR_INTEGER | CVAR_ARCHIVE, "use a log curve instead of a power curve for movement" );
+idCVar joy_powerScale( "joy_powerScale", "2", CVAR_FLOAT | CVAR_ARCHIVE, "Raise joystick values to this power" );
+idCVar joy_pitchSpeed( "joy_pitchSpeed", "130",	CVAR_ARCHIVE | CVAR_FLOAT, "pitch speed when pressing up or down on the joystick", 60, 600 );
+idCVar joy_yawSpeed( "joy_yawSpeed", "240",	CVAR_ARCHIVE | CVAR_FLOAT, "pitch speed when pressing left or right on the joystick", 60, 600 );
+
+// these were a bad idea!
+idCVar joy_dampenLook( "joy_dampenLook", "1", CVAR_BOOL | CVAR_ARCHIVE, "Do not allow full acceleration on look" );
+idCVar joy_deltaPerMSLook( "joy_deltaPerMSLook", "0.003", CVAR_FLOAT | CVAR_ARCHIVE, "Max amount to be added on look per MS" );
+
+idCVar in_useJoystick( "in_useJoystick", "1", CVAR_ARCHIVE | CVAR_BOOL, "enables/disables the gamepad for PC use" );
+idCVar in_invertLook( "in_invertLook", "0", CVAR_ARCHIVE | CVAR_BOOL, "inverts the look controls so the forward looks up (flight controls) - the proper way to play games!" );
+idCVar in_mouseInvertLook( "in_mouseInvertLook", "0", CVAR_ARCHIVE | CVAR_BOOL, "inverts the look controls so the forward looks up (flight controls) - the proper way to play games!" );
+
 static idUsercmdGenLocal localUsercmdGen;
 idUsercmdGen	*usercmdGen = &localUsercmdGen;
 
@@ -573,9 +600,17 @@ void idUsercmdGenLocal::KeyMove( void ) {
 	forward += KEY_MOVESPEED * ButtonState( UB_FORWARD );
 	forward -= KEY_MOVESPEED * ButtonState( UB_BACK );
 
-	cmd.forwardmove = idMath::ClampChar( forward );
-	cmd.rightmove = idMath::ClampChar( side );
-	cmd.upmove = idMath::ClampChar( up );
+	// only set each movement variable if its unset at this point.
+	// NOTE: joystick input happens before this.
+	if (cmd.forwardmove == 0) {
+		cmd.forwardmove = idMath::ClampChar( forward );
+	}
+	if (cmd.rightmove == 0) {
+		cmd.rightmove = idMath::ClampChar( side );
+	}
+	if (cmd.upmove == 0) {
+		cmd.upmove = idMath::ClampChar( up );
+	}
 }
 
 /*
@@ -672,29 +707,199 @@ void idUsercmdGenLocal::MouseMove( void ) {
 	}
 }
 
+/*
+========================
+idUsercmdGenLocal::CircleToSquare
+========================
+*/
+void idUsercmdGenLocal::CircleToSquare( float & axis_x, float & axis_y ) const {
+	// bring everything in the first quadrant
+	bool flip_x = false;
+	if ( axis_x < 0.0f ) {
+		flip_x = true;
+		axis_x *= -1.0f;
+	}
+	bool flip_y = false;
+	if ( axis_y < 0.0f ) {
+		flip_y = true;
+		axis_y *= -1.0f;
+	}
+
+	// swap the two axes so we project against the vertical line X = 1
+	bool swap = false;
+	if ( axis_y > axis_x ) {
+		float tmp = axis_x;
+		axis_x = axis_y;
+		axis_y = tmp;
+		swap = true;
+	}
+
+	if ( axis_x < 0.001f ) {
+		// on one of the axes where no correction is needed
+		return;
+	}
+
+	// length (max 1.0f at the unit circle)
+	float len = idMath::Sqrt( axis_x * axis_x + axis_y * axis_y );
+	if ( len > 1.0f ) {
+		len = 1.0f;
+	}
+	// thales
+	float axis_y_us = axis_y / axis_x;
+
+	// use a power curve to shift the correction to happen closer to the unit circle
+	float correctionRatio = Square( len );
+	axis_x += correctionRatio * ( len - axis_x );
+	axis_y += correctionRatio * ( axis_y_us - axis_y );
+
+	// go back through the symmetries
+	if ( swap ) {
+		float tmp = axis_x;
+		axis_x = axis_y;
+		axis_y = tmp;
+	}
+	if ( flip_x ) {
+		axis_x *= -1.0f;
+	}
+	if ( flip_y ) {
+		axis_y *= -1.0f;
+	}
+}
+
+/*
+========================
+idUsercmdGenLocal::HandleJoystickAxis
+========================
+*/
+void idUsercmdGenLocal::HandleJoystickAxis( int keyNum, float unclampedValue, float threshold, bool positive ) {
+	if ( ( unclampedValue > 0.0f ) && !positive ) {
+		return;
+	}
+	if ( ( unclampedValue < 0.0f ) && positive ) {
+		return;
+	}
+	float value = 0.0f;
+	bool pressed = false;
+	if ( unclampedValue > threshold ) {
+		value = idMath::Fabs( ( unclampedValue - threshold ) / ( 1.0f - threshold ) );
+		pressed = true;
+	} else if ( unclampedValue < -threshold ) {
+		value = idMath::Fabs( ( unclampedValue + threshold ) / ( 1.0f - threshold ) );
+		pressed = true;
+	}
+
+	int action = idKeyInput::GetUsercmdAction( keyNum );
+	if ( action >= UB_ATTACK ) {
+		Key( keyNum, pressed );
+		return;
+	}
+	if ( !pressed ) {
+		return;
+	}
+
+	float lookValue = 0.0f;
+	if ( joy_gammaLook.GetBool() ) {
+		lookValue = idMath::Pow( 1.04712854805f, value * 100.0f ) * 0.01f;
+	} else {
+		lookValue = idMath::Pow( value, joy_powerScale.GetFloat() );
+	}
+
+#if 0 // TODO: aim assist maybe.
+	idGame * game = common->Game();
+	if ( game != NULL ) {
+		lookValue *= game->GetAimAssistSensitivity();
+	}
+#endif
+
+	switch ( action ) {
+		case UB_FORWARD: {
+			float move = (float)cmd.forwardmove + ( KEY_MOVESPEED * value );
+			cmd.forwardmove = idMath::ClampChar( idMath::Ftoi( move ) );
+			break;
+		}
+		case UB_BACK: {
+			float move = (float)cmd.forwardmove - ( KEY_MOVESPEED * value );
+			cmd.forwardmove = idMath::ClampChar( idMath::Ftoi( move ) );
+			break;
+		}
+		case UB_MOVELEFT: {
+			float move = (float)cmd.rightmove - ( KEY_MOVESPEED * value );
+			cmd.rightmove = idMath::ClampChar( idMath::Ftoi( move ) );
+			break;
+		}
+		case UB_MOVERIGHT: {
+			float move = (float)cmd.rightmove + ( KEY_MOVESPEED * value );
+			cmd.rightmove = idMath::ClampChar( idMath::Ftoi( move ) );
+			break;
+		}
+		case UB_LOOKUP: {
+			if ( joy_dampenLook.GetBool() ) {
+				lookValue = Min( lookValue, ( pollTime - lastPollTime ) * joy_deltaPerMSLook.GetFloat() + lastLookValuePitch );
+				lastLookValuePitch = lookValue;
+			}
+
+			float invertPitch = in_invertLook.GetBool() ? -1.0f : 1.0f;
+			viewangles[PITCH] -= MS2SEC( pollTime - lastPollTime ) * lookValue * joy_pitchSpeed.GetFloat() * invertPitch;
+			break;
+		}
+		case UB_LOOKDOWN: {
+			if ( joy_dampenLook.GetBool() ) {
+				lookValue = Min( lookValue, ( pollTime - lastPollTime ) * joy_deltaPerMSLook.GetFloat() + lastLookValuePitch );
+				lastLookValuePitch = lookValue;
+			}
+
+			float invertPitch = in_invertLook.GetBool() ? -1.0f : 1.0f;
+			viewangles[PITCH] += MS2SEC( pollTime - lastPollTime ) * lookValue * joy_pitchSpeed.GetFloat() * invertPitch;
+			break;
+		}
+		case UB_LEFT: {
+			if ( joy_dampenLook.GetBool() ) {
+				lookValue = Min( lookValue, ( pollTime - lastPollTime ) * joy_deltaPerMSLook.GetFloat() + lastLookValueYaw );
+				lastLookValueYaw = lookValue;
+			}
+			viewangles[YAW] += MS2SEC( pollTime - lastPollTime ) * lookValue * joy_yawSpeed.GetFloat();
+			break;
+		}
+		case UB_RIGHT: {
+			if ( joy_dampenLook.GetBool() ) {
+				lookValue = Min( lookValue, ( pollTime - lastPollTime ) * joy_deltaPerMSLook.GetFloat() + lastLookValueYaw );
+				lastLookValueYaw = lookValue;
+			}
+			viewangles[YAW] -= MS2SEC( pollTime - lastPollTime ) * lookValue * joy_yawSpeed.GetFloat();
+			break;
+		}
+	}
+}
+
 /*
 =================
 idUsercmdGenLocal::JoystickMove
 =================
 */
-void idUsercmdGenLocal::JoystickMove( void ) {
-	float	anglespeed;
+void idUsercmdGenLocal::JoystickMove() {
+	float threshold = joy_deadZone.GetFloat();
+	float triggerThreshold = joy_triggerThreshold.GetFloat();
 
-	if ( toggled_run.on ^ ( in_alwaysRun.GetBool() && idAsyncNetwork::IsActive() ) ) {
-		anglespeed = idMath::M_MS2SEC * USERCMD_MSEC * in_angleSpeedKey.GetFloat();
-	} else {
-		anglespeed = idMath::M_MS2SEC * USERCMD_MSEC;
-	}
+	float axis_y = joystickAxis[ AXIS_LEFT_Y ];
+	float axis_x = joystickAxis[ AXIS_LEFT_X ];
+	CircleToSquare( axis_x, axis_y );
 
-	if ( !ButtonState( UB_STRAFE ) ) {
-		viewangles[YAW] += anglespeed * in_yawSpeed.GetFloat() * joystickAxis[AXIS_SIDE];
-		viewangles[PITCH] += anglespeed * in_pitchSpeed.GetFloat() * joystickAxis[AXIS_FORWARD];
-	} else {
-		cmd.rightmove = idMath::ClampChar( cmd.rightmove + joystickAxis[AXIS_SIDE] );
-		cmd.forwardmove = idMath::ClampChar( cmd.forwardmove + joystickAxis[AXIS_FORWARD] );
-	}
+	HandleJoystickAxis( K_JOY_STICK1_UP, axis_y, threshold, false );
+	HandleJoystickAxis( K_JOY_STICK1_DOWN, axis_y, threshold, true );
+	HandleJoystickAxis( K_JOY_STICK1_LEFT, axis_x, threshold, false );
+	HandleJoystickAxis( K_JOY_STICK1_RIGHT, axis_x, threshold, true );
 
-	cmd.upmove = idMath::ClampChar( cmd.upmove + joystickAxis[AXIS_UP] );
+	axis_y = joystickAxis[ AXIS_RIGHT_Y ];
+	axis_x = joystickAxis[ AXIS_RIGHT_X ];
+	CircleToSquare( axis_x, axis_y );
+
+	HandleJoystickAxis( K_JOY_STICK2_UP, axis_y, threshold, false );
+	HandleJoystickAxis( K_JOY_STICK2_DOWN, axis_y, threshold, true );
+	HandleJoystickAxis( K_JOY_STICK2_LEFT, axis_x, threshold, false );
+	HandleJoystickAxis( K_JOY_STICK2_RIGHT, axis_x, threshold, true );
+
+	HandleJoystickAxis( K_JOY_TRIGGER1, joystickAxis[ AXIS_LEFT_TRIG ], triggerThreshold, true );
+	HandleJoystickAxis( K_JOY_TRIGGER2, joystickAxis[ AXIS_RIGHT_TRIG ], triggerThreshold, true );
 }
 
 /*
@@ -778,6 +983,9 @@ void idUsercmdGenLocal::MakeCurrent( void ) {
 		// keyboard angle adjustment
 		AdjustAngles();
 
+		// get basic movement from joystick
+		JoystickMove();
+
 		// set button bits
 		CmdButtons();
 
@@ -787,9 +995,6 @@ void idUsercmdGenLocal::MakeCurrent( void ) {
 		// get basic movement from mouse
 		MouseMove();
 
-		// get basic movement from joystick
-		JoystickMove();
-
 		// check to make sure the angles haven't wrapped
 		if ( viewangles[PITCH] - oldAngles[PITCH] > 90 ) {
 			viewangles[PITCH] = oldAngles[PITCH] + 90;
@@ -877,6 +1082,7 @@ void idUsercmdGenLocal::Clear( void ) {
 	// clears all key states
 	memset( buttonState, 0, sizeof( buttonState ) );
 	memset( keyState, false, sizeof( keyState ) );
+	memset( joystickAxis, 0, sizeof( joystickAxis ) );
 
 	inhibitCommands = false;
 
@@ -939,6 +1145,8 @@ void idUsercmdGenLocal::Key( int keyNum, bool down ) {
 
 	int action = idKeyInput::GetUsercmdAction( keyNum );
 
+	// TODO: if action == 0 return ?
+
 	if ( down ) {
 
 		buttonState[ action ]++;
@@ -1039,7 +1247,28 @@ idUsercmdGenLocal::Joystick
 ===============
 */
 void idUsercmdGenLocal::Joystick( void ) {
-	memset( joystickAxis, 0, sizeof( joystickAxis ) );
+	int numEvents = Sys_PollJoystickInputEvents( 0 );
+
+	// Study each of the buffer elements and process them.
+	for ( int i = 0; i < numEvents; i++ ) {
+		int action;
+		int value;
+		if ( Sys_ReturnJoystickInputEvent( i, action, value ) ) {
+			if ( action >= J_ACTION1 && action <= J_ACTION_MAX ) {
+				int joyButton = K_JOY1 + ( action - J_ACTION1 );
+				Key( joyButton, ( value != 0 ) );
+			} else if ( ( action >= J_AXIS_MIN ) && ( action <= J_AXIS_MAX ) ) {
+				joystickAxis[ action - J_AXIS_MIN ] = static_cast<float>( value ) / 32767.0f;
+			} else if ( action >= J_DPAD_UP && action <= J_DPAD_RIGHT ) {
+				int joyButton = K_JOY_DPAD_UP + ( action - J_DPAD_UP );
+				Key( joyButton, ( value != 0 ) );
+			} else {
+				//assert( !"Unknown joystick event" );
+			}
+		}
+	}
+
+	Sys_EndJoystickInputEvents();
 }
 
 /*
@@ -1065,7 +1294,9 @@ void idUsercmdGenLocal::UsercmdInterrupt( void ) {
 	Keyboard();
 
 	// process the system joystick events
-	Joystick();
+	if ( in_useJoystick.GetBool() ) {
+		Joystick();
+	}
 
 	// create the usercmd for com_ticNumber+1
 	MakeCurrent();
@@ -1095,6 +1326,11 @@ idUsercmdGenLocal::GetDirectUsercmd
 */
 usercmd_t idUsercmdGenLocal::GetDirectUsercmd( void ) {
 
+	pollTime = Sys_Milliseconds();
+	if ( pollTime - lastPollTime > 100 ) {
+		lastPollTime = pollTime - 100;
+	}
+
 	// initialize current usercmd
 	InitCurrent();
 
@@ -1105,12 +1341,15 @@ usercmd_t idUsercmdGenLocal::GetDirectUsercmd( void ) {
 	Keyboard();
 
 	// process the system joystick events
-	Joystick();
-
+	if ( in_useJoystick.GetBool() ) {
+		Joystick();
+	}
 	// create the usercmd
 	MakeCurrent();
 
 	cmd.duplicateCount = 0;
 
+	lastPollTime = pollTime;
+
 	return cmd;
 }
diff --git a/neo/sys/events.cpp b/neo/sys/events.cpp
index 6cc04516..f310dd22 100644
--- a/neo/sys/events.cpp
+++ b/neo/sys/events.cpp
@@ -108,8 +108,26 @@ struct mouse_poll_t {
 	}
 };
 
+struct joystick_poll_t {
+	int action;
+	int value;
+
+	joystick_poll_t() : action(0), value(0) {} // TODO: or -1?
+
+	joystick_poll_t(int a, int v) {
+		action = a;
+		value = v;
+	}
+};
+
 static idList<kbd_poll_t> kbd_polls;
 static idList<mouse_poll_t> mouse_polls;
+static idList<joystick_poll_t> joystick_polls;
+
+static bool buttonStates[K_LAST_KEY];
+static int  joyAxis[MAX_JOYSTICK_AXIS];
+
+static idList<sysEvent_t> event_overflow;
 
 #if SDL_VERSION_ATLEAST(2, 0, 0)
 // for utf8ToISO8859_1() - used for non-ascii text input and Sys_GetLocalizedScancodeName()
@@ -477,6 +495,73 @@ static byte mapkey(SDL_Keycode key) {
 	return 0;
 }
 
+static sys_jEvents mapjoybutton(SDL_GameControllerButton button) {
+
+	switch (button)
+	{
+	case SDL_CONTROLLER_BUTTON_A:
+		return J_ACTION1;
+	case SDL_CONTROLLER_BUTTON_B:
+		return J_ACTION2;
+	case SDL_CONTROLLER_BUTTON_X:
+		return J_ACTION3;
+	case SDL_CONTROLLER_BUTTON_Y:
+		return J_ACTION4;
+	case SDL_CONTROLLER_BUTTON_BACK:
+		return J_ACTION10;
+	case SDL_CONTROLLER_BUTTON_GUIDE:
+		// TODO:
+		break;
+	case SDL_CONTROLLER_BUTTON_START:
+		return J_ACTION9;
+	case SDL_CONTROLLER_BUTTON_LEFTSTICK:
+		return J_ACTION7;
+	case SDL_CONTROLLER_BUTTON_RIGHTSTICK:
+		return J_ACTION8;
+	case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:
+		return J_ACTION5;
+	case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER:
+		return J_ACTION6;
+	case SDL_CONTROLLER_BUTTON_DPAD_UP:
+		return J_DPAD_UP;
+	case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
+		return J_DPAD_DOWN;
+	case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
+		return J_DPAD_LEFT;
+	case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
+		return J_DPAD_RIGHT;
+	default:
+		common->Warning("unknown game controller button %u", button);
+		break;
+	}
+
+	return MAX_JOY_EVENT;
+}
+
+static sys_jEvents mapjoyaxis(SDL_GameControllerAxis axis) {
+
+	switch (axis)
+	{
+	case SDL_CONTROLLER_AXIS_LEFTX:
+		return J_AXIS_LEFT_X;
+	case SDL_CONTROLLER_AXIS_LEFTY:
+		return J_AXIS_LEFT_Y;
+	case SDL_CONTROLLER_AXIS_RIGHTX:
+		return J_AXIS_RIGHT_X;
+	case SDL_CONTROLLER_AXIS_RIGHTY:
+		return J_AXIS_RIGHT_Y;
+	case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
+		return J_AXIS_LEFT_TRIG;
+	case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
+		return J_AXIS_RIGHT_TRIG;
+	default:
+		common->Warning("unknown game controller axis %u", axis);
+		break;
+	}
+
+	return J_AXIS_MAX;
+}
+
 static void PushConsoleEvent(const char *s) {
 	char *b;
 	size_t len;
@@ -531,6 +616,19 @@ void Sys_InitInput() {
 #else // SDL1.2 doesn't support this
 	in_grabKeyboard.ClearModified();
 #endif
+
+	joystick_polls.SetGranularity(64);
+	event_overflow.SetGranularity(64);
+
+	memset( buttonStates, 0, sizeof( buttonStates ) );
+	memset( joyAxis, 0, sizeof( joyAxis ) );
+
+	const int NumJoysticks = SDL_NumJoysticks();
+	printf("XXX found %d joysticks\n", NumJoysticks);
+	for( int i = 0; i < NumJoysticks; ++i )
+	{
+		SDL_GameController* gc = SDL_GameControllerOpen( i );
+	}
 }
 
 /*
@@ -541,6 +639,8 @@ Sys_ShutdownInput
 void Sys_ShutdownInput() {
 	kbd_polls.Clear();
 	mouse_polls.Clear();
+	joystick_polls.Clear();
+	event_overflow.Clear();
 #if SDL_VERSION_ATLEAST(2, 0, 0)
 	SDL_iconv_close( iconvDesc ); // used by utf8ToISO8859_1()
 	iconvDesc = ( SDL_iconv_t ) -1; 
@@ -666,6 +766,18 @@ void Sys_GrabMouseCursor(bool grabIt) {
 	GLimp_GrabInput(flags);
 }
 
+
+static void PushButton( int key, bool value ) {
+	// So we don't keep sending the same SE_KEY message over and over again
+	if ( buttonStates[key] != value ) {
+		buttonStates[key] = value;
+		sysEvent_t res = { SE_KEY, key, value ? 1 : 0, 0, NULL };
+		// this is done to generate two events per controller axis event
+		// one SE_JOYSTICK and one SE_KEY
+		event_overflow.Append(res);
+	}
+}
+
 /*
 ================
 Sys_GetEvent
@@ -678,6 +790,14 @@ sysEvent_t Sys_GetEvent() {
 
 	static const sysEvent_t res_none = { SE_NONE, 0, 0, 0, NULL };
 
+	// process any overflow.
+	if (event_overflow.Num() > 0)
+	{
+		res = event_overflow[0];
+		event_overflow.RemoveIndex(0);
+		return res;
+	}
+
 #if SDL_VERSION_ATLEAST(2, 0, 0)
 	static char s[SDL_TEXTINPUTEVENT_TEXT_SIZE] = {0};
 	static size_t s_pos = 0;
@@ -958,6 +1078,76 @@ sysEvent_t Sys_GetEvent() {
 
 			return res;
 
+		case SDL_CONTROLLERBUTTONDOWN:
+		case SDL_CONTROLLERBUTTONUP:
+		{
+			sys_jEvents jEvent =  mapjoybutton( (SDL_GameControllerButton)ev.cbutton.button);
+			joystick_polls.Append(joystick_poll_t(jEvent, ev.cbutton.state == SDL_PRESSED ? 1 : 0) );
+
+			res.evType = SE_KEY;
+			res.evValue2 = ev.cbutton.state == SDL_PRESSED ? 1 : 0;
+			if ( ( jEvent >= J_ACTION1 ) && ( jEvent <= J_ACTION_MAX ) ) {
+				res.evValue = K_JOY1 + ( jEvent - J_ACTION1 );
+				return res;
+			} else if ( ( jEvent >= J_DPAD_UP ) && ( jEvent <= J_DPAD_RIGHT ) ) {
+				res.evValue = K_JOY_DPAD_UP + ( jEvent - J_DPAD_UP );
+				return res;
+			}
+
+			continue; // try to get a decent event.
+		}
+
+		case SDL_CONTROLLERAXISMOTION:
+		{
+			const int range = 16384;
+
+			sys_jEvents jEvent = mapjoyaxis( (SDL_GameControllerAxis)ev.caxis.axis);
+			joystick_polls.Append(joystick_poll_t(	jEvent, ev.caxis.value) );
+
+			if ( jEvent == J_AXIS_LEFT_X ) {
+				PushButton( K_JOY_STICK1_LEFT, ( ev.caxis.value < -range ) );
+				PushButton( K_JOY_STICK1_RIGHT, ( ev.caxis.value > range ) );
+			} else if ( jEvent == J_AXIS_LEFT_Y ) {
+				PushButton( K_JOY_STICK1_UP, ( ev.caxis.value < -range ) );
+				PushButton( K_JOY_STICK1_DOWN, ( ev.caxis.value > range ) );
+			} else if ( jEvent == J_AXIS_RIGHT_X ) {
+				PushButton( K_JOY_STICK2_LEFT, ( ev.caxis.value < -range ) );
+				PushButton( K_JOY_STICK2_RIGHT, ( ev.caxis.value > range ) );
+			} else if ( jEvent == J_AXIS_RIGHT_Y ) {
+				PushButton( K_JOY_STICK2_UP, ( ev.caxis.value < -range ) );
+				PushButton( K_JOY_STICK2_DOWN, ( ev.caxis.value > range ) );
+			} else if ( jEvent == J_AXIS_LEFT_TRIG ) {
+				PushButton( K_JOY_TRIGGER1, ( ev.caxis.value > range ) );
+			} else if ( jEvent == J_AXIS_RIGHT_TRIG ) {
+				PushButton( K_JOY_TRIGGER2, ( ev.caxis.value > range ) );
+			}
+			if ( jEvent >= J_AXIS_MIN && jEvent <= J_AXIS_MAX ) {
+				int axis = jEvent - J_AXIS_MIN;
+				int percent = ( ev.caxis.value * 16 ) / range;
+				if ( joyAxis[axis] != percent ) {
+					joyAxis[axis] = percent;
+					res.evType = SE_JOYSTICK;
+					res.evValue = axis;
+					res.evValue2 = percent;
+					return res;
+				}
+			}
+
+			continue; // try to get a decent event.
+		}
+		break;
+
+		case SDL_JOYDEVICEADDED:
+			SDL_GameControllerOpen( ev.jdevice.which );
+			// TODO: hot swapping maybe.
+			//lbOnControllerPlugIn(event.jdevice.which);
+			break;
+
+		case SDL_JOYDEVICEREMOVED:
+			// TODO: hot swapping maybe.
+			//lbOnControllerUnPlug(event.jdevice.which);
+			break;
+
 		case SDL_QUIT:
 			PushConsoleEvent("quit");
 			return res_none;
@@ -996,6 +1186,12 @@ void Sys_ClearEvents() {
 
 	kbd_polls.SetNum(0, false);
 	mouse_polls.SetNum(0, false);
+	joystick_polls.SetNum(0, false);
+
+	memset( buttonStates, 0, sizeof( buttonStates ) );
+	memset( joyAxis, 0, sizeof( joyAxis ) );
+
+	event_overflow.SetNum(0, false);
 }
 
 static void handleMouseGrab() {
@@ -1146,3 +1342,35 @@ Sys_EndMouseInputEvents
 void Sys_EndMouseInputEvents() {
 	mouse_polls.SetNum(0, false);
 }
+
+/*
+================
+Joystick Input Methods
+================
+*/
+void Sys_SetRumble( int device, int low, int hi ) {
+	// TODO: support multiple controllers.
+	assert(device == 0);
+	// TODO: support rumble maybe.
+	assert(0);
+}
+
+int	Sys_PollJoystickInputEvents( int deviceNum ) {
+	// TODO: support multiple controllers.
+	assert(deviceNum == 0);
+	return joystick_polls.Num();
+}
+
+int	Sys_ReturnJoystickInputEvent( const int n, int &action, int &value ) {
+	if (n >= joystick_polls.Num())
+		return 0;
+
+	action = joystick_polls[n].action;
+	value = joystick_polls[n].value;
+	return 1;
+}
+
+void Sys_EndJoystickInputEvents() {
+	joystick_polls.SetNum(0, false);
+}
+
diff --git a/neo/sys/sys_public.h b/neo/sys/sys_public.h
index d84ae8a9..d409a638 100644
--- a/neo/sys/sys_public.h
+++ b/neo/sys/sys_public.h
@@ -44,12 +44,12 @@ typedef enum {
 } cpuidSimd_t;
 
 typedef enum {
-	AXIS_SIDE,
-	AXIS_FORWARD,
-	AXIS_UP,
-	AXIS_ROLL,
-	AXIS_YAW,
-	AXIS_PITCH,
+	AXIS_LEFT_X,
+	AXIS_LEFT_Y,
+	AXIS_RIGHT_X,
+	AXIS_RIGHT_Y,
+	AXIS_LEFT_TRIG,
+	AXIS_RIGHT_TRIG,
 	MAX_JOYSTICK_AXIS
 } joystickAxis_t;
 
@@ -59,7 +59,7 @@ typedef enum {
 	SE_CHAR,				// evValue is an ascii char
 	SE_MOUSE,				// evValue and evValue2 are relative signed x / y moves
 	SE_MOUSE_ABS,			// evValue and evValue2 are absolute x / y coordinates in the window
-	SE_JOYSTICK_AXIS,		// evValue is an axis number and evValue2 is the current state (-127 to 127)
+	SE_JOYSTICK,			// evValue is an axis number and evValue2 is the current state (-127 to 127)
 	SE_CONSOLE				// evPtr is a char*, from typing something at a non-game console
 } sysEventType_t;
 
@@ -77,6 +77,59 @@ typedef enum {
 	M_DELTAZ
 } sys_mEvents;
 
+typedef enum {
+	J_ACTION1,
+	J_ACTION2,
+	J_ACTION3,
+	J_ACTION4,
+	J_ACTION5,
+	J_ACTION6,
+	J_ACTION7,
+	J_ACTION8,
+	J_ACTION9,
+	J_ACTION10,
+	J_ACTION11,
+	J_ACTION12,
+	J_ACTION13,
+	J_ACTION14,
+	J_ACTION15,
+	J_ACTION16,
+	J_ACTION17,
+	J_ACTION18,
+	J_ACTION19,
+	J_ACTION20,
+	J_ACTION21,
+	J_ACTION22,
+	J_ACTION23,
+	J_ACTION24,
+	J_ACTION25,
+	J_ACTION26,
+	J_ACTION27,
+	J_ACTION28,
+	J_ACTION29,
+	J_ACTION30,
+	J_ACTION31,
+	J_ACTION32,
+	J_ACTION_MAX = J_ACTION32,
+
+	J_AXIS_MIN,
+	J_AXIS_LEFT_X = J_AXIS_MIN + AXIS_LEFT_X,
+	J_AXIS_LEFT_Y = J_AXIS_MIN + AXIS_LEFT_Y,
+	J_AXIS_RIGHT_X = J_AXIS_MIN + AXIS_RIGHT_X,
+	J_AXIS_RIGHT_Y = J_AXIS_MIN + AXIS_RIGHT_Y,
+	J_AXIS_LEFT_TRIG = J_AXIS_MIN + AXIS_LEFT_TRIG,
+	J_AXIS_RIGHT_TRIG = J_AXIS_MIN + AXIS_RIGHT_TRIG,
+
+	J_AXIS_MAX = J_AXIS_MIN + MAX_JOYSTICK_AXIS - 1,
+
+	J_DPAD_UP,
+	J_DPAD_DOWN,
+	J_DPAD_LEFT,
+	J_DPAD_RIGHT,
+
+	MAX_JOY_EVENT
+} sys_jEvents;
+
 struct sysEvent_t {
 	sysEventType_t	evType;
 	int				evValue;
@@ -189,6 +242,12 @@ int				Sys_PollMouseInputEvents( void );
 int				Sys_ReturnMouseInputEvent( const int n, int &action, int &value );
 void			Sys_EndMouseInputEvents( void );
 
+// joystick input polling
+void			Sys_SetRumble( int device, int low, int hi );
+int				Sys_PollJoystickInputEvents( int deviceNum );
+int				Sys_ReturnJoystickInputEvent( const int n, int &action, int &value );
+void			Sys_EndJoystickInputEvents();
+
 // when the console is down, or the game is about to perform a lengthy
 // operation like map loading, the system can release the mouse cursor
 // when in windowed mode
diff --git a/neo/ui/Window.cpp b/neo/ui/Window.cpp
index 3948cea3..8e82453f 100644
--- a/neo/ui/Window.cpp
+++ b/neo/ui/Window.cpp
@@ -736,7 +736,7 @@ const char *idWindow::HandleEvent(const sysEvent_t *event, bool *updateVisuals)
 				*updateVisuals = true;
 			}
 
-			if (event->evValue == K_MOUSE1) {
+			if (event->evValue == K_MOUSE1 || event->evValue == K_JOY2) {
 
 				if (!event->evValue2 && GetCaptureChild()) {
 					GetCaptureChild()->LoseCapture();
@@ -785,7 +785,7 @@ const char *idWindow::HandleEvent(const sysEvent_t *event, bool *updateVisuals)
 				} else if (!actionUpRun) {
 					actionUpRun = RunScript( ON_ACTIONRELEASE );
 				}
-			} else if (event->evValue == K_MOUSE2) {
+			} else if (event->evValue == K_MOUSE2 || event->evValue == K_JOY1) {
 
 				if (!event->evValue2 && GetCaptureChild()) {
 					GetCaptureChild()->LoseCapture();

From 700b3ee558d78727dee9379d6572a9792fde32ac Mon Sep 17 00:00:00 2001
From: Daniel Gibson <metalcaedes@gmail.com>
Date: Mon, 8 Jan 2024 05:26:27 +0100
Subject: [PATCH 02/14] Clean up gamepad code a bit, rename buttons

- renamed gamepad/joystick actions and keys to have some meaning
  for buttons (instead of just JOY1, JOY2 etc)
- compiles with SDL1.2 again (there gamepads aren't supported though)
- shorter names for gamepad keys/axis in the key bindings menu
---
 neo/framework/Common.cpp     |   6 ++
 neo/framework/KeyInput.cpp   |  67 ++++++++------
 neo/framework/KeyInput.h     |  37 ++++----
 neo/framework/UsercmdGen.cpp |   4 +-
 neo/sys/events.cpp           | 168 +++++++++++++++++++++++++++++++----
 neo/sys/sys_public.h         |  63 ++++++-------
 6 files changed, 244 insertions(+), 101 deletions(-)

diff --git a/neo/framework/Common.cpp b/neo/framework/Common.cpp
index 9289a833..0e504481 100644
--- a/neo/framework/Common.cpp
+++ b/neo/framework/Common.cpp
@@ -2919,8 +2919,14 @@ void idCommonLocal::Init( int argc, char **argv ) {
 #endif
 #endif
 
+#if SDL_VERSION_ATLEAST(2, 0, 0)
 	if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER)) // init joystick to work around SDL 2.0.9 bug #4391
+#else
+	if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO)) // no gamecontroller support in SDL1
+#endif
+	{
 		Sys_Error("Error while initializing SDL: %s", SDL_GetError());
+	}
 
 	Sys_InitThreads();
 
diff --git a/neo/framework/KeyInput.cpp b/neo/framework/KeyInput.cpp
index b239613e..56584114 100644
--- a/neo/framework/KeyInput.cpp
+++ b/neo/framework/KeyInput.cpp
@@ -109,39 +109,42 @@ static const keyname_t keynames[] =
 	{"MWHEELUP",		K_MWHEELUP,			"#str_07131"},
 	{"MWHEELDOWN",		K_MWHEELDOWN,		"#str_07132"},
 
-	{"JOY1",			K_JOY1,				"#str_07062"},
-	{"JOY2",			K_JOY2,				"#str_07063"},
-	{"JOY3",			K_JOY3,				"#str_07064"},
-	{"JOY4",			K_JOY4,				"#str_07065"},
-	{"JOY5",			K_JOY5,				"#str_07066"},
-	{"JOY6",			K_JOY6,				"#str_07067"},
-	{"JOY7",			K_JOY7,				"#str_07068"},
-	{"JOY8",			K_JOY8,				"#str_07069"},
-	{"JOY9",			K_JOY9,				"#str_07070"},
-	{"JOY10",			K_JOY10,			"#str_07071"},
-	{"JOY11",			K_JOY11,			"#str_07072"},
-	{"JOY12",			K_JOY12,			"#str_07073"},
-	{"JOY13",			K_JOY13,			"#str_07074"},
-	{"JOY14",			K_JOY14,			"#str_07075"},
-	{"JOY15",			K_JOY15,			"#str_07076"},
+	// Note: for localized gamepad key names, we use Sys_GetLocalizedJoyKeyName()
+	//       so the last column is just NULL
+	{"JOY_BTN_SOUTH",		K_JOY_BTN_SOUTH,		NULL},
+	{"JOY_BTN_EAST",		K_JOY_BTN_EAST,			NULL},
+	{"JOY_BTN_WEST",		K_JOY_BTN_WEST,			NULL},
+	{"JOY_BTN_NORTH",		K_JOY_BTN_NORTH,		NULL},
+	{"JOY_BTN_BACK",		K_JOY_BTN_BACK,			NULL},
+	// leaving out K_JOY_BTN_GUIDE, as I think it shouldn't be used (might open Steam or similar)
+	{"JOY_BTN_START",		K_JOY_BTN_START,		NULL},
+	{"JOY_BTN_LSTICK",		K_JOY_BTN_LSTICK,		NULL},
+	{"JOY_BTN_RSTICK",		K_JOY_BTN_RSTICK,		NULL},
+	{"JOY_BTN_LSHOULDER",	K_JOY_BTN_LSHOULDER,	NULL},
+	{"JOY_BTN_RSHOULDER",	K_JOY_BTN_RSHOULDER,	NULL},
+	{"JOY_BTN_MISC1",		K_JOY_BTN_MISC1,		NULL},
+	{"JOY_BTN_RPADDLE1",	K_JOY_BTN_RPADDLE1,		NULL},
+	{"JOY_BTN_LPADDLE1",	K_JOY_BTN_LPADDLE1,		NULL},
+	{"JOY_BTN_RPADDLE2",	K_JOY_BTN_RPADDLE2,		NULL},
+	{"JOY_BTN_LPADDLE2",	K_JOY_BTN_LPADDLE2,		NULL},
 
-	{"JOY_STICK1_UP", 		K_JOY_STICK1_UP, 		"JOY_STICK1_UP"},
-	{"JOY_STICK1_DOWN", 	K_JOY_STICK1_DOWN, 		"JOY_STICK1_DOWN"},
-	{"JOY_STICK1_LEFT", 	K_JOY_STICK1_LEFT, 		"JOY_STICK1_LEFT"},
-	{"JOY_STICK1_RIGHT", 	K_JOY_STICK1_RIGHT, 	"JOY_STICK1_RIGHT"},
+	{"JOY_STICK1_UP", 		K_JOY_STICK1_UP, 		NULL},
+	{"JOY_STICK1_DOWN", 	K_JOY_STICK1_DOWN, 		NULL},
+	{"JOY_STICK1_LEFT", 	K_JOY_STICK1_LEFT, 		NULL},
+	{"JOY_STICK1_RIGHT", 	K_JOY_STICK1_RIGHT, 	NULL},
 
-	{"JOY_STICK2_UP", 		K_JOY_STICK2_UP, 		"JOY_STICK2_UP"},
-	{"JOY_STICK2_DOWN", 	K_JOY_STICK2_DOWN, 		"JOY_STICK2_DOWN"},
-	{"JOY_STICK2_LEFT", 	K_JOY_STICK2_LEFT, 		"JOY_STICK2_LEFT"},
-	{"JOY_STICK2_RIGHT", 	K_JOY_STICK2_RIGHT, 	"JOY_STICK2_RIGHT"},
+	{"JOY_STICK2_UP", 		K_JOY_STICK2_UP, 		NULL},
+	{"JOY_STICK2_DOWN", 	K_JOY_STICK2_DOWN, 		NULL},
+	{"JOY_STICK2_LEFT", 	K_JOY_STICK2_LEFT, 		NULL},
+	{"JOY_STICK2_RIGHT", 	K_JOY_STICK2_RIGHT, 	NULL},
 
-	{"JOY_TRIGGER1", 		K_JOY_TRIGGER1, 		"JOY_TRIGGER1"},
-	{"JOY_TRIGGER2", 		K_JOY_TRIGGER2, 		"JOY_TRIGGER2"},
+	{"JOY_TRIGGER1", 		K_JOY_TRIGGER1, 		NULL},
+	{"JOY_TRIGGER2", 		K_JOY_TRIGGER2, 		NULL},
 
-	{"JOY_DPAD_UP", 		K_JOY_DPAD_UP, 			"JOY_DPAD_UP"},
-	{"JOY_DPAD_DOWN", 		K_JOY_DPAD_DOWN, 		"JOY_DPAD_DOWN"},
-	{"JOY_DPAD_LEFT", 		K_JOY_DPAD_LEFT, 		"JOY_DPAD_LEFT"},
-	{"JOY_DPAD_RIGHT", 		K_JOY_DPAD_RIGHT, 		"JOY_DPAD_RIGHT"},
+	{"JOY_DPAD_UP", 		K_JOY_DPAD_UP, 			NULL},
+	{"JOY_DPAD_DOWN", 		K_JOY_DPAD_DOWN, 		NULL},
+	{"JOY_DPAD_LEFT", 		K_JOY_DPAD_LEFT, 		NULL},
+	{"JOY_DPAD_RIGHT", 		K_JOY_DPAD_RIGHT, 		NULL},
 
 	{"AUX1",			K_AUX1,				"#str_07094"},
 	{"AUX2",			K_AUX2,				"#str_07095"},
@@ -410,6 +413,12 @@ const char *idKeyInput::KeyNumToString( int keynum, bool localized ) {
 		}
 	}
 
+	if ( localized && keynum >= K_FIRST_JOY && keynum <= K_LAST_JOY ) {
+		const char* jname = Sys_GetLocalizedJoyKeyName(keynum);
+		if(jname != NULL)
+			return jname;
+	}
+
 	// check for a key string
 	for ( kn = keynames; kn->name; kn++ ) {
 		if ( keynum == kn->keynum ) {
diff --git a/neo/framework/KeyInput.h b/neo/framework/KeyInput.h
index e959a1f7..c394ba47 100644
--- a/neo/framework/KeyInput.h
+++ b/neo/framework/KeyInput.h
@@ -139,25 +139,27 @@ typedef enum {
 	K_MWHEELUP,
 
 	//------------------------
-	// K_JOY codes must be contiguous, too
+	// K_JOY codes must be contiguous, too, and K_JOY_BTN_* should be kept in sync with J_BTN_* of sys_jEvents
 	//------------------------
 
-	K_JOY1 = 197,
-	K_JOY2,
-	K_JOY3,
-	K_JOY4,
-	K_JOY5,
-	K_JOY6,
-	K_JOY7,
-	K_JOY8,
-	K_JOY9,
-	K_JOY10,
-	K_JOY11,
-	K_JOY12,
-	K_JOY13,
-	K_JOY14,
-	K_JOY15,
-	K_JOY16,
+	K_FIRST_JOY = 197,
+	K_JOY_BTN_SOUTH = K_FIRST_JOY, // bottom face button, like Xbox A
+	K_JOY_BTN_EAST,  // right face button, like Xbox B
+	K_JOY_BTN_WEST,  // left face button, like Xbox X
+	K_JOY_BTN_NORTH, // top face button, like Xbox Y
+	K_JOY_BTN_BACK,
+	K_JOY_BTN_GUIDE, // Note: this one should probably not be used?
+	K_JOY_BTN_START,
+	K_JOY_BTN_LSTICK, // press left stick
+	K_JOY_BTN_RSTICK, // press right stick
+	K_JOY_BTN_LSHOULDER,
+	K_JOY_BTN_RSHOULDER,
+	// NOTE: in SDL3, the 4 DPAD buttons would be following, we have those later
+	K_JOY_BTN_MISC1, // Additional button (e.g. Xbox Series X share button, PS5 microphone button, Nintendo Switch Pro capture button, Amazon Luna microphone button)
+	K_JOY_BTN_RPADDLE1, // Upper or primary paddle, under your right hand (e.g. Xbox Elite paddle P1)
+	K_JOY_BTN_LPADDLE1, // Upper or primary paddle, under your left hand (e.g. Xbox Elite paddle P3)
+	K_JOY_BTN_RPADDLE2, // Lower or secondary paddle, under your right hand (e.g. Xbox Elite paddle P2)
+	K_JOY_BTN_LPADDLE2, //  Lower or secondary paddle, under your left hand (e.g. Xbox Elite paddle P4)
 
 	K_JOY_STICK1_UP,
 	K_JOY_STICK1_DOWN,
@@ -176,6 +178,7 @@ typedef enum {
 	K_JOY_DPAD_DOWN,
 	K_JOY_DPAD_LEFT,
 	K_JOY_DPAD_RIGHT,
+	K_LAST_JOY = K_JOY_DPAD_RIGHT,
 
 	K_GRAVE_A = 229,	// lowercase a with grave accent FIXME: used to be 224; this probably isn't used anyway
 
diff --git a/neo/framework/UsercmdGen.cpp b/neo/framework/UsercmdGen.cpp
index 6fd46169..2b70e8d7 100644
--- a/neo/framework/UsercmdGen.cpp
+++ b/neo/framework/UsercmdGen.cpp
@@ -1254,8 +1254,8 @@ void idUsercmdGenLocal::Joystick( void ) {
 		int action;
 		int value;
 		if ( Sys_ReturnJoystickInputEvent( i, action, value ) ) {
-			if ( action >= J_ACTION1 && action <= J_ACTION_MAX ) {
-				int joyButton = K_JOY1 + ( action - J_ACTION1 );
+			if ( action >= J_ACTION_FIRST && action <= J_ACTION_MAX ) {
+				int joyButton = K_FIRST_JOY + ( action - J_ACTION_FIRST );
 				Key( joyButton, ( value != 0 ) );
 			} else if ( ( action >= J_AXIS_MIN ) && ( action <= J_AXIS_MAX ) ) {
 				joystickAxis[ action - J_AXIS_MIN ] = static_cast<float>( value ) / 32767.0f;
diff --git a/neo/sys/events.cpp b/neo/sys/events.cpp
index f310dd22..31a57f20 100644
--- a/neo/sys/events.cpp
+++ b/neo/sys/events.cpp
@@ -189,7 +189,7 @@ static scancodename_t scancodemappings[] = {
 	D3_SC_MAPPING(COMMA),
 	D3_SC_MAPPING(PERIOD),
 	D3_SC_MAPPING(SLASH),
-	// leaving out lots of key incl. from keypad, we already handle them as normal keys
+	// leaving out lots of keys incl. from keypad, we already handle them as normal keys
 	D3_SC_MAPPING(NONUSBACKSLASH),
 	D3_SC_MAPPING(INTERNATIONAL1), /**< used on Asian keyboards, see footnotes in USB doc */
 	D3_SC_MAPPING(INTERNATIONAL2),
@@ -264,6 +264,121 @@ static bool utf8ToISO8859_1(const char* inbuf, char* outbuf, size_t outsize) {
 }
 #endif // SDL2
 
+const char* Sys_GetLocalizedJoyKeyName( int key ) {
+	// Note: trying to keep the returned names short, because the Doom3 binding window doesn't have much space for names..
+
+#if SDL_VERSION_ATLEAST(2, 0, 0) // gamecontroller/gamepad not supported in SDL1
+	if (key >= K_FIRST_JOY && key <= K_LAST_JOY) {
+
+		if (key <= K_JOY_BTN_NORTH) {
+#if SDL_VERSION_ATLEAST(3, 0, 0)
+
+			SDL_GamepadButton gpbtn = SDL_GAMEPAD_BUTTON_SOUTH + (key - K_JOY_BTN_NORTH);
+			SDL_GamepadButtonLabel label = SDL_GetGamepadButtonLabeForTypel(TODO, gpbtn);
+			switch(label) {
+				case SDL_GAMEPAD_BUTTON_LABEL_A:
+					return "Pad A";
+				case SDL_GAMEPAD_BUTTON_LABEL_B:
+					return "Pad B";
+				case SDL_GAMEPAD_BUTTON_LABEL_X:
+					return "Pad X";
+				case SDL_GAMEPAD_BUTTON_LABEL_Y:
+					return "Pad Y";
+				case SDL_GAMEPAD_BUTTON_LABEL_CROSS:
+					return "Pad Cross";
+				case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE:
+					return "Pad Circle";
+				case SDL_GAMEPAD_BUTTON_LABEL_SQUARE:
+					return "Pad Square";
+				case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE:
+					return "Pad Triangle";
+			}
+
+#else // SDL2
+			// using xbox-style names, like SDL2 does (SDL can't tell us if this is a xbox or PS or nintendo or whatever-style gamepad)
+			switch(key) {
+				case K_JOY_BTN_SOUTH:
+					return "Pad A";
+				case K_JOY_BTN_EAST:
+					return "Pad B";
+				case K_JOY_BTN_WEST:
+					return "Pad X";
+				case K_JOY_BTN_NORTH:
+					return "Pad Y";
+			}
+#endif // face button names for SDL2
+		}
+
+		// the labels for the remaining keys are the same for SDL2 and SDL3 (and all controllers)
+		// Note: Would be nicer with "Pad " at the beginning, but then it's too long for the keybinding window :-/
+		switch(key) {
+			case K_JOY_BTN_BACK:
+				return "Pad Back";
+
+			case K_JOY_BTN_GUIDE:
+				return NULL; // ???
+
+			case K_JOY_BTN_START:
+				return "Pad Start";
+			case K_JOY_BTN_LSTICK:
+				return "Pad LStick";
+			case K_JOY_BTN_RSTICK:
+				return "Pad RStick";
+			case K_JOY_BTN_LSHOULDER:
+				return "Pad LShoulder";
+			case K_JOY_BTN_RSHOULDER:
+				return "Pad RShoulder";
+			// NOTE: in SDL3, the 4 DPAD buttons would be following, we have those later
+			case K_JOY_BTN_MISC1:
+				return "Pad Misc";
+			case K_JOY_BTN_RPADDLE1:
+				return "Pad P1";
+			case K_JOY_BTN_LPADDLE1:
+				return "Pad P3";
+			case K_JOY_BTN_RPADDLE2:
+				return "Pad P2";
+			case K_JOY_BTN_LPADDLE2:
+				return "Pad P4";
+
+			case K_JOY_STICK1_UP:
+				return "Stick1 Up";
+			case K_JOY_STICK1_DOWN:
+				return "Stick1 Down";
+			case K_JOY_STICK1_LEFT:
+				return "Stick1 Left";
+			case K_JOY_STICK1_RIGHT:
+				return "Stick1 Right";
+
+			case K_JOY_STICK2_UP:
+				return "Stick2 Up";
+			case K_JOY_STICK2_DOWN:
+				return "Stick2 Down";
+			case K_JOY_STICK2_LEFT:
+				return "Stick2 Left";
+			case K_JOY_STICK2_RIGHT:
+				return "Stick2 Right";
+
+			case K_JOY_TRIGGER1:
+				return "Trigger 1";
+			case K_JOY_TRIGGER2:
+				return "Trigger 2";
+
+			case K_JOY_DPAD_UP:
+				return "DPad Up";
+			case K_JOY_DPAD_DOWN:
+				return "DPad Down";
+			case K_JOY_DPAD_LEFT:
+				return "DPad Left";
+			case K_JOY_DPAD_RIGHT:
+				return "DPad Right";
+			default:
+				assert(0 && "missing a case in Sys_GetLocalizedJoyKeyName() for axes or dpad!");
+		}
+	}
+#endif // SDL2+
+	return NULL;
+}
+
 // returns localized name of the key (between K_FIRST_SCANCODE and K_LAST_SCANCODE),
 // regarding the current keyboard layout - if that name is in ASCII or corresponds
 // to a "High-ASCII" char supported by Doom3.
@@ -495,33 +610,35 @@ static byte mapkey(SDL_Keycode key) {
 	return 0;
 }
 
-static sys_jEvents mapjoybutton(SDL_GameControllerButton button) {
+#if SDL_VERSION_ATLEAST(2, 0, 0)
 
+static sys_jEvents mapjoybutton(SDL_GameControllerButton button) {
 	switch (button)
 	{
 	case SDL_CONTROLLER_BUTTON_A:
-		return J_ACTION1;
+		return J_BTN_SOUTH;
 	case SDL_CONTROLLER_BUTTON_B:
-		return J_ACTION2;
+		return J_BTN_EAST;
 	case SDL_CONTROLLER_BUTTON_X:
-		return J_ACTION3;
+		return J_BTN_WEST;
 	case SDL_CONTROLLER_BUTTON_Y:
-		return J_ACTION4;
+		return J_BTN_NORTH;
 	case SDL_CONTROLLER_BUTTON_BACK:
-		return J_ACTION10;
+		return J_BTN_BACK;
 	case SDL_CONTROLLER_BUTTON_GUIDE:
-		// TODO:
+		// TODO: this one should probably not be bindable?
+		//return J_BTN_GUIDE;
 		break;
 	case SDL_CONTROLLER_BUTTON_START:
-		return J_ACTION9;
+		return J_BTN_START;
 	case SDL_CONTROLLER_BUTTON_LEFTSTICK:
-		return J_ACTION7;
+		return J_BTN_LSTICK;
 	case SDL_CONTROLLER_BUTTON_RIGHTSTICK:
-		return J_ACTION8;
+		return J_BTN_RSTICK;
 	case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:
-		return J_ACTION5;
+		return J_BTN_LSHOULDER;
 	case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER:
-		return J_ACTION6;
+		return J_BTN_RSHOULDER;
 	case SDL_CONTROLLER_BUTTON_DPAD_UP:
 		return J_DPAD_UP;
 	case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
@@ -530,16 +647,26 @@ static sys_jEvents mapjoybutton(SDL_GameControllerButton button) {
 		return J_DPAD_LEFT;
 	case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
 		return J_DPAD_RIGHT;
+	// TODO: have the following always been supported in SDL2?
+	case SDL_CONTROLLER_BUTTON_MISC1:
+		return J_BTN_MISC1;
+	case SDL_CONTROLLER_BUTTON_PADDLE1:
+		return J_BTN_RPADDLE1;
+	case SDL_CONTROLLER_BUTTON_PADDLE2:
+		return J_BTN_RPADDLE2;
+	case SDL_CONTROLLER_BUTTON_PADDLE3:
+		return J_BTN_LPADDLE1;
+	case SDL_CONTROLLER_BUTTON_PADDLE4:
+		return J_BTN_LPADDLE2;
+
 	default:
 		common->Warning("unknown game controller button %u", button);
 		break;
 	}
-
 	return MAX_JOY_EVENT;
 }
 
 static sys_jEvents mapjoyaxis(SDL_GameControllerAxis axis) {
-
 	switch (axis)
 	{
 	case SDL_CONTROLLER_AXIS_LEFTX:
@@ -558,9 +685,9 @@ static sys_jEvents mapjoyaxis(SDL_GameControllerAxis axis) {
 		common->Warning("unknown game controller axis %u", axis);
 		break;
 	}
-
 	return J_AXIS_MAX;
 }
+#endif // SDL2+ gamecontroller code
 
 static void PushConsoleEvent(const char *s) {
 	char *b;
@@ -623,12 +750,13 @@ void Sys_InitInput() {
 	memset( buttonStates, 0, sizeof( buttonStates ) );
 	memset( joyAxis, 0, sizeof( joyAxis ) );
 
+#if SDL_VERSION_ATLEAST(2, 0, 0) // gamecontroller/gamepad not supported in SDL1
 	const int NumJoysticks = SDL_NumJoysticks();
-	printf("XXX found %d joysticks\n", NumJoysticks);
 	for( int i = 0; i < NumJoysticks; ++i )
 	{
 		SDL_GameController* gc = SDL_GameControllerOpen( i );
 	}
+#endif
 }
 
 /*
@@ -1078,6 +1206,7 @@ sysEvent_t Sys_GetEvent() {
 
 			return res;
 
+#if SDL_VERSION_ATLEAST(2, 0, 0) // gamecontroller/gamepad not supported in SDL1
 		case SDL_CONTROLLERBUTTONDOWN:
 		case SDL_CONTROLLERBUTTONUP:
 		{
@@ -1086,8 +1215,8 @@ sysEvent_t Sys_GetEvent() {
 
 			res.evType = SE_KEY;
 			res.evValue2 = ev.cbutton.state == SDL_PRESSED ? 1 : 0;
-			if ( ( jEvent >= J_ACTION1 ) && ( jEvent <= J_ACTION_MAX ) ) {
-				res.evValue = K_JOY1 + ( jEvent - J_ACTION1 );
+			if ( ( jEvent >= J_BTN_SOUTH ) && ( jEvent <= J_ACTION_MAX ) ) {
+				res.evValue = K_JOY_BTN_SOUTH + ( jEvent - J_BTN_SOUTH );
 				return res;
 			} else if ( ( jEvent >= J_DPAD_UP ) && ( jEvent <= J_DPAD_RIGHT ) ) {
 				res.evValue = K_JOY_DPAD_UP + ( jEvent - J_DPAD_UP );
@@ -1147,6 +1276,7 @@ sysEvent_t Sys_GetEvent() {
 			// TODO: hot swapping maybe.
 			//lbOnControllerUnPlug(event.jdevice.which);
 			break;
+#endif // SDL2+
 
 		case SDL_QUIT:
 			PushConsoleEvent("quit");
diff --git a/neo/sys/sys_public.h b/neo/sys/sys_public.h
index d409a638..03592d66 100644
--- a/neo/sys/sys_public.h
+++ b/neo/sys/sys_public.h
@@ -78,41 +78,30 @@ typedef enum {
 } sys_mEvents;
 
 typedef enum {
-	J_ACTION1,
-	J_ACTION2,
-	J_ACTION3,
-	J_ACTION4,
-	J_ACTION5,
-	J_ACTION6,
-	J_ACTION7,
-	J_ACTION8,
-	J_ACTION9,
-	J_ACTION10,
-	J_ACTION11,
-	J_ACTION12,
-	J_ACTION13,
-	J_ACTION14,
-	J_ACTION15,
-	J_ACTION16,
-	J_ACTION17,
-	J_ACTION18,
-	J_ACTION19,
-	J_ACTION20,
-	J_ACTION21,
-	J_ACTION22,
-	J_ACTION23,
-	J_ACTION24,
-	J_ACTION25,
-	J_ACTION26,
-	J_ACTION27,
-	J_ACTION28,
-	J_ACTION29,
-	J_ACTION30,
-	J_ACTION31,
-	J_ACTION32,
-	J_ACTION_MAX = J_ACTION32,
+	J_ACTION_FIRST,
+	// these names are similar to the SDL3 SDL_GamepadButton names
+	J_BTN_SOUTH = J_ACTION_FIRST, // bottom face button, like Xbox A
+	J_BTN_EAST,  // right face button, like Xbox B
+	J_BTN_WEST,  // left face button, like Xbox X
+	J_BTN_NORTH, // top face button, like Xbox Y
+	J_BTN_BACK,
+	J_BTN_GUIDE, // Note: this one should probably not be used?
+	J_BTN_START,
+	J_BTN_LSTICK, // press left stick
+	J_BTN_RSTICK, // press right stick
+	J_BTN_LSHOULDER,
+	J_BTN_RSHOULDER,
+	// NOTE: in SDL3, the 4 DPAD buttons would be following, we have those at the end
+	J_BTN_MISC1, // Additional button (e.g. Xbox Series X share button, PS5 microphone button, Nintendo Switch Pro capture button, Amazon Luna microphone button)
+	J_BTN_RPADDLE1, // Upper or primary paddle, under your right hand (e.g. Xbox Elite paddle P1)
+	J_BTN_LPADDLE1, // Upper or primary paddle, under your left hand (e.g. Xbox Elite paddle P3)
+	J_BTN_RPADDLE2, // Lower or secondary paddle, under your right hand (e.g. Xbox Elite paddle P2)
+	J_BTN_LPADDLE2, //  Lower or secondary paddle, under your left hand (e.g. Xbox Elite paddle P4)
 
-	J_AXIS_MIN,
+	J_ACTION_MAX = J_BTN_LPADDLE2,
+	// leaving some space here for about 16 additional J_ACTIONs, if needed
+
+	J_AXIS_MIN = 32,
 	J_AXIS_LEFT_X = J_AXIS_MIN + AXIS_LEFT_X,
 	J_AXIS_LEFT_Y = J_AXIS_MIN + AXIS_LEFT_Y,
 	J_AXIS_RIGHT_X = J_AXIS_MIN + AXIS_RIGHT_X,
@@ -232,6 +221,12 @@ const char* Sys_GetLocalizedScancodeName( int key );
 // returns keyNum_t (K_SC_* constant) for given scancode name (like "SC_A")
 int Sys_GetKeynumForScancodeName( const char* name );
 
+// returns display name of the key (between K_FIRST_JOY and K_LAST_JOY)
+// With SDL2 it'll return the name in the SDL_GameController standard layout
+// (which is based on XBox/XInput => on Nintendo gamepads, A/B and X/Y will be flipped),
+// with SDL3 it will return the "real" button name
+const char* Sys_GetLocalizedJoyKeyName( int key );
+
 // keyboard input polling
 int				Sys_PollKeyboardInputEvents( void );
 int				Sys_ReturnKeyboardInputEvent( const int n, int &ch, bool &state );

From f8557f6bd58753800bb4f8ca326914b931eee5dd Mon Sep 17 00:00:00 2001
From: wof8317 <wof8317@gmail.com>
Date: Mon, 15 Jan 2024 15:18:55 -0600
Subject: [PATCH 03/14] Modified some code to compile

---
 neo/ui/Window.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/neo/ui/Window.cpp b/neo/ui/Window.cpp
index 8e82453f..3dda7118 100644
--- a/neo/ui/Window.cpp
+++ b/neo/ui/Window.cpp
@@ -736,7 +736,7 @@ const char *idWindow::HandleEvent(const sysEvent_t *event, bool *updateVisuals)
 				*updateVisuals = true;
 			}
 
-			if (event->evValue == K_MOUSE1 || event->evValue == K_JOY2) {
+			if (event->evValue == K_MOUSE1 || event->evValue == K_JOY_BTN_EAST) {
 
 				if (!event->evValue2 && GetCaptureChild()) {
 					GetCaptureChild()->LoseCapture();
@@ -785,7 +785,7 @@ const char *idWindow::HandleEvent(const sysEvent_t *event, bool *updateVisuals)
 				} else if (!actionUpRun) {
 					actionUpRun = RunScript( ON_ACTIONRELEASE );
 				}
-			} else if (event->evValue == K_MOUSE2 || event->evValue == K_JOY1) {
+			} else if (event->evValue == K_MOUSE2 || event->evValue == K_JOY_BTN_SOUTH) {
 
 				if (!event->evValue2 && GetCaptureChild()) {
 					GetCaptureChild()->LoseCapture();

From 6eac0540bf0f20160af528db679d09eb41b3bc47 Mon Sep 17 00:00:00 2001
From: Daniel Gibson <metalcaedes@gmail.com>
Date: Tue, 16 Jan 2024 17:26:45 +0100
Subject: [PATCH 04/14] Various gamepad improvements

- treat DPad as 4 regular buttons (was already the case mostly, but now
  the code is simpler)
- rename in_invertLook to joy_invertLook and in_useJoystick to
  in_useGamepad and remove unused CVars
- make controller Start button generate K_ESCAPE events, so it can
  always be used to open/close the menu (similar to D3BFG)
- move mousecursor with sticks, A button (south) for left-click,
  B button (east) for right-click (doesn't work in PDA yet)
- removed special handling of K_JOY_BTN_* in idWindow::HandleEvent()
  by generating fake mouse button events for gamepad A/B
  in idUserInterfaceLocal::HandleEvent()
---
 neo/framework/KeyInput.cpp   |  9 ++--
 neo/framework/KeyInput.h     | 14 +++---
 neo/framework/UsercmdGen.cpp | 20 +++-----
 neo/sys/events.cpp           | 95 ++++++++++++++++++++++++++----------
 neo/sys/sys_public.h         | 16 +++---
 neo/ui/UserInterface.cpp     | 31 ++++++++++++
 neo/ui/Window.cpp            |  4 +-
 7 files changed, 131 insertions(+), 58 deletions(-)

diff --git a/neo/framework/KeyInput.cpp b/neo/framework/KeyInput.cpp
index 56584114..299519c9 100644
--- a/neo/framework/KeyInput.cpp
+++ b/neo/framework/KeyInput.cpp
@@ -122,6 +122,10 @@ static const keyname_t keynames[] =
 	{"JOY_BTN_RSTICK",		K_JOY_BTN_RSTICK,		NULL},
 	{"JOY_BTN_LSHOULDER",	K_JOY_BTN_LSHOULDER,	NULL},
 	{"JOY_BTN_RSHOULDER",	K_JOY_BTN_RSHOULDER,	NULL},
+	{"JOY_DPAD_UP", 		K_JOY_DPAD_UP, 			NULL},
+	{"JOY_DPAD_DOWN", 		K_JOY_DPAD_DOWN, 		NULL},
+	{"JOY_DPAD_LEFT", 		K_JOY_DPAD_LEFT, 		NULL},
+	{"JOY_DPAD_RIGHT", 		K_JOY_DPAD_RIGHT, 		NULL},
 	{"JOY_BTN_MISC1",		K_JOY_BTN_MISC1,		NULL},
 	{"JOY_BTN_RPADDLE1",	K_JOY_BTN_RPADDLE1,		NULL},
 	{"JOY_BTN_LPADDLE1",	K_JOY_BTN_LPADDLE1,		NULL},
@@ -141,11 +145,6 @@ static const keyname_t keynames[] =
 	{"JOY_TRIGGER1", 		K_JOY_TRIGGER1, 		NULL},
 	{"JOY_TRIGGER2", 		K_JOY_TRIGGER2, 		NULL},
 
-	{"JOY_DPAD_UP", 		K_JOY_DPAD_UP, 			NULL},
-	{"JOY_DPAD_DOWN", 		K_JOY_DPAD_DOWN, 		NULL},
-	{"JOY_DPAD_LEFT", 		K_JOY_DPAD_LEFT, 		NULL},
-	{"JOY_DPAD_RIGHT", 		K_JOY_DPAD_RIGHT, 		NULL},
-
 	{"AUX1",			K_AUX1,				"#str_07094"},
 	{"AUX2",			K_AUX2,				"#str_07095"},
 	{"AUX3",			K_AUX3,				"#str_07096"},
diff --git a/neo/framework/KeyInput.h b/neo/framework/KeyInput.h
index c394ba47..acf7527d 100644
--- a/neo/framework/KeyInput.h
+++ b/neo/framework/KeyInput.h
@@ -147,6 +147,7 @@ typedef enum {
 	K_JOY_BTN_EAST,  // right face button, like Xbox B
 	K_JOY_BTN_WEST,  // left face button, like Xbox X
 	K_JOY_BTN_NORTH, // top face button, like Xbox Y
+
 	K_JOY_BTN_BACK,
 	K_JOY_BTN_GUIDE, // Note: this one should probably not be used?
 	K_JOY_BTN_START,
@@ -154,7 +155,12 @@ typedef enum {
 	K_JOY_BTN_RSTICK, // press right stick
 	K_JOY_BTN_LSHOULDER,
 	K_JOY_BTN_RSHOULDER,
-	// NOTE: in SDL3, the 4 DPAD buttons would be following, we have those later
+
+	K_JOY_DPAD_UP,
+	K_JOY_DPAD_DOWN,
+	K_JOY_DPAD_LEFT,
+	K_JOY_DPAD_RIGHT,
+
 	K_JOY_BTN_MISC1, // Additional button (e.g. Xbox Series X share button, PS5 microphone button, Nintendo Switch Pro capture button, Amazon Luna microphone button)
 	K_JOY_BTN_RPADDLE1, // Upper or primary paddle, under your right hand (e.g. Xbox Elite paddle P1)
 	K_JOY_BTN_LPADDLE1, // Upper or primary paddle, under your left hand (e.g. Xbox Elite paddle P3)
@@ -174,11 +180,7 @@ typedef enum {
 	K_JOY_TRIGGER1,
 	K_JOY_TRIGGER2,
 
-	K_JOY_DPAD_UP,
-	K_JOY_DPAD_DOWN,
-	K_JOY_DPAD_LEFT,
-	K_JOY_DPAD_RIGHT,
-	K_LAST_JOY = K_JOY_DPAD_RIGHT,
+	K_LAST_JOY = K_JOY_TRIGGER2,
 
 	K_GRAVE_A = 229,	// lowercase a with grave accent FIXME: used to be 224; this probably isn't used anyway
 
diff --git a/neo/framework/UsercmdGen.cpp b/neo/framework/UsercmdGen.cpp
index 2b70e8d7..8c5cc388 100644
--- a/neo/framework/UsercmdGen.cpp
+++ b/neo/framework/UsercmdGen.cpp
@@ -428,8 +428,6 @@ idCVar idUsercmdGenLocal::m_smooth( "m_smooth", "1", CVAR_SYSTEM | CVAR_ARCHIVE
 idCVar idUsercmdGenLocal::m_strafeSmooth( "m_strafeSmooth", "4", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_INTEGER, "number of samples blended for mouse moving", 1, 8, idCmdSystem::ArgCompletion_Integer<1,8> );
 idCVar idUsercmdGenLocal::m_showMouseRate( "m_showMouseRate", "0", CVAR_SYSTEM | CVAR_BOOL, "shows mouse movement" );
 
-idCVar joy_mergedThreshold( "joy_mergedThreshold", "1", CVAR_BOOL | CVAR_ARCHIVE, "If the thresholds aren't merged, you drift more off center" );
-idCVar joy_newCode( "joy_newCode", "0", CVAR_BOOL | CVAR_ARCHIVE, "Use the new codepath" );
 idCVar joy_triggerThreshold( "joy_triggerThreshold", "0.05", CVAR_FLOAT | CVAR_ARCHIVE, "how far the joystick triggers have to be pressed before they register as down" );
 idCVar joy_deadZone( "joy_deadZone", "0.4", CVAR_FLOAT | CVAR_ARCHIVE, "specifies how large the dead-zone is on the joystick" );
 idCVar joy_range( "joy_range", "1.0", CVAR_FLOAT | CVAR_ARCHIVE, "allow full range to be mapped to a smaller offset" );
@@ -437,14 +435,15 @@ idCVar joy_gammaLook( "joy_gammaLook", "1", CVAR_INTEGER | CVAR_ARCHIVE, "use a
 idCVar joy_powerScale( "joy_powerScale", "2", CVAR_FLOAT | CVAR_ARCHIVE, "Raise joystick values to this power" );
 idCVar joy_pitchSpeed( "joy_pitchSpeed", "130",	CVAR_ARCHIVE | CVAR_FLOAT, "pitch speed when pressing up or down on the joystick", 60, 600 );
 idCVar joy_yawSpeed( "joy_yawSpeed", "240",	CVAR_ARCHIVE | CVAR_FLOAT, "pitch speed when pressing left or right on the joystick", 60, 600 );
+idCVar joy_invertLook( "joy_invertLook", "0", CVAR_ARCHIVE | CVAR_BOOL, "inverts the look controls so the forward looks up (flight controls) - the proper way to play games!" );
 
 // these were a bad idea!
 idCVar joy_dampenLook( "joy_dampenLook", "1", CVAR_BOOL | CVAR_ARCHIVE, "Do not allow full acceleration on look" );
 idCVar joy_deltaPerMSLook( "joy_deltaPerMSLook", "0.003", CVAR_FLOAT | CVAR_ARCHIVE, "Max amount to be added on look per MS" );
 
-idCVar in_useJoystick( "in_useJoystick", "1", CVAR_ARCHIVE | CVAR_BOOL, "enables/disables the gamepad for PC use" );
-idCVar in_invertLook( "in_invertLook", "0", CVAR_ARCHIVE | CVAR_BOOL, "inverts the look controls so the forward looks up (flight controls) - the proper way to play games!" );
-idCVar in_mouseInvertLook( "in_mouseInvertLook", "0", CVAR_ARCHIVE | CVAR_BOOL, "inverts the look controls so the forward looks up (flight controls) - the proper way to play games!" );
+idCVar in_useGamepad( "in_useGamepad", "1", CVAR_ARCHIVE | CVAR_BOOL, "enables/disables the gamepad for PC use" );
+
+// TODO idCVar in_mouseInvertLook( "in_mouseInvertLook", "0", CVAR_ARCHIVE | CVAR_BOOL, "inverts the look controls so the forward looks up (flight controls) - the proper way to play games!" );
 
 static idUsercmdGenLocal localUsercmdGen;
 idUsercmdGen	*usercmdGen = &localUsercmdGen;
@@ -838,7 +837,7 @@ void idUsercmdGenLocal::HandleJoystickAxis( int keyNum, float unclampedValue, fl
 				lastLookValuePitch = lookValue;
 			}
 
-			float invertPitch = in_invertLook.GetBool() ? -1.0f : 1.0f;
+			float invertPitch = joy_invertLook.GetBool() ? -1.0f : 1.0f;
 			viewangles[PITCH] -= MS2SEC( pollTime - lastPollTime ) * lookValue * joy_pitchSpeed.GetFloat() * invertPitch;
 			break;
 		}
@@ -848,7 +847,7 @@ void idUsercmdGenLocal::HandleJoystickAxis( int keyNum, float unclampedValue, fl
 				lastLookValuePitch = lookValue;
 			}
 
-			float invertPitch = in_invertLook.GetBool() ? -1.0f : 1.0f;
+			float invertPitch = joy_invertLook.GetBool() ? -1.0f : 1.0f;
 			viewangles[PITCH] += MS2SEC( pollTime - lastPollTime ) * lookValue * joy_pitchSpeed.GetFloat() * invertPitch;
 			break;
 		}
@@ -1259,9 +1258,6 @@ void idUsercmdGenLocal::Joystick( void ) {
 				Key( joyButton, ( value != 0 ) );
 			} else if ( ( action >= J_AXIS_MIN ) && ( action <= J_AXIS_MAX ) ) {
 				joystickAxis[ action - J_AXIS_MIN ] = static_cast<float>( value ) / 32767.0f;
-			} else if ( action >= J_DPAD_UP && action <= J_DPAD_RIGHT ) {
-				int joyButton = K_JOY_DPAD_UP + ( action - J_DPAD_UP );
-				Key( joyButton, ( value != 0 ) );
 			} else {
 				//assert( !"Unknown joystick event" );
 			}
@@ -1294,7 +1290,7 @@ void idUsercmdGenLocal::UsercmdInterrupt( void ) {
 	Keyboard();
 
 	// process the system joystick events
-	if ( in_useJoystick.GetBool() ) {
+	if ( in_useGamepad.GetBool() ) {
 		Joystick();
 	}
 
@@ -1341,7 +1337,7 @@ usercmd_t idUsercmdGenLocal::GetDirectUsercmd( void ) {
 	Keyboard();
 
 	// process the system joystick events
-	if ( in_useJoystick.GetBool() ) {
+	if ( in_useGamepad.GetBool() ) {
 		Joystick();
 	}
 	// create the usercmd
diff --git a/neo/sys/events.cpp b/neo/sys/events.cpp
index 31a57f20..91497e56 100644
--- a/neo/sys/events.cpp
+++ b/neo/sys/events.cpp
@@ -60,6 +60,8 @@ If you have questions concerning this license or the applicable additional terms
 #define SDLK_PRINTSCREEN SDLK_PRINT
 #endif
 
+extern idCVar in_useGamepad; // from UsercmdGen.cpp
+
 // NOTE: g++-4.7 doesn't like when this is static (for idCmdSystem::ArgCompletion_String<kbdNames>)
 const char *_in_kbdNames[] = {
 #if SDL_VERSION_ATLEAST(2, 0, 0) // auto-detection is only available for SDL2
@@ -125,7 +127,7 @@ static idList<mouse_poll_t> mouse_polls;
 static idList<joystick_poll_t> joystick_polls;
 
 static bool buttonStates[K_LAST_KEY];
-static int  joyAxis[MAX_JOYSTICK_AXIS];
+static float joyAxis[MAX_JOYSTICK_AXIS];
 
 static idList<sysEvent_t> event_overflow;
 
@@ -273,7 +275,7 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 		if (key <= K_JOY_BTN_NORTH) {
 #if SDL_VERSION_ATLEAST(3, 0, 0)
 
-			SDL_GamepadButton gpbtn = SDL_GAMEPAD_BUTTON_SOUTH + (key - K_JOY_BTN_NORTH);
+			SDL_GamepadButton gpbtn = SDL_GAMEPAD_BUTTON_SOUTH + (key - K_JOY_BTN_SOUTH);
 			SDL_GamepadButtonLabel label = SDL_GetGamepadButtonLabeForTypel(TODO, gpbtn);
 			switch(label) {
 				case SDL_GAMEPAD_BUTTON_LABEL_A:
@@ -310,7 +312,6 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 		}
 
 		// the labels for the remaining keys are the same for SDL2 and SDL3 (and all controllers)
-		// Note: Would be nicer with "Pad " at the beginning, but then it's too long for the keybinding window :-/
 		switch(key) {
 			case K_JOY_BTN_BACK:
 				return "Pad Back";
@@ -328,7 +329,16 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 				return "Pad LShoulder";
 			case K_JOY_BTN_RSHOULDER:
 				return "Pad RShoulder";
-			// NOTE: in SDL3, the 4 DPAD buttons would be following, we have those later
+
+			case K_JOY_DPAD_UP:
+				return "DPad Up";
+			case K_JOY_DPAD_DOWN:
+				return "DPad Down";
+			case K_JOY_DPAD_LEFT:
+				return "DPad Left";
+			case K_JOY_DPAD_RIGHT:
+				return "DPad Right";
+
 			case K_JOY_BTN_MISC1:
 				return "Pad Misc";
 			case K_JOY_BTN_RPADDLE1:
@@ -340,6 +350,8 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 			case K_JOY_BTN_LPADDLE2:
 				return "Pad P4";
 
+			// Note: Would be nicer with "Pad " (or even "Gamepad ") at the beginning,
+			//       but then it's too long for the keybinding window :-/
 			case K_JOY_STICK1_UP:
 				return "Stick1 Up";
 			case K_JOY_STICK1_DOWN:
@@ -363,16 +375,8 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 			case K_JOY_TRIGGER2:
 				return "Trigger 2";
 
-			case K_JOY_DPAD_UP:
-				return "DPad Up";
-			case K_JOY_DPAD_DOWN:
-				return "DPad Down";
-			case K_JOY_DPAD_LEFT:
-				return "DPad Left";
-			case K_JOY_DPAD_RIGHT:
-				return "DPad Right";
 			default:
-				assert(0 && "missing a case in Sys_GetLocalizedJoyKeyName() for axes or dpad!");
+				assert(0 && "missing a case in Sys_GetLocalizedJoyKeyName()!");
 		}
 	}
 #endif // SDL2+
@@ -1210,16 +1214,26 @@ sysEvent_t Sys_GetEvent() {
 		case SDL_CONTROLLERBUTTONDOWN:
 		case SDL_CONTROLLERBUTTONUP:
 		{
+			if ( !in_useGamepad.GetBool() ) {
+				common->Warning( "Gamepad support is disabled! Set the in_useGamepad CVar to 1 to enable it!\n" );
+				continue;
+			}
+			// special case: always treat the start button as escape so it opens/closes the menu
+			// (also makes that button non-bindable)
+			if ( ev.cbutton.button == SDL_CONTROLLER_BUTTON_START ) {
+				res.evType = SE_KEY;
+				res.evValue = K_ESCAPE;
+				res.evValue2 = ev.cbutton.state == SDL_PRESSED ? 1 : 0;
+				return res;
+			}
+
 			sys_jEvents jEvent =  mapjoybutton( (SDL_GameControllerButton)ev.cbutton.button);
 			joystick_polls.Append(joystick_poll_t(jEvent, ev.cbutton.state == SDL_PRESSED ? 1 : 0) );
 
 			res.evType = SE_KEY;
 			res.evValue2 = ev.cbutton.state == SDL_PRESSED ? 1 : 0;
-			if ( ( jEvent >= J_BTN_SOUTH ) && ( jEvent <= J_ACTION_MAX ) ) {
-				res.evValue = K_JOY_BTN_SOUTH + ( jEvent - J_BTN_SOUTH );
-				return res;
-			} else if ( ( jEvent >= J_DPAD_UP ) && ( jEvent <= J_DPAD_RIGHT ) ) {
-				res.evValue = K_JOY_DPAD_UP + ( jEvent - J_DPAD_UP );
+			if ( ( jEvent >= J_ACTION_FIRST ) && ( jEvent <= J_ACTION_MAX ) ) {
+				res.evValue = K_FIRST_JOY + ( jEvent - J_ACTION_FIRST );
 				return res;
 			}
 
@@ -1230,6 +1244,12 @@ sysEvent_t Sys_GetEvent() {
 		{
 			const int range = 16384;
 
+			if ( !in_useGamepad.GetBool() ) {
+				// not printing a message here, I guess we get lots of spurious axis events..
+				// TODO: or print a message if value is big enough?
+				continue;
+			}
+
 			sys_jEvents jEvent = mapjoyaxis( (SDL_GameControllerAxis)ev.caxis.axis);
 			joystick_polls.Append(joystick_poll_t(	jEvent, ev.caxis.value) );
 
@@ -1251,15 +1271,18 @@ sysEvent_t Sys_GetEvent() {
 				PushButton( K_JOY_TRIGGER2, ( ev.caxis.value > range ) );
 			}
 			if ( jEvent >= J_AXIS_MIN && jEvent <= J_AXIS_MAX ) {
+				// NOTE: the stuff set here is only used to move the cursor in menus
+				//       ingame movement is done via joystick_polls
 				int axis = jEvent - J_AXIS_MIN;
-				int percent = ( ev.caxis.value * 16 ) / range;
-				if ( joyAxis[axis] != percent ) {
-					joyAxis[axis] = percent;
-					res.evType = SE_JOYSTICK;
-					res.evValue = axis;
-					res.evValue2 = percent;
-					return res;
+				float val = ev.caxis.value * (1.25f / 32767.0f);
+				// 25% deadzone
+				if( val < 0.0f ) {
+					val = fminf(val + 0.25f, 0.0f);
+				} else {
+					val = fmaxf(val - 0.25f, 0.0f);
 				}
+
+				joyAxis[axis] = val;
 			}
 
 			continue; // try to get a decent event.
@@ -1300,6 +1323,28 @@ sysEvent_t Sys_GetEvent() {
 		}
 	}
 
+	// first return joyaxis events, if gamepad is enabled and, and 16ms are over
+	// (or we haven't returned the values for all axis yet)
+	if ( in_useGamepad.GetBool() ) {
+		static unsigned int lastMS = 0;
+		static int joyAxisToSend = 0;
+		unsigned int nowMS = Sys_Milliseconds();
+		if ( nowMS - lastMS >= 16 ) {
+			int val = joyAxis[joyAxisToSend] * 100; // float to percent
+			res.evType = SE_JOYSTICK;
+			res.evValue = joyAxisToSend;
+			res.evValue2 = val;
+			++joyAxisToSend;
+			if(joyAxisToSend == MAX_JOYSTICK_AXIS) {
+				// we're done for this frame, so update lastMS and reset joyAxisToSend
+				joyAxisToSend = 0;
+				lastMS = nowMS;
+			}
+			return res;
+		}
+
+	}
+
 	return res_none;
 }
 
diff --git a/neo/sys/sys_public.h b/neo/sys/sys_public.h
index 03592d66..c7bd89be 100644
--- a/neo/sys/sys_public.h
+++ b/neo/sys/sys_public.h
@@ -91,7 +91,12 @@ typedef enum {
 	J_BTN_RSTICK, // press right stick
 	J_BTN_LSHOULDER,
 	J_BTN_RSHOULDER,
-	// NOTE: in SDL3, the 4 DPAD buttons would be following, we have those at the end
+
+	J_DPAD_UP,
+	J_DPAD_DOWN,
+	J_DPAD_LEFT,
+	J_DPAD_RIGHT,
+
 	J_BTN_MISC1, // Additional button (e.g. Xbox Series X share button, PS5 microphone button, Nintendo Switch Pro capture button, Amazon Luna microphone button)
 	J_BTN_RPADDLE1, // Upper or primary paddle, under your right hand (e.g. Xbox Elite paddle P1)
 	J_BTN_LPADDLE1, // Upper or primary paddle, under your left hand (e.g. Xbox Elite paddle P3)
@@ -111,18 +116,13 @@ typedef enum {
 
 	J_AXIS_MAX = J_AXIS_MIN + MAX_JOYSTICK_AXIS - 1,
 
-	J_DPAD_UP,
-	J_DPAD_DOWN,
-	J_DPAD_LEFT,
-	J_DPAD_RIGHT,
-
 	MAX_JOY_EVENT
 } sys_jEvents;
 
 struct sysEvent_t {
 	sysEventType_t	evType;
-	int				evValue;
-	int				evValue2;
+	int				evValue;	// for keys: K_* or ASCII code; for joystick: axis; for mouse: mouseX
+	int				evValue2;	// for keys: 0/1 for up/down; for axis: value; for mouse: mouseY
 	int				evPtrLength;		// bytes of data pointed to by evPtr, for journaling
 	void *			evPtr;				// this must be manually freed if not NULL
 };
diff --git a/neo/ui/UserInterface.cpp b/neo/ui/UserInterface.cpp
index 032b3c4b..0d101e02 100644
--- a/neo/ui/UserInterface.cpp
+++ b/neo/ui/UserInterface.cpp
@@ -344,6 +344,9 @@ const char *idUserInterfaceLocal::HandleEvent( const sysEvent_t *event, int _tim
 		return ret;
 	}
 
+	// DG: used to turn gamepad A into left mouse click
+	sysEvent_t fakedEvent = {};
+
 	if ( event->evType == SE_MOUSE || event->evType == SE_MOUSE_ABS ) {
 		if ( !desktop || (desktop->GetFlags() & WIN_MENUGUI) ) {
 			// DG: this is a fullscreen GUI, scale the mousedelta added to cursorX/Y
@@ -401,6 +404,34 @@ const char *idUserInterfaceLocal::HandleEvent( const sysEvent_t *event, int _tim
 			cursorY = 0;
 		}
 	}
+	else if ( event->evType == SE_JOYSTICK && event->evValue2 != 0 )
+	{
+		// evValue:  axis = jEvent - J_AXIS_MIN;
+		// evValue2: percent (-100 to 100)
+
+		// currently uses both sticks for cursor movement
+		// TODO could use one stick for scrolling (maybe by generating K_UPARROW/DOWNARROW events?)
+		float addVal = event->evValue2 * 0.1f;
+		if( event->evValue == 0 || event->evValue == 2 ) {
+			cursorX += addVal;
+		} else if( event->evValue == 1 || event->evValue == 3 ) {
+			cursorY += addVal;
+		}
+
+		if (cursorX < 0) {
+			cursorX = 0;
+		}
+		if (cursorY < 0) {
+			cursorY = 0;
+		}
+	}
+	else if( event->evType == SE_KEY && (event->evValue == K_JOY_BTN_SOUTH || event->evValue == K_JOY_BTN_EAST) )
+	{
+		// map gamepad buttons south/east (A/B on xbox controller) to mouse1/2
+		fakedEvent = *event;
+		fakedEvent.evValue = (event->evValue == K_JOY_BTN_SOUTH) ? K_MOUSE1 : K_MOUSE2;
+		event = &fakedEvent;
+	}
 
 	if ( desktop ) {
 		return desktop->HandleEvent( event, updateVisuals );
diff --git a/neo/ui/Window.cpp b/neo/ui/Window.cpp
index 3dda7118..3948cea3 100644
--- a/neo/ui/Window.cpp
+++ b/neo/ui/Window.cpp
@@ -736,7 +736,7 @@ const char *idWindow::HandleEvent(const sysEvent_t *event, bool *updateVisuals)
 				*updateVisuals = true;
 			}
 
-			if (event->evValue == K_MOUSE1 || event->evValue == K_JOY_BTN_EAST) {
+			if (event->evValue == K_MOUSE1) {
 
 				if (!event->evValue2 && GetCaptureChild()) {
 					GetCaptureChild()->LoseCapture();
@@ -785,7 +785,7 @@ const char *idWindow::HandleEvent(const sysEvent_t *event, bool *updateVisuals)
 				} else if (!actionUpRun) {
 					actionUpRun = RunScript( ON_ACTIONRELEASE );
 				}
-			} else if (event->evValue == K_MOUSE2 || event->evValue == K_JOY_BTN_SOUTH) {
+			} else if (event->evValue == K_MOUSE2) {
 
 				if (!event->evValue2 && GetCaptureChild()) {
 					GetCaptureChild()->LoseCapture();

From e0bb01ef52439030a289a94b5440ab888c493453 Mon Sep 17 00:00:00 2001
From: Daniel Gibson <metalcaedes@gmail.com>
Date: Tue, 16 Jan 2024 19:06:08 +0100
Subject: [PATCH 05/14] Gamepad cursor control improvements

- make moving the cursor more precise by using an exponential curve
  for axis value => cursor speed
- emulate cursor keys with DPad and Enter with left trigger
- also use right trigger for leftclick, as it's usually used for firing
  a weapon and thus used for "clicking" ingame UIs
- fix hovering/highlighting menu elements when moving cursor
  with gamepad
---
 neo/ui/UserInterface.cpp | 62 ++++++++++++++++++++++++++++++++++------
 1 file changed, 53 insertions(+), 9 deletions(-)

diff --git a/neo/ui/UserInterface.cpp b/neo/ui/UserInterface.cpp
index 0d101e02..98e3dea5 100644
--- a/neo/ui/UserInterface.cpp
+++ b/neo/ui/UserInterface.cpp
@@ -344,7 +344,7 @@ const char *idUserInterfaceLocal::HandleEvent( const sysEvent_t *event, int _tim
 		return ret;
 	}
 
-	// DG: used to turn gamepad A into left mouse click
+	// DG: used to translate gamepad input into events the UI system is familiar with
 	sysEvent_t fakedEvent = {};
 
 	if ( event->evType == SE_MOUSE || event->evType == SE_MOUSE_ABS ) {
@@ -404,14 +404,17 @@ const char *idUserInterfaceLocal::HandleEvent( const sysEvent_t *event, int _tim
 			cursorY = 0;
 		}
 	}
-	else if ( event->evType == SE_JOYSTICK && event->evValue2 != 0 )
+	else if ( event->evType == SE_JOYSTICK && event->evValue2 != 0 && event->evValue < 4 )
 	{
 		// evValue:  axis = jEvent - J_AXIS_MIN;
 		// evValue2: percent (-100 to 100)
 
 		// currently uses both sticks for cursor movement
 		// TODO could use one stick for scrolling (maybe by generating K_UPARROW/DOWNARROW events?)
-		float addVal = event->evValue2 * 0.1f;
+		float addVal = expf( fabsf(event->evValue2 * 0.03f) ) * 0.5f;
+		if(event->evValue2 < 0)
+			addVal = -addVal;
+
 		if( event->evValue == 0 || event->evValue == 2 ) {
 			cursorX += addVal;
 		} else if( event->evValue == 1 || event->evValue == 3 ) {
@@ -424,14 +427,55 @@ const char *idUserInterfaceLocal::HandleEvent( const sysEvent_t *event, int _tim
 		if (cursorY < 0) {
 			cursorY = 0;
 		}
-	}
-	else if( event->evType == SE_KEY && (event->evValue == K_JOY_BTN_SOUTH || event->evValue == K_JOY_BTN_EAST) )
-	{
-		// map gamepad buttons south/east (A/B on xbox controller) to mouse1/2
-		fakedEvent = *event;
-		fakedEvent.evValue = (event->evValue == K_JOY_BTN_SOUTH) ? K_MOUSE1 : K_MOUSE2;
+
+		// some things like highlighting hovered UI elements need a mouse event,
+		// so create a fake mouse event
+		fakedEvent.evType = SE_MOUSE;
+		// the coordinates (evValue/evValue2) aren't used, but keeping them at 0
+		// (as default-initialized above) shouldn't hurt either way
 		event = &fakedEvent;
 	}
+	else if ( event->evType == SE_KEY && event->evValue >= K_FIRST_JOY && event->evValue <= K_LAST_JOY )
+	{
+		// map some gamepad buttons to SE_KEY events that the UI already knows how to use
+		int key = 0;
+		switch(event->evValue) {
+			// emulate mouse buttons
+			case K_JOY_TRIGGER2:
+				// the right trigger is often used for shooting, so for ingame UIs
+				// it'll behave like mouseclick - do the same in menus
+				// fall-through
+			case K_JOY_BTN_SOUTH: // A on xbox controller
+				key = K_MOUSE1;
+				break;
+			case K_JOY_BTN_EAST: // B on xbox controller
+				key = K_MOUSE2;
+				break;
+			// emulate cursor keys (sometimes used for scrolling or selecting in a list)
+			case K_JOY_DPAD_UP:
+				key = K_UPARROW;
+				break;
+			case K_JOY_DPAD_DOWN:
+				key = K_DOWNARROW;
+				break;
+			case K_JOY_DPAD_LEFT:
+				key = K_LEFTARROW;
+				break;
+			case K_JOY_DPAD_RIGHT:
+				key = K_RIGHTARROW;
+				break;
+			// enter is useful after selecting something with cursor keys (or dpad)
+			// in a list, like selecting a savegame - I guess left trigger is suitable for that?
+			case K_JOY_TRIGGER1:
+				key = K_ENTER;
+				break;
+		}
+		if (key != 0) {
+			fakedEvent = *event;
+			fakedEvent.evValue = key;
+			event = &fakedEvent;
+		}
+	}
 
 	if ( desktop ) {
 		return desktop->HandleEvent( event, updateVisuals );

From 03ec74fd6fdf632b8898c600dd9c227f911f7d77 Mon Sep 17 00:00:00 2001
From: Daniel Gibson <metalcaedes@gmail.com>
Date: Wed, 17 Jan 2024 06:33:06 +0100
Subject: [PATCH 06/14] Make PDA work with gamepad, incl. making Pad A emulate
 leftclick

this is a bit hacky and ugly, and doesn't work properly in multiplayer
mode yet, see FIXME in idUserInterfaceLocal::Activate()
---
 neo/framework/UsercmdGen.cpp | 34 ++++++++++++++++++++-
 neo/sys/events.cpp           | 57 +++++++++++++++++++++++++++++-------
 neo/sys/sys_public.h         |  5 ++++
 neo/ui/UserInterface.cpp     |  5 ++++
 4 files changed, 90 insertions(+), 11 deletions(-)

diff --git a/neo/framework/UsercmdGen.cpp b/neo/framework/UsercmdGen.cpp
index 8c5cc388..b212223f 100644
--- a/neo/framework/UsercmdGen.cpp
+++ b/neo/framework/UsercmdGen.cpp
@@ -353,6 +353,7 @@ private:
 	void			CircleToSquare( float & axis_x, float & axis_y ) const;
 	void			HandleJoystickAxis( int keyNum, float unclampedValue, float threshold, bool positive );
 	void			JoystickMove( void );
+	void			JoystickFakeMouse(float axis_x, float axis_y, float deadzone);
 	void			MouseMove( void );
 	void			CmdButtons( void );
 
@@ -429,7 +430,7 @@ idCVar idUsercmdGenLocal::m_strafeSmooth( "m_strafeSmooth", "4", CVAR_SYSTEM | C
 idCVar idUsercmdGenLocal::m_showMouseRate( "m_showMouseRate", "0", CVAR_SYSTEM | CVAR_BOOL, "shows mouse movement" );
 
 idCVar joy_triggerThreshold( "joy_triggerThreshold", "0.05", CVAR_FLOAT | CVAR_ARCHIVE, "how far the joystick triggers have to be pressed before they register as down" );
-idCVar joy_deadZone( "joy_deadZone", "0.4", CVAR_FLOAT | CVAR_ARCHIVE, "specifies how large the dead-zone is on the joystick" );
+idCVar joy_deadZone( "joy_deadZone", "0.25", CVAR_FLOAT | CVAR_ARCHIVE, "specifies how large the dead-zone is on the joystick" );
 idCVar joy_range( "joy_range", "1.0", CVAR_FLOAT | CVAR_ARCHIVE, "allow full range to be mapped to a smaller offset" );
 idCVar joy_gammaLook( "joy_gammaLook", "1", CVAR_INTEGER | CVAR_ARCHIVE, "use a log curve instead of a power curve for movement" );
 idCVar joy_powerScale( "joy_powerScale", "2", CVAR_FLOAT | CVAR_ARCHIVE, "Raise joystick values to this power" );
@@ -870,6 +871,33 @@ void idUsercmdGenLocal::HandleJoystickAxis( int keyNum, float unclampedValue, fl
 	}
 }
 
+static float joyAxisToMouseDelta(float axis, float deadzone)
+{
+	float ret = 0.0f;
+	float val = fabsf(axis); // calculations below require a positive value
+	if(val > deadzone) {
+		// from deadzone .. 1 to 0 .. 1-deadzone
+		val -= deadzone;
+		// and then to 0..1
+		val = val * (1.0f / (1.0f - deadzone));
+
+		// make it exponential curve - exp(val*3) should return sth between 1 and 20;
+		// then turning that into 0.5 .. 10
+		ret = expf( val * 3.0f ) * 0.5f;
+		if(axis < 0.0f) // restore sign
+			ret = -ret;
+	}
+	return ret;
+}
+
+void idUsercmdGenLocal::JoystickFakeMouse(float axis_x, float axis_y, float deadzone)
+{
+	float x = joyAxisToMouseDelta(axis_x, deadzone);
+	float y = joyAxisToMouseDelta(axis_y, deadzone);
+	continuousMouseX += x;
+	continuousMouseY += y;
+}
+
 /*
 =================
 idUsercmdGenLocal::JoystickMove
@@ -888,6 +916,8 @@ void idUsercmdGenLocal::JoystickMove() {
 	HandleJoystickAxis( K_JOY_STICK1_LEFT, axis_x, threshold, false );
 	HandleJoystickAxis( K_JOY_STICK1_RIGHT, axis_x, threshold, true );
 
+	JoystickFakeMouse( axis_x, axis_y, threshold );
+
 	axis_y = joystickAxis[ AXIS_RIGHT_Y ];
 	axis_x = joystickAxis[ AXIS_RIGHT_X ];
 	CircleToSquare( axis_x, axis_y );
@@ -897,6 +927,8 @@ void idUsercmdGenLocal::JoystickMove() {
 	HandleJoystickAxis( K_JOY_STICK2_LEFT, axis_x, threshold, false );
 	HandleJoystickAxis( K_JOY_STICK2_RIGHT, axis_x, threshold, true );
 
+	JoystickFakeMouse( axis_x, axis_y, threshold );
+
 	HandleJoystickAxis( K_JOY_TRIGGER1, joystickAxis[ AXIS_LEFT_TRIG ], triggerThreshold, true );
 	HandleJoystickAxis( K_JOY_TRIGGER2, joystickAxis[ AXIS_RIGHT_TRIG ], triggerThreshold, true );
 }
diff --git a/neo/sys/events.cpp b/neo/sys/events.cpp
index 91497e56..986e9400 100644
--- a/neo/sys/events.cpp
+++ b/neo/sys/events.cpp
@@ -61,6 +61,7 @@ If you have questions concerning this license or the applicable additional terms
 #endif
 
 extern idCVar in_useGamepad; // from UsercmdGen.cpp
+extern idCVar joy_deadZone;  // ditto
 
 // NOTE: g++-4.7 doesn't like when this is static (for idCmdSystem::ArgCompletion_String<kbdNames>)
 const char *_in_kbdNames[] = {
@@ -898,6 +899,25 @@ void Sys_GrabMouseCursor(bool grabIt) {
 	GLimp_GrabInput(flags);
 }
 
+static bool interactiveGuiActive = false;
+/*
+===============
+Sys_SetInteractiveIngameGuiActive
+Tell the input system that currently an interactive *ingame* UI has focus,
+so there is an active cursor.
+Used for an ungodly hack to make gamepad button south (A) behave like
+left mouse button in that case, so "clicking" with gamepad in the PDA
+(and ingame GUIs) works as expected.
+Not set for proper menus like main menu etc - the gamepad hacks for that
+are in idUserInterfaceLocal::HandleEvent().
+I hope this won't explode in my face :-p
+===============
+ */
+void Sys_SetInteractiveIngameGuiActive(bool active)
+{
+	interactiveGuiActive = active;
+}
+
 
 static void PushButton( int key, bool value ) {
 	// So we don't keep sending the same SE_KEY message over and over again
@@ -1218,20 +1238,27 @@ sysEvent_t Sys_GetEvent() {
 				common->Warning( "Gamepad support is disabled! Set the in_useGamepad CVar to 1 to enable it!\n" );
 				continue;
 			}
+
+			res.evType = SE_KEY;
+			res.evValue2 = ev.cbutton.state == SDL_PRESSED ? 1 : 0;
+
 			// special case: always treat the start button as escape so it opens/closes the menu
 			// (also makes that button non-bindable)
 			if ( ev.cbutton.button == SDL_CONTROLLER_BUTTON_START ) {
-				res.evType = SE_KEY;
 				res.evValue = K_ESCAPE;
-				res.evValue2 = ev.cbutton.state == SDL_PRESSED ? 1 : 0;
+				return res;
+			} else if( ev.cbutton.button == SDL_CONTROLLER_BUTTON_A && interactiveGuiActive && sessLocal.GetActiveMenu() == NULL ) {
+				// ugly hack: currently an interactive ingame GUI (with a cursor) is active/focused
+				// so pretend that the gamepads A (south) button is the left mouse button
+				// so it can be used for "clicking"..
+				mouse_polls.Append( mouse_poll_t(M_ACTION1, res.evValue2) );
+				res.evValue = K_MOUSE1;
 				return res;
 			}
 
 			sys_jEvents jEvent =  mapjoybutton( (SDL_GameControllerButton)ev.cbutton.button);
 			joystick_polls.Append(joystick_poll_t(jEvent, ev.cbutton.state == SDL_PRESSED ? 1 : 0) );
 
-			res.evType = SE_KEY;
-			res.evValue2 = ev.cbutton.state == SDL_PRESSED ? 1 : 0;
 			if ( ( jEvent >= J_ACTION_FIRST ) && ( jEvent <= J_ACTION_MAX ) ) {
 				res.evValue = K_FIRST_JOY + ( jEvent - J_ACTION_FIRST );
 				return res;
@@ -1274,18 +1301,28 @@ sysEvent_t Sys_GetEvent() {
 				// NOTE: the stuff set here is only used to move the cursor in menus
 				//       ingame movement is done via joystick_polls
 				int axis = jEvent - J_AXIS_MIN;
-				float val = ev.caxis.value * (1.25f / 32767.0f);
-				// 25% deadzone
-				if( val < 0.0f ) {
-					val = fminf(val + 0.25f, 0.0f);
+				float dz = joy_deadZone.GetFloat();
+
+				float val = fabsf(ev.caxis.value * (1.0f / 32767.0f));
+				if(val < dz) {
+					val = 0.0f;
 				} else {
-					val = fmaxf(val - 0.25f, 0.0f);
+					// from deadzone .. 1 to 0 .. 1-deadzone
+					val -= dz;
+					// and then to 0..1
+					val = val * (1.0f / (1.0f - dz));
+
+					if( ev.caxis.value < 0 ) {
+						val = -val;
+					}
 				}
 
 				joyAxis[axis] = val;
 			}
 
-			continue; // try to get a decent event.
+			// handle next event; joy axis events are generated below,
+			// when there are no further SDL events
+			continue;
 		}
 		break;
 
diff --git a/neo/sys/sys_public.h b/neo/sys/sys_public.h
index c7bd89be..e20ab923 100644
--- a/neo/sys/sys_public.h
+++ b/neo/sys/sys_public.h
@@ -248,6 +248,11 @@ void			Sys_EndJoystickInputEvents();
 // when in windowed mode
 void			Sys_GrabMouseCursor( bool grabIt );
 
+// DG: added this for an ungodly hack for gamepad support
+// active = true means "currently a GUI with a cursor is active/focused"
+// active = false means "that GUI is not active anymore"
+void			Sys_SetInteractiveIngameGuiActive(bool active);
+
 void			Sys_ShowWindow( bool show );
 bool			Sys_IsWindowVisible( void );
 void			Sys_ShowConsole( int visLevel, bool quitOnClose );
diff --git a/neo/ui/UserInterface.cpp b/neo/ui/UserInterface.cpp
index 98e3dea5..c8b3e167 100644
--- a/neo/ui/UserInterface.cpp
+++ b/neo/ui/UserInterface.cpp
@@ -582,6 +582,11 @@ const char *idUserInterfaceLocal::Activate(bool activate, int _time) {
 	time = _time;
 	active = activate;
 	if ( desktop ) {
+		// FIXME: this works ok, mostly, except in multiplayer, where this function
+		//   is called twice with activate=true, and the first time GetActiveMenu() returns NULL, the second time not
+		if(interactive && sessLocal.GetActiveMenu() == NULL) {
+			Sys_SetInteractiveIngameGuiActive(activate);
+		}
 		activateStr = "";
 		desktop->Activate( activate, activateStr );
 		return activateStr;

From cf5d10f4e6fe80f2e266a0419bb10f5f6a349d0e Mon Sep 17 00:00:00 2001
From: Daniel Gibson <metalcaedes@gmail.com>
Date: Wed, 17 Jan 2024 16:47:21 +0100
Subject: [PATCH 07/14] Fix gamepad pseudo-mouse input for UIs in multiplayer
 mode

also, only generate pseudo-mouse-move events for gamecode
(by modifying idUsercmdGenLocal::continuousMouseX/Y) when an
interactive ingame UI is active
---
 neo/framework/Session.cpp    |  3 +++
 neo/framework/UsercmdGen.cpp | 11 ++++++----
 neo/framework/UsercmdGen.h   |  4 ++--
 neo/sys/events.cpp           | 40 +++++++++++++++++++++++++++++++-----
 neo/sys/sys_public.h         |  3 ++-
 neo/ui/UserInterface.cpp     | 12 ++++++-----
 6 files changed, 56 insertions(+), 17 deletions(-)

diff --git a/neo/framework/Session.cpp b/neo/framework/Session.cpp
index 609c178e..837f7f52 100644
--- a/neo/framework/Session.cpp
+++ b/neo/framework/Session.cpp
@@ -1447,6 +1447,9 @@ void idSessionLocal::UnloadMap() {
 	}
 
 	mapSpawned = false;
+
+	// DG: that state needs to be reset now
+	Sys_SetInteractiveIngameGuiActive( false, NULL );
 }
 
 /*
diff --git a/neo/framework/UsercmdGen.cpp b/neo/framework/UsercmdGen.cpp
index b212223f..a64a4984 100644
--- a/neo/framework/UsercmdGen.cpp
+++ b/neo/framework/UsercmdGen.cpp
@@ -890,12 +890,15 @@ static float joyAxisToMouseDelta(float axis, float deadzone)
 	return ret;
 }
 
+extern bool D3_IN_interactiveIngameGuiActive; // from sys/events.cpp
 void idUsercmdGenLocal::JoystickFakeMouse(float axis_x, float axis_y, float deadzone)
 {
-	float x = joyAxisToMouseDelta(axis_x, deadzone);
-	float y = joyAxisToMouseDelta(axis_y, deadzone);
-	continuousMouseX += x;
-	continuousMouseY += y;
+	if ( D3_IN_interactiveIngameGuiActive ) {
+		float x = joyAxisToMouseDelta(axis_x, deadzone);
+		float y = joyAxisToMouseDelta(axis_y, deadzone);
+		continuousMouseX += x;
+		continuousMouseY += y;
+	}
 }
 
 /*
diff --git a/neo/framework/UsercmdGen.h b/neo/framework/UsercmdGen.h
index 17e88fe8..c9e9ecae 100644
--- a/neo/framework/UsercmdGen.h
+++ b/neo/framework/UsercmdGen.h
@@ -96,8 +96,8 @@ public:
 	signed char	rightmove;						// left/right movement
 	signed char	upmove;							// up/down movement
 	short		angles[3];						// view angles
-	short		mx;								// mouse delta x
-	short		my;								// mouse delta y
+	short		mx;								// mouse delta x - DG: not really delta, but from continuousMouseX which accumulates
+	short		my;								// mouse delta y - DG: same but from continuousMouseY
 	signed char impulse;						// impulse command
 	byte		flags;							// additional flags
 	int			sequence;						// just for debugging
diff --git a/neo/sys/events.cpp b/neo/sys/events.cpp
index 986e9400..8d7fffca 100644
--- a/neo/sys/events.cpp
+++ b/neo/sys/events.cpp
@@ -899,7 +899,7 @@ void Sys_GrabMouseCursor(bool grabIt) {
 	GLimp_GrabInput(flags);
 }
 
-static bool interactiveGuiActive = false;
+
 /*
 ===============
 Sys_SetInteractiveIngameGuiActive
@@ -910,12 +910,42 @@ left mouse button in that case, so "clicking" with gamepad in the PDA
 (and ingame GUIs) works as expected.
 Not set for proper menus like main menu etc - the gamepad hacks for that
 are in idUserInterfaceLocal::HandleEvent().
+Call with ui = NULL to clear the state.
 I hope this won't explode in my face :-p
 ===============
- */
-void Sys_SetInteractiveIngameGuiActive(bool active)
+*/
+bool D3_IN_interactiveIngameGuiActive = false;
+void Sys_SetInteractiveIngameGuiActive( bool active, idUserInterface* ui )
 {
-	interactiveGuiActive = active;
+	static idList<idUserInterface*> lastuis;
+	if ( ui == NULL ) {
+		// special case for clearing
+		D3_IN_interactiveIngameGuiActive = false;
+		lastuis.Clear();
+		return;
+	}
+	int idx = lastuis.FindIndex( ui );
+
+	if ( sessLocal.GetActiveMenu() == NULL && active ) {
+		// add ui to lastuis, if it has been activated and no proper menu
+		// (like main menu) is currently open
+		lastuis.Append( ui );
+	} else if ( idx != -1 ) {
+		// if the UI is in lastuis and has been deactivated, or there
+		// is a proper menu opened, remove it from the list.
+		// this both handles the regular deactivate case and also works around
+		// main-menu-in-multiplayer weirdness: that menu calls idUserInterface::Activate()
+		// with activate = true twice, but on first call sessLocal.GetActiveMenu() is NULL
+		// so we want to remove it once we realize that it really is a "proper" menu after all.
+		// And because it's possible that we have an ingame UI focussed while opening
+		// the multiplayer-main-menu, we keep a list of lastuis, instead of just one,
+		// so D3_IN_interactiveIngameGuiActive remains true in that case
+		// (the ingame UI is still in the list)
+
+		lastuis.RemoveIndex( idx );
+	}
+
+	D3_IN_interactiveIngameGuiActive = lastuis.Num() != 0;
 }
 
 
@@ -1247,7 +1277,7 @@ sysEvent_t Sys_GetEvent() {
 			if ( ev.cbutton.button == SDL_CONTROLLER_BUTTON_START ) {
 				res.evValue = K_ESCAPE;
 				return res;
-			} else if( ev.cbutton.button == SDL_CONTROLLER_BUTTON_A && interactiveGuiActive && sessLocal.GetActiveMenu() == NULL ) {
+			} else if( ev.cbutton.button == SDL_CONTROLLER_BUTTON_A && D3_IN_interactiveIngameGuiActive && sessLocal.GetActiveMenu() == NULL ) {
 				// ugly hack: currently an interactive ingame GUI (with a cursor) is active/focused
 				// so pretend that the gamepads A (south) button is the left mouse button
 				// so it can be used for "clicking"..
diff --git a/neo/sys/sys_public.h b/neo/sys/sys_public.h
index e20ab923..9c32f9f9 100644
--- a/neo/sys/sys_public.h
+++ b/neo/sys/sys_public.h
@@ -251,7 +251,8 @@ void			Sys_GrabMouseCursor( bool grabIt );
 // DG: added this for an ungodly hack for gamepad support
 // active = true means "currently a GUI with a cursor is active/focused"
 // active = false means "that GUI is not active anymore"
-void			Sys_SetInteractiveIngameGuiActive(bool active);
+class idUserInterface;
+void			Sys_SetInteractiveIngameGuiActive( bool active, idUserInterface* ui );
 
 void			Sys_ShowWindow( bool show );
 bool			Sys_IsWindowVisible( void );
diff --git a/neo/ui/UserInterface.cpp b/neo/ui/UserInterface.cpp
index c8b3e167..58379b87 100644
--- a/neo/ui/UserInterface.cpp
+++ b/neo/ui/UserInterface.cpp
@@ -118,6 +118,9 @@ void idUserInterfaceManagerLocal::EndLevelLoad() {
 			}
 		}
 	}
+
+	// DG: this should probably be reset at this point
+	Sys_SetInteractiveIngameGuiActive( false, NULL );
 }
 
 void idUserInterfaceManagerLocal::Reload( bool all ) {
@@ -582,11 +585,10 @@ const char *idUserInterfaceLocal::Activate(bool activate, int _time) {
 	time = _time;
 	active = activate;
 	if ( desktop ) {
-		// FIXME: this works ok, mostly, except in multiplayer, where this function
-		//   is called twice with activate=true, and the first time GetActiveMenu() returns NULL, the second time not
-		if(interactive && sessLocal.GetActiveMenu() == NULL) {
-			Sys_SetInteractiveIngameGuiActive(activate);
-		}
+		// DG: added this hack for gamepad input
+		if ( interactive ) {
+			Sys_SetInteractiveIngameGuiActive( activate, this );
+		} // DG end
 		activateStr = "";
 		desktop->Activate( activate, activateStr );
 		return activateStr;

From 86690df24e822187bf3a9b461775757524c0290e Mon Sep 17 00:00:00 2001
From: Daniel Gibson <metalcaedes@gmail.com>
Date: Wed, 17 Jan 2024 18:54:09 +0100
Subject: [PATCH 08/14] Add joy_gamepadLayout CVar to better support
 nintendo/PS-style gamepads

The button names shown in the controls menu now depend on this CVar.
So if you set it to 1 (Nintendo), the "A" button (which, based on its
position, would be "B" on XBox/XInput gamepads) is actually shown as
"Pad A", and if it's set to 2 (Playstation), it's shown as "Pad Cross".

The "real" names, used in the config, remain the same and are based on
position: JOY_BTN_SOUTH, JOY_BTN_EAST, JOY_BTN_WEST, JOY_BTN_NORTH
---
 neo/framework/Session_menu.cpp | 10 ++++++++++
 neo/sys/events.cpp             | 33 +++++++++++++++++++++------------
 2 files changed, 31 insertions(+), 12 deletions(-)

diff --git a/neo/framework/Session_menu.cpp b/neo/framework/Session_menu.cpp
index b979b825..c69859a7 100644
--- a/neo/framework/Session_menu.cpp
+++ b/neo/framework/Session_menu.cpp
@@ -39,6 +39,8 @@ If you have questions concerning this license or the applicable additional terms
 
 idCVar	idSessionLocal::gui_configServerRate( "gui_configServerRate", "0", CVAR_GUI | CVAR_ARCHIVE | CVAR_ROM | CVAR_INTEGER, "" );
 
+extern idCVar joy_gamepadLayout; // DG: used here to update bindings window when cvar is changed
+
 // implements the setup for, and commands from, the main menu
 
 /*
@@ -1209,6 +1211,14 @@ void idSessionLocal::GuiFrameEvents() {
 	sysEvent_t  ev;
 	idUserInterface	*gui;
 
+	// DG: if joy_gamepadLayout changes, the binding names in the main/controls menu must be updated
+	if ( joy_gamepadLayout.IsModified() ) {
+		if ( guiMainMenu != NULL ) {
+			guiMainMenu->SetKeyBindingNames();
+		}
+		joy_gamepadLayout.ClearModified();
+	}
+
 	// stop generating move and button commands when a local console or menu is active
 	// running here so SP, async networking and no game all go through it
 	if ( console->Active() || guiActive ) {
diff --git a/neo/sys/events.cpp b/neo/sys/events.cpp
index 8d7fffca..a7ccac4c 100644
--- a/neo/sys/events.cpp
+++ b/neo/sys/events.cpp
@@ -80,6 +80,9 @@ static idCVar in_nograb("in_nograb", "0", CVAR_SYSTEM | CVAR_NOCHEAT, "prevents
 static idCVar in_grabKeyboard("in_grabKeyboard", "0", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_NOCHEAT | CVAR_BOOL,
 		"if enabled, grabs all keyboard input if mouse is grabbed (so keyboard shortcuts from the OS like Alt-Tab or Windows Key won't work)");
 
+idCVar joy_gamepadLayout("joy_gamepadLayout", "0", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_NOCHEAT | CVAR_INTEGER,
+		"Button layout of gamepad - 0: XBox-style, 1: Nintendo-style, 2: Playstation-style", idCmdSystem::ArgCompletion_Integer<0, 2> );
+
 // set in handleMouseGrab(), used in Sys_GetEvent() to decide what kind of internal mouse event to generate
 static bool in_relativeMouseMode = true;
 // set in Sys_GetEvent() on window focus gained/lost events
@@ -275,9 +278,9 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 
 		if (key <= K_JOY_BTN_NORTH) {
 #if SDL_VERSION_ATLEAST(3, 0, 0)
-
+			// TODO: or use the SDL2 code and just set joy_gamepadLayout automatically based on SDL_GetGamepadType() ?
 			SDL_GamepadButton gpbtn = SDL_GAMEPAD_BUTTON_SOUTH + (key - K_JOY_BTN_SOUTH);
-			SDL_GamepadButtonLabel label = SDL_GetGamepadButtonLabeForTypel(TODO, gpbtn);
+			SDL_GamepadButtonLabel label = SDL_GetGamepadButtonLabelForType(TODO, gpbtn);
 			switch(label) {
 				case SDL_GAMEPAD_BUTTON_LABEL_A:
 					return "Pad A";
@@ -298,16 +301,22 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 			}
 
 #else // SDL2
-			// using xbox-style names, like SDL2 does (SDL can't tell us if this is a xbox or PS or nintendo or whatever-style gamepad)
-			switch(key) {
-				case K_JOY_BTN_SOUTH:
-					return "Pad A";
-				case K_JOY_BTN_EAST:
-					return "Pad B";
-				case K_JOY_BTN_WEST:
-					return "Pad X";
-				case K_JOY_BTN_NORTH:
-					return "Pad Y";
+			//                                          South,   East,       West,         North
+			static const char* xboxBtnNames[4]     = { "Pad A", "Pad B",    "Pad X",      "Pad Y" };
+			static const char* nintendoBtnNames[4] = { "Pad B", "Pad A",    "Pad X",      "Pad Y" };
+			static const char* psBtnNames[4] = { "Pad Cross", "Pad Circle", "Pad Square", "Pad Triangle" };
+
+			unsigned btnIdx = key - K_JOY_BTN_SOUTH;
+			assert(btnIdx < 4);
+			switch( joy_gamepadLayout.GetInteger() ) {
+				default:
+					common->Warning( "joy_gamepadLayout has invalid value %d !\n", joy_gamepadLayout.GetInteger() );
+				case 0:
+					return xboxBtnNames[btnIdx];
+				case 1:
+					return nintendoBtnNames[btnIdx];
+				case 2:
+					return psBtnNames[btnIdx];
 			}
 #endif // face button names for SDL2
 		}

From bb568bc3dac4286f0b894b581094ac7a79dfce71 Mon Sep 17 00:00:00 2001
From: Daniel Gibson <metalcaedes@gmail.com>
Date: Wed, 17 Jan 2024 21:25:33 +0100
Subject: [PATCH 09/14] Gamepad layout autodetection (for SDL 2.0.12 and newer)

turns out SDL 2.0.12 added SDL_GameControllerGetType() which tells you
what kind of controller it is (xbox, playstation, nintendo, ..).

Using this to implement an auto-mode for joy_gamepadLayout, when it's
set to -1 (the new default).

This should still build with older versions of SDL2 (but won't have
that autodetection then).
---
 neo/sys/events.cpp | 102 ++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 91 insertions(+), 11 deletions(-)

diff --git a/neo/sys/events.cpp b/neo/sys/events.cpp
index a7ccac4c..e37663dc 100644
--- a/neo/sys/events.cpp
+++ b/neo/sys/events.cpp
@@ -80,14 +80,20 @@ static idCVar in_nograb("in_nograb", "0", CVAR_SYSTEM | CVAR_NOCHEAT, "prevents
 static idCVar in_grabKeyboard("in_grabKeyboard", "0", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_NOCHEAT | CVAR_BOOL,
 		"if enabled, grabs all keyboard input if mouse is grabbed (so keyboard shortcuts from the OS like Alt-Tab or Windows Key won't work)");
 
-idCVar joy_gamepadLayout("joy_gamepadLayout", "0", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_NOCHEAT | CVAR_INTEGER,
-		"Button layout of gamepad - 0: XBox-style, 1: Nintendo-style, 2: Playstation-style", idCmdSystem::ArgCompletion_Integer<0, 2> );
+idCVar joy_gamepadLayout("joy_gamepadLayout", "-1", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_NOCHEAT | CVAR_INTEGER,
+		"Button layout of gamepad - -1: auto (needs SDL 2.0.12 or newer), 0: XBox-style, 1: Nintendo-style, 2: Playstation-style", idCmdSystem::ArgCompletion_Integer<-1, 2> );
 
 // set in handleMouseGrab(), used in Sys_GetEvent() to decide what kind of internal mouse event to generate
 static bool in_relativeMouseMode = true;
 // set in Sys_GetEvent() on window focus gained/lost events
 static bool in_hasFocus = true;
 
+static enum D3_Gamepad_Type {
+	D3_GAMEPAD_XINPUT,     // XBox/XInput standard, the default
+	D3_GAMEPAD_NINTENDO,   // nintendo-like (A/B and X/Y are switched)
+	D3_GAMEPAD_PLAYSTATION // PS-like (geometric symbols instead of A/B/X/Y)
+} gamepadType = D3_GAMEPAD_XINPUT;
+
 struct kbd_poll_t {
 	int key;
 	bool state;
@@ -306,16 +312,23 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 			static const char* nintendoBtnNames[4] = { "Pad B", "Pad A",    "Pad X",      "Pad Y" };
 			static const char* psBtnNames[4] = { "Pad Cross", "Pad Circle", "Pad Square", "Pad Triangle" };
 
+			int layout = joy_gamepadLayout.GetInteger();
+			if ( layout == -1 ) {
+				layout = gamepadType;
+			}
+
 			unsigned btnIdx = key - K_JOY_BTN_SOUTH;
 			assert(btnIdx < 4);
-			switch( joy_gamepadLayout.GetInteger() ) {
+
+			switch( layout ) {
 				default:
 					common->Warning( "joy_gamepadLayout has invalid value %d !\n", joy_gamepadLayout.GetInteger() );
-				case 0:
+					// fall-through
+				case D3_GAMEPAD_XINPUT:
 					return xboxBtnNames[btnIdx];
-				case 1:
+				case D3_GAMEPAD_NINTENDO:
 					return nintendoBtnNames[btnIdx];
-				case 2:
+				case D3_GAMEPAD_PLAYSTATION:
 					return psBtnNames[btnIdx];
 			}
 #endif // face button names for SDL2
@@ -626,6 +639,19 @@ static byte mapkey(SDL_Keycode key) {
 
 #if SDL_VERSION_ATLEAST(2, 0, 0)
 
+#if ! SDL_VERSION_ATLEAST(2, 0, 14)
+// Hack: to support newer SDL2 runtime versions than the one built against,
+//       define these controller buttons if needed
+enum {
+	SDL_CONTROLLER_BUTTON_MISC1 = 15, /* Xbox Series X share button, PS5 microphone button, Nintendo Switch Pro capture button, Amazon Luna microphone button */
+	SDL_CONTROLLER_BUTTON_PADDLE1,  /* Xbox Elite paddle P1 */
+	SDL_CONTROLLER_BUTTON_PADDLE2,  /* Xbox Elite paddle P3 */
+	SDL_CONTROLLER_BUTTON_PADDLE3,  /* Xbox Elite paddle P2 */
+	SDL_CONTROLLER_BUTTON_PADDLE4,  /* Xbox Elite paddle P4 */
+	SDL_CONTROLLER_BUTTON_TOUCHPAD, /* PS4/PS5 touchpad button */
+};
+#endif // ! SDL_VERSION_ATLEAST(2, 0, 14)
+
 static sys_jEvents mapjoybutton(SDL_GameControllerButton button) {
 	switch (button)
 	{
@@ -661,7 +687,7 @@ static sys_jEvents mapjoybutton(SDL_GameControllerButton button) {
 		return J_DPAD_LEFT;
 	case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
 		return J_DPAD_RIGHT;
-	// TODO: have the following always been supported in SDL2?
+
 	case SDL_CONTROLLER_BUTTON_MISC1:
 		return J_BTN_MISC1;
 	case SDL_CONTROLLER_BUTTON_PADDLE1:
@@ -672,7 +698,6 @@ static sys_jEvents mapjoybutton(SDL_GameControllerButton button) {
 		return J_BTN_LPADDLE1;
 	case SDL_CONTROLLER_BUTTON_PADDLE4:
 		return J_BTN_LPADDLE2;
-
 	default:
 		common->Warning("unknown game controller button %u", button);
 		break;
@@ -701,6 +726,52 @@ static sys_jEvents mapjoyaxis(SDL_GameControllerAxis axis) {
 	}
 	return J_AXIS_MAX;
 }
+
+#if ! SDL_VERSION_ATLEAST(2, 24, 0)
+// Hack: to support newer SDL2 runtime versions than the one compiled against,
+// define some controller types that were added after 2.0.12
+enum {
+#if ! SDL_VERSION_ATLEAST(2, 0, 14)
+	SDL_CONTROLLER_TYPE_PS5 = 7,
+#endif
+
+	// leaving out luna and stadia (from 2.0.16)
+	// and nvidia shield (from 2.24), they're similar enough to XBox/XInput
+
+	// the following were added in 2.24
+	SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT = 11,
+	SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT,
+	SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR
+};
+#endif // ! SDL_VERSION_ATLEAST(2, 24, 0)
+
+static void setGamepadType( SDL_GameController* gc )
+{
+#if SDL_VERSION_ATLEAST(2, 0, 12)
+	switch( SDL_GameControllerGetType( gc ) ) {
+		default: // the other controller like luna, stadia, whatever, have a very similar layout
+		case SDL_CONTROLLER_TYPE_UNKNOWN:
+		case SDL_CONTROLLER_TYPE_XBOX360:
+		case SDL_CONTROLLER_TYPE_XBOXONE:
+			gamepadType = D3_GAMEPAD_XINPUT;
+			break;
+
+		case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO:
+		case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT:
+		case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT:
+		case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR:
+			gamepadType = D3_GAMEPAD_NINTENDO;
+			break;
+
+		case SDL_CONTROLLER_TYPE_PS3:
+		case SDL_CONTROLLER_TYPE_PS4:
+		case SDL_CONTROLLER_TYPE_PS5:
+			gamepadType = D3_GAMEPAD_PLAYSTATION;
+			break;
+	}
+#endif // SDL_VERSION_ATLEAST(2, 0, 12)
+}
+
 #endif // SDL2+ gamecontroller code
 
 static void PushConsoleEvent(const char *s) {
@@ -769,6 +840,9 @@ void Sys_InitInput() {
 	for( int i = 0; i < NumJoysticks; ++i )
 	{
 		SDL_GameController* gc = SDL_GameControllerOpen( i );
+		if ( gc != NULL ) {
+			setGamepadType( gc );
+		}
 	}
 #endif
 }
@@ -1286,7 +1360,9 @@ sysEvent_t Sys_GetEvent() {
 			if ( ev.cbutton.button == SDL_CONTROLLER_BUTTON_START ) {
 				res.evValue = K_ESCAPE;
 				return res;
-			} else if( ev.cbutton.button == SDL_CONTROLLER_BUTTON_A && D3_IN_interactiveIngameGuiActive && sessLocal.GetActiveMenu() == NULL ) {
+			} else if( ev.cbutton.button == SDL_CONTROLLER_BUTTON_A
+					   && D3_IN_interactiveIngameGuiActive && sessLocal.GetActiveMenu() == NULL )
+			{
 				// ugly hack: currently an interactive ingame GUI (with a cursor) is active/focused
 				// so pretend that the gamepads A (south) button is the left mouse button
 				// so it can be used for "clicking"..
@@ -1366,11 +1442,15 @@ sysEvent_t Sys_GetEvent() {
 		break;
 
 		case SDL_JOYDEVICEADDED:
-			SDL_GameControllerOpen( ev.jdevice.which );
+		{
+			SDL_GameController* gc = SDL_GameControllerOpen( ev.jdevice.which );
+			if ( gc != NULL ) {
+				setGamepadType( gc );
+			}
 			// TODO: hot swapping maybe.
 			//lbOnControllerPlugIn(event.jdevice.which);
 			break;
-
+		}
 		case SDL_JOYDEVICEREMOVED:
 			// TODO: hot swapping maybe.
 			//lbOnControllerUnPlug(event.jdevice.which);

From 09c89206a462409983026dbed1d138200f4b6a47 Mon Sep 17 00:00:00 2001
From: Daniel Gibson <metalcaedes@gmail.com>
Date: Thu, 18 Jan 2024 03:11:23 +0100
Subject: [PATCH 10/14] Fix gamepad button names for nintendo gamepads

oops, forgot to switch X and Y
---
 neo/sys/events.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/neo/sys/events.cpp b/neo/sys/events.cpp
index e37663dc..7a0eafde 100644
--- a/neo/sys/events.cpp
+++ b/neo/sys/events.cpp
@@ -309,7 +309,7 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 #else // SDL2
 			//                                          South,   East,       West,         North
 			static const char* xboxBtnNames[4]     = { "Pad A", "Pad B",    "Pad X",      "Pad Y" };
-			static const char* nintendoBtnNames[4] = { "Pad B", "Pad A",    "Pad X",      "Pad Y" };
+			static const char* nintendoBtnNames[4] = { "Pad B", "Pad A",    "Pad Y",      "Pad X" };
 			static const char* psBtnNames[4] = { "Pad Cross", "Pad Circle", "Pad Square", "Pad Triangle" };
 
 			int layout = joy_gamepadLayout.GetInteger();

From e7eb7d17fed697d963cb5d8d78b37e2bd44bc714 Mon Sep 17 00:00:00 2001
From: Daniel Gibson <metalcaedes@gmail.com>
Date: Thu, 18 Jan 2024 06:09:33 +0100
Subject: [PATCH 11/14] Improve Nintendo Pro Controller support

and some general gamepad support improvements, like logging the
detected gamepad name and type
---
 neo/framework/KeyInput.h |  2 +-
 neo/sys/events.cpp       | 42 +++++++++++++++++++++++++---------------
 neo/sys/sys_public.h     |  2 +-
 3 files changed, 28 insertions(+), 18 deletions(-)

diff --git a/neo/framework/KeyInput.h b/neo/framework/KeyInput.h
index acf7527d..402649bd 100644
--- a/neo/framework/KeyInput.h
+++ b/neo/framework/KeyInput.h
@@ -150,7 +150,7 @@ typedef enum {
 
 	K_JOY_BTN_BACK,
 	K_JOY_BTN_GUIDE, // Note: this one should probably not be used?
-	K_JOY_BTN_START,
+	K_JOY_BTN_START, // hardcoded to generate Esc to open/close menu
 	K_JOY_BTN_LSTICK, // press left stick
 	K_JOY_BTN_RSTICK, // press right stick
 	K_JOY_BTN_LSHOULDER,
diff --git a/neo/sys/events.cpp b/neo/sys/events.cpp
index 7a0eafde..d1143a14 100644
--- a/neo/sys/events.cpp
+++ b/neo/sys/events.cpp
@@ -282,7 +282,7 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 #if SDL_VERSION_ATLEAST(2, 0, 0) // gamecontroller/gamepad not supported in SDL1
 	if (key >= K_FIRST_JOY && key <= K_LAST_JOY) {
 
-		if (key <= K_JOY_BTN_NORTH) {
+		if (key <= K_JOY_BTN_BACK) {
 #if SDL_VERSION_ATLEAST(3, 0, 0)
 			// TODO: or use the SDL2 code and just set joy_gamepadLayout automatically based on SDL_GetGamepadType() ?
 			SDL_GamepadButton gpbtn = SDL_GAMEPAD_BUTTON_SOUTH + (key - K_JOY_BTN_SOUTH);
@@ -307,10 +307,11 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 			}
 
 #else // SDL2
-			//                                          South,   East,       West,         North
-			static const char* xboxBtnNames[4]     = { "Pad A", "Pad B",    "Pad X",      "Pad Y" };
-			static const char* nintendoBtnNames[4] = { "Pad B", "Pad A",    "Pad Y",      "Pad X" };
-			static const char* psBtnNames[4] = { "Pad Cross", "Pad Circle", "Pad Square", "Pad Triangle" };
+			//                                          South,   East,       West,         North        Back
+			static const char* xboxBtnNames[5]     = { "Pad A", "Pad B",    "Pad X",      "Pad Y",   "Pad Back" };
+			static const char* nintendoBtnNames[5] = { "Pad B", "Pad A",    "Pad Y",      "Pad X",   "Pad -" };
+			// TODO: on PS3 and older, back is "Select"; on PS4+ back it might be "share"?
+			static const char* psBtnNames[5] = { "Pad Cross", "Pad Circle", "Pad Square", "Pad Triangle", "Pad Select" };
 
 			int layout = joy_gamepadLayout.GetInteger();
 			if ( layout == -1 ) {
@@ -318,7 +319,7 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 			}
 
 			unsigned btnIdx = key - K_JOY_BTN_SOUTH;
-			assert(btnIdx < 4);
+			assert(btnIdx < 5);
 
 			switch( layout ) {
 				default:
@@ -336,14 +337,10 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 
 		// the labels for the remaining keys are the same for SDL2 and SDL3 (and all controllers)
 		switch(key) {
-			case K_JOY_BTN_BACK:
-				return "Pad Back";
+			case K_JOY_BTN_GUIDE: // can't be used in dhewm3, because it opens steam on some systems
+			case K_JOY_BTN_START: // can't be used for bindings, because it's hardcoded to generate Esc
+				return NULL;
 
-			case K_JOY_BTN_GUIDE:
-				return NULL; // ???
-
-			case K_JOY_BTN_START:
-				return "Pad Start";
 			case K_JOY_BTN_LSTICK:
 				return "Pad LStick";
 			case K_JOY_BTN_RSTICK:
@@ -748,12 +745,14 @@ enum {
 static void setGamepadType( SDL_GameController* gc )
 {
 #if SDL_VERSION_ATLEAST(2, 0, 12)
+	const char* typestr = NULL;
 	switch( SDL_GameControllerGetType( gc ) ) {
 		default: // the other controller like luna, stadia, whatever, have a very similar layout
 		case SDL_CONTROLLER_TYPE_UNKNOWN:
 		case SDL_CONTROLLER_TYPE_XBOX360:
 		case SDL_CONTROLLER_TYPE_XBOXONE:
 			gamepadType = D3_GAMEPAD_XINPUT;
+			typestr = "XBox-like";
 			break;
 
 		case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO:
@@ -761,14 +760,19 @@ static void setGamepadType( SDL_GameController* gc )
 		case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT:
 		case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR:
 			gamepadType = D3_GAMEPAD_NINTENDO;
+			typestr = "Nintendo-like";
 			break;
 
 		case SDL_CONTROLLER_TYPE_PS3:
 		case SDL_CONTROLLER_TYPE_PS4:
 		case SDL_CONTROLLER_TYPE_PS5:
 			gamepadType = D3_GAMEPAD_PLAYSTATION;
+			typestr = "Playstation-like";
 			break;
 	}
+
+	common->Printf( "Detected Gamepad %s as type %s\n", SDL_GameControllerName( gc ), typestr );
+
 #endif // SDL_VERSION_ATLEAST(2, 0, 12)
 }
 
@@ -836,6 +840,11 @@ void Sys_InitInput() {
 	memset( joyAxis, 0, sizeof( joyAxis ) );
 
 #if SDL_VERSION_ATLEAST(2, 0, 0) // gamecontroller/gamepad not supported in SDL1
+	// use button positions instead of button labels,
+	// Sys_GetLocalizedJoyKeyName() will do the translation
+	// (I think this also was the default before 2.0.12?)
+	SDL_SetHint("SDL_GAMECONTROLLER_USE_BUTTON_LABELS", "0");
+
 	const int NumJoysticks = SDL_NumJoysticks();
 	for( int i = 0; i < NumJoysticks; ++i )
 	{
@@ -1371,8 +1380,8 @@ sysEvent_t Sys_GetEvent() {
 				return res;
 			}
 
-			sys_jEvents jEvent =  mapjoybutton( (SDL_GameControllerButton)ev.cbutton.button);
-			joystick_polls.Append(joystick_poll_t(jEvent, ev.cbutton.state == SDL_PRESSED ? 1 : 0) );
+			sys_jEvents jEvent =  mapjoybutton( (SDL_GameControllerButton)ev.cbutton.button );
+			joystick_polls.Append( joystick_poll_t(jEvent, ev.cbutton.state == SDL_PRESSED ? 1 : 0) );
 
 			if ( ( jEvent >= J_ACTION_FIRST ) && ( jEvent <= J_ACTION_MAX ) ) {
 				res.evValue = K_FIRST_JOY + ( jEvent - J_ACTION_FIRST );
@@ -1479,7 +1488,8 @@ sysEvent_t Sys_GetEvent() {
 		}
 	}
 
-	// first return joyaxis events, if gamepad is enabled and, and 16ms are over
+	// before returning res_none for "these were all events for now",
+	// first return joyaxis events, if gamepad is enabled and 16ms are over
 	// (or we haven't returned the values for all axis yet)
 	if ( in_useGamepad.GetBool() ) {
 		static unsigned int lastMS = 0;
diff --git a/neo/sys/sys_public.h b/neo/sys/sys_public.h
index 9c32f9f9..b006a394 100644
--- a/neo/sys/sys_public.h
+++ b/neo/sys/sys_public.h
@@ -104,7 +104,7 @@ typedef enum {
 	J_BTN_LPADDLE2, //  Lower or secondary paddle, under your left hand (e.g. Xbox Elite paddle P4)
 
 	J_ACTION_MAX = J_BTN_LPADDLE2,
-	// leaving some space here for about 16 additional J_ACTIONs, if needed
+	// leaving some space here for about 12 additional J_ACTIONs, if needed
 
 	J_AXIS_MIN = 32,
 	J_AXIS_LEFT_X = J_AXIS_MIN + AXIS_LEFT_X,

From 9e8d399257fe20c6b71fd4bc7000b724a75686b6 Mon Sep 17 00:00:00 2001
From: Daniel Gibson <metalcaedes@gmail.com>
Date: Thu, 18 Jan 2024 19:53:45 +0100
Subject: [PATCH 12/14] Further gamepad improvements

- the gamepad button (or trigger) bound to attack (fire) now always
  acts like the left mouse button in menus
- Display correct button name for "Back" button on Playstation-like
  gamepads, even depending on whether it's PS3-like ("Select") or
  PS4/5-like ("Share")
- Log some more information about detected gamepads
---
 neo/framework/UsercmdGen.cpp |  2 +-
 neo/sys/events.cpp           | 25 ++++++++++++---
 neo/ui/UserInterface.cpp     | 62 +++++++++++++++++++-----------------
 3 files changed, 54 insertions(+), 35 deletions(-)

diff --git a/neo/framework/UsercmdGen.cpp b/neo/framework/UsercmdGen.cpp
index a64a4984..2d33722e 100644
--- a/neo/framework/UsercmdGen.cpp
+++ b/neo/framework/UsercmdGen.cpp
@@ -93,7 +93,7 @@ typedef enum {
 	UB_BUTTON6,
 	UB_BUTTON7,
 
-	UB_ATTACK,
+	UB_ATTACK, // NOTE: this value (20) is hardcoded in idUserInterfaceLocal::HandleEvent() !
 	UB_SPEED,
 	UB_ZOOM,
 	UB_SHOWSCORES,
diff --git a/neo/sys/events.cpp b/neo/sys/events.cpp
index d1143a14..1d0b741b 100644
--- a/neo/sys/events.cpp
+++ b/neo/sys/events.cpp
@@ -81,7 +81,7 @@ static idCVar in_grabKeyboard("in_grabKeyboard", "0", CVAR_SYSTEM | CVAR_ARCHIVE
 		"if enabled, grabs all keyboard input if mouse is grabbed (so keyboard shortcuts from the OS like Alt-Tab or Windows Key won't work)");
 
 idCVar joy_gamepadLayout("joy_gamepadLayout", "-1", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_NOCHEAT | CVAR_INTEGER,
-		"Button layout of gamepad - -1: auto (needs SDL 2.0.12 or newer), 0: XBox-style, 1: Nintendo-style, 2: Playstation-style", idCmdSystem::ArgCompletion_Integer<-1, 2> );
+		"Button layout of gamepad. -1: auto (needs SDL 2.0.12 or newer), 0: XBox-style, 1: Nintendo-style, 2: PS4/5-style, 3: PS2/3-style", idCmdSystem::ArgCompletion_Integer<-1, 3> );
 
 // set in handleMouseGrab(), used in Sys_GetEvent() to decide what kind of internal mouse event to generate
 static bool in_relativeMouseMode = true;
@@ -91,7 +91,8 @@ static bool in_hasFocus = true;
 static enum D3_Gamepad_Type {
 	D3_GAMEPAD_XINPUT,     // XBox/XInput standard, the default
 	D3_GAMEPAD_NINTENDO,   // nintendo-like (A/B and X/Y are switched)
-	D3_GAMEPAD_PLAYSTATION // PS-like (geometric symbols instead of A/B/X/Y)
+	D3_GAMEPAD_PLAYSTATION, // PS-like (geometric symbols instead of A/B/X/Y)
+	D3_GAMEPAD_PLAYSTATION_OLD // PS2/PS3-like: the back button is called "select" instead of "share"
 } gamepadType = D3_GAMEPAD_XINPUT;
 
 struct kbd_poll_t {
@@ -310,8 +311,7 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 			//                                          South,   East,       West,         North        Back
 			static const char* xboxBtnNames[5]     = { "Pad A", "Pad B",    "Pad X",      "Pad Y",   "Pad Back" };
 			static const char* nintendoBtnNames[5] = { "Pad B", "Pad A",    "Pad Y",      "Pad X",   "Pad -" };
-			// TODO: on PS3 and older, back is "Select"; on PS4+ back it might be "share"?
-			static const char* psBtnNames[5] = { "Pad Cross", "Pad Circle", "Pad Square", "Pad Triangle", "Pad Select" };
+			static const char* psBtnNames[5] = { "Pad Cross", "Pad Circle", "Pad Square", "Pad Triangle", "Pad Share" };
 
 			int layout = joy_gamepadLayout.GetInteger();
 			if ( layout == -1 ) {
@@ -329,6 +329,11 @@ const char* Sys_GetLocalizedJoyKeyName( int key ) {
 					return xboxBtnNames[btnIdx];
 				case D3_GAMEPAD_NINTENDO:
 					return nintendoBtnNames[btnIdx];
+				case D3_GAMEPAD_PLAYSTATION_OLD:
+					if ( key == K_JOY_BTN_BACK )
+						return "Pad Select";
+					// the other button names are identical for PS2/3 and PS4/5
+					// fall-through
 				case D3_GAMEPAD_PLAYSTATION:
 					return psBtnNames[btnIdx];
 			}
@@ -764,6 +769,9 @@ static void setGamepadType( SDL_GameController* gc )
 			break;
 
 		case SDL_CONTROLLER_TYPE_PS3:
+			gamepadType = D3_GAMEPAD_PLAYSTATION_OLD;
+			typestr = "Playstation2/3-like";
+			break;
 		case SDL_CONTROLLER_TYPE_PS4:
 		case SDL_CONTROLLER_TYPE_PS5:
 			gamepadType = D3_GAMEPAD_PLAYSTATION;
@@ -772,6 +780,15 @@ static void setGamepadType( SDL_GameController* gc )
 	}
 
 	common->Printf( "Detected Gamepad %s as type %s\n", SDL_GameControllerName( gc ), typestr );
+	SDL_Joystick* joy = SDL_GameControllerGetJoystick( gc );
+	SDL_JoystickGUID guid = SDL_JoystickGetGUID( joy );
+	char guidstr[34] = {};
+	SDL_JoystickGetGUIDString( guid, guidstr, sizeof(guidstr) );
+	Uint16 vendor = SDL_GameControllerGetVendor( gc );
+	Uint16 product = SDL_GameControllerGetProduct( gc );
+	const char* joyname = SDL_JoystickName( joy );
+
+	common->Printf( "  USB IDs: %.4hx:%.4hx Joystick Name: \"%s\" GUID: %s\n", vendor, product, joyname, guidstr );
 
 #endif // SDL_VERSION_ATLEAST(2, 0, 12)
 }
diff --git a/neo/ui/UserInterface.cpp b/neo/ui/UserInterface.cpp
index 58379b87..bab78000 100644
--- a/neo/ui/UserInterface.cpp
+++ b/neo/ui/UserInterface.cpp
@@ -442,36 +442,38 @@ const char *idUserInterfaceLocal::HandleEvent( const sysEvent_t *event, int _tim
 	{
 		// map some gamepad buttons to SE_KEY events that the UI already knows how to use
 		int key = 0;
-		switch(event->evValue) {
-			// emulate mouse buttons
-			case K_JOY_TRIGGER2:
-				// the right trigger is often used for shooting, so for ingame UIs
-				// it'll behave like mouseclick - do the same in menus
-				// fall-through
-			case K_JOY_BTN_SOUTH: // A on xbox controller
-				key = K_MOUSE1;
-				break;
-			case K_JOY_BTN_EAST: // B on xbox controller
-				key = K_MOUSE2;
-				break;
-			// emulate cursor keys (sometimes used for scrolling or selecting in a list)
-			case K_JOY_DPAD_UP:
-				key = K_UPARROW;
-				break;
-			case K_JOY_DPAD_DOWN:
-				key = K_DOWNARROW;
-				break;
-			case K_JOY_DPAD_LEFT:
-				key = K_LEFTARROW;
-				break;
-			case K_JOY_DPAD_RIGHT:
-				key = K_RIGHTARROW;
-				break;
-			// enter is useful after selecting something with cursor keys (or dpad)
-			// in a list, like selecting a savegame - I guess left trigger is suitable for that?
-			case K_JOY_TRIGGER1:
-				key = K_ENTER;
-				break;
+		if( idKeyInput::GetUsercmdAction( event->evValue ) ==  20 /* UB_ATTACK*/ ) {
+			// if this button is bound to _attack (fire), treat it as left mouse button
+			key = K_MOUSE1;
+		} else {
+			switch(event->evValue) {
+				// emulate mouse buttons
+				case K_JOY_BTN_SOUTH: // A on xbox controller
+					key = K_MOUSE1;
+					break;
+				case K_JOY_BTN_EAST: // B on xbox controller
+					key = K_MOUSE2;
+					break;
+				// emulate cursor keys (sometimes used for scrolling or selecting in a list)
+				case K_JOY_DPAD_UP:
+					key = K_UPARROW;
+					break;
+				case K_JOY_DPAD_DOWN:
+					key = K_DOWNARROW;
+					break;
+				case K_JOY_DPAD_LEFT:
+					key = K_LEFTARROW;
+					break;
+				case K_JOY_DPAD_RIGHT:
+					key = K_RIGHTARROW;
+					break;
+				// enter is useful after selecting something with cursor keys (or dpad)
+				// in a list, like selecting a savegame - I guess left trigger is suitable for that?
+				// (right trigger is often used for shooting, which we use as K_MOUSE1 here)
+				case K_JOY_TRIGGER1:
+					key = K_ENTER;
+					break;
+			}
 		}
 		if (key != 0) {
 			fakedEvent = *event;

From 5b8e67762b85329730ac82003cf6911a7604c8b4 Mon Sep 17 00:00:00 2001
From: Daniel Gibson <metalcaedes@gmail.com>
Date: Mon, 22 Jan 2024 05:50:45 +0100
Subject: [PATCH 13/14] Also allow using "Pad Y" for leftclick in menus

---
 neo/framework/UsercmdGen.cpp | 3 ++-
 neo/sys/events.cpp           | 6 +++---
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/neo/framework/UsercmdGen.cpp b/neo/framework/UsercmdGen.cpp
index 2d33722e..f52fde16 100644
--- a/neo/framework/UsercmdGen.cpp
+++ b/neo/framework/UsercmdGen.cpp
@@ -431,7 +431,6 @@ idCVar idUsercmdGenLocal::m_showMouseRate( "m_showMouseRate", "0", CVAR_SYSTEM |
 
 idCVar joy_triggerThreshold( "joy_triggerThreshold", "0.05", CVAR_FLOAT | CVAR_ARCHIVE, "how far the joystick triggers have to be pressed before they register as down" );
 idCVar joy_deadZone( "joy_deadZone", "0.25", CVAR_FLOAT | CVAR_ARCHIVE, "specifies how large the dead-zone is on the joystick" );
-idCVar joy_range( "joy_range", "1.0", CVAR_FLOAT | CVAR_ARCHIVE, "allow full range to be mapped to a smaller offset" );
 idCVar joy_gammaLook( "joy_gammaLook", "1", CVAR_INTEGER | CVAR_ARCHIVE, "use a log curve instead of a power curve for movement" );
 idCVar joy_powerScale( "joy_powerScale", "2", CVAR_FLOAT | CVAR_ARCHIVE, "Raise joystick values to this power" );
 idCVar joy_pitchSpeed( "joy_pitchSpeed", "130",	CVAR_ARCHIVE | CVAR_FLOAT, "pitch speed when pressing up or down on the joystick", 60, 600 );
@@ -466,6 +465,8 @@ idUsercmdGenLocal::idUsercmdGenLocal( void ) {
 	toggled_zoom.Clear();
 	toggled_run.on = in_alwaysRun.GetBool();
 
+	lastLookValuePitch = lastLookValueYaw = 0.0f;
+
 	ClearAngles();
 	Clear();
 }
diff --git a/neo/sys/events.cpp b/neo/sys/events.cpp
index 1d0b741b..7da003bf 100644
--- a/neo/sys/events.cpp
+++ b/neo/sys/events.cpp
@@ -1386,12 +1386,12 @@ sysEvent_t Sys_GetEvent() {
 			if ( ev.cbutton.button == SDL_CONTROLLER_BUTTON_START ) {
 				res.evValue = K_ESCAPE;
 				return res;
-			} else if( ev.cbutton.button == SDL_CONTROLLER_BUTTON_A
+			} else if( (ev.cbutton.button == SDL_CONTROLLER_BUTTON_A || ev.cbutton.button == SDL_CONTROLLER_BUTTON_Y)
 					   && D3_IN_interactiveIngameGuiActive && sessLocal.GetActiveMenu() == NULL )
 			{
 				// ugly hack: currently an interactive ingame GUI (with a cursor) is active/focused
-				// so pretend that the gamepads A (south) button is the left mouse button
-				// so it can be used for "clicking"..
+				// so pretend that the gamepads A (south) or Y (north, used by D3BFG to click ingame GUIs) button
+				// is the left mouse button so it can be used for "clicking"..
 				mouse_polls.Append( mouse_poll_t(M_ACTION1, res.evValue2) );
 				res.evValue = K_MOUSE1;
 				return res;

From d5f2dc4916069d83212c2daecd8bde25dac15e8b Mon Sep 17 00:00:00 2001
From: Daniel Gibson <metalcaedes@gmail.com>
Date: Mon, 22 Jan 2024 05:52:16 +0100
Subject: [PATCH 14/14] Add and link Configuration.md and gamepad configs

it documents dhewm3-specific configuration, esp. for gamepads
(but also listing other CVars added in dhewm3)
---
 Changelog.md          |   2 +
 Configuration.md      | 143 ++++++++++++++++++++++++++++++++++++++++++
 README.md             |   6 ++
 base/gamepad-d3xp.cfg |  21 +++++++
 base/gamepad.cfg      |  21 +++++++
 5 files changed, 193 insertions(+)
 create mode 100644 Configuration.md
 create mode 100755 base/gamepad-d3xp.cfg
 create mode 100755 base/gamepad.cfg

diff --git a/Changelog.md b/Changelog.md
index 09c6a259..a133bb0f 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -23,6 +23,8 @@ Note: Numbers starting with a "#" like #330 refer to the bugreport with that num
   (0 = TGA, still the default, 1 = BMP, 2 = PNG, 3 = JPG). `r_screenshotJpgQuality` and
   `r_screenshotPngCompression` allow configuring how JPG/PNG are compressed.
   Thanks *eezstreet (Nick Whitlock)*!
+* Support for gamepads (based on code from [Quadrilateral Cowboy](https://github.com/blendogames/quadrilateralcowboy),
+  but heavily expanded). See [Configuration.md](./Configuration.md#using-gamepads) for more information.
 
 1.5.2 (2022-06-13)
 ------------------------------------------------------------------------
diff --git a/Configuration.md b/Configuration.md
new file mode 100644
index 00000000..4747aa04
--- /dev/null
+++ b/Configuration.md
@@ -0,0 +1,143 @@
+# Configuration
+
+This document explains some dhewm3-specific configuration options.
+
+For general Doom3 configuration see for example [this list of CVars](https://modwiki.dhewm3.org/CVars_%28Doom_3%29)
+and [this list of Console Commands](https://modwiki.dhewm3.org/Commands_%28Doom_3%29).
+
+**CVars** are set by entering `cvarName value` in the console, for example `com_showFPS 1`.  
+They can also be set as commandline arguments when starting dhewm3, for example `./dhewm3 +set r_fullscreen 0`.
+
+Just entering a CVar's name (without a value) will show its current value, its default value
+and a short description of what it does.
+
+Starting dhewm3 with the commandline argument `-h` (for example `dhewm3.exe -h`) will show some
+useful commandline arguments, for example how to tell dhewm3 where the game data can be found on your system.
+
+## The Console
+
+Like most id Software games from Quake 1 on, Doom3 has a console that allows entering commands 
+and setting Console Variables ("CVars"), often for advanced configuration or to aid development,
+see also https://modwiki.dhewm3.org/Console.  
+
+Unlike in original Doom3, in dhewm3 the console is always available (no need to set `com_allowconsole 1`
+or similar), and **can be opened with the key combination `Shift + Esc`**.  
+The classic "console key" (the one between `Esc`, `Tab` and `1`) should also still work with
+most keyboard layouts. However you can disable that, so you can bind that key like any other key
+(for example to select the chainsaw), by setting `in_ignoreConsoleKey 1`.
+
+## Using Gamepads
+
+Starting with 1.5.3 (or the git commits preceding the one adding this document), dhewm3 supports
+using gamepads, as long as they're supported by SDL2.  
+This includes XBox Controllers (and compatible ones), Playstation 3-5 controllers,
+Nintendo Switch Pro Controllers, many thirdparty controllers for those consoles, and lots of other
+gamepads for PC.
+
+Some notes:
+* By default, no bindings for the gamepad exist, so you need to configure them once in the
+  Settings -> Controls menu.
+    - You need to bind *Turn Left*, *Turn Right*, *Look Up* and *Look Down* to the corresponding
+      directions of one stick to use it to look around or aim.
+    - Similarly, you need to bind *Forward*, *Backpedal*, *Move Left* and *Move Right* to the
+      corresponding directions of a stick to use it for player movement.
+* The "Start" button ("+" on Nintendo gamepads, "Options" on Playstation 4/5 controllers) acts
+  like the Escape key, so it will **open/close the menu** and can not be bound.  
+  The other buttons, axes and triggers can be bound to arbitrary actions in the Controls menu,
+  except for the Home button, which can't be used by dhewm3 at all (because it opens Steam when that is running).
+* In **menus**, either stick will move the cursor, and the button you assign to *attack* (fire) acts
+  like the left mouse button, and so does the lower face button (A on XBox controllers, B on Nintendo
+  controllers, Cross on PS controllers) and the upper face button (Y on XBox, X on Nintendo, Triangle on PS).
+* The layout of the controller (XBox-like, Nintendo-like, Playstation-like) should be automatically
+  detected and is used to display the button names according to the layout. If yours isn't detected
+  correctly, you can overwrite it with the `joy_gamepadLayout` CVar.
+* Requires SDL2, layout detection requires SDL 2.0.12 or newer.
+* Only one gamepad is supported or, more specifically, if multiple are connected, they all behave the same
+  and you can't bind their buttons/axes to different actions, and the auto-layout detection will use the
+  last gamepad it found to determine the layout.
+* You can disable gamepads by setting the `in_useGamepad` CVar to `0`.
+* There are several CVars to tweak the behavior:
+    - `joy_deadZone` Deadzone of the sticks, where `1.0` would be "stick moved fully in one direction".
+       This means that values below this register as 0. If you move or look around ingame even though
+       you're not moving a stick, try increasing the `joy_deadZone` value (default is `0.25`).
+    - `joy_triggerThreshold` Basically the deadzone for triggers. If your trigger triggers without
+       being touched, try increasing this value (default is `0.05`).
+    - `joy_gamepadLayout` overwrite automatically detected layout (XBox, Nintendo, PS), see above.
+    - `joy_pitchSpeed` How fast you look up/down (when the stick is at a maximum position)
+    - `joy_yawSpeed` Same for turning left/right
+    - `joy_invertLook` Inverts the controls for looking up/down (like in a flight simulator)
+    - `joy_gammaLook` If set to `1`, use a log curve instead of a power curve for looking around,
+       affects how fast you turn (or look up/down) when the stick is between center and maximum.
+    - `joy_powerScale` If `joy_gammaLook` is `0`, this is the exponent used for the power curve.
+    - `joy_dampenLook` if enabled (`1`), somehow reduced the speed of looking around, depending on
+      `joy_deltaPerMSLook`.
+
+I created gamepad configs for the base game and d3xp (Resurrection of Evil), based on the standard bindings
+of Doom3 BFG, see gamepad.cfg and gamepad-d3xp.cfg in the [base/ directory](./base/).  
+Put them in your base/ folder, open the console and enter `exec gamepad.cfg` for the base game,
+or `exec gamepad-d3xp.cfg` for Resurrection of Evil (probably also works for Doom3: Lost Mission).
+
+**_Note_** that in *configs* (or `bind` commands in the console), the following names are used for
+gamepad buttons, sticks and triggers:
+
+<details><summary>Click to see the list of gamepad button/stick/trigger names</summary>
+
+* "JOY_BTN_SOUTH" - `A` button on XBox-style gamepads, `B` on Nintendo-style gamepads or `Cross` on Playstation-style gamepads 
+* "JOY_BTN_EAST" - `B` (XBox), `A` (Nintendo), `Circle` (Playstation)
+* "JOY_BTN_WEST" - `X` (XBox), `Y` (Nintendo), `Square` (Playstation)
+* "JOY_BTN_NORTH" - `Y` (XBox), `X` (Nintendo), `Triangle` (Playstation)
+* "JOY_BTN_BACK" - The `Back` button, aka `-` (Nintendo) or `Select`/`Share` (Playstation)
+* "JOY_BTN_LSTICK" - Pressing the Left Stick down
+* "JOY_BTN_RSTICK" - Pressing the Right Stick down
+* "JOY_BTN_LSHOULDER" - Left Shoulder Button
+* "JOY_BTN_RSHOULDER" - Right Shoulder button
+* "JOY_DPAD_UP" - DPad Up
+* "JOY_DPAD_DOWN" - DPad Down
+* "JOY_DPAD_LEFT" - DPad Left
+* "JOY_DPAD_RIGHT" - DPad Right
+* "JOY_BTN_MISC1" - misc. additional button, like Xbox Series X share button, PS5 microphone button, Nintendo Switch Pro capture button, Amazon Luna microphone button
+* "JOY_BTN_RPADDLE1" - Upper or primary paddle, under your right hand (e.g. Xbox Elite paddle P1)
+* "JOY_BTN_LPADDLE1" - Upper or primary paddle, under your left hand (e.g. Xbox Elite paddle P3)
+* "JOY_BTN_RPADDLE2" - Lower or secondary paddle, under your right hand (e.g. Xbox Elite paddle P2)
+* "JOY_BTN_LPADDLE2" - Lower or secondary paddle, under your left hand (e.g. Xbox Elite paddle P4
+* "JOY_STICK1_UP" - Moving Left Stick up
+* "JOY_STICK1_DOWN" - Moving Left Stick down
+* "JOY_STICK1_LEFT" - Moving Left Stick to the left
+* "JOY_STICK1_RIGHT" - Moving Left Stick to the right
+* "JOY_STICK2_UP" - Moving Right Stick up
+* "JOY_STICK2_DOWN" - Moving Right Stick down
+* "JOY_STICK2_LEFT" - Moving Right Stick to the left
+* "JOY_STICK2_RIGHT" - Moving Right Stick to the right
+* "JOY_TRIGGER1" - Pressing the Left Trigger
+* "JOY_TRIGGER2" - Pressing the Right Trigger
+
+</details>
+
+## Screenshot configuration
+
+Doom3 always supported taking screenshots, but dhewm3 (from 1.5.3 on) supports using different
+formats than TGA.  
+This can be configured with the following CVars:
+
+- `r_screenshotFormat` What format screenshots should be in:
+  `0` = TGA (default), `1` = BMP, `2` = PNG, `3` = JPG
+- `r_screenshotJpgQuality` Quality when using JPG screenshots (`0` - `100`)
+- `r_screenshotPngCompression` Compression level when using PNG screenshots (`0` - `9`)
+
+## CVars added in dhewm3 that I'm currently too lazy to document more thoroughly
+
+- g_hitEffect
+
+- in_nograb
+- in_grabKeyboard
+
+- in_tty
+- in_kbd
+
+- r_fullscreenDesktop
+- r_fillWindowAlphaChan
+
+- r_useCarmacksReverse
+- r_useStencilOpSeparate
+
+- s_alReverbGain
diff --git a/README.md b/README.md
index ce79d2a8..1df3653f 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,7 @@ Compared to the original _DOOM 3_, the changes of _dhewm 3_ worth mentioning are
 - SDL for low-level OS support, OpenGL and input handling
 - OpenAL for audio output, all OS-specific audio backends are gone
 - OpenAL EFX for EAX reverb effects (read: EAX-like sound effects on all platforms/hardware)
+- Gamepad support
 - Better support for widescreen (and arbitrary display resolutions)
 - A portable build system based on CMake
 - (Cross-)compilation with MinGW-w64
@@ -55,6 +56,11 @@ https://store.steampowered.com/app/208200/DOOM_3/
 
 See https://dhewm3.org/#how-to-install for game data installation instructions.
 
+## Configuration
+
+See [Configuration.md](./Configuration.md) for dhewm3-specific configuration, especially for 
+using gamepads.
+
 ## Compiling
 
 The build system is based on CMake: http://cmake.org/
diff --git a/base/gamepad-d3xp.cfg b/base/gamepad-d3xp.cfg
new file mode 100755
index 00000000..3ced364b
--- /dev/null
+++ b/base/gamepad-d3xp.cfg
@@ -0,0 +1,21 @@
+bind "JOY_BTN_SOUTH" "_moveUp"
+bind "JOY_BTN_WEST" "_impulse13"
+bind "JOY_BTN_BACK" "_impulse19"
+bind "JOY_BTN_LSTICK" "_speed"
+bind "JOY_BTN_RSTICK" "_moveDown"
+bind "JOY_BTN_LSHOULDER" "_impulse15"
+bind "JOY_BTN_RSHOULDER" "_impulse14"
+bind "JOY_DPAD_UP" "_impulse8"
+bind "JOY_DPAD_DOWN" "_impulse11"
+bind "JOY_DPAD_LEFT" "_impulse12"
+bind "JOY_DPAD_RIGHT" "_impulse1"
+bind "JOY_STICK1_UP" "_forward"
+bind "JOY_STICK1_DOWN" "_back"
+bind "JOY_STICK1_LEFT" "_moveLeft"
+bind "JOY_STICK1_RIGHT" "_moveRight"
+bind "JOY_STICK2_UP" "_lookUp"
+bind "JOY_STICK2_DOWN" "_lookDown"
+bind "JOY_STICK2_LEFT" "_left"
+bind "JOY_STICK2_RIGHT" "_right"
+bind "JOY_TRIGGER1" "_impulse0"
+bind "JOY_TRIGGER2" "_attack"
\ No newline at end of file
diff --git a/base/gamepad.cfg b/base/gamepad.cfg
new file mode 100755
index 00000000..99c470df
--- /dev/null
+++ b/base/gamepad.cfg
@@ -0,0 +1,21 @@
+bind "JOY_BTN_SOUTH" "_moveUp"
+bind "JOY_BTN_WEST" "_impulse13"
+bind "JOY_BTN_BACK" "_impulse19"
+bind "JOY_BTN_LSTICK" "_speed"
+bind "JOY_BTN_RSTICK" "_moveDown"
+bind "JOY_BTN_LSHOULDER" "_impulse15"
+bind "JOY_BTN_RSHOULDER" "_impulse14"
+bind "JOY_DPAD_UP" "_impulse5"
+bind "JOY_DPAD_DOWN" "_impulse8"
+bind "JOY_DPAD_LEFT" "_impulse9"
+bind "JOY_DPAD_RIGHT" "_impulse0"
+bind "JOY_STICK1_UP" "_forward"
+bind "JOY_STICK1_DOWN" "_back"
+bind "JOY_STICK1_LEFT" "_moveLeft"
+bind "JOY_STICK1_RIGHT" "_moveRight"
+bind "JOY_STICK2_UP" "_lookUp"
+bind "JOY_STICK2_DOWN" "_lookDown"
+bind "JOY_STICK2_LEFT" "_left"
+bind "JOY_STICK2_RIGHT" "_right"
+bind "JOY_TRIGGER1" "_impulse11"
+bind "JOY_TRIGGER2" "_attack"
\ No newline at end of file