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)