Maelstrom: Added Emscripten persistent filesystem support

From 38a76abfed9d4b4c0194f50e84cd9eef83be0ca6 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 19 Mar 2026 22:45:15 -0700
Subject: [PATCH] Added Emscripten persistent filesystem support

---
 CMakeLists.txt           |  2 +-
 game/Maelstrom_Globals.h |  3 ++
 game/init.cpp            | 44 +++++++++++++++++++++--------
 game/main.cpp            |  7 -----
 utils/files.c            | 61 ++++++++++++++++++++++++++++++++++++++++
 utils/files.h            |  1 +
 utils/prefs.cpp          |  3 ++
 7 files changed, 101 insertions(+), 20 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 301e5ee5..995bdfac 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -197,7 +197,7 @@ target_link_libraries(Maelstrom PRIVATE SDL3_net::SDL3_net)
 
 if(EMSCRIPTEN)
     # Increase the default stack size
-    target_link_options(Maelstrom PRIVATE -sSTACK_SIZE=1048576)
+    target_link_options(Maelstrom PRIVATE -sSTACK_SIZE=1048576 -lidbfs.js)
 
     # on the web, we have to put the files inside of the webassembly
     # somewhat unintuitively, this is done via a linker argument.
diff --git a/game/Maelstrom_Globals.h b/game/Maelstrom_Globals.h
index 69b239e0..51ede40d 100644
--- a/game/Maelstrom_Globals.h
+++ b/game/Maelstrom_Globals.h
@@ -43,6 +43,9 @@
 #include "gameinfo.h"
 #include "steam.h"
 
+#define MAELSTROM_ORGANIZATION	"Ambrosia Software"
+#define MAELSTROM_NAME		"Maelstrom"
+
 // Preferences keys
 #define PREFERENCES_RESOLUTION "Resolution"
 #define PREFERENCES_HANDLE "Handle"
diff --git a/game/init.cpp b/game/init.cpp
index 54cd2577..23c7cca6 100644
--- a/game/init.cpp
+++ b/game/init.cpp
@@ -107,6 +107,7 @@ enum LoadingStage
 	LOAD_STAGE_BLITS25,
 	LOAD_STAGE_SHOTS,
 	LOAD_STAGE_SPRITES,
+	LOAD_STAGE_FILESYSTEM,
 	LOAD_STAGE_COMPLETE
 };
 static int gLoadingStage = LOAD_STAGE_WAITING;
@@ -794,18 +795,12 @@ bool StartInitialization(int window_width, int window_height, Uint32 window_flag
 	gNetworkAvailable = NET_Init();
 
 	// -- Initialize some variables
-	gLastHigh = -1;
-
-	// -- Create our scores file
-	LoadScores();
-
-	// -- Load our preferences files
 	prefs = new Prefs(GAME_PREFS_FILE);
-	prefs->Load();
+	gLastHigh = -1;
 
-	// -- Load our controls
-	LoadControls();
-	InitPlayerControls();
+	if (!InitFilesystem(MAELSTROM_ORGANIZATION, MAELSTROM_NAME)) {
+		return false;
+	}
 
 	/* Load the Maelstrom icon */
 #if !defined(SDL_PLATFORM_APPLE) || defined(ENABLE_STEAM)
@@ -833,14 +828,14 @@ bool StartInitialization(int window_width, int window_height, Uint32 window_flag
 
 	/* Load the Font Server and fonts */
 	fontserv = new FontServ(screen, "Maelstrom Fonts");
-	if ( fontserv->Error() ) {
+	if (fontserv->Error()) {
 		error("Fatal: %s\n", fontserv->Error());
 		return false;
 	}
 
 	/* Load the Sound Server and initialize sound */
 	sound = new Sound("Maelstrom Sounds", gSoundLevel);
-	if ( sound->Error() ) {
+	if (sound->Error()) {
 		error("Fatal: %s\n", sound->Error());
 		return false;
 	}
@@ -882,6 +877,8 @@ bool StartInitialization(int window_width, int window_height, Uint32 window_flag
 
 bool ContinueInitialization()
 {
+	bool failed;
+
 	switch (gLoadingStage) {
 	case LOAD_STAGE_STARTING:
 		/* -- Load in the prize CICN's */
@@ -944,6 +941,29 @@ bool ContinueInitialization()
 			return false;
 		}
 
+		gLoadingStage = LOAD_STAGE_FILESYSTEM;
+
+		// Fallthrough...
+		//break;
+
+	case LOAD_STAGE_FILESYSTEM:
+		if (!FilesystemReady(&failed)) {
+			if (failed) {
+				return false;
+			}
+			break;
+		}
+
+		// -- Create our scores file
+		LoadScores();
+
+		// -- Load our preferences files
+		prefs->Load();
+
+		// -- Load our controls
+		LoadControls();
+		InitPlayerControls();
+
 		gLoadingStage = LOAD_STAGE_COMPLETE;
 
 		// Fallthrough...
diff --git a/game/main.cpp b/game/main.cpp
index 380d33d4..e990095b 100644
--- a/game/main.cpp
+++ b/game/main.cpp
@@ -49,9 +49,6 @@
 #include "../screenlib/UIElementCheckbox.h"
 #include "../screenlib/UIElementEditbox.h"
 
-#define MAELSTROM_ORGANIZATION	"Ambrosia Software"
-#define MAELSTROM_NAME		"Maelstrom"
-
 static const char *Version =
 "Maelstrom v1.4.3 (GPL version 4.0.0) -- 10/08/2011 by Sam Lantinga\n";
 
@@ -161,10 +158,6 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
 	/* Initializing Steam can set up environment variables, so do this first */
 	InitSteam();
 
-	if ( !InitFilesystem(MAELSTROM_ORGANIZATION, MAELSTROM_NAME) ) {
-		return SDL_APP_FAILURE;
-	}
-
 	/* Seed the random number generator */
 	SeedRandom(0L);
 
diff --git a/utils/files.c b/utils/files.c
index 1f057ff7..25ad7e44 100644
--- a/utils/files.c
+++ b/utils/files.c
@@ -20,6 +20,10 @@
 
 #include "files.h"
 
+#ifdef SDL_PLATFORM_EMSCRIPTEN
+#include <emscripten/emscripten.h>
+#endif
+
 #ifndef PATH_MAX
 #define PATH_MAX    256
 #endif
@@ -35,6 +39,29 @@ bool InitFilesystem(const char *org, const char *app)
 	storage_org = org;
 	storage_app = app;
 
+#ifdef SDL_PLATFORM_EMSCRIPTEN
+	char *prefpath = SDL_GetPrefPath(org, app);
+	if (prefpath) {
+		MAIN_THREAD_EM_ASM({
+			const prefpath = UTF8ToString($0);
+			FS.mkdirTree(prefpath);
+			FS.mount(IDBFS, {}, prefpath);
+			filesystem_ready = 0;
+			FS.syncfs(true, function(err) {
+				if (err) {
+					console.log("Couldn't mount " + prefpath + ": " + err);
+					filesystem_ready = -1;
+				} else {
+					//console.log("Filesystem ready");
+					filesystem_ready = 1;
+				}
+			});
+		}, prefpath);
+
+		SDL_free(prefpath);
+	}
+#endif // SDL_PLATFORM_EMSCRIPTEN
+
 	if (env) {
 		SDL_strlcpy(datapath, env, sizeof(datapath));
 		return true;
@@ -69,6 +96,25 @@ bool InitFilesystem(const char *org, const char *app)
 #endif // MAELSTROM_DATA
 }
 
+bool FilesystemReady(bool *failed)
+{
+	*failed = false;
+
+#ifdef SDL_PLATFORM_EMSCRIPTEN
+	int result = MAIN_THREAD_EM_ASM_INT({ return filesystem_ready; });
+	switch (result) {
+	case -1:
+		*failed = true;
+		return false;
+	case 0:
+		return false;
+	default:
+		return true;
+	}
+#endif
+	return true;
+}
+
 SDL_IOStream *OpenRead(const char *file)
 {
 	char path[PATH_MAX];
@@ -185,5 +231,20 @@ bool SaveUserFile(const char *file, SDL_IOStream *src)
 		result = false;
 	}
 	SDL_CloseIO(src);
+
+#ifdef SDL_PLATFORM_EMSCRIPTEN
+	if (result) {
+		MAIN_THREAD_EM_ASM({
+			FS.syncfs(false, function(err) {
+				if (err) {
+					console.log("Couldn't save file: " + err);
+				} else {
+					//console.log("File saved!");
+				}
+			});
+		});
+	}
+#endif // SDL_PLATFORM_EMSCRIPTEN
+
 	return result;
 }
diff --git a/utils/files.h b/utils/files.h
index 8f9cbe92..d391f986 100644
--- a/utils/files.h
+++ b/utils/files.h
@@ -28,6 +28,7 @@ extern "C" {
 #endif
 
 bool InitFilesystem(const char *org, const char *app);
+bool FilesystemReady(bool *failed);
 
 SDL_IOStream *OpenRead(const char *file);
 
diff --git a/utils/prefs.cpp b/utils/prefs.cpp
index 8f5de9ff..3d686c51 100644
--- a/utils/prefs.cpp
+++ b/utils/prefs.cpp
@@ -163,6 +163,9 @@ Prefs::SetString(const char *key, const char *value, bool dirty)
 
 	if (dirty) {
 		m_dirty = true;
+
+		// Save immediately, in case we crash or are unloaded
+		Save();
 	}
 }