]> git.rm.cloudns.org Git - xonotic/darkplaces.git/commitdiff
fs: recognise mods by -game cmdline args, deduplicate gamedirs, refactoring
authorbones_was_here <bones_was_here@xonotic.au>
Sun, 28 Apr 2024 03:03:12 +0000 (13:03 +1000)
committerbones_was_here <bones_was_here@xonotic.au>
Thu, 2 May 2024 21:00:50 +0000 (07:00 +1000)
Players often start mods using -game even when they have their own
cmdline option, and modders often provide scripts which use -game.

This was already implemented but only for the `gamedir` command.
Refactoring was required to avoid chicken vs egg problems when doing it
with cmdline gamedirs too.

Fixes a bug: the first gamedir (in the cmdline or `gamedir` args) was
used to identify the mod but it needs to be the last one that matches
because the last gamedir is the primary (first in the search path, and
where files are saved).

Includes supported game and mod directories in the main gamedir list for
interface consistency, less special cases, and to support deduplication.
Refactoring was required in the menu modlist where there was also an
opportunity to simplify.

Fixes inability to change from a supported mod (eg rogue) back to id1.

Signed-off-by: bones_was_here <bones_was_here@xonotic.au>
cl_parse.c
com_game.c
com_game.h
fs.c
fs.h
menu.c

index dbeda3864f362d9fc8721b9b4822841b138203ab..b5d0cd4fbec8f2379b5ce91525c0e4af309cca39 100644 (file)
@@ -1677,7 +1677,7 @@ CL_ParseServerInfo
 */
 static void CL_ParseServerInfo (void)
 {
-       char *str;
+       const char *str;
        int i;
        protocolversion_t protocol;
        int nummodels, numsounds;
@@ -1730,16 +1730,13 @@ static void CL_ParseServerInfo (void)
 
        if (protocol == PROTOCOL_QUAKEWORLD)
        {
-               char gamedir[1][MAX_QPATH];
-
                cl.qw_servercount = MSG_ReadLong(&cl_message);
 
                str = MSG_ReadString(&cl_message, cl_readstring, sizeof(cl_readstring));
                Con_Printf("server gamedir is %s\n", str);
-               dp_strlcpy(gamedir[0], str, sizeof(gamedir[0]));
 
                // change gamedir if needed
-               if (!FS_ChangeGameDirs(1, gamedir, true, false))
+               if (!FS_ChangeGameDirs(1, &str, false))
                        Host_Error("CL_ParseServerInfo: unable to switch to server specified gamedir");
 
                cl.gametype = GAME_DEATHMATCH;
index 0dcde1ea082ec5a55cadd84310df9d0e8d1a95ef..b6a215647534ee8cedc4f916b1d28788e46d1f2f 100644 (file)
@@ -93,7 +93,7 @@ void COM_InitGameType (void)
 {
        char name [MAX_OSPATH];
        int i;
-       int index = 0;
+       int index = GAME_NORMAL;
 
 #ifdef FORCEGAME
        COM_ToLowerString(FORCEGAME, name, sizeof (name));
@@ -116,10 +116,11 @@ void COM_InitGameType (void)
        COM_SetGameType(index);
 }
 
-void COM_ChangeGameTypeForGameDirs(void)
+int COM_ChangeGameTypeForGameDirs(unsigned numgamedirs, const char *gamedirs[], qbool failmissing, qbool init)
 {
        unsigned i, gamemode_count = sizeof(gamemode_info) / sizeof(gamemode_info[0]);
-       int index = -1;
+       int j, index = -1;
+       addgamedirs_t ret = GAMEDIRS_SUCCESS;
        // this will not not change the gamegroup
 
        // first check if a base game (single gamedir) matches
@@ -131,38 +132,62 @@ void COM_ChangeGameTypeForGameDirs(void)
                        break;
                }
        }
-       // now that we have a base game, see if there is a derivative game matching the startup one (two gamedirs)
-       // bones_was_here: this prevents a Quake expansion (eg Rogue) getting switched to Quake,
-       // and its gamedirname2 (eg "rogue") being lost from the search path, when adding a miscellaneous gamedir.
-       for (i = 0; i < gamemode_count; i++)
+       if (index < 0)
+               Sys_Error("BUG: failed to find the base game!");
+
+       // See if there is a derivative game matching the startup one (two gamedirs),
+       // this prevents a Quake expansion (eg Rogue) getting switched to Quake during startup,
+       // not used when changing gamedirs later so that it's possible to change from the startup mod to the base one.
+       if (init)
        {
-               if (gamemode_info[i].group == com_startupgamegroup && (gamemode_info[i].gamedirname2 && gamemode_info[i].gamedirname2[0]) && gamemode_info[i].mode == com_startupgamemode)
+               for (i = 0; i < gamemode_count; i++)
                {
-                       index = i;
-                       break;
+                       if (gamemode_info[i].group == com_startupgamegroup && (gamemode_info[i].gamedirname2 && gamemode_info[i].gamedirname2[0]) && gamemode_info[i].mode == com_startupgamemode)
+                       {
+                               index = i;
+                               break;
+                       }
                }
        }
-       // also see if the first gamedir (from -game parm or gamedir command) matches a derivative game (two/three gamedirs)
-       if (fs_numgamedirs)
+
+       // See if the base game or mod can be identified by a gamedir,
+       // if more than one matches the last is used because the last gamedir is the primary.
+       for (j = numgamedirs - 1; j >= 0; --j)
        {
                for (i = 0; i < gamemode_count; i++)
                {
-                       if (gamemode_info[i].group == com_startupgamegroup && (gamemode_info[i].gamedirname2 && gamemode_info[i].gamedirname2[0]) && !strcasecmp(fs_gamedirs[0], gamemode_info[i].gamedirname2))
+                       if (gamemode_info[i].group == com_startupgamegroup)
+                       if ((gamemode_info[i].gamedirname2 && gamemode_info[i].gamedirname2[0] && !strcasecmp(gamedirs[j], gamemode_info[i].gamedirname2))
+                       || (!gamemode_info[i].gamedirname2 && !strcasecmp(gamedirs[j], gamemode_info[i].gamedirname1)))
                        {
                                index = i;
-                               break;
+                               goto double_break;
                        }
                }
        }
+double_break:
+
        // we now have a good guess at which game this is meant to be...
-       if (index >= 0 && gamemode != gamemode_info[index].mode)
-               COM_SetGameType(index);
+       COM_SetGameType(index);
+
+       ret = FS_SetGameDirs(numgamedirs, gamedirs, failmissing, !init);
+       if (ret == GAMEDIRS_SUCCESS)
+       {
+               Con_Printf("Game is %s using %s", gamename, fs_numgamedirs > 1 ? "gamedirs" : "gamedir");
+               for (j = 0; j < fs_numgamedirs; ++j)
+                       Con_Printf(" %s%s", (strcasecmp(fs_gamedirs[j], gamedirname1) && (!gamedirname2 || strcasecmp(fs_gamedirs[j], gamedirname2))) ? "^7" : "^9", fs_gamedirs[j]);
+               Con_Printf("\n");
+
+               Con_Printf("gamename for server filtering: %s\n", gamenetworkfiltername);
+       }
+       return ret;
 }
 
 static void COM_SetGameType(int index)
 {
        static char gamenetworkfilternamebuffer[64];
-       int i, t;
+       int t;
+
        if (index < 0 || index >= (int)(sizeof (gamemode_info) / sizeof (gamemode_info[0])))
                index = 0;
        gamemode = gamemode_info[index].mode;
@@ -189,18 +214,6 @@ static void COM_SetGameType(int index)
                        gameuserdirname = sys.argv[t+1];
        }
 
-       if (gamedirname2 && gamedirname2[0])
-               Con_Printf("Game is %s using base gamedirs %s %s", gamename, gamedirname1, gamedirname2);
-       else
-               Con_Printf("Game is %s using base gamedir %s", gamename, gamedirname1);
-       for (i = 0;i < fs_numgamedirs;i++)
-       {
-               if (i == 0)
-                       Con_Printf(", with mod gamedirs");
-               Con_Printf(" %s", fs_gamedirs[i]);
-       }
-       Con_Printf("\n");
-
        if (strchr(gamenetworkfiltername, ' '))
        {
                char *s;
@@ -212,6 +225,4 @@ static void COM_SetGameType(int index)
                        *s = '_';
                gamenetworkfiltername = gamenetworkfilternamebuffer;
        }
-
-       Con_Printf("gamename for server filtering: %s\n", gamenetworkfiltername);
 }
index 4f615568c0c8ac4e5b570e3157a09d03f73ced1d..aa2cdbe6d8754bb89dd7c801f15dca1385a89526 100644 (file)
@@ -79,6 +79,6 @@ extern const char *gameuserdirname;
 extern char com_modname[MAX_OSPATH];
 
 void COM_InitGameType (void);
-void COM_ChangeGameTypeForGameDirs(void);
+int COM_ChangeGameTypeForGameDirs(unsigned numgamedirs, const char *gamedirs[], qbool failmissing, qbool init);
 
 #endif
diff --git a/fs.c b/fs.c
index e2afcf052ca4b89b8227b7fd1f4a30b277aefea3..df9cf04fdbe929b1073cd38f5da0a3ce9685c841 100644 (file)
--- a/fs.c
+++ b/fs.c
@@ -438,7 +438,7 @@ char fs_gamedir[MAX_OSPATH];
 char fs_basedir[MAX_OSPATH];
 static pack_t *fs_selfpack = NULL;
 
-// list of active game directories (empty if not running a mod)
+// list of active game directories
 int fs_numgamedirs = 0;
 char fs_gamedirs[MAX_GAMEDIRS][MAX_QPATH];
 
@@ -1544,36 +1544,17 @@ FS_Rescan
 void FS_Rescan (void)
 {
        int i;
-       qbool fs_modified = false;
-       qbool reset = false;
        char gamedirbuf[MAX_INPUTLINE];
        char vabuf[1024];
 
-       if (fs_searchpaths)
-               reset = true;
        FS_ClearSearchPath();
 
-       // automatically activate gamemode for the gamedirs specified
-       if (reset)
-               COM_ChangeGameTypeForGameDirs();
-
-       // add the game-specific paths
-       // gamedirname1 (typically id1)
-       FS_AddGameHierarchy (gamedirname1);
        // update the com_modname (used for server info)
        if (gamedirname2 && gamedirname2[0])
                dp_strlcpy(com_modname, gamedirname2, sizeof(com_modname));
        else
                dp_strlcpy(com_modname, gamedirname1, sizeof(com_modname));
 
-       // add the game-specific path, if any
-       // (only used for mission packs and the like, which should set fs_modified)
-       if (gamedirname2 && gamedirname2[0])
-       {
-               fs_modified = true;
-               FS_AddGameHierarchy (gamedirname2);
-       }
-
        // -game <gamedir>
        // Adds basedir/gamedir as an override game
        // LadyHavoc: now supports multiple -game directories
@@ -1581,7 +1562,6 @@ void FS_Rescan (void)
        *gamedirbuf = 0;
        for (i = 0;i < fs_numgamedirs;i++)
        {
-               fs_modified = true;
                FS_AddGameHierarchy (fs_gamedirs[i]);
                // update the com_modname (used server info)
                dp_strlcpy (com_modname, fs_gamedirs[i], sizeof (com_modname));
@@ -1622,7 +1602,7 @@ void FS_Rescan (void)
        case GAME_ROGUE:
                if (!registered.integer)
                {
-                       if (fs_modified)
+                       if (fs_numgamedirs > 1)
                                Con_Print("Playing shareware version, with modification.\nwarning: most mods require full quake data.\n");
                        else
                                Con_Print("Playing shareware version.\n");
@@ -1651,57 +1631,105 @@ static void FS_Rescan_f(cmd_state_t *cmd)
 
 /*
 ================
-FS_ChangeGameDirs
+FS_AddGameDirs
 ================
 */
-extern qbool vid_opened;
-qbool FS_ChangeGameDirs(int numgamedirs, char gamedirs[][MAX_QPATH], qbool complain, qbool failmissing)
+addgamedirs_t FS_SetGameDirs(int numgamedirs, const char *gamedirs[], qbool failmissing, qbool abortonfail)
 {
-       int i;
+       int i, j, k;
        const char *p;
+       const char *gamedirs_ok[MAX_GAMEDIRS + 2];
+       int numgamedirs_ok;
 
-       if (fs_numgamedirs == numgamedirs)
+       // prepend the game-specific gamedirs (the primary and search order can be overriden)
+       gamedirs_ok[0] = gamedirname1;
+       numgamedirs_ok = 1;
+       if (gamedirname2 && gamedirname2[0])
        {
-               for (i = 0;i < numgamedirs;i++)
-                       if (strcasecmp(fs_gamedirs[i], gamedirs[i]))
-                               break;
-               if (i == numgamedirs)
-                       return true; // already using this set of gamedirs, do nothing
+               gamedirs_ok[1] = gamedirname2;
+               ++numgamedirs_ok;
        }
 
-       if (numgamedirs > MAX_GAMEDIRS)
+       // check the game-specific gamedirs
+       for (i = 0; i < numgamedirs_ok; ++i)
        {
-               if (complain)
-                       Con_Printf("That is too many gamedirs (%i > %i)\n", numgamedirs, MAX_GAMEDIRS);
-               return false; // too many gamedirs
+               p = FS_CheckGameDir(gamedirs_ok[i]);
+               if(!p)
+                       Sys_Error("BUG: nasty gamedir name \"%s\" in gamemode_info", gamedirs_ok[i]);
+               if(p == fs_checkgamedir_missing && failmissing)
+               {
+                       Con_Printf(abortonfail ? CON_ERROR : CON_WARN "Base gamedir \"%s\" empty or not found!\n", gamedirs_ok[i]);
+                       if (abortonfail)
+                               return GAMEDIRS_FAILURE; // missing gamedirs
+               }
        }
 
-       for (i = 0;i < numgamedirs;i++)
+       // copy and check the user-specified gamedirs
+       for (i = 0; i < numgamedirs && (size_t)numgamedirs_ok < sizeof(gamedirs_ok) / sizeof(gamedirs_ok[0]); ++i)
        {
+               // remove any previously-added duplicate (last one wins)
+               for (j = 0; j < numgamedirs_ok; ++j)
+                       if (!strcasecmp(gamedirs_ok[j], gamedirs[i]))
+                       {
+                               --numgamedirs_ok;
+                               for (k = j; k < numgamedirs_ok; ++k)
+                                       gamedirs_ok[k] = gamedirs_ok[k + 1];
+                       }
+
                // if string is nasty, reject it
                p = FS_CheckGameDir(gamedirs[i]);
                if(!p)
                {
-                       if (complain)
-                               Con_Printf("Nasty gamedir name rejected: %s\n", gamedirs[i]);
-                       return false; // nasty gamedirs
+                       Con_Printf(abortonfail ? CON_ERROR : CON_WARN "Nasty gamedir name \"%s\" rejected\n", gamedirs[i]);
+                       if (abortonfail)
+                               return GAMEDIRS_FAILURE; // nasty gamedirs
+                       else
+                               continue;
                }
                if(p == fs_checkgamedir_missing && failmissing)
                {
-                       if (complain)
-                               Con_Printf("Gamedir missing: %s%s/\n", fs_basedir, gamedirs[i]);
-                       return false; // missing gamedirs
+                       Con_Printf(abortonfail ? CON_ERROR : CON_WARN "Gamedir \"%s\" empty or not found!\n", gamedirs[i]);
+                       if (abortonfail)
+                               return GAMEDIRS_FAILURE; // missing gamedirs
+                       else
+                               continue;
                }
+
+               gamedirs_ok[numgamedirs_ok++] = gamedirs[i];
        }
 
-       Host_SaveConfig(CONFIGFILENAME);
+       if (fs_numgamedirs == numgamedirs_ok)
+       {
+               for (i = 0;i < numgamedirs_ok;i++)
+                       if (strcasecmp(fs_gamedirs[i], gamedirs_ok[i]))
+                               break;
+               if (i == numgamedirs_ok)
+                       return GAMEDIRS_ALLGOOD; // already using this set of gamedirs, do nothing
+       }
 
-       fs_numgamedirs = numgamedirs;
-       for (i = 0;i < fs_numgamedirs;i++)
-               dp_strlcpy(fs_gamedirs[i], gamedirs[i], sizeof(fs_gamedirs[i]));
+       if (numgamedirs_ok > MAX_GAMEDIRS)
+       {
+               Con_Printf(abortonfail ? CON_ERROR : CON_WARN "That is too many gamedirs (%i > %i)\n", numgamedirs_ok, MAX_GAMEDIRS);
+               if (abortonfail)
+                       return GAMEDIRS_FAILURE; // too many gamedirs
+       }
 
-       // reinitialize filesystem to detect the new paks
-       FS_Rescan();
+       for (i = 0, fs_numgamedirs = 0; i < numgamedirs_ok && fs_numgamedirs < MAX_GAMEDIRS; ++i)
+               dp_strlcpy(fs_gamedirs[fs_numgamedirs++], gamedirs_ok[i], sizeof(fs_gamedirs[0]));
+
+       return GAMEDIRS_SUCCESS;
+}
+
+qbool FS_ChangeGameDirs(int numgamedirs, const char *gamedirs[], qbool failmissing)
+{
+       addgamedirs_t addresult = COM_ChangeGameTypeForGameDirs(numgamedirs, gamedirs, failmissing, false);
+
+       if (addresult == GAMEDIRS_ALLGOOD)
+               return true; // already using this set of gamedirs, do nothing
+       else if (addresult == GAMEDIRS_FAILURE)
+               return false;
+
+       Host_SaveConfig(CONFIGFILENAME);
 
        if (cls.demoplayback)
        {
@@ -1712,7 +1740,10 @@ qbool FS_ChangeGameDirs(int numgamedirs, char gamedirs[][MAX_QPATH], qbool compl
        // unload all sounds so they will be reloaded from the new files as needed
        S_UnloadAllSounds_f(cmd_local);
 
-       // reset everything that can be and reload configs
+       // reinitialize filesystem to detect the new paks
+       FS_Rescan();
+
+       // reload assets after the config is executed
        Cbuf_InsertText(cmd_local, "\nloadconfig\n");
 
        return true;
@@ -1727,13 +1758,13 @@ static void FS_GameDir_f(cmd_state_t *cmd)
 {
        int i;
        int numgamedirs;
-       char gamedirs[MAX_GAMEDIRS][MAX_QPATH];
+       const char *gamedirs[MAX_GAMEDIRS];
 
        if (Cmd_Argc(cmd) < 2)
        {
                Con_Printf("gamedirs active:");
                for (i = 0;i < fs_numgamedirs;i++)
-                       Con_Printf(" %s", fs_gamedirs[i]);
+                       Con_Printf(" %s%s", (strcasecmp(fs_gamedirs[i], gamedirname1) && (!gamedirname2 || strcasecmp(fs_gamedirs[i], gamedirname2))) ? "^7" : "^9", fs_gamedirs[i]);
                Con_Printf("\n");
                return;
        }
@@ -1741,24 +1772,21 @@ static void FS_GameDir_f(cmd_state_t *cmd)
        numgamedirs = Cmd_Argc(cmd) - 1;
        if (numgamedirs > MAX_GAMEDIRS)
        {
-               Con_Printf("Too many gamedirs (%i > %i)\n", numgamedirs, MAX_GAMEDIRS);
+               Con_Printf(CON_ERROR "Too many gamedirs (%i > %i)\n", numgamedirs, MAX_GAMEDIRS);
                return;
        }
 
        for (i = 0;i < numgamedirs;i++)
-               dp_strlcpy(gamedirs[i], Cmd_Argv(cmd, i+1), sizeof(gamedirs[i]));
+               gamedirs[i] = Cmd_Argv(cmd, i+1);
 
        if ((cls.state == ca_connected && !cls.demoplayback) || sv.active)
        {
                // actually, changing during game would work fine, but would be stupid
-               Con_Printf("Can not change gamedir while client is connected or server is running!\n");
+               Con_Printf(CON_ERROR "Can not change gamedir while client is connected or server is running!\n");
                return;
        }
 
-       // halt demo playback to close the file
-       CL_Disconnect();
-
-       FS_ChangeGameDirs(numgamedirs, gamedirs, true, true);
+       FS_ChangeGameDirs(numgamedirs, gamedirs, true);
 }
 
 static const char *FS_SysCheckGameDir(const char *gamedir, char *buf, size_t buflength)
@@ -2109,8 +2137,9 @@ void FS_Init_Commands(void)
 
 static void FS_Init_Dir (void)
 {
-       const char *p;
        int i;
+       int numgamedirs;
+       const char *cmdline_gamedirs[MAX_GAMEDIRS];
 
        *fs_basedir = 0;
        *fs_userdir = 0;
@@ -2234,37 +2263,21 @@ static void FS_Init_Dir (void)
 
        FS_ListGameDirs();
 
-       p = FS_CheckGameDir(gamedirname1);
-       if(!p || p == fs_checkgamedir_missing)
-               Con_Printf(CON_WARN "WARNING: base gamedir %s%s/ not found!\n", fs_basedir, gamedirname1);
-
-       if(gamedirname2)
-       {
-               p = FS_CheckGameDir(gamedirname2);
-               if(!p || p == fs_checkgamedir_missing)
-                       Con_Printf(CON_WARN "WARNING: base gamedir %s%s/ not found!\n", fs_basedir, gamedirname2);
-       }
-
        // -game <gamedir>
        // Adds basedir/gamedir as an override game
        // LadyHavoc: now supports multiple -game directories
-       for (i = 1;i < sys.argc && fs_numgamedirs < MAX_GAMEDIRS;i++)
+       // the last one is the primary (where files are saved) and is used to identify mods
+       for (i = 1, numgamedirs = 0; i < sys.argc && numgamedirs < MAX_GAMEDIRS; i++)
        {
                if (!sys.argv[i])
                        continue;
                if (!strcmp (sys.argv[i], "-game") && i < sys.argc-1)
                {
                        i++;
-                       p = FS_CheckGameDir(sys.argv[i]);
-                       if(!p)
-                               Con_Printf("WARNING: Nasty -game name rejected: %s\n", sys.argv[i]);
-                       if(p == fs_checkgamedir_missing)
-                               Con_Printf(CON_WARN "WARNING: -game %s%s/ not found!\n", fs_basedir, sys.argv[i]);
-                       // add the gamedir to the list of active gamedirs
-                       dp_strlcpy (fs_gamedirs[fs_numgamedirs], sys.argv[i], sizeof(fs_gamedirs[fs_numgamedirs]));
-                       fs_numgamedirs++;
+                       cmdline_gamedirs[numgamedirs++] = sys.argv[i];
                }
        }
+       COM_ChangeGameTypeForGameDirs(numgamedirs, cmdline_gamedirs, true, true);
 
        // generate the searchpath
        FS_Rescan();
diff --git a/fs.h b/fs.h
index d839833172cbadd9d58ea3e17abee5b458f0477c..b81d84d2d88a4ba248e976beb77c607a4ddfc4f5 100644 (file)
--- a/fs.h
+++ b/fs.h
@@ -43,7 +43,7 @@ extern char fs_basedir [MAX_OSPATH];
 extern char fs_userdir [MAX_OSPATH];
 
 // list of active game directories (empty if not running a mod)
-#define MAX_GAMEDIRS 16
+#define MAX_GAMEDIRS 17 // 16 + gamedirname1
 extern int fs_numgamedirs;
 extern char fs_gamedirs[MAX_GAMEDIRS][MAX_QPATH];
 
@@ -99,7 +99,13 @@ gamedir_t;
 extern gamedir_t *fs_all_gamedirs; // terminated by entry with empty name
 extern int fs_all_gamedirs_count;
 
-qbool FS_ChangeGameDirs(int numgamedirs, char gamedirs[][MAX_QPATH], qbool complain, qbool failmissing);
+typedef enum addgamedirs_e {
+       GAMEDIRS_ALLGOOD = -1,
+       GAMEDIRS_FAILURE = 0,
+       GAMEDIRS_SUCCESS = 1
+} addgamedirs_t;
+addgamedirs_t FS_SetGameDirs(int numgamedirs, const char *gamedirs[], qbool failmissing, qbool abortonfail);
+qbool FS_ChangeGameDirs(int numgamedirs, const char *gamedirs[], qbool failmissing);
 qbool FS_IsRegisteredQuakePack(const char *name);
 int FS_CRCFile(const char *filename, size_t *filesizepointer);
 void FS_UnloadPacks_dlcache(void);
diff --git a/menu.c b/menu.c
index 8034f2f3d54fe70184cd83bac3f151ec11a9b592..de4b100817e8d52d94c419db7b4ac77965ecf1e9 100644 (file)
--- a/menu.c
+++ b/menu.c
@@ -4514,15 +4514,13 @@ static void M_ServerList_Key(cmd_state_t *cmd, int k, int ascii)
 
 //=============================================================================
 /* MODLIST MENU */
-// same limit of mod dirs as in fs.c
-#define MODLIST_MAXDIRS 16
-static int modlist_enabled [MODLIST_MAXDIRS];  //array of indexs to modlist
+// same limit of mod dirs as in fs.c (allowing that one is used by gamedirname1)
+#define MODLIST_MAXDIRS MAX_GAMEDIRS - 1
 static int modlist_numenabled;                 //number of enabled (or in process to be..) mods
 
 typedef struct modlist_entry_s
 {
        qbool loaded;   // used to determine whether this entry is loaded and running
-       int enabled;            // index to array of modlist_enabled
 
        // name of the modification, this is displayed on the menu entry
        char name[128];
@@ -4541,16 +4539,15 @@ static void ModList_RebuildList(void)
        int i,j;
        stringlist_t list;
        const char *description;
+       int desc_len;
 
        stringlistinit(&list);
        listdirectory(&list, fs_basedir, "");
        stringlistsort(&list, true);
        modlist_count = 0;
-       modlist_numenabled = fs_numgamedirs;
+       modlist_numenabled = 0;
        for (i = 0;i < list.numstrings && modlist_count < MODLIST_TOTALSIZE;i++)
        {
-               int desc_len;
-
                // reject any dirs that are part of the base game
                if (gamedirname1 && !strcasecmp(gamedirname1, list.strings[i])) continue;
                //if (gamedirname2 && !strcasecmp(gamedirname2, list.strings[i])) continue;
@@ -4570,17 +4567,17 @@ static void ModList_RebuildList(void)
                        }
 
                dp_strlcpy (modlist[modlist_count].dir, list.strings[i], sizeof(modlist[modlist_count].dir));
-               //check currently loaded mods
+
+               // check if this mod is currently loaded
                modlist[modlist_count].loaded = false;
-               if (fs_numgamedirs)
-                       for (j = 0; j < fs_numgamedirs; j++)
-                               if (!strcasecmp(fs_gamedirs[j], modlist[modlist_count].dir))
-                               {
-                                       modlist[modlist_count].loaded = true;
-                                       modlist[modlist_count].enabled = j;
-                                       modlist_enabled[j] = modlist_count;
-                                       break;
-                               }
+               for (j = 0; j < fs_numgamedirs; j++)
+                       if (!strcasecmp(fs_gamedirs[j], modlist[modlist_count].dir))
+                       {
+                               modlist[modlist_count].loaded = true;
+                               modlist_numenabled++;
+                               break;
+                       }
+
                modlist_count ++;
        }
        stringlistfreecontents(&list);
@@ -4590,22 +4587,7 @@ static void ModList_Enable (void)
 {
        int i;
        int numgamedirs;
-       char gamedirs[MODLIST_MAXDIRS][MAX_QPATH];
-
-       // copy our mod list into an array for FS_ChangeGameDirs
-       numgamedirs = modlist_numenabled;
-       for (i = 0; i < modlist_numenabled; i++)
-               dp_strlcpy (gamedirs[i], modlist[modlist_enabled[i]].dir,sizeof (gamedirs[i]));
-
-       // this code snippet is from FS_ChangeGameDirs
-       if (fs_numgamedirs == numgamedirs)
-       {
-               for (i = 0;i < numgamedirs;i++)
-                       if (strcasecmp(fs_gamedirs[i], gamedirs[i]))
-                               break;
-               if (i == numgamedirs)
-                       return; // already using this set of gamedirs, do nothing
-       }
+       const char *gamedirs[MODLIST_MAXDIRS];
 
        // this part is basically the same as the FS_GameDir_f function
        if ((cls.state == ca_connected && !cls.demoplayback) || sv.active)
@@ -4615,7 +4597,18 @@ static void ModList_Enable (void)
                return;
        }
 
-       FS_ChangeGameDirs (modlist_numenabled, gamedirs, true, true);
+       // copy our mod list into an array for FS_ChangeGameDirs
+       for (i = 0, numgamedirs = 0; i < modlist_count && numgamedirs < MODLIST_MAXDIRS; i++)
+               if (modlist[i].loaded)
+                       gamedirs[numgamedirs++] = modlist[i].dir;
+       // allow disabling all active mods using the menu
+       if (numgamedirs == 0)
+       {
+               numgamedirs = 1;
+               gamedirs[0] = gamedirname1;
+       }
+
+       FS_ChangeGameDirs(numgamedirs, gamedirs, true);
 }
 
 void M_Menu_ModList_f(cmd_state_t *cmd)
@@ -4630,28 +4623,13 @@ void M_Menu_ModList_f(cmd_state_t *cmd)
 
 static void M_Menu_ModList_AdjustSliders (int dir)
 {
-       int i;
        S_LocalSound ("sound/misc/menu3.wav");
 
        // stop adding mods, we reach the limit
        if (!modlist[modlist_cursor].loaded && (modlist_numenabled == MODLIST_MAXDIRS)) return;
+
        modlist[modlist_cursor].loaded = !modlist[modlist_cursor].loaded;
-       if (modlist[modlist_cursor].loaded)
-       {
-               modlist[modlist_cursor].enabled = modlist_numenabled;
-               //push the value on the enabled list
-               modlist_enabled[modlist_numenabled++] = modlist_cursor;
-       }
-       else
-       {
-               //eliminate the value from the enabled list
-               for (i = modlist[modlist_cursor].enabled; i < modlist_numenabled; i++)
-               {
-                       modlist_enabled[i] = modlist_enabled[i+1];
-                       modlist[modlist_enabled[i]].enabled--;
-               }
-               modlist_numenabled--;
-       }
+       modlist_numenabled += modlist[modlist_cursor].loaded ? 1 : -1;
 }
 
 static void M_ModList_Draw (void)
@@ -4671,8 +4649,12 @@ static void M_ModList_Draw (void)
        M_PrintRed(432, 32, s_enabled);
        // Draw a list box with all enabled mods
        DrawQ_Pic(menu_x + 432, menu_y + 48, NULL, 172, 8 * modlist_numenabled, 0, 0, 0, 0.5, 0);
-       for (y = 0; y < modlist_numenabled; y++)
-               M_PrintRed(432, 48 + y * 8, modlist[modlist_enabled[y]].dir);
+       for (n = 0, y = 48; n < modlist_count; n++)
+               if (modlist[n].loaded)
+               {
+                       M_PrintRed(432, y, modlist[n].dir);
+                       y += 8;
+               }
 
        if (*cl_connect_status)
                M_Print(16, menu_height - 8, cl_connect_status);