diff --git a/appveyor.yml b/appveyor.yml
index d5f76c344c33d5b1f0469956e06ee3390dd4ab62..b9f84f395a5afccc741f0999b64e766695fce7d2 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -2,15 +2,11 @@ version: 2.2.9.{branch}-{build}
 os: MinGW
 
 environment:
- CC: ccache
- CCACHE_CC: i686-w64-mingw32-gcc
- CCACHE_CC_64: x86_64-w64-mingw32-gcc
+ CC: i686-w64-mingw32-gcc
  WINDRES: windres
  # c:\mingw-w64 i686 has gcc 6.3.0, so use c:\msys64 7.3.0 instead
  MINGW_SDK: c:\msys64\mingw32
- # c:\msys64 x86_64 has gcc 8.2.0, so use c:\mingw-w64 7.3.0 instead
- MINGW_SDK_64: C:\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64
- CFLAGS: -Wall -W -Werror -Wno-error=implicit-fallthrough -Wimplicit-fallthrough=3 -Wno-tautological-compare -Wno-error=suggest-attribute=noreturn
+ CFLAGS: -Wno-implicit-fallthrough
  NASM_ZIP: nasm-2.12.01
  NASM_URL: http://www.nasm.us/pub/nasm/releasebuilds/2.12.01/win64/nasm-2.12.01-win64.zip
  UPX_ZIP: upx391w
@@ -19,8 +15,6 @@ environment:
  CCACHE_URL: http://alam.srb2.org/ccache.exe
  CCACHE_COMPRESS: true
  CCACHE_DIR: C:\Users\appveyor\.ccache
- # Disable UPX by default. The user can override this in their Appveyor project settings
- NOUPX: 1
  ##############################
  # DEPLOYER VARIABLES
  # DPL_ENABLED=1 builds installers for branch names starting with `deployer`.
@@ -53,11 +47,6 @@ cache:
 - C:\Users\appveyor\srb2_cache
 
 install:
-- if [%CONFIGURATION%] == [SDL64] ( set "X86_64=1" )
-- if [%CONFIGURATION%] == [SDL64] ( set "CONFIGURATION=SDL" )
-- if [%X86_64%] == [1] ( set "MINGW_SDK=%MINGW_SDK_64%" )
-- if [%X86_64%] == [1] ( set "CCACHE_CC=%CCACHE_CC_64%" )
-
 - if not exist "%NASM_ZIP%.zip" appveyor DownloadFile "%NASM_URL%" -FileName "%NASM_ZIP%.zip"
 - 7z x -y "%NASM_ZIP%.zip" -o%TMP% >null
 - robocopy /S /xx /ns /nc /nfl /ndl /np /njh /njs "%TMP%\%NASM_ZIP%" "%MINGW_SDK%\bin" nasm.exe || exit 0
@@ -72,34 +61,27 @@ install:
 
 configuration:
 - SDL
-- SDL64
 
 before_build:
 - set "Path=%MINGW_SDK%\bin;%Path%"
-- if [%X86_64%] == [1] ( x86_64-w64-mingw32-gcc --version ) else ( i686-w64-mingw32-gcc --version )
 - mingw32-make --version
-- if not [%X86_64%] == [1] ( nasm -v )
+- nasm -v
 - if not [%NOUPX%] == [1] ( upx -V )
 - ccache -V
 - ccache -s
-- if [%NOUPX%] == [1] ( set "NOUPX=NOUPX=1" ) else ( set "NOUPX=" )
 - if defined [%APPVEYOR_PULL_REQUEST_HEAD_COMMIT%] ( set "COMMIT=%APPVEYOR_PULL_REQUEST_HEAD_COMMIT%" ) else ( set "COMMIT=%APPVEYOR_REPO_COMMIT%" )
 - cmd: git rev-parse --short %COMMIT%>%TMP%/gitshort.txt
 - cmd: set /P GITSHORT=<%TMP%/gitshort.txt
 # for pull requests, take the owner's name only, if this isn't the same repo of course
 - set "REPO=%APPVEYOR_REPO_BRANCH%"
 - if not [%APPVEYOR_PULL_REQUEST_HEAD_REPO_NAME%] == [] ( if not [%APPVEYOR_PULL_REQUEST_HEAD_REPO_NAME%] == [%APPVEYOR_REPO_NAME%] (  for /f "delims=/" %%a in ("%APPVEYOR_PULL_REQUEST_HEAD_REPO_NAME%") do set "REPO=%%a-%APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH%" ) )
-- set "EXENAME=EXENAME=srb2win-%REPO%-%GITSHORT%.exe"
-- set "SRB2_MFLAGS=-C src WARNINGMODE=1 CCACHE=1 NOOBJDUMP=1 %NOUPX% %EXENAME%"
-- if [%X86_64%] == [1] ( set "MINGW_FLAGS=MINGW64=1 X86_64=1 GCC81=1" ) else ( set "MINGW_FLAGS=MINGW=1 GCC91=1" )
-- set "SRB2_MFLAGS=%SRB2_MFLAGS% %MINGW_FLAGS% %CONFIGURATION%=1"
+- set "SRB2_MFLAGS=-C src NOECHOFILENAMES=1 CCACHE=1 EXENAME=srb2win-%REPO%-%GITSHORT%.exe"
 
 build_script:
 - cmd: mingw32-make.exe %SRB2_MFLAGS% clean
 - cmd: mingw32-make.exe %SRB2_MFLAGS% ERRORMODE=1 -k
 
 after_build:
-- if [%X86_64%] == [1] ( set "CONFIGURATION=%CONFIGURATION%64" )
 - ccache -s
 - set BUILD_ARCHIVE=%REPO%-%GITSHORT%-%CONFIGURATION%.7z
 - set BUILDSARCHIVE=%REPO%-%CONFIGURATION%.7z
@@ -134,3 +116,4 @@ test: off
 on_finish:
 #- cmd: echo xfreerdp /u:appveyor /cert-ignore +clipboard /v:<ip>:<port>
 #- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
+# vim: et ts=1
diff --git a/debian-template/rules b/debian-template/rules
index 0a77624cb490639564b0212abc902dbfdda88be5..12ceaf98b97a09926f41b502c0ae88ec8f63e4a8 100644
--- a/debian-template/rules
+++ b/debian-template/rules
@@ -78,7 +78,7 @@ NONX86	= $(shell test "`echo $(CROSS_COMPILE_HOST) | grep 'i[3-6]86'`" || echo "
 MAKEARGS = $(OS) $(NONX86) $(PREFIX) EXENAME=$(EXENAME) DBGNAME=$(DBGNAME) NOOBJDUMP=1 # SDL_PKGCONFIG=sdl2 PNG_PKGCONFIG=libpng
 MENUFILE1 = ?package($(PACKAGE)):needs="X11" section="$(SECTION)"
 MENUFILE2 = title="$(TITLE)" command="/$(PKGDIR)/$(PACKAGE)"
-BINDIR :=  $(DIR)/bin/Linux/Release
+BINDIR :=  $(DIR)/bin/
 
 # FIXME pkg-config dir hacks
 # Launchpad doesn't need this; it actually makes i386 builds fail due to cross-compile
diff --git a/extras/conf/SRB2-22.cfg b/extras/conf/SRB2-22.cfg
index 3f345da4f480cfdbcc62b91bff8c47fb46669fc7..6272874e5818295908af9356bd8ad4b77f37db96 100644
--- a/extras/conf/SRB2-22.cfg
+++ b/extras/conf/SRB2-22.cfg
@@ -1998,7 +1998,7 @@ linedeftypes
 			title = "Set Tagged Sector's Floor Height/Texture";
 			prefix = "(400)";
 			flags8text = "[3] Set delay by backside sector";
-			flags64text = "[6] Keep floor flat";
+			flags64text = "[6] Don't change floor texture";
 		}
 
 		401
@@ -2006,6 +2006,7 @@ linedeftypes
 			title = "Set Tagged Sector's Ceiling Height/Texture";
 			prefix = "(401)";
 			flags8text = "[3] Set delay by backside sector";
+			flags64text = "[6] Don't change ceiling texture";
 		}
 
 		402
@@ -2096,7 +2097,7 @@ linedeftypes
 			prefix = "(403)";
 			flags2text = "[1] Trigger linedef executor";
 			flags8text = "[3] Set delay by backside sector";
-			flags64text = "[6] Change floor flat";
+			flags64text = "[6] Change floor texture";
 		}
 
 		404
@@ -2105,7 +2106,7 @@ linedeftypes
 			prefix = "(404)";
 			flags2text = "[1] Trigger linedef executor";
 			flags8text = "[3] Set delay by backside sector";
-			flags64text = "[6] Change ceiling flat";
+			flags64text = "[6] Change ceiling texture";
 		}
 
 		405
@@ -2956,8 +2957,10 @@ linedeftypes
 			prefix = "(700)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 1;
+			copyslopeargs = 1;
 		}
 
 		701
@@ -2966,8 +2969,10 @@ linedeftypes
 			prefix = "(701)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 2;
+			copyslopeargs = 4;
 		}
 
 		702
@@ -2976,8 +2981,10 @@ linedeftypes
 			prefix = "(702)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 3;
+			copyslopeargs = 5;
 		}
 
 		703
@@ -2986,8 +2993,10 @@ linedeftypes
 			prefix = "(703)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 9;
+			copyslopeargs = 8;
 		}
 
 		704
@@ -3018,8 +3027,10 @@ linedeftypes
 			prefix = "(710)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 4;
+			copyslopeargs = 2;
 		}
 
 		711
@@ -3028,8 +3039,10 @@ linedeftypes
 			prefix = "(711)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 8;
+			copyslopeargs = 8;
 		}
 
 		712
@@ -3038,8 +3051,10 @@ linedeftypes
 			prefix = "(712)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 12;
+			copyslopeargs = 10;
 		}
 
 		713
@@ -3048,8 +3063,10 @@ linedeftypes
 			prefix = "(713)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 6;
+			copyslopeargs = 6;
 		}
 
 		714
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4c125c4b8a553c29fe7506d88a05a8d0eea58f0a..ae93aac370b8fb93f22f4bbdce9aa48ac6aed94a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -4,7 +4,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32)
 
 # Core sources
 target_sourcefile(c)
-target_sources(SRB2SDL2 PRIVATE comptime.c md5.c config.h)
+target_sources(SRB2SDL2 PRIVATE comptime.c md5.c config.h.in)
 
 set(SRB2_ASM_SOURCES vid_copy.s)
 
@@ -60,7 +60,7 @@ if(${SRB2_CONFIG_HAVE_GME})
 	endif()
 	if(${GME_FOUND})
 		set(SRB2_HAVE_GME ON)
-		target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_LIBGME)
+		target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_GME)
 	else()
 		message(WARNING "You have specified that GME is available but it was not found.")
 	endif()
diff --git a/src/Makefile b/src/Makefile
index 90776b8125efe7ec18561f034f5bb60c5b89104e..9659a4994c1dce6e94981ff090b0b4f1e9a171bf 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -295,6 +295,8 @@ ifndef destructive
 $(shell $(CC) -v)
 define flags =
 
+SHELL ..... $(SHELL)
+
 CC ........ $(cc)
 
 CFLAGS .... $(opts)
@@ -374,7 +376,7 @@ ifdef Echo_name
 	@printf '%-20.20s\r' $$<
 endif
 endif
-	$(.)$(cc) -MM -MF $$@ -MT $(objdir)/$$(*F).o $(2) $$<
+	$(.)$(cc) -MM -MF $$@ -MT $(objdir)/$$*.o $(2) $$<
 endef
 
 $(eval $(call _recipe,c))
@@ -404,7 +406,7 @@ clean :
 	$(call _rm,$(exe) $(dbg) $(dbg).txt $(objects))
 
 distclean :
-	$(call _rm,../bin ../objs ../deps ../make comptime.h)
+	$(call _rm,../bin ../objs ../dep ../make comptime.h)
 
 info:
 ifdef WINDOWSHELL
diff --git a/src/Makefile.d/detect.mk b/src/Makefile.d/detect.mk
index 3edf0dad4b6b86eec70dfa0d6e9c4be1ca7bf8e0..f458b044cf8c2f8d973b50e32a2f3500e6e6c7ef 100644
--- a/src/Makefile.d/detect.mk
+++ b/src/Makefile.d/detect.mk
@@ -29,10 +29,6 @@ $(call Print,$(_m))
 
 # go for a 32-bit sdl mingw exe by default
 MINGW:=1
-# cmd.exe uses native Windows semicolon delimited PATH
-ifneq (,$(findstring ;,$(PATH)))
-WINDOWSHELL:=1
-endif
 
 else # if you on the *nix
 
@@ -75,13 +71,17 @@ latest_gcc_version:=10.2
 # manually set. And don't bother if this is a clean only
 # run.
 ifeq (,$(call Wildvar,GCC% destructive))
-version:=$(shell $(CC) --version)
+
+# can't use $(CC) --version here since that uses argv[0] to display the name
+# also gcc outputs the information to stderr, so I had to do 2>&1
+# this program really doesn't like identifying itself
+version:=$(shell $(CC) -v 2>&1)
 
 # check if this is in fact GCC
-ifneq (,$(or $(findstring gcc,$(version)),\
-	$(findstring GCC,$(version))))
+ifneq (,$(findstring gcc version,$(version)))
 
-version:=$(shell $(CC) -dumpversion)
+# in stark contrast to the name, gcc will give me a nicely formatted version number for free
+version:=$(shell $(CC) -dumpfullversion)
 
 # Turn version into words of major, minor
 v:=$(subst ., ,$(version))
diff --git a/src/Makefile.d/old.mk b/src/Makefile.d/old.mk
index e5890eedd11f9bc69556c08d240ea369dd511334..ec9b6d776c53ccca325c510506ff34d1e27d4d5d 100644
--- a/src/Makefile.d/old.mk
+++ b/src/Makefile.d/old.mk
@@ -3,7 +3,7 @@
 #
 
 _old:=$(wildcard $(addprefix ../bin/,FreeBSD Linux \
-		Linux64 Mingw Mingw64 SDL dummy) ../objs ../deps)
+		Linux64 Mingw Mingw64 SDL dummy) ../objs ../dep)
 
 ifdef _old
 $(foreach v,$(_old),$(info $(abspath $(v))))
diff --git a/src/Makefile.d/versions.mk b/src/Makefile.d/versions.mk
index d7d0c3dd1ef59f5ca3d9b6b6d7e6191af9f671d8..f0b59658ee741e7d709b4d4037b1745a1e3bfefb 100644
--- a/src/Makefile.d/versions.mk
+++ b/src/Makefile.d/versions.mk
@@ -35,8 +35,6 @@ ifndef GCC295
  WFLAGS+=-Wendif-labels
 endif
 ifdef GCC41
- WFLAGS+=-Wdeclaration-after-statement
- WFLAGS+=-Wno-error=declaration-after-statement
  WFLAGS+=-Wshadow
 endif
 #WFLAGS+=-Wlarger-than-%len%
diff --git a/src/Makefile.d/win32.mk b/src/Makefile.d/win32.mk
index 0c671b26876e1740c1ab101a711b561f9ad2dd7a..768133c151c7a597871ff605fa0eb045cfc7df05 100644
--- a/src/Makefile.d/win32.mk
+++ b/src/Makefile.d/win32.mk
@@ -8,6 +8,11 @@ else
 EXENAME?=srb2win64.exe
 endif
 
+# disable dynamicbase if under msys2
+ifdef MSYSTEM
+libs+=-Wl,--disable-dynamicbase
+endif
+
 sources+=win32/Srb2win.rc
 opts+=-DSTDC_HEADERS
 libs+=-ladvapi32 -lkernel32 -lmsvcrt -luser32
diff --git a/src/am_map.c b/src/am_map.c
index ef0ebb88cd33e3d865095015cfd8be09a8487388..24379e2f13d91822c8aed096ae3f972a95a0bfee 100644
--- a/src/am_map.c
+++ b/src/am_map.c
@@ -458,7 +458,7 @@ boolean AM_Responder(event_t *ev)
 	{
 		if (!automapactive)
 		{
-			if (ev->type == ev_keydown && ev->data1 == AM_TOGGLEKEY)
+			if (ev->type == ev_keydown && ev->key == AM_TOGGLEKEY)
 			{
 				//faB: prevent alt-tab in win32 version to activate automap just before
 				//     minimizing the app; doesn't do any harm to the DOS version
@@ -473,7 +473,7 @@ boolean AM_Responder(event_t *ev)
 		else if (ev->type == ev_keydown)
 		{
 			rc = true;
-			switch (ev->data1)
+			switch (ev->key)
 			{
 				case AM_PANRIGHTKEY: // pan right
 					if (!followplayer)
@@ -550,7 +550,7 @@ boolean AM_Responder(event_t *ev)
 		else if (ev->type == ev_keyup)
 		{
 			rc = false;
-			switch (ev->data1)
+			switch (ev->key)
 			{
 				case AM_PANRIGHTKEY:
 					if (!followplayer)
diff --git a/src/b_bot.c b/src/b_bot.c
index 9213da95eb87464a19fc93e0a8ef70952b355624..cdd74fc0757522ac2a7c30dfe3dec50237c446ea 100644
--- a/src/b_bot.c
+++ b/src/b_bot.c
@@ -18,29 +18,38 @@
 #include "b_bot.h"
 #include "lua_hook.h"
 
-// If you want multiple bots, variables like this will
-// have to be stuffed in something accessible through player_t.
-static boolean lastForward = false;
-static boolean lastBlocked = false;
-static boolean blocked = false;
-
-static boolean jump_last = false;
-static boolean spin_last = false;
-static UINT8 anxiety = 0;
-static boolean panic = false;
-static UINT8 flymode = 0;
-static boolean spinmode = false;
-static boolean thinkfly = false;
-
-static inline void B_ResetAI(void)
+void B_UpdateBotleader(player_t *player)
 {
-	jump_last = false;
-	spin_last = false;
-	anxiety = 0;
-	panic = false;
-	flymode = 0;
-	spinmode = false;
-	thinkfly = false;
+	UINT32 i;
+	fixed_t dist;
+	fixed_t neardist = INT32_MAX;
+	player_t *nearplayer = NULL;
+	//Find new botleader
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (players[i].bot || players[i].playerstate != PST_LIVE || players[i].spectator || !players[i].mo)
+			continue;
+		if (!player->mo) //Can't do distance calculations if there's no player object, so we'll just take the first we find
+		{
+			player->botleader = &players[i];
+			return;
+		}
+		//Update best candidate based on nearest distance
+		dist = R_PointToDist2(player->mo->x, player->mo->y, players[i].mo->x, players[i].mo->y);
+		if (neardist > dist)
+		{
+			neardist = dist;
+			nearplayer = &players[i];
+		}
+	}
+	//Set botleader to best candidate (or null if none available)
+	player->botleader = nearplayer;
+}
+
+static inline void B_ResetAI(botmem_t *mem)
+{
+	mem->thinkstate = AI_FOLLOW;
+	mem->catchup_tics = 0;
 }
 
 static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
@@ -49,39 +58,47 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 
 	player_t *player = sonic->player, *bot = tails->player;
 	ticcmd_t *pcmd = &player->cmd;
-	boolean water = tails->eflags & MFE_UNDERWATER;
+	botmem_t *mem = &bot->botmem;
+	boolean water = (tails->eflags & MFE_UNDERWATER);
 	SINT8 flip = P_MobjFlip(tails);
 	boolean _2d = (tails->flags2 & MF2_TWOD) || twodlevel;
 	fixed_t scale = tails->scale;
+	boolean jump_last = (bot->lastbuttons & BT_JUMP);
+	boolean spin_last = (bot->lastbuttons & BT_SPIN);
 
 	fixed_t dist = P_AproxDistance(sonic->x - tails->x, sonic->y - tails->y);
 	fixed_t zdist = flip * (sonic->z - tails->z);
 	angle_t ang = sonic->angle;
 	fixed_t pmom = P_AproxDistance(sonic->momx, sonic->momy);
 	fixed_t bmom = P_AproxDistance(tails->momx, tails->momy);
-	fixed_t followmax = 128 * 8 * scale; // Max follow distance before AI begins to enter "panic" state
+	fixed_t followmax = 128 * 8 * scale; // Max follow distance before AI begins to enter catchup state
 	fixed_t followthres = 92 * scale; // Distance that AI will try to reach
 	fixed_t followmin = 32 * scale;
 	fixed_t comfortheight = 96 * scale;
 	fixed_t touchdist = 24 * scale;
 	boolean stalled = (bmom < scale >> 1) && dist > followthres; // Helps to see if the AI is having trouble catching up
 	boolean samepos = (sonic->x == tails->x && sonic->y == tails->y);
-
+	boolean blocked = bot->blocked;
+	
 	if (!samepos)
 		ang = R_PointToAngle2(tails->x, tails->y, sonic->x, sonic->y);
 
-	// We can't follow Sonic if he's not around!
-	if (!sonic || sonic->health <= 0)
-		return;
-
 	// Lua can handle it!
 	if (LUA_HookBotAI(sonic, tails, cmd))
 		return;
 
+	// We can't follow Sonic if he's not around!
+	if (!sonic || sonic->health <= 0)
+	{
+		mem->thinkstate = AI_STANDBY;
+		return;
+	}
+	else if (mem->thinkstate == AI_STANDBY)
+		mem->thinkstate = AI_FOLLOW;
+
 	if (tails->player->powers[pw_carry] == CR_MACESPIN || tails->player->powers[pw_carry] == CR_GENERIC)
 	{
 		boolean isrelevant = (sonic->player->powers[pw_carry] == CR_MACESPIN || sonic->player->powers[pw_carry] == CR_GENERIC);
-		dist = P_AproxDistance(tails->x-sonic->x, tails->y-sonic->y);
 		if (sonic->player->cmd.buttons & BT_JUMP && (sonic->player->pflags & PF_JUMPED) && isrelevant)
 			cmd->buttons |= BT_JUMP;
 		if (isrelevant)
@@ -103,56 +120,57 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 		followmin = 0;
 		followthres = 16*scale;
 		followmax >>= 1;
-		thinkfly = false;
+		if (mem->thinkstate == AI_THINKFLY)
+			mem->thinkstate = AI_FOLLOW;
 	}
 
-	// Check anxiety
-	if (spinmode)
+	// Update catchup_tics
+	if (mem->thinkstate == AI_SPINFOLLOW)
 	{
-		anxiety = 0;
-		panic = false;
+		mem-> catchup_tics = 0;
 	}
 	else if (dist > followmax || zdist > comfortheight || stalled)
 	{
-		anxiety = min(anxiety + 2, 70);
-		if (anxiety >= 70)
-			panic = true;
+		mem-> catchup_tics = min(mem-> catchup_tics + 2, 70);
+		if (mem-> catchup_tics >= 70)
+			mem->thinkstate = AI_CATCHUP;
 	}
 	else
 	{
-		anxiety = max(anxiety - 1, 0);
-		panic = false;
+		mem-> catchup_tics = max(mem-> catchup_tics - 1, 0);
+		if (mem->thinkstate == AI_CATCHUP)
+			mem->thinkstate = AI_FOLLOW;
 	}
 
 	// Orientation
+	// cmd->angleturn won't be relative to player angle, since we're not going through G_BuildTiccmd.
 	if (bot->pflags & (PF_SPINNING|PF_STARTDASH))
 	{
-		cmd->angleturn = (sonic->angle - tails->angle) >> 16; // NOT FRACBITS DAMNIT
+		cmd->angleturn = (sonic->angle) >> 16; // NOT FRACBITS DAMNIT
 	}
-	else if (flymode == 2)
+	else if (mem->thinkstate == AI_FLYCARRY)
 	{
-		cmd->angleturn = sonic->player->cmd.angleturn - (tails->angle >> 16);
+		cmd->angleturn = sonic->player->cmd.angleturn;
 	}
 	else
 	{
-		cmd->angleturn = (ang - tails->angle) >> 16; // NOT FRACBITS DAMNIT
+		cmd->angleturn = (ang) >> 16; // NOT FRACBITS DAMNIT
 	}
 
 	// ********
 	// FLY MODE
-	// spinmode check
-	if (spinmode || player->exiting)
-		thinkfly = false;
+	// exiting check
+	if (player->exiting && mem->thinkstate == AI_THINKFLY)
+		mem->thinkstate = AI_FOLLOW;
 	else
 	{
 		// Activate co-op flight
-		if (thinkfly && player->pflags & PF_JUMPED)
+		if (mem->thinkstate == AI_THINKFLY && player->pflags & PF_JUMPED)
 		{
 			if (!jump_last)
 			{
 				jump = true;
-				flymode = 1;
-				thinkfly = false;
+				mem->thinkstate = AI_FLYSTANDBY;
 				bot->pflags |= PF_CANCARRY;
 			}
 		}
@@ -165,20 +183,19 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 			&& P_IsObjectOnGround(sonic) && P_IsObjectOnGround(tails)
 			&& !(player->pflags & PF_STASIS)
 			&& bot->charability == CA_FLY)
-				thinkfly = true;
-		else
-			thinkfly = false;
+				mem->thinkstate = AI_THINKFLY;
+		else if (mem->thinkstate == AI_THINKFLY)
+			mem->thinkstate = AI_FOLLOW;
 
 		// Set carried state
 		if (player->powers[pw_carry] == CR_PLAYER && sonic->tracer == tails)
 		{
-			flymode = 2;
+			mem->thinkstate = AI_FLYCARRY;
 		}
 
 		// Ready for takeoff
-		if (flymode == 1)
+		if (mem->thinkstate == AI_FLYSTANDBY)
 		{
-			thinkfly = false;
 			if (zdist < -64*scale || (flip * tails->momz) > scale) // Make sure we're not too high up
 				spin = true;
 			else if (!jump_last)
@@ -186,10 +203,10 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 
 			// Abort if the player moves away or spins
 			if (dist > followthres || player->dashspeed)
-				flymode = 0;
+				mem->thinkstate = AI_FOLLOW;
 		}
 		// Read player inputs while carrying
-		else if (flymode == 2)
+		else if (mem->thinkstate == AI_FLYCARRY)
 		{
 			cmd->forwardmove = pcmd->forwardmove;
 			cmd->sidemove = pcmd->sidemove;
@@ -203,19 +220,19 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 			// End flymode
 			if (player->powers[pw_carry] != CR_PLAYER)
 			{
-				flymode = 0;
+				mem->thinkstate = AI_FOLLOW;
 			}
 		}
 	}
 
-	if (flymode && P_IsObjectOnGround(tails) && !(pcmd->buttons & BT_JUMP))
-		flymode = 0;
+	if (P_IsObjectOnGround(tails) && !(pcmd->buttons & BT_JUMP) && (mem->thinkstate == AI_FLYSTANDBY || mem->thinkstate == AI_FLYCARRY))
+		mem->thinkstate = AI_FOLLOW;
 
 	// ********
 	// SPINNING
-	if (panic || flymode || !(player->pflags & PF_SPINNING) || (player->pflags & PF_JUMPED))
-		spinmode = false;
-	else
+	if (!(player->pflags & (PF_SPINNING|PF_STARTDASH)) && mem->thinkstate == AI_SPINFOLLOW)
+		mem->thinkstate = AI_FOLLOW;
+	else if (mem->thinkstate == AI_FOLLOW || mem->thinkstate == AI_SPINFOLLOW)
 	{
 		if (!_2d)
 		{
@@ -224,21 +241,21 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 			{
 				if (dist < followthres && dist > touchdist) // Do positioning
 				{
-					cmd->angleturn = (ang - tails->angle) >> 16; // NOT FRACBITS DAMNIT
+					cmd->angleturn = (ang) >> 16; // NOT FRACBITS DAMNIT
 					cmd->forwardmove = 50;
-					spinmode = true;
+					mem->thinkstate = AI_SPINFOLLOW;
 				}
 				else if (dist < touchdist)
 				{
 					if (!bmom && (!(bot->pflags & PF_SPINNING) || (bot->dashspeed && bot->pflags & PF_SPINNING)))
 					{
-						cmd->angleturn = (sonic->angle - tails->angle) >> 16; // NOT FRACBITS DAMNIT
+						cmd->angleturn = (sonic->angle) >> 16; // NOT FRACBITS DAMNIT
 						spin = true;
 					}
-					spinmode = true;
+					mem->thinkstate = AI_SPINFOLLOW;
 				}
 				else
-					spinmode = false;
+					mem->thinkstate = AI_FOLLOW;
 			}
 			// Spin
 			else if (player->dashspeed == bot->dashspeed && player->pflags & PF_SPINNING)
@@ -246,12 +263,12 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 				if (bot->pflags & PF_SPINNING || !spin_last)
 				{
 					spin = true;
-					cmd->angleturn = (sonic->angle - tails->angle) >> 16; // NOT FRACBITS DAMNIT
+					cmd->angleturn = (sonic->angle) >> 16; // NOT FRACBITS DAMNIT
 					cmd->forwardmove = MAXPLMOVE;
-					spinmode = true;
+					mem->thinkstate = AI_SPINFOLLOW;
 				}
 				else
-					spinmode = false;
+					mem->thinkstate = AI_FOLLOW;
 			}
 		}
 		// 2D mode
@@ -261,17 +278,19 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 				&& ((bot->pflags & PF_SPINNING) || !spin_last))
 			{
 				spin = true;
-				spinmode = true;
+				mem->thinkstate = AI_SPINFOLLOW;
 			}
+			else
+				mem->thinkstate = AI_FOLLOW;
 		}
 	}
 
 	// ********
 	// FOLLOW
-	if (!(flymode || spinmode))
+	if (mem->thinkstate == AI_FOLLOW || mem->thinkstate == AI_CATCHUP)
 	{
 		// Too far
-		if (panic || dist > followthres)
+		if (mem->thinkstate == AI_CATCHUP || dist > followthres)
 		{
 			if (!_2d)
 				cmd->forwardmove = MAXPLMOVE;
@@ -281,7 +300,7 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 				cmd->sidemove = -MAXPLMOVE;
 		}
 		// Within threshold
-		else if (!panic && dist > followmin && abs(zdist) < 192*scale)
+		else if (dist > followmin && abs(zdist) < 192*scale)
 		{
 			if (!_2d)
 				cmd->forwardmove = FixedHypot(pcmd->forwardmove, pcmd->sidemove);
@@ -292,7 +311,7 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 		else if (dist < followmin)
 		{
 			// Copy inputs
-			cmd->angleturn = (sonic->angle - tails->angle) >> 16; // NOT FRACBITS DAMNIT
+			cmd->angleturn = (sonic->angle) >> 16; // NOT FRACBITS DAMNIT
 			bot->drawangle = ang;
 			cmd->forwardmove = 8 * pcmd->forwardmove / 10;
 			cmd->sidemove = 8 * pcmd->sidemove / 10;
@@ -301,7 +320,7 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 
 	// ********
 	// JUMP
-	if (!(flymode || spinmode))
+	if (mem->thinkstate == AI_FOLLOW || mem->thinkstate == AI_CATCHUP || (mem->thinkstate == AI_SPINFOLLOW && player->pflags & PF_JUMPED))
 	{
 		// Flying catch-up
 		if (bot->pflags & PF_THOKKED)
@@ -319,31 +338,30 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 		// Start jump
 		else if (!jump_last && !(bot->pflags & PF_JUMPED) //&& !(player->pflags & PF_SPINNING)
 			&& ((zdist > 32*scale && player->pflags & PF_JUMPED) // Following
-				|| (zdist > 64*scale && panic) // Vertical catch-up
-				|| (stalled && anxiety > 20 && bot->powers[pw_carry] == CR_NONE)
+				|| (zdist > 64*scale && mem->thinkstate == AI_CATCHUP) // Vertical catch-up
+				|| (stalled && mem-> catchup_tics > 20 && bot->powers[pw_carry] == CR_NONE)
 				//|| (bmom < scale>>3 && dist > followthres && !(bot->powers[pw_carry])) // Stopped & not in carry state
 				|| (bot->pflags & PF_SPINNING && !(bot->pflags & PF_JUMPED)))) // Spinning
 					jump = true;
 		// Hold jump
-		else if (bot->pflags & PF_JUMPED && jump_last && tails->momz*flip > 0 && (zdist > 0 || panic))
+		else if (bot->pflags & PF_JUMPED && jump_last && tails->momz*flip > 0 && (zdist > 0 || mem->thinkstate == AI_CATCHUP))
 			jump = true;
 		// Start flying
-		else if (bot->pflags & PF_JUMPED && panic && !jump_last && bot->charability == CA_FLY)
+		else if (bot->pflags & PF_JUMPED && mem->thinkstate == AI_CATCHUP && !jump_last && bot->charability == CA_FLY)
 			jump = true;
 	}
 
 	// ********
 	// HISTORY
-	jump_last = jump;
-	spin_last = spin;
+	//jump_last = jump;
+	//spin_last = spin;
 
 	// Turn the virtual keypresses into ticcmd_t.
 	B_KeysToTiccmd(tails, cmd, forward, backward, left, right, false, false, jump, spin);
 
 	// Update our status
-	lastForward = forward;
-	lastBlocked = blocked;
-	blocked = false;
+	mem->lastForward = forward;
+	mem->lastBlocked = blocked;
 }
 
 void B_BuildTiccmd(player_t *player, ticcmd_t *cmd)
@@ -366,22 +384,25 @@ void B_BuildTiccmd(player_t *player, ticcmd_t *cmd)
 	if (LUA_HookTiccmd(player, cmd, HOOK(BotTiccmd)))
 		return;
 
-	// We don't have any main character AI, sorry. D:
-	if (player-players == consoleplayer)
+	// Make sure we have a valid main character to follow
+	 B_UpdateBotleader(player);
+	if (!player->botleader)
 		return;
 
-	// Basic Tails AI
-	B_BuildTailsTiccmd(players[consoleplayer].mo, player->mo, cmd);
+	// Single Player Tails AI
+	//B_BuildTailsTiccmd(players[consoleplayer].mo, player->mo, cmd);
+	B_BuildTailsTiccmd(player->botleader->mo, player->mo, cmd);
 }
 
 void B_KeysToTiccmd(mobj_t *mo, ticcmd_t *cmd, boolean forward, boolean backward, boolean left, boolean right, boolean strafeleft, boolean straferight, boolean jump, boolean spin)
 {
+	player_t *player = mo->player;
 	// don't try to do stuff if your sonic is in a minecart or something
-	if (players[consoleplayer].powers[pw_carry] && players[consoleplayer].powers[pw_carry] != CR_PLAYER)
+	if (&player->botleader && player->botleader->powers[pw_carry] && player->botleader->powers[pw_carry] != CR_PLAYER)
 		return;
 	// Turn the virtual keypresses into ticcmd_t.
 	if (twodlevel || mo->flags2 & MF2_TWOD) {
-		if (players[consoleplayer].climbing
+		if (player->botleader->climbing
 		|| mo->player->pflags & PF_GLIDING) {
 			// Don't mess with bot inputs during these unhandled movement conditions.
 			// The normal AI doesn't use abilities, so custom AI should be sending us exactly what it wants anyway.
@@ -420,10 +441,10 @@ void B_KeysToTiccmd(mobj_t *mo, ticcmd_t *cmd, boolean forward, boolean backward
 			cmd->forwardmove += MAXPLMOVE<<FRACBITS>>16;
 		if (backward)
 			cmd->forwardmove -= MAXPLMOVE<<FRACBITS>>16;
-		if (left)
+ 		if (left)
 			cmd->angleturn += 1280;
 		if (right)
-			cmd->angleturn -= 1280;
+			cmd->angleturn -= 1280; 
 		if (strafeleft)
 			cmd->sidemove -= MAXPLMOVE<<FRACBITS>>16;
 		if (straferight)
@@ -447,14 +468,19 @@ void B_KeysToTiccmd(mobj_t *mo, ticcmd_t *cmd, boolean forward, boolean backward
 void B_MoveBlocked(player_t *player)
 {
 	(void)player;
-	blocked = true;
+	player->blocked = true;
 }
 
 boolean B_CheckRespawn(player_t *player)
 {
-	mobj_t *sonic = players[consoleplayer].mo;
+	mobj_t *sonic;
 	mobj_t *tails = player->mo;
 
+	//We don't have a main player to spawn to!
+	if (!player->botleader)
+		return false;
+	
+	sonic = player->botleader->mo;
 	// We can't follow Sonic if he's not around!
 	if (!sonic || sonic->health <= 0)
 		return false;
@@ -505,15 +531,19 @@ void B_RespawnBot(INT32 playernum)
 {
 	player_t *player = &players[playernum];
 	fixed_t x,y,z;
-	mobj_t *sonic = players[consoleplayer].mo;
+	mobj_t *sonic;
 	mobj_t *tails;
 
+	if (!player->botleader)
+		return;
+
+	sonic = player->botleader->mo;
 	if (!sonic || sonic->health <= 0)
 		return;
 
-	B_ResetAI();
+	B_ResetAI(&player->botmem);
 
-	player->bot = 1;
+	player->bot = BOT_2PAI;
 	P_SpawnPlayer(playernum);
 	tails = player->mo;
 
@@ -540,10 +570,6 @@ void B_RespawnBot(INT32 playernum)
 	player->powers[pw_spacetime] = sonic->player->powers[pw_spacetime];
 	player->powers[pw_gravityboots] = sonic->player->powers[pw_gravityboots];
 	player->powers[pw_nocontrol] = sonic->player->powers[pw_nocontrol];
-	player->acceleration = sonic->player->acceleration;
-	player->accelstart = sonic->player->accelstart;
-	player->thrustfactor = sonic->player->thrustfactor;
-	player->normalspeed = sonic->player->normalspeed;
 	player->pflags |= PF_AUTOBRAKE|(sonic->player->pflags & PF_DIRECTIONCHAR);
 
 	P_TeleportMove(tails, x, y, z);
@@ -561,11 +587,11 @@ void B_RespawnBot(INT32 playernum)
 void B_HandleFlightIndicator(player_t *player)
 {
 	mobj_t *tails = player->mo;
-
+	botmem_t *mem = &player->botmem;
 	if (!tails)
 		return;
 
-	if (thinkfly && player->bot == 1 && tails->health)
+	if (mem->thinkstate == AI_THINKFLY && player->bot == BOT_2PAI && tails->health)
 	{
 		if (!tails->hnext)
 		{
diff --git a/src/b_bot.h b/src/b_bot.h
index 9f55637d14d8543f14f5dd2926e21fb9d36a955c..a89cfab19535477180971b0ba428f248b678b455 100644
--- a/src/b_bot.h
+++ b/src/b_bot.h
@@ -10,6 +10,7 @@
 /// \file  b_bot.h
 /// \brief Basic bot handling
 
+void B_UpdateBotleader(player_t *player);
 void B_BuildTiccmd(player_t *player, ticcmd_t *cmd);
 void B_KeysToTiccmd(mobj_t *mo, ticcmd_t *cmd, boolean forward, boolean backward, boolean left, boolean right, boolean strafeleft, boolean straferight, boolean jump, boolean spin);
 boolean B_CheckRespawn(player_t *player);
diff --git a/src/blua/lbaselib.c b/src/blua/lbaselib.c
index 644565c28847204daa8e312459ef75e3fb6cfe31..0fc222038dd97dbc4306018a88f87b69b612c167 100644
--- a/src/blua/lbaselib.c
+++ b/src/blua/lbaselib.c
@@ -274,7 +274,7 @@ static int luaB_dofile (lua_State *L) {
 	UINT16 lumpnum;
 	int n = lua_gettop(L);
 
-	if (wadfiles[numwadfiles - 1]->type != RET_PK3)
+	if (!W_FileHasFolders(wadfiles[numwadfiles - 1]))
 		luaL_error(L, "dofile() only works with PK3 files");
 
 	snprintf(fullfilename, sizeof(fullfilename), "Lua/%s", filename);
diff --git a/src/command.c b/src/command.c
index 95b1fd67d84a31686a2943cc2702c0d6acb90607..ae4a7178e437c9039ae4717defb377c091fad215 100644
--- a/src/command.c
+++ b/src/command.c
@@ -650,7 +650,7 @@ static void COM_ExecuteString(char *ptext)
 			else
 			{ // Monster Iestyn: keep track of how many levels of recursion we're in
 				recursion++;
-				COM_BufInsertText(a->value);
+				COM_BufInsertTextEx(a->value, com_flags);
 				recursion--;
 			}
 			return;
@@ -1738,6 +1738,8 @@ void CV_SaveVars(UINT8 **p, boolean in_demo)
 static void CV_LoadVars(UINT8 **p,
 		consvar_t *(*got)(UINT8 **p, char **ret_value, boolean *ret_stealth))
 {
+	const boolean store = (client || demoplayback);
+
 	consvar_t *cvar;
 	UINT16 count;
 
@@ -1751,7 +1753,7 @@ static void CV_LoadVars(UINT8 **p,
 	{
 		if (cvar->flags & CV_NETVAR)
 		{
-			if (client && cvar->revert.v.string == NULL)
+			if (store && cvar->revert.v.string == NULL)
 			{
 				cvar->revert.v.const_munge = cvar->string;
 				cvar->revert.allocated = ( cvar->zstring != NULL );
@@ -2364,7 +2366,10 @@ static boolean CV_Command(void)
 		return false;
 
 	if (( com_flags & COM_SAFE ) && ( v->flags & CV_NOLUA ))
-		return false;
+	{
+		CONS_Alert(CONS_WARNING, "Variable '%s' cannot be changed from Lua.\n", v->name);
+		return true;
+	}
 
 	// perform a variable print or set
 	if (COM_Argc() == 1)
diff --git a/src/console.c b/src/console.c
index b3c41384051bf3cd8a01cbd07b5a873f503551b8..6f21aeb3dd4d022c51e37c1781ec28aa029a652c 100644
--- a/src/console.c
+++ b/src/console.c
@@ -221,7 +221,7 @@ static void CONS_Bind_f(void)
 		for (key = 0; key < NUMINPUTS; key++)
 			if (bindtable[key])
 			{
-				CONS_Printf("%s : \"%s\"\n", G_KeyNumToString(key), bindtable[key]);
+				CONS_Printf("%s : \"%s\"\n", G_KeyNumToName(key), bindtable[key]);
 				na = 1;
 			}
 		if (!na)
@@ -229,7 +229,7 @@ static void CONS_Bind_f(void)
 		return;
 	}
 
-	key = G_KeyStringToNum(COM_Argv(1));
+	key = G_KeyNameToNum(COM_Argv(1));
 	if (key <= 0 || key >= NUMINPUTS)
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("Invalid key name\n"));
@@ -484,6 +484,19 @@ void CON_Init(void)
 		Unlock_state();
 	}
 }
+
+void CON_StartRefresh(void)
+{
+	if (con_startup)
+		con_refresh = true;
+}
+
+void CON_StopRefresh(void)
+{
+	if (con_startup)
+		con_refresh = false;
+}
+
 // Console input initialization
 //
 static void CON_InputInit(void)
@@ -913,12 +926,12 @@ boolean CON_Responder(event_t *ev)
 	// let go keyup events, don't eat them
 	if (ev->type != ev_keydown && ev->type != ev_console)
 	{
-		if (ev->data1 == gamecontrol[gc_console][0] || ev->data1 == gamecontrol[gc_console][1])
+		if (ev->key == gamecontrol[GC_CONSOLE][0] || ev->key == gamecontrol[GC_CONSOLE][1])
 			consdown = false;
 		return false;
 	}
 
-	key = ev->data1;
+	key = ev->key;
 
 	// check for console toggle key
 	if (ev->type != ev_console)
@@ -926,7 +939,7 @@ boolean CON_Responder(event_t *ev)
 		if (modeattacking || metalrecording || marathonmode)
 			return false;
 
-		if (key == gamecontrol[gc_console][0] || key == gamecontrol[gc_console][1])
+		if (key == gamecontrol[GC_CONSOLE][0] || key == gamecontrol[GC_CONSOLE][1])
 		{
 			if (consdown) // ignore repeat
 				return true;
@@ -1759,8 +1772,8 @@ static void CON_DrawBackpic(void)
 	}
 
 	// Draw the patch.
-	V_DrawCroppedPatch(x << FRACBITS, 0, FRACUNIT, V_NOSCALESTART, con_backpic,
-			0, ( BASEVIDHEIGHT - h ), BASEVIDWIDTH, h);
+	V_DrawCroppedPatch(x << FRACBITS, 0, FRACUNIT, FRACUNIT, V_NOSCALESTART, con_backpic, NULL,
+			0, (BASEVIDHEIGHT - h) << FRACBITS, BASEVIDWIDTH << FRACBITS, h << FRACBITS);
 
 	// Unlock the cached patch.
 	W_UnlockCachedPatch(con_backpic);
diff --git a/src/console.h b/src/console.h
index 28f40d308270cc279bc70b5995b6f3fdf378536c..accf89d960faf6975ecd02345bba66d6ef3fd264 100644
--- a/src/console.h
+++ b/src/console.h
@@ -16,6 +16,9 @@
 
 void CON_Init(void);
 
+void CON_StartRefresh(void);
+void CON_StopRefresh(void);
+
 boolean CON_Responder(event_t *ev);
 
 #ifdef HAVE_THREADS
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 1549811c127ec64ffde85558bffdf92487223aaf..78a3ebe6cb961cff2fb22e5e0f4520559078865a 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -43,6 +43,7 @@
 #include "lzf.h"
 #include "lua_script.h"
 #include "lua_hook.h"
+#include "lua_libs.h"
 #include "md5.h"
 #include "m_perfstats.h"
 
@@ -127,10 +128,14 @@ static UINT8 localtextcmd[MAXTEXTCMD];
 static UINT8 localtextcmd2[MAXTEXTCMD]; // splitscreen
 static tic_t neededtic;
 SINT8 servernode = 0; // the number of the server node
+
 /// \brief do we accept new players?
 /// \todo WORK!
 boolean acceptnewnode = true;
 
+static boolean serverisfull = false; //lets us be aware if the server was full after we check files, but before downloading, so we can ask if the user still wants to download or not
+static tic_t firstconnectattempttime = 0;
+
 // engine
 
 // Must be a power of two
@@ -510,18 +515,24 @@ static INT16 Consistancy(void);
 typedef enum
 {
 	CL_SEARCHING,
+	CL_CHECKFILES,
 	CL_DOWNLOADFILES,
 	CL_ASKJOIN,
+	CL_LOADFILES,
 	CL_WAITJOINRESPONSE,
 	CL_DOWNLOADSAVEGAME,
 	CL_CONNECTED,
-	CL_ABORTED
+	CL_ABORTED,
+	CL_ASKFULLFILELIST,
+	CL_CONFIRMCONNECT
 } cl_mode_t;
 
 static void GetPackets(void);
 
 static cl_mode_t cl_mode = CL_SEARCHING;
 
+static UINT16 cl_lastcheckedfilecount = 0;	// used for full file list
+
 #ifndef NONET
 #define SNAKE_SPEED 5
 
@@ -663,14 +674,14 @@ static void Snake_Handle(void)
 	UINT16 i;
 
 	// Handle retry
-	if (snake->gameover && (PLAYER1INPUTDOWN(gc_jump) || gamekeydown[KEY_ENTER]))
+	if (snake->gameover && (PLAYER1INPUTDOWN(GC_JUMP) || gamekeydown[KEY_ENTER]))
 	{
 		Snake_Initialise();
 		snake->pausepressed = true; // Avoid accidental pause on respawn
 	}
 
 	// Handle pause
-	if (PLAYER1INPUTDOWN(gc_pause) || gamekeydown[KEY_ENTER])
+	if (PLAYER1INPUTDOWN(GC_PAUSE) || gamekeydown[KEY_ENTER])
 	{
 		if (!snake->pausepressed)
 			snake->paused = !snake->paused;
@@ -919,6 +930,8 @@ static void Snake_Draw(void)
 	INT16 i;
 
 	// Background
+	V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+
 	V_DrawFlatFill(
 		SNAKE_LEFT_X + SNAKE_BORDER_SIZE,
 		SNAKE_TOP_Y  + SNAKE_BORDER_SIZE,
@@ -1020,6 +1033,13 @@ static void Snake_Draw(void)
 		);
 }
 
+static void CL_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");
+}
+
 //
 // CL_DrawConnectionStatus
 //
@@ -1030,28 +1050,32 @@ static inline void CL_DrawConnectionStatus(void)
 	INT32 ccstime = I_GetTime();
 
 	// Draw background fade
-	if (!menuactive) // menu already draws its own fade
-		V_DrawFadeScreen(0xFF00, 16); // force default
-
-	// Draw the bottom box.
-	M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
-	V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
+	V_DrawFadeScreen(0xFF00, 16); // force default
 
-	if (cl_mode != CL_DOWNLOADFILES)
+	if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_LOADFILES)
 	{
 		INT32 i, animtime = ((ccstime / 4) & 15) + 16;
-		UINT8 palstart = (cl_mode == CL_SEARCHING) ? 32 : 96;
-		// 15 pal entries total.
+		UINT8 palstart;
 		const char *cltext;
 
+		// Draw the bottom box.
+		CL_DrawConnectionStatusBox();
+
+		if (cl_mode == CL_SEARCHING)
+			palstart = 32; // Red
+		else if (cl_mode == CL_CONFIRMCONNECT)
+			palstart = 48; // Orange
+		else
+			palstart = 96; // Green
+
 		if (!(cl_mode == CL_DOWNLOADSAVEGAME && lastfilenum != -1))
-			for (i = 0; i < 16; ++i)
+			for (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 (lastfilenum != -1)
+				if (fileneeded && lastfilenum != -1)
 				{
 					UINT32 currentsize = fileneeded[lastfilenum].currentsize;
 					UINT32 totalsize = fileneeded[lastfilenum].totalsize;
@@ -1075,9 +1099,22 @@ static inline void CL_DrawConnectionStatus(void)
 				else
 					cltext = M_GetText("Waiting to download game state...");
 				break;
+			case CL_ASKFULLFILELIST:
+			case CL_CHECKFILES:
+				cltext = M_GetText("Checking server addon list...");
+				break;
+			case CL_CONFIRMCONNECT:
+				cltext = "";
+				break;
+			case CL_LOADFILES:
+				cltext = M_GetText("Loading server addons...");
+				break;
 			case CL_ASKJOIN:
 			case CL_WAITJOINRESPONSE:
-				cltext = M_GetText("Requesting to join...");
+				if (serverisfull)
+					cltext = M_GetText("Server full, waiting for a slot...");
+				else
+					cltext = M_GetText("Requesting to join...");
 				break;
 			default:
 				cltext = M_GetText("Connecting to server...");
@@ -1087,14 +1124,51 @@ static inline void CL_DrawConnectionStatus(void)
 	}
 	else
 	{
-		if (lastfilenum != -1)
+		if (cl_mode == CL_LOADFILES)
+		{
+			INT32 totalfileslength;
+			INT32 loadcompletednum = 0;
+			INT32 i;
+
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
+
+			//ima just count files here
+			if (fileneeded)
+			{
+				for (i = 0; i < fileneedednum; i++)
+					if (fileneeded[i].status == FS_OPEN)
+						loadcompletednum++;
+			}
+
+			// Loading progress
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, "Loading server addons...");
+			totalfileslength = (INT32)((loadcompletednum/(double)(fileneedednum)) * 256);
+			M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
+			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));
+		}
+		else if (lastfilenum != -1)
 		{
 			INT32 dldlength;
 			static char tempname[28];
-			fileneeded_t *file = &fileneeded[lastfilenum];
-			char *filename = file->filename;
+			fileneeded_t *file;
+			char *filename;
+
+			if (snake)
+				Snake_Draw();
 
-			Snake_Draw();
+			// Draw the bottom box.
+			CL_DrawConnectionStatusBox();
+
+			if (fileneeded)
+			{
+				file = &fileneeded[lastfilenum];
+				filename = file->filename;
+			}
+			else
+				return;
 
 			Net_GetNetStat();
 			dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256);
@@ -1127,20 +1201,32 @@ static inline void CL_DrawConnectionStatus(void)
 				va("%3.1fK/s ", ((double)getbps)/1024));
 		}
 		else
+		{
+			if (snake)
+				Snake_Draw();
+
+			CL_DrawConnectionStatusBox();
 			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
 				M_GetText("Waiting to download files..."));
+		}
 	}
 }
 #endif
 
+static boolean CL_AskFileList(INT32 firstfile)
+{
+	netbuffer->packettype = PT_TELLFILESNEEDED;
+	netbuffer->u.filesneedednum = firstfile;
+
+	return HSendPacket(servernode, false, 0, sizeof (INT32));
+}
+
 /** Sends a special packet to declare how many players in local
   * Used only in arbitratrenetstart()
   * Sends a PT_CLIENTJOIN packet to the server
   *
   * \return True if the packet was successfully sent
   * \todo Improve the description...
-  *       Because to be honest, I have no idea what arbitratrenetstart is...
-  *       Is it even used...?
   *
   */
 static boolean CL_SendJoin(void)
@@ -1150,15 +1236,14 @@ static boolean CL_SendJoin(void)
 		CONS_Printf(M_GetText("Sending join request...\n"));
 	netbuffer->packettype = PT_CLIENTJOIN;
 
+	netbuffer->u.clientcfg.modversion = MODVERSION;
+	strncpy(netbuffer->u.clientcfg.application,
+			SRB2APPLICATION,
+			sizeof netbuffer->u.clientcfg.application);
+
 	if (splitscreen || botingame)
 		localplayers++;
 	netbuffer->u.clientcfg.localplayers = localplayers;
-	netbuffer->u.clientcfg._255 = 255;
-	netbuffer->u.clientcfg.packetversion = PACKETVERSION;
-	netbuffer->u.clientcfg.version = VERSION;
-	netbuffer->u.clientcfg.subversion = SUBVERSION;
-	strncpy(netbuffer->u.clientcfg.application, SRB2APPLICATION,
-			sizeof netbuffer->u.clientcfg.application);
 
 	CleanupPlayerName(consoleplayer, cv_playername.zstring);
 	if (splitscreen)
@@ -1201,6 +1286,21 @@ static INT32 FindRejoinerNum(SINT8 node)
 	return -1;
 }
 
+static UINT8
+GetRefuseReason (INT32 node)
+{
+	if (!node || FindRejoinerNum(node) != -1)
+		return 0;
+	else if (bannednode && bannednode[node])
+		return REFUSE_BANNED;
+	else if (!cv_allownewplayer.value)
+		return REFUSE_JOINS_DISABLED;
+	else if (D_NumPlayers() >= cv_maxplayers.value)
+		return REFUSE_SLOTS_FULL;
+	else
+		return 0;
+}
+
 static void SV_SendServerInfo(INT32 node, tic_t servertime)
 {
 	UINT8 *p;
@@ -1219,20 +1319,13 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
 	netbuffer->u.serverinfo.numberofplayer = (UINT8)D_NumPlayers();
 	netbuffer->u.serverinfo.maxplayer = (UINT8)cv_maxplayers.value;
 
-	if (!node || FindRejoinerNum(node) != -1)
-		netbuffer->u.serverinfo.refusereason = 0;
-	else if (!cv_allownewplayer.value)
-		netbuffer->u.serverinfo.refusereason = 1;
-	else if (D_NumPlayers() >= cv_maxplayers.value)
-		netbuffer->u.serverinfo.refusereason = 2;
-	else
-		netbuffer->u.serverinfo.refusereason = 0;
+	netbuffer->u.serverinfo.refusereason = GetRefuseReason(node);
 
 	strncpy(netbuffer->u.serverinfo.gametypename, Gametype_Names[gametype],
 			sizeof netbuffer->u.serverinfo.gametypename);
 	netbuffer->u.serverinfo.modifiedgame = (UINT8)modifiedgame;
 	netbuffer->u.serverinfo.cheatsenabled = CV_CheatsEnabled();
-	netbuffer->u.serverinfo.isdedicated = (UINT8)dedicated;
+	netbuffer->u.serverinfo.flags = (dedicated ? SV_DEDICATED : 0);
 	strncpy(netbuffer->u.serverinfo.servername, cv_servername.string,
 		MAXSERVERNAME);
 	strncpy(netbuffer->u.serverinfo.mapname, G_BuildMapName(gamemap), 7);
@@ -1267,7 +1360,7 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
 	if (mapheaderinfo[gamemap-1])
 		netbuffer->u.serverinfo.actnum = mapheaderinfo[gamemap-1]->actnum;
 
-	p = PutFileNeeded();
+	p = PutFileNeeded(0);
 
 	HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
 }
@@ -1344,9 +1437,6 @@ static boolean SV_SendServerConfig(INT32 node)
 
 	netbuffer->packettype = PT_SERVERCFG;
 
-	netbuffer->u.servercfg.version = VERSION;
-	netbuffer->u.servercfg.subversion = SUBVERSION;
-
 	netbuffer->u.servercfg.serverplayer = (UINT8)serverplayer;
 	netbuffer->u.servercfg.totalslotnum = (UINT8)(doomcom->numslots);
 	netbuffer->u.servercfg.gametic = (tic_t)LONG(gametic);
@@ -1521,6 +1611,8 @@ static void CL_LoadReceivedSavegame(boolean reloading)
 	size_t length, decompressedlen;
 	char tmpsave[256];
 
+	FreeFileNeeded();
+
 	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
 
 	length = FIL_ReadFile(tmpsave, &savebuffer);
@@ -1667,20 +1759,24 @@ static void SL_InsertServer(serverinfo_pak* info, SINT8 node)
 		if (serverlistcount >= MAXSERVERLIST)
 			return; // list full
 
-		if (info->_255 != 255)
-			return;/* old packet format */
+		/* check it later if connecting to this one */
+		if (node != servernode)
+		{
+			if (info->_255 != 255)
+				return;/* old packet format */
 
-		if (info->packetversion != PACKETVERSION)
-			return;/* old new packet format */
+			if (info->packetversion != PACKETVERSION)
+				return;/* old new packet format */
 
-		if (info->version != VERSION)
-			return; // Not same version.
+			if (info->version != VERSION)
+				return; // Not same version.
 
-		if (info->subversion != SUBVERSION)
-			return; // Close, but no cigar.
+			if (info->subversion != SUBVERSION)
+				return; // Close, but no cigar.
 
-		if (strcmp(info->application, SRB2APPLICATION))
-			return;/* that's a different mod */
+			if (strcmp(info->application, SRB2APPLICATION))
+				return;/* that's a different mod */
+		}
 
 		i = serverlistcount++;
 	}
@@ -1829,6 +1925,222 @@ void CL_UpdateServerList(boolean internetsearch, INT32 room)
 
 #endif // ifndef NONET
 
+static void M_ConfirmConnect(event_t *ev)
+{
+#ifndef NONET
+	if (ev->type == ev_keydown)
+	{
+		if (ev->key == ' ' || ev->key == 'y' || ev->key == KEY_ENTER)
+		{
+			if (totalfilesrequestednum > 0)
+			{
+				if (CL_SendFileRequest())
+				{
+					cl_mode = CL_DOWNLOADFILES;
+					Snake_Initialise();
+				}
+			}
+			else
+				cl_mode = CL_LOADFILES;
+
+			M_ClearMenus(true);
+		}
+		else if (ev->key == 'n' || ev->key == KEY_ESCAPE)
+		{
+			cl_mode = CL_ABORTED;
+			M_ClearMenus(true);
+		}
+	}
+#else
+	(void)ev;
+#endif
+}
+
+static boolean CL_FinishedFileList(void)
+{
+	INT32 i;
+	char *downloadsize = NULL;
+	//CONS_Printf(M_GetText("Checking files...\n"));
+	i = CL_CheckFiles();
+	if (i == 4) // still checking ...
+	{
+		return true;
+	}
+	else if (i == 3) // too many files
+	{
+		D_QuitNetGame();
+		CL_Reset();
+		D_StartTitle();
+		M_StartMessage(M_GetText(
+			"You have too many WAD files loaded\n"
+			"to add ones the server is using.\n"
+			"Please restart SRB2 before connecting.\n\n"
+			"Press ESC\n"
+		), NULL, MM_NOTHING);
+		return false;
+	}
+	else if (i == 2) // cannot join for some reason
+	{
+		D_QuitNetGame();
+		CL_Reset();
+		D_StartTitle();
+		M_StartMessage(M_GetText(
+			"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"
+			"Press ESC\n"
+		), NULL, MM_NOTHING);
+		return false;
+	}
+	else if (i == 1)
+	{
+		if (serverisfull)
+		{
+			M_StartMessage(M_GetText(
+				"This server is full!\n"
+				"\n"
+				"You may load server addons (if any), and wait for a slot.\n"
+				"\n"
+				"Press ENTER to continue\nor ESC to cancel.\n\n"
+			), M_ConfirmConnect, MM_EVENTHANDLER);
+			cl_mode = CL_CONFIRMCONNECT;
+			curfadevalue = 0;
+		}
+		else
+			cl_mode = CL_LOADFILES;
+	}
+	else
+	{
+		// must download something
+		// can we, though?
+		if (!CL_CheckDownloadable()) // nope!
+		{
+			D_QuitNetGame();
+			CL_Reset();
+			D_StartTitle();
+			M_StartMessage(M_GetText(
+				"An error occured when trying to\n"
+				"download missing addons.\n"
+				"(This is almost always a problem\n"
+				"with the server, not your game.)\n\n"
+				"See the console or log file\n"
+				"for additional details.\n\n"
+				"Press ESC\n"
+			), NULL, MM_NOTHING);
+			return false;
+		}
+
+#ifndef NONET
+		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));
+#endif
+
+		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);
+		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;
+	}
+	return true;
+}
+
+static const char * InvalidServerReason (serverinfo_pak *info)
+{
+#define EOT "\nPress ESC\n"
+
+	/* magic number for new packet format */
+	if (info->_255 != 255)
+	{
+		return
+			"Outdated server (version unknown).\n" EOT;
+	}
+
+	if (strncmp(info->application, SRB2APPLICATION, sizeof
+				info->application))
+	{
+		return va(
+				"%s cannot connect\n"
+				"to %s servers.\n" EOT,
+				SRB2APPLICATION,
+				info->application);
+	}
+
+	if (
+			info->packetversion != PACKETVERSION ||
+			info->version != VERSION ||
+			info->subversion != SUBVERSION
+	){
+		return va(
+				"Incompatible %s versions.\n"
+				"(server version %d.%d.%d)\n" EOT,
+				SRB2APPLICATION,
+				info->version / 100,
+				info->version % 100,
+				info->subversion);
+	}
+
+	switch (info->refusereason)
+	{
+		case REFUSE_BANNED:
+			return
+				"You have been banned\n"
+				"from the server.\n" EOT;
+		case REFUSE_JOINS_DISABLED:
+			return
+				"The server is not accepting\n"
+				"joins for the moment.\n" EOT;
+		case REFUSE_SLOTS_FULL:
+			return va(
+					"Maximum players reached: %d\n" EOT,
+					info->maxplayer);
+		default:
+			if (info->refusereason)
+			{
+				return
+					"You can't join.\n"
+					"I don't know why,\n"
+					"but you can't join.\n" EOT;
+			}
+	}
+
+	return NULL;
+
+#undef EOT
+}
+
 /** Called by CL_ServerConnectionTicker
   *
   * \param asksent The last time we asked the server to join. We re-ask every second in case our request got lost in transmit.
@@ -1859,88 +2171,46 @@ static boolean CL_ServerConnectionSearchTicker(tic_t *asksent)
 				return true;
 		}
 
-		// Quit here rather than downloading files and being refused later.
-		if (serverlist[i].info.refusereason)
-		{
-			D_QuitNetGame();
-			CL_Reset();
-			D_StartTitle();
-			if (serverlist[i].info.refusereason == 1)
-				M_StartMessage(M_GetText("The server is not accepting\njoins for the moment.\n\nPress ESC\n"), NULL, MM_NOTHING);
-			else if (serverlist[i].info.refusereason == 2)
-				M_StartMessage(va(M_GetText("Maximum players reached: %d\n\nPress ESC\n"), serverlist[i].info.maxplayer), NULL, MM_NOTHING);
-			else
-				M_StartMessage(M_GetText("You can't join.\nI don't know why,\nbut you can't join.\n\nPress ESC\n"), NULL, MM_NOTHING);
-			return false;
-		}
-
 		if (client)
 		{
-			D_ParseFileneeded(serverlist[i].info.fileneedednum,
-				serverlist[i].info.fileneeded);
-			CONS_Printf(M_GetText("Checking files...\n"));
-			i = CL_CheckFiles();
-			if (i == 3) // too many files
-			{
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
-				M_StartMessage(M_GetText(
-					"You have too many WAD files loaded\n"
-					"to add ones the server is using.\n"
-					"Please restart SRB2 before connecting.\n\n"
-					"Press ESC\n"
-				), NULL, MM_NOTHING);
-				return false;
-			}
-			else if (i == 2) // cannot join for some reason
-			{
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
-				M_StartMessage(M_GetText(
-					"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"
-					"Press ESC\n"
-				), NULL, MM_NOTHING);
-				return false;
-			}
-			else if (i == 1)
-				cl_mode = CL_ASKJOIN;
+			serverinfo_pak *info = &serverlist[i].info;
+
+			if (info->refusereason == REFUSE_SLOTS_FULL)
+				serverisfull = true;
 			else
 			{
-				// must download something
-				// can we, though?
-				if (!CL_CheckDownloadable()) // nope!
+				const char *reason = InvalidServerReason(info);
+
+				// Quit here rather than downloading files
+				// and being refused later.
+				if (reason)
 				{
+					char *message = Z_StrDup(reason);
 					D_QuitNetGame();
 					CL_Reset();
 					D_StartTitle();
-					M_StartMessage(M_GetText(
-						"You cannot connect to this server\n"
-						"because you cannot download the files\n"
-						"that you are missing from the server.\n\n"
-						"See the console or log file for\n"
-						"more details.\n\n"
-						"Press ESC\n"
-					), NULL, MM_NOTHING);
+					M_StartMessage(message, NULL, MM_NOTHING);
+					Z_Free(message);
 					return false;
 				}
-				// no problem if can't send packet, we will retry later
-				if (CL_SendFileRequest())
-				{
-					cl_mode = CL_DOWNLOADFILES;
-#ifndef NONET
-					Snake_Initialise();
-#endif
-				}
 			}
+
+			D_ParseFileneeded(info->fileneedednum, info->fileneeded, 0);
+
+			if (info->flags & SV_LOTSOFADDONS)
+			{
+				cl_mode = CL_ASKFULLFILELIST;
+				cl_lastcheckedfilecount = 0;
+				return true;
+			}
+
+			cl_mode = CL_CHECKFILES;
 		}
 		else
+		{
 			cl_mode = CL_ASKJOIN; // files need not be checked for the server.
+			*asksent = 0;
+		}
 
 		return true;
 	}
@@ -1986,6 +2256,22 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 				return false;
 			break;
 
+		case CL_ASKFULLFILELIST:
+			if (cl_lastcheckedfilecount == UINT16_MAX) // All files retrieved
+				cl_mode = CL_CHECKFILES;
+			else if (fileneedednum != cl_lastcheckedfilecount || I_GetTime() >= *asksent)
+			{
+				if (CL_AskFileList(fileneedednum))
+				{
+					cl_lastcheckedfilecount = fileneedednum;
+					*asksent = I_GetTime() + NEWTICRATE;
+				}
+			}
+			break;
+		case CL_CHECKFILES:
+			if (!CL_FinishedFileList())
+				return false;
+			break;
 		case CL_DOWNLOADFILES:
 			waitmore = false;
 			for (i = 0; i < fileneedednum; i++)
@@ -2006,21 +2292,51 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 			}
 #endif
 
-			cl_mode = CL_ASKJOIN; // don't break case continue to cljoin request now
-			/* FALLTHRU */
-
+			cl_mode = CL_LOADFILES;
+			break;
+		case CL_LOADFILES:
+			if (CL_LoadServerFiles())
+			{
+				FreeFileNeeded();
+				*asksent = 0; //This ensure the first join ask is right away
+				firstconnectattempttime = I_GetTime();
+				cl_mode = CL_ASKJOIN;
+			}
+			break;
 		case CL_ASKJOIN:
-			CL_LoadServerFiles();
+			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();
+				M_StartMessage(M_GetText(
+					"5 minute wait time exceeded.\n"
+					"You may retry connection.\n"
+					"\n"
+					"Press ESC\n"
+				), NULL, MM_NOTHING);
+				return false;
+			}
 #ifndef NONET
 			// prepare structures to save the file
 			// WARNING: this can be useless in case of server not in GS_LEVEL
 			// but since the network layer doesn't provide ordered packets...
 			CL_PrepareDownloadSaveGame(tmpsave);
 #endif
-			if (CL_SendJoin())
+			if (I_GetTime() >= *asksent && CL_SendJoin())
+			{
+				*asksent = I_GetTime() + NEWTICRATE*3;
 				cl_mode = CL_WAITJOINRESPONSE;
+			}
+			break;
+		case CL_WAITJOINRESPONSE:
+			if (I_GetTime() >= *asksent)
+			{
+				cl_mode = CL_ASKJOIN;
+			}
 			break;
-
 #ifndef NONET
 		case CL_DOWNLOADSAVEGAME:
 			// At this state, the first (and only) needed file is the gamestate
@@ -2034,8 +2350,8 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 				break;
 #endif
 
-		case CL_WAITJOINRESPONSE:
 		case CL_CONNECTED:
+		case CL_CONFIRMCONNECT: //logic is handled by M_ConfirmConnect
 		default:
 			break;
 
@@ -2043,7 +2359,6 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 		case CL_ABORTED:
 			cl_mode = CL_SEARCHING;
 			return false;
-
 	}
 
 	GetPackets();
@@ -2053,13 +2368,19 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 	if (*oldtic != I_GetTime())
 	{
 		I_OsPolling();
-		for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
-			G_MapEventsToControls(&events[eventtail]);
 
-		if (gamekeydown[KEY_ESCAPE] || gamekeydown[KEY_JOY1+1])
+		if (cl_mode == CL_CONFIRMCONNECT)
+			D_ProcessEvents(); //needed for menu system to receive inputs
+		else
+		{
+			for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
+				G_MapEventsToControls(&events[eventtail]);
+		}
+
+		if (gamekeydown[KEY_ESCAPE] || gamekeydown[KEY_JOY1+1] || cl_mode == CL_ABORTED)
 		{
 			CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
-//				M_StartMessage(M_GetText("Network game synchronization aborted.\n\nPress ESC\n"), NULL, MM_NOTHING);
+			M_StartMessage(M_GetText("Network game synchronization aborted.\n\nPress ESC\n"), NULL, MM_NOTHING);
 
 #ifndef NONET
 			if (snake)
@@ -2092,13 +2413,20 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 #ifndef NONET
 		if (client && cl_mode != CL_CONNECTED && cl_mode != CL_ABORTED)
 		{
-			if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_DOWNLOADSAVEGAME)
+			if (!snake)
 			{
 				F_MenuPresTicker(true); // title sky
 				F_TitleScreenTicker(true);
 				F_TitleScreenDrawer();
 			}
 			CL_DrawConnectionStatus();
+#ifdef HAVE_THREADS
+			I_lock_mutex(&m_menu_mutex);
+#endif
+			M_Drawer(); //Needed for drawing messageboxes on the connection screen
+#ifdef HAVE_THREADS
+			I_unlock_mutex(m_menu_mutex);
+#endif
 			I_UpdateNoVsync(); // page flip or blit buffer
 			if (moviemode)
 				M_SaveFrame();
@@ -2160,8 +2488,10 @@ static void CL_ConnectToServer(void)
 	ClearAdminPlayers();
 	pnumnodes = 1;
 	oldtic = I_GetTime() - 1;
+
 #ifndef NONET
 	asksent = (tic_t) - TICRATE;
+	firstconnectattempttime = I_GetTime();
 
 	i = SL_SearchServer(servernode);
 
@@ -2463,7 +2793,7 @@ void CL_ClearPlayer(INT32 playernum)
 //
 // Removes a player from the current game
 //
-static void CL_RemovePlayer(INT32 playernum, kickreason_t reason)
+void CL_RemovePlayer(INT32 playernum, kickreason_t reason)
 {
 	// Sanity check: exceptional cases (i.e. c-fails) can cause multiple
 	// kick commands to be issued for the same player.
@@ -2599,11 +2929,18 @@ void CL_Reset(void)
 	doomcom->numslots = 1;
 	SV_StopServer();
 	SV_ResetServer();
-	CV_RevertNetVars();
 
 	// make sure we don't leave any fileneeded gunk over from a failed join
+	FreeFileNeeded();
 	fileneedednum = 0;
-	memset(fileneeded, 0, sizeof(fileneeded));
+
+#ifndef NONET
+	totalfilesrequestednum = 0;
+	totalfilesrequestedsize = 0;
+#endif
+	firstconnectattempttime = 0;
+	serverisfull = false;
+	connectiontimeout = (tic_t)cv_nettimeout.value; //reset this temporary hack
 
 	// D_StartTitle should get done now, but the calling function will handle it
 }
@@ -2858,6 +3195,34 @@ static void Command_Kick(void)
 	else
 		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
 }
+
+static void Command_ResendGamestate(void)
+{
+	SINT8 playernum;
+
+	if (COM_Argc() == 1)
+	{
+		CONS_Printf(M_GetText("resendgamestate <playername/playernum>: resend the game state to a player\n"));
+		return;
+	}
+	else if (client)
+	{
+		CONS_Printf(M_GetText("Only the server can use this.\n"));
+		return;
+	}
+
+	playernum = nametonum(COM_Argv(1));
+	if (playernum == -1 || playernum == 0)
+		return;
+
+	// Send a PT_WILLRESENDGAMESTATE packet to the client so they know what's going on
+	netbuffer->packettype = PT_WILLRESENDGAMESTATE;
+	if (!HSendPacket(playernode[playernum], true, 0, 0))
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("A problem occured, please try again.\n"));
+		return;
+	}
+}
 #endif
 
 static void Got_KickCmd(UINT8 **p, INT32 playernum)
@@ -3065,34 +3430,6 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 		CL_RemovePlayer(pnum, kickreason);
 }
 
-static void Command_ResendGamestate(void)
-{
-	SINT8 playernum;
-
-	if (COM_Argc() == 1)
-	{
-		CONS_Printf(M_GetText("resendgamestate <playername/playernum>: resend the game state to a player\n"));
-		return;
-	}
-	else if (client)
-	{
-		CONS_Printf(M_GetText("Only the server can use this.\n"));
-		return;
-	}
-
-	playernum = nametonum(COM_Argv(1));
-	if (playernum == -1 || playernum == 0)
-		return;
-
-	// Send a PT_WILLRESENDGAMESTATE packet to the client so they know what's going on
-	netbuffer->packettype = PT_WILLRESENDGAMESTATE;
-	if (!HSendPacket(playernode[playernum], true, 0, 0))
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("A problem occured, please try again.\n"));
-		return;
-	}
-}
-
 static CV_PossibleValue_t netticbuffer_cons_t[] = {{0, "MIN"}, {3, "MAX"}, {0, NULL}};
 consvar_t cv_netticbuffer = CVAR_INIT ("netticbuffer", "1", CV_SAVE, netticbuffer_cons_t, NULL);
 
@@ -3115,7 +3452,7 @@ consvar_t cv_maxsend = CVAR_INIT ("maxsend", "4096", CV_SAVE|CV_NETVAR, maxsend_
 consvar_t cv_noticedownload = CVAR_INIT ("noticedownload", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
 
 // Speed of file downloading (in packets per tic)
-static CV_PossibleValue_t downloadspeed_cons_t[] = {{0, "MIN"}, {32, "MAX"}, {0, NULL}};
+static CV_PossibleValue_t downloadspeed_cons_t[] = {{1, "MIN"}, {300, "MAX"}, {0, NULL}};
 consvar_t cv_downloadspeed = CVAR_INIT ("downloadspeed", "16", CV_SAVE|CV_NETVAR, downloadspeed_cons_t, NULL);
 
 static void Got_AddPlayer(UINT8 **p, INT32 playernum);
@@ -3231,6 +3568,8 @@ void SV_ResetServer(void)
 	// clear server_context
 	memset(server_context, '-', 8);
 
+	CV_RevertNetVars();
+
 	DEBFILE("\n-=-=-=-=-=-=-= Server Reset =-=-=-=-=-=-=-\n\n");
 }
 
@@ -3256,6 +3595,9 @@ static inline void SV_GenContext(void)
 //
 void D_QuitNetGame(void)
 {
+	mousegrabbedbylua = true;
+	I_UpdateMouseGrab();
+
 	if (!netgame || !netbuffer)
 		return;
 
@@ -3620,6 +3962,78 @@ static size_t TotalTextCmdPerTic(tic_t tic)
 	return total;
 }
 
+static const char *
+ConnectionRefused (SINT8 node, INT32 rejoinernum)
+{
+	clientconfig_pak *cc = &netbuffer->u.clientcfg;
+
+	boolean rejoining = (rejoinernum != -1);
+
+	if (!node)/* server connecting to itself */
+		return NULL;
+
+	if (
+			cc->modversion != MODVERSION ||
+			strncmp(cc->application, SRB2APPLICATION,
+				sizeof cc->application)
+	){
+		return/* this is probably client's fault */
+			"Incompatible.";
+	}
+	else if (bannednode && bannednode[node])
+	{
+		return
+			"You have been banned\n"
+			"from the server.";
+	}
+	else if (cc->localplayers != 1)
+	{
+		return
+			"Wrong player count.";
+	}
+
+	if (!rejoining)
+	{
+		if (!cv_allownewplayer.value)
+		{
+			return
+				"The server is not accepting\n"
+				"joins for the moment.";
+		}
+		else if (D_NumPlayers() >= cv_maxplayers.value)
+		{
+			return va(
+					"Maximum players reached: %d",
+					cv_maxplayers.value);
+		}
+	}
+
+	if (luafiletransfers)
+	{
+		return
+			"The serveris broadcasting a file\n"
+			"requested by a Lua script.\n"
+			"Please wait a bit and then\n"
+			"try rejoining.";
+	}
+
+	if (netgame)
+	{
+		const tic_t th = 2 * cv_joindelay.value * TICRATE;
+
+		if (joindelay > th)
+		{
+			return va(
+					"Too many people are connecting.\n"
+					"Please wait %d seconds and then\n"
+					"try rejoining.",
+					(joindelay - th) / TICRATE);
+		}
+	}
+
+	return NULL;
+}
+
 /** Called when a PT_CLIENTJOIN packet is received
   *
   * \param node The packet sender
@@ -3630,33 +4044,14 @@ static void HandleConnect(SINT8 node)
 	char names[MAXSPLITSCREENPLAYERS][MAXPLAYERNAME + 1];
 	INT32 rejoinernum;
 	INT32 i;
+	const char *refuse;
 
 	rejoinernum = FindRejoinerNum(node);
 
-	if (bannednode && bannednode[node])
-		SV_SendRefuse(node, M_GetText("You have been banned\nfrom the server."));
-	else if (netbuffer->u.clientcfg._255 != 255 ||
-			netbuffer->u.clientcfg.packetversion != PACKETVERSION)
-		SV_SendRefuse(node, "Incompatible packet formats.");
-	else if (strncmp(netbuffer->u.clientcfg.application, SRB2APPLICATION,
-				sizeof netbuffer->u.clientcfg.application))
-		SV_SendRefuse(node, "Different SRB2 modifications\nare not compatible.");
-	else if (netbuffer->u.clientcfg.version != VERSION
-		|| netbuffer->u.clientcfg.subversion != SUBVERSION)
-		SV_SendRefuse(node, va(M_GetText("Different SRB2 versions cannot\nplay a netgame!\n(server version %d.%d.%d)"), VERSION/100, VERSION%100, SUBVERSION));
-	else if (!cv_allownewplayer.value && node && rejoinernum == -1)
-		SV_SendRefuse(node, M_GetText("The server is not accepting\njoins for the moment."));
-	else if (D_NumPlayers() >= cv_maxplayers.value && rejoinernum == -1)
-		SV_SendRefuse(node, va(M_GetText("Maximum players reached: %d"), cv_maxplayers.value));
-	else if (netgame && netbuffer->u.clientcfg.localplayers > 1) // Hacked client?
-		SV_SendRefuse(node, M_GetText("Too many players from\nthis node."));
-	else if (netgame && !netbuffer->u.clientcfg.localplayers) // Stealth join?
-		SV_SendRefuse(node, M_GetText("No players from\nthis node."));
-	else if (luafiletransfers)
-		SV_SendRefuse(node, M_GetText("The server is broadcasting a file\nrequested by a Lua script.\nPlease wait a bit and then\ntry rejoining."));
-	else if (netgame && joindelay > 2 * (tic_t)cv_joindelay.value * TICRATE)
-		SV_SendRefuse(node, va(M_GetText("Too many people are connecting.\nPlease wait %d seconds and then\ntry rejoining."),
-			(joindelay - 2 * cv_joindelay.value * TICRATE) / TICRATE));
+	refuse = ConnectionRefused(node, rejoinernum);
+
+	if (refuse)
+		SV_SendRefuse(node, refuse);
 	else
 	{
 #ifndef NONET
@@ -3771,6 +4166,7 @@ static void HandleServerInfo(SINT8 node)
 
 static void PT_WillResendGamestate(void)
 {
+#ifndef NONET
 	char tmpsave[256];
 
 	if (server || cl_redownloadinggamestate)
@@ -3793,10 +4189,12 @@ static void PT_WillResendGamestate(void)
 	CL_PrepareDownloadSaveGame(tmpsave);
 
 	cl_redownloadinggamestate = true;
+#endif
 }
 
 static void PT_CanReceiveGamestate(SINT8 node)
 {
+#ifndef NONET
 	if (client || sendingsavegame[node])
 		return;
 
@@ -3804,6 +4202,9 @@ static void PT_CanReceiveGamestate(SINT8 node)
 
 	SV_SendSaveGame(node, true); // Resend a complete game state
 	resendingsavegame[node] = true;
+#else
+	(void)node;
+#endif
 }
 
 /** Handles a packet received from a node that isn't in game
@@ -3830,31 +4231,40 @@ static void HandlePacketFromAwayNode(SINT8 node)
 	switch (netbuffer->packettype)
 	{
 		case PT_ASKINFOVIAMS:
-#if 0
+			Net_CloseConnection(node);
+			break;
+
+		case PT_TELLFILESNEEDED:
 			if (server && serverrunning)
 			{
-				INT32 clientnode;
-				if (ms_RoomId < 0) // ignore if we're not actually on the MS right now
-				{
-					Net_CloseConnection(node); // and yes, close connection
-					return;
-				}
-				clientnode = I_NetMakeNode(netbuffer->u.msaskinfo.clientaddr);
-				if (clientnode != -1)
-				{
-					SV_SendServerInfo(clientnode, (tic_t)LONG(netbuffer->u.msaskinfo.time));
-					SV_SendPlayerInfo(clientnode); // Send extra info
-					Net_CloseConnection(clientnode);
-					// Don't close connection to MS...
-				}
-				else
-					Net_CloseConnection(node); // ...unless the IP address is not valid
+				UINT8 *p;
+				INT32 firstfile = netbuffer->u.filesneedednum;
+
+				netbuffer->packettype = PT_MOREFILESNEEDED;
+				netbuffer->u.filesneededcfg.first = firstfile;
+				netbuffer->u.filesneededcfg.more = 0;
+
+				p = PutFileNeeded(firstfile);
+
+				HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
+			}
+			else // Shouldn't get this if you aren't the server...?
+				Net_CloseConnection(node);
+			break;
+
+		case PT_MOREFILESNEEDED:
+			if (server && serverrunning)
+			{ // But wait I thought I'm the server?
+				Net_CloseConnection(node);
+				break;
+			}
+			SERVERONLY
+			if (cl_mode == CL_ASKFULLFILELIST && netbuffer->u.filesneededcfg.first == fileneedednum)
+			{
+				D_ParseFileneeded(netbuffer->u.filesneededcfg.num, netbuffer->u.filesneededcfg.files, netbuffer->u.filesneededcfg.first);
+				if (!netbuffer->u.filesneededcfg.more)
+					cl_lastcheckedfilecount = UINT16_MAX; // Got the whole file list
 			}
-			else
-				Net_CloseConnection(node); // you're not supposed to get it, so ignore it
-#else
-			Net_CloseConnection(node);
-#endif
 			break;
 
 		case PT_ASKINFO:
@@ -3880,13 +4290,24 @@ static void HandlePacketFromAwayNode(SINT8 node)
 				if (!reason)
 					I_Error("Out of memory!\n");
 
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
+				if (strstr(reason, "Maximum players reached"))
+				{
+					serverisfull = true;
+					//Special timeout for when refusing due to player cap. The client will wait 3 seconds between join requests when waiting for a slot, so we need this to be much longer
+					//We set it back to the value of cv_nettimeout.value in CL_Reset
+					connectiontimeout = NEWTICRATE*7;
+					cl_mode = CL_ASKJOIN;
+					free(reason);
+					break;
+				}
 
 				M_StartMessage(va(M_GetText("Server refuses connection\n\nReason:\n%s"),
 					reason), NULL, MM_NOTHING);
 
+				D_QuitNetGame();
+				CL_Reset();
+				D_StartTitle();
+
 				free(reason);
 
 				// Will be reset by caller. Signals refusal.
@@ -4090,8 +4511,10 @@ static void HandlePacketFromPlayer(SINT8 node)
 			// Check player consistancy during the level
 			if (realstart <= gametic && realstart + BACKUPTICS - 1 > gametic && gamestate == GS_LEVEL
 				&& consistancy[realstart%BACKUPTICS] != SHORT(netbuffer->u.clientpak.consistancy)
-				&& !resendingsavegame[node] && savegameresendcooldown[node] <= I_GetTime()
-				&& !SV_ResendingSavegameToAnyone())
+#ifndef NONET
+				&& !SV_ResendingSavegameToAnyone()
+#endif
+				&& !resendingsavegame[node] && savegameresendcooldown[node] <= I_GetTime())
 			{
 				if (cv_resynchattempts.value)
 				{
@@ -4842,16 +5265,23 @@ void TryRunTics(tic_t realtics)
 			// run the count * tics
 			while (neededtic > gametic)
 			{
+				boolean update_stats = !(paused || P_AutoPause());
+
 				DEBFILE(va("============ Running tic %d (local %d)\n", gametic, localgametic));
 
-				ps_tictime = I_GetPreciseTime();
+				if (update_stats)
+					PS_START_TIMING(ps_tictime);
 
 				G_Ticker((gametic % NEWTICRATERATIO) == 0);
 				ExtraDataTicker();
 				gametic++;
 				consistancy[gametic%BACKUPTICS] = Consistancy();
 
-				ps_tictime = I_GetPreciseTime() - ps_tictime;
+				if (update_stats)
+				{
+					PS_STOP_TIMING(ps_tictime);
+					PS_UpdateTickStats();
+				}
 
 				// Leave a certain amount of tics present in the net buffer as long as we've ran at least one tic this frame.
 				if (client && gamestate == GS_LEVEL && leveltime > 3 && neededtic <= gametic + cv_netticbuffer.value)
@@ -4995,9 +5425,11 @@ void NetUpdate(void)
 
 	if (client)
 	{
+#ifndef NONET
 		// If the client just finished redownloading the game state, load it
 		if (cl_redownloadinggamestate && fileneeded[0].status == FS_FOUND)
 			CL_ReloadReceivedSavegame();
+#endif
 
 		CL_SendClientCmd(); // Send tic cmd
 		hu_redownloadinggamestate = cl_redownloadinggamestate;
diff --git a/src/d_clisrv.h b/src/d_clisrv.h
index f3eb524238d84461147b38252c107ed248dea578..8e75fb963c860d64e91557bc47b17daea22af7f4 100644
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -22,11 +22,15 @@
 #include "mserv.h"
 
 /*
-The 'packet version' is used to distinguish packet formats.
-This version is independent of VERSION and SUBVERSION. Different
-applications may follow different packet versions.
+The 'packet version' is used to distinguish packet
+formats. This version is independent of VERSION and
+SUBVERSION. Different applications may follow different
+packet versions.
+
+If you change the struct or the meaning of a field
+therein, increment this number.
 */
-#define PACKETVERSION 3
+#define PACKETVERSION 4
 
 // Network play related stuff.
 // There is a data struct that stores network
@@ -90,6 +94,9 @@ typedef enum
 
 	PT_LOGIN,         // Login attempt from the client.
 
+	PT_TELLFILESNEEDED, // Client, to server: "what other files do I need starting from this number?"
+	PT_MOREFILESNEEDED, // Server, to client: "you need these (+ more on top of those)"
+
 	PT_PING,          // Packet sent to tell clients the other client's latency to server.
 	NUMPACKETTYPE
 } packettype_t;
@@ -141,9 +148,6 @@ typedef struct
 
 typedef struct
 {
-	UINT8 version; // Different versions don't work
-	UINT8 subversion; // Contains build version
-
 	// Server launch stuffs
 	UINT8 serverplayer;
 	UINT8 totalslotnum; // "Slots": highest player number in use plus one.
@@ -190,16 +194,22 @@ typedef struct
 
 typedef struct
 {
-	UINT8 _255;/* see serverinfo_pak */
-	UINT8 packetversion;
+	UINT8 modversion;
 	char application[MAXAPPLICATION];
-	UINT8 version; // Different versions don't work
-	UINT8 subversion; // Contains build version
 	UINT8 localplayers;
 	UINT8 mode;
 	char names[MAXSPLITSCREENPLAYERS][MAXPLAYERNAME];
 } ATTRPACK clientconfig_pak;
 
+#define SV_DEDICATED    0x40 // server is dedicated
+#define SV_LOTSOFADDONS 0x20 // flag used to ask for full file list in d_netfil
+
+enum {
+	REFUSE_JOINS_DISABLED = 1,
+	REFUSE_SLOTS_FULL,
+	REFUSE_BANNED,
+};
+
 #define MAXSERVERNAME 32
 #define MAXFILENEEDED 915
 // This packet is too large
@@ -217,11 +227,11 @@ typedef struct
 	UINT8 subversion;
 	UINT8 numberofplayer;
 	UINT8 maxplayer;
-	UINT8 refusereason; // 0: joinable, 1: joins disabled, 2: full
+	UINT8 refusereason; // 0: joinable, REFUSE enum
 	char gametypename[24];
 	UINT8 modifiedgame;
 	UINT8 cheatsenabled;
-	UINT8 isdedicated;
+	UINT8 flags;
 	UINT8 fileneedednum;
 	tic_t time;
 	tic_t leveltime;
@@ -275,6 +285,14 @@ typedef struct
 	UINT8 ctfteam;
 } ATTRPACK plrconfig;
 
+typedef struct
+{
+	INT32 first;
+	UINT8 num;
+	UINT8 more;
+	UINT8 files[MAXFILENEEDED]; // is filled with writexxx (byteptr.h)
+} ATTRPACK filesneededconfig_pak;
+
 //
 // Network packet data
 //
@@ -304,6 +322,8 @@ typedef struct
 		msaskinfo_pak msaskinfo;            //          22 bytes
 		plrinfo playerinfo[MAXPLAYERS];     //         576 bytes(?)
 		plrconfig playerconfig[MAXPLAYERS]; // (up to) 528 bytes(?)
+		INT32 filesneedednum;               //           4 bytes
+		filesneededconfig_pak filesneededcfg; //       ??? bytes
 		UINT32 pingtable[MAXPLAYERS+1];     //          68 bytes
 	} u; // This is needed to pack diff packet types data together
 } ATTRPACK doomdata_t;
@@ -401,6 +421,7 @@ void CL_Reset(void);
 void CL_ClearPlayer(INT32 playernum);
 void CL_QueryServerList(msg_server_t *list);
 void CL_UpdateServerList(boolean internetsearch, INT32 room);
+void CL_RemovePlayer(INT32 playernum, kickreason_t reason);
 // Is there a game running
 boolean Playing(void);
 
diff --git a/src/d_event.h b/src/d_event.h
index 1fd2e3824251082d6c059fffef8ddbe76f67a15c..c30a8ced2b09cd7887446211cbc2b96622c0aba6 100644
--- a/src/d_event.h
+++ b/src/d_event.h
@@ -33,9 +33,10 @@ typedef enum
 typedef struct
 {
 	evtype_t type;
-	INT32 data1; // keys / mouse/joystick buttons
-	INT32 data2; // mouse/joystick x move
-	INT32 data3; // mouse/joystick y move
+	INT32 key; // keys/mouse/joystick buttons
+	INT32 x; // mouse/joystick x move
+	INT32 y; // mouse/joystick y move
+	boolean repeated; // key repeat
 } event_t;
 
 //
diff --git a/src/d_main.c b/src/d_main.c
index 1b3449ec1cb531238feabfec25cd189c1c278c69..83419d266c84703694d06644a28a953e3550f6b1 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -65,7 +65,7 @@
 #include "m_cond.h" // condition initialization
 #include "fastcmp.h"
 #include "keys.h"
-#include "filesrch.h" // refreshdirmenu, mainwadstally
+#include "filesrch.h" // refreshdirmenu
 #include "g_input.h" // tutorial mode control scheming
 #include "m_perfstats.h"
 
@@ -96,11 +96,8 @@ int SUBVERSION;
 // platform independant focus loss
 UINT8 window_notinfocus = false;
 
-//
-// DEMO LOOP
-//
-static char *startupwadfiles[MAX_WADFILES];
-static char *startuppwads[MAX_WADFILES];
+static addfilelist_t startupwadfiles;
+static addfilelist_t startuppwads;
 
 boolean devparm = false; // started game with -devparm
 
@@ -119,6 +116,9 @@ boolean midi_disabled = false;
 boolean sound_disabled = false;
 boolean digital_disabled = false;
 
+//
+// DEMO LOOP
+//
 boolean advancedemo;
 #ifdef DEBUGFILE
 INT32 debugload = 0;
@@ -191,22 +191,22 @@ void D_ProcessEvents(void)
 		if (ev->type == ev_keydown || ev->type == ev_keyup)
 		{
 			// Mouse buttons
-			if ((UINT32)(ev->data1 - KEY_MOUSE1) < MOUSEBUTTONS)
+			if ((UINT32)(ev->key - KEY_MOUSE1) < MOUSEBUTTONS)
 			{
 				if (ev->type == ev_keydown)
-					mouse.buttons |= 1 << (ev->data1 - KEY_MOUSE1);
+					mouse.buttons |= 1 << (ev->key - KEY_MOUSE1);
 				else
-					mouse.buttons &= ~(1 << (ev->data1 - KEY_MOUSE1));
+					mouse.buttons &= ~(1 << (ev->key - KEY_MOUSE1));
 			}
-			else if ((UINT32)(ev->data1 - KEY_2MOUSE1) < MOUSEBUTTONS)
+			else if ((UINT32)(ev->key - KEY_2MOUSE1) < MOUSEBUTTONS)
 			{
 				if (ev->type == ev_keydown)
-					mouse2.buttons |= 1 << (ev->data1 - KEY_2MOUSE1);
+					mouse2.buttons |= 1 << (ev->key - KEY_2MOUSE1);
 				else
-					mouse2.buttons &= ~(1 << (ev->data1 - KEY_2MOUSE1));
+					mouse2.buttons &= ~(1 << (ev->key - KEY_2MOUSE1));
 			}
 			// Scroll (has no keyup event)
-			else switch (ev->data1) {
+			else switch (ev->key) {
 				case KEY_MOUSEWHEELUP:
 					mouse.buttons |= MB_SCROLLUP;
 					break;
@@ -272,7 +272,7 @@ void D_ProcessEvents(void)
 		if (eaten)
 			continue; // ate the event
 
-		if (!hooked && G_LuaResponder(ev))
+		if (!hooked && !CON_Ready() && G_LuaResponder(ev))
 			continue;
 
 		G_Responder(ev);
@@ -476,7 +476,7 @@ static void D_Display(void)
 
 			if (!automapactive && !dedicated && cv_renderview.value)
 			{
-				ps_rendercalltime = I_GetPreciseTime();
+				PS_START_TIMING(ps_rendercalltime);
 				if (players[displayplayer].mo || players[displayplayer].playerstate == PST_DEAD)
 				{
 					topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
@@ -523,7 +523,7 @@ static void D_Display(void)
 					if (postimgtype2)
 						V_DoPostProcessor(1, postimgtype2, postimgparam2);
 				}
-				ps_rendercalltime = I_GetPreciseTime() - ps_rendercalltime;
+				PS_STOP_TIMING(ps_rendercalltime);
 			}
 
 			if (lastdraw)
@@ -537,7 +537,7 @@ static void D_Display(void)
 				lastdraw = false;
 			}
 
-			ps_uitime = I_GetPreciseTime();
+			PS_START_TIMING(ps_uitime);
 
 			if (gamestate == GS_LEVEL)
 			{
@@ -550,7 +550,7 @@ static void D_Display(void)
 		}
 		else
 		{
-			ps_uitime = I_GetPreciseTime();
+			PS_START_TIMING(ps_uitime);
 		}
 	}
 
@@ -592,7 +592,7 @@ static void D_Display(void)
 
 	CON_Drawer();
 
-	ps_uitime = I_GetPreciseTime() - ps_uitime;
+	PS_STOP_TIMING(ps_uitime);
 
 	//
 	// wipe update
@@ -678,9 +678,9 @@ static void D_Display(void)
 			M_DrawPerfStats();
 		}
 
-		ps_swaptime = I_GetPreciseTime();
+		PS_START_TIMING(ps_swaptime);
 		I_FinishUpdate(); // page flip or blit buffer
-		ps_swaptime = I_GetPreciseTime() - ps_swaptime;
+		PS_STOP_TIMING(ps_swaptime);
 	}
 }
 
@@ -923,35 +923,68 @@ void D_StartTitle(void)
 	tutorialmode = false;
 }
 
-//
-// D_AddFile
-//
-static void D_AddFile(char **list, const char *file)
+#define REALLOC_FILE_LIST \
+	if (list->files == NULL) \
+	{ \
+		list->files = calloc(sizeof(list->files), 2); \
+		list->numfiles = 1; \
+	} \
+	else \
+	{ \
+		index = list->numfiles; \
+		list->files = realloc(list->files, sizeof(list->files) * ((++list->numfiles) + 1)); \
+		if (list->files == NULL) \
+			I_Error("%s: No more free memory to add file %s", __FUNCTION__, file); \
+	}
+
+static void D_AddFile(addfilelist_t *list, const char *file)
 {
-	size_t pnumwadfiles;
 	char *newfile;
+	size_t index = 0;
 
-	for (pnumwadfiles = 0; list[pnumwadfiles]; pnumwadfiles++)
-		;
+	REALLOC_FILE_LIST
 
 	newfile = malloc(strlen(file) + 1);
 	if (!newfile)
-	{
-		I_Error("No more free memory to AddFile %s",file);
-	}
+		I_Error("D_AddFile: No more free memory to add file %s", file);
+
 	strcpy(newfile, file);
+	list->files[index] = newfile;
+}
+
+static void D_AddFolder(addfilelist_t *list, const char *file)
+{
+	char *newfile;
+	size_t index = 0;
+
+	REALLOC_FILE_LIST
 
-	list[pnumwadfiles] = newfile;
+	newfile = malloc(strlen(file) + 2); // Path delimiter + NULL terminator
+	if (!newfile)
+		I_Error("D_AddFolder: No more free memory to add folder %s", file);
+
+	strcpy(newfile, file);
+	strcat(newfile, PATHSEP);
+
+	list->files[index] = newfile;
 }
 
-static inline void D_CleanFile(char **list)
+#undef REALLOC_FILE_LIST
+
+static inline void D_CleanFile(addfilelist_t *list)
 {
-	size_t pnumwadfiles;
-	for (pnumwadfiles = 0; list[pnumwadfiles]; pnumwadfiles++)
+	if (list->files)
 	{
-		free(list[pnumwadfiles]);
-		list[pnumwadfiles] = NULL;
+		size_t pnumwadfiles = 0;
+
+		for (; pnumwadfiles < list->numfiles; pnumwadfiles++)
+			free(list->files[pnumwadfiles]);
+
+		free(list->files);
+		list->files = NULL;
 	}
+
+	list->numfiles = 0;
 }
 
 ///\brief Checks if a netgame URL is being handled, and changes working directory to the EXE's if so.
@@ -1035,7 +1068,7 @@ static void IdentifyVersion(void)
 
 	// Load the IWAD
 	if (srb2wad != NULL && FIL_ReadFileOK(srb2wad))
-		D_AddFile(startupwadfiles, srb2wad);
+		D_AddFile(&startupwadfiles, srb2wad);
 	else
 		I_Error("srb2.pk3 not found! Expected in %s, ss file: %s\n", srb2waddir, srb2wad);
 
@@ -1046,14 +1079,14 @@ static void IdentifyVersion(void)
 	// checking in D_SRB2Main
 
 	// Add the maps
-	D_AddFile(startupwadfiles, va(pandf,srb2waddir,"zones.pk3"));
+	D_AddFile(&startupwadfiles, va(pandf,srb2waddir, "zones.pk3"));
 
 	// Add the players
-	D_AddFile(startupwadfiles, va(pandf,srb2waddir, "player.dta"));
+	D_AddFile(&startupwadfiles, va(pandf,srb2waddir, "player.dta"));
 
 #ifdef USE_PATCH_DTA
 	// Add our crappy patches to fix our bugs
-	D_AddFile(startupwadfiles, va(pandf,srb2waddir,"patch.pk3"));
+	D_AddFile(&startupwadfiles, va(pandf,srb2waddir, "patch.pk3"));
 #endif
 
 #if !defined (HAVE_SDL) || defined (HAVE_MIXER)
@@ -1063,7 +1096,7 @@ static void IdentifyVersion(void)
 			const char *musicpath = va(pandf,srb2waddir,str);\
 			int ms = W_VerifyNMUSlumps(musicpath, false); \
 			if (ms == 1) \
-				D_AddFile(startupwadfiles, musicpath); \
+				D_AddFile(&startupwadfiles, musicpath); \
 			else if (ms == 0) \
 				I_Error("File "str" has been modified with non-music/sound lumps"); \
 		}
@@ -1237,21 +1270,25 @@ void D_SRB2Main(void)
 	// Do this up here so that WADs loaded through the command line can use ExecCfg
 	COM_Init();
 
-	// add any files specified on the command line with -file wadfile
-	// to the wad list
+	// Add any files specified on the command line with
+	// "-file <file>" or "-folder <folder>" to the add-on list
 	if (!((M_GetUrlProtocolArg() || M_CheckParm("-connect")) && !M_CheckParm("-server")))
 	{
-		if (M_CheckParm("-file"))
-		{
-			// the parms after p are wadfile/lump names,
-			// until end of parms or another - preceded parm
-			while (M_IsNextParm())
-			{
-				const char *s = M_GetNextParm();
+		INT32 addontype = 0;
+		INT32 i;
 
-				if (s) // Check for NULL?
-					D_AddFile(startuppwads, s);
-			}
+		for (i = 1; i < myargc; i++)
+		{
+			if (!strcasecmp(myargv[i], "-file"))
+				addontype = 1;
+			else if (!strcasecmp(myargv[i], "-folder"))
+				addontype = 2;
+			else if (myargv[i][0] == '-' || myargv[i][0] == '+')
+				addontype = 0;
+			else if (addontype == 1)
+				D_AddFile(&startuppwads, myargv[i]);
+			else if (addontype == 2)
+				D_AddFolder(&startuppwads, myargv[i]);
 		}
 	}
 
@@ -1290,8 +1327,8 @@ void D_SRB2Main(void)
 
 	// load wad, including the main wad file
 	CONS_Printf("W_InitMultipleFiles(): Adding IWAD and main PWADs.\n");
-	W_InitMultipleFiles(startupwadfiles);
-	D_CleanFile(startupwadfiles);
+	W_InitMultipleFiles(&startupwadfiles);
+	D_CleanFile(&startupwadfiles);
 
 #ifndef DEVELOP // md5s last updated 22/02/20 (ddmmyy)
 
@@ -1306,8 +1343,6 @@ void D_SRB2Main(void)
 	// ...except it does if they slip maps in there, and that's what W_VerifyNMUSlumps is for.
 #endif //ifndef DEVELOP
 
-	mainwadstally = packetsizetally; // technically not accurate atm, remember to port the two-stage -file process from kart in 2.2.x
-
 	cht_Init();
 
 	//---------------------------------------------------- READY SCREEN
@@ -1338,9 +1373,16 @@ void D_SRB2Main(void)
 
 	I_RegisterSysCommands();
 
-	CONS_Printf("W_InitMultipleFiles(): Adding extra PWADs.\n");
-	W_InitMultipleFiles(startuppwads);
-	D_CleanFile(startuppwads);
+	CON_StopRefresh(); // Temporarily stop refreshing the screen for wad loading
+
+	if (startuppwads.numfiles)
+	{
+		CONS_Printf("W_InitMultipleFiles(): Adding extra PWADs.\n");
+		W_InitMultipleFiles(&startuppwads);
+		D_CleanFile(&startuppwads);
+	}
+
+	CON_StartRefresh(); // Restart the refresh!
 
 	CONS_Printf("HU_LoadGraphics()...\n");
 	HU_LoadGraphics();
diff --git a/src/d_net.c b/src/d_net.c
index 9e5abe24a0adbd1b81d5ab67e102b7689bca53b3..3a4746002eb87efe8dd57e45729cefc96943bdca 100644
--- a/src/d_net.c
+++ b/src/d_net.c
@@ -815,6 +815,8 @@ static const char *packettypename[NUMPACKETTYPE] =
 	"CLIENTJOIN",
 	"NODETIMEOUT",
 	"LOGIN",
+	"TELLFILESNEEDED",
+	"MOREFILESNEEDED",
 	"PING"
 };
 
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 0a7172b127d5d815e58cfbf7ca863a2254461770..cca3102d085aac427b5a3469dd00f3085a3fee3d 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -47,6 +47,7 @@
 #include "m_cond.h"
 #include "m_anigif.h"
 #include "md5.h"
+#include "m_perfstats.h"
 
 #ifdef NETGAME_DEVMODE
 #define CV_RESTRICT CV_NETVAR
@@ -63,7 +64,9 @@ static void Got_WeaponPref(UINT8 **cp, INT32 playernum);
 static void Got_Mapcmd(UINT8 **cp, INT32 playernum);
 static void Got_ExitLevelcmd(UINT8 **cp, INT32 playernum);
 static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum);
+static void Got_RequestAddfoldercmd(UINT8 **cp, INT32 playernum);
 static void Got_Addfilecmd(UINT8 **cp, INT32 playernum);
+static void Got_Addfoldercmd(UINT8 **cp, INT32 playernum);
 static void Got_Pause(UINT8 **cp, INT32 playernum);
 static void Got_Suicide(UINT8 **cp, INT32 playernum);
 static void Got_RandomSeed(UINT8 **cp, INT32 playernum);
@@ -115,6 +118,7 @@ static void Command_Map_f(void);
 static void Command_ResetCamera_f(void);
 
 static void Command_Addfile(void);
+static void Command_Addfolder(void);
 static void Command_ListWADS_f(void);
 static void Command_RunSOC(void);
 static void Command_Pause(void);
@@ -284,7 +288,7 @@ consvar_t cv_gravity = CVAR_INIT ("gravity", "0.5", CV_RESTRICT|CV_FLOAT|CV_CALL
 
 consvar_t cv_soundtest = CVAR_INIT ("soundtest", "0", CV_CALL, NULL, SoundTest_OnChange);
 
-static CV_PossibleValue_t minitimelimit_cons_t[] = {{15, "MIN"}, {9999, "MAX"}, {0, NULL}};
+static CV_PossibleValue_t minitimelimit_cons_t[] = {{1, "MIN"}, {9999, "MAX"}, {0, NULL}};
 consvar_t cv_countdowntime = CVAR_INIT ("countdowntime", "60", CV_SAVE|CV_NETVAR|CV_CHEAT, minitimelimit_cons_t, NULL);
 
 consvar_t cv_touchtag = CVAR_INIT ("touchtag", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
@@ -371,7 +375,14 @@ consvar_t cv_sleep = CVAR_INIT ("cpusleep", "1", CV_SAVE, sleeping_cons_t, NULL)
 
 static CV_PossibleValue_t perfstats_cons_t[] = {
 	{0, "Off"}, {1, "Rendering"}, {2, "Logic"}, {3, "ThinkFrame"}, {0, NULL}};
-consvar_t cv_perfstats = CVAR_INIT ("perfstats", "Off", 0, perfstats_cons_t, NULL);
+consvar_t cv_perfstats = CVAR_INIT ("perfstats", "Off", CV_CALL, perfstats_cons_t, PS_PerfStats_OnChange);
+static CV_PossibleValue_t ps_samplesize_cons_t[] = {
+	{1, "MIN"}, {1000, "MAX"}, {0, NULL}};
+consvar_t cv_ps_samplesize = CVAR_INIT ("ps_samplesize", "1", CV_CALL, ps_samplesize_cons_t, PS_SampleSize_OnChange);
+static CV_PossibleValue_t ps_descriptor_cons_t[] = {
+	{1, "Average"}, {2, "SD"}, {3, "Minimum"}, {4, "Maximum"}, {0, NULL}};
+consvar_t cv_ps_descriptor = CVAR_INIT ("ps_descriptor", "Average", 0, ps_descriptor_cons_t, NULL);
+
 consvar_t cv_freedemocamera = CVAR_INIT("freedemocamera", "Off", CV_SAVE, CV_OnOff, NULL);
 
 char timedemo_name[256];
@@ -398,16 +409,16 @@ const char *netxcmdnames[MAXNETXCMD - 1] =
 	"MAP",
 	"EXITLEVEL",
 	"ADDFILE",
+	"ADDFOLDER",
 	"PAUSE",
 	"ADDPLAYER",
 	"TEAMCHANGE",
 	"CLEARSCORES",
-	"LOGIN",
 	"VERIFIED",
 	"RANDOMSEED",
 	"RUNSOC",
 	"REQADDFILE",
-	"DELFILE", // replace next time we add an XD
+	"REQADDFOLDER",
 	"SETMOTD",
 	"SUICIDE",
 	"LUACMD",
@@ -441,7 +452,9 @@ void D_RegisterServerCommands(void)
 	RegisterNetXCmd(XD_MAP, Got_Mapcmd);
 	RegisterNetXCmd(XD_EXITLEVEL, Got_ExitLevelcmd);
 	RegisterNetXCmd(XD_ADDFILE, Got_Addfilecmd);
+	RegisterNetXCmd(XD_ADDFOLDER, Got_Addfoldercmd);
 	RegisterNetXCmd(XD_REQADDFILE, Got_RequestAddfilecmd);
+	RegisterNetXCmd(XD_REQADDFOLDER, Got_RequestAddfoldercmd);
 	RegisterNetXCmd(XD_PAUSE, Got_Pause);
 	RegisterNetXCmd(XD_SUICIDE, Got_Suicide);
 	RegisterNetXCmd(XD_RUNSOC, Got_RunSOCcmd);
@@ -472,6 +485,7 @@ void D_RegisterServerCommands(void)
 	COM_AddCommand("showmap", Command_Showmap_f);
 	COM_AddCommand("mapmd5", Command_Mapmd5_f);
 
+	COM_AddCommand("addfolder", Command_Addfolder);
 	COM_AddCommand("addfile", Command_Addfile);
 	COM_AddCommand("listwad", Command_ListWADS_f);
 
@@ -861,6 +875,8 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_soundtest);
 
 	CV_RegisterVar(&cv_perfstats);
+	CV_RegisterVar(&cv_ps_samplesize);
+	CV_RegisterVar(&cv_ps_descriptor);
 
 	// ingame object placing
 	COM_AddCommand("objectplace", Command_ObjectPlace_f);
@@ -1512,7 +1528,7 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum)
 			{
 				illegalMask &= ~(1 << i);
 			}
-			
+
 			if ((p->availabilities & illegalMask) != 0)
 			{
 				kick = true;
@@ -3217,7 +3233,7 @@ static void Command_RunSOC(void)
 static void Got_RunSOCcmd(UINT8 **cp, INT32 playernum)
 {
 	char filename[256];
-	filestatus_t ncs = FS_NOTFOUND;
+	filestatus_t ncs = FS_NOTCHECKED;
 
 	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
 	{
@@ -3341,10 +3357,9 @@ static void Command_Addfile(void)
 				break;
 		++p;
 
-		// check total packet size and no of files currently loaded
+		// check no of files currently loaded
 		// See W_LoadWadFile in w_wad.c
-		if ((numwadfiles >= MAX_WADFILES)
-		|| ((packetsizetally + nameonlylength(fn) + 22) > MAXFILENEEDED*sizeof(UINT8)))
+		if (numwadfiles >= MAX_WADFILES)
 		{
 			CONS_Alert(CONS_ERROR, M_GetText("Too many files loaded to add %s\n"), fn);
 			return;
@@ -3373,6 +3388,9 @@ static void Command_Addfile(void)
 
 			for (i = 0; i < numwadfiles; i++)
 			{
+				if (wadfiles[i]->type == RET_FOLDER)
+					continue;
+
 				if (!memcmp(wadfiles[i]->md5sum, md5sum, 16))
 				{
 					CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), fn);
@@ -3392,10 +3410,142 @@ static void Command_Addfile(void)
 	}
 }
 
+static void Command_Addfolder(void)
+{
+	size_t argc = COM_Argc(); // amount of arguments total
+	size_t curarg; // current argument index
+
+	const char *addedfolders[argc]; // list of filenames already processed
+	size_t numfoldersadded = 0; // the amount of filenames processed
+
+	if (argc < 2)
+	{
+		CONS_Printf(M_GetText("addfolder <path> [path2...] [...]: Load add-ons\n"));
+		return;
+	}
+
+	// start at one to skip command name
+	for (curarg = 1; curarg < argc; curarg++)
+	{
+		const char *fn, *p;
+		char *fullpath;
+		char buf[256];
+		char *buf_p = buf;
+		INT32 i, stat;
+		size_t ii;
+		boolean folderadded = false;
+
+		fn = COM_Argv(curarg);
+
+		// For the amount of filenames previously processed...
+		for (ii = 0; ii < numfoldersadded; ii++)
+		{
+			// If this is one of them, don't try to add it.
+			if (!strcmp(fn, addedfolders[ii]))
+			{
+				folderadded = true;
+				break;
+			}
+		}
+
+		// If we've added this one, skip to the next one.
+		if (folderadded)
+		{
+			CONS_Alert(CONS_WARNING, M_GetText("Already processed %s, skipping\n"), fn);
+			continue;
+		}
+
+		// Disallow non-printing characters and semicolons.
+		for (i = 0; fn[i] != '\0'; i++)
+			if (!isprint(fn[i]) || fn[i] == ';')
+				return;
+
+		// Add file on your client directly if you aren't in a netgame.
+		if (!(netgame || multiplayer))
+		{
+			P_AddFolder(fn);
+			addedfolders[numfoldersadded++] = fn;
+			continue;
+		}
+
+		p = fn+strlen(fn);
+		while(--p >= fn)
+			if (*p == '\\' || *p == '/' || *p == ':')
+				break;
+		++p;
+
+		// Don't add an empty path.
+		if (M_IsStringEmpty(fn))
+		{
+			CONS_Alert(CONS_WARNING, M_GetText("Folder name is empty, skipping\n"));
+			continue;
+		}
+
+		// check no of files currently loaded
+		// See W_LoadWadFile in w_wad.c
+		if (numwadfiles >= MAX_WADFILES)
+		{
+			CONS_Alert(CONS_ERROR, M_GetText("Too many files loaded to add %s\n"), fn);
+			return;
+		}
+
+		// Check if the path is valid.
+		stat = W_IsPathToFolderValid(fn);
+
+		if (stat == 0)
+		{
+			CONS_Alert(CONS_WARNING, M_GetText("Path %s is invalid, skipping\n"), fn);
+			continue;
+		}
+		else if (stat < 0)
+		{
+#ifndef AVOID_ERRNO
+			CONS_Alert(CONS_WARNING, M_GetText("Error accessing %s (%s), skipping\n"), fn, strerror(direrror));
+#else
+			CONS_Alert(CONS_WARNING, M_GetText("Error accessing %s, skipping\n"), fn);
+#endif
+			continue;
+		}
+
+		// Get the full path for this folder.
+		fullpath = W_GetFullFolderPath(fn);
+
+		if (fullpath == NULL)
+		{
+			CONS_Alert(CONS_WARNING, M_GetText("Path %s is invalid, skipping\n"), fn);
+			continue;
+		}
+
+		// Check if the folder is already added.
+		for (i = 0; i < numwadfiles; i++)
+		{
+			if (wadfiles[i]->type != RET_FOLDER)
+				continue;
+
+			if (samepaths(wadfiles[i]->path, fullpath) > 0)
+			{
+				CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), fn);
+				continue;
+			}
+		}
+
+		Z_Free(fullpath);
+
+		addedfolders[numfoldersadded++] = fn;
+
+		WRITESTRINGN(buf_p,p,240);
+
+		if (IsPlayerAdmin(consoleplayer) && (!server)) // Request to add file
+			SendNetXCmd(XD_REQADDFOLDER, buf, buf_p - buf);
+		else
+			SendNetXCmd(XD_ADDFOLDER, buf, buf_p - buf);
+	}
+}
+
 static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
 {
 	char filename[241];
-	filestatus_t ncs = FS_NOTFOUND;
+	filestatus_t ncs = FS_NOTCHECKED;
 	UINT8 md5sum[16];
 	boolean kick = false;
 	boolean toomany = false;
@@ -3420,9 +3570,7 @@ static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
 		return;
 	}
 
-	// See W_LoadWadFile in w_wad.c
-	if ((numwadfiles >= MAX_WADFILES)
-	|| ((packetsizetally + nameonlylength(filename) + 22) > MAXFILENEEDED*sizeof(UINT8)))
+	if (numwadfiles >= MAX_WADFILES)
 		toomany = true;
 	else
 		ncs = findfile(filename,md5sum,true);
@@ -3452,10 +3600,66 @@ static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
 	COM_BufAddText(va("addfile %s\n", filename));
 }
 
+static void Got_RequestAddfoldercmd(UINT8 **cp, INT32 playernum)
+{
+	char path[241];
+	filestatus_t ncs = FS_NOTCHECKED;
+	boolean kick = false;
+	boolean toomany = false;
+	INT32 i,j;
+
+	READSTRINGN(*cp, path, 240);
+
+	/// \todo Integrity checks.
+
+	// Only the server processes this message.
+	if (client)
+		return;
+
+	// Disallow non-printing characters and semicolons.
+	for (i = 0; path[i] != '\0'; i++)
+		if (!isprint(path[i]) || path[i] == ';')
+			kick = true;
+
+	if ((playernum != serverplayer && !IsPlayerAdmin(playernum)) || kick)
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("Illegal addfolder command received from %s\n"), player_names[playernum]);
+		SendKick(playernum, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
+		return;
+	}
+
+	if (numwadfiles >= MAX_WADFILES)
+		toomany = true;
+	else
+		ncs = findfolder(path);
+
+	if (ncs != FS_FOUND || toomany)
+	{
+		char message[256];
+
+		if (toomany)
+			sprintf(message, M_GetText("Too many files loaded to add %s\n"), path);
+		else if (ncs == FS_NOTFOUND)
+			sprintf(message, M_GetText("The server doesn't have %s\n"), path);
+		else
+			sprintf(message, M_GetText("Unknown error finding folder (%s)\n"), path);
+
+		CONS_Printf("%s",message);
+
+		for (j = 0; j < MAXPLAYERS; j++)
+			if (adminplayers[j])
+				COM_BufAddText(va("sayto %d %s", adminplayers[j], message));
+
+		return;
+	}
+
+	COM_BufAddText(va("addfolder \"%s\"\n", path));
+}
+
 static void Got_Addfilecmd(UINT8 **cp, INT32 playernum)
 {
 	char filename[241];
-	filestatus_t ncs = FS_NOTFOUND;
+	filestatus_t ncs = FS_NOTCHECKED;
 	UINT8 md5sum[16];
 
 	READSTRINGN(*cp, filename, 240);
@@ -3500,11 +3704,60 @@ static void Got_Addfilecmd(UINT8 **cp, INT32 playernum)
 	G_SetGameModified(true);
 }
 
+static void Got_Addfoldercmd(UINT8 **cp, INT32 playernum)
+{
+	char path[241];
+	filestatus_t ncs = FS_NOTCHECKED;
+
+	READSTRINGN(*cp, path, 240);
+
+	/// \todo Integrity checks.
+
+	if (playernum != serverplayer)
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("Illegal addfolder command received from %s\n"), player_names[playernum]);
+		if (server)
+			SendKick(playernum, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
+		return;
+	}
+
+	ncs = findfolder(path);
+
+	if (ncs != FS_FOUND || !P_AddFolder(path))
+	{
+		Command_ExitGame_f();
+		if (ncs == FS_FOUND)
+		{
+			CONS_Printf(M_GetText("The server tried to add %s,\nbut you have too many files added.\nRestart the game to clear loaded files\nand play on this server."), path);
+			M_StartMessage(va("The server added a folder \n(%s)\nbut you have too many files added.\nRestart the game to clear loaded files.\n\nPress ESC\n",path), NULL, MM_NOTHING);
+		}
+		else if (ncs == FS_NOTFOUND)
+		{
+			CONS_Printf(M_GetText("The server tried to add %s,\nbut you don't have this file.\nYou need to find it in order\nto play on this server."), path);
+			M_StartMessage(va("The server added a folder \n(%s)\nthat you do not have.\n\nPress ESC\n",path), NULL, MM_NOTHING);
+		}
+		else
+		{
+			CONS_Printf(M_GetText("Unknown error finding folder (%s) the server added.\n"), path);
+			M_StartMessage(va("Unknown error trying to load a folder\nthat the server added \n(%s).\n\nPress ESC\n",path), NULL, MM_NOTHING);
+		}
+		return;
+	}
+
+	G_SetGameModified(true);
+}
+
 static void Command_ListWADS_f(void)
 {
 	INT32 i = numwadfiles;
 	char *tempname;
-	CONS_Printf(M_GetText("There are %d wads loaded:\n"),numwadfiles);
+
+#ifdef ENFORCE_WAD_LIMIT
+	CONS_Printf(M_GetText("There are %d/%d files loaded:\n"),numwadfiles,MAX_WADFILES);
+#else
+	CONS_Printf(M_GetText("There are %d files loaded:\n"),numwadfiles);
+#endif
+
 	for (i--; i >= 0; i--)
 	{
 		nameonly(tempname = va("%s", wadfiles[i]->filename));
@@ -3514,6 +3767,8 @@ static void Command_ListWADS_f(void)
 			CONS_Printf("\x82 * %.2d\x80: %s\n", i, tempname);
 		else if (!wadfiles[i]->important)
 			CONS_Printf("\x86   %.2d: %s\n", i, tempname);
+		else if (wadfiles[i]->type == RET_FOLDER)
+			CONS_Printf("\x82 * %.2d\x84: %s\n", i, tempname);
 		else
 			CONS_Printf("   %.2d: %s\n", i, tempname);
 	}
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index bc5f72209987792f20b09b6a37e6f0255436ea00..7bb7eab03a1345190c670b3d9309c159cc3d3c07 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -73,6 +73,7 @@ extern consvar_t cv_teamscramble;
 extern consvar_t cv_scrambleonchange;
 
 extern consvar_t cv_netstat;
+extern consvar_t cv_nettimeout;
 
 extern consvar_t cv_countdowntime;
 extern consvar_t cv_runscripts;
@@ -110,6 +111,8 @@ extern consvar_t cv_skipmapcheck;
 extern consvar_t cv_sleep;
 
 extern consvar_t cv_perfstats;
+extern consvar_t cv_ps_samplesize;
+extern consvar_t cv_ps_descriptor;
 
 extern char timedemo_name[256];
 extern boolean timedemo_csv;
@@ -128,16 +131,16 @@ typedef enum
 	XD_MAP,         // 6
 	XD_EXITLEVEL,   // 7
 	XD_ADDFILE,     // 8
-	XD_PAUSE,       // 9
-	XD_ADDPLAYER,   // 10
-	XD_TEAMCHANGE,  // 11
-	XD_CLEARSCORES, // 12
-	// UNUSED          13 (Because I don't want to change these comments)
-	XD_VERIFIED = 14,//14
+	XD_ADDFOLDER,   // 9
+	XD_PAUSE,       // 10
+	XD_ADDPLAYER,   // 11
+	XD_TEAMCHANGE,  // 12
+	XD_CLEARSCORES, // 13
+	XD_VERIFIED,    // 14
 	XD_RANDOMSEED,  // 15
 	XD_RUNSOC,      // 16
 	XD_REQADDFILE,  // 17
-	XD_DELFILE,     // 18 - replace next time we add an XD
+	XD_REQADDFOLDER,// 18
 	XD_SETMOTD,     // 19
 	XD_SUICIDE,     // 20
 	XD_DEMOTED,     // 21
diff --git a/src/d_netfil.c b/src/d_netfil.c
index 55ab4ea238f8564e73e3ff9a3b2cfe9b5636ba14..fdc0026a8bbf2a87b12fa0444fbfa2156e40c5c1 100644
--- a/src/d_netfil.c
+++ b/src/d_netfil.c
@@ -52,7 +52,7 @@
 #include <errno.h>
 
 // Prototypes
-static boolean AddFileToSendQueue(INT32 node, const char *filename, UINT8 fileid);
+static boolean AddFileToSendQueue(INT32 node, UINT8 fileid);
 
 // Sender structure
 typedef struct filetx_s
@@ -87,7 +87,7 @@ static filetran_t transfer[MAXNETNODES];
 
 // Receiver structure
 INT32 fileneedednum; // Number of files needed to join the server
-fileneeded_t fileneeded[MAX_WADFILES]; // List of needed files
+fileneeded_t *fileneeded; // List of needed files
 static tic_t lasttimeackpacketsent = 0;
 char downloaddir[512] = "DOWNLOAD";
 
@@ -105,6 +105,10 @@ static pauseddownload_t *pauseddownload = NULL;
 #ifndef NONET
 // for cl loading screen
 INT32 lastfilenum = -1;
+INT32 downloadcompletednum = 0;
+UINT32 downloadcompletedsize = 0;
+INT32 totalfilesrequestednum = 0;
+UINT32 totalfilesrequestedsize = 0;
 #endif
 
 luafiletransfer_t *luafiletransfers = NULL;
@@ -113,29 +117,67 @@ boolean waitingforluafilecommand = false;
 char luafiledir[256 + 16] = "luafiles";
 
 
+static UINT16 GetWadNumFromFileNeededId(UINT8 id)
+{
+	UINT16 wadnum;
+
+	for (wadnum = mainwads; wadnum < numwadfiles; wadnum++)
+	{
+		if (!wadfiles[wadnum]->important)
+			continue;
+		if (id == 0)
+			return wadnum;
+		id--;
+	}
+
+	return UINT16_MAX;
+}
+
 /** Fills a serverinfo packet with information about wad files loaded.
   *
   * \todo Give this function a better name since it is in global scope.
-  * Used to have size limiting built in - now handled via W_LoadWadFile in w_wad.c
+  * Used to have size limiting built in - now handled via W_InitFile in w_wad.c
   *
   */
-UINT8 *PutFileNeeded(void)
+UINT8 *PutFileNeeded(UINT16 firstfile)
 {
-	size_t i, count = 0;
-	UINT8 *p = netbuffer->u.serverinfo.fileneeded;
+	size_t i;
+	UINT8 count = 0;
+	UINT8 *p_start = netbuffer->packettype == PT_MOREFILESNEEDED ? netbuffer->u.filesneededcfg.files : netbuffer->u.serverinfo.fileneeded;
+	UINT8 *p = p_start;
 	char wadfilename[MAX_WADPATH] = "";
-	UINT8 filestatus;
+	UINT8 filestatus, folder;
 
-	for (i = 0; i < numwadfiles; i++)
+	for (i = mainwads; i < numwadfiles; i++) //mainwads, otherwise we start on the first mainwad
 	{
 		// If it has only music/sound lumps, don't put it in the list
 		if (!wadfiles[i]->important)
 			continue;
 
+		if (firstfile)
+		{ // Skip files until we reach the first file.
+			firstfile--;
+			continue;
+		}
+
+		nameonly(strcpy(wadfilename, wadfiles[i]->filename));
+
+		// Look below at the WRITE macros to understand what these numbers mean.
+		if (p + 1 + 4 + min(strlen(wadfilename) + 1, MAX_WADPATH) + 16 > p_start + MAXFILENEEDED)
+		{
+			// Too many files to send all at once
+			if (netbuffer->packettype == PT_MOREFILESNEEDED)
+				netbuffer->u.filesneededcfg.more = 1;
+			else
+				netbuffer->u.serverinfo.flags |= SV_LOTSOFADDONS;
+			break;
+		}
+
 		filestatus = 1; // Importance - not really used any more, holds 1 by default for backwards compat with MS
+		folder = (wadfiles[i]->type == RET_FOLDER);
 
 		// Store in the upper four bits
-		if (!cv_downloading.value)
+		if (!cv_downloading.value || folder) /// \todo Implement folder downloading.
 			filestatus += (2 << 4); // Won't send
 		else if ((wadfiles[i]->filesize <= (UINT32)cv_maxsend.value * 1024))
 			filestatus += (1 << 4); // Will send if requested
@@ -143,37 +185,60 @@ UINT8 *PutFileNeeded(void)
 			// filestatus += (0 << 4); -- Won't send, too big
 
 		WRITEUINT8(p, filestatus);
+		WRITEUINT8(p, folder);
 
 		count++;
 		WRITEUINT32(p, wadfiles[i]->filesize);
-		nameonly(strcpy(wadfilename, wadfiles[i]->filename));
 		WRITESTRINGN(p, wadfilename, MAX_WADPATH);
 		WRITEMEM(p, wadfiles[i]->md5sum, 16);
 	}
-	netbuffer->u.serverinfo.fileneedednum = (UINT8)count;
+
+	if (netbuffer->packettype == PT_MOREFILESNEEDED)
+		netbuffer->u.filesneededcfg.num = count;
+	else
+		netbuffer->u.serverinfo.fileneedednum = count;
 
 	return p;
 }
 
+void AllocFileNeeded(INT32 size)
+{
+	if (fileneeded == NULL)
+		fileneeded = Z_Calloc(sizeof(fileneeded_t) * size, PU_STATIC, NULL);
+	else
+		fileneeded = Z_Realloc(fileneeded, sizeof(fileneeded_t) * size, PU_STATIC, NULL);
+}
+
+void FreeFileNeeded(void)
+{
+	Z_Free(fileneeded);
+	fileneeded = NULL;
+}
+
 /** Parses the serverinfo packet and fills the fileneeded table on client
   *
   * \param fileneedednum_parm The number of files needed to join the server
   * \param fileneededstr The memory block containing the list of needed files
   *
   */
-void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr)
+void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 firstfile)
 {
 	INT32 i;
 	UINT8 *p;
 	UINT8 filestatus;
 
-	fileneedednum = fileneedednum_parm;
+	fileneedednum = firstfile + fileneedednum_parm;
 	p = (UINT8 *)fileneededstr;
-	for (i = 0; i < fileneedednum; i++)
+
+	AllocFileNeeded(fileneedednum);
+
+	for (i = firstfile; i < fileneedednum; i++)
 	{
-		fileneeded[i].status = FS_NOTFOUND; // We haven't even started looking for the file yet
+		fileneeded[i].type = FILENEEDED_WAD;
+		fileneeded[i].status = FS_NOTCHECKED; // We haven't even started looking for the file yet
 		fileneeded[i].justdownloaded = false;
 		filestatus = READUINT8(p); // The first byte is the file status
+		fileneeded[i].folder = READUINT8(p); // The second byte is the folder flag
 		fileneeded[i].willsend = (UINT8)(filestatus >> 4);
 		fileneeded[i].totalsize = READUINT32(p); // The four next bytes are the file size
 		fileneeded[i].file = NULL; // The file isn't open yet
@@ -188,7 +253,11 @@ void CL_PrepareDownloadSaveGame(const char *tmpsave)
 	lastfilenum = -1;
 #endif
 
+	FreeFileNeeded();
+	AllocFileNeeded(1);
+
 	fileneedednum = 1;
+	fileneeded[0].type = FILENEEDED_SAVEGAME;
 	fileneeded[0].status = FS_REQUESTED;
 	fileneeded[0].justdownloaded = false;
 	fileneeded[0].totalsize = UINT32_MAX;
@@ -319,14 +388,18 @@ boolean CL_SendFileRequest(void)
 		if ((fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD))
 		{
 			totalfreespaceneeded += fileneeded[i].totalsize;
-			nameonly(fileneeded[i].filename);
+
 			WRITEUINT8(p, i); // fileid
-			WRITESTRINGN(p, fileneeded[i].filename, MAX_WADPATH);
+
 			// put it in download dir
+			nameonly(fileneeded[i].filename);
 			strcatbf(fileneeded[i].filename, downloaddir, "/");
+
 			fileneeded[i].status = FS_REQUESTED;
 		}
+
 	WRITEUINT8(p, 0xFF);
+
 	I_GetDiskFreeSpace(&availablefreespace);
 	if (totalfreespaceneeded > availablefreespace)
 		I_Error("To play on this server you must download %s KB,\n"
@@ -342,21 +415,22 @@ boolean CL_SendFileRequest(void)
 // returns false if a requested file was not found or cannot be sent
 boolean PT_RequestFile(INT32 node)
 {
-	char wad[MAX_WADPATH+1];
 	UINT8 *p = netbuffer->u.textcmd;
 	UINT8 id;
+
 	while (p < netbuffer->u.textcmd + MAXTEXTCMD-1) // Don't allow hacked client to overflow
 	{
 		id = READUINT8(p);
 		if (id == 0xFF)
 			break;
-		READSTRINGN(p, wad, MAX_WADPATH);
-		if (!AddFileToSendQueue(node, wad, id))
+
+		if (!AddFileToSendQueue(node, id))
 		{
 			SV_AbortSendFiles(node);
 			return false; // don't read the rest of the files
 		}
 	}
+
 	return true; // no problems with any files
 }
 
@@ -365,23 +439,16 @@ boolean PT_RequestFile(INT32 node)
   * \return 0 if some files are missing
   *         1 if all files exist
   *         2 if some already loaded files are not requested or are in a different order
+  *         3 too many files, over WADLIMIT
+  *         4 still checking, continuing next tic
   *
   */
 INT32 CL_CheckFiles(void)
 {
 	INT32 i, j;
 	char wadfilename[MAX_WADPATH];
-	INT32 ret = 1;
-	size_t packetsize = 0;
-	size_t filestoget = 0;
-
-//	if (M_CheckParm("-nofiles"))
-//		return 1;
-
-	// the first is the iwad (the main wad file)
-	// we don't care if it's called srb2.pk3 or not.
-	// Never download the IWAD, just assume it's there and identical
-	fileneeded[0].status = FS_OPEN;
+	size_t filestoload = 0;
+	boolean downloadrequired = false;
 
 	// Modified game handling -- check for an identical file list
 	// must be identical in files loaded AND in order
@@ -389,7 +456,7 @@ INT32 CL_CheckFiles(void)
 	if (modifiedgame)
 	{
 		CONS_Debug(DBG_NETPLAY, "game is modified; only doing basic checks\n");
-		for (i = 1, j = 1; i < fileneedednum || j < numwadfiles;)
+		for (i = 0, j = mainwads; i < fileneedednum || j < numwadfiles;)
 		{
 			if (j < numwadfiles && !wadfiles[j]->important)
 			{
@@ -416,15 +483,21 @@ INT32 CL_CheckFiles(void)
 		return 1;
 	}
 
-	// See W_LoadWadFile in w_wad.c
-	packetsize = packetsizetally;
-
-	for (i = 1; i < fileneedednum; i++)
+	for (i = 0; i < fileneedednum; i++)
 	{
+		if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
+			downloadrequired = true;
+
+		if (fileneeded[i].status != FS_OPEN)
+			filestoload++;
+
+		if (fileneeded[i].status != FS_NOTCHECKED) //since we're running this over multiple tics now, its possible for us to come across files checked in previous tics
+			continue;
+
 		CONS_Debug(DBG_NETPLAY, "searching for '%s' ", fileneeded[i].filename);
 
 		// Check in already loaded files
-		for (j = 1; wadfiles[j]; j++)
+		for (j = mainwads; wadfiles[j]; j++)
 		{
 			nameonly(strcpy(wadfilename, wadfiles[j]->filename));
 			if (!stricmp(wadfilename, fileneeded[i].filename) &&
@@ -432,45 +505,46 @@ INT32 CL_CheckFiles(void)
 			{
 				CONS_Debug(DBG_NETPLAY, "already loaded\n");
 				fileneeded[i].status = FS_OPEN;
-				break;
+				return 4;
 			}
 		}
-		if (fileneeded[i].status != FS_NOTFOUND)
-			continue;
-
-		packetsize += nameonlylength(fileneeded[i].filename) + 22;
 
-		if ((numwadfiles+filestoget >= MAX_WADFILES)
-		|| (packetsize > MAXFILENEEDED*sizeof(UINT8)))
-			return 3;
-
-		filestoget++;
+		if (fileneeded[i].folder)
+			fileneeded[i].status = findfolder(fileneeded[i].filename);
+		else
+			fileneeded[i].status = findfile(fileneeded[i].filename, fileneeded[i].md5sum, true);
 
-		fileneeded[i].status = findfile(fileneeded[i].filename, fileneeded[i].md5sum, true);
 		CONS_Debug(DBG_NETPLAY, "found %d\n", fileneeded[i].status);
-		if (fileneeded[i].status != FS_FOUND)
-			ret = 0;
+		return 4;
 	}
-	return ret;
+
+	//now making it here means we've checked the entire list and no FS_NOTCHECKED files remain
+	if (numwadfiles+filestoload > MAX_WADFILES)
+		return 3;
+	else if (downloadrequired)
+		return 0; //some stuff is FS_NOTFOUND, needs download
+	else
+		return 1; //everything is FS_OPEN or FS_FOUND, proceed to loading
 }
 
 // Load it now
-void CL_LoadServerFiles(void)
+boolean CL_LoadServerFiles(void)
 {
 	INT32 i;
 
-//	if (M_CheckParm("-nofiles"))
-//		return;
-
-	for (i = 1; i < fileneedednum; i++)
+	for (i = 0; i < fileneedednum; i++)
 	{
 		if (fileneeded[i].status == FS_OPEN)
 			continue; // Already loaded
 		else if (fileneeded[i].status == FS_FOUND)
 		{
-			P_AddWadFile(fileneeded[i].filename);
+			if (fileneeded[i].folder)
+				P_AddFolder(fileneeded[i].filename);
+			else
+				P_AddWadFile(fileneeded[i].filename);
 			G_SetGameModified(true);
 			fileneeded[i].status = FS_OPEN;
+			return false;
 		}
 		else if (fileneeded[i].status == FS_MD5SUMBAD)
 			I_Error("Wrong version of file %s", fileneeded[i].filename);
@@ -496,6 +570,7 @@ void CL_LoadServerFiles(void)
 				fileneeded[i].status, s);
 		}
 	}
+	return true;
 }
 
 void AddLuaFileTransfer(const char *filename, const char *mode)
@@ -677,7 +752,11 @@ void CL_PrepareDownloadLuaFile(void)
 	netbuffer->packettype = PT_ASKLUAFILE;
 	HSendPacket(servernode, true, 0, 0);
 
+	FreeFileNeeded();
+	AllocFileNeeded(1);
+
 	fileneedednum = 1;
+	fileneeded[0].type = FILENEEDED_LUAFILE;
 	fileneeded[0].status = FS_REQUESTED;
 	fileneeded[0].justdownloaded = false;
 	fileneeded[0].totalsize = UINT32_MAX;
@@ -704,15 +783,11 @@ static INT32 filestosend = 0;
   * \sa AddLuaFileToSendQueue
   *
   */
-static boolean AddFileToSendQueue(INT32 node, const char *filename, UINT8 fileid)
+static boolean AddFileToSendQueue(INT32 node, UINT8 fileid)
 {
 	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];
-
-	if (cv_noticedownload.value)
-		CONS_Printf("Sending file \"%s\" to node %d (%s)\n", filename, node, I_GetNodeAddress(node));
+	UINT16 wadnum;
 
 	// Find the last file in the list and set a pointer to its "next" field
 	q = &transfer[node].txlist;
@@ -732,51 +807,43 @@ static boolean AddFileToSendQueue(INT32 node, const char *filename, UINT8 fileid
 	if (!p->id.filename)
 		I_Error("AddFileToSendQueue: No more memory\n");
 
-	// Set the file name and get rid of the path
-	strlcpy(p->id.filename, filename, MAX_WADPATH);
-	nameonly(p->id.filename);
-
-	// Look for the requested file through all loaded files
-	for (i = 0; wadfiles[i]; i++)
-	{
-		strlcpy(wadfilename, wadfiles[i]->filename, MAX_WADPATH);
-		nameonly(wadfilename);
-		if (!stricmp(wadfilename, p->id.filename))
-		{
-			// Copy file name with full path
-			strlcpy(p->id.filename, wadfiles[i]->filename, MAX_WADPATH);
-			break;
-		}
-	}
+	// Find the wad the ID refers to
+	wadnum = GetWadNumFromFileNeededId(fileid);
 
 	// Handle non-loaded file requests
-	if (!wadfiles[i])
+	if (wadnum == UINT16_MAX)
 	{
-		DEBFILE(va("%s not found in wadfiles\n", filename));
+		DEBFILE(va("fileneeded %d not found in wadfiles\n", fileid));
 		// This formerly checked if (!findfile(p->id.filename, NULL, true))
 
 		// Not found
-		// Don't inform client (probably someone who thought they could leak 2.2 ACZ)
-		DEBFILE(va("Client %d request %s: not found\n", node, filename));
+		// Don't inform client
+		DEBFILE(va("Client %d request fileneeded %d: not found\n", node, fileid));
 		free(p->id.filename);
 		free(p);
 		*q = NULL;
 		return false; // cancel the rest of the requests
 	}
 
+	// Set the file name and get rid of the path
+	strlcpy(p->id.filename, wadfiles[wadnum]->filename, MAX_WADPATH);
+
 	// Handle huge file requests (i.e. bigger than cv_maxsend.value KB)
-	if (wadfiles[i]->filesize > (UINT32)cv_maxsend.value * 1024)
+	if (wadfiles[wadnum]->filesize > (UINT32)cv_maxsend.value * 1024)
 	{
 		// Too big
 		// Don't inform client (client sucks, man)
-		DEBFILE(va("Client %d request %s: file too big, not sending\n", node, filename));
+		DEBFILE(va("Client %d request %s: file too big, not sending\n", node, p->id.filename));
 		free(p->id.filename);
 		free(p);
 		*q = NULL;
 		return false; // cancel the rest of the requests
 	}
 
-	DEBFILE(va("Sending file %s (id=%d) to %d\n", filename, fileid, node));
+	if (cv_noticedownload.value)
+		CONS_Printf("Sending file \"%s\" to node %d (%s)\n", p->id.filename, node, I_GetNodeAddress(node));
+
+	DEBFILE(va("Sending file %s (id=%d) to %d\n", p->id.filename, fileid, node));
 	p->ram = SF_FILE; // It's a file, we need to close it and free its name once we're done sending it
 	p->fileid = fileid;
 	p->next = NULL; // End of list
@@ -913,7 +980,6 @@ static void SV_EndFileSend(INT32 node)
 	filestosend--;
 }
 
-#define PACKETPERTIC net_bandwidth/(TICRATE*software_MAXPACKETLENGTH)
 #define FILEFRAGMENTSIZE (software_MAXPACKETLENGTH - (FILETXHEADER + BASEPACKETSIZE))
 
 /** Handles file transmission
@@ -946,14 +1012,7 @@ void FileSendTicker(void)
 	if (!filestosend) // No file to send
 		return;
 
-	if (cv_downloadspeed.value) // New behavior
-		packetsent = cv_downloadspeed.value;
-	else // Old behavior
-	{
-		packetsent = PACKETPERTIC;
-		if (!packetsent)
-			packetsent = 1;
-	}
+	packetsent = cv_downloadspeed.value;
 
 	netbuffer->packettype = PT_FILEFRAGMENT;
 
@@ -1230,6 +1289,9 @@ void PT_FileFragment(void)
 	UINT16 boundedfragmentsize = doomcom->datalength - BASEPACKETSIZE - sizeof(netbuffer->u.filetxpak);
 	char *filename;
 
+	if (!file)
+		return;
+
 	filename = va("%s", file->filename);
 	nameonly(filename);
 
@@ -1341,6 +1403,7 @@ void PT_FileFragment(void)
 					// Tell the server we have received the file
 					netbuffer->packettype = PT_HASLUAFILE;
 					HSendPacket(servernode, true, 0, 0);
+					FreeFileNeeded();
 				}
 			}
 		}
@@ -1411,32 +1474,37 @@ void CloseNetFile(void)
 		SV_AbortSendFiles(i);
 
 	// Receiving a file?
-	for (i = 0; i < MAX_WADFILES; i++)
-		if (fileneeded[i].status == FS_DOWNLOADING && fileneeded[i].file)
-		{
-			fclose(fileneeded[i].file);
-			free(fileneeded[i].ackpacket);
-
-			if (!pauseddownload && i != 0) // 0 is either srb2.srb or the gamestate...
-			{
-				// Don't remove the file, save it for later in case we resume the download
-				pauseddownload = malloc(sizeof(*pauseddownload));
-				if (!pauseddownload)
-					I_Error("CloseNetFile: No more memory\n");
-
-				strcpy(pauseddownload->filename, fileneeded[i].filename);
-				memcpy(pauseddownload->md5sum, fileneeded[i].md5sum, 16);
-				pauseddownload->currentsize = fileneeded[i].currentsize;
-				pauseddownload->receivedfragments = fileneeded[i].receivedfragments;
-				pauseddownload->fragmentsize = fileneeded[i].fragmentsize;
-			}
-			else
+	if (fileneeded)
+	{
+		for (i = 0; i < fileneedednum; i++)
+			if (fileneeded[i].status == FS_DOWNLOADING && fileneeded[i].file)
 			{
-				free(fileneeded[i].receivedfragments);
-				// File is not complete delete it
-				remove(fileneeded[i].filename);
+				fclose(fileneeded[i].file);
+				free(fileneeded[i].ackpacket);
+
+				if (!pauseddownload && (fileneeded[i].type == FILENEEDED_WAD || i != 0)) // 0 is the gamestate...
+				{
+					// Don't remove the file, save it for later in case we resume the download
+					pauseddownload = malloc(sizeof(*pauseddownload));
+					if (!pauseddownload)
+						I_Error("CloseNetFile: No more memory\n");
+
+					strcpy(pauseddownload->filename, fileneeded[i].filename);
+					memcpy(pauseddownload->md5sum, fileneeded[i].md5sum, 16);
+					pauseddownload->currentsize = fileneeded[i].currentsize;
+					pauseddownload->receivedfragments = fileneeded[i].receivedfragments;
+					pauseddownload->fragmentsize = fileneeded[i].fragmentsize;
+				}
+				else
+				{
+					// File is not complete, delete it.
+					free(fileneeded[i].receivedfragments);
+					remove(fileneeded[i].filename);
+				}
 			}
-		}
+	}
+
+	FreeFileNeeded();
 }
 
 void Command_Downloads_f(void)
@@ -1571,3 +1639,26 @@ filestatus_t findfile(char *filename, const UINT8 *wantedmd5sum, boolean complet
 
 	return (badmd5 ? FS_MD5SUMBAD : FS_NOTFOUND); // md5 sum bad or file not found
 }
+
+// Searches for a folder.
+// This can be used with a full path, or an incomplete path.
+// In the latter case, the function will try to find folders in
+// srb2home, srb2path, and the current directory.
+filestatus_t findfolder(const char *path)
+{
+	// Check the path by itself first.
+	if (concatpaths(path, NULL) == 1)
+		return FS_FOUND;
+
+#define checkpath(startpath) \
+	if (concatpaths(path, startpath) == 1) \
+		return FS_FOUND
+
+	checkpath(srb2home); // Then, look in srb2home.
+	checkpath(srb2path); // Now, look in srb2path.
+	checkpath("."); // Finally, look in the current directory.
+
+#undef checkpath
+
+	return FS_NOTFOUND;
+}
diff --git a/src/d_netfil.h b/src/d_netfil.h
index ddcbcfec385b64c45bef9438992be47e8bbaf047..3d713c150fad6f520a618d8e158f96599f081323 100644
--- a/src/d_netfil.h
+++ b/src/d_netfil.h
@@ -27,6 +27,7 @@ typedef enum
 
 typedef enum
 {
+	FS_NOTCHECKED,
 	FS_NOTFOUND,
 	FS_FOUND,
 	FS_REQUESTED,
@@ -35,12 +36,21 @@ typedef enum
 	FS_MD5SUMBAD
 } filestatus_t;
 
+typedef enum
+{
+	FILENEEDED_WAD,
+	FILENEEDED_SAVEGAME,
+	FILENEEDED_LUAFILE
+} fileneededtype_t;
+
 typedef struct
 {
-	UINT8 willsend; // Is the server willing to send it?
 	char filename[MAX_WADPATH];
 	UINT8 md5sum[16];
 	filestatus_t status; // The value returned by recsearch
+	UINT8 willsend; // Is the server willing to send it?
+	UINT8 folder; // File is a folder
+	fileneededtype_t type;
 	boolean justdownloaded; // To prevent late fragments from causing an I_Error
 
 	// Used only for download
@@ -54,20 +64,28 @@ typedef struct
 	UINT32 ackresendposition; // Used when resuming downloads
 } fileneeded_t;
 
+#define FILENEEDEDSIZE 23
+
 extern INT32 fileneedednum;
-extern fileneeded_t fileneeded[MAX_WADFILES];
+extern fileneeded_t *fileneeded;
 extern char downloaddir[512];
 
 #ifndef NONET
 extern INT32 lastfilenum;
+extern INT32 downloadcompletednum;
+extern UINT32 downloadcompletedsize;
+extern INT32 totalfilesrequestednum;
+extern UINT32 totalfilesrequestedsize;
 #endif
 
-UINT8 *PutFileNeeded(void);
-void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr);
+void AllocFileNeeded(INT32 size);
+void FreeFileNeeded(void);
+UINT8 *PutFileNeeded(UINT16 firstfile);
+void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 firstfile);
 void CL_PrepareDownloadSaveGame(const char *tmpsave);
 
 INT32 CL_CheckFiles(void);
-void CL_LoadServerFiles(void);
+boolean CL_LoadServerFiles(void);
 void AddRamToSendQueue(INT32 node, void *data, size_t size, freemethod_t freemethod,
 	UINT8 fileid);
 
@@ -135,6 +153,9 @@ filestatus_t findfile(char *filename, const UINT8 *wantedmd5sum,
 	boolean completepath);
 filestatus_t checkfilemd5(char *filename, const UINT8 *wantedmd5sum);
 
+// Searches for a folder
+filestatus_t findfolder(const char *path);
+
 void nameonly(char *s);
 size_t nameonlylength(const char *s);
 
diff --git a/src/d_player.h b/src/d_player.h
index 54ab342886ccaf6e0a8e2c4bf044444a6e487d8a..a0db1402df153beb928c54d743bc4388f3bc2122 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -313,9 +313,43 @@ typedef enum
 	RW_RAIL    = 32
 } ringweapons_t;
 
+//Bot types
+typedef enum
+{
+	BOT_NONE = 0,
+	BOT_2PAI,
+	BOT_2PHUMAN,
+	BOT_MPAI
+} bottype_t;
+
+//AI states
+typedef enum
+{
+	AI_STANDBY = 0,
+	AI_FOLLOW,
+	AI_CATCHUP,
+	AI_THINKFLY,
+	AI_FLYSTANDBY,
+	AI_FLYCARRY,
+	AI_SPINFOLLOW
+} aistatetype_t;
+
+
 // ========================================================================
 //                          PLAYER STRUCTURE
 // ========================================================================
+
+//Bot memory struct
+typedef struct botmem_s
+{
+	boolean lastForward;
+	boolean lastBlocked;
+	boolean blocked;	
+	UINT8 catchup_tics;
+	UINT8 thinkstate;
+} botmem_t;
+
+//Main struct
 typedef struct player_s
 {
 	mobj_t *mo;
@@ -525,8 +559,13 @@ typedef struct player_s
 
 	boolean spectator;
 	boolean outofcoop;
+	boolean removing;
 	UINT8 bot;
-
+	struct player_s *botleader;
+	UINT16 lastbuttons;
+	botmem_t botmem;
+	boolean blocked;
+	
 	tic_t jointime; // Timer when player joins game to change skin/color
 	tic_t quittime; // Time elapsed since user disconnected, zero if connected
 #ifdef HWRENDER
diff --git a/src/deh_tables.c b/src/deh_tables.c
index 438ff20901d013cad71a7d99f05c9374d287b520..9a0d919911312fc57759f0621f2dccd46a4c9391 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -5170,6 +5170,12 @@ struct int_const_s const INT_CONST[] = {
 	{"GF_REDFLAG",GF_REDFLAG},
 	{"GF_BLUEFLAG",GF_BLUEFLAG},
 
+	// Bot types
+	{"BOT_NONE",BOT_NONE},
+	{"BOT_2PAI",BOT_2PAI},
+	{"BOT_2PHUMAN",BOT_2PHUMAN},
+	{"BOT_MPAI",BOT_MPAI},
+
 	// Customisable sounds for Skins, from sounds.h
 	{"SKSSPIN",SKSSPIN},
 	{"SKSPUTPUT",SKSPUTPUT},
@@ -5480,49 +5486,49 @@ struct int_const_s const INT_CONST[] = {
 	{"JOYAXISRANGE",JOYAXISRANGE},
 
 	// Game controls
-	{"gc_null",gc_null},
-	{"gc_forward",gc_forward},
-	{"gc_backward",gc_backward},
-	{"gc_strafeleft",gc_strafeleft},
-	{"gc_straferight",gc_straferight},
-	{"gc_turnleft",gc_turnleft},
-	{"gc_turnright",gc_turnright},
-	{"gc_weaponnext",gc_weaponnext},
-	{"gc_weaponprev",gc_weaponprev},
-	{"gc_wepslot1",gc_wepslot1},
-	{"gc_wepslot2",gc_wepslot2},
-	{"gc_wepslot3",gc_wepslot3},
-	{"gc_wepslot4",gc_wepslot4},
-	{"gc_wepslot5",gc_wepslot5},
-	{"gc_wepslot6",gc_wepslot6},
-	{"gc_wepslot7",gc_wepslot7},
-	{"gc_wepslot8",gc_wepslot8},
-	{"gc_wepslot9",gc_wepslot9},
-	{"gc_wepslot10",gc_wepslot10},
-	{"gc_fire",gc_fire},
-	{"gc_firenormal",gc_firenormal},
-	{"gc_tossflag",gc_tossflag},
-	{"gc_spin",gc_spin},
-	{"gc_camtoggle",gc_camtoggle},
-	{"gc_camreset",gc_camreset},
-	{"gc_lookup",gc_lookup},
-	{"gc_lookdown",gc_lookdown},
-	{"gc_centerview",gc_centerview},
-	{"gc_mouseaiming",gc_mouseaiming},
-	{"gc_talkkey",gc_talkkey},
-	{"gc_teamkey",gc_teamkey},
-	{"gc_scores",gc_scores},
-	{"gc_jump",gc_jump},
-	{"gc_console",gc_console},
-	{"gc_pause",gc_pause},
-	{"gc_systemmenu",gc_systemmenu},
-	{"gc_screenshot",gc_screenshot},
-	{"gc_recordgif",gc_recordgif},
-	{"gc_viewpoint",gc_viewpoint},
-	{"gc_custom1",gc_custom1},
-	{"gc_custom2",gc_custom2},
-	{"gc_custom3",gc_custom3},
-	{"num_gamecontrols",num_gamecontrols},
+	{"GC_NULL",GC_NULL},
+	{"GC_FORWARD",GC_FORWARD},
+	{"GC_BACKWARD",GC_BACKWARD},
+	{"GC_STRAFELEFT",GC_STRAFELEFT},
+	{"GC_STRAFERIGHT",GC_STRAFERIGHT},
+	{"GC_TURNLEFT",GC_TURNLEFT},
+	{"GC_TURNRIGHT",GC_TURNRIGHT},
+	{"GC_WEAPONNEXT",GC_WEAPONNEXT},
+	{"GC_WEAPONPREV",GC_WEAPONPREV},
+	{"GC_WEPSLOT1",GC_WEPSLOT1},
+	{"GC_WEPSLOT2",GC_WEPSLOT2},
+	{"GC_WEPSLOT3",GC_WEPSLOT3},
+	{"GC_WEPSLOT4",GC_WEPSLOT4},
+	{"GC_WEPSLOT5",GC_WEPSLOT5},
+	{"GC_WEPSLOT6",GC_WEPSLOT6},
+	{"GC_WEPSLOT7",GC_WEPSLOT7},
+	{"GC_WEPSLOT8",GC_WEPSLOT8},
+	{"GC_WEPSLOT9",GC_WEPSLOT9},
+	{"GC_WEPSLOT10",GC_WEPSLOT10},
+	{"GC_FIRE",GC_FIRE},
+	{"GC_FIRENORMAL",GC_FIRENORMAL},
+	{"GC_TOSSFLAG",GC_TOSSFLAG},
+	{"GC_SPIN",GC_SPIN},
+	{"GC_CAMTOGGLE",GC_CAMTOGGLE},
+	{"GC_CAMRESET",GC_CAMRESET},
+	{"GC_LOOKUP",GC_LOOKUP},
+	{"GC_LOOKDOWN",GC_LOOKDOWN},
+	{"GC_CENTERVIEW",GC_CENTERVIEW},
+	{"GC_MOUSEAIMING",GC_MOUSEAIMING},
+	{"GC_TALKKEY",GC_TALKKEY},
+	{"GC_TEAMKEY",GC_TEAMKEY},
+	{"GC_SCORES",GC_SCORES},
+	{"GC_JUMP",GC_JUMP},
+	{"GC_CONSOLE",GC_CONSOLE},
+	{"GC_PAUSE",GC_PAUSE},
+	{"GC_SYSTEMMENU",GC_SYSTEMMENU},
+	{"GC_SCREENSHOT",GC_SCREENSHOT},
+	{"GC_RECORDGIF",GC_RECORDGIF},
+	{"GC_VIEWPOINT",GC_VIEWPOINT},
+	{"GC_CUSTOM1",GC_CUSTOM1},
+	{"GC_CUSTOM2",GC_CUSTOM2},
+	{"GC_CUSTOM3",GC_CUSTOM3},
+	{"NUM_GAMECONTROLS",NUM_GAMECONTROLS},
 
 	// Mouse buttons
 	{"MB_BUTTON1",MB_BUTTON1},
diff --git a/src/doomdef.h b/src/doomdef.h
index 11ca80538a0014563eb5761107a8da9f4efa1d4d..7e7e355990d422a8a6ecc86f54f49a17902ea66b 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -127,6 +127,7 @@ extern char logfilename[1024];
 //#define DEVELOP // Disable this for release builds to remove excessive cheat commands and enable MD5 checking and stuff, all in one go. :3
 #ifdef DEVELOP
 #define VERSIONSTRING "Development EXE"
+#define VERSIONSTRING_RC "Development EXE" "\0"
 // most interface strings are ignored in development mode.
 // we use comprevision and compbranch instead.
 // VERSIONSTRING_RC is for the resource-definition script used by windows builds
@@ -151,6 +152,9 @@ extern char logfilename[1024];
 // Comment or uncomment this as necessary.
 #define USE_PATCH_DTA
 
+// Enforce a limit of loaded WAD files.
+//#define ENFORCE_WAD_LIMIT
+
 // Use .kart extension addons
 //#define USE_KART
 
diff --git a/src/f_finale.c b/src/f_finale.c
index e8757c18adcaad692faac6d83542eccd00650a10..8dd03d44f5d068f2b5762dbaea7e51dbd408e5b7 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -41,6 +41,7 @@
 #include "console.h"
 
 #include "lua_hud.h"
+#include "lua_hook.h"
 
 // Stage of animation:
 // 0 = text, 1 = art screen
@@ -1011,7 +1012,7 @@ void F_IntroTicker(void)
 //
 boolean F_IntroResponder(event_t *event)
 {
-	INT32 key = event->data1;
+	INT32 key = event->key;
 
 	// remap virtual keys (mouse & joystick buttons)
 	switch (key)
@@ -1089,19 +1090,19 @@ static const char *credits[] = {
 	"\"Hannu_Hanhi\"", // For many OpenGL performance improvements!
 	"Kepa \"Nev3r\" Iceta",
 	"Thomas \"Shadow Hog\" Igoe",
-	"\"james\"",
 	"Iestyn \"Monster Iestyn\" Jealous",
-	"\"Jimita\"",
 	"\"Kaito Sinclaire\"",
 	"\"Kalaron\"", // Coded some of Sryder13's collection of OpenGL fixes, especially fog
 	"Ronald \"Furyhunter\" Kinard", // The SDL2 port
 	"\"Lat'\"", // SRB2-CHAT, the chat window from Kart
+	"\"LZA\"",
 	"Matthew \"Shuffle\" Marsalko",
 	"Steven \"StroggOnMeth\" McGranahan",
 	"\"Morph\"", // For SRB2Morphed stuff
 	"Louis-Antoine \"LJ Sonic\" de Moulins", // de Rochefort doesn't quite fit on the screen sorry lol
 	"John \"JTE\" Muniz",
 	"Colin \"Sonict\" Pfaff",
+	"James \"james\" Robert Roman",
 	"Sean \"Sryder13\" Ryder",
 	"Ehab \"Wolfy\" Saeed",
 	"Tasos \"tatokis\" Sahanidis", // Corrected C FixedMul, making 64-bit builds netplay compatible
@@ -1166,9 +1167,8 @@ static const char *credits[] = {
 	"Alexander \"DrTapeworm\" Moench-Ford",
 	"Stefan \"Stuf\" Rimalia",
 	"Shane Mychal Sexton",
-	"\"Spazzo\"",
-	"David \"Big Wave Dave\" Spencer Sr.",
-	"David \"Instant Sonic\" Spencer Jr.",
+	"Dave \"Big Wave Dave\" Spencer",
+	"David \"instantSonic\" Spencer",
 	"\"SSNTails\"",
 	"",
 	"\1Level Design",
@@ -1191,7 +1191,6 @@ static const char *credits[] = {
 	"\"Revan\"",
 	"Anna \"QueenDelta\" Sandlin",
 	"Wessel \"sphere\" Smit",
-	"\"Spazzo\"",
 	"\"SSNTails\"",
 	"Rob Tisdell",
 	"\"Torgo\"",
@@ -1220,7 +1219,7 @@ static const char *credits[] = {
 	"Bill \"Tets\" Reed",
 	"",
 	"\1Special Thanks",
-	"iD Software",
+	"id Software",
 	"Doom Legacy Project",
 	"FreeDoom Project", // Used some of the mancubus and rocket launcher sprites for Brak
 	"Kart Krew",
@@ -1399,7 +1398,7 @@ void F_CreditTicker(void)
 
 boolean F_CreditResponder(event_t *event)
 {
-	INT32 key = event->data1;
+	INT32 key = event->key;
 
 	// remap virtual keys (mouse & joystick buttons)
 	switch (key)
@@ -3423,7 +3422,7 @@ void F_TitleScreenDrawer(void)
 	}
 
 luahook:
-	LUAh_TitleHUD();
+	LUA_HUDHOOK(title);
 }
 
 // separate animation timer for backgrounds, since we also count
@@ -3823,7 +3822,7 @@ void F_ContinueTicker(void)
 
 boolean F_ContinueResponder(event_t *event)
 {
-	INT32 key = event->data1;
+	INT32 key = event->key;
 
 	if (keypressed)
 		return true;
diff --git a/src/filesrch.c b/src/filesrch.c
index cb53d07be958fe1ab7ad42ffac3efc0ee768e44f..ec095518e824d540675750c1b70c56fad9065b96 100644
--- a/src/filesrch.c
+++ b/src/filesrch.c
@@ -13,6 +13,7 @@
 ///	        FS_FOUND
 
 #include <stdio.h>
+#include <errno.h>
 #ifdef __GNUC__
 #include <dirent.h>
 #endif
@@ -29,10 +30,10 @@
 #include "m_misc.h"
 #include "z_zone.h"
 #include "m_menu.h" // Addons_option_Onchange
+#include "w_wad.h"
 
 #if defined (_WIN32) && defined (_MSC_VER)
 
-#include <errno.h>
 #include <io.h>
 #include <tchar.h>
 
@@ -337,8 +338,10 @@ size_t dir_on[menudepth];
 UINT8 refreshdirmenu = 0;
 char *refreshdirname = NULL;
 
-size_t packetsizetally = 0;
-size_t mainwadstally = 0;
+#define dirpathlen 1024
+#define maxdirdepth 48
+
+#define isuptree(dirent) ((dirent)[0]=='.' && ((dirent)[1]=='\0' || ((dirent)[1]=='.' && (dirent)[2]=='\0')))
 
 filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *wantedmd5sum, boolean completepath, int maxsearchdepth)
 {
@@ -387,10 +390,7 @@ filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *want
 			continue;
 		}
 
-		if (dent->d_name[0]=='.' &&
-				(dent->d_name[1]=='\0' ||
-					(dent->d_name[1]=='.' &&
-						dent->d_name[2]=='\0')))
+		if (isuptree(dent->d_name))
 		{
 			// we don't want to scan uptree
 			continue;
@@ -445,6 +445,380 @@ filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *want
 	return retval;
 }
 
+#ifndef AVOID_ERRNO
+int direrror = 0;
+#endif
+
+// Checks if the specified path is a directory.
+// Returns 1 if so, 0 if not, and -1 if an error occurred.
+// direrror is set if there was an error.
+INT32 pathisdirectory(const char *path)
+{
+	struct stat fsstat;
+
+	if (stat(path, &fsstat) < 0)
+	{
+#ifndef AVOID_ERRNO
+		direrror = errno;
+#endif
+		return -1;
+	}
+	else if (S_ISDIR(fsstat.st_mode))
+		return 1;
+
+	return 0;
+}
+
+// Concatenates two paths, and checks if it is a directory that can be opened.
+// Returns 1 if so, 0 if not, and -1 if an error occurred.
+INT32 concatpaths(const char *path, const char *startpath)
+{
+	char dirpath[dirpathlen];
+	DIR *dirhandle;
+	INT32 stat;
+
+	if (startpath)
+	{
+		char basepath[dirpathlen];
+
+		snprintf(basepath, sizeof basepath, "%s" PATHSEP, startpath);
+		snprintf(dirpath, sizeof dirpath, "%s%s", basepath, path);
+
+		// Base path and directory path are the same? Not valid.
+		stat = samepaths(basepath, dirpath);
+
+		if (stat == 1)
+			return 0;
+		else if (stat < 0)
+			return -1;
+	}
+	else
+		snprintf(dirpath, sizeof dirpath, "%s", path);
+
+	// Check if the path is a directory.
+	// Will return -1 if there was an error.
+	stat = pathisdirectory(dirpath);
+	if (stat == 0)
+		return 0;
+	else if (stat < 0)
+	{
+		// The path doesn't exist, so it can't be a directory.
+		if (direrror == ENOENT)
+			return 0;
+
+		return -1;
+	}
+
+	// Open the directory.
+	// Will return 0 if it couldn't be opened.
+	dirhandle = opendir(dirpath);
+	if (dirhandle == NULL)
+		return 0;
+	else
+		closedir(dirhandle);
+
+	return 1;
+}
+
+// Checks if two paths are the same. Returns 1 if so, and 0 if not.
+// Returns -1 if an error occurred with the first path,
+// and returns -2 if an error occurred with the second path.
+// direrror is set if there was an error.
+INT32 samepaths(const char *path1, const char *path2)
+{
+	struct stat stat1;
+	struct stat stat2;
+
+	if (stat(path1, &stat1) < 0)
+	{
+#ifndef AVOID_ERRNO
+		direrror = errno;
+#endif
+		return -1;
+	}
+	if (stat(path2, &stat2) < 0)
+	{
+#ifndef AVOID_ERRNO
+		direrror = errno;
+#endif
+		return -2;
+	}
+
+	if (stat1.st_dev == stat2.st_dev)
+	{
+#if !defined(_WIN32)
+		return (stat1.st_ino == stat2.st_ino);
+#else
+		// The above doesn't work on NTFS or FAT.
+		HANDLE file1 = CreateFileA(path1, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
+		HANDLE file2 = CreateFileA(path2, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
+		BY_HANDLE_FILE_INFORMATION file1info, file2info;
+
+		if (file1 == INVALID_HANDLE_VALUE)
+		{
+#ifndef AVOID_ERRNO
+			direrror = ENOENT;
+#endif
+			return -1;
+		}
+		else if (file2 == INVALID_HANDLE_VALUE)
+		{
+			CloseHandle(file1);
+#ifndef AVOID_ERRNO
+			direrror = ENOENT;
+#endif
+			return -2;
+		}
+
+		// I have no idea why GetFileInformationByHandle would fail.
+		// Microsoft's documentation doesn't tell me.
+		// I'll just use EIO...
+		if (!GetFileInformationByHandle(file1, &file1info))
+		{
+#ifndef AVOID_ERRNO
+			direrror = EIO;
+#endif
+			return -1;
+		}
+		else if (!GetFileInformationByHandle(file2, &file2info))
+		{
+			CloseHandle(file1);
+			CloseHandle(file2);
+#ifndef AVOID_ERRNO
+			direrror = EIO;
+#endif
+			return -2;
+		}
+
+		if (file1info.dwVolumeSerialNumber == file2info.dwVolumeSerialNumber
+		&& file1info.nFileIndexLow == file2info.nFileIndexLow
+		&& file1info.nFileIndexHigh == file2info.nFileIndexHigh)
+		{
+			CloseHandle(file1);
+			CloseHandle(file2);
+			return 1;
+		}
+
+		return 0;
+#endif
+	}
+
+	return 0;
+}
+
+//
+// Directory loading
+//
+
+static void initdirpath(char *dirpath, size_t *dirpathindex, int depthleft)
+{
+	dirpathindex[depthleft] = strlen(dirpath) + 1;
+
+	if (dirpath[dirpathindex[depthleft]-2] != PATHSEP[0])
+	{
+		dirpath[dirpathindex[depthleft]-1] = PATHSEP[0];
+		dirpath[dirpathindex[depthleft]] = 0;
+	}
+	else
+		dirpathindex[depthleft]--;
+}
+
+lumpinfo_t *getdirectoryfiles(const char *path, UINT16 *nlmp, UINT16 *nfolders)
+{
+	DIR **dirhandle;
+	struct dirent *dent;
+	struct stat fsstat;
+
+	int rootdir = (maxdirdepth - 1);
+	int depthleft = rootdir;
+
+	char dirpath[dirpathlen];
+	size_t *dirpathindex;
+
+	lumpinfo_t *lumpinfo, *lump_p;
+	UINT16 i = 0, numlumps = 0;
+
+	boolean failure = false;
+
+	dirhandle = (DIR **)malloc(maxdirdepth * sizeof (DIR*));
+	dirpathindex = (size_t *)malloc(maxdirdepth * sizeof(size_t));
+
+	// Open the root directory
+	strlcpy(dirpath, path, dirpathlen);
+	dirhandle[depthleft] = opendir(dirpath);
+
+	if (dirhandle[depthleft] == NULL)
+	{
+		free(dirhandle);
+		free(dirpathindex);
+		return NULL;
+	}
+
+	initdirpath(dirpath, dirpathindex, depthleft);
+	(*nfolders) = 0;
+
+	// Count files and directories
+	while (depthleft < maxdirdepth)
+	{
+		dirpath[dirpathindex[depthleft]] = 0;
+		dent = readdir(dirhandle[depthleft]);
+
+		if (!dent)
+		{
+			if (depthleft != rootdir) // Don't close the root directory
+				closedir(dirhandle[depthleft]);
+			depthleft++;
+			continue;
+		}
+		else if (isuptree(dent->d_name))
+			continue;
+
+		strcpy(&dirpath[dirpathindex[depthleft]], dent->d_name);
+
+		if (stat(dirpath, &fsstat) < 0)
+			;
+		else if (S_ISDIR(fsstat.st_mode) && depthleft)
+		{
+			dirpathindex[--depthleft] = strlen(dirpath) + 1;
+			dirhandle[depthleft] = opendir(dirpath);
+
+			if (dirhandle[depthleft])
+				(*nfolders)++;
+			else
+				depthleft++;
+
+			dirpath[dirpathindex[depthleft]-1] = '/';
+			dirpath[dirpathindex[depthleft]] = 0;
+		}
+		else
+			numlumps++;
+
+		// Failure: Too many files.
+		if (numlumps == UINT16_MAX)
+		{
+			(*nlmp) = UINT16_MAX;
+			failure = true;
+			break;
+		}
+	}
+
+	// Failure: No files have been found.
+	if (!numlumps)
+	{
+		(*nlmp) = 0;
+		failure = true;
+	}
+
+	// Close any open directories and return if something went wrong.
+	if (failure)
+	{
+		free(dirpathindex);
+		free(dirhandle);
+		for (; depthleft < maxdirdepth; closedir(dirhandle[depthleft++]));
+		return NULL;
+	}
+
+	// Create the files and directories as lump entries
+	// It's possible to create lumps and count files at the same time,
+	// but I didn't want to have to reallocate memory for every lump.
+	rewinddir(dirhandle[rootdir]);
+	depthleft = rootdir;
+
+	strlcpy(dirpath, path, dirpathlen);
+	initdirpath(dirpath, dirpathindex, depthleft);
+
+	lump_p = lumpinfo = Z_Calloc(numlumps * sizeof(lumpinfo_t), PU_STATIC, NULL);
+
+	while (depthleft < maxdirdepth)
+	{
+		char *fullname, *trimname;
+
+		dirpath[dirpathindex[depthleft]] = 0;
+		dent = readdir(dirhandle[depthleft]);
+
+		if (!dent)
+		{
+			closedir(dirhandle[depthleft++]);
+			continue;
+		}
+		else if (isuptree(dent->d_name))
+			continue;
+
+		strcpy(&dirpath[dirpathindex[depthleft]], dent->d_name);
+
+		if (stat(dirpath, &fsstat) < 0)
+			continue;
+		else if (S_ISDIR(fsstat.st_mode) && depthleft)
+		{
+			dirpathindex[--depthleft] = strlen(dirpath) + 1;
+			dirhandle[depthleft] = opendir(dirpath);
+
+			if (dirhandle[depthleft])
+			{
+				dirpath[dirpathindex[depthleft]-1] = '/';
+				dirpath[dirpathindex[depthleft]] = 0;
+			}
+			else
+				depthleft++;
+
+			continue;
+		}
+
+		lump_p->diskpath = Z_StrDup(dirpath); // Path in the filesystem to the file
+		lump_p->compression = CM_NOCOMPRESSION; // Lump is uncompressed
+
+		// Remove the directory's path.
+		fullname = lump_p->diskpath;
+		if (strstr(fullname, path))
+			fullname += strlen(path) + 1;
+
+		// Get the 8-character long lump name.
+		trimname = strrchr(fullname, '/');
+		if (trimname)
+			trimname++;
+		else
+			trimname = fullname;
+
+		if (trimname[0])
+		{
+			char *dotpos = strrchr(trimname, '.');
+			if (dotpos == NULL)
+				dotpos = fullname + strlen(fullname);
+
+			strncpy(lump_p->name, trimname, min(8, dotpos - trimname));
+
+			// The name of the file, without the extension.
+			lump_p->longname = Z_Calloc(dotpos - trimname + 1, PU_STATIC, NULL);
+			strlcpy(lump_p->longname, trimname, dotpos - trimname + 1);
+		}
+		else
+			lump_p->longname = Z_Calloc(1, PU_STATIC, NULL);
+
+		// The complete name of the file, with its extension,
+		// excluding the path of the directory where it resides.
+		lump_p->fullname = Z_StrDup(fullname);
+
+		lump_p++;
+		i++;
+
+		if (i > numlumps || i == (UINT16_MAX-1))
+		{
+			for (; depthleft < maxdirdepth; closedir(dirhandle[depthleft++])); // Close any open directories.
+			break;
+		}
+	}
+
+	free(dirpathindex);
+	free(dirhandle);
+
+	(*nlmp) = numlumps;
+	return lumpinfo;
+}
+
+//
+// Addons menu
+//
+
 char exttable[NUM_EXT_TABLE][7] = { // maximum extension length (currently 4) plus 3 (null terminator, stop, and length including previous two)
 	"\5.txt", "\5.cfg", // exec
 	"\5.wad",
@@ -453,8 +827,7 @@ char exttable[NUM_EXT_TABLE][7] = { // maximum extension length (currently 4) pl
 #endif
 	"\5.pk3", "\5.soc", "\5.lua"}; // addfile
 
-char filenamebuf[MAX_WADFILES][MAX_WADPATH];
-
+static char (*filenamebuf)[MAX_WADPATH];
 
 static boolean filemenucmp(char *haystack, char *needle)
 {
@@ -640,10 +1013,7 @@ boolean preparefilemenu(boolean samedepth)
 
 		if (!dent)
 			break;
-		else if (dent->d_name[0]=='.' &&
-				(dent->d_name[1]=='\0' ||
-					(dent->d_name[1]=='.' &&
-						dent->d_name[2]=='\0')))
+		else if (isuptree(dent->d_name))
 			continue; // we don't want to scan uptree
 
 		strcpy(&menupath[menupathindex[menudepthleft]],dent->d_name);
@@ -704,10 +1074,7 @@ boolean preparefilemenu(boolean samedepth)
 
 		if (!dent)
 			break;
-		else if (dent->d_name[0]=='.' &&
-				(dent->d_name[1]=='\0' ||
-					(dent->d_name[1]=='.' &&
-						dent->d_name[2]=='\0')))
+		else if (isuptree(dent->d_name))
 			continue; // we don't want to scan uptree
 
 		strcpy(&menupath[menupathindex[menudepthleft]],dent->d_name);
@@ -732,6 +1099,10 @@ boolean preparefilemenu(boolean samedepth)
 				if (ext >= EXT_LOADSTART)
 				{
 					size_t i;
+
+					if (filenamebuf == NULL)
+						filenamebuf = calloc(sizeof(char) * MAX_WADPATH, numwadfiles);
+
 					for (i = 0; i < numwadfiles; i++)
 					{
 						if (!filenamebuf[i][0])
@@ -781,6 +1152,12 @@ boolean preparefilemenu(boolean samedepth)
 		}
 	}
 
+	if (filenamebuf)
+	{
+		free(filenamebuf);
+		filenamebuf = NULL;
+	}
+
 	closedir(dirhandle);
 
 	if ((menudepthleft != menudepth-1) // now for UP... entry
diff --git a/src/filesrch.h b/src/filesrch.h
index dfea8979e9c19d3a5a733e8303636762c9027d04..59ef5269b194f0918a14927a6fc1eaf003e1a40b 100644
--- a/src/filesrch.h
+++ b/src/filesrch.h
@@ -7,6 +7,7 @@
 #include "doomdef.h"
 #include "d_netfil.h"
 #include "m_menu.h" // MAXSTRINGLENGTH
+#include "w_wad.h"
 
 extern consvar_t cv_addons_option, cv_addons_folder, cv_addons_md5, cv_addons_showall, cv_addons_search_case, cv_addons_search_type;
 
@@ -28,6 +29,16 @@ extern consvar_t cv_addons_option, cv_addons_folder, cv_addons_md5, cv_addons_sh
 filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *wantedmd5sum,
 	boolean completepath, int maxsearchdepth);
 
+INT32 pathisdirectory(const char *path);
+INT32 samepaths(const char *path1, const char *path2);
+INT32 concatpaths(const char *path, const char *startpath);
+
+#ifndef AVOID_ERRNO
+extern int direrror;
+#endif
+
+lumpinfo_t *getdirectoryfiles(const char *path, UINT16 *nlmp, UINT16 *nfolders);
+
 #define menudepth 20
 
 extern char menupath[1024];
@@ -42,9 +53,6 @@ extern size_t dir_on[menudepth];
 extern UINT8 refreshdirmenu;
 extern char *refreshdirname;
 
-extern size_t packetsizetally;
-extern size_t mainwadstally;
-
 typedef enum
 {
 	EXT_FOLDER = 0,
@@ -94,5 +102,4 @@ typedef enum
 void closefilemenu(boolean validsize);
 void searchfilemenu(char *tempname);
 boolean preparefilemenu(boolean samedepth);
-
 #endif // __FILESRCH_H__
diff --git a/src/g_demo.c b/src/g_demo.c
index 701f930e511a8234aa00ca82ca3b055085fabbb9..c97dbcf9ee0559e23e6635e4a62f2de1cb843af2 100644
--- a/src/g_demo.c
+++ b/src/g_demo.c
@@ -1680,6 +1680,7 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
 	switch(oldversion) // demoversion
 	{
 	case DEMOVERSION: // latest always supported
+	case 0x000d: // The previous demoversion also supported
 	case 0x000c: // all that changed between then and now was longer color name
 		break;
 	// too old, cannot support.
@@ -2023,7 +2024,7 @@ void G_AddGhost(char *defdemoname)
 	char name[17],skin[17],color[MAXCOLORNAME+1],*n,*pdemoname,md5[16];
 	UINT8 cnamelen;
 	demoghost *gh;
-	UINT8 flags;
+	UINT8 flags, subversion;
 	UINT8 *buffer,*p;
 	mapthing_t *mthing;
 	UINT16 count, ghostversion;
@@ -2071,7 +2072,7 @@ void G_AddGhost(char *defdemoname)
 		return;
 	} p += 12; // DEMOHEADER
 	p++; // VERSION
-	p++; // SUBVERSION
+	subversion = READUINT8(p); // SUBVERSION
 	ghostversion = READUINT16(p);
 	switch(ghostversion)
 	{
@@ -2170,9 +2171,19 @@ void G_AddGhost(char *defdemoname)
 	count = READUINT16(p);
 	while (count--)
 	{
-		SKIPSTRING(p);
-		SKIPSTRING(p);
-		p++;
+		// In 2.2.7 netvar saving was updated
+		if (subversion < 7)
+		{
+			p += 2;
+			SKIPSTRING(p);
+			p++;
+		}
+		else
+		{
+			SKIPSTRING(p);
+			SKIPSTRING(p);
+			p++;
+		}
 	}
 
 	if (*p == DEMOMARKER)
@@ -2422,12 +2433,13 @@ ATTRNORETURN void FUNCNORETURN G_StopMetalRecording(boolean kill)
 	{
 		WRITEUINT8(demo_p, (kill) ? METALDEATH : DEMOMARKER); // add the demo end (or metal death) marker
 		WriteDemoChecksum();
-		saved = FIL_WriteFile(va("%sMS.LMP", G_BuildMapName(gamemap)), demobuffer, demo_p - demobuffer); // finally output the file.
+		sprintf(demoname, "%sMS.LMP", G_BuildMapName(gamemap));
+		saved = FIL_WriteFile(va(pandf, srb2home, demoname), demobuffer, demo_p - demobuffer); // finally output the file.
 	}
 	free(demobuffer);
 	metalrecording = false;
 	if (saved)
-		I_Error("Saved to %sMS.LMP", G_BuildMapName(gamemap));
+		I_Error("Saved to %s", demoname);
 	I_Error("Failed to save demo!");
 }
 
diff --git a/src/g_game.c b/src/g_game.c
index 13fdab8777ab277df306a332a56fb99d8d189a6b..ce2aa41f52da22da2e0d3d856426c9d7ce6fdf1c 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -45,6 +45,7 @@
 #include "lua_hook.h"
 #include "b_bot.h"
 #include "m_cond.h" // condition sets
+#include "lua_script.h"
 
 #include "lua_hud.h"
 
@@ -1071,7 +1072,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	boolean turnleft, turnright, strafelkey, straferkey, movefkey, movebkey, mouseaiming, analogjoystickmove, gamepadjoystickmove, thisjoyaiming;
 	boolean strafeisturn; // Simple controls only
 	player_t *player = &players[ssplayer == 2 ? secondarydisplayplayer : consoleplayer];
-	camera_t *thiscam = ((ssplayer == 1 || player->bot == 2) ? &camera : &camera2);
+	camera_t *thiscam = ((ssplayer == 1 || player->bot == BOT_2PHUMAN) ? &camera : &camera2);
 	angle_t *myangle = (ssplayer == 1 ? &localangle : &localangle2);
 	INT32 *myaiming = (ssplayer == 1 ? &localaiming : &localaiming2);
 
@@ -1139,13 +1140,13 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		return;
 	}
 
-	turnright = PLAYERINPUTDOWN(ssplayer, gc_turnright);
-	turnleft = PLAYERINPUTDOWN(ssplayer, gc_turnleft);
+	turnright = PLAYERINPUTDOWN(ssplayer, GC_TURNRIGHT);
+	turnleft = PLAYERINPUTDOWN(ssplayer, GC_TURNLEFT);
 
-	straferkey = PLAYERINPUTDOWN(ssplayer, gc_straferight);
-	strafelkey = PLAYERINPUTDOWN(ssplayer, gc_strafeleft);
-	movefkey = PLAYERINPUTDOWN(ssplayer, gc_forward);
-	movebkey = PLAYERINPUTDOWN(ssplayer, gc_backward);
+	straferkey = PLAYERINPUTDOWN(ssplayer, GC_STRAFERIGHT);
+	strafelkey = PLAYERINPUTDOWN(ssplayer, GC_STRAFELEFT);
+	movefkey = PLAYERINPUTDOWN(ssplayer, GC_FORWARD);
+	movebkey = PLAYERINPUTDOWN(ssplayer, GC_BACKWARD);
 
 	if (strafeisturn)
 	{
@@ -1154,7 +1155,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		straferkey = strafelkey = false;
 	}
 
-	mouseaiming = (PLAYERINPUTDOWN(ssplayer, gc_mouseaiming)) ^
+	mouseaiming = (PLAYERINPUTDOWN(ssplayer, GC_MOUSEAIMING)) ^
 		((chasecam && !player->spectator) ? chasefreelook : alwaysfreelook);
 	analogjoystickmove = usejoystick && !Joystick.bGamepadStyle;
 	gamepadjoystickmove = usejoystick && Joystick.bGamepadStyle;
@@ -1270,11 +1271,11 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	// forward with key or button
 	if (movefkey || (gamepadjoystickmove && movejoystickvector.yaxis < 0)
 		|| ((player->powers[pw_carry] == CR_NIGHTSMODE)
-			&& (PLAYERINPUTDOWN(ssplayer, gc_lookup) || (gamepadjoystickmove && lookjoystickvector.yaxis > 0))))
+			&& (PLAYERINPUTDOWN(ssplayer, GC_LOOKUP) || (gamepadjoystickmove && lookjoystickvector.yaxis > 0))))
 		forward = forwardmove[speed];
 	if (movebkey || (gamepadjoystickmove && movejoystickvector.yaxis > 0)
 		|| ((player->powers[pw_carry] == CR_NIGHTSMODE)
-			&& (PLAYERINPUTDOWN(ssplayer, gc_lookdown) || (gamepadjoystickmove && lookjoystickvector.yaxis < 0))))
+			&& (PLAYERINPUTDOWN(ssplayer, GC_LOOKDOWN) || (gamepadjoystickmove && lookjoystickvector.yaxis < 0))))
 		forward -= forwardmove[speed];
 
 	if (analogjoystickmove && movejoystickvector.yaxis != 0)
@@ -1287,9 +1288,9 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	if (strafelkey)
 		side -= sidemove[speed];
 
-	if (PLAYERINPUTDOWN(ssplayer, gc_weaponnext))
+	if (PLAYERINPUTDOWN(ssplayer, GC_WEAPONNEXT))
 		cmd->buttons |= BT_WEAPONNEXT; // Next Weapon
-	if (PLAYERINPUTDOWN(ssplayer, gc_weaponprev))
+	if (PLAYERINPUTDOWN(ssplayer, GC_WEAPONPREV))
 		cmd->buttons |= BT_WEAPONPREV; // Previous Weapon
 
 #if NUM_WEAPONS > 10
@@ -1298,7 +1299,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	//use the four avaliable bits to determine the weapon.
 	cmd->buttons &= ~BT_WEAPONMASK;
 	for (i = 0; i < NUM_WEAPONS; ++i)
-		if (PLAYERINPUTDOWN(ssplayer, gc_wepslot1 + i))
+		if (PLAYERINPUTDOWN(ssplayer, GC_WEPSLOT1 + i))
 		{
 			cmd->buttons |= (UINT16)(i + 1);
 			break;
@@ -1306,34 +1307,34 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 
 	// fire with any button/key
 	axis = PlayerJoyAxis(ssplayer, JA_FIRE);
-	if (PLAYERINPUTDOWN(ssplayer, gc_fire) || (usejoystick && axis > 0))
+	if (PLAYERINPUTDOWN(ssplayer, GC_FIRE) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_ATTACK;
 
 	// fire normal with any button/key
 	axis = PlayerJoyAxis(ssplayer, JA_FIRENORMAL);
-	if (PLAYERINPUTDOWN(ssplayer, gc_firenormal) || (usejoystick && axis > 0))
+	if (PLAYERINPUTDOWN(ssplayer, GC_FIRENORMAL) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_FIRENORMAL;
 
-	if (PLAYERINPUTDOWN(ssplayer, gc_tossflag))
+	if (PLAYERINPUTDOWN(ssplayer, GC_TOSSFLAG))
 		cmd->buttons |= BT_TOSSFLAG;
 
 	// Lua scriptable buttons
-	if (PLAYERINPUTDOWN(ssplayer, gc_custom1))
+	if (PLAYERINPUTDOWN(ssplayer, GC_CUSTOM1))
 		cmd->buttons |= BT_CUSTOM1;
-	if (PLAYERINPUTDOWN(ssplayer, gc_custom2))
+	if (PLAYERINPUTDOWN(ssplayer, GC_CUSTOM2))
 		cmd->buttons |= BT_CUSTOM2;
-	if (PLAYERINPUTDOWN(ssplayer, gc_custom3))
+	if (PLAYERINPUTDOWN(ssplayer, GC_CUSTOM3))
 		cmd->buttons |= BT_CUSTOM3;
 
 	// use with any button/key
 	axis = PlayerJoyAxis(ssplayer, JA_SPIN);
-	if (PLAYERINPUTDOWN(ssplayer, gc_spin) || (usejoystick && axis > 0))
+	if (PLAYERINPUTDOWN(ssplayer, GC_SPIN) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_SPIN;
 
 	// Centerview can be a toggle in simple mode!
 	{
 		static boolean last_centerviewdown[2], centerviewhold[2]; // detect taps for toggle behavior
-		boolean down = PLAYERINPUTDOWN(ssplayer, gc_centerview);
+		boolean down = PLAYERINPUTDOWN(ssplayer, GC_CENTERVIEW);
 
 		if (!(controlstyle == CS_SIMPLE && cv_cam_centertoggle[forplayer].value))
 			centerviewdown = down;
@@ -1432,7 +1433,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	if (ticcmd_centerviewdown[forplayer] && controlstyle == CS_SIMPLE)
 		controlstyle = CS_LEGACY;
 
-	if (PLAYERINPUTDOWN(ssplayer, gc_camreset))
+	if (PLAYERINPUTDOWN(ssplayer, GC_CAMRESET))
 	{
 		if (thiscam->chase && !resetdown[forplayer])
 			P_ResetCamera(&players[ssplayer == 1 ? displayplayer : secondarydisplayplayer], thiscam);
@@ -1445,7 +1446,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 
 	// jump button
 	axis = PlayerJoyAxis(ssplayer, JA_JUMP);
-	if (PLAYERINPUTDOWN(ssplayer, gc_jump) || (usejoystick && axis > 0))
+	if (PLAYERINPUTDOWN(ssplayer, GC_JUMP) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_JUMP;
 
 	// player aiming shit, ahhhh...
@@ -1475,12 +1476,12 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 
 		if (!(player->powers[pw_carry] == CR_NIGHTSMODE))
 		{
-			if (PLAYERINPUTDOWN(ssplayer, gc_lookup) || (gamepadjoystickmove && lookjoystickvector.yaxis < 0))
+			if (PLAYERINPUTDOWN(ssplayer, GC_LOOKUP) || (gamepadjoystickmove && lookjoystickvector.yaxis < 0))
 			{
 				*myaiming += KB_LOOKSPEED * screen_invert;
 				keyboard_look[forplayer] = true;
 			}
-			else if (PLAYERINPUTDOWN(ssplayer, gc_lookdown) || (gamepadjoystickmove && lookjoystickvector.yaxis > 0))
+			else if (PLAYERINPUTDOWN(ssplayer, GC_LOOKDOWN) || (gamepadjoystickmove && lookjoystickvector.yaxis > 0))
 			{
 				*myaiming -= KB_LOOKSPEED * screen_invert;
 				keyboard_look[forplayer] = true;
@@ -1545,23 +1546,13 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	cmd->forwardmove = (SINT8)(cmd->forwardmove + forward);
 	cmd->sidemove = (SINT8)(cmd->sidemove + side);
 
-	if (player->bot == 1) { // Tailsbot for P2
-		if (!player->powers[pw_tailsfly] && (cmd->forwardmove || cmd->sidemove || cmd->buttons))
-		{
-			player->bot = 2; // A player-controlled bot. Returns to AI when it respawns.
-			CV_SetValue(&cv_analog[1], true);
-		}
-		else
-		{
-			G_CopyTiccmd(cmd,  I_BaseTiccmd2(), 1); // empty, or external driver
-			B_BuildTiccmd(player, cmd);
-		}
-		B_HandleFlightIndicator(player);
-	}
-	else if (player->bot == 2)
-		// Fix offset angle for P2-controlled Tailsbot when P2's controls are set to non-Legacy
+	// Note: Majority of botstuffs are handled in G_Ticker now.
+	if (player->bot == BOT_2PHUMAN) //Player-controlled bot
+	{
+		// Fix offset angle for P2-controlled Tailsbot when P2's controls are set to non-Strafe
 		cmd->angleturn = (INT16)((localangle - *myangle) >> 16);
-
+	}	
+	
 	*myangle += (cmd->angleturn<<16);
 
 	if (controlstyle == CS_LMAOGALOG) {
@@ -1968,7 +1959,7 @@ boolean G_Responder(event_t *ev)
 	if (gameaction == ga_nothing && !singledemo &&
 		((demoplayback && !modeattacking && !titledemo) || gamestate == GS_TITLESCREEN))
 	{
-		if (ev->type == ev_keydown && ev->data1 != 301 && !(gamestate == GS_TITLESCREEN && finalecount < TICRATE))
+		if (ev->type == ev_keydown && ev->key != 301 && !(gamestate == GS_TITLESCREEN && finalecount < TICRATE))
 		{
 			M_StartControlPanel();
 			return true;
@@ -2044,7 +2035,7 @@ boolean G_Responder(event_t *ev)
 
 	// allow spy mode changes even during the demo
 	if (gamestate == GS_LEVEL && ev->type == ev_keydown
-		&& (ev->data1 == KEY_F12 || ev->data1 == gamecontrol[gc_viewpoint][0] || ev->data1 == gamecontrol[gc_viewpoint][1]))
+		&& (ev->key == KEY_F12 || ev->key == gamecontrol[GC_VIEWPOINT][0] || ev->key == gamecontrol[GC_VIEWPOINT][1]))
 	{
 		// ViewpointSwitch Lua hook.
 		UINT8 canSwitchView = 0;
@@ -2117,13 +2108,13 @@ boolean G_Responder(event_t *ev)
 	switch (ev->type)
 	{
 		case ev_keydown:
-			if (ev->data1 == gamecontrol[gc_pause][0]
-				|| ev->data1 == gamecontrol[gc_pause][1]
-				|| ev->data1 == KEY_PAUSE)
+			if (ev->key == gamecontrol[GC_PAUSE][0]
+				|| ev->key == gamecontrol[GC_PAUSE][1]
+				|| ev->key == KEY_PAUSE)
 			{
 				if (modeattacking && !demoplayback && (gamestate == GS_LEVEL))
 				{
-					pausebreakkey = (ev->data1 == KEY_PAUSE);
+					pausebreakkey = (ev->key == KEY_PAUSE);
 					if (menuactive || pausedelay < 0 || leveltime < 2)
 						return true;
 
@@ -2148,8 +2139,8 @@ boolean G_Responder(event_t *ev)
 					}
 				}
 			}
-			if (ev->data1 == gamecontrol[gc_camtoggle][0]
-				|| ev->data1 == gamecontrol[gc_camtoggle][1])
+			if (ev->key == gamecontrol[GC_CAMTOGGLE][0]
+				|| ev->key == gamecontrol[GC_CAMTOGGLE][1])
 			{
 				if (!camtoggledelay)
 				{
@@ -2157,8 +2148,8 @@ boolean G_Responder(event_t *ev)
 					CV_SetValue(&cv_chasecam, cv_chasecam.value ? 0 : 1);
 				}
 			}
-			if (ev->data1 == gamecontrolbis[gc_camtoggle][0]
-				|| ev->data1 == gamecontrolbis[gc_camtoggle][1])
+			if (ev->key == gamecontrolbis[GC_CAMTOGGLE][0]
+				|| ev->key == gamecontrolbis[GC_CAMTOGGLE][1])
 			{
 				if (!camtoggledelay2)
 				{
@@ -2194,8 +2185,20 @@ boolean G_Responder(event_t *ev)
 //
 boolean G_LuaResponder(event_t *ev)
 {
-	return (ev->type == ev_keydown && LUA_HookKey(ev->data1, HOOK(KeyDown))) ||
-		(ev->type == ev_keyup && LUA_HookKey(ev->data1, HOOK(KeyUp)));
+	boolean cancelled = false;
+
+	if (ev->type == ev_keydown)
+	{
+		cancelled = LUA_HookKey(ev, HOOK(KeyDown));
+		LUA_InvalidateUserdata(ev);
+	}
+	else if (ev->type == ev_keyup)
+	{
+		cancelled = LUA_HookKey(ev, HOOK(KeyUp));
+		LUA_InvalidateUserdata(ev);
+	}
+
+	return cancelled;
 }
 
 //
@@ -2207,6 +2210,23 @@ void G_Ticker(boolean run)
 	UINT32 i;
 	INT32 buf;
 
+	// Bot players queued for removal
+	for (i = MAXPLAYERS-1; i != UINT32_MAX; i--)
+	{
+		if (playeringame[i] && players[i].removing)
+		{
+			CL_RemovePlayer(i, i);
+			if (netgame)
+			{
+				char kickmsg[256];
+
+				strcpy(kickmsg, M_GetText("\x82*Bot %s has been removed"));
+				strcpy(kickmsg, va(kickmsg, player_names[i], i));
+				HU_AddChatText(kickmsg, false);
+			}
+		}
+	}
+
 	// see also SCR_DisplayMarathonInfo
 	if ((marathonmode & (MA_INIT|MA_INGAME)) == MA_INGAME && gamestate == GS_LEVEL)
 		marathontime++;
@@ -2292,23 +2312,58 @@ void G_Ticker(boolean run)
 		if (playeringame[i])
 		{
 			INT16 received;
+			// Save last frame's button readings
+			players[i].lastbuttons = players[i].cmd.buttons;
 
 			G_CopyTiccmd(&players[i].cmd, &netcmds[buf][i], 1);
-
-			received = (players[i].cmd.angleturn & TICCMD_RECEIVED);
-
-			players[i].angleturn += players[i].cmd.angleturn - players[i].oldrelangleturn;
-			players[i].oldrelangleturn = players[i].cmd.angleturn;
-			if (P_ControlStyle(&players[i]) == CS_LMAOGALOG)
-				P_ForceLocalAngle(&players[i], players[i].angleturn << 16);
-			else
-				players[i].cmd.angleturn = players[i].angleturn;
-
-			players[i].cmd.angleturn &= ~TICCMD_RECEIVED;
+			// Bot ticcmd handling
+			// Yes, ordinarily this would be handled in G_BuildTiccmd...
+			// ...however, bot players won't have a corresponding consoleplayer or splitscreen player 2 to send that information.
+			// Therefore, this has to be done after ticcmd sends are received.
+			if (players[i].bot == BOT_2PAI) { // Tailsbot for P2
+				if (!players[i].powers[pw_tailsfly] && (players[i].cmd.forwardmove || players[i].cmd.sidemove || players[i].cmd.buttons))
+				{
+					players[i].bot = BOT_2PHUMAN; // A player-controlled bot. Returns to AI when it respawns.
+					CV_SetValue(&cv_analog[1], true);
+				}
+				else
+				{
+					B_BuildTiccmd(&players[i], &players[i].cmd);
+				}
+				B_HandleFlightIndicator(&players[i]);
+			}
+			else if (players[i].bot == BOT_MPAI) {
+				B_BuildTiccmd(&players[i], &players[i].cmd);
+			}
+			
+			// Do angle adjustments.
+			if (players[i].bot == BOT_NONE || players[i].bot == BOT_2PHUMAN)
+			{
+				received = (players[i].cmd.angleturn & TICCMD_RECEIVED);
+				players[i].angleturn += players[i].cmd.angleturn - players[i].oldrelangleturn;
+				players[i].oldrelangleturn = players[i].cmd.angleturn;
+				if (P_ControlStyle(&players[i]) == CS_LMAOGALOG)
+					P_ForceLocalAngle(&players[i], players[i].angleturn << 16);
+				else
+					players[i].cmd.angleturn = players[i].angleturn;
+    			if (P_ControlStyle(&players[i]) == CS_LMAOGALOG)
+    				P_ForceLocalAngle(&players[i], players[i].angleturn << 16);
+    			else
+    				players[i].cmd.angleturn = players[i].angleturn;
+    
+    			players[i].cmd.angleturn &= ~TICCMD_RECEIVED;
+				// Use the leveltime sent in the player's ticcmd to determine control lag
+    			players[i].cmd.latency = min(((leveltime & 0xFF) - players[i].cmd.latency) & 0xFF, MAXPREDICTTICS-1);
+			}
+			else // Less work is required if we're building a bot ticcmd.
+			{
+    			// Since bot TicCmd is pre-determined for both the client and server, the latency and packet checks are simplified.
+    			received = 1;
+    			players[i].cmd.latency = 0;
+				players[i].angleturn = players[i].cmd.angleturn;
+				players[i].oldrelangleturn = players[i].cmd.angleturn;
+			}
 			players[i].cmd.angleturn |= received;
-
-			// Use the leveltime sent in the player's ticcmd to determine control lag
-			players[i].cmd.latency = min(((leveltime & 0xFF) - players[i].cmd.latency) & 0xFF, MAXPREDICTTICS-1);
 		}
 	}
 
@@ -2494,6 +2549,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	tic_t quittime;
 	boolean spectator;
 	boolean outofcoop;
+	boolean removing;
 	INT16 bot;
 	SINT8 pity;
 	INT16 rings;
@@ -2510,6 +2566,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	quittime = players[player].quittime;
 	spectator = players[player].spectator;
 	outofcoop = players[player].outofcoop;
+	removing = players[player].removing;
 	pflags = (players[player].pflags & (PF_FLIPCAM|PF_ANALOGMODE|PF_DIRECTIONCHAR|PF_AUTOBRAKE|PF_TAGIT|PF_GAMETYPEOVER));
 	playerangleturn = players[player].angleturn;
 	oldrelangleturn = players[player].oldrelangleturn;
@@ -2586,6 +2643,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	p->quittime = quittime;
 	p->spectator = spectator;
 	p->outofcoop = outofcoop;
+	p->removing = removing;
 	p->angleturn = playerangleturn;
 	p->oldrelangleturn = oldrelangleturn;
 
@@ -2630,8 +2688,10 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	p->totalring = totalring;
 
 	p->mare = mare;
-	if (bot)
-		p->bot = 1; // reset to AI-controlled
+	if (bot == BOT_2PHUMAN)
+		p->bot = BOT_2PAI; // reset to AI-controlled
+	else
+		p->bot = bot;
 	p->pity = pity;
 	p->rings = rings;
 	p->spheres = spheres;
@@ -2977,7 +3037,8 @@ void G_DoReborn(INT32 playernum)
 	// Make sure objectplace is OFF when you first start the level!
 	OP_ResetObjectplace();
 
-	if (player->bot && playernum != consoleplayer)
+	// Tailsbot
+	if (player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN)
 	{ // Bots respawn next to their master.
 		mobj_t *oldmo = NULL;
 
@@ -2995,6 +3056,28 @@ void G_DoReborn(INT32 playernum)
 
 		return;
 	}
+	
+	// Additional players (e.g. independent bots) in Single Player
+	if (playernum != consoleplayer && !(netgame || multiplayer)) 
+	{		
+		mobj_t *oldmo = NULL;
+		// Do nothing if out of lives
+		if (player->lives <= 0)
+			return;
+		
+		// Otherwise do respawn, starting by removing the player object
+		if (player->mo)
+		{
+			oldmo = player->mo;
+			P_RemoveMobj(player->mo);
+		}
+		// Do spawning
+		G_SpawnPlayer(playernum);
+		if (oldmo)
+			G_ChangePlayerReferences(oldmo, players[playernum].mo);
+		
+		return; //Exit function to avoid proccing other SP related mechanics
+	}
 
 	if (countdowntimeup || (!(netgame || multiplayer) && (gametyperules & GTR_CAMPAIGN)))
 		resetlevel = true;
@@ -3176,7 +3259,7 @@ void G_AddPlayer(INT32 playernum)
 			if (!playeringame[i])
 				continue;
 
-			if (players[i].bot) // ignore dumb, stupid tails
+			if (players[i].bot == BOT_2PAI || players[i].bot == BOT_2PHUMAN) // ignore dumb, stupid tails
 				continue;
 
 			countplayers++;
@@ -3217,7 +3300,7 @@ boolean G_EnoughPlayersFinished(void)
 
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
-		if (!playeringame[i] || players[i].spectator || players[i].bot)
+		if (!playeringame[i] || players[i].spectator || players[i].bot == BOT_2PAI || players[i].bot == BOT_2PHUMAN)
 			continue;
 		if (players[i].quittime > 30 * TICRATE)
 			continue;
@@ -5240,4 +5323,3 @@ INT32 G_TicsToMilliseconds(tic_t tics)
 {
 	return (INT32)((tics%TICRATE) * (1000.00f/TICRATE));
 }
-
diff --git a/src/g_input.c b/src/g_input.c
index 2f7980c647d1eba7b794274200e9f9a11477c8b3..6383c3f0068a3c47007f6fc50422f46b9e4bbb2c 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -41,49 +41,49 @@ INT32 joyxmove[JOYAXISSET], joyymove[JOYAXISSET], joy2xmove[JOYAXISSET], joy2ymo
 UINT8 gamekeydown[NUMINPUTS];
 
 // two key codes (or virtual key) per game control
-INT32 gamecontrol[num_gamecontrols][2];
-INT32 gamecontrolbis[num_gamecontrols][2]; // secondary splitscreen player
-INT32 gamecontroldefault[num_gamecontrolschemes][num_gamecontrols][2]; // default control storage, use 0 (gcs_custom) for memory retention
-INT32 gamecontrolbisdefault[num_gamecontrolschemes][num_gamecontrols][2];
+INT32 gamecontrol[NUM_GAMECONTROLS][2];
+INT32 gamecontrolbis[NUM_GAMECONTROLS][2]; // secondary splitscreen player
+INT32 gamecontroldefault[num_gamecontrolschemes][NUM_GAMECONTROLS][2]; // default control storage, use 0 (gcs_custom) for memory retention
+INT32 gamecontrolbisdefault[num_gamecontrolschemes][NUM_GAMECONTROLS][2];
 
 // lists of GC codes for selective operation
 const INT32 gcl_tutorial_check[num_gcl_tutorial_check] = {
-	gc_forward, gc_backward, gc_strafeleft, gc_straferight,
-	gc_turnleft, gc_turnright
+	GC_FORWARD, GC_BACKWARD, GC_STRAFELEFT, GC_STRAFERIGHT,
+	GC_TURNLEFT, GC_TURNRIGHT
 };
 
 const INT32 gcl_tutorial_used[num_gcl_tutorial_used] = {
-	gc_forward, gc_backward, gc_strafeleft, gc_straferight,
-	gc_turnleft, gc_turnright,
-	gc_jump, gc_spin
+	GC_FORWARD, GC_BACKWARD, GC_STRAFELEFT, GC_STRAFERIGHT,
+	GC_TURNLEFT, GC_TURNRIGHT,
+	GC_JUMP, GC_SPIN
 };
 
 const INT32 gcl_tutorial_full[num_gcl_tutorial_full] = {
-	gc_forward, gc_backward, gc_strafeleft, gc_straferight,
-	gc_lookup, gc_lookdown, gc_turnleft, gc_turnright, gc_centerview,
-	gc_jump, gc_spin,
-	gc_fire, gc_firenormal
+	GC_FORWARD, GC_BACKWARD, GC_STRAFELEFT, GC_STRAFERIGHT,
+	GC_LOOKUP, GC_LOOKDOWN, GC_TURNLEFT, GC_TURNRIGHT, GC_CENTERVIEW,
+	GC_JUMP, GC_SPIN,
+	GC_FIRE, GC_FIRENORMAL
 };
 
 const INT32 gcl_movement[num_gcl_movement] = {
-	gc_forward, gc_backward, gc_strafeleft, gc_straferight
+	GC_FORWARD, GC_BACKWARD, GC_STRAFELEFT, GC_STRAFERIGHT
 };
 
 const INT32 gcl_camera[num_gcl_camera] = {
-	gc_turnleft, gc_turnright
+	GC_TURNLEFT, GC_TURNRIGHT
 };
 
 const INT32 gcl_movement_camera[num_gcl_movement_camera] = {
-	gc_forward, gc_backward, gc_strafeleft, gc_straferight,
-	gc_turnleft, gc_turnright
+	GC_FORWARD, GC_BACKWARD, GC_STRAFELEFT, GC_STRAFERIGHT,
+	GC_TURNLEFT, GC_TURNRIGHT
 };
 
-const INT32 gcl_jump[num_gcl_jump] = { gc_jump };
+const INT32 gcl_jump[num_gcl_jump] = { GC_JUMP };
 
-const INT32 gcl_spin[num_gcl_spin] = { gc_spin };
+const INT32 gcl_spin[num_gcl_spin] = { GC_SPIN };
 
 const INT32 gcl_jump_spin[num_gcl_jump_spin] = {
-	gc_jump, gc_spin
+	GC_JUMP, GC_SPIN
 };
 
 typedef struct
@@ -115,54 +115,54 @@ void G_MapEventsToControls(event_t *ev)
 	switch (ev->type)
 	{
 		case ev_keydown:
-			if (ev->data1 < NUMINPUTS)
-				gamekeydown[ev->data1] = 1;
+			if (ev->key < NUMINPUTS)
+				gamekeydown[ev->key] = 1;
 #ifdef PARANOIA
 			else
 			{
-				CONS_Debug(DBG_GAMELOGIC, "Bad downkey input %d\n",ev->data1);
+				CONS_Debug(DBG_GAMELOGIC, "Bad downkey input %d\n",ev->key);
 			}
 
 #endif
 			break;
 
 		case ev_keyup:
-			if (ev->data1 < NUMINPUTS)
-				gamekeydown[ev->data1] = 0;
+			if (ev->key < NUMINPUTS)
+				gamekeydown[ev->key] = 0;
 #ifdef PARANOIA
 			else
 			{
-				CONS_Debug(DBG_GAMELOGIC, "Bad upkey input %d\n",ev->data1);
+				CONS_Debug(DBG_GAMELOGIC, "Bad upkey input %d\n",ev->key);
 			}
 #endif
 			break;
 
 		case ev_mouse: // buttons are virtual keys
-			mouse.rdx = ev->data2;
-			mouse.rdy = ev->data3;
+			mouse.rdx = ev->x;
+			mouse.rdy = ev->y;
 			break;
 
 		case ev_joystick: // buttons are virtual keys
-			i = ev->data1;
+			i = ev->key;
 			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on)
 				break;
-			if (ev->data2 != INT32_MAX) joyxmove[i] = ev->data2;
-			if (ev->data3 != INT32_MAX) joyymove[i] = ev->data3;
+			if (ev->x != INT32_MAX) joyxmove[i] = ev->x;
+			if (ev->y != INT32_MAX) joyymove[i] = ev->y;
 			break;
 
 		case ev_joystick2: // buttons are virtual keys
-			i = ev->data1;
+			i = ev->key;
 			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on)
 				break;
-			if (ev->data2 != INT32_MAX) joy2xmove[i] = ev->data2;
-			if (ev->data3 != INT32_MAX) joy2ymove[i] = ev->data3;
+			if (ev->x != INT32_MAX) joy2xmove[i] = ev->x;
+			if (ev->y != INT32_MAX) joy2ymove[i] = ev->y;
 			break;
 
 		case ev_mouse2: // buttons are virtual keys
 			if (menuactive || CON_Ready() || chat_on)
 				break;
-			mouse2.rdx = ev->data2;
-			mouse2.rdy = ev->data3;
+			mouse2.rdx = ev->x;
+			mouse2.rdy = ev->y;
 			break;
 
 		default:
@@ -233,329 +233,329 @@ typedef struct
 
 static keyname_t keynames[] =
 {
-	{KEY_SPACE, "SPACE"},
-	{KEY_CAPSLOCK, "CAPS LOCK"},
-	{KEY_ENTER, "ENTER"},
-	{KEY_TAB, "TAB"},
-	{KEY_ESCAPE, "ESCAPE"},
-	{KEY_BACKSPACE, "BACKSPACE"},
+	{KEY_SPACE, "space"},
+	{KEY_CAPSLOCK, "caps lock"},
+	{KEY_ENTER, "enter"},
+	{KEY_TAB, "tab"},
+	{KEY_ESCAPE, "escape"},
+	{KEY_BACKSPACE, "backspace"},
 
-	{KEY_NUMLOCK, "NUMLOCK"},
-	{KEY_SCROLLLOCK, "SCROLLLOCK"},
+	{KEY_NUMLOCK, "numlock"},
+	{KEY_SCROLLLOCK, "scrolllock"},
 
 	// bill gates keys
-	{KEY_LEFTWIN, "LEFTWIN"},
-	{KEY_RIGHTWIN, "RIGHTWIN"},
-	{KEY_MENU, "MENU"},
-
-	{KEY_LSHIFT, "LSHIFT"},
-	{KEY_RSHIFT, "RSHIFT"},
-	{KEY_LSHIFT, "SHIFT"},
-	{KEY_LCTRL, "LCTRL"},
-	{KEY_RCTRL, "RCTRL"},
-	{KEY_LCTRL, "CTRL"},
-	{KEY_LALT, "LALT"},
-	{KEY_RALT, "RALT"},
-	{KEY_LALT, "ALT"},
+	{KEY_LEFTWIN, "leftwin"},
+	{KEY_RIGHTWIN, "rightwin"},
+	{KEY_MENU, "menu"},
+
+	{KEY_LSHIFT, "lshift"},
+	{KEY_RSHIFT, "rshift"},
+	{KEY_LSHIFT, "shift"},
+	{KEY_LCTRL, "lctrl"},
+	{KEY_RCTRL, "rctrl"},
+	{KEY_LCTRL, "ctrl"},
+	{KEY_LALT, "lalt"},
+	{KEY_RALT, "ralt"},
+	{KEY_LALT, "alt"},
 
 	// keypad keys
-	{KEY_KPADSLASH, "KEYPAD /"},
-	{KEY_KEYPAD7, "KEYPAD 7"},
-	{KEY_KEYPAD8, "KEYPAD 8"},
-	{KEY_KEYPAD9, "KEYPAD 9"},
-	{KEY_MINUSPAD, "KEYPAD -"},
-	{KEY_KEYPAD4, "KEYPAD 4"},
-	{KEY_KEYPAD5, "KEYPAD 5"},
-	{KEY_KEYPAD6, "KEYPAD 6"},
-	{KEY_PLUSPAD, "KEYPAD +"},
-	{KEY_KEYPAD1, "KEYPAD 1"},
-	{KEY_KEYPAD2, "KEYPAD 2"},
-	{KEY_KEYPAD3, "KEYPAD 3"},
-	{KEY_KEYPAD0, "KEYPAD 0"},
-	{KEY_KPADDEL, "KEYPAD ."},
+	{KEY_KPADSLASH, "keypad /"},
+	{KEY_KEYPAD7, "keypad 7"},
+	{KEY_KEYPAD8, "keypad 8"},
+	{KEY_KEYPAD9, "keypad 9"},
+	{KEY_MINUSPAD, "keypad -"},
+	{KEY_KEYPAD4, "keypad 4"},
+	{KEY_KEYPAD5, "keypad 5"},
+	{KEY_KEYPAD6, "keypad 6"},
+	{KEY_PLUSPAD, "keypad +"},
+	{KEY_KEYPAD1, "keypad 1"},
+	{KEY_KEYPAD2, "keypad 2"},
+	{KEY_KEYPAD3, "keypad 3"},
+	{KEY_KEYPAD0, "keypad 0"},
+	{KEY_KPADDEL, "keypad ."},
 
 	// extended keys (not keypad)
-	{KEY_HOME, "HOME"},
-	{KEY_UPARROW, "UP ARROW"},
-	{KEY_PGUP, "PGUP"},
-	{KEY_LEFTARROW, "LEFT ARROW"},
-	{KEY_RIGHTARROW, "RIGHT ARROW"},
-	{KEY_END, "END"},
-	{KEY_DOWNARROW, "DOWN ARROW"},
-	{KEY_PGDN, "PGDN"},
-	{KEY_INS, "INS"},
-	{KEY_DEL, "DEL"},
+	{KEY_HOME, "home"},
+	{KEY_UPARROW, "up arrow"},
+	{KEY_PGUP, "pgup"},
+	{KEY_LEFTARROW, "left arrow"},
+	{KEY_RIGHTARROW, "right arrow"},
+	{KEY_END, "end"},
+	{KEY_DOWNARROW, "down arrow"},
+	{KEY_PGDN, "pgdn"},
+	{KEY_INS, "ins"},
+	{KEY_DEL, "del"},
 
 	// other keys
-	{KEY_F1, "F1"},
-	{KEY_F2, "F2"},
-	{KEY_F3, "F3"},
-	{KEY_F4, "F4"},
-	{KEY_F5, "F5"},
-	{KEY_F6, "F6"},
-	{KEY_F7, "F7"},
-	{KEY_F8, "F8"},
-	{KEY_F9, "F9"},
-	{KEY_F10, "F10"},
-	{KEY_F11, "F11"},
-	{KEY_F12, "F12"},
+	{KEY_F1, "f1"},
+	{KEY_F2, "f2"},
+	{KEY_F3, "f3"},
+	{KEY_F4, "f4"},
+	{KEY_F5, "f5"},
+	{KEY_F6, "f6"},
+	{KEY_F7, "f7"},
+	{KEY_F8, "f8"},
+	{KEY_F9, "f9"},
+	{KEY_F10, "f10"},
+	{KEY_F11, "f11"},
+	{KEY_F12, "f12"},
 
 	// KEY_CONSOLE has an exception in the keyname code
 	{'`', "TILDE"},
-	{KEY_PAUSE, "PAUSE/BREAK"},
+	{KEY_PAUSE, "pause/break"},
 
 	// virtual keys for mouse buttons and joystick buttons
-	{KEY_MOUSE1+0,"MOUSE1"},
-	{KEY_MOUSE1+1,"MOUSE2"},
-	{KEY_MOUSE1+2,"MOUSE3"},
-	{KEY_MOUSE1+3,"MOUSE4"},
-	{KEY_MOUSE1+4,"MOUSE5"},
-	{KEY_MOUSE1+5,"MOUSE6"},
-	{KEY_MOUSE1+6,"MOUSE7"},
-	{KEY_MOUSE1+7,"MOUSE8"},
-	{KEY_2MOUSE1+0,"SEC_MOUSE2"}, // BP: sorry my mouse handler swap button 1 and 2
-	{KEY_2MOUSE1+1,"SEC_MOUSE1"},
-	{KEY_2MOUSE1+2,"SEC_MOUSE3"},
-	{KEY_2MOUSE1+3,"SEC_MOUSE4"},
-	{KEY_2MOUSE1+4,"SEC_MOUSE5"},
-	{KEY_2MOUSE1+5,"SEC_MOUSE6"},
-	{KEY_2MOUSE1+6,"SEC_MOUSE7"},
-	{KEY_2MOUSE1+7,"SEC_MOUSE8"},
-	{KEY_MOUSEWHEELUP, "Wheel 1 UP"},
-	{KEY_MOUSEWHEELDOWN, "Wheel 1 Down"},
-	{KEY_2MOUSEWHEELUP, "Wheel 2 UP"},
-	{KEY_2MOUSEWHEELDOWN, "Wheel 2 Down"},
-
-	{KEY_JOY1+0, "JOY1"},
-	{KEY_JOY1+1, "JOY2"},
-	{KEY_JOY1+2, "JOY3"},
-	{KEY_JOY1+3, "JOY4"},
-	{KEY_JOY1+4, "JOY5"},
-	{KEY_JOY1+5, "JOY6"},
-	{KEY_JOY1+6, "JOY7"},
-	{KEY_JOY1+7, "JOY8"},
-	{KEY_JOY1+8, "JOY9"},
+	{KEY_MOUSE1+0,"mouse1"},
+	{KEY_MOUSE1+1,"mouse2"},
+	{KEY_MOUSE1+2,"mouse3"},
+	{KEY_MOUSE1+3,"mouse4"},
+	{KEY_MOUSE1+4,"mouse5"},
+	{KEY_MOUSE1+5,"mouse6"},
+	{KEY_MOUSE1+6,"mouse7"},
+	{KEY_MOUSE1+7,"mouse8"},
+	{KEY_2MOUSE1+0,"sec_mouse2"}, // BP: sorry my mouse handler swap button 1 and 2
+	{KEY_2MOUSE1+1,"sec_mouse1"},
+	{KEY_2MOUSE1+2,"sec_mouse3"},
+	{KEY_2MOUSE1+3,"sec_mouse4"},
+	{KEY_2MOUSE1+4,"sec_mouse5"},
+	{KEY_2MOUSE1+5,"sec_mouse6"},
+	{KEY_2MOUSE1+6,"sec_mouse7"},
+	{KEY_2MOUSE1+7,"sec_mouse8"},
+	{KEY_MOUSEWHEELUP, "wheel 1 up"},
+	{KEY_MOUSEWHEELDOWN, "wheel 1 down"},
+	{KEY_2MOUSEWHEELUP, "wheel 2 up"},
+	{KEY_2MOUSEWHEELDOWN, "wheel 2 down"},
+
+	{KEY_JOY1+0, "joy1"},
+	{KEY_JOY1+1, "joy2"},
+	{KEY_JOY1+2, "joy3"},
+	{KEY_JOY1+3, "joy4"},
+	{KEY_JOY1+4, "joy5"},
+	{KEY_JOY1+5, "joy6"},
+	{KEY_JOY1+6, "joy7"},
+	{KEY_JOY1+7, "joy8"},
+	{KEY_JOY1+8, "joy9"},
 #if !defined (NOMOREJOYBTN_1S)
 	// we use up to 32 buttons in DirectInput
-	{KEY_JOY1+9, "JOY10"},
-	{KEY_JOY1+10, "JOY11"},
-	{KEY_JOY1+11, "JOY12"},
-	{KEY_JOY1+12, "JOY13"},
-	{KEY_JOY1+13, "JOY14"},
-	{KEY_JOY1+14, "JOY15"},
-	{KEY_JOY1+15, "JOY16"},
-	{KEY_JOY1+16, "JOY17"},
-	{KEY_JOY1+17, "JOY18"},
-	{KEY_JOY1+18, "JOY19"},
-	{KEY_JOY1+19, "JOY20"},
-	{KEY_JOY1+20, "JOY21"},
-	{KEY_JOY1+21, "JOY22"},
-	{KEY_JOY1+22, "JOY23"},
-	{KEY_JOY1+23, "JOY24"},
-	{KEY_JOY1+24, "JOY25"},
-	{KEY_JOY1+25, "JOY26"},
-	{KEY_JOY1+26, "JOY27"},
-	{KEY_JOY1+27, "JOY28"},
-	{KEY_JOY1+28, "JOY29"},
-	{KEY_JOY1+29, "JOY30"},
-	{KEY_JOY1+30, "JOY31"},
-	{KEY_JOY1+31, "JOY32"},
+	{KEY_JOY1+9, "joy10"},
+	{KEY_JOY1+10, "joy11"},
+	{KEY_JOY1+11, "joy12"},
+	{KEY_JOY1+12, "joy13"},
+	{KEY_JOY1+13, "joy14"},
+	{KEY_JOY1+14, "joy15"},
+	{KEY_JOY1+15, "joy16"},
+	{KEY_JOY1+16, "joy17"},
+	{KEY_JOY1+17, "joy18"},
+	{KEY_JOY1+18, "joy19"},
+	{KEY_JOY1+19, "joy20"},
+	{KEY_JOY1+20, "joy21"},
+	{KEY_JOY1+21, "joy22"},
+	{KEY_JOY1+22, "joy23"},
+	{KEY_JOY1+23, "joy24"},
+	{KEY_JOY1+24, "joy25"},
+	{KEY_JOY1+25, "joy26"},
+	{KEY_JOY1+26, "joy27"},
+	{KEY_JOY1+27, "joy28"},
+	{KEY_JOY1+28, "joy29"},
+	{KEY_JOY1+29, "joy30"},
+	{KEY_JOY1+30, "joy31"},
+	{KEY_JOY1+31, "joy32"},
 #endif
 	// the DOS version uses Allegro's joystick support
-	{KEY_HAT1+0, "HATUP"},
-	{KEY_HAT1+1, "HATDOWN"},
-	{KEY_HAT1+2, "HATLEFT"},
-	{KEY_HAT1+3, "HATRIGHT"},
-	{KEY_HAT1+4, "HATUP2"},
-	{KEY_HAT1+5, "HATDOWN2"},
-	{KEY_HAT1+6, "HATLEFT2"},
-	{KEY_HAT1+7, "HATRIGHT2"},
-	{KEY_HAT1+8, "HATUP3"},
-	{KEY_HAT1+9, "HATDOWN3"},
-	{KEY_HAT1+10, "HATLEFT3"},
-	{KEY_HAT1+11, "HATRIGHT3"},
-	{KEY_HAT1+12, "HATUP4"},
-	{KEY_HAT1+13, "HATDOWN4"},
-	{KEY_HAT1+14, "HATLEFT4"},
-	{KEY_HAT1+15, "HATRIGHT4"},
-
-	{KEY_DBLMOUSE1+0, "DBLMOUSE1"},
-	{KEY_DBLMOUSE1+1, "DBLMOUSE2"},
-	{KEY_DBLMOUSE1+2, "DBLMOUSE3"},
-	{KEY_DBLMOUSE1+3, "DBLMOUSE4"},
-	{KEY_DBLMOUSE1+4, "DBLMOUSE5"},
-	{KEY_DBLMOUSE1+5, "DBLMOUSE6"},
-	{KEY_DBLMOUSE1+6, "DBLMOUSE7"},
-	{KEY_DBLMOUSE1+7, "DBLMOUSE8"},
-	{KEY_DBL2MOUSE1+0, "DBLSEC_MOUSE2"}, // BP: sorry my mouse handler swap button 1 and 2
-	{KEY_DBL2MOUSE1+1, "DBLSEC_MOUSE1"},
-	{KEY_DBL2MOUSE1+2, "DBLSEC_MOUSE3"},
-	{KEY_DBL2MOUSE1+3, "DBLSEC_MOUSE4"},
-	{KEY_DBL2MOUSE1+4, "DBLSEC_MOUSE5"},
-	{KEY_DBL2MOUSE1+5, "DBLSEC_MOUSE6"},
-	{KEY_DBL2MOUSE1+6, "DBLSEC_MOUSE7"},
-	{KEY_DBL2MOUSE1+7, "DBLSEC_MOUSE8"},
-
-	{KEY_DBLJOY1+0, "DBLJOY1"},
-	{KEY_DBLJOY1+1, "DBLJOY2"},
-	{KEY_DBLJOY1+2, "DBLJOY3"},
-	{KEY_DBLJOY1+3, "DBLJOY4"},
-	{KEY_DBLJOY1+4, "DBLJOY5"},
-	{KEY_DBLJOY1+5, "DBLJOY6"},
-	{KEY_DBLJOY1+6, "DBLJOY7"},
-	{KEY_DBLJOY1+7, "DBLJOY8"},
+	{KEY_HAT1+0, "hatup"},
+	{KEY_HAT1+1, "hatdown"},
+	{KEY_HAT1+2, "hatleft"},
+	{KEY_HAT1+3, "hatright"},
+	{KEY_HAT1+4, "hatup2"},
+	{KEY_HAT1+5, "hatdown2"},
+	{KEY_HAT1+6, "hatleft2"},
+	{KEY_HAT1+7, "hatright2"},
+	{KEY_HAT1+8, "hatup3"},
+	{KEY_HAT1+9, "hatdown3"},
+	{KEY_HAT1+10, "hatleft3"},
+	{KEY_HAT1+11, "hatright3"},
+	{KEY_HAT1+12, "hatup4"},
+	{KEY_HAT1+13, "hatdown4"},
+	{KEY_HAT1+14, "hatleft4"},
+	{KEY_HAT1+15, "hatright4"},
+
+	{KEY_DBLMOUSE1+0, "dblmouse1"},
+	{KEY_DBLMOUSE1+1, "dblmouse2"},
+	{KEY_DBLMOUSE1+2, "dblmouse3"},
+	{KEY_DBLMOUSE1+3, "dblmouse4"},
+	{KEY_DBLMOUSE1+4, "dblmouse5"},
+	{KEY_DBLMOUSE1+5, "dblmouse6"},
+	{KEY_DBLMOUSE1+6, "dblmouse7"},
+	{KEY_DBLMOUSE1+7, "dblmouse8"},
+	{KEY_DBL2MOUSE1+0, "dblsec_mouse2"}, // BP: sorry my mouse handler swap button 1 and 2
+	{KEY_DBL2MOUSE1+1, "dblsec_mouse1"},
+	{KEY_DBL2MOUSE1+2, "dblsec_mouse3"},
+	{KEY_DBL2MOUSE1+3, "dblsec_mouse4"},
+	{KEY_DBL2MOUSE1+4, "dblsec_mouse5"},
+	{KEY_DBL2MOUSE1+5, "dblsec_mouse6"},
+	{KEY_DBL2MOUSE1+6, "dblsec_mouse7"},
+	{KEY_DBL2MOUSE1+7, "dblsec_mouse8"},
+
+	{KEY_DBLJOY1+0, "dbljoy1"},
+	{KEY_DBLJOY1+1, "dbljoy2"},
+	{KEY_DBLJOY1+2, "dbljoy3"},
+	{KEY_DBLJOY1+3, "dbljoy4"},
+	{KEY_DBLJOY1+4, "dbljoy5"},
+	{KEY_DBLJOY1+5, "dbljoy6"},
+	{KEY_DBLJOY1+6, "dbljoy7"},
+	{KEY_DBLJOY1+7, "dbljoy8"},
 #if !defined (NOMOREJOYBTN_1DBL)
-	{KEY_DBLJOY1+8, "DBLJOY9"},
-	{KEY_DBLJOY1+9, "DBLJOY10"},
-	{KEY_DBLJOY1+10, "DBLJOY11"},
-	{KEY_DBLJOY1+11, "DBLJOY12"},
-	{KEY_DBLJOY1+12, "DBLJOY13"},
-	{KEY_DBLJOY1+13, "DBLJOY14"},
-	{KEY_DBLJOY1+14, "DBLJOY15"},
-	{KEY_DBLJOY1+15, "DBLJOY16"},
-	{KEY_DBLJOY1+16, "DBLJOY17"},
-	{KEY_DBLJOY1+17, "DBLJOY18"},
-	{KEY_DBLJOY1+18, "DBLJOY19"},
-	{KEY_DBLJOY1+19, "DBLJOY20"},
-	{KEY_DBLJOY1+20, "DBLJOY21"},
-	{KEY_DBLJOY1+21, "DBLJOY22"},
-	{KEY_DBLJOY1+22, "DBLJOY23"},
-	{KEY_DBLJOY1+23, "DBLJOY24"},
-	{KEY_DBLJOY1+24, "DBLJOY25"},
-	{KEY_DBLJOY1+25, "DBLJOY26"},
-	{KEY_DBLJOY1+26, "DBLJOY27"},
-	{KEY_DBLJOY1+27, "DBLJOY28"},
-	{KEY_DBLJOY1+28, "DBLJOY29"},
-	{KEY_DBLJOY1+29, "DBLJOY30"},
-	{KEY_DBLJOY1+30, "DBLJOY31"},
-	{KEY_DBLJOY1+31, "DBLJOY32"},
+	{KEY_DBLJOY1+8, "dbljoy9"},
+	{KEY_DBLJOY1+9, "dbljoy10"},
+	{KEY_DBLJOY1+10, "dbljoy11"},
+	{KEY_DBLJOY1+11, "dbljoy12"},
+	{KEY_DBLJOY1+12, "dbljoy13"},
+	{KEY_DBLJOY1+13, "dbljoy14"},
+	{KEY_DBLJOY1+14, "dbljoy15"},
+	{KEY_DBLJOY1+15, "dbljoy16"},
+	{KEY_DBLJOY1+16, "dbljoy17"},
+	{KEY_DBLJOY1+17, "dbljoy18"},
+	{KEY_DBLJOY1+18, "dbljoy19"},
+	{KEY_DBLJOY1+19, "dbljoy20"},
+	{KEY_DBLJOY1+20, "dbljoy21"},
+	{KEY_DBLJOY1+21, "dbljoy22"},
+	{KEY_DBLJOY1+22, "dbljoy23"},
+	{KEY_DBLJOY1+23, "dbljoy24"},
+	{KEY_DBLJOY1+24, "dbljoy25"},
+	{KEY_DBLJOY1+25, "dbljoy26"},
+	{KEY_DBLJOY1+26, "dbljoy27"},
+	{KEY_DBLJOY1+27, "dbljoy28"},
+	{KEY_DBLJOY1+28, "dbljoy29"},
+	{KEY_DBLJOY1+29, "dbljoy30"},
+	{KEY_DBLJOY1+30, "dbljoy31"},
+	{KEY_DBLJOY1+31, "dbljoy32"},
 #endif
-	{KEY_DBLHAT1+0, "DBLHATUP"},
-	{KEY_DBLHAT1+1, "DBLHATDOWN"},
-	{KEY_DBLHAT1+2, "DBLHATLEFT"},
-	{KEY_DBLHAT1+3, "DBLHATRIGHT"},
-	{KEY_DBLHAT1+4, "DBLHATUP2"},
-	{KEY_DBLHAT1+5, "DBLHATDOWN2"},
-	{KEY_DBLHAT1+6, "DBLHATLEFT2"},
-	{KEY_DBLHAT1+7, "DBLHATRIGHT2"},
-	{KEY_DBLHAT1+8, "DBLHATUP3"},
-	{KEY_DBLHAT1+9, "DBLHATDOWN3"},
-	{KEY_DBLHAT1+10, "DBLHATLEFT3"},
-	{KEY_DBLHAT1+11, "DBLHATRIGHT3"},
-	{KEY_DBLHAT1+12, "DBLHATUP4"},
-	{KEY_DBLHAT1+13, "DBLHATDOWN4"},
-	{KEY_DBLHAT1+14, "DBLHATLEFT4"},
-	{KEY_DBLHAT1+15, "DBLHATRIGHT4"},
-
-	{KEY_2JOY1+0, "SEC_JOY1"},
-	{KEY_2JOY1+1, "SEC_JOY2"},
-	{KEY_2JOY1+2, "SEC_JOY3"},
-	{KEY_2JOY1+3, "SEC_JOY4"},
-	{KEY_2JOY1+4, "SEC_JOY5"},
-	{KEY_2JOY1+5, "SEC_JOY6"},
-	{KEY_2JOY1+6, "SEC_JOY7"},
-	{KEY_2JOY1+7, "SEC_JOY8"},
+	{KEY_DBLHAT1+0, "dblhatup"},
+	{KEY_DBLHAT1+1, "dblhatdown"},
+	{KEY_DBLHAT1+2, "dblhatleft"},
+	{KEY_DBLHAT1+3, "dblhatright"},
+	{KEY_DBLHAT1+4, "dblhatup2"},
+	{KEY_DBLHAT1+5, "dblhatdown2"},
+	{KEY_DBLHAT1+6, "dblhatleft2"},
+	{KEY_DBLHAT1+7, "dblhatright2"},
+	{KEY_DBLHAT1+8, "dblhatup3"},
+	{KEY_DBLHAT1+9, "dblhatdown3"},
+	{KEY_DBLHAT1+10, "dblhatleft3"},
+	{KEY_DBLHAT1+11, "dblhatright3"},
+	{KEY_DBLHAT1+12, "dblhatup4"},
+	{KEY_DBLHAT1+13, "dblhatdown4"},
+	{KEY_DBLHAT1+14, "dblhatleft4"},
+	{KEY_DBLHAT1+15, "dblhatright4"},
+
+	{KEY_2JOY1+0, "sec_joy1"},
+	{KEY_2JOY1+1, "sec_joy2"},
+	{KEY_2JOY1+2, "sec_joy3"},
+	{KEY_2JOY1+3, "sec_joy4"},
+	{KEY_2JOY1+4, "sec_joy5"},
+	{KEY_2JOY1+5, "sec_joy6"},
+	{KEY_2JOY1+6, "sec_joy7"},
+	{KEY_2JOY1+7, "sec_joy8"},
 #if !defined (NOMOREJOYBTN_2S)
 	// we use up to 32 buttons in DirectInput
-	{KEY_2JOY1+8, "SEC_JOY9"},
-	{KEY_2JOY1+9, "SEC_JOY10"},
-	{KEY_2JOY1+10, "SEC_JOY11"},
-	{KEY_2JOY1+11, "SEC_JOY12"},
-	{KEY_2JOY1+12, "SEC_JOY13"},
-	{KEY_2JOY1+13, "SEC_JOY14"},
-	{KEY_2JOY1+14, "SEC_JOY15"},
-	{KEY_2JOY1+15, "SEC_JOY16"},
-	{KEY_2JOY1+16, "SEC_JOY17"},
-	{KEY_2JOY1+17, "SEC_JOY18"},
-	{KEY_2JOY1+18, "SEC_JOY19"},
-	{KEY_2JOY1+19, "SEC_JOY20"},
-	{KEY_2JOY1+20, "SEC_JOY21"},
-	{KEY_2JOY1+21, "SEC_JOY22"},
-	{KEY_2JOY1+22, "SEC_JOY23"},
-	{KEY_2JOY1+23, "SEC_JOY24"},
-	{KEY_2JOY1+24, "SEC_JOY25"},
-	{KEY_2JOY1+25, "SEC_JOY26"},
-	{KEY_2JOY1+26, "SEC_JOY27"},
-	{KEY_2JOY1+27, "SEC_JOY28"},
-	{KEY_2JOY1+28, "SEC_JOY29"},
-	{KEY_2JOY1+29, "SEC_JOY30"},
-	{KEY_2JOY1+30, "SEC_JOY31"},
-	{KEY_2JOY1+31, "SEC_JOY32"},
+	{KEY_2JOY1+8, "sec_joy9"},
+	{KEY_2JOY1+9, "sec_joy10"},
+	{KEY_2JOY1+10, "sec_joy11"},
+	{KEY_2JOY1+11, "sec_joy12"},
+	{KEY_2JOY1+12, "sec_joy13"},
+	{KEY_2JOY1+13, "sec_joy14"},
+	{KEY_2JOY1+14, "sec_joy15"},
+	{KEY_2JOY1+15, "sec_joy16"},
+	{KEY_2JOY1+16, "sec_joy17"},
+	{KEY_2JOY1+17, "sec_joy18"},
+	{KEY_2JOY1+18, "sec_joy19"},
+	{KEY_2JOY1+19, "sec_joy20"},
+	{KEY_2JOY1+20, "sec_joy21"},
+	{KEY_2JOY1+21, "sec_joy22"},
+	{KEY_2JOY1+22, "sec_joy23"},
+	{KEY_2JOY1+23, "sec_joy24"},
+	{KEY_2JOY1+24, "sec_joy25"},
+	{KEY_2JOY1+25, "sec_joy26"},
+	{KEY_2JOY1+26, "sec_joy27"},
+	{KEY_2JOY1+27, "sec_joy28"},
+	{KEY_2JOY1+28, "sec_joy29"},
+	{KEY_2JOY1+29, "sec_joy30"},
+	{KEY_2JOY1+30, "sec_joy31"},
+	{KEY_2JOY1+31, "sec_joy32"},
 #endif
 	// the DOS version uses Allegro's joystick support
-	{KEY_2HAT1+0,  "SEC_HATUP"},
-	{KEY_2HAT1+1,  "SEC_HATDOWN"},
-	{KEY_2HAT1+2,  "SEC_HATLEFT"},
-	{KEY_2HAT1+3,  "SEC_HATRIGHT"},
-	{KEY_2HAT1+4, "SEC_HATUP2"},
-	{KEY_2HAT1+5, "SEC_HATDOWN2"},
-	{KEY_2HAT1+6, "SEC_HATLEFT2"},
-	{KEY_2HAT1+7, "SEC_HATRIGHT2"},
-	{KEY_2HAT1+8, "SEC_HATUP3"},
-	{KEY_2HAT1+9, "SEC_HATDOWN3"},
-	{KEY_2HAT1+10, "SEC_HATLEFT3"},
-	{KEY_2HAT1+11, "SEC_HATRIGHT3"},
-	{KEY_2HAT1+12, "SEC_HATUP4"},
-	{KEY_2HAT1+13, "SEC_HATDOWN4"},
-	{KEY_2HAT1+14, "SEC_HATLEFT4"},
-	{KEY_2HAT1+15, "SEC_HATRIGHT4"},
-
-	{KEY_DBL2JOY1+0, "DBLSEC_JOY1"},
-	{KEY_DBL2JOY1+1, "DBLSEC_JOY2"},
-	{KEY_DBL2JOY1+2, "DBLSEC_JOY3"},
-	{KEY_DBL2JOY1+3, "DBLSEC_JOY4"},
-	{KEY_DBL2JOY1+4, "DBLSEC_JOY5"},
-	{KEY_DBL2JOY1+5, "DBLSEC_JOY6"},
-	{KEY_DBL2JOY1+6, "DBLSEC_JOY7"},
-	{KEY_DBL2JOY1+7, "DBLSEC_JOY8"},
+	{KEY_2HAT1+0,  "sec_hatup"},
+	{KEY_2HAT1+1,  "sec_hatdown"},
+	{KEY_2HAT1+2,  "sec_hatleft"},
+	{KEY_2HAT1+3,  "sec_hatright"},
+	{KEY_2HAT1+4, "sec_hatup2"},
+	{KEY_2HAT1+5, "sec_hatdown2"},
+	{KEY_2HAT1+6, "sec_hatleft2"},
+	{KEY_2HAT1+7, "sec_hatright2"},
+	{KEY_2HAT1+8, "sec_hatup3"},
+	{KEY_2HAT1+9, "sec_hatdown3"},
+	{KEY_2HAT1+10, "sec_hatleft3"},
+	{KEY_2HAT1+11, "sec_hatright3"},
+	{KEY_2HAT1+12, "sec_hatup4"},
+	{KEY_2HAT1+13, "sec_hatdown4"},
+	{KEY_2HAT1+14, "sec_hatleft4"},
+	{KEY_2HAT1+15, "sec_hatright4"},
+
+	{KEY_DBL2JOY1+0, "dblsec_joy1"},
+	{KEY_DBL2JOY1+1, "dblsec_joy2"},
+	{KEY_DBL2JOY1+2, "dblsec_joy3"},
+	{KEY_DBL2JOY1+3, "dblsec_joy4"},
+	{KEY_DBL2JOY1+4, "dblsec_joy5"},
+	{KEY_DBL2JOY1+5, "dblsec_joy6"},
+	{KEY_DBL2JOY1+6, "dblsec_joy7"},
+	{KEY_DBL2JOY1+7, "dblsec_joy8"},
 #if !defined (NOMOREJOYBTN_2DBL)
-	{KEY_DBL2JOY1+8, "DBLSEC_JOY9"},
-	{KEY_DBL2JOY1+9, "DBLSEC_JOY10"},
-	{KEY_DBL2JOY1+10, "DBLSEC_JOY11"},
-	{KEY_DBL2JOY1+11, "DBLSEC_JOY12"},
-	{KEY_DBL2JOY1+12, "DBLSEC_JOY13"},
-	{KEY_DBL2JOY1+13, "DBLSEC_JOY14"},
-	{KEY_DBL2JOY1+14, "DBLSEC_JOY15"},
-	{KEY_DBL2JOY1+15, "DBLSEC_JOY16"},
-	{KEY_DBL2JOY1+16, "DBLSEC_JOY17"},
-	{KEY_DBL2JOY1+17, "DBLSEC_JOY18"},
-	{KEY_DBL2JOY1+18, "DBLSEC_JOY19"},
-	{KEY_DBL2JOY1+19, "DBLSEC_JOY20"},
-	{KEY_DBL2JOY1+20, "DBLSEC_JOY21"},
-	{KEY_DBL2JOY1+21, "DBLSEC_JOY22"},
-	{KEY_DBL2JOY1+22, "DBLSEC_JOY23"},
-	{KEY_DBL2JOY1+23, "DBLSEC_JOY24"},
-	{KEY_DBL2JOY1+24, "DBLSEC_JOY25"},
-	{KEY_DBL2JOY1+25, "DBLSEC_JOY26"},
-	{KEY_DBL2JOY1+26, "DBLSEC_JOY27"},
-	{KEY_DBL2JOY1+27, "DBLSEC_JOY28"},
-	{KEY_DBL2JOY1+28, "DBLSEC_JOY29"},
-	{KEY_DBL2JOY1+29, "DBLSEC_JOY30"},
-	{KEY_DBL2JOY1+30, "DBLSEC_JOY31"},
-	{KEY_DBL2JOY1+31, "DBLSEC_JOY32"},
+	{KEY_DBL2JOY1+8, "dblsec_joy9"},
+	{KEY_DBL2JOY1+9, "dblsec_joy10"},
+	{KEY_DBL2JOY1+10, "dblsec_joy11"},
+	{KEY_DBL2JOY1+11, "dblsec_joy12"},
+	{KEY_DBL2JOY1+12, "dblsec_joy13"},
+	{KEY_DBL2JOY1+13, "dblsec_joy14"},
+	{KEY_DBL2JOY1+14, "dblsec_joy15"},
+	{KEY_DBL2JOY1+15, "dblsec_joy16"},
+	{KEY_DBL2JOY1+16, "dblsec_joy17"},
+	{KEY_DBL2JOY1+17, "dblsec_joy18"},
+	{KEY_DBL2JOY1+18, "dblsec_joy19"},
+	{KEY_DBL2JOY1+19, "dblsec_joy20"},
+	{KEY_DBL2JOY1+20, "dblsec_joy21"},
+	{KEY_DBL2JOY1+21, "dblsec_joy22"},
+	{KEY_DBL2JOY1+22, "dblsec_joy23"},
+	{KEY_DBL2JOY1+23, "dblsec_joy24"},
+	{KEY_DBL2JOY1+24, "dblsec_joy25"},
+	{KEY_DBL2JOY1+25, "dblsec_joy26"},
+	{KEY_DBL2JOY1+26, "dblsec_joy27"},
+	{KEY_DBL2JOY1+27, "dblsec_joy28"},
+	{KEY_DBL2JOY1+28, "dblsec_joy29"},
+	{KEY_DBL2JOY1+29, "dblsec_joy30"},
+	{KEY_DBL2JOY1+30, "dblsec_joy31"},
+	{KEY_DBL2JOY1+31, "dblsec_joy32"},
 #endif
-	{KEY_DBL2HAT1+0, "DBLSEC_HATUP"},
-	{KEY_DBL2HAT1+1, "DBLSEC_HATDOWN"},
-	{KEY_DBL2HAT1+2, "DBLSEC_HATLEFT"},
-	{KEY_DBL2HAT1+3, "DBLSEC_HATRIGHT"},
-	{KEY_DBL2HAT1+4, "DBLSEC_HATUP2"},
-	{KEY_DBL2HAT1+5, "DBLSEC_HATDOWN2"},
-	{KEY_DBL2HAT1+6, "DBLSEC_HATLEFT2"},
-	{KEY_DBL2HAT1+7, "DBLSEC_HATRIGHT2"},
-	{KEY_DBL2HAT1+8, "DBLSEC_HATUP3"},
-	{KEY_DBL2HAT1+9, "DBLSEC_HATDOWN3"},
-	{KEY_DBL2HAT1+10, "DBLSEC_HATLEFT3"},
-	{KEY_DBL2HAT1+11, "DBLSEC_HATRIGHT3"},
-	{KEY_DBL2HAT1+12, "DBLSEC_HATUP4"},
-	{KEY_DBL2HAT1+13, "DBLSEC_HATDOWN4"},
-	{KEY_DBL2HAT1+14, "DBLSEC_HATLEFT4"},
-	{KEY_DBL2HAT1+15, "DBLSEC_HATRIGHT4"},
+	{KEY_DBL2HAT1+0, "dblsec_hatup"},
+	{KEY_DBL2HAT1+1, "dblsec_hatdown"},
+	{KEY_DBL2HAT1+2, "dblsec_hatleft"},
+	{KEY_DBL2HAT1+3, "dblsec_hatright"},
+	{KEY_DBL2HAT1+4, "dblsec_hatup2"},
+	{KEY_DBL2HAT1+5, "dblsec_hatdown2"},
+	{KEY_DBL2HAT1+6, "dblsec_hatleft2"},
+	{KEY_DBL2HAT1+7, "dblsec_hatright2"},
+	{KEY_DBL2HAT1+8, "dblsec_hatup3"},
+	{KEY_DBL2HAT1+9, "dblsec_hatdown3"},
+	{KEY_DBL2HAT1+10, "dblsec_hatleft3"},
+	{KEY_DBL2HAT1+11, "dblsec_hatright3"},
+	{KEY_DBL2HAT1+12, "dblsec_hatup4"},
+	{KEY_DBL2HAT1+13, "dblsec_hatdown4"},
+	{KEY_DBL2HAT1+14, "dblsec_hatleft4"},
+	{KEY_DBL2HAT1+15, "dblsec_hatright4"},
 
 };
 
-static const char *gamecontrolname[num_gamecontrols] =
+static const char *gamecontrolname[NUM_GAMECONTROLS] =
 {
-	"nothing", // a key/button mapped to gc_null has no effect
+	"nothing", // a key/button mapped to GC_NULL has no effect
 	"forward",
 	"backward",
 	"strafeleft",
@@ -613,7 +613,7 @@ void G_ClearControlKeys(INT32 (*setupcontrols)[2], INT32 control)
 void G_ClearAllControlKeys(void)
 {
 	INT32 i;
-	for (i = 0; i < num_gamecontrols; i++)
+	for (i = 0; i < NUM_GAMECONTROLS; i++)
 	{
 		G_ClearControlKeys(gamecontrol, i);
 		G_ClearControlKeys(gamecontrolbis, i);
@@ -624,7 +624,7 @@ void G_ClearAllControlKeys(void)
 // Returns the name of a key (or virtual key for mouse and joy)
 // the input value being an keynum
 //
-const char *G_KeyNumToString(INT32 keynum)
+const char *G_KeyNumToName(INT32 keynum)
 {
 	static char keynamestr[8];
 
@@ -648,7 +648,7 @@ const char *G_KeyNumToString(INT32 keynum)
 	return keynamestr;
 }
 
-INT32 G_KeyStringToNum(const char *keystr)
+INT32 G_KeyNameToNum(const char *keystr)
 {
 	UINT32 j;
 
@@ -676,92 +676,92 @@ void G_DefineDefaultControls(void)
 	INT32 i;
 
 	// FPS game controls (WASD)
-	gamecontroldefault[gcs_fps][gc_forward    ][0] = 'w';
-	gamecontroldefault[gcs_fps][gc_backward   ][0] = 's';
-	gamecontroldefault[gcs_fps][gc_strafeleft ][0] = 'a';
-	gamecontroldefault[gcs_fps][gc_straferight][0] = 'd';
-	gamecontroldefault[gcs_fps][gc_lookup     ][0] = KEY_UPARROW;
-	gamecontroldefault[gcs_fps][gc_lookdown   ][0] = KEY_DOWNARROW;
-	gamecontroldefault[gcs_fps][gc_turnleft   ][0] = KEY_LEFTARROW;
-	gamecontroldefault[gcs_fps][gc_turnright  ][0] = KEY_RIGHTARROW;
-	gamecontroldefault[gcs_fps][gc_centerview ][0] = KEY_END;
-	gamecontroldefault[gcs_fps][gc_jump       ][0] = KEY_SPACE;
-	gamecontroldefault[gcs_fps][gc_spin       ][0] = KEY_LSHIFT;
-	gamecontroldefault[gcs_fps][gc_fire       ][0] = KEY_RCTRL;
-	gamecontroldefault[gcs_fps][gc_fire       ][1] = KEY_MOUSE1+0;
-	gamecontroldefault[gcs_fps][gc_firenormal ][0] = 'c';
+	gamecontroldefault[gcs_fps][GC_FORWARD    ][0] = 'w';
+	gamecontroldefault[gcs_fps][GC_BACKWARD   ][0] = 's';
+	gamecontroldefault[gcs_fps][GC_STRAFELEFT ][0] = 'a';
+	gamecontroldefault[gcs_fps][GC_STRAFERIGHT][0] = 'd';
+	gamecontroldefault[gcs_fps][GC_LOOKUP     ][0] = KEY_UPARROW;
+	gamecontroldefault[gcs_fps][GC_LOOKDOWN   ][0] = KEY_DOWNARROW;
+	gamecontroldefault[gcs_fps][GC_TURNLEFT   ][0] = KEY_LEFTARROW;
+	gamecontroldefault[gcs_fps][GC_TURNRIGHT  ][0] = KEY_RIGHTARROW;
+	gamecontroldefault[gcs_fps][GC_CENTERVIEW ][0] = KEY_END;
+	gamecontroldefault[gcs_fps][GC_JUMP       ][0] = KEY_SPACE;
+	gamecontroldefault[gcs_fps][GC_SPIN       ][0] = KEY_LSHIFT;
+	gamecontroldefault[gcs_fps][GC_FIRE       ][0] = KEY_RCTRL;
+	gamecontroldefault[gcs_fps][GC_FIRE       ][1] = KEY_MOUSE1+0;
+	gamecontroldefault[gcs_fps][GC_FIRENORMAL ][0] = 'c';
 
 	// Platform game controls (arrow keys)
-	gamecontroldefault[gcs_platform][gc_forward    ][0] = KEY_UPARROW;
-	gamecontroldefault[gcs_platform][gc_backward   ][0] = KEY_DOWNARROW;
-	gamecontroldefault[gcs_platform][gc_strafeleft ][0] = 'a';
-	gamecontroldefault[gcs_platform][gc_straferight][0] = 'd';
-	gamecontroldefault[gcs_platform][gc_lookup     ][0] = KEY_PGUP;
-	gamecontroldefault[gcs_platform][gc_lookdown   ][0] = KEY_PGDN;
-	gamecontroldefault[gcs_platform][gc_turnleft   ][0] = KEY_LEFTARROW;
-	gamecontroldefault[gcs_platform][gc_turnright  ][0] = KEY_RIGHTARROW;
-	gamecontroldefault[gcs_platform][gc_centerview ][0] = KEY_END;
-	gamecontroldefault[gcs_platform][gc_jump       ][0] = KEY_SPACE;
-	gamecontroldefault[gcs_platform][gc_spin       ][0] = KEY_LSHIFT;
-	gamecontroldefault[gcs_platform][gc_fire       ][0] = 's';
-	gamecontroldefault[gcs_platform][gc_fire       ][1] = KEY_MOUSE1+0;
-	gamecontroldefault[gcs_platform][gc_firenormal ][0] = 'w';
+	gamecontroldefault[gcs_platform][GC_FORWARD    ][0] = KEY_UPARROW;
+	gamecontroldefault[gcs_platform][GC_BACKWARD   ][0] = KEY_DOWNARROW;
+	gamecontroldefault[gcs_platform][GC_STRAFELEFT ][0] = 'a';
+	gamecontroldefault[gcs_platform][GC_STRAFERIGHT][0] = 'd';
+	gamecontroldefault[gcs_platform][GC_LOOKUP     ][0] = KEY_PGUP;
+	gamecontroldefault[gcs_platform][GC_LOOKDOWN   ][0] = KEY_PGDN;
+	gamecontroldefault[gcs_platform][GC_TURNLEFT   ][0] = KEY_LEFTARROW;
+	gamecontroldefault[gcs_platform][GC_TURNRIGHT  ][0] = KEY_RIGHTARROW;
+	gamecontroldefault[gcs_platform][GC_CENTERVIEW ][0] = KEY_END;
+	gamecontroldefault[gcs_platform][GC_JUMP       ][0] = KEY_SPACE;
+	gamecontroldefault[gcs_platform][GC_SPIN       ][0] = KEY_LSHIFT;
+	gamecontroldefault[gcs_platform][GC_FIRE       ][0] = 's';
+	gamecontroldefault[gcs_platform][GC_FIRE       ][1] = KEY_MOUSE1+0;
+	gamecontroldefault[gcs_platform][GC_FIRENORMAL ][0] = 'w';
 
 	for (i = 1; i < num_gamecontrolschemes; i++) // skip gcs_custom (0)
 	{
-		gamecontroldefault[i][gc_weaponnext ][0] = KEY_MOUSEWHEELUP+0;
-		gamecontroldefault[i][gc_weaponprev ][0] = KEY_MOUSEWHEELDOWN+0;
-		gamecontroldefault[i][gc_wepslot1   ][0] = '1';
-		gamecontroldefault[i][gc_wepslot2   ][0] = '2';
-		gamecontroldefault[i][gc_wepslot3   ][0] = '3';
-		gamecontroldefault[i][gc_wepslot4   ][0] = '4';
-		gamecontroldefault[i][gc_wepslot5   ][0] = '5';
-		gamecontroldefault[i][gc_wepslot6   ][0] = '6';
-		gamecontroldefault[i][gc_wepslot7   ][0] = '7';
-		gamecontroldefault[i][gc_wepslot8   ][0] = '8';
-		gamecontroldefault[i][gc_wepslot9   ][0] = '9';
-		gamecontroldefault[i][gc_wepslot10  ][0] = '0';
-		gamecontroldefault[i][gc_tossflag   ][0] = '\'';
-		gamecontroldefault[i][gc_camtoggle  ][0] = 'v';
-		gamecontroldefault[i][gc_camreset   ][0] = 'r';
-		gamecontroldefault[i][gc_talkkey    ][0] = 't';
-		gamecontroldefault[i][gc_teamkey    ][0] = 'y';
-		gamecontroldefault[i][gc_scores     ][0] = KEY_TAB;
-		gamecontroldefault[i][gc_console    ][0] = KEY_CONSOLE;
-		gamecontroldefault[i][gc_pause      ][0] = 'p';
-		gamecontroldefault[i][gc_screenshot ][0] = KEY_F8;
-		gamecontroldefault[i][gc_recordgif  ][0] = KEY_F9;
-		gamecontroldefault[i][gc_viewpoint  ][0] = KEY_F12;
+		gamecontroldefault[i][GC_WEAPONNEXT ][0] = KEY_MOUSEWHEELUP+0;
+		gamecontroldefault[i][GC_WEAPONPREV ][0] = KEY_MOUSEWHEELDOWN+0;
+		gamecontroldefault[i][GC_WEPSLOT1   ][0] = '1';
+		gamecontroldefault[i][GC_WEPSLOT2   ][0] = '2';
+		gamecontroldefault[i][GC_WEPSLOT3   ][0] = '3';
+		gamecontroldefault[i][GC_WEPSLOT4   ][0] = '4';
+		gamecontroldefault[i][GC_WEPSLOT5   ][0] = '5';
+		gamecontroldefault[i][GC_WEPSLOT6   ][0] = '6';
+		gamecontroldefault[i][GC_WEPSLOT7   ][0] = '7';
+		gamecontroldefault[i][GC_WEPSLOT8   ][0] = '8';
+		gamecontroldefault[i][GC_WEPSLOT9   ][0] = '9';
+		gamecontroldefault[i][GC_WEPSLOT10  ][0] = '0';
+		gamecontroldefault[i][GC_TOSSFLAG   ][0] = '\'';
+		gamecontroldefault[i][GC_CAMTOGGLE  ][0] = 'v';
+		gamecontroldefault[i][GC_CAMRESET   ][0] = 'r';
+		gamecontroldefault[i][GC_TALKKEY    ][0] = 't';
+		gamecontroldefault[i][GC_TEAMKEY    ][0] = 'y';
+		gamecontroldefault[i][GC_SCORES     ][0] = KEY_TAB;
+		gamecontroldefault[i][GC_CONSOLE    ][0] = KEY_CONSOLE;
+		gamecontroldefault[i][GC_PAUSE      ][0] = 'p';
+		gamecontroldefault[i][GC_SCREENSHOT ][0] = KEY_F8;
+		gamecontroldefault[i][GC_RECORDGIF  ][0] = KEY_F9;
+		gamecontroldefault[i][GC_VIEWPOINT  ][0] = KEY_F12;
 
 		// Gamepad controls -- same for both schemes
-		gamecontroldefault[i][gc_weaponnext ][1] = KEY_JOY1+1; // B
-		gamecontroldefault[i][gc_weaponprev ][1] = KEY_JOY1+2; // X
-		gamecontroldefault[i][gc_tossflag   ][1] = KEY_JOY1+0; // A
-		gamecontroldefault[i][gc_spin       ][1] = KEY_JOY1+4; // LB
-		gamecontroldefault[i][gc_camtoggle  ][1] = KEY_HAT1+0; // D-Pad Up
-		gamecontroldefault[i][gc_camreset   ][1] = KEY_JOY1+3; // Y
-		gamecontroldefault[i][gc_centerview ][1] = KEY_JOY1+9; // Right Stick
-		gamecontroldefault[i][gc_talkkey    ][1] = KEY_HAT1+2; // D-Pad Left
-		gamecontroldefault[i][gc_scores     ][1] = KEY_HAT1+3; // D-Pad Right
-		gamecontroldefault[i][gc_jump       ][1] = KEY_JOY1+5; // RB
-		gamecontroldefault[i][gc_pause      ][1] = KEY_JOY1+6; // Back
-		gamecontroldefault[i][gc_screenshot ][1] = KEY_HAT1+1; // D-Pad Down
-		gamecontroldefault[i][gc_systemmenu ][0] = KEY_JOY1+7; // Start
+		gamecontroldefault[i][GC_WEAPONNEXT ][1] = KEY_JOY1+1; // B
+		gamecontroldefault[i][GC_WEAPONPREV ][1] = KEY_JOY1+2; // X
+		gamecontroldefault[i][GC_TOSSFLAG   ][1] = KEY_JOY1+0; // A
+		gamecontroldefault[i][GC_SPIN       ][1] = KEY_JOY1+4; // LB
+		gamecontroldefault[i][GC_CAMTOGGLE  ][1] = KEY_HAT1+0; // D-Pad Up
+		gamecontroldefault[i][GC_CAMRESET   ][1] = KEY_JOY1+3; // Y
+		gamecontroldefault[i][GC_CENTERVIEW ][1] = KEY_JOY1+9; // Right Stick
+		gamecontroldefault[i][GC_TALKKEY    ][1] = KEY_HAT1+2; // D-Pad Left
+		gamecontroldefault[i][GC_SCORES     ][1] = KEY_HAT1+3; // D-Pad Right
+		gamecontroldefault[i][GC_JUMP       ][1] = KEY_JOY1+5; // RB
+		gamecontroldefault[i][GC_PAUSE      ][1] = KEY_JOY1+6; // Back
+		gamecontroldefault[i][GC_SCREENSHOT ][1] = KEY_HAT1+1; // D-Pad Down
+		gamecontroldefault[i][GC_SYSTEMMENU ][0] = KEY_JOY1+7; // Start
 
 		// Second player controls only have joypad defaults
-		gamecontrolbisdefault[i][gc_weaponnext][0] = KEY_2JOY1+1; // B
-		gamecontrolbisdefault[i][gc_weaponprev][0] = KEY_2JOY1+2; // X
-		gamecontrolbisdefault[i][gc_tossflag  ][0] = KEY_2JOY1+0; // A
-		gamecontrolbisdefault[i][gc_spin      ][0] = KEY_2JOY1+4; // LB
-		gamecontrolbisdefault[i][gc_camreset  ][0] = KEY_2JOY1+3; // Y
-		gamecontrolbisdefault[i][gc_centerview][0] = KEY_2JOY1+9; // Right Stick
-		gamecontrolbisdefault[i][gc_jump      ][0] = KEY_2JOY1+5; // RB
-		//gamecontrolbisdefault[i][gc_pause     ][0] = KEY_2JOY1+6; // Back
-		//gamecontrolbisdefault[i][gc_systemmenu][0] = KEY_2JOY1+7; // Start
-		gamecontrolbisdefault[i][gc_camtoggle ][0] = KEY_2HAT1+0; // D-Pad Up
-		gamecontrolbisdefault[i][gc_screenshot][0] = KEY_2HAT1+1; // D-Pad Down
-		//gamecontrolbisdefault[i][gc_talkkey   ][0] = KEY_2HAT1+2; // D-Pad Left
-		//gamecontrolbisdefault[i][gc_scores    ][0] = KEY_2HAT1+3; // D-Pad Right
+		gamecontrolbisdefault[i][GC_WEAPONNEXT][0] = KEY_2JOY1+1; // B
+		gamecontrolbisdefault[i][GC_WEAPONPREV][0] = KEY_2JOY1+2; // X
+		gamecontrolbisdefault[i][GC_TOSSFLAG  ][0] = KEY_2JOY1+0; // A
+		gamecontrolbisdefault[i][GC_SPIN      ][0] = KEY_2JOY1+4; // LB
+		gamecontrolbisdefault[i][GC_CAMRESET  ][0] = KEY_2JOY1+3; // Y
+		gamecontrolbisdefault[i][GC_CENTERVIEW][0] = KEY_2JOY1+9; // Right Stick
+		gamecontrolbisdefault[i][GC_JUMP      ][0] = KEY_2JOY1+5; // RB
+		//gamecontrolbisdefault[i][GC_PAUSE     ][0] = KEY_2JOY1+6; // Back
+		//gamecontrolbisdefault[i][GC_SYSTEMMENU][0] = KEY_2JOY1+7; // Start
+		gamecontrolbisdefault[i][GC_CAMTOGGLE ][0] = KEY_2HAT1+0; // D-Pad Up
+		gamecontrolbisdefault[i][GC_SCREENSHOT][0] = KEY_2HAT1+1; // D-Pad Down
+		//gamecontrolbisdefault[i][GC_TALKKEY   ][0] = KEY_2HAT1+2; // D-Pad Left
+		//gamecontrolbisdefault[i][GC_SCORES    ][0] = KEY_2HAT1+3; // D-Pad Right
 	}
 }
 
@@ -773,7 +773,7 @@ INT32 G_GetControlScheme(INT32 (*fromcontrols)[2], const INT32 *gclist, INT32 gc
 	for (i = 1; i < num_gamecontrolschemes; i++) // skip gcs_custom (0)
 	{
 		skipscheme = false;
-		for (j = 0; j < (gclist && gclen ? gclen : num_gamecontrols); j++)
+		for (j = 0; j < (gclist && gclen ? gclen : NUM_GAMECONTROLS); j++)
 		{
 			gc = (gclist && gclen) ? gclist[j] : j;
 			if (((fromcontrols[gc][0] && gamecontroldefault[i][gc][0]) ? fromcontrols[gc][0] != gamecontroldefault[i][gc][0] : true) &&
@@ -796,7 +796,7 @@ void G_CopyControls(INT32 (*setupcontrols)[2], INT32 (*fromcontrols)[2], const I
 {
 	INT32 i, gc;
 
-	for (i = 0; i < (gclist && gclen ? gclen : num_gamecontrols); i++)
+	for (i = 0; i < (gclist && gclen ? gclen : NUM_GAMECONTROLS); i++)
 	{
 		gc = (gclist && gclen) ? gclist[i] : i;
 		setupcontrols[gc][0] = fromcontrols[gc][0];
@@ -808,24 +808,24 @@ void G_SaveKeySetting(FILE *f, INT32 (*fromcontrols)[2], INT32 (*fromcontrolsbis
 {
 	INT32 i;
 
-	for (i = 1; i < num_gamecontrols; i++)
+	for (i = 1; i < NUM_GAMECONTROLS; i++)
 	{
 		fprintf(f, "setcontrol \"%s\" \"%s\"", gamecontrolname[i],
-			G_KeyNumToString(fromcontrols[i][0]));
+			G_KeyNumToName(fromcontrols[i][0]));
 
 		if (fromcontrols[i][1])
-			fprintf(f, " \"%s\"\n", G_KeyNumToString(fromcontrols[i][1]));
+			fprintf(f, " \"%s\"\n", G_KeyNumToName(fromcontrols[i][1]));
 		else
 			fprintf(f, "\n");
 	}
 
-	for (i = 1; i < num_gamecontrols; i++)
+	for (i = 1; i < NUM_GAMECONTROLS; i++)
 	{
 		fprintf(f, "setcontrol2 \"%s\" \"%s\"", gamecontrolname[i],
-			G_KeyNumToString(fromcontrolsbis[i][0]));
+			G_KeyNumToName(fromcontrolsbis[i][0]));
 
 		if (fromcontrolsbis[i][1])
-			fprintf(f, " \"%s\"\n", G_KeyNumToString(fromcontrolsbis[i][1]));
+			fprintf(f, " \"%s\"\n", G_KeyNumToName(fromcontrolsbis[i][1]));
 		else
 			fprintf(f, "\n");
 	}
@@ -833,11 +833,11 @@ void G_SaveKeySetting(FILE *f, INT32 (*fromcontrols)[2], INT32 (*fromcontrolsbis
 
 INT32 G_CheckDoubleUsage(INT32 keynum, boolean modify)
 {
-	INT32 result = gc_null;
+	INT32 result = GC_NULL;
 	if (cv_controlperkey.value == 1)
 	{
 		INT32 i;
-		for (i = 0; i < num_gamecontrols; i++)
+		for (i = 0; i < NUM_GAMECONTROLS; i++)
 		{
 			if (gamecontrol[i][0] == keynum)
 			{
@@ -883,11 +883,11 @@ static INT32 G_FilterKeyByVersion(INT32 numctrl, INT32 keyidx, INT32 player, INT
 		return -1; // skip setting control
 
 	if (GETMAJOREXECVERSION(cv_execversion.value) < 27 && ( // v2.1.22
-		numctrl == gc_weaponnext || numctrl == gc_weaponprev || numctrl == gc_tossflag ||
-		numctrl == gc_spin || numctrl == gc_camreset || numctrl == gc_jump ||
-		numctrl == gc_pause || numctrl == gc_systemmenu || numctrl == gc_camtoggle ||
-		numctrl == gc_screenshot || numctrl == gc_talkkey || numctrl == gc_scores ||
-		numctrl == gc_centerview
+		numctrl == GC_WEAPONNEXT || numctrl == GC_WEAPONPREV || numctrl == GC_TOSSFLAG ||
+		numctrl == GC_SPIN || numctrl == GC_CAMRESET || numctrl == GC_JUMP ||
+		numctrl == GC_PAUSE || numctrl == GC_SYSTEMMENU || numctrl == GC_CAMTOGGLE ||
+		numctrl == GC_SCREENSHOT || numctrl == GC_TALKKEY || numctrl == GC_SCORES ||
+		numctrl == GC_CENTERVIEW
 	))
 	{
 		INT32 keynum = 0, existingctrl = 0;
@@ -895,7 +895,7 @@ static INT32 G_FilterKeyByVersion(INT32 numctrl, INT32 keyidx, INT32 player, INT
 		boolean defaultoverride = false;
 
 		// get the default gamecontrol
-		if (player == 0 && numctrl == gc_systemmenu)
+		if (player == 0 && numctrl == GC_SYSTEMMENU)
 			defaultkey = gamecontrol[numctrl][0];
 		else
 			defaultkey = (player == 1 ? gamecontrolbis[numctrl][0] : gamecontrol[numctrl][1]);
@@ -993,16 +993,16 @@ static void setcontrol(INT32 (*gc)[2])
 	// Update me for 2.3
 	namectrl = (stricmp(COM_Argv(1), "use")) ? COM_Argv(1) : "spin";
 
-	for (numctrl = 0; numctrl < num_gamecontrols && stricmp(namectrl, gamecontrolname[numctrl]);
+	for (numctrl = 0; numctrl < NUM_GAMECONTROLS && stricmp(namectrl, gamecontrolname[numctrl]);
 		numctrl++)
 		;
-	if (numctrl == num_gamecontrols)
+	if (numctrl == NUM_GAMECONTROLS)
 	{
 		CONS_Printf(M_GetText("Control '%s' unknown\n"), namectrl);
 		return;
 	}
-	keynum1 = G_KeyStringToNum(COM_Argv(2));
-	keynum2 = G_KeyStringToNum(COM_Argv(3));
+	keynum1 = G_KeyNameToNum(COM_Argv(2));
+	keynum2 = G_KeyNameToNum(COM_Argv(3));
 	keynum = G_FilterKeyByVersion(numctrl, 0, player, &keynum1, &keynum2, &nestedoverride);
 
 	if (keynum >= 0)
diff --git a/src/g_input.h b/src/g_input.h
index ffd0cb560b5f0921fb3595c970f93b7e5d1ff5f0..2e9f53dcf4e4ff57aa9deccbffc7537ebaa6c261 100644
--- a/src/g_input.h
+++ b/src/g_input.h
@@ -58,49 +58,49 @@ typedef enum
 
 typedef enum
 {
-	gc_null = 0, // a key/button mapped to gc_null has no effect
-	gc_forward,
-	gc_backward,
-	gc_strafeleft,
-	gc_straferight,
-	gc_turnleft,
-	gc_turnright,
-	gc_weaponnext,
-	gc_weaponprev,
-	gc_wepslot1,
-	gc_wepslot2,
-	gc_wepslot3,
-	gc_wepslot4,
-	gc_wepslot5,
-	gc_wepslot6,
-	gc_wepslot7,
-	gc_wepslot8,
-	gc_wepslot9,
-	gc_wepslot10,
-	gc_fire,
-	gc_firenormal,
-	gc_tossflag,
-	gc_spin,
-	gc_camtoggle,
-	gc_camreset,
-	gc_lookup,
-	gc_lookdown,
-	gc_centerview,
-	gc_mouseaiming, // mouse aiming is momentary (toggleable in the menu)
-	gc_talkkey,
-	gc_teamkey,
-	gc_scores,
-	gc_jump,
-	gc_console,
-	gc_pause,
-	gc_systemmenu,
-	gc_screenshot,
-	gc_recordgif,
-	gc_viewpoint,
-	gc_custom1, // Lua scriptable
-	gc_custom2, // Lua scriptable
-	gc_custom3, // Lua scriptable
-	num_gamecontrols
+	GC_NULL = 0, // a key/button mapped to GC_NULL has no effect
+	GC_FORWARD,
+	GC_BACKWARD,
+	GC_STRAFELEFT,
+	GC_STRAFERIGHT,
+	GC_TURNLEFT,
+	GC_TURNRIGHT,
+	GC_WEAPONNEXT,
+	GC_WEAPONPREV,
+	GC_WEPSLOT1,
+	GC_WEPSLOT2,
+	GC_WEPSLOT3,
+	GC_WEPSLOT4,
+	GC_WEPSLOT5,
+	GC_WEPSLOT6,
+	GC_WEPSLOT7,
+	GC_WEPSLOT8,
+	GC_WEPSLOT9,
+	GC_WEPSLOT10,
+	GC_FIRE,
+	GC_FIRENORMAL,
+	GC_TOSSFLAG,
+	GC_SPIN,
+	GC_CAMTOGGLE,
+	GC_CAMRESET,
+	GC_LOOKUP,
+	GC_LOOKDOWN,
+	GC_CENTERVIEW,
+	GC_MOUSEAIMING, // mouse aiming is momentary (toggleable in the menu)
+	GC_TALKKEY,
+	GC_TEAMKEY,
+	GC_SCORES,
+	GC_JUMP,
+	GC_CONSOLE,
+	GC_PAUSE,
+	GC_SYSTEMMENU,
+	GC_SCREENSHOT,
+	GC_RECORDGIF,
+	GC_VIEWPOINT,
+	GC_CUSTOM1, // Lua scriptable
+	GC_CUSTOM2, // Lua scriptable
+	GC_CUSTOM3, // Lua scriptable
+	NUM_GAMECONTROLS
 } gamecontrols_e;
 
 typedef enum
@@ -146,10 +146,10 @@ extern INT32 joyxmove[JOYAXISSET], joyymove[JOYAXISSET], joy2xmove[JOYAXISSET],
 extern UINT8 gamekeydown[NUMINPUTS];
 
 // two key codes (or virtual key) per game control
-extern INT32 gamecontrol[num_gamecontrols][2];
-extern INT32 gamecontrolbis[num_gamecontrols][2]; // secondary splitscreen player
-extern INT32 gamecontroldefault[num_gamecontrolschemes][num_gamecontrols][2]; // default control storage, use 0 (gcs_custom) for memory retention
-extern INT32 gamecontrolbisdefault[num_gamecontrolschemes][num_gamecontrols][2];
+extern INT32 gamecontrol[NUM_GAMECONTROLS][2];
+extern INT32 gamecontrolbis[NUM_GAMECONTROLS][2]; // secondary splitscreen player
+extern INT32 gamecontroldefault[num_gamecontrolschemes][NUM_GAMECONTROLS][2]; // default control storage, use 0 (gcs_custom) for memory retention
+extern INT32 gamecontrolbisdefault[num_gamecontrolschemes][NUM_GAMECONTROLS][2];
 #define PLAYER1INPUTDOWN(gc) (gamekeydown[gamecontrol[gc][0]] || gamekeydown[gamecontrol[gc][1]])
 #define PLAYER2INPUTDOWN(gc) (gamekeydown[gamecontrolbis[gc][0]] || gamekeydown[gamecontrolbis[gc][1]])
 #define PLAYERINPUTDOWN(p, gc) ((p) == 2 ? PLAYER2INPUTDOWN(gc) : PLAYER1INPUTDOWN(gc))
@@ -181,8 +181,8 @@ extern const INT32 gcl_jump_spin[num_gcl_jump_spin];
 void G_MapEventsToControls(event_t *ev);
 
 // returns the name of a key
-const char *G_KeyNumToString(INT32 keynum);
-INT32 G_KeyStringToNum(const char *keystr);
+const char *G_KeyNumToName(INT32 keynum);
+INT32 G_KeyNameToNum(const char *keystr);
 
 // detach any keys associated to the given game control
 void G_ClearControlKeys(INT32 (*setupcontrols)[2], INT32 control);
diff --git a/src/hardware/hw_batching.c b/src/hardware/hw_batching.c
index 0ac33d1361d2a4bf92070a9d944c1bfeab9cd980..da0319bccfecbd70901fe05b1658a49c54c24998 100644
--- a/src/hardware/hw_batching.c
+++ b/src/hardware/hw_batching.c
@@ -245,13 +245,16 @@ void HWR_RenderBatches(void)
 	currently_batching = false;// no longer collecting batches
 	if (!polygonArraySize)
 	{
-		ps_hw_numpolys = ps_hw_numcalls = ps_hw_numshaders = ps_hw_numtextures = ps_hw_numpolyflags = ps_hw_numcolors = 0;
+		ps_hw_numpolys.value.i = ps_hw_numcalls.value.i = ps_hw_numshaders.value.i
+			= ps_hw_numtextures.value.i = ps_hw_numpolyflags.value.i
+			= ps_hw_numcolors.value.i = 0;
 		return;// nothing to draw
 	}
 	// init stats vars
-	ps_hw_numpolys = polygonArraySize;
-	ps_hw_numcalls = ps_hw_numverts = 0;
-	ps_hw_numshaders = ps_hw_numtextures = ps_hw_numpolyflags = ps_hw_numcolors = 1;
+	ps_hw_numpolys.value.i = polygonArraySize;
+	ps_hw_numcalls.value.i = ps_hw_numverts.value.i = 0;
+	ps_hw_numshaders.value.i = ps_hw_numtextures.value.i
+		= ps_hw_numpolyflags.value.i = ps_hw_numcolors.value.i = 1;
 	// init polygonIndexArray
 	for (i = 0; i < polygonArraySize; i++)
 	{
@@ -259,12 +262,12 @@ void HWR_RenderBatches(void)
 	}
 
 	// sort polygons
-	ps_hw_batchsorttime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_batchsorttime);
 	if (cv_glshaders.value && gl_shadersavailable)
 		qsort(polygonIndexArray, polygonArraySize, sizeof(unsigned int), comparePolygons);
 	else
 		qsort(polygonIndexArray, polygonArraySize, sizeof(unsigned int), comparePolygonsNoShaders);
-	ps_hw_batchsorttime = I_GetPreciseTime() - ps_hw_batchsorttime;
+	PS_STOP_TIMING(ps_hw_batchsorttime);
 	// sort order
 	// 1. shader
 	// 2. texture
@@ -272,7 +275,7 @@ void HWR_RenderBatches(void)
 	// 4. colors + light level
 	// not sure about what order of the last 2 should be, or if it even matters
 
-	ps_hw_batchdrawtime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_batchdrawtime);
 
 	currentShader = polygonArray[polygonIndexArray[0]].shader;
 	currentTexture = polygonArray[polygonIndexArray[0]].texture;
@@ -408,8 +411,8 @@ void HWR_RenderBatches(void)
 			// execute draw call
             HWD.pfnDrawIndexedTriangles(&currentSurfaceInfo, finalVertexArray, finalIndexWritePos, currentPolyFlags, finalVertexIndexArray);
 			// update stats
-			ps_hw_numcalls++;
-			ps_hw_numverts += finalIndexWritePos;
+			ps_hw_numcalls.value.i++;
+			ps_hw_numverts.value.i += finalIndexWritePos;
 			// reset write positions
 			finalVertexWritePos = 0;
 			finalIndexWritePos = 0;
@@ -426,7 +429,7 @@ void HWR_RenderBatches(void)
 			currentShader = nextShader;
 			changeShader = false;
 
-			ps_hw_numshaders++;
+			ps_hw_numshaders.value.i++;
 		}
 		if (changeTexture)
 		{
@@ -435,21 +438,21 @@ void HWR_RenderBatches(void)
 			currentTexture = nextTexture;
 			changeTexture = false;
 
-			ps_hw_numtextures++;
+			ps_hw_numtextures.value.i++;
 		}
 		if (changePolyFlags)
 		{
 			currentPolyFlags = nextPolyFlags;
 			changePolyFlags = false;
 
-			ps_hw_numpolyflags++;
+			ps_hw_numpolyflags.value.i++;
 		}
 		if (changeSurfaceInfo)
 		{
 			currentSurfaceInfo = nextSurfaceInfo;
 			changeSurfaceInfo = false;
 
-			ps_hw_numcolors++;
+			ps_hw_numcolors.value.i++;
 		}
 		// and that should be it?
 	}
@@ -457,7 +460,7 @@ void HWR_RenderBatches(void)
 	polygonArraySize = 0;
 	unsortedVertexArraySize = 0;
 
-	ps_hw_batchdrawtime = I_GetPreciseTime() - ps_hw_batchdrawtime;
+	PS_STOP_TIMING(ps_hw_batchdrawtime);
 }
 
 
diff --git a/src/hardware/hw_draw.c b/src/hardware/hw_draw.c
index e83aff0d778989a36bf3367c51600228b8a14189..8223705bd1afa4a30e6d1c1239693fcab98d8374 100644
--- a/src/hardware/hw_draw.c
+++ b/src/hardware/hw_draw.c
@@ -317,7 +317,7 @@ void HWR_DrawStretchyFixedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t p
 		}
 	}
 
-	if (pscale != FRACUNIT || (splitscreen && option & V_PERPLAYER))
+	if (pscale != FRACUNIT || vscale != FRACUNIT || (splitscreen && option & V_PERPLAYER))
 	{
 		fwidth = (float)(gpatch->width) * fscalew * dupx;
 		fheight = (float)(gpatch->height) * fscaleh * dupy;
@@ -382,7 +382,7 @@ void HWR_DrawStretchyFixedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t p
 		HWD.pfnDrawPolygon(NULL, v, 4, flags);
 }
 
-void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale, INT32 option, fixed_t sx, fixed_t sy, fixed_t w, fixed_t h)
+void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale, fixed_t vscale, INT32 option, const UINT8 *colormap, fixed_t sx, fixed_t sy, fixed_t w, fixed_t h)
 {
 	FOutVector v[4];
 	FBITFIELD flags;
@@ -395,13 +395,19 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 //  | /|
 //  |/ |
 //  0--1
-	float dupx, dupy, fscale, fwidth, fheight;
+	float dupx, dupy, fscalew, fscaleh, fwidth, fheight;
+
+	UINT8 perplayershuffle = 0;
 
 	if (alphalevel >= 10 && alphalevel < 13)
 		return;
 
 	// make patch ready in hardware cache
-	HWR_GetPatch(gpatch);
+	if (!colormap)
+		HWR_GetPatch(gpatch);
+	else
+		HWR_GetMappedPatch(gpatch, colormap);
+
 	hwrPatch = ((GLPatch_t *)gpatch->hardware);
 
 	dupx = (float)vid.dupx;
@@ -423,12 +429,80 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 	}
 
 	dupx = dupy = (dupx < dupy ? dupx : dupy);
-	fscale = FIXED_TO_FLOAT(pscale);
+	fscalew = fscaleh = FIXED_TO_FLOAT(pscale);
+	if (vscale != pscale)
+		fscaleh = FIXED_TO_FLOAT(vscale);
 
-	// fuck it, no GL support for croppedpatch v_perplayer right now. it's not like it's accessible to Lua or anything, and we only use it for menus...
+	cx -= (float)(gpatch->leftoffset) * fscalew;
+	cy -= (float)(gpatch->topoffset) * fscaleh;
 
-	cy -= (float)(gpatch->topoffset) * fscale;
-	cx -= (float)(gpatch->leftoffset) * fscale;
+	if (splitscreen && (option & V_PERPLAYER))
+	{
+		float adjusty = ((option & V_NOSCALESTART) ? vid.height : BASEVIDHEIGHT)/2.0f;
+		fscaleh /= 2;
+		cy /= 2;
+#ifdef QUADS
+		if (splitscreen > 1) // 3 or 4 players
+		{
+			float adjustx = ((option & V_NOSCALESTART) ? vid.width : BASEVIDWIDTH)/2.0f;
+			fscalew /= 2;
+			cx /= 2;
+			if (stplyr == &players[displayplayer])
+			{
+				if (!(option & (V_SNAPTOTOP|V_SNAPTOBOTTOM)))
+					perplayershuffle |= 1;
+				if (!(option & (V_SNAPTOLEFT|V_SNAPTORIGHT)))
+					perplayershuffle |= 4;
+				option &= ~V_SNAPTOBOTTOM|V_SNAPTORIGHT;
+			}
+			else if (stplyr == &players[secondarydisplayplayer])
+			{
+				if (!(option & (V_SNAPTOTOP|V_SNAPTOBOTTOM)))
+					perplayershuffle |= 1;
+				if (!(option & (V_SNAPTOLEFT|V_SNAPTORIGHT)))
+					perplayershuffle |= 8;
+				cx += adjustx;
+				option &= ~V_SNAPTOBOTTOM|V_SNAPTOLEFT;
+			}
+			else if (stplyr == &players[thirddisplayplayer])
+			{
+				if (!(option & (V_SNAPTOTOP|V_SNAPTOBOTTOM)))
+					perplayershuffle |= 2;
+				if (!(option & (V_SNAPTOLEFT|V_SNAPTORIGHT)))
+					perplayershuffle |= 4;
+				cy += adjusty;
+				option &= ~V_SNAPTOTOP|V_SNAPTORIGHT;
+			}
+			else if (stplyr == &players[fourthdisplayplayer])
+			{
+				if (!(option & (V_SNAPTOTOP|V_SNAPTOBOTTOM)))
+					perplayershuffle |= 2;
+				if (!(option & (V_SNAPTOLEFT|V_SNAPTORIGHT)))
+					perplayershuffle |= 8;
+				cx += adjustx;
+				cy += adjusty;
+				option &= ~V_SNAPTOTOP|V_SNAPTOLEFT;
+			}
+		}
+		else
+#endif
+		// 2 players
+		{
+			if (stplyr == &players[displayplayer])
+			{
+				if (!(option & (V_SNAPTOTOP|V_SNAPTOBOTTOM)))
+					perplayershuffle = 1;
+				option &= ~V_SNAPTOBOTTOM;
+			}
+			else //if (stplyr == &players[secondarydisplayplayer])
+			{
+				if (!(option & (V_SNAPTOTOP|V_SNAPTOBOTTOM)))
+					perplayershuffle = 2;
+				cy += adjusty;
+				option &= ~V_SNAPTOTOP;
+			}
+		}
+	}
 
 	if (!(option & V_NOSCALESTART))
 	{
@@ -447,6 +521,10 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx));
 				else if (!(option & V_SNAPTOLEFT))
 					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx))/2;
+				if (perplayershuffle & 4)
+					cx -= ((float)vid.width - ((float)BASEVIDWIDTH * dupx))/4;
+				else if (perplayershuffle & 8)
+					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx))/4;
 			}
 			if (fabsf((float)vid.height - (float)BASEVIDHEIGHT * dupy) > 1.0E-36f)
 			{
@@ -454,23 +532,27 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy));
 				else if (!(option & V_SNAPTOTOP))
 					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy))/2;
+				if (perplayershuffle & 1)
+					cy -= ((float)vid.height - ((float)BASEVIDHEIGHT * dupy))/4;
+				else if (perplayershuffle & 2)
+					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy))/4;
 			}
 		}
 	}
 
-	fwidth = w;
-	fheight = h;
+	fwidth = FIXED_TO_FLOAT(w);
+	fheight = FIXED_TO_FLOAT(h);
 
-	if (sx + w > gpatch->width)
-		fwidth = gpatch->width - sx;
+	if (sx + w > gpatch->width<<FRACBITS)
+		fwidth = FIXED_TO_FLOAT((gpatch->width<<FRACBITS) - sx);
 
-	if (sy + h > gpatch->height)
-		fheight = gpatch->height - sy;
+	if (sy + h > gpatch->height<<FRACBITS)
+		fheight = FIXED_TO_FLOAT((gpatch->height<<FRACBITS) - sy);
 
-	if (pscale != FRACUNIT)
+	if (pscale != FRACUNIT || vscale != FRACUNIT || (splitscreen && option & V_PERPLAYER))
 	{
-		fwidth *=  fscale * dupx;
-		fheight *=  fscale * dupy;
+		fwidth *= fscalew * dupx;
+		fheight *= fscaleh * dupy;
 	}
 	else
 	{
@@ -495,17 +577,17 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 
 	v[0].z = v[1].z = v[2].z = v[3].z = 1.0f;
 
-	v[0].s = v[3].s = ((sx)/(float)(gpatch->width))*hwrPatch->max_s;
-	if (sx + w > gpatch->width)
+	v[0].s = v[3].s = (FIXED_TO_FLOAT(sx)/(float)(gpatch->width))*hwrPatch->max_s;
+	if (sx + w > gpatch->width<<FRACBITS)
 		v[2].s = v[1].s = hwrPatch->max_s;
 	else
-		v[2].s = v[1].s = ((sx+w)/(float)(gpatch->width))*hwrPatch->max_s;
+		v[2].s = v[1].s = (FIXED_TO_FLOAT(sx+w)/(float)(gpatch->width))*hwrPatch->max_s;
 
-	v[0].t = v[1].t = ((sy)/(float)(gpatch->height))*hwrPatch->max_t;
-	if (sy + h > gpatch->height)
+	v[0].t = v[1].t = (FIXED_TO_FLOAT(sy)/(float)(gpatch->height))*hwrPatch->max_t;
+	if (sy + h > gpatch->height<<FRACBITS)
 		v[2].t = v[3].t = hwrPatch->max_t;
 	else
-		v[2].t = v[3].t = ((sy+h)/(float)(gpatch->height))*hwrPatch->max_t;
+		v[2].t = v[3].t = (FIXED_TO_FLOAT(sy+h)/(float)(gpatch->height))*hwrPatch->max_t;
 
 	flags = PF_Translucent|PF_NoDepthTest;
 
@@ -514,6 +596,76 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 	if (option & V_WRAPY)
 		flags |= PF_ForceWrapY;
 
+	// Auto-crop at splitscreen borders!
+	if (splitscreen && (option & V_PERPLAYER))
+	{
+#define flerp(a,b,amount) (((a) * (1.0f - (amount))) + ((b) * (amount))) // Float lerp
+
+#ifdef QUADS
+		if (splitscreen > 1) // 3 or 4 players
+		{
+			#error Auto-cropping doesnt take quadscreen into account! Fix it!
+			// Hint: For player 1/2, copy player 1's code below. For player 3/4, copy player 2's code below
+			// For player 1/3 and 2/4, mangle the below code to apply horizontally instead of vertically
+		}
+		else
+#endif
+		// 2 players
+		{
+			if (stplyr == &players[displayplayer]) // Player 1's screen, crop at the bottom
+			{
+				if ((cy - fheight) < 0) // If the bottom is below the border
+				{
+					if (cy <= 0) // If the whole patch is beyond the border...
+						return; // ...crop away the entire patch, don't draw anything
+
+					if (fheight <= 0) // Don't divide by zero
+						return;
+
+					v[2].y = v[3].y = 0; // Clamp the polygon edge vertex position
+					// Now for the UV-map... Uh-oh, math time!
+
+					// On second thought, a basic linear interpolation suffices
+					//float full_height = fheight;
+					//float cropped_height = fheight - cy;
+					//float remaining_height = cy;
+					//float cropped_percentage = (fheight - cy) / fheight;
+					//float remaining_percentage = cy / fheight;
+					//v[2].t = v[3].t = lerp(v[2].t, v[0].t, cropped_percentage);
+					// By swapping v[2] and v[0], we can use remaining_percentage for less operations
+					//v[2].t = v[3].t = lerp(v[0].t, v[2].t, remaining_percentage);
+
+					v[2].t = v[3].t = flerp(v[0].t, v[2].t, cy/fheight);
+				}
+			}
+			else //if (stplyr == &players[secondarydisplayplayer]) // Player 2's screen, crop at the top
+			{
+				if (cy > 0) // If the top is above the border
+				{
+					if ((cy - fheight) >= 0) // If the whole patch is beyond the border...
+						return; // ...crop away the entire patch, don't draw anything
+
+					if (fheight <= 0) // Don't divide by zero
+						return;
+
+					v[0].y = v[1].y = 0; // Clamp the polygon edge vertex position
+					// Now for the UV-map... Uh-oh, math time!
+
+					// On second thought, a basic linear interpolation suffices
+					//float full_height = fheight;
+					//float cropped_height = cy;
+					//float remaining_height = fheight - cy;
+					//float cropped_percentage = cy / fheight;
+					//float remaining_percentage = (fheight - cy) / fheight;
+					//v[0].t = v[1].t = lerp(v[0].t, v[2].t, cropped_percentage);
+
+					v[0].t = v[1].t = flerp(v[0].t, v[2].t, cy/fheight);
+				}
+			}
+		}
+#undef flerp
+	}
+
 	// clip it since it is used for bunny scroll in doom I
 	if (alphalevel)
 	{
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index d413e3bbebff7d487f9756103631baf64d180822..9bade3d6fb19676cd988bc53fbce8ff8cd2a705e 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -147,22 +147,22 @@ static angle_t gl_aimingangle;
 static void HWR_SetTransformAiming(FTransform *trans, player_t *player, boolean skybox);
 
 // Render stats
-precise_t ps_hw_skyboxtime = 0;
-precise_t ps_hw_nodesorttime = 0;
-precise_t ps_hw_nodedrawtime = 0;
-precise_t ps_hw_spritesorttime = 0;
-precise_t ps_hw_spritedrawtime = 0;
+ps_metric_t ps_hw_skyboxtime = {0};
+ps_metric_t ps_hw_nodesorttime = {0};
+ps_metric_t ps_hw_nodedrawtime = {0};
+ps_metric_t ps_hw_spritesorttime = {0};
+ps_metric_t ps_hw_spritedrawtime = {0};
 
 // Render stats for batching
-int ps_hw_numpolys = 0;
-int ps_hw_numverts = 0;
-int ps_hw_numcalls = 0;
-int ps_hw_numshaders = 0;
-int ps_hw_numtextures = 0;
-int ps_hw_numpolyflags = 0;
-int ps_hw_numcolors = 0;
-precise_t ps_hw_batchsorttime = 0;
-precise_t ps_hw_batchdrawtime = 0;
+ps_metric_t ps_hw_numpolys = {0};
+ps_metric_t ps_hw_numverts = {0};
+ps_metric_t ps_hw_numcalls = {0};
+ps_metric_t ps_hw_numshaders = {0};
+ps_metric_t ps_hw_numtextures = {0};
+ps_metric_t ps_hw_numpolyflags = {0};
+ps_metric_t ps_hw_numcolors = {0};
+ps_metric_t ps_hw_batchsorttime = {0};
+ps_metric_t ps_hw_batchdrawtime = {0};
 
 boolean gl_init = false;
 boolean gl_maploaded = false;
@@ -3235,7 +3235,7 @@ static void HWR_Subsector(size_t num)
 		}
 
 		// for render stats
-		ps_numpolyobjects += numpolys;
+		ps_numpolyobjects.value.i += numpolys;
 
 		// Sort polyobjects
 		R_SortPolyObjects(sub);
@@ -3343,7 +3343,7 @@ static void HWR_RenderBSPNode(INT32 bspnum)
 	// Decide which side the view point is on
 	INT32 side;
 
-	ps_numbspcalls++;
+	ps_numbspcalls.value.i++;
 
 	// Found a subsector?
 	if (bspnum & NF_SUBSECTOR)
@@ -4718,7 +4718,7 @@ static void HWR_CreateDrawNodes(void)
 	// that is already lying around. This should all be in some sort of linked list or lists.
 	sortindex = Z_Calloc(sizeof(size_t) * (numplanes + numpolyplanes + numwalls), PU_STATIC, NULL);
 
-	ps_hw_nodesorttime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_nodesorttime);
 
 	for (i = 0; i < numplanes; i++, p++)
 	{
@@ -4738,7 +4738,7 @@ static void HWR_CreateDrawNodes(void)
 		sortindex[p] = p;
 	}
 
-	ps_numdrawnodes = p;
+	ps_numdrawnodes.value.i = p;
 
 	// p is the number of stuff to sort
 
@@ -4773,9 +4773,9 @@ static void HWR_CreateDrawNodes(void)
 		}
 	}
 
-	ps_hw_nodesorttime = I_GetPreciseTime() - ps_hw_nodesorttime;
+	PS_STOP_TIMING(ps_hw_nodesorttime);
 
-	ps_hw_nodedrawtime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_nodedrawtime);
 
 	// Okay! Let's draw it all! Woo!
 	HWD.pfnSetTransform(&atransform);
@@ -4812,7 +4812,7 @@ static void HWR_CreateDrawNodes(void)
 		}
 	}
 
-	ps_hw_nodedrawtime = I_GetPreciseTime() - ps_hw_nodedrawtime;
+	PS_STOP_TIMING(ps_hw_nodedrawtime);
 
 	numwalls = 0;
 	numplanes = 0;
@@ -6095,10 +6095,10 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	if (viewnumber == 0) // Only do it if it's the first screen being rendered
 		HWD.pfnClearBuffer(true, false, &ClearColor); // Clear the Color Buffer, stops HOMs. Also seems to fix the skybox issue on Intel GPUs.
 
-	ps_hw_skyboxtime = I_GetPreciseTime();
+	PS_START_TIMING(ps_hw_skyboxtime);
 	if (skybox && drawsky) // If there's a skybox and we should be drawing the sky, draw the skybox
 		HWR_RenderSkyboxView(viewnumber, player); // This is drawn before everything else so it is placed behind
-	ps_hw_skyboxtime = I_GetPreciseTime() - ps_hw_skyboxtime;
+	PS_STOP_TIMING(ps_hw_skyboxtime);
 
 	{
 		// do we really need to save player (is it not the same)?
@@ -6208,9 +6208,9 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	// Reset the shader state.
 	HWR_SetShaderState();
 
-	ps_numbspcalls = 0;
-	ps_numpolyobjects = 0;
-	ps_bsptime = I_GetPreciseTime();
+	ps_numbspcalls.value.i = 0;
+	ps_numpolyobjects.value.i = 0;
+	PS_START_TIMING(ps_bsptime);
 
 	validcount++;
 
@@ -6248,7 +6248,7 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	}
 #endif
 
-	ps_bsptime = I_GetPreciseTime() - ps_bsptime;
+	PS_STOP_TIMING(ps_bsptime);
 
 	if (cv_glbatching.value)
 		HWR_RenderBatches();
@@ -6263,22 +6263,22 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 #endif
 
 	// Draw MD2 and sprites
-	ps_numsprites = gl_visspritecount;
-	ps_hw_spritesorttime = I_GetPreciseTime();
+	ps_numsprites.value.i = gl_visspritecount;
+	PS_START_TIMING(ps_hw_spritesorttime);
 	HWR_SortVisSprites();
-	ps_hw_spritesorttime = I_GetPreciseTime() - ps_hw_spritesorttime;
-	ps_hw_spritedrawtime = I_GetPreciseTime();
+	PS_STOP_TIMING(ps_hw_spritesorttime);
+	PS_START_TIMING(ps_hw_spritedrawtime);
 	HWR_DrawSprites();
-	ps_hw_spritedrawtime = I_GetPreciseTime() - ps_hw_spritedrawtime;
+	PS_STOP_TIMING(ps_hw_spritedrawtime);
 
 #ifdef NEWCORONAS
 	//Hurdler: they must be drawn before translucent planes, what about gl fog?
 	HWR_DrawCoronas();
 #endif
 
-	ps_numdrawnodes = 0;
-	ps_hw_nodesorttime = 0;
-	ps_hw_nodedrawtime = 0;
+	ps_numdrawnodes.value.i = 0;
+	ps_hw_nodesorttime.value.p = 0;
+	ps_hw_nodedrawtime.value.p = 0;
 	if (numplanes || numpolyplanes || numwalls) //Hurdler: render 3D water and transparent walls after everything
 	{
 		HWR_CreateDrawNodes();
@@ -6767,7 +6767,7 @@ void HWR_LoadAllCustomShaders(void)
 
 	// read every custom shader
 	for (i = 0; i < numwadfiles; i++)
-		HWR_LoadCustomShadersFromFile(i, (wadfiles[i]->type == RET_PK3));
+		HWR_LoadCustomShadersFromFile(i, W_FileHasFolders(wadfiles[i]));
 }
 
 void HWR_LoadCustomShadersFromFile(UINT16 wadnum, boolean PK3)
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index 218650b53283a6d285f5a8e79d4fe04a8463cca7..3f90f0ae17a58070948148f168c48f248510ce61 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -20,6 +20,8 @@
 #include "../d_player.h"
 #include "../r_defs.h"
 
+#include "../m_perfstats.h"
+
 // Startup & Shutdown the hardware mode renderer
 void HWR_Startup(void);
 void HWR_Switch(void);
@@ -39,7 +41,7 @@ void HWR_InitTextureMapping(void);
 void HWR_SetViewSize(void);
 void HWR_DrawPatch(patch_t *gpatch, INT32 x, INT32 y, INT32 option);
 void HWR_DrawStretchyFixedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale, fixed_t vscale, INT32 option, const UINT8 *colormap);
-void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t scale, INT32 option, fixed_t sx, fixed_t sy, fixed_t w, fixed_t h);
+void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale, fixed_t vscale, INT32 option, const UINT8 *colormap, fixed_t sx, fixed_t sy, fixed_t w, fixed_t h);
 void HWR_MakePatch(const patch_t *patch, GLPatch_t *grPatch, GLMipmap_t *grMipmap, boolean makebitmap);
 void HWR_CreatePlanePolygons(INT32 bspnum);
 void HWR_CreateStaticLightmaps(INT32 bspnum);
@@ -116,22 +118,22 @@ extern FTransform atransform;
 
 
 // Render stats
-extern precise_t ps_hw_skyboxtime;
-extern precise_t ps_hw_nodesorttime;
-extern precise_t ps_hw_nodedrawtime;
-extern precise_t ps_hw_spritesorttime;
-extern precise_t ps_hw_spritedrawtime;
+extern ps_metric_t ps_hw_skyboxtime;
+extern ps_metric_t ps_hw_nodesorttime;
+extern ps_metric_t ps_hw_nodedrawtime;
+extern ps_metric_t ps_hw_spritesorttime;
+extern ps_metric_t ps_hw_spritedrawtime;
 
 // Render stats for batching
-extern int ps_hw_numpolys;
-extern int ps_hw_numverts;
-extern int ps_hw_numcalls;
-extern int ps_hw_numshaders;
-extern int ps_hw_numtextures;
-extern int ps_hw_numpolyflags;
-extern int ps_hw_numcolors;
-extern precise_t ps_hw_batchsorttime;
-extern precise_t ps_hw_batchdrawtime;
+extern ps_metric_t ps_hw_numpolys;
+extern ps_metric_t ps_hw_numverts;
+extern ps_metric_t ps_hw_numcalls;
+extern ps_metric_t ps_hw_numshaders;
+extern ps_metric_t ps_hw_numtextures;
+extern ps_metric_t ps_hw_numpolyflags;
+extern ps_metric_t ps_hw_numcolors;
+extern ps_metric_t ps_hw_batchsorttime;
+extern ps_metric_t ps_hw_batchdrawtime;
 
 extern boolean gl_init;
 extern boolean gl_maploaded;
diff --git a/src/hardware/r_opengl/r_opengl.c b/src/hardware/r_opengl/r_opengl.c
index 645a3bbaeca39338b1112e29d1920c9ef4ef9c6e..de0e8c6a65aab0c1cbdc8e6e7f77cccd9118c6fd 100644
--- a/src/hardware/r_opengl/r_opengl.c
+++ b/src/hardware/r_opengl/r_opengl.c
@@ -62,6 +62,9 @@ static  FBITFIELD   CurrentPolyFlags;
 static FTextureInfo *TexCacheTail = NULL;
 static FTextureInfo *TexCacheHead = NULL;
 
+static RGBA_t *textureBuffer = NULL;
+static size_t textureBufferSize = 0;
+
 RGBA_t  myPaletteData[256];
 GLint   screen_width    = 0;               // used by Draw2DLine()
 GLint   screen_height   = 0;
@@ -131,7 +134,6 @@ static const GLfloat byte2float[256] = {
 // -----------------+
 // GL_DBG_Printf    : Output debug messages to debug log if DEBUG_TO_FILE is defined,
 //                  : else do nothing
-// Returns          :
 // -----------------+
 
 #ifdef DEBUG_TO_FILE
@@ -159,8 +161,6 @@ FUNCPRINTF void GL_DBG_Printf(const char *format, ...)
 
 // -----------------+
 // GL_MSG_Warning   : Raises a warning.
-//                  :
-// Returns          :
 // -----------------+
 
 static void GL_MSG_Warning(const char *format, ...)
@@ -184,8 +184,6 @@ static void GL_MSG_Warning(const char *format, ...)
 
 // -----------------+
 // GL_MSG_Error     : Raises an error.
-//                  :
-// Returns          :
 // -----------------+
 
 static void GL_MSG_Error(const char *format, ...)
@@ -1345,6 +1343,10 @@ void Flush(void)
 
 	TexCacheTail = TexCacheHead = NULL; //Hurdler: well, TexCacheHead is already NULL
 	tex_downloaded = 0;
+
+	free(textureBuffer);
+	textureBuffer = NULL;
+	textureBufferSize = 0;
 }
 
 
@@ -1378,7 +1380,6 @@ INT32 isExtAvailable(const char *extension, const GLubyte *start)
 
 // -----------------+
 // Init             : Initialise the OpenGL interface API
-// Returns          :
 // -----------------+
 EXPORT boolean HWRAPI(Init) (void)
 {
@@ -1738,37 +1739,48 @@ EXPORT void HWRAPI(SetBlend) (FBITFIELD PolyFlags)
 	CurrentPolyFlags = PolyFlags;
 }
 
+static void AllocTextureBuffer(GLMipmap_t *pTexInfo)
+{
+	size_t size = pTexInfo->width * pTexInfo->height;
+	if (size > textureBufferSize)
+	{
+		textureBuffer = realloc(textureBuffer, size * sizeof(RGBA_t));
+		if (textureBuffer == NULL)
+			I_Error("AllocTextureBuffer: out of memory allocating %s bytes", sizeu1(size * sizeof(RGBA_t)));
+		textureBufferSize = size;
+	}
+}
+
 // -----------------+
-// UpdateTexture    : Updates the texture data.
+// UpdateTexture    : Updates texture data.
 // -----------------+
 EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 {
-	// Download a mipmap
-	boolean updatemipmap = true;
-	static RGBA_t   tex[2048*2048];
-	const GLvoid   *ptex = tex;
-	INT32             w, h;
-	GLuint texnum = 0;
+	// Upload a texture
+	GLuint num = pTexInfo->downloaded;
+	boolean update = true;
+
+	INT32 w = pTexInfo->width, h = pTexInfo->height;
+	INT32 i, j;
+
+	const GLubyte *pImgData = (const GLubyte *)pTexInfo->data;
+	const GLvoid *ptex = NULL;
+	RGBA_t *tex = NULL;
 
-	if (!pTexInfo->downloaded)
+	// Generate a new texture name.
+	if (!num)
 	{
-		pglGenTextures(1, &texnum);
-		pTexInfo->downloaded = texnum;
-		updatemipmap = false;
+		pglGenTextures(1, &num);
+		pTexInfo->downloaded = num;
+		update = false;
 	}
-	else
-		texnum = pTexInfo->downloaded;
-
-	//GL_DBG_Printf ("DownloadMipmap %d %x\n",(INT32)texnum,pTexInfo->data);
 
-	w = pTexInfo->width;
-	h = pTexInfo->height;
+	//GL_DBG_Printf("UpdateTexture %d %x\n", (INT32)num, pImgData);
 
-	if ((pTexInfo->format == GL_TEXFMT_P_8) ||
-		(pTexInfo->format == GL_TEXFMT_AP_88))
+	if ((pTexInfo->format == GL_TEXFMT_P_8) || (pTexInfo->format == GL_TEXFMT_AP_88))
 	{
-		const GLubyte *pImgData = (const GLubyte *)pTexInfo->data;
-		INT32 i, j;
+		AllocTextureBuffer(pTexInfo);
+		ptex = tex = textureBuffer;
 
 		for (j = 0; j < h; j++)
 		{
@@ -1799,20 +1811,18 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 						tex[w*j+i].s.alpha = *pImgData;
 					pImgData++;
 				}
-
 			}
 		}
 	}
 	else if (pTexInfo->format == GL_TEXFMT_RGBA)
 	{
-		// corona test : passed as ARGB 8888, which is not in glide formats
-		// Hurdler: not used for coronas anymore, just for dynamic lighting
-		ptex = pTexInfo->data;
+		// Directly upload the texture data without any kind of conversion.
+		ptex = pImgData;
 	}
 	else if (pTexInfo->format == GL_TEXFMT_ALPHA_INTENSITY_88)
 	{
-		const GLubyte *pImgData = (const GLubyte *)pTexInfo->data;
-		INT32 i, j;
+		AllocTextureBuffer(pTexInfo);
+		ptex = tex = textureBuffer;
 
 		for (j = 0; j < h; j++)
 		{
@@ -1829,8 +1839,8 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 	}
 	else if (pTexInfo->format == GL_TEXFMT_ALPHA_8) // Used for fade masks
 	{
-		const GLubyte *pImgData = (const GLubyte *)pTexInfo->data;
-		INT32 i, j;
+		AllocTextureBuffer(pTexInfo);
+		ptex = tex = textureBuffer;
 
 		for (j = 0; j < h; j++)
 		{
@@ -1845,11 +1855,10 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 		}
 	}
 	else
-		GL_MSG_Warning ("SetTexture(bad format) %ld\n", pTexInfo->format);
+		GL_MSG_Warning("UpdateTexture: bad format %d\n", pTexInfo->format);
 
-	// the texture number was already generated by pglGenTextures
-	pglBindTexture(GL_TEXTURE_2D, texnum);
-	tex_downloaded = texnum;
+	pglBindTexture(GL_TEXTURE_2D, num);
+	tex_downloaded = num;
 
 	// disable texture filtering on any texture that has holes so there's no dumb borders or blending issues
 	if (pTexInfo->flags & TF_TRANSPARENT)
@@ -1878,7 +1887,7 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 		}
 		else
 		{
-			if (updatemipmap)
+			if (update)
 				pglTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
 			else
 				pglTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
@@ -1899,7 +1908,7 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 		}
 		else
 		{
-			if (updatemipmap)
+			if (update)
 				pglTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
 			else
 				pglTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
@@ -1919,7 +1928,7 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 		}
 		else
 		{
-			if (updatemipmap)
+			if (update)
 				pglTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
 			else
 				pglTexImage2D(GL_TEXTURE_2D, 0, textureformatGL, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index e0eaf8fb178f1252b60b93e329d2f14d05858e53..f4c5e4c3b14cbef8dd9bc39b241b30358166bea9 100644
--- a/src/hu_stuff.c
+++ b/src/hu_stuff.c
@@ -936,7 +936,7 @@ void HU_Ticker(void)
 	hu_tick++;
 	hu_tick &= 7; // currently only to blink chat input cursor
 
-	if (PLAYER1INPUTDOWN(gc_scores))
+	if (PLAYER1INPUTDOWN(GC_SCORES))
 		hu_showscores = !chat_on;
 	else
 		hu_showscores = false;
@@ -1111,26 +1111,26 @@ boolean HU_Responder(event_t *ev)
 	// (Unless if you're sharing a keyboard, since you probably establish when you start chatting that you have dibs on it...)
 	// (Ahhh, the good ol days when I was a kid who couldn't afford an extra USB controller...)
 
-	if (ev->data1 >= KEY_MOUSE1)
+	if (ev->key >= KEY_MOUSE1)
 	{
 		INT32 i;
-		for (i = 0; i < num_gamecontrols; i++)
+		for (i = 0; i < NUM_GAMECONTROLS; i++)
 		{
-			if (gamecontrol[i][0] == ev->data1 || gamecontrol[i][1] == ev->data1)
+			if (gamecontrol[i][0] == ev->key || gamecontrol[i][1] == ev->key)
 				break;
 		}
 
-		if (i == num_gamecontrols)
+		if (i == NUM_GAMECONTROLS)
 			return false;
 	}*/	//We don't actually care about that unless we get splitscreen netgames. :V
 
 #ifndef NONET
-	c = (INT32)ev->data1;
+	c = (INT32)ev->key;
 
 	if (!chat_on)
 	{
 		// enter chat mode
-		if ((ev->data1 == gamecontrol[gc_talkkey][0] || ev->data1 == gamecontrol[gc_talkkey][1])
+		if ((ev->key == gamecontrol[GC_TALKKEY][0] || ev->key == gamecontrol[GC_TALKKEY][1])
 			&& netgame && !OLD_MUTE) // check for old chat mute, still let the players open the chat incase they want to scroll otherwise.
 		{
 			chat_on = true;
@@ -1140,7 +1140,7 @@ boolean HU_Responder(event_t *ev)
 			typelines = 1;
 			return true;
 		}
-		if ((ev->data1 == gamecontrol[gc_teamkey][0] || ev->data1 == gamecontrol[gc_teamkey][1])
+		if ((ev->key == gamecontrol[GC_TEAMKEY][0] || ev->key == gamecontrol[GC_TEAMKEY][1])
 			&& netgame && !OLD_MUTE)
 		{
 			chat_on = true;
@@ -1157,12 +1157,12 @@ boolean HU_Responder(event_t *ev)
 		// Ignore modifier keys
 		// Note that we do this here so users can still set
 		// their chat keys to one of these, if they so desire.
-		if (ev->data1 == KEY_LSHIFT || ev->data1 == KEY_RSHIFT
-		 || ev->data1 == KEY_LCTRL || ev->data1 == KEY_RCTRL
-		 || ev->data1 == KEY_LALT || ev->data1 == KEY_RALT)
+		if (ev->key == KEY_LSHIFT || ev->key == KEY_RSHIFT
+		 || ev->key == KEY_LCTRL || ev->key == KEY_RCTRL
+		 || ev->key == KEY_LALT || ev->key == KEY_RALT)
 			return true;
 
-		c = (INT32)ev->data1;
+		c = (INT32)ev->key;
 
 		// I know this looks very messy but this works. If it ain't broke, don't fix it!
 		// shift LETTERS to uppercase if we have capslock or are holding shift
@@ -1234,8 +1234,8 @@ boolean HU_Responder(event_t *ev)
 			I_UpdateMouseGrab();
 		}
 		else if (c == KEY_ESCAPE
-			|| ((c == gamecontrol[gc_talkkey][0] || c == gamecontrol[gc_talkkey][1]
-			|| c == gamecontrol[gc_teamkey][0] || c == gamecontrol[gc_teamkey][1])
+			|| ((c == gamecontrol[GC_TALKKEY][0] || c == gamecontrol[GC_TALKKEY][1]
+			|| c == gamecontrol[GC_TEAMKEY][0] || c == gamecontrol[GC_TEAMKEY][1])
 			&& c >= KEY_MOUSE1)) // If it's not a keyboard key, then the chat button is used as a toggle.
 		{
 			chat_on = false;
@@ -2104,7 +2104,7 @@ void HU_Drawer(void)
 		}
 		else
 			HU_DrawCoopOverlay();
-		LUAh_ScoresHUD();
+		LUA_HUDHOOK(scores);
 	}
 
 	if (gamestate != GS_LEVEL)
diff --git a/src/i_system.h b/src/i_system.h
index e046fd620114161ddf49e7bba50685547cd7d32a..a2dd81cca3ef815ec6121d09b64308f54c6adc3e 100644
--- a/src/i_system.h
+++ b/src/i_system.h
@@ -50,7 +50,7 @@ tic_t I_GetTime(void);
   */
 precise_t I_GetPreciseTime(void);
 
-/**	\brief	Returns the difference between precise times as microseconds.
+/**	\brief	Converts a precise_t to microseconds and casts it to a 32 bit integer.
   */
 int I_PreciseToMicros(precise_t);
 
@@ -318,10 +318,6 @@ void I_RegisterSysCommands(void);
 */
 void I_GetCursorPosition(INT32 *x, INT32 *y);
 
-/** \brief Returns whether the mouse is grabbed
-*/
-boolean I_GetMouseGrab(void);
-
 /** \brief Sets whether the mouse is grabbed
 */
 void I_SetMouseGrab(boolean grab);
diff --git a/src/info.c b/src/info.c
index efcf1c044141225bf9fd64e1af619548df2c7298..f56e5d78e3e786b33806a1a596f0051a182144fd 100644
--- a/src/info.c
+++ b/src/info.c
@@ -2069,7 +2069,7 @@ state_t states[NUMSTATES] =
 	{SPR_TVFL, 2, 18, {A_GiveShield}, SH_FLAMEAURA, 0, S_NULL}, // S_FLAMEAURA_ICON2
 
 	{SPR_TVBB, FF_ANIMATE|2, 18, {NULL}, 3, 4, S_BUBBLEWRAP_ICON2}, // S_BUBBLEWRAP_ICON1
-	{SPR_TVBB, 2, 18, {A_GiveShield}, SH_BUBBLEWRAP, 0, S_NULL}, // S_BUBBLERWAP_ICON2
+	{SPR_TVBB, 2, 18, {A_GiveShield}, SH_BUBBLEWRAP, 0, S_NULL}, // S_BUBBLEWRAP_ICON2
 
 	{SPR_TVZP, FF_ANIMATE|2, 18, {NULL}, 3, 4, S_THUNDERCOIN_ICON2}, // S_THUNDERCOIN_ICON1
 	{SPR_TVZP, 2, 18, {A_GiveShield}, SH_THUNDERCOIN, 0, S_NULL}, // S_THUNDERCOIN_ICON2
@@ -5199,7 +5199,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		24*FRACUNIT,    // radius
 		34*FRACUNIT,    // height
 		0,              // display offset
-		100,            // mass
+		DMG_FIRE,       // mass
 		0,              // damage
 		sfx_None,       // activesound
 		MF_NOGRAVITY|MF_NOBLOCKMAP|MF_FIRE|MF_PAIN, // flags
@@ -7974,7 +7974,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		8*FRACUNIT,     // radius
 		32*FRACUNIT,    // height
 		0,              // display offset
-		4,              // mass
+		DMG_SPIKE,      // mass
 		0,              // damage
 		sfx_None,       // activesound
 		MF_NOBLOCKMAP|MF_SCENERY|MF_NOCLIPHEIGHT,  // flags
@@ -8001,7 +8001,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		16*FRACUNIT,    // radius
 		14*FRACUNIT,    // height
 		0,              // display offset
-		4,              // mass
+		DMG_SPIKE,      // mass
 		0,              // damage
 		sfx_None,       // activesound
 		MF_NOBLOCKMAP|MF_NOGRAVITY|MF_SCENERY|MF_NOCLIPHEIGHT|MF_PAPERCOLLISION,  // flags
@@ -11430,7 +11430,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		17*FRACUNIT,    // radius
 		34*FRACUNIT,    // height
 		1,              // display offset
-		0,              // mass
+		DMG_SPIKE,      // mass
 		1,              // damage
 		sfx_s3kc9s, //sfx_mswing, -- activesound
 		MF_SCENERY|MF_PAIN|MF_NOGRAVITY, // flags
@@ -11457,7 +11457,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		34*FRACUNIT,    // radius
 		68*FRACUNIT,    // height
 		1,              // display offset
-		0,              // mass
+		DMG_SPIKE,      // mass
 		1,              // damage
 		sfx_s3kc9s, //sfx_mswing, -- activesound
 		MF_SCENERY|MF_PAIN|MF_NOGRAVITY, // flags
@@ -13401,7 +13401,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		30*FRACUNIT,    // radius
 		48*FRACUNIT,    // height
 		0,              // display offset
-		100,            // mass
+		DMG_FIRE,       // mass
 		0,              // damage
 		sfx_None,       // activesound
 		MF_SPECIAL|MF_PAIN|MF_NOGRAVITY|MF_FIRE, // flags
@@ -13806,7 +13806,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		8*FRACUNIT,     // radius
 		32*FRACUNIT,    // height
 		0,              // display offset
-		0,       // mass
+		0,              // mass
 		0,              // damage
 		sfx_None,       // activesound
 		MF_NOGRAVITY|MF_PAIN, // flags
@@ -20380,7 +20380,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		18*FRACUNIT,    // radius
 		28*FRACUNIT,    // height
 		0,              // display offset
-		0,              // mass
+		DMG_SPIKE,      // mass
 		0,              // damage
 		sfx_None,       // activesound
 		MF_NOGRAVITY|MF_PAIN, // flags
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index d6f40846c66dc5ef6b9a4a02abab6de42352a213..12ad4fee0549bbaad9d5ea2dd3106c9bd9b8f955 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -29,6 +29,9 @@
 #include "d_netcmd.h" // IsPlayerAdmin
 #include "m_menu.h" // Player Setup menu color stuff
 #include "m_misc.h" // M_MapNumber
+#include "b_bot.h" // B_UpdateBotleader
+#include "d_clisrv.h" // CL_RemovePlayer
+#include "i_system.h" // I_GetPreciseTime, I_PreciseToMicros
 
 #include "lua_script.h"
 #include "lua_libs.h"
@@ -185,6 +188,8 @@ static const struct {
 	{META_MAPHEADER,    "mapheader_t"},
 
 	{META_POLYOBJ,      "polyobj_t"},
+	{META_POLYOBJVERTICES, "polyobj_t.vertices"},
+	{META_POLYOBJLINES, "polyobj_t.lines"},
 
 	{META_CVAR,         "consvar_t"},
 
@@ -214,6 +219,7 @@ static const struct {
 
 	{META_LUABANKS,     "luabanks[]"},
 
+	{META_KEYEVENT,     "keyevent_t"},
 	{META_MOUSE,        "mouse_t"},
 	{NULL,              NULL}
 };
@@ -1883,6 +1889,37 @@ static int lib_pDoSpring(lua_State *L)
 	return 1;
 }
 
+static int lib_pTryCameraMove(lua_State *L)
+{
+	camera_t *cam = *((camera_t **)luaL_checkudata(L, 1, META_CAMERA));
+	fixed_t x = luaL_checkfixed(L, 2);
+	fixed_t y = luaL_checkfixed(L, 3);
+
+	if (!cam)
+		return LUA_ErrInvalid(L, "camera_t");
+	lua_pushboolean(L, P_TryCameraMove(x, y, cam));
+	return 1;
+}
+
+static int lib_pTeleportCameraMove(lua_State *L)
+{
+	camera_t *cam = *((camera_t **)luaL_checkudata(L, 1, META_CAMERA));
+	fixed_t x = luaL_checkfixed(L, 2);
+	fixed_t y = luaL_checkfixed(L, 3);
+	fixed_t z = luaL_checkfixed(L, 4);
+
+	if (!cam)
+		return LUA_ErrInvalid(L, "camera_t");
+	cam->x = x;
+	cam->y = y;
+	cam->z = z;
+	P_CheckCameraPosition(x, y, cam);
+	cam->subsector = R_PointInSubsector(x, y);
+	cam->floorz = tmfloorz;
+	cam->ceilingz = tmceilingz;
+	return 0;
+}
+
 // P_INTER
 ////////////
 
@@ -3397,6 +3434,111 @@ static int lib_gAddGametype(lua_State *L)
 	return 0;
 }
 
+// Bot adding function!
+// Partly lifted from Got_AddPlayer
+static int lib_gAddPlayer(lua_State *L)
+{
+	INT16 i, newplayernum, botcount = 1;
+	player_t *newplayer;
+	SINT8 skinnum = 0, bot;
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!playeringame[i])
+			break;
+
+		if (players[i].bot)
+			botcount++; // How many of us are there already?
+	}
+	if (i >= MAXPLAYERS)
+	{
+		lua_pushnil(L);
+		return 1;
+	}
+	
+
+	newplayernum = i;
+
+	CL_ClearPlayer(newplayernum);
+
+	playeringame[newplayernum] = true;
+	G_AddPlayer(newplayernum);
+	newplayer = &players[newplayernum];
+
+	newplayer->jointime = 0;
+	newplayer->quittime = 0;
+
+	// Set the bot name (defaults to Bot #)
+	strcpy(player_names[newplayernum], va("Bot %d", botcount));
+
+	// Read the skin argument (defaults to Sonic)
+	if (!lua_isnoneornil(L, 1))
+	{
+		skinnum = R_SkinAvailable(luaL_checkstring(L, 1));
+		skinnum = skinnum < 0 ? 0 : skinnum;
+	}
+
+	// Read the color (defaults to skin prefcolor)
+	if (!lua_isnoneornil(L, 2))
+		newplayer->skincolor = R_GetColorByName(luaL_checkstring(L, 2));
+	else
+		newplayer->skincolor = skins[newplayer->skin].prefcolor;
+
+	// Read the bot name, if given
+	if (!lua_isnoneornil(L, 3))
+		strcpy(player_names[newplayernum], luaL_checkstring(L, 3));
+	
+	bot = luaL_optinteger(L, 4, 3);
+	newplayer->bot = (bot >= BOT_NONE && bot <= BOT_MPAI) ? bot : BOT_MPAI;
+	
+	// If our bot is a 2P type, we'll need to set its leader so it can spawn
+	if (newplayer->bot == BOT_2PAI || newplayer->bot == BOT_2PHUMAN)
+		B_UpdateBotleader(newplayer);
+	
+	// Set the skin (can't do this until AFTER bot type is set!)
+	SetPlayerSkinByNum(newplayernum, skinnum);
+
+
+	if (netgame)
+	{
+		char joinmsg[256];
+
+		strcpy(joinmsg, M_GetText("\x82*Bot %s has joined the game (player %d)"));
+		strcpy(joinmsg, va(joinmsg, player_names[newplayernum], newplayernum));
+		HU_AddChatText(joinmsg, false);
+	}
+	
+	LUA_PushUserdata(L, newplayer, META_PLAYER);
+	return 1;
+}
+
+
+// Bot removing function
+static int lib_gRemovePlayer(lua_State *L)
+{
+	UINT8 pnum = -1;
+	if (!lua_isnoneornil(L, 1))
+		pnum = luaL_checkinteger(L, 1);
+	else // No argument
+		return luaL_error(L, "argument #1 not given (expected number)");
+	if (pnum >= MAXPLAYERS) // Out of range
+		return luaL_error(L, "playernum %d out of range (0 - %d)", pnum, MAXPLAYERS-1);
+	if (playeringame[pnum]) // Found player
+	{
+		if (players[pnum].bot == BOT_NONE) // Can't remove clients.
+			return luaL_error(L, "G_RemovePlayer can only be used on players with a bot value other than BOT_NONE.");
+		else
+		{
+			players[pnum].removing = true;
+			lua_pushboolean(L, true);
+			return 1;
+		}
+	}
+	// Fell through. Invalid player
+	return LUA_ErrInvalid(L, "player_t");
+}
+
+
 static int Lcheckmapnumber (lua_State *L, int idx, const char *fun)
 {
 	if (ISINLEVEL)
@@ -3738,6 +3880,12 @@ static int lib_gTicsToMilliseconds(lua_State *L)
 	return 1;
 }
 
+static int lib_getTimeMicros(lua_State *L)
+{
+	lua_pushinteger(L, I_PreciseToMicros(I_GetPreciseTime()));
+	return 1;
+}
+
 static luaL_Reg lib[] = {
 	{"print", lib_print},
 	{"chatprint", lib_chatprint},
@@ -3881,6 +4029,8 @@ static luaL_Reg lib[] = {
 	{"P_FloorzAtPos",lib_pFloorzAtPos},
 	{"P_CeilingzAtPos",lib_pCeilingzAtPos},
 	{"P_DoSpring",lib_pDoSpring},
+	{"P_TryCameraMove", lib_pTryCameraMove},
+	{"P_TeleportCameraMove", lib_pTeleportCameraMove},
 
 	// p_inter
 	{"P_RemoveShield",lib_pRemoveShield},
@@ -3983,6 +4133,8 @@ static luaL_Reg lib[] = {
 
 	// g_game
 	{"G_AddGametype", lib_gAddGametype},
+	{"G_AddPlayer", lib_gAddPlayer},
+	{"G_RemovePlayer", lib_gRemovePlayer},
 	{"G_BuildMapName",lib_gBuildMapName},
 	{"G_BuildMapTitle",lib_gBuildMapTitle},
 	{"G_FindMap",lib_gFindMap},
@@ -4008,6 +4160,8 @@ static luaL_Reg lib[] = {
 	{"G_TicsToCentiseconds",lib_gTicsToCentiseconds},
 	{"G_TicsToMilliseconds",lib_gTicsToMilliseconds},
 
+	{"getTimeMicros",lib_getTimeMicros},
+
 	{NULL, NULL}
 };
 
diff --git a/src/lua_consolelib.c b/src/lua_consolelib.c
index 414d9692a08fea3681a8ea0bdba654c022611440..2b8cad69b8b5fb920294bdc4e1c0475fa6a59da0 100644
--- a/src/lua_consolelib.c
+++ b/src/lua_consolelib.c
@@ -433,7 +433,7 @@ static int CVarSetFunction
 	consvar_t *cvar = *(consvar_t **)luaL_checkudata(L, 1, META_CVAR);
 
 	if (cvar->flags & CV_NOLUA)
-		return luaL_error(L, "Variable %s cannot be set from Lua.", cvar->name);
+		return luaL_error(L, "Variable '%s' cannot be set from Lua.", cvar->name);
 
 	switch (lua_type(L, 2))
 	{
diff --git a/src/lua_hook.h b/src/lua_hook.h
index e3af951f5fd15444a8d3907b3dfb5f750ec95b4a..531d16288ab507e19dc6f47e92e0157772306aa8 100644
--- a/src/lua_hook.h
+++ b/src/lua_hook.h
@@ -13,6 +13,7 @@
 #include "r_defs.h"
 #include "d_player.h"
 #include "s_sound.h"
+#include "d_event.h"
 
 /*
 Do you know what an 'X Macro' is? Such a macro is called over each element of
@@ -78,6 +79,13 @@ automatically.
 	X (LinedefExecute),\
 	X (ShouldJingleContinue),/* should jingle of the given music continue playing */\
 
+#define HUD_HOOK_LIST(X) \
+	X (game),\
+	X (scores),/* emblems/multiplayer list */\
+	X (title),/* titlescreen */\
+	X (titlecard),\
+	X (intermission),\
+
 /*
 I chose to access the hook enums through a macro as well. This could provide
 a hint to lookup the macro's definition instead of the enum's definition.
@@ -88,18 +96,26 @@ grepped and found in the lists above.
 
 #define   MOBJ_HOOK(name)   mobjhook_ ## name
 #define        HOOK(name)       hook_ ## name
+#define    HUD_HOOK(name)    hudhook_ ## name
 #define STRING_HOOK(name) stringhook_ ## name
 
-enum {   MOBJ_HOOK_LIST   (MOBJ_HOOK)    MOBJ_HOOK(MAX) };
-enum {        HOOK_LIST        (HOOK)         HOOK(MAX) };
-enum { STRING_HOOK_LIST (STRING_HOOK)  STRING_HOOK(MAX) };
+#define ENUM(X) enum { X ## _LIST (X)  X(MAX) }
+
+ENUM   (MOBJ_HOOK);
+ENUM        (HOOK);
+ENUM    (HUD_HOOK);
+ENUM (STRING_HOOK);
+
+#undef ENUM
 
 /* dead simple, LUA_HOOK(GameQuit) */
 #define LUA_HOOK(type) LUA_HookVoid(HOOK(type))
+#define LUA_HUDHOOK(type) LUA_HookHUD(HUD_HOOK(type))
 
 extern boolean hook_cmd_running;
 
 void LUA_HookVoid(int hook);
+void LUA_HookHUD(int hook);
 
 int  LUA_HookMobj(mobj_t *, int hook);
 int  LUA_Hook2Mobj(mobj_t *, mobj_t *, int hook);
@@ -107,6 +123,7 @@ void LUA_HookInt(INT32 integer, int hook);
 void LUA_HookBool(boolean value, int hook);
 int  LUA_HookPlayer(player_t *, int hook);
 int  LUA_HookTiccmd(player_t *, ticcmd_t *, int hook);
+int  LUA_HookKey(event_t *event, int hook); // Hooks for key events
 
 void LUA_HookThinkFrame(void);
 int  LUA_HookMobjLineCollide(mobj_t *, line_t *);
@@ -114,6 +131,7 @@ int  LUA_HookTouchSpecial(mobj_t *special, mobj_t *toucher);
 int  LUA_HookShouldDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype);
 int  LUA_HookMobjDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype);
 int  LUA_HookMobjDeath(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damagetype);
+int  LUA_HookMobjMoveBlocked(mobj_t *, mobj_t *, line_t *);
 int  LUA_HookBotAI(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd);
 void LUA_HookLinedefExecute(line_t *, mobj_t *, sector_t *);
 int  LUA_HookPlayerMsg(int source, int target, int flags, char *msg);
@@ -130,4 +148,3 @@ int  LUA_HookPlayerCmd(player_t *, ticcmd_t *);
 int  LUA_HookMusicChange(const char *oldname, struct MusicChange *);
 fixed_t LUA_HookPlayerHeight(player_t *player);
 int  LUA_HookPlayerCanEnterSpinGaps(player_t *player);
-int  LUA_HookKey(INT32 keycode, int hooktype); // Hooks for key events
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 39aa5ea18048dc74f96caadd7bdc828b79b0327c..a72b22b5a62b953bbccde13f271c76fb6e24cad1 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -31,12 +31,15 @@
                                   ABSTRACTION
    ========================================================================= */
 
-static const char * const mobjHookNames[] = { MOBJ_HOOK_LIST (TOSTR)  NULL };
-static const char * const     hookNames[] = {      HOOK_LIST (TOSTR)  NULL };
+#define LIST(id, M) \
+	static const char * const id [] = { M (TOSTR)  NULL }
 
-static const char * const stringHookNames[] = {
-	STRING_HOOK_LIST (TOSTR)  NULL
-};
+LIST   (mobjHookNames,   MOBJ_HOOK_LIST);
+LIST       (hookNames,        HOOK_LIST);
+LIST    (hudHookNames,    HUD_HOOK_LIST);
+LIST (stringHookNames, STRING_HOOK_LIST);
+
+#undef LIST
 
 typedef struct {
 	int numHooks;
@@ -49,6 +52,7 @@ typedef struct {
 } stringhook_t;
 
 static hook_t hookIds[HOOK(MAX)];
+static hook_t hudHookIds[HUD_HOOK(MAX)];
 static hook_t mobjHookIds[NUMMOBJTYPES][MOBJ_HOOK(MAX)];
 
 // Lua tables are used to lookup string hook ids.
@@ -56,6 +60,7 @@ static stringhook_t stringHooks[STRING_HOOK(MAX)];
 
 // This will be indexed by hook id, the value of which fetches the registry.
 static int * hookRefs;
+static int   nextid;
 
 // After a hook errors once, don't print the error again.
 static UINT8 * hooksErrored;
@@ -104,13 +109,13 @@ static void get_table(lua_State *L)
 	lua_remove(L, -2);
 }
 
-static void add_hook_to_table(lua_State *L, int id, int n)
+static void add_hook_to_table(lua_State *L, int n)
 {
-	lua_pushnumber(L, id);
+	lua_pushnumber(L, nextid);
 	lua_rawseti(L, -2, n);
 }
 
-static void add_string_hook(lua_State *L, int type, int id)
+static void add_string_hook(lua_State *L, int type)
 {
 	stringhook_t * hook = &stringHooks[type];
 
@@ -146,33 +151,54 @@ static void add_string_hook(lua_State *L, int type, int id)
 	{
 		lua_pushstring(L, string);
 		get_table(L);
-		add_hook_to_table(L, id, 1 + lua_objlen(L, -1));
+		add_hook_to_table(L, 1 + lua_objlen(L, -1));
 	}
 	else
-		add_hook_to_table(L, id, ++hook->numGeneric);
+		add_hook_to_table(L, ++hook->numGeneric);
 }
 
-static void add_hook(hook_t *map, int id)
+static void add_hook(hook_t *map)
 {
 	Z_Realloc(map->ids, (map->numHooks + 1) * sizeof *map->ids,
 			PU_STATIC, &map->ids);
-	map->ids[map->numHooks++] = id;
+	map->ids[map->numHooks++] = nextid;
 }
 
-static void add_mobj_hook(lua_State *L, int hook_type, int id)
+static void add_mobj_hook(lua_State *L, int hook_type)
 {
 	mobjtype_t   mobj_type = luaL_optnumber(L, 3, MT_NULL);
 
 	luaL_argcheck(L, mobj_type < NUMMOBJTYPES, 3, "invalid mobjtype_t");
 
-	add_hook(&mobjHookIds[mobj_type][hook_type], id);
+	add_hook(&mobjHookIds[mobj_type][hook_type]);
+}
+
+static void add_hud_hook(lua_State *L, int idx)
+{
+	add_hook(&hudHookIds[luaL_checkoption(L,
+				idx, "game", hudHookNames)]);
+}
+
+static void add_hook_ref(lua_State *L, int idx)
+{
+	if (!(nextid & 7))
+	{
+		Z_Realloc(hooksErrored,
+				BIT_ARRAY_SIZE (nextid + 1) * sizeof *hooksErrored,
+				PU_STATIC, &hooksErrored);
+		hooksErrored[nextid >> 3] = 0;
+	}
+
+	Z_Realloc(hookRefs, (nextid + 1) * sizeof *hookRefs, PU_STATIC, &hookRefs);
+
+	// set the hook function in the registry.
+	lua_pushvalue(L, idx);
+	hookRefs[nextid++] = luaL_ref(L, LUA_REGISTRYINDEX);
 }
 
 // Takes hook, function, and additional arguments (mobj type to act on, etc.)
 static int lib_addHook(lua_State *L)
 {
-	static int nextid;
-
 	const char * name;
 	int type;
 
@@ -185,34 +211,26 @@ static int lib_addHook(lua_State *L)
 	/* this is a very special case */
 	if (( type = hook_in_list(name, stringHookNames) ) < STRING_HOOK(MAX))
 	{
-		add_string_hook(L, type, nextid);
+		add_string_hook(L, type);
 	}
 	else if (( type = hook_in_list(name, mobjHookNames) ) < MOBJ_HOOK(MAX))
 	{
-		add_mobj_hook(L, type, nextid);
+		add_mobj_hook(L, type);
 	}
 	else if (( type = hook_in_list(name, hookNames) ) < HOOK(MAX))
 	{
-		add_hook(&hookIds[type], nextid);
+		add_hook(&hookIds[type]);
 	}
-	else
+	else if (strcmp(name, "HUD") == 0)
 	{
-		return luaL_argerror(L, 1, lua_pushfstring(L, "invalid hook " LUA_QS, name));
+		add_hud_hook(L, 3);
 	}
-
-	if (!(nextid & 7))
+	else
 	{
-		Z_Realloc(hooksErrored,
-				BIT_ARRAY_SIZE (nextid + 1) * sizeof *hooksErrored,
-				PU_STATIC, &hooksErrored);
-		hooksErrored[nextid >> 3] = 0;
+		return luaL_argerror(L, 1, lua_pushfstring(L, "invalid hook " LUA_QS, name));
 	}
 
-	Z_Realloc(hookRefs, (nextid + 1) * sizeof *hookRefs, PU_STATIC, &hookRefs);
-
-	// set the hook function in the registry.
-	lua_pushvalue(L, 2);/* the function */
-	hookRefs[nextid++] = luaL_ref(L, LUA_REGISTRYINDEX);
+	add_hook_ref(L, 2);/* the function */
 
 	return 0;
 }
@@ -227,6 +245,23 @@ int LUA_HookLib(lua_State *L)
 	return 0;
 }
 
+/* TODO: remove in next backwards incompatible release */
+#if MODID == 18
+int lib_hudadd(lua_State *L);/* yeah compiler */
+int lib_hudadd(lua_State *L)
+{
+	if (!lua_lumploading)
+		return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
+
+	luaL_checktype(L, 1, LUA_TFUNCTION);
+
+	add_hud_hook(L, 2);
+	add_hook_ref(L, 1);
+
+	return 0;
+}
+#endif
+
 typedef struct Hook_State Hook_State;
 typedef void (*Hook_Callback)(Hook_State *);
 
@@ -259,11 +294,16 @@ static void push_string(void)
 	lua_pushvalue(gL, SINDEX);
 }
 
-static boolean start_hook_stack(void)
+static boolean begin_hook_values(Hook_State *hook)
+{
+	hook->top = lua_gettop(gL);
+	return true;
+}
+
+static void start_hook_stack(void)
 {
 	lua_settop(gL, 0);
 	push_error_handler();
-	return true;
 }
 
 static boolean init_hook_type
@@ -279,10 +319,11 @@ static boolean init_hook_type
 
 	if (nonzero)
 	{
+		start_hook_stack();
 		hook->hook_type = hook_type;
 		hook->mobj_type = mobj_type;
 		hook->string = string;
-		return start_hook_stack();
+		return begin_hook_values(hook);
 	}
 	else
 		return false;
@@ -323,7 +364,7 @@ static boolean prepare_string_hook
 				stringHooks[hook_type].ref))
 	{
 		lua_pushstring(gL, string);
-		return true;
+		return begin_hook_values(hook);
 	}
 	else
 		return false;
@@ -332,12 +373,12 @@ static boolean prepare_string_hook
 static void init_hook_call
 (
 		Hook_State * hook,
-		int    values,
 		int    results,
 		Hook_Callback results_handler
 ){
-	hook->top = lua_gettop(gL);
-	hook->values = values;
+	const int top = lua_gettop(gL);
+	hook->values = (top - hook->top);
+	hook->top = top;
 	hook->results = results;
 	hook->results_handler = results_handler;
 }
@@ -447,13 +488,12 @@ static int call_mobj_type_hooks(Hook_State *hook, mobjtype_t mobj_type)
 static int call_hooks
 (
 		Hook_State * hook,
-		int        values,
 		int        results,
 		Hook_Callback results_handler
 ){
 	int calls = 0;
 
-	init_hook_call(hook, values, results, results_handler);
+	init_hook_call(hook, results, results_handler);
 
 	if (hook->string)
 	{
@@ -465,7 +505,7 @@ static int call_hooks
 		calls += call_mobj_type_hooks(hook, MT_NULL);
 		calls += call_mobj_type_hooks(hook, hook->mobj_type);
 
-		ps_lua_mobjhooks += calls;
+		ps_lua_mobjhooks.value.i += calls;
 	}
 	else
 		calls += call_mapped(hook, &hookIds[hook->hook_type]);
@@ -514,7 +554,7 @@ int LUA_HookMobj(mobj_t *mobj, int hook_type)
 	if (prepare_mobj_hook(&hook, false, hook_type, mobj->type))
 	{
 		LUA_PushUserdata(gL, mobj, META_MOBJ);
-		call_hooks(&hook, 1, 1, res_true);
+		call_hooks(&hook, 1, res_true);
 	}
 	return hook.status;
 }
@@ -526,7 +566,7 @@ int LUA_Hook2Mobj(mobj_t *t1, mobj_t *t2, int hook_type)
 	{
 		LUA_PushUserdata(gL, t1, META_MOBJ);
 		LUA_PushUserdata(gL, t2, META_MOBJ);
-		call_hooks(&hook, 2, 1, res_force);
+		call_hooks(&hook, 1, res_force);
 	}
 	return hook.status;
 }
@@ -535,7 +575,7 @@ void LUA_HookVoid(int type)
 {
 	Hook_State hook;
 	if (prepare_hook(&hook, 0, type))
-		call_hooks(&hook, 0, 0, res_none);
+		call_hooks(&hook, 0, res_none);
 }
 
 void LUA_HookInt(INT32 number, int hook_type)
@@ -544,7 +584,7 @@ void LUA_HookInt(INT32 number, int hook_type)
 	if (prepare_hook(&hook, 0, hook_type))
 	{
 		lua_pushinteger(gL, number);
-		call_hooks(&hook, 1, 0, res_none);
+		call_hooks(&hook, 0, res_none);
 	}
 }
 
@@ -554,7 +594,7 @@ void LUA_HookBool(boolean value, int hook_type)
 	if (prepare_hook(&hook, 0, hook_type))
 	{
 		lua_pushboolean(gL, value);
-		call_hooks(&hook, 1, 0, res_none);
+		call_hooks(&hook, 0, res_none);
 	}
 }
 
@@ -564,7 +604,7 @@ int LUA_HookPlayer(player_t *player, int hook_type)
 	if (prepare_hook(&hook, false, hook_type))
 	{
 		LUA_PushUserdata(gL, player, META_PLAYER);
-		call_hooks(&hook, 1, 1, res_true);
+		call_hooks(&hook, 1, res_true);
 	}
 	return hook.status;
 }
@@ -580,7 +620,7 @@ int LUA_HookTiccmd(player_t *player, ticcmd_t *cmd, int hook_type)
 		if (hook_type == HOOK(PlayerCmd))
 			hook_cmd_running = true;
 
-		call_hooks(&hook, 2, 1, res_true);
+		call_hooks(&hook, 1, res_true);
 
 		if (hook_type == HOOK(PlayerCmd))
 			hook_cmd_running = false;
@@ -588,6 +628,35 @@ int LUA_HookTiccmd(player_t *player, ticcmd_t *cmd, int hook_type)
 	return hook.status;
 }
 
+int LUA_HookKey(event_t *event, int hook_type)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, false, hook_type))
+	{
+		LUA_PushUserdata(gL, event, META_KEYEVENT);
+		call_hooks(&hook, 1, res_true);
+	}
+	return hook.status;
+}
+
+void LUA_HookHUD(int hook_type)
+{
+	const hook_t * map = &hudHookIds[hook_type];
+	Hook_State hook;
+	if (map->numHooks > 0)
+	{
+		start_hook_stack();
+		begin_hook_values(&hook);
+
+		LUA_SetHudHook(hook_type);
+
+		hud_running = true; // local hook
+		init_hook_call(&hook, 0, res_none);
+		call_mapped(&hook, map);
+		hud_running = false;
+	}
+}
+
 /* =========================================================================
                                SPECIALIZED HOOKS
    ========================================================================= */
@@ -607,7 +676,7 @@ void LUA_HookThinkFrame(void)
 
 	if (prepare_hook(&hook, 0, type))
 	{
-		init_hook_call(&hook, 0, 0, res_none);
+		init_hook_call(&hook, 0, res_none);
 
 		for (k = 0; k < map->numHooks; ++k)
 		{
@@ -642,7 +711,7 @@ int LUA_HookMobjLineCollide(mobj_t *mobj, line_t *line)
 	{
 		LUA_PushUserdata(gL, mobj, META_MOBJ);
 		LUA_PushUserdata(gL, line, META_LINE);
-		call_hooks(&hook, 2, 1, res_force);
+		call_hooks(&hook, 1, res_force);
 	}
 	return hook.status;
 }
@@ -654,7 +723,7 @@ int LUA_HookTouchSpecial(mobj_t *special, mobj_t *toucher)
 	{
 		LUA_PushUserdata(gL, special, META_MOBJ);
 		LUA_PushUserdata(gL, toucher, META_MOBJ);
-		call_hooks(&hook, 2, 1, res_true);
+		call_hooks(&hook, 1, res_true);
 	}
 	return hook.status;
 }
@@ -667,7 +736,6 @@ static int damage_hook
 		INT32   damage,
 		UINT8   damagetype,
 		int     hook_type,
-		int     values,
 		Hook_Callback results_handler
 ){
 	Hook_State hook;
@@ -676,10 +744,10 @@ static int damage_hook
 		LUA_PushUserdata(gL, target, META_MOBJ);
 		LUA_PushUserdata(gL, inflictor, META_MOBJ);
 		LUA_PushUserdata(gL, source, META_MOBJ);
-		if (values == 5)
+		if (hook_type != MOBJ_HOOK(MobjDeath))
 			lua_pushinteger(gL, damage);
 		lua_pushinteger(gL, damagetype);
-		call_hooks(&hook, values, 1, results_handler);
+		call_hooks(&hook, 1, results_handler);
 	}
 	return hook.status;
 }
@@ -687,19 +755,32 @@ static int damage_hook
 int LUA_HookShouldDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
 {
 	return damage_hook(target, inflictor, source, damage, damagetype,
-			MOBJ_HOOK(ShouldDamage), 5, res_force);
+			MOBJ_HOOK(ShouldDamage), res_force);
 }
 
 int LUA_HookMobjDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
 {
 	return damage_hook(target, inflictor, source, damage, damagetype,
-			MOBJ_HOOK(MobjDamage), 5, res_true);
+			MOBJ_HOOK(MobjDamage), res_true);
 }
 
 int LUA_HookMobjDeath(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damagetype)
 {
 	return damage_hook(target, inflictor, source, 0, damagetype,
-			MOBJ_HOOK(MobjDeath), 4, res_true);
+			MOBJ_HOOK(MobjDeath), res_true);
+}
+
+int LUA_HookMobjMoveBlocked(mobj_t *t1, mobj_t *t2, line_t *line)
+{
+	Hook_State hook;
+	if (prepare_mobj_hook(&hook, 0, MOBJ_HOOK(MobjMoveBlocked), t1->type))
+	{
+		LUA_PushUserdata(gL, t1, META_MOBJ);
+		LUA_PushUserdata(gL, t2, META_MOBJ);
+		LUA_PushUserdata(gL, line, META_LINE);
+		call_hooks(&hook, 1, res_true);
+	}
+	return hook.status;
 }
 
 typedef struct {
@@ -772,7 +853,7 @@ int LUA_HookBotAI(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 
 		hook.userdata = &botai;
 
-		call_hooks(&hook, 2, 8, res_botai);
+		call_hooks(&hook, 8, res_botai);
 	}
 
 	return hook.status;
@@ -787,7 +868,7 @@ void LUA_HookLinedefExecute(line_t *line, mobj_t *mo, sector_t *sector)
 		LUA_PushUserdata(gL, line, META_LINE);
 		LUA_PushUserdata(gL, mo, META_MOBJ);
 		LUA_PushUserdata(gL, sector, META_SECTOR);
-		ps_lua_mobjhooks += call_hooks(&hook, 3, 0, res_none);
+		ps_lua_mobjhooks.value.i += call_hooks(&hook, 0, res_none);
 	}
 }
 
@@ -811,7 +892,7 @@ int LUA_HookPlayerMsg(int source, int target, int flags, char *msg)
 			LUA_PushUserdata(gL, &players[target-1], META_PLAYER); // target
 		}
 		lua_pushstring(gL, msg); // msg
-		call_hooks(&hook, 4, 1, res_true);
+		call_hooks(&hook, 1, res_true);
 	}
 	return hook.status;
 }
@@ -825,7 +906,7 @@ int LUA_HookHurtMsg(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 d
 		LUA_PushUserdata(gL, inflictor, META_MOBJ);
 		LUA_PushUserdata(gL, source, META_MOBJ);
 		lua_pushinteger(gL, damagetype);
-		call_hooks(&hook, 4, 1, res_true);
+		call_hooks(&hook, 1, res_true);
 	}
 	return hook.status;
 }
@@ -844,12 +925,14 @@ void LUA_HookNetArchive(lua_CFunction archFunc)
 		push_error_handler();
 		lua_insert(gL, EINDEX);
 
+		begin_hook_values(&hook);
+
 		// tables becomes an upvalue of archFunc
 		lua_pushvalue(gL, -1);
 		lua_pushcclosure(gL, archFunc, 1);
 		// stack: tables, archFunc
 
-		init_hook_call(&hook, 1, 0, res_none);
+		init_hook_call(&hook, 0, res_none);
 		call_mapped(&hook, map);
 
 		lua_pop(gL, 1); // pop archFunc
@@ -865,7 +948,7 @@ int LUA_HookMapThingSpawn(mobj_t *mobj, mapthing_t *mthing)
 	{
 		LUA_PushUserdata(gL, mobj, META_MOBJ);
 		LUA_PushUserdata(gL, mthing, META_MAPTHING);
-		call_hooks(&hook, 2, 1, res_true);
+		call_hooks(&hook, 1, res_true);
 	}
 	return hook.status;
 }
@@ -877,7 +960,7 @@ int LUA_HookFollowMobj(player_t *player, mobj_t *mobj)
 	{
 		LUA_PushUserdata(gL, player, META_PLAYER);
 		LUA_PushUserdata(gL, mobj, META_MOBJ);
-		call_hooks(&hook, 2, 1, res_true);
+		call_hooks(&hook, 1, res_true);
 	}
 	return hook.status;
 }
@@ -889,7 +972,7 @@ int LUA_HookPlayerCanDamage(player_t *player, mobj_t *mobj)
 	{
 		LUA_PushUserdata(gL, player, META_PLAYER);
 		LUA_PushUserdata(gL, mobj, META_MOBJ);
-		call_hooks(&hook, 2, 1, res_force);
+		call_hooks(&hook, 1, res_force);
 	}
 	return hook.status;
 }
@@ -901,7 +984,7 @@ void LUA_HookPlayerQuit(player_t *plr, kickreason_t reason)
 	{
 		LUA_PushUserdata(gL, plr, META_PLAYER); // Player that quit
 		lua_pushinteger(gL, reason); // Reason for quitting
-		call_hooks(&hook, 2, 0, res_none);
+		call_hooks(&hook, 0, res_none);
 	}
 }
 
@@ -915,7 +998,7 @@ int LUA_HookTeamSwitch(player_t *player, int newteam, boolean fromspectators, bo
 		lua_pushboolean(gL, fromspectators);
 		lua_pushboolean(gL, tryingautobalance);
 		lua_pushboolean(gL, tryingscramble);
-		call_hooks(&hook, 5, 1, res_false);
+		call_hooks(&hook, 1, res_false);
 	}
 	return hook.status;
 }
@@ -930,7 +1013,7 @@ int LUA_HookViewpointSwitch(player_t *player, player_t *newdisplayplayer, boolea
 		lua_pushboolean(gL, forced);
 
 		hud_running = true; // local hook
-		call_hooks(&hook, 3, 1, res_force);
+		call_hooks(&hook, 1, res_force);
 		hud_running = false;
 	}
 	return hook.status;
@@ -945,7 +1028,7 @@ int LUA_HookSeenPlayer(player_t *player, player_t *seenfriend)
 		LUA_PushUserdata(gL, seenfriend, META_PLAYER);
 
 		hud_running = true; // local hook
-		call_hooks(&hook, 2, 1, res_false);
+		call_hooks(&hook, 1, res_false);
 		hud_running = false;
 	}
 	return hook.status;
@@ -961,7 +1044,7 @@ int LUA_HookShouldJingleContinue(player_t *player, const char *musname)
 		push_string();
 
 		hud_running = true; // local hook
-		call_hooks(&hook, 2, 1, res_true);
+		call_hooks(&hook, 1, res_true);
 		hud_running = false;
 	}
 	return hook.status;
@@ -1027,7 +1110,8 @@ int LUA_HookMusicChange(const char *oldname, struct MusicChange *param)
 
 	if (prepare_hook(&hook, false, type))
 	{
-		init_hook_call(&hook, 7, 6, res_musicchange);
+		init_hook_call(&hook, 6, res_musicchange);
+		hook.values = 7;/* values pushed later */
 		hook.userdata = param;
 
 		lua_pushstring(gL, oldname);/* the only constant value */
@@ -1073,7 +1157,7 @@ fixed_t LUA_HookPlayerHeight(player_t *player)
 	if (prepare_hook(&hook, -1, HOOK(PlayerHeight)))
 	{
 		LUA_PushUserdata(gL, player, META_PLAYER);
-		call_hooks(&hook, 1, 1, res_playerheight);
+		call_hooks(&hook, 1, res_playerheight);
 	}
 	return hook.status;
 }
@@ -1084,18 +1168,7 @@ int LUA_HookPlayerCanEnterSpinGaps(player_t *player)
 	if (prepare_hook(&hook, 0, HOOK(PlayerCanEnterSpinGaps)))
 	{
 		LUA_PushUserdata(gL, player, META_PLAYER);
-		call_hooks(&hook, 1, 1, res_force);
-	}
-	return hook.status;
-}
-
-int LUA_HookKey(INT32 keycode, int hooktype)
-{
-	Hook_State hook;
-	if (prepare_hook(&hook, 0, hooktype))
-	{
-		lua_pushinteger(gL, keycode);
-		call_hooks(&hook, 1, 0, res_true);
+		call_hooks(&hook, 1, res_force);
 	}
 	return hook.status;
 }
diff --git a/src/lua_hud.h b/src/lua_hud.h
index d2f5bceca6f0d56745d2a40c3826ea1e594b9718..c1d2d164b97d6952612e088d80f0bd256a117d4b 100644
--- a/src/lua_hud.h
+++ b/src/lua_hud.h
@@ -47,8 +47,4 @@ extern boolean hud_running;
 
 boolean LUA_HudEnabled(enum hud option);
 
-void LUAh_GameHUD(player_t *stplyr);
-void LUAh_ScoresHUD(void);
-void LUAh_TitleHUD(void);
-void LUAh_TitleCardHUD(player_t *stplayr);
-void LUAh_IntermissionHUD(boolean failedstage);
+void LUA_SetHudHook(int hook);
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index 9a3e676d5fd6168fd64bfaaa44466a5e85e8949c..0dd951efd964e28a04074d311215d059564d88a7 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -23,18 +23,18 @@
 #include "v_video.h"
 #include "w_wad.h"
 #include "z_zone.h"
+#include "y_inter.h"
 
 #include "lua_script.h"
 #include "lua_libs.h"
 #include "lua_hud.h"
+#include "lua_hook.h"
 
 #define HUDONLY if (!hud_running) return luaL_error(L, "HUD rendering code should not be called outside of rendering hooks!");
 
 boolean hud_running = false;
 static UINT8 hud_enabled[(hud_MAX/8)+1];
 
-static UINT8 hudAvailable; // hud hooks field
-
 // must match enum hud in lua_hud.h
 static const char *const hud_disable_options[] = {
 	"stagetitle",
@@ -95,21 +95,6 @@ static const char *const patch_opt[] = {
 	"topoffset",
 	NULL};
 
-enum hudhook {
-	hudhook_game = 0,
-	hudhook_scores,
-	hudhook_intermission,
-	hudhook_title,
-	hudhook_titlecard
-};
-static const char *const hudhook_opt[] = {
-	"game",
-	"scores",
-	"intermission",
-	"title",
-	"titlecard",
-	NULL};
-
 // alignment types for v.drawString
 enum align {
 	align_left = 0,
@@ -384,6 +369,74 @@ static int camera_get(lua_State *L)
 	return 1;
 }
 
+static int camera_set(lua_State *L)
+{
+	camera_t *cam = *((camera_t **)luaL_checkudata(L, 1, META_CAMERA));
+	enum cameraf field = luaL_checkoption(L, 2, NULL, camera_opt);
+
+	I_Assert(cam != NULL);
+
+	switch(field)
+	{
+	case camera_subsector:
+	case camera_floorz:
+	case camera_ceilingz:
+	case camera_x:
+	case camera_y:
+		return luaL_error(L, LUA_QL("camera_t") " field " LUA_QS " should not be set directly. Use " LUA_QL("P_TryCameraMove") " or " LUA_QL("P_TeleportCameraMove") " instead.", camera_opt[field]);
+	case camera_chase: {
+		INT32 chase = luaL_checkboolean(L, 3);
+		if (cam == &camera)
+			CV_SetValue(&cv_chasecam, chase);
+		else if (cam == &camera2)
+			CV_SetValue(&cv_chasecam2, chase);
+		else // ??? this should never happen, but ok
+			cam->chase = chase;
+		break;
+	}
+	case camera_aiming:
+		cam->aiming = luaL_checkangle(L, 3);
+		break;
+	case camera_z:
+		cam->z = luaL_checkfixed(L, 3);
+		P_CheckCameraPosition(cam->x, cam->y, cam);
+		cam->floorz = tmfloorz;
+		cam->ceilingz = tmceilingz;
+		break;
+	case camera_angle:
+		cam->angle = luaL_checkangle(L, 3);
+		break;
+	case camera_radius:
+		cam->radius = luaL_checkfixed(L, 3);
+		if (cam->radius < 0)
+			cam->radius = 0;
+		P_CheckCameraPosition(cam->x, cam->y, cam);
+		cam->floorz = tmfloorz;
+		cam->ceilingz = tmceilingz;
+		break;
+	case camera_height:
+		cam->height = luaL_checkfixed(L, 3);
+		if (cam->height < 0)
+			cam->height = 0;
+		P_CheckCameraPosition(cam->x, cam->y, cam);
+		cam->floorz = tmfloorz;
+		cam->ceilingz = tmceilingz;
+		break;
+	case camera_momx:
+		cam->momx = luaL_checkfixed(L, 3);
+		break;
+	case camera_momy:
+		cam->momy = luaL_checkfixed(L, 3);
+		break;
+	case camera_momz:
+		cam->momz = luaL_checkfixed(L, 3);
+		break;
+	default:
+		return luaL_error(L, LUA_QL("camera_t") " has no field named " LUA_QS, camera_opt[field]);
+	}
+	return 0;
+}
+
 //
 // lib_draw
 //
@@ -663,6 +716,45 @@ static int libd_drawStretched(lua_State *L)
 	return 0;
 }
 
+static int libd_drawCropped(lua_State *L)
+{
+	fixed_t x, y, hscale, vscale, sx, sy, w, h;
+	INT32 flags;
+	patch_t *patch;
+	const UINT8 *colormap = NULL;
+
+	HUDONLY
+	x = luaL_checkinteger(L, 1);
+	y = luaL_checkinteger(L, 2);
+	hscale = luaL_checkinteger(L, 3);
+	if (hscale < 0)
+		return luaL_error(L, "negative horizontal scale");
+	vscale = luaL_checkinteger(L, 4);
+	if (vscale < 0)
+		return luaL_error(L, "negative vertical scale");
+	patch = *((patch_t **)luaL_checkudata(L, 5, META_PATCH));
+	flags = luaL_checkinteger(L, 6);
+	if (!lua_isnoneornil(L, 7))
+		colormap = *((UINT8 **)luaL_checkudata(L, 7, META_COLORMAP));
+	sx = luaL_checkinteger(L, 8);
+	if (sx < 0) // Don't crash. Now, we could do "x-=sx*FRACUNIT; sx=0;" here...
+		return luaL_error(L, "negative crop sx");
+	sy = luaL_checkinteger(L, 9);
+	if (sy < 0) // ...but it's more truthful to just deny it, as negative values would crash
+		return luaL_error(L, "negative crop sy");
+	w = luaL_checkinteger(L, 10);
+	if (w < 0) // Again, don't crash
+		return luaL_error(L, "negative crop w");
+	h = luaL_checkinteger(L, 11);
+	if (h < 0)
+		return luaL_error(L, "negative crop h");
+
+	flags &= ~V_PARAMMASK; // Don't let crashes happen.
+
+	V_DrawCroppedPatch(x, y, hscale, vscale, flags, patch, colormap, sx, sy, w, h);
+	return 0;
+}
+
 static int libd_drawNum(lua_State *L)
 {
 	INT32 x, y, flags, num;
@@ -1121,6 +1213,7 @@ static luaL_Reg lib_draw[] = {
 	{"draw", libd_draw},
 	{"drawScaled", libd_drawScaled},
 	{"drawStretched", libd_drawStretched},
+	{"drawCropped", libd_drawCropped},
 	{"drawNum", libd_drawNum},
 	{"drawPaddedNum", libd_drawPaddedNum},
 	{"drawFill", libd_drawFill},
@@ -1152,6 +1245,8 @@ static luaL_Reg lib_draw[] = {
 	{NULL, NULL}
 };
 
+static int lib_draw_ref;
+
 //
 // lib_hud
 //
@@ -1186,28 +1281,7 @@ static int lib_hudenabled(lua_State *L)
 
 
 // add a HUD element for rendering
-static int lib_hudadd(lua_State *L)
-{
-	enum hudhook field;
-
-	luaL_checktype(L, 1, LUA_TFUNCTION);
-	field = luaL_checkoption(L, 2, "game", hudhook_opt);
-
-	if (!lua_lumploading)
-		return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
-
-	lua_getfield(L, LUA_REGISTRYINDEX, "HUD");
-	I_Assert(lua_istable(L, -1));
-	lua_rawgeti(L, -1, field+2); // HUD[2+]
-	I_Assert(lua_istable(L, -1));
-	lua_remove(L, -2);
-
-	lua_pushvalue(L, 1);
-	lua_rawseti(L, -2, (int)(lua_objlen(L, -2) + 1));
-
-	hudAvailable |= 1<<field;
-	return 0;
-}
+extern int lib_hudadd(lua_State *L);
 
 static luaL_Reg lib_hud[] = {
 	{"enable", lib_hudenable},
@@ -1225,26 +1299,9 @@ int LUA_HudLib(lua_State *L)
 {
 	memset(hud_enabled, 0xff, (hud_MAX/8)+1);
 
-	lua_newtable(L); // HUD registry table
-		lua_newtable(L);
-		luaL_register(L, NULL, lib_draw);
-		lua_rawseti(L, -2, 1); // HUD[1] = lib_draw
-
-		lua_newtable(L);
-		lua_rawseti(L, -2, 2); // HUD[2] = game rendering functions array
-
-		lua_newtable(L);
-		lua_rawseti(L, -2, 3); // HUD[3] = scores rendering functions array
-
-		lua_newtable(L);
-		lua_rawseti(L, -2, 4); // HUD[4] = intermission rendering functions array
-
-		lua_newtable(L);
-		lua_rawseti(L, -2, 5); // HUD[5] = title rendering functions array
-
-		lua_newtable(L);
-		lua_rawseti(L, -2, 6); // HUD[6] = title card rendering functions array
-	lua_setfield(L, LUA_REGISTRYINDEX, "HUD");
+	lua_newtable(L);
+	luaL_register(L, NULL, lib_draw);
+	lib_draw_ref = luaL_ref(L, LUA_REGISTRYINDEX);
 
 	luaL_newmetatable(L, META_HUDINFO);
 		lua_pushcfunction(L, hudinfo_get);
@@ -1283,6 +1340,9 @@ int LUA_HudLib(lua_State *L)
 	luaL_newmetatable(L, META_CAMERA);
 		lua_pushcfunction(L, camera_get);
 		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, camera_set);
+		lua_setfield(L, -2, "__newindex");
 	lua_pop(L,1);
 
 	luaL_register(L, "hud", lib_hud);
@@ -1296,160 +1356,29 @@ boolean LUA_HudEnabled(enum hud option)
 	return false;
 }
 
-// Hook for HUD rendering
-void LUAh_GameHUD(player_t *stplayr)
+void LUA_SetHudHook(int hook)
 {
-	if (!gL || !(hudAvailable & (1<<hudhook_game)))
-		return;
-
-	hud_running = true;
-	lua_settop(gL, 0);
-
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	lua_getfield(gL, LUA_REGISTRYINDEX, "HUD");
-	I_Assert(lua_istable(gL, -1));
-	lua_rawgeti(gL, -1, 2+hudhook_game); // HUD[2] = rendering funcs
-	I_Assert(lua_istable(gL, -1));
+	lua_getref(gL, lib_draw_ref);
 
-	lua_rawgeti(gL, -2, 1); // HUD[1] = lib_draw
-	I_Assert(lua_istable(gL, -1));
-	lua_remove(gL, -3); // pop HUD
-	LUA_PushUserdata(gL, stplayr, META_PLAYER);
-
-	if (splitscreen && stplayr == &players[secondarydisplayplayer])
-		LUA_PushUserdata(gL, &camera2, META_CAMERA);
-	else
-		LUA_PushUserdata(gL, &camera, META_CAMERA);
-
-	lua_pushnil(gL);
-	while (lua_next(gL, -5) != 0) {
-		lua_pushvalue(gL, -5); // graphics library (HUD[1])
-		lua_pushvalue(gL, -5); // stplayr
-		lua_pushvalue(gL, -5); // camera
-		LUA_Call(gL, 3, 0, 1);
-	}
-	lua_settop(gL, 0);
-	hud_running = false;
-}
-
-void LUAh_ScoresHUD(void)
-{
-	if (!gL || !(hudAvailable & (1<<hudhook_scores)))
-		return;
-
-	hud_running = true;
-	lua_settop(gL, 0);
-
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	lua_getfield(gL, LUA_REGISTRYINDEX, "HUD");
-	I_Assert(lua_istable(gL, -1));
-	lua_rawgeti(gL, -1, 2+hudhook_scores); // HUD[3] = rendering funcs
-	I_Assert(lua_istable(gL, -1));
-
-	lua_rawgeti(gL, -2, 1); // HUD[1] = lib_draw
-	I_Assert(lua_istable(gL, -1));
-	lua_remove(gL, -3); // pop HUD
-	lua_pushnil(gL);
-	while (lua_next(gL, -3) != 0) {
-		lua_pushvalue(gL, -3); // graphics library (HUD[1])
-		LUA_Call(gL, 1, 0, 1);
-	}
-	lua_settop(gL, 0);
-	hud_running = false;
-}
-
-void LUAh_TitleHUD(void)
-{
-	if (!gL || !(hudAvailable & (1<<hudhook_title)))
-		return;
-
-	hud_running = true;
-	lua_settop(gL, 0);
-
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	lua_getfield(gL, LUA_REGISTRYINDEX, "HUD");
-	I_Assert(lua_istable(gL, -1));
-	lua_rawgeti(gL, -1, 2+hudhook_title); // HUD[5] = rendering funcs
-	I_Assert(lua_istable(gL, -1));
-
-	lua_rawgeti(gL, -2, 1); // HUD[1] = lib_draw
-	I_Assert(lua_istable(gL, -1));
-	lua_remove(gL, -3); // pop HUD
-	lua_pushnil(gL);
-	while (lua_next(gL, -3) != 0) {
-		lua_pushvalue(gL, -3); // graphics library (HUD[1])
-		LUA_Call(gL, 1, 0, 1);
-	}
-	lua_settop(gL, 0);
-	hud_running = false;
-}
-
-void LUAh_TitleCardHUD(player_t *stplayr)
-{
-	if (!gL || !(hudAvailable & (1<<hudhook_titlecard)))
-		return;
-
-	hud_running = true;
-	lua_settop(gL, 0);
-
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	lua_getfield(gL, LUA_REGISTRYINDEX, "HUD");
-	I_Assert(lua_istable(gL, -1));
-	lua_rawgeti(gL, -1, 2+hudhook_titlecard); // HUD[6] = rendering funcs
-	I_Assert(lua_istable(gL, -1));
-
-	lua_rawgeti(gL, -2, 1); // HUD[1] = lib_draw
-	I_Assert(lua_istable(gL, -1));
-	lua_remove(gL, -3); // pop HUD
-
-	LUA_PushUserdata(gL, stplayr, META_PLAYER);
-	lua_pushinteger(gL, lt_ticker);
-	lua_pushinteger(gL, (lt_endtime + TICRATE));
-	lua_pushnil(gL);
-
-	while (lua_next(gL, -6) != 0) {
-		lua_pushvalue(gL, -6); // graphics library (HUD[1])
-		lua_pushvalue(gL, -6); // stplayr
-		lua_pushvalue(gL, -6); // lt_ticker
-		lua_pushvalue(gL, -6); // lt_endtime
-		LUA_Call(gL, 4, 0, 1);
-	}
-
-	lua_settop(gL, 0);
-	hud_running = false;
-}
-
-void LUAh_IntermissionHUD(boolean failedstage)
-{
-	if (!gL || !(hudAvailable & (1<<hudhook_intermission)))
-		return;
-
-	hud_running = true;
-	lua_settop(gL, 0);
-
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	lua_getfield(gL, LUA_REGISTRYINDEX, "HUD");
-	I_Assert(lua_istable(gL, -1));
-	lua_rawgeti(gL, -1, 2+hudhook_intermission); // HUD[4] = rendering funcs
-	I_Assert(lua_istable(gL, -1));
-
-	lua_rawgeti(gL, -2, 1); // HUD[1] = lib_draw
-	I_Assert(lua_istable(gL, -1));
-	lua_remove(gL, -3); // pop HUD
-
-	lua_pushboolean(gL, failedstage); // stagefailed
-	lua_pushnil(gL);
-
-	while (lua_next(gL, -4) != 0) {
-		lua_pushvalue(gL, -4); // graphics library (HUD[1])
-		lua_pushvalue(gL, -4); // stagefailed
-		LUA_Call(gL, 2, 0, 1);
+	switch (hook)
+	{
+		case HUD_HOOK(game): {
+			camera_t *cam = (splitscreen && stplyr ==
+					&players[secondarydisplayplayer])
+				? &camera2 : &camera;
+
+			LUA_PushUserdata(gL, stplyr, META_PLAYER);
+			LUA_PushUserdata(gL, cam, META_CAMERA);
+		}	break;
+
+		case HUD_HOOK(titlecard):
+			LUA_PushUserdata(gL, stplyr, META_PLAYER);
+			lua_pushinteger(gL, lt_ticker);
+			lua_pushinteger(gL, (lt_endtime + TICRATE));
+			break;
+
+		case HUD_HOOK(intermission):
+			lua_pushboolean(gL, intertype == int_spec &&
+					stagefailed);
 	}
-	lua_settop(gL, 0);
-	hud_running = false;
 }
diff --git a/src/lua_inputlib.c b/src/lua_inputlib.c
index 71eb1033f7fa8467f54fa9d936665d192051dd8b..661d9364166f4e2eec28d281dd0711dfbd45480a 100644
--- a/src/lua_inputlib.c
+++ b/src/lua_inputlib.c
@@ -19,6 +19,8 @@
 #include "lua_script.h"
 #include "lua_libs.h"
 
+boolean mousegrabbedbylua = true;
+
 ///////////////
 // FUNCTIONS //
 ///////////////
@@ -26,8 +28,8 @@
 static int lib_gameControlDown(lua_State *L)
 {
 	int i = luaL_checkinteger(L, 1);
-	if (i < 0 || i >= num_gamecontrols)
-		return luaL_error(L, "gc_* constant %d out of range (0 - %d)", i, num_gamecontrols-1);
+	if (i < 0 || i >= NUM_GAMECONTROLS)
+		return luaL_error(L, "GC_* constant %d out of range (0 - %d)", i, NUM_GAMECONTROLS-1);
 	lua_pushinteger(L, PLAYER1INPUTDOWN(i));
 	return 1;
 }
@@ -35,8 +37,8 @@ static int lib_gameControlDown(lua_State *L)
 static int lib_gameControl2Down(lua_State *L)
 {
 	int i = luaL_checkinteger(L, 1);
-	if (i < 0 || i >= num_gamecontrols)
-		return luaL_error(L, "gc_* constant %d out of range (0 - %d)", i, num_gamecontrols-1);
+	if (i < 0 || i >= NUM_GAMECONTROLS)
+		return luaL_error(L, "GC_* constant %d out of range (0 - %d)", i, NUM_GAMECONTROLS-1);
 	lua_pushinteger(L, PLAYER2INPUTDOWN(i));
 	return 1;
 }
@@ -44,8 +46,8 @@ static int lib_gameControl2Down(lua_State *L)
 static int lib_gameControlToKeyNum(lua_State *L)
 {
 	int i = luaL_checkinteger(L, 1);
-	if (i < 0 || i >= num_gamecontrols)
-		return luaL_error(L, "gc_* constant %d out of range (0 - %d)", i, num_gamecontrols-1);
+	if (i < 0 || i >= NUM_GAMECONTROLS)
+		return luaL_error(L, "GC_* constant %d out of range (0 - %d)", i, NUM_GAMECONTROLS-1);
 	lua_pushinteger(L, gamecontrol[i][0]);
 	lua_pushinteger(L, gamecontrol[i][1]);
 	return 2;
@@ -54,8 +56,8 @@ static int lib_gameControlToKeyNum(lua_State *L)
 static int lib_gameControl2ToKeyNum(lua_State *L)
 {
 	int i = luaL_checkinteger(L, 1);
-	if (i < 0 || i >= num_gamecontrols)
-		return luaL_error(L, "gc_* constant %d out of range (0 - %d)", i, num_gamecontrols-1);
+	if (i < 0 || i >= NUM_GAMECONTROLS)
+		return luaL_error(L, "GC_* constant %d out of range (0 - %d)", i, NUM_GAMECONTROLS-1);
 	lua_pushinteger(L, gamecontrolbis[i][0]);
 	lua_pushinteger(L, gamecontrolbis[i][1]);
 	return 2;
@@ -75,17 +77,17 @@ static int lib_joy2Axis(lua_State *L)
 	return 1;
 }
 
-static int lib_keyNumToString(lua_State *L)
+static int lib_keyNumToName(lua_State *L)
 {
 	int i = luaL_checkinteger(L, 1);
-	lua_pushstring(L, G_KeyNumToString(i));
+	lua_pushstring(L, G_KeyNumToName(i));
 	return 1;
 }
 
-static int lib_keyStringToNum(lua_State *L)
+static int lib_keyNameToNum(lua_State *L)
 {
 	const char *str = luaL_checkstring(L, 1);
-	lua_pushinteger(L, G_KeyStringToNum(str));
+	lua_pushinteger(L, G_KeyNameToNum(str));
 	return 1;
 }
 
@@ -106,14 +108,14 @@ static int lib_shiftKeyNum(lua_State *L)
 
 static int lib_getMouseGrab(lua_State *L)
 {
-	lua_pushboolean(L, I_GetMouseGrab());
+	lua_pushboolean(L, mousegrabbedbylua);
 	return 1;
 }
 
 static int lib_setMouseGrab(lua_State *L)
 {
-	boolean grab = luaL_checkboolean(L, 1);
-	I_SetMouseGrab(grab);
+	mousegrabbedbylua = luaL_checkboolean(L, 1);
+	I_UpdateMouseGrab();
 	return 0;
 }
 
@@ -127,19 +129,19 @@ static int lib_getCursorPosition(lua_State *L)
 }
 
 static luaL_Reg lib[] = {
-	{"G_GameControlDown", lib_gameControlDown},
-	{"G_GameControl2Down", lib_gameControl2Down},
-	{"G_GameControlToKeyNum", lib_gameControlToKeyNum},
-	{"G_GameControl2ToKeyNum", lib_gameControl2ToKeyNum},
-	{"G_JoyAxis", lib_joyAxis},
-	{"G_Joy2Axis", lib_joy2Axis},
-	{"G_KeyNumToString", lib_keyNumToString},
-	{"G_KeyStringToNum", lib_keyStringToNum},
-	{"HU_KeyNumPrintable", lib_keyNumPrintable},
-	{"HU_ShiftKeyNum", lib_shiftKeyNum},
-	{"I_GetMouseGrab", lib_getMouseGrab},
-	{"I_SetMouseGrab", lib_setMouseGrab},
-	{"I_GetCursorPosition", lib_getCursorPosition},
+	{"gameControlDown", lib_gameControlDown},
+	{"gameControl2Down", lib_gameControl2Down},
+	{"gameControlToKeyNum", lib_gameControlToKeyNum},
+	{"gameControl2ToKeyNum", lib_gameControl2ToKeyNum},
+	{"joyAxis", lib_joyAxis},
+	{"joy2Axis", lib_joy2Axis},
+	{"keyNumToName", lib_keyNumToName},
+	{"keyNameToNum", lib_keyNameToNum},
+	{"keyNumPrintable", lib_keyNumPrintable},
+	{"shiftKeyNum", lib_shiftKeyNum},
+	{"getMouseGrab", lib_getMouseGrab},
+	{"setMouseGrab", lib_setMouseGrab},
+	{"getCursorPosition", lib_getCursorPosition},
 	{NULL, NULL}
 };
 
@@ -172,6 +174,29 @@ static int lib_lenGameKeyDown(lua_State *L)
 	return 1;
 }
 
+///////////////
+// KEY EVENT //
+///////////////
+
+static int keyevent_get(lua_State *L)
+{
+	event_t *event = *((event_t **)luaL_checkudata(L, 1, META_KEYEVENT));
+	const char *field = luaL_checkstring(L, 2);
+
+	I_Assert(event != NULL);
+
+	if (fastcmp(field,"name"))
+		lua_pushstring(L, G_KeyNumToName(event->key));
+	else if (fastcmp(field,"num"))
+		lua_pushinteger(L, event->key);
+	else if (fastcmp(field,"repeated"))
+		lua_pushboolean(L, event->repeated);
+	else
+		return luaL_error(L, "keyevent_t has no field named %s", field);
+
+	return 1;
+}
+
 ///////////
 // MOUSE //
 ///////////
@@ -227,6 +252,11 @@ int LUA_InputLib(lua_State *L)
 		lua_setmetatable(L, -2);
 	lua_setglobal(L, "gamekeydown");
 
+	luaL_newmetatable(L, META_KEYEVENT);
+		lua_pushcfunction(L, keyevent_get);
+		lua_setfield(L, -2, "__index");
+	lua_pop(L, 1);
+
 	luaL_newmetatable(L, META_MOUSE);
 		lua_pushcfunction(L, mouse_get);
 		lua_setfield(L, -2, "__index");
@@ -235,8 +265,6 @@ int LUA_InputLib(lua_State *L)
 		lua_setfield(L, -2, "__len");
 	lua_pop(L, 1);
 
-	// Set global functions
-	lua_pushvalue(L, LUA_GLOBALSINDEX);
-	luaL_register(L, NULL, lib);
+	luaL_register(L, "input", lib);
 	return 0;
 }
diff --git a/src/lua_libs.h b/src/lua_libs.h
index 668eb99b001f4cb0607351969028392ee2e9cded..8903834e861c32c72dc9eaf2f22c864cefc218a7 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -12,6 +12,8 @@
 
 extern lua_State *gL;
 
+extern boolean mousegrabbedbylua;
+
 #define MUTABLE_TAGS
 
 #define LREG_VALID "VALID_USERDATA"
@@ -88,6 +90,7 @@ extern lua_State *gL;
 
 #define META_LUABANKS "LUABANKS[]*"
 
+#define META_KEYEVENT "KEYEVENT_T*"
 #define META_MOUSE "MOUSE_T*"
 
 boolean luaL_checkboolean(lua_State *L, int narg);
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index 06104ff2d748e2f6dc2e22232b02f0275cf68da8..1c634da45e233f5b55afdfe3320aedf508fc5cee 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -370,6 +370,12 @@ static int player_get(lua_State *L)
 		lua_pushboolean(L, plr->outofcoop);
 	else if (fastcmp(field,"bot"))
 		lua_pushinteger(L, plr->bot);
+	else if (fastcmp(field,"botleader"))
+		LUA_PushUserdata(L, plr->botleader, META_PLAYER);
+	else if (fastcmp(field,"lastbuttons"))
+		lua_pushinteger(L, plr->lastbuttons);
+	else if (fastcmp(field,"blocked"))
+		lua_pushboolean(L, plr->blocked);
 	else if (fastcmp(field,"jointime"))
 		lua_pushinteger(L, plr->jointime);
 	else if (fastcmp(field,"quittime"))
@@ -719,6 +725,17 @@ static int player_set(lua_State *L)
 		plr->outofcoop = lua_toboolean(L, 3);
 	else if (fastcmp(field,"bot"))
 		return NOSET;
+	else if (fastcmp(field,"botleader"))
+	{
+		player_t *player = NULL;
+		if (!lua_isnil(L, 3))
+			player = *((player_t **)luaL_checkudata(L, 3, META_PLAYER));
+		plr->botleader = player;
+	}
+	else if (fastcmp(field,"lastbuttons"))
+		plr->lastbuttons = (UINT16)luaL_checkinteger(L, 3);
+	else if (fastcmp(field,"blocked"))
+		plr->blocked = (UINT8)luaL_checkinteger(L, 3);
 	else if (fastcmp(field,"jointime"))
 		plr->jointime = (tic_t)luaL_checkinteger(L, 3);
 	else if (fastcmp(field,"quittime"))
diff --git a/src/lua_script.c b/src/lua_script.c
index 6faff8729609bdf3a7e758a81829cb9ac4a74900..a1376ca2e37fbdad25f9c2db6fb34907e87c8cdd 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -25,7 +25,7 @@
 #include "byteptr.h"
 #include "p_saveg.h"
 #include "p_local.h"
-#include "p_slopes.h" // for P_SlopeById
+#include "p_slopes.h" // for P_SlopeById and slopelist
 #include "p_polyobj.h" // polyobj_t, PolyObjects
 #ifdef LUA_ALLOW_BYTECODE
 #include "d_netfil.h" // for LUA_DumpFile
@@ -393,6 +393,14 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 	} else if (fastcmp(word, "mouse2")) {
 		LUA_PushUserdata(L, &mouse2, META_MOUSE);
 		return 1;
+	} else if (fastcmp(word, "camera")) {
+		LUA_PushUserdata(L, &camera, META_CAMERA);
+		return 1;
+	} else if (fastcmp(word, "camera2")) {
+		if (!splitscreen)
+			return 0;
+		LUA_PushUserdata(L, &camera2, META_CAMERA);
+		return 1;
 	}
 	return 0;
 }
@@ -851,6 +859,8 @@ void LUA_InvalidateLevel(void)
 	{
 		LUA_InvalidateUserdata(&lines[i]);
 		LUA_InvalidateUserdata(&lines[i].tags);
+		LUA_InvalidateUserdata(lines[i].args);
+		LUA_InvalidateUserdata(lines[i].stringargs);
 		LUA_InvalidateUserdata(lines[i].sidenum);
 	}
 	for (i = 0; i < numsides; i++)
@@ -863,6 +873,13 @@ void LUA_InvalidateLevel(void)
 		LUA_InvalidateUserdata(&PolyObjects[i].vertices);
 		LUA_InvalidateUserdata(&PolyObjects[i].lines);
 	}
+	for (pslope_t *slope = slopelist; slope; slope = slope->next)
+	{
+		LUA_InvalidateUserdata(slope);
+		LUA_InvalidateUserdata(&slope->normal);
+		LUA_InvalidateUserdata(&slope->o);
+		LUA_InvalidateUserdata(&slope->d);
+	}
 #ifdef HAVE_LUA_SEGS
 	for (i = 0; i < numsegs; i++)
 		LUA_InvalidateUserdata(&segs[i]);
@@ -885,6 +902,8 @@ void LUA_InvalidateMapthings(void)
 	{
 		LUA_InvalidateUserdata(&mapthings[i]);
 		LUA_InvalidateUserdata(&mapthings[i].tags);
+		LUA_InvalidateUserdata(mapthings[i].args);
+		LUA_InvalidateUserdata(mapthings[i].stringargs);
 	}
 }
 
@@ -1373,21 +1392,13 @@ 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)
-			{
-				lua_pushvalue(gL, -2);
-				CONS_Alert(CONS_ERROR, "Index '%s' (%s) of table %d could not be archived!\n", lua_tostring(gL, -1), luaL_typename(gL, -1), i);
-				lua_pop(gL, 1);
-			}
+				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)
 				n++; // the table contained a new table we'll have to archive. :(
 			else if (e == 2) // invalid value type
-			{
-				lua_pushvalue(gL, -2);
-				CONS_Alert(CONS_ERROR, "Type of value for table %d entry '%s' (%s) could not be archived!\n", i, lua_tostring(gL, -1), luaL_typename(gL, -1));
-				lua_pop(gL, 1);
-			}
+				CONS_Alert(CONS_ERROR, "Type of value for table %d entry '%s' (%s) could not be archived!\n", i, lua_tostring(gL, -2), luaL_typename(gL, -1));
 
 			lua_pop(gL, 1);
 		}
diff --git a/src/m_cheat.c b/src/m_cheat.c
index c958bb4a4de31450276067e730c8c77442b0c800..ef896c9911975550905069813394f7c4a4e654d1 100644
--- a/src/m_cheat.c
+++ b/src/m_cheat.c
@@ -203,11 +203,11 @@ boolean cht_Responder(event_t *ev)
 	if (ev->type != ev_keydown)
 		return false;
 
-	if (ev->data1 > 0xFF)
+	if (ev->key > 0xFF)
 	{
 		// map some fake (joy) inputs into keys
 		// map joy inputs into keys
-		switch (ev->data1)
+		switch (ev->key)
 		{
 			case KEY_JOY1:
 			case KEY_JOY1 + 2:
@@ -231,7 +231,7 @@ boolean cht_Responder(event_t *ev)
 		}
 	}
 	else
-		ch = (UINT8)ev->data1;
+		ch = (UINT8)ev->key;
 
 	ret += cht_CheckCheat(&cheat_ultimate, (char)ch);
 	ret += cht_CheckCheat(&cheat_ultimate_joy, (char)ch);
diff --git a/src/m_menu.c b/src/m_menu.c
index db2aa09c63655a4bce83054a5e487143ed82cc2b..fc1e33b67dc16c6e8898a1df754119ecf8cf86ed 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -1106,55 +1106,55 @@ static menuitem_t OP_ChangeControlsMenu[] =
 {
 	{IT_HEADER, NULL, "Movement", NULL, 0},
 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
-	{IT_CALL | IT_STRING2, NULL, "Move Forward",     M_ChangeControl, gc_forward     },
-	{IT_CALL | IT_STRING2, NULL, "Move Backward",    M_ChangeControl, gc_backward    },
-	{IT_CALL | IT_STRING2, NULL, "Move Left",        M_ChangeControl, gc_strafeleft  },
-	{IT_CALL | IT_STRING2, NULL, "Move Right",       M_ChangeControl, gc_straferight },
-	{IT_CALL | IT_STRING2, NULL, "Jump",             M_ChangeControl, gc_jump      },
-	{IT_CALL | IT_STRING2, NULL, "Spin",             M_ChangeControl, gc_spin     },
+	{IT_CALL | IT_STRING2, NULL, "Move Forward",     M_ChangeControl, GC_FORWARD     },
+	{IT_CALL | IT_STRING2, NULL, "Move Backward",    M_ChangeControl, GC_BACKWARD    },
+	{IT_CALL | IT_STRING2, NULL, "Move Left",        M_ChangeControl, GC_STRAFELEFT  },
+	{IT_CALL | IT_STRING2, NULL, "Move Right",       M_ChangeControl, GC_STRAFERIGHT },
+	{IT_CALL | IT_STRING2, NULL, "Jump",             M_ChangeControl, GC_JUMP      },
+	{IT_CALL | IT_STRING2, NULL, "Spin",             M_ChangeControl, GC_SPIN     },
 	{IT_HEADER, NULL, "Camera", NULL, 0},
 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
-	{IT_CALL | IT_STRING2, NULL, "Look Up",        M_ChangeControl, gc_lookup      },
-	{IT_CALL | IT_STRING2, NULL, "Look Down",      M_ChangeControl, gc_lookdown    },
-	{IT_CALL | IT_STRING2, NULL, "Look Left",      M_ChangeControl, gc_turnleft    },
-	{IT_CALL | IT_STRING2, NULL, "Look Right",     M_ChangeControl, gc_turnright   },
-	{IT_CALL | IT_STRING2, NULL, "Center View",      M_ChangeControl, gc_centerview  },
-	{IT_CALL | IT_STRING2, NULL, "Toggle Mouselook", M_ChangeControl, gc_mouseaiming },
-	{IT_CALL | IT_STRING2, NULL, "Toggle Third-Person", M_ChangeControl, gc_camtoggle},
-	{IT_CALL | IT_STRING2, NULL, "Reset Camera",     M_ChangeControl, gc_camreset    },
+	{IT_CALL | IT_STRING2, NULL, "Look Up",        M_ChangeControl, GC_LOOKUP      },
+	{IT_CALL | IT_STRING2, NULL, "Look Down",      M_ChangeControl, GC_LOOKDOWN    },
+	{IT_CALL | IT_STRING2, NULL, "Look Left",      M_ChangeControl, GC_TURNLEFT    },
+	{IT_CALL | IT_STRING2, NULL, "Look Right",     M_ChangeControl, GC_TURNRIGHT   },
+	{IT_CALL | IT_STRING2, NULL, "Center View",      M_ChangeControl, GC_CENTERVIEW  },
+	{IT_CALL | IT_STRING2, NULL, "Toggle Mouselook", M_ChangeControl, GC_MOUSEAIMING },
+	{IT_CALL | IT_STRING2, NULL, "Toggle Third-Person", M_ChangeControl, GC_CAMTOGGLE},
+	{IT_CALL | IT_STRING2, NULL, "Reset Camera",     M_ChangeControl, GC_CAMRESET    },
 	{IT_HEADER, NULL, "Meta", NULL, 0},
 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
 	{IT_CALL | IT_STRING2, NULL, "Game Status",
-    M_ChangeControl, gc_scores      },
-	{IT_CALL | IT_STRING2, NULL, "Pause / Run Retry", M_ChangeControl, gc_pause      },
-	{IT_CALL | IT_STRING2, NULL, "Screenshot",            M_ChangeControl, gc_screenshot },
-	{IT_CALL | IT_STRING2, NULL, "Toggle GIF Recording",  M_ChangeControl, gc_recordgif  },
-	{IT_CALL | IT_STRING2, NULL, "Open/Close Menu (ESC)", M_ChangeControl, gc_systemmenu },
-	{IT_CALL | IT_STRING2, NULL, "Change Viewpoint",      M_ChangeControl, gc_viewpoint  },
-	{IT_CALL | IT_STRING2, NULL, "Console",          M_ChangeControl, gc_console     },
+    M_ChangeControl, GC_SCORES      },
+	{IT_CALL | IT_STRING2, NULL, "Pause / Run Retry", M_ChangeControl, GC_PAUSE      },
+	{IT_CALL | IT_STRING2, NULL, "Screenshot",            M_ChangeControl, GC_SCREENSHOT },
+	{IT_CALL | IT_STRING2, NULL, "Toggle GIF Recording",  M_ChangeControl, GC_RECORDGIF  },
+	{IT_CALL | IT_STRING2, NULL, "Open/Close Menu (ESC)", M_ChangeControl, GC_SYSTEMMENU },
+	{IT_CALL | IT_STRING2, NULL, "Change Viewpoint",      M_ChangeControl, GC_VIEWPOINT  },
+	{IT_CALL | IT_STRING2, NULL, "Console",          M_ChangeControl, GC_CONSOLE     },
 	{IT_HEADER, NULL, "Multiplayer", NULL, 0},
 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
-	{IT_CALL | IT_STRING2, NULL, "Talk",             M_ChangeControl, gc_talkkey     },
-	{IT_CALL | IT_STRING2, NULL, "Talk (Team only)", M_ChangeControl, gc_teamkey     },
+	{IT_CALL | IT_STRING2, NULL, "Talk",             M_ChangeControl, GC_TALKKEY     },
+	{IT_CALL | IT_STRING2, NULL, "Talk (Team only)", M_ChangeControl, GC_TEAMKEY     },
 	{IT_HEADER, NULL, "Ringslinger (Match, CTF, Tag, H&S)", NULL, 0},
 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
-	{IT_CALL | IT_STRING2, NULL, "Fire",             M_ChangeControl, gc_fire        },
-	{IT_CALL | IT_STRING2, NULL, "Fire Normal",      M_ChangeControl, gc_firenormal  },
-	{IT_CALL | IT_STRING2, NULL, "Toss Flag",        M_ChangeControl, gc_tossflag    },
-	{IT_CALL | IT_STRING2, NULL, "Next Weapon",      M_ChangeControl, gc_weaponnext  },
-	{IT_CALL | IT_STRING2, NULL, "Prev Weapon",      M_ChangeControl, gc_weaponprev  },
-	{IT_CALL | IT_STRING2, NULL, "Normal / Infinity",   M_ChangeControl, gc_wepslot1    },
-	{IT_CALL | IT_STRING2, NULL, "Automatic",        M_ChangeControl, gc_wepslot2    },
-	{IT_CALL | IT_STRING2, NULL, "Bounce",           M_ChangeControl, gc_wepslot3    },
-	{IT_CALL | IT_STRING2, NULL, "Scatter",          M_ChangeControl, gc_wepslot4    },
-	{IT_CALL | IT_STRING2, NULL, "Grenade",          M_ChangeControl, gc_wepslot5    },
-	{IT_CALL | IT_STRING2, NULL, "Explosion",        M_ChangeControl, gc_wepslot6    },
-	{IT_CALL | IT_STRING2, NULL, "Rail",             M_ChangeControl, gc_wepslot7    },
+	{IT_CALL | IT_STRING2, NULL, "Fire",             M_ChangeControl, GC_FIRE        },
+	{IT_CALL | IT_STRING2, NULL, "Fire Normal",      M_ChangeControl, GC_FIRENORMAL  },
+	{IT_CALL | IT_STRING2, NULL, "Toss Flag",        M_ChangeControl, GC_TOSSFLAG    },
+	{IT_CALL | IT_STRING2, NULL, "Next Weapon",      M_ChangeControl, GC_WEAPONNEXT  },
+	{IT_CALL | IT_STRING2, NULL, "Prev Weapon",      M_ChangeControl, GC_WEAPONPREV  },
+	{IT_CALL | IT_STRING2, NULL, "Normal / Infinity",   M_ChangeControl, GC_WEPSLOT1    },
+	{IT_CALL | IT_STRING2, NULL, "Automatic",        M_ChangeControl, GC_WEPSLOT2    },
+	{IT_CALL | IT_STRING2, NULL, "Bounce",           M_ChangeControl, GC_WEPSLOT3    },
+	{IT_CALL | IT_STRING2, NULL, "Scatter",          M_ChangeControl, GC_WEPSLOT4    },
+	{IT_CALL | IT_STRING2, NULL, "Grenade",          M_ChangeControl, GC_WEPSLOT5    },
+	{IT_CALL | IT_STRING2, NULL, "Explosion",        M_ChangeControl, GC_WEPSLOT6    },
+	{IT_CALL | IT_STRING2, NULL, "Rail",             M_ChangeControl, GC_WEPSLOT7    },
 	{IT_HEADER, NULL, "Add-ons", NULL, 0},
 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
-	{IT_CALL | IT_STRING2, NULL, "Custom Action 1",  M_ChangeControl, gc_custom1     },
-	{IT_CALL | IT_STRING2, NULL, "Custom Action 2",  M_ChangeControl, gc_custom2     },
-	{IT_CALL | IT_STRING2, NULL, "Custom Action 3",  M_ChangeControl, gc_custom3     },
+	{IT_CALL | IT_STRING2, NULL, "Custom Action 1",  M_ChangeControl, GC_CUSTOM1     },
+	{IT_CALL | IT_STRING2, NULL, "Custom Action 2",  M_ChangeControl, GC_CUSTOM2     },
+	{IT_CALL | IT_STRING2, NULL, "Custom Action 3",  M_ChangeControl, GC_CUSTOM3     },
 };
 
 static menuitem_t OP_Joystick1Menu[] =
@@ -3215,7 +3215,7 @@ boolean M_Responder(event_t *ev)
 	if (gamestate == GS_TITLESCREEN && finalecount < TICRATE)
 		return false;
 
-	if (CON_Ready())
+	if (CON_Ready() && gamestate != GS_WAITINGPLAYERS)
 		return false;
 
 	if (noFurtherInput)
@@ -3229,7 +3229,7 @@ boolean M_Responder(event_t *ev)
 		if (ev->type == ev_keydown)
 		{
 			keydown++;
-			ch = ev->data1;
+			ch = ev->key;
 
 			// added 5-2-98 remap virtual keys (mouse & joystick buttons)
 			switch (ch)
@@ -3262,44 +3262,44 @@ boolean M_Responder(event_t *ev)
 					break;
 			}
 		}
-		else if (ev->type == ev_joystick  && ev->data1 == 0 && joywait < I_GetTime())
+		else if (ev->type == ev_joystick  && ev->key == 0 && joywait < I_GetTime())
 		{
 			const INT32 jdeadzone = (JOYAXISRANGE * cv_digitaldeadzone.value) / FRACUNIT;
-			if (ev->data3 != INT32_MAX)
+			if (ev->y != INT32_MAX)
 			{
-				if (Joystick.bGamepadStyle || abs(ev->data3) > jdeadzone)
+				if (Joystick.bGamepadStyle || abs(ev->y) > jdeadzone)
 				{
-					if (ev->data3 < 0 && pjoyy >= 0)
+					if (ev->y < 0 && pjoyy >= 0)
 					{
 						ch = KEY_UPARROW;
 						joywait = I_GetTime() + NEWTICRATE/7;
 					}
-					else if (ev->data3 > 0 && pjoyy <= 0)
+					else if (ev->y > 0 && pjoyy <= 0)
 					{
 						ch = KEY_DOWNARROW;
 						joywait = I_GetTime() + NEWTICRATE/7;
 					}
-					pjoyy = ev->data3;
+					pjoyy = ev->y;
 				}
 				else
 					pjoyy = 0;
 			}
 
-			if (ev->data2 != INT32_MAX)
+			if (ev->x != INT32_MAX)
 			{
-				if (Joystick.bGamepadStyle || abs(ev->data2) > jdeadzone)
+				if (Joystick.bGamepadStyle || abs(ev->x) > jdeadzone)
 				{
-					if (ev->data2 < 0 && pjoyx >= 0)
+					if (ev->x < 0 && pjoyx >= 0)
 					{
 						ch = KEY_LEFTARROW;
 						joywait = I_GetTime() + NEWTICRATE/17;
 					}
-					else if (ev->data2 > 0 && pjoyx <= 0)
+					else if (ev->x > 0 && pjoyx <= 0)
 					{
 						ch = KEY_RIGHTARROW;
 						joywait = I_GetTime() + NEWTICRATE/17;
 					}
-					pjoyx = ev->data2;
+					pjoyx = ev->x;
 				}
 				else
 					pjoyx = 0;
@@ -3307,7 +3307,7 @@ boolean M_Responder(event_t *ev)
 		}
 		else if (ev->type == ev_mouse && mousewait < I_GetTime())
 		{
-			pmousey -= ev->data3;
+			pmousey -= ev->y;
 			if (pmousey < lasty-30)
 			{
 				ch = KEY_DOWNARROW;
@@ -3321,7 +3321,7 @@ boolean M_Responder(event_t *ev)
 				pmousey = lasty += 30;
 			}
 
-			pmousex += ev->data2;
+			pmousex += ev->x;
 			if (pmousex < lastx - 30)
 			{
 				ch = KEY_LEFTARROW;
@@ -3339,11 +3339,11 @@ boolean M_Responder(event_t *ev)
 			keydown = 0;
 	}
 	else if (ev->type == ev_keydown) // Preserve event for other responders
-		ch = ev->data1;
+		ch = ev->key;
 
 	if (ch == -1)
 		return false;
-	else if (ch == gamecontrol[gc_systemmenu][0] || ch == gamecontrol[gc_systemmenu][1]) // allow remappable ESC key
+	else if (ch == gamecontrol[GC_SYSTEMMENU][0] || ch == gamecontrol[GC_SYSTEMMENU][1]) // allow remappable ESC key
 		ch = KEY_ESCAPE;
 
 	// F-Keys
@@ -4062,14 +4062,6 @@ static void M_DrawSlider(INT32 x, INT32 y, const consvar_t *cv, boolean ontop)
 	for (i = 1; i < SLIDER_RANGE; i++)
 		V_DrawScaledPatch (x+i*8, y, 0,p);
 
-	if (ontop)
-	{
-		V_DrawCharacter(x - 6 - (skullAnimCounter/5), y,
-			'\x1C' | V_YELLOWMAP, false);
-		V_DrawCharacter(x+i*8 + 8 + (skullAnimCounter/5), y,
-			'\x1D' | V_YELLOWMAP, false);
-	}
-
 	p = W_CachePatchName("M_SLIDER", PU_PATCH);
 	V_DrawScaledPatch(x+i*8, y, 0, p);
 
@@ -4105,6 +4097,16 @@ static void M_DrawSlider(INT32 x, INT32 y, const consvar_t *cv, boolean ontop)
 		range = 100;
 
 	V_DrawMappedPatch(x + 2 + (SLIDER_RANGE*8*range)/100, y, 0, p, yellowmap);
+
+	if (ontop)
+	{
+		V_DrawCharacter(x - 6 - (skullAnimCounter/5), y,
+			'\x1C' | V_YELLOWMAP, false);
+		V_DrawCharacter(x + 80 + (skullAnimCounter/5), y,
+			'\x1D' | V_YELLOWMAP, false);
+		V_DrawCenteredString(x + 40, y, V_30TRANS,
+			(cv->flags & CV_FLOAT) ? va("%.2f", FIXED_TO_FLOAT(cv->value)) : va("%d", cv->value));
+	}
 }
 
 //
@@ -4185,7 +4187,7 @@ static void M_DrawStaticBox(fixed_t x, fixed_t y, INT32 flags, fixed_t w, fixed_
 	if (staticalong > pw) // simplified for base LSSTATIC
 		staticalong -= pw;
 
-	V_DrawCroppedPatch(x<<FRACBITS, y<<FRACBITS, FRACUNIT/2, flags, patch, staticalong, 0, sw, h*2); // FixedDiv(h, scale)); -- for scale FRACUNIT/2
+	V_DrawCroppedPatch(x<<FRACBITS, y<<FRACBITS, FRACUNIT/2, FRACUNIT/2, flags, patch, NULL, staticalong<<FRACBITS, 0, sw<<FRACBITS, h*2<<FRACBITS); // FixedDiv(h, scale)); -- for scale FRACUNIT/2
 
 	staticalong += sw; //M_RandomRange(sw/2, 2*sw); -- turns out less randomisation looks better because immediately adjacent frames can't end up close to each other
 
@@ -6410,6 +6412,7 @@ static void M_Addons(INT32 choice)
 	M_SetupNextMenu(&MISC_AddonsDef);
 }
 
+#ifdef ENFORCE_WAD_LIMIT
 #define width 4
 #define vpadding 27
 #define h (BASEVIDHEIGHT-(2*vpadding))
@@ -6457,6 +6460,7 @@ static void M_DrawTemperature(INT32 x, fixed_t t)
 #undef vpadding
 #undef h
 #undef NUMCOLOURS
+#endif
 
 static char *M_AddonsHeaderPath(void)
 {
@@ -6550,21 +6554,20 @@ static void M_DrawAddons(void)
 		V_DrawCenteredString(BASEVIDWIDTH/2, 5, 0, LOCATIONSTRING1);
 			// (recommendedflags == V_SKYMAP ? LOCATIONSTRING2 : LOCATIONSTRING1)
 
+#ifdef ENFORCE_WAD_LIMIT
 	if (numwadfiles <= mainwads+1)
 		y = 0;
 	else if (numwadfiles >= MAX_WADFILES)
 		y = FRACUNIT;
 	else
 	{
-		x = FixedDiv(((ssize_t)(numwadfiles) - (ssize_t)(mainwads+1))<<FRACBITS, ((ssize_t)MAX_WADFILES - (ssize_t)(mainwads+1))<<FRACBITS);
-		y = FixedDiv((((ssize_t)packetsizetally-(ssize_t)mainwadstally)<<FRACBITS), ((((ssize_t)MAXFILENEEDED*sizeof(UINT8)-(ssize_t)mainwadstally)-(5+22))<<FRACBITS)); // 5+22 = (a.ext + checksum length) is minimum addition to packet size tally
-		if (x > y)
-			y = x;
+		y = FixedDiv(((ssize_t)(numwadfiles) - (ssize_t)(mainwads+1))<<FRACBITS, ((ssize_t)MAX_WADFILES - (ssize_t)(mainwads+1))<<FRACBITS);
 		if (y > FRACUNIT) // happens because of how we're shrinkin' it a little
 			y = FRACUNIT;
 	}
 
 	M_DrawTemperature(BASEVIDWIDTH - 19 - 5, y);
+#endif
 
 	// DRAW MENU
 	x = currentMenu->x;
@@ -7187,13 +7190,20 @@ static void M_HandleChecklist(INT32 choice)
 
 static void M_DrawChecklist(void)
 {
-	INT32 i = check_on, j = 0, y = currentMenu->y;
+	INT32 i = check_on, j = 0, y = currentMenu->y, emblems = numemblems+numextraemblems;
 	UINT32 condnum, previd, maxcond;
 	condition_t *cond;
 
 	// draw title (or big pic)
 	M_DrawMenuTitle();
 
+	// draw emblem counter
+	if (emblems > 0)
+	{
+		V_DrawString(42, 20, (emblems == M_CountEmblems()) ? V_GREENMAP : 0, va("%d/%d", M_CountEmblems(), emblems));
+		V_DrawSmallScaledPatch(28, 20, 0, W_CachePatchName("EMBLICON", PU_PATCH));
+	}
+
 	if (check_on)
 		V_DrawString(10, y-(skullAnimCounter/5), V_YELLOWMAP, "\x1A");
 
@@ -8652,6 +8662,12 @@ static void M_DrawLoad(void)
 		loadgameoffset = 0;
 
 	M_DrawLoadGameData();
+
+	if (modifiedgame && !savemoddata)
+	{
+		V_DrawCenteredThinString(BASEVIDWIDTH/2, 184, 0, "\x85WARNING: \x80The game is modified.");
+		V_DrawCenteredThinString(BASEVIDWIDTH/2, 192, 0, "Progress will not be saved.");
+	}
 }
 
 //
@@ -8953,7 +8969,7 @@ static void M_HandleLoadSave(INT32 choice)
 			break;
 
 		case KEY_ENTER:
-			if (ultimate_selectable && saveSlotSelected == NOSAVESLOT)
+			if (ultimate_selectable && saveSlotSelected == NOSAVESLOT && !savemoddata && !modifiedgame)
 			{
 				loadgamescroll = 0;
 				S_StartSound(NULL, sfx_skid);
@@ -12826,13 +12842,13 @@ static void M_DrawControl(void)
 			else
 			{
 				if (keys[0] != KEY_NULL)
-					strcat (tmp, G_KeyNumToString (keys[0]));
+					strcat (tmp, G_KeyNumToName (keys[0]));
 
 				if (keys[0] != KEY_NULL && keys[1] != KEY_NULL)
 					strcat(tmp," or ");
 
 				if (keys[1] != KEY_NULL)
-					strcat (tmp, G_KeyNumToString (keys[1]));
+					strcat (tmp, G_KeyNumToName (keys[1]));
 
 
 			}
@@ -12859,7 +12875,7 @@ static void M_ChangecontrolResponse(event_t *ev)
 {
 	INT32        control;
 	INT32        found;
-	INT32        ch = ev->data1;
+	INT32        ch = ev->key;
 
 	// ESCAPE cancels; dummy out PAUSE
 	if (ch != KEY_ESCAPE && ch != KEY_PAUSE)
@@ -12878,7 +12894,7 @@ static void M_ChangecontrolResponse(event_t *ev)
 			// keypad arrows are converted for the menu in cursor arrows
 			// so use the event instead of ch
 			case ev_keydown:
-				ch = ev->data1;
+				ch = ev->key;
 			break;
 
 			default:
@@ -12929,7 +12945,7 @@ static void M_ChangecontrolResponse(event_t *ev)
 		static char tmp[158];
 		menu_t *prev = currentMenu->prevMenu;
 
-		if (controltochange == gc_pause)
+		if (controltochange == GC_PAUSE)
 			sprintf(tmp, M_GetText("The \x82Pause Key \x80is enabled, but \nit cannot be used to retry runs \nduring Record Attack. \n\nHit another key for\n%s\nESC for Cancel"),
 				controltochangetext);
 		else
diff --git a/src/m_misc.c b/src/m_misc.c
index ac60d49c7227b0087e84e017e8b43ae72d555220..59783d5d30dc8d195d5b732d2de474833459f816 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -1631,14 +1631,14 @@ boolean M_ScreenshotResponder(event_t *ev)
 	if (dedicated || ev->type != ev_keydown)
 		return false;
 
-	ch = ev->data1;
+	ch = ev->key;
 
 	if (ch >= KEY_MOUSE1 && menuactive) // If it's not a keyboard key, then don't allow it in the menus!
 		return false;
 
-	if (ch == KEY_F8 || ch == gamecontrol[gc_screenshot][0] || ch == gamecontrol[gc_screenshot][1]) // remappable F8
+	if (ch == KEY_F8 || ch == gamecontrol[GC_SCREENSHOT][0] || ch == gamecontrol[GC_SCREENSHOT][1]) // remappable F8
 		M_ScreenShot();
-	else if (ch == KEY_F9 || ch == gamecontrol[gc_recordgif][0] || ch == gamecontrol[gc_recordgif][1]) // remappable F9
+	else if (ch == KEY_F9 || ch == gamecontrol[GC_RECORDGIF][0] || ch == gamecontrol[GC_RECORDGIF][1]) // remappable F9
 		((moviemode) ? M_StopMovie : M_StartMovie)();
 	else
 		return false;
@@ -2688,3 +2688,22 @@ const char * M_Ftrim (double f)
 		return &dig[1];/* skip the 0 */
 	}
 }
+
+// Returns true if the string is empty.
+boolean M_IsStringEmpty(const char *s)
+{
+	const char *ch = s;
+
+	if (s == NULL || s[0] == '\0')
+		return true;
+
+	for (;;ch++)
+	{
+		if (!(*ch))
+			break;
+		if (!isspace((*ch)))
+			return false;
+	}
+
+	return true;
+}
diff --git a/src/m_misc.h b/src/m_misc.h
index fc5430f013be79da18dd624701111dc3d24ed8d2..82ccd58c75738ef121e0697ba0a2dc6177906e83 100644
--- a/src/m_misc.h
+++ b/src/m_misc.h
@@ -117,6 +117,9 @@ trailing zeros, or "" if the fractional part is zero.
 */
 const char * M_Ftrim (double);
 
+// Returns true if the string is empty.
+boolean M_IsStringEmpty(const char *s);
+
 // counting bits, for weapon ammo code, usually
 FUNCMATH UINT8 M_CountBits(UINT32 num, UINT8 size);
 
diff --git a/src/m_perfstats.c b/src/m_perfstats.c
index 8a99312e6a3d987de4ef6ab7965e31cbdc220458..439a9da1cd009106bc80fa4f3c81e3337a0acbdf 100644
--- a/src/m_perfstats.c
+++ b/src/m_perfstats.c
@@ -22,560 +22,802 @@
 #include "hardware/hw_main.h"
 #endif
 
-struct perfstatcol;
 struct perfstatrow;
 
-typedef struct perfstatcol perfstatcol_t;
 typedef struct perfstatrow perfstatrow_t;
 
-struct perfstatcol {
-	INT32 lores_x;
-	INT32 hires_x;
-	INT32 color;
-	perfstatrow_t * rows;
+struct perfstatrow {
+	const char  * lores_label;
+	const char  * hires_label;
+	ps_metric_t * metric;
+	UINT8         flags;
 };
 
-struct perfstatrow {
-	const char * lores_label;
-	const char * hires_label;
-	void       * value;
+// perfstatrow_t flags
+
+#define PS_TIME      1  // metric measures time (uses precise_t instead of INT32)
+#define PS_LEVEL     2  // metric is valid only when a level is active
+#define PS_SW        4  // metric is valid only in software mode
+#define PS_HW        8  // metric is valid only in opengl mode
+#define PS_BATCHING  16 // metric is valid only when opengl batching is active
+#define PS_HIDE_ZERO 32 // hide metric if its value is zero
+
+static ps_metric_t ps_frametime = {0};
+
+ps_metric_t ps_tictime = {0};
+
+ps_metric_t ps_playerthink_time = {0};
+ps_metric_t ps_thinkertime = {0};
+
+ps_metric_t ps_thlist_times[NUM_THINKERLISTS];
+
+static ps_metric_t ps_thinkercount = {0};
+static ps_metric_t ps_polythcount = {0};
+static ps_metric_t ps_mainthcount = {0};
+static ps_metric_t ps_mobjcount = {0};
+static ps_metric_t ps_regularcount = {0};
+static ps_metric_t ps_scenerycount = {0};
+static ps_metric_t ps_nothinkcount = {0};
+static ps_metric_t ps_dynslopethcount = {0};
+static ps_metric_t ps_precipcount = {0};
+static ps_metric_t ps_removecount = {0};
+
+ps_metric_t ps_checkposition_calls = {0};
+
+ps_metric_t ps_lua_thinkframe_time = {0};
+ps_metric_t ps_lua_mobjhooks = {0};
+
+ps_metric_t ps_otherlogictime = {0};
+
+// Columns for perfstats pages.
+
+// Position on screen is determined separately in the drawing functions.
+
+// New columns must also be added to the drawing and update functions.
+// Drawing functions: PS_DrawRenderStats, PS_DrawGameLogicStats, etc.
+// Update functions:
+//  - PS_UpdateFrameStats for frame-dependent values
+//  - PS_UpdateTickStats for tick-dependent values
+
+// Rendering stats columns
+
+perfstatrow_t rendertime_rows[] = {
+	{"frmtime", "Frame time:    ", &ps_frametime, PS_TIME},
+	{"drwtime", "3d rendering:  ", &ps_rendercalltime, PS_TIME|PS_LEVEL},
+
+#ifdef HWRENDER
+	{" skybox ", " Skybox render: ", &ps_hw_skyboxtime, PS_TIME|PS_LEVEL|PS_HW},
+	{" bsptime", " RenderBSPNode: ", &ps_bsptime, PS_TIME|PS_LEVEL|PS_HW},
+	{" batsort", " Batch sort:    ", &ps_hw_batchsorttime, PS_TIME|PS_LEVEL|PS_HW|PS_BATCHING},
+	{" batdraw", " Batch render:  ", &ps_hw_batchdrawtime, PS_TIME|PS_LEVEL|PS_HW|PS_BATCHING},
+	{" sprsort", " Sprite sort:   ", &ps_hw_spritesorttime, PS_TIME|PS_LEVEL|PS_HW},
+	{" sprdraw", " Sprite render: ", &ps_hw_spritedrawtime, PS_TIME|PS_LEVEL|PS_HW},
+	{" nodesrt", " Drwnode sort:  ", &ps_hw_nodesorttime, PS_TIME|PS_LEVEL|PS_HW},
+	{" nodedrw", " Drwnode render:", &ps_hw_nodedrawtime, PS_TIME|PS_LEVEL|PS_HW},
+	{" other  ", " Other:         ", &ps_otherrendertime, PS_TIME|PS_LEVEL|PS_HW},
+#endif
+
+	{" bsptime", " RenderBSPNode: ", &ps_bsptime, PS_TIME|PS_LEVEL|PS_SW},
+	{" sprclip", " R_ClipSprites: ", &ps_sw_spritecliptime, PS_TIME|PS_LEVEL|PS_SW},
+	{" portals", " Portals+Skybox:", &ps_sw_portaltime, PS_TIME|PS_LEVEL|PS_SW},
+	{" planes ", " R_DrawPlanes:  ", &ps_sw_planetime, PS_TIME|PS_LEVEL|PS_SW},
+	{" masked ", " R_DrawMasked:  ", &ps_sw_maskedtime, PS_TIME|PS_LEVEL|PS_SW},
+	{" other  ", " Other:         ", &ps_otherrendertime, PS_TIME|PS_LEVEL|PS_SW},
+
+	{"ui     ", "UI render:     ", &ps_uitime, PS_TIME},
+	{"finupdt", "I_FinishUpdate:", &ps_swaptime, PS_TIME},
+	{0}
 };
 
-static precise_t ps_frametime = 0;
+perfstatrow_t gamelogicbrief_row[] = {
+	{"logic  ", "Game logic:    ", &ps_tictime, PS_TIME},
+	{0}
+};
 
-precise_t ps_tictime = 0;
+perfstatrow_t commoncounter_rows[] = {
+	{"bspcall", "BSP calls:   ", &ps_numbspcalls, 0},
+	{"sprites", "Sprites:     ", &ps_numsprites, 0},
+	{"drwnode", "Drawnodes:   ", &ps_numdrawnodes, 0},
+	{"plyobjs", "Polyobjects: ", &ps_numpolyobjects, 0},
+	{0}
+};
 
-precise_t ps_playerthink_time = 0;
-precise_t ps_thinkertime = 0;
+#ifdef HWRENDER
+perfstatrow_t batchcount_rows[] = {
+	{"polygon", "Polygons:  ", &ps_hw_numpolys, 0},
+	{"vertex ", "Vertices:  ", &ps_hw_numverts, 0},
+	{0}
+};
 
-precise_t ps_thlist_times[NUM_THINKERLISTS];
+perfstatrow_t batchcalls_rows[] = {
+	{"drwcall", "Draw calls:", &ps_hw_numcalls, 0},
+	{"shaders", "Shaders:   ", &ps_hw_numshaders, 0},
+	{"texture", "Textures:  ", &ps_hw_numtextures, 0},
+	{"polyflg", "Polyflags: ", &ps_hw_numpolyflags, 0},
+	{"colors ", "Colors:    ", &ps_hw_numcolors, 0},
+	{0}
+};
+#endif
 
-int ps_checkposition_calls = 0;
+// Game logic stats columns
+
+perfstatrow_t gamelogic_rows[] = {
+	{"logic  ", "Game logic:     ", &ps_tictime, PS_TIME},
+	{" plrthnk", " P_PlayerThink:  ", &ps_playerthink_time, PS_TIME|PS_LEVEL},
+	{" thnkers", " P_RunThinkers:  ", &ps_thinkertime, PS_TIME|PS_LEVEL},
+	{"  plyobjs", "  Polyobjects:    ", &ps_thlist_times[THINK_POLYOBJ], PS_TIME|PS_LEVEL},
+	{"  main   ", "  Main:           ", &ps_thlist_times[THINK_MAIN], PS_TIME|PS_LEVEL},
+	{"  mobjs  ", "  Mobjs:          ", &ps_thlist_times[THINK_MOBJ], PS_TIME|PS_LEVEL},
+	{"  dynslop", "  Dynamic slopes: ", &ps_thlist_times[THINK_DYNSLOPE], PS_TIME|PS_LEVEL},
+	{"  precip ", "  Precipitation:  ", &ps_thlist_times[THINK_PRECIP], PS_TIME|PS_LEVEL},
+	{" lthinkf", " LUAh_ThinkFrame:", &ps_lua_thinkframe_time, PS_TIME|PS_LEVEL},
+	{" other  ", " Other:          ", &ps_otherlogictime, PS_TIME|PS_LEVEL},
+	{0}
+};
 
-precise_t ps_lua_thinkframe_time = 0;
-int ps_lua_mobjhooks = 0;
+perfstatrow_t thinkercount_rows[] = {
+	{"thnkers", "Thinkers:       ", &ps_thinkercount, PS_LEVEL},
+	{" plyobjs", " Polyobjects:    ", &ps_polythcount, PS_LEVEL},
+	{" main   ", " Main:           ", &ps_mainthcount, PS_LEVEL},
+	{" mobjs  ", " Mobjs:          ", &ps_mobjcount, PS_LEVEL},
+	{"  regular", "  Regular:        ", &ps_regularcount, PS_LEVEL},
+	{"  scenery", "  Scenery:        ", &ps_scenerycount, PS_LEVEL},
+	{"  nothink", "  Nothink:        ", &ps_nothinkcount, PS_HIDE_ZERO|PS_LEVEL},
+	{" dynslop", " Dynamic slopes: ", &ps_dynslopethcount, PS_LEVEL},
+	{" precip ", " Precipitation:  ", &ps_precipcount, PS_LEVEL},
+	{" remove ", " Pending removal:", &ps_removecount, PS_LEVEL},
+	{0}
+};
+
+perfstatrow_t misc_calls_rows[] = {
+	{"lmhook", "Lua mobj hooks: ", &ps_lua_mobjhooks, PS_LEVEL},
+	{"chkpos", "P_CheckPosition:", &ps_checkposition_calls, PS_LEVEL},
+	{0}
+};
+
+// Sample collection status for averaging.
+// Maximum of these two is shown to user if nonzero to tell that
+// the reported averages are not correct yet.
+int ps_frame_samples_left = 0;
+int ps_tick_samples_left = 0;
+// History writing positions for frame and tick based metrics
+int ps_frame_index = 0;
+int ps_tick_index = 0;
 
 // dynamically allocated resizeable array for thinkframe hook stats
 ps_hookinfo_t *thinkframe_hooks = NULL;
 int thinkframe_hooks_length = 0;
 int thinkframe_hooks_capacity = 16;
 
-static INT32 draw_row;
-
 void PS_SetThinkFrameHookInfo(int index, precise_t time_taken, char* short_src)
 {
 	if (!thinkframe_hooks)
 	{
 		// array needs to be initialized
-		thinkframe_hooks = Z_Malloc(sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity, PU_STATIC, NULL);
+		thinkframe_hooks = Z_Calloc(sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity, PU_STATIC, NULL);
 	}
 	if (index >= thinkframe_hooks_capacity)
 	{
 		// array needs more space, realloc with double size
-		thinkframe_hooks_capacity *= 2;
+		int new_capacity = thinkframe_hooks_capacity * 2;
 		thinkframe_hooks = Z_Realloc(thinkframe_hooks,
-			sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity, PU_STATIC, NULL);
+			sizeof(ps_hookinfo_t) * new_capacity, PU_STATIC, NULL);
+		// initialize new memory with zeros so the pointers in the structs are null
+		memset(&thinkframe_hooks[thinkframe_hooks_capacity], 0,
+			sizeof(ps_hookinfo_t) * thinkframe_hooks_capacity);
+		thinkframe_hooks_capacity = new_capacity;
 	}
-	thinkframe_hooks[index].time_taken = time_taken;
+	thinkframe_hooks[index].time_taken.value.p = time_taken;
 	memcpy(thinkframe_hooks[index].short_src, short_src, LUA_IDSIZE * sizeof(char));
 	// since the values are set sequentially from begin to end, the last call should leave
 	// the correct value to this variable
 	thinkframe_hooks_length = index + 1;
 }
 
-static void PS_SetFrameTime(void)
+static boolean PS_HighResolution(void)
 {
-	precise_t currenttime = I_GetPreciseTime();
-	ps_frametime = currenttime - ps_prevframetime;
-	ps_prevframetime = currenttime;
+	return (vid.width >= 640 && vid.height >= 400);
 }
 
-static boolean M_HighResolution(void)
+static boolean PS_IsLevelActive(void)
 {
-	return (vid.width >= 640 && vid.height >= 400);
+	return gamestate == GS_LEVEL ||
+			(gamestate == GS_TITLESCREEN && titlemapinaction);
 }
 
-enum {
-	PERF_TIME,
-	PERF_COUNT,
-};
+// Is the row valid in the current context?
+static boolean PS_IsRowValid(perfstatrow_t *row)
+{
+	return !((row->flags & PS_LEVEL && !PS_IsLevelActive()) ||
+		(row->flags & PS_SW && rendermode != render_soft) ||
+		(row->flags & PS_HW && rendermode != render_opengl) ||
+		(row->flags & PS_BATCHING && !cv_glbatching.value));
+}
 
-static void M_DrawPerfString(perfstatcol_t *col, int type)
+// Should the row be visible on the screen?
+static boolean PS_IsRowVisible(perfstatrow_t *row)
 {
-	const boolean hires = M_HighResolution();
+	boolean value_is_zero;
 
-	INT32 draw_flags = V_MONOSPACE | col->color;
+	if (row->flags & PS_TIME)
+		value_is_zero = row->metric->value.p == 0;
+	else
+		value_is_zero = row->metric->value.i == 0;
 
-	perfstatrow_t * row;
+	return !(!PS_IsRowValid(row) ||
+		(row->flags & PS_HIDE_ZERO && value_is_zero));
+}
+
+static INT32 PS_GetMetricAverage(ps_metric_t *metric, boolean time_metric)
+{
+	char* history_read_pos = metric->history; // char* used for pointer arithmetic
+	INT64 sum = 0;
+	int i;
+	int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
+
+	for (i = 0; i < cv_ps_samplesize.value; i++)
+	{
+		if (time_metric)
+			sum += I_PreciseToMicros(*((precise_t*)history_read_pos));
+		else
+			sum += *((INT32*)history_read_pos);
+		history_read_pos += value_size;
+	}
+
+	return sum / cv_ps_samplesize.value;
+}
+
+static INT32 PS_GetMetricMinOrMax(ps_metric_t *metric, boolean time_metric, boolean get_max)
+{
+	char* history_read_pos = metric->history; // char* used for pointer arithmetic
+	INT32 found_value = get_max ? INT32_MIN : INT32_MAX;
+	int i;
+	int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
+
+	for (i = 0; i < cv_ps_samplesize.value; i++)
+	{
+		INT32 value;
+		if (time_metric)
+			value = I_PreciseToMicros(*((precise_t*)history_read_pos));
+		else
+			value = *((INT32*)history_read_pos);
+
+		if ((get_max && value > found_value) ||
+			(!get_max && value < found_value))
+		{
+			found_value = value;
+		}
+		history_read_pos += value_size;
+	}
+
+	return found_value;
+}
+
+// Calculates the standard deviation for metric.
+static INT32 PS_GetMetricSD(ps_metric_t *metric, boolean time_metric)
+{
+	char* history_read_pos = metric->history; // char* used for pointer arithmetic
+	INT64 sum = 0;
+	int i;
+	int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
+	INT32 avg = PS_GetMetricAverage(metric, time_metric);
+
+	for (i = 0; i < cv_ps_samplesize.value; i++)
+	{
+		INT64 value;
+		if (time_metric)
+			value = I_PreciseToMicros(*((precise_t*)history_read_pos));
+		else
+			value = *((INT32*)history_read_pos);
+
+		value -= avg;
+		sum += value * value;
+
+		history_read_pos += value_size;
+	}
+
+	return round(sqrt(sum / cv_ps_samplesize.value));
+}
 
-	int value;
+// Returns the value to show on screen for metric.
+static INT32 PS_GetMetricScreenValue(ps_metric_t *metric, boolean time_metric)
+{
+	if (cv_ps_samplesize.value > 1 && metric->history)
+	{
+		if (cv_ps_descriptor.value == 1)
+			return PS_GetMetricAverage(metric, time_metric);
+		else if (cv_ps_descriptor.value == 2)
+			return PS_GetMetricSD(metric, time_metric);
+		else if (cv_ps_descriptor.value == 3)
+			return PS_GetMetricMinOrMax(metric, time_metric, false);
+		else
+			return PS_GetMetricMinOrMax(metric, time_metric, true);
+	}
+	else
+	{
+		if (time_metric)
+			return I_PreciseToMicros(metric->value.p);
+		else
+			return metric->value.i;
+	}
+}
+
+static int PS_DrawPerfRows(int x, int y, int color, perfstatrow_t *rows)
+{
+	const boolean hires = PS_HighResolution();
+	INT32 draw_flags = V_MONOSPACE | color;
+	perfstatrow_t * row;
+	int draw_y = y;
 
 	if (hires)
 		draw_flags |= V_ALLOWLOWERCASE;
 
-	for (row = col->rows; row->lores_label; ++row)
+	for (row = rows; row->lores_label; ++row)
 	{
-		if (type == PERF_TIME)
-			value = I_PreciseToMicros(*(precise_t *)row->value);
-		else
-			value = *(int *)row->value;
+		const char *label;
+		INT32 value;
+		char *final_str;
+
+		if (!PS_IsRowVisible(row))
+			continue;
+
+		label = hires ? row->hires_label : row->lores_label;
+		value = PS_GetMetricScreenValue(row->metric, !!(row->flags & PS_TIME));
+		final_str = va("%s %d", label, value);
 
 		if (hires)
 		{
-			V_DrawSmallString(col->hires_x, draw_row, draw_flags,
-					va("%s %d", row->hires_label, value));
-
-			draw_row += 5;
+			V_DrawSmallString(x, draw_y, draw_flags, final_str);
+			draw_y += 5;
 		}
 		else
 		{
-			V_DrawThinString(col->lores_x, draw_row, draw_flags,
-					va("%s %d", row->lores_label, value));
-
-			draw_row += 8;
+			V_DrawThinString(x, draw_y, draw_flags, final_str);
+			draw_y += 8;
 		}
 	}
+
+	return draw_y;
 }
 
-static void M_DrawPerfTiming(perfstatcol_t *col)
+static void PS_UpdateMetricHistory(ps_metric_t *metric, boolean time_metric, boolean frame_metric, boolean set_user)
 {
-	M_DrawPerfString(col, PERF_TIME);
+	int index = frame_metric ? ps_frame_index : ps_tick_index;
+
+	if (!metric->history)
+	{
+		// allocate history table
+		int value_size = time_metric ? sizeof(precise_t) : sizeof(INT32);
+		void** memory_user = set_user ? &metric->history : NULL;
+
+		metric->history = Z_Calloc(value_size * cv_ps_samplesize.value, PU_PERFSTATS,
+				memory_user);
+
+		// reset "samples left" counter since this history table needs to be filled
+		if (frame_metric)
+			ps_frame_samples_left = cv_ps_samplesize.value;
+		else
+			ps_tick_samples_left = cv_ps_samplesize.value;
+	}
+
+	if (time_metric)
+	{
+		precise_t *history = (precise_t*)metric->history;
+		history[index] = metric->value.p;
+	}
+	else
+	{
+		INT32 *history = (INT32*)metric->history;
+		history[index] = metric->value.i;
+	}
 }
 
-static void M_DrawPerfCount(perfstatcol_t *col)
+static void PS_UpdateRowHistories(perfstatrow_t *rows, boolean frame_metric)
 {
-	M_DrawPerfString(col, PERF_COUNT);
+	perfstatrow_t *row;
+	for (row = rows; row->lores_label; row++)
+	{
+		if (PS_IsRowValid(row))
+			PS_UpdateMetricHistory(row->metric, !!(row->flags & PS_TIME), frame_metric, true);
+	}
 }
 
-static void M_DrawRenderStats(void)
+// Update all metrics that are calculated on every frame.
+static void PS_UpdateFrameStats(void)
 {
-	const boolean hires = M_HighResolution();
-
-	const int half_row = hires ? 5 : 4;
+	// update frame time
+	precise_t currenttime = I_GetPreciseTime();
+	ps_frametime.value.p = currenttime - ps_prevframetime;
+	ps_prevframetime = currenttime;
 
-	precise_t extrarendertime;
-
-	perfstatrow_t frametime_row[] = {
-		{"frmtime", "Frame time:    ", &ps_frametime},
-		{0}
-	};
-
-	perfstatrow_t rendercalltime_row[] = {
-		{"drwtime", "3d rendering:  ", &ps_rendercalltime},
-		{0}
-	};
-
-	perfstatrow_t opengltime_row[] = {
-		{"skybox ", "Skybox render: ", &ps_hw_skyboxtime},
-		{"bsptime", "RenderBSPNode: ", &ps_bsptime},
-		{"nodesrt", "Drwnode sort:  ", &ps_hw_nodesorttime},
-		{"nodedrw", "Drwnode render:", &ps_hw_nodedrawtime},
-		{"sprsort", "Sprite sort:   ", &ps_hw_spritesorttime},
-		{"sprdraw", "Sprite render: ", &ps_hw_spritedrawtime},
-		{"other  ", "Other:         ", &extrarendertime},
-		{0}
-	};
-
-	perfstatrow_t softwaretime_row[] = {
-		{"bsptime", "RenderBSPNode: ", &ps_bsptime},
-		{"sprclip", "R_ClipSprites: ", &ps_sw_spritecliptime},
-		{"portals", "Portals+Skybox:", &ps_sw_portaltime},
-		{"planes ", "R_DrawPlanes:  ", &ps_sw_planetime},
-		{"masked ", "R_DrawMasked:  ", &ps_sw_maskedtime},
-		{"other  ", "Other:         ", &extrarendertime},
-		{0}
-	};
-
-	perfstatrow_t uiswaptime_row[] = {
-		{"ui     ", "UI render:     ", &ps_uitime},
-		{"finupdt", "I_FinishUpdate:", &ps_swaptime},
-		{0}
-	};
-
-	perfstatrow_t tictime_row[] = {
-		{"logic  ", "Game logic:    ", &ps_tictime},
-		{0}
-	};
-
-	perfstatrow_t rendercalls_row[] = {
-		{"bspcall", "BSP calls:   ", &ps_numbspcalls},
-		{"sprites", "Sprites:     ", &ps_numsprites},
-		{"drwnode", "Drawnodes:   ", &ps_numdrawnodes},
-		{"plyobjs", "Polyobjects: ", &ps_numpolyobjects},
-		{0}
-	};
-
-	perfstatrow_t batchtime_row[] = {
-		{"batsort", "Batch sort:  ", &ps_hw_batchsorttime},
-		{"batdraw", "Batch render:", &ps_hw_batchdrawtime},
-		{0}
-	};
-
-	perfstatrow_t batchcount_row[] = {
-		{"polygon", "Polygons:  ", &ps_hw_numpolys},
-		{"vertex ", "Vertices:  ", &ps_hw_numverts},
-		{0}
-	};
-
-	perfstatrow_t batchcalls_row[] = {
-		{"drwcall", "Draw calls:", &ps_hw_numcalls},
-		{"shaders", "Shaders:   ", &ps_hw_numshaders},
-		{"texture", "Textures:  ", &ps_hw_numtextures},
-		{"polyflg", "Polyflags: ", &ps_hw_numpolyflags},
-		{"colors ", "Colors:    ", &ps_hw_numcolors},
-		{0}
-	};
-
-	perfstatcol_t      frametime_col =  {20,  20, V_YELLOWMAP,      frametime_row};
-	perfstatcol_t rendercalltime_col =  {20,  20, V_YELLOWMAP, rendercalltime_row};
-
-	perfstatcol_t     opengltime_col =  {24,  24, V_YELLOWMAP,     opengltime_row};
-	perfstatcol_t   softwaretime_col =  {24,  24, V_YELLOWMAP,   softwaretime_row};
-
-	perfstatcol_t     uiswaptime_col =  {20,  20, V_YELLOWMAP,     uiswaptime_row};
-	perfstatcol_t        tictime_col =  {20,  20, V_GRAYMAP,          tictime_row};
-
-	perfstatcol_t    rendercalls_col =  {90, 115, V_BLUEMAP,      rendercalls_row};
-
-	perfstatcol_t      batchtime_col =  {90, 115, V_REDMAP,         batchtime_row};
-
-	perfstatcol_t     batchcount_col = {155, 200, V_PURPLEMAP,     batchcount_row};
-	perfstatcol_t     batchcalls_col = {220, 200, V_PURPLEMAP,     batchcalls_row};
-
-
-	boolean rendering = (
-			gamestate == GS_LEVEL ||
-			(gamestate == GS_TITLESCREEN && titlemapinaction)
-	);
-
-	draw_row = 10;
-	M_DrawPerfTiming(&frametime_col);
-
-	if (rendering)
+	// update 3d rendering stats
+	if (PS_IsLevelActive())
 	{
-		M_DrawPerfTiming(&rendercalltime_col);
-
 		// Remember to update this calculation when adding more 3d rendering stats!
-		extrarendertime = ps_rendercalltime - ps_bsptime;
+		ps_otherrendertime.value.p = ps_rendercalltime.value.p - ps_bsptime.value.p;
 
 #ifdef HWRENDER
 		if (rendermode == render_opengl)
 		{
-			extrarendertime -=
-				ps_hw_skyboxtime +
-				ps_hw_nodesorttime +
-				ps_hw_nodedrawtime +
-				ps_hw_spritesorttime +
-				ps_hw_spritedrawtime;
+			ps_otherrendertime.value.p -=
+				ps_hw_skyboxtime.value.p +
+				ps_hw_nodesorttime.value.p +
+				ps_hw_nodedrawtime.value.p +
+				ps_hw_spritesorttime.value.p +
+				ps_hw_spritedrawtime.value.p;
 
 			if (cv_glbatching.value)
 			{
-				extrarendertime -=
-					ps_hw_batchsorttime +
-					ps_hw_batchdrawtime;
+				ps_otherrendertime.value.p -=
+					ps_hw_batchsorttime.value.p +
+					ps_hw_batchdrawtime.value.p;
 			}
-
-			M_DrawPerfTiming(&opengltime_col);
 		}
 		else
 #endif
 		{
-			extrarendertime -=
-				ps_sw_spritecliptime +
-				ps_sw_portaltime +
-				ps_sw_planetime +
-				ps_sw_maskedtime;
-
-			M_DrawPerfTiming(&softwaretime_col);
+			ps_otherrendertime.value.p -=
+				ps_sw_spritecliptime.value.p +
+				ps_sw_portaltime.value.p +
+				ps_sw_planetime.value.p +
+				ps_sw_maskedtime.value.p;
 		}
 	}
 
-	M_DrawPerfTiming(&uiswaptime_col);
-
-	draw_row += half_row;
-	M_DrawPerfTiming(&tictime_col);
-
-	if (rendering)
+	if (cv_ps_samplesize.value > 1)
 	{
-		draw_row = 10;
-		M_DrawPerfCount(&rendercalls_col);
+		PS_UpdateRowHistories(rendertime_rows, true);
+		if (PS_IsLevelActive())
+			PS_UpdateRowHistories(commoncounter_rows, true);
 
 #ifdef HWRENDER
 		if (rendermode == render_opengl && cv_glbatching.value)
 		{
-			draw_row += half_row;
-			M_DrawPerfTiming(&batchtime_col);
-
-			draw_row = 10;
-			M_DrawPerfCount(&batchcount_col);
-
-			if (hires)
-				draw_row += half_row;
-			else
-				draw_row  = 10;
-
-			M_DrawPerfCount(&batchcalls_col);
+			PS_UpdateRowHistories(batchcount_rows, true);
+			PS_UpdateRowHistories(batchcalls_rows, true);
 		}
 #endif
+
+		ps_frame_index++;
+		if (ps_frame_index >= cv_ps_samplesize.value)
+			ps_frame_index = 0;
+		if (ps_frame_samples_left)
+			ps_frame_samples_left--;
 	}
 }
 
-static void M_DrawTickStats(void)
+// Update thinker counters by iterating the thinker lists.
+static void PS_CountThinkers(void)
 {
-	int i = 0;
+	int i;
 	thinker_t *thinker;
-	int thinkercount = 0;
-	int polythcount = 0;
-	int mainthcount = 0;
-	int mobjcount = 0;
-	int nothinkcount = 0;
-	int scenerycount = 0;
-	int regularcount = 0;
-	int dynslopethcount = 0;
-	int precipcount = 0;
-	int removecount = 0;
-
-	precise_t extratime =
-		ps_tictime -
-		ps_playerthink_time -
-		ps_thinkertime -
-		ps_lua_thinkframe_time;
-
-	perfstatrow_t tictime_row[] = {
-		{"logic  ", "Game logic:     ", &ps_tictime},
-		{0}
-	};
-
-	perfstatrow_t thinker_time_row[] = {
-		{"plrthnk", "P_PlayerThink:  ", &ps_playerthink_time},
-		{"thnkers", "P_RunThinkers:  ", &ps_thinkertime},
-		{0}
-	};
-
-	perfstatrow_t detailed_thinker_time_row[] = {
-		{"plyobjs", "Polyobjects:    ", &ps_thlist_times[THINK_POLYOBJ]},
-		{"main   ", "Main:           ", &ps_thlist_times[THINK_MAIN]},
-		{"mobjs  ", "Mobjs:          ", &ps_thlist_times[THINK_MOBJ]},
-		{"dynslop", "Dynamic slopes: ", &ps_thlist_times[THINK_DYNSLOPE]},
-		{"precip ", "Precipitation:  ", &ps_thlist_times[THINK_PRECIP]},
-		{0}
-	};
-
-	perfstatrow_t extra_thinker_time_row[] = {
-		{"lthinkf", "LUAh_ThinkFrame:", &ps_lua_thinkframe_time},
-		{"other  ", "Other:          ", &extratime},
-		{0}
-	};
-
-	perfstatrow_t thinkercount_row[] = {
-		{"thnkers", "Thinkers:       ", &thinkercount},
-		{0}
-	};
-
-	perfstatrow_t detailed_thinkercount_row[] = {
-		{"plyobjs", "Polyobjects:    ", &polythcount},
-		{"main   ", "Main:           ", &mainthcount},
-		{"mobjs  ", "Mobjs:          ", &mobjcount},
-		{0}
-	};
-
-	perfstatrow_t mobjthinkercount_row[] = {
-		{"regular", "Regular:        ", &regularcount},
-		{"scenery", "Scenery:        ", &scenerycount},
-		{0}
-	};
-
-	perfstatrow_t nothinkcount_row[] = {
-		{"nothink", "Nothink:        ", &nothinkcount},
-		{0}
-	};
-
-	perfstatrow_t detailed_thinkercount_row2[] = {
-		{"dynslop", "Dynamic slopes: ", &dynslopethcount},
-		{"precip ", "Precipitation:  ", &precipcount},
-		{"remove ", "Pending removal:", &removecount},
-		{0}
-	};
-
-	perfstatrow_t misc_calls_row[] = {
-		{"lmhook", "Lua mobj hooks: ", &ps_lua_mobjhooks},
-		{"chkpos", "P_CheckPosition:", &ps_checkposition_calls},
-		{0}
-	};
-
-	perfstatcol_t               tictime_col  =  {20,  20, V_YELLOWMAP,               tictime_row};
-	perfstatcol_t          thinker_time_col  =  {24,  24, V_YELLOWMAP,          thinker_time_row};
-	perfstatcol_t detailed_thinker_time_col  =  {28,  28, V_YELLOWMAP, detailed_thinker_time_row};
-	perfstatcol_t    extra_thinker_time_col  =  {24,  24, V_YELLOWMAP,    extra_thinker_time_row};
-
-	perfstatcol_t          thinkercount_col  =  {90, 115, V_BLUEMAP,            thinkercount_row};
-	perfstatcol_t detailed_thinkercount_col  =  {94, 119, V_BLUEMAP,   detailed_thinkercount_row};
-	perfstatcol_t      mobjthinkercount_col  =  {98, 123, V_BLUEMAP,        mobjthinkercount_row};
-	perfstatcol_t          nothinkcount_col  =  {98, 123, V_BLUEMAP,            nothinkcount_row};
-	perfstatcol_t detailed_thinkercount_col2 =  {94, 119, V_BLUEMAP,   detailed_thinkercount_row2};
-	perfstatcol_t            misc_calls_col  = {170, 216, V_PURPLEMAP,            misc_calls_row};
+
+	ps_thinkercount.value.i = 0;
+	ps_polythcount.value.i = 0;
+	ps_mainthcount.value.i = 0;
+	ps_mobjcount.value.i = 0;
+	ps_regularcount.value.i = 0;
+	ps_scenerycount.value.i = 0;
+	ps_nothinkcount.value.i = 0;
+	ps_dynslopethcount.value.i = 0;
+	ps_precipcount.value.i = 0;
+	ps_removecount.value.i = 0;
 
 	for (i = 0; i < NUM_THINKERLISTS; i++)
 	{
 		for (thinker = thlist[i].next; thinker != &thlist[i]; thinker = thinker->next)
 		{
-			thinkercount++;
+			ps_thinkercount.value.i++;
 			if (thinker->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
-				removecount++;
+				ps_removecount.value.i++;
 			else if (i == THINK_POLYOBJ)
-				polythcount++;
+				ps_polythcount.value.i++;
 			else if (i == THINK_MAIN)
-				mainthcount++;
+				ps_mainthcount.value.i++;
 			else if (i == THINK_MOBJ)
 			{
 				if (thinker->function.acp1 == (actionf_p1)P_MobjThinker)
 				{
 					mobj_t *mobj = (mobj_t*)thinker;
-					mobjcount++;
+					ps_mobjcount.value.i++;
 					if (mobj->flags & MF_NOTHINK)
-						nothinkcount++;
+						ps_nothinkcount.value.i++;
 					else if (mobj->flags & MF_SCENERY)
-						scenerycount++;
+						ps_scenerycount.value.i++;
 					else
-						regularcount++;
+						ps_regularcount.value.i++;
 				}
 			}
 			else if (i == THINK_DYNSLOPE)
-				dynslopethcount++;
+				ps_dynslopethcount.value.i++;
 			else if (i == THINK_PRECIP)
-				precipcount++;
+				ps_precipcount.value.i++;
 		}
 	}
+}
 
-	draw_row = 10;
-	M_DrawPerfTiming(&tictime_col);
-	M_DrawPerfTiming(&thinker_time_col);
-	M_DrawPerfTiming(&detailed_thinker_time_col);
-	M_DrawPerfTiming(&extra_thinker_time_col);
-
-	draw_row = 10;
-	M_DrawPerfCount(&thinkercount_col);
-	M_DrawPerfCount(&detailed_thinkercount_col);
-	M_DrawPerfCount(&mobjthinkercount_col);
+// Update all metrics that are calculated on every tick.
+void PS_UpdateTickStats(void)
+{
+	if (cv_perfstats.value == 1 && cv_ps_samplesize.value > 1)
+	{
+		PS_UpdateRowHistories(gamelogicbrief_row, false);
+	}
+	if (cv_perfstats.value == 2)
+	{
+		if (PS_IsLevelActive())
+		{
+			ps_otherlogictime.value.p =
+				ps_tictime.value.p -
+				ps_playerthink_time.value.p -
+				ps_thinkertime.value.p -
+				ps_lua_thinkframe_time.value.p;
 
-	if (nothinkcount)
-		M_DrawPerfCount(&nothinkcount_col);
+			PS_CountThinkers();
+		}
 
-	M_DrawPerfCount(&detailed_thinkercount_col2);
+		if (cv_ps_samplesize.value > 1)
+		{
+			PS_UpdateRowHistories(gamelogic_rows, false);
+			PS_UpdateRowHistories(thinkercount_rows, false);
+			PS_UpdateRowHistories(misc_calls_rows, false);
+		}
+	}
+	if (cv_perfstats.value == 3 && cv_ps_samplesize.value > 1 && PS_IsLevelActive())
+	{
+		int i;
+		for (i = 0; i < thinkframe_hooks_length; i++)
+		{
+			PS_UpdateMetricHistory(&thinkframe_hooks[i].time_taken, true, false, false);
+		}
+	}
+	if (cv_perfstats.value && cv_ps_samplesize.value > 1)
+	{
+		ps_tick_index++;
+		if (ps_tick_index >= cv_ps_samplesize.value)
+			ps_tick_index = 0;
+		if (ps_tick_samples_left)
+			ps_tick_samples_left--;
+	}
+}
 
-	if (M_HighResolution())
+static void PS_DrawDescriptorHeader(void)
+{
+	if (cv_ps_samplesize.value > 1)
 	{
-		V_DrawSmallString(212, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, "Calls:");
+		const char* descriptor_names[] = {
+			"average",
+			"standard deviation",
+			"minimum",
+			"maximum"
+		};
+		const boolean hires = PS_HighResolution();
+		char* str;
+		INT32 flags = V_MONOSPACE | V_ALLOWLOWERCASE;
+		int samples_left = max(ps_frame_samples_left, ps_tick_samples_left);
+		int x, y;
+
+		if (cv_perfstats.value == 3)
+		{
+			x = 2;
+			y = 0;
+		}
+		else
+		{
+			x = 20;
+			y = hires ? 5 : 2;
+		}
+
+		if (samples_left)
+		{
+			str = va("Samples needed for correct results: %d", samples_left);
+			flags |= V_REDMAP;
+		}
+		else
+		{
+			str = va("Showing the %s of %d samples.",
+					descriptor_names[cv_ps_descriptor.value - 1], cv_ps_samplesize.value);
+			flags |= V_GREENMAP;
+		}
 
-		draw_row = 15;
+		if (hires)
+			V_DrawSmallString(x, y, flags, str);
+		else
+			V_DrawThinString(x, y, flags, str);
 	}
-	else
+}
+
+static void PS_DrawRenderStats(void)
+{
+	const boolean hires = PS_HighResolution();
+	const int half_row = hires ? 5 : 4;
+	int x, y;
+
+	PS_DrawDescriptorHeader();
+
+	y = PS_DrawPerfRows(20, 10, V_YELLOWMAP, rendertime_rows);
+
+	PS_DrawPerfRows(20, y + half_row, V_GRAYMAP, gamelogicbrief_row);
+
+	if (PS_IsLevelActive())
 	{
-		draw_row = 10;
+		x = hires ? 115 : 90;
+		PS_DrawPerfRows(x, 10, V_BLUEMAP, commoncounter_rows);
+
+#ifdef HWRENDER
+		if (rendermode == render_opengl && cv_glbatching.value)
+		{
+			x = hires ? 200 : 155;
+			y = PS_DrawPerfRows(x, 10, V_PURPLEMAP, batchcount_rows);
+
+			x = hires ? 200 : 220;
+			y = hires ? y + half_row : 10;
+			PS_DrawPerfRows(x, y, V_PURPLEMAP, batchcalls_rows);
+		}
+#endif
 	}
+}
+
+static void PS_DrawGameLogicStats(void)
+{
+	const boolean hires = PS_HighResolution();
+	int x, y;
+
+	PS_DrawDescriptorHeader();
+
+	PS_DrawPerfRows(20, 10, V_YELLOWMAP, gamelogic_rows);
 
-	M_DrawPerfCount(&misc_calls_col);
+	x = hires ? 115 : 90;
+	PS_DrawPerfRows(x, 10, V_BLUEMAP, thinkercount_rows);
+
+	if (hires)
+		V_DrawSmallString(212, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, "Calls:");
+
+	x = hires ? 216 : 170;
+	y = hires ? 15 : 10;
+	PS_DrawPerfRows(x, y, V_PURPLEMAP, misc_calls_rows);
 }
 
-void M_DrawPerfStats(void)
+static void PS_DrawThinkFrameStats(void)
 {
 	char s[100];
+	int i;
+	// text writing position
+	int x = 2;
+	int y = 4;
+	UINT32 text_color;
+	char tempbuffer[LUA_IDSIZE];
+	char last_mod_name[LUA_IDSIZE];
+	last_mod_name[0] = '\0';
+
+	PS_DrawDescriptorHeader();
+
+	for (i = 0; i < thinkframe_hooks_length; i++)
+	{
 
-	PS_SetFrameTime();
+#define NEXT_ROW() \
+y += 4; \
+if (y > 192) \
+{ \
+	y = 4; \
+	x += 106; \
+	if (x > 214) \
+		break; \
+}
+
+		char* str = thinkframe_hooks[i].short_src;
+		char* tempstr = tempbuffer;
+		int len = (int)strlen(str);
+		char* str_ptr;
+		if (strcmp(".lua", str + len - 4) == 0)
+		{
+			str[len-4] = '\0'; // remove .lua at end
+			len -= 4;
+		}
+		// we locate the wad/pk3 name in the string and compare it to
+		// what we found on the previous iteration.
+		// if the name has changed, print it out on the screen
+		strcpy(tempstr, str);
+		str_ptr = strrchr(tempstr, '|');
+		if (str_ptr)
+		{
+			*str_ptr = '\0';
+			str = str_ptr + 1; // this is the name of the hook without the mod file name
+			str_ptr = strrchr(tempstr, PATHSEP[0]);
+			if (str_ptr)
+				tempstr = str_ptr + 1;
+			// tempstr should now point to the mod name, (wad/pk3) possibly truncated
+			if (strcmp(tempstr, last_mod_name) != 0)
+			{
+				strcpy(last_mod_name, tempstr);
+				len = (int)strlen(tempstr);
+				if (len > 25)
+					tempstr += len - 25;
+				snprintf(s, sizeof s - 1, "%s", tempstr);
+				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
+				NEXT_ROW()
+			}
+			text_color = V_YELLOWMAP;
+		}
+		else
+		{
+			// probably a standalone lua file
+			// cut off the folder if it's there
+			str_ptr = strrchr(tempstr, PATHSEP[0]);
+			if (str_ptr)
+				str = str_ptr + 1;
+			text_color = 0; // white
+		}
+		len = (int)strlen(str);
+		if (len > 20)
+			str += len - 20;
+		snprintf(s, sizeof s - 1, "%20s: %d", str,
+				PS_GetMetricScreenValue(&thinkframe_hooks[i].time_taken, true));
+		V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | text_color, s);
+		NEXT_ROW()
+
+#undef NEXT_ROW
+
+	}
+}
 
+void M_DrawPerfStats(void)
+{
 	if (cv_perfstats.value == 1) // rendering
 	{
-		M_DrawRenderStats();
+		PS_UpdateFrameStats();
+		PS_DrawRenderStats();
 	}
 	else if (cv_perfstats.value == 2) // logic
 	{
-		M_DrawTickStats();
+		// PS_UpdateTickStats is called in TryRunTics, since otherwise it would miss
+		// tics when frame skips happen
+		PS_DrawGameLogicStats();
 	}
 	else if (cv_perfstats.value == 3) // lua thinkframe
 	{
-		if (!(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
+		if (!PS_IsLevelActive())
 			return;
-		if (vid.width < 640 || vid.height < 400) // low resolution
+		if (!PS_HighResolution())
 		{
-			// it's not gonna fit very well..
-			V_DrawThinString(30, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "Not available for resolutions below 640x400");
+			// Low resolutions can't really use V_DrawSmallString that is used by thinkframe stats.
+			// A low-res version using V_DrawThinString could be implemented,
+			// but it would have much less space for information.
+			V_DrawThinString(80, 92, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "Perfstats 3 is not available");
+			V_DrawThinString(80, 100, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, "for resolutions below 640x400.");
 		}
-		else // high resolution
+		else
 		{
-			int i;
-			// text writing position
-			int x = 2;
-			int y = 4;
-			UINT32 text_color;
-			char tempbuffer[LUA_IDSIZE];
-			char last_mod_name[LUA_IDSIZE];
-			last_mod_name[0] = '\0';
-			for (i = 0; i < thinkframe_hooks_length; i++)
-			{
-				char* str = thinkframe_hooks[i].short_src;
-				char* tempstr = tempbuffer;
-				int len = (int)strlen(str);
-				char* str_ptr;
-				if (strcmp(".lua", str + len - 4) == 0)
-				{
-					str[len-4] = '\0'; // remove .lua at end
-					len -= 4;
-				}
-				// we locate the wad/pk3 name in the string and compare it to
-				// what we found on the previous iteration.
-				// if the name has changed, print it out on the screen
-				strcpy(tempstr, str);
-				str_ptr = strrchr(tempstr, '|');
-				if (str_ptr)
-				{
-					*str_ptr = '\0';
-					str = str_ptr + 1; // this is the name of the hook without the mod file name
-					str_ptr = strrchr(tempstr, PATHSEP[0]);
-					if (str_ptr)
-						tempstr = str_ptr + 1;
-					// tempstr should now point to the mod name, (wad/pk3) possibly truncated
-					if (strcmp(tempstr, last_mod_name) != 0)
-					{
-						strcpy(last_mod_name, tempstr);
-						len = (int)strlen(tempstr);
-						if (len > 25)
-							tempstr += len - 25;
-						snprintf(s, sizeof s - 1, "%s", tempstr);
-						V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
-						y += 4; // repeated code!
-						if (y > 192)
-						{
-							y = 4;
-							x += 106;
-							if (x > 214)
-								break;
-						}
-					}
-					text_color = V_YELLOWMAP;
-				}
-				else
-				{
-					// probably a standalone lua file
-					// cut off the folder if it's there
-					str_ptr = strrchr(tempstr, PATHSEP[0]);
-					if (str_ptr)
-						str = str_ptr + 1;
-					text_color = 0; // white
-				}
-				len = (int)strlen(str);
-				if (len > 20)
-					str += len - 20;
-				snprintf(s, sizeof s - 1, "%20s: %d", str, I_PreciseToMicros(thinkframe_hooks[i].time_taken));
-				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | text_color, s);
-				y += 4; // repeated code!
-				if (y > 192)
-				{
-					y = 4;
-					x += 106;
-					if (x > 214)
-						break;
-				}
-			}
+			PS_DrawThinkFrameStats();
 		}
 	}
 }
+
+// remove and unallocate history from all metrics
+static void PS_ClearHistory(void)
+{
+	int i;
+
+	Z_FreeTag(PU_PERFSTATS);
+	// thinkframe hook metric history pointers need to be cleared manually
+	for (i = 0; i < thinkframe_hooks_length; i++)
+	{
+		thinkframe_hooks[i].time_taken.history = NULL;
+	}
+
+	ps_frame_index = ps_tick_index = 0;
+	// PS_UpdateMetricHistory will set these correctly when it runs
+	ps_frame_samples_left = ps_tick_samples_left = 0;
+}
+
+void PS_PerfStats_OnChange(void)
+{
+	if (cv_perfstats.value && cv_ps_samplesize.value > 1)
+		PS_ClearHistory();
+}
+
+void PS_SampleSize_OnChange(void)
+{
+	if (cv_ps_samplesize.value > 1)
+		PS_ClearHistory();
+}
diff --git a/src/m_perfstats.h b/src/m_perfstats.h
index 71208fbc19f95d886e7cce17c1982ff61de0089f..3ff0e6c6b014b4c24099be602e507d3c07cfcc43 100644
--- a/src/m_perfstats.h
+++ b/src/m_perfstats.h
@@ -16,26 +16,45 @@
 #include "lua_script.h"
 #include "p_local.h"
 
-extern precise_t ps_tictime;
-
-extern precise_t ps_playerthink_time;
-extern precise_t ps_thinkertime;
-
-extern precise_t ps_thlist_times[];
-
-extern int       ps_checkposition_calls;
-
-extern precise_t ps_lua_thinkframe_time;
-extern int       ps_lua_mobjhooks;
+typedef struct
+{
+	union {
+		precise_t p;
+		INT32 i;
+	} value;
+	void *history;
+} ps_metric_t;
 
 typedef struct
 {
-	precise_t time_taken;
+	ps_metric_t time_taken;
 	char short_src[LUA_IDSIZE];
 } ps_hookinfo_t;
 
+#define PS_START_TIMING(metric) metric.value.p = I_GetPreciseTime()
+#define PS_STOP_TIMING(metric) metric.value.p = I_GetPreciseTime() - metric.value.p
+
+extern ps_metric_t ps_tictime;
+
+extern ps_metric_t ps_playerthink_time;
+extern ps_metric_t ps_thinkertime;
+
+extern ps_metric_t ps_thlist_times[];
+
+extern ps_metric_t ps_checkposition_calls;
+
+extern ps_metric_t ps_lua_thinkframe_time;
+extern ps_metric_t ps_lua_mobjhooks;
+
+extern ps_metric_t ps_otherlogictime;
+
 void PS_SetThinkFrameHookInfo(int index, precise_t time_taken, char* short_src);
 
+void PS_UpdateTickStats(void);
+
 void M_DrawPerfStats(void);
 
+void PS_PerfStats_OnChange(void);
+void PS_SampleSize_OnChange(void);
+
 #endif
diff --git a/src/p_ceilng.c b/src/p_ceilng.c
index 43f3cc1d52400ef4086a09503d4039d9df336c5c..e28f9b5b10ce215422e3d11ec90847c1833c801a 100644
--- a/src/p_ceilng.c
+++ b/src/p_ceilng.c
@@ -67,7 +67,8 @@ void T_MoveCeiling(ceiling_t *ceiling)
 				switch (ceiling->type)
 				{
 					case instantMoveCeilingByFrontSector:
-						ceiling->sector->ceilingpic = ceiling->texture;
+						if (ceiling->texture > -1)
+							ceiling->sector->ceilingpic = ceiling->texture;
 						ceiling->sector->ceilingdata = NULL;
 						ceiling->sector->ceilspeed = 0;
 						P_RemoveThinker(&ceiling->thinker);
@@ -186,7 +187,8 @@ void T_MoveCeiling(ceiling_t *ceiling)
 						break;
 
 					case instantMoveCeilingByFrontSector:
-						ceiling->sector->ceilingpic = ceiling->texture;
+						if (ceiling->texture > -1)
+							ceiling->sector->ceilingpic = ceiling->texture;
 						ceiling->sector->ceilingdata = NULL;
 						ceiling->sector->ceilspeed = 0;
 						P_RemoveThinker(&ceiling->thinker);
@@ -512,7 +514,10 @@ INT32 EV_DoCeiling(line_t *line, ceiling_e type)
 					ceiling->direction = -1;
 					ceiling->bottomheight = line->frontsector->ceilingheight;
 				}
-				ceiling->texture = line->frontsector->ceilingpic;
+				if (line->flags & ML_NOCLIMB)
+					ceiling->texture = -1;
+				else
+					ceiling->texture = line->frontsector->ceilingpic;
 				break;
 
 			case moveCeilingByFrontTexture:
diff --git a/src/p_enemy.c b/src/p_enemy.c
index 6a92c5d3337e233e713fbb23e2fa71632ee5b1f4..9d51aced50899c8574997fa1d18b187db9e20371 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -744,8 +744,8 @@ boolean P_LookForPlayers(mobj_t *actor, boolean allaround, boolean tracer, fixed
 		if (player->mo->health <= 0)
 			continue; // dead
 
-		if (player->bot)
-			continue; // ignore bots
+		if (player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN)
+			continue; // ignore followbots
 
 		if (player->quittime)
 			continue; // Ignore uncontrolled bodies
@@ -3518,9 +3518,7 @@ void A_Scream(mobj_t *actor)
 	if (LUA_CallAction(A_SCREAM, actor))
 		return;
 
-	if (actor->tracer && (actor->tracer->type == MT_SHELL || actor->tracer->type == MT_FIREBALL))
-		S_StartScreamSound(actor, sfx_mario2);
-	else if (actor->info->deathsound)
+	if (actor->info->deathsound && !S_SoundPlaying(actor, sfx_mario2))
 		S_StartScreamSound(actor, actor->info->deathsound);
 }
 
@@ -3591,7 +3589,7 @@ void A_1upThinker(mobj_t *actor)
 
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
-		if (!playeringame[i] || players[i].bot || players[i].spectator)
+		if (!playeringame[i] || players[i].bot == BOT_2PAI || players[i].bot == BOT_2PHUMAN || players[i].spectator)
 			continue;
 
 		if (!players[i].mo)
@@ -5297,7 +5295,7 @@ void A_OverlayThink(mobj_t *actor)
 		actor->z = actor->target->z + actor->target->height - mobjinfo[actor->type].height  - ((var2>>16) ? -1 : 1)*(var2&0xFFFF)*FRACUNIT;
 	else
 		actor->z = actor->target->z + ((var2>>16) ? -1 : 1)*(var2&0xFFFF)*FRACUNIT;
-	actor->angle = actor->target->angle + actor->movedir;
+	actor->angle = (actor->target->player ? actor->target->player->drawangle : actor->target->angle) + actor->movedir;
 	actor->eflags = actor->target->eflags;
 
 	actor->momx = actor->target->momx;
@@ -8272,7 +8270,7 @@ void A_Boss3ShockThink(mobj_t *actor)
 		fixed_t x0, y0, x1, y1;
 
 		// Break the link if movements are too different
-		if (FixedHypot(snext->momx - actor->momx, snext->momy - actor->momy) > 12*actor->scale)
+		if (R_PointToDist2(0, 0, snext->momx - actor->momx, snext->momy - actor->momy) > 12*actor->scale)
 		{
 			P_SetTarget(&actor->hnext, NULL);
 			return;
@@ -8283,15 +8281,21 @@ void A_Boss3ShockThink(mobj_t *actor)
 		y0 = actor->y;
 		x1 = snext->x;
 		y1 = snext->y;
-		if (FixedHypot(x1 - x0, y1 - y0) > 2*actor->radius)
+		if (R_PointToDist2(0, 0, x1 - x0, y1 - y0) > 2*actor->radius)
 		{
-			snew = P_SpawnMobj((x0 + x1) >> 1, (y0 + y1) >> 1, (actor->z + snext->z) >> 1, actor->type);
+			snew = P_SpawnMobj((x0 >> 1) + (x1 >> 1),
+				(y0 >> 1) + (y1 >> 1),
+				(actor->z >> 1) + (snext->z >> 1), actor->type);
 			snew->momx = (actor->momx + snext->momx) >> 1;
 			snew->momy = (actor->momy + snext->momy) >> 1;
 			snew->momz = (actor->momz + snext->momz) >> 1; // is this really needed?
 			snew->angle = (actor->angle + snext->angle) >> 1;
 			P_SetTarget(&snew->target, actor->target);
 			snew->fuse = actor->fuse;
+			
+			P_SetScale(snew, actor->scale);
+			snew->destscale = actor->destscale;
+			snew->scalespeed = actor->scalespeed;
 
 			P_SetTarget(&actor->hnext, snew);
 			P_SetTarget(&snew->hnext, snext);
diff --git a/src/p_floor.c b/src/p_floor.c
index 6641c6904f31f5a50dca6322daa66e4b1958117a..78cd0a18401fab7cd87cd7adb8f16ffa32be5394 100644
--- a/src/p_floor.c
+++ b/src/p_floor.c
@@ -1041,6 +1041,7 @@ static mobj_t *SearchMarioNode(msecnode_t *node)
 		case MT_THUNDERCOIN_ORB:
 		case MT_IVSP:
 		case MT_SUPERSPARK:
+		case MT_BOXSPARKLE:
 		case MT_RAIN:
 		case MT_SNOWFLAKE:
 		case MT_SPLISH:
diff --git a/src/p_inter.c b/src/p_inter.c
index 89989d1af9aee7cffa44cc84c897c2648a0ad374..b37689fd8f080bb85e790da77c105c5e550dcaf4 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -151,7 +151,7 @@ boolean P_CanPickupItem(player_t *player, boolean weapon)
 	if (!player->mo || player->mo->health <= 0)
 		return false;
 
-	if (player->bot)
+	if (player->bot && player->bot != BOT_MPAI)
 	{
 		if (weapon)
 			return false;
@@ -178,7 +178,7 @@ void P_DoNightsScore(player_t *player)
 		return; // Don't do any fancy shit for failures.
 
 	dummymo = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z+player->mo->height/2, MT_NIGHTSCORE);
-	if (player->bot)
+	if (player->bot && player->bot != BOT_MPAI)
 		player = &players[consoleplayer];
 
 	if (G_IsSpecialStage(gamemap)) // Global link count? Maybe not a good idea...
@@ -470,14 +470,14 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				if (!(player->charability2 == CA2_MELEE && player->panim == PA_ABILITY2))
 				{
 					fixed_t setmomz = -toucher->momz; // Store this, momz get changed by P_DoJump within P_DoBubbleBounce
-
+					
 					if (elementalpierce == 2) // Reset bubblewrap, part 1
 						P_DoBubbleBounce(player);
 					toucher->momz = setmomz;
 					if (elementalpierce == 2) // Reset bubblewrap, part 2
 					{
 						boolean underwater = toucher->eflags & MFE_UNDERWATER;
-
+							
 						if (underwater)
 							toucher->momz /= 2;
 						toucher->momz -= (toucher->momz/(underwater ? 8 : 4)); // Cap the height!
@@ -630,7 +630,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 // ***************************** //
 		// Special Stage Token
 		case MT_TOKEN:
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 
 			P_AddPlayerScore(player, 1000);
@@ -670,7 +670,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 
 		// Emerald Hunt
 		case MT_EMERHUNT:
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 
 			if (hunt1 == special)
@@ -701,7 +701,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 		case MT_EMERALD5:
 		case MT_EMERALD6:
 		case MT_EMERALD7:
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 
 			if (special->threshold)
@@ -738,7 +738,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 		// Secret emblem thingy
 		case MT_EMBLEM:
 			{
-				if (demoplayback || player->bot)
+				if (demoplayback || (player->bot && player->bot != BOT_MPAI))
 					return;
 				emblemlocations[special->health-1].collected = true;
 
@@ -751,7 +751,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 		// CTF Flags
 		case MT_REDFLAG:
 		case MT_BLUEFLAG:
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 			if (player->powers[pw_flashing] || player->tossdelay)
 				return;
@@ -826,7 +826,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			{
 				boolean spec = G_IsSpecialStage(gamemap);
 				boolean cangiveemmy = false;
-				if (player->bot)
+				if (player->bot && player->bot != BOT_MPAI)
 					return;
 				if (player->exiting)
 					return;
@@ -1072,7 +1072,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			return;
 		case MT_EGGCAPSULE:
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 
 			// make sure everything is as it should be, THEN take rings from players in special stages
@@ -1164,7 +1164,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			return;
 		case MT_NIGHTSSUPERLOOP:
-			if (player->bot || !(player->powers[pw_carry] == CR_NIGHTSMODE))
+			if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
 				return;
 			if (!G_IsSpecialStage(gamemap))
 				player->powers[pw_nights_superloop] = (UINT16)special->info->speed;
@@ -1186,7 +1186,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			break;
 		case MT_NIGHTSDRILLREFILL:
-			if (player->bot || !(player->powers[pw_carry] == CR_NIGHTSMODE))
+			if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
 				return;
 			if (!G_IsSpecialStage(gamemap))
 				player->drillmeter = special->info->speed;
@@ -1208,7 +1208,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			break;
 		case MT_NIGHTSHELPER:
-			if (player->bot || !(player->powers[pw_carry] == CR_NIGHTSMODE))
+			if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
 				return;
 			if (!G_IsSpecialStage(gamemap))
 			{
@@ -1240,7 +1240,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			break;
 		case MT_NIGHTSEXTRATIME:
-			if (player->bot || !(player->powers[pw_carry] == CR_NIGHTSMODE))
+			if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
 				return;
 			if (!G_IsSpecialStage(gamemap))
 			{
@@ -1272,7 +1272,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			break;
 		case MT_NIGHTSLINKFREEZE:
-			if (player->bot || !(player->powers[pw_carry] == CR_NIGHTSMODE))
+			if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
 				return;
 			if (!G_IsSpecialStage(gamemap))
 			{
@@ -1332,7 +1332,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 					if (playeringame[i] && players[i].powers[pw_carry] == CR_NIGHTSMODE)
 						players[i].drillmeter += TICRATE/2;
 			}
-			else if (player->bot)
+			else if (player->bot && player->bot != BOT_MPAI)
 				players[consoleplayer].drillmeter += TICRATE/2;
 			else
 				player->drillmeter += TICRATE/2;
@@ -1385,9 +1385,9 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				thinker_t  *th;
 				mobj_t *mo2;
 
-				if (player->bot)
+				if (player->bot && player->bot != BOT_MPAI)
 					return;
-
+					
 				// Initialize my junk
 				junk.tags.tags = NULL;
 				junk.tags.count = 0;
@@ -1423,7 +1423,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			return;
 		}
 		case MT_FIREFLOWER:
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 
 			S_StartSound(toucher, sfx_mario3);
@@ -1617,7 +1617,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 
 				if (special->tracer && !(special->tracer->flags2 & MF2_STRONGBOX))
 					macespin = true;
-
+				
 				if (macespin ? (player->powers[pw_ignorelatch] & (1<<15)) : (player->powers[pw_ignorelatch]))
 					return;
 
@@ -1685,7 +1685,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				return; // Only go in the mouth
 
 			// Eaten by player!
-			if ((!player->bot) && (player->powers[pw_underwater] && player->powers[pw_underwater] <= 12*TICRATE + 1))
+			if ((!player->bot || player->bot == BOT_MPAI) && (player->powers[pw_underwater] && player->powers[pw_underwater] <= 12*TICRATE + 1))
 			{
 				player->powers[pw_underwater] = underwatertics + 1;
 				P_RestoreMusic(player);
@@ -1696,7 +1696,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 
 			if (!player->climbing)
 			{
-				if (player->bot && toucher->state-states != S_PLAY_GASP)
+				if (player->bot && player->bot != BOT_MPAI && toucher->state-states != S_PLAY_GASP)
 					S_StartSound(toucher, special->info->deathsound); // Force it to play a sound for bots
 				P_SetPlayerMobjState(toucher, S_PLAY_GASP);
 				P_ResetPlayer(player);
@@ -1704,7 +1704,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 
 			toucher->momx = toucher->momy = toucher->momz = 0;
 
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 			else
 				break;
@@ -1736,7 +1736,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			return;
 
 		case MT_MINECARTSPAWNER:
-			if (!player->bot && special->fuse <= TICRATE && player->powers[pw_carry] != CR_MINECART && !(player->powers[pw_ignorelatch] & (1<<15)))
+			if (!player->bot && player->bot != BOT_MPAI && special->fuse <= TICRATE && player->powers[pw_carry] != CR_MINECART && !(player->powers[pw_ignorelatch] & (1<<15)))
 			{
 				mobj_t *mcart = P_SpawnMobj(special->x, special->y, special->z, MT_MINECART);
 				P_SetTarget(&mcart->target, toucher);
@@ -1789,7 +1789,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			return;
 		default: // SOC or script pickup
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 			P_SetTarget(&special->target, toucher);
 			break;
@@ -1813,7 +1813,7 @@ void P_TouchStarPost(mobj_t *post, player_t *player, boolean snaptopost)
 	mobj_t *toucher = player->mo;
 	mobj_t *checkbase = snaptopost ? post : toucher;
 
-	if (player->bot)
+	if (player->bot && player->bot != BOT_MPAI)
 		return;
 	// In circuit, player must have touched all previous starposts
 	if (circuitmap
@@ -2391,7 +2391,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 	mobj_t *mo;
 
 	if (inflictor && (inflictor->type == MT_SHELL || inflictor->type == MT_FIREBALL))
-		P_SetTarget(&target->tracer, inflictor);
+		S_StartScreamSound(target, sfx_mario2);
 
 	if (!(maptol & TOL_NIGHTS) && G_IsSpecialStage(gamemap) && target->player && target->player->nightstime > 6)
 		target->player->nightstime = 6; // Just let P_Ticker take care of the rest.
@@ -2555,7 +2555,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 
 		if ((target->player->lives <= 1) && (netgame || multiplayer) && G_GametypeUsesCoopLives() && (cv_cooplives.value == 0))
 			;
-		else if (!target->player->bot && !target->player->spectator && (target->player->lives != INFLIVES)
+		else if ((!target->player->bot || target->player->bot == BOT_MPAI) && !target->player->spectator && (target->player->lives != INFLIVES)
 		 && G_GametypeUsesLives())
 		{
 			if (!(target->player->pflags & PF_FINISHED))
@@ -3475,7 +3475,7 @@ void P_SpecialStageDamage(player_t *player, mobj_t *inflictor, mobj_t *source)
 	if (inflictor && inflictor->type == MT_LHRT)
 		return;
 
-	if (player->powers[pw_shield] || player->bot)  //If One-Hit Shield
+	if (player->powers[pw_shield] || (player->bot && player->bot != BOT_MPAI))  //If One-Hit Shield
 	{
 		P_RemoveShield(player);
 		S_StartSound(player->mo, sfx_shldls); // Ba-Dum! Shield loss.
@@ -3566,7 +3566,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 			return false;
 
 		// Make sure that boxes cannot be popped by enemies, red rings, etc.
-		if (target->flags & MF_MONITOR && ((!source || !source->player || source->player->bot)
+		if (target->flags & MF_MONITOR && ((!source || !source->player || (source->player->bot && source->player->bot != BOT_MPAI))
 		|| (inflictor && (inflictor->type == MT_REDRING || (inflictor->type >= MT_THROWNBOUNCE && inflictor->type <= MT_THROWNGRENADE)))))
 			return false;
 	}
@@ -3651,7 +3651,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 			return true;
 		}
 
-		if (!force && inflictor && inflictor->flags & MF_FIRE)
+		if (!force && inflictor && inflictor->flags & MF_FIRE && !(damagetype && damagetype != DMG_FIRE))
 		{
 			if (player->powers[pw_shield] & SH_PROTECTFIRE)
 				return false; // Invincible to fire objects
@@ -3701,7 +3701,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 		}
 		else if (LUA_HookMobjDamage(target, inflictor, source, damage, damagetype))
 			return true;
-		else if (player->powers[pw_shield] || (player->bot && !ultimatemode))  //If One-Hit Shield
+		else if (player->powers[pw_shield] || (player->bot && player->bot != BOT_MPAI && !ultimatemode))  //If One-Hit Shield
 		{
 			P_ShieldDamage(player, inflictor, source, damage, damagetype);
 			damage = 0;
diff --git a/src/p_map.c b/src/p_map.c
index e55bebb9a74d9a3428b270398570373b593d8fc8..836e75c4e8bb08937e79fadf5f572125c9841b90 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -1156,7 +1156,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		else
 			thing->z = tmthing->z + tmthing->height + FixedMul(FRACUNIT, tmthing->scale);
 		if (thing->flags & MF_SHOOTABLE)
-			P_DamageMobj(thing, tmthing, tmthing, 1, 0);
+			P_DamageMobj(thing, tmthing, tmthing, 1, DMG_SPIKE);
 		return true;
 	}
 
@@ -2029,7 +2029,7 @@ boolean P_CheckPosition(mobj_t *thing, fixed_t x, fixed_t y)
 	subsector_t *newsubsec;
 	boolean blockval = true;
 
-	ps_checkposition_calls++;
+	ps_checkposition_calls.value.i++;
 
 	I_Assert(thing != NULL);
 #ifdef PARANOIA
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 2688818115c2ddec1d5a8a931c990f24dee94df9..da0bde867e6ddb3efe40c311e5a2e9fe1c7bec7b 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -78,7 +78,7 @@ void P_AddCachedAction(mobj_t *mobj, INT32 statenum)
 //
 // P_SetupStateAnimation
 //
-FUNCINLINE static ATTRINLINE void P_SetupStateAnimation(mobj_t *mobj, state_t *st)
+static void P_SetupStateAnimation(mobj_t *mobj, state_t *st)
 {
 	INT32 animlength = (mobj->sprite == SPR_PLAY && mobj->skin)
 		? (INT32)(((skin_t *)mobj->skin)->sprites[mobj->sprite2].numframes) - 1
@@ -1844,12 +1844,10 @@ void P_XYMovement(mobj_t *mo)
 		// blocked move
 		moved = false;
 
-		if (player) {
-			if (player->bot)
-				B_MoveBlocked(player);
-		}
+		if (player) 
+			B_MoveBlocked(player);
 
-		if (LUA_HookMobj(mo, MOBJ_HOOK(MobjMoveBlocked)))
+		if (LUA_HookMobjMoveBlocked(mo, tmhitthing, blockingline))
 		{
 			if (P_MobjWasRemoved(mo))
 				return;
@@ -2554,6 +2552,10 @@ boolean P_ZMovement(mobj_t *mo)
 		}
 
 		P_CheckPosition(mo, mo->x, mo->y); // Sets mo->standingslope correctly
+
+		if (P_MobjWasRemoved(mo)) // mobjs can be removed by P_CheckPosition -- Monster Iestyn 31/07/21
+			return false;
+
 		if (((mo->eflags & MFE_VERTICALFLIP) ? tmceilingslope : tmfloorslope) && (mo->type != MT_STEAM))
 		{
 			mo->standingslope = (mo->eflags & MFE_VERTICALFLIP) ? tmceilingslope : tmfloorslope;
@@ -3239,8 +3241,8 @@ void P_MobjCheckWater(mobj_t *mobj)
 		 || ((rover->flags & FF_BLOCKOTHERS) && !mobj->player)))
 			continue;
 
-		topheight    = P_GetFFloorTopZAt   (rover, mobj->x, mobj->y);
-		bottomheight = P_GetFFloorBottomZAt(rover, mobj->x, mobj->y);
+		topheight = P_GetSpecialTopZ(mobj, sectors + rover->secnum, sector);
+		bottomheight = P_GetSpecialBottomZ(mobj, sectors + rover->secnum, sector);
 
 		if (mobj->eflags & MFE_VERTICALFLIP)
 		{
@@ -4141,7 +4143,7 @@ boolean P_BossTargetPlayer(mobj_t *actor, boolean closest)
 
 		player = &players[actor->lastlook];
 
-		if (player->pflags & PF_INVIS || player->bot || player->spectator)
+		if (player->pflags & PF_INVIS || player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN || player->spectator)
 			continue; // ignore notarget
 
 		if (!player->mo || P_MobjWasRemoved(player->mo))
@@ -4182,7 +4184,7 @@ boolean P_SupermanLook4Players(mobj_t *actor)
 			if (players[c].pflags & PF_INVIS)
 				continue; // ignore notarget
 
-			if (!players[c].mo || players[c].bot)
+			if (!players[c].mo || players[c].bot == BOT_2PAI || players[c].bot == BOT_2PHUMAN)
 				continue;
 
 			if (players[c].mo->health <= 0)
@@ -6840,7 +6842,7 @@ void P_RunOverlays(void)
 
 		mo->eflags = (mo->eflags & ~MFE_VERTICALFLIP) | (mo->target->eflags & MFE_VERTICALFLIP);
 		mo->scale = mo->destscale = mo->target->scale;
-		mo->angle = mo->target->angle + mo->movedir;
+		mo->angle = (mo->target->player ? mo->target->player->drawangle : mo->target->angle) + mo->movedir;
 
 		if (!(mo->state->frame & FF_ANIMATE))
 			zoffs = FixedMul(((signed)mo->state->var2)*FRACUNIT, mo->scale);
@@ -7311,7 +7313,7 @@ static void P_RosySceneryThink(mobj_t *mobj)
 			continue;
 		if (!players[i].mo)
 			continue;
-		if (players[i].bot)
+		if (players[i].bot == BOT_2PAI || players[i].bot == BOT_2PHUMAN)
 			continue;
 		if (!players[i].mo->health)
 			continue;
@@ -10392,6 +10394,9 @@ static fixed_t P_DefaultMobjShadowScale (mobj_t *thing)
 
 		case MT_RING:
 		case MT_FLINGRING:
+		
+		case MT_COIN:
+		case MT_FLINGCOIN:
 
 		case MT_BLUESPHERE:
 		case MT_FLINGBLUESPHERE:
@@ -11056,7 +11061,7 @@ void P_SpawnPrecipitation(void)
 	subsector_t *precipsector = NULL;
 	precipmobj_t *rainmo = NULL;
 
-	if (dedicated || !(cv_drawdist_precip.value) || curWeather == PRECIP_NONE)
+	if (dedicated || !(cv_drawdist_precip.value) || curWeather == PRECIP_NONE || curWeather == PRECIP_STORM_NORAIN)
 		return;
 
 	// Use the blockmap to narrow down our placing patterns
@@ -11102,22 +11107,14 @@ void P_SpawnPrecipitation(void)
 				continue;
 
 			rainmo = P_SpawnRainMobj(x, y, height, MT_RAIN);
+			if (curWeather == PRECIP_BLANK)
+				rainmo->precipflags |= PCF_INVISIBLE;
 		}
 
 		// Randomly assign a height, now that floorz is set.
 		rainmo->z = M_RandomRange(rainmo->floorz>>FRACBITS, rainmo->ceilingz>>FRACBITS)<<FRACBITS;
 	}
 
-	if (curWeather == PRECIP_BLANK)
-	{
-		curWeather = PRECIP_RAIN;
-		P_SwitchWeather(PRECIP_BLANK);
-	}
-	else if (curWeather == PRECIP_STORM_NORAIN)
-	{
-		curWeather = PRECIP_RAIN;
-		P_SwitchWeather(PRECIP_STORM_NORAIN);
-	}
 }
 
 //
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 770c641b924f8e233fdccc884a53729516d4cafa..1270064c01f1494abc582fc7a61c8a80791f4635 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -193,6 +193,19 @@ static void P_NetArchivePlayers(void)
 		WRITEUINT32(save_p, players[i].dashmode);
 		WRITEUINT32(save_p, players[i].skidtime);
 
+		//////////
+		// Bots //
+		//////////
+		WRITEUINT8(save_p, players[i].bot);
+		WRITEUINT8(save_p, players[i].botmem.lastForward);
+		WRITEUINT8(save_p, players[i].botmem.lastBlocked);
+		WRITEUINT8(save_p, players[i].botmem.catchup_tics);
+		WRITEUINT8(save_p, players[i].botmem.thinkstate);
+		WRITEUINT8(save_p, players[i].removing);
+		
+		WRITEUINT8(save_p, players[i].blocked);
+		WRITEUINT16(save_p, players[i].lastbuttons);
+
 		////////////////////////////
 		// Conveyor Belt Movement //
 		////////////////////////////
@@ -407,6 +420,20 @@ static void P_NetUnArchivePlayers(void)
 		players[i].dashmode = READUINT32(save_p); // counter for dashmode ability
 		players[i].skidtime = READUINT32(save_p); // Skid timer
 
+		//////////
+		// Bots //
+		//////////
+		players[i].bot = READUINT8(save_p);
+		
+		players[i].botmem.lastForward = READUINT8(save_p);
+		players[i].botmem.lastBlocked = READUINT8(save_p);
+		players[i].botmem.catchup_tics = READUINT8(save_p);
+		players[i].botmem.thinkstate = READUINT8(save_p);
+		players[i].removing = READUINT8(save_p);
+
+		players[i].blocked = READUINT8(save_p);
+		players[i].lastbuttons = READUINT16(save_p);
+		
 		////////////////////////////
 		// Conveyor Belt Movement //
 		////////////////////////////
diff --git a/src/p_setup.c b/src/p_setup.c
index 83a5fe26c6d054feeb704e3768d89e4848953f48..fd8b19f1c03c020dc7c1c197e9cfa172ad9d6a47 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -1501,6 +1501,22 @@ typedef struct textmap_colormap_s {
 
 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,
+} planedef_t;
+
+typedef struct textmap_plane_s {
+    UINT8 defined;
+    fixed_t a, b, c, d;
+} textmap_plane_t;
+
+textmap_plane_t textmap_planefloor = {0, 0, 0, 0, 0};
+textmap_plane_t textmap_planeceiling = {0, 0, 0, 0, 0};
+
 static void ParseTextmapSectorParameter(UINT32 i, char *param, char *val)
 {
 	if (fastcmp(param, "heightfloor"))
@@ -1539,6 +1555,46 @@ static void ParseTextmapSectorParameter(UINT32 i, char *param, char *val)
 		sectors[i].floorpic_angle = FixedAngle(FLOAT_TO_FIXED(atof(val)));
 	else if (fastcmp(param, "rotationceiling"))
 		sectors[i].ceilingpic_angle = FixedAngle(FLOAT_TO_FIXED(atof(val)));
+	else if (fastcmp(param, "floorplane_a"))
+	{
+		textmap_planefloor.defined |= PD_A;
+		textmap_planefloor.a = FLOAT_TO_FIXED(atof(val));
+	}
+	else if (fastcmp(param, "floorplane_b"))
+	{
+		textmap_planefloor.defined |= PD_B;
+		textmap_planefloor.b = FLOAT_TO_FIXED(atof(val));
+	}
+	else if (fastcmp(param, "floorplane_c"))
+	{
+		textmap_planefloor.defined |= PD_C;
+		textmap_planefloor.c = FLOAT_TO_FIXED(atof(val));
+	}
+	else if (fastcmp(param, "floorplane_d"))
+	{
+		textmap_planefloor.defined |= PD_D;
+		textmap_planefloor.d = FLOAT_TO_FIXED(atof(val));
+	}
+	else if (fastcmp(param, "ceilingplane_a"))
+	{
+		textmap_planeceiling.defined |= PD_A;
+		textmap_planeceiling.a = FLOAT_TO_FIXED(atof(val));
+	}
+	else if (fastcmp(param, "ceilingplane_b"))
+	{
+		textmap_planeceiling.defined |= PD_B;
+		textmap_planeceiling.b = FLOAT_TO_FIXED(atof(val));
+	}
+	else if (fastcmp(param, "ceilingplane_c"))
+	{
+		textmap_planeceiling.defined |= PD_C;
+		textmap_planeceiling.c = FLOAT_TO_FIXED(atof(val));
+	}
+	else if (fastcmp(param, "ceilingplane_d"))
+	{
+		textmap_planeceiling.defined |= PD_D;
+		textmap_planeceiling.d = FLOAT_TO_FIXED(atof(val));
+	}
 	else if (fastcmp(param, "lightcolor"))
 	{
 		textmap_colormap.used = true;
@@ -1868,6 +1924,10 @@ static void P_LoadTextmap(void)
 		textmap_colormap.fadestart = 0;
 		textmap_colormap.fadeend = 31;
 		textmap_colormap.flags = 0;
+
+		textmap_planefloor.defined = 0;
+		textmap_planeceiling.defined = 0;
+
 		TextmapParse(sectorsPos[i], i, ParseTextmapSectorParameter);
 
 		P_InitializeSector(sc);
@@ -1877,6 +1937,19 @@ static void P_LoadTextmap(void)
 			INT32 fadergba = P_ColorToRGBA(textmap_colormap.fadecolor, textmap_colormap.fadealpha);
 			sc->extra_colormap = sc->spawn_extra_colormap = R_CreateColormap(rgba, fadergba, textmap_colormap.fadestart, textmap_colormap.fadeend, textmap_colormap.flags);
 		}
+
+		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->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->hasslope = true;
+        }
+
 		TextmapFixFlatOffsets(sc);
 	}
 
@@ -3586,6 +3659,12 @@ static void P_ConvertBinaryMap(void)
 			if (lines[i].flags & ML_NONET)
 				lines[i].args[2] |= TMSL_DYNAMIC;
 
+			if (lines[i].flags & ML_TFERLINE)
+			{
+					lines[i].args[4] |= backfloor ? TMSC_BACKTOFRONTFLOOR : (frontfloor ? TMSC_FRONTTOBACKFLOOR : 0);
+					lines[i].args[4] |= backceil ? TMSC_BACKTOFRONTCEILING : (frontceil ? TMSC_FRONTTOBACKCEILING : 0);
+			}
+
 			lines[i].special = 700;
 			break;
 		}
@@ -3667,7 +3746,7 @@ static void P_ConvertBinaryMap(void)
 				lines[i].args[4] |= TMSC_BACKTOFRONTCEILING;
 			lines[i].special = 720;
 			break;
-		
+
 		case 900: //Translucent wall (10%)
 		case 901: //Translucent wall (20%)
 		case 902: //Translucent wall (30%)
@@ -4704,6 +4783,8 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 
 	P_MapStart(); // tmthing can be used starting from this point
 
+	P_InitSlopes();
+
 	if (!P_LoadMapFromFile())
 		return false;
 
@@ -4915,10 +4996,9 @@ static lumpinfo_t* FindFolder(const char *folName, UINT16 *start, UINT16 *end, l
 // Add a wadfile to the active wad files,
 // replace sounds, musics, patches, textures, sprites and maps
 //
-boolean P_AddWadFile(const char *wadfilename)
+static boolean P_LoadAddon(UINT16 wadnum, UINT16 numlumps)
 {
 	size_t i, j, sreplaces = 0, mreplaces = 0, digmreplaces = 0;
-	UINT16 numlumps, wadnum;
 	char *name;
 	lumpinfo_t *lumpinfo;
 
@@ -4939,18 +5019,10 @@ boolean P_AddWadFile(const char *wadfilename)
 //	UINT16 flaPos, flaNum = 0;
 //	UINT16 mapPos, mapNum = 0;
 
-	// Init file.
-	if ((numlumps = W_InitFile(wadfilename, false, false)) == INT16_MAX)
-	{
-		refreshdirmenu |= REFRESHDIR_NOTLOADED;
-		return false;
-	}
-	else
-		wadnum = (UINT16)(numwadfiles-1);
-
 	switch(wadfiles[wadnum]->type)
 	{
 	case RET_PK3:
+	case RET_FOLDER:
 		// Look for the lumps that act as resource delimitation markers.
 		lumpinfo = wadfiles[wadnum]->lumpinfo;
 		for (i = 0; i < numlumps; i++, lumpinfo++)
@@ -5114,3 +5186,35 @@ boolean P_AddWadFile(const char *wadfilename)
 
 	return true;
 }
+
+boolean P_AddWadFile(const char *wadfilename)
+{
+	UINT16 numlumps, wadnum;
+
+	// Init file.
+	if ((numlumps = W_InitFile(wadfilename, false, false)) == INT16_MAX)
+	{
+		refreshdirmenu |= REFRESHDIR_NOTLOADED;
+		return false;
+	}
+	else
+		wadnum = (UINT16)(numwadfiles-1);
+
+	return P_LoadAddon(wadnum, numlumps);
+}
+
+boolean P_AddFolder(const char *folderpath)
+{
+	UINT16 numlumps, wadnum;
+
+	// Init file.
+	if ((numlumps = W_InitFolder(folderpath, false, false)) == INT16_MAX)
+	{
+		refreshdirmenu |= REFRESHDIR_NOTLOADED;
+		return false;
+	}
+	else
+		wadnum = (UINT16)(numwadfiles-1);
+
+	return P_LoadAddon(wadnum, numlumps);
+}
diff --git a/src/p_setup.h b/src/p_setup.h
index 9fa70d516fe1ee7a17ed29a0829302b5a4966eeb..c3c680fdd3176392580373f8aedec1d154987006 100644
--- a/src/p_setup.h
+++ b/src/p_setup.h
@@ -103,6 +103,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate);
 void HWR_LoadLevel(void);
 #endif
 boolean P_AddWadFile(const char *wadfilename);
+boolean P_AddFolder(const char *folderpath);
 boolean P_RunSOC(const char *socfilename);
 void P_LoadSoundsRange(UINT16 wadnum, UINT16 first, UINT16 num);
 void P_LoadMusicsRange(UINT16 wadnum, UINT16 first, UINT16 num);
diff --git a/src/p_slopes.c b/src/p_slopes.c
index 4e93e4a45278978f6e98921d8827b65b22e887a5..bfca153a628c8246fb5c6a2629e6d609115b7a13 100644
--- a/src/p_slopes.c
+++ b/src/p_slopes.c
@@ -90,6 +90,36 @@ static void ReconfigureViaVertexes (pslope_t *slope, const vector3_t v1, const v
 	}
 }
 
+/// 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)
+{
+	fixed_t m;
+	vector3_t *normal = &slope->normal;
+
+	// Set origin.
+	FV3_Load(&slope->o, 0, 0, c ? -FixedDiv(d, c) : 0);
+
+	// Get slope's normal.
+	FV3_Load(normal, a, b, c);
+	FV3_Normalize(normal);
+
+	// Invert normal if it's facing down.
+	if (normal->z < 0)
+		FV3_Negate(normal);
+
+	// Get direction vector
+	m = FixedHypot(normal->x, normal->y);
+	slope->d.x = -FixedDiv(normal->x, m);
+	slope->d.y = -FixedDiv(normal->y, m);
+
+	// Z delta
+	slope->zdelta = FixedDiv(m, normal->z);
+
+	// 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));
+}
+
 /// Recalculate dynamic slopes.
 void T_DynamicSlopeLine (dynplanethink_t* th)
 {
@@ -631,13 +661,20 @@ pslope_t *P_SlopeById(UINT16 id)
 	return ret;
 }
 
+/// 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* ret = Slope_Add(0);
+
+	ReconfigureViaConstants(ret, a, b, c, d);
+
+	return ret;
+}
+
 /// Initializes and reads the slopes from the map data.
 void P_SpawnSlopes(const boolean fromsave) {
 	size_t i;
 
-	slopelist = NULL;
-	slopecount = 0;
-
 	/// Generates vertex slopes.
 	SpawnVertexSlopes();
 
@@ -664,6 +701,9 @@ void P_SpawnSlopes(const boolean fromsave) {
 	for (i = 0; i < numlines; i++)
 		switch (lines[i].special)
 		{
+			case 700:
+				if (lines[i].flags & ML_TFERLINE) P_CopySectorSlope(&lines[i]);
+				break;
 			case 720:
 				P_CopySectorSlope(&lines[i]);
 			default:
@@ -671,6 +711,13 @@ void P_SpawnSlopes(const boolean fromsave) {
 		}
 }
 
+/// Initializes slopes.
+void P_InitSlopes(void)
+{
+	slopelist = NULL;
+	slopecount = 0;
+}
+
 // ============================================================================
 //
 // Various utilities related to slopes
@@ -773,7 +820,7 @@ void P_SlopeLaunch(mobj_t *mo)
 		mo->momx = slopemom.x;
 		mo->momy = slopemom.y;
 		mo->momz = slopemom.z/2;
-		
+
 	    if (mo->player)
 		    mo->player->powers[pw_justlaunched] = 1;
 	}
diff --git a/src/p_slopes.h b/src/p_slopes.h
index ae040ae5698f1d3f29933dc412552573d1e63ab9..43cd3edb0d9009f341b9ab82c168c3429c3521a5 100644
--- a/src/p_slopes.h
+++ b/src/p_slopes.h
@@ -50,6 +50,7 @@ typedef enum
 void P_LinkSlopeThinkers (void);
 
 void P_CalculateSlopeNormal(pslope_t *slope);
+void P_InitSlopes(void);
 void P_SpawnSlopes(const boolean fromsave);
 
 //
@@ -86,6 +87,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);
 
 /// Dynamic plane type enum for the thinker. Will have a different functionality depending on this.
 typedef enum {
diff --git a/src/p_spec.c b/src/p_spec.c
index 508c340b043456558652c2fe716738f3f1d5d444..a7983746a39a6234cb13ccabefd8c64d675d6cc9 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -1982,6 +1982,22 @@ void P_LinedefExecute(INT16 tag, mobj_t *actor, sector_t *caller)
 	}
 }
 
+static boolean is_rain_type (INT32 weathernum)
+{
+	switch (weathernum)
+	{
+		case PRECIP_SNOW:
+		case PRECIP_RAIN:
+		case PRECIP_STORM:
+		case PRECIP_STORM_NOSTRIKES:
+		case PRECIP_BLANK:
+			return true;
+
+		default:
+			return false;
+	}
+}
+
 //
 // P_SwitchWeather
 //
@@ -1989,53 +2005,14 @@ void P_LinedefExecute(INT16 tag, mobj_t *actor, sector_t *caller)
 //
 void P_SwitchWeather(INT32 weathernum)
 {
-	boolean purge = false;
-	INT32 swap = 0;
+	boolean purge = true;
 
-	switch (weathernum)
-	{
-		case PRECIP_NONE: // None
-			if (curWeather == PRECIP_NONE)
-				return; // Nothing to do.
-			purge = true;
-			break;
-		case PRECIP_STORM: // Storm
-		case PRECIP_STORM_NOSTRIKES: // Storm w/ no lightning
-		case PRECIP_RAIN: // Rain
-			if (curWeather == PRECIP_SNOW || curWeather == PRECIP_BLANK || curWeather == PRECIP_STORM_NORAIN)
-				swap = PRECIP_RAIN;
-			break;
-		case PRECIP_SNOW: // Snow
-			if (curWeather == PRECIP_SNOW)
-				return; // Nothing to do.
-			if (curWeather == PRECIP_RAIN || curWeather == PRECIP_STORM || curWeather == PRECIP_STORM_NOSTRIKES || curWeather == PRECIP_BLANK || curWeather == PRECIP_STORM_NORAIN)
-				swap = PRECIP_SNOW; // Need to delete the other precips.
-			break;
-		case PRECIP_STORM_NORAIN: // Storm w/o rain
-			if (curWeather == PRECIP_SNOW
-				|| curWeather == PRECIP_STORM
-				|| curWeather == PRECIP_STORM_NOSTRIKES
-				|| curWeather == PRECIP_RAIN
-				|| curWeather == PRECIP_BLANK)
-				swap = PRECIP_STORM_NORAIN;
-			else if (curWeather == PRECIP_STORM_NORAIN)
-				return;
-			break;
-		case PRECIP_BLANK:
-			if (curWeather == PRECIP_SNOW
-				|| curWeather == PRECIP_STORM
-				|| curWeather == PRECIP_STORM_NOSTRIKES
-				|| curWeather == PRECIP_RAIN)
-				swap = PRECIP_BLANK;
-			else if (curWeather == PRECIP_STORM_NORAIN)
-				swap = PRECIP_BLANK;
-			else if (curWeather == PRECIP_BLANK)
-				return;
-			break;
-		default:
-			CONS_Debug(DBG_GAMELOGIC, "P_SwitchWeather: Unknown weather type %d.\n", weathernum);
-			break;
-	}
+	if (weathernum == curWeather)
+		return;
+
+	if (is_rain_type(weathernum) &&
+			is_rain_type(curWeather))
+		purge = false;
 
 	if (purge)
 	{
@@ -2052,7 +2029,7 @@ void P_SwitchWeather(INT32 weathernum)
 			P_RemovePrecipMobj(precipmobj);
 		}
 	}
-	else if (swap && !((swap == PRECIP_BLANK && curWeather == PRECIP_STORM_NORAIN) || (swap == PRECIP_STORM_NORAIN && curWeather == PRECIP_BLANK))) // Rather than respawn all that crap, reuse it!
+	else // Rather than respawn all that crap, reuse it!
 	{
 		thinker_t *think;
 		precipmobj_t *precipmobj;
@@ -2064,7 +2041,7 @@ void P_SwitchWeather(INT32 weathernum)
 				continue; // not a precipmobj thinker
 			precipmobj = (precipmobj_t *)think;
 
-			if (swap == PRECIP_RAIN) // Snow To Rain
+			if (weathernum == PRECIP_RAIN || weathernum == PRECIP_STORM || weathernum == PRECIP_STORM_NOSTRIKES) // Snow To Rain
 			{
 				precipmobj->flags = mobjinfo[MT_RAIN].flags;
 				st = &states[mobjinfo[MT_RAIN].spawnstate];
@@ -2079,7 +2056,7 @@ void P_SwitchWeather(INT32 weathernum)
 				precipmobj->precipflags |= PCF_RAIN;
 				//think->function.acp1 = (actionf_p1)P_RainThinker;
 			}
-			else if (swap == PRECIP_SNOW) // Rain To Snow
+			else if (weathernum == PRECIP_SNOW) // Rain To Snow
 			{
 				INT32 z;
 
@@ -2104,7 +2081,7 @@ void P_SwitchWeather(INT32 weathernum)
 
 				//think->function.acp1 = (actionf_p1)P_SnowThinker;
 			}
-			else if (swap == PRECIP_BLANK || swap == PRECIP_STORM_NORAIN) // Remove precip, but keep it around for reuse.
+			else // Remove precip, but keep it around for reuse.
 			{
 				//think->function.acp1 = (actionf_p1)P_NullPrecipThinker;
 
@@ -2117,49 +2094,34 @@ void P_SwitchWeather(INT32 weathernum)
 	{
 		case PRECIP_SNOW: // snow
 			curWeather = PRECIP_SNOW;
-
-			if (!swap)
+			
+			if (purge)
 				P_SpawnPrecipitation();
 
 			break;
 		case PRECIP_RAIN: // rain
 		{
-			boolean dontspawn = false;
-
-			if (curWeather == PRECIP_RAIN || curWeather == PRECIP_STORM || curWeather == PRECIP_STORM_NOSTRIKES)
-				dontspawn = true;
-
 			curWeather = PRECIP_RAIN;
 
-			if (!dontspawn && !swap)
+			if (purge)
 				P_SpawnPrecipitation();
 
 			break;
 		}
 		case PRECIP_STORM: // storm
 		{
-			boolean dontspawn = false;
-
-			if (curWeather == PRECIP_RAIN || curWeather == PRECIP_STORM || curWeather == PRECIP_STORM_NOSTRIKES)
-				dontspawn = true;
-
 			curWeather = PRECIP_STORM;
 
-			if (!dontspawn && !swap)
+			if (purge)
 				P_SpawnPrecipitation();
 
 			break;
 		}
 		case PRECIP_STORM_NOSTRIKES: // storm w/o lightning
 		{
-			boolean dontspawn = false;
-
-			if (curWeather == PRECIP_RAIN || curWeather == PRECIP_STORM || curWeather == PRECIP_STORM_NOSTRIKES)
-				dontspawn = true;
-
 			curWeather = PRECIP_STORM_NOSTRIKES;
 
-			if (!dontspawn && !swap)
+			if (purge)
 				P_SpawnPrecipitation();
 
 			break;
@@ -2167,14 +2129,11 @@ void P_SwitchWeather(INT32 weathernum)
 		case PRECIP_STORM_NORAIN: // storm w/o rain
 			curWeather = PRECIP_STORM_NORAIN;
 
-			if (!swap)
-				P_SpawnPrecipitation();
-
 			break;
-		case PRECIP_BLANK:
+		case PRECIP_BLANK: //preloaded
 			curWeather = PRECIP_BLANK;
 
-			if (!swap)
+			if (purge)
 				P_SpawnPrecipitation();
 
 			break;
@@ -7307,7 +7266,8 @@ void P_SpawnSpecials(boolean fromnetsave)
 		}
 	}
 
-	P_RunLevelLoadExecutors();
+	if (!fromnetsave)
+		P_RunLevelLoadExecutors();
 }
 
 /** Adds 3Dfloors as appropriate based on a common control linedef.
diff --git a/src/p_tick.c b/src/p_tick.c
index d7357eb828508b49cc60b9e6d80beedab282982c..55a16fd81cb8b582fd8ab53c2d64c4f18809b8e2 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -323,7 +323,7 @@ static inline void P_RunThinkers(void)
 	size_t i;
 	for (i = 0; i < NUM_THINKERLISTS; i++)
 	{
-		ps_thlist_times[i] = I_GetPreciseTime();
+		PS_START_TIMING(ps_thlist_times[i]);
 		for (currentthinker = thlist[i].next; currentthinker != &thlist[i]; currentthinker = currentthinker->next)
 		{
 #ifdef PARANOIA
@@ -331,7 +331,7 @@ static inline void P_RunThinkers(void)
 #endif
 			currentthinker->function.acp1(currentthinker);
 		}
-		ps_thlist_times[i] = I_GetPreciseTime() - ps_thlist_times[i];
+		PS_STOP_TIMING(ps_thlist_times[i]);
 	}
 
 }
@@ -487,7 +487,7 @@ static inline void P_DoSpecialStageStuff(void)
 					continue;
 
 				// If in water, deplete timer 6x as fast.
-				if (players[i].mo->eflags & (MFE_TOUCHWATER|MFE_UNDERWATER) && !(players[i].powers[pw_shield] & SH_PROTECTWATER))
+				if (players[i].mo->eflags & (MFE_TOUCHWATER|MFE_UNDERWATER) && !(players[i].powers[pw_shield] & ((players[i].mo->eflags & MFE_TOUCHLAVA) ? SH_PROTECTFIRE : SH_PROTECTWATER)))
 					players[i].nightstime -= 5;
 				if (--players[i].nightstime > 6)
 				{
@@ -653,16 +653,16 @@ void P_Ticker(boolean run)
 			}
 		}
 
-		ps_lua_mobjhooks = 0;
-		ps_checkposition_calls = 0;
+		ps_lua_mobjhooks.value.i = 0;
+		ps_checkposition_calls.value.i = 0;
 
 		LUA_HOOK(PreThinkFrame);
 
-		ps_playerthink_time = I_GetPreciseTime();
+		PS_START_TIMING(ps_playerthink_time);
 		for (i = 0; i < MAXPLAYERS; i++)
 			if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
 				P_PlayerThink(&players[i]);
-		ps_playerthink_time = I_GetPreciseTime() - ps_playerthink_time;
+		PS_STOP_TIMING(ps_playerthink_time);
 	}
 
 	// Keep track of how long they've been playing!
@@ -677,18 +677,18 @@ void P_Ticker(boolean run)
 
 	if (run)
 	{
-		ps_thinkertime = I_GetPreciseTime();
+		PS_START_TIMING(ps_thinkertime);
 		P_RunThinkers();
-		ps_thinkertime = I_GetPreciseTime() - ps_thinkertime;
+		PS_STOP_TIMING(ps_thinkertime);
 
 		// Run any "after all the other thinkers" stuff
 		for (i = 0; i < MAXPLAYERS; i++)
 			if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
 				P_PlayerAfterThink(&players[i]);
 
-		ps_lua_thinkframe_time = I_GetPreciseTime();
+		PS_START_TIMING(ps_lua_thinkframe_time);
 		LUA_HookThinkFrame();
-		ps_lua_thinkframe_time = I_GetPreciseTime() - ps_lua_thinkframe_time;
+		PS_STOP_TIMING(ps_lua_thinkframe_time);
 	}
 
 	// Run shield positioning
diff --git a/src/p_user.c b/src/p_user.c
index 6466a5316f5966a95a8938e9bab8edccd1be14b1..9921ed078742bd3850f76ef4fb104d066efd3fa0 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -1367,8 +1367,8 @@ void P_AddPlayerScore(player_t *player, UINT32 amount)
 {
 	UINT32 oldscore;
 
-	if (player->bot)
-		player = &players[consoleplayer];
+	if ((player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN) && player->botleader)
+		player = player->botleader;
 
 	// NiGHTS does it different!
 	if (gamestate == GS_LEVEL && mapheaderinfo[gamemap-1]->typeoflevel & TOL_NIGHTS)
@@ -2819,7 +2819,6 @@ static void P_CheckBouncySectors(player_t *player)
 					player->pflags |= PF_THOKKED;
 				}
 			}
-
 			goto bouncydone;
 		}
 	}
@@ -5333,9 +5332,9 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 						// disabled because it seemed to disorient people and Z-targeting exists now
 						/*if (!demoplayback)
 						{
-							if (player == &players[consoleplayer] && cv_cam_turnfacingability[0].value > 0 && !(PLAYER1INPUTDOWN(gc_turnleft) || PLAYER1INPUTDOWN(gc_turnright)))
+							if (player == &players[consoleplayer] && cv_cam_turnfacingability[0].value > 0 && !(PLAYER1INPUTDOWN(GC_TURNLEFT) || PLAYER1INPUTDOWN(GC_TURNRIGHT)))
 								P_SetPlayerAngle(player, player->mo->angle);;
-							else if (player == &players[secondarydisplayplayer] && cv_cam_turnfacingability[1].value > 0 && !(PLAYER2INPUTDOWN(gc_turnleft) || PLAYER2INPUTDOWN(gc_turnright)))
+							else if (player == &players[secondarydisplayplayer] && cv_cam_turnfacingability[1].value > 0 && !(PLAYER2INPUTDOWN(GC_TURNLEFT) || PLAYER2INPUTDOWN(GC_TURNRIGHT)))
 								P_SetPlayerAngle(player, player->mo->angle);
 						}*/
 					}
@@ -5354,7 +5353,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 						player->powers[pw_tailsfly] = tailsflytics + 1; // Set the fly timer
 
 						player->pflags &= ~(PF_JUMPED|PF_NOJUMPDAMAGE|PF_SPINNING|PF_STARTDASH);
-						if (player->bot == 1)
+						if (player->bot == BOT_2PAI)
 							player->pflags |= PF_THOKKED;
 						else
 							player->pflags |= (PF_THOKKED|PF_CANCARRY);
@@ -5600,16 +5599,10 @@ INT32 P_GetPlayerControlDirection(player_t *player)
 {
 	ticcmd_t *cmd = &player->cmd;
 	angle_t controllerdirection, controlplayerdirection;
-	camera_t *thiscam;
 	angle_t dangle;
 	fixed_t tempx = 0, tempy = 0;
 	angle_t tempangle, origtempangle;
 
-	if (splitscreen && player == &players[secondarydisplayplayer])
-		thiscam = &camera2;
-	else
-		thiscam = &camera;
-
 	if (!cmd->forwardmove && !cmd->sidemove)
 		return 0;
 
@@ -5625,17 +5618,15 @@ INT32 P_GetPlayerControlDirection(player_t *player)
 		origtempangle = tempangle = 0; // relative to the axis rather than the player!
 		controlplayerdirection = R_PointToAngle2(0, 0, player->mo->momx, player->mo->momy);
 	}
-	else if ((P_ControlStyle(player) & CS_LMAOGALOG) && thiscam->chase)
+	else
 	{
 		if (player->awayviewtics)
 			origtempangle = tempangle = player->awayviewmobj->angle;
+		else if (P_ControlStyle(player) & CS_LMAOGALOG)
+			origtempangle = tempangle = (cmd->angleturn << 16);
 		else
-			origtempangle = tempangle = thiscam->angle;
-		controlplayerdirection = player->mo->angle;
-	}
-	else
-	{
-		origtempangle = tempangle = player->mo->angle;
+			origtempangle = tempangle = player->mo->angle;
+
 		controlplayerdirection = R_PointToAngle2(0, 0, player->mo->momx, player->mo->momy);
 	}
 
@@ -5941,22 +5932,6 @@ static void P_3dMovement(player_t *player)
 		acceleration = 96 + (FixedDiv(player->speed, player->mo->scale)>>FRACBITS) * 40;
 		topspeed = normalspd;
 	}
-	else if (player->bot)
-	{ // Bot steals player 1's stats
-		normalspd = FixedMul(players[consoleplayer].normalspeed, player->mo->scale);
-		thrustfactor = players[consoleplayer].thrustfactor;
-		acceleration = players[consoleplayer].accelstart + (FixedDiv(player->speed, player->mo->scale)>>FRACBITS) * players[consoleplayer].acceleration;
-
-		if (player->powers[pw_tailsfly])
-			topspeed = normalspd/2;
-		else if (player->mo->eflags & (MFE_UNDERWATER|MFE_GOOWATER))
-		{
-			topspeed = normalspd/2;
-			acceleration = 2*acceleration/3;
-		}
-		else
-			topspeed = normalspd;
-	}
 	else
 	{
 		if (player->powers[pw_super] || player->powers[pw_sneakers])
@@ -9486,11 +9461,11 @@ static void P_DeathThink(player_t *player)
 	if (player->deadtimer < INT32_MAX)
 		player->deadtimer++;
 
-	if (player->bot) // don't allow bots to do any of the below, B_CheckRespawn does all they need for respawning already
+	if (player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN) // don't allow followbots to do any of the below, B_CheckRespawn does all they need for respawning already
 		goto notrealplayer;
 
 	// continue logic
-	if (!(netgame || multiplayer) && player->lives <= 0)
+	if (!(netgame || multiplayer) && player->lives <= 0 && player == &players[consoleplayer]) //Extra players in SP can't be allowed to continue or end game
 	{
 		if (player->deadtimer > (3*TICRATE) && (cmd->buttons & BT_SPIN || cmd->buttons & BT_JUMP) && (!continuesInSession || player->continues > 0))
 			G_UseContinue();
@@ -11463,6 +11438,9 @@ void P_PlayerThink(player_t *player)
 		I_Error("p_playerthink: players[%s].mo == NULL", sizeu1(playeri));
 #endif
 
+	// Reset terrain blocked status for this frame
+	player->blocked = false;
+
 	// todo: Figure out what is actually causing these problems in the first place...
 	if (player->mo->health <= 0 && player->playerstate == PST_LIVE) //you should be DEAD!
 	{
@@ -11470,7 +11448,7 @@ void P_PlayerThink(player_t *player)
 		player->playerstate = PST_DEAD;
 	}
 
-	if (player->bot)
+	if (player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN)
 	{
 		if (player->playerstate == PST_LIVE || player->playerstate == PST_DEAD)
 		{
@@ -11614,7 +11592,7 @@ void P_PlayerThink(player_t *player)
 
 			for (i = 0; i < MAXPLAYERS; i++)
 			{
-				if (!playeringame[i] || players[i].spectator || players[i].bot)
+				if (!playeringame[i] || players[i].spectator || players[i].bot == BOT_2PAI || players[i].bot == BOT_2PHUMAN)
 					continue;
 				if (players[i].lives <= 0)
 					continue;
@@ -11645,8 +11623,8 @@ void P_PlayerThink(player_t *player)
 			INT32 i, total = 0, exiting = 0;
 
 			for (i = 0; i < MAXPLAYERS; i++)
-			{
-				if (!playeringame[i] || players[i].spectator || players[i].bot)
+			{ 
+				if (!playeringame[i] || players[i].spectator || players[i].bot == BOT_2PAI || players[i].bot == BOT_2PHUMAN)
 					continue;
 				if (players[i].quittime > 30 * TICRATE)
 					continue;
@@ -12586,8 +12564,8 @@ void P_PlayerAfterThink(player_t *player)
 					player->mo->momy = tails->momy;
 					player->mo->momz = tails->momz;
 				}
-
-				if (G_CoopGametype() && tails->player && tails->player->bot != 1)
+				
+				if (G_CoopGametype() && tails->player && tails->player->bot != BOT_2PAI)
 				{
 					player->mo->angle = tails->angle;
 
diff --git a/src/r_bsp.c b/src/r_bsp.c
index 5acd4e70c5fa083eaf675055be9d623e15f014e3..b8559d39e54cb5debb00878487bae7b9b7a5a29c 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -804,7 +804,7 @@ static void R_AddPolyObjects(subsector_t *sub)
 	}
 
 	// for render stats
-	ps_numpolyobjects += numpolys;
+	ps_numpolyobjects.value.i += numpolys;
 
 	// sort polyobjects
 	R_SortPolyObjects(sub);
@@ -1239,7 +1239,7 @@ void R_RenderBSPNode(INT32 bspnum)
 	node_t *bsp;
 	INT32 side;
 
-	ps_numbspcalls++;
+	ps_numbspcalls.value.i++;
 
 	while (!(bspnum & NF_SUBSECTOR))  // Found a subsector?
 	{
diff --git a/src/r_draw8_npo2.c b/src/r_draw8_npo2.c
index 71ec999486497c562214620fcd0510219daf2ce0..2433cb4024295401017fae6b08394a7db7d0d0df 100644
--- a/src/r_draw8_npo2.c
+++ b/src/r_draw8_npo2.c
@@ -666,7 +666,6 @@ void R_DrawTiltedSplat_NPO2_8(void)
 			for (; width != 0; width--)
 			{
 				colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
-				val = source[((v >> nflatyshift) & nflatmask) | (u >> nflatxshift)];
 				// Lactozilla: Non-powers-of-two
 				{
 					fixed_t x = (((fixed_t)u) >> FRACBITS);
diff --git a/src/r_main.c b/src/r_main.c
index 17e124cb9271c714a115e37705ee6ec11ed60faa..8729b5dcb36ccedb8aed999dbf3afcb60e0faf04 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -101,21 +101,22 @@ extracolormap_t *extra_colormaps = NULL;
 
 // Render stats
 precise_t ps_prevframetime = 0;
-precise_t ps_rendercalltime = 0;
-precise_t ps_uitime = 0;
-precise_t ps_swaptime = 0;
+ps_metric_t ps_rendercalltime = {0};
+ps_metric_t ps_otherrendertime = {0};
+ps_metric_t ps_uitime = {0};
+ps_metric_t ps_swaptime = {0};
 
-precise_t ps_bsptime = 0;
+ps_metric_t ps_bsptime = {0};
 
-precise_t ps_sw_spritecliptime = 0;
-precise_t ps_sw_portaltime = 0;
-precise_t ps_sw_planetime = 0;
-precise_t ps_sw_maskedtime = 0;
+ps_metric_t ps_sw_spritecliptime = {0};
+ps_metric_t ps_sw_portaltime = {0};
+ps_metric_t ps_sw_planetime = {0};
+ps_metric_t ps_sw_maskedtime = {0};
 
-int ps_numbspcalls = 0;
-int ps_numsprites = 0;
-int ps_numdrawnodes = 0;
-int ps_numpolyobjects = 0;
+ps_metric_t ps_numbspcalls = {0};
+ps_metric_t ps_numsprites = {0};
+ps_metric_t ps_numdrawnodes = {0};
+ps_metric_t ps_numpolyobjects = {0};
 
 static CV_PossibleValue_t drawdist_cons_t[] = {
 	{256, "256"},	{512, "512"},	{768, "768"},
@@ -1496,11 +1497,11 @@ void R_RenderPlayerView(player_t *player)
 	mytotal = 0;
 	ProfZeroTimer();
 #endif
-	ps_numbspcalls = ps_numpolyobjects = ps_numdrawnodes = 0;
-	ps_bsptime = I_GetPreciseTime();
+	ps_numbspcalls.value.i = ps_numpolyobjects.value.i = ps_numdrawnodes.value.i = 0;
+	PS_START_TIMING(ps_bsptime);
 	R_RenderBSPNode((INT32)numnodes - 1);
-	ps_bsptime = I_GetPreciseTime() - ps_bsptime;
-	ps_numsprites = visspritecount;
+	PS_STOP_TIMING(ps_bsptime);
+	ps_numsprites.value.i = visspritecount;
 #ifdef TIMING
 	RDMSR(0x10, &mycount);
 	mytotal += mycount; // 64bit add
@@ -1510,9 +1511,9 @@ void R_RenderPlayerView(player_t *player)
 //profile stuff ---------------------------------------------------------
 	Mask_Post(&masks[nummasks - 1]);
 
-	ps_sw_spritecliptime = I_GetPreciseTime();
+	PS_START_TIMING(ps_sw_spritecliptime);
 	R_ClipSprites(drawsegs, NULL);
-	ps_sw_spritecliptime = I_GetPreciseTime() - ps_sw_spritecliptime;
+	PS_STOP_TIMING(ps_sw_spritecliptime);
 
 
 	// Add skybox portals caused by sky visplanes.
@@ -1520,7 +1521,7 @@ void R_RenderPlayerView(player_t *player)
 		Portal_AddSkyboxPortals();
 
 	// Portal rendering. Hijacks the BSP traversal.
-	ps_sw_portaltime = I_GetPreciseTime();
+	PS_START_TIMING(ps_sw_portaltime);
 	if (portal_base)
 	{
 		portal_t *portal;
@@ -1560,17 +1561,17 @@ void R_RenderPlayerView(player_t *player)
 			Portal_Remove(portal);
 		}
 	}
-	ps_sw_portaltime = I_GetPreciseTime() - ps_sw_portaltime;
+	PS_STOP_TIMING(ps_sw_portaltime);
 
-	ps_sw_planetime = I_GetPreciseTime();
+	PS_START_TIMING(ps_sw_planetime);
 	R_DrawPlanes();
-	ps_sw_planetime = I_GetPreciseTime() - ps_sw_planetime;
+	PS_STOP_TIMING(ps_sw_planetime);
 
 	// draw mid texture and sprite
 	// And now 3D floors/sides!
-	ps_sw_maskedtime = I_GetPreciseTime();
+	PS_START_TIMING(ps_sw_maskedtime);
 	R_DrawMasked(masks, nummasks);
-	ps_sw_maskedtime = I_GetPreciseTime() - ps_sw_maskedtime;
+	PS_STOP_TIMING(ps_sw_maskedtime);
 
 	free(masks);
 }
diff --git a/src/r_main.h b/src/r_main.h
index f81447c456b8866cf9a0c1347c32a20ca7b7f057..5f3bed9803102cb7592ac755ef09f30870cc3f9f 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -17,6 +17,7 @@
 #include "d_player.h"
 #include "r_data.h"
 #include "r_textures.h"
+#include "m_perfstats.h" // ps_metric_t
 
 //
 // POV related.
@@ -79,21 +80,22 @@ boolean R_DoCulling(line_t *cullheight, line_t *viewcullheight, fixed_t vz, fixe
 // Render stats
 
 extern precise_t ps_prevframetime;// time when previous frame was rendered
-extern precise_t ps_rendercalltime;
-extern precise_t ps_uitime;
-extern precise_t ps_swaptime;
-
-extern precise_t ps_bsptime;
-
-extern precise_t ps_sw_spritecliptime;
-extern precise_t ps_sw_portaltime;
-extern precise_t ps_sw_planetime;
-extern precise_t ps_sw_maskedtime;
-
-extern int ps_numbspcalls;
-extern int ps_numsprites;
-extern int ps_numdrawnodes;
-extern int ps_numpolyobjects;
+extern ps_metric_t ps_rendercalltime;
+extern ps_metric_t ps_otherrendertime;
+extern ps_metric_t ps_uitime;
+extern ps_metric_t ps_swaptime;
+
+extern ps_metric_t ps_bsptime;
+
+extern ps_metric_t ps_sw_spritecliptime;
+extern ps_metric_t ps_sw_portaltime;
+extern ps_metric_t ps_sw_planetime;
+extern ps_metric_t ps_sw_maskedtime;
+
+extern ps_metric_t ps_numbspcalls;
+extern ps_metric_t ps_numsprites;
+extern ps_metric_t ps_numdrawnodes;
+extern ps_metric_t ps_numpolyobjects;
 
 //
 // REFRESH - the actual rendering functions.
diff --git a/src/r_patchrotation.c b/src/r_patchrotation.c
index a9b4a2b8fb701d45f7f21deefee1f75c768ee797..dae3a7b53a6cab88c151e7d1605d99fc5972c321 100644
--- a/src/r_patchrotation.c
+++ b/src/r_patchrotation.c
@@ -227,8 +227,8 @@ void RotatedPatch_DoRotation(rotsprite_t *rotsprite, patch_t *patch, INT32 angle
 
 	ox = (newwidth / 2) + (leftoffset - xpivot);
 	oy = (newheight / 2) + (patch->topoffset - ypivot);
-	width = (maxx - minx);
-	height = (maxy - miny);
+	width = (maxx+1 - minx);
+	height = (maxy+1 - miny);
 
 	if ((unsigned)(width * height) != size)
 	{
diff --git a/src/r_picformats.c b/src/r_picformats.c
index 59b1d16c5bdadbf0de6454f40f87815aee19582d..5c81d1e02186902818415c84088fcfc182235681 100644
--- a/src/r_picformats.c
+++ b/src/r_picformats.c
@@ -901,9 +901,8 @@ static png_bytep *PNG_Read(
 	png_colorp palette;
 	int palette_size;
 
-	png_bytep trans;
-	int trans_num;
-	png_color_16p trans_values;
+	png_bytep trans = NULL;
+	int num_trans = 0;
 
 #ifdef PNG_SETJMP_SUPPORTED
 #ifdef USE_FAR_KEYWORD
@@ -998,12 +997,12 @@ static png_bytep *PNG_Read(
 		// color is present on the image, the palette flag is disabled.
 		if (usepal)
 		{
-			png_get_tRNS(png_ptr, png_info_ptr, &trans, &trans_num, &trans_values);
+			png_uint_32 result = png_get_tRNS(png_ptr, png_info_ptr, &trans, &num_trans, NULL);
 
-			if (trans && trans_num > 0)
+			if ((result & PNG_INFO_tRNS) && num_trans > 0 && trans != NULL)
 			{
 				INT32 i;
-				for (i = 0; i < trans_num; i++)
+				for (i = 0; i < num_trans; i++)
 				{
 					// libpng will transform this image into RGBA even if
 					// the transparent index does not exist in the image,
diff --git a/src/r_picformats.h b/src/r_picformats.h
index b1bb35edd0bbbf71522bb418736caa2968ace940..c74f8a13a60a2c2602d656c5dad6920360f87d9c 100644
--- a/src/r_picformats.h
+++ b/src/r_picformats.h
@@ -116,9 +116,9 @@ void *Picture_PNGConvert(
 	size_t insize, size_t *outsize,
 	pictureflags_t flags);
 boolean Picture_PNGDimensions(UINT8 *png, INT32 *width, INT32 *height, INT16 *topoffset, INT16 *leftoffset, size_t size);
-#endif
 
 #define PICTURE_PNG_USELOOKUP
+#endif
 
 // SpriteInfo
 extern spriteinfo_t spriteinfo[NUMSPRITES];
diff --git a/src/r_plane.c b/src/r_plane.c
index 40170a1417713b562462c95a1db01b05ca251d20..88abed44a48c53a34fa1e42bfdfc44628f7d846b 100644
--- a/src/r_plane.c
+++ b/src/r_plane.c
@@ -89,8 +89,6 @@ static fixed_t planeheight;
 fixed_t yslopetab[MAXVIDHEIGHT*16];
 fixed_t *yslope;
 
-fixed_t basexscale, baseyscale;
-
 fixed_t cachedheight[MAXVIDHEIGHT];
 fixed_t cacheddistance[MAXVIDHEIGHT];
 fixed_t cachedxstep[MAXVIDHEIGHT];
@@ -114,7 +112,7 @@ void R_InitPlanes(void)
 // Sets planeripple.xfrac and planeripple.yfrac, added to ds_xfrac and ds_yfrac, if the span is not tilted.
 //
 
-struct
+static struct
 {
 	INT32 offset;
 	fixed_t xfrac, yfrac;
@@ -143,15 +141,6 @@ static void R_UpdatePlaneRipple(void)
 	planeripple.offset = (leveltime * 140);
 }
 
-//
-// R_MapPlane
-//
-// Uses global vars:
-//  planeheight
-//  basexscale
-//  baseyscale
-//  centerx
-
 static void R_MapPlane(INT32 y, INT32 x1, INT32 x2)
 {
 	angle_t angle, planecos, planesin;
@@ -176,16 +165,13 @@ static void R_MapPlane(INT32 y, INT32 x1, INT32 x2)
 		cacheddistance[y] = distance = FixedMul(planeheight, yslope[y]);
 		span = abs(centery - y);
 
-		if (span) // don't divide by zero
+		if (span) // Don't divide by zero
 		{
 			ds_xstep = FixedMul(planesin, planeheight) / span;
 			ds_ystep = FixedMul(planecos, planeheight) / span;
 		}
 		else
-		{
-			ds_xstep = FixedMul(distance, basexscale);
-			ds_ystep = FixedMul(distance, baseyscale);
-		}
+			ds_xstep = ds_ystep = FRACUNIT;
 
 		cachedxstep[y] = ds_xstep;
 		cachedystep[y] = ds_ystep;
@@ -197,6 +183,11 @@ static void R_MapPlane(INT32 y, INT32 x1, INT32 x2)
 		ds_ystep = cachedystep[y];
 	}
 
+	// [RH] Instead of using the xtoviewangle array, I calculated the fractional values
+	// at the middle of the screen, then used the calculated ds_xstep and ds_ystep
+	// to step from those to the proper texture coordinate to start drawing at.
+	// That way, the texture coordinate is always calculated by its position
+	// on the screen and not by its position relative to the edge of the visplane.
 	ds_xfrac = xoffs + FixedMul(planecos, distance) + (x1 - centerx) * ds_xstep;
 	ds_yfrac = yoffs - FixedMul(planesin, distance) + (x1 - centerx) * ds_ystep;
 
@@ -295,7 +286,6 @@ void R_ClearFFloorClips (void)
 void R_ClearPlanes(void)
 {
 	INT32 i, p;
-	angle_t angle;
 
 	// opening / clipping determination
 	for (i = 0; i < viewwidth; i++)
@@ -321,13 +311,6 @@ void R_ClearPlanes(void)
 
 	// texture calculation
 	memset(cachedheight, 0, sizeof (cachedheight));
-
-	// left to right mapping
-	angle = (viewangle-ANGLE_90)>>ANGLETOFINESHIFT;
-
-	// scale will be unit scale at SCREENWIDTH/2 distance
-	basexscale = FixedDiv (FINECOSINE(angle),centerxfrac);
-	baseyscale = -FixedDiv (FINESINE(angle),centerxfrac);
 }
 
 static visplane_t *new_visplane(unsigned hash)
@@ -335,7 +318,7 @@ static visplane_t *new_visplane(unsigned hash)
 	visplane_t *check = freetail;
 	if (!check)
 	{
-		check = calloc(2, sizeof (*check));
+		check = malloc(sizeof (*check));
 		if (check == NULL) I_Error("%s: Out of memory", "new_visplane"); // FIXME: ugly
 	}
 	else
@@ -380,9 +363,11 @@ visplane_t *R_FindPlane(fixed_t height, INT32 picnum, INT32 lightlevel,
 	{
 		if (polyobj->angle != 0)
 		{
-			angle_t fineshift = polyobj->angle >> ANGLETOFINESHIFT;
-			xoff -= FixedMul(FINECOSINE(fineshift), polyobj->centerPt.x)+FixedMul(FINESINE(fineshift), polyobj->centerPt.y);
-			yoff -= FixedMul(FINESINE(fineshift), polyobj->centerPt.x)-FixedMul(FINECOSINE(fineshift), polyobj->centerPt.y);
+			float ang = ANG2RAD(polyobj->angle);
+			float x = FixedToFloat(polyobj->centerPt.x);
+			float y = FixedToFloat(polyobj->centerPt.y);
+			xoff -= FloatToFixed(x * cos(ang) + y * sin(ang));
+			yoff -= FloatToFixed(x * sin(ang) - y * cos(ang));
 		}
 		else
 		{
@@ -530,79 +515,22 @@ visplane_t *R_CheckPlane(visplane_t *pl, INT32 start, INT32 stop)
 //
 // R_ExpandPlane
 //
-// This function basically expands the visplane or I_Errors.
+// This function basically expands the visplane.
 // The reason for this is that when creating 3D floor planes, there is no
 // need to create new ones with R_CheckPlane, because 3D floor planes
 // are created by subsector and there is no way a subsector can graphically
 // overlap.
 void R_ExpandPlane(visplane_t *pl, INT32 start, INT32 stop)
 {
-//	INT32 unionl, unionh;
-//	INT32 x;
-
 	// Don't expand polyobject planes here - we do that on our own.
 	if (pl->polyobj)
 		return;
 
 	if (pl->minx > start) pl->minx = start;
 	if (pl->maxx < stop)  pl->maxx = stop;
-/*
-	if (start < pl->minx)
-	{
-		unionl = start;
-	}
-	else
-	{
-		unionl = pl->minx;
-	}
-
-	if (stop > pl->maxx)
-	{
-		unionh = stop;
-	}
-	else
-	{
-		unionh = pl->maxx;
-	}
-	for (x = start; x <= stop; x++)
-		if (pl->top[x] != 0xffff || pl->bottom[x] != 0x0000)
-			break;
-
-	if (x <= stop)
-		I_Error("R_ExpandPlane: planes in same subsector overlap?!\nminx: %d, maxx: %d, start: %d, stop: %d\n", pl->minx, pl->maxx, start, stop);
-
-	pl->minx = unionl, pl->maxx = unionh;
-*/
-
-}
-
-static void R_MakeSpans(INT32 x, INT32 t1, INT32 b1, INT32 t2, INT32 b2)
-{
-	//    Alam: from r_splats's R_RasterizeFloorSplat
-	if (t1 >= vid.height) t1 = vid.height-1;
-	if (b1 >= vid.height) b1 = vid.height-1;
-	if (t2 >= vid.height) t2 = vid.height-1;
-	if (b2 >= vid.height) b2 = vid.height-1;
-	if (x-1 >= vid.width) x = vid.width;
-
-	while (t1 < t2 && t1 <= b1)
-	{
-		R_MapPlane(t1, spanstart[t1], x - 1);
-		t1++;
-	}
-	while (b1 > b2 && b1 >= t1)
-	{
-		R_MapPlane(b1, spanstart[b1], x - 1);
-		b1--;
-	}
-
-	while (t2 < t1 && t2 <= b2)
-		spanstart[t2++] = x;
-	while (b2 > b1 && b2 >= t2)
-		spanstart[b2--] = x;
 }
 
-static void R_MakeTiltedSpans(INT32 x, INT32 t1, INT32 b1, INT32 t2, INT32 b2)
+static void R_MakeSpans(void (*mapfunc)(INT32, INT32, INT32), INT32 x, INT32 t1, INT32 b1, INT32 t2, INT32 b2)
 {
 	//    Alam: from r_splats's R_RasterizeFloorSplat
 	if (t1 >= vid.height) t1 = vid.height-1;
@@ -613,12 +541,12 @@ static void R_MakeTiltedSpans(INT32 x, INT32 t1, INT32 b1, INT32 t2, INT32 b2)
 
 	while (t1 < t2 && t1 <= b1)
 	{
-		R_MapTiltedPlane(t1, spanstart[t1], x - 1);
+		mapfunc(t1, spanstart[t1], x - 1);
 		t1++;
 	}
 	while (b1 > b2 && b1 >= t1)
 	{
-		R_MapTiltedPlane(b1, spanstart[b1], x - 1);
+		mapfunc(b1, spanstart[b1], x - 1);
 		b1--;
 	}
 
@@ -865,11 +793,10 @@ void R_DrawSinglePlane(visplane_t *pl)
 {
 	levelflat_t *levelflat;
 	INT32 light = 0;
-	INT32 x;
-	INT32 stop, angle;
+	INT32 x, stop;
 	ffloor_t *rover;
-	INT32 type;
-	INT32 spanfunctype = BASEDRAWFUNC;
+	INT32 type, spanfunctype = BASEDRAWFUNC;
+	void (*mapfunc)(INT32, INT32, INT32) = R_MapPlane;
 
 	if (!(pl->minx <= pl->maxx))
 		return;
@@ -1021,9 +948,6 @@ void R_DrawSinglePlane(visplane_t *pl)
 		&& viewangle != pl->viewangle+pl->plangle)
 	{
 		memset(cachedheight, 0, sizeof (cachedheight));
-		angle = (pl->viewangle+pl->plangle-ANGLE_90)>>ANGLETOFINESHIFT;
-		basexscale = FixedDiv(FINECOSINE(angle),centerxfrac);
-		baseyscale = -FixedDiv(FINESINE(angle),centerxfrac);
 		viewangle = pl->viewangle+pl->plangle;
 	}
 
@@ -1038,6 +962,8 @@ void R_DrawSinglePlane(visplane_t *pl)
 
 	if (pl->slope)
 	{
+		mapfunc = R_MapTiltedPlane;
+
 		if (!pl->plangle)
 		{
 			if (ds_powersoftwo)
@@ -1105,16 +1031,8 @@ void R_DrawSinglePlane(visplane_t *pl)
 
 	stop = pl->maxx + 1;
 
-	if (pl->slope)
-	{
-		for (x = pl->minx; x <= stop; x++)
-			R_MakeTiltedSpans(x, pl->top[x-1], pl->bottom[x-1], pl->top[x], pl->bottom[x]);
-	}
-	else
-	{
-		for (x = pl->minx; x <= stop; x++)
-			R_MakeSpans(x, pl->top[x-1], pl->bottom[x-1], pl->top[x], pl->bottom[x]);
-	}
+	for (x = pl->minx; x <= stop; x++)
+		R_MakeSpans(mapfunc, x, pl->top[x-1], pl->bottom[x-1], pl->top[x], pl->bottom[x]);
 
 /*
 QUINCUNX anti-aliasing technique (sort of)
@@ -1181,7 +1099,7 @@ using the palette colors.
 			stop = pl->maxx + 1;
 
 			for (x = pl->minx; x <= stop; x++)
-				R_MakeSpans(x, pl->top[x-1], pl->bottom[x-1],
+				R_MakeSpans(mapfunc, x, pl->top[x-1], pl->bottom[x-1],
 					pl->top[x], pl->bottom[x]);
 		}
 	}
diff --git a/src/r_plane.h b/src/r_plane.h
index bdad77930af31d3448ea98208e128dfa25a99502..862b95069ddacdd85382ef7f55c476a592b4177b 100644
--- a/src/r_plane.h
+++ b/src/r_plane.h
@@ -69,7 +69,6 @@ extern fixed_t cachedheight[MAXVIDHEIGHT];
 extern fixed_t cacheddistance[MAXVIDHEIGHT];
 extern fixed_t cachedxstep[MAXVIDHEIGHT];
 extern fixed_t cachedystep[MAXVIDHEIGHT];
-extern fixed_t basexscale, baseyscale;
 
 extern fixed_t *yslope;
 extern lighttable_t **planezlight;
diff --git a/src/r_skins.c b/src/r_skins.c
index b7eb24345d469682cc70f4e251a6101c4688e60a..86c0bbc544b7907f30b6ed4ef6c07326f8669a98 100644
--- a/src/r_skins.c
+++ b/src/r_skins.c
@@ -242,6 +242,11 @@ boolean R_SkinUsable(INT32 playernum, INT32 skinnum)
 		// Force 3.
 		return true;
 	}
+	if (playernum != -1 && players[playernum].bot)
+    {
+        //Force 4.
+        return true;
+    }
 
 	// We will now check if this skin is supposed to be locked or not.
 
diff --git a/src/r_splats.c b/src/r_splats.c
index 4783fb6408d560d7dc9578a09e09d6350227fdb5..c554e9b1f002937671e19b1e043460d3168c3e2e 100644
--- a/src/r_splats.c
+++ b/src/r_splats.c
@@ -155,7 +155,6 @@ void R_DrawFloorSplat(vissprite_t *spr)
 	fixed_t xscale, yscale;
 	fixed_t xoffset, yoffset;
 	fixed_t leftoffset, topoffset;
-	pslope_t *slope = NULL;
 	INT32 i;
 
 	boolean hflip = (spr->xiscale < 0);
@@ -188,7 +187,7 @@ void R_DrawFloorSplat(vissprite_t *spr)
 	if (spr->rotateflags & SRF_3D || renderflags & RF_NOSPLATBILLBOARD)
 		splatangle = mobj->angle;
 	else
-		splatangle = spr->viewangle;
+		splatangle = spr->viewpoint.angle;
 
 	if (!(spr->cut & SC_ISROTATED))
 		splatangle += mobj->rollangle;
@@ -218,7 +217,7 @@ void R_DrawFloorSplat(vissprite_t *spr)
 	splat.x = x;
 	splat.y = y;
 	splat.z = mobj->z;
-	splat.tilted = false;
+	splat.slope = NULL;
 
 	// Set positions
 
@@ -238,9 +237,9 @@ void R_DrawFloorSplat(vissprite_t *spr)
 	splat.verts[3].x = w - xoffset;
 	splat.verts[3].y = -h + yoffset;
 
-	angle = -splat.angle;
-	ca = FINECOSINE(angle>>ANGLETOFINESHIFT);
-	sa = FINESINE(angle>>ANGLETOFINESHIFT);
+	angle = -splat.angle>>ANGLETOFINESHIFT;
+	ca = FINECOSINE(angle);
+	sa = FINESINE(angle);
 
 	// Rotate
 	for (i = 0; i < 4; i++)
@@ -255,36 +254,10 @@ void R_DrawFloorSplat(vissprite_t *spr)
 
 		// The slope that was defined for the sprite.
 		if (renderflags & RF_SLOPESPLAT)
-			slope = mobj->floorspriteslope;
+			splat.slope = mobj->floorspriteslope;
 
 		if (standingslope && (renderflags & RF_OBJECTSLOPESPLAT))
-			slope = standingslope;
-
-		// Set splat as tilted
-		splat.tilted = (slope != NULL);
-	}
-
-	if (splat.tilted)
-	{
-		pslope_t *s = &splat.slope;
-
-		s->o.x = slope->o.x;
-		s->o.y = slope->o.y;
-		s->o.z = slope->o.z;
-
-		s->d.x = slope->d.x;
-		s->d.y = slope->d.y;
-
-		s->normal.x = slope->normal.x;
-		s->normal.y = slope->normal.y;
-		s->normal.z = slope->normal.z;
-
-		s->zdelta = slope->zdelta;
-		s->zangle = slope->zangle;
-		s->xydirection = slope->xydirection;
-
-		s->next = NULL;
-		s->flags = 0;
+			splat.slope = standingslope;
 	}
 
 	// Translate
@@ -293,9 +266,9 @@ void R_DrawFloorSplat(vissprite_t *spr)
 		tr_x = rotated[i].x + x;
 		tr_y = rotated[i].y + y;
 
-		if (slope)
+		if (splat.slope)
 		{
-			rot_z = P_GetSlopeZAt(slope, tr_x, tr_y);
+			rot_z = P_GetSlopeZAt(splat.slope, tr_x, tr_y);
 			splat.verts[i].z = rot_z;
 		}
 		else
@@ -305,18 +278,23 @@ void R_DrawFloorSplat(vissprite_t *spr)
 		splat.verts[i].y = tr_y;
 	}
 
+	angle = spr->viewpoint.angle >> ANGLETOFINESHIFT;
+	ca = FINECOSINE(angle);
+	sa = FINESINE(angle);
+
+	// Project
 	for (i = 0; i < 4; i++)
 	{
 		v3d = &splat.verts[i];
 
 		// transform the origin point
-		tr_x = v3d->x - viewx;
-		tr_y = v3d->y - viewy;
+		tr_x = v3d->x - spr->viewpoint.x;
+		tr_y = v3d->y - spr->viewpoint.y;
 
 		// rotation around vertical y axis
-		rot_x = FixedMul(tr_x, viewsin) - FixedMul(tr_y, viewcos);
-		rot_y = FixedMul(tr_x, viewcos) + FixedMul(tr_y, viewsin);
-		rot_z = v3d->z - viewz;
+		rot_x = FixedMul(tr_x, sa) - FixedMul(tr_y, ca);
+		rot_y = FixedMul(tr_x, ca) + FixedMul(tr_y, sa);
+		rot_z = v3d->z - spr->viewpoint.z;
 
 		if (rot_y < FRACUNIT)
 			return;
@@ -416,31 +394,32 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 	if (R_CheckPowersOfTwo())
 		R_CheckFlatLength(ds_flatwidth * ds_flatheight);
 
-	if (pSplat->tilted)
+	if (pSplat->slope)
 	{
 		R_SetTiltedSpan(0);
-		R_SetScaledSlopePlane(&pSplat->slope, viewx, viewy, viewz, pSplat->xscale, pSplat->yscale, -pSplat->verts[0].x, pSplat->verts[0].y, vis->viewangle, pSplat->angle);
+		R_SetScaledSlopePlane(pSplat->slope, vis->viewpoint.x, vis->viewpoint.y, vis->viewpoint.z, pSplat->xscale, pSplat->yscale, -pSplat->verts[0].x, pSplat->verts[0].y, vis->viewpoint.angle, pSplat->angle);
 		R_CalculateSlopeVectors();
 		spanfunctype = SPANDRAWFUNC_TILTEDSPRITE;
 	}
 	else
 	{
-		planeheight = abs(pSplat->z - viewz);
+		planeheight = abs(pSplat->z - vis->viewpoint.z);
 
 		if (pSplat->angle)
 		{
+			memset(cachedheight, 0, sizeof(cachedheight));
+
 			// Add the view offset, rotated by the plane angle.
-			fixed_t a = -pSplat->verts[0].x + viewx;
-			fixed_t b = -pSplat->verts[0].y + viewy;
+			fixed_t a = -pSplat->verts[0].x + vis->viewpoint.x;
+			fixed_t b = -pSplat->verts[0].y + vis->viewpoint.y;
 			angle_t angle = (pSplat->angle >> ANGLETOFINESHIFT);
-			offsetx = FixedMul(a, FINECOSINE(angle)) - FixedMul(b,FINESINE(angle));
-			offsety = -FixedMul(a, FINESINE(angle)) - FixedMul(b,FINECOSINE(angle));
-			memset(cachedheight, 0, sizeof(cachedheight));
+			offsetx = FixedMul(a, FINECOSINE(angle)) - FixedMul(b, FINESINE(angle));
+			offsety = -FixedMul(a, FINESINE(angle)) - FixedMul(b, FINECOSINE(angle));
 		}
 		else
 		{
-			offsetx = viewx - pSplat->verts[0].x;
-			offsety = pSplat->verts[0].y - viewy;
+			offsetx = vis->viewpoint.x - pSplat->verts[0].x;
+			offsety = pSplat->verts[0].y - vis->viewpoint.y;
 		}
 	}
 
@@ -461,7 +440,7 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 	{
 		ds_transmap = vis->transmap;
 
-		if (pSplat->tilted)
+		if (pSplat->slope)
 			spanfunctype = SPANDRAWFUNC_TILTEDTRANSSPRITE;
 		else
 			spanfunctype = SPANDRAWFUNC_TRANSSPRITE;
@@ -528,12 +507,12 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 		if (x2 < x1)
 			continue;
 
-		if (!pSplat->tilted)
+		if (!pSplat->slope)
 		{
 			fixed_t xstep, ystep;
 			fixed_t distance, span;
 
-			angle_t angle = (vis->viewangle + pSplat->angle)>>ANGLETOFINESHIFT;
+			angle_t angle = (vis->viewpoint.angle + pSplat->angle)>>ANGLETOFINESHIFT;
 			angle_t planecos = FINECOSINE(angle);
 			angle_t planesin = FINESINE(angle);
 
@@ -577,7 +556,7 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 		rastertab[y].maxx = INT32_MIN;
 	}
 
-	if (pSplat->angle && !pSplat->tilted)
+	if (pSplat->angle && !pSplat->slope)
 		memset(cachedheight, 0, sizeof(cachedheight));
 }
 
diff --git a/src/r_splats.h b/src/r_splats.h
index cab3d63b66bf8f2bca5324dcd2f677253e7dee7d..7e31406d1290e94861a1f660299a05be3dfc8670 100644
--- a/src/r_splats.h
+++ b/src/r_splats.h
@@ -34,8 +34,7 @@ typedef struct floorsplat_s
 	INT32 width, height;
 	fixed_t scale, xscale, yscale;
 	angle_t angle;
-	boolean tilted; // Uses the tilted drawer
-	pslope_t slope;
+	pslope_t *slope;
 
 	vector3_t verts[4]; // (x,y,z) as viewed from above on map
 	fixed_t x, y, z; // position
diff --git a/src/r_textures.c b/src/r_textures.c
index d5da69018cb39c81a1dcac1fe01736ee67abab65..793e5237f62e64e937c32f97d4d01bbb08a6c065 100644
--- a/src/r_textures.c
+++ b/src/r_textures.c
@@ -727,7 +727,7 @@ Rloadflats (INT32 i, INT32 w)
 	texpatch_t *patch;
 
 	// Yes
-	if (wadfiles[w]->type == RET_PK3)
+	if (W_FileHasFolders(wadfiles[w]))
 	{
 		texstart = W_CheckNumForFolderStartPK3("flats/", (UINT16)w, 0);
 		texend = W_CheckNumForFolderEndPK3("flats/", (UINT16)w, texstart);
@@ -749,7 +749,7 @@ Rloadflats (INT32 i, INT32 w)
 			size_t lumplength;
 			size_t flatsize = 0;
 
-			if (wadfiles[w]->type == RET_PK3)
+			if (W_FileHasFolders(wadfiles[w]))
 			{
 				if (W_IsLumpFolder(wadnum, lumpnum)) // Check if lump is a folder
 					continue; // If it is then SKIP IT
@@ -839,7 +839,7 @@ Rloadtextures (INT32 i, INT32 w)
 	texpatch_t *patch;
 
 	// Get the lump numbers for the markers in the WAD, if they exist.
-	if (wadfiles[w]->type == RET_PK3)
+	if (W_FileHasFolders(wadfiles[w]))
 	{
 		texstart = W_CheckNumForFolderStartPK3("textures/", (UINT16)w, 0);
 		texend = W_CheckNumForFolderEndPK3("textures/", (UINT16)w, texstart);
@@ -870,7 +870,7 @@ Rloadtextures (INT32 i, INT32 w)
 			size_t lumplength;
 #endif
 
-			if (wadfiles[w]->type == RET_PK3)
+			if (W_FileHasFolders(wadfiles[w]))
 			{
 				if (W_IsLumpFolder(wadnum, lumpnum)) // Check if lump is a folder
 					continue; // If it is then SKIP IT
@@ -959,7 +959,7 @@ void R_LoadTextures(void)
 	{
 #ifdef WALLFLATS
 		// Count flats
-		if (wadfiles[w]->type == RET_PK3)
+		if (W_FileHasFolders(wadfiles[w]))
 		{
 			texstart = W_CheckNumForFolderStartPK3("flats/", (UINT16)w, 0);
 			texend = W_CheckNumForFolderEndPK3("flats/", (UINT16)w, texstart);
@@ -973,7 +973,7 @@ void R_LoadTextures(void)
 		if (!( texstart == INT16_MAX || texend == INT16_MAX ))
 		{
 			// PK3s have subfolders, so we can't just make a simple sum
-			if (wadfiles[w]->type == RET_PK3)
+			if (W_FileHasFolders(wadfiles[w]))
 			{
 				for (j = texstart; j < texend; j++)
 				{
@@ -997,7 +997,7 @@ void R_LoadTextures(void)
 		}
 
 		// Count single-patch textures
-		if (wadfiles[w]->type == RET_PK3)
+		if (W_FileHasFolders(wadfiles[w]))
 		{
 			texstart = W_CheckNumForFolderStartPK3("textures/", (UINT16)w, 0);
 			texend = W_CheckNumForFolderEndPK3("textures/", (UINT16)w, texstart);
@@ -1012,7 +1012,7 @@ void R_LoadTextures(void)
 			continue;
 
 		// PK3s have subfolders, so we can't just make a simple sum
-		if (wadfiles[w]->type == RET_PK3)
+		if (W_FileHasFolders(wadfiles[w]))
 		{
 			for (j = texstart; j < texend; j++)
 			{
@@ -1553,6 +1553,7 @@ lumpnum_t R_GetFlatNumForName(const char *name)
 					continue;
 			break;
 		case RET_PK3:
+		case RET_FOLDER:
 			if ((start = W_CheckNumForFolderStartPK3("Flats/", i, 0)) == INT16_MAX)
 				continue;
 			if ((end = W_CheckNumForFolderEndPK3("Flats/", i, start)) == INT16_MAX)
diff --git a/src/r_things.c b/src/r_things.c
index 0283712b85bd610a6c82dadfbeb98d9572bc14ea..bed71a6d791f1c5dbc41a0f517560dc5387c1b08 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -443,6 +443,7 @@ void R_AddSpriteDefs(UINT16 wadnum)
 			end = W_CheckNumForNamePwad("SS_END",wadnum,start);     //deutex compatib.
 		break;
 	case RET_PK3:
+	case RET_FOLDER:
 		start = W_CheckNumForFolderStartPK3("Sprites/", wadnum, 0);
 		end = W_CheckNumForFolderEndPK3("Sprites/", wadnum, start);
 		break;
@@ -1956,9 +1957,12 @@ static void R_ProjectSprite(mobj_t *thing)
 	vis->paperoffset = paperoffset;
 	vis->paperdistance = paperdistance;
 	vis->centerangle = centerangle;
-	vis->viewangle = viewangle;
 	vis->shear.tan = sheartan;
 	vis->shear.offset = 0;
+	vis->viewpoint.x = viewx;
+	vis->viewpoint.y = viewy;
+	vis->viewpoint.z = viewz;
+	vis->viewpoint.angle = viewangle;
 
 	vis->mobj = thing; // Easy access! Tails 06-07-2002
 
@@ -2741,7 +2745,7 @@ static drawnode_t *R_CreateDrawNode(drawnode_t *link)
 	node->ffloor = NULL;
 	node->sprite = NULL;
 
-	ps_numdrawnodes++;
+	ps_numdrawnodes.value.i++;
 	return node;
 }
 
diff --git a/src/r_things.h b/src/r_things.h
index 9315b36e946c35020d88a78049e18a284d9a3790..79dc80d94a2a15691056e34716f038a491cf54ba 100644
--- a/src/r_things.h
+++ b/src/r_things.h
@@ -164,7 +164,12 @@ typedef struct vissprite_s
 	fixed_t xiscale; // negative if flipped
 
 	angle_t centerangle; // for paper sprites
-	angle_t viewangle; // for floor sprites, the viewpoint's current angle
+
+	// for floor sprites
+	struct {
+		fixed_t x, y, z; // the viewpoint's current position
+		angle_t angle; // the viewpoint's current angle
+	} viewpoint;
 
 	struct {
 		fixed_t tan; // The amount to shear the sprite vertically per row
@@ -185,9 +190,10 @@ typedef struct vissprite_s
 
 	extracolormap_t *extra_colormap; // global colormaps
 
-	// Precalculated top and bottom screen coords for the sprite.
 	fixed_t thingheight; // The actual height of the thing (for 3D floors)
 	sector_t *sector; // The sector containing the thing.
+
+	// Precalculated top and bottom screen coords for the sprite.
 	INT16 sz, szt;
 
 	spritecut_e cut;
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index d68e3e435bb6c749b8e770e0cc604aba96206185..ccec37093698edbcc7865702fea0138db5996749 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -550,7 +550,7 @@ static void I_StartupConsole(void)
 void I_GetConsoleEvents(void)
 {
 	// we use this when sending back commands
-	event_t ev = {0,0,0,0};
+	event_t ev = {0};
 	char key = 0;
 	ssize_t d;
 
@@ -572,7 +572,7 @@ void I_GetConsoleEvents(void)
 			tty_con.buffer[tty_con.cursor] = '\0';
 			tty_Back();
 		}
-		ev.data1 = KEY_BACKSPACE;
+		ev.key = KEY_BACKSPACE;
 	}
 	else if (key < ' ') // check if this is a control char
 	{
@@ -580,19 +580,19 @@ void I_GetConsoleEvents(void)
 		{
 			tty_Clear();
 			tty_con.cursor = 0;
-			ev.data1 = KEY_ENTER;
+			ev.key = KEY_ENTER;
 		}
 		else return;
 	}
 	else
 	{
 		// push regular character
-		ev.data1 = tty_con.buffer[tty_con.cursor] = key;
+		ev.key = tty_con.buffer[tty_con.cursor] = key;
 		tty_con.cursor++;
 		// print the current line (this is differential)
 		d = write(STDOUT_FILENO, &key, 1);
 	}
-	if (ev.data1) D_PostEvent(&ev);
+	if (ev.key) D_PostEvent(&ev);
 	//tty_FlushIn();
 	(void)d;
 }
@@ -626,18 +626,18 @@ static void Impl_HandleKeyboardConsoleEvent(KEY_EVENT_RECORD evt, HANDLE co)
 		{
 			case VK_ESCAPE:
 			case VK_TAB:
-				event.data1 = KEY_NULL;
+				event.key = KEY_NULL;
 				break;
 			case VK_RETURN:
 				entering_con_command = false;
 				/* FALLTHRU */
 			default:
-				//event.data1 = MapVirtualKey(evt.wVirtualKeyCode,2); // convert in to char
-				event.data1 = evt.uChar.AsciiChar;
+				//event.key = MapVirtualKey(evt.wVirtualKeyCode,2); // convert in to char
+				event.key = evt.uChar.AsciiChar;
 		}
 		if (co != INVALID_HANDLE_VALUE && GetFileType(co) == FILE_TYPE_CHAR && GetConsoleMode(co, &t))
 		{
-			if (event.data1 && event.data1 != KEY_LSHIFT && event.data1 != KEY_RSHIFT)
+			if (event.key && event.key != KEY_LSHIFT && event.key != KEY_RSHIFT)
 			{
 #ifdef _UNICODE
 				WriteConsole(co, &evt.uChar.UnicodeChar, 1, &t, NULL);
@@ -652,7 +652,7 @@ static void Impl_HandleKeyboardConsoleEvent(KEY_EVENT_RECORD evt, HANDLE co)
 			}
 		}
 	}
-	if (event.data1) D_PostEvent(&event);
+	if (event.key) D_PostEvent(&event);
 }
 
 void I_GetConsoleEvents(void)
@@ -917,7 +917,7 @@ INT32 I_GetKey (void)
 		ev = &events[eventtail];
 		if (ev->type == ev_keydown || ev->type == ev_console)
 		{
-			rc = ev->data1;
+			rc = ev->key;
 			continue;
 		}
 	}
@@ -977,22 +977,22 @@ void I_ShutdownJoystick(void)
 	INT32 i;
 	event_t event;
 	event.type=ev_keyup;
-	event.data2 = 0;
-	event.data3 = 0;
+	event.x = 0;
+	event.y = 0;
 
 	lastjoybuttons = lastjoyhats = 0;
 
 	// emulate the up of all joystick buttons
 	for (i=0;i<JOYBUTTONS;i++)
 	{
-		event.data1=KEY_JOY1+i;
+		event.key=KEY_JOY1+i;
 		D_PostEvent(&event);
 	}
 
 	// emulate the up of all joystick hats
 	for (i=0;i<JOYHATS*4;i++)
 	{
-		event.data1=KEY_HAT1+i;
+		event.key=KEY_HAT1+i;
 		D_PostEvent(&event);
 	}
 
@@ -1000,7 +1000,7 @@ void I_ShutdownJoystick(void)
 	event.type = ev_joystick;
 	for (i=0;i<JOYAXISSET; i++)
 	{
-		event.data1 = i;
+		event.key = i;
 		D_PostEvent(&event);
 	}
 
@@ -1012,7 +1012,7 @@ void I_ShutdownJoystick(void)
 
 void I_GetJoystickEvents(void)
 {
-	static event_t event = {0,0,0,0};
+	static event_t event = {0,0,0,0,false};
 	INT32 i = 0;
 	UINT64 joyhats = 0;
 #if 0
@@ -1049,7 +1049,7 @@ void I_GetJoystickEvents(void)
 					event.type = ev_keydown;
 				else
 					event.type = ev_keyup;
-				event.data1 = KEY_JOY1 + i;
+				event.key = KEY_JOY1 + i;
 				D_PostEvent(&event);
 			}
 		}
@@ -1080,7 +1080,7 @@ void I_GetJoystickEvents(void)
 					event.type = ev_keydown;
 				else
 					event.type = ev_keyup;
-				event.data1 = KEY_HAT1 + i;
+				event.key = KEY_HAT1 + i;
 				D_PostEvent(&event);
 			}
 		}
@@ -1092,7 +1092,7 @@ void I_GetJoystickEvents(void)
 
 	for (i = JOYAXISSET - 1; i >= 0; i--)
 	{
-		event.data1 = i;
+		event.key = i;
 		if (i*2 + 1 <= JoyInfo.axises)
 			axisx = SDL_JoystickGetAxis(JoyInfo.dev, i*2 + 0);
 		else axisx = 0;
@@ -1110,15 +1110,15 @@ void I_GetJoystickEvents(void)
 		{
 			// gamepad control type, on or off, live or die
 			if (axisx < -(JOYAXISRANGE/2))
-				event.data2 = -1;
+				event.x = -1;
 			else if (axisx > (JOYAXISRANGE/2))
-				event.data2 = 1;
-			else event.data2 = 0;
+				event.x = 1;
+			else event.x = 0;
 			if (axisy < -(JOYAXISRANGE/2))
-				event.data3 = -1;
+				event.y = -1;
 			else if (axisy > (JOYAXISRANGE/2))
-				event.data3 = 1;
-			else event.data3 = 0;
+				event.y = 1;
+			else event.y = 0;
 		}
 		else
 		{
@@ -1132,8 +1132,8 @@ void I_GetJoystickEvents(void)
 #endif
 
 			// analog control style , just send the raw data
-			event.data2 = axisx; // x axis
-			event.data3 = axisy; // y axis
+			event.x = axisx; // x axis
+			event.y = axisy; // y axis
 		}
 		D_PostEvent(&event);
 	}
@@ -1247,22 +1247,22 @@ void I_ShutdownJoystick2(void)
 	INT32 i;
 	event_t event;
 	event.type = ev_keyup;
-	event.data2 = 0;
-	event.data3 = 0;
+	event.x = 0;
+	event.y = 0;
 
 	lastjoy2buttons = lastjoy2hats = 0;
 
 	// emulate the up of all joystick buttons
 	for (i = 0; i < JOYBUTTONS; i++)
 	{
-		event.data1 = KEY_2JOY1 + i;
+		event.key = KEY_2JOY1 + i;
 		D_PostEvent(&event);
 	}
 
 	// emulate the up of all joystick hats
 	for (i = 0; i < JOYHATS*4; i++)
 	{
-		event.data1 = KEY_2HAT1 + i;
+		event.key = KEY_2HAT1 + i;
 		D_PostEvent(&event);
 	}
 
@@ -1270,7 +1270,7 @@ void I_ShutdownJoystick2(void)
 	event.type = ev_joystick2;
 	for (i = 0; i < JOYAXISSET; i++)
 	{
-		event.data1 = i;
+		event.key = i;
 		D_PostEvent(&event);
 	}
 
@@ -1282,7 +1282,7 @@ void I_ShutdownJoystick2(void)
 
 void I_GetJoystick2Events(void)
 {
-	static event_t event = {0,0,0,0};
+	static event_t event = {0,0,0,0,false};
 	INT32 i = 0;
 	UINT64 joyhats = 0;
 #if 0
@@ -1321,7 +1321,7 @@ void I_GetJoystick2Events(void)
 					event.type = ev_keydown;
 				else
 					event.type = ev_keyup;
-				event.data1 = KEY_2JOY1 + i;
+				event.key = KEY_2JOY1 + i;
 				D_PostEvent(&event);
 			}
 		}
@@ -1352,7 +1352,7 @@ void I_GetJoystick2Events(void)
 					event.type = ev_keydown;
 				else
 					event.type = ev_keyup;
-				event.data1 = KEY_2HAT1 + i;
+				event.key = KEY_2HAT1 + i;
 				D_PostEvent(&event);
 			}
 		}
@@ -1364,7 +1364,7 @@ void I_GetJoystick2Events(void)
 
 	for (i = JOYAXISSET - 1; i >= 0; i--)
 	{
-		event.data1 = i;
+		event.key = i;
 		if (i*2 + 1 <= JoyInfo2.axises)
 			axisx = SDL_JoystickGetAxis(JoyInfo2.dev, i*2 + 0);
 		else axisx = 0;
@@ -1380,17 +1380,17 @@ void I_GetJoystick2Events(void)
 		{
 			// gamepad control type, on or off, live or die
 			if (axisx < -(JOYAXISRANGE/2))
-				event.data2 = -1;
+				event.x = -1;
 			else if (axisx > (JOYAXISRANGE/2))
-				event.data2 = 1;
+				event.x = 1;
 			else
-				event.data2 = 0;
+				event.x = 0;
 			if (axisy < -(JOYAXISRANGE/2))
-				event.data3 = -1;
+				event.y = -1;
 			else if (axisy > (JOYAXISRANGE/2))
-				event.data3 = 1;
+				event.y = 1;
 			else
-				event.data3 = 0;
+				event.y = 0;
 		}
 		else
 		{
@@ -1404,8 +1404,8 @@ void I_GetJoystick2Events(void)
 #endif
 
 			// analog control style , just send the raw data
-			event.data2 = axisx; // x axis
-			event.data3 = axisy; // y axis
+			event.x = axisx; // x axis
+			event.y = axisy; // y axis
 		}
 		D_PostEvent(&event);
 	}
@@ -1804,7 +1804,7 @@ void I_GetMouseEvents(void)
 					if (!(button & (1<<j))) //keyup
 					{
 						event.type = ev_keyup;
-						event.data1 = KEY_2MOUSE1+j;
+						event.key = KEY_2MOUSE1+j;
 						D_PostEvent(&event);
 						om2b ^= 1 << j;
 					}
@@ -1814,18 +1814,18 @@ void I_GetMouseEvents(void)
 					if (button & (1<<j))
 					{
 						event.type = ev_keydown;
-						event.data1 = KEY_2MOUSE1+j;
+						event.key = KEY_2MOUSE1+j;
 						D_PostEvent(&event);
 						om2b ^= 1 << j;
 					}
 				}
 			}
-			event.data2 = ((SINT8)mdata[1])+((SINT8)mdata[3]);
-			event.data3 = ((SINT8)mdata[2])+((SINT8)mdata[4]);
-			if (event.data2 && event.data3)
+			event.x = ((SINT8)mdata[1])+((SINT8)mdata[3]);
+			event.y = ((SINT8)mdata[2])+((SINT8)mdata[4]);
+			if (event.x && event.y)
 			{
 				event.type = ev_mouse2;
-				event.data1 = 0;
+				event.key = 0;
 				D_PostEvent(&event);
 			}
 		}
@@ -1867,7 +1867,7 @@ static void I_ShutdownMouse2(void)
 	for (i = 0; i < MOUSEBUTTONS; i++)
 	{
 		event.type = ev_keyup;
-		event.data1 = KEY_2MOUSE1+i;
+		event.key = KEY_2MOUSE1+i;
 		D_PostEvent(&event);
 	}
 
@@ -1958,7 +1958,7 @@ void I_GetMouseEvents(void)
 					event.type = ev_keydown;
 				else
 					event.type = ev_keyup;
-				event.data1 = KEY_2MOUSE1+i;
+				event.key = KEY_2MOUSE1+i;
 				D_PostEvent(&event);
 			}
 	}
@@ -1966,10 +1966,10 @@ void I_GetMouseEvents(void)
 	if (handlermouse2x != 0 || handlermouse2y != 0)
 	{
 		event.type = ev_mouse2;
-		event.data1 = 0;
-//		event.data1 = buttons; // not needed
-		event.data2 = handlermouse2x << 1;
-		event.data3 = handlermouse2y << 1;
+		event.key = 0;
+//		event.key = buttons; // not needed
+		event.x = handlermouse2x << 1;
+		event.y = handlermouse2y << 1;
 		handlermouse2x = 0;
 		handlermouse2y = 0;
 
@@ -2163,7 +2163,13 @@ precise_t I_GetPreciseTime(void)
 
 int I_PreciseToMicros(precise_t d)
 {
-	return (int)(d / (timer_frequency / 1000000.0));
+	// d is going to be converted into a double. So remove the highest bits
+	// to avoid loss of precision in the lower bits, for the (probably rare) case
+	// that the higher bits are actually used.
+	d &= ((precise_t)1 << 53) - 1; // The mantissa of a double can handle 53 bits at most.
+	// The resulting double from the calculation is converted first to UINT64 to avoid overflow,
+	// which is undefined behaviour when converting floating point values to integers.
+	return (int)(UINT64)(d / (timer_frequency / 1000000.0));
 }
 
 //
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index 819589eaff1a5d856da1cf76d5aaaf47352f8ab1..ed766ff23dfb79395bbb5648211be73c45fc34b6 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -73,6 +73,8 @@
 #include "../console.h"
 #include "../command.h"
 #include "../r_main.h"
+#include "../lua_script.h"
+#include "../lua_libs.h"
 #include "../lua_hook.h"
 #include "sdlmain.h"
 #ifdef HWRENDER
@@ -372,6 +374,8 @@ static boolean IgnoreMouse(void)
 	if (gamestate != GS_LEVEL && gamestate != GS_INTERMISSION &&
 			gamestate != GS_CONTINUING && gamestate != GS_CUTSCENE)
 		return true;
+	if (!mousegrabbedbylua)
+		return true;
 	return false;
 }
 
@@ -405,11 +409,6 @@ void I_UpdateMouseGrab(void)
 		SDLdoGrabMouse();
 }
 
-boolean I_GetMouseGrab(void)
-{
-	return (boolean)SDL_GetWindowGrab(window);
-}
-
 void I_SetMouseGrab(boolean grab)
 {
 	if (grab)
@@ -663,8 +662,9 @@ static void Impl_HandleKeyboardEvent(SDL_KeyboardEvent evt, Uint32 type)
 	{
 		return;
 	}
-	event.data1 = Impl_SDL_Scancode_To_Keycode(evt.keysym.scancode);
-	if (event.data1) D_PostEvent(&event);
+	event.key = Impl_SDL_Scancode_To_Keycode(evt.keysym.scancode);
+	event.repeated = (evt.repeat != 0);
+	if (event.key) D_PostEvent(&event);
 }
 
 static void Impl_HandleMouseMotionEvent(SDL_MouseMotionEvent evt)
@@ -742,15 +742,15 @@ static void Impl_HandleMouseButtonEvent(SDL_MouseButtonEvent evt, Uint32 type)
 		}
 		else return;
 		if (evt.button == SDL_BUTTON_MIDDLE)
-			event.data1 = KEY_MOUSE1+2;
+			event.key = KEY_MOUSE1+2;
 		else if (evt.button == SDL_BUTTON_RIGHT)
-			event.data1 = KEY_MOUSE1+1;
+			event.key = KEY_MOUSE1+1;
 		else if (evt.button == SDL_BUTTON_LEFT)
-			event.data1 = KEY_MOUSE1;
+			event.key = KEY_MOUSE1;
 		else if (evt.button == SDL_BUTTON_X1)
-			event.data1 = KEY_MOUSE1+3;
+			event.key = KEY_MOUSE1+3;
 		else if (evt.button == SDL_BUTTON_X2)
-			event.data1 = KEY_MOUSE1+4;
+			event.key = KEY_MOUSE1+4;
 		if (event.type == ev_keyup || event.type == ev_keydown)
 		{
 			D_PostEvent(&event);
@@ -766,17 +766,17 @@ static void Impl_HandleMouseWheelEvent(SDL_MouseWheelEvent evt)
 
 	if (evt.y > 0)
 	{
-		event.data1 = KEY_MOUSEWHEELUP;
+		event.key = KEY_MOUSEWHEELUP;
 		event.type = ev_keydown;
 	}
 	if (evt.y < 0)
 	{
-		event.data1 = KEY_MOUSEWHEELDOWN;
+		event.key = KEY_MOUSEWHEELDOWN;
 		event.type = ev_keydown;
 	}
 	if (evt.y == 0)
 	{
-		event.data1 = 0;
+		event.key = 0;
 		event.type = ev_keyup;
 	}
 	if (event.type == ev_keyup || event.type == ev_keydown)
@@ -795,7 +795,7 @@ static void Impl_HandleJoystickAxisEvent(SDL_JoyAxisEvent evt)
 	joyid[1] = SDL_JoystickInstanceID(JoyInfo2.dev);
 
 	evt.axis++;
-	event.data1 = event.data2 = event.data3 = INT32_MAX;
+	event.key = event.x = event.y = INT32_MAX;
 
 	if (evt.which == joyid[0])
 	{
@@ -812,14 +812,14 @@ static void Impl_HandleJoystickAxisEvent(SDL_JoyAxisEvent evt)
 	//vaule
 	if (evt.axis%2)
 	{
-		event.data1 = evt.axis / 2;
-		event.data2 = SDLJoyAxis(evt.value, event.type);
+		event.key = evt.axis / 2;
+		event.x = SDLJoyAxis(evt.value, event.type);
 	}
 	else
 	{
 		evt.axis--;
-		event.data1 = evt.axis / 2;
-		event.data3 = SDLJoyAxis(evt.value, event.type);
+		event.key = evt.axis / 2;
+		event.y = SDLJoyAxis(evt.value, event.type);
 	}
 	D_PostEvent(&event);
 }
@@ -839,11 +839,11 @@ static void Impl_HandleJoystickHatEvent(SDL_JoyHatEvent evt)
 
 	if (evt.which == joyid[0])
 	{
-		event.data1 = KEY_HAT1 + (evt.hat*4);
+		event.key = KEY_HAT1 + (evt.hat*4);
 	}
 	else if (evt.which == joyid[1])
 	{
-		event.data1 = KEY_2HAT1 + (evt.hat*4);
+		event.key = KEY_2HAT1 + (evt.hat*4);
 	}
 	else return;
 
@@ -862,11 +862,11 @@ static void Impl_HandleJoystickButtonEvent(SDL_JoyButtonEvent evt, Uint32 type)
 
 	if (evt.which == joyid[0])
 	{
-		event.data1 = KEY_JOY1;
+		event.key = KEY_JOY1;
 	}
 	else if (evt.which == joyid[1])
 	{
-		event.data1 = KEY_2JOY1;
+		event.key = KEY_2JOY1;
 	}
 	else return;
 	if (type == SDL_JOYBUTTONUP)
@@ -880,7 +880,7 @@ static void Impl_HandleJoystickButtonEvent(SDL_JoyButtonEvent evt, Uint32 type)
 	else return;
 	if (evt.button < JOYBUTTONS)
 	{
-		event.data1 += evt.button;
+		event.key += evt.button;
 	}
 	else return;
 
@@ -1084,9 +1084,9 @@ void I_GetEvent(void)
 		SDL_GetWindowSize(window, &wwidth, &wheight);
 		//SDL_memset(&event, 0, sizeof(event_t));
 		event.type = ev_mouse;
-		event.data1 = 0;
-		event.data2 = (INT32)lround(mousemovex * ((float)wwidth / (float)realwidth));
-		event.data3 = (INT32)lround(mousemovey * ((float)wheight / (float)realheight));
+		event.key = 0;
+		event.x = (INT32)lround(mousemovex * ((float)wwidth / (float)realwidth));
+		event.y = (INT32)lround(mousemovey * ((float)wheight / (float)realheight));
 		D_PostEvent(&event);
 	}
 
diff --git a/src/sdl/mixer_sound.c b/src/sdl/mixer_sound.c
index 2f1a872669443c033d882940fafd43689518265e..35a79acc0b16ece0a457018db2df9d7a94b2781b 100644
--- a/src/sdl/mixer_sound.c
+++ b/src/sdl/mixer_sound.c
@@ -9,7 +9,7 @@
 /// \file
 /// \brief SDL Mixer interface for sound
 
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 #ifdef HAVE_ZLIB
 #ifndef _MSC_VER
 #ifndef _LARGEFILE64_SOURCE
@@ -27,7 +27,7 @@
 
 #include <zlib.h>
 #endif // HAVE_ZLIB
-#endif // HAVE_LIBGME
+#endif // HAVE_GME
 
 #include "../doomdef.h"
 #include "../doomstat.h" // menuactive
@@ -73,11 +73,11 @@
 #define MUS_MODPLUG MUS_MODPLUG_UNUSED
 #endif
 
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 #include "gme/gme.h"
 #define GME_TREBLE 5.0f
 #define GME_BASS 1.0f
-#endif // HAVE_LIBGME
+#endif // HAVE_GME
 
 static UINT16 BUFFERSIZE = 2048;
 static UINT16 SAMPLERATE = 44100;
@@ -110,7 +110,7 @@ static INT32 fading_id;
 static void (*fading_callback)(void);
 static boolean fading_nocleanup;
 
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 static Music_Emu *gme;
 static UINT16 current_track;
 #endif
@@ -220,7 +220,7 @@ static void var_cleanup(void)
 	internal_volume = 100;
 }
 
-#if defined (HAVE_LIBGME) && defined (HAVE_ZLIB)
+#if defined (HAVE_GME) && defined (HAVE_ZLIB)
 static const char* get_zlib_error(int zErr)
 {
 	switch (zErr)
@@ -318,7 +318,7 @@ void I_ShutdownSound(void)
 
 	SDL_QuitSubSystem(SDL_INIT_AUDIO);
 
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	if (gme)
 		gme_delete(gme);
 #endif
@@ -453,7 +453,7 @@ void *I_GetSfx(sfxinfo_t *sfx)
 	void *lump;
 	Mix_Chunk *chunk;
 	SDL_RWops *rw;
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	Music_Emu *emu;
 	gme_info_t *info;
 #endif
@@ -473,7 +473,7 @@ void *I_GetSfx(sfxinfo_t *sfx)
 	}
 
 	// Not a doom sound? Try something else.
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	// VGZ format
 	if (((UINT8 *)lump)[0] == 0x1F
 		&& ((UINT8 *)lump)[1] == 0x8B)
@@ -729,7 +729,7 @@ static UINT32 music_fade(UINT32 interval, void *param)
 	}
 }
 
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 static void mix_gme(void *udata, Uint8 *stream, int len)
 {
 	int i;
@@ -797,7 +797,7 @@ void I_ShutdownMusic(void)
 
 musictype_t I_SongType(void)
 {
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	if (gme)
 		return MU_GME;
 	else
@@ -828,7 +828,7 @@ musictype_t I_SongType(void)
 boolean I_SongPlaying(void)
 {
 	return (
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 		(I_SongType() == MU_GME && gme) ||
 #endif
 #ifdef HAVE_OPENMPT
@@ -851,7 +851,7 @@ boolean I_SetSongSpeed(float speed)
 {
 	if (speed > 250.0f)
 		speed = 250.0f; //limit speed up to 250x
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	if (gme)
 	{
 		SDL_LockAudio();
@@ -893,7 +893,7 @@ UINT32 I_GetSongLength(void)
 {
 	INT32 length;
 
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	if (gme)
 	{
 		gme_info_t *info;
@@ -963,7 +963,7 @@ boolean I_SetSongLoopPoint(UINT32 looppoint)
 
 UINT32 I_GetSongLoopPoint(void)
 {
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	if (gme)
 	{
 		INT32 looppoint;
@@ -992,7 +992,7 @@ UINT32 I_GetSongLoopPoint(void)
 boolean I_SetSongPosition(UINT32 position)
 {
 	UINT32 length;
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	if (gme)
 	{
 		// this is unstable, so fail silently
@@ -1055,7 +1055,7 @@ boolean I_SetSongPosition(UINT32 position)
 
 UINT32 I_GetSongPosition(void)
 {
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	if (gme)
 	{
 		INT32 position = gme_tell(gme);
@@ -1124,7 +1124,7 @@ boolean I_LoadSong(char *data, size_t len)
 	SDL_RWops *rw;
 
 	if (music
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 		|| gme
 #endif
 #ifdef HAVE_OPENMPT
@@ -1136,7 +1136,7 @@ boolean I_LoadSong(char *data, size_t len)
 	// always do this whether or not a music already exists
 	var_cleanup();
 
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	if ((UINT8)data[0] == 0x1F
 		&& (UINT8)data[1] == 0x8B)
 	{
@@ -1271,7 +1271,7 @@ void I_UnloadSong(void)
 {
 	I_StopSong();
 
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	if (gme)
 	{
 		gme_delete(gme);
@@ -1294,7 +1294,7 @@ void I_UnloadSong(void)
 
 boolean I_PlaySong(boolean looping)
 {
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	if (gme)
 	{
 		gme_equalizer_t eq = {GME_TREBLE, GME_BASS, 0,0,0,0,0,0,0,0};
@@ -1360,7 +1360,7 @@ void I_StopSong(void)
 	if (!fading_nocleanup)
 		I_StopFadingSong();
 
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	if (gme)
 	{
 		Mix_HookMusic(NULL, NULL);
@@ -1433,7 +1433,7 @@ void I_SetMusicVolume(UINT8 volume)
 
 boolean I_SetSongTrack(int track)
 {
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	// If the specified track is within the number of tracks playing, then change it
 	if (gme)
 	{
diff --git a/src/st_stuff.c b/src/st_stuff.c
index af14118e36cb87bb0475982345bba8bd1943306f..a328d669e51169ba18d8ae8d046774d3b298eed3 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -43,6 +43,7 @@
 #endif
 
 #include "lua_hud.h"
+#include "lua_hook.h"
 
 UINT16 objectsdrawn = 0;
 
@@ -1391,7 +1392,7 @@ void ST_drawTitleCard(void)
 	lt_lasttic = lt_ticker;
 
 luahook:
-	LUAh_TitleCardHUD(stplyr);
+	LUA_HUDHOOK(titlecard);
 }
 
 //
@@ -2036,9 +2037,8 @@ static void ST_drawNiGHTSHUD(void)
 		else
 			numbersize = 48/2;
 
-		if ((oldspecialstage && leveltime & 2)
-			&& (stplyr->mo->eflags & (MFE_TOUCHWATER|MFE_UNDERWATER))
-			&& !(stplyr->powers[pw_shield] & SH_PROTECTWATER))
+		if ((oldspecialstage && leveltime & 2) &&
+			(stplyr->mo->eflags & (MFE_TOUCHWATER|MFE_UNDERWATER) && !(stplyr->powers[pw_shield] & ((stplyr->mo->eflags & MFE_TOUCHLAVA) ? SH_PROTECTFIRE : SH_PROTECTWATER))))
 			col = SKINCOLOR_ORANGE;
 
 		ST_DrawNightsOverlayNum((160 + numbersize)<<FRACBITS, 14<<FRACBITS, FRACUNIT, V_PERPLAYER|V_SNAPTOTOP, realnightstime, nightsnum, col);
@@ -2187,7 +2187,7 @@ static void ST_drawMatchHUD(void)
 		{
 			sprintf(penaltystr, "-%d", stplyr->ammoremoval);
 			V_DrawString(offset + 8 + stplyr->ammoremovalweapon * 20, y,
-				V_REDMAP|V_SNAPTOBOTTOM, penaltystr);
+				V_REDMAP|V_SNAPTOBOTTOM|V_PERPLAYER, penaltystr);
 		}
 
 	}
@@ -2732,7 +2732,7 @@ static void ST_overlayDrawer(void)
 		ST_drawPowerupHUD(); // same as it ever was...
 
 	if (!(netgame || multiplayer) || !hu_showscores)
-		LUAh_GameHUD(stplyr);
+		LUA_HUDHOOK(game);
 
 	// draw level title Tails
 	if (stagetitle && (!WipeInAction) && (!WipeStageTitle))
diff --git a/src/v_video.c b/src/v_video.c
index de05df2d5df2bacb4d6d9b2d11c5c221fa9ab8fe..c3993854403fe87db28c06e18705a69f01932a84 100644
--- a/src/v_video.c
+++ b/src/v_video.c
@@ -809,13 +809,13 @@ void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vsca
 }
 
 // Draws a patch cropped and scaled to arbitrary size.
-void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_t *patch, fixed_t sx, fixed_t sy, fixed_t w, fixed_t h)
+void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vscale, INT32 scrn, patch_t *patch, const UINT8 *colormap, fixed_t sx, fixed_t sy, fixed_t w, fixed_t h)
 {
 	UINT8 (*patchdrawfunc)(const UINT8*, const UINT8*, fixed_t);
 	UINT32 alphalevel = 0;
 	// boolean flip = false;
 
-	fixed_t col, ofs, colfrac, rowfrac, fdup;
+	fixed_t col, ofs, colfrac, rowfrac, fdup, vdup;
 	INT32 dupx, dupy;
 	const column_t *column;
 	UINT8 *desttop, *dest;
@@ -830,7 +830,7 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_
 	//if (rendermode != render_soft && !con_startup)		// Not this again
 	if (rendermode == render_opengl)
 	{
-		HWR_DrawCroppedPatch(patch,x,y,pscale,scrn,sx,sy,w,h);
+		HWR_DrawCroppedPatch(patch,x,y,pscale,vscale,scrn,colormap,sx,sy,w,h);
 		return;
 	}
 #endif
@@ -857,31 +857,56 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_
 		}
 	}
 
+	v_colormap = NULL;
+	if (colormap)
+	{
+		v_colormap = colormap;
+		patchdrawfunc = (v_translevel) ? transmappedpdraw : mappedpdraw;
+	}
+
+	dupx = vid.dupx;
+	dupy = vid.dupy;
+	if (scrn & V_SCALEPATCHMASK) switch ((scrn & V_SCALEPATCHMASK) >> V_SCALEPATCHSHIFT)
+	{
+		case 1: // V_NOSCALEPATCH
+			dupx = dupy = 1;
+			break;
+		case 2: // V_SMALLSCALEPATCH
+			dupx = vid.smalldupx;
+			dupy = vid.smalldupy;
+			break;
+		case 3: // V_MEDSCALEPATCH
+			dupx = vid.meddupx;
+			dupy = vid.meddupy;
+			break;
+		default:
+			break;
+	}
+
 	// only use one dup, to avoid stretching (har har)
-	dupx = dupy = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy);
-	fdup = FixedMul(dupx<<FRACBITS, pscale);
+	dupx = dupy = (dupx < dupy ? dupx : dupy);
+	fdup = vdup = FixedMul(dupx<<FRACBITS, pscale);
+	if (vscale != pscale)
+		vdup = FixedMul(dupx<<FRACBITS, vscale);
 	colfrac = FixedDiv(FRACUNIT, fdup);
-	rowfrac = FixedDiv(FRACUNIT, fdup);
+	rowfrac = FixedDiv(FRACUNIT, vdup);
 
-	y -= FixedMul(patch->topoffset<<FRACBITS, pscale);
 	x -= FixedMul(patch->leftoffset<<FRACBITS, pscale);
+	y -= FixedMul(patch->topoffset<<FRACBITS, vscale);
 
 	if (splitscreen && (scrn & V_PERPLAYER))
 	{
 		fixed_t adjusty = ((scrn & V_NOSCALESTART) ? vid.height : BASEVIDHEIGHT)<<(FRACBITS-1);
-		fdup >>= 1;
+		vdup >>= 1;
 		rowfrac <<= 1;
 		y >>= 1;
-		sy >>= 1;
-		h >>= 1;
 #ifdef QUADS
 		if (splitscreen > 1) // 3 or 4 players
 		{
 			fixed_t adjustx = ((scrn & V_NOSCALESTART) ? vid.height : BASEVIDHEIGHT)<<(FRACBITS-1));
+			fdup >>= 1;
 			colfrac <<= 1;
 			x >>= 1;
-			sx >>= 1;
-			w >>= 1;
 			if (stplyr == &players[displayplayer])
 			{
 				if (!(scrn & (V_SNAPTOTOP|V_SNAPTOBOTTOM)))
@@ -897,7 +922,6 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_
 				if (!(scrn & (V_SNAPTOLEFT|V_SNAPTORIGHT)))
 					perplayershuffle |= 8;
 				x += adjustx;
-				sx += adjustx;
 				scrn &= ~V_SNAPTOBOTTOM|V_SNAPTOLEFT;
 			}
 			else if (stplyr == &players[thirddisplayplayer])
@@ -907,7 +931,6 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_
 				if (!(scrn & (V_SNAPTOLEFT|V_SNAPTORIGHT)))
 					perplayershuffle |= 4;
 				y += adjusty;
-				sy += adjusty;
 				scrn &= ~V_SNAPTOTOP|V_SNAPTORIGHT;
 			}
 			else //if (stplyr == &players[fourthdisplayplayer])
@@ -917,9 +940,7 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_
 				if (!(scrn & (V_SNAPTOLEFT|V_SNAPTORIGHT)))
 					perplayershuffle |= 8;
 				x += adjustx;
-				sx += adjustx;
 				y += adjusty;
-				sy += adjusty;
 				scrn &= ~V_SNAPTOTOP|V_SNAPTOLEFT;
 			}
 		}
@@ -938,7 +959,6 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_
 				if (!(scrn & (V_SNAPTOTOP|V_SNAPTOBOTTOM)))
 					perplayershuffle |= 2;
 				y += adjusty;
-				sy += adjusty;
 				scrn &= ~V_SNAPTOTOP;
 			}
 		}
@@ -951,7 +971,8 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_
 
 	deststop = desttop + vid.rowbytes * vid.height;
 
-	if (scrn & V_NOSCALESTART) {
+	if (scrn & V_NOSCALESTART)
+	{
 		x >>= FRACBITS;
 		y >>= FRACBITS;
 		desttop += (y*vid.width) + x;
@@ -999,7 +1020,38 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_
 		desttop += (y*vid.width) + x;
 	}
 
-	for (col = sx<<FRACBITS; (col>>FRACBITS) < patch->width && ((col>>FRACBITS) - sx) < w; col += colfrac, ++x, desttop++)
+	// Auto-crop at splitscreen borders!
+	if (splitscreen && (scrn & V_PERPLAYER))
+	{
+#ifdef QUADS
+		if (splitscreen > 1) // 3 or 4 players
+		{
+			#error Auto-cropping doesnt take quadscreen into account! Fix it!
+			// Hint: For player 1/2, copy player 1's code below. For player 3/4, copy player 2's code below
+			// For player 1/3 and 2/4, hijack the X wrap prevention lines? That's probably easiest
+		}
+		else
+#endif
+		// 2 players
+		{
+			if (stplyr == &players[displayplayer]) // Player 1's screen, crop at the bottom
+			{
+				// Just put a big old stop sign halfway through the screen
+				deststop -= vid.rowbytes * (vid.height>>1);
+			}
+			else //if (stplyr == &players[secondarydisplayplayer]) // Player 2's screen, crop at the top
+			{
+				if (y < (vid.height>>1)) // If the top is above the border
+				{
+					sy += ((vid.height>>1) - y) * rowfrac; // Start further down on the patch
+					h -= ((vid.height>>1) - y) * rowfrac; // Draw less downwards from the start
+					desttop += ((vid.height>>1) - y) * vid.width; // Start drawing at the border
+				}
+			}
+		}
+	}
+
+	for (col = sx; (col>>FRACBITS) < patch->width && (col - sx) < w; col += colfrac, ++x, desttop++)
 	{
 		INT32 topdelta, prevdelta = -1;
 		if (x < 0) // don't draw off the left of the screen (WRAP PREVENTION)
@@ -1016,15 +1068,15 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_
 			prevdelta = topdelta;
 			source = (const UINT8 *)(column) + 3;
 			dest = desttop;
-			if (topdelta-sy > 0)
+			if ((topdelta<<FRACBITS)-sy > 0)
 			{
-				dest += FixedInt(FixedMul((topdelta-sy)<<FRACBITS,fdup))*vid.width;
+				dest += FixedInt(FixedMul((topdelta<<FRACBITS)-sy,vdup))*vid.width;
 				ofs = 0;
 			}
 			else
-				ofs = (sy-topdelta)<<FRACBITS;
+				ofs = sy-(topdelta<<FRACBITS);
 
-			for (; dest < deststop && (ofs>>FRACBITS) < column->length && (((ofs>>FRACBITS) - sy) + topdelta) < h; ofs += rowfrac)
+			for (; dest < deststop && (ofs>>FRACBITS) < column->length && ((ofs - sy) + (topdelta<<FRACBITS)) < h; ofs += rowfrac)
 			{
 				if (dest >= screens[scrn&V_PARAMMASK]) // don't draw off the top of the screen (CRASH PREVENTION)
 					*dest = patchdrawfunc(dest, source, ofs);
diff --git a/src/v_video.h b/src/v_video.h
index 7184e799e5ff981a48db6694019bb1b62b01b6f2..c10ab22cea8f56497f998aa449fb672b59f62f84 100644
--- a/src/v_video.h
+++ b/src/v_video.h
@@ -165,7 +165,7 @@ void V_CubeApply(UINT8 *red, UINT8 *green, UINT8 *blue);
 #define V_DrawSciencePatch(x,y,s,p,sc) V_DrawFixedPatch(x,y,sc,s,p,NULL)
 #define V_DrawFixedPatch(x,y,sc,s,p,c) V_DrawStretchyFixedPatch(x,y,sc,sc,s,p,c)
 void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vscale, INT32 scrn, patch_t *patch, const UINT8 *colormap);
-void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, INT32 scrn, patch_t *patch, fixed_t sx, fixed_t sy, fixed_t w, fixed_t h);
+void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vscale, INT32 scrn, patch_t *patch, const UINT8 *colormap, fixed_t sx, fixed_t sy, fixed_t w, fixed_t h);
 
 void V_DrawContinueIcon(INT32 x, INT32 y, INT32 flags, INT32 skinnum, UINT16 skincolor);
 
diff --git a/src/version.h b/src/version.h
index 4470fbd6eaf02397d02f890daf0412822ea70cd1..28fc71c36fca9dceea7f7aecc31f6436028b8d84 100644
--- a/src/version.h
+++ b/src/version.h
@@ -1,6 +1,6 @@
 #define SRB2VERSION "2.2.9"/* this must be the first line, for cmake !! */
 
-// The Modification ID; must be obtained from a Master Server Admin ( https://mb.srb2.org/showgroups.php ).
+// The Modification ID; must be obtained from a Master Server Admin ( https://mb.srb2.org/members/?key=ms_admin ).
 // DO NOT try to set this otherwise, or your modification will be unplayable through the Master Server.
 // "18" is the default mod ID for version 2.2
 #define MODID 18
diff --git a/src/w_wad.c b/src/w_wad.c
index cbff5c67be89de92af1f858ea32d2b7dfb55c688..e49e0ce82f9ffe24c06d757b61b2a69bbc10ee2a 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -50,16 +50,17 @@
 
 #include "filesrch.h"
 
-#include "i_video.h" // rendermode
+#include "d_main.h"
 #include "d_netfil.h"
-#include "dehacked.h"
 #include "d_clisrv.h"
+#include "dehacked.h"
 #include "r_defs.h"
 #include "r_data.h"
 #include "r_textures.h"
 #include "r_patch.h"
 #include "r_picformats.h"
 #include "i_system.h"
+#include "i_video.h" // rendermode
 #include "md5.h"
 #include "lua_script.h"
 #ifdef SCANTHINGS
@@ -104,7 +105,7 @@ static UINT16 lumpnumcacheindex = 0;
 //                                                                    GLOBALS
 //===========================================================================
 UINT16 numwadfiles; // number of active wadfiles
-wadfile_t *wadfiles[MAX_WADFILES]; // 0 to numwadfiles-1 are valid
+wadfile_t **wadfiles; // 0 to numwadfiles-1 are valid
 
 // W_Shutdown
 // Closes all of the WAD files before quitting
@@ -117,10 +118,15 @@ void W_Shutdown(void)
 	{
 		wadfile_t *wad = wadfiles[numwadfiles];
 
-		fclose(wad->handle);
+		if (wad->handle)
+			fclose(wad->handle);
 		Z_Free(wad->filename);
+		if (wad->path)
+			Z_Free(wad->path);
 		while (wad->numlumps--)
 		{
+			if (wad->lumpinfo[wad->numlumps].diskpath)
+				Z_Free(wad->lumpinfo[wad->numlumps].diskpath);
 			Z_Free(wad->lumpinfo[wad->numlumps].longname);
 			Z_Free(wad->lumpinfo[wad->numlumps].fullname);
 		}
@@ -128,6 +134,8 @@ void W_Shutdown(void)
 		Z_Free(wad->lumpinfo);
 		Z_Free(wad);
 	}
+
+	Z_Free(wadfiles);
 }
 
 //===========================================================================
@@ -421,6 +429,7 @@ static lumpinfo_t* ResGetLumpsWad (FILE* handle, UINT16* nlmp, const char* filen
 	{
 		lump_p->position = LONG(fileinfo->filepos);
 		lump_p->size = lump_p->disksize = LONG(fileinfo->size);
+		lump_p->diskpath = NULL;
 		if (compressed) // wad is compressed, lump might be
 		{
 			UINT32 realsize = 0;
@@ -602,6 +611,7 @@ static lumpinfo_t* ResGetLumpsZip (FILE* handle, UINT16* nlmp)
 
 		lump_p->position = zentry.offset; // NOT ACCURATE YET: we still need to read the local entry to find our true position
 		lump_p->disksize = zentry.compsize;
+		lump_p->diskpath = NULL;
 		lump_p->size = zentry.size;
 
 		fullname = malloc(zentry.namelen + 1);
@@ -679,6 +689,114 @@ static lumpinfo_t* ResGetLumpsZip (FILE* handle, UINT16* nlmp)
 	return lumpinfo;
 }
 
+static INT32 CheckPathsNotEqual(const char *path1, const char *path2)
+{
+	INT32 stat = samepaths(path1, path2);
+
+	if (stat == 1)
+		return 0;
+	else if (stat < 0)
+		return -1;
+
+	return 1;
+}
+
+// Returns 1 if the path is valid, 0 if not, and -1 if there was an error.
+INT32 W_IsPathToFolderValid(const char *path)
+{
+	INT32 stat;
+
+	// Remove path delimiters.
+	const char *p = path + (strlen(path) - 1);
+	while (*p == '\\' || *p == '/' || *p == ':')
+	{
+		p--;
+		if (p < path)
+			return 0;
+	}
+
+	// Check if the path is a directory.
+	stat = pathisdirectory(path);
+	if (stat == 0)
+		return 0;
+	else if (stat < 0)
+	{
+		// The path doesn't exist, so it can't be a directory.
+		if (direrror == ENOENT)
+			return 0;
+
+		return -1;
+	}
+
+	// Don't add your home, you sodding tic tac.
+	stat = CheckPathsNotEqual(path, srb2home);
+	if (stat != 1)
+		return stat;
+
+	// Do the same checks for SRB2's path, and the current directory.
+	stat = CheckPathsNotEqual(path, srb2path);
+	if (stat != 1)
+		return stat;
+
+	stat = CheckPathsNotEqual(path, ".");
+	if (stat != 1)
+		return stat;
+
+	return 1;
+}
+
+// Checks if the combination of the first path and the second path are valid.
+// If they are, the concatenated path is returned.
+static char *CheckConcatFolderPath(const char *startpath, const char *path)
+{
+	if (concatpaths(path, startpath) == 1)
+	{
+		char *fn;
+
+		if (startpath)
+		{
+			size_t len = strlen(startpath) + strlen(path) + strlen(PATHSEP) + 1;
+			fn = ZZ_Alloc(len);
+			snprintf(fn, len, "%s" PATHSEP "%s", startpath, path);
+		}
+		else
+			fn = Z_StrDup(path);
+
+		return fn;
+	}
+
+	return NULL;
+}
+
+// Looks for the first valid full path for a folder.
+// Returns NULL if the folder doesn't exist, or it isn't valid.
+char *W_GetFullFolderPath(const char *path)
+{
+	// Check the path by itself first.
+	char *fn = CheckConcatFolderPath(NULL, path);
+	if (fn)
+		return fn;
+
+#define checkpath(startpath) \
+	fn = CheckConcatFolderPath(startpath, path); \
+	if (fn) \
+		return fn
+
+	checkpath(srb2home); // Then, look in srb2home.
+	checkpath(srb2path); // Now, look in srb2path.
+	checkpath("."); // Finally, look in the current directory.
+
+#undef checkpath
+
+	return NULL;
+}
+
+// Loads files from a folder into a lumpinfo structure.
+static lumpinfo_t *ResGetLumpsFolder(const char *path, UINT16 *nlmp, UINT16 *nfolders)
+{
+	return getdirectoryfiles(path, nlmp, nfolders);
+}
+
 static UINT16 W_InitFileError (const char *filename, boolean exitworthy)
 {
 	if (exitworthy)
@@ -694,6 +812,19 @@ static UINT16 W_InitFileError (const char *filename, boolean exitworthy)
 	return INT16_MAX;
 }
 
+static void W_ReadFileShaders(wadfile_t *wadfile)
+{
+#ifdef HWRENDER
+	if (rendermode == render_opengl && (vid.glstate == VID_GL_LIBRARY_LOADED))
+	{
+		HWR_LoadCustomShadersFromFile(numwadfiles - 1, W_FileHasFolders(wadfile));
+		HWR_CompileShaders();
+	}
+#else
+	(void)wadfile;
+#endif
+}
+
 //  Allocate a wadfile, setup the lumpinfo (directory) and
 //  lumpcache, add the wadfile to the current active wadfiles
 //
@@ -715,7 +846,6 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 #ifndef NOMD5
 	size_t i;
 #endif
-	size_t packetsize;
 	UINT8 md5sum[16];
 	int important;
 
@@ -733,9 +863,8 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 		refreshdirname = NULL;
 
 	//CONS_Debug(DBG_SETUP, "Loading %s\n", filename);
-	//
-	// check if limit of active wadfiles
-	//
+
+	// Check if the game reached the limit of active wadfiles.
 	if (numwadfiles >= MAX_WADFILES)
 	{
 		CONS_Alert(CONS_ERROR, M_GetText("Maximum wad files reached\n"));
@@ -755,24 +884,7 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 		return INT16_MAX;
 	}
 
-	// Check if wad files will overflow fileneededbuffer. Only the filename part
-	// is send in the packet; cf.
-	// see PutFileNeeded in d_netfil.c
-	if ((important = !important))
-	{
-		packetsize = packetsizetally + nameonlylength(filename) + 22;
-
-		if (packetsize > MAXFILENEEDED*sizeof(UINT8))
-		{
-			CONS_Alert(CONS_ERROR, M_GetText("Maximum wad files reached\n"));
-			refreshdirmenu |= REFRESHDIR_MAX;
-			if (handle)
-				fclose(handle);
-			return W_InitFileError(filename, startup);
-		}
-
-		packetsizetally = packetsize;
-	}
+	important = !important;
 
 #ifndef NOMD5
 	//
@@ -784,11 +896,12 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 
 	for (i = 0; i < numwadfiles; i++)
 	{
+		if (wadfiles[i]->type == RET_FOLDER)
+			continue;
+
 		if (!memcmp(wadfiles[i]->md5sum, md5sum, 16))
 		{
 			CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), filename);
-			if (important)
-				packetsizetally -= nameonlylength(filename) + 22;
 			if (handle)
 				fclose(handle);
 			return W_InitFileError(filename, false);
@@ -831,9 +944,11 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	//
 	wadfile = Z_Malloc(sizeof (*wadfile), PU_STATIC, NULL);
 	wadfile->filename = Z_StrDup(filename);
+	wadfile->path = NULL;
 	wadfile->type = type;
 	wadfile->handle = handle;
-	wadfile->numlumps = (UINT16)numlumps;
+	wadfile->numlumps = numlumps;
+	wadfile->foldercount = 0;
 	wadfile->lumpinfo = lumpinfo;
 	wadfile->important = important;
 	fseek(handle, 0, SEEK_END);
@@ -853,17 +968,12 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	// add the wadfile
 	//
 	CONS_Printf(M_GetText("Added file %s (%u lumps)\n"), filename, numlumps);
+	wadfiles = Z_Realloc(wadfiles, sizeof(wadfile_t) * (numwadfiles + 1), PU_STATIC, NULL);
 	wadfiles[numwadfiles] = wadfile;
 	numwadfiles++; // must come BEFORE W_LoadDehackedLumps, so any addfile called by COM_BufInsertText called by Lua doesn't overwrite what we just loaded
 
-#ifdef HWRENDER
 	// Read shaders from file
-	if (rendermode == render_opengl && (vid.glstate == VID_GL_LIBRARY_LOADED))
-	{
-		HWR_LoadCustomShadersFromFile(numwadfiles - 1, (type == RET_PK3));
-		HWR_CompileShaders();
-	}
-#endif // HWRENDER
+	W_ReadFileShaders(wadfile);
 
 	// TODO: HACK ALERT - Load Lua & SOC stuff right here. I feel like this should be out of this place, but... Let's stick with this for now.
 	switch (wadfile->type)
@@ -889,6 +999,163 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	return wadfile->numlumps;
 }
 
+//
+// Loads a folder as a WAD.
+//
+UINT16 W_InitFolder(const char *path, boolean mainfile, boolean startup)
+{
+	lumpinfo_t *lumpinfo = NULL;
+	wadfile_t *wadfile;
+	UINT16 numlumps = 0;
+	UINT16 foldercount;
+	size_t i;
+	char *fn, *fullpath;
+	const char *p;
+	int important;
+	INT32 stat;
+
+	if (!(refreshdirmenu & REFRESHDIR_ADDFILE))
+		refreshdirmenu = REFRESHDIR_NORMAL|REFRESHDIR_ADDFILE; // clean out cons_alerts that happened earlier
+
+	if (refreshdirname)
+		Z_Free(refreshdirname);
+	if (dirmenu)
+		refreshdirname = Z_StrDup(path);
+	else
+		refreshdirname = NULL;
+
+	if (numwadfiles >= MAX_WADFILES)
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("Maximum wad files reached\n"));
+		refreshdirmenu |= REFRESHDIR_MAX;
+		return W_InitFileError(path, startup);
+	}
+
+	important = 0; /// \todo Implement a W_VerifyFolder.
+
+	// Remove path delimiters.
+	p = path + (strlen(path) - 1);
+
+	while (*p == '\\' || *p == '/' || *p == ':')
+	{
+		p--;
+		if (p < path)
+		{
+			CONS_Alert(CONS_ERROR, M_GetText("Path %s is invalid\n"), path);
+			return W_InitFileError(path, startup);
+		}
+	}
+	p++;
+
+	// Allocate the new path name.
+	i = (p - path) + 1;
+	fn = ZZ_Alloc(i);
+	strlcpy(fn, path, i);
+
+	// Don't add an empty path.
+	if (M_IsStringEmpty(fn))
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("Folder name is empty\n"));
+		Z_Free(fn);
+
+		if (startup)
+			return W_InitFileError("A folder", true);
+		else
+			return W_InitFileError("a folder", false);
+	}
+
+	// Check if the path is valid.
+	stat = W_IsPathToFolderValid(fn);
+
+	if (stat != 1)
+	{
+		if (stat == 0)
+			CONS_Alert(CONS_ERROR, M_GetText("Path %s is invalid\n"), fn);
+		else if (stat < 0)
+		{
+#ifndef AVOID_ERRNO
+			CONS_Alert(CONS_ERROR, M_GetText("Could not stat %s: %s\n"), fn, strerror(direrror));
+#else
+			CONS_Alert(CONS_ERROR, M_GetText("Could not stat %s\n"), fn);
+#endif
+		}
+
+		Z_Free(fn);
+		return W_InitFileError(path, startup);
+	}
+
+	// Get the full path for this folder.
+	fullpath = W_GetFullFolderPath(fn);
+	if (fullpath == NULL)
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("Path %s is invalid\n"), fn);
+		Z_Free(fn);
+		return W_InitFileError(path, startup);
+	}
+
+	// Check if the folder is already added.
+	for (i = 0; i < numwadfiles; i++)
+	{
+		if (wadfiles[i]->type != RET_FOLDER)
+			continue;
+
+		if (samepaths(wadfiles[i]->path, fullpath) > 0)
+		{
+			CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), path);
+			Z_Free(fn);
+			Z_Free(fullpath);
+			return W_InitFileError(path, false);
+		}
+	}
+
+	lumpinfo = ResGetLumpsFolder(fullpath, &numlumps, &foldercount);
+
+	if (lumpinfo == NULL)
+	{
+		if (!numlumps)
+			CONS_Alert(CONS_ERROR, M_GetText("Folder %s is empty\n"), path);
+		else if (numlumps == UINT16_MAX)
+			CONS_Alert(CONS_ERROR, M_GetText("Folder %s contains too many files\n"), path);
+		else
+			CONS_Alert(CONS_ERROR, M_GetText("Unknown error enumerating files from folder %s\n"), path);
+
+		Z_Free(fn);
+		Z_Free(fullpath);
+
+		return W_InitFileError(path, startup);
+	}
+
+	if (important && !mainfile)
+		G_SetGameModified(true);
+
+	wadfile = Z_Malloc(sizeof (*wadfile), PU_STATIC, NULL);
+	wadfile->filename = fn;
+	wadfile->path = fullpath;
+	wadfile->type = RET_FOLDER;
+	wadfile->handle = NULL;
+	wadfile->numlumps = numlumps;
+	wadfile->foldercount = foldercount;
+	wadfile->lumpinfo = lumpinfo;
+	wadfile->important = important;
+
+	// Irrelevant.
+	wadfile->filesize = 0;
+	memset(wadfile->md5sum, 0x00, 16);
+
+	Z_Calloc(numlumps * sizeof (*wadfile->lumpcache), PU_STATIC, &wadfile->lumpcache);
+	Z_Calloc(numlumps * sizeof (*wadfile->patchcache), PU_STATIC, &wadfile->patchcache);
+
+	CONS_Printf(M_GetText("Added folder %s (%u files, %u folders)\n"), fn, numlumps, foldercount);
+	wadfiles[numwadfiles] = wadfile;
+	numwadfiles++;
+
+	W_ReadFileShaders(wadfile);
+	W_LoadDehackedLumpsPK3(numwadfiles - 1, mainfile);
+	W_InvalidateLumpnumCache();
+
+	return wadfile->numlumps;
+}
+
 /** Tries to load a series of files.
   * All files are wads unless they have an extension of ".soc" or ".lua".
   *
@@ -898,13 +1165,22 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
   *
   * \param filenames A null-terminated list of files to use.
   */
-void W_InitMultipleFiles(char **filenames)
+void W_InitMultipleFiles(addfilelist_t *list)
 {
-	// will be realloced as lumps are added
-	for (; *filenames; filenames++)
+	size_t i = 0;
+
+	for (; i < list->numfiles; i++)
 	{
-		//CONS_Debug(DBG_SETUP, "Loading %s\n", *filenames);
-		W_InitFile(*filenames, numwadfiles < mainwads, true);
+		const char *fn = list->files[i];
+		char pathsep = fn[strlen(fn) - 1];
+		boolean mainfile = (numwadfiles < mainwads);
+
+		//CONS_Debug(DBG_SETUP, "Loading %s\n", fn);
+
+		if (pathsep == '\\' || pathsep == '/')
+			W_InitFolder(fn, mainfile, true);
+		else
+			W_InitFile(fn, mainfile, true);
 	}
 }
 
@@ -913,7 +1189,7 @@ void W_InitMultipleFiles(char **filenames)
   */
 static boolean TestValidLump(UINT16 wad, UINT16 lump)
 {
-	I_Assert(wad < MAX_WADFILES);
+	I_Assert(wad < numwadfiles);
 	if (!wadfiles[wad]) // make sure the wad file exists
 		return false;
 
@@ -1178,7 +1454,7 @@ lumpnum_t W_CheckNumForMap(const char *name)
 				if (!strncmp(name, (wadfiles[i]->lumpinfo + lumpNum)->name, 8))
 					return (i<<16) + lumpNum;
 		}
-		else if (wadfiles[i]->type == RET_PK3)
+		else if (W_FileHasFolders(wadfiles[i]))
 		{
 			lumpNum = W_CheckNumForFolderStartPK3("maps/", i, 0);
 			if (lumpNum != INT16_MAX)
@@ -1276,9 +1552,46 @@ UINT8 W_LumpExists(const char *name)
 
 size_t W_LumpLengthPwad(UINT16 wad, UINT16 lump)
 {
+	lumpinfo_t *l;
+
 	if (!TestValidLump(wad, lump))
 		return 0;
-	return wadfiles[wad]->lumpinfo[lump].size;
+
+	l = wadfiles[wad]->lumpinfo + lump;
+
+	// Open the external file for this lump, if the WAD is a folder.
+	if (wadfiles[wad]->type == RET_FOLDER)
+	{
+		// pathisdirectory calls stat, so if anything wrong has happened,
+		// this is the time to be aware of it.
+		INT32 stat = pathisdirectory(l->diskpath);
+
+		if (stat < 0)
+		{
+#ifndef AVOID_ERRNO
+			if (direrror == ENOENT)
+				I_Error("W_LumpLengthPwad: file %s doesn't exist", l->diskpath);
+			else
+				I_Error("W_LumpLengthPwad: could not stat %s: %s", l->diskpath, strerror(direrror));
+#else
+			I_Error("W_LumpLengthPwad: could not access %s", l->diskpath);
+#endif
+		}
+		else if (stat == 1) // Path is a folder.
+			return 0;
+		else
+		{
+			FILE *handle = fopen(l->diskpath, "rb");
+			if (handle == NULL)
+				I_Error("W_LumpLengthPwad: could not open file %s", l->diskpath);
+
+			fseek(handle, 0, SEEK_END);
+			l->size = l->disksize = ftell(handle);
+			fclose(handle);
+		}
+	}
+
+	return l->size;
 }
 
 /** Returns the buffer size needed to load the given lump.
@@ -1293,11 +1606,11 @@ size_t W_LumpLength(lumpnum_t lumpnum)
 
 //
 // W_IsLumpWad
-// Is the lump a WAD? (presumably in a PK3)
+// Is the lump a WAD? (presumably not in a WAD)
 //
 boolean W_IsLumpWad(lumpnum_t lumpnum)
 {
-	if (wadfiles[WADFILENUM(lumpnum)]->type == RET_PK3)
+	if (W_FileHasFolders(wadfiles[WADFILENUM(lumpnum)]))
 	{
 		const char *lumpfullName = (wadfiles[WADFILENUM(lumpnum)]->lumpinfo + LUMPNUM(lumpnum))->fullname;
 
@@ -1306,23 +1619,23 @@ boolean W_IsLumpWad(lumpnum_t lumpnum)
 		return !strnicmp(lumpfullName + strlen(lumpfullName) - 4, ".wad", 4);
 	}
 
-	return false; // WADs should never be inside non-PK3s as far as SRB2 is concerned
+	return false; // WADs should never be inside WADs as far as SRB2 is concerned
 }
 
 //
 // W_IsLumpFolder
-// Is the lump a folder? (in a PK3 obviously)
+// Is the lump a folder? (not in a WAD obviously)
 //
 boolean W_IsLumpFolder(UINT16 wad, UINT16 lump)
 {
-	if (wadfiles[wad]->type == RET_PK3)
+	if (W_FileHasFolders(wadfiles[wad]))
 	{
 		const char *name = wadfiles[wad]->lumpinfo[lump].fullname;
 
 		return (name[strlen(name)-1] == '/'); // folders end in '/'
 	}
 
-	return false; // non-PK3s don't have folders
+	return false; // WADs don't have folders
 }
 
 #ifdef HAVE_ZLIB
@@ -1365,17 +1678,55 @@ void zerr(int ret)
   */
 size_t W_ReadLumpHeaderPwad(UINT16 wad, UINT16 lump, void *dest, size_t size, size_t offset)
 {
-	size_t lumpsize;
+	size_t lumpsize, bytesread;
 	lumpinfo_t *l;
-	FILE *handle;
+	FILE *handle = NULL;
 
-	if (!TestValidLump(wad,lump))
+	if (!TestValidLump(wad, lump))
 		return 0;
 
+	l = wadfiles[wad]->lumpinfo + lump;
+
+	// Open the external file for this lump, if the WAD is a folder.
+	if (wadfiles[wad]->type == RET_FOLDER)
+	{
+		// pathisdirectory calls stat, so if anything wrong has happened,
+		// this is the time to be aware of it.
+		INT32 stat = pathisdirectory(l->diskpath);
+
+		if (stat < 0)
+		{
+#ifndef AVOID_ERRNO
+			if (direrror == ENOENT)
+				I_Error("W_ReadLumpHeaderPwad: file %s doesn't exist", l->diskpath);
+			else
+				I_Error("W_ReadLumpHeaderPwad: could not stat %s: %s", l->diskpath, strerror(direrror));
+#else
+			I_Error("W_ReadLumpHeaderPwad: could not access %s", l->diskpath);
+#endif
+		}
+		else if (stat == 1) // Path is a folder.
+			return 0;
+		else
+		{
+			handle = fopen(l->diskpath, "rb");
+			if (handle == NULL)
+				I_Error("W_ReadLumpHeaderPwad: could not open file %s", l->diskpath);
+
+			// Find length of file
+			fseek(handle, 0, SEEK_END);
+			l->size = l->disksize = ftell(handle);
+		}
+	}
+
 	lumpsize = wadfiles[wad]->lumpinfo[lump].size;
 	// empty resource (usually markers like S_START, F_END ..)
 	if (!lumpsize || lumpsize<offset)
+	{
+		if (wadfiles[wad]->type == RET_FOLDER)
+			fclose(handle);
 		return 0;
+	}
 
 	// zero size means read all the lump
 	if (!size || size+offset > lumpsize)
@@ -1383,24 +1734,22 @@ size_t W_ReadLumpHeaderPwad(UINT16 wad, UINT16 lump, void *dest, size_t size, si
 
 	// Let's get the raw lump data.
 	// We setup the desired file handle to read the lump data.
-	l = wadfiles[wad]->lumpinfo + lump;
-	handle = wadfiles[wad]->handle;
+	if (wadfiles[wad]->type != RET_FOLDER)
+		handle = wadfiles[wad]->handle;
 	fseek(handle, (long)(l->position + offset), SEEK_SET);
 
 	// But let's not copy it yet. We support different compression formats on lumps, so we need to take that into account.
 	switch(wadfiles[wad]->lumpinfo[lump].compression)
 	{
 	case CM_NOCOMPRESSION:		// If it's uncompressed, we directly write the data into our destination, and return the bytes read.
+		bytesread = fread(dest, 1, size, handle);
+		if (wadfiles[wad]->type == RET_FOLDER)
+			fclose(handle);
 #ifdef NO_PNG_LUMPS
-		{
-			size_t bytesread = fread(dest, 1, size, handle);
-			if (Picture_IsLumpPNG((UINT8 *)dest, bytesread))
-				Picture_ThrowPNGError(l->fullname, wadfiles[wad]->filename);
-			return bytesread;
-		}
-#else
-		return fread(dest, 1, size, handle);
+		if (Picture_IsLumpPNG((UINT8 *)dest, bytesread))
+			Picture_ThrowPNGError(l->fullname, wadfiles[wad]->filename);
 #endif
+		return bytesread;
 	case CM_LZF:		// Is it LZF compressed? Used by ZWADs.
 		{
 #ifdef ZWAD
@@ -1838,7 +2187,7 @@ void W_VerifyFileMD5(UINT16 wadfilenum, const char *matchmd5)
 #else
 		I_Error
 #endif
-			(M_GetText("File is old, is corrupt or has been modified: %s (found md5: %s, wanted: %s)\n"), wadfiles[wadfilenum]->filename, actualmd5text, matchmd5);
+			(M_GetText("File is old, is corrupt or has been modified:\n%s\nFound MD5: %s\nWanted MD5: %s\n"), wadfiles[wadfilenum]->filename, actualmd5text, matchmd5);
 	}
 #endif
 }
diff --git a/src/w_wad.h b/src/w_wad.h
index 1309677128da4005e4d4b4c37e0d256ede5c285e..a41ba1724a93efad3d583a1ea0d4b065f5dd3798 100644
--- a/src/w_wad.h
+++ b/src/w_wad.h
@@ -69,6 +69,7 @@ typedef struct
 	char name[9];           // filelump_t name[] e.g. "LongEntr"
 	char *longname;         //                   e.g. "LongEntryName"
 	char *fullname;         //                   e.g. "Folder/Subfolder/LongEntryName.extension"
+	char *diskpath;         // path to the file  e.g. "/usr/games/srb2/Addon/Folder/Subfolder/LongEntryName.extension"
 	size_t size;            // real (uncompressed) size
 	compmethod compression; // lump compression method
 } lumpinfo_t;
@@ -96,9 +97,15 @@ virtlump_t* vres_Find(const virtres_t*, const char*);
 //                         DYNAMIC WAD LOADING
 // =========================================================================
 
+// Maximum of files that can be loaded
+// (there is a max of simultaneous open files anyway)
+#ifdef ENFORCE_WAD_LIMIT
+#define MAX_WADFILES 2048 // This cannot be any higher than UINT16_MAX.
+#else
+#define MAX_WADFILES UINT16_MAX
+#endif
+
 #define MAX_WADPATH 512
-#define MAX_WADFILES 48 // maximum of wad files used at the same time
-// (there is a max of simultaneous open files anyway, and this should be plenty)
 
 #define lumpcache_t void *
 
@@ -109,17 +116,19 @@ typedef enum restype
 	RET_SOC,
 	RET_LUA,
 	RET_PK3,
+	RET_FOLDER,
 	RET_UNKNOWN,
 } restype_t;
 
 typedef struct wadfile_s
 {
-	char *filename;
+	char *filename, *path;
 	restype_t type;
 	lumpinfo_t *lumpinfo;
 	lumpcache_t *lumpcache;
 	lumpcache_t *patchcache;
 	UINT16 numlumps; // this wad's number of resources
+	UINT16 foldercount; // folder count
 	FILE *handle;
 	UINT32 filesize; // for network
 	UINT8 md5sum[16];
@@ -127,11 +136,17 @@ typedef struct wadfile_s
 	boolean important; // also network - !W_VerifyNMUSlumps
 } wadfile_t;
 
-#define WADFILENUM(lumpnum) (UINT16)((lumpnum)>>16) // wad flumpnum>>16) // wad file number in upper word
+#define WADFILENUM(lumpnum) (UINT16)((lumpnum)>>16) // wad file number in upper word
 #define LUMPNUM(lumpnum) (UINT16)((lumpnum)&0xFFFF) // lump number for this pwad
 
 extern UINT16 numwadfiles;
-extern wadfile_t *wadfiles[MAX_WADFILES];
+extern wadfile_t **wadfiles;
+
+typedef struct
+{
+	char **files;
+	size_t numfiles;
+} addfilelist_t;
 
 // =========================================================================
 
@@ -141,9 +156,16 @@ void W_Shutdown(void);
 FILE *W_OpenWadFile(const char **filename, boolean useerrors);
 // Load and add a wadfile to the active wad files, returns numbers of lumps, INT16_MAX on error
 UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup);
+// Adds a folder as a file
+UINT16 W_InitFolder(const char *path, boolean mainfile, boolean startup);
 
 // W_InitMultipleFiles exits if a file was not found, but not if all is okay.
-void W_InitMultipleFiles(char **filenames);
+void W_InitMultipleFiles(addfilelist_t *list);
+
+#define W_FileHasFolders(wadfile) ((wadfile)->type == RET_PK3 || (wadfile)->type == RET_FOLDER)
+
+INT32 W_IsPathToFolderValid(const char *path);
+char *W_GetFullFolderPath(const char *path);
 
 const char *W_CheckNameForNumPwad(UINT16 wad, UINT16 lump);
 const char *W_CheckNameForNum(lumpnum_t lumpnum);
diff --git a/src/win32/win_main.c b/src/win32/win_main.c
index e1d90881ba4fac766c3720eb318c0a3b58cfbfe6..a5ebf32113f2723dbb5786c0eda8ae79a710d61b 100644
--- a/src/win32/win_main.c
+++ b/src/win32/win_main.c
@@ -188,11 +188,11 @@ static LRESULT CALLBACK MainWndproc(HWND hWnd, UINT message, WPARAM wParam, LPAR
 			ev.type = ev_keydown;
 
 	handleKeyDoom:
-			ev.data1 = 0;
+			ev.key = 0;
 			if (wParam == VK_PAUSE)
 			// intercept PAUSE key
 			{
-				ev.data1 = KEY_PAUSE;
+				ev.key = KEY_PAUSE;
 			}
 			else if (!keyboard_started)
 			// post some keys during the game startup
@@ -201,14 +201,14 @@ static LRESULT CALLBACK MainWndproc(HWND hWnd, UINT message, WPARAM wParam, LPAR
 			{
 				switch (wParam)
 				{
-					case VK_ESCAPE: ev.data1 = KEY_ESCAPE;  break;
-					case VK_RETURN: ev.data1 = KEY_ENTER;   break;
-					case VK_SHIFT:  ev.data1 = KEY_LSHIFT;  break;
-					default: ev.data1 = MapVirtualKey((DWORD)wParam,2); // convert in to char
+					case VK_ESCAPE: ev.key = KEY_ESCAPE;  break;
+					case VK_RETURN: ev.key = KEY_ENTER;   break;
+					case VK_SHIFT:  ev.key = KEY_LSHIFT;  break;
+					default: ev.key = MapVirtualKey((DWORD)wParam,2); // convert in to char
 				}
 			}
 
-			if (ev.data1)
+			if (ev.key)
 				D_PostEvent (&ev);
 
 			return 0;
@@ -240,7 +240,7 @@ static LRESULT CALLBACK MainWndproc(HWND hWnd, UINT message, WPARAM wParam, LPAR
 			if (nodinput)
 			{
 				ev.type = ev_keyup;
-				ev.data1 = KEY_MOUSE1 + 3 + HIWORD(wParam);
+				ev.key = KEY_MOUSE1 + 3 + HIWORD(wParam);
 				D_PostEvent(&ev);
 				return TRUE;
 			}
@@ -249,7 +249,7 @@ static LRESULT CALLBACK MainWndproc(HWND hWnd, UINT message, WPARAM wParam, LPAR
 			if (nodinput)
 			{
 				ev.type = ev_keydown;
-				ev.data1 = KEY_MOUSE1 + 3 + HIWORD(wParam);
+				ev.key = KEY_MOUSE1 + 3 + HIWORD(wParam);
 				D_PostEvent(&ev);
 				return TRUE;
 			}
@@ -258,9 +258,9 @@ static LRESULT CALLBACK MainWndproc(HWND hWnd, UINT message, WPARAM wParam, LPAR
 			//I_OutputMsg("MW_WHEEL dispatched.\n");
 			ev.type = ev_keydown;
 			if ((INT16)HIWORD(wParam) > 0)
-				ev.data1 = KEY_MOUSEWHEELUP;
+				ev.key = KEY_MOUSEWHEELUP;
 			else
-				ev.data1 = KEY_MOUSEWHEELDOWN;
+				ev.key = KEY_MOUSEWHEELDOWN;
 			D_PostEvent(&ev);
 			break;
 
@@ -271,7 +271,7 @@ static LRESULT CALLBACK MainWndproc(HWND hWnd, UINT message, WPARAM wParam, LPAR
 
 		case WM_CLOSE:
 			PostQuitMessage(0);         //to quit while in-game
-			ev.data1 = KEY_ESCAPE;      //to exit network synchronization
+			ev.key = KEY_ESCAPE;      //to exit network synchronization
 			ev.type = ev_keydown;
 			D_PostEvent (&ev);
 			return 0;
diff --git a/src/win32/win_sys.c b/src/win32/win_sys.c
index da0d5b47ee3c26699b4d538575a22b8dc7420218..ff443935fa7f5a925e3539508b2fe5ce18244e09 100644
--- a/src/win32/win_sys.c
+++ b/src/win32/win_sys.c
@@ -322,20 +322,20 @@ static inline VOID I_GetConsoleEvents(VOID)
 					{
 						case VK_ESCAPE:
 						case VK_TAB:
-							ev.data1 = KEY_NULL;
+							ev.key = KEY_NULL;
 							break;
 						case VK_SHIFT:
-							ev.data1 = KEY_LSHIFT;
+							ev.key = KEY_LSHIFT;
 							break;
 						case VK_RETURN:
 							entering_con_command = false;
 							/* FALLTHRU */
 						default:
-							ev.data1 = MapVirtualKey(input.Event.KeyEvent.wVirtualKeyCode,2); // convert in to char
+							ev.key = MapVirtualKey(input.Event.KeyEvent.wVirtualKeyCode,2); // convert in to char
 					}
 					if (co != INVALID_HANDLE_VALUE && GetFileType(co) == FILE_TYPE_CHAR && GetConsoleMode(co, &t))
 					{
-						if (ev.data1 && ev.data1 != KEY_LSHIFT && ev.data1 != KEY_RSHIFT)
+						if (ev.key && ev.key != KEY_LSHIFT && ev.key != KEY_RSHIFT)
 						{
 #ifdef UNICODE
 							WriteConsole(co, &input.Event.KeyEvent.uChar.UnicodeChar, 1, &t, NULL);
@@ -356,13 +356,13 @@ static inline VOID I_GetConsoleEvents(VOID)
 					switch (input.Event.KeyEvent.wVirtualKeyCode)
 					{
 						case VK_SHIFT:
-							ev.data1 = KEY_LSHIFT;
+							ev.key = KEY_LSHIFT;
 							break;
 						default:
 							break;
 					}
 				}
-				if (ev.data1) D_PostEvent(&ev);
+				if (ev.key) D_PostEvent(&ev);
 				break;
 			case MOUSE_EVENT:
 			case WINDOW_BUFFER_SIZE_EVENT:
@@ -945,7 +945,7 @@ static void I_ShutdownMouse2(VOID)
 		for (i = 0; i < MOUSEBUTTONS; i++)
 		{
 			event.type = ev_keyup;
-			event.data1 = KEY_2MOUSE1 + i;
+			event.key = KEY_2MOUSE1 + i;
 			D_PostEvent(&event);
 		}
 
@@ -1135,14 +1135,14 @@ VOID I_GetSysMouseEvents(INT mouse_state)
 		if ((mouse_state & (1 << i)) && !(old_mouse_state & (1 << i)))
 		{
 			event.type = ev_keydown;
-			event.data1 = KEY_MOUSE1 + i;
+			event.key = KEY_MOUSE1 + i;
 			D_PostEvent(&event);
 		}
 		// check if button released
 		if (!(mouse_state & (1 << i)) && (old_mouse_state & (1 << i)))
 		{
 			event.type = ev_keyup;
-			event.data1 = KEY_MOUSE1 + i;
+			event.key = KEY_MOUSE1 + i;
 			D_PostEvent(&event);
 		}
 	}
@@ -1156,9 +1156,9 @@ VOID I_GetSysMouseEvents(INT mouse_state)
 	if (xmickeys || ymickeys)
 	{
 		event.type  = ev_mouse;
-		event.data1 = 0;
-		event.data2 = xmickeys;
-		event.data3 = -ymickeys;
+		event.key = 0;
+		event.x = xmickeys;
+		event.y = -ymickeys;
 		D_PostEvent(&event);
 		SetCursorPos(center_x, center_y);
 	}
@@ -1240,7 +1240,7 @@ static void I_ShutdownMouse(void)
 	for (i = 0; i < MOUSEBUTTONS; i++)
 	{
 		event.type = ev_keyup;
-		event.data1 = KEY_MOUSE1 + i;
+		event.key = KEY_MOUSE1 + i;
 		D_PostEvent(&event);
 	}
 	if (nodinput)
@@ -1281,7 +1281,7 @@ void I_GetMouseEvents(void)
 						event.type = ev_keydown;
 					else
 						event.type = ev_keyup;
-					event.data1 = KEY_2MOUSE1 + i;
+					event.key = KEY_2MOUSE1 + i;
 					D_PostEvent(&event);
 				}
 		}
@@ -1289,9 +1289,9 @@ void I_GetMouseEvents(void)
 		if (handlermouse2x || handlermouse2y)
 		{
 			event.type = ev_mouse2;
-			event.data1 = 0;
-			event.data2 = handlermouse2x<<1;
-			event.data3 = -handlermouse2y<<1;
+			event.key = 0;
+			event.x = handlermouse2x<<1;
+			event.y = -handlermouse2y<<1;
 			handlermouse2x = 0;
 			handlermouse2y = 0;
 
@@ -1330,7 +1330,7 @@ getBufferedData:
 				else
 					event.type = ev_keyup; // Button up
 
-				event.data1 = rgdod[d].dwOfs - DIMOFS_BUTTON0 + KEY_MOUSE1;
+				event.key = rgdod[d].dwOfs - DIMOFS_BUTTON0 + KEY_MOUSE1;
 				D_PostEvent(&event);
 			}
 			else if (rgdod[d].dwOfs == DIMOFS_X)
@@ -1342,9 +1342,9 @@ getBufferedData:
 			{
 				// z-axes the wheel
 				if ((int)rgdod[d].dwData > 0)
-					event.data1 = KEY_MOUSEWHEELUP;
+					event.key = KEY_MOUSEWHEELUP;
 				else
-					event.data1 = KEY_MOUSEWHEELDOWN;
+					event.key = KEY_MOUSEWHEELDOWN;
 				event.type = ev_keydown;
 				D_PostEvent(&event);
 			}
@@ -1354,9 +1354,9 @@ getBufferedData:
 		if (xmickeys || ymickeys)
 		{
 			event.type = ev_mouse;
-			event.data1 = 0;
-			event.data2 = xmickeys;
-			event.data3 = -ymickeys;
+			event.key = 0;
+			event.x = xmickeys;
+			event.y = -ymickeys;
 			D_PostEvent(&event);
 		}
 	}
@@ -2395,14 +2395,14 @@ static VOID I_ShutdownJoystick(VOID)
 	// emulate the up of all joystick buttons
 	for (i = 0;i < JOYBUTTONS;i++)
 	{
-		event.data1 = KEY_JOY1+i;
+		event.key = KEY_JOY1+i;
 		D_PostEvent(&event);
 	}
 
 	// emulate the up of all joystick hats
 	for (i = 0;i < JOYHATS*4;i++)
 	{
-		event.data1 = KEY_HAT1+i;
+		event.key = KEY_HAT1+i;
 		D_PostEvent(&event);
 	}
 
@@ -2410,7 +2410,7 @@ static VOID I_ShutdownJoystick(VOID)
 	event.type = ev_joystick;
 	for (i = 0;i < JOYAXISSET; i++)
 	{
-		event.data1 = i;
+		event.key = i;
 		D_PostEvent(&event);
 	}
 
@@ -2460,14 +2460,14 @@ static VOID I_ShutdownJoystick2(VOID)
 	// emulate the up of all joystick buttons
 	for (i = 0;i < JOYBUTTONS;i++)
 	{
-		event.data1 = KEY_2JOY1+i;
+		event.key = KEY_2JOY1+i;
 		D_PostEvent(&event);
 	}
 
 	// emulate the up of all joystick hats
 	for (i = 0;i < JOYHATS*4;i++)
 	{
-		event.data1 = KEY_2HAT1+i;
+		event.key = KEY_2HAT1+i;
 		D_PostEvent(&event);
 	}
 
@@ -2475,7 +2475,7 @@ static VOID I_ShutdownJoystick2(VOID)
 	event.type = ev_joystick2;
 	for (i = 0;i < JOYAXISSET; i++)
 	{
-		event.data1 = i;
+		event.key = i;
 		D_PostEvent(&event);
 	}
 
@@ -2598,7 +2598,7 @@ acquire:
 					event.type = ev_keydown;
 				else
 					event.type = ev_keyup;
-				event.data1 = KEY_JOY1 + i;
+				event.key = KEY_JOY1 + i;
 				D_PostEvent(&event);
 			}
 		}
@@ -2618,7 +2618,7 @@ acquire:
 					event.type = ev_keydown;
 				else
 					event.type = ev_keyup;
-				event.data1 = KEY_HAT1 + i;
+				event.key = KEY_HAT1 + i;
 				D_PostEvent(&event);
 			}
 		}
@@ -2627,7 +2627,7 @@ acquire:
 
 	// send joystick axis positions
 	event.type = ev_joystick;
-	event.data1 = event.data2 = event.data3 = 0;
+	event.key = event.x = event.y = 0;
 
 	if (Joystick.bGamepadStyle)
 	{
@@ -2635,29 +2635,29 @@ acquire:
 		if (JoyInfo.X)
 		{
 			if (js.lX < -(JOYAXISRANGE/2))
-				event.data2 = -1;
+				event.x = -1;
 			else if (js.lX > JOYAXISRANGE/2)
-				event.data2 = 1;
+				event.x = 1;
 		}
 		if (JoyInfo.Y)
 		{
 			if (js.lY < -(JOYAXISRANGE/2))
-				event.data3 = -1;
+				event.y = -1;
 			else if (js.lY > JOYAXISRANGE/2)
-				event.data3 = 1;
+				event.y = 1;
 		}
 	}
 	else
 	{
 		// analog control style, just send the raw data
-		if (JoyInfo.X)  event.data2 = js.lX; // x axis
-		if (JoyInfo.Y)  event.data3 = js.lY; // y axis
+		if (JoyInfo.X)  event.x = js.lX; // x axis
+		if (JoyInfo.Y)  event.y = js.lY; // y axis
 	}
 
 	D_PostEvent(&event);
 #if JOYAXISSET > 1
-	event.data1 = 1;
-	event.data2 = event.data3 = 0;
+	event.key = 1;
+	event.x = event.y = 0;
 
 	if (Joystick.bGamepadStyle)
 	{
@@ -2665,30 +2665,30 @@ acquire:
 		if (JoyInfo.Z)
 		{
 			if (js.lZ < -(JOYAXISRANGE/2))
-				event.data2 = -1;
+				event.x = -1;
 			else if (js.lZ > JOYAXISRANGE/2)
-				event.data2 = 1;
+				event.x = 1;
 		}
 		if (JoyInfo.Rx)
 		{
 			if (js.lRx < -(JOYAXISRANGE/2))
-				event.data3 = -1;
+				event.y = -1;
 			else if (js.lRx > JOYAXISRANGE/2)
-				event.data3 = 1;
+				event.y = 1;
 		}
 	}
 	else
 	{
 		// analog control style, just send the raw data
-		if (JoyInfo.Z)  event.data2 = js.lZ;  // z axis
-		if (JoyInfo.Rx) event.data3 = js.lRx; // rx axis
+		if (JoyInfo.Z)  event.x = js.lZ;  // z axis
+		if (JoyInfo.Rx) event.y = js.lRx; // rx axis
 	}
 
 	D_PostEvent(&event);
 #endif
 #if JOYAXISSET > 2
-	event.data1 = 2;
-	event.data2 = event.data3 = 0;
+	event.key = 2;
+	event.x = event.y = 0;
 
 	if (Joystick.bGamepadStyle)
 	{
@@ -2696,53 +2696,53 @@ acquire:
 		if (JoyInfo.Rx)
 		{
 			if (js.lRy < -(JOYAXISRANGE/2))
-				event.data2 = -1;
+				event.x = -1;
 			else if (js.lRy > JOYAXISRANGE/2)
-				event.data2 = 1;
+				event.x = 1;
 		}
 		if (JoyInfo.Rz)
 		{
 			if (js.lRz < -(JOYAXISRANGE/2))
-				event.data3 = -1;
+				event.y = -1;
 			else if (js.lRz > JOYAXISRANGE/2)
-				event.data3 = 1;
+				event.y = 1;
 		}
 	}
 	else
 	{
 		// analog control style, just send the raw data
-		if (JoyInfo.Ry) event.data2 = js.lRy; // ry axis
-		if (JoyInfo.Rz) event.data3 = js.lRz; // rz axis
+		if (JoyInfo.Ry) event.x = js.lRy; // ry axis
+		if (JoyInfo.Rz) event.y = js.lRz; // rz axis
 	}
 
 	D_PostEvent(&event);
 #endif
 #if JOYAXISSET > 3
-	event.data1 = 3;
-	event.data2 = event.data3 = 0;
+	event.key = 3;
+	event.x = event.y = 0;
 	if (Joystick.bGamepadStyle)
 	{
 		// gamepad control type, on or off, live or die
 		if (JoyInfo.U)
 		{
 			if (js.rglSlider[0] < -(JOYAXISRANGE/2))
-				event.data2 = -1;
+				event.x = -1;
 			else if (js.rglSlider[0] > JOYAXISRANGE/2)
-				event.data2 = 1;
+				event.x = 1;
 		}
 		if (JoyInfo.V)
 		{
 			if (js.rglSlider[1] < -(JOYAXISRANGE/2))
-				event.data3 = -1;
+				event.y = -1;
 			else if (js.rglSlider[1] > JOYAXISRANGE/2)
-				event.data3 = 1;
+				event.y = 1;
 		}
 	}
 	else
 	{
 		// analog control style, just send the raw data
-		if (JoyInfo.U)  event.data2 = js.rglSlider[0]; // U axis
-		if (JoyInfo.V)  event.data3 = js.rglSlider[1]; // V axis
+		if (JoyInfo.U)  event.x = js.rglSlider[0]; // U axis
+		if (JoyInfo.V)  event.y = js.rglSlider[1]; // V axis
 	}
 	D_PostEvent(&event);
 #endif
@@ -2842,7 +2842,7 @@ acquire:
 					event.type = ev_keydown;
 				else
 					event.type = ev_keyup;
-				event.data1 = KEY_2JOY1 + i;
+				event.key = KEY_2JOY1 + i;
 				D_PostEvent(&event);
 			}
 		}
@@ -2862,7 +2862,7 @@ acquire:
 					event.type = ev_keydown;
 				else
 					event.type = ev_keyup;
-				event.data1 = KEY_2HAT1 + i;
+				event.key = KEY_2HAT1 + i;
 				D_PostEvent(&event);
 			}
 		}
@@ -2871,7 +2871,7 @@ acquire:
 
 	// send joystick axis positions
 	event.type = ev_joystick2;
-	event.data1 = event.data2 = event.data3 = 0;
+	event.key = event.x = event.y = 0;
 
 	if (Joystick2.bGamepadStyle)
 	{
@@ -2879,29 +2879,29 @@ acquire:
 		if (JoyInfo2.X)
 		{
 			if (js.lX < -(JOYAXISRANGE/2))
-				event.data2 = -1;
+				event.x = -1;
 			else if (js.lX > JOYAXISRANGE/2)
-				event.data2 = 1;
+				event.x = 1;
 		}
 		if (JoyInfo2.Y)
 		{
 			if (js.lY < -(JOYAXISRANGE/2))
-				event.data3 = -1;
+				event.y = -1;
 			else if (js.lY > JOYAXISRANGE/2)
-				event.data3 = 1;
+				event.y = 1;
 		}
 	}
 	else
 	{
 		// analog control style, just send the raw data
-		if (JoyInfo2.X)  event.data2 = js.lX; // x axis
-		if (JoyInfo2.Y)  event.data3 = js.lY; // y axis
+		if (JoyInfo2.X)  event.x = js.lX; // x axis
+		if (JoyInfo2.Y)  event.y = js.lY; // y axis
 	}
 
 	D_PostEvent(&event);
 #if JOYAXISSET > 1
-	event.data1 = 1;
-	event.data2 = event.data3 = 0;
+	event.key = 1;
+	event.x = event.y = 0;
 
 	if (Joystick2.bGamepadStyle)
 	{
@@ -2909,30 +2909,30 @@ acquire:
 		if (JoyInfo2.Z)
 		{
 			if (js.lZ < -(JOYAXISRANGE/2))
-				event.data2 = -1;
+				event.x = -1;
 			else if (js.lZ > JOYAXISRANGE/2)
-				event.data2 = 1;
+				event.x = 1;
 		}
 		if (JoyInfo2.Rx)
 		{
 			if (js.lRx < -(JOYAXISRANGE/2))
-				event.data3 = -1;
+				event.y = -1;
 			else if (js.lRx > JOYAXISRANGE/2)
-				event.data3 = 1;
+				event.y = 1;
 		}
 	}
 	else
 	{
 		// analog control style, just send the raw data
-		if (JoyInfo2.Z)  event.data2 = js.lZ;  // z axis
-		if (JoyInfo2.Rx) event.data3 = js.lRx; // rx axis
+		if (JoyInfo2.Z)  event.x = js.lZ;  // z axis
+		if (JoyInfo2.Rx) event.y = js.lRx; // rx axis
 	}
 
 	D_PostEvent(&event);
 #endif
 #if JOYAXISSET > 2
-	event.data1 = 2;
-	event.data2 = event.data3 = 0;
+	event.key = 2;
+	event.x = event.y = 0;
 
 	if (Joystick2.bGamepadStyle)
 	{
@@ -2940,53 +2940,53 @@ acquire:
 		if (JoyInfo2.Rx)
 		{
 			if (js.lRy < -(JOYAXISRANGE/2))
-				event.data2 = -1;
+				event.x = -1;
 			else if (js.lRy > JOYAXISRANGE/2)
-				event.data2 = 1;
+				event.x = 1;
 		}
 		if (JoyInfo2.Rz)
 		{
 			if (js.lRz < -(JOYAXISRANGE/2))
-				event.data3 = -1;
+				event.y = -1;
 			else if (js.lRz > JOYAXISRANGE/2)
-				event.data3 = 1;
+				event.y = 1;
 		}
 	}
 	else
 	{
 		// analog control style, just send the raw data
-		if (JoyInfo2.Ry) event.data2 = js.lRy; // ry axis
-		if (JoyInfo2.Rz) event.data3 = js.lRz; // rz axis
+		if (JoyInfo2.Ry) event.x = js.lRy; // ry axis
+		if (JoyInfo2.Rz) event.y = js.lRz; // rz axis
 	}
 
 	D_PostEvent(&event);
 #endif
 #if JOYAXISSET > 3
-	event.data1 = 3;
-	event.data2 = event.data3 = 0;
+	event.key = 3;
+	event.x = event.y = 0;
 	if (Joystick2.bGamepadStyle)
 	{
 		// gamepad control type, on or off, live or die
 		if (JoyInfo2.U)
 		{
 			if (js.rglSlider[0] < -(JOYAXISRANGE/2))
-				event.data2 = -1;
+				event.x = -1;
 			else if (js.rglSlider[0] > JOYAXISRANGE/2)
-				event.data2 = 1;
+				event.x = 1;
 		}
 		if (JoyInfo2.V)
 		{
 			if (js.rglSlider[1] < -(JOYAXISRANGE/2))
-				event.data3 = -1;
+				event.y = -1;
 			else if (js.rglSlider[1] > JOYAXISRANGE/2)
-				event.data3 = 1;
+				event.y = 1;
 		}
 	}
 	else
 	{
 		// analog control style, just send the raw data
-		if (JoyInfo2.U)  event.data2 = js.rglSlider[0]; // U axis
-		if (JoyInfo2.V)  event.data3 = js.rglSlider[1]; // V axis
+		if (JoyInfo2.U)  event.x = js.rglSlider[0]; // U axis
+		if (JoyInfo2.V)  event.y = js.rglSlider[1]; // V axis
 	}
 	D_PostEvent(&event);
 #endif
@@ -3194,7 +3194,7 @@ INT32 I_GetKey(void)
 		ev = &events[eventtail];
 		eventtail = (eventtail+1) & (MAXEVENTS-1);
 		if (ev->type == ev_keydown || ev->type == ev_console)
-			return ev->data1;
+			return ev->key;
 		else
 			return 0;
 	}
@@ -3308,7 +3308,7 @@ static VOID I_GetKeyboardEvents(VOID)
 	if (!appActive && RepeatKeyCode) // Stop when lost focus
 	{
 		event.type = ev_keyup;
-		event.data1 = RepeatKeyCode;
+		event.key = RepeatKeyCode;
 		D_PostEvent(&event);
 		RepeatKeyCode = 0;
 	}
@@ -3363,9 +3363,9 @@ getBufferedData:
 
 			ch = rgdod[d].dwOfs & 0xFF;
 			if (ASCIINames[ch])
-				event.data1 = ASCIINames[ch];
+				event.key = ASCIINames[ch];
 			else
-				event.data1 = 0x80;
+				event.key = 0x80;
 
 			D_PostEvent(&event);
 		}
@@ -3378,7 +3378,7 @@ getBufferedData:
 			// delay is tripled for first repeating key
 			RepeatKeyTics = hacktics + (KEY_REPEAT_DELAY*3);
 			if (event.type == ev_keydown) // use the last event!
-				RepeatKeyCode = event.data1;
+				RepeatKeyCode = event.key;
 		}
 		else
 		{
@@ -3386,7 +3386,7 @@ getBufferedData:
 			if (RepeatKeyCode && hacktics - RepeatKeyTics > KEY_REPEAT_DELAY)
 			{
 				event.type = ev_keydown;
-				event.data1 = RepeatKeyCode;
+				event.key = RepeatKeyCode;
 				D_PostEvent(&event);
 
 				RepeatKeyTics = hacktics;
diff --git a/src/y_inter.c b/src/y_inter.c
index 6d876d7bdbf1a5d846e74eed2d34a284dfe03d22..f24436d4082e645fbbd9fe103a7ed33e741a18b7 100644
--- a/src/y_inter.c
+++ b/src/y_inter.c
@@ -212,7 +212,7 @@ static void Y_IntermissionTokenDrawer(void)
 	calc = (lowy - y)*2;
 
 	if (calc > 0)
-		V_DrawCroppedPatch(32<<FRACBITS, y<<FRACBITS, FRACUNIT/2, 0, tokenicon, 0, 0, tokenicon->width, calc);
+		V_DrawCroppedPatch(32<<FRACBITS, y<<FRACBITS, FRACUNIT/2, FRACUNIT/2, 0, tokenicon, NULL, 0, 0, tokenicon->width<<FRACBITS, calc<<FRACBITS);
 }
 
 
@@ -239,12 +239,12 @@ void Y_LoadIntermissionData(void)
 			}
 			data.coop.ptotal = W_CachePatchName("YB_TOTAL", PU_PATCH);
 
-			// get background patches
-			bgpatch = W_CachePatchName("INTERSCR", PU_PATCH);
 
 			// grab an interscreen if appropriate
 			if (mapheaderinfo[gamemap-1]->interscreen[0] != '#')
 				interpic = W_CachePatchName(mapheaderinfo[gamemap-1]->interscreen, PU_PATCH);
+			else // no interscreen? use default background
+				bgpatch = W_CachePatchName("INTERSCR", PU_PATCH);
 			break;
 		}
 		case int_spec:
@@ -255,12 +255,11 @@ void Y_LoadIntermissionData(void)
 			data.spec.pscore = W_CachePatchName("YB_SCORE", PU_PATCH);
 			data.spec.pcontinues = W_CachePatchName("YB_CONTI", PU_PATCH);
 
-			// get background tile
-			bgtile = W_CachePatchName("SPECTILE", PU_PATCH);
-
 			// grab an interscreen if appropriate
 			if (mapheaderinfo[gamemap-1]->interscreen[0] != '#')
 				interpic = W_CachePatchName(mapheaderinfo[gamemap-1]->interscreen, PU_PATCH);
+			else // no interscreen? use default background
+				bgtile = W_CachePatchName("SPECTILE", PU_PATCH);
 			break;
 		}
 		case int_ctf:
@@ -430,7 +429,7 @@ void Y_IntermissionDrawer(void)
 	else if (bgtile)
 		V_DrawPatchFill(bgtile);
 
-	LUAh_IntermissionHUD(intertype == int_spec && stagefailed);
+	LUA_HUDHOOK(intermission);
 	if (!LUA_HudEnabled(hud_intermissiontally))
 		goto skiptallydrawer;
 
diff --git a/src/z_zone.h b/src/z_zone.h
index be55bf9940ead1a1385f92ca50d296b0bd4fbdf1..17f572a905b56597bf955ee28b53814c6108bf04 100644
--- a/src/z_zone.h
+++ b/src/z_zone.h
@@ -39,6 +39,7 @@ enum
 	// Tags < PU_LEVEL are not purged until freed explicitly.
 	PU_STATIC                = 1, // static entire execution time
 	PU_LUA                   = 2, // static entire execution time -- used by lua so it doesn't get caught in loops forever
+	PU_PERFSTATS             = 3, // static between changes to ps_samplesize cvar
 
 	PU_SOUND                 = 11, // static while playing
 	PU_MUSIC                 = 12, // static while playing