Maelstrom: Maintain the gamepad list outside of an active game

From 30e7e975729f948593bc0df811d667e66d4879c6 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 8 Mar 2026 10:06:54 -0700
Subject: [PATCH] Maintain the gamepad list outside of an active game

This ensures the setup in the multiplayer lobby matches in-game controls.
---
 game/controls.cpp | 36 ++++++++++++++++++++++++++----------
 game/controls.h   |  1 +
 game/game.cpp     |  3 ---
 game/init.cpp     |  2 ++
 game/lobby.cpp    | 20 +-------------------
 5 files changed, 30 insertions(+), 32 deletions(-)

diff --git a/game/controls.cpp b/game/controls.cpp
index 12482de4..bc997681 100644
--- a/game/controls.cpp
+++ b/game/controls.cpp
@@ -272,6 +272,11 @@ static void UpdateGamepadHandle(SDL_JoystickID id)
 	}
 }
 
+unsigned int GetNumGamepads()
+{
+	return gamepads.length();
+}
+
 static void CloseGamepad(SDL_JoystickID id)
 {
 	for (unsigned int i = 0; i < gamepads.length(); ++i) {
@@ -391,16 +396,6 @@ static void HandleEvent(SDL_Event *event)
 	}
 
 	switch (event->type) {
-		/* -- Handle joystick added */
-		case SDL_EVENT_GAMEPAD_ADDED:
-			OpenGamepad(event->gdevice.which);
-			break;
-
-		/* -- Handle joystick removed */
-		case SDL_EVENT_GAMEPAD_REMOVED:
-			CloseGamepad(event->gdevice.which);
-			break;
-
 		/* -- Handle joystick axis motion */
 		case SDL_EVENT_GAMEPAD_AXIS_MOTION:
 			player = GetJoystickPlayer(event->gaxis.which);
@@ -527,6 +522,25 @@ static void HandleEvent(SDL_Event *event)
 	}
 }
 
+static bool SDLCALL GamepadEventWatch(void *userdata, SDL_Event *event)
+{
+	switch (event->type) {
+		/* -- Handle joystick added */
+		case SDL_EVENT_GAMEPAD_ADDED:
+			OpenGamepad(event->gdevice.which);
+			break;
+
+		/* -- Handle joystick removed */
+		case SDL_EVENT_GAMEPAD_REMOVED:
+			CloseGamepad(event->gdevice.which);
+			break;
+
+		default:
+			break;
+	}
+	return true;
+}
+
 void InitPlayerControls(void)
 {
 	SDL_JoystickID *ids = SDL_GetJoysticks(nullptr);
@@ -536,10 +550,12 @@ void InitPlayerControls(void)
 		}
 		SDL_free(ids);
 	}
+	SDL_AddEventWatch(GamepadEventWatch, nullptr);
 }
 
 void QuitPlayerControls(void)
 {
+	SDL_RemoveEventWatch(GamepadEventWatch, nullptr);
 	for (unsigned int i = 0; i < gamepads.length(); ++i) {
 		Gamepad *gamepad = &gamepads[i];
 		SDL_CloseGamepad(gamepad->gamepad);
diff --git a/game/controls.h b/game/controls.h
index a6b0b84a..53cac236 100644
--- a/game/controls.h
+++ b/game/controls.h
@@ -32,6 +32,7 @@ extern void	LoadControls(void);
 extern void	SaveControls(void);
 extern void	InitPlayerControls(void);
 extern void	QuitPlayerControls(void);
+extern unsigned int GetNumGamepads();
 extern void	HandleEvents(int timeout);
 extern int	DropEvents(void);
 
diff --git a/game/game.cpp b/game/game.cpp
index f6b798aa..8d42445d 100644
--- a/game/game.cpp
+++ b/game/game.cpp
@@ -122,7 +122,6 @@ void NewGame(void)
 	if ( !SetupPlayers() ) {
 		return;
 	}
-	InitPlayerControls();
 
 	/* Send a "NEW_GAME" packet onto the network */
 	if ( gGameInfo.IsMultiplayer() && gGameInfo.IsHosting() ) {
@@ -1061,8 +1060,6 @@ GamePanelDelegate::GameOver()
 {
 	CloseSocket();
 
-	QuitPlayerControls();
-
 	DisableRemoteInput();
 
 	ui->ShowPanel(PANEL_GAMEOVER);
diff --git a/game/init.cpp b/game/init.cpp
index d560ac4e..b21e34ec 100644
--- a/game/init.cpp
+++ b/game/init.cpp
@@ -713,6 +713,7 @@ void CleanUp(void)
 {
 	FreeScores();
 	SaveControls();
+	QuitPlayerControls();
 	if ( gReplayFile ) {
 		SDL_free( gReplayFile );
 		gReplayFile = NULL;
@@ -771,6 +772,7 @@ int DoInitializations(Uint32 window_flags)
 
 	// -- Load our controls
 	LoadControls();
+	InitPlayerControls();
 
 	/* Load the Maelstrom icon */
 	icon = SDL_LoadSurface_IO(OpenRead("icon.png"), true);
diff --git a/game/lobby.cpp b/game/lobby.cpp
index 561ea70f..899c9499 100644
--- a/game/lobby.cpp
+++ b/game/lobby.cpp
@@ -102,7 +102,7 @@ class ControlClickCallback : public UIClickCallback
 		}
 
 		// Show the control dialog
-		int num_gamepads = GetNumGamepads();
+		unsigned int num_gamepads = GetNumGamepads();
 		SetControl(CONTROL_NONE, (m_index > 0) && m_game.IsHosting());
 		SetControl(CONTROL_LOCAL, true);
 		SetControl(CONTROL_JOYSTICK1, num_gamepads >= 1);
@@ -117,24 +117,6 @@ class ControlClickCallback : public UIClickCallback
 	}
 
 private:
-	int GetNumGamepads()
-	{
-		int num_gamepads = 0;
-		SDL_JoystickID* gamepads = SDL_GetGamepads(nullptr);
-		if (gamepads) {
-			for (int i = 0; gamepads[i]; ++i) {
-				SDL_Gamepad* gamepad = SDL_OpenGamepad(gamepads[i]);
-				if (gamepad) {
-					if (!GetRemoteSessionForGamepad(gamepad)) {
-						++num_gamepads;
-					}
-					SDL_CloseGamepad(gamepad);
-				}
-			}
-			SDL_free(gamepads);
-		}
-		return num_gamepads;
-	}
 	void SetControl(Uint8 control, bool enabled) {
 		char name[128];
 		UIElement *element;