Maelstrom: Disable the play button if multiplayer setup isn't complete

From 137fdeecacb33b33c6aced8279abe2664ab2efd0 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 6 Apr 2026 00:30:48 -0700
Subject: [PATCH] Disable the play button if multiplayer setup isn't complete

---
 game/gameinfo.cpp | 14 ++++++++++++++
 game/gameinfo.h   |  1 +
 game/lobby.cpp    | 28 +++++++++++++++++++++++-----
 game/lobby.h      |  4 ++--
 4 files changed, 40 insertions(+), 7 deletions(-)

diff --git a/game/gameinfo.cpp b/game/gameinfo.cpp
index 44cd4265..eca76276 100644
--- a/game/gameinfo.cpp
+++ b/game/gameinfo.cpp
@@ -406,6 +406,20 @@ GameInfo::IsNetworkPlayer(int index) const
 	return (players[index].nodeID != localID);
 }
 
+bool
+GameInfo::HasLocalControl() const
+{
+	for (int i = 0; i < MAX_PLAYERS; ++i) {
+		if (!IsLocalPlayer(i)) {
+			continue;
+		}
+		if (IS_LOCAL_CONTROL(players[i].controlMask)) {
+			return true;
+		}
+	}
+	return false;
+}
+
 int
 GameInfo::GetNumPlayers() const
 {
diff --git a/game/gameinfo.h b/game/gameinfo.h
index b4854cc9..dc8947b4 100644
--- a/game/gameinfo.h
+++ b/game/gameinfo.h
@@ -193,6 +193,7 @@ class GameInfo
 	bool IsValidPlayer(int index) const;
 	bool IsLocalPlayer(int index) const;
 	bool IsNetworkPlayer(int index) const;
+	bool HasLocalControl() const;
 	int GetNumPlayers() const;
 
 	bool IsFull() const;
diff --git a/game/lobby.cpp b/game/lobby.cpp
index e9a1d01a..e711ba8e 100644
--- a/game/lobby.cpp
+++ b/game/lobby.cpp
@@ -83,6 +83,8 @@ class SelectControlCallback : public UIClickCallback
 		}
 		m_game.SetPlayerSlot(m_index, name, m_controlType);
 		m_dialog->Hide();
+
+		m_lobby->UpdateUI();
 	}
 
 private:
@@ -116,14 +118,14 @@ class ControlClickCallback : public UIClickCallback
 
 		// Show the control dialog
 		unsigned int num_gamepads = GetNumGamepads();
-		SetControl(CONTROL_NONE, (m_index > 0) && m_game.IsHosting());
+		SetControl(CONTROL_NONE, m_game.IsHosting());
 		SetControl(CONTROL_LOCAL, true);
 		SetControl(CONTROL_JOYSTICK1, num_gamepads >= 1);
 		SetControl(CONTROL_JOYSTICK2, num_gamepads >= 2);
 		SetControl(CONTROL_JOYSTICK3, num_gamepads >= 3);
-		SetControl(CONTROL_REMOTE1, (m_index > 0) && m_game.IsHosting() && GetRemotePlayerName(CONTROL_REMOTE1));
-		SetControl(CONTROL_REMOTE2, (m_index > 0) && m_game.IsHosting() && GetRemotePlayerName(CONTROL_REMOTE2));
-		SetControl(CONTROL_NETWORK, (m_index > 0) && m_game.IsHosting());
+		SetControl(CONTROL_REMOTE1, m_game.IsHosting() && GetRemotePlayerName(CONTROL_REMOTE1));
+		SetControl(CONTROL_REMOTE2, m_game.IsHosting() && GetRemotePlayerName(CONTROL_REMOTE2));
+		SetControl(CONTROL_NETWORK, m_game.IsHosting());
 
 		m_dialog->SetAnchor(LEFT, RIGHT, m_button, -4, 0);
 		m_dialog->Show();
@@ -430,7 +432,23 @@ LobbyDialogDelegate::UpdateUI()
 		}
 
 		if (m_state == STATE_HOSTING) {
-			m_playButton->SetDisabled(false);
+			bool play_enabled = true;
+			if (m_game.IsDeathmatch()) {
+				// Make sure we have multiple players for deathmatch
+				if (m_game.GetNumPlayers() <= 1) {
+					play_enabled = false;
+				}
+			} else {
+				// Make sure there is a local player for PvE
+				if (!m_game.HasLocalControl()) {
+					play_enabled = false;
+				}
+			}
+			if (play_enabled) {
+				m_playButton->SetDisabled(false);
+			} else {
+				m_playButton->SetDisabled(true);
+			}
 			if (m_deathmatch) {
 				m_deathmatch->SetDisabled(false);
 			}
diff --git a/game/lobby.h b/game/lobby.h
index c9a6555d..ffc1f82c 100644
--- a/game/lobby.h
+++ b/game/lobby.h
@@ -48,6 +48,8 @@ class LobbyDialogDelegate : public UIDialogDelegate
 	virtual void OnPoll() override;
 	virtual bool HandleEvent(const SDL_Event &event) override;
 
+	void UpdateUI();
+
 	void SendKick(int index);
 
 protected:
@@ -57,8 +59,6 @@ class LobbyDialogDelegate : public UIDialogDelegate
 	void DeathmatchChanged(void *, int value);
 	void LivesChanged(void *, const char *text);
 
-	void UpdateUI();
-
 	void CheckPings();
 	void GetGameList();
 	void GetGameInfo();