Maelstrom: Added an option to manually control air brakes

From 78005a94f59818da9bbd4a95229f2c244c0de63e Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 30 Mar 2026 05:41:55 -0700
Subject: [PATCH] Added an option to manually control air brakes

Note that this invalidates existing replays because of a new control inserted in the middle of the enum
---
 Data/UI/game.xml         | 10 ++++++++++
 game/Maelstrom_Globals.h |  1 +
 game/controls.cpp        | 17 +++++++++++++++++
 game/controls.h          | 15 +++++++++------
 game/game.cpp            |  2 ++
 game/gameinfo.cpp        |  3 +++
 game/gameinfo.h          |  6 +++++-
 game/main.cpp            |  4 ++++
 game/player.cpp          | 40 ++++++++++++++++++++++++++++------------
 game/player.h            | 20 +++++++++++++++++++-
 game/replay.h            |  2 +-
 11 files changed, 99 insertions(+), 21 deletions(-)

diff --git a/Data/UI/game.xml b/Data/UI/game.xml
index 5fcef5ac..93e7c86c 100644
--- a/Data/UI/game.xml
+++ b/Data/UI/game.xml
@@ -137,6 +137,11 @@
 							<Anchor anchorFrom="TOP" anchorTo="TOP"/>
 							<Action action_enter="CONTROL_DOWN_THRUST" action_leave="CONTROL_UP_THRUST"/>
 						</Thumbstick>
+						<Thumbstick name="brake">
+							<Size w="30" h="30"/>
+							<Anchor anchorFrom="BOTTOM" anchorTo="BOTTOM"/>
+							<Action action_enter="CONTROL_DOWN_BRAKE" action_leave="CONTROL_UP_BRAKE"/>
+						</Thumbstick>
 					</Elements>
 				</Image>
 
@@ -208,6 +213,11 @@
 							<Anchor anchorFrom="TOP" anchorTo="TOP"/>
 							<Action action_enter="CONTROL_DOWN_THRUST" action_leave="CONTROL_UP_THRUST"/>
 						</Thumbstick>
+						<Thumbstick name="brake">
+							<Size w="45" h="45"/>
+							<Anchor anchorFrom="BOTTOM" anchorTo="BOTTOM"/>
+							<Action action_enter="CONTROL_DOWN_BRAKE" action_leave="CONTROL_UP_BRAKE"/>
+						</Thumbstick>
 					</Elements>
 				</Image>
 
diff --git a/game/Maelstrom_Globals.h b/game/Maelstrom_Globals.h
index 69b239e0..23c8e683 100644
--- a/game/Maelstrom_Globals.h
+++ b/game/Maelstrom_Globals.h
@@ -82,6 +82,7 @@ extern void  SetStar(int which);
 // External variables...
 // in main.cpp : 
 extern Bool	gInitializing;
+extern Bool	gControlBrakes;
 extern Bool	gNetworkAvailable;
 extern Bool	gUpdateBuffer;
 extern Bool	gRunning;
diff --git a/game/controls.cpp b/game/controls.cpp
index 9db9dc15..03975e70 100644
--- a/game/controls.cpp
+++ b/game/controls.cpp
@@ -34,6 +34,7 @@ Controls::Controls() :
 	gPauseControl("Controls.PauseControl", SDLK_PAUSE),
 	gShieldControl("Controls.ShieldControl", SDLK_SPACE),
 	gThrustControl("Controls.ThrustControl", SDLK_UP),
+	gBrakeControl("Controls.BrakeControl", SDLK_DOWN),
 	gTurnRControl("Controls.TurnRControl", SDLK_RIGHT),
 	gTurnLControl("Controls.TurnLControl", SDLK_LEFT),
 	gFireControl("Controls.FireControl", SDLK_TAB),
@@ -47,6 +48,7 @@ Controls::Bind(Prefs *prefs)
 	gPauseControl.Bind(prefs);
 	gShieldControl.Bind(prefs);
 	gThrustControl.Bind(prefs);
+	gBrakeControl.Bind(prefs);
 	gTurnRControl.Bind(prefs);
 	gTurnLControl.Bind(prefs);
 	gFireControl.Bind(prefs);
@@ -183,6 +185,8 @@ ControlsDialogDelegate::GetKeycode(int index)
 			return m_controls.gFireControl;
 		case THRUST_CTL:
 			return m_controls.gThrustControl;
+		case BRAKE_CTL:
+			return m_controls.gBrakeControl;
 		case SHIELD_CTL:
 			return m_controls.gShieldControl;
 		case TURNR_CTL:
@@ -208,6 +212,9 @@ ControlsDialogDelegate::SetKeycode(int index, SDL_Keycode keycode)
 		case THRUST_CTL:
 			m_controls.gThrustControl = keycode;
 			break;
+		case BRAKE_CTL:
+			m_controls.gBrakeControl = keycode;
+			break;
 		case SHIELD_CTL:
 			m_controls.gShieldControl = keycode;
 			break;
@@ -348,6 +355,12 @@ static void UpdateControl(Player *player)
 				SDL_GetGamepadAxis(gamepad->gamepad, SDL_GAMEPAD_AXIS_RIGHTY) <= -16000) {
 				keys[THRUST_KEY] = true;
 			}
+
+			if (SDL_GetGamepadButton(gamepad->gamepad, SDL_GAMEPAD_BUTTON_DPAD_UP) ||
+				SDL_GetGamepadAxis(gamepad->gamepad, SDL_GAMEPAD_AXIS_LEFTY) <= -16000 ||
+				SDL_GetGamepadAxis(gamepad->gamepad, SDL_GAMEPAD_AXIS_RIGHTY) <= -16000) {
+				keys[BRAKE_KEY] = true;
+			}
 		}
 
 		if (!gamepad->sessionID) {
@@ -377,6 +390,9 @@ static void UpdateControl(Player *player)
 		if (keystate[SDL_GetScancodeFromKey(controls.gThrustControl, SDL_KMOD_NONE)]) {
 			keys[THRUST_KEY] = true;
 		}
+		if (keystate[SDL_GetScancodeFromKey(controls.gBrakeControl, SDL_KMOD_NONE)]) {
+			keys[BRAKE_KEY] = true;
+		}
 	}
 
 	player->SetControl(FIRE_KEY, keys[FIRE_KEY]);
@@ -384,6 +400,7 @@ static void UpdateControl(Player *player)
 	player->SetControl(LEFT_KEY, keys[LEFT_KEY]);
 	player->SetControl(RIGHT_KEY, keys[RIGHT_KEY]);
 	player->SetControl(THRUST_KEY, keys[THRUST_KEY]);
+	player->SetControl(BRAKE_KEY, keys[BRAKE_KEY]);
 }
 
 void HandleEvent(SDL_Event *event)
diff --git a/game/controls.h b/game/controls.h
index 7bd06383..d843abb5 100644
--- a/game/controls.h
+++ b/game/controls.h
@@ -37,12 +37,13 @@ extern void	HandleEvent(SDL_Event *event);
 
 /* Generic key control definitions */
 #define THRUST_KEY	0x01
-#define RIGHT_KEY	0x02
-#define LEFT_KEY	0x03
-#define SHIELD_KEY	0x04
-#define FIRE_KEY	0x05
-#define PAUSE_KEY	0x06
-#define ABORT_KEY	0x07
+#define BRAKE_KEY	0x02
+#define RIGHT_KEY	0x03
+#define LEFT_KEY	0x04
+#define SHIELD_KEY	0x05
+#define FIRE_KEY	0x06
+#define PAUSE_KEY	0x07
+#define ABORT_KEY	0x08
 
 /* The controls structure */
 class Controls
@@ -56,6 +57,7 @@ class Controls
 	PrefsVariable<SDL_Keycode> gPauseControl;
 	PrefsVariable<SDL_Keycode> gShieldControl;
 	PrefsVariable<SDL_Keycode> gThrustControl;
+	PrefsVariable<SDL_Keycode> gBrakeControl;
 	PrefsVariable<SDL_Keycode> gTurnRControl;
 	PrefsVariable<SDL_Keycode> gTurnLControl;
 	PrefsVariable<SDL_Keycode> gFireControl;
@@ -92,6 +94,7 @@ class ControlsDialogDelegate : public UIDialogDelegate
 	enum {
 		FIRE_CTL,
 		THRUST_CTL,
+		BRAKE_CTL,
 		SHIELD_CTL,
 		TURNR_CTL,
 		TURNL_CTL,
diff --git a/game/game.cpp b/game/game.cpp
index 22ecdafc..cd81849f 100644
--- a/game/game.cpp
+++ b/game/game.cpp
@@ -545,6 +545,8 @@ GamePanelDelegate::OnAction(UIBaseElement *sender, const char *action)
 		unsigned char control;
 		if (SDL_strcasecmp(action, "THRUST") == 0) {
 			control = THRUST_KEY;
+		} else if (SDL_strcasecmp(action, "BRAKE") == 0) {
+			control = BRAKE_KEY;
 		} else if (SDL_strcasecmp(action, "RIGHT") == 0) {
 			control = RIGHT_KEY;
 		} else if (SDL_strcasecmp(action, "LEFT") == 0) {
diff --git a/game/gameinfo.cpp b/game/gameinfo.cpp
index 7de98e1e..8e328801 100644
--- a/game/gameinfo.cpp
+++ b/game/gameinfo.cpp
@@ -62,6 +62,9 @@ GameInfo::SetHost(Uint8 wave, Uint8 lives, Uint8 turbo, Uint8 deathMatch, bool k
 	if (kidMode) {
 		this->gameMode |= GAME_MODE_KIDS;
 	}
+	if (gControlBrakes) {
+		this->gameMode |= GAME_MODE_CONTROL_BRAKES;
+	}
 	this->deathMatch = deathMatch;
 
 	// We are the host node
diff --git a/game/gameinfo.h b/game/gameinfo.h
index ff311e13..0a680782 100644
--- a/game/gameinfo.h
+++ b/game/gameinfo.h
@@ -44,7 +44,8 @@ enum PLAYER_CONTROL {
 };
 
 enum GAME_MODE {
-	GAME_MODE_KIDS       = 0x01,
+	GAME_MODE_KIDS           = 0x01,
+	GAME_MODE_CONTROL_BRAKES = 0x02,
 };
 
 #define IS_LOCAL_CONTROL(X)	(X & CONTROL_LOCAL)
@@ -195,6 +196,9 @@ class GameInfo
 	bool IsKidMode() const {
 		return (gameMode & GAME_MODE_KIDS) != 0;
 	}
+	bool ControlBrakes() const {
+		return (gameMode & GAME_MODE_CONTROL_BRAKES) != 0;
+	}
 
 	void SetNodeState(int index, Uint8 state);
 	Uint8 GetNodeState(int index) const;
diff --git a/game/main.cpp b/game/main.cpp
index 40dae1b0..0bdac632 100644
--- a/game/main.cpp
+++ b/game/main.cpp
@@ -57,6 +57,7 @@ static const char *Version =
 
 // Global variables set in this file...
 Bool	gInitializing = false;
+Bool	gControlBrakes = false;
 Bool	gNetworkAvailable = false;
 Bool	gUpdateBuffer = false;
 Bool	gDelaySound = false;
@@ -140,6 +141,7 @@ void PrintUsage(const char *progname)
 "    --fullscreen      # Run Maelstrom in full-screen mode\n"
 "    --windowed        # Run Maelstrom in windowed mode\n"
 "    --geometry WxH    # Set the window size to WxH\n"
+"    --control-brakes  # Allow manual brake control\n"
 	);
 }
 
@@ -180,6 +182,8 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
 				PrintUsage(argv[0]);
 				return SDL_APP_FAILURE;
 			}
+		} else if ( strcmp(argv[i], "--control-brakes") == 0 ) {
+			gControlBrakes = true;
 		} else if ( strcmp(argv[i], "-NSDocumentRevisionsDebugMode") == 0 && argv[i+1] ) {
 			// Ignore Xcode debug option
 			++i;
diff --git a/game/player.cpp b/game/player.cpp
index 5859e162..2f5840a0 100644
--- a/game/player.cpp
+++ b/game/player.cpp
@@ -33,8 +33,11 @@
 static void ThrustCallback(Uint8 theChannel)
 {
 	for (int i = 0; i < MAX_PLAYERS; ++i) {
-		if ( gPlayers[i]->IsThrusting() ) {
-			sound->PlaySound(gThrusterSound,1,theChannel,ThrustCallback);
+		if ( !gPlayers[i]->IsValid() ) {
+			continue;
+		}
+		if ( gPlayers[i]->IsThrusting() || gPlayers[i]->IsManualBraking() ) {
+			sound->PlaySound(gThrusterSound, 1, theChannel, ThrustCallback);
 			break;
 		}
 	}
@@ -129,6 +132,7 @@ Player::NewWave(void)
 	);
 	xvec = yvec = 0;
 	Thrusting = 0;
+	Braking = 0;
 	NoThrust = 0;
 	ThrustBlit = gThrust1;
 	Shooting = 0;
@@ -165,8 +169,9 @@ Player::NewShip(void)
 	Sphase = 0;
 	xvec = yvec = 0;
 	Thrusting = 0;
-	WasThrusting = 0;
 	ThrustBlit = gThrust1;
+	Braking = 0;
+	WasThrustingOrManualBraking = 0;
 	Shooting = 0;
 	WasShooting = 0;
 	Rotating = 0;
@@ -361,6 +366,7 @@ Player::Explode(void)
 	/* Finish our explosion */
 	Exploding = 1;
 	Thrusting = 0;
+	Braking = 0;
 	Shooting = 0;
 	ShieldOn = 0;
 	solid = 0;
@@ -463,7 +469,7 @@ printf("\n");
 	/* Update our status... :-) */
 	if ( Alive() && ! Exploding ) {
 		/* Airbrakes slow us down. :) */
-		if ( special & AIR_BRAKES ) {
+		if ( IsBraking() ) {
 			if ( yvec > 0 )
 				--yvec;
 			else if ( yvec < 0 )
@@ -477,19 +483,23 @@ printf("\n");
 
 		/* Thrust speeds us up! :)  */
 		if ( Thrusting ) {
-			if ( ! WasThrusting ) {
-				sound->PlaySound(gThrusterSound,
-							1, 3, ThrustCallback);
-				WasThrusting = 1;
-			}
-
 			/* -- The thrust key is down, increase the thrusters! */
 			if ( ! NoThrust ) {
 				Accelerate(gVelocityTable[phase].h,
 						gVelocityTable[phase].v);
 			}
-		} else
-			WasThrusting = 0;
+		}
+
+		if ( Thrusting || IsManualBraking() ) {
+			if ( ! WasThrustingOrManualBraking ) {
+				sound->PlaySound(gThrusterSound,
+							1, 3, ThrustCallback);
+				WasThrustingOrManualBraking = 1;
+			}
+
+		} else {
+			WasThrustingOrManualBraking = 0;
+		}
 
 		/* Shoot baby, shoot. */
 		if ( Shooting ) {
@@ -600,6 +610,9 @@ Player::HandleKeys(void)
 				case THRUST_KEY:
 					Thrusting = 1;
 					break;
+				case BRAKE_KEY:
+					Braking = 1;
+					break;
 				case RIGHT_KEY:
 					Rotating |= 0x01;
 					break;
@@ -622,6 +635,9 @@ Player::HandleKeys(void)
 					if ( sound->Playing(gThrusterSound) )
 						sound->HaltSound(3);
 					break;
+				case BRAKE_KEY:
+					Braking = 0;
+					break;
 				case RIGHT_KEY:
 					Rotating &= ~0x01;
 					break;
diff --git a/game/player.h b/game/player.h
index 62215a23..c41cfe72 100644
--- a/game/player.h
+++ b/game/player.h
@@ -122,6 +122,23 @@ class Player : public Object {
 	virtual int IsThrusting(void) {
 		return(Thrusting);
 	}
+	int IsBraking() {
+		if ( GetSpecial( AIR_BRAKES ) ) {
+			if ( gGameInfo.ControlBrakes() ) {
+				return(Braking);
+			} else {
+				return(1);
+			}
+		} else {
+			return(0);
+		}
+	}
+	int IsManualBraking() {
+		if ( GetSpecial(AIR_BRAKES) && gGameInfo.ControlBrakes() ) {
+			return(Braking && (xvec || yvec));
+		}
+		return(0);
+	}
 	virtual void SetSpecial(unsigned char Spec) {
 		special |= Spec;
 	}
@@ -164,7 +181,8 @@ class Player : public Object {
 	int Thrusting;
 	int NoThrust;
 	Blit *ThrustBlit;
-	int WasThrusting;
+	int Braking;
+	int WasThrustingOrManualBraking;
 	int Shooting;
 	int WasShooting;
 	int Rotating;
diff --git a/game/replay.h b/game/replay.h
index f5181028..7e5184b8 100644
--- a/game/replay.h
+++ b/game/replay.h
@@ -31,7 +31,7 @@
 // Examples of this would be changing the game play area, game info structure,
 // game logic, etc.
 //
-#define REPLAY_VERSION	1
+#define REPLAY_VERSION	2
 #define REPLAY_DIRECTORY "Games"
 #define REPLAY_FILETYPE "mreplay"
 #define LAST_REPLAY	"LastGame." REPLAY_FILETYPE