Maelstrom: Added support for an addon override directory

From 45ccdfaa4607f6845947b76c37ba30c5a5c4a955 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 31 Mar 2026 08:46:56 -0700
Subject: [PATCH] Added support for an addon override directory

---
 README.md       |  8 +++---
 game/init.cpp   | 13 +++++++--
 game/replay.cpp | 27 ++++++++++++++----
 game/replay.h   |  9 ++++--
 utils/files.c   | 74 +++++++++++++++++++++++++++++++++++++++++++------
 5 files changed, 108 insertions(+), 23 deletions(-)

diff --git a/README.md b/README.md
index c575fe9a..7962c0f5 100644
--- a/README.md
+++ b/README.md
@@ -49,12 +49,12 @@ The classic easter eggs from the original game are all there, and it's up to you
 
 ### Addons
 
-The art and sounds for the game are in the Data directory and can be freely modified for your own use.
+The art and sounds for the game are in the Data directory and can be freely modified for your own use. If you create a directory "addon" next to the Data directory, files in there will override the base game.
 
-If you have access to the original sound and sprite packs for Maelstrom, you can build Maelstrom from source and use the included tool `macres` to unpack them into the Data directory to change the art and sounds for the game:
+If you have access to the original sound and sprite packs for Maelstrom, you can build Maelstrom from source and use the included tool `macres` to unpack them into the addon directory to change the art and sounds for the game:

-macres --export ‘%Maelstrom Sprites’ Data
-macres --export ‘%Maelstrom Sounds’ Data
+macres --export ‘%Maelstrom Sprites’ addon
+macres --export ‘%Maelstrom Sounds’ addon


If you play network multiplayer, all players must have the same set of sprites, otherwise the games will get out of sync.
diff --git a/game/init.cpp b/game/init.cpp
index f51b32e4..88e339b3 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_SCORES,
	LOAD_STAGE_COMPLETE
};
static int gLoadingStage = LOAD_STAGE_WAITING;
@@ -827,9 +828,6 @@ bool StartInitialization(int window_width, int window_height, Uint32 window_flag
	// -- Initialize some variables
	gLastHigh = -1;

-	// -- Create our scores file
-	LoadScores();
-
	// -- Load our preferences files
	prefs = new Prefs(GAME_PREFS_FILE);
	prefs->Load();
@@ -971,6 +969,15 @@ bool ContinueInitialization()
			return false;
		}

+		gLoadingStage = LOAD_STAGE_SCORES;
+
+		// Fallthrough...
+		//break;
+
+	case LOAD_STAGE_SCORES:
+		// -- Create our scores file
+		LoadScores();
+
		gLoadingStage = LOAD_STAGE_COMPLETE;

		// Fallthrough...
diff --git a/game/replay.cpp b/game/replay.cpp
index c5e5a656..96d36a85 100644
--- a/game/replay.cpp
+++ b/game/replay.cpp
@@ -94,8 +94,8 @@ Replay::Load(const char *file, bool headerOnly)
		SDL_Log("Couldn't read data: %s", SDL_GetError());
		goto done;
	}
-	if (version != REPLAY_VERSION) {
-		SDL_Log("Unsupported version %d, expected %d", version, REPLAY_VERSION);
+	if (version != HEADER_VERSION) {
+		SDL_Log("Unsupported version %d, expected %d", version, HEADER_VERSION);
		goto done;
	}
	SDL_ReadU32LE(fp, &m_frameCount);
@@ -121,6 +121,22 @@ Replay::Load(const char *file, bool headerOnly)
	}

	if (!headerOnly) {
+		if (!SDL_ReadIO(fp, &version, 1)) {
+			SDL_Log("Couldn't read data: %s", SDL_GetError());
+			goto done;
+		}
+		if (version != REPLAY_VERSION) {
+			SDL_Log("Unsupported data version %d, expected %d", version, REPLAY_VERSION);
+			goto done;
+		}
+
+		Uint32 spriteCRC = 0;
+		SDL_ReadU32LE(fp, &spriteCRC);
+		if (spriteCRC != gSpriteCRC) {
+			SDL_Log("Game uses a different sprite pack, ignoring");
+			goto done;
+		}
+
		SDL_ReadU32LE(fp, &size);
		m_data.Reset();
		m_data.Grow(size);
@@ -153,7 +169,6 @@ Replay::Save(const char *file)
{
	char path[1024];
	SDL_IOStream *fp = nullptr;
-	Uint8 version;
	DynamicPacket data;
	uLongf destLen;
	bool result = false;
@@ -164,8 +179,7 @@ Replay::Save(const char *file)
		goto done;
	}

-	version = REPLAY_VERSION;
-	SDL_WriteU8(fp, version);
+	SDL_WriteU8(fp, HEADER_VERSION);
	SDL_WriteU32LE(fp, m_frameCount);
	SDL_WriteU8(fp, m_finalPlayer);
	SDL_WriteU8(fp, m_finalWave);
@@ -183,6 +197,9 @@ Replay::Save(const char *file)
		goto done;
	}

+	SDL_WriteU8(fp, REPLAY_VERSION);
+	SDL_WriteU32LE(fp, gSpriteCRC);
+
	destLen = compressBound(m_data.Size());
	data.Reset();
	data.Grow(destLen);
diff --git a/game/replay.h b/game/replay.h
index 5c06b32a..68e45008 100644
--- a/game/replay.h
+++ b/game/replay.h
@@ -25,13 +25,16 @@
#include "gameinfo.h"
#include "packet.h"

+// You should increment this every time the replay header or game info changes
+#define HEADER_VERSION	2
+
// You should increment this every time game code changes in ways that would
// affect the random number sequence for a game.
//
-// Examples of this would be changing the game play area, game info structure,
-// game logic, etc.
+// Examples of this would be changing the game play area, game logic, etc.
//
-#define REPLAY_VERSION	2
+#define REPLAY_VERSION	1
+
#define REPLAY_DIRECTORY "Games"
#define REPLAY_FILETYPE "mreplay"
#define LAST_REPLAY	"LastGame." REPLAY_FILETYPE
diff --git a/utils/files.c b/utils/files.c
index 1f057ff7..54132dab 100644
--- a/utils/files.c
+++ b/utils/files.c
@@ -27,14 +27,12 @@
static const char *storage_org;
static const char *storage_app;
static char datapath[PATH_MAX];
+static char override[PATH_MAX];

-bool InitFilesystem(const char *org, const char *app)
+bool InitDataPath(void)
{
	const char *env = SDL_getenv("MAELSTROM_DATA");

-	storage_org = org;
-	storage_app = app;
-
	if (env) {
		SDL_strlcpy(datapath, env, sizeof(datapath));
		return true;
@@ -69,20 +67,80 @@ bool InitFilesystem(const char *org, const char *app)
#endif // MAELSTROM_DATA
}

+void InitOverridePath(void)
+{
+	const char *env = SDL_getenv("MAELSTROM_DATA_OVERRIDE");
+
+	if (env) {
+		SDL_strlcpy(override, env, sizeof(override));
+		return;
+	}
+
+#ifdef MAELSTROM_DATA_OVERRIDE
+	SDL_strlcpy(override, MAELSTROM_DATA_OVERRIDE, sizeof(override));
+#else
+	SDL_snprintf(override, sizeof(override), "%s../addon/", datapath);
+#endif
+
+	if (!SDL_GetPathInfo(override, NULL)) {
+		override[0] = '\0';
+	}
+}
+
+bool InitFilesystem(const char *org, const char *app)
+{
+	storage_org = org;
+	storage_app = app;
+
+	if (!InitDataPath()) {
+		return false;
+	}
+
+	// Make sure the datapath ends in '/'
+	if (datapath[SDL_strlen(datapath) - 1] != '/') {
+		SDL_strlcat(datapath, "/", sizeof(datapath));
+	}
+
+	InitOverridePath();
+
+	// Make sure the override ends in '/'
+	if (override[SDL_strlen(override) - 1] != '/') {
+		SDL_strlcat(override, "/", sizeof(override));
+	}
+
+	return true;
+}
+
SDL_IOStream *OpenRead(const char *file)
{
+	SDL_IOStream *stream = NULL;
	char path[PATH_MAX];

-	SDL_snprintf(path, sizeof(path), "%s%s", datapath, file);
-	return SDL_IOFromFile(path, "rb");
+	if (*override) {
+		SDL_snprintf(path, sizeof(path), "%s%s", override, file);
+		stream = SDL_IOFromFile(path, "rb");
+	}
+	if (!stream) {
+		SDL_snprintf(path, sizeof(path), "%s%s", datapath, file);
+		stream = SDL_IOFromFile(path, "rb");
+	}
+	return stream;
}

char *LoadFile(const char *file)
{
+	char *data = NULL;
	char path[PATH_MAX];

-	SDL_snprintf(path, sizeof(path), "%s%s", datapath, file);
-	return (char *)SDL_LoadFile(path, NULL);
+	if (*override) {
+		SDL_snprintf(path, sizeof(path), "%s%s", override, file);
+		data = (char *)SDL_LoadFile(path, NULL);
+	}
+	if (!data) {
+		SDL_snprintf(path, sizeof(path), "%s%s", datapath, file);
+		data = (char *)SDL_LoadFile(path, NULL);
+	}
+	return data;
}

SDL_Storage *OpenUserStorage(void)