diff --git a/.gitignore b/.gitignore
index 268e3632906a9b840747e6c015b32848ba45b50f..1dd1c19d51a706b7b792ba2e32895ed9d88f8228 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,4 +23,5 @@ Win32_LIB_ASM_Release
 /bin
 /build
 /build/*
-/CMakeUserPresets.json
\ No newline at end of file
+/CMakeUserPresets.json
+/out
\ No newline at end of file
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 3fed07bcc2457ce00fcf7d571590b8d02f68cf2d..8a2086f46f73292a5d7cc1b6308165707403a66b 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -37,6 +37,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
 	m_random.c
 	m_tokenizer.c
 	m_queue.c
+	m_vector.c
 	info.c
 	p_ceilng.c
 	p_enemy.c
diff --git a/src/Sourcefile b/src/Sourcefile
index 68ffe0f3f603b580c7c47fa3070909f5f3f82b15..3e926e31850a2055d335326096de6034a87b0a69 100644
--- a/src/Sourcefile
+++ b/src/Sourcefile
@@ -31,6 +31,7 @@ m_perfstats.c
 m_random.c
 m_tokenizer.c
 m_queue.c
+m_vector.c
 info.c
 p_ceilng.c
 p_enemy.c
diff --git a/src/console.c b/src/console.c
index 0917d916b02c8adaf3b74dc90ddaaf57ef0689c7..751a6e5cad06a57a1c3d3e7f3653dac13e2e2964 100644
--- a/src/console.c
+++ b/src/console.c
@@ -983,7 +983,7 @@ boolean CON_Responder(event_t *ev)
 		if (modeattacking || metalrecording || marathonmode)
 			return false;
 
-		if ((key == gamecontrol[GC_CONSOLE][0] || key == gamecontrol[GC_CONSOLE][1]) && !shiftdown)
+		if (ev->type == ev_keydown && ((key == gamecontrol[GC_CONSOLE][0] || key == gamecontrol[GC_CONSOLE][1]) && !shiftdown))
 		{
 			if (consdown) // ignore repeat
 				return true;
diff --git a/src/g_game.c b/src/g_game.c
index 90ccf29c1854904a8dd06003ddbd40b99de00ca6..99bb0f73c8a4d39cd34721bee240d0b7b5938f68 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -4612,6 +4612,8 @@ void G_LoadGameData(gamedata_t *data)
 // Saves the main data file, which stores information such as emblems found, etc.
 void G_SaveGameData(gamedata_t *data)
 {
+	UINT8 *data_p;
+
 	size_t length;
 	INT32 i, j;
 	UINT8 btemp;
@@ -4621,8 +4623,8 @@ void G_SaveGameData(gamedata_t *data)
 	if (!data->loaded)
 		return; // If never loaded (-nodata), don't save
 
-	save_p = savebuffer = (UINT8 *)malloc(GAMEDATASIZE);
-	if (!save_p)
+	data_p = savebuffer = (UINT8 *)malloc(GAMEDATASIZE);
+	if (!data_p)
 	{
 		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for saving game data\n"));
 		return;
@@ -4631,20 +4633,20 @@ void G_SaveGameData(gamedata_t *data)
 	if (usedCheats)
 	{
 		free(savebuffer);
-		save_p = savebuffer = NULL;
+		savebuffer = NULL;
 		return;
 	}
 
 	// Version test
-	WRITEUINT32(save_p, GAMEDATA_ID);
+	WRITEUINT32(data_p, GAMEDATA_ID);
 
-	WRITEUINT32(save_p, data->totalplaytime);
+	WRITEUINT32(data_p, data->totalplaytime);
 
-	WRITEUINT32(save_p, quickncasehash(timeattackfolder, sizeof timeattackfolder));
+	WRITEUINT32(data_p, quickncasehash(timeattackfolder, sizeof timeattackfolder));
 
 	// TODO put another cipher on these things? meh, I don't care...
 	for (i = 0; i < NUMMAPS; i++)
-		WRITEUINT8(save_p, (data->mapvisited[i] & MV_MAX));
+		WRITEUINT8(data_p, (data->mapvisited[i] & MV_MAX));
 
 	// To save space, use one bit per collected/achieved/unlocked flag
 	for (i = 0; i < MAXEMBLEMS;)
@@ -4652,7 +4654,7 @@ void G_SaveGameData(gamedata_t *data)
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXEMBLEMS; ++j)
 			btemp |= (data->collected[j+i] << j);
-		WRITEUINT8(save_p, btemp);
+		WRITEUINT8(data_p, btemp);
 		i += j;
 	}
 	for (i = 0; i < MAXEXTRAEMBLEMS;)
@@ -4660,7 +4662,7 @@ void G_SaveGameData(gamedata_t *data)
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXEXTRAEMBLEMS; ++j)
 			btemp |= (data->extraCollected[j+i] << j);
-		WRITEUINT8(save_p, btemp);
+		WRITEUINT8(data_p, btemp);
 		i += j;
 	}
 	for (i = 0; i < MAXUNLOCKABLES;)
@@ -4668,7 +4670,7 @@ void G_SaveGameData(gamedata_t *data)
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j)
 			btemp |= (data->unlocked[j+i] << j);
-		WRITEUINT8(save_p, btemp);
+		WRITEUINT8(data_p, btemp);
 		i += j;
 	}
 	for (i = 0; i < MAXCONDITIONSETS;)
@@ -4676,30 +4678,30 @@ void G_SaveGameData(gamedata_t *data)
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXCONDITIONSETS; ++j)
 			btemp |= (data->achieved[j+i] << j);
-		WRITEUINT8(save_p, btemp);
+		WRITEUINT8(data_p, btemp);
 		i += j;
 	}
 
-	WRITEUINT32(save_p, data->timesBeaten);
-	WRITEUINT32(save_p, data->timesBeatenWithEmeralds);
-	WRITEUINT32(save_p, data->timesBeatenUltimate);
+	WRITEUINT32(data_p, data->timesBeaten);
+	WRITEUINT32(data_p, data->timesBeatenWithEmeralds);
+	WRITEUINT32(data_p, data->timesBeatenUltimate);
 
 	// Main records
 	for (i = 0; i < NUMMAPS; i++)
 	{
 		if (data->mainrecords[i])
 		{
-			WRITEUINT32(save_p, data->mainrecords[i]->score);
-			WRITEUINT32(save_p, data->mainrecords[i]->time);
-			WRITEUINT16(save_p, data->mainrecords[i]->rings);
+			WRITEUINT32(data_p, data->mainrecords[i]->score);
+			WRITEUINT32(data_p, data->mainrecords[i]->time);
+			WRITEUINT16(data_p, data->mainrecords[i]->rings);
 		}
 		else
 		{
-			WRITEUINT32(save_p, 0);
-			WRITEUINT32(save_p, 0);
-			WRITEUINT16(save_p, 0);
+			WRITEUINT32(data_p, 0);
+			WRITEUINT32(data_p, 0);
+			WRITEUINT16(data_p, 0);
 		}
-		WRITEUINT8(save_p, 0); // compat
+		WRITEUINT8(data_p, 0); // compat
 	}
 
 	// NiGHTS records
@@ -4707,25 +4709,25 @@ void G_SaveGameData(gamedata_t *data)
 	{
 		if (!data->nightsrecords[i] || !data->nightsrecords[i]->nummares)
 		{
-			WRITEUINT8(save_p, 0);
+			WRITEUINT8(data_p, 0);
 			continue;
 		}
 
-		WRITEUINT8(save_p, data->nightsrecords[i]->nummares);
+		WRITEUINT8(data_p, data->nightsrecords[i]->nummares);
 
 		for (curmare = 0; curmare < (data->nightsrecords[i]->nummares + 1); ++curmare)
 		{
-			WRITEUINT32(save_p, data->nightsrecords[i]->score[curmare]);
-			WRITEUINT8(save_p, data->nightsrecords[i]->grade[curmare]);
-			WRITEUINT32(save_p, data->nightsrecords[i]->time[curmare]);
+			WRITEUINT32(data_p, data->nightsrecords[i]->score[curmare]);
+			WRITEUINT8(data_p, data->nightsrecords[i]->grade[curmare]);
+			WRITEUINT32(data_p, data->nightsrecords[i]->time[curmare]);
 		}
 	}
 
-	length = save_p - savebuffer;
+	length = data_p - savebuffer;
 
 	FIL_WriteFile(va(pandf, srb2home, gamedatafilename), savebuffer, length);
 	free(savebuffer);
-	save_p = savebuffer = NULL;
+	savebuffer = NULL;
 }
 
 #define VERSIONSIZE 16
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index be0c7ba629087e872b01ac51c8996ada8e78b314..828f247674b4a85aae18de777aa558b0c9f6790d 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -5514,7 +5514,10 @@ static void HWR_ProjectSprite(mobj_t *thing)
 		translation = thing->translation;
 
 	//Hurdler: 25/04/2000: now support colormap in hardware mode
-	vis->colormap = R_GetTranslationForThing(vis->mobj, color, translation);
+	if ((thing->flags2 & MF2_LINKDRAW) && thing->tracer)
+		vis->colormap = R_GetTranslationForThing(thing->tracer, color, translation);
+	else
+		vis->colormap = R_GetTranslationForThing(thing, color, translation);
 
 	// set top/bottom coords
 	vis->gzt = gzt;
diff --git a/src/lua_blockmaplib.c b/src/lua_blockmaplib.c
index 8d47f3dc1e31a092f8a9a25e9e9fe667ca178092..6b4b6229f68ffc869e1f6eccdd6999018d150ae0 100644
--- a/src/lua_blockmaplib.c
+++ b/src/lua_blockmaplib.c
@@ -34,46 +34,28 @@ typedef UINT8 (*blockmap_func)(lua_State *, INT32, INT32, mobj_t *);
 static boolean blockfuncerror = false; // errors should only print once per search blockmap call
 
 // Helper function for "objects" search
-static UINT8 lib_searchBlockmap_Objects(lua_State *L, INT32 x, INT32 y, mobj_t *thing)
+static UINT8 lib_searchBlockmap_Objects(lua_State *L, mobj_t *thing, mobj_t *mobj)
 {
-	mobj_t *mobj, *bnext = NULL;
-
-	if (x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight)
-		return 0;
-
-	// Check interaction with the objects in the blockmap.
-	for (mobj = blocklinks[y*bmapwidth + x]; mobj; mobj = bnext)
-	{
-		P_SetTarget(&bnext, mobj->bnext); // We want to note our reference to bnext here incase it is MF_NOTHINK and gets removed!
-		if (mobj == thing)
-			continue; // our thing just found itself, so move on
-		lua_pushvalue(L, 1); // push function
-		LUA_PushUserdata(L, thing, META_MOBJ);
-		LUA_PushUserdata(L, mobj, META_MOBJ);
-		if (lua_pcall(gL, 2, 1, 0)) {
-			if (!blockfuncerror || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			blockfuncerror = true;
-			P_SetTarget(&bnext, NULL);
-			return 0; // *shrugs*
-		}
-		if (!lua_isnil(gL, -1))
-		{ // if nil, continue
-			P_SetTarget(&bnext, NULL);
-			if (lua_toboolean(gL, -1))
-				return 2; // stop whole search
-			else
-				return 1; // stop block search
-		}
+	lua_pushvalue(L, 1); // push function
+	LUA_PushUserdata(L, thing, META_MOBJ);
+	LUA_PushUserdata(L, mobj, META_MOBJ);
+	if (lua_pcall(gL, 2, 1, 0)) {
+		if (!blockfuncerror || cv_debug & DBG_LUA)
+			CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
 		lua_pop(gL, 1);
-		if (P_MobjWasRemoved(thing) // func just popped our thing, cannot continue.
-		|| (bnext && P_MobjWasRemoved(bnext))) // func just broke blockmap chain, cannot continue.
-		{
-			P_SetTarget(&bnext, NULL);
-			return (P_MobjWasRemoved(thing)) ? 2 : 1;
-		}
+		blockfuncerror = true;
+		return 0; // *shrugs*
 	}
+	if (!lua_isnil(gL, -1))
+	{ // if nil, continue
+		if (lua_toboolean(gL, -1))
+			return 2; // stop whole search
+		else
+			return 1; // stop block search
+	}
+	lua_pop(gL, 1);
+	if (P_MobjWasRemoved(thing)) // func just popped our thing, cannot continue.
+		return P_MobjWasRemoved(thing) ? 2 : 1;
 	return 0;
 }
 
@@ -233,6 +215,7 @@ static int lib_searchBlockmap(lua_State *L)
 	boolean retval = true;
 	UINT8 funcret = 0;
 	blockmap_func searchFunc;
+	boolean searchObjects = false;
 
 	lua_remove(L, 1); // remove searchtype, stack is now function, mobj, [x1, x2, y1, y2]
 	luaL_checktype(L, 1, LUA_TFUNCTION);
@@ -241,7 +224,8 @@ static int lib_searchBlockmap(lua_State *L)
 	{
 		case 0: // "objects"
 		default:
-			searchFunc = lib_searchBlockmap_Objects;
+			searchObjects = true;
+			searchFunc = NULL;
 			break;
 		case 1: // "lines"
 			searchFunc = lib_searchBlockmap_Lines;
@@ -286,24 +270,66 @@ static int lib_searchBlockmap(lua_State *L)
 	BMBOUNDFIX(xl, xh, yl, yh);
 
 	blockfuncerror = false; // reset
-	validcount++;
-	for (bx = xl; bx <= xh; bx++)
-		for (by = yl; by <= yh; by++)
-		{
-			funcret = searchFunc(L, bx, by, mobj);
-			// return value of searchFunc determines searchFunc's return value and/or when to stop
-			if (funcret == 2){ // stop whole search
-				lua_pushboolean(L, false); // return false
-				return 1;
+
+	if (!searchObjects) {
+		validcount++;
+
+		for (bx = xl; bx <= xh; bx++)
+			for (by = yl; by <= yh; by++)
+			{
+				funcret = searchFunc(L, bx, by, mobj);
+
+				// return value of searchFunc determines searchFunc's return value and/or when to stop
+				if (funcret == 2) { // stop whole search
+					lua_pushboolean(L, false); // return false
+					return 1;
+				}
+				else if (funcret == 1) // search was interrupted for this block
+					retval = false; // this changes the return value, but doesn't stop the whole search
+
+				// else don't do anything, continue as normal
+				if (P_MobjWasRemoved(mobj)) { // ...unless the original object was removed
+					lua_pushboolean(L, false); // in which case we have to stop now regardless
+					return 1;
+				}
 			}
-			else if (funcret == 1) // search was interrupted for this block
-				retval = false; // this changes the return value, but doesn't stop the whole search
-			// else don't do anything, continue as normal
-			if (P_MobjWasRemoved(mobj)){ // ...unless the original object was removed
-				lua_pushboolean(L, false); // in which case we have to stop now regardless
-				return 1;
+	}
+	else {
+		bthingit_t *it = P_NewBlockThingsIterator(xl, yl, xh, yh);
+		if (!it) {
+			lua_pushboolean(L, false);
+			return 1;
+		}
+
+		mobj_t *itmobj = NULL;
+
+		do
+		{
+			itmobj = P_BlockThingsIteratorNext(it, false);
+			if (itmobj)
+			{
+				if (mobj == itmobj)
+					continue; // our thing just found itself, so move on
+
+				funcret = lib_searchBlockmap_Objects(L, mobj, itmobj);
+				if (funcret == 2) {
+					lua_pushboolean(L, false);
+					return 1;
+				}
+				else if (funcret == 1)
+					retval = false;
+
+				if (P_MobjWasRemoved(mobj)) {
+					retval = false;
+					break;
+				}
 			}
 		}
+		while (itmobj != NULL);
+
+		P_FreeBlockThingsIterator(it);
+	}
+
 	lua_pushboolean(L, retval);
 	return 1;
 }
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 5b80d4d38c9db705c59c7003f37af2ce87bb0272..6b489f22b1939941c0ebf83a5a3ae0b7e215de5a 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -486,6 +486,8 @@ static int lib_iterateSectorThinglist(lua_State *L)
 	if (!lua_isnil(L, 1))
 	{
 		thing = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+		if (P_MobjWasRemoved(thing))
+			return luaL_error(L, "current entry in thinglist was removed; avoid calling P_RemoveMobj on entries!");
 		thing = thing->snext;
 	}
 	else
@@ -2609,12 +2611,18 @@ static int slope_set(lua_State *L)
 			slope->o.z = luaL_checkfixed(L, -1);
 		else
 			slope->o.z = 0;
+		DVector3_Load(&slope->dorigin,
+			FixedToDouble(slope->o.x),
+			FixedToDouble(slope->o.y),
+			FixedToDouble(slope->o.z)
+		);
 		lua_pop(L, 1);
 		break;
 	}
-	case slope_zdelta: { // zdelta, this is temp until i figure out wtf to do
+	case slope_zdelta: { // zdelta
 		slope->zdelta = luaL_checkfixed(L, 3);
 		slope->zangle = R_PointToAngle2(0, 0, FRACUNIT, -slope->zdelta);
+		slope->dzdelta = FixedToDouble(slope->zdelta);
 		P_CalculateSlopeNormal(slope);
 		break;
 	}
@@ -2624,6 +2632,7 @@ static int slope_set(lua_State *L)
 			return luaL_error(L, "invalid zangle for slope!");
 		slope->zangle = zangle;
 		slope->zdelta = -FINETANGENT(((slope->zangle+ANGLE_90)>>ANGLETOFINESHIFT) & 4095);
+		slope->dzdelta = FixedToDouble(slope->zdelta);
 		P_CalculateSlopeNormal(slope);
 		break;
 	}
@@ -2631,6 +2640,8 @@ static int slope_set(lua_State *L)
 		slope->xydirection = luaL_checkangle(L, 3);
 		slope->d.x = -FINECOSINE((slope->xydirection>>ANGLETOFINESHIFT) & FINEMASK);
 		slope->d.y = -FINESINE((slope->xydirection>>ANGLETOFINESHIFT) & FINEMASK);
+		slope->dnormdir.x = FixedToDouble(slope->d.x);
+		slope->dnormdir.y = FixedToDouble(slope->d.y);
 		P_CalculateSlopeNormal(slope);
 		break;
 	}
diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c
index 85e4590c571e23cfa701f6f9fb15b5948350e1f0..5a3f8ad114b9f1269e3a49b76cb4aa9dd76417ad 100644
--- a/src/lua_mobjlib.c
+++ b/src/lua_mobjlib.c
@@ -355,8 +355,12 @@ static int mobj_get(lua_State *L)
 		lua_pushinteger(L, mo->blendmode);
 		break;
 	case mobj_bnext:
-		LUA_PushUserdata(L, mo->bnext, META_MOBJ);
-		break;
+		if (mo->blocknode && mo->blocknode->bnext) {
+			LUA_PushUserdata(L, mo->blocknode->bnext->mobj, META_MOBJ);
+			break;
+		}
+		else
+			return 0;
 	case mobj_bprev:
 		// bprev -- same deal as sprev above, but for the blockmap.
 		return UNIMPLEMENTED;
@@ -669,7 +673,6 @@ static int mobj_set(lua_State *L)
 				sector_list = NULL;
 			}
 			mo->snext = NULL, mo->sprev = NULL;
-			mo->bnext = NULL, mo->bprev = NULL;
 			P_SetThingPosition(mo);
 		}
 		else
diff --git a/src/lua_script.c b/src/lua_script.c
index 7db9624407818feaba654f6326690a79e5d243b6..2e65dfc2522c590c5cdb82651ffc5dbc474e6f3d 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -1501,8 +1501,11 @@ static void ArchiveTables(void)
 		{
 			// Write key
 			e = ArchiveValue(TABLESINDEX, -2); // key should be either a number or a string, ArchiveValue can handle this.
-			if (e == 2) // invalid key type (function, thread, lightuserdata, or anything we don't recognise)
+			if (e == 1)
+				n++; // the table contained a new table we'll have to archive. :(
+			else if (e == 2) // invalid key type (function, thread, lightuserdata, or anything we don't recognise)
 				CONS_Alert(CONS_ERROR, "Index '%s' (%s) of table %d could not be archived!\n", lua_tostring(gL, -2), luaL_typename(gL, -2), i);
+
 			// Write value
 			e = ArchiveValue(TABLESINDEX, -1);
 			if (e == 1)
@@ -1717,10 +1720,15 @@ static void UnArchiveTables(void)
 		lua_rawgeti(gL, TABLESINDEX, i);
 		while (true)
 		{
-			if (UnArchiveValue(TABLESINDEX) == 1) // read key
+			UINT8 e = UnArchiveValue(TABLESINDEX); // read key
+			if (e == 1) // End of table
 				break;
+			else if (e == 2) // Key contains a new table
+				n++;
+
 			if (UnArchiveValue(TABLESINDEX) == 2) // read value
 				n++;
+
 			if (lua_isnil(gL, -2)) // if key is nil (if a function etc was accidentally saved)
 			{
 				CONS_Alert(CONS_ERROR, "A nil key in table %d was found! (Invalid key type or corrupted save?)\n", i);
diff --git a/src/m_fixed.c b/src/m_fixed.c
index b674e3b2c8e230524d57ea540dada83b8bf75eb6..19c1e80919315071fe40086dc93ec42b937ffb26 100644
--- a/src/m_fixed.c
+++ b/src/m_fixed.c
@@ -3,6 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 2009 by Stephen McGranahan.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_fixed.h b/src/m_fixed.h
index 4e644c9b6d5db3687a211526e3a50a5f03e9702f..f40c7b30870baffdd978cec0ecb447b65f74ba6f 100644
--- a/src/m_fixed.h
+++ b/src/m_fixed.h
@@ -50,6 +50,20 @@ FUNCMATH FUNCINLINE static ATTRINLINE fixed_t FloatToFixed(float f)
 	return (fixed_t)(f * FRACUNIT);
 }
 
+/*!
+  \brief convert fixed_t into double-precision floating number
+*/
+
+FUNCMATH FUNCINLINE static ATTRINLINE double FixedToDouble(fixed_t x)
+{
+	return x / (double)FRACUNIT;
+}
+
+FUNCMATH FUNCINLINE static ATTRINLINE fixed_t DoubleToFixed(double f)
+{
+	return (fixed_t)(f * FRACUNIT);
+}
+
 // for backwards compat
 #define FIXED_TO_FLOAT(x) FixedToFloat(x) // (((float)(x)) / ((float)FRACUNIT))
 #define FLOAT_TO_FIXED(f) FloatToFixed(f) // (fixed_t)((f) * ((float)FRACUNIT))
diff --git a/src/m_vector.c b/src/m_vector.c
new file mode 100644
index 0000000000000000000000000000000000000000..3132a869d458c83aac517ac609258b506b6395b8
--- /dev/null
+++ b/src/m_vector.c
@@ -0,0 +1,53 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Sonic Team Junior.
+// Copyright (C) 2009 by Stephen McGranahan.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  m_vector.c
+/// \brief Basic vector functions
+
+#include "doomdef.h"
+
+#include "m_vector.h"
+
+void DVector3_Load(dvector3_t *vec, double x, double y, double z)
+{
+	vec->x = x;
+	vec->y = y;
+	vec->z = z;
+}
+
+double DVector3_Magnitude(const dvector3_t *a_normal)
+{
+	double xs = a_normal->x * a_normal->x;
+	double ys = a_normal->y * a_normal->y;
+	double zs = a_normal->z * a_normal->z;
+	return sqrt(xs + ys + zs);
+}
+
+double DVector3_Normalize(dvector3_t *a_normal)
+{
+	double magnitude = DVector3_Magnitude(a_normal);
+	a_normal->x /= magnitude;
+	a_normal->y /= magnitude;
+	a_normal->z /= magnitude;
+	return magnitude;
+}
+
+void DVector3_Negate(dvector3_t *a_o)
+{
+	a_o->x = -a_o->x;
+	a_o->y = -a_o->y;
+	a_o->z = -a_o->z;
+}
+
+void DVector3_Cross(const dvector3_t *a_1, const dvector3_t *a_2, dvector3_t *a_o)
+{
+	a_o->x = (a_1->y * a_2->z) - (a_1->z * a_2->y);
+	a_o->y = (a_1->z * a_2->x) - (a_1->x * a_2->z);
+	a_o->z = (a_1->x * a_2->y) - (a_1->y * a_2->x);
+}
diff --git a/src/m_vector.h b/src/m_vector.h
new file mode 100644
index 0000000000000000000000000000000000000000..55669be037c9ce54ffd70bf3782225e913e1a1b4
--- /dev/null
+++ b/src/m_vector.h
@@ -0,0 +1,27 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2024 by Sonic Team Junior.
+// Copyright (C) 2009 by Stephen McGranahan.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  m_vector.h
+/// \brief Basic vector functions
+
+#ifndef __M_VECTOR__
+#define __M_VECTOR__
+
+typedef struct
+{
+	double x, y, z;
+} dvector3_t;
+
+void DVector3_Load(dvector3_t *vec, double x, double y, double z);
+double DVector3_Magnitude(const dvector3_t *a_normal);
+double DVector3_Normalize(dvector3_t *a_normal);
+void DVector3_Negate(dvector3_t *a_o);
+void DVector3_Cross(const dvector3_t *a_1, const dvector3_t *a_2, dvector3_t *a_o);
+
+#endif
diff --git a/src/netcode/client_connection.c b/src/netcode/client_connection.c
index 907021e7dafddfb3f31b2a2dd850f15d4890a16e..36ed718265b3cfa42acc3ef238c436882bb918a1 100644
--- a/src/netcode/client_connection.c
+++ b/src/netcode/client_connection.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -43,11 +43,45 @@ tic_t firstconnectattempttime = 0;
 UINT8 mynode;
 static void *snake = NULL;
 
-static void CL_DrawConnectionStatusBox(void)
+static boolean IsDownloadingFile(void)
+{
+	if (cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADHTTPFILES)
+		return filedownload.current != -1;
+
+	return false;
+}
+
+static void DrawConnectionStatusBox(void)
 {
 	M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
-	if (cl_mode != CL_CONFIRMCONNECT)
-		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
+
+	if (cl_mode == CL_CONFIRMCONNECT || IsDownloadingFile())
+		return;
+
+	V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
+}
+
+static void DrawFileProgress(fileneeded_t *file, int y)
+{
+	Net_GetNetStat();
+
+	INT32 dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256);
+	if (dldlength > 256)
+		dldlength = 256;
+	V_DrawFill(BASEVIDWIDTH/2-128, y, 256, 8, 111);
+	V_DrawFill(BASEVIDWIDTH/2-128, y, dldlength, 8, 96);
+
+	const char *progress_str;
+	if (file->totalsize >= 1024*1024)
+		progress_str = va(" %.2fMiB/%.2fMiB", (double)file->currentsize / (1024*1024), (double)file->totalsize / (1024*1024));
+	else if (file->totalsize < 1024)
+		progress_str = va(" %4uB/%4uB", file->currentsize, file->totalsize);
+	else
+		progress_str = va(" %.2fKiB/%.2fKiB", (double)file->currentsize / 1024, (double)file->totalsize / 1024);
+
+	V_DrawString(BASEVIDWIDTH/2-128, y, V_20TRANS|V_ALLOWLOWERCASE, progress_str);
+
+	V_DrawRightAlignedString(BASEVIDWIDTH/2+128, y, V_20TRANS|V_MONOSPACE, va("%3.1fK/s ", ((double)getbps)/1024));
 }
 
 //
@@ -55,21 +89,21 @@ static void CL_DrawConnectionStatusBox(void)
 //
 // Keep the local client informed of our status.
 //
-static inline void CL_DrawConnectionStatus(void)
+static void CL_DrawConnectionStatus(void)
 {
 	INT32 ccstime = I_GetTime();
 
 	// Draw background fade
 	V_DrawFadeScreen(0xFF00, 16); // force default
 
-	if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_LOADFILES)
+	if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_DOWNLOADHTTPFILES && cl_mode != CL_LOADFILES)
 	{
 		INT32 animtime = ((ccstime / 4) & 15) + 16;
 		UINT8 palstart;
 		const char *cltext;
 
 		// Draw the bottom box.
-		CL_DrawConnectionStatusBox();
+		DrawConnectionStatusBox();
 
 		if (cl_mode == CL_SEARCHING)
 			palstart = 32; // Red
@@ -78,33 +112,20 @@ static inline void CL_DrawConnectionStatus(void)
 		else
 			palstart = 96; // Green
 
-		if (!(cl_mode == CL_DOWNLOADSAVEGAME && lastfilenum != -1))
+		if (!(cl_mode == CL_DOWNLOADSAVEGAME && filedownload.current != -1))
 			for (INT32 i = 0; i < 16; ++i) // 15 pal entries total.
 				V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-16, 16, 8, palstart + ((animtime - i) & 15));
 
 		switch (cl_mode)
 		{
 			case CL_DOWNLOADSAVEGAME:
-				if (fileneeded && lastfilenum != -1)
+				if (fileneeded && filedownload.current != -1)
 				{
-					UINT32 currentsize = fileneeded[lastfilenum].currentsize;
-					UINT32 totalsize = fileneeded[lastfilenum].totalsize;
-					INT32 dldlength;
+					fileneeded_t *file = &fileneeded[filedownload.current];
 
 					cltext = M_GetText("Downloading game state...");
-					Net_GetNetStat();
-
-					dldlength = (INT32)((currentsize/(double)totalsize) * 256);
-					if (dldlength > 256)
-						dldlength = 256;
-					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
-					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
 
-					V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-						va(" %4uK/%4uK",currentsize>>10,totalsize>>10));
-
-					V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-						va("%3.1fK/s ", ((double)getbps)/1024));
+					DrawFileProgress(file, BASEVIDHEIGHT-16);
 				}
 				else
 					cltext = M_GetText("Waiting to download game state...");
@@ -156,12 +177,11 @@ static inline void CL_DrawConnectionStatus(void)
 			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
 			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, totalfileslength, 8, 96);
 			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-				va(" %2u/%2u Files",loadcompletednum,fileneedednum));
+				va(" %2u/%2u files",loadcompletednum,fileneedednum));
 		}
-		else if (lastfilenum != -1)
+		else if (filedownload.current != -1)
 		{
-			INT32 dldlength;
-			static char tempname[28];
+			char tempname[28];
 			fileneeded_t *file;
 			char *filename;
 
@@ -169,24 +189,16 @@ static inline void CL_DrawConnectionStatus(void)
 				Snake_Draw(snake);
 
 			// Draw the bottom box.
-			CL_DrawConnectionStatusBox();
+			DrawConnectionStatusBox();
 
 			if (fileneeded)
 			{
-				file = &fileneeded[lastfilenum];
+				file = &fileneeded[filedownload.current];
 				filename = file->filename;
 			}
 			else
 				return;
 
-			Net_GetNetStat();
-			dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256);
-			if (dldlength > 256)
-				dldlength = 256;
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
-
-			memset(tempname, 0, sizeof(tempname));
 			// offset filename to just the name only part
 			filename += strlen(filename) - nameonlylength(filename);
 
@@ -199,22 +211,56 @@ static inline void CL_DrawConnectionStatus(void)
 			}
 			else // we can copy the whole thing in safely
 			{
-				strncpy(tempname, filename, sizeof(tempname)-1);
+				strlcpy(tempname, filename, sizeof(tempname));
 			}
 
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
-				va(M_GetText("Downloading \"%s\""), tempname));
-			V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-				va(" %4uK/%4uK",fileneeded[lastfilenum].currentsize>>10,file->totalsize>>10));
-			V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-				va("%3.1fK/s ", ((double)getbps)/1024));
+			// Lactozilla: Disabled, see below change
+			// (also it doesn't really fit on a typical SRB2 screen)
+#if 0
+			const char *download_str = cl_mode == CL_DOWNLOADHTTPFILES
+				? M_GetText("HTTP downloading \"%s\"")
+				: M_GetText("Downloading \"%s\"");
+#else
+			const char *download_str = M_GetText("Downloading \"%s\"");
+#endif
+
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_ALLOWLOWERCASE|V_YELLOWMAP,
+				va(download_str, tempname));
+
+			// Rusty: actually lets do this instead
+			if (cl_mode == CL_DOWNLOADHTTPFILES)
+			{
+				const char *http_source = filedownload.http_source;
+
+				if (strlen(http_source) > sizeof(tempname)-1) // too long to display fully
+				{
+					size_t endhalfpos = strlen(http_source)-10;
+					// display as first 14 chars + ... + last 10 chars
+					// which should add up to 27 if our math(s) is correct
+					snprintf(tempname, sizeof(tempname), "%.14s...%.10s", http_source, http_source+endhalfpos);
+				}
+				else // we can copy the whole thing in safely
+				{
+					strlcpy(tempname, http_source, sizeof(tempname));
+				}
+
+				V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_ALLOWLOWERCASE|V_YELLOWMAP,
+					va(M_GetText("from %s"), tempname));
+			}
+			else
+			{
+				V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_ALLOWLOWERCASE|V_YELLOWMAP,
+					M_GetText("from the server"));
+			}
+
+			DrawFileProgress(file, BASEVIDHEIGHT-16);
 		}
 		else
 		{
 			if (snake)
 				Snake_Draw(snake);
 
-			CL_DrawConnectionStatusBox();
+			DrawConnectionStatusBox();
 			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
 				M_GetText("Waiting to download files..."));
 		}
@@ -479,23 +525,98 @@ void CL_UpdateServerList(boolean internetsearch, INT32 room)
 #endif // MASTERSERVER
 }
 
+static boolean IsFileDownloadable(fileneeded_t *file)
+{
+	return file->status == FS_NOTFOUND || file->status == FS_MD5SUMBAD;
+}
+
+static boolean UseDirectDownloader(void)
+{
+	return filedownload.http_source[0] == '\0' || filedownload.http_failed;
+}
+
+static void DoLoadFiles(void)
+{
+	Snake_Free(&snake);
+
+	cl_mode = CL_LOADFILES;
+}
+
+static void AbortConnection(void)
+{
+	Snake_Free(&snake);
+
+	D_QuitNetGame();
+	CL_Reset();
+	D_StartTitle();
+
+	// Will be reset by caller. Signals refusal.
+	cl_mode = CL_ABORTED;
+}
+
+static void BeginDownload(boolean direct)
+{
+	filedownload.current = 0;
+	filedownload.remaining = 0;
+
+	for (int i = 0; i < fileneedednum; i++)
+	{
+		// Lactozilla: Rusty had fixed this SLIGHTLY incorrectly.
+		// Since [redacted] doesn't HAVE its own file transmission code - it spins an HTTP server - it wasn't
+		// instantly obvious to us where to do this, and we didn't want to check the original implementation.
+		if (fileneeded[i].status == FS_FALLBACK)
+			fileneeded[i].status = FS_NOTFOUND;
+
+		if (IsFileDownloadable(&fileneeded[i]))
+			filedownload.remaining++;
+	}
+
+	if (!filedownload.remaining)
+	{
+		DoLoadFiles();
+		return;
+	}
+
+	if (!direct)
+	{
+		cl_mode = CL_DOWNLOADHTTPFILES;
+		Snake_Allocate(&snake);
+
+		// Discard any paused downloads
+		CL_AbortDownloadResume();
+	}
+	else
+	{
+		// do old LEGACY request
+		if (CL_SendFileRequest())
+		{
+			cl_mode = CL_DOWNLOADFILES;
+
+			// don't alloc snake if already alloced
+			if (!snake)
+				Snake_Allocate(&snake);
+		}
+		else
+		{
+			AbortConnection();
+
+			// why was this its own cl_mode_t?
+			M_StartMessage(M_GetText(
+				"The direct downloader encountered an error.\n"
+				"See the logfile for more info.\n\n"
+				"Press ESC\n"
+			), NULL, MM_NOTHING);
+		}
+	}
+}
+
 static void M_ConfirmConnect(event_t *ev)
 {
 	if (ev->type == ev_keydown)
 	{
 		if (ev->key == ' ' || ev->key == 'y' || ev->key == KEY_ENTER || ev->key == KEY_JOY1)
 		{
-			if (totalfilesrequestednum > 0)
-			{
-				if (CL_SendFileRequest())
-				{
-					cl_mode = CL_DOWNLOADFILES;
-					Snake_Allocate(&snake);
-				}
-			}
-			else
-				cl_mode = CL_LOADFILES;
-
+			BeginDownload(UseDirectDownloader());
 			M_ClearMenus(true);
 		}
 		else if (ev->key == 'n' || ev->key == KEY_ESCAPE || ev->key == KEY_JOY1 + 3)
@@ -506,22 +627,122 @@ static void M_ConfirmConnect(event_t *ev)
 	}
 }
 
-static boolean CL_FinishedFileList(void)
+static const char *GetPrintableFileSize(UINT64 filesize)
 {
-	INT32 i;
-	char *downloadsize = NULL;
+	static char downloadsize[32];
+
+	if (filesize >= 1024*1024)
+		snprintf(downloadsize, sizeof(downloadsize), "%.2fMiB", (double)filesize / (1024*1024));
+	else if (filesize < 1024)
+		snprintf(downloadsize, sizeof(downloadsize), "%sB", sizeu1(filesize));
+	else
+		snprintf(downloadsize, sizeof(downloadsize), "%.2fKiB", (double)filesize / 1024);
+
+	return downloadsize;
+}
+
+static void ShowDownloadConsentMessage(void)
+{
+	UINT64 totalsize = 0;
+
+	filedownload.completednum = 0;
+	filedownload.completedsize = 0;
+
+	if (fileneeded == NULL)
+		I_Error("CL_FinishedFileList: fileneeded == NULL");
+
+	for (int i = 0; i < fileneedednum; i++)
+	{
+		if (IsFileDownloadable(&fileneeded[i]))
+			totalsize += fileneeded[i].totalsize;
+	}
+
+	const char *downloadsize = GetPrintableFileSize(totalsize);
+
+	if (serverisfull)
+		M_StartMessage(va(M_GetText(
+			"This server is full!\n"
+			"Download of %s of additional\ncontent is required to join.\n"
+			"\n"
+			"You may download server addons,\nand wait for a slot.\n"
+			"\n"
+			"Press ENTER to continue\nor ESC to cancel.\n"
+		), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
+	else
+		M_StartMessage(va(M_GetText(
+			"Download of %s of additional\ncontent is required to join.\n"
+			"\n"
+			"Press ENTER to continue\nor ESC to cancel.\n"
+		), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
+
+	cl_mode = CL_CONFIRMCONNECT;
+	curfadevalue = 0;
+}
+
+static const char *GetDirectDownloadFailReason(UINT8 dlstatus)
+{
+	switch (dlstatus)
+	{
+		case DLSTATUS_TOOLARGE:
+			return M_GetText("Some addons are larger than the server is willing to send.");
+		case DLSTATUS_WONTSEND:
+			return M_GetText("The server is not allowing download requests.");
+		case DLSTATUS_NODOWNLOAD:
+			return M_GetText("All addons downloadable, but you have chosen to disable addon downloading.");
+		case DLSTATUS_FOLDER:
+			return M_GetText("One or more addons were added as a folder, which the server cannot send.");
+	}
+
+	return "Unknown reason";
+}
+
+static void HandleDirectDownloadFail(UINT8 dlstatus)
+{
+	// not downloadable, put reason in console
+	CONS_Alert(CONS_NOTICE, M_GetText("You need additional addons to connect to this server:\n"));
+
+	for (UINT8 i = 0; i < fileneedednum; i++)
+	{
+		if (fileneeded[i].folder || (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN))
+		{
+			CONS_Printf(" * \"%s\" ", fileneeded[i].filename);
 
-	//CONS_Printf(M_GetText("Checking files...\n"));
-	i = CL_CheckFiles();
+			if (fileneeded[i].folder)
+			{
+				CONS_Printf("(folder)");
+			}
+			else
+			{
+				CONS_Printf("(%s)", GetPrintableFileSize(fileneeded[i].totalsize));
+
+				if (fileneeded[i].status == FS_NOTFOUND)
+					CONS_Printf(M_GetText(" not found, md5: "));
+				else if (fileneeded[i].status == FS_MD5SUMBAD)
+					CONS_Printf(M_GetText(" wrong version, md5: "));
+
+				char md5tmp[33];
+				for (INT32 j = 0; j < 16; j++)
+					sprintf(&md5tmp[j*2], "%02x", fileneeded[i].md5sum[j]);
+				CONS_Printf("%s", md5tmp);
+			}
+
+			CONS_Printf("\n");
+		}
+	}
+
+	CONS_Printf("%s\n", GetDirectDownloadFailReason(dlstatus));
+}
+
+static boolean CL_FinishedFileList(void)
+{
+	INT32 i = CL_CheckFiles();
 	if (i == 4) // still checking ...
 	{
 		return true;
 	}
 	else if (i == 3) // too many files
 	{
-		D_QuitNetGame();
-		CL_Reset();
-		D_StartTitle();
+		AbortConnection();
 		M_StartMessage(M_GetText(
 			"You have too many WAD files loaded\n"
 			"to add ones the server is using.\n"
@@ -532,15 +753,15 @@ static boolean CL_FinishedFileList(void)
 	}
 	else if (i == 2) // cannot join for some reason
 	{
-		D_QuitNetGame();
-		CL_Reset();
-		D_StartTitle();
+		AbortConnection();
 		M_StartMessage(M_GetText(
-			"You have the wrong addons loaded.\n\n"
+			"You have the wrong addons loaded.\n"
+			"\n"
 			"To play on this server, restart\n"
 			"the game and don't load any addons.\n"
 			"SRB2 will automatically add\n"
-			"everything you need when you join.\n\n"
+			"everything you need when you join.\n"
+			"\n"
 			"Press ESC\n"
 		), NULL, MM_NOTHING);
 		return false;
@@ -566,63 +787,38 @@ static boolean CL_FinishedFileList(void)
 	{
 		// must download something
 		// can we, though?
-		if (!CL_CheckDownloadable()) // nope!
+		// Rusty: always check downloadable
+		UINT8 status = CL_CheckDownloadable(UseDirectDownloader());
+		if (status != DLSTATUS_OK)
 		{
-			D_QuitNetGame();
-			CL_Reset();
-			D_StartTitle();
+			HandleDirectDownloadFail(status);
+			AbortConnection();
 			M_StartMessage(M_GetText(
 				"An error occurred when trying to\n"
 				"download missing addons.\n"
 				"(This is almost always a problem\n"
-				"with the server, not your game.)\n\n"
+				"with the server, not your game.)\n"
+				"\n"
 				"See the console or log file\n"
-				"for additional details.\n\n"
+				"for additional details.\n"
+				"\n"
 				"Press ESC\n"
 			), NULL, MM_NOTHING);
 			return false;
 		}
 
-		downloadcompletednum = 0;
-		downloadcompletedsize = 0;
-		totalfilesrequestednum = 0;
-		totalfilesrequestedsize = 0;
-
-		if (fileneeded == NULL)
-			I_Error("CL_FinishedFileList: fileneeded == NULL");
-
-		for (i = 0; i < fileneedednum; i++)
-			if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
-			{
-				totalfilesrequestednum++;
-				totalfilesrequestedsize += fileneeded[i].totalsize;
-			}
-
-		if (totalfilesrequestedsize>>20 >= 100)
-			downloadsize = Z_StrDup(va("%uM",totalfilesrequestedsize>>20));
-		else
-			downloadsize = Z_StrDup(va("%uK",totalfilesrequestedsize>>10));
-
-		if (serverisfull)
-			M_StartMessage(va(M_GetText(
-				"This server is full!\n"
-				"Download of %s additional content\nis required to join.\n"
-				"\n"
-				"You may download, load server addons,\nand wait for a slot.\n"
-				"\n"
-				"Press ENTER to continue\nor ESC to cancel.\n"
-			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
+		if (!filedownload.http_failed)
+		{
+			// show download consent modal ONCE!
+			ShowDownloadConsentMessage();
+		}
 		else
-			M_StartMessage(va(M_GetText(
-				"Download of %s additional content\nis required to join.\n"
-				"\n"
-				"Press ENTER to continue\nor ESC to cancel.\n"
-			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
-
-		Z_Free(downloadsize);
-		cl_mode = CL_CONFIRMCONNECT;
-		curfadevalue = 0;
+		{
+			// do a direct download
+			BeginDownload(true);
+		}
 	}
+
 	return true;
 }
 
@@ -734,15 +930,18 @@ static boolean CL_ServerConnectionSearchTicker(tic_t *asksent)
 				if (reason)
 				{
 					char *message = Z_StrDup(reason);
-					D_QuitNetGame();
-					CL_Reset();
-					D_StartTitle();
+					AbortConnection();
 					M_StartMessage(message, NULL, MM_NOTHING);
 					Z_Free(message);
 					return false;
 				}
 			}
 
+			if (serverlist[i].info.httpsource[0])
+				strlcpy(filedownload.http_source, serverlist[i].info.httpsource, MAX_MIRROR_LENGTH);
+			else
+				filedownload.http_source[0] = '\0';
+
 			D_ParseFileneeded(info->fileneedednum, info->fileneeded, 0);
 
 			if (info->flags & SV_LOTSOFADDONS)
@@ -773,6 +972,42 @@ static boolean CL_ServerConnectionSearchTicker(tic_t *asksent)
 	return true;
 }
 
+static void HandleHTTPDownloadFail(void)
+{
+	char filename[MAX_WADPATH];
+
+	CONS_Alert(CONS_WARNING, M_GetText("One or more addons failed to download:\n"));
+
+	for (int i = 0; i < fileneedednum; i++)
+	{
+		if (fileneeded[i].failed == FDOWNLOAD_FAIL_NONE)
+			continue;
+
+		strlcpy(filename, fileneeded[i].filename, sizeof filename);
+		nameonly(filename);
+
+		CONS_Printf(" * \"%s\" (%s)", filename, GetPrintableFileSize(fileneeded[i].totalsize));
+
+		if (fileneeded[i].failed == FDOWNLOAD_FAIL_NOTFOUND)
+			CONS_Printf(M_GetText(" not found, md5: "));
+		else if (fileneeded[i].failed == FDOWNLOAD_FAIL_MD5SUMBAD)
+			CONS_Printf(M_GetText(" wrong version, md5: "));
+		else
+			CONS_Printf(M_GetText(" other error, md5: "));
+
+		char md5tmp[33];
+		for (INT32 j = 0; j < 16; j++)
+			snprintf(&md5tmp[j*2], sizeof(md5tmp), "%02x", fileneeded[i].md5sum[j]);
+		CONS_Printf("%s\n", md5tmp);
+
+		fileneeded[i].failed = FDOWNLOAD_FAIL_NONE;
+	}
+
+	CONS_Printf(M_GetText("Falling back to direct downloader.\n"));
+
+	cl_mode = CL_CHECKFILES;
+}
+
 /** Called by CL_ConnectToServer
   *
   * \param tmpsave The name of the gamestate file???
@@ -810,21 +1045,43 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 			if (!CL_FinishedFileList())
 				return false;
 			break;
-		case CL_DOWNLOADFILES:
+
+		case CL_DOWNLOADHTTPFILES:
 			waitmore = false;
-			for (INT32 i = 0; i < fileneedednum; i++)
-				if (fileneeded[i].status == FS_DOWNLOADING
-					|| fileneeded[i].status == FS_REQUESTED)
+			for (int i = filedownload.current; i < fileneedednum; i++)
+			{
+				if (IsFileDownloadable(&fileneeded[i]))
 				{
+					if (!filedownload.http_running)
+					{
+						if (!CURLPrepareFile(filedownload.http_source, i))
+							HandleHTTPDownloadFail();
+					}
 					waitmore = true;
 					break;
 				}
+			}
+
+			// Rusty TODO: multithread
+			if (filedownload.http_running)
+				CURLGetFile();
+
 			if (waitmore)
 				break; // exit the case
 
-			Snake_Free(&snake);
-
-			cl_mode = CL_LOADFILES;
+			// Done downloading files
+			if (!filedownload.remaining)
+			{
+				if (filedownload.http_failed)
+					HandleHTTPDownloadFail();
+				else
+					DoLoadFiles();
+			}
+			break;
+		case CL_DOWNLOADFILES:
+			// Done downloading files
+			if (!filedownload.remaining)
+				DoLoadFiles();
 			break;
 		case CL_LOADFILES:
 			if (CL_LoadServerFiles())
@@ -839,10 +1096,7 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 			if (firstconnectattempttime + NEWTICRATE*300 < I_GetTime() && !server)
 			{
 				CONS_Printf(M_GetText("5 minute wait time exceeded.\n"));
-				CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
+				AbortConnection();
 				M_StartMessage(M_GetText(
 					"5 minute wait time exceeded.\n"
 					"You may retry connection.\n"
@@ -914,15 +1168,12 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 			CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
 			M_StartMessage(M_GetText("Network game synchronization aborted.\n\nPress ESC\n"), NULL, MM_NOTHING);
 
-			Snake_Free(&snake);
+			AbortConnection();
 
-			D_QuitNetGame();
-			CL_Reset();
-			D_StartTitle();
 			memset(gamekeydown, 0, NUMKEYS);
 			return false;
 		}
-		else if (cl_mode == CL_DOWNLOADFILES && snake)
+		else if ((cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADHTTPFILES) && snake)
 			Snake_Update(snake);
 
 		if (client && (cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADSAVEGAME))
@@ -973,7 +1224,7 @@ void CL_ConnectToServer(void)
 
 	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
 
-	lastfilenum = -1;
+	filedownload.current = -1;
 
 	cl_mode = CL_SEARCHING;
 
@@ -1121,14 +1372,9 @@ void PT_ServerRefuse(SINT8 node)
 		M_StartMessage(va(M_GetText("Server refuses connection\n\nReason:\n%s"),
 			reason), NULL, MM_NOTHING);
 
-		D_QuitNetGame();
-		CL_Reset();
-		D_StartTitle();
+		AbortConnection();
 
 		free(reason);
-
-		// Will be reset by caller. Signals refusal.
-		cl_mode = CL_ABORTED;
 	}
 }
 
diff --git a/src/netcode/client_connection.h b/src/netcode/client_connection.h
index 4d75160d4a280061f08410b1f834df049a1b2a3e..ff054236b6ed8ca1b20a3f283c48522d46f8ac10 100644
--- a/src/netcode/client_connection.h
+++ b/src/netcode/client_connection.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -36,7 +36,8 @@ typedef enum
 	CL_CONNECTED,
 	CL_ABORTED,
 	CL_ASKFULLFILELIST,
-	CL_CONFIRMCONNECT
+	CL_CONFIRMCONNECT,
+	CL_DOWNLOADHTTPFILES
 } cl_mode_t;
 
 extern serverelem_t serverlist[MAXSERVERLIST];
diff --git a/src/netcode/commands.c b/src/netcode/commands.c
index e7d51437e42d381827a3e702f9c49a8f9d1780b2..46dfbc741d9c8f036bf53e0ba6d77bed60ac39f3 100644
--- a/src/netcode/commands.c
+++ b/src/netcode/commands.c
@@ -274,7 +274,9 @@ void Command_BanIP(void)
 
 	if (server) // Only the server can use this, otherwise does nothing.
 	{
+		char *addrbuf = NULL;
 		const char *address = (COM_Argv(1));
+		const char *mask = strchr(address, '/');
 		const char *reason;
 
 		if (COM_Argc() == 2)
@@ -282,8 +284,16 @@ void Command_BanIP(void)
 		else
 			reason = COM_Argv(2);
 
+		if (mask != NULL)
+		{
+			addrbuf = Z_Malloc(mask - address + 1, PU_STATIC, NULL);
+			memcpy(addrbuf, address, mask - address);
+			addrbuf[mask - address] = '\0';
+			address = addrbuf;
+			mask++;
+		}
 
-		if (I_SetBanAddress && I_SetBanAddress(address, NULL))
+		if (I_SetBanAddress && I_SetBanAddress(address, mask))
 		{
 			if (reason)
 				CONS_Printf("Banned IP address %s for: %s\n", address, reason);
@@ -295,8 +305,9 @@ void Command_BanIP(void)
 		}
 		else
 		{
-			return;
+			CONS_Printf("Unable to apply ban: address in malformed or invalid, or too many bans are applied\n");
 		}
+		Z_Free(addrbuf);
 	}
 }
 
diff --git a/src/netcode/d_clisrv.c b/src/netcode/d_clisrv.c
index 0d5d3fa90b95075ac7657c8a9dc3f38458593970..d735e8132172e66a5c808ef572b7edca1ae8a8cc 100644
--- a/src/netcode/d_clisrv.c
+++ b/src/netcode/d_clisrv.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -116,6 +116,8 @@ consvar_t cv_playbackspeed = CVAR_INIT ("playbackspeed", "1", 0, playbackspeed_c
 consvar_t cv_idletime = CVAR_INIT ("idletime", "0", CV_SAVE, CV_Unsigned, NULL);
 consvar_t cv_dedicatedidletime = CVAR_INIT ("dedicatedidletime", "10", CV_SAVE, CV_Unsigned, NULL);
 
+consvar_t cv_httpsource = CVAR_INIT ("http_source", "", CV_SAVE, NULL, NULL);
+
 void ResetNode(INT32 node)
 {
 	memset(&netnodes[node], 0, sizeof(*netnodes));
@@ -153,12 +155,15 @@ void CL_Reset(void)
 	FreeFileNeeded();
 	fileneedednum = 0;
 
-	totalfilesrequestednum = 0;
-	totalfilesrequestedsize = 0;
 	firstconnectattempttime = 0;
 	serverisfull = false;
 	connectiontimeout = (tic_t)cv_nettimeout.value; //reset this temporary hack
 
+	filedownload.remaining = 0;
+	filedownload.http_failed = false;
+	filedownload.http_running = false;
+	filedownload.http_source[0] = '\0';
+
 	// D_StartTitle should get done now, but the calling function will handle it
 }
 
@@ -892,6 +897,74 @@ static void PT_Login(SINT8 node, INT32 netconsole)
 #endif
 }
 
+/** Add a login for HTTP downloads. If the
+  * user/password is missing, remove it.
+  *
+  * \sa Command_list_http_logins
+  */
+static void Command_set_http_login (void)
+{
+	HTTP_login  *login;
+	HTTP_login **prev_next;
+
+	if (COM_Argc() < 2)
+	{
+		CONS_Printf(
+				"set_http_login <URL> [user:password]: Set or remove a login to "
+				"authenticate HTTP downloads.\n"
+		);
+		return;
+	}
+
+	login = CURLGetLogin(COM_Argv(1), &prev_next);
+
+	if (COM_Argc() == 2)
+	{
+		if (login)
+		{
+			(*prev_next) = login->next;
+			CONS_Printf("Login for '%s' removed.\n", login->url);
+			Z_Free(login);
+		}
+	}
+	else
+	{
+		if (login)
+			Z_Free(login->auth);
+		else
+		{
+			login = ZZ_Alloc(sizeof *login);
+			login->url = Z_StrDup(COM_Argv(1));
+		}
+
+		login->auth = Z_StrDup(COM_Argv(2));
+
+		login->next = curl_logins;
+		curl_logins = login;
+	}
+}
+
+/** List logins for HTTP downloads.
+  *
+  * \sa Command_set_http_login
+  */
+static void Command_list_http_logins (void)
+{
+	HTTP_login *login;
+
+	for (
+			login = curl_logins;
+			login;
+			login = login->next
+	){
+		CONS_Printf(
+				"'%s' -> '%s'\n",
+				login->url,
+				login->auth
+		);
+	}
+}
+
 static void PT_AskLuaFile(SINT8 node)
 {
 	if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_ASKED)
@@ -1568,6 +1641,8 @@ void D_ClientServerInit(void)
 	COM_AddCommand("reloadbans", Command_ReloadBan, COM_LUA);
 	COM_AddCommand("connect", Command_connect, COM_LUA);
 	COM_AddCommand("nodes", Command_Nodes, COM_LUA);
+	COM_AddCommand("set_http_login", Command_set_http_login, 0);
+	COM_AddCommand("list_http_logins", Command_list_http_logins, 0);
 	COM_AddCommand("resendgamestate", Command_ResendGamestate, COM_LUA);
 #ifdef PACKETDROP
 	COM_AddCommand("drop", Command_Drop, COM_LUA);
diff --git a/src/netcode/d_clisrv.h b/src/netcode/d_clisrv.h
index 61823e65d29f3856a807877478490d9e91084c1a..5aac4693d2fdd166cee7a796166ba7c798e183c8 100644
--- a/src/netcode/d_clisrv.h
+++ b/src/netcode/d_clisrv.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -74,6 +74,7 @@ extern UINT32 playerpingtable[MAXPLAYERS];
 extern tic_t servermaxping;
 
 extern consvar_t cv_netticbuffer, cv_resynchattempts, cv_blamecfail, cv_playbackspeed, cv_idletime, cv_dedicatedidletime;
+extern consvar_t cv_httpsource;
 
 // Used in d_net, the only dependence
 void D_ClientServerInit(void);
diff --git a/src/netcode/d_netcmd.c b/src/netcode/d_netcmd.c
index 3cda178959cfc930bae14d1c6fc7ae0e86a70110..87f0110a9490d8a85e88c39d51d745ccede2af0d 100644
--- a/src/netcode/d_netcmd.c
+++ b/src/netcode/d_netcmd.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -606,6 +606,7 @@ void D_RegisterServerCommands(void)
 	CV_RegisterVar(&cv_blamecfail);
 	CV_RegisterVar(&cv_dedicatedidletime);
 	CV_RegisterVar(&cv_idletime);
+	CV_RegisterVar(&cv_httpsource);
 
 	COM_AddCommand("ping", Command_Ping_f, COM_LUA);
 	CV_RegisterVar(&cv_nettimeout);
diff --git a/src/netcode/d_netfil.c b/src/netcode/d_netfil.c
index 7782939c3804d4ab044eedeb9aabe428fd81a55e..03ad8303e6571a477b9f107154d9ed3102ac58a9 100644
--- a/src/netcode/d_netfil.c
+++ b/src/netcode/d_netfil.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -10,6 +10,10 @@
 /// \file  d_netfil.c
 /// \brief Transfer a file using HSendPacket.
 
+#ifdef HAVE_CURL
+#include <curl/curl.h>
+#endif
+
 #include <stdio.h>
 #include <sys/stat.h>
 
@@ -104,12 +108,19 @@ typedef struct
 } pauseddownload_t;
 static pauseddownload_t *pauseddownload = NULL;
 
-// for cl loading screen
-INT32 lastfilenum = -1;
-INT32 downloadcompletednum = 0;
-UINT32 downloadcompletedsize = 0;
-INT32 totalfilesrequestednum = 0;
-UINT32 totalfilesrequestedsize = 0;
+file_download_t filedownload;
+
+static CURL *http_handle;
+static CURLM *multi_handle;
+static UINT32 curl_dlnow;
+static UINT32 curl_dltotal;
+static time_t curl_starttime;
+static int curl_runninghandles = 0;
+static UINT32 curl_origfilesize;
+static UINT32 curl_origtotalfilesize;
+static char *curl_realname = NULL;
+static fileneeded_t *curl_curfile = NULL;
+HTTP_login *curl_logins;
 
 luafiletransfer_t *luafiletransfers = NULL;
 boolean waitingforluafiletransfer = false;
@@ -140,6 +151,13 @@ static UINT16 GetWadNumFromFileNeededId(UINT8 id)
 	return UINT16_MAX;
 }
 
+enum
+{
+	WILLSEND_YES,
+	WILLSEND_NO,
+	WILLSEND_TOOLARGE
+};
+
 /** Fills a serverinfo packet with information about wad files loaded.
   *
   * \todo Give this function a better name since it is in global scope.
@@ -186,9 +204,9 @@ UINT8 *PutFileNeeded(UINT16 firstfile)
 		{
 			// Store in the upper four bits
 			if (!cv_downloading.value)
-				filestatus += (2 << 4); // Won't send
+				filestatus += (WILLSEND_NO << 4); // Won't send
 			else if (wadfiles[i]->filesize <= (UINT32)cv_maxsend.value * 1024)
-				filestatus += (1 << 4); // Will send if requested
+				filestatus += (WILLSEND_YES << 4); // Will send if requested
 			// else
 				// filestatus += (0 << 4); -- Won't send, too big
 		}
@@ -250,6 +268,7 @@ void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 fi
 		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
+		fileneeded[i].failed = FDOWNLOAD_FAIL_NONE;
 		READSTRINGN(p, fileneeded[i].filename, MAX_WADPATH); // The next bytes are the file name
 		READMEM(p, fileneeded[i].md5sum, 16); // The last 16 bytes are the file checksum
 	}
@@ -257,7 +276,7 @@ void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 fi
 
 void CL_PrepareDownloadSaveGame(const char *tmpsave)
 {
-	lastfilenum = -1;
+	filedownload.current = -1;
 
 	FreeFileNeeded();
 	AllocFileNeeded(1);
@@ -275,86 +294,37 @@ void CL_PrepareDownloadSaveGame(const char *tmpsave)
 /** Checks the server to see if we CAN download all the files,
   * before starting to create them and requesting.
   *
+  * \param direct Game will do a direct download
   * \return True if we can download all the files
   *
   */
-boolean CL_CheckDownloadable(void)
+UINT8 CL_CheckDownloadable(boolean direct)
 {
-	UINT8 dlstatus = 0;
+	UINT8 dlstatus = DLSTATUS_OK;
 
 	for (UINT8 i = 0; i < fileneedednum; i++)
 		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN)
 		{
 			if (fileneeded[i].folder)
 			{
-				dlstatus = 4;
+				dlstatus = DLSTATUS_FOLDER;
 				break;
 			}
 
-			if (fileneeded[i].willsend == 1)
+			if (!direct || fileneeded[i].willsend == WILLSEND_YES)
 				continue;
 
-			if (fileneeded[i].willsend == 0)
-				dlstatus = 1;
-			else //if (fileneeded[i].willsend == 2)
-				dlstatus = 2;
+			if (fileneeded[i].willsend == WILLSEND_TOOLARGE)
+				dlstatus = DLSTATUS_TOOLARGE;
+			else //if (fileneeded[i].willsend == WILLSEND_NO)
+				dlstatus = DLSTATUS_WONTSEND;
 		}
 
 	// Downloading locally disabled
-	if (!dlstatus && M_CheckParm("-nodownload"))
-		dlstatus = 3;
-
-	if (!dlstatus)
-		return true;
-
-	// not downloadable, put reason in console
-	CONS_Alert(CONS_NOTICE, M_GetText("You need additional addons to connect to this server:\n"));
-
-	for (UINT8 i = 0; i < fileneedednum; i++)
-	{
-		if (fileneeded[i].folder || (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN))
-		{
-			CONS_Printf(" * \"%s\" ", fileneeded[i].filename);
-
-			if (fileneeded[i].folder)
-			{
-				CONS_Printf("(folder)");
-			}
-			else
-			{
-				CONS_Printf("(%dK)", fileneeded[i].totalsize >> 10);
-
-				if (fileneeded[i].status == FS_NOTFOUND)
-					CONS_Printf(M_GetText(" not found, md5: "));
-				else if (fileneeded[i].status == FS_MD5SUMBAD)
-					CONS_Printf(M_GetText(" wrong version, md5: "));
-
-				char md5tmp[33];
-				for (INT32 j = 0; j < 16; j++)
-					sprintf(&md5tmp[j*2], "%02x", fileneeded[i].md5sum[j]);
-				CONS_Printf("%s", md5tmp);
-			}
-
-			CONS_Printf("\n");
-		}
-	}
+	if (direct && !dlstatus && M_CheckParm("-nodownload"))
+		dlstatus = DLSTATUS_NODOWNLOAD;
 
-	switch (dlstatus)
-	{
-		case 1:
-			CONS_Printf(M_GetText("Some addons are larger than the server is willing to send.\n"));
-			break;
-		case 2:
-			CONS_Printf(M_GetText("The server is not allowing download requests.\n"));
-			break;
-		case 3:
-			CONS_Printf(M_GetText("All addons downloadable, but you have chosen to disable addon downloading.\n"));
-			break;
-		case 4:
-			CONS_Printf(M_GetText("One or more addons were added as a folder, which the server cannot send.\n"));
-			break;
-	}
-	return false;
+	return dlstatus;
 }
 
 /** Returns true if a needed file transfer can be resumed
@@ -396,23 +366,42 @@ boolean CL_SendFileRequest(void)
 
 #ifdef PARANOIA
 	if (M_CheckParm("-nodownload"))
-		I_Error("Attempted to download files in -nodownload mode");
+	{
+		CONS_Printf("Attempted to download files in -nodownload mode");
+		return false;
+	}
+#endif
 
 	for (INT32 i = 0; i < fileneedednum; i++)
+	{
 		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN
-			&& (fileneeded[i].willsend == 0 || fileneeded[i].willsend == 2))
+			&& (fileneeded[i].willsend == WILLSEND_TOOLARGE || fileneeded[i].willsend == WILLSEND_NO || fileneeded[i].folder))
 		{
-			I_Error("Attempted to download files that were not sendable");
+			CONS_Printf("Attempted to download files that were not sendable");
+			return false;
 		}
-#endif
+
+		if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
+		{
+			// Error check for the first time around.
+			totalfreespaceneeded += fileneeded[i].totalsize;
+		}
+	}
+
+	I_GetDiskFreeSpace(&availablefreespace);
+	if (totalfreespaceneeded > availablefreespace)
+	{
+		CONS_Printf("To play on this server you must download %s KB,\n"
+			"but you have only %s KB free space on your device\n",
+			sizeu1((size_t)(totalfreespaceneeded>>10)), sizeu2((size_t)(availablefreespace>>10)));
+		return false;
+	}
 
 	netbuffer->packettype = PT_REQUESTFILE;
 	p = (char *)netbuffer->u.textcmd;
 	for (INT32 i = 0; i < fileneedednum; i++)
 		if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
 		{
-			totalfreespaceneeded += fileneeded[i].totalsize;
-
 			WRITEUINT8(p, i); // fileid
 
 			// put it in download dir
@@ -424,15 +413,16 @@ boolean CL_SendFileRequest(void)
 
 	WRITEUINT8(p, 0xFF);
 
-	I_GetDiskFreeSpace(&availablefreespace);
-	if (totalfreespaceneeded > availablefreespace)
-		I_Error("To play on this server you must download %s KB,\n"
-			"but you have only %s KB free space on this drive\n",
-			sizeu1((size_t)(totalfreespaceneeded>>10)), sizeu2((size_t)(availablefreespace>>10)));
+	if (!HSendPacket(servernode, true, 0, p - (char *)netbuffer->u.textcmd))
+	{
+		CONS_Printf("Could not send download request packet to server\n");
+		return false;
+	}
 
 	// prepare to download
 	I_mkdir(downloaddir, 0755);
-	return HSendPacket(servernode, true, 0, p - (char *)netbuffer->u.textcmd);
+
+	return true;
 }
 
 // get request filepak and put it on the send queue
@@ -514,7 +504,7 @@ INT32 CL_CheckFiles(void)
 
 	for (i = 0; i < fileneedednum; i++)
 	{
-		if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
+		if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD || fileneeded[i].status == FS_FALLBACK)
 			downloadrequired = true;
 
 		if (fileneeded[i].status != FS_OPEN)
@@ -932,8 +922,6 @@ boolean AddLuaFileToSendQueue(INT32 node, const char *filename)
 {
 	filetx_t **q; // A pointer to the "next" field of the last file in the list
 	filetx_t *p; // The new file request
-	//INT32 i;
-	//char wadfilename[MAX_WADPATH];
 
 	luafiletransfers->nodestatus[node] = LFTNS_SENDING;
 
@@ -1306,6 +1294,21 @@ void FileReceiveTicker(void)
 	}
 }
 
+static void OpenNewFileForDownload(fileneeded_t *file, const char *filename)
+{
+	file->file = fopen(filename, "wb");
+	if (!file->file)
+		I_Error("Can't create file %s: %s", filename, strerror(errno));
+
+	file->currentsize = 0;
+	file->totalsize = LONG(netbuffer->u.filetxpak.filesize);
+	file->ackresendposition = UINT32_MAX; // Only used for resumed downloads
+
+	file->receivedfragments = calloc(file->totalsize / file->fragmentsize + 1, sizeof(*file->receivedfragments));
+	if (!file->receivedfragments)
+		I_Error("FileSendTicker: No more memory\n");
+}
+
 void PT_FileFragment(SINT8 node, INT32 netconsole)
 {
 	if (netnodes[node].ingame)
@@ -1348,8 +1351,6 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 		))
 		I_Error("Tried to download \"%s\"", filename);
 
-	filename = file->filename;
-
 	if (filenum >= fileneedednum)
 	{
 		DEBFILE(va("fileframent not needed %d>%d\n", filenum, fileneedednum));
@@ -1372,15 +1373,24 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 
 		if (CL_CanResumeDownload(file))
 		{
-			file->file = fopen(filename, "r+b");
+			file->file = fopen(file->filename, "r+b");
 			if (!file->file)
-				I_Error("Can't reopen file %s: %s", filename, strerror(errno));
-			CONS_Printf("\r%s...\n", filename);
+			{
+				CONS_Alert(CONS_ERROR, "Couldn't reopen file %s: %s\n", file->filename, strerror(errno));
+
+				free(pauseddownload->receivedfragments);
 
-			CONS_Printf("Resuming download...\n");
-			file->currentsize = pauseddownload->currentsize;
-			file->receivedfragments = pauseddownload->receivedfragments;
-			file->ackresendposition = 0;
+				CONS_Printf("Restarting download of addon \"%s\"...\n", filename);
+
+				OpenNewFileForDownload(file, file->filename);
+			}
+			else
+			{
+				CONS_Printf("Resuming download of addon \"%s\"...\n", filename);
+				file->currentsize = pauseddownload->currentsize;
+				file->receivedfragments = pauseddownload->receivedfragments;
+				file->ackresendposition = 0;
+			}
 
 			free(pauseddownload);
 			pauseddownload = NULL;
@@ -1388,20 +1398,8 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 		else
 		{
 			CL_AbortDownloadResume();
-
-			file->file = fopen(filename, "wb");
-			if (!file->file)
-				I_Error("Can't create file %s: %s", filename, strerror(errno));
-
-			CONS_Printf("\r%s...\n",filename);
-
-			file->currentsize = 0;
-			file->totalsize = LONG(netbuffer->u.filetxpak.filesize);
-			file->ackresendposition = UINT32_MAX; // Only used for resumed downloads
-
-			file->receivedfragments = calloc(file->totalsize / fragmentsize + 1, sizeof(*file->receivedfragments));
-			if (!file->receivedfragments)
-				I_Error("FileSendTicker: No more memory\n");
+			OpenNewFileForDownload(file, file->filename);
+			CONS_Printf("Downloading addon \"%s\" from the server...\n", filename);
 		}
 
 		lasttimeackpacketsent = I_GetTime();
@@ -1421,7 +1419,7 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 			// We can receive packets in the wrong order, anyway all OSes support gaped files
 			fseek(file->file, fragmentpos, SEEK_SET);
 			if (fragmentsize && fwrite(netbuffer->u.filetxpak.data, boundedfragmentsize, 1, file->file) != 1)
-				I_Error("Can't write to %s: %s\n",filename, M_FileError(file->file));
+				I_Error("Can't write to %s: %s\n",file->filename, M_FileError(file->file));
 			file->currentsize += boundedfragmentsize;
 
 			AddFragmentToAckPacket(file->ackpacket, file->iteration, fragmentpos / fragmentsize, filenum);
@@ -1435,8 +1433,6 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 				free(file->ackpacket);
 				file->status = FS_FOUND;
 				file->justdownloaded = true;
-				CONS_Printf(M_GetText("Downloading %s...(done)\n"),
-					filename);
 
 				// Tell the server we have received the file
 				netbuffer->packettype = PT_FILERECEIVED;
@@ -1450,6 +1446,14 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 					HSendPacket(servernode, true, 0, 0);
 					FreeFileNeeded();
 				}
+				else
+				{
+					filedownload.completednum++;
+					filedownload.completedsize += file->totalsize;
+					filedownload.remaining--;
+				}
+
+				CONS_Printf(M_GetText("Finished download of \"%s\"\n"), filename);
 			}
 		}
 		else // Already received
@@ -1483,7 +1487,7 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 		I_Error("Received a file not requested (file id: %d, file status: %s)\n", filenum, s);
 	}
 
-	lastfilenum = filenum;
+	filedownload.current = filenum;
 }
 
 /** \brief Checks if a node is downloading a file
@@ -1581,6 +1585,224 @@ void Command_Downloads_f(void)
 		}
 }
 
+static size_t curlwrite_data(void *ptr, size_t size, size_t nmemb, FILE *stream)
+{
+    return fwrite(ptr, size, nmemb, stream);
+}
+
+static int curlprogress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
+{
+	(void)clientp;
+	(void)ultotal;
+	(void)ulnow; // Function prototype requires these but we won't use, so just discard
+	curl_dlnow = (UINT32)dlnow;
+	curl_dltotal = (UINT32)dltotal;
+	getbytes = ((double)dlnow) / (time(NULL) - curl_starttime); // To-do: Make this more accurate???
+	return 0;
+}
+
+boolean CURLPrepareFile(const char* url, int dfilenum)
+{
+	HTTP_login *login;
+
+#ifdef PARANOIA
+	if (M_CheckParm("-nodownload"))
+		I_Error("Attempted to download files in -nodownload mode");
+#endif
+
+	curl_global_init(CURL_GLOBAL_ALL);
+
+	http_handle = curl_easy_init();
+	multi_handle = curl_multi_init();
+
+	if (http_handle && multi_handle)
+	{
+		I_mkdir(downloaddir, 0755);
+
+		curl_curfile = &fileneeded[dfilenum];
+		curl_realname = curl_curfile->filename;
+		nameonly(curl_realname);
+
+		curl_origfilesize = curl_curfile->currentsize;
+		curl_origtotalfilesize = curl_curfile->totalsize;
+
+		char md5tmp[33];
+		for (INT32 j = 0; j < 16; j++)
+			sprintf(&md5tmp[j*2], "%02x", curl_curfile->md5sum[j]);
+
+		curl_easy_setopt(http_handle, CURLOPT_URL, va("%s/%s?md5=%s", url, curl_realname, md5tmp));
+
+		// Only allow HTTP and HTTPS
+#if (LIBCURL_VERSION_MAJOR <= 7) && (LIBCURL_VERSION_MINOR < 85)
+		curl_easy_setopt(http_handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP|CURLPROTO_HTTPS);
+#else
+		curl_easy_setopt(http_handle, CURLOPT_PROTOCOLS_STR, "http,https");
+#endif
+
+		// Set user agent, as some servers won't accept invalid user agents.
+		curl_easy_setopt(http_handle, CURLOPT_USERAGENT, va("Sonic Robo Blast 2/v%d.%d", VERSION, SUBVERSION));
+
+		// Authenticate if the user so wishes
+		login = CURLGetLogin(url, NULL);
+
+		if (login)
+		{
+			curl_easy_setopt(http_handle, CURLOPT_USERPWD, login->auth);
+		}
+
+		// Follow a redirect request, if sent by the server.
+		curl_easy_setopt(http_handle, CURLOPT_FOLLOWLOCATION, 1L);
+
+		curl_easy_setopt(http_handle, CURLOPT_FAILONERROR, 1L);
+
+		CONS_Printf("Downloading addon \"%s\" from %s\n", curl_realname, url);
+
+		strcatbf(curl_curfile->filename, downloaddir, "/");
+		curl_curfile->file = fopen(curl_curfile->filename, "wb");
+		curl_easy_setopt(http_handle, CURLOPT_WRITEDATA, curl_curfile->file);
+		curl_easy_setopt(http_handle, CURLOPT_WRITEFUNCTION, curlwrite_data);
+		curl_easy_setopt(http_handle, CURLOPT_NOPROGRESS, 0L);
+		curl_easy_setopt(http_handle, CURLOPT_XFERINFOFUNCTION, curlprogress_callback);
+
+		curl_curfile->status = FS_DOWNLOADING;
+		curl_multi_add_handle(multi_handle, http_handle);
+
+		curl_multi_perform(multi_handle, &curl_runninghandles);
+		curl_starttime = time(NULL);
+
+		filedownload.current = dfilenum;
+		filedownload.http_running = true;
+
+		return true;
+	}
+
+	filedownload.http_running = false;
+
+	return false;
+}
+
+void CURLGetFile(void)
+{
+	CURLMcode mc; /* return code used by curl_multi_wait() */
+	CURLcode easyres; /* Return from easy interface */
+	int numfds;
+	CURLMsg *m; /* for picking up messages with the transfer status */
+	CURL *e;
+	int msgs_left; /* how many messages are left */
+	const char *easy_handle_error;
+
+	if (curl_runninghandles)
+	{
+		curl_multi_perform(multi_handle, &curl_runninghandles);
+
+		/* wait for activity, timeout or "nothing" */
+		mc = curl_multi_wait(multi_handle, NULL, 0, 1000, &numfds);
+
+		if (mc != CURLM_OK)
+		{
+			CONS_Alert(CONS_WARNING, "curl_multi_wait() failed, code %d.\n", mc);
+			return;
+		}
+		curl_curfile->currentsize = curl_dlnow;
+		curl_curfile->totalsize = curl_dltotal;
+	}
+
+	/* See how the transfers went */
+	while ((m = curl_multi_info_read(multi_handle, &msgs_left)))
+	{
+		if (m && (m->msg == CURLMSG_DONE))
+		{
+			e = m->easy_handle;
+			easyres = m->data.result;
+
+			char *filename = Z_StrDup(curl_realname);
+			nameonly(filename);
+
+			if (easyres != CURLE_OK)
+			{
+				long response_code = 0;
+
+				if (easyres == CURLE_HTTP_RETURNED_ERROR)
+					curl_easy_getinfo(e, CURLINFO_RESPONSE_CODE, &response_code);
+
+				if (response_code == 404)
+					curl_curfile->failed = FDOWNLOAD_FAIL_NOTFOUND;
+				else
+					curl_curfile->failed = FDOWNLOAD_FAIL_OTHER;
+
+				easy_handle_error = (response_code) ? va("HTTP response code %ld", response_code) : curl_easy_strerror(easyres);
+				curl_curfile->status = FS_FALLBACK;
+				curl_curfile->currentsize = curl_origfilesize;
+				curl_curfile->totalsize = curl_origtotalfilesize;
+				filedownload.http_failed = true;
+				fclose(curl_curfile->file);
+				remove(curl_curfile->filename);
+				CONS_Alert(CONS_ERROR, M_GetText("Failed to download addon \"%s\" (%s)\n"), filename, easy_handle_error);
+			}
+			else
+			{
+				fclose(curl_curfile->file);
+
+				CONS_Printf(M_GetText("Finished download of \"%s\"\n"), filename);
+
+				if (checkfilemd5(curl_curfile->filename, curl_curfile->md5sum) == FS_MD5SUMBAD)
+				{
+					CONS_Alert(CONS_WARNING, M_GetText("File \"%s\" does not match the version used by the server\n"), filename);
+					curl_curfile->status = FS_FALLBACK;
+					curl_curfile->failed = FDOWNLOAD_FAIL_MD5SUMBAD;
+					filedownload.http_failed = true;
+				}
+				else
+				{
+					filedownload.completednum++;
+					filedownload.completedsize += curl_curfile->totalsize;
+					curl_curfile->status = FS_FOUND;
+				}
+			}
+
+			Z_Free(filename);
+
+			curl_curfile->file = NULL;
+			filedownload.http_running = false;
+			filedownload.remaining--;
+			curl_multi_remove_handle(multi_handle, e);
+			curl_easy_cleanup(e);
+
+			if (!filedownload.remaining)
+				break;
+		}
+	}
+
+	if (!filedownload.remaining)
+	{
+		curl_multi_cleanup(multi_handle);
+		curl_global_cleanup();
+	}
+}
+
+HTTP_login *
+CURLGetLogin (const char *url, HTTP_login ***return_prev_next)
+{
+	HTTP_login  * login;
+	HTTP_login ** prev_next;
+
+	for (
+			prev_next = &curl_logins;
+			( login = (*prev_next));
+			prev_next = &login->next
+	){
+		if (strcmp(login->url, url) == 0)
+		{
+			if (return_prev_next)
+				(*return_prev_next) = prev_next;
+
+			return login;
+		}
+	}
+
+	return NULL;
+}
+
 // Functions cut and pasted from Doomatic :)
 
 void nameonly(char *s)
diff --git a/src/netcode/d_netfil.h b/src/netcode/d_netfil.h
index fdbec8c5396fa1619efccfa6f200fbd7a17903c6..4039b5e2d5cc4b0d81fbdc04e6d7c7bef46f724a 100644
--- a/src/netcode/d_netfil.h
+++ b/src/netcode/d_netfil.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -25,6 +25,23 @@ typedef enum
 	SF_NOFREERAM
 } freemethod_t;
 
+typedef enum
+{
+	DLSTATUS_OK,
+	DLSTATUS_TOOLARGE,
+	DLSTATUS_WONTSEND,
+	DLSTATUS_NODOWNLOAD,
+	DLSTATUS_FOLDER
+} dlstatus_t;
+
+typedef enum
+{
+	FDOWNLOAD_FAIL_NONE,
+	FDOWNLOAD_FAIL_NOTFOUND,
+	FDOWNLOAD_FAIL_MD5SUMBAD,
+	FDOWNLOAD_FAIL_OTHER
+} filedownloadfail_t;
+
 typedef enum
 {
 	FS_NOTCHECKED,
@@ -33,7 +50,8 @@ typedef enum
 	FS_REQUESTED,
 	FS_DOWNLOADING,
 	FS_OPEN, // Is opened and used in w_wad
-	FS_MD5SUMBAD
+	FS_MD5SUMBAD,
+	FS_FALLBACK
 } filestatus_t;
 
 typedef enum
@@ -51,6 +69,7 @@ typedef struct
 	UINT8 willsend; // Is the server willing to send it?
 	UINT8 folder; // File is a folder
 	fileneededtype_t type;
+	filedownloadfail_t failed;
 	boolean justdownloaded; // To prevent late fragments from causing an I_Error
 
 	// Used only for download
@@ -70,11 +89,30 @@ extern INT32 fileneedednum;
 extern fileneeded_t *fileneeded;
 extern char downloaddir[512];
 
-extern INT32 lastfilenum;
-extern INT32 downloadcompletednum;
-extern UINT32 downloadcompletedsize;
-extern INT32 totalfilesrequestednum;
-extern UINT32 totalfilesrequestedsize;
+typedef struct
+{
+	INT32 current;
+	INT32 remaining;
+	INT32 completednum;
+	UINT32 completedsize;
+
+	boolean http_failed;
+	boolean http_running;
+
+	char http_source[MAX_MIRROR_LENGTH];
+} file_download_t;
+
+extern file_download_t filedownload;
+
+typedef struct HTTP_login HTTP_login;
+
+extern struct HTTP_login
+{
+	char       * url;
+	char       * auth;
+	HTTP_login * next;
+}
+*curl_logins;
 
 extern consvar_t cv_maxsend, cv_noticedownload, cv_downloadspeed;
 
@@ -97,10 +135,14 @@ boolean SendingFile(INT32 node);
 void FileReceiveTicker(void);
 void PT_FileFragment(SINT8 node, INT32 netconsole);
 
-boolean CL_CheckDownloadable(void);
+UINT8 CL_CheckDownloadable(boolean direct);
 boolean CL_SendFileRequest(void);
 void PT_RequestFile(SINT8 node);
 
+boolean CURLPrepareFile(const char* url, int dfilenum);
+void CURLGetFile(void);
+HTTP_login * CURLGetLogin (const char *url, HTTP_login ***return_prev_next);
+
 typedef enum
 {
 	LFTNS_NONE,    // This node is not connected
diff --git a/src/netcode/http-mserv.c b/src/netcode/http-mserv.c
index 10137e67b042cef0cd192d00d7d4fcbb62d5b842..2b52380cf506f00f119dc01b5f0ab3b47ff4e6f6 100644
--- a/src/netcode/http-mserv.c
+++ b/src/netcode/http-mserv.c
@@ -493,7 +493,8 @@ HMS_unlist (void)
 	if (! hms)
 		return 0;
 
-	curl_easy_setopt(hms->curl, CURLOPT_CUSTOMREQUEST, "POST");
+	curl_easy_setopt(hms->curl, CURLOPT_POST, 1);
+	curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDSIZE, 0);
 
 	ok = HMS_do(hms);
 	HMS_end(hms);
@@ -508,7 +509,8 @@ HMS_unlist (void)
 		if (! hms)
 			return 0;
 
-		curl_easy_setopt(hms->curl, CURLOPT_CUSTOMREQUEST, "POST");
+		curl_easy_setopt(hms->curl, CURLOPT_POST, 1);
+		curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDSIZE, 0);
 
 		ok = HMS_do(hms);
 		HMS_end(hms);
diff --git a/src/netcode/protocol.h b/src/netcode/protocol.h
index c084d920cfa4dc14d3e0cf475b3c62ea7226020d..4b39fab664995c4e171b95a38b776a8b4df1ea4f 100644
--- a/src/netcode/protocol.h
+++ b/src/netcode/protocol.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -26,7 +26,7 @@ packet versions.
 If you change the struct or the meaning of a field
 therein, increment this number.
 */
-#define PACKETVERSION 4
+#define PACKETVERSION 5
 
 // Network play related stuff.
 // There is a data struct that stores network
@@ -200,6 +200,7 @@ enum {
 
 #define MAXSERVERNAME 32
 #define MAXFILENEEDED 915
+#define MAX_MIRROR_LENGTH 256
 
 // This packet is too large
 typedef struct
@@ -230,6 +231,7 @@ typedef struct
 	unsigned char mapmd5[16];
 	UINT8 actnum;
 	UINT8 iszone;
+	char httpsource[MAX_MIRROR_LENGTH];
 	UINT8 fileneeded[MAXFILENEEDED]; // is filled with writexxx (byteptr.h)
 } ATTRPACK serverinfo_pak;
 
diff --git a/src/netcode/server_connection.c b/src/netcode/server_connection.c
index 376700f0db1a300fee1abf21b48462dab1103df5..bbabc8f1dc9a5d82030159a281c2ffae137d1054 100644
--- a/src/netcode/server_connection.c
+++ b/src/netcode/server_connection.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -127,6 +127,8 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
 
 	memset(netbuffer->u.serverinfo.maptitle, 0, sizeof netbuffer->u.serverinfo.maptitle);
 
+	memset(netbuffer->u.serverinfo.httpsource, 0, MAX_MIRROR_LENGTH);
+
 	if (mapheaderinfo[gamemap-1] && *mapheaderinfo[gamemap-1]->lvlttl)
 	{
 		char *read = mapheaderinfo[gamemap-1]->lvlttl, *writ = netbuffer->u.serverinfo.maptitle;
@@ -153,6 +155,17 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
 	if (mapheaderinfo[gamemap-1])
 		netbuffer->u.serverinfo.actnum = mapheaderinfo[gamemap-1]->actnum;
 
+	const char *httpurl = cv_httpsource.string;
+	size_t mirror_length = strlen(httpurl);
+	if (mirror_length > MAX_MIRROR_LENGTH)
+		mirror_length = MAX_MIRROR_LENGTH;
+
+	if (snprintf(netbuffer->u.serverinfo.httpsource, mirror_length+1, "%s", httpurl) < 0)
+		// If there's an encoding error, send nothing, we accept that the above may be truncated
+		strncpy(netbuffer->u.serverinfo.httpsource, "", mirror_length);
+
+	netbuffer->u.serverinfo.httpsource[MAX_MIRROR_LENGTH-1] = '\0';
+
 	p = PutFileNeeded(0);
 
 	HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
diff --git a/src/p_enemy.c b/src/p_enemy.c
index 4d268117f7f72c4d2de3582e9ae5f15a8c0fa24e..5314de8abfa7807fee22d2f2e08dde8d50ac33ec 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -5821,15 +5821,12 @@ void A_MinusDigging(mobj_t *actor)
 		fixed_t yl = (unsigned)(actor->y - radius - bmaporgy) >> MAPBLOCKSHIFT;
 		fixed_t xh = (unsigned)(actor->x + radius - bmaporgx) >> MAPBLOCKSHIFT;
 		fixed_t xl = (unsigned)(actor->x - radius - bmaporgx) >> MAPBLOCKSHIFT;
-		fixed_t bx, by;
 
 		BMBOUNDFIX(xl, xh, yl, yh);
 
 		minus = actor;
 
-		for (bx = xl; bx <= xh; bx++)
-			for (by = yl; by <= yh; by++)
-				P_BlockThingsIterator(bx, by, PIT_MinusCarry);
+		P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_MinusCarry);
 	}
 	else
 	{
@@ -13889,7 +13886,7 @@ void A_DustDevilThink(mobj_t *actor)
 {
 	fixed_t scale = actor->scale;
 	mobj_t *layer = actor->tracer;
-	INT32 bx, by, xl, xh, yl, yh;
+	INT32 xl, xh, yl, yh;
 	fixed_t radius = actor->radius;
 
 	if (LUA_CallAction(A_DUSTDEVILTHINK, actor))
@@ -13953,9 +13950,7 @@ void A_DustDevilThink(mobj_t *actor)
 
 	dustdevil = actor;
 
-	for (bx = xl; bx <= xh; bx++)
-		for (by = yl; by <= yh; by++)
-			P_BlockThingsIterator(bx, by, PIT_DustDevilLaunch);
+	P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_DustDevilLaunch);
 
 	//Whirlwind sound effect.
 	if (leveltime % 70 == 0)
@@ -14035,7 +14030,6 @@ static boolean PIT_TNTExplode(mobj_t *nearby)
 void A_TNTExplode(mobj_t *actor)
 {
 	INT32 locvar1 = var1;
-	INT32 x, y;
 	INT32 xl, xh, yl, yh;
 	static mappoint_t epicenter = {0,0,0};
 
@@ -14072,9 +14066,7 @@ void A_TNTExplode(mobj_t *actor)
 
 	barrel = actor;
 
-	for (x = xl; x <= xh; x++)
-		for (y = yl; y <= yh; y++)
-			P_BlockThingsIterator(x, y, PIT_TNTExplode);
+	P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_TNTExplode);
 
 	// cause a quake -- P_StartQuake does not exist yet
 	epicenter.x = actor->x;
diff --git a/src/p_local.h b/src/p_local.h
index 2be99ff4a50d43b59c76cea60e08b785dd46dee9..9bc8b4a6597b310b6a762ee545fa3878164a4709 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -464,7 +464,7 @@ extern INT32 bmapwidth;
 extern INT32 bmapheight; // in mapblocks
 extern fixed_t bmaporgx;
 extern fixed_t bmaporgy; // origin of block map
-extern mobj_t **blocklinks; // for thing chains
+extern blocknode_t **blocklinks; // for thing chains
 
 //
 // P_INTER
diff --git a/src/p_map.c b/src/p_map.c
index bfb3ccf10c6d54ab30de9a12ddab549057c3b2e7..3ff7d9d90f5ead16ce4fa14ca1b28994d376f6b2 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -752,20 +752,25 @@ static void P_PlayerBarrelCollide(mobj_t *toucher, mobj_t *barrel)
 		P_DamageMobj(barrel, toucher, toucher, 1, 0);
 }
 
-//
-// PIT_CheckThing
-//
-static boolean PIT_CheckThing(mobj_t *thing)
+enum
+{
+	CHECKTHING_NOCOLLIDE,
+	CHECKTHING_COLLIDE,
+	CHECKTHING_DONE,
+	CHECKTHING_IGNORE
+};
+
+static unsigned PIT_DoCheckThing(mobj_t *thing)
 {
 	fixed_t blockdist;
 
 	// don't clip against self
 	if (thing == tmthing)
-		return true;
+		return CHECKTHING_IGNORE;
 
 	// Ignore... things.
 	if (!tmthing || !thing || P_MobjWasRemoved(thing))
-		return true;
+		return CHECKTHING_IGNORE;
 
 	I_Assert(!P_MobjWasRemoved(tmthing));
 	I_Assert(!P_MobjWasRemoved(thing));
@@ -773,7 +778,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	// Ignore spectators
 	if ((tmthing->player && tmthing->player->spectator)
 	|| (thing->player && thing->player->spectator))
-		return true;
+		return CHECKTHING_IGNORE;
 
 	// Do name checks all the way up here
 	// So that NOTHING ELSE can see MT_NAMECHECK because it is client-side.
@@ -781,24 +786,24 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	{
 		// Ignore things that aren't players, ignore spectators, ignore yourself.
 		if (!thing->player || !(tmthing->target && tmthing->target->player) || thing->player->spectator || (tmthing->target && thing->player == tmthing->target->player))
-			return true;
+			return CHECKTHING_IGNORE;
 
 		// Now check that you actually hit them.
 		blockdist = thing->radius + tmthing->radius;
 		if (abs(thing->x - tmx) >= blockdist || abs(thing->y - tmy) >= blockdist)
-			return true; // didn't hit it
+			return CHECKTHING_NOCOLLIDE; // didn't hit it
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return CHECKTHING_NOCOLLIDE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return CHECKTHING_NOCOLLIDE; // underneath
 
-		// REX HAS SEEN YOU
+		// Call any SeenPlayer Lua hooks
 		if (!LUA_HookSeenPlayer(tmthing->target->player, thing->player))
-			return false;
+			return CHECKTHING_DONE;
 
 		seenplayer = thing->player;
-		return false;
+		return CHECKTHING_DONE;
 	}
 
 	// Metal Sonic destroys tiny baby objects.
@@ -808,15 +813,15 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	|| thing->type == MT_WALLSPIKE)))
 	{
 		if ((thing->flags & (MF_ENEMY|MF_BOSS)) && (thing->health <= 0 || !(thing->flags & MF_SHOOTABLE)))
-			return true;
+			return CHECKTHING_IGNORE;
 		blockdist = thing->radius + tmthing->radius;
 		if (abs(thing->x - tmx) >= blockdist || abs(thing->y - tmy) >= blockdist)
-			return true; // didn't hit it
+			return CHECKTHING_NOCOLLIDE; // didn't hit it
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return CHECKTHING_NOCOLLIDE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return CHECKTHING_NOCOLLIDE; // underneath
 		if (thing->type == MT_SPIKE
 		|| thing->type == MT_WALLSPIKE)
 		{
@@ -832,28 +837,28 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			thing->health = 0;
 			P_KillMobj(thing, tmthing, tmthing, 0);
 		}
-		return true;
+		return CHECKTHING_COLLIDE;
 	}
 
 	// STR_SPIKE users destroy spikes
 	if ((tmthing->player) && ((tmthing->player->powers[pw_strong] & STR_SPIKE) && (thing->type == MT_SPIKE || thing->type == MT_WALLSPIKE)))
 	{
 		mobj_t *iter;
-        	blockdist = thing->radius + tmthing->radius;
-        	if (abs(thing->x - tmx) >= blockdist || abs(thing->y - tmy) >= blockdist)
-            		return true; // didn't hit it
-        	// see if it went over / under
-        	if (tmthing->z > thing->z + thing->height)
-            		return true; // overhead
-        	if (tmthing->z + tmthing->height < thing->z)
-            		return true; // underneath
+		blockdist = thing->radius + tmthing->radius;
+		if (abs(thing->x - tmx) >= blockdist || abs(thing->y - tmy) >= blockdist)
+			return CHECKTHING_NOCOLLIDE; // didn't hit it
+		// see if it went over / under
+		if (tmthing->z > thing->z + thing->height)
+			return CHECKTHING_NOCOLLIDE; // overhead
+		if (tmthing->z + tmthing->height < thing->z)
+			return CHECKTHING_NOCOLLIDE; // underneath
 
 		if (thing->flags & MF_SOLID)
 			S_StartSound(tmthing, thing->info->deathsound);
 		for (iter = thing->subsector->sector->thinglist; iter; iter = iter->snext)
 			if (iter->type == thing->type && iter->health > 0 && iter->flags & MF_SOLID && (iter == thing || P_AproxDistance(P_AproxDistance(thing->x - iter->x, thing->y - iter->y), thing->z - iter->z) < 56*thing->scale))//FixedMul(56*FRACUNIT, thing->scale))
 				P_KillMobj(iter, tmthing, tmthing, 0);
-		return true;
+		return CHECKTHING_COLLIDE;
 	}
 
 	// vectorise metal - done in a special case as at this point neither has the right flags for touching
@@ -865,30 +870,30 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		blockdist = thing->radius + tmthing->radius;
 
 		if (abs(thing->x - tmx) >= blockdist || abs(thing->y - tmy) >= blockdist)
-			return true; // didn't hit it
+			return CHECKTHING_NOCOLLIDE; // didn't hit it
 
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return CHECKTHING_NOCOLLIDE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return CHECKTHING_NOCOLLIDE; // underneath
 
 		thing->flags2 |= MF2_CLASSICPUSH;
 
-		return true;
+		return CHECKTHING_COLLIDE;
 	}
 
 	if ((thing->flags & MF_NOCLIPTHING) || !(thing->flags & (MF_SOLID|MF_SPECIAL|MF_PAIN|MF_SHOOTABLE|MF_SPRING)))
-		return true;
+		return CHECKTHING_IGNORE;
 
 	// Don't collide with your buddies while NiGHTS-flying.
 	if (tmthing->player && thing->player && (maptol & TOL_NIGHTS)
 		&& ((tmthing->player->powers[pw_carry] == CR_NIGHTSMODE) || (thing->player->powers[pw_carry] == CR_NIGHTSMODE)))
-		return true;
+		return CHECKTHING_IGNORE;
 
 	blockdist = thing->radius + tmthing->radius;
 
 	if (abs(thing->x - tmx) >= blockdist || abs(thing->y - tmy) >= blockdist)
-		return true; // didn't hit it
+		return CHECKTHING_NOCOLLIDE; // didn't hit it
 
 	if (thing->flags & MF_PAPERCOLLISION) // CAUTION! Very easy to get stuck inside MF_SOLID objects. Giving the player MF_PAPERCOLLISION is a bad idea unless you know what you're doing.
 	{
@@ -915,23 +920,23 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			fixed_t tmcosradius = FixedMul(tmthing->radius, FINECOSINE(tmthing->angle>>ANGLETOFINESHIFT));
 			fixed_t tmsinradius = FixedMul(tmthing->radius, FINESINE(tmthing->angle>>ANGLETOFINESHIFT));
 			if (abs(thing->x - tmx) >= (abs(tmcosradius) + abs(cosradius)) || abs(thing->y - tmy) >= (abs(tmsinradius) + abs(sinradius)))
-				return true; // didn't hit it
+				return CHECKTHING_NOCOLLIDE; // didn't hit it
 			check1 = P_PointOnLineSide(tmx - tmcosradius, tmy - tmsinradius, &junk);
 			check2 = P_PointOnLineSide(tmx + tmcosradius, tmy + tmsinradius, &junk);
 			check3 = P_PointOnLineSide(tmx + tmthing->momx - tmcosradius, tmy + tmthing->momy - tmsinradius, &junk);
 			check4 = P_PointOnLineSide(tmx + tmthing->momx + tmcosradius, tmy + tmthing->momy + tmsinradius, &junk);
 			if ((check1 == check2) && (check2 == check3) && (check3 == check4))
-				return true; // the line doesn't cross between collider's start or end
+				return CHECKTHING_NOCOLLIDE; // the line doesn't cross between collider's start or end
 		}
 		else
 		{
 			if (abs(thing->x - tmx) >= (tmthing->radius + abs(cosradius)) || abs(thing->y - tmy) >= (tmthing->radius + abs(sinradius)))
-				return true; // didn't hit it
+				return CHECKTHING_NOCOLLIDE; // didn't hit it
 			if ((P_PointOnLineSide(tmx - tmthing->radius, tmy - tmthing->radius, &junk)
 			== P_PointOnLineSide(tmx + tmthing->radius, tmy + tmthing->radius, &junk))
 			&& (P_PointOnLineSide(tmx + tmthing->radius, tmy - tmthing->radius, &junk)
 			== P_PointOnLineSide(tmx - tmthing->radius, tmy + tmthing->radius, &junk)))
-				return true; // the line doesn't cross between either pair of opposite corners
+				return CHECKTHING_NOCOLLIDE; // the line doesn't cross between either pair of opposite corners
 		}
 	}
 	else if (tmthing->flags & MF_PAPERCOLLISION)
@@ -944,7 +949,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		tmsinradius = FixedMul(tmthing->radius, FINESINE(tmthing->angle>>ANGLETOFINESHIFT));
 
 		if (abs(thing->x - tmx) >= (thing->radius + abs(tmcosradius)) || abs(thing->y - tmy) >= (thing->radius + abs(tmsinradius)))
-			return true; // didn't hit it
+			return CHECKTHING_NOCOLLIDE; // didn't hit it
 
 		v1.x = tmx - tmcosradius;
 		v1.y = tmy - tmsinradius;
@@ -961,32 +966,32 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		== P_PointOnLineSide(thing->x + thing->radius, thing->y + thing->radius, &junk))
 		&& (P_PointOnLineSide(thing->x + thing->radius, thing->y - thing->radius, &junk)
 		== P_PointOnLineSide(thing->x - thing->radius, thing->y + thing->radius, &junk)))
-			return true; // the line doesn't cross between either pair of opposite corners
+			return CHECKTHING_NOCOLLIDE; // the line doesn't cross between either pair of opposite corners
 	}
 
 	{
 		UINT8 shouldCollide = LUA_Hook2Mobj(thing, tmthing, MOBJ_HOOK(MobjCollide)); // checks hook for thing's type
 		if (P_MobjWasRemoved(tmthing) || P_MobjWasRemoved(thing))
-			return true; // one of them was removed???
+			return CHECKTHING_NOCOLLIDE; // one of them was removed???
 		if (shouldCollide == 1)
-			return false; // force collide
+			return CHECKTHING_DONE; // force collide
 		else if (shouldCollide == 2)
-			return true; // force no collide
+			return CHECKTHING_NOCOLLIDE; // force no collide
 
 		shouldCollide = LUA_Hook2Mobj(tmthing, thing, MOBJ_HOOK(MobjMoveCollide)); // checks hook for tmthing's type
 		if (P_MobjWasRemoved(tmthing) || P_MobjWasRemoved(thing))
-			return true; // one of them was removed???
+			return CHECKTHING_NOCOLLIDE; // one of them was removed???
 		if (shouldCollide == 1)
-			return false; // force collide
+			return CHECKTHING_DONE; // force collide
 		else if (shouldCollide == 2)
-			return true; // force no collide
+			return CHECKTHING_NOCOLLIDE; // force no collide
 	}
 
 	if (tmthing->type == MT_LAVAFALL_LAVA && (thing->type == MT_RING || thing->type == MT_REDTEAMRING || thing->type == MT_BLUETEAMRING || thing->type == MT_FLINGRING))
 	{
 		//height check
 		if (tmthing->z > thing->z + thing->height || thing->z > tmthing->z + tmthing->height || !(thing->health))
-			return true;
+			return CHECKTHING_NOCOLLIDE;
 
 		P_KillMobj(thing, tmthing, tmthing, DMG_FIRE);
 	}
@@ -995,7 +1000,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	{
 		//height check
 		if (tmthing->z > thing->z + thing->height || thing->z > tmthing->z + tmthing->height || !(thing->health))
-			return true;
+			return CHECKTHING_NOCOLLIDE;
 
 		if (thing->type == MT_TNTBARREL)
 			P_KillMobj(thing, tmthing, tmthing->target, 0);
@@ -1018,7 +1023,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			fixed_t s = FINESINE((ang >> ANGLETOFINESHIFT) & FINEMASK);
 			S_StartSound(tmthing, thing->info->activesound);
 			thing->extravalue2 += 2*FixedMul(s, dm)/3;
-			return true;
+			return CHECKTHING_COLLIDE;
 		}
 	}
 
@@ -1026,14 +1031,14 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	{
 		if (((thing->flags2 & MF2_AMBUSH) && (tmthing->z <= thing->z + thing->height) && (tmthing->z + tmthing->height >= thing->z))
 			|| (tmthing->player->powers[pw_carry] == CR_MINECART && tmthing->tracer && !P_MobjWasRemoved(tmthing->tracer)))
-			return true;
+			return CHECKTHING_COLLIDE;
 	}
 
 	if (thing->type == MT_ROLLOUTROCK && tmthing->player && tmthing->health)
 	{
 		if (tmthing->player->powers[pw_carry] == CR_ROLLOUT)
 		{
-			return true;
+			return CHECKTHING_NOCOLLIDE;
 		}
 		if ((thing->flags & MF_PUSHABLE) // not carrying a player
 			&& (tmthing->player->powers[pw_carry] == CR_NONE) // player is not already riding something
@@ -1052,26 +1057,26 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			P_SetTarget(&tmthing->tracer, thing);
 			if (!P_IsObjectOnGround(thing))
 				thing->momz += tmthing->momz;
-			return true;
+			return CHECKTHING_COLLIDE;
 		}
 	}
 	else if (tmthing->type == MT_ROLLOUTROCK)
 	{
 		if (tmthing->z > thing->z + thing->height || thing->z > tmthing->z + tmthing->height || !thing->health)
-			return true;
+			return CHECKTHING_NOCOLLIDE;
 
 		if (thing == tmthing->tracer) // don't collide with rider
-			return true;
+			return CHECKTHING_IGNORE;
 
 		if (thing->flags & MF_SPRING) // bounce on springs
 		{
 			P_DoSpring(thing, tmthing);
-			return true;
+			return CHECKTHING_COLLIDE;
 		}
 		else if ((thing->flags & (MF_MONITOR|MF_SHOOTABLE)) == (MF_MONITOR|MF_SHOOTABLE) && !(tmthing->flags & MF_PUSHABLE)) // pop monitors while carrying a player
 		{
 			P_KillMobj(thing, tmthing, tmthing->tracer, 0);
-			return true;
+			return CHECKTHING_COLLIDE;
 		}
 
 		if (thing->type == tmthing->type // bounce against other rollout rocks
@@ -1106,11 +1111,11 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	if (tmthing->type == MT_FANG && thing->type == MT_FSGNB)
 	{
 		if (thing->z > tmthing->z + tmthing->height)
-			return true; // overhead
+			return CHECKTHING_NOCOLLIDE; // overhead
 		if (thing->z + thing->height < tmthing->z)
-			return true; // underneath
+			return CHECKTHING_NOCOLLIDE; // underneath
 		if (!thing->tracer || !thing->tracer->tracer)
-			return true;
+			return CHECKTHING_IGNORE;
 		P_SlapStick(tmthing, thing);
 		// no return value was used in the original prototype script at this point,
 		// so I'm assuming we fall back on the solid code to determine how it all ends?
@@ -1123,13 +1128,13 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		if (tmthing->type == MT_BIGMINE)
 		{
 			if (!tmthing->momx && !tmthing->momy)
-				return true;
+				return CHECKTHING_IGNORE;
 			if ((statenum_t)(thing->state-states) >= thing->info->meleestate)
-				return true;
+				return CHECKTHING_IGNORE;
 			if (thing->z > tmthing->z + tmthing->height)
-				return true; // overhead
+				return CHECKTHING_NOCOLLIDE; // overhead
 			if (thing->z + thing->height < tmthing->z)
-				return true; // underneath
+				return CHECKTHING_NOCOLLIDE; // underneath
 
 			thing->momx = tmthing->momx/3;
 			thing->momy = tmthing->momy/3;
@@ -1141,18 +1146,18 @@ static boolean PIT_CheckThing(mobj_t *thing)
 				S_StartSound(thing, thing->info->activesound);
 			P_SetMobjState(thing, thing->info->meleestate);
 			P_SetTarget(&thing->tracer, tmthing->tracer);
-			return true;
+			return CHECKTHING_COLLIDE;
 		}
 		else if (tmthing->type == MT_CRUSHCLAW)
 		{
 			if (tmthing->extravalue1 <= 0)
-				return true;
+				return CHECKTHING_IGNORE;
 			if ((statenum_t)(thing->state-states) >= thing->info->meleestate)
-				return true;
+				return CHECKTHING_IGNORE;
 			if (thing->z > tmthing->z + tmthing->height)
-				return true; // overhead
+				return CHECKTHING_NOCOLLIDE; // overhead
 			if (thing->z + thing->height < tmthing->z)
-				return true; // underneath
+				return CHECKTHING_NOCOLLIDE; // underneath
 
 			thing->momx = P_ReturnThrustX(tmthing, tmthing->angle, 2*tmthing->extravalue1*tmthing->scale/3);
 			thing->momy = P_ReturnThrustY(tmthing, tmthing->angle, 2*tmthing->extravalue1*tmthing->scale/3);
@@ -1161,7 +1166,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			P_SetMobjState(thing, thing->info->meleestate);
 			if (tmthing->tracer)
 				P_SetTarget(&thing->tracer, tmthing->tracer->target);
-			return false;
+			return CHECKTHING_DONE;
 		}
 	}
 
@@ -1169,9 +1174,9 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	if (tmthing->type == MT_SPIKE && (thing->flags & MF_SOLID) && (tmthing->flags & MF_SOLID))
 	{
 		if (thing->z > tmthing->z + tmthing->height)
-			return true; // overhead
+			return CHECKTHING_NOCOLLIDE; // overhead
 		if (thing->z + thing->height < tmthing->z)
-			return true; // underneath
+			return CHECKTHING_NOCOLLIDE; // underneath
 
 		if (tmthing->eflags & MFE_VERTICALFLIP)
 			P_SetOrigin(thing, thing->x, thing->y, tmthing->z - thing->height - FixedMul(FRACUNIT, tmthing->scale));
@@ -1179,16 +1184,16 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			P_SetOrigin(thing, thing->x, thing->y, tmthing->z + tmthing->height + FixedMul(FRACUNIT, tmthing->scale));
 		if (thing->flags & MF_SHOOTABLE)
 			P_DamageMobj(thing, tmthing, tmthing, 1, DMG_SPIKE);
-		return true;
+		return CHECKTHING_COLLIDE;
 	}
 
 	if (thing->flags & MF_PAIN && tmthing->player)
 	{ // Player touches painful thing sitting on the floor
 		// see if it went over / under
 		if (thing->z > tmthing->z + tmthing->height)
-			return true; // overhead
+			return CHECKTHING_NOCOLLIDE; // overhead
 		if (thing->z + thing->height < tmthing->z)
-			return true; // underneath
+			return CHECKTHING_NOCOLLIDE; // underneath
 		if (tmthing->flags & MF_SHOOTABLE && thing->health > 0)
 		{
 			UINT32 damagetype = (thing->info->mass & 0xFF);
@@ -1196,16 +1201,17 @@ static boolean PIT_CheckThing(mobj_t *thing)
 				damagetype = DMG_FIRE;
 			if (P_DamageMobj(tmthing, thing, thing, 1, damagetype) && (damagetype = (thing->info->mass>>8)))
 				S_StartSound(thing, damagetype);
+			return CHECKTHING_COLLIDE;
 		}
-		return true;
+		return CHECKTHING_NOCOLLIDE;
 	}
 	else if (tmthing->flags & MF_PAIN && thing->player)
 	{ // Painful thing splats player in the face
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return CHECKTHING_NOCOLLIDE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return CHECKTHING_NOCOLLIDE; // underneath
 		if (thing->flags & MF_SHOOTABLE && tmthing->health > 0)
 		{
 			UINT32 damagetype = (tmthing->info->mass & 0xFF);
@@ -1213,32 +1219,33 @@ static boolean PIT_CheckThing(mobj_t *thing)
 				damagetype = DMG_FIRE;
 			if (P_DamageMobj(thing, tmthing, tmthing, 1, damagetype) && (damagetype = (tmthing->info->mass>>8)))
 				S_StartSound(tmthing, damagetype);
+			return CHECKTHING_COLLIDE;
 		}
-		return true;
+		return CHECKTHING_NOCOLLIDE;
 	}
 
 	if (thing->type == MT_HOOPCOLLIDE && thing->flags & MF_SPECIAL && tmthing->player)
 	{
 		P_TouchSpecialThing(thing, tmthing, true);
-		return true;
+		return CHECKTHING_COLLIDE;
 	}
 
 	// check for skulls slamming into things
 	if (tmthing->flags2 & MF2_SKULLFLY)
 	{
 		if (tmthing->type == MT_EGGMOBILE) // Don't make Eggman stop!
-			return true; // Let him RUN YOU RIGHT OVER. >:3
+			return CHECKTHING_COLLIDE; // Let him RUN YOU RIGHT OVER. >:3
 		else
 		{
 			// see if it went over / under
 			if (tmthing->z > thing->z + thing->height)
-				return true; // overhead
+				return CHECKTHING_NOCOLLIDE; // overhead
 			if (tmthing->z + tmthing->height < thing->z)
-				return true; // underneath
+				return CHECKTHING_NOCOLLIDE; // underneath
 
 			tmthing->flags2 &= ~MF2_SKULLFLY;
 			tmthing->momx = tmthing->momy = tmthing->momz = 0;
-			return false; // stop moving
+			return CHECKTHING_DONE; // stop moving
 		}
 	}
 
@@ -1254,10 +1261,10 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		if ((tmznext <= thzh && tmz > thzh) || (tmznext > thzh - sprarea && tmznext < thzh))
 		{
 			P_DoSpring(thing, tmthing);
-			return true;
+			return CHECKTHING_COLLIDE;
 		}
 		else if (tmz > thzh - sprarea && tmz < thzh) // Don't damage people springing up / down
-			return true;
+			return CHECKTHING_NOCOLLIDE;
 	}
 
 	// missiles can hit other things
@@ -1265,31 +1272,31 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	{
 		// see if it went over / under
 		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
+			return CHECKTHING_NOCOLLIDE; // overhead
 		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
+			return CHECKTHING_NOCOLLIDE; // underneath
 
 		if (tmthing->type != MT_SHELL && tmthing->target && tmthing->target->type == thing->type)
 		{
 			// Don't hit same species as originator.
 			if (thing == tmthing->target)
-				return true;
+				return CHECKTHING_IGNORE;
 
 			if (thing->type != MT_PLAYER)
 			{
 				// Explode, but do no damage.
 				// Let players missile other players.
-				return false;
+				return CHECKTHING_DONE;
 			}
 		}
 
 		// Special case for bounce rings so they don't get caught behind solid objects.
 		if ((tmthing->type == MT_THROWNBOUNCE && tmthing->fuse > 8*TICRATE) && thing->flags & MF_SOLID)
-			return true;
+			return CHECKTHING_IGNORE;
 
 		// Missiles ignore Brak's helper.
 		if (thing->type == MT_BLACKEGGMAN_HELPER)
-			return true;
+			return CHECKTHING_IGNORE;
 
 		// Hurting Brak
 		if (tmthing->type == MT_BLACKEGGMAN_MISSILE
@@ -1298,19 +1305,22 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			// Not if Brak's already in pain
 			if (!(thing->state >= &states[S_BLACKEGG_PAIN1] && thing->state <= &states[S_BLACKEGG_PAIN35]))
 				P_SetMobjState(thing, thing->info->painstate);
-			return false;
+			return CHECKTHING_DONE;
 		}
 
 		if (!(thing->flags & MF_SHOOTABLE) && !(thing->type == MT_EGGSHIELD))
 		{
 			// didn't do any damage
-			return !(thing->flags & MF_SOLID);
+			if (thing->flags & MF_SOLID)
+				return CHECKTHING_COLLIDE;
+			else
+				return CHECKTHING_NOCOLLIDE;
 		}
 
 		if (tmthing->flags & MF_MISSILE && thing->player && tmthing->target && tmthing->target->player
 		&& thing->player->ctfteam == tmthing->target->player->ctfteam
 		&& thing->player->powers[pw_carry] == CR_PLAYER && thing->tracer == tmthing->target)
-			return true; // Don't give rings to your carry player by accident.
+			return CHECKTHING_IGNORE; // Don't give rings to your carry player by accident.
 
 		if (thing->type == MT_EGGSHIELD)
 		{
@@ -1318,7 +1328,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 
 			if (angle < ANGLE_180) // hit shield from behind, shield is destroyed!
 				P_KillMobj(thing, tmthing, tmthing, 0);
-			return false;
+			return CHECKTHING_DONE;
 		}
 
 		// damage / explode
@@ -1344,7 +1354,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			if (!demoplayback || P_ControlStyle(thing->player) == CS_LMAOGALOG)
 				P_SetPlayerAngle(thing->player, thing->angle);
 
-			return true;
+			return CHECKTHING_COLLIDE;
 		}
 		else if (tmthing->type == MT_BLACKEGGMAN_MISSILE && thing->player && ((thing->player->powers[pw_carry] == CR_GENERIC) || (thing->player->pflags & PF_JUMPED)))
 		{
@@ -1371,11 +1381,10 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			P_KillMobj(tmthing, NULL, NULL, 0);
 
 		// don't traverse any more
-
 		if (tmthing->type == MT_SHELL)
-			return true;
+			return CHECKTHING_COLLIDE;
 		else
-			return false;
+			return CHECKTHING_DONE;
 	}
 
 	if (thing->flags & MF_PUSHABLE && (tmthing->player || tmthing->flags & MF_PUSHABLE)
@@ -1478,13 +1487,13 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	if (thing->flags & MF_SPECIAL && tmthing->player)
 	{
 		P_TouchSpecialThing(thing, tmthing, true); // can remove thing
-		return true;
+		return CHECKTHING_COLLIDE;
 	}
 	// check again for special pickup
 	if (tmthing->flags & MF_SPECIAL && thing->player)
 	{
 		P_TouchSpecialThing(tmthing, thing, true); // can remove thing
-		return true;
+		return CHECKTHING_COLLIDE;
 	}
 
 	// Sprite Spikes!
@@ -1544,7 +1553,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			if (playerangle > ANGLE_180)
 				playerangle = InvAngle(playerangle);
 			if (playerangle < ANGLE_90)
-				return true; // Yes, this is intentionally outside the z-height check. No standing on spikes whilst moving away from them.
+				return CHECKTHING_IGNORE; // Yes, this is intentionally outside the z-height check. No standing on spikes whilst moving away from them.
 		}
 
 		bottomz = thing->z;
@@ -1578,15 +1587,15 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		if (thing->type == MT_FAN || thing->type == MT_STEAM)
 		{
 			P_DoFanAndGasJet(thing, tmthing);
-			return true;
+			return CHECKTHING_COLLIDE;
 		}
 		else if (thing->flags & MF_SPRING)
 		{
 			if ( thing->z <= tmthing->z + tmthing->height
 			&& tmthing->z <= thing->z + thing->height)
 				if (P_DoSpring(thing, tmthing))
-					return false;
-			return true;
+					return CHECKTHING_DONE;
+			return CHECKTHING_COLLIDE;
 		}
 	}
 
@@ -1595,7 +1604,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	{
 		if ((thing->z + thing->height >= tmthing->z)
 		&& (tmthing->z + tmthing->height >= thing->z))
-			return false;
+			return CHECKTHING_DONE;
 	}
 
 	// Damage other players when invincible
@@ -1630,7 +1639,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		if (tmthing->player && thing->player)
 		{
 			P_DoTailsCarry(thing->player, tmthing->player);
-			return true;
+			return CHECKTHING_COLLIDE;
 		}
 	}
 	else if (thing->player) {
@@ -1677,7 +1686,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	if (tmthing->player) // Is the moving/interacting object the player?
 	{
 		if (!tmthing->health)
-			return true;
+			return CHECKTHING_IGNORE;
 
 		if (thing->type == MT_FAN || thing->type == MT_STEAM)
 			P_DoFanAndGasJet(thing, tmthing);
@@ -1686,14 +1695,16 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			if ( thing->z <= tmthing->z + tmthing->height
 			&& tmthing->z <= thing->z + thing->height)
 				if (P_DoSpring(thing, tmthing))
-					return false;
-			return true;
+					return CHECKTHING_DONE;
+			return CHECKTHING_COLLIDE;
 		}
 		// Monitor?
 		else if (thing->flags & MF_MONITOR
 		&& !((thing->type == MT_RING_REDBOX && tmthing->player->ctfteam != 1) || (thing->type == MT_RING_BLUEBOX && tmthing->player->ctfteam != 2))
 		&& (!(thing->flags & MF_SOLID) || P_PlayerCanDamage(tmthing->player, thing)))
 		{
+			unsigned collide = CHECKTHING_NOCOLLIDE;
+
 			if (thing->z - thing->scale <= tmthing->z + tmthing->height
 			&& thing->z + thing->height + thing->scale >= tmthing->z)
 			{
@@ -1725,21 +1736,25 @@ static boolean PIT_CheckThing(mobj_t *thing)
 							*momz /= 2;
 						*momz -= (*momz/(underwater ? 8 : 4)); // Cap the height!
 					}
+					collide = CHECKTHING_COLLIDE;
 				}
 				if (!(elementalpierce == 1 && thing->flags & MF_GRENADEBOUNCE)) // prevent gold monitor clipthrough.
 				{
 					if (player->pflags & PF_BOUNCING)
 						P_DoAbilityBounce(player, false);
-					return false;
+					collide = CHECKTHING_DONE;
 				}
 				else
 					*z -= *momz; // to ensure proper collision.
 			}
 
-			return true;
+			return collide;
 		}
 	}
 
+	// not solid not blocked
+	unsigned collide = CHECKTHING_NOCOLLIDE;
+
 	if ((tmthing->flags & MF_SPRING || tmthing->type == MT_STEAM || tmthing->type == MT_SPIKE || tmthing->type == MT_WALLSPIKE) && (thing->player))
 		; // springs, gas jets and springs should never be able to step up onto a player
 	// z checking at last
@@ -1762,25 +1777,27 @@ static boolean PIT_CheckThing(mobj_t *thing)
 					tmfloorrover = NULL;
 					tmfloorslope = NULL;
 				}
-				return true;
+				return CHECKTHING_COLLIDE;
 			}
 
 			topz = thing->z - thing->scale; // FixedMul(FRACUNIT, thing->scale), but thing->scale == FRACUNIT in base scale anyways
 
 			// block only when jumping not high enough,
 			// (dont climb max. 24units while already in air)
-			// since return false doesn't handle momentum properly,
+			// since return CHECKTHING_DONE doesn't handle momentum properly,
 			// we lie to P_TryMove() so it's always too high
 			if (tmthing->player && tmthing->z + tmthing->height > topz
 				&& tmthing->z + tmthing->height < tmthing->ceilingz)
 			{
 				if (thing->flags & MF_GRENADEBOUNCE && (thing->flags & MF_MONITOR || thing->info->flags & MF_MONITOR)) // Gold monitor hack...
-					return false;
+					return CHECKTHING_DONE;
 
 				tmfloorz = tmceilingz = topz; // block while in air
 				tmceilingrover = NULL;
 				tmceilingslope = NULL;
 				tmfloorthing = thing; // needed for side collision
+
+				collide = CHECKTHING_COLLIDE;
 			}
 			else if (topz < tmceilingz && tmthing->z <= thing->z+thing->height)
 			{
@@ -1788,6 +1805,8 @@ static boolean PIT_CheckThing(mobj_t *thing)
 				tmceilingrover = NULL;
 				tmceilingslope = NULL;
 				tmfloorthing = thing; // thing we may stand on
+
+				collide = CHECKTHING_COLLIDE;
 			}
 		}
 		else
@@ -1803,25 +1822,27 @@ static boolean PIT_CheckThing(mobj_t *thing)
 					tmceilingrover = NULL;
 					tmceilingslope = NULL;
 				}
-				return true;
+				return CHECKTHING_COLLIDE;
 			}
 
 			topz = thing->z + thing->height + thing->scale; // FixedMul(FRACUNIT, thing->scale), but thing->scale == FRACUNIT in base scale anyways
 
 			// block only when jumping not high enough,
 			// (dont climb max. 24units while already in air)
-			// since return false doesn't handle momentum properly,
+			// since return CHECKTHING_DONE doesn't handle momentum properly,
 			// we lie to P_TryMove() so it's always too high
 			if (tmthing->player && tmthing->z < topz
 				&& tmthing->z > tmthing->floorz)
 			{
 				if (thing->flags & MF_GRENADEBOUNCE && (thing->flags & MF_MONITOR || thing->info->flags & MF_MONITOR)) // Gold monitor hack...
-					return false;
+					return CHECKTHING_DONE;
 
 				tmfloorz = tmceilingz = topz; // block while in air
 				tmfloorrover = NULL;
 				tmfloorslope = NULL;
 				tmfloorthing = thing; // needed for side collision
+
+				collide = CHECKTHING_COLLIDE;
 			}
 			else if (topz > tmfloorz && tmthing->z+tmthing->height >= thing->z)
 			{
@@ -1829,12 +1850,18 @@ static boolean PIT_CheckThing(mobj_t *thing)
 				tmfloorrover = NULL;
 				tmfloorslope = NULL;
 				tmfloorthing = thing; // thing we may stand on
+
+				collide = CHECKTHING_COLLIDE;
 			}
 		}
 	}
 
-	// not solid not blocked
-	return true;
+	return collide;
+}
+
+static boolean PIT_CheckThing(mobj_t *thing)
+{
+	return PIT_DoCheckThing(thing) != CHECKTHING_DONE;
 }
 
 // PIT_CheckCameraLine
@@ -2010,38 +2037,6 @@ static boolean PIT_CheckLine(line_t *ld)
 // =========================================================================
 //                         MOVEMENT CLIPPING
 // =========================================================================
-
-//
-// P_CheckPosition
-// This is purely informative, nothing is modified
-// (except things picked up).
-//
-// in:
-//  a mobj_t (can be valid or invalid)
-//  a position to be checked
-//   (doesn't need to be related to the mobj_t->x,y)
-//
-// during:
-//  special things are touched if MF_PICKUP
-//  early out on solid lines?
-//
-// out:
-//  newsubsec
-//  tmfloorz
-//  tmceilingz
-//  tmdropoffz
-//  tmdrpoffceilz
-//   the lowest point contacted
-//   (monsters won't move to a dropoff)
-//  speciallines[]
-//  numspeciallines
-//
-
-// tmfloorz
-//     the nearest floor or thing's top under tmthing
-// tmceilingz
-//     the nearest ceiling or thing's bottom over tmthing
-//
 boolean P_CheckPosition(mobj_t *thing, fixed_t x, fixed_t y)
 {
 	INT32 xl, xh, yl, yh, bx, by;
@@ -2282,9 +2277,7 @@ boolean P_CheckPosition(mobj_t *thing, fixed_t x, fixed_t y)
 	if (tmflags & MF_NOCLIP)
 		return true;
 
-	// Check things first, possibly picking things up.
-
-	// MF_NOCLIPTHING: used by camera to not be blocked by things
+	// Check things first.
 	if (!(thing->flags & MF_NOCLIPTHING))
 	{
 		for (bx = xl; bx <= xh; bx++)
@@ -2893,7 +2886,7 @@ boolean P_TryMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff)
 	// standing on top and move it, too.
 	if (thing->flags & MF_PUSHABLE)
 	{
-		INT32 bx, by, xl, xh, yl, yh;
+		INT32 xl, xh, yl, yh;
 
 		yh = (unsigned)(thing->y + MAXRADIUS - bmaporgy)>>MAPBLOCKSHIFT;
 		yl = (unsigned)(thing->y - MAXRADIUS - bmaporgy)>>MAPBLOCKSHIFT;
@@ -2906,9 +2899,7 @@ boolean P_TryMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff)
 		standx = x;
 		standy = y;
 
-		for (by = yl; by <= yh; by++)
-			for (bx = xl; bx <= xh; bx++)
-				P_BlockThingsIterator(bx, by, PIT_PushableMoved);
+		P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_PushableMoved);
 	}
 
 	// Link the thing into its new position
@@ -4217,7 +4208,6 @@ static boolean PIT_RadiusAttack(mobj_t *thing)
 //
 void P_RadiusAttack(mobj_t *spot, mobj_t *source, fixed_t damagedist, UINT8 damagetype, boolean sightcheck)
 {
-	INT32 x, y;
 	INT32 xl, xh, yl, yh;
 	fixed_t dist;
 
@@ -4235,9 +4225,7 @@ void P_RadiusAttack(mobj_t *spot, mobj_t *source, fixed_t damagedist, UINT8 dama
 	bombdamagetype = damagetype;
 	bombsightcheck = sightcheck;
 
-	for (y = yl; y <= yh; y++)
-		for (x = xl; x <= xh; x++)
-			P_BlockThingsIterator(x, y, PIT_RadiusAttack);
+	P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_RadiusAttack);
 }
 
 //
@@ -4392,16 +4380,18 @@ static boolean P_CheckSectorPolyObjects(sector_t *sector, boolean realcrush, boo
 			for (x = po->blockbox[BOXLEFT]; x <= po->blockbox[BOXRIGHT]; ++x)
 			{
 				mobj_t *mo;
+				blocknode_t *block;
 
 				if (x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight)
 					continue;
 
-				mo = blocklinks[y * bmapwidth + x];
+				block = blocklinks[y * bmapwidth + x];
 
-				for (; mo; mo = mo->bnext)
+				for (; block; block = block->mnext)
 				{
-					// Monster Iestyn: do we need to check if a mobj has already been checked? ...probably not I suspect
+					mo = block->mobj;
 
+					// Monster Iestyn: do we need to check if a mobj has already been checked? ...probably not I suspect
 					if (!P_MobjInsidePolyobj(po, mo))
 						continue;
 
diff --git a/src/p_maputl.c b/src/p_maputl.c
index 2d9f23c7b9bce90d87f43af3ad0ff49b3ecebd95..010aad6eef3c1b34df8d0db05aff0e24739acdd1 100644
--- a/src/p_maputl.c
+++ b/src/p_maputl.c
@@ -680,6 +680,35 @@ void P_LineOpening(line_t *linedef, mobj_t *mobj)
 	openrange = opentop - openbottom;
 }
 
+static blocknode_t *freeblocks;
+
+static blocknode_t *P_CreateBlockNode(mobj_t *thing, int x, int y)
+{
+	blocknode_t *block;
+
+	if (freeblocks != NULL)
+	{
+		block = freeblocks;
+		freeblocks = block->bnext;
+	}
+	else
+		block = Z_Malloc(sizeof(blocknode_t), PU_LEVEL, NULL);
+
+	block->blockindex = x + y*bmapwidth;
+	block->mobj = thing;
+	block->mnext = NULL;
+	block->mprev = NULL;
+	block->bprev = NULL;
+	block->bnext = NULL;
+
+	return block;
+}
+
+static void P_ReleaseBlockNode(blocknode_t *node)
+{
+	node->bnext = freeblocks;
+	freeblocks = node;
+}
 
 //
 // THING POSITION SETTING
@@ -730,20 +759,20 @@ void P_UnsetThingPosition(mobj_t *thing)
 
 	if (!(thing->flags & MF_NOBLOCKMAP))
 	{
-		/* inert things don't need to be in blockmap
-		*
-		* killough 8/11/98: simpler scheme using pointers-to-pointers for prev
-		* pointers, allows head node pointers to be treated like everything else
-		*
-		* Also more robust, since it doesn't depend on current position for
-		* unlinking. Old method required computing head node based on position
-		* at time of unlinking, assuming it was the same position as during
-		* linking.
-		*/
+		// [RH] Unlink from all blocks this actor uses
+		blocknode_t *block = thing->blocknode;
 
-		mobj_t *bnext, **bprev = thing->bprev;
-		if (bprev && (*bprev = bnext = thing->bnext) != NULL)  // unlink from block map
-			bnext->bprev = bprev;
+		while (block != NULL)
+		{
+			if (block->mnext != NULL)
+				block->mnext->mprev = block->mprev;
+			*(block->mprev) = block->mnext;
+			blocknode_t *next = block->bnext;
+			P_ReleaseBlockNode(block);
+			block = next;
+		}
+
+		thing->blocknode = NULL;
 	}
 }
 
@@ -814,24 +843,45 @@ void P_SetThingPosition(mobj_t *thing)
 	if (!(thing->flags & MF_NOBLOCKMAP))
 	{
 		// inert things don't need to be in blockmap
-		const INT32 blockx = (unsigned)(thing->x - bmaporgx)>>MAPBLOCKSHIFT;
-		const INT32 blocky = (unsigned)(thing->y - bmaporgy)>>MAPBLOCKSHIFT;
-		if (blockx >= 0 && blockx < bmapwidth
-			&& blocky >= 0 && blocky < bmapheight)
+		INT32 x1 = (unsigned)(thing->x - thing->radius - bmaporgx)>>MAPBLOCKSHIFT;
+		INT32 y1 = (unsigned)(thing->y - thing->radius - bmaporgy)>>MAPBLOCKSHIFT;
+		INT32 x2 = (unsigned)(thing->x + thing->radius - bmaporgx)>>MAPBLOCKSHIFT;
+		INT32 y2 = (unsigned)(thing->y + thing->radius - bmaporgy)>>MAPBLOCKSHIFT;
+
+		thing->blocknode = NULL;
+
+		blocknode_t **alink = &thing->blocknode;
+
+		if (!(x1 >= bmapwidth || x2 < 0 || y1 >= bmapheight || y2 < 0))
 		{
-			// killough 8/11/98: simpler scheme using
-			// pointer-to-pointer prev pointers --
-			// allows head nodes to be treated like everything else
-
-			mobj_t **link = &blocklinks[blocky*bmapwidth + blockx];
-			mobj_t *bnext = *link;
-			if ((thing->bnext = bnext) != NULL)
-				bnext->bprev = &thing->bnext;
-			thing->bprev = link;
-			*link = thing;
+			// [RH] Link into every block this actor touches, not just the center one
+			x1 = max(0, x1);
+			y1 = max(0, y1);
+			x2 = min(bmapwidth - 1, x2);
+			y2 = min(bmapheight - 1, y2);
+			for (int y = y1; y <= y2; ++y)
+			{
+				for (int x = x1; x <= x2; ++x)
+				{
+					blocknode_t **link = &blocklinks[y*bmapwidth + x];
+					blocknode_t *node = P_CreateBlockNode(thing, x, y);
+
+					// Link in to block
+					if ((node->mnext = *link) != NULL)
+					{
+						(*link)->mprev = &node->mnext;
+					}
+					node->mprev = link;
+					*link = node;
+
+					// Link in to actor
+					node->bprev = alink;
+					node->bnext = NULL;
+					(*alink) = node;
+					alink = &node->bnext;
+				}
+			}
 		}
-		else // thing is off the map
-			thing->bnext = NULL, thing->bprev = NULL;
 	}
 
 	// Allows you to 'step' on a new linedef exec when the previous
@@ -971,34 +1021,220 @@ boolean P_BlockLinesIterator(INT32 x, INT32 y, boolean (*func)(line_t *))
 	return true; // Everything was checked.
 }
 
-
 //
 // P_BlockThingsIterator
 //
 boolean P_BlockThingsIterator(INT32 x, INT32 y, boolean (*func)(mobj_t *))
 {
-	mobj_t *mobj, *bnext = NULL;
+	mobj_t *mobj;
+	blocknode_t *block;
 
 	if (x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight)
 		return true;
 
 	// Check interaction with the objects in the blockmap.
-	for (mobj = blocklinks[y*bmapwidth + x]; mobj; mobj = bnext)
+	for (block = blocklinks[y*bmapwidth + x]; block; block = block->mnext)
 	{
-		P_SetTarget(&bnext, mobj->bnext); // We want to note our reference to bnext here incase it is MF_NOTHINK and gets removed!
+		mobj = block->mobj;
+
 		if (!func(mobj))
-		{
-			P_SetTarget(&bnext, NULL);
 			return false;
+		if (P_MobjWasRemoved(tmthing)) // func just broke blockmap chain, cannot continue.
+			return true;
+	}
+
+	return true;
+}
+
+boolean P_DoBlockThingsIterate(int x1, int y1, int x2, int y2, boolean (*func)(mobj_t *))
+{
+	boolean status = true;
+
+	for (INT32 bx = x1; bx <= x2; bx++)
+		for (INT32 by = y1; by <= y2; by++)
+			if (!P_BlockThingsIterator(bx, by, func))
+				status = false;
+
+	return status;
+}
+
+static bthingit_hash_entry_t *GetHashEntryForIterator(bthingit_t *it, int i)
+{
+	if (i < NUM_BTHINGIT_FIXEDHASH)
+		return &it->fixedhash[i];
+	else
+		return &it->dynhash[i - NUM_BTHINGIT_FIXEDHASH];
+}
+
+static blocknode_t *GetBlockmapBlock(int x, int y)
+{
+	if (x >= 0 && y >= 0 && x < bmapwidth && y < bmapheight)
+	{
+		return blocklinks[y*bmapwidth + x];
+	}
+	else
+	{
+		// invalid block
+		return NULL;
+	}
+}
+
+static bthingit_t *freeiters;
+
+bthingit_t *P_NewBlockThingsIterator(int x1, int y1, int x2, int y2)
+{
+	bthingit_t *it;
+	blocknode_t *block;
+
+	x1 = max(0, x1);
+	y1 = max(0, y1);
+	x2 = min(bmapwidth - 1, x2);
+	y2 = min(bmapheight - 1, y2);
+
+	if (x1 > x2 || y1 > y2)
+		return NULL;
+
+	block = GetBlockmapBlock(x1, y1);
+	if (!block)
+		return NULL;
+
+	if (freeiters != NULL)
+	{
+		it = freeiters;
+		freeiters = it->freechain;
+	}
+	else
+		it = Z_Calloc(sizeof(bthingit_t), PU_LEVEL, NULL);
+
+	it->x1 = x1;
+	it->y1 = y1;
+	it->x2 = x2;
+	it->y2 = y2;
+	it->curx = x1;
+	it->cury = y1;
+	it->block = block;
+	it->freechain = NULL;
+	it->numfixedhash = 0;
+	it->dynhashcount = 0;
+
+	for (size_t i = 0; i < NUM_BTHINGIT_BUCKETS; i++)
+		it->buckets[i] = -1;
+
+	return it;
+}
+
+mobj_t *P_BlockThingsIteratorNext(bthingit_t *it, boolean centeronly)
+{
+	for (;;)
+	{
+		while (it->block != NULL)
+		{
+			mobj_t *mobj = it->block->mobj;
+			blocknode_t *node = it->block;
+
+			it->block = it->block->mnext;
+
+			// Don't recheck things that were already checked
+			if (node->bnext == NULL && node->bprev == &mobj->blocknode)
+			{
+				// This actor doesn't span blocks, so we know it can only ever be checked once.
+				return mobj;
+			}
+			else
+			{
+				// Block boundaries for compatibility mode
+				if (centeronly)
+				{
+					fixed_t blockleft = (it->curx * MAPBLOCKUNITS) + bmaporgx;
+					fixed_t blockright = blockleft + MAPBLOCKUNITS;
+					fixed_t blockbottom = (it->cury * MAPBLOCKUNITS) + bmaporgy;
+					fixed_t blocktop = blockbottom + MAPBLOCKUNITS;
+
+					// only return actors with the center in this block
+					if (mobj->x >= blockleft && mobj->x < blockright &&
+						mobj->y >= blockbottom && mobj->y < blocktop)
+					{
+						return mobj;
+					}
+				}
+				else
+				{
+					bthingit_hash_entry_t *entry;
+					int i;
+					size_t hash = ((size_t)mobj >> 3) % NUM_BTHINGIT_BUCKETS;
+
+					for (i = it->buckets[hash]; i >= 0; )
+					{
+						entry = GetHashEntryForIterator(it, i);
+						if (entry->mobj == mobj)
+						{
+							// I've already been checked. Skip to the next mobj.
+							break;
+						}
+						i = entry->next;
+					}
+
+					if (i < 0)
+					{
+						// Add mobj to the hash table and return it.
+						if (it->numfixedhash < NUM_BTHINGIT_FIXEDHASH)
+						{
+							entry = &it->fixedhash[it->numfixedhash];
+							entry->next = it->buckets[hash];
+							it->buckets[hash] = it->numfixedhash++;
+						}
+						else
+						{
+							if (!it->dynhash)
+							{
+								it->dynhashcapacity = 50;
+								Z_Calloc(it->dynhashcapacity * sizeof(it->dynhashcapacity), PU_LEVEL, &it->dynhash);
+							}
+							if (it->dynhashcount == it->dynhashcapacity)
+							{
+								it->dynhashcapacity *= 2;
+								it->dynhash = Z_Realloc(it->dynhash, it->dynhashcapacity * sizeof(it->dynhashcapacity), PU_LEVEL, &it->dynhash);
+							}
+							i = (int)it->dynhashcount;
+							it->dynhashcount++;
+							entry = &it->dynhash[i];
+							entry->next = it->buckets[hash];
+							it->buckets[hash] = i + NUM_BTHINGIT_FIXEDHASH;
+						}
+
+						entry->mobj = mobj;
+						return mobj;
+					}
+				}
+			}
 		}
-		if (P_MobjWasRemoved(tmthing) // func just popped our tmthing, cannot continue.
-		|| (bnext && P_MobjWasRemoved(bnext))) // func just broke blockmap chain, cannot continue.
+
+		if (++it->curx > it->x2)
 		{
-			P_SetTarget(&bnext, NULL);
-			return true;
+			it->curx = it->x1;
+			if (++it->cury > it->y2)
+				return NULL;
 		}
+
+		it->block = GetBlockmapBlock(it->curx, it->cury);
 	}
-	return true;
+
+	return NULL;
+}
+
+void P_FreeBlockThingsIterator(bthingit_t *it)
+{
+	if (it)
+	{
+		it->freechain = freeiters;
+		freeiters = it;
+	}
+}
+
+void P_ClearBlockNodes(void)
+{
+	freeblocks = NULL;
+	freeiters = NULL;
 }
 
 //
diff --git a/src/p_maputl.h b/src/p_maputl.h
index 08de0cb0b5f43bff5609c3cea58b06b14cfd36bb..e894c08a2461caf82397c569549e5bd3213ae3b7 100644
--- a/src/p_maputl.h
+++ b/src/p_maputl.h
@@ -63,6 +63,39 @@ void P_LineOpening(line_t *plinedef, mobj_t *mobj);
 boolean P_BlockLinesIterator(INT32 x, INT32 y, boolean(*func)(line_t *));
 boolean P_BlockThingsIterator(INT32 x, INT32 y, boolean(*func)(mobj_t *));
 
+void P_ClearBlockNodes(void);
+
+typedef struct
+{
+	mobj_t *mobj;
+	int next;
+} bthingit_hash_entry_t;
+
+#define NUM_BTHINGIT_BUCKETS 32
+#define NUM_BTHINGIT_FIXEDHASH 10
+
+typedef struct bthingit_s
+{
+	int x1, y1, x2, y2;
+	int curx, cury;
+	blocknode_t *block;
+
+	int buckets[NUM_BTHINGIT_BUCKETS];
+	bthingit_hash_entry_t fixedhash[NUM_BTHINGIT_FIXEDHASH];
+	int numfixedhash;
+
+	bthingit_hash_entry_t *dynhash;
+	size_t dynhashcount;
+	size_t dynhashcapacity;
+
+	struct bthingit_s *freechain;
+} bthingit_t;
+
+bthingit_t *P_NewBlockThingsIterator(int x1, int y1, int x2, int y2);
+mobj_t *P_BlockThingsIteratorNext(bthingit_t *it, boolean centeronly);
+void P_FreeBlockThingsIterator(bthingit_t *it);
+boolean P_DoBlockThingsIterate(int x1, int y1, int x2, int y2, boolean (*func)(mobj_t *));
+
 #define PT_ADDLINES     1
 #define PT_ADDTHINGS    2
 #define PT_EARLYOUT     4
diff --git a/src/p_mobj.c b/src/p_mobj.c
index f16fef2f00312ef6dd7d8a0cc60bffb447377ccc..28dd9304f6ed390fe23861c66200e6d51dfcb656 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -9455,7 +9455,7 @@ static inline boolean PIT_PushThing(mobj_t *thing)
 
 static void P_PointPushThink(mobj_t *mobj)
 {
-	INT32 xl, xh, yl, yh, bx, by;
+	INT32 xl, xh, yl, yh;
 	fixed_t radius;
 
 	if (!mobj->spawnpoint)
@@ -9470,9 +9470,8 @@ static void P_PointPushThink(mobj_t *mobj)
 	xh = (unsigned)(mobj->x + radius - bmaporgx + MAXRADIUS)>>MAPBLOCKSHIFT;
 	yl = (unsigned)(mobj->y - radius - bmaporgy - MAXRADIUS)>>MAPBLOCKSHIFT;
 	yh = (unsigned)(mobj->y + radius - bmaporgy + MAXRADIUS)>>MAPBLOCKSHIFT;
-	for (bx = xl; bx <= xh; bx++)
-		for (by = yl; by <= yh; by++)
-			P_BlockThingsIterator(bx, by, PIT_PushThing);
+
+	P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_PushThing);
 }
 
 static boolean P_MobjRegularThink(mobj_t *mobj)
diff --git a/src/p_mobj.h b/src/p_mobj.h
index f2e4cbf3d63ff1675825078c1e6fc165aa515af8..f833415ed1d0af3ee377ed6d4ced943d2782f499 100644
--- a/src/p_mobj.h
+++ b/src/p_mobj.h
@@ -273,6 +273,19 @@ typedef enum {
 	PCF_THUNK = 32,
 } precipflag_t;
 
+// [RH] Like msecnode_t, but for the blockmap
+typedef struct blocknode_s
+{
+	struct mobj_s *mobj;
+
+	int blockindex;     // index into blocklinks for the block this node is in
+
+	struct blocknode_s **mprev; // previous actor in this block
+	struct blocknode_s *mnext;  // next actor in this block
+	struct blocknode_s **bprev; // previous block this actor is in
+	struct blocknode_s *bnext;  // next block this actor is in
+} blocknode_t;
+
 // Map Object definition.
 typedef struct mobj_s
 {
@@ -344,8 +357,7 @@ typedef struct mobj_s
 
 	// Interaction info, by BLOCKMAP.
 	// Links in blocks (if needed).
-	struct mobj_s *bnext;
-	struct mobj_s **bprev; // killough 8/11/98: change to ptr-to-ptr
+	blocknode_t *blocknode;
 
 	// Additional pointers for NiGHTS hoops
 	struct mobj_s *hnext;
diff --git a/src/p_polyobj.c b/src/p_polyobj.c
index 331bc5c7f0397876d10c92ad6c316d2ffeb56d41..e779956e8d92092bf41a97db8efa5a7e7fd1d973 100644
--- a/src/p_polyobj.c
+++ b/src/p_polyobj.c
@@ -877,14 +877,17 @@ static void Polyobj_carryThings(polyobj_t *po, fixed_t dx, fixed_t dy)
 		for (x = po->blockbox[BOXLEFT]; x <= po->blockbox[BOXRIGHT]; ++x)
 		{
 			mobj_t *mo;
+			blocknode_t *block;
 
 			if (x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight)
 				continue;
 
-			mo = blocklinks[y * bmapwidth + x];
+			block = blocklinks[y * bmapwidth + x];
 
-			for (; mo; mo = mo->bnext)
+			for (; block; block = block->mnext)
 			{
+				mo = block->mobj;
+
 				if (mo->lastlook == pomovecount)
 					continue;
 
@@ -937,10 +940,12 @@ static INT32 Polyobj_clipThings(polyobj_t *po, line_t *line)
 		{
 			if (!(x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight))
 			{
-				mobj_t *mo = blocklinks[y * bmapwidth + x];
+				mobj_t *mo = NULL;
+				blocknode_t *block = blocklinks[y * bmapwidth + x];
 
-				for (; mo; mo = mo->bnext)
+				for (; block; block = block->mnext)
 				{
+					mo = block->mobj;
 
 					// Don't scroll objects that aren't affected by gravity
 					if (mo->flags & MF_NOGRAVITY)
@@ -1109,14 +1114,17 @@ static void Polyobj_rotateThings(polyobj_t *po, vector2_t origin, angle_t delta,
 		for (x = po->blockbox[BOXLEFT]; x <= po->blockbox[BOXRIGHT]; ++x)
 		{
 			mobj_t *mo;
+			blocknode_t *block;
 
 			if (x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight)
 				continue;
 
-			mo = blocklinks[y * bmapwidth + x];
+			block = blocklinks[y * bmapwidth + x];
 
-			for (; mo; mo = mo->bnext)
+			for (; block; block = block->mnext)
 			{
+				mo = block->mobj;
+
 				if (mo->lastlook == pomovecount)
 					continue;
 
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 8e2517473de63c166a1867518485495174b9fed9..6c6548c567f195cfc7bbc884e1e45dea18afde1a 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -3221,6 +3221,8 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 		slope->normal.x = READFIXED(save_p);
 		slope->normal.y = READFIXED(save_p);
 		slope->normal.z = READFIXED(save_p);
+
+		slope->moved = true;
 	}
 	if (diff2 & MD2_DRAWONLYFORPLAYER)
 		mobj->drawonlyforplayer = &players[READUINT8(save_p)];
diff --git a/src/p_setup.c b/src/p_setup.c
index 851231c423dbd77ba1df777e7de2edb6015faf00..614a820806ca714ba3d86c39b630b9b1ba6ab1bf 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -138,7 +138,7 @@ INT32 *blockmaplump; // Big blockmap
 // origin of block map
 fixed_t bmaporgx, bmaporgy;
 // for thing chains
-mobj_t **blocklinks;
+blocknode_t **blocklinks;
 
 // REJECT
 // For fast sight rejection.
@@ -1650,15 +1650,15 @@ textmap_colormap_t textmap_colormap = { false, 0, 25, 0, 25, 0, 31, 0 };
 
 typedef enum
 {
-    PD_A = 1,
-    PD_B = 1<<1,
-    PD_C = 1<<2,
-    PD_D = 1<<3,
+	PD_A = 1,
+	PD_B = 1<<1,
+	PD_C = 1<<2,
+	PD_D = 1<<3,
 } planedef_t;
 
 typedef struct textmap_plane_s {
-    UINT8 defined;
-    fixed_t a, b, c, d;
+	UINT8 defined;
+	double a, b, c, d;
 } textmap_plane_t;
 
 textmap_plane_t textmap_planefloor = {0, 0, 0, 0, 0};
@@ -1719,42 +1719,42 @@ static void ParseTextmapSectorParameter(UINT32 i, const char *param, const char
 	else if (fastcmp(param, "floorplane_a"))
 	{
 		textmap_planefloor.defined |= PD_A;
-		textmap_planefloor.a = FLOAT_TO_FIXED(atof(val));
+		textmap_planefloor.a = atof(val);
 	}
 	else if (fastcmp(param, "floorplane_b"))
 	{
 		textmap_planefloor.defined |= PD_B;
-		textmap_planefloor.b = FLOAT_TO_FIXED(atof(val));
+		textmap_planefloor.b = atof(val);
 	}
 	else if (fastcmp(param, "floorplane_c"))
 	{
 		textmap_planefloor.defined |= PD_C;
-		textmap_planefloor.c = FLOAT_TO_FIXED(atof(val));
+		textmap_planefloor.c = atof(val);
 	}
 	else if (fastcmp(param, "floorplane_d"))
 	{
 		textmap_planefloor.defined |= PD_D;
-		textmap_planefloor.d = FLOAT_TO_FIXED(atof(val));
+		textmap_planefloor.d = atof(val);
 	}
 	else if (fastcmp(param, "ceilingplane_a"))
 	{
 		textmap_planeceiling.defined |= PD_A;
-		textmap_planeceiling.a = FLOAT_TO_FIXED(atof(val));
+		textmap_planeceiling.a = atof(val);
 	}
 	else if (fastcmp(param, "ceilingplane_b"))
 	{
 		textmap_planeceiling.defined |= PD_B;
-		textmap_planeceiling.b = FLOAT_TO_FIXED(atof(val));
+		textmap_planeceiling.b = atof(val);
 	}
 	else if (fastcmp(param, "ceilingplane_c"))
 	{
 		textmap_planeceiling.defined |= PD_C;
-		textmap_planeceiling.c = FLOAT_TO_FIXED(atof(val));
+		textmap_planeceiling.c = atof(val);
 	}
 	else if (fastcmp(param, "ceilingplane_d"))
 	{
 		textmap_planeceiling.defined |= PD_D;
-		textmap_planeceiling.d = FLOAT_TO_FIXED(atof(val));
+		textmap_planeceiling.d = atof(val);
 	}
 	else if (fastcmp(param, "lightcolor"))
 	{
@@ -2992,13 +2992,13 @@ static void P_LoadTextmap(void)
 
 		if (textmap_planefloor.defined == (PD_A|PD_B|PD_C|PD_D))
 		{
-			sc->f_slope = MakeViaEquationConstants(textmap_planefloor.a, textmap_planefloor.b, textmap_planefloor.c, textmap_planefloor.d);
+			sc->f_slope = P_MakeSlopeViaEquationConstants(textmap_planefloor.a, textmap_planefloor.b, textmap_planefloor.c, textmap_planefloor.d);
 			sc->hasslope = true;
 		}
 
 		if (textmap_planeceiling.defined == (PD_A|PD_B|PD_C|PD_D))
 		{
-			sc->c_slope = MakeViaEquationConstants(textmap_planeceiling.a, textmap_planeceiling.b, textmap_planeceiling.c, textmap_planeceiling.d);
+			sc->c_slope = P_MakeSlopeViaEquationConstants(textmap_planeceiling.a, textmap_planeceiling.b, textmap_planeceiling.c, textmap_planeceiling.d);
 			sc->hasslope = true;
 		}
 
@@ -7869,6 +7869,8 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 		Z_Free(ss->attachedsolid);
 	}
 
+	P_ClearBlockNodes();
+
 	// Clear pointers that would be left dangling by the purge
 	R_FlushTranslationColormapCache();
 
@@ -7913,6 +7915,24 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	if (!P_LoadMapFromFile())
 		return false;
 
+	if (!demoplayback)
+	{
+		clientGamedata->mapvisited[gamemap-1] |= MV_VISITED;
+		serverGamedata->mapvisited[gamemap-1] |= MV_VISITED;
+
+		M_SilentUpdateUnlockablesAndEmblems(serverGamedata);
+
+		if (M_UpdateUnlockablesAndExtraEmblems(clientGamedata))
+		{
+			S_StartSound(NULL, sfx_s3k68);
+			G_SaveGameData(clientGamedata);
+		}
+		else if (!reloadinggamestate)
+		{
+			G_SaveGameData(clientGamedata);
+		}
+	}
+
 	// init anything that P_SpawnSlopes/P_LoadThings needs to know
 	P_InitSpecials();
 
@@ -7971,24 +7991,6 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	nextmapoverride = 0;
 	skipstats = 0;
 
-	if (!demoplayback)
-	{
-		clientGamedata->mapvisited[gamemap-1] |= MV_VISITED;
-		serverGamedata->mapvisited[gamemap-1] |= MV_VISITED;
-
-		M_SilentUpdateUnlockablesAndEmblems(serverGamedata);
-
-		if (M_UpdateUnlockablesAndExtraEmblems(clientGamedata))
-		{
-			S_StartSound(NULL, sfx_s3k68);
-			G_SaveGameData(clientGamedata);
-		}
-		else if (!reloadinggamestate)
-		{
-			G_SaveGameData(clientGamedata);
-		}
-	}
-
 	levelloading = false;
 
 	P_RunCachedActions();
diff --git a/src/p_slopes.c b/src/p_slopes.c
index 1c0ee81a7e9453d204049d2f0ab5986a07849edb..e75d36edefe8d8484cb0c9bebb4d02d5f1b1e174 100644
--- a/src/p_slopes.c
+++ b/src/p_slopes.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2004      by Stephen McGranahan
+// Copyright (C) 2009      by Stephen McGranahan.
 // Copyright (C) 2015-2023 by Sonic Team Junior.
 //
 // This program is free software distributed under the
@@ -14,6 +14,7 @@
 #include "r_defs.h"
 #include "r_state.h"
 #include "m_bbox.h"
+#include "m_vector.h"
 #include "z_zone.h"
 #include "p_local.h"
 #include "p_spec.h"
@@ -28,12 +29,40 @@ pslope_t *slopelist = NULL;
 UINT16 slopecount = 0;
 
 // Calculate line normal
-void P_CalculateSlopeNormal(pslope_t *slope) {
+void P_CalculateSlopeNormal(pslope_t *slope)
+{
 	slope->normal.z = FINECOSINE(slope->zangle>>ANGLETOFINESHIFT);
 	slope->normal.x = FixedMul(FINESINE(slope->zangle>>ANGLETOFINESHIFT), slope->d.x);
 	slope->normal.y = FixedMul(FINESINE(slope->zangle>>ANGLETOFINESHIFT), slope->d.y);
 }
 
+static void CalculateNormalDir(pslope_t *slope, dvector3_t *dnormal)
+{
+	double hyp = hypot(dnormal->x, dnormal->y);
+
+	if (fpclassify(hyp) == FP_NORMAL)
+	{
+		slope->dnormdir.x = -dnormal->x / hyp;
+		slope->dnormdir.y = -dnormal->y / hyp;
+		slope->dzdelta = hyp / dnormal->z;
+	}
+	else
+	{
+		slope->dnormdir.x = slope->dnormdir.y = 0.0;
+		slope->dzdelta = 0.0;
+	}
+}
+
+void P_CalculateSlopeVectors(pslope_t *slope)
+{
+	dvector3_t dnormal;
+
+	DVector3_Load(&dnormal, FixedToDouble(slope->normal.x), FixedToDouble(slope->normal.y), FixedToDouble(slope->normal.z));
+	DVector3_Load(&slope->dorigin, FixedToDouble(slope->o.x), FixedToDouble(slope->o.y), FixedToDouble(slope->o.z));
+
+	CalculateNormalDir(slope, &dnormal);
+}
+
 /// Setup slope via 3 vertexes.
 static void ReconfigureViaVertexes (pslope_t *slope, const vector3_t v1, const vector3_t v2, const vector3_t v3)
 {
@@ -89,22 +118,31 @@ static void ReconfigureViaVertexes (pslope_t *slope, const vector3_t v1, const v
 		slope->xydirection = R_PointToAngle2(0, 0, slope->d.x, slope->d.y)+ANGLE_180;
 		slope->zangle = InvAngle(R_PointToAngle2(0, 0, FRACUNIT, slope->zdelta));
 	}
+
+	P_CalculateSlopeVectors(slope);
 }
 
 /// Setup slope via constants.
-static void ReconfigureViaConstants (pslope_t *slope, const fixed_t a, const fixed_t b, const fixed_t c, const fixed_t d)
+static void ReconfigureViaConstants (pslope_t *slope, const double pa, const double pb, const double pc, const double pd)
 {
 	fixed_t m;
 	fixed_t o = 0;
-	vector3_t *normal = &slope->normal;
+	double d_o = 0.0;
+
+	fixed_t a = DoubleToFixed(pa), b = DoubleToFixed(pb), c = DoubleToFixed(pc), d = DoubleToFixed(pd);
 
 	if (c)
+	{
+		d_o = abs(c) <= FRACUNIT ? -(pd * (1.0 / pc)) : -(pd / pc);
 		o = abs(c) <= FRACUNIT ? -FixedMul(d, FixedDiv(FRACUNIT, c)) : -FixedDiv(d, c);
+	}
 
 	// Set origin.
 	FV3_Load(&slope->o, 0, 0, o);
 
 	// Get slope's normal.
+	vector3_t *normal = &slope->normal;
+
 	FV3_Load(normal, a, b, c);
 	FV3_Normalize(normal);
 
@@ -123,6 +161,17 @@ static void ReconfigureViaConstants (pslope_t *slope, const fixed_t a, const fix
 	// Get angles
 	slope->xydirection = R_PointToAngle2(0, 0, slope->d.x, slope->d.y)+ANGLE_180;
 	slope->zangle = InvAngle(R_PointToAngle2(0, 0, FRACUNIT, slope->zdelta));
+
+	dvector3_t dnormal;
+
+	DVector3_Load(&dnormal, pa, pb, pc);
+	DVector3_Normalize(&dnormal);
+	if (dnormal.z < 0)
+		DVector3_Negate(&dnormal);
+
+	DVector3_Load(&slope->dorigin, 0, 0, d_o);
+
+	CalculateNormalDir(slope, &dnormal);
 }
 
 /// Recalculate dynamic slopes.
@@ -161,6 +210,7 @@ void T_DynamicSlopeLine (dynlineplanethink_t* th)
 	if (slope->zdelta != FixedDiv(zdelta, th->extent)) {
 		slope->zdelta = FixedDiv(zdelta, th->extent);
 		slope->zangle = R_PointToAngle2(0, 0, th->extent, -zdelta);
+		slope->moved = true;
 		P_CalculateSlopeNormal(slope);
 	}
 }
@@ -392,6 +442,7 @@ static void line_SpawnViaLine(const int linenum, const boolean spawnthinker)
 			fslope->xydirection = R_PointToAngle2(origin.x, origin.y, point.x, point.y);
 
 			P_CalculateSlopeNormal(fslope);
+			P_CalculateSlopeVectors(fslope);
 
 			if (spawnthinker && (flags & SL_DYNAMIC))
 				P_AddDynLineSlopeThinker(fslope, DP_FRONTFLOOR, line, extent);
@@ -409,6 +460,7 @@ static void line_SpawnViaLine(const int linenum, const boolean spawnthinker)
 			cslope->xydirection = R_PointToAngle2(origin.x, origin.y, point.x, point.y);
 
 			P_CalculateSlopeNormal(cslope);
+			P_CalculateSlopeVectors(cslope);
 
 			if (spawnthinker && (flags & SL_DYNAMIC))
 				P_AddDynLineSlopeThinker(cslope, DP_FRONTCEIL, line, extent);
@@ -449,6 +501,7 @@ static void line_SpawnViaLine(const int linenum, const boolean spawnthinker)
 			fslope->xydirection = R_PointToAngle2(origin.x, origin.y, point.x, point.y);
 
 			P_CalculateSlopeNormal(fslope);
+			P_CalculateSlopeVectors(fslope);
 
 			if (spawnthinker && (flags & SL_DYNAMIC))
 				P_AddDynLineSlopeThinker(fslope, DP_BACKFLOOR, line, extent);
@@ -466,6 +519,7 @@ static void line_SpawnViaLine(const int linenum, const boolean spawnthinker)
 			cslope->xydirection = R_PointToAngle2(origin.x, origin.y, point.x, point.y);
 
 			P_CalculateSlopeNormal(cslope);
+			P_CalculateSlopeVectors(cslope);
 
 			if (spawnthinker && (flags & SL_DYNAMIC))
 				P_AddDynLineSlopeThinker(cslope, DP_BACKCEIL, line, extent);
@@ -695,7 +749,7 @@ pslope_t *P_SlopeById(UINT16 id)
 }
 
 /// Creates a new slope from equation constants.
-pslope_t *MakeViaEquationConstants(const fixed_t a, const fixed_t b, const fixed_t c, const fixed_t d)
+pslope_t *P_MakeSlopeViaEquationConstants(const double a, const double b, const double c, const double d)
 {
 	pslope_t* ret = Slope_Add(0);
 
diff --git a/src/p_slopes.h b/src/p_slopes.h
index 096bf8f82a3342494b692a5e16cb6fbde6647438..fdc07f67e257ba05eef35fc481c3be3a0ebdfd7c 100644
--- a/src/p_slopes.h
+++ b/src/p_slopes.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2004      by Stephen McGranahan
+// Copyright (C) 2009      by Stephen McGranahan.
 // Copyright (C) 2015-2023 by Sonic Team Junior.
 //
 // This program is free software distributed under the
@@ -13,7 +13,7 @@
 #ifndef P_SLOPES_H__
 #define P_SLOPES_H__
 
-#include "m_fixed.h" // Vectors
+#include "m_fixed.h"
 
 extern pslope_t *slopelist;
 extern UINT16 slopecount;
@@ -51,6 +51,7 @@ typedef enum
 void P_LinkSlopeThinkers (void);
 
 void P_CalculateSlopeNormal(pslope_t *slope);
+void P_CalculateSlopeVectors(pslope_t *slope);
 void P_InitSlopes(void);
 void P_SpawnSlopes(const boolean fromsave);
 
@@ -88,7 +89,7 @@ fixed_t P_GetWallTransferMomZ(mobj_t *mo, pslope_t *slope);
 void P_HandleSlopeLanding(mobj_t *thing, pslope_t *slope);
 void P_ButteredSlope(mobj_t *mo);
 
-pslope_t *MakeViaEquationConstants(const fixed_t a, const fixed_t b, const fixed_t c, const fixed_t d);
+pslope_t *P_MakeSlopeViaEquationConstants(const double a, const double b, const double c, const double d);
 
 /// Dynamic plane type enum for the thinker. Will have a different functionality depending on this.
 typedef enum {
diff --git a/src/p_user.c b/src/p_user.c
index 7e19e8d6367dbb874c7b21b105dd5f18e6dba9fb..200b7673659eb4f4975cb76723c998da7f3c449c 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -3807,6 +3807,8 @@ static boolean PIT_CheckSolidsTeeter(mobj_t *thing)
 	if (abs(thing->x - teeterer->x) >= blockdist || abs(thing->y - teeterer->y) >= blockdist)
 		return true; // didn't hit it
 
+	highesttop = INT32_MIN;
+
 	if (teeterer->eflags & MFE_VERTICALFLIP)
 	{
 		if (thingtop < teeterer->z)
@@ -4110,13 +4112,8 @@ static void P_DoTeeter(player_t *player)
 			teeteryl = teeteryh = player->mo->y;
 			couldteeter = false;
 			solidteeter = teeter;
-			for (by = yl; by <= yh; by++)
-				for (bx = xl; bx <= xh; bx++)
-				{
-					highesttop = INT32_MIN;
-					if (!P_BlockThingsIterator(bx, by, PIT_CheckSolidsTeeter))
-						goto teeterdone; // we've found something that stops us teetering at all, how about we stop already
-				}
+			if (!P_DoBlockThingsIterate(xl, yl, xh, yh, PIT_CheckSolidsTeeter))
+				goto teeterdone; // we've found something that stops us teetering at all
 teeterdone:
 			teeter = solidteeter;
 			P_SetTarget(&tmthing, oldtmthing); // restore old tmthing, goodness knows what the game does with this before mobj thinkers
diff --git a/src/r_defs.h b/src/r_defs.h
index 16c660b01938ed86669550f709f7b65336cfaebc..6e0375e615aed5596e68eb67b113fe61a907e477 100644
--- a/src/r_defs.h
+++ b/src/r_defs.h
@@ -16,6 +16,7 @@
 
 // Some more or less basic data types we depend on.
 #include "m_fixed.h"
+#include "m_vector.h"
 
 // We rely on the thinker data struct to handle sound origins in sectors.
 #include "d_think.h"
@@ -343,6 +344,13 @@ typedef struct pslope_s
 	angle_t zangle;		/// Precomputed angle of the plane going up from the ground (not measured in degrees).
 	angle_t xydirection;/// Precomputed angle of the normal's projection on the XY plane.
 
+	dvector3_t dorigin;
+	dvector3_t dnormdir;
+
+	double dzdelta;
+
+	boolean moved : 1;
+
 	UINT8 flags; // Slope options
 } pslope_t;
 
diff --git a/src/r_draw.c b/src/r_draw.c
index ff2e43df31d9291ed6ae07facf2da0c1beaaa4e1..380bacdf9f565bba613f698cd05d70368a187838 100644
--- a/src/r_draw.c
+++ b/src/r_draw.c
@@ -113,8 +113,9 @@ UINT8 *ds_source; // points to the start of a flat
 UINT8 *ds_transmap; // one of the translucency tables
 
 // Vectors for Software's tilted slope drawers
-floatv3_t ds_su, ds_sv, ds_sz, ds_slopelight;
-float focallengthf, zeroheight;
+dvector3_t ds_su, ds_sv, ds_sz, ds_slopelight;
+double zeroheight;
+float focallengthf;
 
 /**	\brief Variable flat sizes
 */
@@ -129,7 +130,7 @@ static colorcache_t **translationtablecache[TT_CACHE_SIZE] = {NULL};
 
 boolean skincolor_modified[MAXSKINCOLORS];
 
-static INT32 SkinToCacheIndex(INT32 translation)
+INT32 R_SkinTranslationToCacheIndex(INT32 translation)
 {
 	switch (translation)
 	{
@@ -556,7 +557,7 @@ UINT8* R_GetTranslationColormap(INT32 skinnum, skincolornum_t color, UINT8 flags
 	else if (skinnum <= TC_DEFAULT)
 	{
 		// Do default translation
-		index = SkinToCacheIndex(skinnum);
+		index = R_SkinTranslationToCacheIndex(skinnum);
 	}
 	else
 		I_Error("Invalid translation %d", skinnum);
diff --git a/src/r_draw.h b/src/r_draw.h
index 29370015a1c46cb6405bf5b698d6c23425ee79e2..0af911233bfdb6097c8326bf014be84ee7418cad 100644
--- a/src/r_draw.h
+++ b/src/r_draw.h
@@ -66,13 +66,10 @@ extern boolean ds_powersoftwo, ds_solidcolor, ds_fog;
 extern UINT8 *ds_source;
 extern UINT8 *ds_transmap;
 
-typedef struct {
-	float x, y, z;
-} floatv3_t;
-
 // Vectors for Software's tilted slope drawers
-extern floatv3_t ds_su, ds_sv, ds_sz, ds_slopelight;
-extern float focallengthf, zeroheight;
+extern dvector3_t ds_su, ds_sv, ds_sz, ds_slopelight;
+extern double zeroheight;
+extern float focallengthf;
 
 // Variable flat sizes
 extern UINT32 nflatxshift;
@@ -117,6 +114,8 @@ enum
 	TC_DEFAULT
 };
 
+INT32 R_SkinTranslationToCacheIndex(INT32 translation);
+
 // Amount of colors in the palette
 #define NUM_PALETTE_ENTRIES 256
 
diff --git a/src/r_fps.c b/src/r_fps.c
index de450aaa7f465b8d47891baec585c757de361c60..83fd0eec15f4c9fd0f41fa60104be022b9b4c7eb 100644
--- a/src/r_fps.c
+++ b/src/r_fps.c
@@ -19,9 +19,9 @@
 #include "i_video.h"
 #include "r_plane.h"
 #include "p_spec.h"
+#include "p_slopes.h"
 #include "r_state.h"
 #include "z_zone.h"
-#include "console.h" // con_startup_loadprogress
 #include "m_perfstats.h" // ps_metric_t
 #ifdef HWRENDER
 #include "hardware/hw_main.h" // for cv_glshearing
@@ -482,6 +482,7 @@ void R_CreateInterpolator_Polyobj(thinker_t *thinker, polyobj_t *polyobj)
 
 	interp->polyobj.oldcx = interp->polyobj.bakcx = polyobj->centerPt.x;
 	interp->polyobj.oldcy = interp->polyobj.bakcy = polyobj->centerPt.y;
+	interp->polyobj.oldangle = interp->polyobj.bakangle = polyobj->angle;
 }
 
 void R_CreateInterpolator_DynSlope(thinker_t *thinker, pslope_t *slope)
@@ -505,6 +506,15 @@ void R_InitializeLevelInterpolators(void)
 	levelinterpolators = NULL;
 }
 
+static void RecalculatePolyobjectSegAngles(polyobj_t *polyobj)
+{
+	for (size_t i = 0; i < polyobj->segCount; i++)
+	{
+		seg_t *seg = polyobj->segs[i];
+		seg->angle = R_PointToAngle2(seg->v1->x, seg->v1->y, seg->v2->x, seg->v2->y);
+	}
+}
+
 static void UpdateLevelInterpolatorState(levelinterpolator_t *interp)
 {
 	size_t i;
@@ -535,10 +545,13 @@ static void UpdateLevelInterpolatorState(levelinterpolator_t *interp)
 			interp->polyobj.bakvertices[i * 2    ] = interp->polyobj.polyobj->vertices[i]->x;
 			interp->polyobj.bakvertices[i * 2 + 1] = interp->polyobj.polyobj->vertices[i]->y;
 		}
+		RecalculatePolyobjectSegAngles(interp->polyobj.polyobj);
 		interp->polyobj.oldcx = interp->polyobj.bakcx;
 		interp->polyobj.oldcy = interp->polyobj.bakcy;
+		interp->polyobj.oldangle = interp->polyobj.bakangle;
 		interp->polyobj.bakcx = interp->polyobj.polyobj->centerPt.x;
 		interp->polyobj.bakcy = interp->polyobj.polyobj->centerPt.y;
+		interp->polyobj.bakangle = interp->polyobj.polyobj->angle;
 		break;
 	case LVLINTERP_DynSlope:
 		FV3_Copy(&interp->dynslope.oldo, &interp->dynslope.bako);
@@ -624,13 +637,16 @@ void R_ApplyLevelInterpolators(fixed_t frac)
 				interp->polyobj.polyobj->vertices[ii]->x = R_LerpFixed(interp->polyobj.oldvertices[ii * 2    ], interp->polyobj.bakvertices[ii * 2    ], frac);
 				interp->polyobj.polyobj->vertices[ii]->y = R_LerpFixed(interp->polyobj.oldvertices[ii * 2 + 1], interp->polyobj.bakvertices[ii * 2 + 1], frac);
 			}
+			RecalculatePolyobjectSegAngles(interp->polyobj.polyobj);
 			interp->polyobj.polyobj->centerPt.x = R_LerpFixed(interp->polyobj.oldcx, interp->polyobj.bakcx, frac);
 			interp->polyobj.polyobj->centerPt.y = R_LerpFixed(interp->polyobj.oldcy, interp->polyobj.bakcy, frac);
+			interp->polyobj.polyobj->angle = R_LerpAngle(interp->polyobj.oldangle, interp->polyobj.bakangle, frac);
 			break;
 		case LVLINTERP_DynSlope:
 			R_LerpVector3(&interp->dynslope.oldo, &interp->dynslope.bako, frac, &interp->dynslope.slope->o);
 			R_LerpVector2(&interp->dynslope.oldd, &interp->dynslope.bakd, frac, &interp->dynslope.slope->d);
 			interp->dynslope.slope->zdelta = R_LerpFixed(interp->dynslope.oldzdelta, interp->dynslope.bakzdelta, frac);
+			interp->dynslope.slope->moved = true;
 			break;
 		}
 	}
@@ -679,8 +695,10 @@ void R_RestoreLevelInterpolators(void)
 				interp->polyobj.polyobj->vertices[ii]->x = interp->polyobj.bakvertices[ii * 2    ];
 				interp->polyobj.polyobj->vertices[ii]->y = interp->polyobj.bakvertices[ii * 2 + 1];
 			}
+			RecalculatePolyobjectSegAngles(interp->polyobj.polyobj);
 			interp->polyobj.polyobj->centerPt.x = interp->polyobj.bakcx;
 			interp->polyobj.polyobj->centerPt.y = interp->polyobj.bakcy;
+			interp->polyobj.polyobj->angle = interp->polyobj.bakangle;
 			break;
 		case LVLINTERP_DynSlope:
 			FV3_Copy(&interp->dynslope.slope->o, &interp->dynslope.bako);
diff --git a/src/r_fps.h b/src/r_fps.h
index f43d29f300a8a6707a3e4c6f5fa24e1e3f0ea37f..cd40b0a9a572b23b97bb9b2cc493ea6a34a243a7 100644
--- a/src/r_fps.h
+++ b/src/r_fps.h
@@ -108,6 +108,7 @@ typedef struct levelinterpolator_s {
 			fixed_t *bakvertices;
 			size_t vertices_size;
 			fixed_t oldcx, oldcy, bakcx, bakcy;
+			angle_t oldangle, bakangle;
 		} polyobj;
 		struct {
 			pslope_t *slope;
diff --git a/src/r_plane.c b/src/r_plane.c
index 612c650fcb0dd8c66cab3dcfaaefbbe15477b446..11aa6c941ae916cfcd924d0ea55c20709f1f3081 100644
--- a/src/r_plane.c
+++ b/src/r_plane.c
@@ -84,11 +84,11 @@ fixed_t yslopetab[MAXVIDHEIGHT*16];
 fixed_t *yslope;
 
 static fixed_t xoffs, yoffs;
-static floatv3_t slope_origin, slope_u, slope_v;
-static floatv3_t slope_lightu, slope_lightv;
+static dvector3_t slope_origin, slope_u, slope_v;
+static dvector3_t slope_lightu, slope_lightv;
 
 static void CalcSlopePlaneVectors(visplane_t *pl, fixed_t xoff, fixed_t yoff);
-static void CalcSlopeLightVectors(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t height, float ang, angle_t plangle);
+static void CalcSlopeLightVectors(pslope_t *slope, fixed_t xpos, fixed_t ypos, double height, float ang, angle_t plangle);
 
 static void DoSlopeCrossProducts(void);
 static void DoSlopeLightCrossProduct(void);
@@ -660,16 +660,18 @@ static void R_DrawSkyPlane(visplane_t *pl)
 	}
 }
 
-// Returns the height of the sloped plane at (x, y) as a 32.16 fixed_t
-static INT64 R_GetSlopeZAt(const pslope_t *slope, fixed_t x, fixed_t y)
+// Returns the height of the sloped plane at (x, y) as a double
+static double R_GetSlopeZAt(const pslope_t *slope, fixed_t x, fixed_t y)
 {
-	INT64 x64 = ((INT64)x - (INT64)slope->o.x);
-	INT64 y64 = ((INT64)y - (INT64)slope->o.y);
+	// If you want to reimplement this using just the equation constants, use this instead:
+	// (d + a*x + b*y) * -(1.0 / c)
 
-	x64 = (x64 * (INT64)slope->d.x) / FRACUNIT;
-	y64 = (y64 * (INT64)slope->d.y) / FRACUNIT;
+	double px = FixedToDouble(x) - slope->dorigin.x;
+	double py = FixedToDouble(y) - slope->dorigin.y;
 
-	return (INT64)slope->o.z + ((x64 + y64) * (INT64)slope->zdelta) / FRACUNIT;
+	double dist = (px * slope->dnormdir.x) + (py * slope->dnormdir.y);
+
+	return slope->dorigin.z + (dist * slope->dzdelta);
 }
 
 // Sets the texture origin vector of the sloped plane.
@@ -687,19 +689,25 @@ static void R_SetSlopePlaneOrigin(pslope_t *slope, fixed_t xpos, fixed_t ypos, f
 	// errors if the flat is rotated.
 	slope_origin.x = vxf * cos(ang) - vyf * sin(ang);
 	slope_origin.z = vxf * sin(ang) + vyf * cos(ang);
-	slope_origin.y = (R_GetSlopeZAt(slope, -xoff, yoff) - zpos) / (float)FRACUNIT;
+	slope_origin.y = R_GetSlopeZAt(slope, -xoff, yoff) - FixedToDouble(zpos);
 }
 
 // This function calculates all of the vectors necessary for drawing a sloped plane.
 void R_SetSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, fixed_t xoff, fixed_t yoff, angle_t angle, angle_t plangle)
 {
 	// I copied ZDoom's code and adapted it to SRB2... -Red
-	fixed_t height, z_at_xy;
+	double height, z_at_xy;
 	float ang;
 
+	if (slope->moved)
+	{
+		P_CalculateSlopeVectors(slope);
+		slope->moved = false;
+	}
+
 	R_SetSlopePlaneOrigin(slope, xpos, ypos, zpos, xoff, yoff, angle);
-	height = P_GetSlopeZAt(slope, xpos, ypos);
-	zeroheight = FixedToFloat(height - zpos);
+	height = R_GetSlopeZAt(slope, xpos, ypos);
+	zeroheight = height - FixedToDouble(zpos);
 
 	ang = ANG2RAD(ANGLE_180 - (angle + plangle));
 
@@ -711,19 +719,19 @@ void R_SetSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos,
 		return;
 	}
 
-	// slope_v is the v direction vector in view space
+	// the v direction vector in view space
 	slope_v.x = cos(ang);
 	slope_v.z = sin(ang);
 
-	// slope_u is the u direction vector in view space
+	// the u direction vector in view space
 	slope_u.x = sin(ang);
 	slope_u.z = -cos(ang);
 
 	plangle >>= ANGLETOFINESHIFT;
-	z_at_xy = P_GetSlopeZAt(slope, xpos + FINESINE(plangle), ypos + FINECOSINE(plangle));
-	slope_v.y = FixedToFloat(z_at_xy - height);
-	z_at_xy = P_GetSlopeZAt(slope, xpos + FINECOSINE(plangle), ypos - FINESINE(plangle));
-	slope_u.y = FixedToFloat(z_at_xy - height);
+	z_at_xy = R_GetSlopeZAt(slope, xpos + FINESINE(plangle), ypos + FINECOSINE(plangle));
+	slope_v.y = z_at_xy - height;
+	z_at_xy = R_GetSlopeZAt(slope, xpos + FINECOSINE(plangle), ypos - FINESINE(plangle));
+	slope_u.y = z_at_xy - height;
 
 	DoSlopeCrossProducts();
 	DoSlopeLightCrossProduct();
@@ -732,13 +740,18 @@ void R_SetSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos,
 // This function calculates all of the vectors necessary for drawing a sloped and scaled plane.
 void R_SetScaledSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, fixed_t xs, fixed_t ys, fixed_t xoff, fixed_t yoff, angle_t angle, angle_t plangle)
 {
-	fixed_t height, z_at_xy;
-
+	double height, z_at_xy;
 	float ang;
 
+	if (slope->moved)
+	{
+		P_CalculateSlopeVectors(slope);
+		slope->moved = false;
+	}
+
 	R_SetSlopePlaneOrigin(slope, xpos, ypos, zpos, xoff, yoff, angle);
-	height = P_GetSlopeZAt(slope, xpos, ypos);
-	zeroheight = FixedToFloat(height - zpos);
+	height = R_GetSlopeZAt(slope, xpos, ypos);
+	zeroheight = height - FixedToDouble(zpos);
 
 	ang = ANG2RAD(ANGLE_180 - (angle + plangle));
 
@@ -753,27 +766,27 @@ void R_SetScaledSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t
 	float xscale = FixedToFloat(xs);
 	float yscale = FixedToFloat(ys);
 
-	// m is the v direction vector in view space
+	// the v direction vector in view space
 	slope_v.x = yscale * cos(ang);
 	slope_v.z = yscale * sin(ang);
 
-	// n is the u direction vector in view space
+	// the u direction vector in view space
 	slope_u.x = xscale * sin(ang);
 	slope_u.z = -xscale * cos(ang);
 
 	ang = ANG2RAD(plangle);
-	z_at_xy = P_GetSlopeZAt(slope, xpos + FloatToFixed(yscale * sin(ang)), ypos + FloatToFixed(yscale * cos(ang)));
-	slope_v.y = FixedToFloat(z_at_xy - height);
-	z_at_xy = P_GetSlopeZAt(slope, xpos + FloatToFixed(xscale * cos(ang)), ypos - FloatToFixed(xscale * sin(ang)));
-	slope_u.y = FixedToFloat(z_at_xy - height);
+	z_at_xy = R_GetSlopeZAt(slope, xpos + FloatToFixed(yscale * sin(ang)), ypos + FloatToFixed(yscale * cos(ang)));
+	slope_v.y = z_at_xy - height;
+	z_at_xy = R_GetSlopeZAt(slope, xpos + FloatToFixed(xscale * cos(ang)), ypos - FloatToFixed(xscale * sin(ang)));
+	slope_u.y = z_at_xy - height;
 
 	DoSlopeCrossProducts();
 	DoSlopeLightCrossProduct();
 }
 
-static void CalcSlopeLightVectors(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t height, float ang, angle_t plangle)
+static void CalcSlopeLightVectors(pslope_t *slope, fixed_t xpos, fixed_t ypos, double height, float ang, angle_t plangle)
 {
-	fixed_t z_at_xy;
+	double z_at_xy;
 
 	slope_lightv.x = cos(ang);
 	slope_lightv.z = sin(ang);
@@ -782,25 +795,17 @@ static void CalcSlopeLightVectors(pslope_t *slope, fixed_t xpos, fixed_t ypos, f
 	slope_lightu.z = -cos(ang);
 
 	plangle >>= ANGLETOFINESHIFT;
-	z_at_xy = P_GetSlopeZAt(slope, xpos + FINESINE(plangle), ypos + FINECOSINE(plangle));
-	slope_lightv.y = FixedToFloat(z_at_xy - height);
-	z_at_xy = P_GetSlopeZAt(slope, xpos + FINECOSINE(plangle), ypos - FINESINE(plangle));
-	slope_lightu.y = FixedToFloat(z_at_xy - height);
+	z_at_xy = R_GetSlopeZAt(slope, xpos + FINESINE(plangle), ypos + FINECOSINE(plangle));
+	slope_lightv.y = z_at_xy - height;
+	z_at_xy = R_GetSlopeZAt(slope, xpos + FINECOSINE(plangle), ypos - FINESINE(plangle));
+	slope_lightu.y = z_at_xy - height;
 }
 
-// Eh. I tried making this stuff fixed-point and it exploded on me. Here's a macro for the only floating-point vector function I recall using.
-#define CROSS(d, v1, v2) \
-d.x = (v1.y * v2.z) - (v1.z * v2.y);\
-d.y = (v1.z * v2.x) - (v1.x * v2.z);\
-d.z = (v1.x * v2.y) - (v1.y * v2.x)
-
 static void DoSlopeCrossProducts(void)
 {
-	float sfmult = 65536.f;
-
-	CROSS(ds_su, slope_origin, slope_v);
-	CROSS(ds_sv, slope_origin, slope_u);
-	CROSS(ds_sz, slope_v, slope_u);
+	DVector3_Cross(&slope_origin, &slope_v, &ds_su);
+	DVector3_Cross(&slope_origin, &slope_u, &ds_sv);
+	DVector3_Cross(&slope_v, &slope_u, &ds_sz);
 
 	ds_su.z *= focallengthf;
 	ds_sv.z *= focallengthf;
@@ -810,6 +815,8 @@ static void DoSlopeCrossProducts(void)
 		return;
 
 	// Premultiply the texture vectors with the scale factors
+	float sfmult = 65536.f;
+
 	if (ds_powersoftwo)
 		sfmult *= 1 << nflatshiftup;
 
@@ -823,13 +830,11 @@ static void DoSlopeCrossProducts(void)
 
 static void DoSlopeLightCrossProduct(void)
 {
-	CROSS(ds_slopelight, slope_lightv, slope_lightu);
+	DVector3_Cross(&slope_lightv, &slope_lightu, &ds_slopelight);
 
 	ds_slopelight.z *= focallengthf;
 }
 
-#undef CROSS
-
 static void CalcSlopePlaneVectors(visplane_t *pl, fixed_t xoff, fixed_t yoff)
 {
 	if (!ds_fog && (pl->xscale != FRACUNIT || pl->yscale != FRACUNIT))
diff --git a/src/r_segs.c b/src/r_segs.c
index 9340ca50c7cf2e01f4c4a40f65df869d8d72dfc1..7877f1353e5b24739d86a62bb940dbe729d85afc 100644
--- a/src/r_segs.c
+++ b/src/r_segs.c
@@ -1705,26 +1705,23 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 		// left
 		temp = xtoviewangle[start]+viewangle;
 
-#define FIXED_TO_DOUBLE(x) (((double)(x)) / ((double)FRACUNIT))
-#define DOUBLE_TO_FIXED(x) (fixed_t)((x) * ((double)FRACUNIT))
-
 		{
 			// Both lines can be written in slope-intercept form, so figure out line intersection
 			double a1, b1, c1, a2, b2, c2, det; // 1 is the seg, 2 is the view angle vector...
 			///TODO: convert to fixed point
 
-			a1 = FIXED_TO_DOUBLE(curline->v2->y-curline->v1->y);
-			b1 = FIXED_TO_DOUBLE(curline->v1->x-curline->v2->x);
-			c1 = a1*FIXED_TO_DOUBLE(curline->v1->x) + b1*FIXED_TO_DOUBLE(curline->v1->y);
+			a1 = FixedToDouble(curline->v2->y-curline->v1->y);
+			b1 = FixedToDouble(curline->v1->x-curline->v2->x);
+			c1 = a1*FixedToDouble(curline->v1->x) + b1*FixedToDouble(curline->v1->y);
 
-			a2 = -FIXED_TO_DOUBLE(FINESINE(temp>>ANGLETOFINESHIFT));
-			b2 = FIXED_TO_DOUBLE(FINECOSINE(temp>>ANGLETOFINESHIFT));
-			c2 = a2*FIXED_TO_DOUBLE(viewx) + b2*FIXED_TO_DOUBLE(viewy);
+			a2 = -FixedToDouble(FINESINE(temp>>ANGLETOFINESHIFT));
+			b2 = FixedToDouble(FINECOSINE(temp>>ANGLETOFINESHIFT));
+			c2 = a2*FixedToDouble(viewx) + b2*FixedToDouble(viewy);
 
 			det = a1*b2 - a2*b1;
 
-			ds_p->leftpos.x = segleft.x = DOUBLE_TO_FIXED((b2*c1 - b1*c2)/det);
-			ds_p->leftpos.y = segleft.y = DOUBLE_TO_FIXED((a1*c2 - a2*c1)/det);
+			ds_p->leftpos.x = segleft.x = DoubleToFixed((b2*c1 - b1*c2)/det);
+			ds_p->leftpos.y = segleft.y = DoubleToFixed((a1*c2 - a2*c1)/det);
 		}
 
 		// right
@@ -1735,26 +1732,21 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			double a1, b1, c1, a2, b2, c2, det; // 1 is the seg, 2 is the view angle vector...
 			///TODO: convert to fixed point
 
-			a1 = FIXED_TO_DOUBLE(curline->v2->y-curline->v1->y);
-			b1 = FIXED_TO_DOUBLE(curline->v1->x-curline->v2->x);
-			c1 = a1*FIXED_TO_DOUBLE(curline->v1->x) + b1*FIXED_TO_DOUBLE(curline->v1->y);
+			a1 = FixedToDouble(curline->v2->y-curline->v1->y);
+			b1 = FixedToDouble(curline->v1->x-curline->v2->x);
+			c1 = a1*FixedToDouble(curline->v1->x) + b1*FixedToDouble(curline->v1->y);
 
-			a2 = -FIXED_TO_DOUBLE(FINESINE(temp>>ANGLETOFINESHIFT));
-			b2 = FIXED_TO_DOUBLE(FINECOSINE(temp>>ANGLETOFINESHIFT));
-			c2 = a2*FIXED_TO_DOUBLE(viewx) + b2*FIXED_TO_DOUBLE(viewy);
+			a2 = -FixedToDouble(FINESINE(temp>>ANGLETOFINESHIFT));
+			b2 = FixedToDouble(FINECOSINE(temp>>ANGLETOFINESHIFT));
+			c2 = a2*FixedToDouble(viewx) + b2*FixedToDouble(viewy);
 
 			det = a1*b2 - a2*b1;
 
-			ds_p->rightpos.x = segright.x = DOUBLE_TO_FIXED((b2*c1 - b1*c2)/det);
-			ds_p->rightpos.y = segright.y = DOUBLE_TO_FIXED((a1*c2 - a2*c1)/det);
+			ds_p->rightpos.x = segright.x = DoubleToFixed((b2*c1 - b1*c2)/det);
+			ds_p->rightpos.y = segright.y = DoubleToFixed((a1*c2 - a2*c1)/det);
 		}
-
-#undef FIXED_TO_DOUBLE
-#undef DOUBLE_TO_FIXED
-
 	}
 
-
 #define SLOPEPARAMS(slope, end1, end2, normalheight) \
 	end1 = P_GetZAt(slope,  segleft.x,  segleft.y, normalheight); \
 	end2 = P_GetZAt(slope, segright.x, segright.y, normalheight);
diff --git a/src/r_things.c b/src/r_things.c
index 03e94d9fc32737c41a555f003f9bf28476c7f9f5..07420e34ad284e9dea9998e443baa37613e071a8 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -770,6 +770,22 @@ UINT8 *R_GetTranslationForThing(mobj_t *mobj, skincolornum_t color, UINT16 trans
 	if (is_player) // This thing is a player!
 		skinnum = ((skin_t*)mobj->skin)->skinnum;
 
+	if (color != SKINCOLOR_NONE)
+	{
+		// New colormap stuff for skins Tails 06-07-2002
+		if (mobj->colorized)
+			skinnum = TC_RAINBOW;
+		else if (mobj->player && mobj->player->dashmode >= DASHMODE_THRESHOLD
+			&& (mobj->player->charflags & SF_DASHMODE)
+			&& ((leveltime/2) & 1))
+		{
+			if (mobj->player->charflags & SF_MACHINE)
+				skinnum = TC_DASHMODE;
+			else
+				skinnum = TC_RAINBOW;
+		}
+	}
+
 	if (R_ThingIsFlashing(mobj)) // Bosses "flash"
 	{
 		if (mobj->type == MT_CYBRAKDEMON || mobj->colorized)
@@ -786,22 +802,7 @@ UINT8 *R_GetTranslationForThing(mobj_t *mobj, skincolornum_t color, UINT16 trans
 			return tr;
 	}
 	else if (color != SKINCOLOR_NONE)
-	{
-		// New colormap stuff for skins Tails 06-07-2002
-		if (mobj->colorized)
-			return R_GetTranslationColormap(TC_RAINBOW, color, GTC_CACHE);
-		else if (mobj->player && mobj->player->dashmode >= DASHMODE_THRESHOLD
-			&& (mobj->player->charflags & SF_DASHMODE)
-			&& ((leveltime/2) & 1))
-		{
-			if (mobj->player->charflags & SF_MACHINE)
-				return R_GetTranslationColormap(TC_DASHMODE, 0, GTC_CACHE);
-			else
-				return R_GetTranslationColormap(TC_RAINBOW, color, GTC_CACHE);
-		}
-		else
-			return R_GetTranslationColormap(skinnum, color, GTC_CACHE);
-	}
+		return R_GetTranslationColormap(skinnum, color, GTC_CACHE);
 	else if (mobj->sprite == SPR_PLAY) // Looks like a player, but doesn't have a color? Get rid of green sonic syndrome.
 		return R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_BLUE, GTC_CACHE);
 
diff --git a/src/r_translation.c b/src/r_translation.c
index 7e1e30d0cdec7afd2d810f6b8d1a6b26da0c42bb..3b123f14ca1732e2e9605dad3153fd5f6d0e2338 100644
--- a/src/r_translation.c
+++ b/src/r_translation.c
@@ -1126,17 +1126,19 @@ UINT8 *R_GetTranslationRemap(int id, skincolornum_t skincolor, INT32 skinnum)
 	if (!tr->skincolor_remaps)
 		Z_Calloc(sizeof(*tr->skincolor_remaps) * TT_CACHE_SIZE, PU_LEVEL, &tr->skincolor_remaps);
 
-	if (!tr->skincolor_remaps[skinnum])
-		tr->skincolor_remaps[skinnum] = Z_Calloc(NUM_PALETTE_ENTRIES * MAXSKINCOLORS, PU_LEVEL, NULL);
+	INT32 index = R_SkinTranslationToCacheIndex(skinnum);
 
-	colorcache_t *cache = tr->skincolor_remaps[skinnum][skincolor];
+	if (!tr->skincolor_remaps[index])
+		tr->skincolor_remaps[index] = Z_Calloc(NUM_PALETTE_ENTRIES * (MAXSKINCOLORS - 1), PU_LEVEL, NULL);
+
+	colorcache_t *cache = tr->skincolor_remaps[index][skincolor - 1];
 	if (!cache)
 	{
 		cache = Z_Calloc(sizeof(colorcache_t), PU_LEVEL, NULL);
 
 		R_ApplyTranslationRemap(tr, cache->colors, skincolor, skinnum);
 
-		tr->skincolor_remaps[skinnum][skincolor] = cache;
+		tr->skincolor_remaps[index][skincolor - 1] = cache;
 	}
 
 	return cache->colors;
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index d282b3b4a1caee6d39a8746b6523d88b7443fadf..7237c491e8c3353e8216ec9478aa652e1d58e40b 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -295,6 +295,7 @@
     <ClInclude Include="..\m_tokenizer.h" />
     <ClInclude Include="..\m_perfstats.h" />
     <ClInclude Include="..\m_queue.h" />
+    <ClInclude Include="..\m_vector.h" />
     <ClInclude Include="..\m_random.h" />
     <ClInclude Include="..\m_swap.h" />
     <ClInclude Include="..\netcode\client_connection.h" />
@@ -474,6 +475,7 @@
     <ClCompile Include="..\m_tokenizer.c" />
     <ClCompile Include="..\m_perfstats.c" />
     <ClCompile Include="..\m_queue.c" />
+    <ClCompile Include="..\m_vector.c" />
     <ClCompile Include="..\m_random.c" />
     <ClCompile Include="..\netcode\client_connection.c" />
     <ClCompile Include="..\netcode\commands.c" />
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj.filters b/src/sdl/Srb2SDL-vc10.vcxproj.filters
index 4a2a5d5950924a079e2141359c96e3c88baa84c0..ecaaa5d2e2568f81eacfc25de71449f82af41bc4 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj.filters
+++ b/src/sdl/Srb2SDL-vc10.vcxproj.filters
@@ -348,6 +348,9 @@
     <ClInclude Include="..\m_queue.h">
       <Filter>M_Misc</Filter>
     </ClInclude>
+    <ClInclude Include="..\m_vector.h">
+      <Filter>M_Misc</Filter>
+    </ClInclude>
     <ClInclude Include="..\m_random.h">
       <Filter>M_Misc</Filter>
     </ClInclude>
@@ -849,6 +852,9 @@
     <ClCompile Include="..\m_queue.c">
       <Filter>M_Misc</Filter>
     </ClCompile>
+    <ClCompile Include="..\m_vector.c">
+      <Filter>M_Misc</Filter>
+    </ClCompile>
     <ClCompile Include="..\m_random.c">
       <Filter>M_Misc</Filter>
     </ClCompile>
diff --git a/src/st_stuff.c b/src/st_stuff.c
index 378d3ba7d0dee09aff27902d61646c880b43aad9..f3a73ce2616186574f3b390ce96dd79b7f0f717f 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -2113,7 +2113,7 @@ static void ST_drawNiGHTSHUD(void)
 			V_DrawString(160 + numbersize + 8, 24, V_SNAPTOTOP|((realnightstime < 10) ? V_REDMAP : V_YELLOWMAP), va("%02d", G_TicsToCentiseconds(stplyr->nightstime)));
 	}
 
-	if (oldspecialstage)
+	if (oldspecialstage && LUA_HudEnabled(hud_nightsrecords))
 	{
 		if (leveltime < 5*TICRATE)
 		{
diff --git a/src/tables.c b/src/tables.c
index 315fe1d7a81717974e51538e6fc3fcd7210ab84b..d0fb428ba8909653cc7f1d340caa9590f1373391 100644
--- a/src/tables.c
+++ b/src/tables.c
@@ -3,6 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 2009 by Stephen McGranahan.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.