Maelstrom: Added a PvE / PvP radio button to the multiplayer setup

From d5268aa21676ec649629a03b054725b48cd8435a Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 5 Apr 2026 21:52:10 -0700
Subject: [PATCH] Added a PvE / PvP radio button to the multiplayer setup

Also allow setting the number of lives for PvP games
---
 Data/UI/lobby.xml        |  41 ++++++++++++---
 game/Maelstrom_Globals.h |   2 +
 game/gameinfo.cpp        |  18 +++----
 game/gameinfo.h          |   6 +--
 game/lobby.cpp           | 105 +++++++++++++++++++++++++++++++--------
 game/lobby.h             |  13 +++--
 game/main.cpp            |   4 +-
 game/player.cpp          |   5 +-
 game/protocol.h          |   3 +-
 game/replay.h            |   2 +-
 10 files changed, 145 insertions(+), 54 deletions(-)

diff --git a/Data/UI/lobby.xml b/Data/UI/lobby.xml
index ba830640..4c299bdb 100644
--- a/Data/UI/lobby.xml
+++ b/Data/UI/lobby.xml
@@ -250,13 +250,40 @@
 					</Elements>
 				</Area>
 
-				<DialogLabel name="deathmatch_label" text="Deathmatch Frags:">
-					<Anchor anchorFrom="BOTTOMLEFT" anchorTo="BOTTOMLEFT" x="12" y="-13"/>
-				</DialogLabel>
-				<DialogEditbox name="deathmatch" numeric="true" maxlen="2" bindText="Network.Deathmatch" text="0">
-					<Size w="30" h="21"/>
-					<Anchor anchorFrom="LEFT" anchorTo="RIGHT" anchor="deathmatch_label" x="8"/>
-				</DialogEditbox>
+				<DialogRadioGroup name="deathmatch" bindValue="Network.Deathmatch">
+					<Elements>
+						<DialogRadioButton name="pve" text="PvE" checked="true" id="0">
+							<Anchor anchorFrom="BOTTOMLEFT" anchorTo="BOTTOMLEFT" x="18" y="-13"/>
+						</DialogRadioButton>
+						<DialogRadioButton name="pvp" text="PvP" id="1">
+							<Anchor anchorFrom="LEFT" anchorTo="RIGHT" anchor="pve"/>
+						</DialogRadioButton>
+					</Elements>
+				</DialogRadioGroup>
+
+				<Area name="lives">
+					<Elements>
+						<DialogLabel name="lives_label" text="Lives:">
+							<Anchor anchorFrom="LEFT" anchorTo="RIGHT" anchor="pvp"/>
+						</DialogLabel>
+						<DialogEditbox name="lives_value" numeric="true" maxlen="2" bindText="Network.Lives" text="0">
+							<Size w="30" h="21"/>
+							<Anchor anchorFrom="LEFT" anchorTo="RIGHT" anchor="lives_label" x="8"/>
+						</DialogEditbox>
+					</Elements>
+				</Area>
+
+				<Area name="frags">
+					<Elements>
+						<DialogLabel name="frags_label" text="Frags:">
+							<Anchor anchorFrom="LEFT" anchorTo="RIGHT" anchor="pvp"/>
+						</DialogLabel>
+						<DialogEditbox name="frags_value" numeric="true" maxlen="2" bindText="Network.Frags" text="0">
+							<Size w="30" h="21"/>
+							<Anchor anchorFrom="LEFT" anchorTo="RIGHT" anchor="frags_label" x="8"/>
+						</DialogEditbox>
+					</Elements>
+				</Area>
 			</Elements>
 		</Area>
 
diff --git a/game/Maelstrom_Globals.h b/game/Maelstrom_Globals.h
index 3bec4e64..6f13fb09 100644
--- a/game/Maelstrom_Globals.h
+++ b/game/Maelstrom_Globals.h
@@ -47,6 +47,8 @@
 #define PREFERENCES_RESOLUTION "Resolution"
 #define PREFERENCES_HANDLE "Handle"
 #define PREFERENCES_DEATHMATCH "Network.Deathmatch"
+#define PREFERENCES_MULTIPLAYER_LIVES "Network.Lives"
+#define PREFERENCES_MULTIPLAYER_FRAGS "Network.Frags"
 #define PREFERENCES_KIDMODE "Cheat.KidMode"
 #define PREFERENCES_CONTINUES "Cheat.Continues"
 
diff --git a/game/gameinfo.cpp b/game/gameinfo.cpp
index 36d45e5c..9b6ddc74 100644
--- a/game/gameinfo.cpp
+++ b/game/gameinfo.cpp
@@ -42,30 +42,31 @@ GameInfo::Reset()
 	lives = 0;
 	turbo = 0;
 	gameMode = 0;
-	deathMatch = 0;
 	numNodes = 0;
 	SDL_zero(nodes);
 	SDL_zero(players);
 }
 
 void
-GameInfo::SetHost(Uint8 wave, Uint8 lives, Uint8 turbo, Uint8 deathMatch, bool kidMode)
+GameInfo::SetHost(Uint8 wave, Uint8 lives, Uint8 turbo, bool deathmatch)
 {
 	Reset();
 
 	this->gameID = localID;
 	this->seed = GetRandSeed();
 	this->wave = wave;
-	this->lives = deathMatch ? deathMatch : lives;
+	this->lives = lives;
 	this->turbo = turbo;
 	this->gameMode = 0;
-	if (kidMode) {
-		this->gameMode |= GAME_MODE_KIDS;
+	if (deathmatch) {
+		this->gameMode |= GAME_MODE_DEATHMATCH;
+	}
+	if (prefs->GetBool(PREFERENCES_KIDMODE)) {
+        this->gameMode |= GAME_MODE_KIDS;
 	}
 	if (gControlBrakes) {
 		this->gameMode |= GAME_MODE_CONTROL_BRAKES;
 	}
-	this->deathMatch = deathMatch;
 
 	// We are the host node
 	assert(HOST_NODE == 0);
@@ -144,7 +145,6 @@ GameInfo::CopyFrom(const GameInfo &rhs)
 	lives = rhs.lives;
 	turbo = rhs.turbo;
 	gameMode = rhs.gameMode;
-	deathMatch = rhs.deathMatch;
 
 	for (i = 0; i < MAX_NODES; ++i) {
 		const GameInfoNode *node = rhs.GetNode(i);
@@ -210,9 +210,6 @@ GameInfo::ReadFromPacket(DynamicPacket &packet)
 	if (!packet.Read(gameMode)) {
 		return false;
 	}
-	if (!packet.Read(deathMatch)) {
-		return false;
-	}
 
 	if (!packet.Read(numNodes)) {
 		return false;
@@ -263,7 +260,6 @@ GameInfo::WriteToPacket(DynamicPacket &packet)
 	packet.Write(lives);
 	packet.Write(turbo);
 	packet.Write(gameMode);
-	packet.Write(deathMatch);
 
 	packet.Write(numNodes);
 	for (i = 0; i < MAX_NODES; ++i) {
diff --git a/game/gameinfo.h b/game/gameinfo.h
index 72bd0da8..b4854cc9 100644
--- a/game/gameinfo.h
+++ b/game/gameinfo.h
@@ -46,6 +46,7 @@ enum PLAYER_CONTROL {
 enum GAME_MODE {
 	GAME_MODE_KIDS           = 0x01,
 	GAME_MODE_CONTROL_BRAKES = 0x02,
+	GAME_MODE_DEATHMATCH     = 0x04,
 };
 
 #define IS_LOCAL_CONTROL(X)	(X & CONTROL_LOCAL)
@@ -128,7 +129,7 @@ class GameInfo
 		localID = uniqueID;
 	}
 
-	void SetHost(Uint8 wave, Uint8 lives, Uint8 turbo, Uint8 deathMatch, bool kidMode);
+	void SetHost(Uint8 wave, Uint8 lives, Uint8 turbo, bool deathMatch);
 
 	void SetPlayerSlot(int slot, const char *name, Uint8 controlMask);
 	void SetPlayerName(int slot, const char *name);
@@ -197,7 +198,7 @@ class GameInfo
 	bool IsFull() const;
 
 	bool IsDeathmatch() const {
-		return deathMatch != 0;
+		return (gameMode & GAME_MODE_DEATHMATCH) != 0;
 	}
 	bool IsKidMode() const {
 		return (gameMode & GAME_MODE_KIDS) != 0;
@@ -235,7 +236,6 @@ class GameInfo
 	Uint8 lives;
 	Uint8 turbo;
 	Uint8 gameMode;
-	Uint8 deathMatch;
 
 	Uint32 localID;
 
diff --git a/game/lobby.cpp b/game/lobby.cpp
index 4cf9aaa1..1e4c8aaa 100644
--- a/game/lobby.cpp
+++ b/game/lobby.cpp
@@ -169,12 +169,24 @@ LobbyDialogDelegate::OnLoad()
 	}
 	m_hostOrJoin->SetValueCallback(this, &LobbyDialogDelegate::SetHostOrJoin);
 
-	m_deathmatch = m_dialog->GetElement<UIElement>("deathmatch");
-	if (!m_deathmatch) {
-		SDL_Log("Warning: Couldn't find editbox 'deathmatch'");
-		return false;
+	m_deathmatch = m_dialog->GetElement<UIElementRadioGroup>("deathmatch");
+	if (m_deathmatch) {
+		m_deathmatch->SetValueCallback(this, &LobbyDialogDelegate::DeathmatchChanged);
+	}
+
+	m_lives = m_dialog->GetElement<UIElement>("lives");
+	m_livesLabel = m_dialog->GetElement<UIElement>("lives_label");
+	m_livesValue = m_dialog->GetElement<UIElement>("lives_value");
+	if (m_livesValue) {
+		m_livesValue->SetTextCallback(this, &LobbyDialogDelegate::LivesChanged, nullptr);
+	}
+
+	m_frags = m_dialog->GetElement<UIElement>("frags");
+	m_fragsLabel = m_dialog->GetElement<UIElement>("frags_label");
+	m_fragsValue = m_dialog->GetElement<UIElement>("frags_value");
+	if (m_fragsValue) {
+		m_fragsValue->SetTextCallback(this, &LobbyDialogDelegate::LivesChanged, nullptr);
 	}
-	m_deathmatch->SetTextCallback(this, &LobbyDialogDelegate::DeathmatchChanged, nullptr);
 
 	if (!GetElement("gamelist", m_gameListArea)) {
 		return false;
@@ -334,9 +346,24 @@ LobbyDialogDelegate::JoinGameClicked(void *_element)
 }
 
 void
-LobbyDialogDelegate::DeathmatchChanged(void *, const char *text)
+LobbyDialogDelegate::DeathmatchChanged(void*, int value)
 {
-	m_game.deathMatch = SDL_atoi(text);
+	if (m_state == STATE_HOSTING) {
+		if (value) {
+			m_game.gameMode |= GAME_MODE_DEATHMATCH;
+			m_game.lives = prefs->GetNumber(PREFERENCES_MULTIPLAYER_FRAGS, DEFAULT_START_LIVES);
+		} else {
+			m_game.gameMode &= ~GAME_MODE_DEATHMATCH;
+			m_game.lives = prefs->GetNumber(PREFERENCES_MULTIPLAYER_LIVES, DEFAULT_START_LIVES);
+		}
+		UpdateUI();
+	}
+}
+
+void
+LobbyDialogDelegate::LivesChanged(void *, const char *text)
+{
+	m_game.lives = SDL_atoi(text);
 }
 
 void
@@ -363,15 +390,48 @@ LobbyDialogDelegate::UpdateUI()
 			m_game.BindPlayerToUI(i, m_gameInfoPlayers[i]);
 		}
 
-		char deathmatch[10];
-		m_deathmatch->SetText(SDL_itoa(m_game.deathMatch, deathmatch, 10));
-	}
-	if (m_state == STATE_HOSTING) {
-		m_playButton->SetDisabled(false);
-		m_deathmatch->SetDisabled(false);
-	} else {
-		m_playButton->SetDisabled(true);
-		m_deathmatch->SetDisabled(true);
+		if (m_deathmatch) {
+			m_deathmatch->SetValue(m_game.IsDeathmatch());
+		}
+		if (m_game.IsDeathmatch()) {
+			m_lives->Hide();
+			m_frags->Show();
+			if (m_fragsValue) {
+				char lives[10];
+				m_fragsValue->SetText(SDL_itoa(m_game.lives, lives, 10));
+			}
+		} else {
+			m_lives->Show();
+			m_frags->Hide();
+			if (m_livesValue) {
+				char lives[10];
+				m_livesValue->SetText(SDL_itoa(m_game.lives, lives, 10));
+			}
+		}
+
+		if (m_state == STATE_HOSTING) {
+			m_playButton->SetDisabled(false);
+			if (m_deathmatch) {
+				m_deathmatch->SetDisabled(false);
+			}
+			if (m_lives) {
+				m_lives->SetDisabled(false);
+			}
+			if (m_frags) {
+				m_frags->SetDisabled(false);
+			}
+		} else {
+			m_playButton->SetDisabled(true);
+			if (m_deathmatch) {
+				m_deathmatch->SetDisabled(true);
+			}
+			if (m_lives) {
+				m_lives->SetDisabled(true);
+			}
+			if (m_frags) {
+				m_frags->SetDisabled(true);
+			}
+		}
 	}
 }
 
@@ -412,11 +472,14 @@ LobbyDialogDelegate::SetState(LOBBY_STATE state)
 			SendLeaveRequest();
 		}
 	} else if (state == STATE_HOSTING) {
-		m_game.SetHost(DEFAULT_START_WAVE,
-				DEFAULT_START_LIVES,
-				DEFAULT_START_TURBO,
-				prefs->GetNumber(PREFERENCES_DEATHMATCH),
-				prefs->GetBool(PREFERENCES_KIDMODE));
+		bool deathmatch = prefs->GetBool(PREFERENCES_DEATHMATCH);
+		Uint8 lives;
+		if (deathmatch) {
+			lives = prefs->GetNumber(PREFERENCES_MULTIPLAYER_FRAGS, DEFAULT_START_LIVES);
+		} else {
+			lives = prefs->GetNumber(PREFERENCES_MULTIPLAYER_LIVES, DEFAULT_START_LIVES);
+		}
+		m_game.SetHost(DEFAULT_START_WAVE, lives, DEFAULT_START_TURBO, deathmatch);
 
 		// Set up the controls for this game
 		for (i = 0; i < MAX_PLAYERS; ++i) {
diff --git a/game/lobby.h b/game/lobby.h
index e65b8e6f..d7fd3998 100644
--- a/game/lobby.h
+++ b/game/lobby.h
@@ -51,9 +51,10 @@ class LobbyDialogDelegate : public UIDialogDelegate
 
 protected:
 	bool GetElement(const char *name, UIElement *&element);
-	void SetHostOrJoin(void*, int value);
+	void SetHostOrJoin(void *, int value);
 	void JoinGameClicked(void *element);
-	void DeathmatchChanged(void *, const char *text);
+	void DeathmatchChanged(void *, int value);
+	void LivesChanged(void *, const char *text);
 
 	void UpdateUI();
 
@@ -96,7 +97,13 @@ class LobbyDialogDelegate : public UIDialogDelegate
 	DynamicPacket m_packet, m_reply;
 
 	UIElementRadioGroup *m_hostOrJoin;
-	UIElement *m_deathmatch;
+	UIElementRadioGroup *m_deathmatch;
+	UIElement *m_lives;
+	UIElement *m_livesLabel;
+	UIElement *m_livesValue;
+	UIElement *m_frags;
+	UIElement *m_fragsLabel;
+	UIElement *m_fragsValue;
 	UIElement *m_gameListArea;
 	UIElement *m_gameListElements[5];
 	UIElement *m_gameInfoArea;
diff --git a/game/main.cpp b/game/main.cpp
index 678c0c91..4fe8e237 100644
--- a/game/main.cpp
+++ b/game/main.cpp
@@ -123,7 +123,7 @@ static void CheatDialogDone(void*, UIDialog *dialog, int status)
 		Delay(SOUND_DELAY);
 		sound->PlaySound(gNewLife, 5);
 		Delay(SOUND_DELAY);
-		gGameInfo.SetHost(wave, lives, turbo, 0, prefs->GetBool(PREFERENCES_KIDMODE));
+		gGameInfo.SetHost(wave, lives, turbo, false);
 		gGameInfo.SetPlayerSlot(0, prefs->GetString(PREFERENCES_HANDLE), CONTROL_LOCAL);
 		RunSinglePlayerGame();
 	}
@@ -462,7 +462,7 @@ MainPanelDelegate::OnAction(UIBaseElement *sender, const char *action)
 void
 MainPanelDelegate::OnActionPlay()
 {
-	gGameInfo.SetHost(DEFAULT_START_WAVE, DEFAULT_START_LIVES, DEFAULT_START_TURBO, 0, prefs->GetBool(PREFERENCES_KIDMODE));
+	gGameInfo.SetHost(DEFAULT_START_WAVE, DEFAULT_START_LIVES, DEFAULT_START_TURBO, false);
 	gGameInfo.SetPlayerSlot(0, prefs->GetString(PREFERENCES_HANDLE), CONTROL_LOCAL);
 	RunSinglePlayerGame();
 }
diff --git a/game/player.cpp b/game/player.cpp
index 6e993a09..6241768c 100644
--- a/game/player.cpp
+++ b/game/player.cpp
@@ -198,7 +198,7 @@ void
 Player::IncrFrags(void)
 {
 	++Frags;
-	if ( gGameInfo.IsDeathmatch() && (Frags >= gGameInfo.deathMatch) ) {
+	if ( gGameInfo.IsDeathmatch() && (Frags >= gGameInfo.lives) ) {
 		/* Game over, we got a stud. :) */
 		int i;
 		OBJ_LOOP(i, MAX_PLAYERS) {
@@ -207,9 +207,6 @@ Player::IncrFrags(void)
 			}
 			gPlayers[i]->IncrLives(-1);
 			gPlayers[i]->Explode();
-#ifdef DEBUG
-error("Killing player %d\n", i+1);
-#endif
 		}
 	}
 }
diff --git a/game/protocol.h b/game/protocol.h
index 45889cb7..6479b994 100644
--- a/game/protocol.h
+++ b/game/protocol.h
@@ -72,8 +72,7 @@ enum LobbyProtocol {
 	/* Sent by the hosting game, if there are slots open
 
 		Uint32 timestamp
-		Uint32 gameID
-		Uint8 deathMatch;
+		GameInfo game
 		Uint32 player1_uniqueID;
 		Uint32 player1_host;
 		Uint16 player1_port;
diff --git a/game/replay.h b/game/replay.h
index 79cb6db7..f06856ae 100644
--- a/game/replay.h
+++ b/game/replay.h
@@ -33,7 +33,7 @@
 //
 // Examples of this would be changing the game play area, game logic, etc.
 //
-#define REPLAY_VERSION	2
+#define REPLAY_VERSION	3
 
 #define REPLAY_DIRECTORY "Games"
 #define REPLAY_FILETYPE "mreplay"