Maelstrom: Added support for Emscripten

From c18518ed4e177a9d52b6ca011fa4c00da76b02ec Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 15 Mar 2026 03:16:55 -0700
Subject: [PATCH] Added support for Emscripten

As part of this work we needed to switch over to the SDL main callbacks and make sure that we return to the main event loop any time we want to render a frame or delay for a bit.
---
 CMakeLists.txt             | 132 +++++-----
 Data/UI/main.xml           |  21 +-
 external/SDL_net           |   2 +-
 game/MaelstromUI.cpp       |  12 +-
 game/Maelstrom_Globals.h   |   4 +
 game/controls.cpp          |  37 +--
 game/controls.h            |   3 +-
 game/game.cpp              | 312 ++++++++++++++++-------
 game/game.h                |  28 +++
 game/gameinfo.cpp          |   2 +-
 game/gameover.cpp          |  12 +-
 game/init.cpp              | 490 ++++++++++++++++++++++---------------
 game/init.h                |   3 +-
 game/main.cpp              | 186 ++++++++------
 game/main.h                |   5 +-
 game/scores.cpp            |   6 -
 screenlib/SDL_FrameBuf.cpp |  62 +++--
 screenlib/SDL_FrameBuf.h   |  21 +-
 18 files changed, 827 insertions(+), 511 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1f8a21e2..aef893cb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -14,6 +14,11 @@ project(Maelstrom
 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$<CONFIGURATION>")
 set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$<CONFIGURATION>")
 
+# on Web targets, we need CMake to generate a HTML webpage.
+if(EMSCRIPTEN)
+    set(CMAKE_EXECUTABLE_SUFFIX_CXX ".html" CACHE INTERNAL "")
+endif()
+
 add_subdirectory(maclib)
 add_subdirectory(external/SDL EXCLUDE_FROM_ALL)
 block()
@@ -188,74 +193,85 @@ endif()
 
 target_link_libraries(Maelstrom PRIVATE SDLmac)
 target_link_libraries(Maelstrom PRIVATE SDL3::SDL3)
-target_link_libraries(Maelstrom PRIVATE SDL3_net::SDL3_net-static)
+target_link_libraries(Maelstrom PRIVATE SDL3_net::SDL3_net)
 
-option(STANDALONE_INSTALL "Build Maelstrom installed into a single directory" TRUE)
+if(EMSCRIPTEN)
+    # Increase the default stack size
+    target_link_options(Maelstrom PRIVATE -sSTACK_SIZE=1048576)
 
-if(STANDALONE_INSTALL)
-    set(CMAKE_INSTALL_BINDIR ".")
-    set(CMAKE_INSTALL_LIBDIR ".")
-    set(CMAKE_INSTALL_DOCDIR "Docs")
-    set(GAME_INSTALL_DATADIR ".")
-    if(APPLE)
-        set_property(TARGET Maelstrom SDL3-shared PROPERTY INSTALL_RPATH "@executable_path")
-    else()
-        set_property(TARGET Maelstrom SDL3-shared PROPERTY INSTALL_RPATH "$ORIGIN")
-    endif()
+    # on the web, we have to put the files inside of the webassembly
+    # somewhat unintuitively, this is done via a linker argument.
+    target_link_libraries(Maelstrom PRIVATE 
+        "--preload-file \"${CMAKE_CURRENT_LIST_DIR}/Data@/\""
+    )
 else()
-    include(GNUInstallDirs)
-    set(GAME_INSTALL_DATADIR "${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}")
+    option(STANDALONE_INSTALL "Build Maelstrom installed into a single directory" TRUE)
 
-    target_compile_definitions(Maelstrom PRIVATE MAELSTROM_DATA=\"${CMAKE_INSTALL_PREFIX}/${GAME_INSTALL_DATADIR}/Data/\")
-endif()
+    if(STANDALONE_INSTALL)
+        set(CMAKE_INSTALL_BINDIR ".")
+        set(CMAKE_INSTALL_LIBDIR ".")
+        set(CMAKE_INSTALL_DOCDIR "Docs")
+        set(GAME_INSTALL_DATADIR ".")
+        if(APPLE)
+            set_property(TARGET Maelstrom SDL3-shared PROPERTY INSTALL_RPATH "@executable_path")
+        else()
+            set_property(TARGET Maelstrom SDL3-shared PROPERTY INSTALL_RPATH "$ORIGIN")
+        endif()
+    else()
+        include(GNUInstallDirs)
+        set(GAME_INSTALL_DATADIR "${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}")
 
-install(TARGETS Maelstrom SDL3-shared LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" NAMELINK_SKIP RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
-if(STEAM)
-    install(IMPORTED_RUNTIME_ARTIFACTS SteamworksSDK::steam_api)
-endif()
-install(DIRECTORY Data DESTINATION "${GAME_INSTALL_DATADIR}")
+        target_compile_definitions(Maelstrom PRIVATE MAELSTROM_DATA=\"${CMAKE_INSTALL_PREFIX}/${GAME_INSTALL_DATADIR}/Data/\")
+    endif()
 
-file(GLOB docs "Docs/*.txt" "README*")
-install(FILES ${docs} "COPYING" TYPE DOC)
+    install(TARGETS Maelstrom SDL3-shared LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" NAMELINK_SKIP RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
+    if(STEAM)
+        install(IMPORTED_RUNTIME_ARTIFACTS SteamworksSDK::steam_api)
+    endif()
+    install(DIRECTORY Data DESTINATION "${GAME_INSTALL_DATADIR}")
 
-#
-# Uninstall
-#
-configure_file(cmake/cmake_uninstall.cmake.in cmake_uninstall.cmake IMMEDIATE @ONLY)
+    file(GLOB docs "Docs/*.txt" "README*")
+    install(FILES ${docs} "COPYING" TYPE DOC)
 
-add_custom_target(uninstall
-  COMMAND ${CMAKE_COMMAND} -P "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake")
+    #
+    # Uninstall
+    #
+    configure_file(cmake/cmake_uninstall.cmake.in cmake_uninstall.cmake IMMEDIATE @ONLY)
 
-#
-# Source package
-#
-find_package(Git QUIET)
+    add_custom_target(uninstall
+      COMMAND ${CMAKE_COMMAND} -P "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake")
 
-if(GIT_FOUND)
-    add_custom_target(package_sources
-      COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/dist
-      COMMAND ${GIT_EXECUTABLE} archive HEAD -o "${PROJECT_BINARY_DIR}/dist/${PROJECT_NAME}-${PROJECT_VERSION}.zip" --prefix "${PROJECT_NAME}-${PROJECT_VERSION}/"
-      COMMAND ${GIT_EXECUTABLE} archive HEAD -o "${PROJECT_BINARY_DIR}/dist/${PROJECT_NAME}-${PROJECT_VERSION}.tar.gz" --prefix "${PROJECT_NAME}-${PROJECT_VERSION}/"
-      WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
-      COMMENT "Creating source archive..."
-      VERBATIM
-    )
-else()
-    message(WARNING "Git not found, 'package_sources' target will not be available.")
-endif()
+    #
+    # Source package
+    #
+    find_package(Git QUIET)
 
-#
-# Binary package
-#
+    if(GIT_FOUND)
+        add_custom_target(package_sources
+          COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/dist
+          COMMAND ${GIT_EXECUTABLE} archive HEAD -o "${PROJECT_BINARY_DIR}/dist/${PROJECT_NAME}-${PROJECT_VERSION}.zip" --prefix "${PROJECT_NAME}-${PROJECT_VERSION}/"
+          COMMAND ${GIT_EXECUTABLE} archive HEAD -o "${PROJECT_BINARY_DIR}/dist/${PROJECT_NAME}-${PROJECT_VERSION}.tar.gz" --prefix "${PROJECT_NAME}-${PROJECT_VERSION}/"
+          WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+          COMMENT "Creating source archive..."
+          VERBATIM
+        )
+    else()
+        message(WARNING "Git not found, 'package_sources' target will not be available.")
+    endif()
 
-if(MSVC)
-    set(CPACK_GENERATOR "ZIP")
-else()
-    set(CPACK_GENERATOR "TGZ")
+    #
+    # Binary package
+    #
+
+    if(MSVC)
+        set(CPACK_GENERATOR "ZIP")
+    else()
+        set(CPACK_GENERATOR "TGZ")
+    endif()
+    configure_file(cmake/CPackProjectConfig.cmake.in CPackProjectConfig.cmake @ONLY)
+    set(CPACK_PROJECT_CONFIG_FILE "${PROJECT_BINARY_DIR}/CPackProjectConfig.cmake")
+    # CPACK_SOURCE_PACKAGE_FILE_NAME must end with "-src" (so we can block creating a source archive)
+    set(CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-src")
+    set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}/dist")
+    include(CPack)
 endif()
-configure_file(cmake/CPackProjectConfig.cmake.in CPackProjectConfig.cmake @ONLY)
-set(CPACK_PROJECT_CONFIG_FILE "${PROJECT_BINARY_DIR}/CPackProjectConfig.cmake")
-# CPACK_SOURCE_PACKAGE_FILE_NAME must end with "-src" (so we can block creating a source archive)
-set(CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-src")
-set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}/dist")
-include(CPack)
diff --git a/Data/UI/main.xml b/Data/UI/main.xml
index ccbbe31f..8ff48544 100644
--- a/Data/UI/main.xml
+++ b/Data/UI/main.xml
@@ -190,14 +190,20 @@
 		<Label template="SmallYellow" text=" Start playing Maelstrom">
 			<Anchor anchorFrom="BOTTOMLEFT" anchorTo="TOPRIGHT" anchor="PlayButton" x="3" y="21"/>
 		</Label>
-		<Button name="MultiplayerButton" template="MenuButton" hotkey="M" text="M" action="multiplayer" clickSound="114">
+		<Button condition="NETWORK_AVAILABLE" name="MultiplayerButton" template="MenuButton" hotkey="M" text="M" action="multiplayer" clickSound="114">
 			<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="PlayButton" y="34"/>
 		</Button>
-		<Label template="SmallYellow" text=" Start a multiplayer game">
+		<Label condition="NETWORK_AVAILABLE" template="SmallYellow" text=" Start a multiplayer game">
 			<Anchor anchorFrom="BOTTOMLEFT" anchorTo="TOPRIGHT" anchor="MultiplayerButton" x="3" y="21"/>
 		</Label>
+		<Area condition="NETWORK_AVAILABLE" name="ControlsButtonAnchor">
+			<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="MultiplayerButton"/>
+		</Area>
+		<Area condition="!NETWORK_AVAILABLE" name="ControlsButtonAnchor">
+			<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="PlayButton"/>
+		</Area>
 		<Button name="ControlsButton" template="MenuButton" hotkey="C" text="C" action="show_controls" clickSound="119">
-			<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="MultiplayerButton" y="34"/>
+			<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="ControlsButtonAnchor" y="34"/>
 		</Button>
 		<Label template="SmallYellow" text=" Configure the game controls">
 			<Anchor anchorFrom="BOTTOMLEFT" anchorTo="TOPRIGHT" anchor="ControlsButton" x="3" y="21"/>
@@ -214,15 +220,14 @@
 		<Label template="SmallYellow" text=" About Maelstrom...">
 			<Anchor anchorFrom="BOTTOMLEFT" anchorTo="TOPRIGHT" anchor="AboutButton" x="3" y="21"/>
 		</Label>
-		<!-- You can't explicitly quit the mobile app -->
-		<Button condition="!MOBILE" name="QuitButton" template="MenuButton" hotkey="Q" text="Q" action="quit" clickSound="106">
-			<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="AboutButton" y="34"/>
+		<Button condition="QUIT_AVAILABLE" name="QuitButton" template="MenuButton" hotkey="Q" text="Q" action="quit" clickSound="106">
+			<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="PlayButton" y="170"/>
 		</Button>
-		<Label condition="!MOBILE" template="SmallYellow" text=" Quit Maelstrom">
+		<Label condition="QUIT_AVAILABLE" template="SmallYellow" text=" Quit Maelstrom">
 			<Anchor anchorFrom="BOTTOMLEFT" anchorTo="TOPRIGHT" anchor="QuitButton" x="3" y="21"/>
 		</Label>
 		<Button name="VolumeDownButton" template="MenuButton" hotkey="0" text="0" action="volume_down">
-			<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="AboutButton" y="68"/>
+			<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="PlayButton" y="204"/>
 		</Button>
 		<Label template="SmallYellow" text="-">
 			<Anchor anchorFrom="BOTTOMLEFT" anchorTo="TOPRIGHT" anchor="VolumeDownButton" x="3" y="21"/>
diff --git a/external/SDL_net b/external/SDL_net
index 58628517..6623e846 160000
--- a/external/SDL_net
+++ b/external/SDL_net
@@ -1 +1 @@
-Subproject commit 58628517696194b846e5a9bd5adc809d16cb5f13
+Subproject commit 6623e846d93e0e7474877c21380aef852aac7e18
diff --git a/game/MaelstromUI.cpp b/game/MaelstromUI.cpp
index d0f44e55..becb2871 100644
--- a/game/MaelstromUI.cpp
+++ b/game/MaelstromUI.cpp
@@ -202,9 +202,17 @@ MaelstromUI::MaelstromUI(FrameBuf *screen, Prefs *prefs) : UIManager(screen, pre
 	m_strings = hash_create(screen, hash_hash_string, hash_keymatch_string, hash_nuke_string_text);
 
 	/* Set up some conditions useful for UI loading */
-#if defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_ANDROID)
-	SetCondition("MOBILE");
+#if defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_EMSCRIPTEN)
+	bool quit_available = false;
+#else
+	bool quit_available = true;
 #endif
+	if (quit_available) {
+		SetCondition("QUIT_AVAILABLE");
+	}
+	if (gNetworkAvailable) {
+		SetCondition("NETWORK_AVAILABLE");
+	}
 
 	/* Load up our UI templates */
 	ClearLoadPath();
diff --git a/game/Maelstrom_Globals.h b/game/Maelstrom_Globals.h
index 338a7c99..50934a08 100644
--- a/game/Maelstrom_Globals.h
+++ b/game/Maelstrom_Globals.h
@@ -73,6 +73,7 @@ extern void   PrintUsage(void);
 extern int    DrawText(int x, int y, const char *text, MFont *font, Uint8 style,
 						Uint8 R, Uint8 G, Uint8 B);
 extern void   DelayFrame(void);
+extern void   DelaySound(void);
 extern void   DelayAndDraw(int ticks);
 
 // Functions from init.cpp
@@ -80,9 +81,12 @@ extern void  SetStar(int which);
 
 // External variables...
 // in main.cpp : 
+extern Bool	gInitializing;
+extern Bool	gNetworkAvailable;
 extern Bool	gUpdateBuffer;
 extern Bool	gRunning;
 
+
 // in init.cpp : 
 struct Resolution {
 	int w, h;
diff --git a/game/controls.cpp b/game/controls.cpp
index bc997681..9db9dc15 100644
--- a/game/controls.cpp
+++ b/game/controls.cpp
@@ -386,7 +386,7 @@ static void UpdateControl(Player *player)
 	player->SetControl(THRUST_KEY, keys[THRUST_KEY]);
 }
 
-static void HandleEvent(SDL_Event *event)
+void HandleEvent(SDL_Event *event)
 {
 	Player *player;
 	SDL_Keycode key;
@@ -395,6 +395,10 @@ static void HandleEvent(SDL_Event *event)
 		return;
 	}
 
+	if (!gGameOn) {
+		return;
+	}
+
 	switch (event->type) {
 		/* -- Handle joystick axis motion */
 		case SDL_EVENT_GAMEPAD_AXIS_MOTION:
@@ -563,34 +567,3 @@ void QuitPlayerControls(void)
 	gamepads.clear();
 }
 
-/* This function gives a good way to delay a specified amount of time
-   while handling keyboard/joystick events, or just to poll for events.
-*/
-void HandleEvents(int timeout)
-{
-	SDL_Event event;
-
-	do { 
-		while ( screen->PollEvent(&event) ) {
-			HandleEvent(&event);
-		}
-		if ( timeout ) {
-			/* Delay 1/60 of a second... */
-			Delay(1);
-		}
-	} while ( timeout-- );
-}
-
-int DropEvents(void)
-{
-	SDL_Event event;
-	int keys = 0;
-
-	while ( screen->PollEvent(&event) ) {
-		if ( event.type == SDL_EVENT_KEY_DOWN ) {
-			++keys;
-		}
-	}
-	return(keys);
-}
-
diff --git a/game/controls.h b/game/controls.h
index 53cac236..7bd06383 100644
--- a/game/controls.h
+++ b/game/controls.h
@@ -33,8 +33,7 @@ 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);
+extern void	HandleEvent(SDL_Event *event);
 
 /* Generic key control definitions */
 #define THRUST_KEY	0x01
diff --git a/game/game.cpp b/game/game.cpp
index 3792bdd1..854caf3c 100644
--- a/game/game.cpp
+++ b/game/game.cpp
@@ -259,6 +259,47 @@ GamePanelDelegate::OnTick()
 	int i, j;
 	SYNC_RESULT syncResult;
 
+	switch (m_state) {
+	case STATE_SHOW_BONUS:
+		ShowBonus();
+		return;
+	case STATE_BONUS_SHOW_VALUE:
+		BonusShowValue();
+		return;
+	case STATE_BONUS_SHOW_MULTIPLIER:
+		BonusShowMultiplier();
+		return;
+	case STATE_BONUS_DISPLAY_DELAY:
+		BonusDisplayDelay();
+		return;
+	case STATE_BONUS_DISPLAY:
+		BonusDisplay();
+		return;
+	case STATE_BONUS_CHECK_SOUND:
+		BonusCheckSound();
+		return;
+	case STATE_BONUS_TAUNT:
+		BonusTaunt();
+		return;
+	case STATE_BONUS_PRAISE:
+		BonusPraise();
+		return;
+	case STATE_BONUS_COUNTDOWN:
+		BonusCountdown();
+		return;
+	case STATE_BONUS_NEXT_WAVE:
+		BonusNextWave();
+		return;
+	case STATE_BONUS_HIDE:
+		BonusHide();
+		return;
+	case STATE_START_NEXT_WAVE:
+		StartNextWave();
+		return;
+	default:
+		break;
+	}
+
 	if (!gGameOn) {
 		// This generally shouldn't happen, but could if there were
 		// a consistency error during a replay at the bonus screen.
@@ -269,9 +310,6 @@ GamePanelDelegate::OnTick()
 		return;
 	}
 
-	/* -- Read in keyboard input for our ship */
-	HandleEvents(0);
-
 	/* -- Send Sync! signal to all players, and handle keyboard. */
 	if (!gReplay.HandlePlayback()) {
 		GameOver();
@@ -431,6 +469,10 @@ GamePanelDelegate::OnDraw(DRAWLEVEL drawLevel)
 		return;
 	}
 
+	if (m_state != STATE_PLAYING) {
+		return;
+	}
+
 	/* Draw the status frame */
 	DrawStatus(false);
 
@@ -782,18 +824,16 @@ void
 GamePanelDelegate::DoBonus()
 {
 	UIPanel *panel;
-	UIElement *image;
 	UIElement *label;
-	UIElement *bonus;
-	UIElement *score;
-	int i;
 	char numbuf[128];
+	int i;
 
 	/* -- Now do the bonus */
 	sound->HaltSound();
 
 	panel = ui->GetPanel(PANEL_BONUS);
 	if (!panel) {
+		m_state = STATE_START_NEXT_WAVE;
 		return;
 	}
 	panel->HideAll();
@@ -816,21 +856,7 @@ GamePanelDelegate::DoBonus()
 
 	gGameInfo.SetLocalState(STATE_BONUS, true);
 
-	/* Fade out */
-	screen->FadeOut();
-
-	ui->ShowPanel(PANEL_BONUS);
-	ui->Draw();
-
-	/* Fade in */
-	screen->FadeIn();
-	while ( sound->Playing() )
-		DelayAndDraw(SOUND_DELAY);
-
-	/* -- Count the score down */
-
-	bonus = panel->GetElement<UIElement>("bonus");
-	score = panel->GetElement<UIElement>("score");
+	// Handle the bonus for other players
 	OBJ_LOOP(i, MAX_PLAYERS) {
 		if (!gPlayers[i]->IsValid()) {
 			continue;
@@ -851,33 +877,99 @@ GamePanelDelegate::DoBonus()
 
 		if (i != gDisplayed) {
 			gPlayers[i]->MultBonus();
-			continue;
+			gPlayers[i]->IncrScore(gPlayers[i]->GetBonus());
+			gPlayers[i]->IncrBonus(-gPlayers[i]->GetBonus());
 		}
+	}
 
-		if (TheShip->GetBonusMult() != 1) {
-			if (bonus) {
-				SDL_snprintf(numbuf, sizeof(numbuf), "%-5.1d", TheShip->GetBonus());
-				bonus->SetText(numbuf);
-				bonus->Show();
-			}
-			bonus = panel->GetElement<UIElement>("multiplied_bonus");
+	/* Fade out */
+	screen->FadeOut();
+
+	m_state = STATE_SHOW_BONUS;
+}
 
-			TheShip->MultBonus();
-			DelayAndDraw(SOUND_DELAY);
-			sound->PlaySound(gMultiplier, 5);
 
-			SDL_snprintf(numbuf, sizeof(numbuf), "multiplier%d", TheShip->GetBonusMult());
-			image = panel->GetElement<UIElement>(numbuf);
-			if (image) {
-				image->Show();
-			}
+void
+GamePanelDelegate::ShowBonus()
+{
+	ui->ShowPanel(PANEL_BONUS);
+
+	/* Fade in */
+	screen->FadeIn();
+	DelaySound();
+	m_state = STATE_BONUS_SHOW_VALUE;
+}
+
+void
+GamePanelDelegate::BonusShowValue()
+{
+	UIPanel *panel;
+	UIElement *bonus;
+	char numbuf[128];
 
-			DelayAndDraw(60);
+	if (TheShip->GetBonusMult() != 1) {
+		panel = ui->GetPanel(PANEL_BONUS);
+		bonus = panel->GetElement<UIElement>("bonus");
+		if (bonus) {
+			SDL_snprintf(numbuf, sizeof(numbuf), "%-5.1d", TheShip->GetBonus());
+			bonus->SetText(numbuf);
+			bonus->Show();
 		}
+
+		TheShip->MultBonus();
+		DelayAndDraw(SOUND_DELAY);
+		m_state = STATE_BONUS_SHOW_MULTIPLIER;
+		return;
+	}
+	m_state = STATE_BONUS_DISPLAY_DELAY;
+}
+
+void
+GamePanelDelegate::BonusShowMultiplier()
+{
+	UIPanel *panel;
+	UIElement *image;
+	char numbuf[128];
+
+	sound->PlaySound(gMultiplier, 5);
+
+	panel = ui->GetPanel(PANEL_BONUS);
+	SDL_snprintf(numbuf, sizeof(numbuf), "multiplier%d", TheShip->GetBonusMult());
+	image = panel->GetElement<UIElement>(numbuf);
+	if (image) {
+		image->Show();
 	}
+
+	DelayAndDraw(60);
+
+	m_state = STATE_BONUS_DISPLAY_DELAY;
+}
+
+void
+GamePanelDelegate::BonusDisplayDelay()
+{
 	DelayAndDraw(SOUND_DELAY);
+	m_state = STATE_BONUS_DISPLAY;
+}
+
+void
+GamePanelDelegate::BonusDisplay()
+{
+	UIPanel *panel;
+	UIElement *bonus;
+	UIElement *score;
+	char numbuf[128];
+
 	sound->PlaySound(gFunk, 5);
 
+	panel = ui->GetPanel(PANEL_BONUS);
+	if (TheShip->GetBonusMult() != 1) {
+		bonus = panel->GetElement<UIElement>("multiplied_bonus");
+	} else {
+		bonus = panel->GetElement<UIElement>("bonus");
+	}
+	score = panel->GetElement<UIElement>("score");
+
 	if (bonus) {
 		SDL_snprintf(numbuf, sizeof(numbuf), "%-5.1d", TheShip->GetBonus());
 		bonus->SetText(numbuf);
@@ -888,73 +980,109 @@ GamePanelDelegate::DoBonus()
 		score->SetText(numbuf);
 		score->Show();
 	}
-	ui->Draw();
 	DelayAndDraw(60);
 
+	m_state = STATE_BONUS_CHECK_SOUND;
+}
+
+void
+GamePanelDelegate::BonusCheckSound()
+{
 	/* -- Praise them or taunt them as the case may be */
 	if (TheShip->GetBonus() == 0) {
 		DelayAndDraw(SOUND_DELAY);
-		sound->PlaySound(gNoBonus, 5);
+		m_state = STATE_BONUS_TAUNT;
+		return;
 	}
 	if (TheShip->GetBonus() > 10000) {
 		DelayAndDraw(SOUND_DELAY);
-		sound->PlaySound(gPrettyGood, 5);
+		m_state = STATE_BONUS_PRAISE;
+		return;
 	}
-	while ( sound->Playing() )
-		DelayAndDraw(SOUND_DELAY);
+	DelaySound();
+	m_state = STATE_BONUS_COUNTDOWN; 
+}
 
-	/* -- Count the score down */
-	OBJ_LOOP(i, MAX_PLAYERS) {
-		if (!gPlayers[i]->IsValid()) {
-			continue;
-		}
-		if (i != gDisplayed) {
-			while ( gPlayers[i]->GetBonus() > 500 ) {
-				gPlayers[i]->IncrScore(500);
-				gPlayers[i]->IncrBonus(-500);
-			}
-			continue;
-		}
+void
+GamePanelDelegate::BonusTaunt()
+{
+	sound->PlaySound(gNoBonus, 5);
+	DelaySound();
+	m_state = STATE_BONUS_COUNTDOWN; 
+}
 
-		while (TheShip->GetBonus() > 0) {
-			while ( sound->Playing() )
-				DelayAndDraw(SOUND_DELAY);
+void
+GamePanelDelegate::BonusPraise()
+{
+	sound->PlaySound(gPrettyGood, 5);
+	DelaySound();
+	m_state = STATE_BONUS_COUNTDOWN; 
+}
 
-			sound->PlaySound(gBonk, 5);
-			if ( TheShip->GetBonus() >= 500 ) {
-				TheShip->IncrScore(500);
-				TheShip->IncrBonus(-500);
-			} else {
-				TheShip->IncrScore(TheShip->GetBonus());
-				TheShip->IncrBonus(-TheShip->GetBonus());
-			}
+void
+GamePanelDelegate::BonusCountdown()
+{
+	UIPanel *panel;
+	UIElement *bonus;
+	UIElement *score;
+	char numbuf[128];
 
-			if (bonus) {
-				SDL_snprintf(numbuf, sizeof(numbuf), "%-5.1d", TheShip->GetBonus());
-				bonus->SetText(numbuf);
-			}
-			if (score) {
-				SDL_snprintf(numbuf, sizeof(numbuf), "%-5.1d", TheShip->GetScore());
-				score->SetText(numbuf);
-			}
+	panel = ui->GetPanel(PANEL_BONUS);
+	if (TheShip->GetBonusMult() != 1) {
+		bonus = panel->GetElement<UIElement>("multiplied_bonus");
+	}
+	else {
+		bonus = panel->GetElement<UIElement>("bonus");
+	}
+	score = panel->GetElement<UIElement>("score");
+
+	if (TheShip->GetBonus() > 0) {
+		sound->PlaySound(gBonk, 5);
+
+		if ( TheShip->GetBonus() >= 500 ) {
+			TheShip->IncrScore(500);
+			TheShip->IncrBonus(-500);
+		} else {
+			TheShip->IncrScore(TheShip->GetBonus());
+			TheShip->IncrBonus(-TheShip->GetBonus());
+		}
 
-			ui->Draw();
+		if (bonus) {
+			SDL_snprintf(numbuf, sizeof(numbuf), "%-5.1d", TheShip->GetBonus());
+			bonus->SetText(numbuf);
 		}
+		if (score) {
+			SDL_snprintf(numbuf, sizeof(numbuf), "%-5.1d", TheShip->GetScore());
+			score->SetText(numbuf);
+		}
+		DelaySound();
+		return;
 	}
-	while ( sound->Playing() )
-		DelayAndDraw(SOUND_DELAY);
-	HandleEvents(10);
+	m_state = STATE_BONUS_NEXT_WAVE;
+}
+
+void
+GamePanelDelegate::BonusNextWave()
+{
+	UIPanel *panel;
+	UIElement *label;
+	char numbuf[128];
 
 	/* -- Draw the "next wave" message */
+	panel = ui->GetPanel(PANEL_BONUS);
 	label = panel->GetElement<UIElement>("next");
 	if (label) {
 		SDL_snprintf(numbuf, sizeof(numbuf), "Prepare for Wave %d...", gWave+1);
 		label->SetText(numbuf);
 		label->Show();
 	}
-	ui->Draw();
-	HandleEvents(100);
+	DelayAndDraw(6);
+	m_state = STATE_BONUS_HIDE;
+}
 
+void
+GamePanelDelegate::BonusHide()
+{
 	ui->HidePanel(PANEL_BONUS);
 
 	gGameInfo.SetLocalState(STATE_BONUS, false);
@@ -963,7 +1091,8 @@ GamePanelDelegate::DoBonus()
 	screen->FadeOut();
 	screen->Clear();
 
-}	/* -- DoBonus */
+	m_state = STATE_START_NEXT_WAVE;
+}
 
 /* ----------------------------------------------------------------- */
 /* -- Start the next wave! */
@@ -971,9 +1100,7 @@ GamePanelDelegate::DoBonus()
 void
 GamePanelDelegate::NextWave()
 {
-	int	i, x, y;
-	int	NewRoids;
-	short	temp;
+	int i;
 
 	gEnemySprite = NULL;
 
@@ -997,8 +1124,19 @@ GamePanelDelegate::NextWave()
 		}
 	}
 
-	if (gWave != (gGameInfo.wave - 1))
+	if (gWave != (gGameInfo.wave - 1)) {
 		DoBonus();
+	} else {
+		m_state = STATE_START_NEXT_WAVE;
+	}
+}
+
+void
+GamePanelDelegate::StartNextWave()
+{
+	int	i, x, y;
+	int	NewRoids;
+	short	temp;
 
 	gWave++;
 
@@ -1094,6 +1232,8 @@ GamePanelDelegate::NextWave()
 
 	SetSteamTimelineLevelStarted(gWave);
 
+	m_state = STATE_PLAYING;
+
 }	/* -- NextWave */
 
 /* ----------------------------------------------------------------- */
diff --git a/game/game.h b/game/game.h
index b7e8417c..da5d9ebe 100644
--- a/game/game.h
+++ b/game/game.h
@@ -45,7 +45,19 @@ class GamePanelDelegate : public UIPanelDelegate
 	bool UpdateGameState();
 	void DoHousekeeping();
 	void DoBonus();
+	void ShowBonus();
+	void BonusShowValue();
+	void BonusShowMultiplier();
+	void BonusDisplayDelay();
+	void BonusDisplay();
+	void BonusCheckSound();
+	void BonusTaunt();
+	void BonusPraise();
+	void BonusCountdown();
+	void BonusNextWave();
+	void BonusHide();
 	void NextWave();
+	void StartNextWave();
 	void GameOver();
 
 protected:
@@ -68,6 +80,22 @@ class GamePanelDelegate : public UIPanelDelegate
 	UIElement *m_multiplayerColor;
 	UIElement *m_fragsLabel;
 	UIElement *m_frags;
+
+	enum {
+		STATE_PLAYING,
+		STATE_SHOW_BONUS,
+		STATE_BONUS_SHOW_VALUE,
+		STATE_BONUS_SHOW_MULTIPLIER,
+		STATE_BONUS_DISPLAY_DELAY,
+		STATE_BONUS_DISPLAY,
+		STATE_BONUS_CHECK_SOUND,
+		STATE_BONUS_TAUNT,
+		STATE_BONUS_PRAISE,
+		STATE_BONUS_COUNTDOWN,
+		STATE_BONUS_NEXT_WAVE,
+		STATE_BONUS_HIDE,
+		STATE_START_NEXT_WAVE,
+	} m_state;
 };
 
 /* ----------------------------------------------------------------- */
diff --git a/game/gameinfo.cpp b/game/gameinfo.cpp
index f5e6c3a2..7de98e1e 100644
--- a/game/gameinfo.cpp
+++ b/game/gameinfo.cpp
@@ -29,7 +29,7 @@
 
 GameInfo::GameInfo()
 {
-	localID = rand();
+	localID = SDL_rand(SDL_MAX_SINT32);
 	Reset();
 }
 
diff --git a/game/gameover.cpp b/game/gameover.cpp
index e07e247e..6229a57c 100644
--- a/game/gameover.cpp
+++ b/game/gameover.cpp
@@ -150,10 +150,6 @@ void GameOverPanelDelegate::OnHide()
 	   update UI in a future replay
 	*/
 	gGameInfo.Reset();
-
-	while ( sound->Playing() )
-		Delay(SOUND_DELAY);
-	HandleEvents(0);
 }
 
 void GameOverPanelDelegate::OnTick()
@@ -163,8 +159,9 @@ void GameOverPanelDelegate::OnTick()
 	}
 
 	/* -- Wait for the game over sound */
-	if ( sound->Playing() )
+	if (sound->Playing()) {
 		return;
+	}
 
 	if (gGameInfo.IsMultiplayer()) { /* Let them watch their ranking */
 		const Uint32 MULTIPLAYER_SHOW_TIME = 3000;
@@ -249,8 +246,6 @@ void GameOverPanelDelegate::BeginEnterName()
 	}
 	m_handleSize = (int)SDL_strlen(m_handle);
 
-	// Flush events before enabling text input
-	HandleEvents(0);
 	screen->EnableTextInput();
 }
 
@@ -264,9 +259,8 @@ void GameOverPanelDelegate::FinishEnterName()
 		gReplay.Save();
 		LoadScores();
 	}
+	m_handleLabel = nullptr;
 
 	sound->HaltSound();
 	sound->PlaySound(gGotPrize, 6);
-
-	ui->ShowPanel(PANEL_MAIN);
 }
diff --git a/game/init.cpp b/game/init.cpp
index a5a1861f..375f175a 100644
--- a/game/init.cpp
+++ b/game/init.cpp
@@ -40,15 +40,15 @@
 #define GAME_PREFS_FILE	"Maelstrom_Prefs.txt"
 
 // Global variables set in this file...
-Prefs    *prefs = NULL;
-Sound    *sound = NULL;
-FontServ *fontserv = NULL;
-FrameBuf *screen = NULL;
-UIManager *ui = NULL;
+Prefs    *prefs = nullptr;
+Sound    *sound = nullptr;
+FontServ *fontserv = nullptr;
+FrameBuf *screen = nullptr;
+UIManager *ui = nullptr;
 
 array<Resolution> gResolutions;
 int	gResolutionIndex;
-char   *gReplayFile;
+char   *gReplayFile = nullptr;
 Sint32	gLastHigh;
 Uint64	gLastDrawn;
 int     gNumSprites;
@@ -76,8 +76,42 @@ UITexture *gAutoFireIcon, *gAirBrakesIcon, *gMult2Icon, *gMult3Icon;
 UITexture *gMult4Icon, *gMult5Icon, *gLuckOfTheIrishIcon, *gLongFireIcon;
 UITexture *gTripleFireIcon, *gShieldIcon;
 
+enum LoadingStage
+{
+	LOAD_STAGE_STARTING,
+	LOAD_STAGE_BLITS1,
+	LOAD_STAGE_BLITS2,
+	LOAD_STAGE_BLITS3,
+	LOAD_STAGE_BLITS4,
+	LOAD_STAGE_BLITS5,
+	LOAD_STAGE_BLITS6,
+	LOAD_STAGE_BLITS7,
+	LOAD_STAGE_BLITS8,
+	LOAD_STAGE_BLITS9,
+	LOAD_STAGE_BLITS10,
+	LOAD_STAGE_BLITS11,
+	LOAD_STAGE_BLITS12,
+	LOAD_STAGE_BLITS13,
+	LOAD_STAGE_BLITS14,
+	LOAD_STAGE_BLITS15,
+	LOAD_STAGE_BLITS16,
+	LOAD_STAGE_BLITS17,
+	LOAD_STAGE_BLITS18,
+	LOAD_STAGE_BLITS19,
+	LOAD_STAGE_BLITS20,
+	LOAD_STAGE_BLITS21,
+	LOAD_STAGE_BLITS22,
+	LOAD_STAGE_BLITS23,
+	LOAD_STAGE_BLITS24,
+	LOAD_STAGE_BLITS25,
+	LOAD_STAGE_SHOTS,
+	LOAD_STAGE_SPRITES,
+	LOAD_STAGE_COMPLETE
+};
+static int gLoadingStage = LOAD_STAGE_STARTING;
+
 // Local functions used in this file.
-static void DrawLoadBar(int stage);
+static void DrawLoadBar();
 static int InitSprites(void);
 static int LoadBlits(void);
 static int LoadCICNS(void);
@@ -106,7 +140,7 @@ static bool InitResolutions(int &w, int &h)
 		if (!attr) {
 			error("Resolution missing 'w' attribute in resolutions.xml\n");
 			SDL_free(buffer);
-			return false;;
+			return false;
 		}
 		resolution.w = SDL_atoi(attr->value());
 
@@ -114,7 +148,7 @@ static bool InitResolutions(int &w, int &h)
 		if (!attr) {
 			error("Resolution missing 'h' attribute in resolutions.xml\n");
 			SDL_free(buffer);
-			return false;;
+			return false;
 		}
 		resolution.h = SDL_atoi(attr->value());
 
@@ -122,7 +156,7 @@ static bool InitResolutions(int &w, int &h)
 		if (!attr) {
 			error("Resolution missing 'path_suffix' attribute in resolutions.xml\n");
 			SDL_free(buffer);
-			return false;;
+			return false;
 		}
 		SDL_strlcpy(resolution.path_suffix, attr->value(), sizeof(resolution.path_suffix));
 
@@ -130,7 +164,7 @@ static bool InitResolutions(int &w, int &h)
 		if (!attr) {
 			error("Resolution missing 'file_suffix' attribute in resolutions.xml\n");
 			SDL_free(buffer);
-			return false;;
+			return false;
 		}
 		SDL_strlcpy(resolution.file_suffix, attr->value(), sizeof(resolution.file_suffix));
 
@@ -138,7 +172,7 @@ static bool InitResolutions(int &w, int &h)
 		if (!attr) {
 			error("Resolution missing 'scale' attribute in resolutions.xml\n");
 			SDL_free(buffer);
-			return false;;
+			return false;
 		}
 		int numerator, denominator;
 		SDL_sscanf(attr->value(), "%d/%d", &numerator, &denominator);
@@ -159,8 +193,9 @@ static bool InitResolutions(int &w, int &h)
 
 #define	MAX_BAR	26
 
-static void DrawLoadBar(int stage)
+static void DrawLoadBar()
 {
+	static int stage = 1;
 	UIPanel *panel;
 	UIElement *progress = NULL;
 	int fact;
@@ -174,7 +209,7 @@ static void DrawLoadBar(int stage)
 		fact = (FULL_WIDTH * stage) / MAX_BAR;
 		progress->SetWidth(fact);
 	}
-	ui->Draw();
+	++stage;
 }	/* -- DrawLoadBar */
 
 
@@ -714,10 +749,6 @@ void CleanUp(void)
 	FreeScores();
 	SaveControls();
 	QuitPlayerControls();
-	if ( gReplayFile ) {
-		SDL_free( gReplayFile );
-		gReplayFile = NULL;
-	}
 	if ( ui ) {
 		delete ui;
 		ui = NULL;
@@ -746,20 +777,21 @@ void CleanUp(void)
 
 /* ----------------------------------------------------------------- */
 /* -- Perform some initializations and report failure if we choke */
-int DoInitializations(int window_width, int window_height, Uint32 window_flags)
+bool StartInitialization(int window_width, int window_height, Uint32 window_flags)
 {
 	int w, h;
 	SDL_Surface *icon = nullptr;
 
+	gInitializing = true;
+
 	if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD)) {
 		error("Couldn't initialize SDL: %s\n", SDL_GetError());
-		return(-1);
-	}
-	if (!NET_Init()) {
-		error("Couldn't initialize SDL_net: %s\n", SDL_GetError());
-		return(-1);
+		return false;
 	}
 
+	// It's okay if this fails, we'll disable multiplayer in that case
+	gNetworkAvailable = NET_Init();
+
 	// -- Initialize some variables
 	gLastHigh = -1;
 
@@ -779,22 +811,19 @@ int DoInitializations(int window_width, int window_height, Uint32 window_flags)
 	icon = SDL_LoadSurface_IO(OpenRead("icon.png"), true);
 	if ( icon == NULL ) {
 		error("Fatal: Couldn't load icon: %s\n", SDL_GetError());
-		return(-1);
+		return false;
 	}
 #endif
 
-	/* We will handle drag and drop events */
-	SDL_SetEventEnabled(SDL_EVENT_DROP_FILE, true);
-
 	/* Initialize the screen */
 	screen = new FrameBuf;
 	if (!InitResolutions(w, h)) {
-		return(-1);
+		return false;
 	}
 	window_flags |= SDL_WINDOW_HIDDEN;
 	if (screen->Init(w, h, window_flags, "Mael

(Patch may be truncated, please check the link at the top of this post.)