--- a/src/MacSrc/Modding.c 2026-07-19 13:11:51.000000000 +0200 +++ b/src/MacSrc/Modding.c 2026-09-27 19:44:24.718059783 +0200 @@ -27,6 +27,11 @@ char *modding_additional_files[MAX_MOD_FILES]; int num_mod_files; +// One filenum per modding_additional_files[] entry, or -1 if not open yet - +// lets LoadModFiles() close a mod file's previous handle before reopening it +// (see LoadModFiles() below for why it needs to be called more than once). +static int32_t modding_loaded_filenum[MAX_MOD_FILES]; + int StringEndsWith(char *src, char *dst) { char *s = strrchr(src, '.'); @@ -86,6 +91,7 @@ modding_archive_override = NULL; for (int i = 0; i < MAX_MOD_FILES; i++) { modding_additional_files[i] = NULL; + modding_loaded_filenum[i] = -1; } // Now go process args @@ -135,10 +141,24 @@ } int LoadModFiles() { + // Called again by obj_set_secondary_properties() (objsim.c) after it + // re-opens the base game's own object art files, which would otherwise + // silently reclaim any resource ID a mod file already overrode here (see + // ResReadDirEntries() in resfile.c - opening a file re-registers every + // resource ID it contains, last one in wins, with no check for whether + // an ID is already claimed). Re-registering means actually re-opening + // (there is no "just re-register" primitive), so a mod file already open + // from an earlier call is closed first - one open handle per mod file at + // a time, rather than leaking a fresh resFile[] slot on every repeat call + // (new game, load game - object_data_load() can run more than once per + // process). for (int i = 0; i < num_mod_files; i++) { if (modding_additional_files[i] != NULL) { + if (modding_loaded_filenum[i] >= 0) { + ResCloseFile(modding_loaded_filenum[i]); + } printf("Loading mod file %s\n", modding_additional_files[i]); - ResOpenFile(modding_additional_files[i]); + modding_loaded_filenum[i] = ResOpenFile(modding_additional_files[i]); } } --- a/src/GameSrc/objsim.c 2026-07-19 13:11:51.000000000 +0200 +++ b/src/GameSrc/objsim.c 2026-09-27 19:44:17.014441306 +0200 @@ -43,6 +43,7 @@ #include "gettmaps.h" #include "lvldata.h" #include "mapflags.h" +#include "Modding.h" #include "objart2.h" #include "objart3.h" #include "objects.h" @@ -1877,6 +1878,13 @@ // Warning(("Problem opening object art cache!\n")); return (ERR_NOEFFECT); } + // The two opens above re-register every resource ID objart2.res/ + // objart3.res contain, reclaiming any of them a mod file already + // overrode (see LoadModFiles() in Modding.c) - restore mod priority + // for the reftable reads below. load_dynamic_memory() (cybmem.c) reopens + // these same two files again later, persistently, for actual rendering - + // restoring priority there too. + LoadModFiles(); // Now go and set specific computed data, like frame counts for (i = 0; i < NUM_CRITTER; i++) { --- a/src/GameSrc/cybmem.c 2026-07-19 13:11:51.000000000 +0200 +++ b/src/GameSrc/cybmem.c 2026-09-27 19:44:24.718059783 +0200 @@ -33,6 +33,7 @@ #include "textmaps.h" #include "objcrit.h" #include "dynmem.h" +#include "Modding.h" #include "Shock.h" #include "sideicon.h" #include "criterr.h" @@ -120,6 +121,15 @@ critical_error(CRITERR_RES | 8); } + // The four opens above re-register every resource ID their files + // contain, reclaiming any of them a mod file already overrode (see + // LoadModFiles() in Modding.c) - these are the persistent handles + // actual gameplay renders from (unlike obj_set_secondary_properties()'s + // own transient open/close of objart2.res/objart3.res for reading + // frame-count metadata), so this is the last point mod priority needs + // restoring before rendering. + LoadModFiles(); + loadcount |= mask; }