diff --git a/src/d_iwad.cpp b/src/d_iwad.cpp
index 1fc63e03d3..6fa8604a0c 100644
--- a/src/d_iwad.cpp
+++ b/src/d_iwad.cpp
@@ -292,6 +292,7 @@ void FIWadManager::ParseIWadInfos(const char *fn)
 int FIWadManager::ScanIWAD (const char *iwad)
 {
 	FResourceFile *iwadfile = FResourceFile::OpenResourceFile(iwad, NULL, true);
+	if (iwadfile == NULL) iwadfile = FResourceFile::OpenDirectory(iwad, true); //mxd. A directory can also work as an IWAD
 
 	if (iwadfile != NULL)
 	{
@@ -344,7 +345,7 @@ int FIWadManager::CheckIWAD (const char *doomwaddir, WadStuff *wads)
 			
 			iwad.Format ("%s%s%s", doomwaddir, slash, mIWadNames[i].GetChars());
 			FixPathSeperator (iwad);
-			if (FileExists (iwad))
+			if (DirEntryExists(iwad))
 			{
 				wads[i].Type = ScanIWAD (iwad);
 				if (wads[i].Type != -1)
@@ -413,7 +414,7 @@ int FIWadManager::IdentifyVersion (TArray<FString> &wadfiles, const char *iwad,
 		}
 		else
 		{
-			DefaultExtension (custwad, ".wad");
+			if(FileExists(custwad)) DefaultExtension (custwad, ".wad"); //mxd. Don't treat folders as .wads
 			iwadparm = custwad;
 			mIWadNames[0] = custwad;
 			CheckIWAD ("", &wads[0]);
diff --git a/src/g_game.cpp b/src/g_game.cpp
index 48bda24f5f..b0c8775fc5 100644
--- a/src/g_game.cpp
+++ b/src/g_game.cpp
@@ -1655,9 +1655,10 @@ static void G_QueueBody (AActor *body)
 //
 // G_DoReborn
 //
+EXTERN_CVAR(Bool, sv_singleplayerrespawn)
 void G_DoReborn (int playernum, bool freshbot)
 {
-	if (!multiplayer && !(level.flags2 & LEVEL2_ALLOWRESPAWN))
+	if (!multiplayer && !(level.flags2 & LEVEL2_ALLOWRESPAWN) && !sv_singleplayerrespawn)
 	{
 		if (BackupSaveName.Len() > 0 && FileExists (BackupSaveName.GetChars()))
 		{ // Load game from the last point it was saved
diff --git a/src/g_level.cpp b/src/g_level.cpp
index 119cde378d..d3a8c4015a 100644
--- a/src/g_level.cpp
+++ b/src/g_level.cpp
@@ -526,6 +526,8 @@ static bool		unloading;
 //
 //==========================================================================
 
+EXTERN_CVAR(Bool, sv_singleplayerrespawn)
+
 void G_ChangeLevel(const char *levelname, int position, int flags, int nextSkill)
 {
 	level_info_t *nextinfo = NULL;
@@ -634,7 +636,7 @@ void G_ChangeLevel(const char *levelname, int position, int flags, int nextSkill
 
 			// If this is co-op, respawn any dead players now so they can
 			// keep their inventory on the next map.
-			if ((multiplayer || level.flags2 & LEVEL2_ALLOWRESPAWN) && !deathmatch && player->playerstate == PST_DEAD)
+			if ((multiplayer || level.flags2 & LEVEL2_ALLOWRESPAWN || sv_singleplayerrespawn) && !deathmatch && player->playerstate == PST_DEAD)
 			{
 				// Copied from the end of P_DeathThink [[
 				player->cls = NULL;		// Force a new class if the player is using a random class
diff --git a/src/p_acs.cpp b/src/p_acs.cpp
index 120fd83fe1..bfd7ef9774 100644
--- a/src/p_acs.cpp
+++ b/src/p_acs.cpp
@@ -2908,7 +2908,8 @@ FSerializer &Serialize(FSerializer &arc, const char *key, SavingRunningscript &r
 void DACSThinker::Serialize(FSerializer &arc)
 {
 	Super::Serialize(arc);
-	arc("scripts", Scripts);
+	arc("scripts", Scripts)
+		("lastscript", LastScript);
 
 	if (arc.isWriting())
 	{
diff --git a/src/p_mobj.cpp b/src/p_mobj.cpp
index 8e630b77a5..5d40d1ed37 100644
--- a/src/p_mobj.cpp
+++ b/src/p_mobj.cpp
@@ -1,4 +1,4 @@
-// Emacs style mode select	 -*- C++ -*- 
+// Emacs style mode select	 -*- C++ -*- 
 //-----------------------------------------------------------------------------
 //
 // $Id:$
@@ -4496,6 +4496,7 @@ void AActor::AdjustFloorClip ()
 // Most of the player structure stays unchanged between levels.
 //
 EXTERN_CVAR (Bool, chasedemo)
+EXTERN_CVAR(Bool, sv_singleplayerrespawn)
 
 extern bool demonew;
 
@@ -4683,7 +4684,7 @@ APlayerPawn *P_SpawnPlayer (FPlayerStart *mthing, int playernum, int flags)
 	{ // Give all cards in death match mode.
 		p->mo->GiveDeathmatchInventory ();
 	}
-	else if ((multiplayer || (level.flags2 & LEVEL2_ALLOWRESPAWN)) && state == PST_REBORN && oldactor != NULL)
+	else if ((multiplayer || (level.flags2 & LEVEL2_ALLOWRESPAWN) || sv_singleplayerrespawn) && state == PST_REBORN && oldactor != NULL)
 	{ // Special inventory handling for respawning in coop
 		p->mo->FilterCoopRespawnInventory (oldactor);
 	}
diff --git a/src/p_user.cpp b/src/p_user.cpp
index 00d22a9bae..70c864c2aa 100644
--- a/src/p_user.cpp
+++ b/src/p_user.cpp
@@ -65,6 +65,9 @@ static FRandom pr_skullpop ("SkullPop");
 // [RH] # of ticks to complete a turn180
 #define TURN180_TICKS	((TICRATE / 4) + 1)
 
+// [SP] Allows respawn in single player
+CVAR(Bool, sv_singleplayerrespawn, false, CVAR_SERVERINFO | CVAR_LATCH)
+
 // Variables for prediction
 CVAR (Bool, cl_noprediction, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
 CVAR(Bool, cl_predict_specials, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
@@ -2211,7 +2214,9 @@ void P_DeathThink (player_t *player)
 		if (level.time >= player->respawn_time || ((player->cmd.ucmd.buttons & BT_USE) && player->Bot == NULL))
 		{
 			player->cls = NULL;		// Force a new class if the player is using a random class
-			player->playerstate = (multiplayer || (level.flags2 & LEVEL2_ALLOWRESPAWN)) ? PST_REBORN : PST_ENTER;
+			player->playerstate = 
+				(multiplayer || (level.flags2 & LEVEL2_ALLOWRESPAWN) || sv_singleplayerrespawn)
+				? PST_REBORN : PST_ENTER;
 			if (player->mo->special1 > 2)
 			{
 				player->mo->special1 = 0;
diff --git a/src/portal.cpp b/src/portal.cpp
index cef79820fe..f1454cdc6c 100644
--- a/src/portal.cpp
+++ b/src/portal.cpp
@@ -1024,7 +1024,8 @@ void P_CreateLinkedPortals()
 		{
 			if (sectors[i].GetPortalType(j) == PORTS_LINKEDPORTAL && sectors[i].PortalGroup == 0)
 			{
-				CollectSectors(sectors[i].GetOppositePortalGroup(j), &sectors[i]);
+				auto p = sectors[i].GetPortal(j);
+				CollectSectors(p->mOrigin->PortalGroup, &sectors[i]);
 			}
 		}
 	}
diff --git a/src/textures/multipatchtexture.cpp b/src/textures/multipatchtexture.cpp
index b0db481a89..340376a25b 100644
--- a/src/textures/multipatchtexture.cpp
+++ b/src/textures/multipatchtexture.cpp
@@ -48,6 +48,7 @@
 #include "v_palette.h"
 #include "v_video.h"
 #include "v_text.h"
+#include "cmdlib.h"
 #include "m_fixed.h"
 #include "textures/textures.h"
 #include "r_data/colormaps.h"
@@ -138,7 +139,6 @@ struct strifemaptexture_t
 struct FPatchLookup
 {
 	FString Name;
-	FTexture *Texture;
 };
 
 
@@ -166,6 +166,7 @@ public:
 	int GetSourceLump() { return DefinitionLump; }
 	FTexture *GetRedirect(bool wantwarped);
 	FTexture *GetRawTexture();
+	void ResolvePatches();
 
 protected:
 	BYTE *Pixels;
@@ -185,8 +186,18 @@ protected:
 		TexPart();
 	};
 
+	struct TexInit
+	{
+		FString TexName;
+		int UseType = TEX_Null;
+		bool Silent = false;
+		bool HasLine = false;
+		FScriptPosition sc;
+	};
+
 	int NumParts;
 	TexPart *Parts;
+	TexInit *Inits;
 	bool bRedirect:1;
 	bool bTranslucentPatches:1;
 
@@ -194,7 +205,7 @@ protected:
 
 private:
 	void CheckForHacks ();
-	void ParsePatch(FScanner &sc, TexPart & part, bool silent, int usetype);
+	void ParsePatch(FScanner &sc, TexPart & part, TexInit &init);
 };
 
 //==========================================================================
@@ -204,7 +215,7 @@ private:
 //==========================================================================
 
 FMultiPatchTexture::FMultiPatchTexture (const void *texdef, FPatchLookup *patchlookup, int maxpatchnum, bool strife, int deflumpnum)
-: Pixels (0), Spans(0), Parts(0), bRedirect(false), bTranslucentPatches(false)
+: Pixels (0), Spans(0), Parts(nullptr), Inits(nullptr), bRedirect(false), bTranslucentPatches(false)
 {
 	union
 	{
@@ -240,7 +251,8 @@ FMultiPatchTexture::FMultiPatchTexture (const void *texdef, FPatchLookup *patchl
 	}
 
 	UseType = FTexture::TEX_Wall;
-	Parts = NumParts > 0 ? new TexPart[NumParts] : NULL;
+	Parts = NumParts > 0 ? new TexPart[NumParts] : nullptr;
+	Inits = NumParts > 0 ? new TexInit[NumParts] : nullptr;
 	Width = SAFESHORT(mtexture.d->width);
 	Height = SAFESHORT(mtexture.d->height);
 	Name = (char *)mtexture.d->name;
@@ -272,17 +284,9 @@ FMultiPatchTexture::FMultiPatchTexture (const void *texdef, FPatchLookup *patchl
 		}
 		Parts[i].OriginX = LittleShort(mpatch.d->originx);
 		Parts[i].OriginY = LittleShort(mpatch.d->originy);
-		Parts[i].Texture = patchlookup[LittleShort(mpatch.d->patch)].Texture;
-		if (Parts[i].Texture == NULL)
-		{
-			Printf(TEXTCOLOR_RED "Unknown patch %s in texture %s\n", patchlookup[LittleShort(mpatch.d->patch)].Name.GetChars(), Name.GetChars());
-			NumParts--;
-			i--;
-		}
-		else
-		{
-			Parts[i].Texture->bKeepAround = true;
-		}
+		Parts[i].Texture = nullptr;
+		Inits[i].TexName = patchlookup[LittleShort(mpatch.d->patch)].Name;
+		Inits[i].UseType = TEX_WallPatch;
 		if (strife)
 			mpatch.s++;
 		else
@@ -295,17 +299,6 @@ FMultiPatchTexture::FMultiPatchTexture (const void *texdef, FPatchLookup *patchl
 
 	CheckForHacks ();
 
-	// If this texture is just a wrapper around a single patch, we can simply
-	// forward GetPixels() and GetColumn() calls to that patch.
-	if (NumParts == 1)
-	{
-		if (Parts->OriginX == 0 && Parts->OriginY == 0 &&
-			Parts->Texture->GetWidth() == Width &&
-			Parts->Texture->GetHeight() == Height)
-		{
-			bRedirect = true;
-		}
-	}
 	DefinitionLump = deflumpnum;
 }
 
@@ -327,6 +320,11 @@ FMultiPatchTexture::~FMultiPatchTexture ()
 		delete[] Parts;
 		Parts = NULL;
 	}
+	if (Inits != nullptr)
+	{
+		delete[] Inits;
+		Inits = nullptr;
+	}
 	if (Spans != NULL)
 	{
 		FreeSpans (Spans);
@@ -863,19 +861,6 @@ void FTextureManager::AddTexturesLump (const void *lumpdata, int lumpsize, int d
 			pnames.Read(pname, 8);
 			pname[8] = '\0';
 			patchlookup[i].Name = pname;
-			FTextureID j = CheckForTexture (patchlookup[i].Name, FTexture::TEX_WallPatch);
-			if (j.isValid())
-			{
-				patchlookup[i].Texture = Textures[j.GetIndex()].Texture;
-			}
-			else
-			{
-				// Shareware Doom has the same PNAMES lump as the registered
-				// Doom, so printing warnings for patches that don't really
-				// exist isn't such a good idea.
-				//Printf ("Patch %s not found.\n", patchlookup[i].Name);
-				patchlookup[i].Texture = NULL;
-			}
 		}
 	}
 
@@ -996,35 +981,13 @@ void FTextureManager::AddTexturesLumps (int lump1, int lump2, int patcheslump)
 //
 //==========================================================================
 
-void FMultiPatchTexture::ParsePatch(FScanner &sc, TexPart & part, bool silent, int usetype)
+void FMultiPatchTexture::ParsePatch(FScanner &sc, TexPart & part, TexInit &init)
 {
 	FString patchname;
+	int Mirror = 0;
 	sc.MustGetString();
 
-	FTextureID texno = TexMan.CheckForTexture(sc.String, usetype);
-	int Mirror = 0;
-
-	if (!texno.isValid())
-	{
-		if (strlen(sc.String) <= 8 && !strpbrk(sc.String, "./"))
-		{
-			int lumpnum = Wads.CheckNumForName(sc.String, usetype == TEX_MiscPatch? ns_graphics : ns_patches);
-			if (lumpnum >= 0)
-			{
-				part.Texture = FTexture::CreateTexture(lumpnum, usetype);
-				TexMan.AddTexture(part.Texture);
-			}
-		}
-	}
-	else
-	{
-		part.Texture = TexMan[texno];
-		bComplex |= part.Texture->bComplex;
-	}
-	if (part.Texture == NULL)
-	{
-		if (!silent) sc.ScriptMessage(TEXTCOLOR_RED "Unknown patch '%s' in texture '%s'\n", sc.String, Name.GetChars());
-	}
+	init.TexName = sc.String;
 	sc.MustGetStringName(",");
 	sc.MustGetNumber();
 	part.OriginX = sc.Number;
@@ -1207,6 +1170,7 @@ FMultiPatchTexture::FMultiPatchTexture (FScanner &sc, int usetype)
 : Pixels (0), Spans(0), Parts(0), bRedirect(false), bTranslucentPatches(false)
 {
 	TArray<TexPart> parts;
+	TArray<TexInit> inits;
 	bool bSilent = false;
 
 	bMultiPatch = true;
@@ -1267,16 +1231,51 @@ FMultiPatchTexture::FMultiPatchTexture (FScanner &sc, int usetype)
 			else if (sc.Compare("Patch"))
 			{
 				TexPart part;
-				ParsePatch(sc, part, bSilent, TEX_WallPatch);
-				if (part.Texture != NULL) parts.Push(part);
+				TexInit init;
+				ParsePatch(sc, part, init);
+				if (init.TexName.IsNotEmpty())
+				{
+					parts.Push(part);
+					init.UseType = TEX_WallPatch;
+					init.Silent = bSilent;
+					init.HasLine = true;
+					init.sc = sc;
+					inits.Push(init);
+				}
+				part.Texture = NULL;
+				part.Translation = NULL;
+			}
+			else if (sc.Compare("Sprite"))
+			{
+				TexPart part;
+				TexInit init;
+				ParsePatch(sc, part, init);
+				if (init.TexName.IsNotEmpty())
+				{
+					parts.Push(part);
+					init.UseType = TEX_Sprite;
+					init.Silent = bSilent;
+					init.HasLine = true;
+					init.sc = sc;
+					inits.Push(init);
+				}
 				part.Texture = NULL;
 				part.Translation = NULL;
 			}
 			else if (sc.Compare("Graphic"))
 			{
 				TexPart part;
-				ParsePatch(sc, part, bSilent, TEX_MiscPatch);
-				if (part.Texture != NULL) parts.Push(part);
+				TexInit init;
+				ParsePatch(sc, part, init);
+				if (init.TexName.IsNotEmpty())
+				{
+					parts.Push(part);
+					init.UseType = TEX_MiscPatch;
+					init.Silent = bSilent;
+					init.HasLine = true;
+					init.sc = sc;
+					inits.Push(init);
+				}
 				part.Texture = NULL;
 				part.Translation = NULL;
 			}
@@ -1297,21 +1296,10 @@ FMultiPatchTexture::FMultiPatchTexture (FScanner &sc, int usetype)
 		NumParts = parts.Size();
 		Parts = new TexPart[NumParts];
 		memcpy(Parts, &parts[0], NumParts * sizeof(*Parts));
-
-		//CalcBitSize ();
-
-		// If this texture is just a wrapper around a single patch, we can simply
-		// forward GetPixels() and GetColumn() calls to that patch.
-		if (NumParts == 1)
+		Inits = new TexInit[NumParts];
+		for (int i = 0; i < NumParts; i++)
 		{
-			if (Parts->OriginX == 0 && Parts->OriginY == 0 &&
-				Parts->Texture->GetWidth() == Width &&
-				Parts->Texture->GetHeight() == Height &&
-				Parts->Rotate == 0 && 
-				!bComplex)
-			{
-				bRedirect = true;
-			}
+			Inits[i] = inits[i];
 		}
 	}
 	
@@ -1328,6 +1316,58 @@ FMultiPatchTexture::FMultiPatchTexture (FScanner &sc, int usetype)
 }
 
 
+void FMultiPatchTexture::ResolvePatches()
+{
+	if (Inits != nullptr)
+	{
+		for (int i = 0; i < NumParts; i++)
+		{
+			FTextureID texno = TexMan.CheckForTexture(Inits[i].TexName, Inits[i].UseType);
+
+			if (!texno.isValid())
+			{
+				if (!Inits[i].Silent)
+				{
+					if (Inits[i].HasLine) Inits[i].sc.Message(MSG_WARNING, "Unknown patch '%s' in texture '%s'\n", Inits[i].TexName.GetChars(), Name.GetChars());
+					else Printf(TEXTCOLOR_YELLOW  "Unknown patch '%s' in texture '%s'\n", Inits[i].TexName.GetChars(), Name.GetChars());
+				}
+			}
+			else
+			{
+				Parts[i].Texture = TexMan[texno];
+				bComplex |= Parts[i].Texture->bComplex;
+				Parts[i].Texture->bKeepAround = true;
+			}
+		}
+		for (int i = 0; i < NumParts; i++)
+		{
+			if (Parts[i].Texture == nullptr)
+			{
+				memcpy(&Parts[i], &Parts[i + 1], NumParts - i - 1);
+				i--;
+				NumParts--;
+			}
+		}
+	}
+	delete[] Inits;
+	Inits = nullptr;
+
+	// If this texture is just a wrapper around a single patch, we can simply
+	// forward GetPixels() and GetColumn() calls to that patch.
+
+	if (NumParts == 1)
+	{
+		if (Parts->OriginX == 0 && Parts->OriginY == 0 &&
+			Parts->Texture->GetWidth() == Width &&
+			Parts->Texture->GetHeight() == Height &&
+			Parts->Rotate == 0 &&
+			!bComplex)
+		{
+			bRedirect = true;
+		}
+	}
+}
+
 
 void FTextureManager::ParseXTexture(FScanner &sc, int usetype)
 {
diff --git a/src/textures/texturemanager.cpp b/src/textures/texturemanager.cpp
index 3fb01dc6cf..07ed71a3bb 100644
--- a/src/textures/texturemanager.cpp
+++ b/src/textures/texturemanager.cpp
@@ -981,6 +981,10 @@ void FTextureManager::Init()
 	{
 		AddTexturesForWad(i);
 	}
+	for (unsigned i = 0; i < Textures.Size(); i++)
+	{
+		Textures[i].Texture->ResolvePatches();
+	}
 
 	// Add one marker so that the last WAD is easier to handle and treat
 	// Build tiles as a completely separate block.
diff --git a/src/textures/textures.h b/src/textures/textures.h
index ad1d9ba8c2..407500f185 100644
--- a/src/textures/textures.h
+++ b/src/textures/textures.h
@@ -209,6 +209,7 @@ public:
 	int GetScaledTopOffset () { int foo = int((TopOffset * 2) / Scale.Y); return (foo >> 1) + (foo & 1); }
 	double GetScaledLeftOffsetDouble() { return LeftOffset / Scale.X; }
 	double GetScaledTopOffsetDouble() { return TopOffset / Scale.Y; }
+	virtual void ResolvePatches() {}
 
 	virtual void SetFrontSkyLayer();