From dca158096df47f730e2827736bf650c1ddb8fe3d Mon Sep 17 00:00:00 2001
From: Jaime Ita Passos <jp6781615@gmail.com>
Date: Mon, 22 Mar 2021 23:56:55 -0300
Subject: [PATCH 1/2] Experimental implementation

---
 src/blua/lbaselib.c    |   2 +-
 src/d_clisrv.c         |   4 +-
 src/d_main.c           |  35 ++++-
 src/d_netcmd.c         | 204 +++++++++++++++++++++++-
 src/d_netcmd.h         |  14 +-
 src/d_netfil.c         |  48 +++++-
 src/d_netfil.h         |   6 +
 src/filesrch.c         | 345 +++++++++++++++++++++++++++++++++++++++--
 src/filesrch.h         |   8 +-
 src/hardware/hw_main.c |   2 +-
 src/m_misc.c           |  19 +++
 src/m_misc.h           |   3 +
 src/p_setup.c          |  45 ++++--
 src/p_setup.h          |   1 +
 src/r_textures.c       |  17 +-
 src/r_things.c         |   1 +
 src/w_wad.c            | 337 ++++++++++++++++++++++++++++++++++++----
 src/w_wad.h            |  11 +-
 18 files changed, 1007 insertions(+), 95 deletions(-)

diff --git a/src/blua/lbaselib.c b/src/blua/lbaselib.c
index 644565c28..0fc222038 100644
--- a/src/blua/lbaselib.c
+++ b/src/blua/lbaselib.c
@@ -274,7 +274,7 @@ static int luaB_dofile (lua_State *L) {
 	UINT16 lumpnum;
 	int n = lua_gettop(L);
 
-	if (wadfiles[numwadfiles - 1]->type != RET_PK3)
+	if (!W_FileHasFolders(wadfiles[numwadfiles - 1]))
 		luaL_error(L, "dofile() only works with PK3 files");
 
 	snprintf(fullfilename, sizeof(fullfilename), "Lua/%s", filename);
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 7c2dec6a1..3dfb5cea8 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -4486,9 +4486,9 @@ static INT16 Consistancy(void)
 		{
 			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
 				continue;
-	
+
 			mo = (mobj_t *)th;
-	
+
 			if (mo->flags & (MF_SPECIAL | MF_SOLID | MF_PUSHABLE | MF_BOSS | MF_MISSILE | MF_SPRING | MF_MONITOR | MF_FIRE | MF_ENEMY | MF_PAIN | MF_STICKY))
 			{
 				ret -= mo->type;
diff --git a/src/d_main.c b/src/d_main.c
index 23a2c0133..09f678ba6 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -873,10 +873,26 @@ static void D_AddFile(char **list, const char *file)
 
 	newfile = malloc(strlen(file) + 1);
 	if (!newfile)
-	{
 		I_Error("No more free memory to AddFile %s",file);
-	}
+
 	strcpy(newfile, file);
+	list[pnumwadfiles] = newfile;
+}
+
+static void D_AddFolder(char **list, const char *file)
+{
+	size_t pnumwadfiles, len = strlen(file);
+	char *newfile;
+
+	for (pnumwadfiles = 0; list[pnumwadfiles]; pnumwadfiles++)
+		;
+
+	newfile = malloc(len + 2); // NULL terminator + path separator
+	if (!newfile)
+		I_Error("No more free memory to AddFolder %s",file);
+
+	strcpy(newfile, file);
+	strcat(newfile, PATHSEP);
 
 	list[pnumwadfiles] = newfile;
 }
@@ -1180,7 +1196,7 @@ void D_SRB2Main(void)
 	{
 		if (M_CheckParm("-file"))
 		{
-			// the parms after p are wadfile/lump names,
+			// the parms after p are wadfile names,
 			// until end of parms or another - preceded parm
 			while (M_IsNextParm())
 			{
@@ -1190,6 +1206,19 @@ void D_SRB2Main(void)
 					D_AddFile(startuppwads, s);
 			}
 		}
+
+		if (M_CheckParm("-folder"))
+		{
+			// the parms after p are folder names,
+			// until end of parms or another - preceded parm
+			while (M_IsNextParm())
+			{
+				const char *s = M_GetNextParm();
+
+				if (s) // Check for NULL?
+					D_AddFolder(startuppwads, s);
+			}
+		}
 	}
 
 	// get map from parms
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 09f9d4651..c177e4a4a 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -63,7 +63,9 @@ static void Got_WeaponPref(UINT8 **cp, INT32 playernum);
 static void Got_Mapcmd(UINT8 **cp, INT32 playernum);
 static void Got_ExitLevelcmd(UINT8 **cp, INT32 playernum);
 static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum);
+static void Got_RequestAddfoldercmd(UINT8 **cp, INT32 playernum);
 static void Got_Addfilecmd(UINT8 **cp, INT32 playernum);
+static void Got_Addfoldercmd(UINT8 **cp, INT32 playernum);
 static void Got_Pause(UINT8 **cp, INT32 playernum);
 static void Got_Suicide(UINT8 **cp, INT32 playernum);
 static void Got_RandomSeed(UINT8 **cp, INT32 playernum);
@@ -115,6 +117,7 @@ static void Command_Map_f(void);
 static void Command_ResetCamera_f(void);
 
 static void Command_Addfile(void);
+static void Command_Addfolder(void);
 static void Command_ListWADS_f(void);
 static void Command_RunSOC(void);
 static void Command_Pause(void);
@@ -398,16 +401,16 @@ const char *netxcmdnames[MAXNETXCMD - 1] =
 	"MAP",
 	"EXITLEVEL",
 	"ADDFILE",
+	"ADDFOLDER",
 	"PAUSE",
 	"ADDPLAYER",
 	"TEAMCHANGE",
 	"CLEARSCORES",
-	"LOGIN",
 	"VERIFIED",
 	"RANDOMSEED",
 	"RUNSOC",
 	"REQADDFILE",
-	"DELFILE", // replace next time we add an XD
+	"REQADDFOLDER",
 	"SETMOTD",
 	"SUICIDE",
 	"LUACMD",
@@ -441,7 +444,9 @@ void D_RegisterServerCommands(void)
 	RegisterNetXCmd(XD_MAP, Got_Mapcmd);
 	RegisterNetXCmd(XD_EXITLEVEL, Got_ExitLevelcmd);
 	RegisterNetXCmd(XD_ADDFILE, Got_Addfilecmd);
+	RegisterNetXCmd(XD_ADDFOLDER, Got_Addfoldercmd);
 	RegisterNetXCmd(XD_REQADDFILE, Got_RequestAddfilecmd);
+	RegisterNetXCmd(XD_REQADDFOLDER, Got_RequestAddfoldercmd);
 	RegisterNetXCmd(XD_PAUSE, Got_Pause);
 	RegisterNetXCmd(XD_SUICIDE, Got_Suicide);
 	RegisterNetXCmd(XD_RUNSOC, Got_RunSOCcmd);
@@ -472,6 +477,7 @@ void D_RegisterServerCommands(void)
 	COM_AddCommand("showmap", Command_Showmap_f);
 	COM_AddCommand("mapmd5", Command_Mapmd5_f);
 
+	COM_AddCommand("addfolder", Command_Addfolder);
 	COM_AddCommand("addfile", Command_Addfile);
 	COM_AddCommand("listwad", Command_ListWADS_f);
 
@@ -3323,9 +3329,9 @@ static void Command_Addfile(void)
 		++p;
 
 		// check total packet size and no of files currently loaded
-		// See W_LoadWadFile in w_wad.c
+		// See W_InitFile in w_wad.c
 		if ((numwadfiles >= MAX_WADFILES)
-		|| ((packetsizetally + nameonlylength(fn) + 22) > MAXFILENEEDED*sizeof(UINT8)))
+		|| ((packetsizetally + nameonlylength(fn) + FILENEEDEDSIZE) > MAXFILENEEDED*sizeof(UINT8)))
 		{
 			CONS_Alert(CONS_ERROR, M_GetText("Too many files loaded to add %s\n"), fn);
 			return;
@@ -3373,6 +3379,89 @@ static void Command_Addfile(void)
 	}
 }
 
+static void Command_Addfolder(void)
+{
+	size_t argc = COM_Argc(); // amount of arguments total
+	size_t curarg; // current argument index
+
+	const char *addedfolders[argc]; // list of filenames already processed
+	size_t numfoldersadded = 0; // the amount of filenames processed
+
+	if (argc < 2)
+	{
+		CONS_Printf(M_GetText("addfolder <path> [path2...] [...]: Load add-ons\n"));
+		return;
+	}
+
+	// start at one to skip command name
+	for (curarg = 1; curarg < argc; curarg++)
+	{
+		const char *fn, *p;
+		char buf[256];
+		char *buf_p = buf;
+		INT32 i;
+		size_t ii;
+		boolean folderadded = false;
+
+		fn = COM_Argv(curarg);
+
+		// For the amount of filenames previously processed...
+		for (ii = 0; ii < numfoldersadded; ii++)
+		{
+			// If this is one of them, don't try to add it.
+			if (!strcmp(fn, addedfolders[ii]))
+			{
+				folderadded = true;
+				break;
+			}
+		}
+
+		// If we've added this one, skip to the next one.
+		if (folderadded)
+		{
+			CONS_Alert(CONS_WARNING, M_GetText("Already processed %s, skipping\n"), fn);
+			continue;
+		}
+
+		// Disallow non-printing characters and semicolons.
+		for (i = 0; fn[i] != '\0'; i++)
+			if (!isprint(fn[i]) || fn[i] == ';')
+				return;
+
+		// Add file on your client directly if you aren't in a netgame.
+		if (!(netgame || multiplayer))
+		{
+			P_AddFolder(fn);
+			addedfolders[numfoldersadded++] = fn;
+			continue;
+		}
+
+		p = fn+strlen(fn);
+		while(--p >= fn)
+			if (*p == '\\' || *p == '/' || *p == ':')
+				break;
+		++p;
+
+		// check total packet size and no of files currently loaded
+		// See W_InitFile in w_wad.c
+		if ((numwadfiles >= MAX_WADFILES)
+		|| ((packetsizetally + strlen(fn) + FILENEEDEDSIZE) > MAXFILENEEDED*sizeof(UINT8)))
+		{
+			CONS_Alert(CONS_ERROR, M_GetText("Too many files loaded to add %s\n"), fn);
+			return;
+		}
+
+		WRITESTRINGN(buf_p,p,240);
+
+		addedfolders[numfoldersadded++] = fn;
+
+		if (IsPlayerAdmin(consoleplayer) && (!server)) // Request to add file
+			SendNetXCmd(XD_REQADDFOLDER, buf, buf_p - buf);
+		else
+			SendNetXCmd(XD_ADDFOLDER, buf, buf_p - buf);
+	}
+}
+
 static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
 {
 	char filename[241];
@@ -3401,9 +3490,9 @@ static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
 		return;
 	}
 
-	// See W_LoadWadFile in w_wad.c
+	// See W_InitFile in w_wad.c
 	if ((numwadfiles >= MAX_WADFILES)
-	|| ((packetsizetally + nameonlylength(filename) + 22) > MAXFILENEEDED*sizeof(UINT8)))
+	|| ((packetsizetally + nameonlylength(filename) + FILENEEDEDSIZE) > MAXFILENEEDED*sizeof(UINT8)))
 		toomany = true;
 	else
 		ncs = findfile(filename,md5sum,true);
@@ -3433,6 +3522,64 @@ static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
 	COM_BufAddText(va("addfile %s\n", filename));
 }
 
+static void Got_RequestAddfoldercmd(UINT8 **cp, INT32 playernum)
+{
+	char path[241];
+	filestatus_t ncs = FS_NOTFOUND;
+	boolean kick = false;
+	boolean toomany = false;
+	INT32 i,j;
+
+	READSTRINGN(*cp, path, 240);
+
+	/// \todo Integrity checks.
+
+	// Only the server processes this message.
+	if (client)
+		return;
+
+	// Disallow non-printing characters and semicolons.
+	for (i = 0; path[i] != '\0'; i++)
+		if (!isprint(path[i]) || path[i] == ';')
+			kick = true;
+
+	if ((playernum != serverplayer && !IsPlayerAdmin(playernum)) || kick)
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("Illegal addfolder command received from %s\n"), player_names[playernum]);
+		SendKick(playernum, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
+		return;
+	}
+
+	// See W_InitFile in w_wad.c
+	if ((numwadfiles >= MAX_WADFILES)
+	|| ((packetsizetally + strlen(path) + FILENEEDEDSIZE) > MAXFILENEEDED*sizeof(UINT8)))
+		toomany = true;
+	else
+		ncs = findfolder(path);
+
+	if (ncs != FS_FOUND || toomany)
+	{
+		char message[256];
+
+		if (toomany)
+			sprintf(message, M_GetText("Too many files loaded to add %s\n"), path);
+		else if (ncs == FS_NOTFOUND)
+			sprintf(message, M_GetText("The server doesn't have %s\n"), path);
+		else
+			sprintf(message, M_GetText("Unknown error finding folder (%s)\n"), path);
+
+		CONS_Printf("%s",message);
+
+		for (j = 0; j < MAXPLAYERS; j++)
+			if (adminplayers[j])
+				COM_BufAddText(va("sayto %d %s", adminplayers[j], message));
+
+		return;
+	}
+
+	COM_BufAddText(va("addfolder \"%s\"\n", path));
+}
+
 static void Got_Addfilecmd(UINT8 **cp, INT32 playernum)
 {
 	char filename[241];
@@ -3481,6 +3628,49 @@ static void Got_Addfilecmd(UINT8 **cp, INT32 playernum)
 	G_SetGameModified(true);
 }
 
+static void Got_Addfoldercmd(UINT8 **cp, INT32 playernum)
+{
+	char path[241];
+	filestatus_t ncs = FS_NOTFOUND;
+
+	READSTRINGN(*cp, path, 240);
+
+	/// \todo Integrity checks.
+
+	if (playernum != serverplayer)
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("Illegal addfolder command received from %s\n"), player_names[playernum]);
+		if (server)
+			SendKick(playernum, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
+		return;
+	}
+
+	ncs = findfolder(path);
+
+	if (ncs != FS_FOUND || !P_AddFolder(path))
+	{
+		Command_ExitGame_f();
+		if (ncs == FS_FOUND)
+		{
+			CONS_Printf(M_GetText("The server tried to add %s,\nbut you have too many files added.\nRestart the game to clear loaded files\nand play on this server."), path);
+			M_StartMessage(va("The server added a folder \n(%s)\nbut you have too many files added.\nRestart the game to clear loaded files.\n\nPress ESC\n",path), NULL, MM_NOTHING);
+		}
+		else if (ncs == FS_NOTFOUND)
+		{
+			CONS_Printf(M_GetText("The server tried to add %s,\nbut you don't have this file.\nYou need to find it in order\nto play on this server."), path);
+			M_StartMessage(va("The server added a folder \n(%s)\nthat you do not have.\n\nPress ESC\n",path), NULL, MM_NOTHING);
+		}
+		else
+		{
+			CONS_Printf(M_GetText("Unknown error finding folder (%s) the server added.\n"), path);
+			M_StartMessage(va("Unknown error trying to load a folder\nthat the server added \n(%s).\n\nPress ESC\n",path), NULL, MM_NOTHING);
+		}
+		return;
+	}
+
+	G_SetGameModified(true);
+}
+
 static void Command_ListWADS_f(void)
 {
 	INT32 i = numwadfiles;
@@ -3495,6 +3685,8 @@ static void Command_ListWADS_f(void)
 			CONS_Printf("\x82 * %.2d\x80: %s\n", i, tempname);
 		else if (!wadfiles[i]->important)
 			CONS_Printf("\x86   %.2d: %s\n", i, tempname);
+		else if (wadfiles[i]->type == RET_FOLDER)
+			CONS_Printf("\x82 * %.2d\x84: %s\n", i, tempname);
 		else
 			CONS_Printf("   %.2d: %s\n", i, tempname);
 	}
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index ac39626a4..e7076cabf 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -128,16 +128,16 @@ typedef enum
 	XD_MAP,         // 6
 	XD_EXITLEVEL,   // 7
 	XD_ADDFILE,     // 8
-	XD_PAUSE,       // 9
-	XD_ADDPLAYER,   // 10
-	XD_TEAMCHANGE,  // 11
-	XD_CLEARSCORES, // 12
-	// UNUSED          13 (Because I don't want to change these comments)
-	XD_VERIFIED = 14,//14
+	XD_ADDFOLDER,   // 9
+	XD_PAUSE,       // 10
+	XD_ADDPLAYER,   // 11
+	XD_TEAMCHANGE,  // 12
+	XD_CLEARSCORES, // 13
+	XD_VERIFIED,    // 14
 	XD_RANDOMSEED,  // 15
 	XD_RUNSOC,      // 16
 	XD_REQADDFILE,  // 17
-	XD_DELFILE,     // 18 - replace next time we add an XD
+	XD_REQADDFOLDER,// 18
 	XD_SETMOTD,     // 19
 	XD_SUICIDE,     // 20
 	XD_DEMOTED,     // 21
diff --git a/src/d_netfil.c b/src/d_netfil.c
index 8f661bb5f..4e93f6600 100644
--- a/src/d_netfil.c
+++ b/src/d_netfil.c
@@ -120,7 +120,7 @@ char luafiledir[256 + 16] = "luafiles";
 /** Fills a serverinfo packet with information about wad files loaded.
   *
   * \todo Give this function a better name since it is in global scope.
-  * Used to have size limiting built in - now handled via W_LoadWadFile in w_wad.c
+  * Used to have size limiting built in - now handled via W_InitFile in w_wad.c
   *
   */
 UINT8 *PutFileNeeded(void)
@@ -128,7 +128,7 @@ UINT8 *PutFileNeeded(void)
 	size_t i, count = 0;
 	UINT8 *p = netbuffer->u.serverinfo.fileneeded;
 	char wadfilename[MAX_WADPATH] = "";
-	UINT8 filestatus;
+	UINT8 filestatus, folder;
 
 	for (i = 0; i < numwadfiles; i++)
 	{
@@ -137,9 +137,10 @@ UINT8 *PutFileNeeded(void)
 			continue;
 
 		filestatus = 1; // Importance - not really used any more, holds 1 by default for backwards compat with MS
+		folder = (wadfiles[i]->type == RET_FOLDER);
 
 		// Store in the upper four bits
-		if (!cv_downloading.value)
+		if (!cv_downloading.value || folder) /// \todo Implement folder downloading.
 			filestatus += (2 << 4); // Won't send
 		else if ((wadfiles[i]->filesize <= (UINT32)cv_maxsend.value * 1024))
 			filestatus += (1 << 4); // Will send if requested
@@ -147,6 +148,7 @@ UINT8 *PutFileNeeded(void)
 			// filestatus += (0 << 4); -- Won't send, too big
 
 		WRITEUINT8(p, filestatus);
+		WRITEUINT8(p, folder);
 
 		count++;
 		WRITEUINT32(p, wadfiles[i]->filesize);
@@ -178,6 +180,7 @@ void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr)
 		fileneeded[i].status = FS_NOTFOUND; // We haven't even started looking for the file yet
 		fileneeded[i].justdownloaded = false;
 		filestatus = READUINT8(p); // The first byte is the file status
+		fileneeded[i].folder = READUINT8(p); // The second byte is the folder flag
 		fileneeded[i].willsend = (UINT8)(filestatus >> 4);
 		fileneeded[i].totalsize = READUINT32(p); // The four next bytes are the file size
 		fileneeded[i].file = NULL; // The file isn't open yet
@@ -420,7 +423,7 @@ INT32 CL_CheckFiles(void)
 		return 1;
 	}
 
-	// See W_LoadWadFile in w_wad.c
+	// See W_InitFile in w_wad.c
 	packetsize = packetsizetally;
 
 	for (i = 1; i < fileneedednum; i++)
@@ -442,7 +445,10 @@ INT32 CL_CheckFiles(void)
 		if (fileneeded[i].status != FS_NOTFOUND)
 			continue;
 
-		packetsize += nameonlylength(fileneeded[i].filename) + 22;
+		if (fileneeded[i].folder)
+			packetsize += strlen(fileneeded[i].filename) + FILENEEDEDSIZE;
+		else
+			packetsize += nameonlylength(fileneeded[i].filename) + FILENEEDEDSIZE;
 
 		if ((numwadfiles+filestoget >= MAX_WADFILES)
 		|| (packetsize > MAXFILENEEDED*sizeof(UINT8)))
@@ -450,7 +456,10 @@ INT32 CL_CheckFiles(void)
 
 		filestoget++;
 
-		fileneeded[i].status = findfile(fileneeded[i].filename, fileneeded[i].md5sum, true);
+		if (fileneeded[i].folder)
+			fileneeded[i].status = findfolder(fileneeded[i].filename);
+		else
+			fileneeded[i].status = findfile(fileneeded[i].filename, fileneeded[i].md5sum, true);
 		CONS_Debug(DBG_NETPLAY, "found %d\n", fileneeded[i].status);
 		if (fileneeded[i].status != FS_FOUND)
 			ret = 0;
@@ -472,7 +481,10 @@ void CL_LoadServerFiles(void)
 			continue; // Already loaded
 		else if (fileneeded[i].status == FS_FOUND)
 		{
-			P_AddWadFile(fileneeded[i].filename);
+			if (fileneeded[i].folder)
+				P_AddFolder(fileneeded[i].filename);
+			else
+				P_AddWadFile(fileneeded[i].filename);
 			G_SetGameModified(true);
 			fileneeded[i].status = FS_OPEN;
 		}
@@ -757,7 +769,7 @@ static boolean AddFileToSendQueue(INT32 node, const char *filename, UINT8 fileid
 		// This formerly checked if (!findfile(p->id.filename, NULL, true))
 
 		// Not found
-		// Don't inform client (probably someone who thought they could leak 2.2 ACZ)
+		// Don't inform client
 		DEBFILE(va("Client %d request %s: not found\n", node, filename));
 		free(p->id.filename);
 		free(p);
@@ -1556,3 +1568,23 @@ filestatus_t findfile(char *filename, const UINT8 *wantedmd5sum, boolean complet
 
 	return (badmd5 ? FS_MD5SUMBAD : FS_NOTFOUND); // md5 sum bad or file not found
 }
+
+filestatus_t findfolder(const char *path)
+{
+	// Check the path by itself first.
+	if (checkfolderpath(path, NULL, true))
+		return FS_FOUND;
+
+#define checkpath(startpath) { \
+	if (checkfolderpath(path, startpath, true)) \
+		return FS_FOUND; \
+	}
+
+	checkpath(srb2home) // Then, look in srb2home.
+	checkpath(srb2path) // Now, look in srb2path.
+	checkpath(".") // Finally, look in ".".
+
+#undef checkpath
+
+	return FS_NOTFOUND;
+}
diff --git a/src/d_netfil.h b/src/d_netfil.h
index 1b399be75..7cd6e06d3 100644
--- a/src/d_netfil.h
+++ b/src/d_netfil.h
@@ -38,6 +38,7 @@ typedef enum
 typedef struct
 {
 	UINT8 willsend; // Is the server willing to send it?
+	UINT8 folder; // File is a folder
 	char filename[MAX_WADPATH];
 	UINT8 md5sum[16];
 	filestatus_t status; // The value returned by recsearch
@@ -54,6 +55,8 @@ typedef struct
 	UINT32 ackresendposition; // Used when resuming downloads
 } fileneeded_t;
 
+#define FILENEEDEDSIZE 23
+
 extern INT32 fileneedednum;
 extern fileneeded_t fileneeded[MAX_WADFILES];
 extern char downloaddir[512];
@@ -133,6 +136,9 @@ filestatus_t findfile(char *filename, const UINT8 *wantedmd5sum,
 	boolean completepath);
 filestatus_t checkfilemd5(char *filename, const UINT8 *wantedmd5sum);
 
+// Searches for a folder
+filestatus_t findfolder(const char *path);
+
 void nameonly(char *s);
 size_t nameonlylength(const char *s);
 
diff --git a/src/filesrch.c b/src/filesrch.c
index cb53d07be..f01fc0bd2 100644
--- a/src/filesrch.c
+++ b/src/filesrch.c
@@ -29,6 +29,7 @@
 #include "m_misc.h"
 #include "z_zone.h"
 #include "m_menu.h" // Addons_option_Onchange
+#include "w_wad.h"
 
 #if defined (_WIN32) && defined (_MSC_VER)
 
@@ -340,6 +341,11 @@ char *refreshdirname = NULL;
 size_t packetsizetally = 0;
 size_t mainwadstally = 0;
 
+#define folderpathlen 1024
+#define maxfolderdepth 48
+
+#define isuptree(dirent) ((dirent)[0]=='.' && ((dirent)[1]=='\0' || ((dirent)[1]=='.' && (dirent)[2]=='\0')))
+
 filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *wantedmd5sum, boolean completepath, int maxsearchdepth)
 {
 	filestatus_t retval = FS_NOTFOUND;
@@ -387,10 +393,7 @@ filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *want
 			continue;
 		}
 
-		if (dent->d_name[0]=='.' &&
-				(dent->d_name[1]=='\0' ||
-					(dent->d_name[1]=='.' &&
-						dent->d_name[2]=='\0')))
+		if (isuptree(dent->d_name))
 		{
 			// we don't want to scan uptree
 			continue;
@@ -445,6 +448,329 @@ filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *want
 	return retval;
 }
 
+// Called from findfolder and ResGetLumpsFolder in w_wad.c.
+// Call with cleanup true if the path has to be verified.
+boolean checkfolderpath(const char *path, const char *startpath, boolean cleanup)
+{
+	char folderpath[folderpathlen], basepath[folderpathlen], *fn = NULL;
+	DIR *dirhandle;
+
+	// Remove path separators from the filename, and don't try adding "/".
+	// See also the same code in W_InitFolder.
+	if (cleanup)
+	{
+		const char *p = path + strlen(path);
+		size_t len;
+
+		--p;
+		while (*p == '\\' || *p == '/' || *p == ':')
+		{
+			p--;
+			if (p < path)
+				return false;
+		}
+		++p;
+
+		// Allocate the new path name.
+		len = (p - path) + 1;
+		fn = ZZ_Alloc(len);
+		strlcpy(fn, path, len);
+	}
+
+	if (startpath)
+	{
+		snprintf(basepath, sizeof basepath, "%s" PATHSEP, startpath);
+
+		if (cleanup)
+		{
+			snprintf(folderpath, sizeof folderpath, "%s%s", basepath, fn);
+			Z_Free(fn); // Don't need this anymore.
+		}
+		else
+			snprintf(folderpath, sizeof folderpath, "%s%s", basepath, path);
+
+		// Home path and folder path are the same? Not valid.
+		if (!strcmp(basepath, folderpath))
+			return false;
+	}
+	else if (cleanup)
+	{
+		snprintf(folderpath, sizeof folderpath, "%s", fn);
+		Z_Free(fn); // Don't need this anymore.
+	}
+	else
+		snprintf(folderpath, sizeof folderpath, "%s", path);
+
+	dirhandle = opendir(folderpath);
+	if (dirhandle == NULL)
+		return false;
+	else
+		closedir(dirhandle);
+
+	return true;
+}
+
+INT32 pathisfolder(const char *path)
+{
+	struct stat fsstat;
+
+	if (stat(path, &fsstat) < 0)
+		return -1;
+	else if (S_ISDIR(fsstat.st_mode))
+		return 1;
+
+	return 0;
+}
+
+INT32 samepaths(const char *path1, const char *path2)
+{
+	struct stat stat1;
+	struct stat stat2;
+
+	if (stat(path1, &stat1) < 0)
+		return -1;
+	if (stat(path2, &stat2) < 0)
+		return -1;
+
+	if (stat1.st_dev == stat2.st_dev)
+	{
+#if !defined(_WIN32)
+		return (stat1.st_ino == stat2.st_ino);
+#else
+		HANDLE file1 = CreateFileA(path1, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
+		HANDLE file2 = CreateFileA(path2, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
+		BY_HANDLE_FILE_INFORMATION file1info, file2info;
+		boolean ok = false;
+
+		if (file1 != INVALID_HANDLE_VALUE && file2 != INVALID_HANDLE_VALUE)
+		{
+			if (GetFileInformationByHandle(file1, &file1info) && GetFileInformationByHandle(file2, &file2info))
+			{
+				if (file1info.dwVolumeSerialNumber == file2info.dwVolumeSerialNumber
+				&& file1info.nFileIndexLow == file2info.nFileIndexLow
+				&& file1info.nFileIndexHigh == file2info.nFileIndexHigh)
+					ok = true;
+			}
+		}
+
+		if (file1 != INVALID_HANDLE_VALUE)
+			CloseHandle(file1);
+		if (file2 != INVALID_HANDLE_VALUE)
+			CloseHandle(file2);
+
+		return ok;
+#endif
+	}
+
+	return false;
+}
+
+//
+// Folder loading
+//
+
+static void initfolderpath(char *folderpath, size_t *folderpathindex, int depthleft)
+{
+	folderpathindex[depthleft] = strlen(folderpath) + 1;
+
+	if (folderpath[folderpathindex[depthleft]-2] != PATHSEP[0])
+	{
+		folderpath[folderpathindex[depthleft]-1] = PATHSEP[0];
+		folderpath[folderpathindex[depthleft]] = 0;
+	}
+	else
+		folderpathindex[depthleft]--;
+}
+
+lumpinfo_t *getfolderfiles(const char *path, UINT16 *nlmp, UINT16 *nfiles, UINT16 *nfolders)
+{
+	DIR **dirhandle;
+	struct dirent *dent;
+	struct stat fsstat;
+
+	int rootfolder = (maxfolderdepth - 1);
+	int depthleft = rootfolder;
+
+	char folderpath[folderpathlen];
+	size_t *folderpathindex;
+
+	lumpinfo_t *lumpinfo, *lump_p;
+	UINT16 i = 0, numlumps = (*nlmp);
+
+	dirhandle = (DIR **)malloc(maxfolderdepth * sizeof (DIR*));
+	folderpathindex = (size_t *)malloc(maxfolderdepth * sizeof(size_t));
+
+	// Open the root directory
+	strlcpy(folderpath, path, folderpathlen);
+	dirhandle[depthleft] = opendir(folderpath);
+
+	if (dirhandle[depthleft] == NULL)
+	{
+		free(dirhandle);
+		free(folderpathindex);
+		return NULL;
+	}
+
+	initfolderpath(folderpath, folderpathindex, depthleft);
+	(*nfiles) = 0;
+	(*nfolders) = 0;
+
+	// Count files and directories
+	while (depthleft < maxfolderdepth)
+	{
+		folderpath[folderpathindex[depthleft]] = 0;
+		dent = readdir(dirhandle[depthleft]);
+
+		if (!dent)
+		{
+			if (depthleft != rootfolder) // Don't close the root directory
+				closedir(dirhandle[depthleft]);
+			depthleft++;
+			continue;
+		}
+		else if (isuptree(dent->d_name))
+			continue;
+
+		strcpy(&folderpath[folderpathindex[depthleft]], dent->d_name);
+
+		if (stat(folderpath, &fsstat) < 0)
+			;
+		else if (S_ISDIR(fsstat.st_mode) && depthleft)
+		{
+			folderpathindex[--depthleft] = strlen(folderpath) + 1;
+			dirhandle[depthleft] = opendir(folderpath);
+
+			if (dirhandle[depthleft])
+			{
+				numlumps++;
+				(*nfolders)++;
+			}
+			else
+				depthleft++;
+
+			folderpath[folderpathindex[depthleft]-1] = '/';
+			folderpath[folderpathindex[depthleft]] = 0;
+		}
+		else
+		{
+			numlumps++;
+			(*nfiles)++;
+		}
+
+		if (numlumps == (UINT16_MAX-1))
+			break;
+	}
+
+	// Failure: No files have been found.
+	if (!(*nfiles))
+	{
+		(*nfiles) = UINT16_MAX;
+		free(folderpathindex);
+		free(dirhandle);
+		for (; depthleft < maxfolderdepth; closedir(dirhandle[depthleft++])); // Close any open directories.
+		return NULL;
+	}
+
+	// Create the files and directories as lump entries
+	// It's possible to create lumps and count files at the same time,
+	// but I didn't to constantly have to reallocate memory for every lump.
+	rewinddir(dirhandle[rootfolder]);
+	depthleft = rootfolder;
+
+	strlcpy(folderpath, path, folderpathlen);
+	initfolderpath(folderpath, folderpathindex, depthleft);
+
+	lump_p = lumpinfo = Z_Calloc(numlumps * sizeof(lumpinfo_t), PU_STATIC, NULL);
+
+	while (depthleft < maxfolderdepth)
+	{
+		char *fullname, *trimname;
+
+		folderpath[folderpathindex[depthleft]] = 0;
+		dent = readdir(dirhandle[depthleft]);
+
+		if (!dent)
+		{
+			closedir(dirhandle[depthleft++]);
+			continue;
+		}
+		else if (isuptree(dent->d_name))
+			continue;
+
+		strcpy(&folderpath[folderpathindex[depthleft]], dent->d_name);
+
+		if (stat(folderpath, &fsstat) < 0)
+			continue;
+		else if (S_ISDIR(fsstat.st_mode) && depthleft)
+		{
+			folderpathindex[--depthleft] = strlen(folderpath) + 1;
+			dirhandle[depthleft] = opendir(folderpath);
+
+			if (!dirhandle[depthleft])
+			{
+				depthleft++;
+				continue;
+			}
+
+			folderpath[folderpathindex[depthleft]-1] = '/';
+			folderpath[folderpathindex[depthleft]] = 0;
+		}
+
+		lump_p->diskpath = Z_StrDup(folderpath); // Path in the filesystem to the file
+		lump_p->compression = CM_NOCOMPRESSION; // Lump is uncompressed
+
+		// Remove the folder path.
+		fullname = lump_p->diskpath;
+		if (strstr(fullname, path))
+			fullname += strlen(path) + 1;
+
+		// Get the 8-character long lump name.
+		trimname = strrchr(fullname, '/');
+		if (trimname)
+			trimname++;
+		else
+			trimname = fullname;
+
+		if (trimname[0])
+		{
+			char *dotpos = strrchr(trimname, '.');
+			if (dotpos == NULL)
+				dotpos = fullname + strlen(fullname);
+
+			strncpy(lump_p->name, trimname, min(8, dotpos - trimname));
+
+			// The name of the file, without the extension.
+			lump_p->longname = Z_Calloc(dotpos - trimname + 1, PU_STATIC, NULL);
+			strlcpy(lump_p->longname, trimname, dotpos - trimname + 1);
+		}
+		else
+			lump_p->longname = Z_Calloc(1, PU_STATIC, NULL);
+
+		// The complete name of the file, with its extension,
+		// excluding the path of the folder where it resides.
+		lump_p->fullname = Z_StrDup(fullname);
+
+		lump_p++;
+		i++;
+
+		if (i > numlumps || i == (UINT16_MAX-1))
+		{
+			for (; depthleft < maxfolderdepth; closedir(dirhandle[depthleft++])); // Close any open directories.
+			break;
+		}
+	}
+
+	free(folderpathindex);
+	free(dirhandle);
+
+	(*nlmp) = numlumps;
+	return lumpinfo;
+}
+
+//
+// Addons menu
+//
+
 char exttable[NUM_EXT_TABLE][7] = { // maximum extension length (currently 4) plus 3 (null terminator, stop, and length including previous two)
 	"\5.txt", "\5.cfg", // exec
 	"\5.wad",
@@ -455,7 +781,6 @@ char exttable[NUM_EXT_TABLE][7] = { // maximum extension length (currently 4) pl
 
 char filenamebuf[MAX_WADFILES][MAX_WADPATH];
 
-
 static boolean filemenucmp(char *haystack, char *needle)
 {
 	static char localhaystack[128];
@@ -640,10 +965,7 @@ boolean preparefilemenu(boolean samedepth)
 
 		if (!dent)
 			break;
-		else if (dent->d_name[0]=='.' &&
-				(dent->d_name[1]=='\0' ||
-					(dent->d_name[1]=='.' &&
-						dent->d_name[2]=='\0')))
+		else if (isuptree(dent->d_name))
 			continue; // we don't want to scan uptree
 
 		strcpy(&menupath[menupathindex[menudepthleft]],dent->d_name);
@@ -704,10 +1026,7 @@ boolean preparefilemenu(boolean samedepth)
 
 		if (!dent)
 			break;
-		else if (dent->d_name[0]=='.' &&
-				(dent->d_name[1]=='\0' ||
-					(dent->d_name[1]=='.' &&
-						dent->d_name[2]=='\0')))
+		else if (isuptree(dent->d_name))
 			continue; // we don't want to scan uptree
 
 		strcpy(&menupath[menupathindex[menudepthleft]],dent->d_name);
diff --git a/src/filesrch.h b/src/filesrch.h
index dfea8979e..92e3341f3 100644
--- a/src/filesrch.h
+++ b/src/filesrch.h
@@ -7,6 +7,7 @@
 #include "doomdef.h"
 #include "d_netfil.h"
 #include "m_menu.h" // MAXSTRINGLENGTH
+#include "w_wad.h"
 
 extern consvar_t cv_addons_option, cv_addons_folder, cv_addons_md5, cv_addons_showall, cv_addons_search_case, cv_addons_search_type;
 
@@ -28,6 +29,12 @@ extern consvar_t cv_addons_option, cv_addons_folder, cv_addons_md5, cv_addons_sh
 filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *wantedmd5sum,
 	boolean completepath, int maxsearchdepth);
 
+INT32 pathisfolder(const char *path);
+boolean checkfolderpath(const char *path, const char *startpath, boolean cleanup);
+INT32 samepaths(const char *path1, const char *path2);
+
+lumpinfo_t *getfolderfiles(const char *path, UINT16 *nlmp, UINT16 *nfiles, UINT16 *nfolders);
+
 #define menudepth 20
 
 extern char menupath[1024];
@@ -94,5 +101,4 @@ typedef enum
 void closefilemenu(boolean validsize);
 void searchfilemenu(char *tempname);
 boolean preparefilemenu(boolean samedepth);
-
 #endif // __FILESRCH_H__
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index c2d617eaf..8bbe78f00 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -6681,7 +6681,7 @@ void HWR_LoadAllCustomShaders(void)
 
 	// read every custom shader
 	for (i = 0; i < numwadfiles; i++)
-		HWR_LoadCustomShadersFromFile(i, (wadfiles[i]->type == RET_PK3));
+		HWR_LoadCustomShadersFromFile(i, W_FileHasFolders(wadfiles[i]));
 }
 
 void HWR_LoadCustomShadersFromFile(UINT16 wadnum, boolean PK3)
diff --git a/src/m_misc.c b/src/m_misc.c
index ad2d133ab..74c30dedd 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -2688,3 +2688,22 @@ const char * M_Ftrim (double f)
 		return &dig[1];/* skip the 0 */
 	}
 }
+
+// Returns true if the string is empty.
+boolean M_IsStringEmpty(const char *s)
+{
+	const char *ch = s;
+
+	if (ch == NULL || (ch && strlen(ch) < 1))
+		return true;
+
+	for (;;ch++)
+	{
+		if (!(*ch))
+			break;
+		if (!isspace((*ch)))
+			return false;
+	}
+
+	return true;
+}
diff --git a/src/m_misc.h b/src/m_misc.h
index c5ef9f9f2..d23c53978 100644
--- a/src/m_misc.h
+++ b/src/m_misc.h
@@ -117,6 +117,9 @@ trailing zeros, or "" if the fractional part is zero.
 */
 const char * M_Ftrim (double);
 
+// Returns true if the string is empty.
+boolean M_IsStringEmpty(const char *s);
+
 // counting bits, for weapon ammo code, usually
 FUNCMATH UINT8 M_CountBits(UINT32 num, UINT8 size);
 
diff --git a/src/p_setup.c b/src/p_setup.c
index 66243fb0e..d1c0f1740 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -4385,10 +4385,9 @@ static lumpinfo_t* FindFolder(const char *folName, UINT16 *start, UINT16 *end, l
 // Add a wadfile to the active wad files,
 // replace sounds, musics, patches, textures, sprites and maps
 //
-boolean P_AddWadFile(const char *wadfilename)
+static boolean P_LoadAddon(UINT16 wadnum, UINT16 numlumps)
 {
 	size_t i, j, sreplaces = 0, mreplaces = 0, digmreplaces = 0;
-	UINT16 numlumps, wadnum;
 	char *name;
 	lumpinfo_t *lumpinfo;
 
@@ -4409,18 +4408,10 @@ boolean P_AddWadFile(const char *wadfilename)
 //	UINT16 flaPos, flaNum = 0;
 //	UINT16 mapPos, mapNum = 0;
 
-	// Init file.
-	if ((numlumps = W_InitFile(wadfilename, false, false)) == INT16_MAX)
-	{
-		refreshdirmenu |= REFRESHDIR_NOTLOADED;
-		return false;
-	}
-	else
-		wadnum = (UINT16)(numwadfiles-1);
-
 	switch(wadfiles[wadnum]->type)
 	{
 	case RET_PK3:
+	case RET_FOLDER:
 		// Look for the lumps that act as resource delimitation markers.
 		lumpinfo = wadfiles[wadnum]->lumpinfo;
 		for (i = 0; i < numlumps; i++, lumpinfo++)
@@ -4584,3 +4575,35 @@ boolean P_AddWadFile(const char *wadfilename)
 
 	return true;
 }
+
+boolean P_AddWadFile(const char *wadfilename)
+{
+	UINT16 numlumps, wadnum;
+
+	// Init file.
+	if ((numlumps = W_InitFile(wadfilename, false, false)) == INT16_MAX)
+	{
+		refreshdirmenu |= REFRESHDIR_NOTLOADED;
+		return false;
+	}
+	else
+		wadnum = (UINT16)(numwadfiles-1);
+
+	return P_LoadAddon(wadnum, numlumps);
+}
+
+boolean P_AddFolder(const char *folderpath)
+{
+	UINT16 numlumps, wadnum;
+
+	// Init file.
+	if ((numlumps = W_InitFolder(folderpath, false, false)) == INT16_MAX)
+	{
+		refreshdirmenu |= REFRESHDIR_NOTLOADED;
+		return false;
+	}
+	else
+		wadnum = (UINT16)(numwadfiles-1);
+
+	return P_LoadAddon(wadnum, numlumps);
+}
diff --git a/src/p_setup.h b/src/p_setup.h
index 5d13ae7d4..ae849acbf 100644
--- a/src/p_setup.h
+++ b/src/p_setup.h
@@ -103,6 +103,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate);
 void HWR_LoadLevel(void);
 #endif
 boolean P_AddWadFile(const char *wadfilename);
+boolean P_AddFolder(const char *folderpath);
 boolean P_RunSOC(const char *socfilename);
 void P_LoadSoundsRange(UINT16 wadnum, UINT16 first, UINT16 num);
 void P_LoadMusicsRange(UINT16 wadnum, UINT16 first, UINT16 num);
diff --git a/src/r_textures.c b/src/r_textures.c
index a006d739f..0a1248100 100644
--- a/src/r_textures.c
+++ b/src/r_textures.c
@@ -732,7 +732,7 @@ Rloadflats (INT32 i, INT32 w)
 	texpatch_t *patch;
 
 	// Yes
-	if (wadfiles[w]->type == RET_PK3)
+	if (W_FileHasFolders(wadfiles[w]))
 	{
 		texstart = W_CheckNumForFolderStartPK3("flats/", (UINT16)w, 0);
 		texend = W_CheckNumForFolderEndPK3("flats/", (UINT16)w, texstart);
@@ -754,7 +754,7 @@ Rloadflats (INT32 i, INT32 w)
 			size_t lumplength;
 			size_t flatsize = 0;
 
-			if (wadfiles[w]->type == RET_PK3)
+			if (W_FileHasFolders(wadfiles[w]))
 			{
 				if (W_IsLumpFolder(wadnum, lumpnum)) // Check if lump is a folder
 					continue; // If it is then SKIP IT
@@ -844,7 +844,7 @@ Rloadtextures (INT32 i, INT32 w)
 	texpatch_t *patch;
 
 	// Get the lump numbers for the markers in the WAD, if they exist.
-	if (wadfiles[w]->type == RET_PK3)
+	if (W_FileHasFolders(wadfiles[w]))
 	{
 		texstart = W_CheckNumForFolderStartPK3("textures/", (UINT16)w, 0);
 		texend = W_CheckNumForFolderEndPK3("textures/", (UINT16)w, texstart);
@@ -875,7 +875,7 @@ Rloadtextures (INT32 i, INT32 w)
 			size_t lumplength;
 #endif
 
-			if (wadfiles[w]->type == RET_PK3)
+			if (W_FileHasFolders(wadfiles[w]))
 			{
 				if (W_IsLumpFolder(wadnum, lumpnum)) // Check if lump is a folder
 					continue; // If it is then SKIP IT
@@ -964,7 +964,7 @@ void R_LoadTextures(void)
 	{
 #ifdef WALLFLATS
 		// Count flats
-		if (wadfiles[w]->type == RET_PK3)
+		if (W_FileHasFolders(wadfiles[w]))
 		{
 			texstart = W_CheckNumForFolderStartPK3("flats/", (UINT16)w, 0);
 			texend = W_CheckNumForFolderEndPK3("flats/", (UINT16)w, texstart);
@@ -978,7 +978,7 @@ void R_LoadTextures(void)
 		if (!( texstart == INT16_MAX || texend == INT16_MAX ))
 		{
 			// PK3s have subfolders, so we can't just make a simple sum
-			if (wadfiles[w]->type == RET_PK3)
+			if (W_FileHasFolders(wadfiles[w]))
 			{
 				for (j = texstart; j < texend; j++)
 				{
@@ -1002,7 +1002,7 @@ void R_LoadTextures(void)
 		}
 
 		// Count single-patch textures
-		if (wadfiles[w]->type == RET_PK3)
+		if (W_FileHasFolders(wadfiles[w]))
 		{
 			texstart = W_CheckNumForFolderStartPK3("textures/", (UINT16)w, 0);
 			texend = W_CheckNumForFolderEndPK3("textures/", (UINT16)w, texstart);
@@ -1017,7 +1017,7 @@ void R_LoadTextures(void)
 			continue;
 
 		// PK3s have subfolders, so we can't just make a simple sum
-		if (wadfiles[w]->type == RET_PK3)
+		if (W_FileHasFolders(wadfiles[w]))
 		{
 			for (j = texstart; j < texend; j++)
 			{
@@ -1558,6 +1558,7 @@ lumpnum_t R_GetFlatNumForName(const char *name)
 					continue;
 			break;
 		case RET_PK3:
+		case RET_FOLDER:
 			if ((start = W_CheckNumForFolderStartPK3("Flats/", i, 0)) == INT16_MAX)
 				continue;
 			if ((end = W_CheckNumForFolderEndPK3("Flats/", i, start)) == INT16_MAX)
diff --git a/src/r_things.c b/src/r_things.c
index 14eed9cf2..135c4c5bf 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -443,6 +443,7 @@ void R_AddSpriteDefs(UINT16 wadnum)
 			end = W_CheckNumForNamePwad("SS_END",wadnum,start);     //deutex compatib.
 		break;
 	case RET_PK3:
+	case RET_FOLDER:
 		start = W_CheckNumForFolderStartPK3("Sprites/", wadnum, 0);
 		end = W_CheckNumForFolderEndPK3("Sprites/", wadnum, start);
 		break;
diff --git a/src/w_wad.c b/src/w_wad.c
index 2cbcdecb5..b305d89e9 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -50,16 +50,17 @@
 
 #include "filesrch.h"
 
-#include "i_video.h" // rendermode
+#include "d_main.h"
 #include "d_netfil.h"
-#include "dehacked.h"
 #include "d_clisrv.h"
+#include "dehacked.h"
 #include "r_defs.h"
 #include "r_data.h"
 #include "r_textures.h"
 #include "r_patch.h"
 #include "r_picformats.h"
 #include "i_system.h"
+#include "i_video.h" // rendermode
 #include "md5.h"
 #include "lua_script.h"
 #ifdef SCANTHINGS
@@ -117,10 +118,15 @@ void W_Shutdown(void)
 	{
 		wadfile_t *wad = wadfiles[numwadfiles];
 
-		fclose(wad->handle);
+		if (wad->handle)
+			fclose(wad->handle);
 		Z_Free(wad->filename);
+		if (wad->path)
+			Z_Free(wad->path);
 		while (wad->numlumps--)
 		{
+			if (wad->lumpinfo[wad->numlumps].diskpath)
+				Z_Free(wad->lumpinfo[wad->numlumps].diskpath);
 			Z_Free(wad->lumpinfo[wad->numlumps].longname);
 			Z_Free(wad->lumpinfo[wad->numlumps].fullname);
 		}
@@ -421,6 +427,7 @@ static lumpinfo_t* ResGetLumpsWad (FILE* handle, UINT16* nlmp, const char* filen
 	{
 		lump_p->position = LONG(fileinfo->filepos);
 		lump_p->size = lump_p->disksize = LONG(fileinfo->size);
+		lump_p->diskpath = NULL;
 		if (compressed) // wad is compressed, lump might be
 		{
 			UINT32 realsize = 0;
@@ -602,6 +609,7 @@ static lumpinfo_t* ResGetLumpsZip (FILE* handle, UINT16* nlmp)
 
 		lump_p->position = zentry.offset; // NOT ACCURATE YET: we still need to read the local entry to find our true position
 		lump_p->disksize = zentry.compsize;
+		lump_p->diskpath = NULL;
 		lump_p->size = zentry.size;
 
 		fullname = malloc(zentry.namelen + 1);
@@ -679,6 +687,58 @@ static lumpinfo_t* ResGetLumpsZip (FILE* handle, UINT16* nlmp)
 	return lumpinfo;
 }
 
+// Checks if the combination of the first path and the second path are valid.
+// If they are, the concatenated path is returned.
+static char *W_CheckFolderPath(const char *startpath, const char *path)
+{
+	if (checkfolderpath(path, startpath, false))
+	{
+		char *fn;
+
+		if (startpath)
+		{
+			size_t len = strlen(startpath) + strlen(path) + strlen(PATHSEP) + 1;
+			fn = ZZ_Alloc(len);
+			snprintf(fn, len, "%s" PATHSEP "%s", startpath, path);
+		}
+		else
+			fn = Z_StrDup(path);
+
+		return fn;
+	}
+
+	return NULL;
+}
+
+// Returns the first valid path for a folder.
+static char *W_GetFullFolderPath(const char *path)
+{
+	// Check the path by itself first.
+	char *fn = W_CheckFolderPath(NULL, path);
+	if (fn)
+		return fn;
+
+#define checkpath(startpath) { \
+	fn = W_CheckFolderPath(startpath, path); \
+	if (fn) \
+		return fn; \
+} \
+
+	checkpath(srb2home) // Then, look in srb2home.
+	checkpath(srb2path) // Now, look in srb2path.
+	checkpath(".") // Finally, look in ".".
+
+#undef checkpath
+
+	return NULL;
+}
+
+// Loads files from a folder into a lumpinfo structure.
+static lumpinfo_t *ResGetLumpsFolder(const char *path, UINT16 *nlmp, UINT16 *nfiles, UINT16 *nfolders)
+{
+	return getfolderfiles(path, nlmp, nfiles, nfolders);
+}
+
 static UINT16 W_InitFileError (const char *filename, boolean exitworthy)
 {
 	if (exitworthy)
@@ -694,6 +754,19 @@ static UINT16 W_InitFileError (const char *filename, boolean exitworthy)
 	return INT16_MAX;
 }
 
+static void W_ReadFileShaders(wadfile_t *wadfile)
+{
+#ifdef HWRENDER
+	if (rendermode == render_opengl && (vid.glstate == VID_GL_LIBRARY_LOADED))
+	{
+		HWR_LoadCustomShadersFromFile(numwadfiles - 1, W_FileHasFolders(wadfile));
+		HWR_CompileShaders();
+	}
+#else
+	(void)wadfile;
+#endif
+}
+
 //  Allocate a wadfile, setup the lumpinfo (directory) and
 //  lumpcache, add the wadfile to the current active wadfiles
 //
@@ -760,7 +833,7 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	// see PutFileNeeded in d_netfil.c
 	if ((important = !important))
 	{
-		packetsize = packetsizetally + nameonlylength(filename) + 22;
+		packetsize = packetsizetally + nameonlylength(filename) + FILENEEDEDSIZE;
 
 		if (packetsize > MAXFILENEEDED*sizeof(UINT8))
 		{
@@ -788,7 +861,7 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 		{
 			CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), filename);
 			if (important)
-				packetsizetally -= nameonlylength(filename) + 22;
+				packetsizetally -= nameonlylength(filename) + FILENEEDEDSIZE;
 			if (handle)
 				fclose(handle);
 			return W_InitFileError(filename, false);
@@ -828,9 +901,11 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	//
 	wadfile = Z_Malloc(sizeof (*wadfile), PU_STATIC, NULL);
 	wadfile->filename = Z_StrDup(filename);
+	wadfile->path = NULL;
 	wadfile->type = type;
 	wadfile->handle = handle;
-	wadfile->numlumps = (UINT16)numlumps;
+	wadfile->numlumps = numlumps;
+	wadfile->filecount = wadfile->foldercount = 0;
 	wadfile->lumpinfo = lumpinfo;
 	wadfile->important = important;
 	fseek(handle, 0, SEEK_END);
@@ -853,14 +928,8 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	wadfiles[numwadfiles] = wadfile;
 	numwadfiles++; // must come BEFORE W_LoadDehackedLumps, so any addfile called by COM_BufInsertText called by Lua doesn't overwrite what we just loaded
 
-#ifdef HWRENDER
 	// Read shaders from file
-	if (rendermode == render_opengl && (vid.glstate == VID_GL_LIBRARY_LOADED))
-	{
-		HWR_LoadCustomShadersFromFile(numwadfiles - 1, (type == RET_PK3));
-		HWR_CompileShaders();
-	}
-#endif // HWRENDER
+	W_ReadFileShaders(wadfile);
 
 	// TODO: HACK ALERT - Load Lua & SOC stuff right here. I feel like this should be out of this place, but... Let's stick with this for now.
 	switch (wadfile->type)
@@ -886,6 +955,153 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	return wadfile->numlumps;
 }
 
+//
+// Loads a folder as a WAD.
+//
+UINT16 W_InitFolder(const char *path, boolean mainfile, boolean startup)
+{
+	lumpinfo_t *lumpinfo = NULL;
+	wadfile_t *wadfile;
+	UINT16 numlumps = 0;
+	UINT16 filecount, foldercount;
+	size_t i;
+	char *fn, *fullpath;
+	const char *p;
+	int important;
+
+	if (!(refreshdirmenu & REFRESHDIR_ADDFILE))
+		refreshdirmenu = REFRESHDIR_NORMAL|REFRESHDIR_ADDFILE; // clean out cons_alerts that happened earlier
+
+	if (refreshdirname)
+		Z_Free(refreshdirname);
+	if (dirmenu)
+		refreshdirname = Z_StrDup(path);
+	else
+		refreshdirname = NULL;
+
+	if (numwadfiles >= MAX_WADFILES)
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("Maximum wad files reached\n"));
+		refreshdirmenu |= REFRESHDIR_MAX;
+		return W_InitFileError(path, startup);
+	}
+
+	important = 0; // ???
+
+	/// \todo Implement a W_VerifyFolder.
+	if ((important = !important))
+	{
+		size_t packetsize = packetsizetally + strlen(path) + FILENEEDEDSIZE;
+
+		if (packetsize > MAXFILENEEDED*sizeof(UINT8))
+		{
+			CONS_Alert(CONS_ERROR, M_GetText("Maximum wad files reached\n"));
+			refreshdirmenu |= REFRESHDIR_MAX;
+			return W_InitFileError(path, startup);
+		}
+
+		packetsizetally = packetsize;
+	}
+
+	// Remove path separators from the filename, and don't try adding "/".
+	p = path+strlen(path);
+	--p;
+
+	while (*p == '\\' || *p == '/' || *p == ':')
+	{
+		p--;
+		if (p < path)
+		{
+			CONS_Alert(CONS_ERROR, M_GetText("Path %s is prohibited\n"), path);
+			return W_InitFileError(path, startup);
+		}
+	}
+	p++;
+
+	// Allocate the new path name.
+	i = (p - path) + 1;
+	fn = ZZ_Alloc(i);
+	strlcpy(fn, path, i);
+
+	if (M_IsStringEmpty(fn))
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("Folder name is empty\n"));
+		Z_Free(fn);
+
+		if (startup)
+			return W_InitFileError("A folder", true);
+		else
+			return W_InitFileError("a folder", false);
+	}
+
+	// Get the full path for this filename.
+	fullpath = W_GetFullFolderPath(fn);
+	if (fullpath == NULL)
+	{
+		Z_Free(fn);
+		return W_InitFileError(path, false);
+	}
+
+	for (i = 0; i < numwadfiles; i++)
+	{
+		if (wadfiles[i]->type != RET_FOLDER)
+			continue;
+
+		if (samepaths(wadfiles[i]->path, fullpath) > 0)
+		{
+			CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), path);
+			if (important)
+				packetsizetally -= strlen(path) + FILENEEDEDSIZE;
+			Z_Free(fn);
+			Z_Free(fullpath);
+			return W_InitFileError(path, false);
+		}
+	}
+
+	lumpinfo = ResGetLumpsFolder(fullpath, &numlumps, &filecount, &foldercount);
+	if (lumpinfo == NULL)
+	{
+		if (filecount == UINT16_MAX)
+			CONS_Alert(CONS_ERROR, M_GetText("Folder %s is empty\n"), path);
+
+		Z_Free(fn);
+		Z_Free(fullpath);
+
+		return W_InitFileError(path, startup);
+	}
+
+	if (important && !mainfile)
+		G_SetGameModified(true);
+
+	wadfile = Z_Malloc(sizeof (*wadfile), PU_STATIC, NULL);
+	wadfile->filename = fn;
+	wadfile->path = fullpath;
+	wadfile->type = RET_FOLDER;
+	wadfile->handle = NULL;
+	wadfile->numlumps = numlumps;
+	wadfile->filecount = filecount;
+	wadfile->foldercount = foldercount;
+	wadfile->lumpinfo = lumpinfo;
+	wadfile->important = important;
+
+	// Irrelevant.
+	wadfile->filesize = 0;
+	memset(wadfile->md5sum, 0x00, 16);
+
+	Z_Calloc(numlumps * sizeof (*wadfile->lumpcache), PU_STATIC, &wadfile->lumpcache);
+	Z_Calloc(numlumps * sizeof (*wadfile->patchcache), PU_STATIC, &wadfile->patchcache);
+
+	CONS_Printf(M_GetText("Added folder %s (%u files, %u folders)\n"), fn, filecount, foldercount);
+	wadfiles[numwadfiles] = wadfile;
+	numwadfiles++;
+
+	W_ReadFileShaders(wadfile);
+	W_LoadDehackedLumpsPK3(numwadfiles - 1, mainfile);
+	W_InvalidateLumpnumCache();
+
+	return wadfile->numlumps;
+}
+
 /** Tries to load a series of files.
   * All files are wads unless they have an extension of ".soc" or ".lua".
   *
@@ -897,11 +1113,18 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
   */
 void W_InitMultipleFiles(char **filenames)
 {
-	// will be realloced as lumps are added
 	for (; *filenames; filenames++)
 	{
-		//CONS_Debug(DBG_SETUP, "Loading %s\n", *filenames);
-		W_InitFile(*filenames, numwadfiles < mainwads, true);
+		const char *fn = (*filenames);
+		char pathsep = fn[strlen(fn) - 1];
+		boolean mainfile = (numwadfiles < mainwads);
+
+		//CONS_Debug(DBG_SETUP, "Loading %s\n", fn);
+
+		if (pathsep == '\\' || pathsep == '/')
+			W_InitFolder(fn, mainfile, true);
+		else
+			W_InitFile(fn, mainfile, true);
 	}
 }
 
@@ -1175,7 +1398,7 @@ lumpnum_t W_CheckNumForMap(const char *name)
 				if (!strncmp(name, (wadfiles[i]->lumpinfo + lumpNum)->name, 8))
 					return (i<<16) + lumpNum;
 		}
-		else if (wadfiles[i]->type == RET_PK3)
+		else if (W_FileHasFolders(wadfiles[i]))
 		{
 			lumpNum = W_CheckNumForFolderStartPK3("maps/", i, 0);
 			if (lumpNum != INT16_MAX)
@@ -1273,9 +1496,34 @@ UINT8 W_LumpExists(const char *name)
 
 size_t W_LumpLengthPwad(UINT16 wad, UINT16 lump)
 {
+	lumpinfo_t *l;
+
 	if (!TestValidLump(wad, lump))
 		return 0;
-	return wadfiles[wad]->lumpinfo[lump].size;
+
+	l = wadfiles[wad]->lumpinfo + lump;
+
+	if (wadfiles[wad]->type == RET_FOLDER)
+	{
+		INT32 stat = pathisfolder(l->diskpath);
+
+		if (stat < 0)
+			I_Error("W_LumpLengthPwad: could not stat %s", l->diskpath);
+		else if (stat == 1) // Path is a folder.
+			return 0;
+		else
+		{
+			FILE *handle = fopen(l->diskpath, "rb");
+			if (handle == NULL)
+				I_Error("W_LumpLengthPwad: could not open file %s", l->diskpath);
+
+			fseek(handle, 0, SEEK_END);
+			l->size = l->disksize = ftell(handle);
+			fclose(handle);
+		}
+	}
+
+	return l->size;
 }
 
 /** Returns the buffer size needed to load the given lump.
@@ -1294,7 +1542,7 @@ size_t W_LumpLength(lumpnum_t lumpnum)
 //
 boolean W_IsLumpWad(lumpnum_t lumpnum)
 {
-	if (wadfiles[WADFILENUM(lumpnum)]->type == RET_PK3)
+	if (W_FileHasFolders(wadfiles[WADFILENUM(lumpnum)]))
 	{
 		const char *lumpfullName = (wadfiles[WADFILENUM(lumpnum)]->lumpinfo + LUMPNUM(lumpnum))->fullname;
 
@@ -1312,7 +1560,7 @@ boolean W_IsLumpWad(lumpnum_t lumpnum)
 //
 boolean W_IsLumpFolder(UINT16 wad, UINT16 lump)
 {
-	if (wadfiles[wad]->type == RET_PK3)
+	if (W_FileHasFolders(wadfiles[wad]))
 	{
 		const char *name = wadfiles[wad]->lumpinfo[lump].fullname;
 
@@ -1362,17 +1610,44 @@ void zerr(int ret)
   */
 size_t W_ReadLumpHeaderPwad(UINT16 wad, UINT16 lump, void *dest, size_t size, size_t offset)
 {
-	size_t lumpsize;
+	size_t lumpsize, bytesread;
 	lumpinfo_t *l;
-	FILE *handle;
+	FILE *handle = NULL;
 
 	if (!TestValidLump(wad,lump))
 		return 0;
 
+	l = wadfiles[wad]->lumpinfo + lump;
+
+	// Open the external file for this lump, if the WAD is a folder.
+	if (wadfiles[wad]->type == RET_FOLDER)
+	{
+		INT32 stat = pathisfolder(l->diskpath);
+
+		if (stat < 0)
+			I_Error("W_ReadLumpHeaderPwad: could not stat %s", l->diskpath);
+		else if (stat == 1) // Path is a folder.
+			return 0;
+		else
+		{
+			handle = fopen(l->diskpath, "rb");
+			if (handle == NULL)
+				I_Error("W_ReadLumpHeaderPwad: could not open file %s", l->diskpath);
+
+			// Find length of file
+			fseek(handle, 0, SEEK_END);
+			l->size = l->disksize = ftell(handle);
+		}
+	}
+
 	lumpsize = wadfiles[wad]->lumpinfo[lump].size;
 	// empty resource (usually markers like S_START, F_END ..)
 	if (!lumpsize || lumpsize<offset)
+	{
+		if (wadfiles[wad]->type == RET_FOLDER)
+			fclose(handle);
 		return 0;
+	}
 
 	// zero size means read all the lump
 	if (!size || size+offset > lumpsize)
@@ -1380,24 +1655,22 @@ size_t W_ReadLumpHeaderPwad(UINT16 wad, UINT16 lump, void *dest, size_t size, si
 
 	// Let's get the raw lump data.
 	// We setup the desired file handle to read the lump data.
-	l = wadfiles[wad]->lumpinfo + lump;
-	handle = wadfiles[wad]->handle;
+	if (wadfiles[wad]->type != RET_FOLDER)
+		handle = wadfiles[wad]->handle;
 	fseek(handle, (long)(l->position + offset), SEEK_SET);
 
 	// But let's not copy it yet. We support different compression formats on lumps, so we need to take that into account.
 	switch(wadfiles[wad]->lumpinfo[lump].compression)
 	{
 	case CM_NOCOMPRESSION:		// If it's uncompressed, we directly write the data into our destination, and return the bytes read.
+		bytesread = fread(dest, 1, size, handle);
+		if (wadfiles[wad]->type == RET_FOLDER)
+			fclose(handle);
 #ifdef NO_PNG_LUMPS
-		{
-			size_t bytesread = fread(dest, 1, size, handle);
-			if (Picture_IsLumpPNG((UINT8 *)dest, bytesread))
-				Picture_ThrowPNGError(l->fullname, wadfiles[wad]->filename);
-			return bytesread;
-		}
-#else
-		return fread(dest, 1, size, handle);
+		if (Picture_IsLumpPNG((UINT8 *)dest, bytesread))
+			Picture_ThrowPNGError(l->fullname, wadfiles[wad]->filename);
 #endif
+		return bytesread;
 	case CM_LZF:		// Is it LZF compressed? Used by ZWADs.
 		{
 #ifdef ZWAD
diff --git a/src/w_wad.h b/src/w_wad.h
index d0a86bcb4..8b3c3808e 100644
--- a/src/w_wad.h
+++ b/src/w_wad.h
@@ -69,6 +69,7 @@ typedef struct
 	char name[9];           // filelump_t name[] e.g. "LongEntr"
 	char *longname;         //                   e.g. "LongEntryName"
 	char *fullname;         //                   e.g. "Folder/Subfolder/LongEntryName.extension"
+	char *diskpath;         // path to the file  e.g. "/usr/games/srb2/Addon/Folder/Subfolder/LongEntryName.extension"
 	size_t size;            // real (uncompressed) size
 	compmethod compression; // lump compression method
 } lumpinfo_t;
@@ -109,17 +110,19 @@ typedef enum restype
 	RET_SOC,
 	RET_LUA,
 	RET_PK3,
+	RET_FOLDER,
 	RET_UNKNOWN,
 } restype_t;
 
 typedef struct wadfile_s
 {
-	char *filename;
+	char *filename, *path;
 	restype_t type;
 	lumpinfo_t *lumpinfo;
 	lumpcache_t *lumpcache;
 	lumpcache_t *patchcache;
 	UINT16 numlumps; // this wad's number of resources
+	UINT16 filecount, foldercount; // file and folder count
 	FILE *handle;
 	UINT32 filesize; // for network
 	UINT8 md5sum[16];
@@ -127,7 +130,7 @@ typedef struct wadfile_s
 	boolean important; // also network - !W_VerifyNMUSlumps
 } wadfile_t;
 
-#define WADFILENUM(lumpnum) (UINT16)((lumpnum)>>16) // wad flumpnum>>16) // wad file number in upper word
+#define WADFILENUM(lumpnum) (UINT16)((lumpnum)>>16) // wad file number in upper word
 #define LUMPNUM(lumpnum) (UINT16)((lumpnum)&0xFFFF) // lump number for this pwad
 
 extern UINT16 numwadfiles;
@@ -141,10 +144,14 @@ void W_Shutdown(void);
 FILE *W_OpenWadFile(const char **filename, boolean useerrors);
 // Load and add a wadfile to the active wad files, returns numbers of lumps, INT16_MAX on error
 UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup);
+// Adds a folder as a file
+UINT16 W_InitFolder(const char *path, boolean mainfile, boolean startup);
 
 // W_InitMultipleFiles exits if a file was not found, but not if all is okay.
 void W_InitMultipleFiles(char **filenames);
 
+#define W_FileHasFolders(wadfile) ((wadfile)->type == RET_PK3 || (wadfile)->type == RET_FOLDER)
+
 const char *W_CheckNameForNumPwad(UINT16 wad, UINT16 lump);
 const char *W_CheckNameForNum(lumpnum_t lumpnum);
 

From 0482eacb7c9a625c47a19c017304a3f4af2bd24c Mon Sep 17 00:00:00 2001
From: LJ Sonic <lamr@free.fr>
Date: Sat, 3 Jul 2021 19:58:59 +0200
Subject: [PATCH 2/2] Load add-ons in the order in which the -file and -folder
 arguments are specified

---
 src/d_main.c | 41 ++++++++++++++++-------------------------
 1 file changed, 16 insertions(+), 25 deletions(-)

diff --git a/src/d_main.c b/src/d_main.c
index 0d0e2434a..7866ccbed 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -1253,34 +1253,25 @@ void D_SRB2Main(void)
 	// Do this up here so that WADs loaded through the command line can use ExecCfg
 	COM_Init();
 
-	// add any files specified on the command line with -file wadfile
-	// to the wad list
+	// Add any files specified on the command line with
+	// "-file <file>" or "-folder <folder>" to the add-on list
 	if (!((M_GetUrlProtocolArg() || M_CheckParm("-connect")) && !M_CheckParm("-server")))
 	{
-		if (M_CheckParm("-file"))
+		INT32 addontype = 0;
+		INT32 i;
+
+		for (i = 1; i < myargc; i++)
 		{
-			// the parms after p are wadfile names,
-			// until end of parms or another - preceded parm
-			while (M_IsNextParm())
-			{
-				const char *s = M_GetNextParm();
-
-				if (s) // Check for NULL?
-					D_AddFile(startuppwads, s);
-			}
-		}
-
-		if (M_CheckParm("-folder"))
-		{
-			// the parms after p are folder names,
-			// until end of parms or another - preceded parm
-			while (M_IsNextParm())
-			{
-				const char *s = M_GetNextParm();
-
-				if (s) // Check for NULL?
-					D_AddFolder(startuppwads, s);
-			}
+			if (!strcasecmp(myargv[i], "-file"))
+				addontype = 1;
+			else if (!strcasecmp(myargv[i], "-folder"))
+				addontype = 2;
+			else if (myargv[i][0] == '-' || myargv[i][0] == '+')
+				addontype = 0;
+			else if (addontype == 1)
+				D_AddFile(startuppwads, myargv[i]);
+			else if (addontype == 2)
+				D_AddFolder(startuppwads, myargv[i]);
 		}
 	}