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"