diff --git a/.gitignore b/.gitignore
index 3090417dd6b00b8796d2743675301615e488707d..7023aaa80b08949f6d1a1a9d35ff413e5dd02a3a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,11 +13,11 @@ Win32_LIB_ASM_Release
 *.dgb
 *.debug
 *.debug.txt
-/bin/VC10/
-/objs/VC10/
 *.user
 *.db
 *.opendb
 /.vs
 /debian
 /assets/debian
+/make
+/bin
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5d2d4a7e65982e052c9d8cb222e30fe4fcefe393..6f901d3d79e44eee1cc1453a4eedd5e2e7ca1caf 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.0)
+cmake_minimum_required(VERSION 3.13)
 
 # Enable CCache early
 set(SRB2_USE_CCACHE OFF CACHE BOOL "Use CCache")
@@ -34,12 +34,11 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/")
 
 ### Useful functions
 
-# Prepend sources with current source directory
-function(prepend_sources SOURCE_FILES)
-	foreach(SOURCE_FILE ${${SOURCE_FILES}})
-		set(MODIFIED ${MODIFIED} ${CMAKE_CURRENT_SOURCE_DIR}/${SOURCE_FILE})
-	endforeach()
-	set(${SOURCE_FILES} ${MODIFIED} PARENT_SCOPE)
+# Add sources from Sourcefile
+function(target_sourcefile type)
+	file(STRINGS Sourcefile list
+		REGEX "[-0-9A-Za-z_]+\.${type}")
+	target_sources(SRB2SDL2 PRIVATE ${list})
 endfunction()
 
 # Macro to add OSX framework
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..7ee12d837c16d1bcd38b51e463d8c6fe4a04439c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,8 @@
+ifdef SILENT
+MAKEFLAGS+=--no-print-directory
+endif
+
+all :
+
+% ::
+	@$(MAKE) -C src $(MAKECMDGOALS)
diff --git a/README.md b/README.md
index 8a5ca1a1ff0193df4aeeb9d59f5b38ae65869458..49a3cc36d167169467a2d65bec7527610691694d 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
 # Sonic Robo Blast 2
+[![latest release](https://badgen.net/github/release/STJr/SRB2/stable)](https://github.com/STJr/SRB2/releases/latest)
 
 [![Build status](https://ci.appveyor.com/api/projects/status/399d4hcw9yy7hg2y?svg=true)](https://ci.appveyor.com/project/STJr/srb2)
 [![Build status](https://travis-ci.org/STJr/SRB2.svg?branch=master)](https://travis-ci.org/STJr/SRB2)
diff --git a/appveyor.yml b/appveyor.yml
index 2acc2f71235be24cd7190e511ee5106f16bcc295..b9f84f395a5afccc741f0999b64e766695fce7d2 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,16 +1,12 @@
-version: 2.2.8.{branch}-{build}
+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,43 +61,31 @@ 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 "BUILD_PATH=bin\Mingw64\Release"
-  ) else (
-    set "BUILD_PATH=bin\Mingw\Release"
-  )
-- if [%X86_64%] == [1] ( set "CONFIGURATION=%CONFIGURATION%64" )
 - ccache -s
 - set BUILD_ARCHIVE=%REPO%-%GITSHORT%-%CONFIGURATION%.7z
 - set BUILDSARCHIVE=%REPO%-%CONFIGURATION%.7z
-- cmd: 7z a %BUILD_ARCHIVE% %BUILD_PATH% -xr!.gitignore
+- cmd: 7z a %BUILD_ARCHIVE% bin -xr!.gitignore
 - appveyor PushArtifact %BUILD_ARCHIVE%
 #- cmd: copy %BUILD_ARCHIVE% %BUILDSARCHIVE%
 #- appveyor PushArtifact %BUILDSARCHIVE%
@@ -139,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/bin/FreeBSD/Debug/.gitignore b/bin/FreeBSD/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/bin/FreeBSD/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/bin/FreeBSD/Release/.gitignore b/bin/FreeBSD/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/bin/FreeBSD/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/bin/Linux/Debug/.gitignore b/bin/Linux/Debug/.gitignore
deleted file mode 100644
index 56dee6f950de65be41987cd682a7a5e2683e1962..0000000000000000000000000000000000000000
--- a/bin/Linux/Debug/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/lsdlsrb2
diff --git a/bin/Linux/Release/.gitignore b/bin/Linux/Release/.gitignore
deleted file mode 100644
index 5b5c54a548a30868ad14fe059ca6ec0e1728f19e..0000000000000000000000000000000000000000
--- a/bin/Linux/Release/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/lsdlsrb2
-/pnd
-/*.mo
diff --git a/bin/Linux64/Debug/.gitignore b/bin/Linux64/Debug/.gitignore
deleted file mode 100644
index 56dee6f950de65be41987cd682a7a5e2683e1962..0000000000000000000000000000000000000000
--- a/bin/Linux64/Debug/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/lsdlsrb2
diff --git a/bin/Linux64/Release/.gitignore b/bin/Linux64/Release/.gitignore
deleted file mode 100644
index 56dee6f950de65be41987cd682a7a5e2683e1962..0000000000000000000000000000000000000000
--- a/bin/Linux64/Release/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/lsdlsrb2
diff --git a/bin/Mingw/Debug/.gitignore b/bin/Mingw/Debug/.gitignore
deleted file mode 100644
index 834f313e3eae612617885430c8071e6e41483d88..0000000000000000000000000000000000000000
--- a/bin/Mingw/Debug/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-*.exe
-*.mo
-r_opengl.dll
diff --git a/bin/Mingw/Release/.gitignore b/bin/Mingw/Release/.gitignore
deleted file mode 100644
index 3458ff7648f27c14076ff2aee101446a323f5a04..0000000000000000000000000000000000000000
--- a/bin/Mingw/Release/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-*.exe
-*.mo
-r_opengl.dll
-*.bat
diff --git a/bin/Mingw64/Debug/.gitignore b/bin/Mingw64/Debug/.gitignore
deleted file mode 100644
index e431dca5d25bfe0ea739d34ca086c9e87b2c13ab..0000000000000000000000000000000000000000
--- a/bin/Mingw64/Debug/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/srb2sdl.exe
-/srb2win.exe
-/r_opengl.dll
diff --git a/bin/Mingw64/Release/.gitignore b/bin/Mingw64/Release/.gitignore
deleted file mode 100644
index e431dca5d25bfe0ea739d34ca086c9e87b2c13ab..0000000000000000000000000000000000000000
--- a/bin/Mingw64/Release/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/srb2sdl.exe
-/srb2win.exe
-/r_opengl.dll
diff --git a/bin/SDL/Debug/.gitignore b/bin/SDL/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/bin/SDL/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/bin/SDL/Release/.gitignore b/bin/SDL/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/bin/SDL/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/bin/VC/.gitignore b/bin/VC/.gitignore
deleted file mode 100644
index e52f825b2455a0db165bfab861ad93c52b8f8f0e..0000000000000000000000000000000000000000
--- a/bin/VC/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/Release
-/Debug
diff --git a/bin/VC9/.gitignore b/bin/VC9/.gitignore
deleted file mode 100644
index 205fe45deb9ebe556ff38988507a10183a30feb7..0000000000000000000000000000000000000000
--- a/bin/VC9/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/Win32
-/x64
diff --git a/bin/dummy/.gitignore b/bin/dummy/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/bin/dummy/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
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 a0d40cdf0003dcc9f8ed81b0812825a625f6048a..4ed68e1ca41430e7225e1b2936c033f11c52a91a 100644
--- a/extras/conf/SRB2-22.cfg
+++ b/extras/conf/SRB2-22.cfg
@@ -640,6 +640,39 @@ linedeftypes
 			prefix = "(63)";
 		}
 
+		96
+		{
+			title = "Apply Tag to Tagged Sectors";
+			prefix = "(96)";
+			flags1024text = "[10] Offsets are target tags";
+			flags8192text = "[13] Use front side offsets";
+			flags32768text = "[15] Use back side offsets";
+		}
+
+		97
+		{
+			title = "Apply Tag to Front Sector";
+			prefix = "(97)";
+			flags8192text = "[13] Use front side offsets";
+			flags32768text = "[15] Use back side offsets";
+		}
+
+		98
+		{
+			title = "Apply Tag to Back Sector";
+			prefix = "(98)";
+			flags8192text = "[13] Use front side offsets";
+			flags32768text = "[15] Use back side offsets";
+		}
+
+		99
+		{
+			title = "Apply Tag to Front and Back Sectors";
+			prefix = "(99)";
+			flags8192text = "[13] Use front side offsets";
+			flags32768text = "[15] Use back side offsets";
+		}
+
 		540
 		{
 			title = "Floor Friction";
@@ -746,13 +779,13 @@ linedeftypes
 
 		20
 		{
-			title = "First Line";
+			title = "PolyObject First Line";
 			prefix = "(20)";
 		}
 
 		22
 		{
-			title = "Parameters";
+			title = "PolyObject Parameters";
 			prefix = "(22)";
 			flags8text = "[3] Set translucency by X offset";
 			flags32text = "[5] Render outer sides only";
@@ -765,19 +798,19 @@ linedeftypes
 
 		30
 		{
-			title = "Waving Flag";
+			title = "PolyObject Waving Flag";
 			prefix = "(30)";
 		}
 
 		31
 		{
-			title = "Displacement by Front Sector";
+			title = "Move PolyObject by Front Sector Displacement";
 			prefix = "(31)";
 		}
 
 		32
 		{
-			title = "Angular Displacement by Front Sector";
+			title = "Rotate PolyObject by Front Sector Displacement";
 			prefix = "(32)";
 			flags64text = "[6] Don't turn players";
 			flags512text = "[9] Turn all objects";
@@ -1959,7 +1992,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
@@ -1967,6 +2000,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
@@ -2057,7 +2091,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
@@ -2066,7 +2100,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
@@ -2498,35 +2532,35 @@ linedeftypes
 
 		480
 		{
-			title = "Door Slide";
+			title = "PolyObject Door Slide";
 			prefix = "(480)";
 			flags8text = "[3] Set delay by backside sector";
 		}
 
 		481
 		{
-			title = "Door Swing";
+			title = "PolyObject Door Swing";
 			prefix = "(481)";
 			flags8text = "[3] Set delay by backside sector";
 		}
 
 		482
 		{
-			title = "Move";
+			title = "Move PolyObject";
 			prefix = "(482)";
 			flags8text = "[3] Set delay by backside sector";
 		}
 
 		483
 		{
-			title = "Move, Override";
+			title = "Move PolyObject, Override";
 			prefix = "(483)";
 			flags8text = "[3] Set delay by backside sector";
 		}
 
 		484
 		{
-			title = "Rotate Right";
+			title = "Rotate PolyObject Right";
 			prefix = "(484)";
 			flags8text = "[3] Set delay by backside sector";
 			flags64text = "[6] Don't turn players";
@@ -2535,7 +2569,7 @@ linedeftypes
 
 		485
 		{
-			title = "Rotate Right, Override";
+			title = "Rotate PolyObject Right, Override";
 			prefix = "(485)";
 			flags8text = "[3] Set delay by backside sector";
 			flags64text = "[6] Don't turn players";
@@ -2544,7 +2578,7 @@ linedeftypes
 
 		486
 		{
-			title = "Rotate Left";
+			title = "Rotate PolyObject Left";
 			prefix = "(486)";
 			flags8text = "[3] Set delay by backside sector";
 			flags64text = "[6] Don't turn players";
@@ -2553,7 +2587,7 @@ linedeftypes
 
 		487
 		{
-			title = "Rotate Left, Override";
+			title = "Rotate PolyObject Left, Override";
 			prefix = "(487)";
 			flags8text = "[3] Set delay by backside sector";
 			flags64text = "[6] Don't turn players";
@@ -2562,7 +2596,7 @@ linedeftypes
 
 		488
 		{
-			title = "Move by Waypoints";
+			title = "Move PolyObject by Waypoints";
 			prefix = "(488)";
 			flags8text = "[3] Set delay by backside sector";
 			flags32text = "[5] Reverse order";
@@ -2573,7 +2607,7 @@ linedeftypes
 
 		489
 		{
-			title = "Turn Invisible, Intangible";
+			title = "Turn PolyObject Invisible, Intangible";
 			prefix = "(489)";
 			flags8text = "[3] Set delay by backside sector";
 			flags64text = "[6] Only invisible";
@@ -2581,7 +2615,7 @@ linedeftypes
 
 		490
 		{
-			title = "Turn Visible, Tangible";
+			title = "Turn PolyObject Visible, Tangible";
 			prefix = "(490)";
 			flags8text = "[3] Set delay by backside sector";
 			flags64text = "[6] Only visible";
@@ -2589,7 +2623,7 @@ linedeftypes
 
 		491
 		{
-			title = "Set Translucency";
+			title = "Set PolyObject Translucency";
 			prefix = "(491)";
 			flags8text = "[3] Set delay by backside sector";
 			flags16text = "[4] Set raw alpha by Front X";
@@ -2598,7 +2632,7 @@ linedeftypes
 
 		492
 		{
-			title = "Fade Translucency";
+			title = "Fade PolyObject Translucency";
 			prefix = "(492)";
 			flags8text = "[3] Set delay by backside sector";
 			flags16text = "[4] Set raw alpha by Front X";
@@ -2917,8 +2951,10 @@ linedeftypes
 			prefix = "(700)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 1;
+			copyslopeargs = 1;
 		}
 
 		701
@@ -2927,8 +2963,10 @@ linedeftypes
 			prefix = "(701)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 2;
+			copyslopeargs = 4;
 		}
 
 		702
@@ -2937,8 +2975,10 @@ linedeftypes
 			prefix = "(702)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 3;
+			copyslopeargs = 5;
 		}
 
 		703
@@ -2947,8 +2987,10 @@ linedeftypes
 			prefix = "(703)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 9;
+			copyslopeargs = 8;
 		}
 
 		704
@@ -2979,8 +3021,10 @@ linedeftypes
 			prefix = "(710)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 4;
+			copyslopeargs = 2;
 		}
 
 		711
@@ -2989,8 +3033,10 @@ linedeftypes
 			prefix = "(711)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 8;
+			copyslopeargs = 8;
 		}
 
 		712
@@ -2999,8 +3045,10 @@ linedeftypes
 			prefix = "(712)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 12;
+			copyslopeargs = 10;
 		}
 
 		713
@@ -3009,8 +3057,10 @@ linedeftypes
 			prefix = "(713)";
 			flags2048text = "[11] No physics";
 			flags4096text = "[12] Dynamic";
+			flags32768text = "[15] Copy to other side";
 			slope = "regular";
 			slopeargs = 6;
+			copyslopeargs = 6;
 		}
 
 		714
@@ -3059,6 +3109,78 @@ linedeftypes
 			slopeargs = 3;
 		}
 
+		723
+		{
+			title = "Copy Backside Floor Slope from Line Tag";
+			prefix = "(720)";
+			slope = "copy";
+			slopeargs = 4;
+		}
+
+		724
+		{
+			title = "Copy Backside Ceiling Slope from Line Tag";
+			prefix = "(721)";
+			slope = "copy";
+			slopeargs = 8;
+		}
+
+		725
+		{
+			title = "Copy Backside Floor and Ceiling Slope from Line Tag";
+			prefix = "(722)";
+			slope = "copy";
+			slopeargs = 12;
+		}
+
+		730
+		{
+			title = "Copy Frontside Floor Slope to Backside";
+			prefix = "(730)";
+			slope = "copy";
+			copyslopeargs = 1;
+		}
+
+		731
+		{
+			title = "Copy Frontside Ceiling Slope to Backside";
+			prefix = "(731)";
+			slope = "copy";
+			copyslopeargs = 4;
+		}
+
+		732
+		{
+			title = "Copy Frontside Floor and Ceiling Slope to Backside";
+			prefix = "(732)";
+			slope = "copy";
+			copyslopeargs = 5;
+		}
+
+		733
+		{
+			title = "Copy Backside Floor Slope to Frontside";
+			prefix = "(733)";
+			slope = "copy";
+			copyslopeargs = 2;
+		}
+
+		734
+		{
+			title = "Copy Backside Ceiling Slope to Frontside";
+			prefix = "(734)";
+			slope = "copy";
+			copyslopeargs = 8;
+		}
+
+		735
+		{
+			title = "Copy Backside Floor and Ceiling Slope to Frontside";
+			prefix = "(735)";
+			slope = "copy";
+			copyslopeargs = 10;
+		}
+
 		799
 		{
 			title = "Set Tagged Dynamic Slope Vertex to Front Sector Height";
@@ -3393,6 +3515,7 @@ thingtypes
 			width = 8;
 			height = 28;
 			angletext = "Jump strength";
+			fixedrotation = 1;
 		}
 		103
 		{
@@ -3431,6 +3554,7 @@ thingtypes
 			width = 12;
 			height = 64;
 			angletext = "Firing delay";
+			fixedrotation = 1;
 		}
 		122
 		{
@@ -3482,6 +3606,7 @@ thingtypes
 			sprite = "ARCHA1";
 			width = 24;
 			height = 32;
+			flags8text = "[8] Don't jump away";
 		}
 		118
 		{
@@ -3547,9 +3672,10 @@ thingtypes
 		{
 			title = "Pterabyte Spawner";
 			sprite = "PTERA2A8";
-			width = 16;
-			height = 16;
-			parametertext = "No. Pterabytes";
+			width = 24;
+			height = 48;
+			parametertext = "Spawns +1";
+			arrow = 0;
 		}
 		136
 		{
@@ -3771,6 +3897,7 @@ thingtypes
 			height = 16;
 			sprite = "internal:capsule";
 			angletext = "Tag";
+			fixedrotation = 1;
 		}
 		292
 		{
@@ -3781,11 +3908,13 @@ thingtypes
 			flags8text = "[8] Sea Egg shooting point";
 			sprite = "internal:eggmanway";
 			angletext = "No. (Sea Egg)";
+			fixedrotation = 1;
 			flagsvaluetext = "No. (Brak)";
 			parametertext = "Next";
 		}
 		293
 		{
+			arrow = 0;
 			title = "Metal Sonic Gather Point";
 			sprite = "internal:metal";
 			width = 8;
@@ -3793,6 +3922,7 @@ thingtypes
 		}
 		294
 		{
+			arrow = 0;
 			title = "Fang Waypoint";
 			flags8text = "[8] Center waypoint";
 			sprite = "internal:eggmanway";
@@ -3820,79 +3950,79 @@ thingtypes
 		301
 		{
 			title = "Bounce Ring";
-			sprite = "internal:RNGBA0";
+			sprite = "RNGBA0";
 		}
 		302
 		{
 			title = "Rail Ring";
-			sprite = "internal:RNGRA0";
+			sprite = "RNGRA0";
 		}
 		303
 		{
 			title = "Infinity Ring";
-			sprite = "internal:RNGIA0";
+			sprite = "RNGIA0";
 		}
 		304
 		{
 			title = "Automatic Ring";
-			sprite = "internal:RNGAA0";
+			sprite = "RNGAA0";
 		}
 		305
 		{
 			title = "Explosion Ring";
-			sprite = "internal:RNGEA0";
+			sprite = "RNGEA0";
 		}
 		306
 		{
 			title = "Scatter Ring";
-			sprite = "internal:RNGSA0";
+			sprite = "RNGSA0";
 		}
 		307
 		{
 			title = "Grenade Ring";
-			sprite = "internal:RNGGA0";
+			sprite = "RNGGA0";
 		}
 		308
 		{
 			title = "CTF Team Ring (Red)";
-			sprite = "internal:RRNGA0";
+			sprite = "internal:TRNGA0r";
 			width = 16;
 		}
 		309
 		{
 			title = "CTF Team Ring (Blue)";
-			sprite = "internal:BRNGA0";
+			sprite = "internal:TRNGA0b";
 			width = 16;
 		}
 		330
 		{
 			title = "Bounce Ring Panel";
-			sprite = "internal:PIKBA0";
+			sprite = "PIKBA0";
 		}
 		331
 		{
 			title = "Rail Ring Panel";
-			sprite = "internal:PIKRA0";
+			sprite = "PIKRA0";
 		}
 		332
 		{
 			title = "Automatic Ring Panel";
-			sprite = "internal:PIKAA0";
+			sprite = "PIKAA0";
 		}
 		333
 		{
 			title = "Explosion Ring Panel";
-			sprite = "internal:PIKEA0";
+			sprite = "PIKEA0";
 		}
 		334
 		{
 			title = "Scatter Ring Panel";
-			sprite = "internal:PIKSA0";
+			sprite = "PIKSA0";
 		}
 		335
 		{
 			title = "Grenade Ring Panel";
-			sprite = "internal:PIKGA0";
+			sprite = "PIKGA0";
 		}
 	}
 
@@ -3986,6 +4116,7 @@ thingtypes
 			flags8height = 24;
 			flags8text = "[8] Float";
 			angletext = "Tag";
+			fixedrotation = 1;
 		}
 	}
 
@@ -4000,6 +4131,7 @@ thingtypes
 		flags4text = "[4] Random (Strong)";
 		flags8text = "[8] Random (Weak)";
 		angletext = "Tag";
+		fixedrotation = 1;
 
 		400
 		{
@@ -4131,6 +4263,7 @@ thingtypes
 		height = 44;
 		flags1text = "[1] Run linedef executor on pop";
 		angletext = "Tag";
+		fixedrotation = 1;
 
 		431
 		{
@@ -4228,6 +4361,7 @@ thingtypes
 			height = 128;
 			flags4text = "[4] Respawn at center";
 			angletext = "Angle/Order";
+			fixedrotation = 1;
 			parametertext = "Order";
 		}
 		520
@@ -4259,7 +4393,7 @@ thingtypes
 			flags1text = "[1] Start retracted";
 			flags4text = "[4] Retractable";
 			flags8text = "[8] Intangible";
-			parametertext = "Initial delay";
+			parametertext = "Start delay";
 		}
 		523
 		{
@@ -4271,7 +4405,8 @@ thingtypes
 			flags4text = "[4] Retractable";
 			flags8text = "[8] Intangible";
 			angletext = "Retraction interval";
-			parametertext = "Initial delay";
+			fixedrotation = 1;
+			parametertext = "Start delay";
 		}
 		1130
 		{
@@ -4320,6 +4455,7 @@ thingtypes
 			flags4text = "[4] Invisible";
 			flags8text = "[8] No distance check";
 			angletext = "Lift height";
+			fixedrotation = 1;
 		}
 		541
 		{
@@ -4335,6 +4471,7 @@ thingtypes
 			width = 32;
 			height = 64;
 			angletext = "Strength";
+			fixedrotation = 1;
 		}
 		543
 		{
@@ -4344,6 +4481,7 @@ thingtypes
 			height = 64;
 			flags8text = "[8] Respawn";
 			angletext = "Color";
+			fixedrotation = 1;
 		}
 		550
 		{
@@ -4617,6 +4755,9 @@ thingtypes
 			title = "Slope Vertex";
 			sprite = "internal:vertexslope";
 			angletext = "Tag";
+			fixedrotation = 1;
+			parametertext = "Absolute?";
+			flagsvaluetext = "Absolute Z";
 		}
 
 		751
@@ -4638,6 +4779,7 @@ thingtypes
 			title = "Zoom Tube Waypoint";
 			sprite = "internal:zoom";
 			angletext = "Order";
+			fixedrotation = 1;
 		}
 
 		754
@@ -4647,6 +4789,7 @@ thingtypes
 			flags8text = "[8] Push using XYZ";
 			sprite = "GWLGA0";
 			angletext = "Radius";
+			fixedrotation = 1;
 		}
 		755
 		{
@@ -4655,6 +4798,7 @@ thingtypes
 			flags8text = "[8] Pull using XYZ";
 			sprite = "GWLRA0";
 			angletext = "Radius";
+			fixedrotation = 1;
 		}
 		756
 		{
@@ -4663,6 +4807,7 @@ thingtypes
 			width = 32;
 			height = 16;
 			angletext = "Tag";
+			fixedrotation = 1;
 		}
 		757
 		{
@@ -4671,6 +4816,7 @@ thingtypes
 			width = 8;
 			height = 16;
 			angletext = "Tag";
+			fixedrotation = 1;
 		}
 		758
 		{
@@ -4681,21 +4827,24 @@ thingtypes
 		{
 			title = "PolyObject Anchor";
 			sprite = "internal:polyanchor";
-			angletext = "ID";
+			angletext = "Tag";
+			fixedrotation = 1;
 		}
 
 		761
 		{
 			title = "PolyObject Spawn Point";
 			sprite = "internal:polycenter";
-			angletext = "ID";
+			angletext = "Tag";
+			fixedrotation = 1;
 		}
 
 		762
 		{
 			title = "PolyObject Spawn Point (Crush)";
 			sprite = "internal:polycentercrush";
-			angletext = "ID";
+			angletext = "Tag";
+			fixedrotation = 1;
 		}
 		780
 		{
@@ -4703,6 +4852,7 @@ thingtypes
 			sprite = "internal:skyb";
 			flags4text = "[4] In-map centerpoint";
 			parametertext = "ID";
+			fixedrotation = 1;
 		}
 	}
 
@@ -4897,6 +5047,7 @@ thingtypes
 			height = 16;
 			hangs = 1;
 			angletext = "Dripping interval";
+			fixedrotation = 1;
 		}
 		1003
 		{
@@ -4953,7 +5104,7 @@ thingtypes
 		1011
 		{
 			title = "Stalagmite (DSZ2)";
-			sprite = "DSTGA0";
+			sprite = "DSTGB0";
 			width = 8;
 			height = 116;
 			flags4text = "[4] Double size";
@@ -5038,6 +5189,8 @@ thingtypes
 			flags4text = "[4] No sounds";
 			flags8text = "[8] Double size";
 			angletext = "Tag";
+			parametertext = "Spokes";
+			fixedrotation = 1;
 		}
 		1105
 		{
@@ -5048,6 +5201,8 @@ thingtypes
 			flags4text = "[4] No sounds";
 			flags8text = "[8] Double size";
 			angletext = "Tag";
+			parametertext = "Spokes";
+			fixedrotation = 1;
 		}
 		1106
 		{
@@ -5058,6 +5213,8 @@ thingtypes
 			flags4text = "[4] No sounds";
 			flags8text = "[8] Red spring";
 			angletext = "Tag";
+			parametertext = "Spokes";
+			fixedrotation = 1;
 		}
 		1107
 		{
@@ -5067,6 +5224,8 @@ thingtypes
 			height = 34;
 			flags8text = "[8] Double size";
 			angletext = "Tag";
+			parametertext = "Spokes";
+			fixedrotation = 1;
 		}
 		1108
 		{
@@ -5086,6 +5245,8 @@ thingtypes
 			flags4text = "[4] No sounds";
 			flags8text = "[8] Double size";
 			angletext = "Tag";
+			parametertext = "Spokes";
+			fixedrotation = 1;
 		}
 		1110
 		{
@@ -5095,6 +5256,8 @@ thingtypes
 			height = 34;
 			flags4text = "[4] No sounds";
 			angletext = "Tag";
+			parametertext = "Spokes";
+			fixedrotation = 1;
 		}
 		1111
 		{
@@ -5224,6 +5387,7 @@ thingtypes
 			sprite = "EGR1A1";
 			width = 20;
 			height = 72;
+			arrow = 1;
 		}
 		1128
 		{
@@ -5272,6 +5436,7 @@ thingtypes
 			width = 8;
 			height = 16;
 			angletext = "Tag";
+			fixedrotation = 1;
 		}
 		1203
 		{
@@ -5342,6 +5507,7 @@ thingtypes
 			sprite = "WWSGAR";
 			width = 22;
 			height = 64;
+			arrow = 1;
 		}
 		1213
 		{
@@ -5349,6 +5515,7 @@ thingtypes
 			sprite = "WWS2AR";
 			width = 22;
 			height = 64;
+			arrow = 1;
 		}
 		1214
 		{
@@ -5356,6 +5523,7 @@ thingtypes
 			sprite = "WWS3ALAR";
 			width = 16;
 			height = 192;
+			arrow = 1;
 		}
 		1215
 		{
@@ -5371,6 +5539,7 @@ thingtypes
 			sprite = "BARRA1";
 			width = 24;
 			height = 63;
+			arrow = 1;
 		}
 		1217
 		{
@@ -5392,6 +5561,7 @@ thingtypes
 			sprite = "MCRTCLFR";
 			width = 22;
 			height = 32;
+			arrow = 1;
 		}
 		1220
 		{
@@ -5399,6 +5569,7 @@ thingtypes
 			sprite = "MCRTIR";
 			width = 32;
 			height = 32;
+			arrow = 1;
 		}
 		1221
 		{
@@ -5406,6 +5577,7 @@ thingtypes
 			sprite = "SALDARAL";
 			width = 96;
 			height = 160;
+			arrow = 1;
 			flags8text = "[8] Allow non-minecart players";
 		}
 		1222
@@ -5467,6 +5639,7 @@ thingtypes
 			height = 40;
 			flags8text = "[8] Waves vertically";
 			angletext = "On/Off time";
+			fixedrotation = 1;
 			parametertext = "Strength";
 		}
 		1301
@@ -5477,6 +5650,7 @@ thingtypes
 			height = 40;
 			flags8text = "[8] Shoot downwards";
 			angletext = "On/Off time";
+			fixedrotation = 1;
 			parametertext = "Strength";
 		}
 		1302
@@ -5500,6 +5674,7 @@ thingtypes
 			width = 30;
 			height = 32;
 			angletext = "Initial delay";
+			fixedrotation = 1;
 			flags8text = "[8] Double size";
 		}
 		1305
@@ -5537,6 +5712,7 @@ thingtypes
 			sprite = "WVINALAR";
 			width = 1;
 			height = 288;
+			arrow = 1;
 		}
 		1310
 		{
@@ -5544,6 +5720,7 @@ thingtypes
 			sprite = "WVINBLBR";
 			width = 1;
 			height = 288;
+			arrow = 1;
 		}
 	}
 
@@ -5901,6 +6078,7 @@ thingtypes
 		width = 8;
 		height = 4096;
 		sprite = "UNKNA0";
+		fixedrotation = 1;
 
 		1700
 		{
@@ -5959,6 +6137,7 @@ thingtypes
 			flags4text = "[4] Align player to top";
 			flags8text = "[8] Die upon time up";
 			angletext = "Time limit";
+			fixedrotation = 1;
 			parametertext = "Height";
 		}
 		1704
@@ -5971,6 +6150,7 @@ thingtypes
 			unflippable = true;
 			flagsvaluetext = "Pitch";
 			angletext = "Yaw";
+			fixedrotation = 1;
 		}
 		1705
 		{
@@ -5983,6 +6163,7 @@ thingtypes
 			centerHitbox = true;
 			flagsvaluetext = "Height";
 			angletext = "Pitch/Yaw";
+			fixedrotation = 1;
 		}
 		1706
 		{
@@ -6104,6 +6285,7 @@ thingtypes
 			width = 8;
 			height = 16;
 			angletext = "Jump strength";
+			fixedrotation = 1;
 		}
 		1806
 		{
@@ -6336,6 +6518,7 @@ thingtypes
 			width = 18;
 			height = 28;
 			angletext = "Initial delay";
+			fixedrotation = 1;
 		}
 		2001
 		{
@@ -6459,6 +6642,7 @@ thingtypes
 			sprite = "XMS6A0";
 			width = 52;
 			height = 106;
+			hangs = 1;
 		}
 	}
 
@@ -6472,6 +6656,7 @@ thingtypes
 		flags4text = "[4] No movement";
 		flags8text = "[8] Hop";
 		angletext = "Radius";
+		fixedrotation = 1;
 
 		2200
 		{
diff --git a/extras/conf/udb/Includes/SRB222_things.cfg b/extras/conf/udb/Includes/SRB222_things.cfg
index 0ea452155181cfd080a65a0713afd8e966531196..113c1a4c26eb08a7109da2afeba747dfb1cdda14 100644
--- a/extras/conf/udb/Includes/SRB222_things.cfg
+++ b/extras/conf/udb/Includes/SRB222_things.cfg
@@ -1247,6 +1247,7 @@ patterns
 		sprite = "SPHRA0";
 		width = 96;
 		height = 192;
+	}
 	609
 	{
 		title = "Circle of Rings and Spheres (Big)";
diff --git a/objs/.gitignore b/objs/.gitignore
deleted file mode 100644
index 35ecd6def21e7cdb60882510005e3b9833df5a08..0000000000000000000000000000000000000000
--- a/objs/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-#All folders
-SRB2.res
-depend.dep
-depend.ped
-*.o
-#VC9 folder only
-/VC9/Win32
-/VC9/x64
diff --git a/objs/FreeBSD/SDL/Debug/.gitignore b/objs/FreeBSD/SDL/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/FreeBSD/SDL/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/FreeBSD/SDL/Release/.gitignore b/objs/FreeBSD/SDL/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/FreeBSD/SDL/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/Linux/SDL/Debug/.gitignore b/objs/Linux/SDL/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/Linux/SDL/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/Linux/SDL/Release/.gitignore b/objs/Linux/SDL/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/Linux/SDL/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/Linux64/SDL/Debug/.gitignore b/objs/Linux64/SDL/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/Linux64/SDL/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/Linux64/SDL/Release/.gitignore b/objs/Linux64/SDL/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/Linux64/SDL/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/MasterClient/.gitignore b/objs/MasterClient/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/MasterClient/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/MasterServer/.gitignore b/objs/MasterServer/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/MasterServer/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/Mingw/Debug/.gitignore b/objs/Mingw/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/Mingw/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/Mingw/Release/.gitignore b/objs/Mingw/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/Mingw/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/Mingw/SDL/Debug/.gitignore b/objs/Mingw/SDL/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/Mingw/SDL/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/Mingw/SDL/Release/.gitignore b/objs/Mingw/SDL/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/Mingw/SDL/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/Mingw64/Debug/.gitignore b/objs/Mingw64/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/Mingw64/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/Mingw64/Release/.gitignore b/objs/Mingw64/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/Mingw64/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/Mingw64/SDL/Debug/.gitignore b/objs/Mingw64/SDL/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/Mingw64/SDL/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/Mingw64/SDL/Release/.gitignore b/objs/Mingw64/SDL/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/Mingw64/SDL/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/SDL/Release/.gitignore b/objs/SDL/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/SDL/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/VC/.gitignore b/objs/VC/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/VC/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/VC9/.gitignore b/objs/VC9/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/VC9/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/cygwin/Debug/.gitignore b/objs/cygwin/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/cygwin/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/cygwin/Release/.gitignore b/objs/cygwin/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/cygwin/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/objs/dummy/.gitignore b/objs/dummy/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/objs/dummy/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index d35e774e934d5b624b4d2bcc5c19b5bc24b6abf6..ae93aac370b8fb93f22f4bbdce9aa48ac6aed94a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,238 +1,14 @@
 # SRB2 Core
 
-# Core sources
-set(SRB2_CORE_SOURCES
-	am_map.c
-	b_bot.c
-	command.c
-	comptime.c
-	console.c
-	d_clisrv.c
-	d_main.c
-	d_net.c
-	d_netcmd.c
-	d_netfil.c
-	dehacked.c
-	deh_soc.c
-	deh_lua.c
-	deh_tables.c
-	f_finale.c
-	f_wipe.c
-	filesrch.c
-	g_demo.c
-	g_game.c
-	g_input.c
-	hu_stuff.c
-	i_tcp.c
-	info.c
-	lzf.c
-	m_aatree.c
-	m_anigif.c
-	m_argv.c
-	m_bbox.c
-	m_cheat.c
-	m_cond.c
-	m_fixed.c
-	m_menu.c
-	m_misc.c
-	m_perfstats.c
-	m_queue.c
-	m_random.c
-	md5.c
-	mserv.c
-	http-mserv.c
-	s_sound.c
-	screen.c
-	sounds.c
-	st_stuff.c
-	#string.c
-	tables.c
-	v_video.c
-	w_wad.c
-	y_inter.c
-	z_zone.c
-)
-
-set(SRB2_CORE_HEADERS
-	am_map.h
-	b_bot.h
-	byteptr.h
-	command.h
-	console.h
-	d_clisrv.h
-	d_event.h
-	d_main.h
-	d_net.h
-	d_netcmd.h
-	d_netfil.h
-	d_player.h
-	d_think.h
-	d_ticcmd.h
-	dehacked.h
-	deh_soc.h
-	deh_lua.h
-	deh_tables.h
-	doomdata.h
-	doomdef.h
-	doomstat.h
-	doomtype.h
-	endian.h
-	f_finale.h
-	fastcmp.h
-	filesrch.h
-	g_demo.h
-	g_game.h
-	g_input.h
-	g_state.h
-	hu_stuff.h
-	i_joy.h
-	i_net.h
-	i_sound.h
-	i_system.h
-	i_tcp.h
-	i_video.h
-	info.h
-	keys.h
-	lzf.h
-	m_aatree.h
-	m_anigif.h
-	m_argv.h
-	m_bbox.h
-	m_cheat.h
-	m_cond.h
-	m_dllist.h
-	m_fixed.h
-	m_menu.h
-	m_misc.h
-	m_perfstats.h
-	m_queue.h
-	m_random.h
-	m_swap.h
-	md5.h
-	mserv.h
-	p5prof.h
-	s_sound.h
-	screen.h
-	sounds.h
-	st_stuff.h
-	tables.h
-	v_video.h
-	w_wad.h
-	y_inter.h
-	z_zone.h
-
-	config.h.in
-)
-
-set(SRB2_CORE_RENDER_SOURCES
-	r_bsp.c
-	r_data.c
-	r_draw.c
-	r_main.c
-	r_plane.c
-	r_segs.c
-	r_skins.c
-	r_sky.c
-	r_splats.c
-	r_things.c
-	r_textures.c
-	r_patch.c
-	r_patchrotation.c
-	r_picformats.c
-	r_portal.c
-
-	r_bsp.h
-	r_data.h
-	r_defs.h
-	r_draw.h
-	r_local.h
-	r_main.h
-	r_plane.h
-	r_segs.h
-	r_skins.h
-	r_sky.h
-	r_splats.h
-	r_state.h
-	r_things.h
-	r_textures.h
-	r_patch.h
-	r_patchrotation.h
-	r_picformats.h
-	r_portal.h
-)
-
-set(SRB2_CORE_GAME_SOURCES
-	p_ceilng.c
-	p_enemy.c
-	p_floor.c
-	p_inter.c
-	p_lights.c
-	p_map.c
-	p_maputl.c
-	p_mobj.c
-	p_polyobj.c
-	p_saveg.c
-	p_setup.c
-	p_sight.c
-	p_slopes.c
-	p_spec.c
-	p_telept.c
-	p_tick.c
-	p_user.c
-	taglist.c
-
-	p_local.h
-	p_maputl.h
-	p_mobj.h
-	p_polyobj.h
-	p_pspr.h
-	p_saveg.h
-	p_setup.h
-	p_slopes.h
-	p_spec.h
-	p_tick.h
-	taglist.h
-)
-
-if(NOT (CMAKE_CXX_COMPILER_ID MATCHES "Clang"))
-	set(SRB2_CORE_SOURCES ${SRB2_CORE_SOURCES} string.c)
-endif()
-
-prepend_sources(SRB2_CORE_SOURCES)
-prepend_sources(SRB2_CORE_HEADERS)
-prepend_sources(SRB2_CORE_RENDER_SOURCES)
-prepend_sources(SRB2_CORE_GAME_SOURCES)
-
-set(SRB2_CORE_HEADERS ${SRB2_CORE_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/config.h)
-source_group("Main" FILES ${SRB2_CORE_SOURCES} ${SRB2_CORE_HEADERS})
-source_group("Renderer" FILES ${SRB2_CORE_RENDER_SOURCES})
-source_group("Game" FILES ${SRB2_CORE_GAME_SOURCES})
-
-
-set(SRB2_ASM_SOURCES
-	${CMAKE_CURRENT_SOURCE_DIR}/vid_copy.s
-)
-
-set(SRB2_NASM_SOURCES
-	${CMAKE_CURRENT_SOURCE_DIR}/tmap_mmx.nas
-	${CMAKE_CURRENT_SOURCE_DIR}/tmap.nas
-)
-
-if(MSVC)
-	list(APPEND SRB2_NASM_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/tmap_vc.nas)
-endif()
+add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32)
 
-set(SRB2_NASM_OBJECTS
-	${CMAKE_CURRENT_BINARY_DIR}/tmap_mmx.obj
-	${CMAKE_CURRENT_BINARY_DIR}/tmap.obj
-)
-
-if(MSVC)
-	list(APPEND SRB2_NASM_OBJECTS ${CMAKE_CURRENT_BINARY_DIR}/tmap_vc.obj)
-endif()
+# Core sources
+target_sourcefile(c)
+target_sources(SRB2SDL2 PRIVATE comptime.c md5.c config.h.in)
 
-source_group("Assembly" FILES ${SRB2_ASM_SOURCES} ${SRB2_NASM_SOURCES})
+set(SRB2_ASM_SOURCES vid_copy.s)
 
+set(SRB2_NASM_SOURCES tmap_mmx.nas tmap.nas)
 
 ### Configuration
 set(SRB2_CONFIG_HAVE_PNG ON CACHE BOOL
@@ -268,91 +44,7 @@ if(${CMAKE_SYSTEM} MATCHES "Windows") ###set on Windows only
 	"Use SRB2's internal copies of required dependencies (SDL2, PNG, zlib, GME, OpenMPT).")
 endif()
 
-set(SRB2_LUA_SOURCES
-	lua_baselib.c
-	lua_blockmaplib.c
-	lua_consolelib.c
-	lua_hooklib.c
-	lua_hudlib.c
-	lua_infolib.c
-	lua_maplib.c
-	lua_mathlib.c
-	lua_mobjlib.c
-	lua_playerlib.c
-	lua_polyobjlib.c
-	lua_script.c
-	lua_skinlib.c
-	lua_thinkerlib.c
-)
-set(SRB2_LUA_HEADERS
-	lua_hook.h
-	lua_hud.h
-	lua_libs.h
-	lua_script.h
-)
-
-prepend_sources(SRB2_LUA_SOURCES)
-prepend_sources(SRB2_LUA_HEADERS)
-
-source_group("LUA" FILES ${SRB2_LUA_SOURCES} ${SRB2_LUA_HEADERS})
-
-set(SRB2_BLUA_SOURCES
-	blua/lapi.c
-	blua/lauxlib.c
-	blua/lbaselib.c
-	blua/lcode.c
-	blua/ldebug.c
-	blua/ldo.c
-	blua/ldump.c
-	blua/lfunc.c
-	blua/lgc.c
-	blua/linit.c
-	blua/liolib.c
-	blua/llex.c
-	blua/lmem.c
-	blua/lobject.c
-	blua/lopcodes.c
-	blua/lparser.c
-	blua/lstate.c
-	blua/lstring.c
-	blua/lstrlib.c
-	blua/ltable.c
-	blua/ltablib.c
-	blua/ltm.c
-	blua/lundump.c
-	blua/lvm.c
-	blua/lzio.c
-)
-set(SRB2_BLUA_HEADERS
-	blua/lapi.h
-	blua/lauxlib.h
-	blua/lcode.h
-	blua/ldebug.h
-	blua/ldo.h
-	blua/lfunc.h
-	blua/lgc.h
-	blua/llex.h
-	blua/llimits.h
-	blua/lmem.h
-	blua/lobject.h
-	blua/lopcodes.h
-	blua/lparser.h
-	blua/lstate.h
-	blua/lstring.h
-	blua/ltable.h
-	blua/ltm.h
-	blua/lua.h
-	blua/luaconf.h
-	blua/lualib.h
-	blua/lundump.h
-	blua/lvm.h
-	blua/lzio.h
-)
-
-prepend_sources(SRB2_BLUA_SOURCES)
-prepend_sources(SRB2_BLUA_HEADERS)
-
-source_group("LUA\\Interpreter" FILES ${SRB2_BLUA_SOURCES} ${SRB2_BLUA_HEADERS})
+add_subdirectory(blua)
 
 if(${SRB2_CONFIG_HAVE_GME})
 	if(${SRB2_CONFIG_USE_INTERNAL_LIBRARIES})
@@ -368,7 +60,7 @@ if(${SRB2_CONFIG_HAVE_GME})
 	endif()
 	if(${GME_FOUND})
 		set(SRB2_HAVE_GME ON)
-		add_definitions(-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()
@@ -388,7 +80,7 @@ if(${SRB2_CONFIG_HAVE_OPENMPT})
 	endif()
 	if(${OPENMPT_FOUND})
 		set(SRB2_HAVE_OPENMPT ON)
-		add_definitions(-DHAVE_OPENMPT)
+		target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_OPENMPT)
 	else()
 		message(WARNING "You have specified that OpenMPT is available but it was not found.")
 	endif()
@@ -411,8 +103,7 @@ if(${SRB2_CONFIG_HAVE_MIXERX})
 	endif()
 	if(${MIXERX_FOUND})
 		set(SRB2_HAVE_MIXERX ON)
-		set(SRB2_SDL2_SOUNDIMPL mixer_sound.c)
-		add_definitions(-DHAVE_MIXERX)
+		target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_MIXERX)
 	else()
 		message(WARNING "You have specified that SDL Mixer X is available but it was not found.")
 	endif()
@@ -432,7 +123,7 @@ if(${SRB2_CONFIG_HAVE_ZLIB})
 	endif()
 	if(${ZLIB_FOUND})
 		set(SRB2_HAVE_ZLIB ON)
-		add_definitions(-DHAVE_ZLIB)
+		target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_ZLIB)
 	else()
 		message(WARNING "You have specified that ZLIB is available but it was not found. SRB2 may not compile correctly.")
 	endif()
@@ -453,14 +144,9 @@ if(${SRB2_CONFIG_HAVE_PNG} AND ${SRB2_CONFIG_HAVE_ZLIB})
 		endif()
 		if(${PNG_FOUND})
 			set(SRB2_HAVE_PNG ON)
-			add_definitions(-DHAVE_PNG)
-			add_definitions(-D_LARGEFILE64_SOURCE)
-			set(SRB2_PNG_SOURCES apng.c)
-			set(SRB2_PNG_HEADERS apng.h)
-			prepend_sources(SRB2_PNG_SOURCES)
-			prepend_sources(SRB2_PNG_HEADERS)
-			source_group("Main" FILES ${SRB2_CORE_SOURCES} ${SRB2_CORE_HEADERS}
-				${SRB2_PNG_SOURCES} ${SRB2_PNG_HEADERS})
+			target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_PNG)
+			target_compile_definitions(SRB2SDL2 PRIVATE -D_LARGEFILE64_SOURCE)
+			target_sources(SRB2SDL2 PRIVATE apng.c)
 		else()
 			message(WARNING "You have specified that PNG is available but it was not found. SRB2 may not compile correctly.")
 		endif()
@@ -481,7 +167,7 @@ if(${SRB2_CONFIG_HAVE_CURL})
 	endif()
 	if(${CURL_FOUND})
 		set(SRB2_HAVE_CURL ON)
-		add_definitions(-DHAVE_CURL)
+		target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_CURL)
 	else()
 		message(WARNING "You have specified that CURL is available but it was not found. SRB2 may not compile correctly.")
 	endif()
@@ -489,59 +175,19 @@ endif()
 
 if(${SRB2_CONFIG_HAVE_THREADS})
 	set(SRB2_HAVE_THREADS ON)
-	set(SRB2_CORE_HEADERS ${SRB2_CORE_HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/i_threads.h)
-	add_definitions(-DHAVE_THREADS)
+	target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_THREADS)
 endif()
 
 if(${SRB2_CONFIG_HWRENDER})
-	add_definitions(-DHWRENDER)
-	set(SRB2_HWRENDER_SOURCES
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_batching.c
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_bsp.c
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_cache.c
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_clip.c
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_draw.c
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_light.c
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_main.c
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_md2.c
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_md2load.c
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_md3load.c
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_model.c
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/u_list.c
-	)
-
-	set (SRB2_HWRENDER_HEADERS
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_batching.h
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_clip.h
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_data.h
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_defs.h
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_dll.h
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_drv.h
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_glob.h
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_light.h
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_main.h
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_md2.h
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_md2load.h
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_md3load.h
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_model.h
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/u_list.h
-	)
-
-	set(SRB2_R_OPENGL_SOURCES
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/r_opengl/r_opengl.c
-	)
-
-	set(SRB2_R_OPENGL_HEADERS
-		${CMAKE_CURRENT_SOURCE_DIR}/hardware/r_opengl/r_opengl.h
-	)
-
+	target_compile_definitions(SRB2SDL2 PRIVATE -DHWRENDER)
+	add_subdirectory(hardware)
 endif()
 
 if(${SRB2_CONFIG_HWRENDER} AND ${SRB2_CONFIG_STATIC_OPENGL})
 	find_package(OpenGL)
 	if(${OPENGL_FOUND})
-		add_definitions(-DHWRENDER)
-		add_definitions(-DSTATIC_OPENGL)
+		target_compile_definitions(SRB2SDL2 PRIVATE -DHWRENDER)
+		target_compile_definitions(SRB2SDL2 PRIVATE -DSTATIC_OPENGL)
 	else()
 		message(WARNING "You have specified static opengl but opengl was not found. Not setting HWRENDER.")
 	endif()
@@ -562,12 +208,16 @@ if(${SRB2_CONFIG_USEASM})
 		set(CMAKE_ASM_NASM_FLAGS "${SRB2_ASM_FLAGS}" CACHE STRING "Flags used by the assembler during all build types.")
 		enable_language(ASM_NASM)
 	endif()
+
 	set(SRB2_USEASM ON)
-	add_definitions(-DUSEASM)
+	target_compile_definitions(SRB2SDL2 PRIVATE -DUSEASM)
 	set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -msse3 -mfpmath=sse")
+
+	target_sources(SRB2SDL2 PRIVATE ${SRB2_ASM_SOURCES}
+		${SRB2_NASM_SOURCES})
 else()
 	set(SRB2_USEASM OFF)
-	add_definitions(-DNONX86 -DNORUSEASM)
+	target_compile_definitions(SRB2SDL2 PRIVATE -DNONX86 -DNORUSEASM)
 endif()
 
 # Targets
@@ -603,7 +253,9 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
 	set(CMAKE_C_FLAGS ${CMAKE_C_FLAGS} -Wno-absolute-value)
 endif()
 
-add_definitions(-DCMAKECONFIG)
+set(CMAKE_C_FLAGS ${CMAKE_C_FLAGS} -Wno-trigraphs)
+
+target_compile_definitions(SRB2SDL2 PRIVATE -DCMAKECONFIG)
 
 #add_library(SRB2Core STATIC
 #	${SRB2_CORE_SOURCES}
diff --git a/src/Makefile b/src/Makefile
index 0c1626fc955c5465f9ae23b62875bf9395c73e25..9659a4994c1dce6e94981ff090b0b4f1e9a171bf 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -1,875 +1,416 @@
-
-#     GNU Make makefile for SRB2
-#############################################################################
-# Copyright (C) 1998-2000 by DooM Legacy Team.
-# Copyright (C) 2003-2020 by Sonic Team Junior.
+# GNU Makefile for SRB2
+# the poly3 Makefile adapted over and over...
+#
+# Copyright 1998-2000 DooM Legacy Team.
+# Copyright 2020-2021 James R.
+# Copyright 2003-2021 Sonic Team Junior.
 #
 # This program is free software distributed under the
 # terms of the GNU General Public License, version 2.
 # See the 'LICENSE' file for more details.
 #
-#     -DLINUX     -> use for the GNU/Linux specific
-#     -D_WINDOWS  -> use for the Win32/DirectX specific
-#     -DHAVE_SDL  -> use for the SDL interface
+# Special targets:
 #
-# Sets:
-#     Compile the DirectX/Mingw version with 'make MINGW=1'
-#     Compile the SDL/Mingw version with 'make MINGW=1 SDL=1'
-#     Compile the SDL/Linux version with 'make LINUX=1'
-#     Compile the SDL/Solaris version with 'make SOLARIS=1'
-#     Compile the SDL/FreeBSD version with 'gmake FREEBSD=1'
-#     Compile the SDL/Cygwin version with 'make CYGWIN32=1'
-#     Compile the SDL/other version try with 'make SDL=1'
+# clean - remove executables and objects for this build
+# cleandep - remove dependency files for this build
+# distclean - remove entire executable, object and
+#             dependency file directory structure.
+# dump - disassemble executable
+# info - print settings
 #
-# 'Targets':
-#     clean
-#       Remove all object files
-#     cleandep
-#       Remove depend.dep
-#     dll
-#       compile primary HW render DLL/SO
-#     all_dll
-#       compile all HW render and 3D sound DLLs for the set
-#     opengl_dll
-#       Pure Mingw only, compile OpenGL HW render DLL
-#     ds3d_dll
-#       Pure Mingw only, compile DirectX DirectSound HW sound DLL
-#     fmod_dll
-#       Pure Mingw only, compile FMOD HW sound DLL
-#     openal_dll
-#       Pure Mingw only, compile OpenAL HW sound DLL
-#     fmod_so
-#       Non-Mingw, compile FMOD HW sound SO
-#     openal_so
-#       Non-Mingw, compile OpenAL HW sound SO
+# This Makefile can automatically detect the host system
+# as well as the compiler version. If system or compiler
+# version cannot be detected, you may need to set a flag
+# manually.
 #
+# On Windows machines, 32-bit Windows is always targetted.
 #
-# Addon:
-#     To Cross-Compile, CC=gcc-version make * PREFIX=<dir>
-#     Compile with GCC 2.97 version, add 'GCC29=1'
-#     Compile with GCC 4.0x version, add 'GCC40=1'
-#     Compile with GCC 4.1x version, add 'GCC41=1'
-#     Compile with GCC 4.2x version, add 'GCC42=1'
-#     Compile with GCC 4.3x version, add 'GCC43=1'
-#     Compile with GCC 4.4x version, add 'GCC44=1'
-#     Compile with GCC 4.5x version, add 'GCC45=1'
-#     Compile with GCC 4.6x version, add 'GCC46=1'
-#     Compile a profile version, add 'PROFILEMODE=1'
-#     Compile a debug version, add 'DEBUGMODE=1'
-#     Compile with less warnings, add 'RELAXWARNINGS=1'
-#     Generate compiler errors for most compiler warnings, add 'ERRORMODE=1'
-#     Compile without NASM's tmap.nas, add 'NOASM=1'
-#     Compile without 3D hardware support, add 'NOHW=1'
-#     Compile with GDBstubs, add 'RDB=1'
-#     Compile without PNG, add 'NOPNG=1'
-#     Compile without zlib, add 'NOZLIB=1'
+# Platform/system flags:
 #
-# Addon for SDL:
-#     To Cross-Compile, add 'SDL_CONFIG=/usr/*/bin/sdl-config'
-#     Compile without SDL_Mixer, add 'NOMIXER=1'
-#     Compile without SDL_Mixer_X, add 'NOMIXERX=1' (Win32 only)
-#     Compile without GME, add 'NOGME=1'
-#     Compile without BSD API, add 'NONET=1'
-#     Compile without IPX/SPX, add 'NOIPX=1'
-#     Compile Mingw/SDL with S_DS3S, add 'DS3D=1'
-#     Compile without libopenmpt, add 'NOOPENMPT=1'
-#     Compile with S_FMOD3D, add 'FMOD=1' (WIP)
-#     Compile with S_OPENAL, add 'OPENAL=1' (WIP)
-#     To link with the whole SDL_Image lib to load Icons, add 'SDL_IMAGE=1' but it isn't not realy needed
-#     To link with SDLMain to hide console or make on a console-less binary, add 'SDLMAIN=1'
+# LINUX=1, LINUX64=1
+# MINGW=1, MINGW64=1 - Windows (MinGW toolchain)
+# UNIX=1 - Generic Unix like system
+# FREEBSD=1
+# SDL=1 - Use SDL backend. SDL is the only backend though
+#         and thus, always enabled.
 #
-#############################################################################
-
-ALL_SYSTEMS=\
-	PANDORA\
-	LINUX64\
-	MINGW64\
-	HAIKU\
-	DUMMY\
-	DJGPPDOS\
-	MINGW\
-	UNIX\
-	LINUX\
-	SOLARIS\
-	FREEBSD\
-	MACOSX\
-	SDL\
-
-# check for user specified system
-ifeq (,$(filter $(ALL_SYSTEMS),$(.VARIABLES)))
-ifeq ($(OS),Windows_NT) # all windows are Windows_NT...
-
- $(info Detected a Windows system, compiling for 32-bit MinGW SDL2...)
-
- # go for a 32-bit sdl mingw exe by default
- MINGW=1
- SDL=1
- WINDOWSHELL=1
-
-else # if you on the *nix
-
- system:=$(shell uname -s)
-
- ifeq ($(system),Linux)
- new_system=LINUX
- else
-
- $(error \
-	 Could not automatically detect your system,\
-	 try specifying a system manually)
-
- endif
-
- ifeq ($(shell getconf LONG_BIT),64)
- system+=64-bit
- new_system:=$(new_system)64
- endif
-
- $(info Detected $(system) ($(new_system))...)
- $(new_system)=1
-
-endif
-endif
-
-
-# SRB2 data files
-D_DIR?=../bin/Resources
-D_FILES=$(D_DIR)/srb2.pk3 \
-	$(D_DIR)/player.dta \
-	$(D_DIR)/zones.pk3 \
-	$(D_DIR)/music.dta \
-
-PKG_CONFIG?=pkg-config
-
-ifdef PANDORA
-LINUX=1
-endif
-
-ifdef LINUX64
-LINUX=1
-NONX86=1
-# LINUX64 does not imply X86_64=1; could mean ARM64 or Itanium
-endif
-
-ifdef MINGW64
-MINGW=1
-NONX86=1
-NOASM=1
-# MINGW64 should not necessarily imply X86_64=1, but we make that assumption elsewhere
-# Once that changes, remove this
-X86_64=1
-endif #ifdef MINGW64
-
-ifdef HAIKU
-SDL=1
-endif
-
-include Makefile.cfg
-
-ifdef DUMMY
-NOPNG=1
-NOZLIB=1
-NONET=1
-NOHW=1
-NOASM=1
-NOIPX=1
-EXENAME?=srb2dummy
-OBJS=$(OBJDIR)/i_video.o
-LIBS=-lm
-endif
-
-ifdef HAIKU
-NOIPX=1
-NOASM=1
-ifndef NONET
-LIBS=-lnetwork
-endif
-CFLAGS+=-DUNIXCOMMON
-PNG_CFLAGS?=
-PNG_LDFLAGS?=-lpng
-endif
-
-ifdef PANDORA
-NONX86=1
-NOHW=1
-endif
-
-ifndef NOOPENMPT
-HAVE_OPENMPT=1
-endif
-
-ifdef MINGW
-include win32/Makefile.cfg
-endif #ifdef MINGW
+# A list of supported GCC versions can be found in
+# Makefile.d/detect.mk -- search 'gcc_versions'.
+#
+# Feature flags:
+#
+# Safe to use online
+# ------------------
+# NO_IPV6=1 - Disable IPv6 address support.
+# NOHW=1 - Disable OpenGL renderer.
+# ZDEBUG=1 - Enable more detailed memory debugging
+# HAVE_MINIUPNPC=1 - Enable automated port forwarding.
+#                    Already enabled by default for 32-bit
+#                    Windows.
+# NOASM=1 - Disable hand optimized assembly code for the
+#           Software renderer.
+# NOPNG=1 - Disable PNG graphics support. (TODO: double
+#           check netplay compatible.)
+# NOCURL=1 - Disable libcurl--HTTP capability.
+# NOGME=1 - Disable game music emu, retro VGM support.
+# NOOPENMPT=1 - Disable module (tracker) music support.
+# NOMIXER=1 - Disable SDL Mixer (audio playback).
+# NOMIXERX=1 - Forgo SDL Mixer X--revert to standard SDL
+#              Mixer. Mixer X is the default for Windows
+#              builds.
+# HAVE_MIXERX=1 - Enable SDL Mixer X. Outside of Windows
+#                 builds, SDL Mixer X is not the default.
+# NOTHREADS=1 - Disable multithreading.
+#
+# Netplay incompatible
+# --------------------
+# NONET=1 - Disable online capability.
+# NOMD5=1 - Disable MD5 checksum (validation tool).
+# NOPOSTPROCESSING=1 - ?
+# MOBJCONSISTANCY=1 - ??
+# PACKETDROP=1 - ??
+# DEBUGMODE=1 - Enable various debugging capabilities.
+#               Also disables optimizations.
+# NOZLIB=1 - Disable some compression capability. Implies
+#            NOPNG=1.
+#
+# Development flags:
+#
+# VALGRIND=1 - Enable Valgrind memory debugging support.
+# PROFILEMODE=1 - Enable performance profiling (gprof).
+#
+# General flags for building:
+#
+# STATIC=1 - Use static linking.
+# DISTCC=1
+# CCACHE=1
+# UPX= - UPX command to use for compressing final
+#        executable.
+# WINDOWSHELL=1 - Use Windows commands.
+# PREFIX= - Prefix to many commands, for cross compiling.
+# YASM=1 - Use Yasm instead of NASM assembler.
+# STABS=1 - ?
+# ECHO=1 - Print out each command in the build process.
+# NOECHOFILENAMES=1 - Don't print out each that is being
+#                     worked on.
+# SILENT=1 - Print absolutely nothing except errors.
+# RELAXWARNINGS=1 - Use less compiler warnings/errors.
+# ERRORMODE=1 - Treat most compiler warnings as errors.
+# NOCASTALIGNWARN=1 - ?
+# NOLDWARNING=1 - ?
+# NOSDLMAIN=1 - ?
+# SDLMAIN=1 - ?
+#
+# Library configuration flags:
+# Everything here is an override.
+#
+# PNG_PKGCONFIG= - libpng-config command.
+# PNG_CFLAGS=, PNG_LDFLAGS=
+#
+# CURLCONFIG= - curl-config command.
+# CURL_CFLAGS=, CURL_LDFLAGS=
+#
+# VALGRIND_PKGCONFIG= - pkg-config package name.
+# VALGRIND_CFLAGS=, VALGRIND_LDFLAGS=
+#
+# LIBGME_PKGCONFIG=, LIBGME_CFLAGS=, LIBGME_LDFLAGS=
 
-ifdef UNIX
-UNIXCOMMON=1
-endif
+# LIBOPENMPT_PKGCONFIG=
+# LIBOPENMPT_CFLAGS=, LIBOPENMPT_LDFLAGS=
+#
+# ZLIB_PKGCONFIG=, ZLIB_CFLAGS=, ZLIB_LDFLAGS=
+#
+# SDL_PKGCONFIG=
+# SDL_CONFIG= - sdl-config command.
+# SDL_CFLAGS=, SDL_LDFLAGS=
 
-ifdef LINUX
-UNIXCOMMON=1
-ifndef NOGME
-HAVE_LIBGME=1
-endif
-endif
+clean_targets=cleandep clean distclean info
 
-ifdef SOLARIS
-UNIXCOMMON=1
-endif
+.PHONY : $(clean_targets) all
 
-ifdef FREEBSD
-UNIXCOMMON=1
-endif
+goals:=$(or $(MAKECMDGOALS),all)
+cleanonly:=$(filter $(clean_targets),$(goals))
+destructive:=$(filter-out info,$(cleanonly))
 
-ifdef MACOSX
-UNIXCOMMON=1
+ifndef cleanonly
+include Makefile.d/old.mk
 endif
 
-ifdef SDL
-	include sdl/Makefile.cfg
-endif #ifdef SDL
-
-ifdef DISTCC
-        CC:=distcc $(CC)
-endif
+include Makefile.d/util.mk
 
-ifdef CCACHE
-        CC:=ccache $(CC)
+ifdef PREFIX
+CC:=$(PREFIX)-gcc
 endif
 
-MSGFMT?=msgfmt
+OBJDUMP_OPTS?=--wide --source --line-numbers
 
-ifndef ECHO
-	NASM:=@$(NASM)
-	REMOVE:=@$(REMOVE)
-	CC:=@$(CC)
-	CXX:=@$(CXX)
-	OBJCOPY:=@$(OBJCOPY)
-	OBJDUMP:=@$(OBJDUMP)
-	STRIP:=@$(STRIP)
-	WINDRES:=@$(WINDRES)
-	MKDIR:=@$(MKDIR)
-	GZIP:=@$(GZIP)
-	MSGFMT:=@$(MSGFMT)
-	UPX:=@$(UPX)
-	UPX_OPTS+=-q
-endif
+OBJCOPY:=$(call Prefix,objcopy)
+OBJDUMP:=$(call Prefix,objdump)
+WINDRES:=$(call Prefix,windres)
 
-ifdef NONET
-	OPTS+=-DNONET
-	NOCURL=1
-else
-ifdef NO_IPV6
-	OPTS+=-DNO_IPV6
-endif
-endif
-
-ifdef NOHW
-	OPTS+=-DNOHW
+ifdef YASM
+NASM?=yasm
 else
-	OPTS+=-DHWRENDER
-	OBJS+=$(OBJDIR)/hw_bsp.o $(OBJDIR)/hw_draw.o $(OBJDIR)/hw_light.o \
-		 $(OBJDIR)/hw_main.o $(OBJDIR)/hw_clip.o $(OBJDIR)/hw_md2.o $(OBJDIR)/hw_cache.o \
-		 $(OBJDIR)/hw_md2load.o $(OBJDIR)/hw_md3load.o $(OBJDIR)/hw_model.o $(OBJDIR)/u_list.o $(OBJDIR)/hw_batching.o
+NASM?=nasm
 endif
 
-OPTS += -DCOMPVERSION
-
-ifndef NONX86
-ifndef GCC29
-	ARCHOPTS?=-msse3 -mfpmath=sse
+ifdef YASM
+ifdef STABS
+NASMOPTS?=-g stabs
 else
-	ARCHOPTS?=-mpentium
+NASMOPTS?=-g dwarf2
 endif
 else
-ifdef X86_64
-	ARCHOPTS?=-march=nocona
-endif
+NASMOPTS?=-g
 endif
 
-ifndef NOASM
-ifndef NONX86
-	OBJS+=$(OBJDIR)/tmap.o $(OBJDIR)/tmap_mmx.o
-	OPTS+=-DUSEASM
-endif
-endif
-
-ifndef NOPNG
-OPTS+=-DHAVE_PNG
-
-ifdef PNG_PKGCONFIG
-PNG_CFLAGS?=$(shell $(PKG_CONFIG) $(PNG_PKGCONFIG) --cflags)
-PNG_LDFLAGS?=$(shell $(PKG_CONFIG) $(PNG_PKGCONFIG) --libs)
-else
-ifdef PREFIX
-PNG_CONFIG?=$(PREFIX)-libpng-config
-else
-PNG_CONFIG?=libpng-config
-endif
-
-ifdef PNG_STATIC
-PNG_CFLAGS?=$(shell $(PNG_CONFIG) --static --cflags)
-PNG_LDFLAGS?=$(shell $(PNG_CONFIG) --static --ldflags)
-else
-PNG_CFLAGS?=$(shell $(PNG_CONFIG) --cflags)
-PNG_LDFLAGS?=$(shell $(PNG_CONFIG) --ldflags)
-endif
+GZIP?=gzip
+GZIP_OPTS?=-9 -f -n
+ifdef WINDOWSHELL
+GZIP_OPTS+=--rsyncable
 endif
 
-ifdef LINUX
-PNG_CFLAGS+=-D_LARGEFILE64_SOURCE
+UPX_OPTS?=--best --preserve-build-id
+ifndef ECHO
+UPX_OPTS+=-qq
 endif
 
-LIBS+=$(PNG_LDFLAGS)
-CFLAGS+=$(PNG_CFLAGS)
+include Makefile.d/detect.mk
 
-OBJS+=$(OBJDIR)/apng.o
-endif
+# make would try to remove the implicitly made directories
+.PRECIOUS : %/ comptime.c
 
-ifdef HAVE_LIBGME
-OPTS+=-DHAVE_LIBGME
+sources:=
+makedir:=../make
 
-LIBGME_PKGCONFIG?=libgme
-LIBGME_CFLAGS?=$(shell $(PKG_CONFIG) $(LIBGME_PKGCONFIG) --cflags)
-LIBGME_LDFLAGS?=$(shell $(PKG_CONFIG) $(LIBGME_PKGCONFIG) --libs)
+# -DCOMPVERSION: flag to use comptime.h
+opts:=-DCOMPVERSION -g
+libs:=
 
-LIBS+=$(LIBGME_LDFLAGS)
-CFLAGS+=$(LIBGME_CFLAGS)
-endif
+nasm_format:=
 
-ifdef HAVE_OPENMPT
-OPTS+=-DHAVE_OPENMPT
+# This is a list of variables names, of which if defined,
+# also defines the name as a macro to the compiler.
+passthru_opts:=
 
-LIBOPENMPT_PKGCONFIG?=libopenmpt
-LIBOPENMPT_CFLAGS?=$(shell $(PKG_CONFIG) $(LIBOPENMPT_PKGCONFIG) --cflags)
-LIBOPENMPT_LDFLAGS?=$(shell $(PKG_CONFIG) $(LIBOPENMPT_PKGCONFIG) --libs)
+include Makefile.d/platform.mk
+include Makefile.d/features.mk
+include Makefile.d/versions.mk
 
-LIBS+=$(LIBOPENMPT_LDFLAGS)
-CFLAGS+=$(LIBOPENMPT_CFLAGS)
+ifdef DEBUGMODE
+makedir:=$(makedir)/debug
 endif
 
-ifndef NOZLIB
-OPTS+=-DHAVE_ZLIB
-ZLIB_PKGCONFIG?=zlib
-ZLIB_CFLAGS?=$(shell $(PKG_CONFIG) $(ZLIB_PKGCONFIG) --cflags)
-ZLIB_LDFLAGS?=$(shell $(PKG_CONFIG) $(ZLIB_PKGCONFIG) --libs)
+depdir:=$(makedir)/deps
+objdir:=$(makedir)/objs
 
-LIBS+=$(ZLIB_LDFLAGS)
-CFLAGS+=$(ZLIB_CFLAGS)
-else
-NOPNG=1
-endif
+# very sophisticated dependency
+sources+=\
+	$(call List,Sourcefile)\
+	$(call List,blua/Sourcefile)\
 
-ifndef NOCURL
-OPTS+=-DHAVE_CURL
-CURLCONFIG?=curl-config
-CURL_CFLAGS?=$(shell $(CURLCONFIG) --cflags)
-CURL_LDFLAGS?=$(shell $(CURLCONFIG) --libs)
+depends:=$(basename $(filter %.c %.s,$(sources)))
+objects:=$(basename $(filter %.c %.s %.nas,$(sources)))
 
-LIBS+=$(CURL_LDFLAGS)
-CFLAGS+=$(CURL_CFLAGS)
-endif
+depends:=$(depends:%=$(depdir)/%.d)
 
-ifdef STATIC
-LIBS:=-static $(LIBS)
-endif
+# comptime.o added directly to objects instead of thru
+# sources because comptime.c includes comptime.h, but
+# comptime.h may not exist yet. It's a headache so this is
+# easier.
+objects:=$(objects:=.o) comptime.o
 
-ifdef HAVE_MINIUPNPC
-ifdef NONET
-HAVE_MINIUPNPC=''
-else
-LIBS+=-lminiupnpc
-ifdef MINGW
-LIBS+=-lws2_32 -liphlpapi
-endif
-CFLAGS+=-DHAVE_MINIUPNPC
-endif
+# windows resource file
+rc_file:=$(basename $(filter %.rc,$(sources)))
+ifdef rc_file
+objects+=$(rc_file:=.res)
 endif
 
-include blua/Makefile.cfg
+objects:=$(addprefix $(objdir)/,$(objects))
 
-ifdef NOMD5
-	OPTS+=-DNOMD5
+ifdef DEBUGMODE
+bin:=../bin/debug
 else
-	OBJS:=$(OBJDIR)/md5.o $(OBJS)
-endif
-
-ifdef NOPOSTPROCESSING
-	OPTS+=-DNOPOSTPROCESSING
-endif
-
-	OPTS:=-fno-exceptions $(OPTS)
-
-ifdef MOBJCONSISTANCY
-	OPTS+=-DMOBJCONSISTANCY
+bin:=../bin
 endif
 
-ifdef PACKETDROP
-	OPTS+=-DPACKETDROP
-endif
+# default EXENAME (usually set by platform)
+EXENAME?=srb2
+DBGNAME?=$(EXENAME).debug
 
-ifdef DEBUGMODE
+exe:=$(bin)/$(EXENAME)
+dbg:=$(bin)/$(DBGNAME)
 
-	# build with debugging information
-	WINDRESFLAGS = -D_DEBUG
-ifdef GCC48
-	CFLAGS+=-Og
-else
-	CFLAGS+=-O0
-endif
-	CFLAGS+= -Wall -DPARANOIA -DRANGECHECK -DPACKETDROP -DMOBJCONSISTANCY
-else
+build_done==== Build is done, look for \
+           $(<F) at $(abspath $(<D)) ===
 
+all : $(exe)
+	$(call Echo,$(build_done))
 
-	# build a normal optimised version
-	WINDRESFLAGS = -DNDEBUG
-	CFLAGS+=-O3
+ifndef VALGRIND
+dump : $(dbg).txt
 endif
-	CFLAGS+=-g $(OPTS) $(ARCHOPTS) $(WINDRESFLAGS)
 
-ifdef YASM
-ifdef STABS
-	NASMOPTS?= -g stabs
-else
-	NASMOPTS?= -g dwarf2
-endif
-else
-	NASMOPTS?= -g
+ifdef STATIC
+libs+=-static
 endif
 
+# build with profiling information
 ifdef PROFILEMODE
-	# build with profiling information
-	CFLAGS+=-pg
-	LDFLAGS+=-pg
+opts+=-pg
+libs+=-pg
 endif
 
-ifdef ZDEBUG
-	CPPFLAGS+=-DZDEBUG
+ifdef DEBUGMODE
+debug_opts=-D_DEBUG
+else # build a normal optimized version
+debug_opts=-DNDEBUG
+opts+=-O3
 endif
 
-OPTS+=$(CPPFLAGS)
+# debug_opts also get passed to windres
+opts+=$(debug_opts)
 
-# default EXENAME if all else fails
-EXENAME?=srb2
-DBGNAME?=$(EXENAME).debug
+opts+=$(foreach v,$(passthru_opts),$(if $($(v)),-D$(v)))
 
-# $(OBJDIR)/dstrings.o \
-
-# not too sophisticated dependency
-OBJS:=$(i_main_o) \
-		$(OBJDIR)/comptime.o \
-		$(OBJDIR)/string.o   \
-		$(OBJDIR)/d_main.o   \
-		$(OBJDIR)/d_clisrv.o \
-		$(OBJDIR)/d_net.o    \
-		$(OBJDIR)/d_netfil.o \
-		$(OBJDIR)/d_netcmd.o \
-		$(OBJDIR)/dehacked.o \
-		$(OBJDIR)/deh_soc.o  \
-		$(OBJDIR)/deh_lua.o  \
-		$(OBJDIR)/deh_tables.o \
-		$(OBJDIR)/z_zone.o   \
-		$(OBJDIR)/f_finale.o \
-		$(OBJDIR)/f_wipe.o   \
-		$(OBJDIR)/g_demo.o   \
-		$(OBJDIR)/g_game.o   \
-		$(OBJDIR)/g_input.o  \
-		$(OBJDIR)/am_map.o   \
-		$(OBJDIR)/command.o  \
-		$(OBJDIR)/console.o  \
-		$(OBJDIR)/hu_stuff.o \
-		$(OBJDIR)/y_inter.o  \
-		$(OBJDIR)/st_stuff.o \
-		$(OBJDIR)/m_aatree.o \
-		$(OBJDIR)/m_anigif.o \
-		$(OBJDIR)/m_argv.o   \
-		$(OBJDIR)/m_bbox.o   \
-		$(OBJDIR)/m_cheat.o  \
-		$(OBJDIR)/m_cond.o   \
-		$(OBJDIR)/m_fixed.o  \
-		$(OBJDIR)/m_menu.o   \
-		$(OBJDIR)/m_misc.o   \
-		$(OBJDIR)/m_perfstats.o \
-		$(OBJDIR)/m_random.o \
-		$(OBJDIR)/m_queue.o  \
-		$(OBJDIR)/info.o     \
-		$(OBJDIR)/p_ceilng.o \
-		$(OBJDIR)/p_enemy.o  \
-		$(OBJDIR)/p_floor.o  \
-		$(OBJDIR)/p_inter.o  \
-		$(OBJDIR)/p_lights.o \
-		$(OBJDIR)/p_map.o    \
-		$(OBJDIR)/p_maputl.o \
-		$(OBJDIR)/p_mobj.o   \
-		$(OBJDIR)/p_polyobj.o\
-		$(OBJDIR)/p_saveg.o  \
-		$(OBJDIR)/p_setup.o  \
-		$(OBJDIR)/p_sight.o  \
-		$(OBJDIR)/p_spec.o   \
-		$(OBJDIR)/p_telept.o \
-		$(OBJDIR)/p_tick.o   \
-		$(OBJDIR)/p_user.o   \
-		$(OBJDIR)/p_slopes.o \
-		$(OBJDIR)/tables.o   \
-		$(OBJDIR)/r_bsp.o    \
-		$(OBJDIR)/r_data.o   \
-		$(OBJDIR)/r_draw.o   \
-		$(OBJDIR)/r_main.o   \
-		$(OBJDIR)/r_plane.o  \
-		$(OBJDIR)/r_segs.o   \
-		$(OBJDIR)/r_skins.o  \
-		$(OBJDIR)/r_sky.o    \
-		$(OBJDIR)/r_splats.o \
-		$(OBJDIR)/r_things.o \
-		$(OBJDIR)/r_textures.o \
-		$(OBJDIR)/r_patch.o \
-		$(OBJDIR)/r_patchrotation.o \
-		$(OBJDIR)/r_picformats.o \
-		$(OBJDIR)/r_portal.o \
-		$(OBJDIR)/screen.o   \
-		$(OBJDIR)/taglist.o  \
-		$(OBJDIR)/v_video.o  \
-		$(OBJDIR)/s_sound.o  \
-		$(OBJDIR)/sounds.o   \
-		$(OBJDIR)/w_wad.o    \
-		$(OBJDIR)/filesrch.o \
-		$(OBJDIR)/mserv.o    \
-		$(OBJDIR)/http-mserv.o\
-		$(OBJDIR)/i_tcp.o    \
-		$(OBJDIR)/lzf.o	     \
-		$(OBJDIR)/vid_copy.o \
-		$(OBJDIR)/b_bot.o \
-		$(i_net_o)      \
-		$(i_system_o)   \
-		$(i_sound_o)    \
-		$(OBJS)
+opts+=$(WFLAGS) $(CPPFLAGS) $(CFLAGS)
+libs+=$(LDFLAGS)
+asflags:=$(ASFLAGS) -x assembler-with-cpp
 
+cc=$(CC)
 
-ifndef ECHO
-ifndef NOECHOFILENAMES
-define echoName =
-	@echo -- $< ...
-endef
+ifdef DISTCC
+cc=distcc $(CC)
 endif
+
+ifdef CCACHE
+cc=ccache $(CC)
 endif
 
-# List of languages to compile.
-# For reference, this is the command I use to build a srb2.pot file from the source code.
-# (The listed source files are the ones containing translated strings).
-# FILES=""; for file in `find ./ | grep "\.c" | grep -v svn`; do [ "`grep "M_GetText(" $file`" ] && FILES="$FILES $file"; done; xgettext -d srb2 -o locale/srb2.pot -kM_GetText -F --no-wrap $FILES
-ifdef GETTEXT
-POS:=$(BIN)/en.mo
+ifndef SILENT
+# makefile will 'restart' when it finishes including the
+# dependencies.
+ifndef MAKE_RESTARTS
+ifndef destructive
+$(shell $(CC) -v)
+define flags =
 
-OPTS+=-DGETTEXT
-endif
+SHELL ..... $(SHELL)
 
-ifdef PANDORA
-all:	pre-build $(BIN)/$(PNDNAME)
-endif
+CC ........ $(cc)
 
+CFLAGS .... $(opts)
 
-ifdef MINGW
-ifndef SDL
-all:	 pre-build $(BIN)/$(EXENAME) dll
-endif
-endif
+LDFLAGS ... $(libs)
 
-ifdef SDL
-all:	 pre-build $(BIN)/$(EXENAME)
+endef
+$(info $(flags))
 endif
-
-ifdef DUMMY
-all:	$(BIN)/$(EXENAME)
+# don't generate dependency files if only cleaning
+ifndef cleanonly
+$(info Checking dependency files...)
+include $(depends)
 endif
-
-cleandep:
-	$(REMOVE) $(OBJDIR)/depend.dep
-	$(REMOVE) comptime.h
-
-pre-build:
-ifdef WINDOWSHELL
-	-..\comptime.bat .
-else
-	-@../comptime.sh .
 endif
-
-clean:
-	$(REMOVE) *~ *.flc
-	$(REMOVE) $(OBJDIR)/*.o
-
-ifdef MINGW
-	$(REMOVE) $(OBJDIR)/*.res
 endif
 
-ifdef CYGWIN32
-	$(REMOVE) $(OBJDIR)/*.res
+LD:=$(CC)
+cc:=$(cc) $(opts)
+nasm=$(NASM) $(NASMOPTS) -f $(nasm_format)
+ifdef UPX
+upx=$(UPX) $(UPX_OPTS)
 endif
+windres=$(WINDRES) $(WINDRESFLAGS)\
+	$(debug_opts) --include-dir=win32 -O coff
 
-#make a big srb2.s that is the disasm of the exe (dos only ?)
-asm:
-	$(CC) $(LDFLAGS) $(OBJS) -o $(OBJDIR)/tmp.exe $(LIBS)
-	$(OBJDUMP) -d $(OBJDIR)/tmp.exe --no-show-raw-insn > srb2.s
-	$(REMOVE) $(OBJDIR)/tmp.exe
+%/ :
+	$(.)$(mkdir) $(call Windows_path,$@)
 
-# executable
-# NOTE: DJGPP's objcopy do not have --add-gnu-debuglink
+# this is needed so the target can be referenced in the
+# prerequisites
+.SECONDEXPANSION :
 
-$(BIN)/$(EXENAME): $(POS) $(OBJS)
-	-$(MKDIR) $(BIN)
-	@echo Linking $(EXENAME)...
-	$(LD) $(LDFLAGS) $(OBJS) -o $(BIN)/$(EXENAME) $(LIBS)
-ifndef VALGRIND
-ifndef NOOBJDUMP
-	@echo Dumping debugging info
-	$(OBJDUMP) $(OBJDUMP_OPTS) $(BIN)/$(EXENAME) > $(BIN)/$(DBGNAME).txt
-ifdef WINDOWSHELL
-	-$(GZIP) $(GZIP_OPTS) $(BIN)/$(DBGNAME).txt
-else
-	-$(GZIP) $(GZIP_OPT2) $(BIN)/$(DBGNAME).txt
-endif
-endif
+# 'UPX' is also recognized in the environment by upx
+unexport UPX
 
-# mac os x lsdlsrb2 does not like objcopy
-ifndef MACOSX
-	$(OBJCOPY) $(BIN)/$(EXENAME) $(BIN)/$(DBGNAME)
-	$(OBJCOPY) --strip-debug $(BIN)/$(EXENAME)
-	-$(OBJCOPY) --add-gnu-debuglink=$(BIN)/$(DBGNAME) $(BIN)/$(EXENAME)
-endif
-ifndef NOUPX
-	-$(UPX) $(UPX_OPTS) $(BIN)/$(EXENAME)
+# executable stripped of debugging symbols
+$(exe) : $(dbg) | $$(@D)/
+	$(.)$(OBJCOPY) --strip-debug $< $@
+	$(.)-$(OBJCOPY) --add-gnu-debuglink=$< $@
+ifdef UPX
+	$(call Echo,Compressing final executable...)
+	$(.)-$(upx) $@
 endif
-endif
-	@echo Build is done, please look for $(EXENAME) in $(BIN), \(checking for post steps\)
 
-reobjdump:
-	@echo Redumping debugging info
-	$(OBJDUMP) $(OBJDUMP_OPTS) $(BIN)/$(DBGNAME) > $(BIN)/$(DBGNAME).txt
-ifdef WINDOWSHELL
-	-$(GZIP) $(GZIP_OPTS) $(BIN)/$(DBGNAME).txt
-else
-	-$(GZIP) $(GZIP_OPT2) $(BIN)/$(DBGNAME).txt
-endif
+# original executable with debugging symbols
+$(dbg) : $(objects) | $$(@D)/
+	$(call Echo,Linking $(@F)...)
+	$(.)$(LD) -o $@ $^ $(libs)
 
-$(OBJDIR):
-	-$(MKDIR) $(OBJDIR)
+# disassembly of executable
+$(dbg).txt : $(dbg)
+	$(call Echo,Dumping debugging info...)
+	$(.)$(OBJDUMP) $(OBJDUMP_OPTS) $< > $@
+	$(.)$(GZIP) $(GZIP_OPTS) $@
 
-ifndef SDL
-ifdef NOHW
-dll :
+# '::' means run unconditionally
+# this really updates comptime.h
+comptime.c ::
+ifdef WINDOWSHELL
+	$(.)..\comptime.bat .
 else
-dll : opengl_dll
-endif
-ifdef MINGW
-all_dll: opengl_dll ds3d_dll fmod_dll openal_dll
-
-opengl_dll: $(BIN)/r_opengl.dll
-$(BIN)/r_opengl.dll: $(OBJDIR)/ogl_win.o $(OBJDIR)/r_opengl.o
-	-$(MKDIR) $(BIN)
-	@echo Linking R_OpenGL.dll...
-	$(CC) --shared  $^ -o $@ -g -Wl,--add-stdcall-alias -lgdi32 -static-libgcc
-ifndef NOUPX
-	-$(UPX) $(UPX_OPTS) $@
+	$(.)../comptime.sh .
 endif
 
-ds3d_dll: $(BIN)/s_ds3d.dll
-$(BIN)/s_ds3d.dll: $(OBJDIR)/s_ds3d.o
-	@echo Linking S_DS3d.dll...
-	$(CC) --shared  $^ -o $@ -g -Wl,--add-stdcall-alias -ldsound -luuid
-
-fmod_dll: $(BIN)/s_fmod.dll
-$(BIN)/s_fmod.dll: $(OBJDIR)/s_fmod.o
-	-$(MKDIR) $(BIN)
-	@echo Linking S_FMOD.dll...
-	$(CC) --shared  $^ -o $@ -g -Wl,--add-stdcall-alias -lfmod
-
-openal_dll: $(BIN)/s_openal.dll
-$(BIN)/s_openal.dll: $(OBJDIR)/s_openal.o
-	-$(MKDIR) $(BIN)
-	@echo Linking S_OpenAL.dll...
-	$(CC) --shared  $^ -o $@ -g -Wl,--add-stdcall-alias -lopenal32
-else
-all_dll: fmod_so openal_so
-
-fmod_so: $(BIN)/s_fmod.so
-$(BIN)/s_fmod.so: $(OBJDIR)/s_fmod.o
-	-$(MKDIR) $(BIN)
-	@echo Linking S_FMOD.so...
-	$(CC) --shared $^ -o $@ -g --nostartfiles -lm -lfmod
-
-openal_so: $(BIN)/s_openal.so
-$(BIN)/s_openal.so: $(OBJDIR)/s_openal.o
-	-$(MKDIR) $(BIN)
-	@echo Linking S_OpenAL.so...
-	$(CC) --shared $^ -o $@ -g --nostartfiles -lm -lopenal
-endif
+# I wish I could make dependencies out of rc files :(
+$(objdir)/win32/Srb2win.res : \
+	win32/afxres.h win32/resource.h
 
-else
-ifdef SDL
-ifdef MINGW
-$(OBJDIR)/r_opengl.o: hardware/r_opengl/r_opengl.c hardware/r_opengl/r_opengl.h \
- doomdef.h doomtype.h g_state.h m_swap.h hardware/hw_drv.h screen.h \
- command.h hardware/hw_data.h hardware/hw_defs.h \
- hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h \
- hardware/hw_md2load.h hardware/hw_md3load.h hardware/hw_model.h hardware/u_list.h \
- am_map.h d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
- p_mobj.h doomdata.h d_ticcmd.h r_defs.h hardware/hw_dll.h
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
-else
-$(OBJDIR)/r_opengl.o: hardware/r_opengl/r_opengl.c hardware/r_opengl/r_opengl.h \
- doomdef.h doomtype.h g_state.h m_swap.h hardware/hw_drv.h screen.h \
- command.h hardware/hw_data.h hardware/hw_defs.h \
- hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h \
- hardware/hw_md2load.h hardware/hw_md3load.h hardware/hw_model.h hardware/u_list.h \
- am_map.h d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
- p_mobj.h doomdata.h d_ticcmd.h r_defs.h hardware/hw_dll.h
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -I/usr/X11R6/include -c $< -o $@
+# dependency recipe template
+# 1: source file suffix
+# 2: extra flags to gcc
+define _recipe =
+$(depdir)/%.d : %.$(1) | $$$$(@D)/
+ifndef WINDOWSHELL
+ifdef Echo_name
+	@printf '%-20.20s\r' $$<
 endif
 endif
+	$(.)$(cc) -MM -MF $$@ -MT $(objdir)/$$*.o $(2) $$<
+endef
 
-endif
-
-#dependecy made by gcc itself !
-$(OBJS):
-ifndef DUMMY
--include $(OBJDIR)/depend.dep
-endif
-
-$(OBJDIR)/depend.dep:
-	@echo "Creating dependency file, depend.dep"
-	@echo > comptime.h
-	-$(MKDIR) $(OBJDIR)
-	$(CC) $(CFLAGS) -MM *.c > $(OBJDIR)/depend.ped
-	$(CC) $(CFLAGS) -MM $(INTERFACE)/*.c >> $(OBJDIR)/depend.ped
-ifndef NOHW
-	$(CC) $(CFLAGS) -MM hardware/*.c >> $(OBJDIR)/depend.ped
-endif
-	$(CC) $(CFLAGS) -MM blua/*.c >> $(OBJDIR)/depend.ped
-	@sed -e 's,\(.*\)\.o: ,$(subst /,\/,$(OBJDIR))\/&,g' < $(OBJDIR)/depend.ped > $(OBJDIR)/depend.dep
-	$(REMOVE) $(OBJDIR)/depend.ped
-	@echo "Created dependency file, depend.dep"
-
-ifdef VALGRIND
-$(OBJDIR)/z_zone.o: z_zone.c
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -DHAVE_VALGRIND $(VALGRIND_CFLAGS) -c $< -o $@
-endif
-
-$(OBJDIR)/comptime.o: comptime.c pre-build
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
-
-$(BIN)/%.mo: locale/%.po
-	-$(MKDIR) $(BIN)
-	$(echoName)
-	$(MSGFMT) -f -o $@ $<
+$(eval $(call _recipe,c))
+$(eval $(call _recipe,s,$(asflags)))
+
+# compiling recipe template
+# 1: target file suffix
+# 2: source file suffix
+# 3: compile command
+define _recipe =
+$(objdir)/%.$(1) : %.$(2) | $$$$(@D)/
+	$(call Echo_name,$$<)
+	$(.)$(3)
+endef
 
-$(OBJDIR)/%.o: %.c
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
+$(eval $(call _recipe,o,c,$(cc) -c -o $$@ $$<))
+$(eval $(call _recipe,o,nas,$(nasm) -o $$@ $$<))
+$(eval $(call _recipe,o,s,$(cc) $(asflags) -c -o $$@ $$<))
+$(eval $(call _recipe,res,rc,$(windres) -i $$< -o $$@))
 
-$(OBJDIR)/%.o: $(INTERFACE)/%.c
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
+_rm=$(.)$(rmrf) $(call Windows_path,$(1))
 
-ifdef MACOSX
-$(OBJDIR)/%.o: sdl/macosx/%.c
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
-endif
+cleandep :
+	$(call _rm,$(depends) comptime.h)
 
-$(OBJDIR)/%.o: hardware/%.c
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
-
-$(OBJDIR)/%.o: blua/%.c
-	$(echoName)
-	$(CC) $(CFLAGS) $(LUA_CFLAGS) $(WFLAGS) -c $< -o $@
-
-$(OBJDIR)/%.o: %.nas
-	$(echoName)
-	$(NASM) $(NASMOPTS) -o $@ -f $(NASMFORMAT) $<
-
-$(OBJDIR)/vid_copy.o: vid_copy.s asm_defs.inc
-	$(echoName)
-	$(CC) $(OPTS) $(ASFLAGS) -x assembler-with-cpp -c $< -o $@
-
-$(OBJDIR)/%.o: %.s
-	$(echoName)
-	$(CC) $(OPTS) -x assembler-with-cpp -c $< -o $@
-
-$(OBJDIR)/SRB2.res: win32/Srb2win.rc win32/afxres.h win32/resource.h
-	$(echoName)
-	$(WINDRES) -i $< -O rc $(WINDRESFLAGS) --include-dir=win32 -o $@ -O coff
-
-
-ifdef MINGW
-ifndef SDL
-ifndef NOHW
-$(OBJDIR)/r_opengl.o: hardware/r_opengl/r_opengl.c hardware/r_opengl/r_opengl.h \
- doomdef.h doomtype.h g_state.h m_swap.h hardware/hw_drv.h screen.h \
- command.h hardware/hw_data.h hardware/hw_defs.h \
- hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h \
- hardware/hw_md2load.h hardware/hw_md3load.h hardware/hw_model.h hardware/u_list.h \
- am_map.h d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
- p_mobj.h doomdata.h d_ticcmd.h r_defs.h hardware/hw_dll.h
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -D_WINDOWS -mwindows -c $< -o $@
-
-$(OBJDIR)/ogl_win.o: hardware/r_opengl/ogl_win.c hardware/r_opengl/r_opengl.h \
- doomdef.h doomtype.h g_state.h m_swap.h hardware/hw_drv.h screen.h \
- command.h hardware/hw_data.h hardware/hw_defs.h \
- hardware/hw_md2.h hardware/hw_glob.h hardware/hw_main.h hardware/hw_clip.h \
- hardware/hw_md2load.h hardware/hw_md3load.h hardware/hw_model.h hardware/u_list.h \
- am_map.h d_event.h d_player.h p_pspr.h m_fixed.h tables.h info.h d_think.h \
- p_mobj.h doomdata.h d_ticcmd.h r_defs.h hardware/hw_dll.h
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -D_WINDOWS -mwindows -c $< -o $@
-endif
+clean :
+	$(call _rm,$(exe) $(dbg) $(dbg).txt $(objects))
 
-endif
-endif
+distclean :
+	$(call _rm,../bin ../objs ../dep ../make comptime.h)
 
-ifdef SDL
-
-ifdef MINGW
-$(OBJDIR)/win_dbg.o: win32/win_dbg.c
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
-endif
-
-ifdef STATICHS
-$(OBJDIR)/s_openal.o: hardware/s_openal/s_openal.c hardware/hw3dsdrv.h \
- hardware/hw_dll.h
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
-
-$(OBJDIR)/s_fmod.o: hardware/s_fmod/s_fmod.c hardware/hw3dsdrv.h \
- hardware/hw_dll.h
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
-
-ifdef MINGW
-$(OBJDIR)/s_ds3d.o: hardware/s_ds3d/s_ds3d.c hardware/hw3dsdrv.h \
- hardware/hw_dll.h
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
-endif
+info:
+ifdef WINDOWSHELL
+	@REM
 else
-
-$(OBJDIR)/s_fmod.o: hardware/s_fmod/s_fmod.c hardware/hw3dsdrv.h \
- hardware/hw_dll.h
-	$(echoName)
-	$(CC) $(ARCHOPTS) -Os -o $(OBJDIR)/s_fmod.o -DHW3SOUND -DUNIXCOMMON -shared -nostartfiles -c hardware/s_fmod/s_fmod.c
-
-$(OBJDIR)/s_openal.o: hardware/s_openal/s_openal.c hardware/hw3dsdrv.h \
- hardware/hw_dll.h
-	$(echoName)
-	$(CC) $(ARCHOPTS) -Os -o $(OBJDIR)/s_openal.o -DHW3SOUND -DUNIXCOMMON -shared -nostartfiles -c hardware/s_openal/s_openal.c
-endif
+	@:
 endif
-
-#############################################################
-#
-#############################################################
diff --git a/src/Makefile.cfg b/src/Makefile.cfg
deleted file mode 100644
index db7230bb4b42ffe4a21f27d94fddf73174153e1c..0000000000000000000000000000000000000000
--- a/src/Makefile.cfg
+++ /dev/null
@@ -1,462 +0,0 @@
-# vim: ft=make
-#
-# Makefile.cfg for SRB2
-#
-
-#
-# GNU compiler & tools' flags
-# and other things
-#
-
-# See the following variable don't start with 'GCC'. This is
-# to avoid a false positive with the version detection...
-
-SUPPORTED_GCC_VERSIONS:=\
-	101 102\
-	91 92 93\
-	81 82 83 84\
-	71 72 73 74 75\
-	61 62 63 64\
-	51 52 53 54 55\
-	40 41 42 43 44 45 46 47 48 49
-
-LATEST_GCC_VERSION=10.2
-
-# gcc or g++
-ifdef PREFIX
-	CC=$(PREFIX)-gcc
-	CXX=$(PREFIX)-g++
-	OBJCOPY=$(PREFIX)-objcopy
-	OBJDUMP=$(PREFIX)-objdump
-	STRIP=$(PREFIX)-strip
-	WINDRES=$(PREFIX)-windres
-else
-	OBJCOPY=objcopy
-	OBJDUMP=objdump
-	STRIP=strip
-	WINDRES=windres
-endif
-
-# because Apple screws with us on this
-# need to get bintools from homebrew
-ifdef MACOSX
-	CC=clang
-	CXX=clang
-	OBJCOPY=gobjcopy
-	OBJDUMP=gobjdump
-endif
-
-# Automatically set version flag, but not if one was manually set
-ifeq   (,$(filter GCC%,$(.VARIABLES)))
- version:=$(shell $(CC) --version)
- # check if this is in fact GCC
- ifneq (,$(or $(findstring gcc,$(version)),$(findstring GCC,$(version))))
-  version:=$(shell $(CC) -dumpversion)
-
-  # Turn version into words of major, minor
-  v:=$(subst ., ,$(version))
-  # concat. major minor
-  v:=$(word 1,$(v))$(word 2,$(v))
-
-  # If this version is not in the list, default to the latest supported
-  ifeq (,$(filter $(v),$(SUPPORTED_GCC_VERSIONS)))
-   $(info\
-		Your compiler version, GCC $(version), is not supported by the Makefile.\
-		The Makefile will assume GCC $(LATEST_GCC_VERSION).)
-   GCC$(subst .,,$(LATEST_GCC_VERSION))=1
-  else
-   $(info Detected GCC $(version) (GCC$(v)))
-   GCC$(v)=1
-  endif
- endif
-endif
-
-ifdef GCC102
-GCC101=1
-endif
-
-ifdef GCC101
-GCC93=1
-endif
-
-ifdef GCC93
-GCC92=1
-endif
-
-ifdef GCC92
-GCC91=1
-endif
-
-ifdef GCC91
-GCC84=1
-endif
-
-ifdef GCC84
-GCC83=1
-endif
-
-ifdef GCC83
-GCC82=1
-endif
-
-ifdef GCC82
-GCC81=1
-endif
-
-ifdef GCC81
-GCC75=1
-endif
-
-ifdef GCC75
-GCC74=1
-endif
-
-ifdef GCC74
-GCC73=1
-endif
-
-ifdef GCC73
-GCC72=1
-endif
-
-ifdef GCC72
-GCC71=1
-endif
-
-ifdef GCC71
-GCC64=1
-endif
-
-ifdef GCC64
-GCC63=1
-endif
-
-ifdef GCC63
-GCC62=1
-endif
-
-ifdef GCC62
-GCC61=1
-endif
-
-ifdef GCC61
-GCC55=1
-endif
-
-ifdef GCC55
-GCC54=1
-endif
-
-ifdef GCC54
-GCC53=1
-endif
-
-ifdef GCC53
-GCC52=1
-endif
-
-ifdef GCC52
-GCC51=1
-endif
-
-ifdef GCC51
-GCC49=1
-endif
-
-ifdef GCC49
-GCC48=1
-endif
-
-ifdef GCC48
-GCC47=1
-endif
-
-ifdef GCC47
-GCC46=1
-endif
-
-ifdef GCC46
-GCC45=1
-endif
-
-ifdef GCC45
-GCC44=1
-endif
-
-ifdef GCC44
-GCC43=1
-endif
-
-ifdef GCC43
-GCC42=1
-endif
-
-ifdef GCC42
-GCC41=1
-endif
-
-ifdef GCC41
-GCC40=1
-VCHELP=1
-endif
-
-ifdef GCC295
-GCC29=1
-endif
-
-OLDWFLAGS:=$(WFLAGS)
-# -W -Wno-unused
-WFLAGS=-Wall
-ifndef GCC295
-#WFLAGS+=-Wno-packed
-endif
-ifndef RELAXWARNINGS
- WFLAGS+=-W
-#WFLAGS+=-Wno-sign-compare
-ifndef GCC295
- WFLAGS+=-Wno-div-by-zero
-endif
-#WFLAGS+=-Wsystem-headers
-WFLAGS+=-Wfloat-equal
-#WFLAGS+=-Wtraditional
-ifdef VCHELP
- WFLAGS+=-Wdeclaration-after-statement
- WFLAGS+=-Wno-error=declaration-after-statement
-endif
- WFLAGS+=-Wundef
-ifndef GCC295
- WFLAGS+=-Wendif-labels
-endif
-ifdef GCC41
- WFLAGS+=-Wshadow
-endif
-#WFLAGS+=-Wlarger-than-%len%
- WFLAGS+=-Wpointer-arith -Wbad-function-cast
-ifdef GCC45
-#WFLAGS+=-Wc++-compat
-endif
- WFLAGS+=-Wcast-qual
-ifndef NOCASTALIGNWARN
- WFLAGS+=-Wcast-align
-endif
- WFLAGS+=-Wwrite-strings
-ifndef ERRORMODE
-#WFLAGS+=-Wconversion
-ifdef GCC43
- #WFLAGS+=-Wno-sign-conversion
-endif
-endif
- WFLAGS+=-Wsign-compare
-ifdef GCC91
- WFLAGS+=-Wno-error=address-of-packed-member
-endif
-ifdef GCC45
- WFLAGS+=-Wlogical-op
-endif
- WFLAGS+=-Waggregate-return
-ifdef HAIKU
-ifdef GCC41
- #WFLAGS+=-Wno-attributes
-endif
-endif
-#WFLAGS+=-Wstrict-prototypes
-ifdef GCC40
- WFLAGS+=-Wold-style-definition
-endif
- WFLAGS+=-Wmissing-prototypes -Wmissing-declarations
-ifdef GCC40
- WFLAGS+=-Wmissing-field-initializers
-endif
- WFLAGS+=-Wmissing-noreturn
-#WFLAGS+=-Wmissing-format-attribute
-#WFLAGS+=-Wno-multichar
-#WFLAGS+=-Wno-deprecated-declarations
-#WFLAGS+=-Wpacked
-#WFLAGS+=-Wpadded
-#WFLAGS+=-Wredundant-decls
- WFLAGS+=-Wnested-externs
-#WFLAGS+=-Wunreachable-code
- WFLAGS+=-Winline
-ifdef GCC43
- WFLAGS+=-funit-at-a-time
- WFLAGS+=-Wlogical-op
-endif
-ifndef GCC295
- WFLAGS+=-Wdisabled-optimization
-endif
-endif
-WFLAGS+=-Wformat-y2k
-ifdef GCC71
-WFLAGS+=-Wno-error=format-overflow=2
-endif
-WFLAGS+=-Wformat-security
-ifndef GCC29
-#WFLAGS+=-Winit-self
-endif
-ifdef GCC46
-WFLAGS+=-Wno-suggest-attribute=noreturn
-endif
-
-ifdef NOLDWARNING
-LDFLAGS+=-Wl,--as-needed
-endif
-
-ifdef ERRORMODE
-WFLAGS+=-Werror
-endif
-
-WFLAGS+=$(OLDWFLAGS)
-
-ifdef GCC43
- #WFLAGS+=-Wno-error=clobbered
-endif
-ifdef GCC44
- WFLAGS+=-Wno-error=array-bounds
-endif
-ifdef GCC46
- WFLAGS+=-Wno-error=suggest-attribute=noreturn
-endif
-ifdef GCC54
- WFLAGS+=-Wno-logical-op -Wno-error=logical-op
-endif
-ifdef GCC61
- WFLAGS+=-Wno-tautological-compare -Wno-error=tautological-compare
-endif
-ifdef GCC71
- WFLAGS+=-Wimplicit-fallthrough=4
-endif
-ifdef GCC81
- WFLAGS+=-Wno-error=format-overflow
- WFLAGS+=-Wno-error=stringop-truncation
- WFLAGS+=-Wno-error=stringop-overflow
- WFLAGS+=-Wno-format-overflow
- WFLAGS+=-Wno-stringop-truncation
- WFLAGS+=-Wno-stringop-overflow
- WFLAGS+=-Wno-error=multistatement-macros
-endif
-
-
-#indicate platform and what interface use with
-ifndef LINUX
-ifndef FREEBSD
-ifndef CYGWIN32
-ifndef MINGW
-ifndef MINGW64
-ifndef SDL
-ifndef DUMMY
-$(error No interface or platform flag defined)
-endif
-endif
-endif
-endif
-endif
-endif
-endif
-
-#determine the interface directory (where you put all i_*.c)
-i_net_o=$(OBJDIR)/i_net.o
-i_system_o=$(OBJDIR)/i_system.o
-i_sound_o=$(OBJDIR)/i_sound.o
-i_main_o=$(OBJDIR)/i_main.o
-#set OBJDIR and BIN's starting place
-OBJDIR=../objs
-BIN=../bin
-#Nasm ASM and rm
-ifdef YASM
-NASM?=yasm
-else
-NASM?=nasm
-endif
-REMOVE?=rm -f
-MKDIR?=mkdir -p
-GZIP?=gzip
-GZIP_OPTS?=-9 -f -n
-GZIP_OPT2=$(GZIP_OPTS) --rsyncable
-UPX?=upx
-UPX_OPTS?=--best --preserve-build-id
-ifndef ECHO
-UPX_OPTS+=-q
-endif
-
-#Interface Setup
-ifdef DUMMY
-	INTERFACE=dummy
-	OBJDIR:=$(OBJDIR)/dummy
-	BIN:=$(BIN)/dummy
-else
-ifdef LINUX
-	NASMFORMAT=elf -DLINUX
-	SDL=1
-ifdef LINUX64
-	OBJDIR:=$(OBJDIR)/Linux64
-	BIN:=$(BIN)/Linux64
-else
-	OBJDIR:=$(OBJDIR)/Linux
-	BIN:=$(BIN)/Linux
-endif
-else
-ifdef FREEBSD
-	INTERFACE=sdl
-	NASMFORMAT=elf -DLINUX
-	SDL=1
-
-	OBJDIR:=$(OBJDIR)/FreeBSD
-	BIN:=$(BIN)/FreeBSD
-else
-ifdef SOLARIS
-	INTERFACE=sdl
-	NASMFORMAT=elf -DLINUX
-	SDL=1
-
-	OBJDIR:=$(OBJDIR)/Solaris
-	BIN:=$(BIN)/Solaris
-else
-ifdef CYGWIN32
-	INTERFACE=sdl
-	NASMFORMAT=win32
-	SDL=1
-
-	OBJDIR:=$(OBJDIR)/cygwin
-	BIN:=$(BIN)/Cygwin
-else
-ifdef MINGW64
-	INTERFACE=win32
-	#NASMFORMAT=win64
-	OBJDIR:=$(OBJDIR)/Mingw64
-	BIN:=$(BIN)/Mingw64
-else
-ifdef MINGW
-	INTERFACE=win32
-	NASMFORMAT=win32
-	OBJDIR:=$(OBJDIR)/Mingw
-	BIN:=$(BIN)/Mingw
-endif
-endif
-endif
-endif
-endif
-endif
-endif
-
-ifdef ARCHNAME
-	OBJDIR:=$(OBJDIR)/$(ARCHNAME)
-	BIN:=$(BIN)/$(ARCHNAME)
-endif
-
-OBJDUMP_OPTS?=--wide --source --line-numbers
-LD=$(CC)
-
-ifdef SDL
-	INTERFACE=sdl
-	OBJDIR:=$(OBJDIR)/SDL
-endif
-
-ifndef DUMMY
-ifdef DEBUGMODE
-	OBJDIR:=$(OBJDIR)/Debug
-	BIN:=$(BIN)/Debug
-else
-	OBJDIR:=$(OBJDIR)/Release
-	BIN:=$(BIN)/Release
-endif
-endif
diff --git a/src/Makefile.d/detect.mk b/src/Makefile.d/detect.mk
new file mode 100644
index 0000000000000000000000000000000000000000..f458b044cf8c2f8d973b50e32a2f3500e6e6c7ef
--- /dev/null
+++ b/src/Makefile.d/detect.mk
@@ -0,0 +1,107 @@
+#
+# Detect the host system and compiler version.
+#
+
+# Previously featured:\
+	PANDORA\
+	HAIKU\
+	DUMMY\
+	DJGPPDOS\
+	SOLARIS\
+	MACOSX\
+
+all_systems:=\
+	LINUX64\
+	MINGW64\
+	MINGW\
+	UNIX\
+	LINUX\
+	FREEBSD\
+	SDL\
+
+# check for user specified system
+ifeq (,$(filter $(all_systems),$(.VARIABLES)))
+ifeq ($(OS),Windows_NT) # all windows are Windows_NT...
+
+_m=Detected a Windows system,\
+	compiling for 32-bit MinGW SDL...)
+$(call Print,$(_m))
+
+# go for a 32-bit sdl mingw exe by default
+MINGW:=1
+
+else # if you on the *nix
+
+system:=$(shell uname -s)
+
+ifeq ($(system),Linux)
+new_system:=LINUX
+else
+
+$(error \
+	Could not automatically detect your system,\
+	try specifying a system manually)
+
+endif
+
+ifeq ($(shell getconf LONG_BIT),64)
+system+=64-bit
+new_system:=$(new_system)64
+endif
+
+$(call Print,Detected $(system) ($(new_system))...)
+$(new_system):=1
+
+endif
+endif
+
+# This must have high to low order.
+gcc_versions:=\
+	102 101\
+	93 92 91\
+	84 83 82 81\
+	75 74 73 72 71\
+	64 63 62 61\
+	55 54 53 52 51\
+	49 48 47 46 45 44 43 42 41 40
+
+latest_gcc_version:=10.2
+
+# Automatically set version flag, but not if one was
+# manually set. And don't bother if this is a clean only
+# run.
+ifeq (,$(call Wildvar,GCC% destructive))
+
+# 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 (,$(findstring gcc version,$(version)))
+
+# 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))
+# concat. major minor
+v:=$(word 1,$(v))$(word 2,$(v))
+
+# If this version is not in the list,
+# default to the latest supported
+ifeq (,$(filter $(v),$(gcc_versions)))
+define line =
+Your compiler version, GCC $(version), \
+is not supported by the Makefile.
+The Makefile will assume GCC $(latest_gcc_version).
+endef
+$(call Print,$(line))
+GCC$(subst .,,$(latest_gcc_version)):=1
+else
+$(call Print,Detected GCC $(version) (GCC$(v)))
+GCC$(v):=1
+endif
+
+endif
+endif
diff --git a/src/Makefile.d/features.mk b/src/Makefile.d/features.mk
new file mode 100644
index 0000000000000000000000000000000000000000..46194390d70f1f4c8b2f186f6ebf5542c4e228f9
--- /dev/null
+++ b/src/Makefile.d/features.mk
@@ -0,0 +1,75 @@
+#
+# Makefile for feature flags.
+#
+
+passthru_opts+=\
+	NONET NO_IPV6 NOHW NOMD5 NOPOSTPROCESSING\
+	MOBJCONSISTANCY PACKETDROP ZDEBUG\
+	HAVE_MINIUPNPC\
+
+# build with debugging information
+ifdef DEBUGMODE
+PACKETDROP=1
+opts+=-DPARANOIA -DRANGECHECK
+endif
+
+ifndef NOHW
+opts+=-DHWRENDER
+sources+=$(call List,hardware/Sourcefile)
+endif
+
+ifndef NOASM
+ifndef NONX86
+sources+=tmap.nas tmap_mmx.nas
+opts+=-DUSEASM
+endif
+endif
+
+ifndef NOMD5
+sources+=md5.c
+endif
+
+ifndef NOZLIB
+ifndef NOPNG
+ifdef PNG_PKGCONFIG
+$(eval $(call Use_pkg_config,PNG_PKGCONFIG))
+else
+PNG_CONFIG?=$(call Prefix,libpng-config)
+$(eval $(call Configure,PNG,$(PNG_CONFIG) \
+	$(if $(PNG_STATIC),--static),,--ldflags))
+endif
+ifdef LINUX
+opts+=-D_LARGFILE64_SOURCE
+endif
+opts+=-DHAVE_PNG
+sources+=apng.c
+endif
+endif
+
+ifndef NONET
+ifndef NOCURL
+CURLCONFIG?=curl-config
+$(eval $(call Configure,CURL,$(CURLCONFIG)))
+opts+=-DHAVE_CURL
+endif
+endif
+
+ifdef HAVE_MINIUPNPC
+libs+=-lminiupnpc
+endif
+
+# (Valgrind is a memory debugger.)
+ifdef VALGRIND
+VALGRIND_PKGCONFIG?=valgrind
+$(eval $(call Use_pkg_config,VALGRIND))
+ZDEBUG=1
+opts+=-DHAVE_VALGRIND
+endif
+
+default_packages:=\
+	GME/libgme/LIBGME\
+	OPENMPT/libopenmpt/LIBOPENMPT\
+	ZLIB/zlib\
+
+$(foreach p,$(default_packages),\
+	$(eval $(call Check_pkg_config,$(p))))
diff --git a/src/Makefile.d/nix.mk b/src/Makefile.d/nix.mk
new file mode 100644
index 0000000000000000000000000000000000000000..6642a6bcc202b9a62dbaf98668f37666baea57b3
--- /dev/null
+++ b/src/Makefile.d/nix.mk
@@ -0,0 +1,42 @@
+#
+# Makefile options for unices (linux, bsd...)
+#
+
+EXENAME?=lsdl2srb2
+
+opts+=-DUNIXCOMMON -DLUA_USE_POSIX
+# Use -rdynamic so a backtrace log shows function names
+# instead of addresses
+libs+=-lm -rdynamic
+
+ifndef nasm_format
+nasm_format:=elf -DLINUX
+endif
+
+ifndef NOHW
+opts+=-I/usr/X11R6/include
+libs+=-L/usr/X11R6/lib
+endif
+
+SDL=1
+
+# In common usage.
+ifdef LINUX
+libs+=-lrt
+passthru_opts+=NOTERMIOS
+endif
+
+# Tested by Steel, as of release 2.2.8.
+ifdef FREEBSD
+opts+=-I/usr/X11R6/include -DLINUX -DFREEBSD
+libs+=-L/usr/X11R6/lib -lipx -lkvm
+endif
+
+# FIXME: UNTESTED
+#ifdef SOLARIS
+#NOIPX=1
+#NOASM=1
+#opts+=-I/usr/local/include -I/opt/sfw/include \
+#		-DSOLARIS -DINADDR_NONE=INADDR_ANY -DBSD_COMP
+#libs+=-L/opt/sfw/lib -lsocket -lnsl
+#endif
diff --git a/src/Makefile.d/old.mk b/src/Makefile.d/old.mk
new file mode 100644
index 0000000000000000000000000000000000000000..ec9b6d776c53ccca325c510506ff34d1e27d4d5d
--- /dev/null
+++ b/src/Makefile.d/old.mk
@@ -0,0 +1,16 @@
+#
+# Warn about old build directories and offer to purge.
+#
+
+_old:=$(wildcard $(addprefix ../bin/,FreeBSD Linux \
+		Linux64 Mingw Mingw64 SDL dummy) ../objs ../dep)
+
+ifdef _old
+$(foreach v,$(_old),$(info $(abspath $(v))))
+$(info )
+$(info These directories are no longer\
+       required and should be removed.)
+$(info You may remove them manually or\
+       by using 'make distclean')
+$(error )
+endif
diff --git a/src/Makefile.d/platform.mk b/src/Makefile.d/platform.mk
new file mode 100644
index 0000000000000000000000000000000000000000..fad4be191639266760e689628a0a5054635e67ff
--- /dev/null
+++ b/src/Makefile.d/platform.mk
@@ -0,0 +1,69 @@
+#
+# Platform specific options.
+#
+
+PKG_CONFIG?=pkg-config
+
+ifdef WINDOWSHELL
+rmrf=-2>NUL DEL /S /Q
+mkdir=-2>NUL MD
+cat=TYPE
+else
+rmrf=rm -rf
+mkdir=mkdir -p
+cat=cat
+endif
+
+ifdef LINUX64
+LINUX=1
+endif
+
+ifdef MINGW64
+MINGW=1
+endif
+
+ifdef LINUX
+UNIX=1
+ifdef LINUX64
+NONX86=1
+# LINUX64 does not imply X86_64=1;
+# could mean ARM64 or Itanium
+platform=linux/64
+else
+platform=linux
+endif
+else ifdef FREEBSD
+UNIX=1
+platform=freebsd
+else ifdef SOLARIS # FIXME: UNTESTED
+UNIX=1
+platform=solaris
+else ifdef CYGWIN32 # FIXME: UNTESTED
+nasm_format=win32
+platform=cygwin
+else ifdef MINGW
+ifdef MINGW64
+NONX86=1
+NOASM=1
+# MINGW64 should not necessarily imply X86_64=1,
+# but we make that assumption elsewhere
+# Once that changes, remove this
+X86_64=1
+platform=mingw/64
+else
+platform=mingw
+endif
+include Makefile.d/win32.mk
+endif
+
+ifdef platform
+makedir:=$(makedir)/$(platform)
+endif
+
+ifdef UNIX
+include Makefile.d/nix.mk
+endif
+
+ifdef SDL
+include Makefile.d/sdl.mk
+endif
diff --git a/src/Makefile.d/sdl.mk b/src/Makefile.d/sdl.mk
new file mode 100644
index 0000000000000000000000000000000000000000..99ca624e69f2f18c10625c93585f14681636f36e
--- /dev/null
+++ b/src/Makefile.d/sdl.mk
@@ -0,0 +1,79 @@
+#
+# Makefile options for SDL2 backend.
+#
+
+#
+# SDL...., *looks at Alam*, THIS IS A MESS!
+# 
+# ...a little bird flexes its muscles...
+#
+
+makedir:=$(makedir)/SDL
+
+sources+=$(call List,sdl/Sourcefile)
+opts+=-DDIRECTFULLSCREEN -DHAVE_SDL
+
+# FIXME: UNTESTED
+#ifdef PANDORA
+#include sdl/SRB2Pandora/Makefile.cfg
+#endif #ifdef PANDORA
+
+# FIXME: UNTESTED
+#ifdef CYGWIN32
+#include sdl/MakeCYG.cfg
+#endif #ifdef CYGWIN32
+
+ifndef NOHW
+sources+=sdl/ogl_sdl.c
+endif
+
+ifdef NOMIXER
+sources+=sdl/sdl_sound.c
+else
+opts+=-DHAVE_MIXER
+sources+=sdl/mixer_sound.c
+
+  ifdef HAVE_MIXERX
+  opts+=-DHAVE_MIXERX
+  libs+=-lSDL2_mixer_ext
+  else
+  libs+=-lSDL2_mixer
+  endif
+endif
+
+ifndef NOTHREADS
+opts+=-DHAVE_THREADS
+sources+=sdl/i_threads.c
+endif
+
+ifdef SDL_PKGCONFIG
+$(eval $(call Use_pkg_config,SDL))
+else
+SDL_CONFIG?=$(call Prefix,sdl2-config)
+SDL_CFLAGS?=$(shell $(SDL_CONFIG) --cflags)
+SDL_LDFLAGS?=$(shell $(SDL_CONFIG) \
+		$(if $(STATIC),--static-libs,--libs))
+$(eval $(call Propogate_flags,SDL))
+endif
+
+# use the x86 asm code
+ifndef CYGWIN32
+ifndef NOASM
+USEASM=1
+endif
+endif
+
+ifdef MINGW
+ifndef NOSDLMAIN
+SDLMAIN=1
+endif
+endif
+
+ifdef SDLMAIN
+opts+=-DSDLMAIN
+else
+ifdef MINGW
+opts+=-Umain
+libs+=-mconsole
+endif
+endif
diff --git a/src/Makefile.d/util.mk b/src/Makefile.d/util.mk
new file mode 100644
index 0000000000000000000000000000000000000000..bda68df13a3d25892ec2a3933201d88686c580f4
--- /dev/null
+++ b/src/Makefile.d/util.mk
@@ -0,0 +1,93 @@
+#
+# Utility macros for the rest of the Makefiles.
+#
+
+Ifnot=$(if $(1),$(3),$(2))
+Ifndef=$(call Ifnot,$($(1)),$(2),$(3))
+
+# Match and expand a list of variables by pattern.
+Wildvar=$(foreach v,$(filter $(1),$(.VARIABLES)),$($(v)))
+
+# Read a list of words from file and prepend each with the
+# directory of the file.
+_cat=$(shell $(cat) $(call Windows_path,$(1)))
+List=$(addprefix $(dir $(1)),$(call _cat,$(1)))
+
+# Convert path separators to backslash on Windows.
+Windows_path=$(if $(WINDOWSHELL),$(subst /,\,$(1)),$(1))
+
+define Propogate_flags =
+opts+=$$($(1)_CFLAGS)
+libs+=$$($(1)_LDFLAGS)
+endef
+
+# Set library's _CFLAGS and _LDFLAGS from some command.
+# Automatically propogates the flags too.
+# 1: variable prefix (e.g. CURL)
+# 2: start of command (e.g. curl-config)
+# --- optional ----
+# 3: CFLAGS command arguments, default '--cflags'
+# 4: LDFLAGS command arguments, default '--libs'
+# 5: common command arguments at the end of command
+define Configure =
+$(1)_CFLAGS?=$$(shell $(2) $(or $(3),--cflags) $(5))
+$(1)_LDFLAGS?=$$(shell $(2) $(or $(4),--libs) $(5))
+$(call Propogate_flags,$(1))
+endef
+
+# Configure library with pkg-config. The package name is
+# taken from a _PKGCONFIG variable.
+# 1: variable prefix
+#
+#     LIBGME_PKGCONFIG=libgme
+#     $(eval $(call Use_pkg_config,LIBGME))
+define Use_pkg_config =
+$(call Configure,$(1),$(PKG_CONFIG),,,$($(1)_PKGCONFIG))
+endef
+
+# Check disabling flag and configure package in one step
+# according to delimited argument.
+# (There is only one argument, but it split by slash.)
+# 1/: short form library name (uppercase). This is
+#     prefixed with 'NO' and 'HAVE_'. E.g. NOGME, HAVE_GME
+# /2: package name (e.g. libgme)
+# /3: variable prefix
+#
+# The following example would check if NOGME is not
+# defined before attempting to define LIBGME_CFLAGS and
+# LIBGME_LDFLAGS as with Use_pkg_config.
+#
+#     $(eval $(call Check_pkg_config,GME/libgme/LIBGME))
+define Check_pkg_config =
+_p:=$(subst /, ,$(1))
+_v1:=$$(word 1,$$(_p))
+_v2:=$$(or $$(word 3,$$(_p)),$$(_v1))
+ifndef NO$$(_v1)
+$$(_v2)_PKGCONFIG?=$$(word 2,$$(_p))
+$$(eval $$(call Use_pkg_config,$$(_v2)))
+opts+=-DHAVE_$$(_v1)
+endif
+endef
+
+#     $(call Prefix,gcc)
+Prefix=$(if $(PREFIX),$(PREFIX)-)$(1)
+
+Echo=
+Echo_name=
+Print=
+
+ifndef SILENT
+Echo=@echo $(1)
+ifndef ECHO
+ifndef NOECHOFILENAMES
+Echo_name=$(call Echo,-- $(1) ...)
+endif
+endif
+ifndef MAKE_RESTARTS
+ifndef destructive
+Print=$(info $(1))
+endif
+endif
+endif
+
+.=$(call Ifndef,ECHO,@)
diff --git a/src/Makefile.d/versions.mk b/src/Makefile.d/versions.mk
new file mode 100644
index 0000000000000000000000000000000000000000..f0b59658ee741e7d709b4d4037b1745a1e3bfefb
--- /dev/null
+++ b/src/Makefile.d/versions.mk
@@ -0,0 +1,175 @@
+#
+# Flags to put a sock in GCC!
+#
+
+# See the versions list in detect.mk
+# This will define all version flags going backward.
+# Yes, it's magic.
+define _predecessor =
+ifdef GCC$(firstword $(1))
+GCC$(lastword $(1)):=1
+endif
+endef
+_n:=$(words $(gcc_versions))
+$(foreach v,$(join $(wordlist 2,$(_n),- $(gcc_versions)),\
+	$(addprefix =,$(wordlist 2,$(_n),$(gcc_versions)))),\
+	$(and $(findstring =,$(v)),\
+	$(eval $(call _predecessor,$(subst =, ,$(v))))))
+
+# -W -Wno-unused
+WFLAGS:=-Wall -Wno-trigraphs
+ifndef GCC295
+#WFLAGS+=-Wno-packed
+endif
+ifndef RELAXWARNINGS
+ WFLAGS+=-W
+#WFLAGS+=-Wno-sign-compare
+ifndef GCC295
+ WFLAGS+=-Wno-div-by-zero
+endif
+#WFLAGS+=-Wsystem-headers
+WFLAGS+=-Wfloat-equal
+#WFLAGS+=-Wtraditional
+ WFLAGS+=-Wundef
+ifndef GCC295
+ WFLAGS+=-Wendif-labels
+endif
+ifdef GCC41
+ WFLAGS+=-Wshadow
+endif
+#WFLAGS+=-Wlarger-than-%len%
+ WFLAGS+=-Wpointer-arith -Wbad-function-cast
+ifdef GCC45
+#WFLAGS+=-Wc++-compat
+endif
+ WFLAGS+=-Wcast-qual
+ifndef NOCASTALIGNWARN
+ WFLAGS+=-Wcast-align
+endif
+ WFLAGS+=-Wwrite-strings
+ifndef ERRORMODE
+#WFLAGS+=-Wconversion
+ifdef GCC43
+ #WFLAGS+=-Wno-sign-conversion
+endif
+endif
+ WFLAGS+=-Wsign-compare
+ifdef GCC91
+ WFLAGS+=-Wno-error=address-of-packed-member
+endif
+ifdef GCC45
+ WFLAGS+=-Wlogical-op
+endif
+ WFLAGS+=-Waggregate-return
+ifdef HAIKU
+ifdef GCC41
+ #WFLAGS+=-Wno-attributes
+endif
+endif
+#WFLAGS+=-Wstrict-prototypes
+ifdef GCC40
+ WFLAGS+=-Wold-style-definition
+endif
+ WFLAGS+=-Wmissing-prototypes -Wmissing-declarations
+ifdef GCC40
+ WFLAGS+=-Wmissing-field-initializers
+endif
+ WFLAGS+=-Wmissing-noreturn
+#WFLAGS+=-Wmissing-format-attribute
+#WFLAGS+=-Wno-multichar
+#WFLAGS+=-Wno-deprecated-declarations
+#WFLAGS+=-Wpacked
+#WFLAGS+=-Wpadded
+#WFLAGS+=-Wredundant-decls
+ WFLAGS+=-Wnested-externs
+#WFLAGS+=-Wunreachable-code
+ WFLAGS+=-Winline
+ifdef GCC43
+ WFLAGS+=-funit-at-a-time
+ WFLAGS+=-Wlogical-op
+endif
+ifndef GCC295
+ WFLAGS+=-Wdisabled-optimization
+endif
+endif
+WFLAGS+=-Wformat-y2k
+ifdef GCC71
+WFLAGS+=-Wno-error=format-overflow=2
+endif
+WFLAGS+=-Wformat-security
+ifndef GCC29
+#WFLAGS+=-Winit-self
+endif
+ifdef GCC46
+WFLAGS+=-Wno-suggest-attribute=noreturn
+endif
+
+ifdef NOLDWARNING
+LDFLAGS+=-Wl,--as-needed
+endif
+
+ifdef ERRORMODE
+WFLAGS+=-Werror
+endif
+
+ifdef GCC43
+ #WFLAGS+=-Wno-error=clobbered
+endif
+ifdef GCC44
+ WFLAGS+=-Wno-error=array-bounds
+endif
+ifdef GCC46
+ WFLAGS+=-Wno-error=suggest-attribute=noreturn
+endif
+ifdef GCC54
+ WFLAGS+=-Wno-logical-op -Wno-error=logical-op
+endif
+ifdef GCC61
+ WFLAGS+=-Wno-tautological-compare -Wno-error=tautological-compare
+endif
+ifdef GCC71
+ WFLAGS+=-Wimplicit-fallthrough=4
+endif
+ifdef GCC81
+ WFLAGS+=-Wno-error=format-overflow
+ WFLAGS+=-Wno-error=stringop-truncation
+ WFLAGS+=-Wno-error=stringop-overflow
+ WFLAGS+=-Wno-format-overflow
+ WFLAGS+=-Wno-stringop-truncation
+ WFLAGS+=-Wno-stringop-overflow
+ WFLAGS+=-Wno-error=multistatement-macros
+endif
+
+ifdef NONX86
+  ifdef X86_64 # yeah that SEEMS contradictory
+  opts+=-march=nocona
+  endif
+else
+  ifndef GCC29
+  opts+=-msse3 -mfpmath=sse
+  else
+  opts+=-mpentium
+  endif
+endif
+
+ifdef DEBUGMODE
+ifdef GCC48
+opts+=-Og
+else
+opts+=O0
+endif
+endif
+
+ifdef VALGRIND
+ifdef GCC46
+WFLAGS+=-Wno-error=unused-but-set-variable
+WFLAGS+=-Wno-unused-but-set-variable
+endif
+endif
+
+# Lua
+ifdef GCC43
+ifndef GCC44
+WFLAGS+=-Wno-logical-op
+endif
+endif
diff --git a/src/Makefile.d/win32.mk b/src/Makefile.d/win32.mk
new file mode 100644
index 0000000000000000000000000000000000000000..768133c151c7a597871ff605fa0eb045cfc7df05
--- /dev/null
+++ b/src/Makefile.d/win32.mk
@@ -0,0 +1,104 @@
+#
+# Mingw, if you don't know, that's Win32/Win64
+#
+
+ifndef MINGW64
+EXENAME?=srb2win.exe
+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
+
+nasm_format:=win32
+
+SDL=1
+
+ifndef NOHW
+opts+=-DUSE_WGL_SWAP
+endif
+
+ifdef MINGW64
+libs+=-lws2_32
+else
+ifdef NO_IPV6
+libs+=-lwsock32
+else
+libs+=-lws2_32
+endif
+endif
+
+ifndef NONET
+ifndef MINGW64 # miniupnc is broken with MINGW64
+opts+=-I../libs -DSTATIC_MINIUPNPC
+libs+=-L../libs/miniupnpc/mingw$(32) -lws2_32 -liphlpapi
+endif
+endif
+
+ifndef MINGW64
+32=32
+x86=x86
+i686=i686
+else
+32=64
+x86=x86_64
+i686=x86_64
+endif
+
+mingw:=$(i686)-w64-mingw32
+
+define _set =
+$(1)_CFLAGS?=$($(1)_opts)
+$(1)_LDFLAGS?=$($(1)_libs)
+endef
+
+lib:=../libs/gme
+LIBGME_opts:=-I$(lib)/include
+LIBGME_libs:=-L$(lib)/win$(32) -lgme
+$(eval $(call _set,LIBGME))
+
+lib:=../libs/libopenmpt
+LIBOPENMPT_opts:=-I$(lib)/inc
+LIBOPENMPT_libs:=-L$(lib)/lib/$(x86)/mingw -lopenmpt
+$(eval $(call _set,LIBOPENMPT))
+
+ifndef NOMIXERX
+HAVE_MIXERX=1
+lib:=../libs/SDLMixerX/$(mingw)
+else
+lib:=../libs/SDL2_mixer/$(mingw)
+endif
+
+mixer_opts:=-I$(lib)/include/SDL2
+mixer_libs:=-L$(lib)/lib
+
+lib:=../libs/SDL2/$(mingw)
+SDL_opts:=-I$(lib)/include/SDL2\
+	$(mixer_opts) -Dmain=SDL_main
+SDL_libs:=-L$(lib)/lib $(mixer_libs)\
+	-lmingw32 -lSDL2main -lSDL2 -mwindows
+$(eval $(call _set,SDL))
+
+lib:=../libs/zlib
+ZLIB_opts:=-I$(lib)
+ZLIB_libs:=-L$(lib)/win32 -lz$(32)
+$(eval $(call _set,ZLIB))
+
+ifndef PNG_CONFIG
+lib:=../libs/libpng-src
+PNG_opts:=-I$(lib)
+PNG_libs:=-L$(lib)/projects -lpng$(32)
+$(eval $(call _set,PNG))
+endif
+
+lib:=../libs/curl
+CURL_opts:=-I$(lib)/include
+CURL_libs:=-L$(lib)/lib$(32) -lcurl
+$(eval $(call _set,CURL))
diff --git a/src/Sourcefile b/src/Sourcefile
new file mode 100644
index 0000000000000000000000000000000000000000..983dadaf0cbea42079ce68f032323c6c03a4a595
--- /dev/null
+++ b/src/Sourcefile
@@ -0,0 +1,98 @@
+string.c
+d_main.c
+d_clisrv.c
+d_net.c
+d_netfil.c
+d_netcmd.c
+dehacked.c
+deh_soc.c
+deh_lua.c
+deh_tables.c
+z_zone.c
+f_finale.c
+f_wipe.c
+g_demo.c
+g_game.c
+g_input.c
+am_map.c
+command.c
+console.c
+hu_stuff.c
+y_inter.c
+st_stuff.c
+m_aatree.c
+m_anigif.c
+m_argv.c
+m_bbox.c
+m_cheat.c
+m_cond.c
+m_easing.c
+m_fixed.c
+m_menu.c
+m_misc.c
+m_perfstats.c
+m_random.c
+m_queue.c
+info.c
+p_ceilng.c
+p_enemy.c
+p_floor.c
+p_inter.c
+p_lights.c
+p_map.c
+p_maputl.c
+p_mobj.c
+p_polyobj.c
+p_saveg.c
+p_setup.c
+p_sight.c
+p_spec.c
+p_telept.c
+p_tick.c
+p_user.c
+p_slopes.c
+tables.c
+r_bsp.c
+r_data.c
+r_draw.c
+r_main.c
+r_plane.c
+r_segs.c
+r_skins.c
+r_sky.c
+r_splats.c
+r_things.c
+r_textures.c
+r_patch.c
+r_patchrotation.c
+r_picformats.c
+r_portal.c
+screen.c
+taglist.c
+v_video.c
+s_sound.c
+sounds.c
+w_wad.c
+filesrch.c
+mserv.c
+http-mserv.c
+i_tcp.c
+lzf.c
+vid_copy.s
+b_bot.c
+lua_script.c
+lua_baselib.c
+lua_mathlib.c
+lua_hooklib.c
+lua_consolelib.c
+lua_infolib.c
+lua_mobjlib.c
+lua_playerlib.c
+lua_skinlib.c
+lua_thinkerlib.c
+lua_maplib.c
+lua_taglib.c
+lua_polyobjlib.c
+lua_blockmaplib.c
+lua_hudlib.c
+lua_inputlib.c
diff --git a/src/am_map.c b/src/am_map.c
index 53a7480a5468d113226cdcbdde34d495f735e55d..24379e2f13d91822c8aed096ae3f972a95a0bfee 100644
--- a/src/am_map.c
+++ b/src/am_map.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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/am_map.h b/src/am_map.h
index 1c8fa70e4b8274b76b17444fe6b61d28cfdc4617..022a7208b3fdf5a94f38101f5f1daccb2ff802ed 100644
--- a/src/am_map.h
+++ b/src/am_map.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/apng.c b/src/apng.c
index 0abbe541d822082b37b83e3aaaa73a96f76d4240..36b205c60998099009c21ce6a49fe97a71d44faa 100644
--- a/src/apng.c
+++ b/src/apng.c
@@ -1,5 +1,5 @@
 /*
-Copyright 2019-2020, James R.
+Copyright 2019-2021, James R.
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/src/apng.h b/src/apng.h
index a8b5c8f2422ef9fdb23a04038335d7200f37516b..893b523cbcacb8fd9f58a5be59b8dac3ef787596 100644
--- a/src/apng.h
+++ b/src/apng.h
@@ -1,5 +1,5 @@
 /*
-Copyright 2019-2020, James R.
+Copyright 2019-2021, James R.
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/src/asm_defs.inc b/src/asm_defs.inc
index ec286b0bd1bc7a6633fa0708c64751a28e90d353..9074f20f86d53523bf19030f01fb9e4e63e6283d 100644
--- a/src/asm_defs.inc
+++ b/src/asm_defs.inc
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/b_bot.c b/src/b_bot.c
index d3635f32c5d75ca1ad56c199241ebfa91c79ea0f..cdd74fc0757522ac2a7c30dfe3dec50237c446ea 100644
--- a/src/b_bot.c
+++ b/src/b_bot.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2007-2016 by John "JTE" Muniz.
-// Copyright (C) 2011-2020 by Sonic Team Junior.
+// Copyright (C) 2011-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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)
+	// Lua can handle it!
+	if (LUA_HookBotAI(sonic, tails, cmd))
 		return;
 
-	// Lua can handle it!
-	if (LUAh_BotAI(sonic, tails, cmd))
+	// 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)
@@ -363,25 +381,28 @@ void B_BuildTiccmd(player_t *player, ticcmd_t *cmd)
 	CV_SetValue(&cv_analog[1], false);
 
 	// Let Lua scripts build ticcmds
-	if (LUAh_BotTiccmd(player, 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,21 +468,26 @@ 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;
 
 	// B_RespawnBot doesn't do anything if the condition above this isn't met
 	{
-		UINT8 shouldForce = LUAh_BotRespawn(sonic, tails);
+		UINT8 shouldForce = LUA_Hook2Mobj(sonic, tails, MOBJ_HOOK(BotRespawn));
 
 		if (P_MobjWasRemoved(sonic) || P_MobjWasRemoved(tails))
 			return (shouldForce == 1); // mobj was removed
@@ -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 2806bd68f892ab394ccb7db6a03ceee79062e565..a89cfab19535477180971b0ba428f248b678b455 100644
--- a/src/b_bot.h
+++ b/src/b_bot.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2007-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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/CMakeLists.txt b/src/blua/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4e9c67d2f348a8bfed899e4002d25136284b031f
--- /dev/null
+++ b/src/blua/CMakeLists.txt
@@ -0,0 +1 @@
+target_sourcefile(c)
diff --git a/src/blua/Makefile.cfg b/src/blua/Makefile.cfg
deleted file mode 100644
index eae95ba3ae2cf52656cd40eaff40b185a694cf51..0000000000000000000000000000000000000000
--- a/src/blua/Makefile.cfg
+++ /dev/null
@@ -1,52 +0,0 @@
-ifdef UNIXCOMMON
-LUA_CFLAGS+=-DLUA_USE_POSIX
-endif
-ifdef LINUX
-LUA_CFLAGS+=-DLUA_USE_POSIX
-endif
-ifdef GCC43
-ifndef GCC44
-WFLAGS+=-Wno-logical-op
-endif
-endif
-
-OBJS:=$(OBJS) \
-	$(OBJDIR)/lapi.o \
-	$(OBJDIR)/lbaselib.o \
-	$(OBJDIR)/ldo.o \
-	$(OBJDIR)/lfunc.o \
-	$(OBJDIR)/linit.o \
-	$(OBJDIR)/liolib.o \
-	$(OBJDIR)/llex.o \
-	$(OBJDIR)/lmem.o \
-	$(OBJDIR)/lobject.o \
-	$(OBJDIR)/lstate.o \
-	$(OBJDIR)/lstrlib.o \
-	$(OBJDIR)/ltablib.o \
-	$(OBJDIR)/lundump.o \
-	$(OBJDIR)/lzio.o \
-	$(OBJDIR)/lauxlib.o \
-	$(OBJDIR)/lcode.o \
-	$(OBJDIR)/ldebug.o \
-	$(OBJDIR)/ldump.o \
-	$(OBJDIR)/lgc.o \
-	$(OBJDIR)/lopcodes.o \
-	$(OBJDIR)/lparser.o \
-	$(OBJDIR)/lstring.o \
-	$(OBJDIR)/ltable.o \
-	$(OBJDIR)/ltm.o \
-	$(OBJDIR)/lvm.o \
-	$(OBJDIR)/lua_script.o \
-	$(OBJDIR)/lua_baselib.o \
-	$(OBJDIR)/lua_mathlib.o \
-	$(OBJDIR)/lua_hooklib.o \
-	$(OBJDIR)/lua_consolelib.o \
-	$(OBJDIR)/lua_infolib.o \
-	$(OBJDIR)/lua_mobjlib.o \
-	$(OBJDIR)/lua_playerlib.o \
-	$(OBJDIR)/lua_skinlib.o \
-	$(OBJDIR)/lua_thinkerlib.o \
-	$(OBJDIR)/lua_maplib.o \
-	$(OBJDIR)/lua_polyobjlib.o \
-	$(OBJDIR)/lua_blockmaplib.o \
-	$(OBJDIR)/lua_hudlib.o
diff --git a/src/blua/Sourcefile b/src/blua/Sourcefile
new file mode 100644
index 0000000000000000000000000000000000000000..f99c89c8dfb8e8b5da643cb2c8625a764e84580d
--- /dev/null
+++ b/src/blua/Sourcefile
@@ -0,0 +1,25 @@
+lapi.c
+lbaselib.c
+ldo.c
+lfunc.c
+linit.c
+liolib.c
+llex.c
+lmem.c
+lobject.c
+lstate.c
+lstrlib.c
+ltablib.c
+lundump.c
+lzio.c
+lauxlib.c
+lcode.c
+ldebug.c
+ldump.c
+lgc.c
+lopcodes.c
+lparser.c
+lstring.c
+ltable.c
+ltm.c
+lvm.c
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/blua/lcode.c b/src/blua/lcode.c
index 5c7fed4541a4442d9d40691663965250de434619..fd4aaff24c33ae99e936497bac797d9bce95b061 100644
--- a/src/blua/lcode.c
+++ b/src/blua/lcode.c
@@ -686,6 +686,15 @@ static void codearith (FuncState *fs, OpCode op, expdesc *e1, expdesc *e2) {
 }
 
 
+static void codeunaryarith (FuncState *fs, OpCode op, expdesc *e) {
+  expdesc e2;
+  e2.t = e2.f = NO_JUMP; e2.k = VKNUM; e2.u.nval = 0;
+  if (op == OP_LEN || !isnumeral(e))
+    luaK_exp2anyreg(fs, e);  /* cannot operate on non-numeric constants */
+  codearith(fs, op, e, &e2);
+}
+
+
 static void codecomp (FuncState *fs, OpCode op, int cond, expdesc *e1,
                                                           expdesc *e2) {
   int o1 = luaK_exp2RK(fs, e1);
@@ -703,27 +712,11 @@ static void codecomp (FuncState *fs, OpCode op, int cond, expdesc *e1,
 
 
 void luaK_prefix (FuncState *fs, UnOpr op, expdesc *e) {
-  expdesc e2;
-  e2.t = e2.f = NO_JUMP; e2.k = VKNUM; e2.u.nval = 0;
   switch (op) {
-    case OPR_MINUS: {
-      if (!isnumeral(e))
-        luaK_exp2anyreg(fs, e);  /* cannot operate on non-numeric constants */
-      codearith(fs, OP_UNM, e, &e2);
-      break;
-    }
-    case OPR_BNOT: {
-      if (e->k == VK)
-        luaK_exp2anyreg(fs, e);  /* cannot operate on non-numeric constants */
-      codearith(fs, OP_BNOT, e, &e2);
-      break;
-    }
+    case OPR_MINUS: codeunaryarith(fs, OP_UNM, e); break;
+    case OPR_BNOT: codeunaryarith(fs, OP_BNOT, e); break;
     case OPR_NOT: codenot(fs, e); break;
-    case OPR_LEN: {
-      luaK_exp2anyreg(fs, e);  /* cannot operate on constants */
-      codearith(fs, OP_LEN, e, &e2);
-      break;
-    }
+    case OPR_LEN: codeunaryarith(fs, OP_LEN, e); break;
     default: lua_assert(0);
   }
 }
diff --git a/src/byteptr.h b/src/byteptr.h
index 01a6293b41401f9b663b6b672986a286b85e449a..4c8414fae29c7d3498a9a085f607243d261c3af9 100644
--- a/src/byteptr.h
+++ b/src/byteptr.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/command.c b/src/command.c
index 58434ef8983a1a0ed1816a9522783214896351b1..ae4a7178e437c9039ae4717defb377c091fad215 100644
--- a/src/command.c
+++ b/src/command.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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;
@@ -1433,6 +1433,7 @@ static void Setvalue(consvar_t *var, const char *valstr, boolean stealth)
 						if (var->revert.allocated)
 						{
 							Z_Free(var->revert.v.string);
+							var->revert.allocated = false; // the below value is not allocated in zone memory, don't try to free it!
 						}
 
 						var->revert.v.const_munge = var->PossibleValue[i].strvalue;
@@ -1440,6 +1441,10 @@ static void Setvalue(consvar_t *var, const char *valstr, boolean stealth)
 						return;
 					}
 
+					// free the old value string
+					Z_Free(var->zstring);
+					var->zstring = NULL;
+
 					var->value = var->PossibleValue[i].value;
 					var->string = var->PossibleValue[i].strvalue;
 					goto finish;
@@ -1502,13 +1507,7 @@ static void Setvalue(consvar_t *var, const char *valstr, boolean stealth)
 found:
 			if (client && execversion_enabled)
 			{
-				if (var->revert.allocated)
-				{
-					Z_Free(var->revert.v.string);
-				}
-
 				var->revert.v.const_munge = var->PossibleValue[i].strvalue;
-
 				return;
 			}
 
@@ -1523,6 +1522,7 @@ found:
 		if (var->revert.allocated)
 		{
 			Z_Free(var->revert.v.string);
+			// Z_StrDup creates a new zone memory block, so we can keep the allocated flag on
 		}
 
 		var->revert.v.string = Z_StrDup(valstr);
@@ -1577,7 +1577,7 @@ finish:
 	}
 	var->flags |= CV_MODIFIED;
 	// raise 'on change' code
-	LUA_CVarChanged(var->name); // let consolelib know what cvar this is.
+	LUA_CVarChanged(var); // let consolelib know what cvar this is.
 	if (var->flags & CV_CALL && !stealth)
 		var->func();
 
@@ -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 );
@@ -1787,6 +1789,7 @@ void CV_RevertNetVars(void)
 			if (cvar->revert.allocated)
 			{
 				Z_Free(cvar->revert.v.string);
+				cvar->revert.allocated = false; // no value being held now
 			}
 
 			cvar->revert.v.string = NULL;
@@ -2363,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/command.h b/src/command.h
index ea5593395cc369e4f771bb7f55abf737045a503b..34fd15963262d6ec4e4416ac8cbb514301f72d6f 100644
--- a/src/command.h
+++ b/src/command.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -158,7 +158,7 @@ typedef struct consvar_s //NULL, NULL, 0, NULL, NULL |, 0, NULL, NULL, 0, 0, NUL
 
 /* name, defaultvalue, flags, PossibleValue, func */
 #define CVAR_INIT( ... ) \
-{ __VA_ARGS__, 0, NULL, NULL, {0}, 0U, (char)0, NULL }
+{ __VA_ARGS__, 0, NULL, NULL, {0, {NULL}}, 0U, (char)0, NULL }
 
 #ifdef OLD22DEMOCOMPAT
 typedef struct old_demo_var old_demo_var_t;
diff --git a/src/config.h.in b/src/config.h.in
index a6f43a7d7b6ab1df4f2abc00e110b97c4290a0cd..db794cccc82a59eb378f53f9adaa4d5d1bd3cb20 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -34,12 +34,13 @@
  * Last updated 2020 / 07 / 10 - v2.2.6 - player.dta & patch.pk3
  * Last updated 2020 / 09 / 27 - v2.2.7 - patch.pk3
  * Last updated 2020 / 10 / 02 - v2.2.8 - patch.pk3
+ * Last updated 2021 / 05 / 06 - v2.2.9 - patch.pk3 & zones.pk3
  */
 #define ASSET_HASH_SRB2_PK3   "0277c9416756627004e83cbb5b2e3e28"
-#define ASSET_HASH_ZONES_PK3  "f7e88afb6af7996a834c7d663144bead"
+#define ASSET_HASH_ZONES_PK3  "f8f3e2b5deacf40f14e36686a07d44bb"
 #define ASSET_HASH_PLAYER_DTA "49dad7b24634c89728cc3e0b689e12bb"
 #ifdef USE_PATCH_DTA
-#define ASSET_HASH_PATCH_PK3  "466cdf60075262b3f5baa5e07f0999e8"
+#define ASSET_HASH_PATCH_PK3  "7d467a883f7887b3c311798ee2f56b6a"
 #endif
 
 #endif
diff --git a/src/console.c b/src/console.c
index b19b8818d709bca4e55f61c033cdd5e97d5f8413..6f21aeb3dd4d022c51e37c1781ec28aa029a652c 100644
--- a/src/console.c
+++ b/src/console.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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"));
@@ -360,30 +360,48 @@ static void CON_SetupColormaps(void)
 	for (i = 0; i < (256*15); i++, ++memorysrc)
 		*memorysrc = (UINT8)(i & 0xFF); // remap each color to itself...
 
-#define colset(map, a, b, c) \
-	map[1] = (UINT8)a;\
-	map[3] = (UINT8)b;\
-	map[9] = (UINT8)c
-
-	colset(magentamap, 177, 178, 184);
-	colset(yellowmap,   82,  73,  66);
-	colset(lgreenmap,   97,  98, 106);
-	colset(bluemap,    146, 147, 155);
-	colset(redmap,     210,  32,  39);
-	colset(graymap,      6,  8,   14);
-	colset(orangemap,   51,  52,  57);
-	colset(skymap,     129, 130, 133);
-	colset(purplemap,  160, 161, 163);
-	colset(aquamap,    120, 121, 123);
-	colset(peridotmap,  88, 188, 190);
-	colset(azuremap,   144, 145, 170);
-	colset(brownmap,   219, 221, 224);
-	colset(rosymap,    200, 201, 203);
-	colset(invertmap,   27,  26,  22);
-	invertmap[26] = (UINT8)3;
+#define colset(map, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) \
+	map[0x0] = (UINT8)a;\
+	map[0x1] = (UINT8)b;\
+	map[0x2] = (UINT8)c;\
+	map[0x3] = (UINT8)d;\
+	map[0x4] = (UINT8)e;\
+	map[0x5] = (UINT8)f;\
+	map[0x6] = (UINT8)g;\
+	map[0x7] = (UINT8)h;\
+	map[0x8] = (UINT8)i;\
+	map[0x9] = (UINT8)j;\
+	map[0xA] = (UINT8)k;\
+	map[0xB] = (UINT8)l;\
+	map[0xC] = (UINT8)m;\
+	map[0xD] = (UINT8)n;\
+	map[0xE] = (UINT8)o;\
+	map[0xF] = (UINT8)p;
+
+	// Tried to keep the colors vanilla while adding some shades in between them ~SonicX8000
+
+	//                      0x1       0x3                           0x9                           0xF
+	colset(magentamap, 177, 177, 178, 178, 178, 180, 180, 180, 182, 182, 182, 182, 184, 184, 184, 185);
+	colset(yellowmap,   82,  82,  73,  73,  73,  64,  64,  64,  66,  66,  66,  66,  67,  67,  67,  68);
+	colset(lgreenmap,   96,  96,  98,  98,  98, 101, 101, 101, 104, 104, 104, 104, 106, 106, 106, 107);
+	colset(bluemap,    146, 146, 147, 147, 147, 149, 149, 149, 152, 152, 152, 152, 155, 155, 155, 157);
+	colset(redmap,      32,  32,  33,  33,  33,  35,  35,  35,  39,  39,  39,  39,  42,  42,  42,  44);
+	colset(graymap,      8,   9,  10,  11,  12,  13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23);
+	colset(orangemap,   50,  50,  52,  52,  52,  54,  54,  54,  56,  56,  56,  56,  59,  59,  59,  60);
+	colset(skymap,     129, 129, 130, 130, 130, 131, 131, 131, 133, 133, 133, 133, 135, 135, 135, 136);
+	colset(purplemap,  160, 160, 161, 161, 161, 162, 162, 162, 163, 163, 163, 163, 164, 164, 164, 165);
+	colset(aquamap,    120, 120, 121, 121, 121, 122, 122, 122, 123, 123, 123, 123, 124, 124, 124, 125);
+	colset(peridotmap,  72,  72, 188, 188, 189, 189, 189, 189, 190, 190, 190, 190, 191, 191, 191,  94);
+	colset(azuremap,   144, 144, 145, 145, 145, 146, 146, 146, 170, 170, 170, 170, 171, 171, 171, 172);
+	colset(brownmap,   219, 219, 221, 221, 221, 222, 222, 222, 224, 224, 224, 224, 227, 227, 227, 229);
+	colset(rosymap,    200, 200, 201, 201, 201, 202, 202, 202, 203, 203, 203, 203, 204, 204, 204, 205);
 
 #undef colset
 
+	// Yeah just straight up invert it like a normal person
+	for (i = 0x00; i <= 0x1F; i++)
+		invertmap[0x1F - i] = i;
+
 	// Init back colormap
 	CON_SetupBackColormap();
 }
@@ -466,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)
@@ -808,6 +839,12 @@ static void CON_InputDelSelection(void)
 
 	Lock_state();
 
+	if (!input_cur)
+	{
+		Unlock_state();
+		return;
+	}
+
 	if (input_cur > input_sel)
 	{
 		start = input_sel;
@@ -889,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)
@@ -902,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;
@@ -1279,10 +1316,6 @@ boolean CON_Responder(event_t *ev)
 	if (key < 32 || key > 127)
 		return true;
 
-	// add key to cmd line here
-	if (key >= 'A' && key <= 'Z' && !(shiftdown ^ capslock)) //this is only really necessary for dedicated servers
-		key = key + 'a' - 'A';
-
 	if (input_sel != input_cur)
 		CON_InputDelSelection();
 	CON_InputAddChar(key);
@@ -1677,7 +1710,10 @@ static void CON_DrawHudlines(void)
 			{
 				charflags = (*p & 0x7f) << V_CHARCOLORSHIFT;
 				p++;
+				c++;
 			}
+			if (c >= con_width)
+				break;
 			if (*p < HU_FONTSTART)
 				;//charwidth = 4 * con_scalefactor;
 			else
@@ -1736,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);
@@ -1798,7 +1834,10 @@ static void CON_DrawConsole(void)
 			{
 				charflags = (*p & 0x7f) << V_CHARCOLORSHIFT;
 				p++;
+				c++;
 			}
+			if (c >= con_width)
+				break;
 			V_DrawCharacter(x, y, (INT32)(*p) | charflags | cv_constextsize.value | V_NOSCALESTART, true);
 		}
 	}
diff --git a/src/console.h b/src/console.h
index 0296f4f6e658e82a01d78a2ae05f636d90e411ed..accf89d960faf6975ecd02345bba66d6ef3fd264 100644
--- a/src/console.h
+++ b/src/console.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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 b198011a0eeff38f0e3936cc7a28b1ce3281554e..78a3ebe6cb961cff2fb22e5e0f4520559078865a 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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);
@@ -1565,15 +1657,6 @@ static void CL_LoadReceivedSavegame(boolean reloading)
 		}
 		CONS_Printf("\"\n");
 	}
-	else
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("Can't load the level!\n"));
-		Z_Free(savebuffer);
-		save_p = NULL;
-		if (unlink(tmpsave) == -1)
-			CONS_Alert(CONS_ERROR, M_GetText("Can't delete %s\n"), tmpsave);
-		return;
-	}
 
 	// done
 	Z_Free(savebuffer);
@@ -1595,9 +1678,7 @@ static void CL_ReloadReceivedSavegame(void)
 
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
-#ifdef HAVE_BLUA
 		LUA_InvalidatePlayer(&players[i]);
-#endif
 		sprintf(player_names[i], "Player %d", i + 1);
 	}
 
@@ -1678,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++;
 	}
@@ -1840,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.
@@ -1870,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;
 	}
@@ -1997,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++)
@@ -2017,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
@@ -2045,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;
 
@@ -2054,7 +2359,6 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 		case CL_ABORTED:
 			cl_mode = CL_SEARCHING;
 			return false;
-
 	}
 
 	GetPackets();
@@ -2064,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)
@@ -2103,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();
@@ -2171,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);
 
@@ -2260,11 +2579,15 @@ void D_SaveBan(void)
 	size_t i;
 	banreason_t *reasonlist = reasonhead;
 	const char *address, *mask;
+	const char *path = va("%s"PATHSEP"%s", srb2home, "ban.txt");
 
 	if (!reasonhead)
+	{
+		remove(path);
 		return;
+	}
 
-	f = fopen(va("%s"PATHSEP"%s", srb2home, "ban.txt"), "w");
+	f = fopen(path, "w");
 
 	if (!f)
 	{
@@ -2308,16 +2631,14 @@ static void Ban_Add(const char *reason)
 	reasontail = reasonlist;
 }
 
-static void Command_ClearBans(void)
+static void Ban_Clear(void)
 {
 	banreason_t *temp;
 
-	if (!I_ClearBans)
-		return;
-
 	I_ClearBans();
-	D_SaveBan();
+
 	reasontail = NULL;
+
 	while (reasonhead)
 	{
 		temp = reasonhead->next;
@@ -2327,6 +2648,15 @@ static void Command_ClearBans(void)
 	}
 }
 
+static void Command_ClearBans(void)
+{
+	if (!I_ClearBans)
+		return;
+
+	Ban_Clear();
+	D_SaveBan();
+}
+
 static void Ban_Load_File(boolean warning)
 {
 	FILE *f;
@@ -2334,6 +2664,9 @@ static void Ban_Load_File(boolean warning)
 	const char *address, *mask;
 	char buffer[MAX_WADPATH];
 
+	if (!I_ClearBans)
+		return;
+
 	f = fopen(va("%s"PATHSEP"%s", srb2home, "ban.txt"), "r");
 
 	if (!f)
@@ -2343,13 +2676,7 @@ static void Ban_Load_File(boolean warning)
 		return;
 	}
 
-	if (I_ClearBans)
-		Command_ClearBans();
-	else
-	{
-		fclose(f);
-		return;
-	}
+	Ban_Clear();
 
 	for (i=0; fgets(buffer, (int)sizeof(buffer), f); i++)
 	{
@@ -2466,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.
@@ -2539,14 +2866,14 @@ static void CL_RemovePlayer(INT32 playernum, kickreason_t reason)
 		}
 	}
 
-	LUAh_PlayerQuit(&players[playernum], reason); // Lua hook for player quitting
+	LUA_HookPlayerQuit(&players[playernum], reason); // Lua hook for player quitting
 
 	// don't look through someone's view who isn't there
 	if (playernum == displayplayer)
 	{
 		// Call ViewpointSwitch hooks here.
 		// The viewpoint was forcibly changed.
-		LUAh_ViewpointSwitch(&players[consoleplayer], &players[consoleplayer], true);
+		LUA_HookViewpointSwitch(&players[consoleplayer], &players[consoleplayer], true);
 		displayplayer = consoleplayer;
 	}
 
@@ -2602,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
 }
@@ -2861,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)
@@ -2955,7 +3317,7 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 	{
 		case KICK_MSG_GO_AWAY:
 			if (!players[pnum].quittime)
-				HU_AddChatText(va("\x82*%s has been kicked (Go away)", player_names[pnum]), false);
+				HU_AddChatText(va("\x82*%s has been kicked (No reason given)", player_names[pnum]), false);
 			kickreason = KR_KICK;
 			break;
 		case KICK_MSG_PING_HIGH:
@@ -2963,7 +3325,7 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 			kickreason = KR_PINGLIMIT;
 			break;
 		case KICK_MSG_CON_FAIL:
-			HU_AddChatText(va("\x82*%s left the game (Synch Failure)", player_names[pnum]), false);
+			HU_AddChatText(va("\x82*%s left the game (Synch failure)", player_names[pnum]), false);
 			kickreason = KR_SYNCH;
 
 			if (M_CheckParm("-consisdump")) // Helps debugging some problems
@@ -3009,7 +3371,7 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 			kickreason = KR_LEAVE;
 			break;
 		case KICK_MSG_BANNED:
-			HU_AddChatText(va("\x82*%s has been banned (Don't come back)", player_names[pnum]), false);
+			HU_AddChatText(va("\x82*%s has been banned (No reason given)", player_names[pnum]), false);
 			kickreason = KR_BAN;
 			break;
 		case KICK_MSG_CUSTOM_KICK:
@@ -3026,8 +3388,7 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 
 	if (pnum == consoleplayer)
 	{
-		if (Playing())
-			LUAh_GameQuit();
+		LUA_HookBool(false, HOOK(GameQuit));
 #ifdef DUMPCONSISTENCY
 		if (msg == KICK_MSG_CON_FAIL) SV_SavedGame();
 #endif
@@ -3069,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);
 
@@ -3107,7 +3440,7 @@ consvar_t cv_maxplayers = CVAR_INIT ("maxplayers", "8", CV_SAVE|CV_NETVAR, maxpl
 static CV_PossibleValue_t joindelay_cons_t[] = {{1, "MIN"}, {3600, "MAX"}, {0, "Off"}, {0, NULL}};
 consvar_t cv_joindelay = CVAR_INIT ("joindelay", "10", CV_SAVE|CV_NETVAR, joindelay_cons_t, NULL);
 static CV_PossibleValue_t rejointimeout_cons_t[] = {{1, "MIN"}, {60 * FRACUNIT, "MAX"}, {0, "Off"}, {0, NULL}};
-consvar_t cv_rejointimeout = CVAR_INIT ("rejointimeout", "Off", CV_SAVE|CV_NETVAR|CV_FLOAT, rejointimeout_cons_t, NULL);
+consvar_t cv_rejointimeout = CVAR_INIT ("rejointimeout", "2", CV_SAVE|CV_NETVAR|CV_FLOAT, rejointimeout_cons_t, NULL);
 
 static CV_PossibleValue_t resynchattempts_cons_t[] = {{1, "MIN"}, {20, "MAX"}, {0, "No"}, {0, NULL}};
 consvar_t cv_resynchattempts = CVAR_INIT ("resynchattempts", "10", CV_SAVE|CV_NETVAR, resynchattempts_cons_t, NULL);
@@ -3119,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);
@@ -3235,6 +3568,8 @@ void SV_ResetServer(void)
 	// clear server_context
 	memset(server_context, '-', 8);
 
+	CV_RevertNetVars();
+
 	DEBFILE("\n-=-=-=-=-=-=-= Server Reset =-=-=-=-=-=-=-\n\n");
 }
 
@@ -3260,6 +3595,9 @@ static inline void SV_GenContext(void)
 //
 void D_QuitNetGame(void)
 {
+	mousegrabbedbylua = true;
+	I_UpdateMouseGrab();
+
 	if (!netgame || !netbuffer)
 		return;
 
@@ -3447,7 +3785,7 @@ static void Got_AddPlayer(UINT8 **p, INT32 playernum)
 		COM_BufAddText(va("sayto %d %s\n", newplayernum, motd));
 
 	if (!rejoined)
-		LUAh_PlayerJoin(newplayernum);
+		LUA_HookInt(newplayernum, HOOK(PlayerJoin));
 }
 
 static boolean SV_AddWaitingPlayers(const char *name, const char *name2)
@@ -3624,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
@@ -3634,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
@@ -3727,8 +4118,7 @@ static void HandleConnect(SINT8 node)
 static void HandleShutdown(SINT8 node)
 {
 	(void)node;
-	if (Playing())
-		LUAh_GameQuit();
+	LUA_HookBool(false, HOOK(GameQuit));
 	D_QuitNetGame();
 	CL_Reset();
 	D_StartTitle();
@@ -3743,8 +4133,7 @@ static void HandleShutdown(SINT8 node)
 static void HandleTimeout(SINT8 node)
 {
 	(void)node;
-	if (Playing())
-		LUAh_GameQuit();
+	LUA_HookBool(false, HOOK(GameQuit));
 	D_QuitNetGame();
 	CL_Reset();
 	D_StartTitle();
@@ -3777,6 +4166,7 @@ static void HandleServerInfo(SINT8 node)
 
 static void PT_WillResendGamestate(void)
 {
+#ifndef NONET
 	char tmpsave[256];
 
 	if (server || cl_redownloadinggamestate)
@@ -3799,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;
 
@@ -3810,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
@@ -3836,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:
@@ -3886,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.
@@ -4096,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)
 				{
@@ -4265,7 +4682,7 @@ static void HandlePacketFromPlayer(SINT8 node)
 		case PT_RECEIVEDGAMESTATE:
 			sendingsavegame[node] = false;
 			resendingsavegame[node] = false;
-			savegameresendcooldown[node] = I_GetTime() + 15 * TICRATE;
+			savegameresendcooldown[node] = I_GetTime() + 5 * TICRATE;
 			break;
 // -------------------------------------------- CLIENT RECEIVE ----------
 		case PT_SERVERTICS:
@@ -4477,70 +4894,73 @@ static INT16 Consistancy(void)
 		ret += P_GetRandSeed();
 
 #ifdef MOBJCONSISTANCY
-	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
+	if (gamestate == GS_LEVEL)
 	{
-		if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
-			continue;
+		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
+		{
+			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+				continue;
 
-		mo = (mobj_t *)th;
+			mo = (mobj_t *)th;
 
-		if (mo->flags & (MF_SPECIAL | MF_SOLID | MF_PUSHABLE | MF_BOSS | MF_MISSILE | MF_SPRING | MF_MONITOR | MF_FIRE | MF_ENEMY | MF_PAIN | MF_STICKY))
-		{
-			ret -= mo->type;
-			ret += mo->x;
-			ret -= mo->y;
-			ret += mo->z;
-			ret -= mo->momx;
-			ret += mo->momy;
-			ret -= mo->momz;
-			ret += mo->angle;
-			ret -= mo->flags;
-			ret += mo->flags2;
-			ret -= mo->eflags;
-			if (mo->target)
-			{
-				ret += mo->target->type;
-				ret -= mo->target->x;
-				ret += mo->target->y;
-				ret -= mo->target->z;
-				ret += mo->target->momx;
-				ret -= mo->target->momy;
-				ret += mo->target->momz;
-				ret -= mo->target->angle;
-				ret += mo->target->flags;
-				ret -= mo->target->flags2;
-				ret += mo->target->eflags;
-				ret -= mo->target->state - states;
-				ret += mo->target->tics;
-				ret -= mo->target->sprite;
-				ret += mo->target->frame;
-			}
-			else
-				ret ^= 0x3333;
-			if (mo->tracer && mo->tracer->type != MT_OVERLAY)
+			if (mo->flags & (MF_SPECIAL | MF_SOLID | MF_PUSHABLE | MF_BOSS | MF_MISSILE | MF_SPRING | MF_MONITOR | MF_FIRE | MF_ENEMY | MF_PAIN | MF_STICKY))
 			{
-				ret += mo->tracer->type;
-				ret -= mo->tracer->x;
-				ret += mo->tracer->y;
-				ret -= mo->tracer->z;
-				ret += mo->tracer->momx;
-				ret -= mo->tracer->momy;
-				ret += mo->tracer->momz;
-				ret -= mo->tracer->angle;
-				ret += mo->tracer->flags;
-				ret -= mo->tracer->flags2;
-				ret += mo->tracer->eflags;
-				ret -= mo->tracer->state - states;
-				ret += mo->tracer->tics;
-				ret -= mo->tracer->sprite;
-				ret += mo->tracer->frame;
+				ret -= mo->type;
+				ret += mo->x;
+				ret -= mo->y;
+				ret += mo->z;
+				ret -= mo->momx;
+				ret += mo->momy;
+				ret -= mo->momz;
+				ret += mo->angle;
+				ret -= mo->flags;
+				ret += mo->flags2;
+				ret -= mo->eflags;
+				if (mo->target)
+				{
+					ret += mo->target->type;
+					ret -= mo->target->x;
+					ret += mo->target->y;
+					ret -= mo->target->z;
+					ret += mo->target->momx;
+					ret -= mo->target->momy;
+					ret += mo->target->momz;
+					ret -= mo->target->angle;
+					ret += mo->target->flags;
+					ret -= mo->target->flags2;
+					ret += mo->target->eflags;
+					ret -= mo->target->state - states;
+					ret += mo->target->tics;
+					ret -= mo->target->sprite;
+					ret += mo->target->frame;
+				}
+				else
+					ret ^= 0x3333;
+				if (mo->tracer && mo->tracer->type != MT_OVERLAY)
+				{
+					ret += mo->tracer->type;
+					ret -= mo->tracer->x;
+					ret += mo->tracer->y;
+					ret -= mo->tracer->z;
+					ret += mo->tracer->momx;
+					ret -= mo->tracer->momy;
+					ret += mo->tracer->momz;
+					ret -= mo->tracer->angle;
+					ret += mo->tracer->flags;
+					ret -= mo->tracer->flags2;
+					ret += mo->tracer->eflags;
+					ret -= mo->tracer->state - states;
+					ret += mo->tracer->tics;
+					ret -= mo->tracer->sprite;
+					ret += mo->tracer->frame;
+				}
+				else
+					ret ^= 0xAAAA;
+				ret -= mo->state - states;
+				ret += mo->tics;
+				ret -= mo->sprite;
+				ret += mo->frame;
 			}
-			else
-				ret ^= 0xAAAA;
-			ret -= mo->state - states;
-			ret += mo->tics;
-			ret -= mo->sprite;
-			ret += mo->frame;
 		}
 	}
 #endif
@@ -4845,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_GetTimeMicros();
+				if (update_stats)
+					PS_START_TIMING(ps_tictime);
 
 				G_Ticker((gametic % NEWTICRATERATIO) == 0);
 				ExtraDataTicker();
 				gametic++;
 				consistancy[gametic%BACKUPTICS] = Consistancy();
 
-				ps_tictime = I_GetTimeMicros() - 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)
@@ -4998,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 3d67525dacc65dd6c79d18c544cb7ff9fdffebac..8e75fb963c860d64e91557bc47b17daea22af7f4 100644
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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 3cce8fad1fe07908bd72f5220f7c20d724d46240..c30a8ced2b09cd7887446211cbc2b96622c0aba6 100644
--- a/src/d_event.h
+++ b/src/d_event.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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 1045d4d99b86d2295f26d5342bf195d8da9a71f1..83419d266c84703694d06644a28a953e3550f6b1 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -15,7 +15,7 @@
 ///        plus functions to parse command line parameters, configure game
 ///        parameters, and call the startup functions.
 
-#if (defined (__unix__) && !defined (MSDOS)) || defined(__APPLE__) || defined (UNIXCOMMON)
+#if defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)
 #include <sys/stat.h>
 #include <sys/types.h>
 #endif
@@ -61,11 +61,11 @@
 #include "p_local.h" // chasecam
 #include "mserv.h" // ms_RoomId
 #include "m_misc.h" // screenshot functionality
-#include "dehacked.h" // Dehacked list test
+#include "deh_tables.h" // Dehacked list test
 #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;
@@ -175,10 +175,53 @@ void D_ProcessEvents(void)
 
 	boolean eaten;
 
+	// Reset possibly stale mouse info
+	G_SetMouseDeltas(0, 0, 1);
+	G_SetMouseDeltas(0, 0, 2);
+	mouse.buttons &= ~(MB_SCROLLUP|MB_SCROLLDOWN);
+	mouse2.buttons &= ~(MB_SCROLLUP|MB_SCROLLDOWN);
+
 	for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
 	{
+		boolean hooked = false;
+
 		ev = &events[eventtail];
 
+		// Set mouse buttons early in case event is eaten later
+		if (ev->type == ev_keydown || ev->type == ev_keyup)
+		{
+			// Mouse buttons
+			if ((UINT32)(ev->key - KEY_MOUSE1) < MOUSEBUTTONS)
+			{
+				if (ev->type == ev_keydown)
+					mouse.buttons |= 1 << (ev->key - KEY_MOUSE1);
+				else
+					mouse.buttons &= ~(1 << (ev->key - KEY_MOUSE1));
+			}
+			else if ((UINT32)(ev->key - KEY_2MOUSE1) < MOUSEBUTTONS)
+			{
+				if (ev->type == ev_keydown)
+					mouse2.buttons |= 1 << (ev->key - KEY_2MOUSE1);
+				else
+					mouse2.buttons &= ~(1 << (ev->key - KEY_2MOUSE1));
+			}
+			// Scroll (has no keyup event)
+			else switch (ev->key) {
+				case KEY_MOUSEWHEELUP:
+					mouse.buttons |= MB_SCROLLUP;
+					break;
+				case KEY_MOUSEWHEELDOWN:
+					mouse.buttons |= MB_SCROLLDOWN;
+					break;
+				case KEY_2MOUSEWHEELUP:
+					mouse2.buttons |= MB_SCROLLUP;
+					break;
+				case KEY_2MOUSEWHEELDOWN:
+					mouse2.buttons |= MB_SCROLLDOWN;
+					break;
+			}
+		}
+
 		// Screenshots over everything so that they can be taken anywhere.
 		if (M_ScreenshotResponder(ev))
 			continue; // ate the event
@@ -189,6 +232,12 @@ void D_ProcessEvents(void)
 				continue;
 		}
 
+		if (!CON_Ready() && !menuactive) {
+			if (G_LuaResponder(ev))
+				continue;
+			hooked = true;
+		}
+
 		// Menu input
 #ifdef HAVE_THREADS
 		I_lock_mutex(&m_menu_mutex);
@@ -203,6 +252,12 @@ void D_ProcessEvents(void)
 		if (eaten)
 			continue; // menu ate the event
 
+		if (!hooked && !CON_Ready()) {
+			if (G_LuaResponder(ev))
+				continue;
+			hooked = true;
+		}
+
 		// console input
 #ifdef HAVE_THREADS
 		I_lock_mutex(&con_mutex);
@@ -217,8 +272,16 @@ void D_ProcessEvents(void)
 		if (eaten)
 			continue; // ate the event
 
+		if (!hooked && !CON_Ready() && G_LuaResponder(ev))
+			continue;
+
 		G_Responder(ev);
 	}
+
+	if (mouse.rdx || mouse.rdy)
+		G_SetMouseDeltas(mouse.rdx, mouse.rdy, 1);
+	if (mouse2.rdx || mouse2.rdy)
+		G_SetMouseDeltas(mouse2.rdx, mouse2.rdy, 2);
 }
 
 //
@@ -413,7 +476,7 @@ static void D_Display(void)
 
 			if (!automapactive && !dedicated && cv_renderview.value)
 			{
-				ps_rendercalltime = I_GetTimeMicros();
+				PS_START_TIMING(ps_rendercalltime);
 				if (players[displayplayer].mo || players[displayplayer].playerstate == PST_DEAD)
 				{
 					topleft = screens[0] + viewwindowy*vid.width + viewwindowx;
@@ -460,7 +523,7 @@ static void D_Display(void)
 					if (postimgtype2)
 						V_DoPostProcessor(1, postimgtype2, postimgparam2);
 				}
-				ps_rendercalltime = I_GetTimeMicros() - ps_rendercalltime;
+				PS_STOP_TIMING(ps_rendercalltime);
 			}
 
 			if (lastdraw)
@@ -474,7 +537,7 @@ static void D_Display(void)
 				lastdraw = false;
 			}
 
-			ps_uitime = I_GetTimeMicros();
+			PS_START_TIMING(ps_uitime);
 
 			if (gamestate == GS_LEVEL)
 			{
@@ -487,7 +550,7 @@ static void D_Display(void)
 		}
 		else
 		{
-			ps_uitime = I_GetTimeMicros();
+			PS_START_TIMING(ps_uitime);
 		}
 	}
 
@@ -529,7 +592,7 @@ static void D_Display(void)
 
 	CON_Drawer();
 
-	ps_uitime = I_GetTimeMicros() - ps_uitime;
+	PS_STOP_TIMING(ps_uitime);
 
 	//
 	// wipe update
@@ -615,9 +678,9 @@ static void D_Display(void)
 			M_DrawPerfStats();
 		}
 
-		ps_swaptime = I_GetTimeMicros();
+		PS_START_TIMING(ps_swaptime);
 		I_FinishUpdate(); // page flip or blit buffer
-		ps_swaptime = I_GetTimeMicros() - ps_swaptime;
+		PS_STOP_TIMING(ps_swaptime);
 	}
 }
 
@@ -860,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
+
+	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[pnumwadfiles] = newfile;
+	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.
@@ -934,7 +1030,7 @@ static void IdentifyVersion(void)
 	char *srb2wad;
 	const char *srb2waddir = NULL;
 
-#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
+#if defined (__unix__) || defined (UNIXCOMMON) || defined (HAVE_SDL)
 	// change to the directory where 'srb2.pk3' is found
 	srb2waddir = I_LocateWad();
 #endif
@@ -972,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);
 
@@ -983,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)
@@ -998,9 +1094,9 @@ static void IdentifyVersion(void)
 #define MUSICTEST(str) \
 		{\
 			const char *musicpath = va(pandf,srb2waddir,str);\
-			int ms = W_VerifyNMUSlumps(musicpath); \
+			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"); \
 		}
@@ -1045,7 +1141,7 @@ void D_SRB2Main(void)
 	// Print GPL notice for our console users (Linux)
 	CONS_Printf(
 	"\n\nSonic Robo Blast 2\n"
-	"Copyright (C) 1998-2020 by Sonic Team Junior\n\n"
+	"Copyright (C) 1998-2021 by Sonic Team Junior\n\n"
 	"This program comes with ABSOLUTELY NO WARRANTY.\n\n"
 	"This is free software, and you are welcome to redistribute it\n"
 	"and/or modify it under the terms of the GNU General Public License\n"
@@ -1072,7 +1168,7 @@ void D_SRB2Main(void)
 	G_LoadGameSettings();
 
 	// Test Dehacked lists
-	DEH_Check();
+	DEH_TableCheck();
 
 	// Netgame URL special case: change working dir to EXE folder.
 	ChangeDirForUrlHandler();
@@ -1107,7 +1203,7 @@ void D_SRB2Main(void)
 
 		if (!userhome)
 		{
-#if ((defined (__unix__) && !defined (MSDOS)) || defined(__APPLE__) || defined (UNIXCOMMON)) && !defined (__CYGWIN__)
+#if (defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)) && !defined (__CYGWIN__)
 			I_Error("Please set $HOME to your home directory\n");
 #else
 			if (dedicated)
@@ -1174,25 +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?
-				{
-					if (!W_VerifyNMUSlumps(s))
-						G_SetGameModified(true);
-					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]);
 		}
 	}
 
@@ -1231,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)
 
@@ -1247,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
@@ -1279,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();
@@ -1291,7 +1392,7 @@ void D_SRB2Main(void)
 
 	G_LoadGameData();
 
-#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
+#if defined (__unix__) || defined (UNIXCOMMON) || defined (HAVE_SDL)
 	VID_PrepareModeList(); // Regenerate Modelist according to cv_fullscreen
 #endif
 
@@ -1557,7 +1658,7 @@ const char *D_Home(void)
 		userhome = M_GetNextParm();
 	else
 	{
-#if !((defined (__unix__) && !defined (MSDOS)) || defined(__APPLE__) || defined (UNIXCOMMON)) && !defined (__APPLE__)
+#if !(defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON))
 		if (FIL_FileOK(CONFIGFILENAME))
 			usehome = false; // Let's NOT use home
 		else
diff --git a/src/d_main.h b/src/d_main.h
index 81de0634d0ca9ebe4cc03972590e99db631b6991..e282906d9577fa9f869018dbc57b5e7899634d60 100644
--- a/src/d_main.h
+++ b/src/d_main.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -40,10 +40,6 @@ void D_SRB2Main(void);
 
 // Called by IO functions when input is detected.
 void D_PostEvent(const event_t *ev);
-#if defined (PC_DOS) && !defined (DOXYGEN)
-void D_PostEvent_end(void);    // delimiter for locking memory
-#endif
-
 void D_ProcessEvents(void);
 
 const char *D_Home(void);
diff --git a/src/d_net.c b/src/d_net.c
index d534b1b081360da6f7274f33e14cb178c1d2f632..3a4746002eb87efe8dd57e45729cefc96943bdca 100644
--- a/src/d_net.c
+++ b/src/d_net.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -815,6 +815,8 @@ static const char *packettypename[NUMPACKETTYPE] =
 	"CLIENTJOIN",
 	"NODETIMEOUT",
 	"LOGIN",
+	"TELLFILESNEEDED",
+	"MOREFILESNEEDED",
 	"PING"
 };
 
diff --git a/src/d_net.h b/src/d_net.h
index ea6b5d4d9a58e6b5d8fd8f62a3ca980e2986b4e6..dbc6d8ba5ab6288ad76e2003cf67fdd2d6aa43b1 100644
--- a/src/d_net.h
+++ b/src/d_net.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 31c10f58a8e0dbe7f709c0ffd98e63772fe175d6..cca3102d085aac427b5a3469dd00f3085a3fee3d 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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);
@@ -168,7 +172,7 @@ void SendWeaponPref(void);
 void SendWeaponPref2(void);
 
 static CV_PossibleValue_t usemouse_cons_t[] = {{0, "Off"}, {1, "On"}, {2, "Force"}, {0, NULL}};
-#if (defined (__unix__) && !defined (MSDOS)) || defined(__APPLE__) || defined (UNIXCOMMON)
+#if defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)
 static CV_PossibleValue_t mouse2port_cons_t[] = {{0, "/dev/gpmdata"}, {1, "/dev/ttyS0"},
 	{2, "/dev/ttyS1"}, {3, "/dev/ttyS2"}, {4, "/dev/ttyS3"}, {0, NULL}};
 #else
@@ -214,11 +218,9 @@ consvar_t cv_respawntime = CVAR_INIT ("respawndelay", "3", CV_SAVE|CV_NETVAR|CV_
 
 consvar_t cv_competitionboxes = CVAR_INIT ("competitionboxes", "Mystery", CV_SAVE|CV_NETVAR|CV_CHEAT, competitionboxes_cons_t, NULL);
 
-#ifdef SEENAMES
 static CV_PossibleValue_t seenames_cons_t[] = {{0, "Off"}, {1, "Colorless"}, {2, "Team"}, {3, "Ally/Foe"}, {0, NULL}};
 consvar_t cv_seenames = CVAR_INIT ("seenames", "Ally/Foe", CV_SAVE, seenames_cons_t, 0);
 consvar_t cv_allowseenames = CVAR_INIT ("allowseenames", "Yes", CV_SAVE|CV_NETVAR, CV_YesNo, NULL);
-#endif
 
 // names
 consvar_t cv_playername = CVAR_INIT ("name", "Sonic", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Name_OnChange);
@@ -257,7 +259,7 @@ consvar_t cv_joyscale2 = CVAR_INIT ("padscale2", "1", CV_SAVE|CV_CALL, NULL, I_J
 consvar_t cv_joyscale = CVAR_INIT ("padscale", "1", CV_SAVE|CV_HIDEN, NULL, NULL); //Alam: Dummy for save
 consvar_t cv_joyscale2 = CVAR_INIT ("padscale2", "1", CV_SAVE|CV_HIDEN, NULL, NULL); //Alam: Dummy for save
 #endif
-#if (defined (__unix__) && !defined (MSDOS)) || defined(__APPLE__) || defined (UNIXCOMMON)
+#if defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)
 consvar_t cv_mouse2port = CVAR_INIT ("mouse2port", "/dev/gpmdata", CV_SAVE, mouse2port_cons_t, NULL);
 consvar_t cv_mouse2opt = CVAR_INIT ("mouse2opt", "0", CV_SAVE, NULL, NULL);
 #else
@@ -286,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);
@@ -373,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];
@@ -400,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",
@@ -443,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);
@@ -474,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);
 
@@ -597,9 +609,7 @@ void D_RegisterServerCommands(void)
 	CV_RegisterVar(&cv_pingtimeout);
 	CV_RegisterVar(&cv_showping);
 
-#ifdef SEENAMES
-	 CV_RegisterVar(&cv_allowseenames);
-#endif
+	CV_RegisterVar(&cv_allowseenames);
 
 	CV_RegisterVar(&cv_dummyconsvar);
 }
@@ -670,6 +680,7 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_zlib_strategya);
 	CV_RegisterVar(&cv_zlib_window_bitsa);
 	CV_RegisterVar(&cv_apng_delay);
+	CV_RegisterVar(&cv_apng_downscale);
 	// GIF variables
 	CV_RegisterVar(&cv_gif_optimize);
 	CV_RegisterVar(&cv_gif_downscale);
@@ -690,9 +701,7 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_defaultplayercolor2);
 	CV_RegisterVar(&cv_defaultskin2);
 
-#ifdef SEENAMES
 	CV_RegisterVar(&cv_seenames);
-#endif
 	CV_RegisterVar(&cv_rollingdemos);
 	CV_RegisterVar(&cv_netstat);
 	CV_RegisterVar(&cv_netticbuffer);
@@ -793,7 +802,7 @@ void D_RegisterClientCommands(void)
 	// WARNING: the order is important when initialising mouse2
 	// we need the mouse2port
 	CV_RegisterVar(&cv_mouse2port);
-#if (defined (__unix__) && !defined (MSDOS)) || defined(__APPLE__) || defined (UNIXCOMMON)
+#if defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)
 	CV_RegisterVar(&cv_mouse2opt);
 #endif
 	CV_RegisterVar(&cv_controlperkey);
@@ -866,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);
@@ -878,7 +889,7 @@ void D_RegisterClientCommands(void)
 //	CV_RegisterVar(&cv_snapto);
 
 	CV_RegisterVar(&cv_freedemocamera);
-	
+
 	// add cheat commands
 	COM_AddCommand("noclip", Command_CheatNoClip_f);
 	COM_AddCommand("god", Command_CheatGod_f);
@@ -1318,8 +1329,9 @@ static void SendNameAndColor(void)
 	cv_skin.value = R_SkinAvailable(cv_skin.string);
 	if ((cv_skin.value < 0) || !R_SkinUsable(consoleplayer, cv_skin.value))
 	{
-		CV_StealthSet(&cv_skin, DEFAULTSKIN);
-		cv_skin.value = 0;
+		INT32 defaultSkinNum = GetPlayerDefaultSkin(consoleplayer);
+		CV_StealthSet(&cv_skin, skins[defaultSkinNum].name);
+		cv_skin.value = defaultSkinNum;
 	}
 
 	// Finally write out the complete packet and send it off.
@@ -1480,7 +1492,8 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum)
 	if (server && (p != &players[consoleplayer] && p != &players[secondarydisplayplayer]))
 	{
 		boolean kick = false;
-		INT32 s;
+		UINT32 unlockShift = 0;
+		UINT32 i;
 
 		// team colors
 		if (G_GametypeHasTeams())
@@ -1496,12 +1509,29 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum)
 			kick = true;
 
 		// availabilities
-		for (s = 0; s < MAXSKINS; s++)
+		for (i = 0; i < MAXUNLOCKABLES; i++)
+		{
+			if (unlockables[i].type != SECRET_SKIN)
+			{
+				continue;
+			}
+
+			unlockShift++;
+		}
+
+		// If they set an invalid bit to true, then likely a modified client
+		if (unlockShift < 32) // 32 is the max the data type allows
 		{
-			if (!skins[s].availability && (p->availabilities & (1 << s)))
+			UINT32 illegalMask = UINT32_MAX;
+
+			for (i = 0; i < unlockShift; i++)
+			{
+				illegalMask &= ~(1 << i);
+			}
+
+			if ((p->availabilities & illegalMask) != 0)
 			{
 				kick = true;
-				break;
 			}
 		}
 
@@ -2103,7 +2133,7 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 	}
 
 	mapnumber = M_MapNumber(mapname[3], mapname[4]);
-	LUAh_MapChange(mapnumber);
+	LUA_HookInt(mapnumber, HOOK(MapChange));
 
 	G_InitNew(ultimatemode, mapname, resetplayer, skipprecutscene, FLS);
 	if (demoplayback && !timingdemo)
@@ -2135,7 +2165,7 @@ static void Command_Pause(void)
 
 	if (cv_pause.value || server || (IsPlayerAdmin(consoleplayer)))
 	{
-		if (modeattacking || !(gamestate == GS_LEVEL || gamestate == GS_INTERMISSION))
+		if (modeattacking || !(gamestate == GS_LEVEL || gamestate == GS_INTERMISSION) || (marathonmode && gamestate == GS_INTERMISSION))
 		{
 			CONS_Printf(M_GetText("You can't pause here.\n"));
 			return;
@@ -2688,7 +2718,7 @@ static void Got_Teamchange(UINT8 **cp, INT32 playernum)
 	}
 
 	// Don't switch team, just go away, please, go awaayyyy, aaauuauugghhhghgh
-	if (!LUAh_TeamSwitch(&players[playernum], NetPacket.packet.newteam, players[playernum].spectator, NetPacket.packet.autobalance, NetPacket.packet.scrambled))
+	if (!LUA_HookTeamSwitch(&players[playernum], NetPacket.packet.newteam, players[playernum].spectator, NetPacket.packet.autobalance, NetPacket.packet.scrambled))
 		return;
 
 	//no status changes after hidetime
@@ -2849,7 +2879,7 @@ static void Got_Teamchange(UINT8 **cp, INT32 playernum)
 		// Call ViewpointSwitch hooks here.
 		// The viewpoint was forcibly changed.
 		if (displayplayer != consoleplayer) // You're already viewing yourself. No big deal.
-			LUAh_ViewpointSwitch(&players[consoleplayer], &players[consoleplayer], true);
+			LUA_HookViewpointSwitch(&players[consoleplayer], &players[consoleplayer], true);
 		displayplayer = consoleplayer;
 	}
 
@@ -3203,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))
 	{
@@ -3294,7 +3324,13 @@ static void Command_Addfile(void)
 			if (!isprint(fn[i]) || fn[i] == ';')
 				return;
 
-		musiconly = W_VerifyNMUSlumps(fn);
+		musiconly = W_VerifyNMUSlumps(fn, false);
+
+		if (musiconly == -1)
+		{
+			addedfiles[numfilesadded++] = fn;
+			continue;
+		}
 
 		if (!musiconly)
 		{
@@ -3321,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;
@@ -3353,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);
@@ -3372,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;
@@ -3400,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);
@@ -3432,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);
@@ -3480,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));
@@ -3494,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);
 	}
@@ -3606,8 +3881,7 @@ static void Command_Playintro_f(void)
   */
 FUNCNORETURN static ATTRNORETURN void Command_Quit_f(void)
 {
-	if (Playing())
-		LUAh_GameQuit();
+	LUA_HookBool(true, HOOK(GameQuit));
 	I_Quit();
 }
 
@@ -4269,8 +4543,7 @@ void Command_ExitGame_f(void)
 {
 	INT32 i;
 
-	if (Playing())
-		LUAh_GameQuit();
+	LUA_HookBool(false, HOOK(GameQuit));
 
 	D_QuitNetGame();
 	CL_Reset();
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index 98d8f142576e46d18dd84a2820a1812b3289d7d6..7bb7eab03a1345190c670b3d9309c159cc3d3c07 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -31,9 +31,7 @@ extern consvar_t cv_defaultskin;
 extern consvar_t cv_defaultplayercolor2;
 extern consvar_t cv_defaultskin2;
 
-#ifdef SEENAMES
 extern consvar_t cv_seenames, cv_allowseenames;
-#endif
 extern consvar_t cv_usemouse;
 extern consvar_t cv_usejoystick;
 extern consvar_t cv_usejoystick2;
@@ -47,7 +45,7 @@ extern consvar_t cv_joyscale2;
 // splitscreen with second mouse
 extern consvar_t cv_mouse2port;
 extern consvar_t cv_usemouse2;
-#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON)
+#if defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)
 extern consvar_t cv_mouse2opt;
 #endif
 
@@ -75,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;
@@ -112,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;
@@ -130,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 8f661bb5fb26f8f341ba77daa379b77c25fa6dbc..fdc0026a8bbf2a87b12fa0444fbfa2156e40c5c1 100644
--- a/src/d_netfil.c
+++ b/src/d_netfil.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -15,7 +15,7 @@
 
 #include <time.h>
 
-#if defined (_WIN32) || defined (__DJGPP__)
+#ifdef _WIN32
 #include <io.h>
 #include <direct.h>
 #else
@@ -30,10 +30,6 @@
 #elif defined (_WIN32)
 #include <sys/utime.h>
 #endif
-#ifdef __DJGPP__
-#include <dir.h>
-#include <utime.h>
-#endif
 
 #include "doomdef.h"
 #include "doomstat.h"
@@ -56,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
@@ -91,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";
 
@@ -109,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;
@@ -117,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
@@ -147,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
@@ -192,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;
@@ -323,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"
@@ -346,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
 }
 
@@ -369,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
@@ -393,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)
 			{
@@ -420,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) &&
@@ -436,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);
@@ -500,6 +570,7 @@ void CL_LoadServerFiles(void)
 				fileneeded[i].status, s);
 		}
 	}
+	return true;
 }
 
 void AddLuaFileTransfer(const char *filename, const char *mode)
@@ -562,7 +633,7 @@ static void SV_PrepareSendLuaFileToNextNode(void)
 
     // Find a client to send the file to
 	for (i = 1; i < MAXNETNODES; i++)
-		if (nodeingame[i] && luafiletransfers->nodestatus[i] == LFTNS_WAITING) // Node waiting
+		if (luafiletransfers->nodestatus[i] == LFTNS_WAITING) // Node waiting
 		{
 			// Tell the client we're about to send them the file
 			netbuffer->packettype = PT_SENDINGLUAFILE;
@@ -570,6 +641,7 @@ static void SV_PrepareSendLuaFileToNextNode(void)
 				I_Error("Failed to send a PT_SENDINGLUAFILE packet\n"); // !!! Todo: Handle failure a bit better lol
 
 			luafiletransfers->nodestatus[i] = LFTNS_ASKED;
+			luafiletransfers->nodetimeouts[i] = I_GetTime() + 30 * TICRATE;
 
 			return;
 		}
@@ -588,7 +660,7 @@ void SV_PrepareSendLuaFile(void)
 
 	// Set status to "waiting" for everyone
 	for (i = 0; i < MAXNETNODES; i++)
-		luafiletransfers->nodestatus[i] = LFTNS_WAITING;
+		luafiletransfers->nodestatus[i] = (nodeingame[i] ? LFTNS_WAITING : LFTNS_NONE);
 
 	if (FIL_ReadFileOK(luafiletransfers->realfilename))
 	{
@@ -649,12 +721,14 @@ void RemoveAllLuaFileTransfers(void)
 
 void SV_AbortLuaFileTransfer(INT32 node)
 {
-	if (luafiletransfers
-	&& (luafiletransfers->nodestatus[node] == LFTNS_ASKED
-	||  luafiletransfers->nodestatus[node] == LFTNS_SENDING))
+	if (luafiletransfers)
 	{
-		luafiletransfers->nodestatus[node] = LFTNS_WAITING;
-		SV_PrepareSendLuaFileToNextNode();
+		if (luafiletransfers->nodestatus[node] == LFTNS_ASKED
+			|| luafiletransfers->nodestatus[node] == LFTNS_SENDING)
+		{
+			SV_PrepareSendLuaFileToNextNode();
+		}
+		luafiletransfers->nodestatus[node] = LFTNS_NONE;
 	}
 }
 
@@ -678,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;
@@ -705,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;
@@ -733,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
@@ -914,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
@@ -928,17 +993,26 @@ void FileSendTicker(void)
 	filetx_t *f;
 	INT32 packetsent, ram, i, j;
 
+	// If someone is taking too long to download, kick them with a timeout
+	// to prevent blocking the rest of the server...
+	if (luafiletransfers)
+	{
+		for (i = 1; i < MAXNETNODES; i++)
+		{
+			luafiletransfernodestatus_t status = luafiletransfers->nodestatus[i];
+
+			if (status != LFTNS_NONE && status != LFTNS_WAITING && status != LFTNS_SENT
+				&& I_GetTime() > luafiletransfers->nodetimeouts[i])
+			{
+				Net_ConnectionTimeout(i);
+			}
+		}
+	}
+
 	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;
 
@@ -1215,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);
 
@@ -1326,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();
 				}
 			}
 		}
@@ -1396,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)
@@ -1556,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 1b399be75f31ae472de43c65cce64f24a8202376..3d713c150fad6f520a618d8e158f96599f081323 100644
--- a/src/d_netfil.h
+++ b/src/d_netfil.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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);
 
@@ -85,10 +103,11 @@ boolean PT_RequestFile(INT32 node);
 
 typedef enum
 {
+	LFTNS_NONE,    // This node is not connected
 	LFTNS_WAITING, // This node is waiting for the server to send the file
-	LFTNS_ASKED, // The server has told the node they're ready to send the file
+	LFTNS_ASKED,   // The server has told the node they're ready to send the file
 	LFTNS_SENDING, // The server is sending the file to this node
-	LFTNS_SENT // The node already has the file
+	LFTNS_SENT     // The node already has the file
 } luafiletransfernodestatus_t;
 
 typedef struct luafiletransfer_s
@@ -99,6 +118,7 @@ typedef struct luafiletransfer_s
 	INT32 id; // Callback ID
 	boolean ongoing;
 	luafiletransfernodestatus_t nodestatus[MAXNETNODES];
+	tic_t nodetimeouts[MAXNETNODES];
 	struct luafiletransfer_s *next;
 } luafiletransfer_t;
 
@@ -133,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 eb03728320fdd60d60163235082e51cd51ca474e..a0db1402df153beb928c54d743bc4388f3bc2122 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -51,7 +51,9 @@ typedef enum
 	SF_NONIGHTSSUPER    = 1<<15, // Disable super colors for NiGHTS (if you have SF_SUPER)
 	SF_NOSUPERSPRITES   = 1<<16, // Don't use super sprites while super
 	SF_NOSUPERJUMPBOOST = 1<<17, // Disable the jump boost given while super (i.e. Knuckles)
-	SF_CANBUSTWALLS		= 1<<18, // Can naturally bust walls on contact? (i.e. Knuckles)
+	SF_CANBUSTWALLS     = 1<<18, // Can naturally bust walls on contact? (i.e. Knuckles)
+	SF_NOSHIELDABILITY  = 1<<19, // Disable shield abilities
+
 	// free up to and including 1<<31
 } skinflags_t;
 
@@ -311,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;
@@ -523,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/d_think.h b/src/d_think.h
index 4bdac46272ae071e5b500bde07c83bd2ac96f39c..c3f91edc4392238c92499146ea6aefd869e7848b 100644
--- a/src/d_think.h
+++ b/src/d_think.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/d_ticcmd.h b/src/d_ticcmd.h
index 2a5ef09818f05fb001b0b03200ca092e47b77607..182b30e6aef84b9e4148157594918be325c75095 100644
--- a/src/d_ticcmd.h
+++ b/src/d_ticcmd.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -21,6 +21,8 @@
 #pragma interface
 #endif
 
+#define MAXPREDICTTICS 12
+
 // Button/action code definitions.
 typedef enum
 {
@@ -63,6 +65,7 @@ typedef struct
 	INT16 angleturn; // <<16 for angle delta - saved as 1 byte into demos
 	INT16 aiming; // vertical aiming, see G_BuildTicCmd
 	UINT16 buttons;
+	UINT8 latency; // Netgames: how many tics ago was this ticcmd generated from this player's end?
 } ATTRPACK ticcmd_t;
 
 #if defined(_MSC_VER)
diff --git a/src/deh_lua.c b/src/deh_lua.c
index e6a436421cc14096bb1bc02689455a48df499dc1..a2ffca95b0622826dd69d350c60dba52e221b0f9 100644
--- a/src/deh_lua.c
+++ b/src/deh_lua.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -25,10 +25,6 @@
 #include "deh_lua.h"
 #include "deh_tables.h"
 
-#ifdef MUSICSLOT_COMPATIBILITY
-#include "deh_soc.h" // for get_mus
-#endif
-
 // freeslot takes a name (string only!)
 // and allocates it to the appropriate free slot.
 // Returns the slot number allocated for it or nil if failed.
@@ -264,6 +260,11 @@ static inline int lib_getenum(lua_State *L)
 				lua_pushinteger(L, ((lua_Integer)1<<i));
 				return 1;
 			}
+		if (fastcmp(p, "REVERSESUPER"))
+		{
+			lua_pushinteger(L, (lua_Integer)MFE_REVERSESUPER);
+			return 1;
+		}
 		if (mathlib) return luaL_error(L, "mobjeflag '%s' could not be found.\n", word);
 		return 0;
 	}
@@ -430,29 +431,6 @@ static inline int lib_getenum(lua_State *L)
 		if (mathlib) return luaL_error(L, "sfx '%s' could not be found.\n", word);
 		return 0;
 	}
-#ifdef MUSICSLOT_COMPATIBILITY
-	else if (!mathlib && fastncmp("mus_",word,4)) {
-		p = word+4;
-		if ((i = get_mus(p, false)) == 0)
-			return 0;
-		lua_pushinteger(L, i);
-		return 1;
-	}
-	else if (mathlib && fastncmp("MUS_",word,4)) { // SOCs are ALL CAPS!
-		p = word+4;
-		if ((i = get_mus(p, false)) == 0)
-			return luaL_error(L, "music '%s' could not be found.\n", word);
-		lua_pushinteger(L, i);
-		return 1;
-	}
-	else if (mathlib && (fastncmp("O_",word,2) || fastncmp("D_",word,2))) {
-		p = word+2;
-		if ((i = get_mus(p, false)) == 0)
-			return luaL_error(L, "music '%s' could not be found.\n", word);
-		lua_pushinteger(L, i);
-		return 1;
-	}
-#endif
 	else if (!mathlib && fastncmp("pw_",word,3)) {
 		p = word+3;
 		for (i = 0; i < NUMPOWERS; i++)
diff --git a/src/deh_lua.h b/src/deh_lua.h
index cd927b9fd51bb98f3d6674dd129d9df29726ae33..9df4028bdcf9619a14fe267d202aed2343125e2f 100644
--- a/src/deh_lua.h
+++ b/src/deh_lua.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/deh_soc.c b/src/deh_soc.c
index 5b12ea1b0b9b0339890b094516e0593e252d6556..3a611f3ba18daaacd3c9f5154505176f3a1ad4cd 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -127,6 +127,33 @@ static float searchfvalue(const char *s)
 #endif
 
 // These are for clearing all of various things
+void clear_emblems(void)
+{
+	INT32 i;
+
+	for (i = 0; i < MAXEMBLEMS; ++i)
+	{
+		Z_Free(emblemlocations[i].stringVar);
+		emblemlocations[i].stringVar = NULL;
+	}
+
+	memset(&emblemlocations, 0, sizeof(emblemlocations));
+	numemblems = 0;
+}
+
+void clear_unlockables(void)
+{
+	INT32 i;
+
+	for (i = 0; i < MAXUNLOCKABLES; ++i)
+	{
+		Z_Free(unlockables[i].stringVar);
+		unlockables[i].stringVar = NULL;
+	}
+
+	memset(&unlockables, 0, sizeof(unlockables));
+}
+
 void clear_conditionsets(void)
 {
 	UINT8 i;
@@ -229,7 +256,10 @@ void readPlayer(MYFILE *f, INT32 num)
 
 				SLOTFOUND
 
-				for (i = 0; i < MAXLINELEN-3; i++)
+				// A friendly neighborhood alias for brevity's sake
+#define NOTE_SIZE sizeof(description[num].notes)
+
+				for (i = 0; i < (INT32)(MAXLINELEN-NOTE_SIZE-3); i++)
 				{
 					if (s[i] == '=')
 					{
@@ -239,8 +269,9 @@ void readPlayer(MYFILE *f, INT32 num)
 				}
 				if (playertext)
 				{
-					strcpy(description[num].notes, playertext);
-					strcat(description[num].notes, myhashfgets(playertext, sizeof (description[num].notes), f));
+					strlcpy(description[num].notes, playertext, NOTE_SIZE);
+					strlcat(description[num].notes,
+						myhashfgets(playertext, NOTE_SIZE, f), NOTE_SIZE);
 				}
 				else
 					strcpy(description[num].notes, "");
@@ -249,7 +280,7 @@ void readPlayer(MYFILE *f, INT32 num)
 				// It works down here, though.
 				{
 					INT32 numline = 0;
-					for (i = 0; (size_t)i < sizeof(description[num].notes)-1; i++)
+					for (i = 0; (size_t)i < NOTE_SIZE-1; i++)
 					{
 						if (numline < 20 && description[num].notes[i] == '\n')
 							numline++;
@@ -260,6 +291,7 @@ void readPlayer(MYFILE *f, INT32 num)
 				}
 				description[num].notes[strlen(description[num].notes)-1] = '\0';
 				description[num].notes[i] = '\0';
+#undef NOTE_SIZE
 				continue;
 			}
 
@@ -1140,8 +1172,10 @@ void readgametype(MYFILE *f, char *gtname)
 				}
 				if (descr)
 				{
-					strcpy(gtdescription, descr);
-					strcat(gtdescription, myhashfgets(descr, sizeof (gtdescription), f));
+					strlcpy(gtdescription, descr, sizeof (gtdescription));
+					strlcat(gtdescription,
+						myhashfgets(descr, sizeof (gtdescription), f),
+						sizeof (gtdescription));
 				}
 				else
 					strcpy(gtdescription, "");
@@ -1574,19 +1608,8 @@ void readlevelheader(MYFILE *f, INT32 num)
 						sizeof(mapheaderinfo[num-1]->musname), va("Level header %d: music", num));
 				}
 			}
-#ifdef MUSICSLOT_COMPATIBILITY
 			else if (fastcmp(word, "MUSICSLOT"))
-			{
-				i = get_mus(word2, true);
-				if (i && i <= 1035)
-					snprintf(mapheaderinfo[num-1]->musname, 7, "%sM", G_BuildMapName(i));
-				else if (i && i <= 1050)
-					strncpy(mapheaderinfo[num-1]->musname, compat_special_music_slots[i - 1036], 7);
-				else
-					mapheaderinfo[num-1]->musname[0] = 0; // becomes empty string
-				mapheaderinfo[num-1]->musname[6] = 0;
-			}
-#endif
+				deh_warning("Level header %d: MusicSlot parameter is deprecated and will be removed.\nUse \"Music\" instead.", num);
 			else if (fastcmp(word, "MUSICTRACK"))
 				mapheaderinfo[num-1]->mustrack = ((UINT16)i - 1);
 			else if (fastcmp(word, "MUSICPOS"))
@@ -1964,19 +1987,6 @@ static void readcutscenescene(MYFILE *f, INT32 num, INT32 scenenum)
 				strncpy(cutscenes[num]->scene[scenenum].musswitch, word2, 7);
 				cutscenes[num]->scene[scenenum].musswitch[6] = 0;
 			}
-#ifdef MUSICSLOT_COMPATIBILITY
-			else if (fastcmp(word, "MUSICSLOT"))
-			{
-				i = get_mus(word2, true);
-				if (i && i <= 1035)
-					snprintf(cutscenes[num]->scene[scenenum].musswitch, 7, "%sM", G_BuildMapName(i));
-				else if (i && i <= 1050)
-					strncpy(cutscenes[num]->scene[scenenum].musswitch, compat_special_music_slots[i - 1036], 7);
-				else
-					cutscenes[num]->scene[scenenum].musswitch[0] = 0; // becomes empty string
-				cutscenes[num]->scene[scenenum].musswitch[6] = 0;
-			}
-#endif
 			else if (fastcmp(word, "MUSICTRACK"))
 			{
 				cutscenes[num]->scene[scenenum].musswitchflags = ((UINT16)i) & MUSIC_TRACKMASK;
@@ -2239,19 +2249,6 @@ static void readtextpromptpage(MYFILE *f, INT32 num, INT32 pagenum)
 				strncpy(textprompts[num]->page[pagenum].musswitch, word2, 7);
 				textprompts[num]->page[pagenum].musswitch[6] = 0;
 			}
-#ifdef MUSICSLOT_COMPATIBILITY
-			else if (fastcmp(word, "MUSICSLOT"))
-			{
-				i = get_mus(word2, true);
-				if (i && i <= 1035)
-					snprintf(textprompts[num]->page[pagenum].musswitch, 7, "%sM", G_BuildMapName(i));
-				else if (i && i <= 1050)
-					strncpy(textprompts[num]->page[pagenum].musswitch, compat_special_music_slots[i - 1036], 7);
-				else
-					textprompts[num]->page[pagenum].musswitch[0] = 0; // becomes empty string
-				textprompts[num]->page[pagenum].musswitch[6] = 0;
-			}
-#endif
 			else if (fastcmp(word, "MUSICTRACK"))
 			{
 				textprompts[num]->page[pagenum].musswitchflags = ((UINT16)i) & MUSIC_TRACKMASK;
@@ -2577,20 +2574,6 @@ void readmenu(MYFILE *f, INT32 num)
 				menupres[num].musname[6] = 0;
 				titlechanged = true;
 			}
-#ifdef MUSICSLOT_COMPATIBILITY
-			else if (fastcmp(word, "MUSICSLOT"))
-			{
-				value = get_mus(word2, true);
-				if (value && value <= 1035)
-					snprintf(menupres[num].musname, 7, "%sM", G_BuildMapName(value));
-				else if (value && value <= 1050)
-					strncpy(menupres[num].musname, compat_special_music_slots[value - 1036], 7);
-				else
-					menupres[num].musname[0] = 0; // becomes empty string
-				menupres[num].musname[6] = 0;
-				titlechanged = true;
-			}
-#endif
 			else if (fastcmp(word, "MUSICTRACK"))
 			{
 				menupres[num].mustrack = ((UINT16)value - 1);
@@ -2839,26 +2822,31 @@ void readsound(MYFILE *f, INT32 num)
 			if (s[0] == '\n')
 				break;
 
+			// First remove trailing newline, if there is one
+			tmp = strchr(s, '\n');
+			if (tmp)
+				*tmp = '\0';
+
 			tmp = strchr(s, '#');
 			if (tmp)
 				*tmp = '\0';
 			if (s == tmp)
 				continue; // Skip comment lines, but don't break.
 
-			word = strtok(s, " ");
-			if (word)
-				strupr(word);
+			// Set / reset word
+			word = s;
+
+			// Get the part before the " = "
+			tmp = strchr(s, '=');
+			if (tmp)
+				*(tmp-1) = '\0';
 			else
 				break;
+			strupr(word);
 
-			word2 = strtok(NULL, " ");
-			if (word2)
-				value = atoi(word2);
-			else
-			{
-				deh_warning("No value for token %s", word);
-				continue;
-			}
+			// Now get the part after
+			word2 = tmp += 2;
+			value = atoi(word2); // used for numerical settings
 
 			if (fastcmp(word, "SINGULAR"))
 			{
@@ -3017,7 +3005,12 @@ void reademblemdata(MYFILE *f, INT32 num)
 			else if (fastcmp(word, "COLOR"))
 				emblemlocations[num-1].color = get_number(word2);
 			else if (fastcmp(word, "VAR"))
+			{
+				Z_Free(emblemlocations[num-1].stringVar);
+				emblemlocations[num-1].stringVar = Z_StrDup(word2);
+
 				emblemlocations[num-1].var = get_number(word2);
+			}
 			else
 				deh_warning("Emblem %d: unknown word '%s'", num, word);
 		}
@@ -3219,11 +3212,16 @@ void readunlockable(MYFILE *f, INT32 num)
 						unlockables[num].type = SECRET_WARP;
 					else if (fastcmp(word2, "SOUNDTEST"))
 						unlockables[num].type = SECRET_SOUNDTEST;
+					else if (fastcmp(word2, "SKIN"))
+						unlockables[num].type = SECRET_SKIN;
 					else
 						unlockables[num].type = (INT16)i;
 				}
 				else if (fastcmp(word, "VAR"))
 				{
+					Z_Free(unlockables[num].stringVar);
+					unlockables[num].stringVar = Z_StrDup(word2);
+
 					// Support using the actual map name,
 					// i.e., Level AB, Level FZ, etc.
 
@@ -4178,46 +4176,6 @@ sfxenum_t get_sfx(const char *word)
 	return sfx_None;
 }
 
-#ifdef MUSICSLOT_COMPATIBILITY
-UINT16 get_mus(const char *word, UINT8 dehacked_mode)
-{ // Returns the value of MUS_ enumerations
-	UINT16 i;
-	char lumptmp[4];
-
-	if (*word >= '0' && *word <= '9')
-		return atoi(word);
-	if (!word[2] && toupper(word[0]) >= 'A' && toupper(word[0]) <= 'Z')
-		return (UINT16)M_MapNumber(word[0], word[1]);
-
-	if (fastncmp("MUS_",word,4))
-		word += 4; // take off the MUS_
-	else if (fastncmp("O_",word,2) || fastncmp("D_",word,2))
-		word += 2; // take off the O_ or D_
-
-	strncpy(lumptmp, word, 4);
-	lumptmp[3] = 0;
-	if (fasticmp("MAP",lumptmp))
-	{
-		word += 3;
-		if (toupper(word[0]) >= 'A' && toupper(word[0]) <= 'Z')
-			return (UINT16)M_MapNumber(word[0], word[1]);
-		else if ((i = atoi(word)))
-			return i;
-
-		word -= 3;
-		if (dehacked_mode)
-			deh_warning("Couldn't find music named 'MUS_%s'",word);
-		return 0;
-	}
-	for (i = 0; compat_special_music_slots[i][0]; ++i)
-		if (fasticmp(word, compat_special_music_slots[i]))
-			return i + 1036;
-	if (dehacked_mode)
-		deh_warning("Couldn't find music named 'MUS_%s'",word);
-	return 0;
-}
-#endif
-
 hudnum_t get_huditem(const char *word)
 { // Returns the value of HUD_ enumerations
 	hudnum_t i;
@@ -4448,13 +4406,6 @@ static fixed_t find_const(const char **rword)
 		free(word);
 		return r;
 	}
-#ifdef MUSICSLOT_COMPATIBILITY
-	else if (fastncmp("MUS_",word,4) || fastncmp("O_",word,2)) {
-		r = get_mus(word, true);
-		free(word);
-		return r;
-	}
-#endif
 	else if (fastncmp("PW_",word,3)) {
 		r = get_power(word);
 		free(word);
diff --git a/src/deh_soc.h b/src/deh_soc.h
index 2bcb52e709e9ef1003753fdf68ee6647af9c3e60..28e3c9512336b91700a648f35f0fe3e922356d69 100644
--- a/src/deh_soc.h
+++ b/src/deh_soc.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -43,7 +43,7 @@
 
 #include "info.h"
 #include "dehacked.h"
-#include "doomdef.h" // MUSICSLOT_COMPATIBILITY, HWRENDER
+#include "doomdef.h" // HWRENDER
 
 // Crazy word-reading stuff
 /// \todo Put these in a seperate file or something.
@@ -52,9 +52,6 @@ statenum_t get_state(const char *word);
 spritenum_t get_sprite(const char *word);
 playersprite_t get_sprite2(const char *word);
 sfxenum_t get_sfx(const char *word);
-#ifdef MUSICSLOT_COMPATIBILITY
-UINT16 get_mus(const char *word, UINT8 dehacked_mode);
-#endif
 hudnum_t get_huditem(const char *word);
 menutype_t get_menutype(const char *word);
 //INT16 get_gametype(const char *word);
@@ -84,6 +81,8 @@ void readskincolor(MYFILE *f, INT32 num);
 void readthing(MYFILE *f, INT32 num);
 void readfreeslots(MYFILE *f);
 void readPlayer(MYFILE *f, INT32 num);
+void clear_emblems(void);
+void clear_unlockables(void);
 void clear_levels(void);
 void clear_conditionsets(void);
 #endif
diff --git a/src/deh_tables.c b/src/deh_tables.c
index 5733d9b0ede7466fab11fc0db2094f12d5f3d45b..f30f7c14dbca4ecf514976077977029c4149d86c 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -22,6 +22,9 @@
 #include "v_video.h" // video flags (for lua)
 #include "i_sound.h" // musictype_t (for lua)
 #include "g_state.h" // gamestate_t (for lua)
+#include "g_game.h" // Joystick axes (for lua)
+#include "i_joy.h"
+#include "g_input.h" // Game controls (for lua)
 
 #include "deh_tables.h"
 
@@ -1522,6 +1525,13 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
 	"S_SPINFIRE5",
 	"S_SPINFIRE6",
 
+	"S_TEAM_SPINFIRE1",
+	"S_TEAM_SPINFIRE2",
+	"S_TEAM_SPINFIRE3",
+	"S_TEAM_SPINFIRE4",
+	"S_TEAM_SPINFIRE5",
+	"S_TEAM_SPINFIRE6",
+
 	// Spikes
 	"S_SPIKE1",
 	"S_SPIKE2",
@@ -3478,9 +3488,7 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
 	"S_BLUEBRICKDEBRIS",
 	"S_YELLOWBRICKDEBRIS",
 
-#ifdef SEENAMES
 	"S_NAMECHECK",
-#endif
 };
 
 // RegEx to generate this from info.h: ^\tMT_([^,]+), --> \t"MT_\1",
@@ -4260,9 +4268,7 @@ const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for sanity t
 	"MT_BLUEBRICKDEBRIS",
 	"MT_YELLOWBRICKDEBRIS",
 
-#ifdef SEENAMES
 	"MT_NAMECHECK",
-#endif
 };
 
 const char *const MOBJFLAG_LIST[] = {
@@ -4331,6 +4337,7 @@ const char *const MOBJFLAG2_LIST[] = {
 	"AMBUSH",         // Alternate behaviour typically set by MTF_AMBUSH
 	"LINKDRAW",       // Draw vissprite of mobj immediately before/after tracer's vissprite (dependent on dispoffset and position)
 	"SHIELD",         // Thinker calls P_AddShield/P_ShieldLook (must be partnered with MF_SCENERY to use)
+	"SPLAT",          // Object is a splat
 	NULL
 };
 
@@ -4347,6 +4354,8 @@ const char *const MOBJEFLAG_LIST[] = {
 	"SPRUNG", // Mobj was already sprung this tic
 	"APPLYPMOMZ", // Platform movement
 	"TRACERANGLE", // Compute and trigger on mobj angle relative to tracer
+	"FORCESUPER", // Forces an object to use super sprites with SPR_PLAY.
+	"FORCENOSUPER", // Forces an object to NOT use super sprites with SPR_PLAY.
 	NULL
 };
 
@@ -4794,6 +4803,7 @@ struct int_const_s const INT_CONST[] = {
 
 	// fixed_t constants, from m_fixed.h
 	{"FRACUNIT",FRACUNIT},
+	{"FU"      ,FRACUNIT},
 	{"FRACBITS",FRACBITS},
 
 	// doomdef.h constants
@@ -4871,6 +4881,36 @@ struct int_const_s const INT_CONST[] = {
 	{"tr_trans90",tr_trans90},
 	{"NUMTRANSMAPS",NUMTRANSMAPS},
 
+	// Alpha styles (blend modes)
+	{"AST_COPY",AST_COPY},
+	{"AST_TRANSLUCENT",AST_TRANSLUCENT},
+	{"AST_ADD",AST_ADD},
+	{"AST_SUBTRACT",AST_SUBTRACT},
+	{"AST_REVERSESUBTRACT",AST_REVERSESUBTRACT},
+	{"AST_MODULATE",AST_MODULATE},
+	{"AST_OVERLAY",AST_OVERLAY},
+
+	// Render flags
+	{"RF_HORIZONTALFLIP",RF_HORIZONTALFLIP},
+	{"RF_VERTICALFLIP",RF_VERTICALFLIP},
+	{"RF_ABSOLUTEOFFSETS",RF_ABSOLUTEOFFSETS},
+	{"RF_FLIPOFFSETS",RF_FLIPOFFSETS},
+	{"RF_SPLATMASK",RF_SPLATMASK},
+	{"RF_SLOPESPLAT",RF_SLOPESPLAT},
+	{"RF_OBJECTSLOPESPLAT",RF_OBJECTSLOPESPLAT},
+	{"RF_NOSPLATBILLBOARD",RF_NOSPLATBILLBOARD},
+	{"RF_NOSPLATROLLANGLE",RF_NOSPLATROLLANGLE},
+	{"RF_BLENDMASK",RF_BLENDMASK},
+	{"RF_FULLBRIGHT",RF_FULLBRIGHT},
+	{"RF_FULLDARK",RF_FULLDARK},
+	{"RF_NOCOLORMAPS",RF_NOCOLORMAPS},
+	{"RF_SPRITETYPEMASK",RF_SPRITETYPEMASK},
+	{"RF_PAPERSPRITE",RF_PAPERSPRITE},
+	{"RF_FLOORSPRITE",RF_FLOORSPRITE},
+	{"RF_SHADOWDRAW",RF_SHADOWDRAW},
+	{"RF_SHADOWEFFECTS",RF_SHADOWEFFECTS},
+	{"RF_DROPSHADOW",RF_DROPSHADOW},
+
 	// Level flags
 	{"LF_SCRIPTISFILE",LF_SCRIPTISFILE},
 	{"LF_SPEEDMUSIC",LF_SPEEDMUSIC},
@@ -4987,6 +5027,7 @@ struct int_const_s const INT_CONST[] = {
 	{"SF_NOSUPERSPRITES",SF_NOSUPERSPRITES},
 	{"SF_NOSUPERJUMPBOOST",SF_NOSUPERJUMPBOOST},
 	{"SF_CANBUSTWALLS",SF_CANBUSTWALLS},
+	{"SF_NOSHIELDABILITY",SF_NOSHIELDABILITY},
 
 	// Dashmode constants
 	{"DASHMODE_THRESHOLD",DASHMODE_THRESHOLD},
@@ -5131,6 +5172,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},
@@ -5419,5 +5466,99 @@ struct int_const_s const INT_CONST[] = {
 	{"GS_DEDICATEDSERVER",GS_DEDICATEDSERVER},
 	{"GS_WAITINGPLAYERS",GS_WAITINGPLAYERS},
 
+	// Joystick axes
+	{"JA_NONE",JA_NONE},
+	{"JA_TURN",JA_TURN},
+	{"JA_MOVE",JA_MOVE},
+	{"JA_LOOK",JA_LOOK},
+	{"JA_STRAFE",JA_STRAFE},
+	{"JA_DIGITAL",JA_DIGITAL},
+	{"JA_JUMP",JA_JUMP},
+	{"JA_SPIN",JA_SPIN},
+	{"JA_FIRE",JA_FIRE},
+	{"JA_FIRENORMAL",JA_FIRENORMAL},
+	{"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},
+
+	// Mouse buttons
+	{"MB_BUTTON1",MB_BUTTON1},
+	{"MB_BUTTON2",MB_BUTTON2},
+	{"MB_BUTTON3",MB_BUTTON3},
+	{"MB_BUTTON4",MB_BUTTON4},
+	{"MB_BUTTON5",MB_BUTTON5},
+	{"MB_BUTTON6",MB_BUTTON6},
+	{"MB_BUTTON7",MB_BUTTON7},
+	{"MB_BUTTON8",MB_BUTTON8},
+	{"MB_SCROLLUP",MB_SCROLLUP},
+	{"MB_SCROLLDOWN",MB_SCROLLDOWN},
+
 	{NULL,0}
 };
+
+// For this to work compile-time without being in this file,
+// this function would need to check sizes at runtime, without sizeof
+void DEH_TableCheck(void)
+{
+#if defined(_DEBUG) || defined(PARANOIA)
+	const size_t dehstates = sizeof(STATE_LIST)/sizeof(const char*);
+	const size_t dehmobjs  = sizeof(MOBJTYPE_LIST)/sizeof(const char*);
+	const size_t dehpowers = sizeof(POWERS_LIST)/sizeof(const char*);
+	const size_t dehcolors = sizeof(COLOR_ENUMS)/sizeof(const char*);
+
+	if (dehstates != S_FIRSTFREESLOT)
+		I_Error("You forgot to update the Dehacked states list, you dolt!\n(%d states defined, versus %s in the Dehacked list)\n", S_FIRSTFREESLOT, sizeu1(dehstates));
+
+	if (dehmobjs != MT_FIRSTFREESLOT)
+		I_Error("You forgot to update the Dehacked mobjtype list, you dolt!\n(%d mobj types defined, versus %s in the Dehacked list)\n", MT_FIRSTFREESLOT, sizeu1(dehmobjs));
+
+	if (dehpowers != NUMPOWERS)
+		I_Error("You forgot to update the Dehacked powers list, you dolt!\n(%d powers defined, versus %s in the Dehacked list)\n", NUMPOWERS, sizeu1(dehpowers));
+
+	if (dehcolors != SKINCOLOR_FIRSTFREESLOT)
+		I_Error("You forgot to update the Dehacked colors list, you dolt!\n(%d colors defined, versus %s in the Dehacked list)\n", SKINCOLOR_FIRSTFREESLOT, sizeu1(dehcolors));
+#endif
+}
diff --git a/src/deh_tables.h b/src/deh_tables.h
index 2c6b3e20407ec454a47a9b301fcf5003cb4220a8..1f265cc9992da1c3d5c6e53781f6151710bc798f 100644
--- a/src/deh_tables.h
+++ b/src/deh_tables.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -72,4 +72,7 @@ extern const char *const MENUTYPES_LIST[];
 
 extern struct int_const_s const INT_CONST[];
 
+// Moved to this file because it can't work compile-time otherwise
+void DEH_TableCheck(void);
+
 #endif
diff --git a/src/dehacked.c b/src/dehacked.c
index e98ff71cf2f8a30baa8bd0e7fadd6114359fbaae..da8c81c351f845a98b59e3cd494e8f29c2a6b7d1 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -59,6 +59,12 @@ ATTRINLINE static FUNCINLINE char myfget_color(MYFILE *f)
 
 	if (c >= '0' && c <= '9')
 		return 0x80+(c-'0');
+
+	c = tolower(c);
+
+	if (c >= 'a' && c <= 'f')
+		return 0x80+10+(c-'a');
+
 	return 0x80; // Unhandled -- default to no color
 }
 
@@ -182,26 +188,11 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile)
 	dbg_line = -1; // start at -1 so the first line is 0.
 	while (!myfeof(f))
 	{
-		char origpos[128];
-		INT32 size = 0;
-		char *traverse;
-
 		myfgets(s, MAXLINELEN, f);
 		memcpy(textline, s, MAXLINELEN);
 		if (s[0] == '\n' || s[0] == '#')
 			continue;
 
-		traverse = s;
-
-		while (traverse[0] != '\n')
-		{
-			traverse++;
-			size++;
-		}
-
-		strncpy(origpos, s, size);
-		origpos[size] = '\0';
-
 		if (NULL != (word = strtok(s, " "))) {
 			strupr(word);
 			if (word[strlen(word)-1] == '\n')
@@ -556,13 +547,10 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile)
 					}
 
 					if (clearall || fastcmp(word2, "UNLOCKABLES"))
-						memset(&unlockables, 0, sizeof(unlockables));
+						clear_unlockables();
 
 					if (clearall || fastcmp(word2, "EMBLEMS"))
-					{
-						memset(&emblemlocations, 0, sizeof(emblemlocations));
-						numemblems = 0;
-					}
+						clear_emblems();
 
 					if (clearall || fastcmp(word2, "EXTRAEMBLEMS"))
 					{
@@ -639,25 +627,3 @@ void DEH_LoadDehackedLump(lumpnum_t lumpnum)
 {
 	DEH_LoadDehackedLumpPwad(WADFILENUM(lumpnum),LUMPNUM(lumpnum), false);
 }
-
-void DEH_Check(void)
-{
-#if defined(_DEBUG) || defined(PARANOIA)
-	const size_t dehstates = sizeof(STATE_LIST)/sizeof(const char*);
-	const size_t dehmobjs  = sizeof(MOBJTYPE_LIST)/sizeof(const char*);
-	const size_t dehpowers = sizeof(POWERS_LIST)/sizeof(const char*);
-	const size_t dehcolors = sizeof(COLOR_ENUMS)/sizeof(const char*);
-
-	if (dehstates != S_FIRSTFREESLOT)
-		I_Error("You forgot to update the Dehacked states list, you dolt!\n(%d states defined, versus %s in the Dehacked list)\n", S_FIRSTFREESLOT, sizeu1(dehstates));
-
-	if (dehmobjs != MT_FIRSTFREESLOT)
-		I_Error("You forgot to update the Dehacked mobjtype list, you dolt!\n(%d mobj types defined, versus %s in the Dehacked list)\n", MT_FIRSTFREESLOT, sizeu1(dehmobjs));
-
-	if (dehpowers != NUMPOWERS)
-		I_Error("You forgot to update the Dehacked powers list, you dolt!\n(%d powers defined, versus %s in the Dehacked list)\n", NUMPOWERS, sizeu1(dehpowers));
-
-	if (dehcolors != SKINCOLOR_FIRSTFREESLOT)
-		I_Error("You forgot to update the Dehacked colors list, you dolt!\n(%d colors defined, versus %s in the Dehacked list)\n", SKINCOLOR_FIRSTFREESLOT, sizeu1(dehcolors));
-#endif
-}
diff --git a/src/dehacked.h b/src/dehacked.h
index d5256be23f0b05b9b51e80faf12823e3a43e0366..1b200e2466f58994bb4db317db2dd9908565d5e5 100644
--- a/src/dehacked.h
+++ b/src/dehacked.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -30,8 +30,6 @@ typedef enum
 void DEH_LoadDehackedLump(lumpnum_t lumpnum);
 void DEH_LoadDehackedLumpPwad(UINT16 wad, UINT16 lump, boolean mainfile);
 
-void DEH_Check(void);
-
 fixed_t get_number(const char *word);
 FUNCPRINTF void deh_warning(const char *first, ...);
 void deh_strlcpy(char *dst, const char *src, size_t size, const char *warntext);
diff --git a/src/doomdata.h b/src/doomdata.h
index b3f7f5c4dbcc4ea5ce9ee8ff1cdf34a1845cdafa..e317fec1b352e21ab571810d2911ffbf4c03f7f5 100644
--- a/src/doomdata.h
+++ b/src/doomdata.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/doomdef.h b/src/doomdef.h
index d0b7ea0c2391334c703051d02e0dae693dfdfe19..7e7e355990d422a8a6ecc86f54f49a17902ea66b 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -100,7 +100,7 @@
 #include <sys/stat.h>
 #include <ctype.h>
 
-#if defined (_WIN32) || defined (__DJGPP__)
+#ifdef _WIN32
 #include <io.h>
 #endif
 
@@ -112,7 +112,7 @@
 //#define PARANOIA // do some tests that never fail but maybe
 // turn this on by make etc.. DEBUGMODE = 1 or use the Debug profile in the VC++ projects
 //#endif
-#if defined (_WIN32) || (defined (__unix__) && !defined (MSDOS)) || defined(__APPLE__) || defined (UNIXCOMMON) || defined (macintosh)
+#if defined (_WIN32) || defined (__unix__) || defined(__APPLE__) || defined (UNIXCOMMON) || defined (macintosh)
 #define LOGMESSAGES // write message in log.txt
 #endif
 
@@ -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
 
@@ -415,7 +419,7 @@ enum {
 };
 
 // Name of local directory for config files and savegames
-#if (((defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON)) && !defined (__CYGWIN__)) && !defined (__APPLE__)
+#if (defined (__unix__) || defined (UNIXCOMMON)) && !defined (__CYGWIN__) && !defined (__APPLE__)
 #define DEFAULTDIR ".srb2"
 #else
 #define DEFAULTDIR "srb2"
@@ -582,9 +586,6 @@ extern const char *compdate, *comptime, *comprevision, *compbranch;
 ///	Dumps the contents of a network save game upon consistency failure for debugging.
 //#define DUMPCONSISTENCY
 
-///	See name of player in your crosshair
-#define SEENAMES
-
 ///	Who put weights on my recycler?  ... Inuyasha did.
 ///	\note	XMOD port.
 //#define WEIGHTEDRECYCLER
@@ -607,10 +608,6 @@ extern const char *compdate, *comptime, *comprevision, *compbranch;
 /// Experimental tweaks to analog mode. (Needs a lot of work before it's ready for primetime.)
 //#define REDSANALOG
 
-/// Backwards compatibility with musicslots.
-/// \note	You should leave this enabled unless you're working with a future SRB2 version.
-#define MUSICSLOT_COMPATIBILITY
-
 /// Experimental attempts at preventing MF_PAPERCOLLISION objects from getting stuck in walls.
 //#define PAPER_COLLISIONCORRECTION
 
diff --git a/src/doomstat.h b/src/doomstat.h
index 2d28b81af7f007b380e466242c2ff0e49c317f01..32669b68bdecbda94eea9cebd7564907149b4d14 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -337,9 +337,9 @@ typedef struct
 	fixed_t gravity;        ///< Map-wide gravity.
 
 	// Title card.
-	char ltzzpatch[8];      ///< Zig zag patch.
-	char ltzztext[8];       ///< Zig zag text.
-	char ltactdiamond[8];   ///< Act diamond.
+	char ltzzpatch[9];      ///< Zig zag patch.
+	char ltzztext[9];       ///< Zig zag text.
+	char ltactdiamond[9];   ///< Act diamond.
 
 	// Freed animals stuff.
 	UINT8 numFlickies;     ///< Internal. For freed flicky support.
@@ -496,7 +496,7 @@ extern UINT32 lastcustomtol;
 
 extern tic_t totalplaytime;
 
-extern UINT8 stagefailed;
+extern boolean stagefailed;
 
 // Emeralds stored as bits to throw savegame hackers off.
 extern UINT16 emeralds;
diff --git a/src/doomtype.h b/src/doomtype.h
index 4e13ba96d3795bb9992369a48b7aacd27500ea1a..3a57d90e81f25998e80fc7181cd1aa14df50bde3 100644
--- a/src/doomtype.h
+++ b/src/doomtype.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -54,17 +54,6 @@ typedef long ssize_t;
 		#define PDWORD_PTR PDWORD
 	#endif
 #endif
-#elif defined (__DJGPP__)
-#define UINT8 unsigned char
-#define SINT8 signed char
-
-#define UINT16 unsigned short int
-#define INT16 signed short int
-
-#define INT32 signed long
-#define UINT32 unsigned long
-#define INT64  signed long long
-#define UINT64 unsigned long long
 #else
 #define __STDC_LIMIT_MACROS
 #include <stdint.h>
@@ -108,7 +97,7 @@ typedef long ssize_t;
 	#define strncasecmp             strnicmp
 	#define strcasecmp              strcmpi
 #endif
-#if (defined (__unix__) && !defined (MSDOS)) || defined(__APPLE__) || defined (UNIXCOMMON)
+#if defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)
 	#undef stricmp
 	#define stricmp(x,y) strcasecmp(x,y)
 	#undef strnicmp
@@ -136,7 +125,7 @@ char *strcasestr(const char *in, const char *what);
 	#endif
 #endif //macintosh
 
-#if defined (PC_DOS) || defined (_WIN32) || defined (__HAIKU__)
+#if defined (_WIN32) || defined (__HAIKU__)
 #define HAVE_DOSSTR_FUNCS
 #endif
 
@@ -367,6 +356,8 @@ typedef UINT32 tic_t;
 #define UINT2RGBA(a) (UINT32)((a&0xff)<<24)|((a&0xff00)<<8)|((a&0xff0000)>>8)|(((UINT32)a&0xff000000)>>24)
 #endif
 
+#define TOSTR(x) #x
+
 /* preprocessor dumb and needs second macro to expand input */
 #define WSTRING2(s) L ## s
 #define WSTRING(s) WSTRING2 (s)
@@ -379,4 +370,30 @@ Needed for some lua shenanigans.
 #define FIELDFROM( type, field, have, want ) \
 	(void *)((intptr_t)(field) - offsetof (type, have) + offsetof (type, want))
 
+typedef UINT8 bitarray_t;
+
+#define BIT_ARRAY_SIZE(n) (((n) + 7) >> 3)
+
+static inline int
+in_bit_array (const bitarray_t * const array, const int value)
+{
+	return (array[value >> 3] & (1<<(value & 7)));
+}
+
+static inline void
+set_bit_array (bitarray_t * const array, const int value)
+{
+	array[value >> 3] |= (1<<(value & 7));
+}
+
+static inline void
+unset_bit_array (bitarray_t * const array, const int value)
+{
+	array[value >> 3] &= ~(1<<(value & 7));
+}
+
+#ifdef HAVE_SDL
+typedef UINT64 precise_t;
+#endif
+
 #endif //__DOOMTYPE__
diff --git a/src/endian.h b/src/endian.h
index 24d8e35cd541f88bc9975f582f0e7c3d503a7ff8..e78204e723a43715cd7182b6aaebf59e61ee4358 100644
--- a/src/endian.h
+++ b/src/endian.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2014-2020 by Sonic Team Junior.
+// Copyright (C) 2014-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/f_finale.c b/src/f_finale.c
index 688cd4fc7f24cf472d2e8302b0bd2f496cc9f427..8dd03d44f5d068f2b5762dbaea7e51dbd408e5b7 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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)
@@ -1063,7 +1064,7 @@ boolean F_IntroResponder(event_t *event)
 //  CREDITS
 // =========
 static const char *credits[] = {
-	"\1Sonic Robo Blast II",
+	"\1Sonic Robo Blast 2",
 	"\1Credits",
 	"",
 	"\1Game Design",
@@ -1074,6 +1075,7 @@ static const char *credits[] = {
 	"\1Programming",
 	"Alam \"GBC\" Arias",
 	"Logan \"GBA\" Arias",
+	"Zolton \"Zippy_Zolton\" Auburn",
 	"Colette \"fickleheart\" Bordelon",
 	"Andrew \"orospakr\" Clunis",
 	"Sally \"TehRealSalt\" Cochenour",
@@ -1088,22 +1090,23 @@ 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
+	"Riku \"Ors\" Salminen", // Demo consistency improvements
 	"Jonas \"MascaraSnake\" Sauer",
 	"Wessel \"sphere\" Smit",
 	"\"SSNTails\"",
@@ -1136,6 +1139,7 @@ static const char *credits[] = {
 	"Iestyn \"Monster Iestyn\" Jealous",
 	"William \"GuyWithThePie\" Kloppenberg",
 	"Alice \"Alacroix\" de Lemos",
+	"Logan \"Hyperchaotix\" McCloud",
 	"Alexander \"DrTapeworm\" Moench-Ford",
 	"Andrew \"Senku Niola\" Moran",
 	"\"MotorRoach\"",
@@ -1163,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",
@@ -1188,7 +1191,6 @@ static const char *credits[] = {
 	"\"Revan\"",
 	"Anna \"QueenDelta\" Sandlin",
 	"Wessel \"sphere\" Smit",
-	"\"Spazzo\"",
 	"\"SSNTails\"",
 	"Rob Tisdell",
 	"\"Torgo\"",
@@ -1217,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",
@@ -1396,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)
@@ -2543,28 +2545,28 @@ static void F_UnloadAlacroixGraphics(SINT8 oldttscale)
 	oldttscale--; // zero-based index
 	for (i = 0; i < TTMAX_ALACROIX; i++)
 	{
-		if(ttembl[oldttscale][i]) { Z_Free(ttembl[oldttscale][i]); ttembl[oldttscale][i] = 0; }
-		if(ttribb[oldttscale][i]) { Z_Free(ttribb[oldttscale][i]); ttribb[oldttscale][i] = 0; }
-		if(ttsont[oldttscale][i]) { Z_Free(ttsont[oldttscale][i]); ttsont[oldttscale][i] = 0; }
-		if(ttrobo[oldttscale][i]) { Z_Free(ttrobo[oldttscale][i]); ttrobo[oldttscale][i] = 0; }
-		if(tttwot[oldttscale][i]) { Z_Free(tttwot[oldttscale][i]); tttwot[oldttscale][i] = 0; }
-		if(ttrbtx[oldttscale][i]) { Z_Free(ttrbtx[oldttscale][i]); ttrbtx[oldttscale][i] = 0; }
-		if(ttsoib[oldttscale][i]) { Z_Free(ttsoib[oldttscale][i]); ttsoib[oldttscale][i] = 0; }
-		if(ttsoif[oldttscale][i]) { Z_Free(ttsoif[oldttscale][i]); ttsoif[oldttscale][i] = 0; }
-		if(ttsoba[oldttscale][i]) { Z_Free(ttsoba[oldttscale][i]); ttsoba[oldttscale][i] = 0; }
-		if(ttsobk[oldttscale][i]) { Z_Free(ttsobk[oldttscale][i]); ttsobk[oldttscale][i] = 0; }
-		if(ttsodh[oldttscale][i]) { Z_Free(ttsodh[oldttscale][i]); ttsodh[oldttscale][i] = 0; }
-		if(tttaib[oldttscale][i]) { Z_Free(tttaib[oldttscale][i]); tttaib[oldttscale][i] = 0; }
-		if(tttaif[oldttscale][i]) { Z_Free(tttaif[oldttscale][i]); tttaif[oldttscale][i] = 0; }
-		if(tttaba[oldttscale][i]) { Z_Free(tttaba[oldttscale][i]); tttaba[oldttscale][i] = 0; }
-		if(tttabk[oldttscale][i]) { Z_Free(tttabk[oldttscale][i]); tttabk[oldttscale][i] = 0; }
-		if(tttabt[oldttscale][i]) { Z_Free(tttabt[oldttscale][i]); tttabt[oldttscale][i] = 0; }
-		if(tttaft[oldttscale][i]) { Z_Free(tttaft[oldttscale][i]); tttaft[oldttscale][i] = 0; }
-		if(ttknib[oldttscale][i]) { Z_Free(ttknib[oldttscale][i]); ttknib[oldttscale][i] = 0; }
-		if(ttknif[oldttscale][i]) { Z_Free(ttknif[oldttscale][i]); ttknif[oldttscale][i] = 0; }
-		if(ttknba[oldttscale][i]) { Z_Free(ttknba[oldttscale][i]); ttknba[oldttscale][i] = 0; }
-		if(ttknbk[oldttscale][i]) { Z_Free(ttknbk[oldttscale][i]); ttknbk[oldttscale][i] = 0; }
-		if(ttkndh[oldttscale][i]) { Z_Free(ttkndh[oldttscale][i]); ttkndh[oldttscale][i] = 0; }
+		if(ttembl[oldttscale][i]) { Patch_Free(ttembl[oldttscale][i]); ttembl[oldttscale][i] = 0; }
+		if(ttribb[oldttscale][i]) { Patch_Free(ttribb[oldttscale][i]); ttribb[oldttscale][i] = 0; }
+		if(ttsont[oldttscale][i]) { Patch_Free(ttsont[oldttscale][i]); ttsont[oldttscale][i] = 0; }
+		if(ttrobo[oldttscale][i]) { Patch_Free(ttrobo[oldttscale][i]); ttrobo[oldttscale][i] = 0; }
+		if(tttwot[oldttscale][i]) { Patch_Free(tttwot[oldttscale][i]); tttwot[oldttscale][i] = 0; }
+		if(ttrbtx[oldttscale][i]) { Patch_Free(ttrbtx[oldttscale][i]); ttrbtx[oldttscale][i] = 0; }
+		if(ttsoib[oldttscale][i]) { Patch_Free(ttsoib[oldttscale][i]); ttsoib[oldttscale][i] = 0; }
+		if(ttsoif[oldttscale][i]) { Patch_Free(ttsoif[oldttscale][i]); ttsoif[oldttscale][i] = 0; }
+		if(ttsoba[oldttscale][i]) { Patch_Free(ttsoba[oldttscale][i]); ttsoba[oldttscale][i] = 0; }
+		if(ttsobk[oldttscale][i]) { Patch_Free(ttsobk[oldttscale][i]); ttsobk[oldttscale][i] = 0; }
+		if(ttsodh[oldttscale][i]) { Patch_Free(ttsodh[oldttscale][i]); ttsodh[oldttscale][i] = 0; }
+		if(tttaib[oldttscale][i]) { Patch_Free(tttaib[oldttscale][i]); tttaib[oldttscale][i] = 0; }
+		if(tttaif[oldttscale][i]) { Patch_Free(tttaif[oldttscale][i]); tttaif[oldttscale][i] = 0; }
+		if(tttaba[oldttscale][i]) { Patch_Free(tttaba[oldttscale][i]); tttaba[oldttscale][i] = 0; }
+		if(tttabk[oldttscale][i]) { Patch_Free(tttabk[oldttscale][i]); tttabk[oldttscale][i] = 0; }
+		if(tttabt[oldttscale][i]) { Patch_Free(tttabt[oldttscale][i]); tttabt[oldttscale][i] = 0; }
+		if(tttaft[oldttscale][i]) { Patch_Free(tttaft[oldttscale][i]); tttaft[oldttscale][i] = 0; }
+		if(ttknib[oldttscale][i]) { Patch_Free(ttknib[oldttscale][i]); ttknib[oldttscale][i] = 0; }
+		if(ttknif[oldttscale][i]) { Patch_Free(ttknif[oldttscale][i]); ttknif[oldttscale][i] = 0; }
+		if(ttknba[oldttscale][i]) { Patch_Free(ttknba[oldttscale][i]); ttknba[oldttscale][i] = 0; }
+		if(ttknbk[oldttscale][i]) { Patch_Free(ttknbk[oldttscale][i]); ttknbk[oldttscale][i] = 0; }
+		if(ttkndh[oldttscale][i]) { Patch_Free(ttkndh[oldttscale][i]); ttkndh[oldttscale][i] = 0; }
 	}
 	ttloaded[oldttscale] = false;
 }
@@ -3420,7 +3422,7 @@ void F_TitleScreenDrawer(void)
 	}
 
 luahook:
-	LUAh_TitleHUD();
+	LUA_HUDHOOK(title);
 }
 
 // separate animation timer for backgrounds, since we also count
@@ -3820,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/f_finale.h b/src/f_finale.h
index b3abf1778408a43e54fe310d28d5410f821f98da..4aa2c3f05b121a17c4fcfe7c4163e4b1db274415 100644
--- a/src/f_finale.h
+++ b/src/f_finale.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/f_wipe.c b/src/f_wipe.c
index 6afb8a6a7934709c90b1428fcd5f5a6f55d54c20..7526aeca36f6bd94cd8179c79fc7be299d395e40 100644
--- a/src/f_wipe.c
+++ b/src/f_wipe.c
@@ -3,7 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 2013-2016 by Matthew "Kaito Sinclaire" Walsh.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
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 9d3b8601584385d06bbd14e5ff8a2ec4ea63ca3c..c97dbcf9ee0559e23e6635e4a62f2de1cb843af2 100644
--- a/src/g_demo.c
+++ b/src/g_demo.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -109,6 +109,7 @@ demoghost *ghosts = NULL;
 #define ZT_ANGLE   0x04
 #define ZT_BUTTONS 0x08
 #define ZT_AIMING  0x10
+#define ZT_LATENCY 0x20
 #define DEMOMARKER 0x80 // demoend
 #define METALDEATH 0x44
 #define METALSNICE 0x69
@@ -181,6 +182,8 @@ void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
 		oldcmd.buttons = (oldcmd.buttons & (BT_CAMLEFT|BT_CAMRIGHT)) | (READUINT16(demo_p) & ~(BT_CAMLEFT|BT_CAMRIGHT));
 	if (ziptic & ZT_AIMING)
 		oldcmd.aiming = READINT16(demo_p);
+	if (ziptic & ZT_LATENCY)
+		oldcmd.latency = READUINT8(demo_p);
 
 	G_CopyTiccmd(cmd, &oldcmd, 1);
 	players[playernum].angleturn = cmd->angleturn;
@@ -238,6 +241,13 @@ void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
 		ziptic |= ZT_AIMING;
 	}
 
+	if (cmd->latency != oldcmd.latency)
+	{
+		WRITEUINT8(demo_p,cmd->latency);
+		oldcmd.latency = cmd->latency;
+		ziptic |= ZT_LATENCY;
+	}
+
 	*ziptic_p = ziptic;
 
 	// attention here for the ticcmd size!
@@ -679,6 +689,8 @@ void G_GhostTicker(void)
 			g->p += 2;
 		if (ziptic & ZT_AIMING)
 			g->p += 2;
+		if (ziptic & ZT_LATENCY)
+			g->p++;
 
 		// Grab ghost data.
 		ziptic = READUINT8(g->p);
@@ -1668,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.
@@ -1956,9 +1969,7 @@ void G_DoPlayDemo(char *defdemoname)
 	// Set skin
 	SetPlayerSkin(0, skin);
 
-#ifdef HAVE_BLUA
-	LUAh_MapChange(gamemap);
-#endif
+	LUA_HookInt(gamemap, HOOK(MapChange));
 	displayplayer = consoleplayer = 0;
 	memset(playeringame,0,sizeof(playeringame));
 	playeringame[0] = true;
@@ -2013,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;
@@ -2061,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)
 	{
@@ -2160,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)
@@ -2412,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_demo.h b/src/g_demo.h
index df25042c48030402622a71a611c8a7f2a53649e7..73cf273582ff1baeffcb2638ebe81654e6b8d70d 100644
--- a/src/g_demo.h
+++ b/src/g_demo.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/g_game.c b/src/g_game.c
index 283113bbeaec2a78b7756b6361d90e33764ec315..3955834b2170fa203222a5c76f8f9ead60e74fa6 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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"
 
@@ -169,7 +170,7 @@ static boolean exitgame = false;
 static boolean retrying = false;
 static boolean retryingmodeattack = false;
 
-UINT8 stagefailed; // Used for GEMS BONUS? Also to see if you beat the stage.
+boolean stagefailed = false; // Used for GEMS BONUS? Also to see if you beat the stage.
 
 UINT16 emeralds;
 INT32 luabanks[NUM_LUABANKS];
@@ -406,22 +407,6 @@ consvar_t cv_cam_lockonboss[2] = {
 	CVAR_INIT ("cam2_lockaimassist", "Bosses", CV_SAVE, lockedassist_cons_t, NULL),
 };
 
-typedef enum
-{
-	AXISNONE = 0,
-	AXISTURN,
-	AXISMOVE,
-	AXISLOOK,
-	AXISSTRAFE,
-
-	AXISDIGITAL, // axes below this use digital deadzone
-
-	AXISJUMP,
-	AXISSPIN,
-	AXISFIRE,
-	AXISFIRENORMAL,
-} axis_input_e;
-
 consvar_t cv_turnaxis = CVAR_INIT ("joyaxis_turn", "X-Rudder", CV_SAVE, joyaxis_cons_t, NULL);
 consvar_t cv_moveaxis = CVAR_INIT ("joyaxis_move", "Y-Axis", CV_SAVE, joyaxis_cons_t, NULL);
 consvar_t cv_sideaxis = CVAR_INIT ("joyaxis_side", "X-Axis", CV_SAVE, joyaxis_cons_t, NULL);
@@ -444,9 +429,7 @@ consvar_t cv_firenaxis2 = CVAR_INIT ("joyaxis2_firenormal", "Z-Axis", CV_SAVE, j
 consvar_t cv_deadzone2 = CVAR_INIT ("joy_deadzone2", "0.125", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
 consvar_t cv_digitaldeadzone2 = CVAR_INIT ("joy_digdeadzone2", "0.25", CV_FLOAT|CV_SAVE, zerotoone_cons_t, NULL);
 
-#ifdef SEENAMES
 player_t *seenplayer; // player we're aiming at right now
-#endif
 
 // now automatically allocated in D_RegisterClientCommands
 // so that it doesn't have to be updated depending on the value of MAXPLAYERS
@@ -843,7 +826,7 @@ INT16 G_SoftwareClipAimingPitch(INT32 *aiming)
 	return (INT16)((*aiming)>>16);
 }
 
-static INT32 JoyAxis(axis_input_e axissel)
+INT32 JoyAxis(joyaxis_e axissel)
 {
 	INT32 retaxis;
 	INT32 axisval;
@@ -852,28 +835,28 @@ static INT32 JoyAxis(axis_input_e axissel)
 	//find what axis to get
 	switch (axissel)
 	{
-		case AXISTURN:
+		case JA_TURN:
 			axisval = cv_turnaxis.value;
 			break;
-		case AXISMOVE:
+		case JA_MOVE:
 			axisval = cv_moveaxis.value;
 			break;
-		case AXISLOOK:
+		case JA_LOOK:
 			axisval = cv_lookaxis.value;
 			break;
-		case AXISSTRAFE:
+		case JA_STRAFE:
 			axisval = cv_sideaxis.value;
 			break;
-		case AXISJUMP:
+		case JA_JUMP:
 			axisval = cv_jumpaxis.value;
 			break;
-		case AXISSPIN:
+		case JA_SPIN:
 			axisval = cv_spinaxis.value;
 			break;
-		case AXISFIRE:
+		case JA_FIRE:
 			axisval = cv_fireaxis.value;
 			break;
-		case AXISFIRENORMAL:
+		case JA_FIRENORMAL:
 			axisval = cv_firenaxis.value;
 			break;
 		default:
@@ -905,7 +888,7 @@ static INT32 JoyAxis(axis_input_e axissel)
 	if (retaxis > (+JOYAXISRANGE))
 		retaxis = +JOYAXISRANGE;
 
-	if (!Joystick.bGamepadStyle && axissel > AXISDIGITAL)
+	if (!Joystick.bGamepadStyle && axissel >= JA_DIGITAL)
 	{
 		const INT32 jdeadzone = ((JOYAXISRANGE-1) * cv_digitaldeadzone.value) >> FRACBITS;
 		if (-jdeadzone < retaxis && retaxis < jdeadzone)
@@ -916,7 +899,7 @@ static INT32 JoyAxis(axis_input_e axissel)
 	return retaxis;
 }
 
-static INT32 Joy2Axis(axis_input_e axissel)
+INT32 Joy2Axis(joyaxis_e axissel)
 {
 	INT32 retaxis;
 	INT32 axisval;
@@ -925,28 +908,28 @@ static INT32 Joy2Axis(axis_input_e axissel)
 	//find what axis to get
 	switch (axissel)
 	{
-		case AXISTURN:
+		case JA_TURN:
 			axisval = cv_turnaxis2.value;
 			break;
-		case AXISMOVE:
+		case JA_MOVE:
 			axisval = cv_moveaxis2.value;
 			break;
-		case AXISLOOK:
+		case JA_LOOK:
 			axisval = cv_lookaxis2.value;
 			break;
-		case AXISSTRAFE:
+		case JA_STRAFE:
 			axisval = cv_sideaxis2.value;
 			break;
-		case AXISJUMP:
+		case JA_JUMP:
 			axisval = cv_jumpaxis2.value;
 			break;
-		case AXISSPIN:
+		case JA_SPIN:
 			axisval = cv_spinaxis2.value;
 			break;
-		case AXISFIRE:
+		case JA_FIRE:
 			axisval = cv_fireaxis2.value;
 			break;
-		case AXISFIRENORMAL:
+		case JA_FIRENORMAL:
 			axisval = cv_firenaxis2.value;
 			break;
 		default:
@@ -980,7 +963,7 @@ static INT32 Joy2Axis(axis_input_e axissel)
 	if (retaxis > (+JOYAXISRANGE))
 		retaxis = +JOYAXISRANGE;
 
-	if (!Joystick2.bGamepadStyle && axissel > AXISDIGITAL)
+	if (!Joystick2.bGamepadStyle && axissel >= JA_DIGITAL)
 	{
 		const INT32 jdeadzone = ((JOYAXISRANGE-1) * cv_digitaldeadzone2.value) >> FRACBITS;
 		if (-jdeadzone < retaxis && retaxis < jdeadzone)
@@ -1089,14 +1072,14 @@ 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);
 
 	angle_t drawangleoffset = (player->powers[pw_carry] == CR_ROLLOUT) ? ANGLE_180 : 0;
 	INT32 chasecam, chasefreelook, alwaysfreelook, usejoystick, invertmouse, turnmultiplier, mousemove;
 	controlstyle_e controlstyle = G_ControlStyle(ssplayer);
-	INT32 *mx; INT32 *my; INT32 *mly;
+	INT32 mdx, mdy, mldy;
 
 	static INT32 turnheld[2]; // for accelerative turning
 	static boolean keyboard_look[2]; // true if lookup/down using keyboard
@@ -1119,9 +1102,9 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		invertmouse = cv_invertmouse.value;
 		turnmultiplier = cv_cam_turnmultiplier.value;
 		mousemove = cv_mousemove.value;
-		mx = &mousex;
-		my = &mousey;
-		mly = &mlooky;
+		mdx = mouse.dx;
+		mdy = -mouse.dy;
+		mldy = -mouse.mlookdy;
 		G_CopyTiccmd(cmd, I_BaseTiccmd(), 1); // empty, or external driver
 	}
 	else
@@ -1133,12 +1116,15 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		invertmouse = cv_invertmouse2.value;
 		turnmultiplier = cv_cam2_turnmultiplier.value;
 		mousemove = cv_mousemove2.value;
-		mx = &mouse2x;
-		my = &mouse2y;
-		mly = &mlook2y;
+		mdx = mouse2.dx;
+		mdy = -mouse2.dy;
+		mldy = -mouse2.mlookdy;
 		G_CopyTiccmd(cmd, I_BaseTiccmd2(), 1); // empty, or external driver
 	}
 
+	if (menuactive || CON_Ready() || chat_on)
+		mdx = mdy = mldy = 0;
+
 	strafeisturn = controlstyle == CS_SIMPLE && ticcmd_centerviewdown[forplayer] &&
 		((cv_cam_lockedinput[forplayer].value && !ticcmd_ztargetfocus[forplayer]) || (player->pflags & PF_STARTDASH)) &&
 		!player->climbing && player->powers[pw_carry] != CR_MINECART;
@@ -1154,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)
 	{
@@ -1169,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;
@@ -1181,10 +1167,10 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		*myaiming = 0;
 	joyaiming[forplayer] = thisjoyaiming;
 
-	turnaxis = PlayerJoyAxis(ssplayer, AXISTURN);
+	turnaxis = PlayerJoyAxis(ssplayer, JA_TURN);
 	if (strafeisturn)
-		turnaxis += PlayerJoyAxis(ssplayer, AXISSTRAFE);
-	lookaxis = PlayerJoyAxis(ssplayer, AXISLOOK);
+		turnaxis += PlayerJoyAxis(ssplayer, JA_STRAFE);
+	lookaxis = PlayerJoyAxis(ssplayer, JA_LOOK);
 	lookjoystickvector.xaxis = turnaxis;
 	lookjoystickvector.yaxis = lookaxis;
 	G_HandleAxisDeadZone(forplayer, &lookjoystickvector);
@@ -1263,8 +1249,8 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 			tta_factor[forplayer] = 0; // suspend turn to angle
 	}
 
-	strafeaxis = strafeisturn ? 0 : PlayerJoyAxis(ssplayer, AXISSTRAFE);
-	moveaxis = PlayerJoyAxis(ssplayer, AXISMOVE);
+	strafeaxis = strafeisturn ? 0 : PlayerJoyAxis(ssplayer, JA_STRAFE);
+	moveaxis = PlayerJoyAxis(ssplayer, JA_MOVE);
 	movejoystickvector.xaxis = strafeaxis;
 	movejoystickvector.yaxis = moveaxis;
 	G_HandleAxisDeadZone(forplayer, &movejoystickvector);
@@ -1285,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)
@@ -1302,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
@@ -1313,42 +1299,42 @@ 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;
 		}
 
 	// fire with any button/key
-	axis = PlayerJoyAxis(ssplayer, AXISFIRE);
-	if (PLAYERINPUTDOWN(ssplayer, gc_fire) || (usejoystick && axis > 0))
+	axis = PlayerJoyAxis(ssplayer, JA_FIRE);
+	if (PLAYERINPUTDOWN(ssplayer, GC_FIRE) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_ATTACK;
 
 	// fire normal with any button/key
-	axis = PlayerJoyAxis(ssplayer, AXISFIRENORMAL);
-	if (PLAYERINPUTDOWN(ssplayer, gc_firenormal) || (usejoystick && axis > 0))
+	axis = PlayerJoyAxis(ssplayer, JA_FIRENORMAL);
+	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, AXISSPIN);
-	if (PLAYERINPUTDOWN(ssplayer, gc_spin) || (usejoystick && axis > 0))
+	axis = PlayerJoyAxis(ssplayer, JA_SPIN);
+	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;
@@ -1447,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);
@@ -1459,8 +1445,8 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 
 
 	// jump button
-	axis = PlayerJoyAxis(ssplayer, AXISJUMP);
-	if (PLAYERINPUTDOWN(ssplayer, gc_jump) || (usejoystick && axis > 0))
+	axis = PlayerJoyAxis(ssplayer, JA_JUMP);
+	if (PLAYERINPUTDOWN(ssplayer, GC_JUMP) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_JUMP;
 
 	// player aiming shit, ahhhh...
@@ -1478,7 +1464,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 			keyboard_look[forplayer] = false;
 
 			// looking up/down
-			*myaiming += (*mly<<19)*player_invert*screen_invert;
+			*myaiming += (mldy<<19)*player_invert*screen_invert;
 		}
 
 		if (analogjoystickmove && joyaiming[forplayer] && lookjoystickvector.yaxis != 0 && configlookaxis != 0)
@@ -1490,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;
@@ -1512,24 +1498,22 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	}
 
 	if (!mouseaiming && mousemove)
-		forward += *my;
+		forward += mdy;
 
 	if ((!demoplayback && (player->pflags & PF_SLIDING))) // Analog for mouse
-		side += *mx*2;
+		side += mdx*2;
 	else if (controlstyle == CS_LMAOGALOG)
 	{
-		if (*mx)
+		if (mdx)
 		{
-			if (*mx > 0)
+			if (mdx > 0)
 				cmd->buttons |= BT_CAMRIGHT;
 			else
 				cmd->buttons |= BT_CAMLEFT;
 		}
 	}
 	else
-		cmd->angleturn = (INT16)(cmd->angleturn - (*mx*8));
-
-	*mx = *my = *mly = 0;
+		cmd->angleturn = (INT16)(cmd->angleturn - (mdx*8));
 
 	if (forward > MAXPLMOVE)
 		forward = MAXPLMOVE;
@@ -1562,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) {
@@ -1680,7 +1654,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	// At this point, cmd doesn't contain the final angle yet,
 	// So we need to temporarily transform it so Lua scripters
 	// don't need to handle it differently than in other hooks.
-	if (gamestate == GS_LEVEL)
+	if (addedtogame && gamestate == GS_LEVEL)
 	{
 		INT16 extra = ticcmd_oldangleturn[forplayer] - player->oldrelangleturn;
 		INT16 origangle = cmd->angleturn;
@@ -1689,12 +1663,16 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 
 		cmd->angleturn = orighookangle;
 
-		LUAh_PlayerCmd(player, cmd);
+		LUA_HookTiccmd(player, cmd, HOOK(PlayerCmd));
 
 		extra = cmd->angleturn - orighookangle;
 		cmd->angleturn = origangle + extra;
 		*myangle += extra << 16;
 		*myaiming += (cmd->aiming - origaiming) << 16;
+
+		// Send leveltime when this tic was generated to the server for control lag calculations.
+		// Only do this when in a level. Also do this after the hook, so that it can't overwrite this.
+		cmd->latency = (leveltime & 0xFF);
 	}
 
 	//Reset away view if a command is given.
@@ -1703,7 +1681,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	{
 		// Call ViewpointSwitch hooks here.
 		// The viewpoint was forcibly changed.
-		LUAh_ViewpointSwitch(player, &players[consoleplayer], true);
+		LUA_HookViewpointSwitch(player, &players[consoleplayer], true);
 		displayplayer = consoleplayer;
 	}
 
@@ -1726,6 +1704,7 @@ ticcmd_t *G_MoveTiccmd(ticcmd_t* dest, const ticcmd_t* src, const size_t n)
 		dest[i].angleturn = SHORT(src[i].angleturn);
 		dest[i].aiming = (INT16)SHORT(src[i].aiming);
 		dest[i].buttons = (UINT16)SHORT(src[i].buttons);
+		dest[i].latency = src[i].latency;
 	}
 	return dest;
 }
@@ -1875,8 +1854,8 @@ void G_DoLoadLevel(boolean resetplayer)
 		joyxmove[i] = joyymove[i] = 0;
 		joy2xmove[i] = joy2ymove[i] = 0;
 	}
-	mousex = mousey = 0;
-	mouse2x = mouse2y = 0;
+	G_SetMouseDeltas(0, 0, 1);
+	G_SetMouseDeltas(0, 0, 2);
 
 	// clear hud messages remains (usually from game startup)
 	CON_ClearHUD();
@@ -1980,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;
@@ -2056,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;
@@ -2076,7 +2055,7 @@ boolean G_Responder(event_t *ev)
 					continue;
 
 				// Call ViewpointSwitch hooks here.
-				canSwitchView = LUAh_ViewpointSwitch(&players[consoleplayer], &players[displayplayer], false);
+				canSwitchView = LUA_HookViewpointSwitch(&players[consoleplayer], &players[displayplayer], false);
 				if (canSwitchView == 1) // Set viewpoint to this player
 					break;
 				else if (canSwitchView == 2) // Skip this player
@@ -2129,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;
 
@@ -2160,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)
 				{
@@ -2169,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)
 				{
@@ -2200,6 +2179,28 @@ boolean G_Responder(event_t *ev)
 	return false;
 }
 
+//
+// G_LuaResponder
+// Let Lua handle key events.
+//
+boolean G_LuaResponder(event_t *ev)
+{
+	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;
+}
+
 //
 // G_Ticker
 // Make ticcmd_ts for the players.
@@ -2209,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++;
@@ -2232,8 +2250,35 @@ void G_Ticker(boolean run)
 				// Costs a life to retry ... unless the player in question is dead already, or you haven't even touched the first starpost in marathon run.
 				if (marathonmode && gamemap == spmarathon_start && !players[consoleplayer].starposttime)
 				{
+					player_t *p = &players[consoleplayer];
 					marathonmode |= MA_INIT;
 					marathontime = 0;
+
+					numgameovers = tokenlist = token = 0;
+					countdown = countdown2 = exitfadestarted = 0;
+
+					p->playerstate = PST_REBORN;
+					p->starpostx = p->starposty = p->starpostz = 0;
+
+					p->lives = startinglivesbalance[0];
+					p->continues = 1;
+
+					p->score = 0;
+
+					// The latter two should clear by themselves, but just in case
+					p->pflags &= ~(PF_TAGIT|PF_GAMETYPEOVER|PF_FULLSTASIS);
+
+					// Clear cheatcodes too, just in case.
+					p->pflags &= ~(PF_GODMODE|PF_NOCLIP|PF_INVIS);
+
+					p->xtralife = 0;
+
+					// Reset unlockable triggers
+					unlocktriggers = 0;
+
+					emeralds = 0;
+
+					memset(&luabanks, 0, sizeof(luabanks));
 				}
 				else if (G_GametypeUsesLives() && players[consoleplayer].playerstate == PST_LIVE && players[consoleplayer].lives != INFLIVES)
 					players[consoleplayer].lives -= 1;
@@ -2266,14 +2311,59 @@ void G_Ticker(boolean run)
 	{
 		if (playeringame[i])
 		{
-			G_CopyTiccmd(&players[i].cmd, &netcmds[buf][i], 1);
+			INT16 received;
+			// Save last frame's button readings
+			players[i].lastbuttons = players[i].cmd.buttons;
 
-			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;
+			G_CopyTiccmd(&players[i].cmd, &netcmds[buf][i], 1);
+			// 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;
 		}
 	}
 
@@ -2459,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;
@@ -2475,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;
@@ -2551,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;
 
@@ -2595,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;
@@ -2713,7 +2808,7 @@ void G_SpawnPlayer(INT32 playernum)
 
 	P_SpawnPlayer(playernum);
 	G_MovePlayerToSpawnOrStarpost(playernum);
-	LUAh_PlayerSpawn(&players[playernum]); // Lua hook for player spawning :)
+	LUA_HookPlayer(&players[playernum], HOOK(PlayerSpawn)); // Lua hook for player spawning :)
 }
 
 void G_MovePlayerToSpawnOrStarpost(INT32 playernum)
@@ -2942,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;
 
@@ -2960,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;
@@ -3063,8 +3181,8 @@ void G_DoReborn(INT32 playernum)
 				joyxmove[i] = joyymove[i] = 0;
 				joy2xmove[i] = joy2ymove[i] = 0;
 			}
-			mousex = mousey = 0;
-			mouse2x = mouse2y = 0;
+			G_SetMouseDeltas(0, 0, 1);
+			G_SetMouseDeltas(0, 0, 2);
 
 			// clear hud messages remains (usually from game startup)
 			CON_ClearHUD();
@@ -3092,7 +3210,7 @@ void G_DoReborn(INT32 playernum)
 		}
 		else
 		{
-			LUAh_MapChange(gamemap);
+			LUA_HookInt(gamemap, HOOK(MapChange));
 			titlecardforreload = true;
 			G_DoLoadLevel(true);
 			titlecardforreload = false;
@@ -3141,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++;
@@ -3182,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;
@@ -3475,6 +3593,7 @@ tolinfo_t TYPEOFLEVEL[NUMTOLNAMES] = {
 	{"MARIO",TOL_MARIO},
 	{"NIGHTS",TOL_NIGHTS},
 	{"OLDBRAK",TOL_ERZ3},
+	{"ERZ3",TOL_ERZ3},
 
 	{"XMAS",TOL_XMAS},
 	{"CHRISTMAS",TOL_XMAS},
@@ -3710,7 +3829,7 @@ static void G_UpdateVisited(void)
 	// Update visitation flags?
 	if ((!modifiedgame || savemoddata) // Not modified
 		&& !multiplayer && !demoplayback && (gametype == GT_COOP) // SP/RA/NiGHTS mode
-		&& !(spec && stagefailed)) // Not failed the special stage
+		&& !stagefailed) // Did not fail the stage
 	{
 		UINT8 earnedEmblems;
 
@@ -3807,6 +3926,9 @@ static void G_DoCompleted(void)
 	if (metalrecording)
 		G_StopMetalRecording(false);
 
+	G_SetGamestate(GS_NULL);
+	wipegamestate = GS_NULL;
+
 	for (i = 0; i < MAXPLAYERS; i++)
 		if (playeringame[i])
 			G_PlayerFinishLevel(i); // take away cards and stuff
@@ -3895,12 +4017,13 @@ static void G_DoCompleted(void)
 	{
 		token--;
 
-		for (i = 0; i < 7; i++)
-			if (!(emeralds & (1<<i)))
-			{
-				nextmap = ((netgame || multiplayer) ? smpstage_start : sstage_start) + i - 1; // to special stage!
-				break;
-			}
+		if (!nextmapoverride)
+			for (i = 0; i < 7; i++)
+				if (!(emeralds & (1<<i)))
+				{
+					nextmap = ((netgame || multiplayer) ? smpstage_start : sstage_start) + i - 1; // to special stage!
+					break;
+				}
 
 		if (i == 7)
 		{
@@ -3931,7 +4054,7 @@ static void G_DoCompleted(void)
 	// If the current gametype has no intermission screen set, then don't start it.
 	Y_DetermineIntermissionType();
 
-	if ((skipstats && !modeattacking) || (spec && modeattacking && stagefailed) || (intertype == int_none))
+	if ((skipstats && !modeattacking) || (modeattacking && stagefailed) || (intertype == int_none))
 	{
 		G_UpdateVisited();
 		G_HandleSaveLevel();
@@ -3941,6 +4064,7 @@ static void G_DoCompleted(void)
 	{
 		G_SetGamestate(GS_INTERMISSION);
 		Y_StartIntermission();
+		Y_LoadIntermissionData();
 		G_UpdateVisited();
 		G_HandleSaveLevel();
 	}
@@ -3962,8 +4086,15 @@ void G_AfterIntermission(void)
 
 	HU_ClearCEcho();
 
-	if ((gametyperules & GTR_CUTSCENES) && mapheaderinfo[gamemap-1]->cutscenenum && !modeattacking && skipstats <= 1 && (gamecomplete || !(marathonmode & MA_NOCUTSCENES))) // Start a custom cutscene.
+	if ((gametyperules & GTR_CUTSCENES) && mapheaderinfo[gamemap-1]->cutscenenum
+		&& !modeattacking
+		&& skipstats <= 1
+		&& (gamecomplete || !(marathonmode & MA_NOCUTSCENES))
+		&& stagefailed == false)
+	{
+		// Start a custom cutscene.
 		F_StartCustomCutscene(mapheaderinfo[gamemap-1]->cutscenenum-1, false, false);
+	}
 	else
 	{
 		if (nextmap < 1100-1)
@@ -4585,6 +4716,9 @@ void G_SaveGameOver(UINT32 slot, boolean modifylives)
 		UINT8 *end_p = savebuffer + length;
 		UINT8 *lives_p;
 		SINT8 pllives;
+#ifdef NEWSKINSAVES
+		INT16 backwardsCompat = 0;
+#endif
 
 		save_p = savebuffer;
 		// Version check
@@ -4603,9 +4737,23 @@ void G_SaveGameOver(UINT32 slot, boolean modifylives)
 
 		// P_UnArchivePlayer()
 		CHECKPOS
-		(void)READUINT16(save_p);
+#ifdef NEWSKINSAVES
+		backwardsCompat = READUINT16(save_p);
 		CHECKPOS
 
+		if (backwardsCompat == NEWSKINSAVES) // New save, read skin names
+#endif
+		{
+			char ourSkinName[SKINNAMESIZE+1];
+			char botSkinName[SKINNAMESIZE+1];
+
+			READSTRINGN(save_p, ourSkinName, SKINNAMESIZE);
+			CHECKPOS
+
+			READSTRINGN(save_p, botSkinName, SKINNAMESIZE);
+			CHECKPOS
+		}
+
 		WRITEUINT8(save_p, numgameovers);
 		CHECKPOS
 
@@ -5178,4 +5326,3 @@ INT32 G_TicsToMilliseconds(tic_t tics)
 {
 	return (INT32)((tics%TICRATE) * (1000.00f/TICRATE));
 }
-
diff --git a/src/g_game.h b/src/g_game.h
index 2bcf444c234c244c2f8f76aab62cebee226d18ee..f98269fcec2280ceac908c310236c88bb01ee73f 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -25,9 +25,7 @@ extern char timeattackfolder[64];
 extern char customversionstring[32];
 #define GAMEDATASIZE (4*8192)
 
-#ifdef SEENAMES
 extern player_t *seenplayer;
-#endif
 extern char  player_names[MAXPLAYERS][MAXPLAYERNAME+1];
 extern INT32 player_name_changes[MAXPLAYERS];
 
@@ -87,6 +85,25 @@ typedef enum
 } lockassist_e;
 
 
+typedef enum
+{
+	JA_NONE = 0,
+	JA_TURN,
+	JA_MOVE,
+	JA_LOOK,
+	JA_STRAFE,
+
+	JA_DIGITAL, // axes henceforth use digital deadzone
+
+	JA_JUMP = JA_DIGITAL,
+	JA_SPIN,
+	JA_FIRE,
+	JA_FIRENORMAL,
+} joyaxis_e;
+
+INT32 JoyAxis(joyaxis_e axissel);
+INT32 Joy2Axis(joyaxis_e axissel);
+
 // mouseaiming (looking up/down with the mouse or keyboard)
 #define KB_LOOKSPEED (1<<25)
 #define MAXPLMOVE (50)
@@ -206,6 +223,7 @@ void G_EndGame(void); // moved from y_inter.c/h and renamed
 
 void G_Ticker(boolean run);
 boolean G_Responder(event_t *ev);
+boolean G_LuaResponder(event_t *ev);
 
 void G_AddPlayer(INT32 playernum);
 
diff --git a/src/g_input.c b/src/g_input.c
index d3c21e774c4359d4f2433379dcbbe6e7fdc79436..6383c3f0068a3c47007f6fc50422f46b9e4bbb2c 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -31,10 +31,8 @@ consvar_t cv_mouseysens = CVAR_INIT ("mouseysens", "20", CV_SAVE, mousesens_cons
 consvar_t cv_mouseysens2 = CVAR_INIT ("mouseysens2", "20", CV_SAVE, mousesens_cons_t, NULL);
 consvar_t cv_controlperkey = CVAR_INIT ("controlperkey", "One", CV_SAVE, onecontrolperkey_cons_t, NULL);
 
-INT32 mousex, mousey;
-INT32 mlooky; // like mousey but with a custom sensitivity for mlook
-
-INT32 mouse2x, mouse2y, mlook2y;
+mouse_t mouse;
+mouse_t mouse2;
 
 // joystick values are repeated
 INT32 joyxmove[JOYAXISSET], joyymove[JOYAXISSET], joy2xmove[JOYAXISSET], joy2ymove[JOYAXISSET];
@@ -43,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
@@ -117,58 +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
-			if (menuactive || CON_Ready() || chat_on)
-				break;
-			mousex = (INT32)(ev->data2*((cv_mousesens.value*cv_mousesens.value)/110.0f + 0.1f));
-			mousey = (INT32)(ev->data3*((cv_mousesens.value*cv_mousesens.value)/110.0f + 0.1f));
-			mlooky = (INT32)(ev->data3*((cv_mouseysens.value*cv_mousesens.value)/110.0f + 0.1f));
+			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;
-			mouse2x = (INT32)(ev->data2*((cv_mousesens2.value*cv_mousesens2.value)/110.0f + 0.1f));
-			mouse2y = (INT32)(ev->data3*((cv_mousesens2.value*cv_mousesens2.value)/110.0f + 0.1f));
-			mlook2y = (INT32)(ev->data3*((cv_mouseysens2.value*cv_mousesens2.value)/110.0f + 0.1f));
+			mouse2.rdx = ev->x;
+			mouse2.rdy = ev->y;
 			break;
 
 		default:
@@ -239,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",
@@ -619,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);
@@ -630,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];
 
@@ -654,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;
 
@@ -682,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
 	}
 }
 
@@ -779,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) &&
@@ -802,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];
@@ -814,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");
 	}
@@ -839,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)
 			{
@@ -889,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;
@@ -901,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]);
@@ -999,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)
@@ -1073,3 +1067,17 @@ void Command_Setcontrol2_f(void)
 
 	setcontrol(gamecontrolbis);
 }
+
+void G_SetMouseDeltas(INT32 dx, INT32 dy, UINT8 ssplayer)
+{
+	mouse_t *m = ssplayer == 1 ? &mouse : &mouse2;
+	consvar_t *cvsens, *cvysens;
+
+	cvsens = ssplayer == 1 ? &cv_mousesens : &cv_mousesens2;
+	cvysens = ssplayer == 1 ? &cv_mouseysens : &cv_mouseysens2;
+	m->rdx = dx;
+	m->rdy = dy;
+	m->dx = (INT32)(m->rdx*((cvsens->value*cvsens->value)/110.0f + 0.1f));
+	m->dy = (INT32)(m->rdy*((cvsens->value*cvsens->value)/110.0f + 0.1f));
+	m->mlookdy = (INT32)(m->rdy*((cvysens->value*cvsens->value)/110.0f + 0.1f));
+}
diff --git a/src/g_input.h b/src/g_input.h
index ce38f6ba9d68a623b880361d868aeebdd18eb135..2e9f53dcf4e4ff57aa9deccbffc7537ebaa6c261 100644
--- a/src/g_input.h
+++ b/src/g_input.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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
@@ -116,9 +116,29 @@ extern consvar_t cv_mousesens, cv_mouseysens;
 extern consvar_t cv_mousesens2, cv_mouseysens2;
 extern consvar_t cv_controlperkey;
 
-extern INT32 mousex, mousey;
-extern INT32 mlooky; //mousey with mlookSensitivity
-extern INT32 mouse2x, mouse2y, mlook2y;
+typedef struct
+{
+	INT32 dx; // deltas with mousemove sensitivity
+	INT32 dy;
+	INT32 mlookdy; // dy with mouselook sensitivity
+	INT32 rdx; // deltas without sensitivity
+	INT32 rdy;
+	UINT16 buttons;
+} mouse_t;
+
+#define MB_BUTTON1    0x0001
+#define MB_BUTTON2    0x0002
+#define MB_BUTTON3    0x0004
+#define MB_BUTTON4    0x0008
+#define MB_BUTTON5    0x0010
+#define MB_BUTTON6    0x0020
+#define MB_BUTTON7    0x0040
+#define MB_BUTTON8    0x0080
+#define MB_SCROLLUP   0x0100
+#define MB_SCROLLDOWN 0x0200
+
+extern mouse_t mouse;
+extern mouse_t mouse2;
 
 extern INT32 joyxmove[JOYAXISSET], joyymove[JOYAXISSET], joy2xmove[JOYAXISSET], joy2ymove[JOYAXISSET];
 
@@ -126,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))
@@ -161,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);
@@ -175,4 +195,7 @@ void G_CopyControls(INT32 (*setupcontrols)[2], INT32 (*fromcontrols)[2], const I
 void G_SaveKeySetting(FILE *f, INT32 (*fromcontrols)[2], INT32 (*fromcontrolsbis)[2]);
 INT32 G_CheckDoubleUsage(INT32 keynum, boolean modify);
 
+// sets the members of a mouse_t given position deltas
+void G_SetMouseDeltas(INT32 dx, INT32 dy, UINT8 ssplayer);
+
 #endif
diff --git a/src/g_state.h b/src/g_state.h
index e364c5a35b62c323464783d518bf266b2abe4185..589dc6361705747ad0da81fddf79cbf327849228 100644
--- a/src/g_state.h
+++ b/src/g_state.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/hardware/CMakeLists.txt b/src/hardware/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4e9c67d2f348a8bfed899e4002d25136284b031f
--- /dev/null
+++ b/src/hardware/CMakeLists.txt
@@ -0,0 +1 @@
+target_sourcefile(c)
diff --git a/src/hardware/Sourcefile b/src/hardware/Sourcefile
new file mode 100644
index 0000000000000000000000000000000000000000..1c05de76cca6d71251023e3e9e7bdde7d8cffaab
--- /dev/null
+++ b/src/hardware/Sourcefile
@@ -0,0 +1,13 @@
+hw_bsp.c
+hw_draw.c
+hw_light.c
+hw_main.c
+hw_clip.c
+hw_md2.c
+hw_cache.c
+hw_md2load.c
+hw_md3load.c
+hw_model.c
+u_list.c
+hw_batching.c
+r_opengl/r_opengl.c
diff --git a/src/hardware/hw_batching.c b/src/hardware/hw_batching.c
index 5ea9f55d4c9ba88cc36ffa6ba2e44f72ce79ff60..da0319bccfecbd70901fe05b1658a49c54c24998 100644
--- a/src/hardware/hw_batching.c
+++ b/src/hardware/hw_batching.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020 by Sonic Team Junior.
+// Copyright (C) 2020-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -137,6 +137,8 @@ static int comparePolygons(const void *p1, const void *p2)
 	PolygonArrayEntry* poly2 = &polygonArray[index2];
 	int diff;
 	INT64 diff64;
+	UINT32 downloaded1 = 0;
+	UINT32 downloaded2 = 0;
 
 	int shader1 = poly1->shader;
 	int shader2 = poly2->shader;
@@ -152,7 +154,11 @@ static int comparePolygons(const void *p1, const void *p2)
 	if (shader1 == -1 && shader2 == -1)
 		return index1 - index2;
 
-	diff64 = poly1->texture - poly2->texture;
+	if (poly1->texture)
+		downloaded1 = poly1->texture->downloaded; // there should be a opengl texture name here, usable for comparisons
+	if (poly2->texture)
+		downloaded2 = poly2->texture->downloaded;
+	diff64 = downloaded1 - downloaded2;
 	if (diff64 != 0) return diff64;
 
 	diff = poly1->polyFlags - poly2->polyFlags;
@@ -184,16 +190,21 @@ static int comparePolygonsNoShaders(const void *p1, const void *p2)
 
 	GLMipmap_t *texture1 = poly1->texture;
 	GLMipmap_t *texture2 = poly2->texture;
+	UINT32 downloaded1 = 0;
+	UINT32 downloaded2 = 0;
 	if (poly1->polyFlags & PF_NoTexture || poly1->horizonSpecial)
 		texture1 = NULL;
 	if (poly2->polyFlags & PF_NoTexture || poly2->horizonSpecial)
 		texture2 = NULL;
-	diff64 = texture1 - texture2;
-	if (diff64 != 0) return diff64;
-
+	if (texture1)
+		downloaded1 = texture1->downloaded; // there should be a opengl texture name here, usable for comparisons
+	if (texture2)
+		downloaded2 = texture2->downloaded;
 	// skywalls and horizon lines must retain their order for horizon lines to work
-	if (texture1 == NULL && texture2 == NULL)
+	if (!texture1 && !texture2)
 		return index1 - index2;
+	diff64 = downloaded1 - downloaded2;
+	if (diff64 != 0) return diff64;
 
 	diff = poly1->polyFlags - poly2->polyFlags;
 	if (diff != 0) return diff;
@@ -234,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++)
 	{
@@ -248,12 +262,12 @@ void HWR_RenderBatches(void)
 	}
 
 	// sort polygons
-	ps_hw_batchsorttime = I_GetTimeMicros();
+	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_GetTimeMicros() - ps_hw_batchsorttime;
+	PS_STOP_TIMING(ps_hw_batchsorttime);
 	// sort order
 	// 1. shader
 	// 2. texture
@@ -261,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_GetTimeMicros();
+	PS_START_TIMING(ps_hw_batchdrawtime);
 
 	currentShader = polygonArray[polygonIndexArray[0]].shader;
 	currentTexture = polygonArray[polygonIndexArray[0]].texture;
@@ -397,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;
@@ -415,7 +429,7 @@ void HWR_RenderBatches(void)
 			currentShader = nextShader;
 			changeShader = false;
 
-			ps_hw_numshaders++;
+			ps_hw_numshaders.value.i++;
 		}
 		if (changeTexture)
 		{
@@ -424,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?
 	}
@@ -446,7 +460,7 @@ void HWR_RenderBatches(void)
 	polygonArraySize = 0;
 	unsortedVertexArraySize = 0;
 
-	ps_hw_batchdrawtime = I_GetTimeMicros() - ps_hw_batchdrawtime;
+	PS_STOP_TIMING(ps_hw_batchdrawtime);
 }
 
 
diff --git a/src/hardware/hw_batching.h b/src/hardware/hw_batching.h
index 3d22324ac8aaef8baf7bae73ccd89e92dcd78385..9ccc7de3df503a12211b41d871c2734af70b0bf6 100644
--- a/src/hardware/hw_batching.h
+++ b/src/hardware/hw_batching.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020 by Sonic Team Junior.
+// Copyright (C) 2020-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -16,7 +16,7 @@
 #include "hw_data.h"
 #include "hw_drv.h"
 
-typedef struct 
+typedef struct
 {
 	FSurfaceInfo surf;// surf also has its own polyflags for some reason, but it seems unused
 	unsigned int vertsIndex;// location of verts in unsortedVertexArray
diff --git a/src/hardware/hw_cache.c b/src/hardware/hw_cache.c
index b4fa7ec6c1d8be984b7f0f86234b42980c6f3b7b..317efd320e749fb4c60e2d5ab36a044d3543a699 100644
--- a/src/hardware/hw_cache.c
+++ b/src/hardware/hw_cache.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -108,7 +108,7 @@ static void HWR_DrawColumnInCache(const column_t *patchcol, UINT8 *block, GLMipm
 
 			//Hurdler: 25/04/2000: now support colormap in hardware mode
 			if (mipmap->colormap)
-				texel = mipmap->colormap[texel];
+				texel = mipmap->colormap->data[texel];
 
 			// hope compiler will get this switch out of the loops (dreams...)
 			// gcc do it ! but vcc not ! (why don't use cygwin gcc for win32 ?)
@@ -218,7 +218,7 @@ static void HWR_DrawFlippedColumnInCache(const column_t *patchcol, UINT8 *block,
 
 			//Hurdler: 25/04/2000: now support colormap in hardware mode
 			if (mipmap->colormap)
-				texel = mipmap->colormap[texel];
+				texel = mipmap->colormap->data[texel];
 
 			// hope compiler will get this switch out of the loops (dreams...)
 			// gcc do it ! but vcc not ! (why don't use cygwin gcc for win32 ?)
@@ -659,7 +659,10 @@ void HWR_FreeTextureColormaps(patch_t *patch)
 		// Free image data from memory.
 		if (next->data)
 			Z_Free(next->data);
+		if (next->colormap)
+			Z_Free(next->colormap);
 		next->data = NULL;
+		next->colormap = NULL;
 		HWD.pfnDeleteTexture(next);
 
 		// Free the old colormap mipmap from memory.
@@ -667,16 +670,29 @@ void HWR_FreeTextureColormaps(patch_t *patch)
 	}
 }
 
+static boolean FreeTextureCallback(void *mem)
+{
+	patch_t *patch = (patch_t *)mem;
+	HWR_FreeTexture(patch);
+	return false;
+}
+
+static boolean FreeColormapsCallback(void *mem)
+{
+	patch_t *patch = (patch_t *)mem;
+	HWR_FreeTextureColormaps(patch);
+	return false;
+}
+
 static void HWR_FreePatchCache(boolean freeall)
 {
-	INT32 i;
+	boolean (*callback)(void *mem) = FreeTextureCallback;
 
-	for (i = 0; i < numwadfiles; i++)
-	{
-		INT32 j = 0;
-		for (; j < wadfiles[i]->numlumps; j++)
-			(freeall ? HWR_FreeTexture : HWR_FreeTextureColormaps)(wadfiles[i]->patchcache[j]);
-	}
+	if (!freeall)
+		callback = FreeColormapsCallback;
+
+	Z_IterateTags(PU_PATCH, PU_PATCH_ROTATED, callback);
+	Z_IterateTags(PU_SPRITE, PU_HUDGFX, callback);
 }
 
 // free all textures after each level
@@ -850,7 +866,7 @@ static void HWR_CacheTextureAsFlat(GLMipmap_t *grMipmap, INT32 texturenum)
 }
 
 // Download a Doom 'flat' to the hardware cache and make it ready for use
-void HWR_LiterallyGetFlat(lumpnum_t flatlumpnum)
+void HWR_GetRawFlat(lumpnum_t flatlumpnum)
 {
 	GLMipmap_t *grmip;
 	patch_t *patch;
@@ -879,7 +895,7 @@ void HWR_GetLevelFlat(levelflat_t *levelflat)
 		return;
 
 	if (levelflat->type == LEVELFLAT_FLAT)
-		HWR_LiterallyGetFlat(levelflat->u.flat.lumpnum);
+		HWR_GetRawFlat(levelflat->u.flat.lumpnum);
 	else if (levelflat->type == LEVELFLAT_TEXTURE)
 	{
 		GLMapTexture_t *grtex;
@@ -918,15 +934,17 @@ void HWR_GetLevelFlat(levelflat_t *levelflat)
 #ifndef NO_PNG_LUMPS
 	else if (levelflat->type == LEVELFLAT_PNG)
 	{
-		INT32 pngwidth = 0, pngheight = 0;
 		GLMipmap_t *mipmap = levelflat->mipmap;
-		UINT8 *flat;
-		size_t size;
 
 		// Cache the picture.
-		if (!levelflat->picture)
+		if (!levelflat->mippic)
 		{
-			levelflat->picture = Picture_PNGConvert(W_CacheLumpNum(levelflat->u.flat.lumpnum, PU_CACHE), PICFMT_FLAT, &pngwidth, &pngheight, NULL, NULL, W_LumpLength(levelflat->u.flat.lumpnum), NULL, 0);
+			INT32 pngwidth = 0, pngheight = 0;
+			void *pic = Picture_PNGConvert(W_CacheLumpNum(levelflat->u.flat.lumpnum, PU_CACHE), PICFMT_FLAT, &pngwidth, &pngheight, NULL, NULL, W_LumpLength(levelflat->u.flat.lumpnum), NULL, 0);
+
+			Z_ChangeTag(pic, PU_LEVEL);
+			Z_SetUser(pic, &levelflat->mippic);
+
 			levelflat->width = (UINT16)pngwidth;
 			levelflat->height = (UINT16)pngheight;
 		}
@@ -934,7 +952,7 @@ void HWR_GetLevelFlat(levelflat_t *levelflat)
 		// Make the mipmap.
 		if (mipmap == NULL)
 		{
-			mipmap = Z_Calloc(sizeof(GLMipmap_t), PU_LEVEL, NULL);
+			mipmap = Z_Calloc(sizeof(GLMipmap_t), PU_STATIC, NULL);
 			mipmap->format = GL_TEXFMT_P_8;
 			mipmap->flags = TF_WRAPXY|TF_CHROMAKEYED;
 			levelflat->mipmap = mipmap;
@@ -942,17 +960,22 @@ void HWR_GetLevelFlat(levelflat_t *levelflat)
 
 		if (!mipmap->data && !mipmap->downloaded)
 		{
+			UINT8 *flat;
+			size_t size;
+
+			if (levelflat->mippic == NULL)
+				I_Error("HWR_GetLevelFlat: levelflat->mippic == NULL");
+
 			mipmap->width = levelflat->width;
 			mipmap->height = levelflat->height;
+
 			size = (mipmap->width * mipmap->height);
 			flat = Z_Malloc(size, PU_LEVEL, &mipmap->data);
-			if (levelflat->picture == NULL)
-				I_Error("HWR_GetLevelFlat: levelflat->picture == NULL");
-			M_Memcpy(flat, levelflat->picture, size);
+			M_Memcpy(flat, levelflat->mippic, size);
 		}
 
 		// Tell the hardware driver to bind the current texture to the flat's mipmap
-		HWD.pfnSetTexture(mipmap);
+		HWR_SetCurrentTexture(mipmap);
 	}
 #endif
 	else // set no texture
@@ -977,8 +1000,28 @@ static void HWR_LoadPatchMipmap(patch_t *patch, GLMipmap_t *grMipmap)
 	Z_ChangeTag(grMipmap->data, PU_HWRCACHE_UNLOCKED);
 }
 
+// ----------------------+
+// HWR_UpdatePatchMipmap : Updates a mipmap.
+// ----------------------+
+static void HWR_UpdatePatchMipmap(patch_t *patch, GLMipmap_t *grMipmap)
+{
+	GLPatch_t *grPatch = patch->hardware;
+	HWR_MakePatch(patch, grPatch, grMipmap, true);
+
+	// If hardware does not have the texture, then call pfnSetTexture to upload it
+	// If it does have the texture, then call pfnUpdateTexture to update it
+	if (!grMipmap->downloaded)
+		HWD.pfnSetTexture(grMipmap);
+	else
+		HWD.pfnUpdateTexture(grMipmap);
+	HWR_SetCurrentTexture(grMipmap);
+
+	// The system-memory data can be purged now.
+	Z_ChangeTag(grMipmap->data, PU_HWRCACHE_UNLOCKED);
+}
+
 // -----------------+
-// HWR_GetPatch     : Download a patch to the hardware cache and make it ready for use
+// HWR_GetPatch     : Downloads a patch to the hardware cache and make it ready for use
 // -----------------+
 void HWR_GetPatch(patch_t *patch)
 {
@@ -1006,14 +1049,20 @@ void HWR_GetMappedPatch(patch_t *patch, const UINT8 *colormap)
 		return;
 	}
 
-	// search for the mimmap
+	// search for the mipmap
 	// skip the first (no colormap translated)
 	for (grMipmap = grPatch->mipmap; grMipmap->nextcolormap; )
 	{
 		grMipmap = grMipmap->nextcolormap;
-		if (grMipmap->colormap == colormap)
+		if (grMipmap->colormap && grMipmap->colormap->source == colormap)
 		{
-			HWR_LoadPatchMipmap(patch, grMipmap);
+			if (memcmp(grMipmap->colormap->data, colormap, 256 * sizeof(UINT8)))
+			{
+				M_Memcpy(grMipmap->colormap->data, colormap, 256 * sizeof(UINT8));
+				HWR_UpdatePatchMipmap(patch, grMipmap);
+			}
+			else
+				HWR_LoadPatchMipmap(patch, grMipmap);
 			return;
 		}
 	}
@@ -1029,7 +1078,10 @@ void HWR_GetMappedPatch(patch_t *patch, const UINT8 *colormap)
 		I_Error("%s: Out of memory", "HWR_GetMappedPatch");
 	grMipmap->nextcolormap = newMipmap;
 
-	newMipmap->colormap = colormap;
+	newMipmap->colormap = Z_Calloc(sizeof(*newMipmap->colormap), PU_HWRPATCHCOLMIPMAP, NULL);
+	newMipmap->colormap->source = colormap;
+	M_Memcpy(newMipmap->colormap->data, colormap, 256 * sizeof(UINT8));
+
 	HWR_LoadPatchMipmap(patch, newMipmap);
 }
 
@@ -1039,7 +1091,6 @@ void HWR_UnlockCachedPatch(GLPatch_t *gpatch)
 		return;
 
 	Z_ChangeTag(gpatch->mipmap->data, PU_HWRCACHE_UNLOCKED);
-	Z_ChangeTag(gpatch, PU_HWRPATCHINFO_UNLOCKED);
 }
 
 static const INT32 picmode2GR[] =
diff --git a/src/hardware/hw_data.h b/src/hardware/hw_data.h
index 3ae4ef8bc2145430846a728523e5d85dc577dd8e..5aba6a2a9b14e27d98fa7f57c6847826c8e791eb 100644
--- a/src/hardware/hw_data.h
+++ b/src/hardware/hw_data.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -39,45 +39,53 @@ typedef enum GLTextureFormat_e
 	GL_TEXFMT_ALPHA_INTENSITY_88  = 0x22,
 } GLTextureFormat_t;
 
-// data holds the address of the graphics data cached in heap memory
-//                NULL if the texture is not in Doom heap cache.
+// Colormap structure for mipmaps.
+struct GLColormap_s
+{
+	const UINT8 *source;
+	UINT8 data[256];
+};
+typedef struct GLColormap_s GLColormap_t;
+
+
+// Texture information (misleadingly named "mipmap" all over the code.)
+// The *data pointer holds the address of the graphics data cached in heap memory.
+// NULL if the texture is not in SRB2's heap cache.
 struct GLMipmap_s
 {
-	// for TexDownloadMipMap
+	// for UpdateTexture
 	GLTextureFormat_t     format;
 	void                 *data;
 
 	UINT32                flags;
 	UINT16                height;
 	UINT16                width;
-	UINT32                downloaded;     // The GPU has this texture.
+	UINT32                downloaded; // The GPU has this texture.
 
 	struct GLMipmap_s    *nextcolormap;
-	const UINT8          *colormap;
-
-	struct GLMipmap_s    *nextmipmap; // Linked list of all textures
+	struct GLColormap_s  *colormap;
 };
 typedef struct GLMipmap_s GLMipmap_t;
 
 
 //
-// Doom texture info, as cached for hardware rendering
+// Level textures, as cached for hardware rendering.
 //
 struct GLMapTexture_s
 {
 	GLMipmap_t  mipmap;
-	float       scaleX;             //used for scaling textures on walls
+	float       scaleX; // Used for scaling textures on walls
 	float       scaleY;
 };
 typedef struct GLMapTexture_s GLMapTexture_t;
 
 
-// a cached patch as converted to hardware format
+// Patch information for the hardware renderer.
 struct GLPatch_s
 {
-	float               max_s,max_t;
-	GLMipmap_t          *mipmap;
-} ATTRPACK;
+	GLMipmap_t *mipmap; // Texture data. Allocated whenever the patch is.
+	float       max_s, max_t;
+};
 typedef struct GLPatch_s GLPatch_t;
 
 #endif //_HWR_DATA_
diff --git a/src/hardware/hw_defs.h b/src/hardware/hw_defs.h
index a782762a38c46dbb4161468b43b3041d215e8d2e..8df9b8916b2e563d7c1eebb16511b222a636a312 100644
--- a/src/hardware/hw_defs.h
+++ b/src/hardware/hw_defs.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -216,28 +216,28 @@ enum EPolyFlags
 	PF_Masked           = 0x00000001,   // Poly is alpha scaled and 0 alpha pixels are discarded (holes in texture)
 	PF_Translucent      = 0x00000002,   // Poly is transparent, alpha = level of transparency
 	PF_Environment      = 0x00000004,   // Poly should be drawn environment mapped. (Hurdler: used for text drawing)
-	PF_Additive         = 0x00000008,   // Additive color blending
-	PF_AdditiveSource   = 0x00000010,   // Source blending factor is additive. This is the opposite of regular additive blending.
-	PF_Subtractive      = 0x00000020,   // Subtractive color blending
-	PF_ReverseSubtract  = 0x00000040,   // Reverse subtract, used in wall splats (decals)
-	PF_Multiplicative   = 0x00000080,   // Multiplicative color blending
+	PF_Additive         = 0x00000008,   // Source blending factor is additive.
+	PF_Subtractive      = 0x00000010,   // Subtractive color blending
+	PF_ReverseSubtract  = 0x00000020,   // Reverse subtract, used in wall splats (decals)
+	PF_Multiplicative   = 0x00000040,   // Multiplicative color blending
 	PF_Fog              = 0x20000000,   // Fog blocks
 	PF_NoAlphaTest      = 0x40000000,   // Disables alpha testing
-	PF_Blending         = (PF_Masked|PF_Translucent|PF_Environment|PF_Additive|PF_AdditiveSource|PF_Subtractive|PF_ReverseSubtract|PF_Multiplicative|PF_Fog) & ~PF_NoAlphaTest,
+	PF_Blending         = (PF_Masked|PF_Translucent|PF_Environment|PF_Additive|PF_Subtractive|PF_ReverseSubtract|PF_Multiplicative|PF_Fog) & ~PF_NoAlphaTest,
 
 	// other flag bits
 	PF_Occlude          = 0x00000100,   // Updates the depth buffer
 	PF_NoDepthTest      = 0x00000200,   // Disables the depth test mode
 	PF_Invisible        = 0x00000400,   // Disables write to color buffer
 	PF_Decal            = 0x00000800,   // Enables polygon offset
-	PF_Modulated        = 0x00001000,   // Modulation (multiply output with constant ARGB)
+	PF_Modulated        = 0x00001000,   // Modulation (multiply output with constant RGBA)
 	                                    // When set, pass the color constant into the FSurfaceInfo -> PolyColor
 	PF_NoTexture        = 0x00002000,   // Disables texturing
 	PF_Corona           = 0x00004000,   // Tells the renderer we are drawing a corona
-	PF_Ripple           = 0x00008000,   // Water effect shader
+	PF_ColorMapped      = 0x00008000,   // Surface has "tint" and "fade" colors, which are sent as uniforms to a shader.
 	PF_RemoveYWrap      = 0x00010000,   // Forces clamp texture on Y
 	PF_ForceWrapX       = 0x00020000,   // Forces repeat texture on X
-	PF_ForceWrapY       = 0x00040000    // Forces repeat texture on Y
+	PF_ForceWrapY       = 0x00040000,   // Forces repeat texture on Y
+	PF_Ripple           = 0x00100000    // Water ripple effect. The current backend doesn't use it for anything.
 };
 
 
@@ -255,9 +255,17 @@ enum ETextureFlags
 	TF_TRANSPARENT = 0x00000040,        // texture with some alpha == 0
 };
 
-typedef struct GLMipmap_s FTextureInfo;
+struct FTextureInfo
+{
+	UINT32 width, height;
+	UINT32 downloaded;
+	UINT32 format;
+
+	struct GLMipmap_s *texture;
+	struct FTextureInfo *prev, *next;
+};
+typedef struct FTextureInfo FTextureInfo;
 
-// jimita 14032019
 struct FLightInfo
 {
 	FUINT			light_level;
@@ -273,7 +281,7 @@ struct FSurfaceInfo
 	RGBA_t			PolyColor;
 	RGBA_t			TintColor;
 	RGBA_t			FadeColor;
-	FLightInfo		LightInfo;	// jimita 14032019
+	FLightInfo		LightInfo;
 };
 typedef struct FSurfaceInfo FSurfaceInfo;
 
diff --git a/src/hardware/hw_draw.c b/src/hardware/hw_draw.c
index b9cb288e910b1263dfa46e48b792c811de9d9beb..8223705bd1afa4a30e6d1c1239693fcab98d8374 100644
--- a/src/hardware/hw_draw.c
+++ b/src/hardware/hw_draw.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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))
 	{
@@ -437,18 +511,9 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 
 		if (!(option & V_SCALEPATCHMASK))
 		{
-			// if it's meant to cover the whole screen, black out the rest (ONLY IF TOP LEFT ISN'T TRANSPARENT)
-			// cx and cy are possibly *slightly* off from float maths
-			// This is done before here compared to software because we directly alter cx and cy to centre
-			if (cx >= -0.1f && cx <= 0.1f && gpatch->width == BASEVIDWIDTH && cy >= -0.1f && cy <= 0.1f && gpatch->height == BASEVIDHEIGHT)
-			{
-				const column_t *column = (const column_t *)((const UINT8 *)(gpatch->columns) + (gpatch->columnofs[0]));
-				if (!column->topdelta)
-				{
-					const UINT8 *source = (const UINT8 *)(column) + 3;
-					HWR_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, (column->topdelta == 0xff ? 31 : source[0]));
-				}
-			}
+			// if it's meant to cover the whole screen, black out the rest
+			// no the patch is cropped do not do this ever
+
 			// centre screen
 			if (fabsf((float)vid.width - (float)BASEVIDWIDTH * dupx) > 1.0E-36f)
 			{
@@ -456,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)
 			{
@@ -463,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 (fwidth > gpatch->width)
-		fwidth = gpatch->width;
+	if (sx + w > gpatch->width<<FRACBITS)
+		fwidth = FIXED_TO_FLOAT((gpatch->width<<FRACBITS) - sx);
 
-	if (fheight > gpatch->height)
-		fheight = gpatch->height;
+	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
 	{
@@ -504,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;
 
@@ -523,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)
 	{
@@ -639,7 +782,7 @@ void HWR_DrawFlatFill (INT32 x, INT32 y, INT32 w, INT32 h, lumpnum_t flatlumpnum
 	v[0].t = v[1].t = (float)((y & flatflag)/dflatsize);
 	v[2].t = v[3].t = (float)(v[0].t + h/dflatsize);
 
-	HWR_LiterallyGetFlat(flatlumpnum);
+	HWR_GetRawFlat(flatlumpnum);
 
 	//Hurdler: Boris, the same comment as above... but maybe for pics
 	// it not a problem since they don't have any transparent pixel
diff --git a/src/hardware/hw_drv.h b/src/hardware/hw_drv.h
index 5a2e0e44eaeb13ffc0465403fdd606e2f556fe2e..d4a586d418de9f3da9a16d432690608b99961c8f 100644
--- a/src/hardware/hw_drv.h
+++ b/src/hardware/hw_drv.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -40,13 +40,12 @@ EXPORT void HWRAPI(DrawIndexedTriangles) (FSurfaceInfo *pSurf, FOutVector *pOutV
 EXPORT void HWRAPI(RenderSkyDome) (gl_sky_t *sky);
 EXPORT void HWRAPI(SetBlend) (FBITFIELD PolyFlags);
 EXPORT void HWRAPI(ClearBuffer) (FBOOLEAN ColorMask, FBOOLEAN DepthMask, FRGBAFloat *ClearColor);
-EXPORT void HWRAPI(SetTexture) (FTextureInfo *TexInfo);
-EXPORT void HWRAPI(UpdateTexture) (FTextureInfo *TexInfo);
-EXPORT void HWRAPI(DeleteTexture) (FTextureInfo *TexInfo);
+EXPORT void HWRAPI(SetTexture) (GLMipmap_t *TexInfo);
+EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *TexInfo);
+EXPORT void HWRAPI(DeleteTexture) (GLMipmap_t *TexInfo);
 EXPORT void HWRAPI(ReadRect) (INT32 x, INT32 y, INT32 width, INT32 height, INT32 dst_stride, UINT16 *dst_data);
 EXPORT void HWRAPI(GClipRect) (INT32 minx, INT32 miny, INT32 maxx, INT32 maxy, float nearclip);
 EXPORT void HWRAPI(ClearMipMapCache) (void);
-EXPORT void HWRAPI(ClearCacheList) (void);
 
 //Hurdler: added for backward compatibility
 EXPORT void HWRAPI(SetSpecialState) (hwdspecialstate_t IdState, INT32 Value);
@@ -69,7 +68,6 @@ EXPORT void HWRAPI(DrawScreenFinalTexture) (int width, int height);
 #define SCREENVERTS 10
 EXPORT void HWRAPI(PostImgRedraw) (float points[SCREENVERTS][SCREENVERTS][2]);
 
-// jimita
 EXPORT boolean HWRAPI(CompileShaders) (void);
 EXPORT void HWRAPI(CleanShaders) (void);
 EXPORT void HWRAPI(SetShader) (int type);
@@ -101,7 +99,6 @@ struct hwdriver_s
 	ReadRect            pfnReadRect;
 	GClipRect           pfnGClipRect;
 	ClearMipMapCache    pfnClearMipMapCache;
-	ClearCacheList      pfnClearCacheList;
 	SetSpecialState     pfnSetSpecialState;//Hurdler: added for backward compatibility
 	DrawModel           pfnDrawModel;
 	CreateModelVBOs     pfnCreateModelVBOs;
diff --git a/src/hardware/hw_glob.h b/src/hardware/hw_glob.h
index 87405d3d457080e1fccd86206ac2d0dfb9b97db4..37d77b467306b823cd36a9a1bdcb57500723cc2e 100644
--- a/src/hardware/hw_glob.h
+++ b/src/hardware/hw_glob.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -118,7 +118,7 @@ patch_t *HWR_GetPic(lumpnum_t lumpnum);
 
 GLMapTexture_t *HWR_GetTexture(INT32 tex);
 void HWR_GetLevelFlat(levelflat_t *levelflat);
-void HWR_LiterallyGetFlat(lumpnum_t flatlumpnum);
+void HWR_GetRawFlat(lumpnum_t flatlumpnum);
 
 void HWR_FreeTexture(patch_t *patch);
 void HWR_FreeTextureData(patch_t *patch);
diff --git a/src/hardware/hw_light.c b/src/hardware/hw_light.c
index 987d70c69e22b293bb8d07cce5ce10520b5d387e..e83d9a6ec025143150754ab6910afed140b4fdcd 100644
--- a/src/hardware/hw_light.c
+++ b/src/hardware/hw_light.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -35,7 +35,7 @@
 
 #define DL_HIGH_QUALITY
 //#define STATICLIGHT  //Hurdler: TODO!
-#define LIGHTMAPFLAGS (PF_Modulated|PF_AdditiveSource)
+#define LIGHTMAPFLAGS (PF_Modulated|PF_Additive)
 
 #ifdef ALAM_LIGHTING
 static dynlights_t view_dynlights[2]; // 2 players in splitscreen mode
@@ -253,6 +253,7 @@ light_t *t_lspr[NUMSPRITES] =
 	&lspr[NOLIGHT],     // SPR_SIGN
 	&lspr[NOLIGHT],     // SPR_SPIK
 	&lspr[NOLIGHT],     // SPR_SFLM
+	&lspr[NOLIGHT],     // SPR_TFLM
 	&lspr[NOLIGHT],     // SPR_USPK
 	&lspr[NOLIGHT],     // SPR_WSPK
 	&lspr[NOLIGHT],     // SPR_WSPB
@@ -1055,7 +1056,7 @@ void HWR_DoCoronasLighting(FOutVector *outVerts, gl_vissprite_t *spr)
 
 		HWR_GetPic(coronalumpnum);  /// \todo use different coronas
 
-		HWD.pfnDrawPolygon (&Surf, light, 4, PF_Modulated | PF_AdditiveSource | PF_Corona | PF_NoDepthTest);
+		HWD.pfnDrawPolygon (&Surf, light, 4, PF_Modulated | PF_Additive | PF_Corona | PF_NoDepthTest);
 	}
 }
 #endif
@@ -1143,7 +1144,7 @@ void HWR_DrawCoronas(void)
 		light[3].y = cy+size*1.33f;
 		light[3].s = 0.0f;   light[3].t = 1.0f;
 
-		HWD.pfnDrawPolygon (&Surf, light, 4, PF_Modulated | PF_AdditiveSource | PF_NoDepthTest | PF_Corona);
+		HWD.pfnDrawPolygon (&Surf, light, 4, PF_Modulated | PF_Additive | PF_NoDepthTest | PF_Corona);
 	}
 }
 #endif
diff --git a/src/hardware/hw_light.h b/src/hardware/hw_light.h
index fed7db47f2a67e6b81f82bfe2e97048594ce43be..244cc921f567e63422868e64259b07f1cb5c0ac4 100644
--- a/src/hardware/hw_light.h
+++ b/src/hardware/hw_light.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index 5dd2727bcc71207ad006943ab7b22e96f5606585..9bade3d6fb19676cd988bc53fbce8ff8cd2a705e 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -147,22 +147,22 @@ static angle_t gl_aimingangle;
 static void HWR_SetTransformAiming(FTransform *trans, player_t *player, boolean skybox);
 
 // Render stats
-int ps_hw_skyboxtime = 0;
-int ps_hw_nodesorttime = 0;
-int ps_hw_nodedrawtime = 0;
-int ps_hw_spritesorttime = 0;
-int 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;
-int ps_hw_batchsorttime = 0;
-int 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;
@@ -173,6 +173,11 @@ boolean gl_shadersavailable = true;
 // Lighting
 // ==========================================================================
 
+static boolean HWR_UseShader(void)
+{
+	return (cv_glshaders.value && gl_shadersavailable);
+}
+
 void HWR_Lighting(FSurfaceInfo *Surface, INT32 light_level, extracolormap_t *colormap)
 {
 	RGBA_t poly_color, tint_color, fade_color;
@@ -182,7 +187,7 @@ void HWR_Lighting(FSurfaceInfo *Surface, INT32 light_level, extracolormap_t *col
 	fade_color.rgba = (colormap != NULL) ? (UINT32)colormap->fadergba : GL_DEFAULTFOG;
 
 	// Crappy backup coloring if you can't do shaders
-	if (!cv_glshaders.value || !gl_shadersavailable)
+	if (!HWR_UseShader())
 	{
 		// be careful, this may get negative for high lightlevel values.
 		float tint_alpha, fade_alpha;
@@ -362,16 +367,16 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 	float fflatwidth = 64.0f, fflatheight = 64.0f;
 	INT32 flatflag = 63;
 	boolean texflat = false;
-	float scrollx = 0.0f, scrolly = 0.0f;
+	float scrollx = 0.0f, scrolly = 0.0f, anglef = 0.0f;
 	angle_t angle = 0;
 	FSurfaceInfo    Surf;
-	fixed_t tempxsow, tempytow;
+	float tempxsow, tempytow;
 	pslope_t *slope = NULL;
 
 	static FOutVector *planeVerts = NULL;
 	static UINT16 numAllocedPlaneVerts = 0;
 
-	int shader;
+	INT32 shader = SHADER_DEFAULT;
 
 	// no convex poly were generated for this subsector
 	if (!xsub->planepoly)
@@ -499,24 +504,15 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 		}
 	}
 
-
 	if (angle) // Only needs to be done if there's an altered angle
 	{
+		tempxsow = flatxref;
+		tempytow = flatyref;
 
-		angle = (InvAngle(angle))>>ANGLETOFINESHIFT;
-
-		// This needs to be done so that it scrolls in a different direction after rotation like software
-		/*tempxsow = FLOAT_TO_FIXED(scrollx);
-		tempytow = FLOAT_TO_FIXED(scrolly);
-		scrollx = (FIXED_TO_FLOAT(FixedMul(tempxsow, FINECOSINE(angle)) - FixedMul(tempytow, FINESINE(angle))));
-		scrolly = (FIXED_TO_FLOAT(FixedMul(tempxsow, FINESINE(angle)) + FixedMul(tempytow, FINECOSINE(angle))));*/
+		anglef = ANG2RAD(InvAngle(angle));
 
-		// This needs to be done so everything aligns after rotation
-		// It would be done so that rotation is done, THEN the translation, but I couldn't get it to rotate AND scroll like software does
-		tempxsow = FLOAT_TO_FIXED(flatxref);
-		tempytow = FLOAT_TO_FIXED(flatyref);
-		flatxref = (FIXED_TO_FLOAT(FixedMul(tempxsow, FINECOSINE(angle)) - FixedMul(tempytow, FINESINE(angle))));
-		flatyref = (FIXED_TO_FLOAT(FixedMul(tempxsow, FINESINE(angle)) + FixedMul(tempytow, FINECOSINE(angle))));
+		flatxref = (tempxsow * cos(anglef)) - (tempytow * sin(anglef));
+		flatyref = (tempxsow * sin(anglef)) + (tempytow * cos(anglef));
 	}
 
 #define SETUP3DVERT(vert, vx, vy) {\
@@ -535,10 +531,10 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 		/* Need to rotate before translate */\
 		if (angle) /* Only needs to be done if there's an altered angle */\
 		{\
-			tempxsow = FLOAT_TO_FIXED(vert->s);\
-			tempytow = FLOAT_TO_FIXED(vert->t);\
-			vert->s = (FIXED_TO_FLOAT(FixedMul(tempxsow, FINECOSINE(angle)) - FixedMul(tempytow, FINESINE(angle))));\
-			vert->t = (FIXED_TO_FLOAT(FixedMul(tempxsow, FINESINE(angle)) + FixedMul(tempytow, FINECOSINE(angle))));\
+			tempxsow = vert->s;\
+			tempytow = vert->t;\
+			vert->s = (tempxsow * cos(anglef)) - (tempytow * sin(anglef));\
+			vert->t = (tempxsow * sin(anglef)) + (tempytow * cos(anglef));\
 		}\
 \
 		vert->x = (vx);\
@@ -568,12 +564,17 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 	else
 		PolyFlags |= PF_Masked|PF_Modulated;
 
-	if (PolyFlags & PF_Fog)
-		shader = SHADER_FOG;	// fog shader
-	else if (PolyFlags & PF_Ripple)
-		shader = SHADER_WATER;	// water shader
-	else
-		shader = SHADER_FLOOR;	// floor shader
+	if (HWR_UseShader())
+	{
+		if (PolyFlags & PF_Fog)
+			shader = SHADER_FOG;
+		else if (PolyFlags & PF_Ripple)
+			shader = SHADER_WATER;
+		else
+			shader = SHADER_FLOOR;
+
+		PolyFlags |= PF_ColorMapped;
+	}
 
 	HWR_ProcessPolygon(&Surf, planeVerts, nrPlaneVerts, PolyFlags, shader, false);
 
@@ -702,10 +703,12 @@ static void HWR_RenderSkyPlane(extrasubsector_t *xsub, fixed_t fixedheight)
 
 #endif //doplanes
 
-FBITFIELD HWR_GetBlendModeFlag(INT32 ast)
+FBITFIELD HWR_GetBlendModeFlag(INT32 style)
 {
-	switch (ast)
+	switch (style)
 	{
+		case AST_TRANSLUCENT:
+			return PF_Translucent;
 		case AST_ADD:
 			return PF_Additive;
 		case AST_SUBTRACT:
@@ -715,10 +718,8 @@ FBITFIELD HWR_GetBlendModeFlag(INT32 ast)
 		case AST_MODULATE:
 			return PF_Multiplicative;
 		default:
-			return PF_Translucent;
+			return PF_Masked;
 	}
-
-	return 0;
 }
 
 UINT8 HWR_GetTranstableAlpha(INT32 transtablenum)
@@ -744,7 +745,7 @@ UINT8 HWR_GetTranstableAlpha(INT32 transtablenum)
 
 FBITFIELD HWR_SurfaceBlend(INT32 style, INT32 transtablenum, FSurfaceInfo *pSurf)
 {
-	if (!transtablenum)
+	if (!transtablenum || style <= AST_COPY || style >= AST_OVERLAY)
 	{
 		pSurf->PolyColor.s.alpha = 0xff;
 		return PF_Masked;
@@ -785,8 +786,17 @@ static void HWR_AddTransparentWall(FOutVector *wallVerts, FSurfaceInfo *pSurf, I
 //
 static void HWR_ProjectWall(FOutVector *wallVerts, FSurfaceInfo *pSurf, FBITFIELD blendmode, INT32 lightlevel, extracolormap_t *wallcolormap)
 {
+	INT32 shader = SHADER_DEFAULT;
+
 	HWR_Lighting(pSurf, lightlevel, wallcolormap);
-	HWR_ProcessPolygon(pSurf, wallVerts, 4, blendmode|PF_Modulated|PF_Occlude, SHADER_WALL, false); // wall shader
+
+	if (HWR_UseShader())
+	{
+		shader = SHADER_WALL;
+		blendmode |= PF_ColorMapped;
+	}
+
+	HWR_ProcessPolygon(pSurf, wallVerts, 4, blendmode|PF_Modulated|PF_Occlude, shader, false);
 }
 
 // ==========================================================================
@@ -831,7 +841,7 @@ static float HWR_ClipViewSegment(INT32 x, polyvertex_t *v1, polyvertex_t *v2)
 //
 // HWR_SplitWall
 //
-static void HWR_SplitWall(sector_t *sector, FOutVector *wallVerts, INT32 texnum, FSurfaceInfo* Surf, INT32 cutflag, ffloor_t *pfloor)
+static void HWR_SplitWall(sector_t *sector, FOutVector *wallVerts, INT32 texnum, FSurfaceInfo* Surf, INT32 cutflag, ffloor_t *pfloor, FBITFIELD polyflags)
 {
 	/* SoM: split up and light walls according to the
 	 lightlist. This may also include leaving out parts
@@ -969,11 +979,11 @@ static void HWR_SplitWall(sector_t *sector, FOutVector *wallVerts, INT32 texnum,
 		wallVerts[1].y = endbot;
 
 		if (cutflag & FF_FOG)
-			HWR_AddTransparentWall(wallVerts, Surf, texnum, PF_Fog|PF_NoTexture, true, lightnum, colormap);
+			HWR_AddTransparentWall(wallVerts, Surf, texnum, PF_Fog|PF_NoTexture|polyflags, true, lightnum, colormap);
 		else if (cutflag & FF_TRANSLUCENT)
-			HWR_AddTransparentWall(wallVerts, Surf, texnum, PF_Translucent, false, lightnum, colormap);
+			HWR_AddTransparentWall(wallVerts, Surf, texnum, PF_Translucent|polyflags, false, lightnum, colormap);
 		else
-			HWR_ProjectWall(wallVerts, Surf, PF_Masked, lightnum, colormap);
+			HWR_ProjectWall(wallVerts, Surf, PF_Masked|polyflags, lightnum, colormap);
 
 		top = bot;
 		endtop = endbot;
@@ -998,11 +1008,11 @@ static void HWR_SplitWall(sector_t *sector, FOutVector *wallVerts, INT32 texnum,
 	wallVerts[1].y = endbot;
 
 	if (cutflag & FF_FOG)
-		HWR_AddTransparentWall(wallVerts, Surf, texnum, PF_Fog|PF_NoTexture, true, lightnum, colormap);
+		HWR_AddTransparentWall(wallVerts, Surf, texnum, PF_Fog|PF_NoTexture|polyflags, true, lightnum, colormap);
 	else if (cutflag & FF_TRANSLUCENT)
-		HWR_AddTransparentWall(wallVerts, Surf, texnum, PF_Translucent, false, lightnum, colormap);
+		HWR_AddTransparentWall(wallVerts, Surf, texnum, PF_Translucent|polyflags, false, lightnum, colormap);
 	else
-		HWR_ProjectWall(wallVerts, Surf, PF_Masked, lightnum, colormap);
+		HWR_ProjectWall(wallVerts, Surf, PF_Masked|polyflags, lightnum, colormap);
 }
 
 // HWR_DrawSkyWall
@@ -1104,7 +1114,6 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 
 		SLOPEPARAMS(gl_backsector->c_slope, worldhigh, worldhighslope, gl_backsector->ceilingheight)
 		SLOPEPARAMS(gl_backsector->f_slope, worldlow,  worldlowslope,  gl_backsector->floorheight)
-#undef SLOPEPARAMS
 
 		// hack to allow height changes in outdoor areas
 		// This is what gets rid of the upper textures if there should be sky
@@ -1183,7 +1192,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 			wallVerts[1].y = FIXED_TO_FLOAT(worldhighslope);
 
 			if (gl_frontsector->numlights)
-				HWR_SplitWall(gl_frontsector, wallVerts, gl_toptexture, &Surf, FF_CUTLEVEL, NULL);
+				HWR_SplitWall(gl_frontsector, wallVerts, gl_toptexture, &Surf, FF_CUTLEVEL, NULL, 0);
 			else if (grTex->mipmap.flags & TF_TRANSPARENT)
 				HWR_AddTransparentWall(wallVerts, &Surf, gl_toptexture, PF_Environment, false, lightnum, colormap);
 			else
@@ -1249,7 +1258,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 			wallVerts[1].y = FIXED_TO_FLOAT(worldbottomslope);
 
 			if (gl_frontsector->numlights)
-				HWR_SplitWall(gl_frontsector, wallVerts, gl_bottomtexture, &Surf, FF_CUTLEVEL, NULL);
+				HWR_SplitWall(gl_frontsector, wallVerts, gl_bottomtexture, &Surf, FF_CUTLEVEL, NULL, 0);
 			else if (grTex->mipmap.flags & TF_TRANSPARENT)
 				HWR_AddTransparentWall(wallVerts, &Surf, gl_bottomtexture, PF_Environment, false, lightnum, colormap);
 			else
@@ -1465,13 +1474,17 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 					blendmode = HWR_TranstableToAlpha(gl_curline->polyseg->translucency, &Surf);
 			}
 
+			// Render midtextures on two-sided lines with a z-buffer offset.
+			// This will cause the midtexture appear on top, if a FOF overlaps with it.
+			blendmode |= PF_Decal;
+
 			if (gl_frontsector->numlights)
 			{
 				if (!(blendmode & PF_Masked))
-					HWR_SplitWall(gl_frontsector, wallVerts, gl_midtexture, &Surf, FF_TRANSLUCENT, NULL);
+					HWR_SplitWall(gl_frontsector, wallVerts, gl_midtexture, &Surf, FF_TRANSLUCENT, NULL, PF_Decal);
 				else
 				{
-					HWR_SplitWall(gl_frontsector, wallVerts, gl_midtexture, &Surf, FF_CUTLEVEL, NULL);
+					HWR_SplitWall(gl_frontsector, wallVerts, gl_midtexture, &Surf, FF_CUTLEVEL, NULL, PF_Decal);
 				}
 			}
 			else if (!(blendmode & PF_Masked))
@@ -1554,7 +1567,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 
 			// I don't think that solid walls can use translucent linedef types...
 			if (gl_frontsector->numlights)
-				HWR_SplitWall(gl_frontsector, wallVerts, gl_midtexture, &Surf, FF_CUTLEVEL, NULL);
+				HWR_SplitWall(gl_frontsector, wallVerts, gl_midtexture, &Surf, FF_CUTLEVEL, NULL, 0);
 			else
 			{
 				if (grTex->mipmap.flags & TF_TRANSPARENT)
@@ -1589,14 +1602,18 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 	{
 		ffloor_t * rover;
 		fixed_t    highcut = 0, lowcut = 0;
+		fixed_t lowcutslope, highcutslope;
+
+		// Used for height comparisons and etc across FOFs and slopes
+		fixed_t high1, highslope1, low1, lowslope1;
 
 		INT32 texnum;
 		line_t * newline = NULL; // Multi-Property FOF
 
-        ///TODO add slope support (fixing cutoffs, proper wall clipping) - maybe just disable highcut/lowcut if either sector or FOF has a slope
-        ///     to allow fun plane intersecting in OGL? But then people would abuse that and make software look bad. :C
-		highcut = gl_frontsector->ceilingheight < gl_backsector->ceilingheight ? gl_frontsector->ceilingheight : gl_backsector->ceilingheight;
-		lowcut = gl_frontsector->floorheight > gl_backsector->floorheight ? gl_frontsector->floorheight : gl_backsector->floorheight;
+		lowcut = max(worldbottom, worldlow);
+		highcut = min(worldtop, worldhigh);
+		lowcutslope = max(worldbottomslope, worldlowslope);
+		highcutslope = min(worldtopslope, worldhighslope);
 
 		if (gl_backsector->ffloors)
 		{
@@ -1618,7 +1635,11 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 					continue;
 				if (!(rover->flags & FF_ALLSIDES) && rover->flags & FF_INVERTSIDES)
 					continue;
-				if (*rover->topheight < lowcut || *rover->bottomheight > highcut)
+
+				SLOPEPARAMS(*rover->t_slope, high1, highslope1, *rover->topheight)
+				SLOPEPARAMS(*rover->b_slope, low1,  lowslope1,  *rover->bottomheight)
+
+				if ((high1 < lowcut && highslope1 < lowcutslope) || (low1 > highcut && lowslope1 > highcutslope))
 					continue;
 
 				texnum = R_GetTextureNum(sides[rover->master->sidenum[0]].midtexture);
@@ -1634,10 +1655,17 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 				hS = P_GetFFloorTopZAt   (rover, v2x, v2y);
 				l  = P_GetFFloorBottomZAt(rover, v1x, v1y);
 				lS = P_GetFFloorBottomZAt(rover, v2x, v2y);
-				if (!(*rover->t_slope) && !gl_frontsector->c_slope && !gl_backsector->c_slope && h > highcut)
-					h = hS = highcut;
-				if (!(*rover->b_slope) && !gl_frontsector->f_slope && !gl_backsector->f_slope && l < lowcut)
-					l = lS = lowcut;
+				// Adjust the heights so the FOF does not overlap with top and bottom textures.
+				if (h >= highcut && hS >= highcutslope)
+				{
+					h = highcut;
+					hS = highcutslope;
+				}
+				if (l <= lowcut && lS <= lowcutslope)
+				{
+					l = lowcut;
+					lS = lowcutslope;
+				}
 				//Hurdler: HW code starts here
 				//FIXME: check if peging is correct
 				// set top/bottom coords
@@ -1717,7 +1745,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 					Surf.PolyColor.s.alpha = HWR_FogBlockAlpha(rover->master->frontsector->lightlevel, rover->master->frontsector->extra_colormap);
 
 					if (gl_frontsector->numlights)
-						HWR_SplitWall(gl_frontsector, wallVerts, 0, &Surf, rover->flags, rover);
+						HWR_SplitWall(gl_frontsector, wallVerts, 0, &Surf, rover->flags, rover, 0);
 					else
 						HWR_AddTransparentWall(wallVerts, &Surf, 0, blendmode, true, lightnum, colormap);
 				}
@@ -1732,7 +1760,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 					}
 
 					if (gl_frontsector->numlights)
-						HWR_SplitWall(gl_frontsector, wallVerts, texnum, &Surf, rover->flags, rover);
+						HWR_SplitWall(gl_frontsector, wallVerts, texnum, &Surf, rover->flags, rover, 0);
 					else
 					{
 						if (blendmode != PF_Masked)
@@ -1764,7 +1792,11 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 					continue;
 				if (!(rover->flags & FF_ALLSIDES || rover->flags & FF_INVERTSIDES))
 					continue;
-				if (*rover->topheight < lowcut || *rover->bottomheight > highcut)
+
+				SLOPEPARAMS(*rover->t_slope, high1, highslope1, *rover->topheight)
+				SLOPEPARAMS(*rover->b_slope, low1,  lowslope1,  *rover->bottomheight)
+
+				if ((high1 < lowcut && highslope1 < lowcutslope) || (low1 > highcut && lowslope1 > highcutslope))
 					continue;
 
 				texnum = R_GetTextureNum(sides[rover->master->sidenum[0]].midtexture);
@@ -1779,10 +1811,17 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 				hS = P_GetFFloorTopZAt   (rover, v2x, v2y);
 				l  = P_GetFFloorBottomZAt(rover, v1x, v1y);
 				lS = P_GetFFloorBottomZAt(rover, v2x, v2y);
-				if (!(*rover->t_slope) && !gl_frontsector->c_slope && !gl_backsector->c_slope && h > highcut)
-					h = hS = highcut;
-				if (!(*rover->b_slope) && !gl_frontsector->f_slope && !gl_backsector->f_slope && l < lowcut)
-					l = lS = lowcut;
+				// Adjust the heights so the FOF does not overlap with top and bottom textures.
+				if (h >= highcut && hS >= highcutslope)
+				{
+					h = highcut;
+					hS = highcutslope;
+				}
+				if (l <= lowcut && lS <= lowcutslope)
+				{
+					l = lowcut;
+					lS = lowcutslope;
+				}
 				//Hurdler: HW code starts here
 				//FIXME: check if peging is correct
 				// set top/bottom coords
@@ -1829,7 +1868,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 					Surf.PolyColor.s.alpha = HWR_FogBlockAlpha(rover->master->frontsector->lightlevel, rover->master->frontsector->extra_colormap);
 
 					if (gl_backsector->numlights)
-						HWR_SplitWall(gl_backsector, wallVerts, 0, &Surf, rover->flags, rover);
+						HWR_SplitWall(gl_backsector, wallVerts, 0, &Surf, rover->flags, rover, 0);
 					else
 						HWR_AddTransparentWall(wallVerts, &Surf, 0, blendmode, true, lightnum, colormap);
 				}
@@ -1844,7 +1883,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 					}
 
 					if (gl_backsector->numlights)
-						HWR_SplitWall(gl_backsector, wallVerts, texnum, &Surf, rover->flags, rover);
+						HWR_SplitWall(gl_backsector, wallVerts, texnum, &Surf, rover->flags, rover, 0);
 					else
 					{
 						if (blendmode != PF_Masked)
@@ -1856,6 +1895,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 			}
 		}
 	}
+#undef SLOPEPARAMS
 //Hurdler: end of 3d-floors test
 }
 
@@ -2659,30 +2699,30 @@ static void HWR_RenderPolyObjectPlane(polyobj_t *polysector, boolean isceiling,
 									FBITFIELD blendmode, UINT8 lightlevel, levelflat_t *levelflat, sector_t *FOFsector,
 									UINT8 alpha, extracolormap_t *planecolormap)
 {
-	float           height; //constant y for all points on the convex flat polygon
-	FOutVector      *v3d;
-	INT32             i;
-	float           flatxref,flatyref;
+	FSurfaceInfo Surf;
+	FOutVector *v3d;
+	INT32 shader = SHADER_DEFAULT;
+
+	size_t nrPlaneVerts = polysector->numVertices;
+	INT32 i;
+
+	float height = FIXED_TO_FLOAT(fixedheight); // constant y for all points on the convex flat polygon
+	float flatxref, flatyref;
 	float fflatwidth = 64.0f, fflatheight = 64.0f;
 	INT32 flatflag = 63;
+
 	boolean texflat = false;
+
 	float scrollx = 0.0f, scrolly = 0.0f;
 	angle_t angle = 0;
-	FSurfaceInfo    Surf;
 	fixed_t tempxs, tempyt;
-	size_t nrPlaneVerts;
 
 	static FOutVector *planeVerts = NULL;
 	static UINT16 numAllocedPlaneVerts = 0;
 
-	nrPlaneVerts = polysector->numVertices;
-
-	height = FIXED_TO_FLOAT(fixedheight);
-
-	if (nrPlaneVerts < 3)   //not even a triangle ?
+	if (nrPlaneVerts < 3)   // Not even a triangle?
 		return;
-
-	if (nrPlaneVerts > (size_t)UINT16_MAX) // FIXME: exceeds plVerts size
+	else if (nrPlaneVerts > (size_t)UINT16_MAX) // FIXME: exceeds plVerts size
 	{
 		CONS_Debug(DBG_RENDER, "polygon size of %s exceeds max value of %d vertices\n", sizeu1(nrPlaneVerts), UINT16_MAX);
 		return;
@@ -2834,7 +2874,6 @@ static void HWR_RenderPolyObjectPlane(polyobj_t *polysector, boolean isceiling,
 		v3d->z = FIXED_TO_FLOAT(polysector->vertices[i]->y);
 	}
 
-
 	HWR_Lighting(&Surf, lightlevel, planecolormap);
 
 	if (blendmode & PF_Translucent)
@@ -2845,7 +2884,13 @@ static void HWR_RenderPolyObjectPlane(polyobj_t *polysector, boolean isceiling,
 	else
 		blendmode |= PF_Masked|PF_Modulated;
 
-	HWR_ProcessPolygon(&Surf, planeVerts, nrPlaneVerts, blendmode, SHADER_FLOOR, false); // floor shader
+	if (HWR_UseShader())
+	{
+		shader = SHADER_FLOOR;
+		blendmode |= PF_ColorMapped;
+	}
+
+	HWR_ProcessPolygon(&Surf, planeVerts, nrPlaneVerts, blendmode, shader, false);
 }
 
 static void HWR_AddPolyObjectPlanes(void)
@@ -3190,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);
@@ -3298,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)
@@ -3566,6 +3611,8 @@ static void HWR_DrawDropShadow(mobj_t *thing, fixed_t scale)
 	FSurfaceInfo sSurf;
 	float fscale; float fx; float fy; float offset;
 	extracolormap_t *colormap = NULL;
+	FBITFIELD blendmode = PF_Translucent|PF_Modulated;
+	INT32 shader = SHADER_DEFAULT;
 	UINT8 i;
 	SINT8 flip = P_MobjFlip(thing);
 
@@ -3658,14 +3705,20 @@ static void HWR_DrawDropShadow(mobj_t *thing, fixed_t scale)
 	HWR_Lighting(&sSurf, 0, colormap);
 	sSurf.PolyColor.s.alpha = alpha;
 
-	HWR_ProcessPolygon(&sSurf, shadowVerts, 4, PF_Translucent|PF_Modulated, SHADER_SPRITE, false); // sprite shader
+	if (HWR_UseShader())
+	{
+		shader = SHADER_SPRITE;
+		blendmode |= PF_ColorMapped;
+	}
+
+	HWR_ProcessPolygon(&sSurf, shadowVerts, 4, blendmode, shader, false);
 }
 
 // This is expecting a pointer to an array containing 4 wallVerts for a sprite
 static void HWR_RotateSpritePolyToAim(gl_vissprite_t *spr, FOutVector *wallVerts, const boolean precip)
 {
 	if (cv_glspritebillboarding.value
-		&& spr && spr->mobj && !(spr->mobj->frame & FF_PAPERSPRITE)
+		&& spr && spr->mobj && !R_ThingIsPaperSprite(spr->mobj)
 		&& wallVerts)
 	{
 		float basey = FIXED_TO_FLOAT(spr->mobj->z);
@@ -3706,8 +3759,8 @@ static void HWR_SplitSprite(gl_vissprite_t *spr)
 	boolean lightset = true;
 	FBITFIELD blend = 0;
 	FBITFIELD occlusion;
+	INT32 shader = SHADER_DEFAULT;
 	boolean use_linkdraw_hack = false;
-	boolean splat = R_ThingIsFloorSprite(spr->mobj);
 	UINT8 alpha;
 
 	INT32 i;
@@ -3766,22 +3819,19 @@ static void HWR_SplitSprite(gl_vissprite_t *spr)
 		baseWallVerts[0].t = baseWallVerts[1].t = ((GLPatch_t *)gpatch->hardware)->max_t;
 	}
 
-	if (!splat)
-	{
-		// if it has a dispoffset, push it a little towards the camera
-		if (spr->dispoffset) {
-			float co = -gl_viewcos*(0.05f*spr->dispoffset);
-			float si = -gl_viewsin*(0.05f*spr->dispoffset);
-			baseWallVerts[0].z = baseWallVerts[3].z = baseWallVerts[0].z+si;
-			baseWallVerts[1].z = baseWallVerts[2].z = baseWallVerts[1].z+si;
-			baseWallVerts[0].x = baseWallVerts[3].x = baseWallVerts[0].x+co;
-			baseWallVerts[1].x = baseWallVerts[2].x = baseWallVerts[1].x+co;
-		}
-
-		// Let dispoffset work first since this adjust each vertex
-		HWR_RotateSpritePolyToAim(spr, baseWallVerts, false);
+	// if it has a dispoffset, push it a little towards the camera
+	if (spr->dispoffset) {
+		float co = -gl_viewcos*(0.05f*spr->dispoffset);
+		float si = -gl_viewsin*(0.05f*spr->dispoffset);
+		baseWallVerts[0].z = baseWallVerts[3].z = baseWallVerts[0].z+si;
+		baseWallVerts[1].z = baseWallVerts[2].z = baseWallVerts[1].z+si;
+		baseWallVerts[0].x = baseWallVerts[3].x = baseWallVerts[0].x+co;
+		baseWallVerts[1].x = baseWallVerts[2].x = baseWallVerts[1].x+co;
 	}
 
+	// Let dispoffset work first since this adjust each vertex
+	HWR_RotateSpritePolyToAim(spr, baseWallVerts, false);
+
 	realtop = top = baseWallVerts[3].y;
 	realbot = bot = baseWallVerts[0].y;
 	ttop = baseWallVerts[3].t;
@@ -3817,8 +3867,6 @@ static void HWR_SplitSprite(gl_vissprite_t *spr)
 	else if (spr->mobj->frame & FF_TRANSMASK)
 	{
 		INT32 trans = (spr->mobj->frame & FF_TRANSMASK)>>FF_TRANSSHIFT;
-		if (spr->mobj->blendmode == AST_TRANSLUCENT && trans >= NUMTRANSMAPS)
-			return;
 		blend = HWR_SurfaceBlend(spr->mobj->blendmode, trans, &Surf);
 	}
 	else
@@ -3832,6 +3880,12 @@ static void HWR_SplitSprite(gl_vissprite_t *spr)
 		if (!occlusion) use_linkdraw_hack = true;
 	}
 
+	if (HWR_UseShader())
+	{
+		shader = SHADER_SPRITE;
+		blend |= PF_ColorMapped;
+	}
+
 	alpha = Surf.PolyColor.s.alpha;
 
 	// Start with the lightlevel and colormap from the top of the sprite
@@ -3914,7 +3968,7 @@ static void HWR_SplitSprite(gl_vissprite_t *spr)
 
 		// The x and y only need to be adjusted in the case that it's not a papersprite
 		if (cv_glspritebillboarding.value
-			&& spr->mobj && !(spr->mobj->frame & FF_PAPERSPRITE))
+			&& spr->mobj && !R_ThingIsPaperSprite(spr->mobj))
 		{
 			// Get the x and z of the vertices so billboarding draws correctly
 			realheight = realbot - realtop;
@@ -3940,7 +3994,7 @@ static void HWR_SplitSprite(gl_vissprite_t *spr)
 
 		Surf.PolyColor.s.alpha = alpha;
 
-		HWR_ProcessPolygon(&Surf, wallVerts, 4, blend|PF_Modulated, SHADER_SPRITE, false); // sprite shader
+		HWR_ProcessPolygon(&Surf, wallVerts, 4, blend|PF_Modulated, shader, false);
 
 		if (use_linkdraw_hack)
 			HWR_LinkDrawHackAdd(wallVerts, spr);
@@ -3969,7 +4023,7 @@ static void HWR_SplitSprite(gl_vissprite_t *spr)
 
 	Surf.PolyColor.s.alpha = alpha;
 
-	HWR_ProcessPolygon(&Surf, wallVerts, 4, blend|PF_Modulated, SHADER_SPRITE, false); // sprite shader
+	HWR_ProcessPolygon(&Surf, wallVerts, 4, blend|PF_Modulated, shader, false);
 
 	if (use_linkdraw_hack)
 		HWR_LinkDrawHackAdd(wallVerts, spr);
@@ -3983,7 +4037,7 @@ static void HWR_SplitSprite(gl_vissprite_t *spr)
 static void HWR_DrawSprite(gl_vissprite_t *spr)
 {
 	FOutVector wallVerts[4];
-	patch_t *gpatch; // sprite patch converted to hardware
+	patch_t *gpatch;
 	FSurfaceInfo Surf;
 	const boolean splat = R_ThingIsFloorSprite(spr->mobj);
 
@@ -4141,6 +4195,11 @@ static void HWR_DrawSprite(gl_vissprite_t *spr)
 		wallVerts[1].z = wallVerts[2].z = spr->z2;
 	}
 
+	// cache the patch in the graphics card memory
+	//12/12/99: Hurdler: same comment as above (for md2)
+	//Hurdler: 25/04/2000: now support colormap in hardware mode
+	HWR_GetMappedPatch(gpatch, spr->colormap);
+
 	if (spr->flip)
 	{
 		wallVerts[0].s = wallVerts[3].s = ((GLPatch_t *)gpatch->hardware)->max_s;
@@ -4160,11 +4219,6 @@ static void HWR_DrawSprite(gl_vissprite_t *spr)
 		wallVerts[0].t = wallVerts[1].t = ((GLPatch_t *)gpatch->hardware)->max_t;
 	}
 
-	// cache the patch in the graphics card memory
-	//12/12/99: Hurdler: same comment as above (for md2)
-	//Hurdler: 25/04/2000: now support colormap in hardware mode
-	HWR_GetMappedPatch(gpatch, spr->colormap);
-
 	if (!splat)
 	{
 		// if it has a dispoffset, push it a little towards the camera
@@ -4219,6 +4273,7 @@ static void HWR_DrawSprite(gl_vissprite_t *spr)
 	}
 
 	{
+		INT32 shader = SHADER_DEFAULT;
 		FBITFIELD blend = 0;
 		FBITFIELD occlusion;
 		boolean use_linkdraw_hack = false;
@@ -4244,8 +4299,6 @@ static void HWR_DrawSprite(gl_vissprite_t *spr)
 		else if (spr->mobj->frame & FF_TRANSMASK)
 		{
 			INT32 trans = (spr->mobj->frame & FF_TRANSMASK)>>FF_TRANSSHIFT;
-			if (spr->mobj->blendmode == AST_TRANSLUCENT && trans >= NUMTRANSMAPS)
-				return;
 			blend = HWR_SurfaceBlend(spr->mobj->blendmode, trans, &Surf);
 		}
 		else
@@ -4271,7 +4324,13 @@ static void HWR_DrawSprite(gl_vissprite_t *spr)
 			if (!occlusion) use_linkdraw_hack = true;
 		}
 
-		HWR_ProcessPolygon(&Surf, wallVerts, 4, blend|PF_Modulated, SHADER_SPRITE, false); // sprite shader
+		if (HWR_UseShader())
+		{
+			shader = SHADER_SPRITE;
+			blend |= PF_ColorMapped;
+		}
+
+		HWR_ProcessPolygon(&Surf, wallVerts, 4, blend|PF_Modulated, shader, false);
 
 		if (use_linkdraw_hack)
 			HWR_LinkDrawHackAdd(wallVerts, spr);
@@ -4282,9 +4341,10 @@ static void HWR_DrawSprite(gl_vissprite_t *spr)
 // Sprite drawer for precipitation
 static inline void HWR_DrawPrecipitationSprite(gl_vissprite_t *spr)
 {
+	INT32 shader = SHADER_DEFAULT;
 	FBITFIELD blend = 0;
 	FOutVector wallVerts[4];
-	patch_t *gpatch; // sprite patch converted to hardware
+	patch_t *gpatch;
 	FSurfaceInfo Surf;
 
 	if (!spr->mobj)
@@ -4337,7 +4397,7 @@ static inline void HWR_DrawPrecipitationSprite(gl_vissprite_t *spr)
 			// Always use the light at the top instead of whatever I was doing before
 			INT32 light = R_GetPlaneLight(sector, spr->mobj->z + spr->mobj->height, false);
 
-			if (!(spr->mobj->frame & FF_FULLBRIGHT))
+			if (!R_ThingIsFullBright(spr->mobj))
 				lightlevel = *sector->lightlist[light].lightlevel > 255 ? 255 : *sector->lightlist[light].lightlevel;
 
 			if (*sector->lightlist[light].extra_colormap)
@@ -4345,7 +4405,7 @@ static inline void HWR_DrawPrecipitationSprite(gl_vissprite_t *spr)
 		}
 		else
 		{
-			if (!(spr->mobj->frame & FF_FULLBRIGHT))
+			if (!R_ThingIsFullBright(spr->mobj))
 				lightlevel = sector->lightlevel > 255 ? 255 : sector->lightlevel;
 
 			if (sector->extra_colormap)
@@ -4358,9 +4418,7 @@ static inline void HWR_DrawPrecipitationSprite(gl_vissprite_t *spr)
 	if (spr->mobj->frame & FF_TRANSMASK)
 	{
 		INT32 trans = (spr->mobj->frame & FF_TRANSMASK)>>FF_TRANSSHIFT;
-		if (spr->mobj->blendmode == AST_TRANSLUCENT && trans >= NUMTRANSMAPS)
-			return;
-		blend = HWR_SurfaceBlend(spr->mobj->blendmode, trans, &Surf);
+		blend = HWR_SurfaceBlend(AST_TRANSLUCENT, trans, &Surf);
 	}
 	else
 	{
@@ -4372,7 +4430,13 @@ static inline void HWR_DrawPrecipitationSprite(gl_vissprite_t *spr)
 		blend = HWR_GetBlendModeFlag(spr->mobj->blendmode)|PF_Occlude;
 	}
 
-	HWR_ProcessPolygon(&Surf, wallVerts, 4, blend|PF_Modulated, SHADER_SPRITE, false); // sprite shader
+	if (HWR_UseShader())
+	{
+		shader = SHADER_SPRITE;
+		blend |= PF_ColorMapped;
+	}
+
+	HWR_ProcessPolygon(&Surf, wallVerts, 4, blend|PF_Modulated, shader, false);
 }
 #endif
 
@@ -4654,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_GetTimeMicros();
+	PS_START_TIMING(ps_hw_nodesorttime);
 
 	for (i = 0; i < numplanes; i++, p++)
 	{
@@ -4674,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
 
@@ -4709,9 +4773,9 @@ static void HWR_CreateDrawNodes(void)
 		}
 	}
 
-	ps_hw_nodesorttime = I_GetTimeMicros() - ps_hw_nodesorttime;
+	PS_STOP_TIMING(ps_hw_nodesorttime);
 
-	ps_hw_nodedrawtime = I_GetTimeMicros();
+	PS_START_TIMING(ps_hw_nodedrawtime);
 
 	// Okay! Let's draw it all! Woo!
 	HWD.pfnSetTransform(&atransform);
@@ -4748,7 +4812,7 @@ static void HWR_CreateDrawNodes(void)
 		}
 	}
 
-	ps_hw_nodedrawtime = I_GetTimeMicros() - ps_hw_nodedrawtime;
+	PS_STOP_TIMING(ps_hw_nodedrawtime);
 
 	numwalls = 0;
 	numplanes = 0;
@@ -4921,8 +4985,8 @@ static void HWR_ProjectSprite(mobj_t *thing)
 
 	angle_t ang;
 	INT32 heightsec, phs;
-	const boolean papersprite = R_ThingIsPaperSprite(thing);
 	const boolean splat = R_ThingIsFloorSprite(thing);
+	const boolean papersprite = (R_ThingIsPaperSprite(thing) && !splat);
 	angle_t mobjangle = (thing->player ? thing->player->drawangle : thing->angle);
 	float z1, z2;
 
@@ -4939,6 +5003,13 @@ static void HWR_ProjectSprite(mobj_t *thing)
 	if (thing->spritexscale < 1 || thing->spriteyscale < 1)
 		return;
 
+	// Visibility check by the blend mode.
+	if (thing->frame & FF_TRANSMASK)
+	{
+		if (!R_BlendLevelVisible(thing->blendmode, (thing->frame & FF_TRANSMASK)>>FF_TRANSSHIFT))
+			return;
+	}
+
 	dispoffset = thing->info->dispoffset;
 
 	this_scale = FIXED_TO_FLOAT(thing->scale);
@@ -5270,7 +5341,7 @@ static void HWR_ProjectSprite(mobj_t *thing)
 		else if (vis->mobj->type == MT_METALSONIC_BATTLE)
 			vis->colormap = R_GetTranslationColormap(TC_METALSONIC, 0, GTC_CACHE);
 		else
-			vis->colormap = R_GetTranslationColormap(TC_BOSS, 0, GTC_CACHE);
+			vis->colormap = R_GetTranslationColormap(TC_BOSS, vis->mobj->color, GTC_CACHE);
 	}
 	else if (thing->color)
 	{
@@ -5295,7 +5366,7 @@ static void HWR_ProjectSprite(mobj_t *thing)
 			vis->colormap = R_GetTranslationColormap(TC_DEFAULT, vis->mobj->color ? vis->mobj->color : SKINCOLOR_CYAN, GTC_CACHE);
 	}
 	else
-		vis->colormap = colormaps;
+		vis->colormap = NULL;
 
 	// set top/bottom coords
 	vis->gzt = gzt;
@@ -5325,6 +5396,13 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 	unsigned rot = 0;
 	UINT8 flip;
 
+	// Visibility check by the blend mode.
+	if (thing->frame & FF_TRANSMASK)
+	{
+		if (!R_BlendLevelVisible(thing->blendmode, (thing->frame & FF_TRANSMASK)>>FF_TRANSSHIFT))
+			return;
+	}
+
 	// transform the origin point
 	tr_x = FIXED_TO_FLOAT(thing->x) - gl_viewx;
 	tr_y = FIXED_TO_FLOAT(thing->y) - gl_viewy;
@@ -5358,7 +5436,7 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 		return;
 #endif
 
-	sprframe = &sprdef->spriteframes[ thing->frame & FF_FRAMEMASK];
+	sprframe = &sprdef->spriteframes[thing->frame & FF_FRAMEMASK];
 
 	// use single rotation for all views
 	lumpoff = sprframe->lumpid[0];
@@ -5396,7 +5474,7 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 	vis->flip = flip;
 	vis->mobj = (mobj_t *)thing;
 
-	vis->colormap = colormaps;
+	vis->colormap = NULL;
 
 	// set top/bottom coords
 	vis->gzt = FIXED_TO_FLOAT(thing->z + spritecachedinfo[lumpoff].topoffset);
@@ -5651,7 +5729,7 @@ static void HWR_DrawSkyBackground(player_t *player)
 
 		dimensionmultiply = ((float)textures[texturetranslation[skytexture]]->width/256.0f);
 
-		v[0].s = v[3].s = (-1.0f * angle) / ((ANGLE_90-1)*dimensionmultiply); // left
+		v[0].s = v[3].s = (-1.0f * angle) / (((float)ANGLE_90-1.0f)*dimensionmultiply); // left
 		v[2].s = v[1].s = v[0].s + (1.0f/dimensionmultiply); // right (or left + 1.0f)
 		// use +angle and -1.0f above instead if you wanted old backwards behavior
 
@@ -6017,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_GetTimeMicros();
+	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_GetTimeMicros() - ps_hw_skyboxtime;
+	PS_STOP_TIMING(ps_hw_skyboxtime);
 
 	{
 		// do we really need to save player (is it not the same)?
@@ -6130,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_GetTimeMicros();
+	ps_numbspcalls.value.i = 0;
+	ps_numpolyobjects.value.i = 0;
+	PS_START_TIMING(ps_bsptime);
 
 	validcount++;
 
@@ -6170,7 +6248,7 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	}
 #endif
 
-	ps_bsptime = I_GetTimeMicros() - ps_bsptime;
+	PS_STOP_TIMING(ps_bsptime);
 
 	if (cv_glbatching.value)
 		HWR_RenderBatches();
@@ -6185,22 +6263,22 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 #endif
 
 	// Draw MD2 and sprites
-	ps_numsprites = gl_visspritecount;
-	ps_hw_spritesorttime = I_GetTimeMicros();
+	ps_numsprites.value.i = gl_visspritecount;
+	PS_START_TIMING(ps_hw_spritesorttime);
 	HWR_SortVisSprites();
-	ps_hw_spritesorttime = I_GetTimeMicros() - ps_hw_spritesorttime;
-	ps_hw_spritedrawtime = I_GetTimeMicros();
+	PS_STOP_TIMING(ps_hw_spritesorttime);
+	PS_START_TIMING(ps_hw_spritedrawtime);
 	HWR_DrawSprites();
-	ps_hw_spritedrawtime = I_GetTimeMicros() - 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();
@@ -6454,24 +6532,29 @@ void HWR_RenderWall(FOutVector *wallVerts, FSurfaceInfo *pSurf, FBITFIELD blend,
 	FBITFIELD blendmode = blend;
 	UINT8 alpha = pSurf->PolyColor.s.alpha; // retain the alpha
 
-	int shader;
+	INT32 shader = SHADER_DEFAULT;
 
 	// Lighting is done here instead so that fog isn't drawn incorrectly on transparent walls after sorting
 	HWR_Lighting(pSurf, lightlevel, wallcolormap);
 
 	pSurf->PolyColor.s.alpha = alpha; // put the alpha back after lighting
 
-	shader = SHADER_WALL;	// wall shader
-
 	if (blend & PF_Environment)
 		blendmode |= PF_Occlude;	// PF_Occlude must be used for solid objects
 
-	if (fogwall)
+	if (HWR_UseShader())
 	{
-		blendmode |= PF_Fog;
-		shader = SHADER_FOG;	// fog shader
+		if (fogwall)
+			shader = SHADER_FOG;
+		else
+			shader = SHADER_WALL;
+
+		blendmode |= PF_ColorMapped;
 	}
 
+	if (fogwall)
+		blendmode |= PF_Fog;
+
 	blendmode |= PF_Modulated;	// No PF_Occlude means overlapping (incorrect) transparency
 	HWR_ProcessPolygon(pSurf, wallVerts, 4, blendmode, shader, false);
 }
@@ -6514,7 +6597,7 @@ void HWR_DoPostProcessor(player_t *player)
 
 		Surf.PolyColor.s.alpha = 0xc0; // match software mode
 
-		HWD.pfnDrawPolygon(&Surf, v, 4, PF_Modulated|PF_AdditiveSource|PF_NoTexture|PF_NoDepthTest);
+		HWD.pfnDrawPolygon(&Surf, v, 4, PF_Modulated|PF_Additive|PF_NoTexture|PF_NoDepthTest);
 	}
 
 	// Capture the screen for intermission and screen waving
@@ -6647,7 +6730,6 @@ void HWR_DrawScreenFinalTexture(int width, int height)
     HWD.pfnDrawScreenFinalTexture(width, height);
 }
 
-// jimita 18032019
 static inline UINT16 HWR_FindShaderDefs(UINT16 wadnum)
 {
 	UINT16 i;
@@ -6685,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 2ce918408b041780b3a5e0294a9ce059a5326381..3f90f0ae17a58070948148f168c48f248510ce61 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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);
@@ -69,7 +71,7 @@ void HWR_Lighting(FSurfaceInfo *Surface, INT32 light_level, extracolormap_t *col
 UINT8 HWR_FogBlockAlpha(INT32 light, extracolormap_t *colormap); // Let's see if this can work
 
 UINT8 HWR_GetTranstableAlpha(INT32 transtablenum);
-FBITFIELD HWR_GetBlendModeFlag(INT32 ast);
+FBITFIELD HWR_GetBlendModeFlag(INT32 style);
 FBITFIELD HWR_SurfaceBlend(INT32 style, INT32 transtablenum, FSurfaceInfo *pSurf);
 FBITFIELD HWR_TranstableToAlpha(INT32 transtablenum, FSurfaceInfo *pSurf);
 
@@ -116,22 +118,22 @@ extern FTransform atransform;
 
 
 // Render stats
-extern int ps_hw_skyboxtime;
-extern int ps_hw_nodesorttime;
-extern int ps_hw_nodedrawtime;
-extern int ps_hw_spritesorttime;
-extern int 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 int ps_hw_batchsorttime;
-extern int 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/hw_md2.c b/src/hardware/hw_md2.c
index 9c786e67ed5a40e395ceb362b28cc3a58a1254c5..b66f91e1962579788ebaae5bdc5baec73cdff325 100644
--- a/src/hardware/hw_md2.c
+++ b/src/hardware/hw_md2.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -158,7 +158,7 @@ static GLTextureFormat_t PNG_Load(const char *filename, int *w, int *h, GLPatch_
 	jmp_buf jmpbuf;
 #endif
 #endif
-	png_FILE_p png_FILE;
+	volatile png_FILE_p png_FILE;
 	//Filename checking fixed ~Monster Iestyn and Golden
 	char *pngfilename = va("%s"PATHSEP"models"PATHSEP"%s", srb2home, filename);
 
@@ -777,24 +777,7 @@ static void HWR_CreateBlendedTexture(patch_t *gpatch, patch_t *blendgpatch, GLMi
 
 	while (size--)
 	{
-		if (skinnum == TC_BOSS)
-		{
-			// Turn everything below a certain threshold white
-			if ((image->s.red == image->s.green) && (image->s.green == image->s.blue) && image->s.blue < 127)
-			{
-				// Lactozilla: Invert the colors
-				cur->s.red = cur->s.green = cur->s.blue = (255 - image->s.blue);
-			}
-			else
-			{
-				cur->s.red = image->s.red;
-				cur->s.green = image->s.green;
-				cur->s.blue = image->s.blue;
-			}
-
-			cur->s.alpha = image->s.alpha;
-		}
-		else if (skinnum == TC_ALLWHITE)
+		if (skinnum == TC_ALLWHITE)
 		{
 			// Turn everything white
 			cur->s.red = cur->s.green = cur->s.blue = 255;
@@ -1065,6 +1048,15 @@ skippixel:
 
 					cur->s.alpha = image->s.alpha;
 				}
+				else if (skinnum == TC_BOSS)
+				{
+					// Turn everything below a certain threshold white
+					if ((image->s.red == image->s.green) && (image->s.green == image->s.blue) && image->s.blue < 127)
+					{
+						// Lactozilla: Invert the colors
+						cur->s.red = cur->s.green = cur->s.blue = (255 - image->s.blue);
+					}
+				}
 			}
 		}
 
@@ -1106,11 +1098,19 @@ static void HWR_GetBlendedTexture(patch_t *patch, patch_t *blendpatch, INT32 ski
 	for (grMipmap = grPatch->mipmap; grMipmap->nextcolormap; )
 	{
 		grMipmap = grMipmap->nextcolormap;
-		if (grMipmap->colormap == colormap)
+		if (grMipmap->colormap && grMipmap->colormap->source == colormap)
 		{
 			if (grMipmap->downloaded && grMipmap->data)
 			{
-				HWD.pfnSetTexture(grMipmap); // found the colormap, set it to the correct texture
+				if (memcmp(grMipmap->colormap->data, colormap, 256 * sizeof(UINT8)))
+				{
+					M_Memcpy(grMipmap->colormap->data, colormap, 256 * sizeof(UINT8));
+					HWR_CreateBlendedTexture(patch, blendpatch, grMipmap, skinnum, color);
+					HWD.pfnUpdateTexture(grMipmap);
+				}
+				else
+					HWD.pfnSetTexture(grMipmap); // found the colormap, set it to the correct texture
+
 				Z_ChangeTag(grMipmap->data, PU_HWRMODELTEXTURE_UNLOCKED);
 				return;
 			}
@@ -1128,7 +1128,10 @@ static void HWR_GetBlendedTexture(patch_t *patch, patch_t *blendpatch, INT32 ski
 	if (newMipmap == NULL)
 		I_Error("%s: Out of memory", "HWR_GetBlendedTexture");
 	grMipmap->nextcolormap = newMipmap;
-	newMipmap->colormap = colormap;
+
+	newMipmap->colormap = Z_Calloc(sizeof(*newMipmap->colormap), PU_HWRPATCHCOLMIPMAP, NULL);
+	newMipmap->colormap->source = colormap;
+	M_Memcpy(newMipmap->colormap->data, colormap, 256 * sizeof(UINT8));
 
 	HWR_CreateBlendedTexture(patch, blendpatch, newMipmap, skinnum, color);
 
@@ -1303,7 +1306,7 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 
 			light = R_GetPlaneLight(sector, spr->mobj->z + spr->mobj->height, false); // Always use the light at the top instead of whatever I was doing before
 
-			if (!(spr->mobj->frame & FF_FULLBRIGHT))
+			if (!R_ThingIsFullBright(spr->mobj))
 				lightlevel = *sector->lightlist[light].lightlevel > 255 ? 255 : *sector->lightlist[light].lightlevel;
 
 			if (*sector->lightlist[light].extra_colormap)
@@ -1311,7 +1314,7 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 		}
 		else
 		{
-			if (!(spr->mobj->frame & FF_FULLBRIGHT))
+			if (!R_ThingIsFullBright(spr->mobj))
 				lightlevel = sector->lightlevel > 255 ? 255 : sector->lightlevel;
 
 			if (sector->extra_colormap)
@@ -1329,10 +1332,9 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 		GLPatch_t *hwrPatch = NULL, *hwrBlendPatch = NULL;
 		INT32 durs = spr->mobj->state->tics;
 		INT32 tics = spr->mobj->tics;
-		//mdlframe_t *next = NULL;
-		const boolean papersprite = (spr->mobj->frame & FF_PAPERSPRITE);
-		const UINT8 flip = (UINT8)(!(spr->mobj->eflags & MFE_VERTICALFLIP) != !(spr->mobj->frame & FF_VERTICALFLIP));
-		const UINT8 hflip = (UINT8)(!(spr->mobj->mirrored) != !(spr->mobj->frame & FF_HORIZONTALFLIP));
+		const boolean papersprite = (R_ThingIsPaperSprite(spr->mobj) && !R_ThingIsFloorSprite(spr->mobj));
+		const UINT8 flip = (UINT8)(!(spr->mobj->eflags & MFE_VERTICALFLIP) != !R_ThingVerticallyFlipped(spr->mobj));
+		const UINT8 hflip = (UINT8)(!(spr->mobj->mirrored) != !R_ThingHorizontallyFlipped(spr->mobj));
 		spritedef_t *sprdef;
 		spriteframe_t *sprframe;
 		spriteinfo_t *sprinfo;
@@ -1394,6 +1396,11 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 			|| ((!hwrBlendPatch->mipmap->format || !hwrBlendPatch->mipmap->downloaded) && !md2->noblendfile)))
 			md2_loadBlendTexture(md2);
 
+		// Load it again, because it isn't being loaded into blendgpatch after md2_loadblendtexture...
+		blendgpatch = md2->blendgrpatch;
+		if (blendgpatch)
+			hwrBlendPatch = ((GLPatch_t *)blendgpatch->hardware);
+
 		if (md2->error)
 			return false; // we already failed loading this before :(
 		if (!md2->model)
@@ -1518,7 +1525,12 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 				{
 					nextFrame = (spr->mobj->frame & FF_FRAMEMASK) + 1;
 					if (nextFrame >= mod)
-						nextFrame = 0;
+					{
+						if (spr->mobj->state->frame & FF_SPR2ENDSTATE)
+							nextFrame--;
+						else
+							nextFrame = 0;
+					}
 					if (frame || !(spr->mobj->state->frame & FF_SPR2ENDSTATE))
 						nextFrame = md2->model->spr2frames[spr2].frames[nextFrame];
 					else
diff --git a/src/hardware/hw_md2.h b/src/hardware/hw_md2.h
index 0f4d2c7bc925f45005757c09a80cabaf103a8a3a..9249c034c2b1394f289681e1d33cc8726a9f161a 100644
--- a/src/hardware/hw_md2.h
+++ b/src/hardware/hw_md2.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/hardware/r_opengl/r_opengl.c b/src/hardware/r_opengl/r_opengl.c
index 8cd948eeadf57a34fa1290479b2f84d26210ba28..de0e8c6a65aab0c1cbdc8e6e7f77cccd9118c6fd 100644
--- a/src/hardware/r_opengl/r_opengl.c
+++ b/src/hardware/r_opengl/r_opengl.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 1998-2020 by Sonic Team Junior.
+// Copyright (C) 1998-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -58,8 +58,12 @@ static  GLuint      tex_downloaded  = 0;
 static  GLfloat     fov             = 90.0f;
 static  FBITFIELD   CurrentPolyFlags;
 
-static  FTextureInfo *gl_cachetail = NULL;
-static  FTextureInfo *gl_cachehead = NULL;
+// Linked list of all textures.
+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()
@@ -130,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
@@ -158,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, ...)
@@ -183,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, ...)
@@ -909,7 +908,6 @@ void SetupGLFunc4(void)
 	pgluBuild2DMipmaps = GetGLFunc("gluBuild2DMipmaps");
 }
 
-// jimita
 EXPORT boolean HWRAPI(CompileShaders) (void)
 {
 #ifdef GL_SHADERS
@@ -961,8 +959,6 @@ EXPORT boolean HWRAPI(CompileShaders) (void)
 		}
 	}
 
-	SetShader(SHADER_DEFAULT);
-
 	return true;
 #else
 	return false;
@@ -1287,10 +1283,34 @@ void SetStates(void)
 // -----------------+
 // DeleteTexture    : Deletes a texture from the GPU and frees its data
 // -----------------+
-EXPORT void HWRAPI(DeleteTexture) (FTextureInfo *pTexInfo)
+EXPORT void HWRAPI(DeleteTexture) (GLMipmap_t *pTexInfo)
 {
-	if (pTexInfo->downloaded)
+	FTextureInfo *head = TexCacheHead;
+
+	if (!pTexInfo)
+		return;
+	else if (pTexInfo->downloaded)
 		pglDeleteTextures(1, (GLuint *)&pTexInfo->downloaded);
+
+	while (head)
+	{
+		if (head->downloaded == pTexInfo->downloaded)
+		{
+			if (head->next)
+				head->next->prev = head->prev;
+			else // no next -> tail is being deleted -> update TexCacheTail
+				TexCacheTail = head->prev;
+			if (head->prev)
+				head->prev->next = head->next;
+			else // no prev -> head is being deleted -> update TexCacheHead
+				TexCacheHead = head->next;
+			free(head);
+			break;
+		}
+
+		head = head->next;
+	}
+
 	pTexInfo->downloaded = 0;
 }
 
@@ -1303,23 +1323,30 @@ void Flush(void)
 {
 	//GL_DBG_Printf ("HWR_Flush()\n");
 
-	while (gl_cachehead)
+	while (TexCacheHead)
 	{
-		DeleteTexture(gl_cachehead);
-		gl_cachehead = gl_cachehead->nextmipmap;
+		FTextureInfo *pTexInfo = TexCacheHead;
+		GLMipmap_t *texture = pTexInfo->texture;
+
+		if (pTexInfo->downloaded)
+		{
+			pglDeleteTextures(1, (GLuint *)&pTexInfo->downloaded);
+			pTexInfo->downloaded = 0;
+		}
+
+		if (texture)
+			texture->downloaded = 0;
+
+		TexCacheHead = pTexInfo->next;
+		free(pTexInfo);
 	}
 
-	ClearCacheList(); //Hurdler: well, gl_cachehead is already NULL
+	TexCacheTail = TexCacheHead = NULL; //Hurdler: well, TexCacheHead is already NULL
 	tex_downloaded = 0;
-}
-
 
-// -----------------+
-// ClearCacheList   : Clears the texture cache tail and head
-// -----------------+
-EXPORT void HWRAPI(ClearCacheList) (void)
-{
-	gl_cachetail = gl_cachehead = NULL;
+	free(textureBuffer);
+	textureBuffer = NULL;
+	textureBufferSize = 0;
 }
 
 
@@ -1353,7 +1380,6 @@ INT32 isExtAvailable(const char *extension, const GLubyte *start)
 
 // -----------------+
 // Init             : Initialise the OpenGL interface API
-// Returns          :
 // -----------------+
 EXPORT boolean HWRAPI(Init) (void)
 {
@@ -1554,12 +1580,11 @@ static void SetBlendMode(FBITFIELD flags)
 		case PF_Additive & PF_Blending:
 		case PF_Subtractive & PF_Blending:
 		case PF_ReverseSubtract & PF_Blending:
+			pglBlendFunc(GL_SRC_ALPHA, GL_ONE); // src * alpha + dest
+			break;
 		case PF_Environment & PF_Blending:
 			pglBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
 			break;
-		case PF_AdditiveSource & PF_Blending:
-			pglBlendFunc(GL_SRC_ALPHA, GL_ONE); // src * alpha + dest
-			break;
 		case PF_Multiplicative & PF_Blending:
 			pglBlendFunc(GL_DST_COLOR, GL_ZERO);
 			break;
@@ -1598,7 +1623,6 @@ static void SetBlendMode(FBITFIELD flags)
 			break;
 		case PF_Translucent & PF_Blending:
 		case PF_Additive & PF_Blending:
-		case PF_AdditiveSource & PF_Blending:
 		case PF_Subtractive & PF_Blending:
 		case PF_ReverseSubtract & PF_Blending:
 		case PF_Environment & PF_Blending:
@@ -1715,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) (FTextureInfo *pTexInfo)
+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;
 
-	if (!pTexInfo->downloaded)
+	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;
+
+	// 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);
+	//GL_DBG_Printf("UpdateTexture %d %x\n", (INT32)num, pImgData);
 
-	w = pTexInfo->width;
-	h = pTexInfo->height;
-
-	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++)
 		{
@@ -1776,20 +1811,18 @@ EXPORT void HWRAPI(UpdateTexture) (FTextureInfo *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++)
 		{
@@ -1806,8 +1839,8 @@ EXPORT void HWRAPI(UpdateTexture) (FTextureInfo *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++)
 		{
@@ -1822,11 +1855,10 @@ EXPORT void HWRAPI(UpdateTexture) (FTextureInfo *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)
@@ -1855,7 +1887,7 @@ EXPORT void HWRAPI(UpdateTexture) (FTextureInfo *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);
@@ -1876,7 +1908,7 @@ EXPORT void HWRAPI(UpdateTexture) (FTextureInfo *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);
@@ -1896,7 +1928,7 @@ EXPORT void HWRAPI(UpdateTexture) (FTextureInfo *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);
@@ -1920,7 +1952,7 @@ EXPORT void HWRAPI(UpdateTexture) (FTextureInfo *pTexInfo)
 // -----------------+
 // SetTexture       : The mipmap becomes the current texture source
 // -----------------+
-EXPORT void HWRAPI(SetTexture) (FTextureInfo *pTexInfo)
+EXPORT void HWRAPI(SetTexture) (GLMipmap_t *pTexInfo)
 {
 	if (!pTexInfo)
 	{
@@ -1937,17 +1969,25 @@ EXPORT void HWRAPI(SetTexture) (FTextureInfo *pTexInfo)
 	}
 	else
 	{
+		FTextureInfo *newTex = calloc(1, sizeof (*newTex));
+
 		UpdateTexture(pTexInfo);
-		pTexInfo->nextmipmap = NULL;
+
+		newTex->texture = pTexInfo;
+		newTex->downloaded = (UINT32)pTexInfo->downloaded;
+		newTex->width = (UINT32)pTexInfo->width;
+		newTex->height = (UINT32)pTexInfo->height;
+		newTex->format = (UINT32)pTexInfo->format;
 
 		// insertion at the tail
-		if (gl_cachetail)
+		if (TexCacheTail)
 		{
-			gl_cachetail->nextmipmap = pTexInfo;
-			gl_cachetail = pTexInfo;
+			newTex->prev = TexCacheTail;
+			TexCacheTail->next = newTex;
+			TexCacheTail = newTex;
 		}
 		else // initialization of the linked list
-			gl_cachetail = gl_cachehead = pTexInfo;
+			TexCacheTail = TexCacheHead = newTex;
 	}
 }
 
@@ -2144,32 +2184,34 @@ static void PreparePolygon(FSurfaceInfo *pSurf, FOutVector *pOutVerts, FBITFIELD
 
 	SetBlend(PolyFlags);    //TODO: inline (#pragma..)
 
-	// PolyColor
 	if (pSurf)
 	{
-		// If Modulated, mix the surface colour to the texture
+		// If modulated, mix the surface colour to the texture
 		if (CurrentPolyFlags & PF_Modulated)
-		{
-			// Poly color
-			poly.red    = byte2float[pSurf->PolyColor.s.red];
-			poly.green  = byte2float[pSurf->PolyColor.s.green];
-			poly.blue   = byte2float[pSurf->PolyColor.s.blue];
-			poly.alpha  = byte2float[pSurf->PolyColor.s.alpha];
-
 			pglColor4ubv((GLubyte*)&pSurf->PolyColor.s);
-		}
 
-		// Tint color
-		tint.red   = byte2float[pSurf->TintColor.s.red];
-		tint.green = byte2float[pSurf->TintColor.s.green];
-		tint.blue  = byte2float[pSurf->TintColor.s.blue];
-		tint.alpha = byte2float[pSurf->TintColor.s.alpha];
+		// If the surface is either modulated or colormapped, or both
+		if (CurrentPolyFlags & (PF_Modulated | PF_ColorMapped))
+		{
+			poly.red   = byte2float[pSurf->PolyColor.s.red];
+			poly.green = byte2float[pSurf->PolyColor.s.green];
+			poly.blue  = byte2float[pSurf->PolyColor.s.blue];
+			poly.alpha = byte2float[pSurf->PolyColor.s.alpha];
+		}
 
-		// Fade color
-		fade.red   = byte2float[pSurf->FadeColor.s.red];
-		fade.green = byte2float[pSurf->FadeColor.s.green];
-		fade.blue  = byte2float[pSurf->FadeColor.s.blue];
-		fade.alpha = byte2float[pSurf->FadeColor.s.alpha];
+		// Only if the surface is colormapped
+		if (CurrentPolyFlags & PF_ColorMapped)
+		{
+			tint.red   = byte2float[pSurf->TintColor.s.red];
+			tint.green = byte2float[pSurf->TintColor.s.green];
+			tint.blue  = byte2float[pSurf->TintColor.s.blue];
+			tint.alpha = byte2float[pSurf->TintColor.s.alpha];
+
+			fade.red   = byte2float[pSurf->FadeColor.s.red];
+			fade.green = byte2float[pSurf->FadeColor.s.green];
+			fade.blue  = byte2float[pSurf->FadeColor.s.blue];
+			fade.alpha = byte2float[pSurf->FadeColor.s.alpha];
+		}
 	}
 
 	// this test is added for new coronas' code (without depth buffer)
@@ -2722,7 +2764,7 @@ static void DrawModelEx(model_t *model, INT32 frameIndex, INT32 duration, INT32
 	fade.alpha = byte2float[Surface->FadeColor.s.alpha];
 
 	flags = (Surface->PolyFlags | PF_Modulated);
-	if (Surface->PolyFlags & (PF_Additive|PF_AdditiveSource|PF_Subtractive|PF_ReverseSubtract|PF_Multiplicative))
+	if (Surface->PolyFlags & (PF_Additive|PF_Subtractive|PF_ReverseSubtract|PF_Multiplicative))
 		flags |= PF_Occlude;
 	else if (Surface->PolyColor.s.alpha == 0xFF)
 		flags |= (PF_Occlude | PF_Masked);
@@ -2983,7 +3025,6 @@ EXPORT void HWRAPI(SetTransform) (FTransform *stransform)
 	pglMatrixMode(GL_PROJECTION);
 	pglLoadIdentity();
 
-	// jimita 14042019
 	// Simulate Software's y-shearing
 	// https://zdoom.org/wiki/Y-shearing
 	if (shearing)
@@ -3011,7 +3052,7 @@ EXPORT void HWRAPI(SetTransform) (FTransform *stransform)
 
 EXPORT INT32  HWRAPI(GetTextureUsed) (void)
 {
-	FTextureInfo *tmp = gl_cachehead;
+	FTextureInfo *tmp = TexCacheHead;
 	INT32 res = 0;
 
 	while (tmp)
@@ -3028,7 +3069,7 @@ EXPORT INT32  HWRAPI(GetTextureUsed) (void)
 
 		// Add it up!
 		res += tmp->height*tmp->width*bpp;
-		tmp = tmp->nextmipmap;
+		tmp = tmp->next;
 	}
 
 	return res;
diff --git a/src/http-mserv.c b/src/http-mserv.c
index 7c7d04495cd8f5641bd7c6f236c47bd0eb344c64..f9134ba5008b0ba5f27e0b6ae48feee7ec95feef 100644
--- a/src/http-mserv.c
+++ b/src/http-mserv.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020 by James R.
+// Copyright (C) 2020-2021 by James R.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index 7e9144f98fd995ae3b2218f76472f3a92792db56..f4c5e4c3b14cbef8dd9bc39b241b30358166bea9 100644
--- a/src/hu_stuff.c
+++ b/src/hu_stuff.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -98,6 +98,7 @@ patch_t *emeraldpics[3][8]; // 0 = normal, 1 = tiny, 2 = coinbox
 static patch_t *emblemicon;
 patch_t *tokenicon;
 static patch_t *exiticon;
+static patch_t *nopingicon;
 
 //-------------------------------------------
 //              misc vars
@@ -286,6 +287,7 @@ void HU_LoadGraphics(void)
 	emblemicon = W_CachePatchName("EMBLICON", PU_HUDGFX);
 	tokenicon = W_CachePatchName("TOKNICON", PU_HUDGFX);
 	exiticon = W_CachePatchName("EXITICON", PU_HUDGFX);
+	nopingicon = W_CachePatchName("NOPINGICON", PU_HUDGFX);
 
 	emeraldpics[0][0] = W_CachePatchName("CHAOS1", PU_HUDGFX);
 	emeraldpics[0][1] = W_CachePatchName("CHAOS2", PU_HUDGFX);
@@ -684,7 +686,7 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 
 	// run the lua hook even if we were supposed to eat the msg, netgame consistency goes first.
 
-	if (LUAh_PlayerMsg(playernum, target, flags, msg))
+	if (LUA_HookPlayerMsg(playernum, target, flags, msg))
 		return;
 
 	if (spam_eatmsg)
@@ -934,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;
@@ -1109,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;
@@ -1138,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;
@@ -1155,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
@@ -1232,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;
@@ -2102,22 +2104,25 @@ void HU_Drawer(void)
 		}
 		else
 			HU_DrawCoopOverlay();
-		LUAh_ScoresHUD();
+		LUA_HUDHOOK(scores);
 	}
 
 	if (gamestate != GS_LEVEL)
 		return;
 
 	// draw the crosshair, not when viewing demos nor with chasecam
-	if (!automapactive && cv_crosshair.value && !demoplayback &&
-		(!camera.chase || ticcmd_ztargetfocus[0])
-	&& !players[displayplayer].spectator)
-		HU_DrawCrosshair();
+	if (LUA_HudEnabled(hud_crosshair))
+	{
+		if (!automapactive && cv_crosshair.value && !demoplayback &&
+			(!camera.chase || ticcmd_ztargetfocus[0])
+		&& !players[displayplayer].spectator)
+			HU_DrawCrosshair();
 
-	if (!automapactive && cv_crosshair2.value && !demoplayback &&
-		(!camera2.chase || ticcmd_ztargetfocus[1])
-	&& !players[secondarydisplayplayer].spectator)
-		HU_DrawCrosshair2();
+		if (!automapactive && cv_crosshair2.value && !demoplayback &&
+			(!camera2.chase || ticcmd_ztargetfocus[1])
+		&& !players[secondarydisplayplayer].spectator)
+			HU_DrawCrosshair2();
+	}
 
 	// draw desynch text
 	if (hu_redownloadinggamestate)
@@ -2243,8 +2248,8 @@ void HU_Erase(void)
 //
 void HU_drawPing(INT32 x, INT32 y, UINT32 ping, boolean notext, INT32 flags)
 {
-	UINT8 numbars = 1; // how many ping bars do we draw?
-	UINT8 barcolor = 35; // color we use for the bars (green, yellow or red)
+	UINT8 numbars = 0; // how many ping bars do we draw?
+	UINT8 barcolor = 31; // color we use for the bars (green, yellow, red or black)
 	SINT8 i = 0;
 	SINT8 yoffset = 6;
 	INT32 dx = x+1 - (V_SmallStringWidth(va("%dms", ping),
@@ -2257,11 +2262,16 @@ void HU_drawPing(INT32 x, INT32 y, UINT32 ping, boolean notext, INT32 flags)
 	}
 	else if (ping < 256)
 	{
-		numbars = 2; // Apparently ternaries w/ multiple statements don't look good in C so I decided against it.
+		numbars = 2;
 		barcolor = 73;
 	}
+	else if (ping < UINT32_MAX)
+	{
+		numbars = 1;
+		barcolor = 35;
+	}
 
-	if (!notext || vid.width >= 640) // how sad, we're using a shit resolution.
+	if (ping < UINT32_MAX && (!notext || vid.width >= 640)) // how sad, we're using a shit resolution.
 		V_DrawSmallString(dx, y+4, V_ALLOWLOWERCASE|flags, va("%dms", ping));
 
 	for (i=0; (i<3); i++) // Draw the ping bar
@@ -2272,6 +2282,9 @@ void HU_drawPing(INT32 x, INT32 y, UINT32 ping, boolean notext, INT32 flags)
 
 		yoffset -= 2;
 	}
+
+	if (ping == UINT32_MAX)
+		V_DrawSmallScaledPatch(x + 4 - nopingicon->width/2, y + 9 - nopingicon->height/2, 0, nopingicon);
 }
 
 //
@@ -2298,16 +2311,17 @@ void HU_DrawTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, I
 
 		if (!splitscreen) // don't draw it on splitscreen,
 		{
-			if (!(tab[i].num == serverplayer || players[tab[i].num].quittime))
-				HU_drawPing(x+ 253, y, playerpingtable[tab[i].num], false, 0);
+			if (tab[i].num != serverplayer)
+				HU_drawPing(x + 253, y, players[tab[i].num].quittime ? UINT32_MAX : playerpingtable[tab[i].num], false, 0);
 			//else
 			//	V_DrawSmallString(x+ 246, y+4, V_YELLOWMAP, "SERVER");
 		}
 
-		V_DrawString(x + 20, y,
-		             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
-		             | (greycheck ? V_60TRANS : 0)
-		             | V_ALLOWLOWERCASE, tab[i].name);
+		if (!players[tab[i].num].quittime || (leveltime / (TICRATE/2) & 1))
+			V_DrawString(x + 20, y,
+		                 ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
+		                 | (greycheck ? V_60TRANS : 0)
+		                 | V_ALLOWLOWERCASE, tab[i].name);
 
 		// Draw emeralds
 		if (players[tab[i].num].powers[pw_invulnerability] && (players[tab[i].num].powers[pw_invulnerability] == players[tab[i].num].powers[pw_sneakers]) && ((leveltime/7) & 1))
@@ -2455,10 +2469,11 @@ static void HU_Draw32TeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 		supercheck = supercheckdef;
 
 		strlcpy(name, tab[i].name, 8);
-		V_DrawString(x + 10, y,
-		             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
-		             | (greycheck ? 0 : V_TRANSLUCENT)
-		             | V_ALLOWLOWERCASE, name);
+		if (!players[tab[i].num].quittime || (leveltime / (TICRATE/2) & 1))
+			V_DrawString(x + 10, y,
+			             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
+			             | (greycheck ? 0 : V_TRANSLUCENT)
+			             | V_ALLOWLOWERCASE, name);
 
 		if (gametyperules & GTR_TEAMFLAGS)
 		{
@@ -2497,10 +2512,10 @@ static void HU_Draw32TeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 		V_DrawRightAlignedThinString(x+128, y, ((players[tab[i].num].spectator || players[tab[i].num].playerstate == PST_DEAD) ? 0 : V_TRANSLUCENT), va("%u", tab[i].count));
 		if (!splitscreen)
 		{
-			if (!(tab[i].num == serverplayer || players[tab[i].num].quittime))
-				HU_drawPing(x+ 135, y+1, playerpingtable[tab[i].num], true, 0);
-		//else
-			//V_DrawSmallString(x+ 129, y+4, V_YELLOWMAP, "HOST");
+			if (tab[i].num != serverplayer)
+				HU_drawPing(x + 135, y+1, players[tab[i].num].quittime ? UINT32_MAX : playerpingtable[tab[i].num], true, 0);
+			//else
+				//V_DrawSmallString(x+ 129, y+4, V_YELLOWMAP, "HOST");
 		}
 	}
 }
@@ -2583,10 +2598,11 @@ void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 		supercheck = supercheckdef;
 
 		strlcpy(name, tab[i].name, 7);
-		V_DrawString(x + 20, y,
-		             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
-		             | (greycheck ? V_TRANSLUCENT : 0)
-		             | V_ALLOWLOWERCASE, name);
+		if (!players[tab[i].num].quittime || (leveltime / (TICRATE/2) & 1))
+			V_DrawString(x + 20, y,
+			             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
+			             | (greycheck ? V_TRANSLUCENT : 0)
+			             | V_ALLOWLOWERCASE, name);
 
 		if (gametyperules & GTR_TEAMFLAGS)
 		{
@@ -2621,10 +2637,10 @@ void HU_DrawTeamTabRankings(playersort_t *tab, INT32 whiteplayer)
 		V_DrawRightAlignedThinString(x+100, y, (greycheck ? V_TRANSLUCENT : 0), va("%u", tab[i].count));
 		if (!splitscreen)
 		{
-			if (!(tab[i].num == serverplayer || players[tab[i].num].quittime))
-				HU_drawPing(x+ 113, y, playerpingtable[tab[i].num], false, 0);
-		//else
-		//	V_DrawSmallString(x+ 94, y+4, V_YELLOWMAP, "SERVER");
+			if (tab[i].num != serverplayer)
+				HU_drawPing(x+ 113, y, players[tab[i].num].quittime ? UINT32_MAX : playerpingtable[tab[i].num], false, 0);
+			//else
+			//	V_DrawSmallString(x+ 94, y+4, V_YELLOWMAP, "SERVER");
 		}
 	}
 }
@@ -2652,15 +2668,16 @@ void HU_DrawDualTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scoreline
 		supercheck = supercheckdef;
 
 		strlcpy(name, tab[i].name, 7);
-		if (!(tab[i].num == serverplayer || players[tab[i].num].quittime))
-			HU_drawPing(x+ 113, y, playerpingtable[tab[i].num], false, 0);
+		if (tab[i].num != serverplayer)
+			HU_drawPing(x+ 113, y, players[tab[i].num].quittime ? UINT32_MAX : playerpingtable[tab[i].num], false, 0);
 		//else
 		//	V_DrawSmallString(x+ 94, y+4, V_YELLOWMAP, "SERVER");
 
-		V_DrawString(x + 20, y,
-		             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
-		             | (greycheck ? V_TRANSLUCENT : 0)
-		             | V_ALLOWLOWERCASE, name);
+		if (!players[tab[i].num].quittime || (leveltime / (TICRATE/2) & 1))
+			V_DrawString(x + 20, y,
+			             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
+			             | (greycheck ? V_TRANSLUCENT : 0)
+			             | V_ALLOWLOWERCASE, name);
 
 		if (G_GametypeUsesLives() && !(G_GametypeUsesCoopLives() && (cv_cooplives.value == 0 || cv_cooplives.value == 3)) && (players[tab[i].num].lives != INFLIVES)) //show lives
 			V_DrawRightAlignedString(x, y+4, V_ALLOWLOWERCASE, va("%dx", players[tab[i].num].lives));
@@ -2760,16 +2777,17 @@ static void HU_Draw32TabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scor
 		strlcpy(name, tab[i].name, 7);
 		if (!splitscreen) // don't draw it on splitscreen,
 		{
-			if (!(tab[i].num == serverplayer || players[tab[i].num].quittime))
-				HU_drawPing(x+ 135, y+1, playerpingtable[tab[i].num], true, 0);
-		//else
-		//	V_DrawSmallString(x+ 129, y+4, V_YELLOWMAP, "HOST");
+			if (tab[i].num != serverplayer)
+				HU_drawPing(x+ 135, y+1, players[tab[i].num].quittime ? UINT32_MAX : playerpingtable[tab[i].num], true, 0);
+			//else
+			//	V_DrawSmallString(x+ 129, y+4, V_YELLOWMAP, "HOST");
 		}
 
-		V_DrawString(x + 10, y,
-		             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
-		             | (greycheck ? 0 : V_TRANSLUCENT)
-		             | V_ALLOWLOWERCASE, name);
+		if (!players[tab[i].num].quittime || (leveltime / (TICRATE/2) & 1))
+			V_DrawString(x + 10, y,
+			             ((tab[i].num == whiteplayer) ? V_YELLOWMAP : 0)
+			             | (greycheck ? 0 : V_TRANSLUCENT)
+			             | V_ALLOWLOWERCASE, name);
 
 		if (G_GametypeUsesLives()) //show lives
 			V_DrawRightAlignedThinString(x-1, y, V_ALLOWLOWERCASE, va("%d", players[tab[i].num].lives));
diff --git a/src/hu_stuff.h b/src/hu_stuff.h
index 63d85f1b81a7637579a1b510a2d979f79368281c..9b7cee2d3053cb63138a08d32dcfb75565ee537e 100644
--- a/src/hu_stuff.h
+++ b/src/hu_stuff.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/i_addrinfo.c b/src/i_addrinfo.c
index e77774549b4b572aa6f61557b3a5286347c077c2..5dcea100299805644612e5773392062ebca3c3cd 100644
--- a/src/i_addrinfo.c
+++ b/src/i_addrinfo.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2011-2020 by Sonic Team Junior.
+// Copyright (C) 2011-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -20,7 +20,7 @@
 #else
 #include <winsock.h>
 #endif
-#elif !defined (__DJGPP__)
+#else
 #include <sys/socket.h>
 #include <arpa/inet.h>
 #include <netdb.h>
diff --git a/src/i_addrinfo.h b/src/i_addrinfo.h
index 7ae0067195d6f46cd052200ebc109b7905d37e70..397a1969d94c866a10e4030abb74df2d73e4e5b4 100644
--- a/src/i_addrinfo.h
+++ b/src/i_addrinfo.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2011-2020 by Sonic Team Junior.
+// Copyright (C) 2011-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/i_joy.h b/src/i_joy.h
index 2a2797fc4045c95ce2a3207a8d4caf72a9923fcb..0c7c8dd3f45003909c956c536653bcdfb247f333 100644
--- a/src/i_joy.h
+++ b/src/i_joy.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/i_net.h b/src/i_net.h
index 5d93f191e5ade02c551384a0624f2a6698abc702..dbc82db65cd94480277e03e23222741741524d48 100644
--- a/src/i_net.h
+++ b/src/i_net.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/i_sound.h b/src/i_sound.h
index d45c0b323ef4ca34ea936e49b8e598471eb9d290..e38a17626b95dac976295a27c53e5583ce4043fb 100644
--- a/src/i_sound.h
+++ b/src/i_sound.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/i_system.h b/src/i_system.h
index dd0b65f6df542d228019f5c3c91d59c5bcd6728c..a2dd81cca3ef815ec6121d09b64308f54c6adc3e 100644
--- a/src/i_system.h
+++ b/src/i_system.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -46,7 +46,13 @@ UINT32 I_GetFreeMem(UINT32 *total);
 */
 tic_t I_GetTime(void);
 
-int I_GetTimeMicros(void);// provides microsecond counter for render stats
+/**	\brief	Returns precise time value for performance measurement.
+  */
+precise_t I_GetPreciseTime(void);
+
+/**	\brief	Converts a precise_t to microseconds and casts it to a 32 bit integer.
+  */
+int I_PreciseToMicros(precise_t);
 
 /**	\brief	The I_Sleep function
 
@@ -308,4 +314,12 @@ const char *I_ClipboardPaste(void);
 
 void I_RegisterSysCommands(void);
 
+/** \brief Return the position of the cursor relative to the top-left window corner.
+*/
+void I_GetCursorPosition(INT32 *x, INT32 *y);
+
+/** \brief Sets whether the mouse is grabbed
+*/
+void I_SetMouseGrab(boolean grab);
+
 #endif
diff --git a/src/i_tcp.c b/src/i_tcp.c
index 5180869a53b60772ded94d18495bd17906cef137..cae97a7d1039349aa570272fe5443d057e4be949 100644
--- a/src/i_tcp.c
+++ b/src/i_tcp.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -20,127 +20,112 @@
 #endif
 
 #ifndef NO_IPV6
-#define HAVE_IPV6
+	#define HAVE_IPV6
 #endif
 
 #ifdef _WIN32
-#define USE_WINSOCK
-#if defined (_WIN64) || defined (HAVE_IPV6)
-#define USE_WINSOCK2
-#else //_WIN64/HAVE_IPV6
-#define USE_WINSOCK1
-#endif
+	#define USE_WINSOCK
+	#if defined (_WIN64) || defined (HAVE_IPV6)
+		#define USE_WINSOCK2
+	#else //_WIN64/HAVE_IPV6
+		#define USE_WINSOCK1
+	#endif
 #endif //WIN32 OS
 
 #ifdef USE_WINSOCK2
-#include <ws2tcpip.h>
+	#include <ws2tcpip.h>
 #endif
 
 #include "doomdef.h"
 
 #if defined (NOMD5) && !defined (NONET)
-//#define NONET
+	//#define NONET
 #endif
 
 #ifdef NONET
-#undef HAVE_MINIUPNPC
+	#undef HAVE_MINIUPNPC
 #else
-#ifdef USE_WINSOCK1
-#include <winsock.h>
-#elif !defined (SCOUW2) && !defined (SCOUW7)
-#ifndef USE_WINSOCK
-#include <arpa/inet.h>
-#endif //normal BSD API
-
-#ifndef USE_WINSOCK
-#ifdef __APPLE_CC__
-#ifndef _BSD_SOCKLEN_T_
-#define _BSD_SOCKLEN_T_
-#endif //_BSD_SOCKLEN_T_
-#endif //__APPLE_CC__
-#include <sys/socket.h>
-#include <netinet/in.h>
-#endif //normal BSD API
-
-#ifndef USE_WINSOCK
-#include <netdb.h>
-#include <sys/ioctl.h>
-#endif //normal BSD API
-
-#include <errno.h>
-#include <time.h>
-
-#if (defined (__unix__) && !defined (MSDOS)) || defined(__APPLE__) || defined (UNIXCOMMON)
-	#include <sys/time.h>
-#endif // UNIXCOMMON
-#endif // !NONET
-
-#ifdef USE_WINSOCK
-	// some undefined under win32
-	#undef errno
-	//#define errno WSAGetLastError() //Alam_GBC: this is the correct way, right?
-	#define errno h_errno // some very strange things happen when not using h_error?!?
-	#ifdef EWOULDBLOCK
-	#undef EWOULDBLOCK
-	#endif
-	#define EWOULDBLOCK WSAEWOULDBLOCK
-	#ifdef EMSGSIZE
-	#undef EMSGSIZE
-	#endif
-	#define EMSGSIZE WSAEMSGSIZE
-	#ifdef ECONNREFUSED
-	#undef ECONNREFUSED
-	#endif
-	#define ECONNREFUSED WSAECONNREFUSED
-	#ifdef ETIMEDOUT
-	#undef ETIMEDOUT
+	#ifdef USE_WINSOCK1
+		#include <winsock.h>
+	#else
+		#ifndef USE_WINSOCK
+			#include <arpa/inet.h>
+			#ifdef __APPLE_CC__
+				#ifndef _BSD_SOCKLEN_T_
+					#define _BSD_SOCKLEN_T_
+				#endif //_BSD_SOCKLEN_T_
+			#endif //__APPLE_CC__
+			#include <sys/socket.h>
+			#include <netinet/in.h>
+			#include <netdb.h>
+			#include <sys/ioctl.h>
+		#endif //normal BSD API
+
+		#include <errno.h>
+		#include <time.h>
+
+		#if defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)
+			#include <sys/time.h>
+		#endif // UNIXCOMMON
 	#endif
-	#define ETIMEDOUT WSAETIMEDOUT
-	#ifndef IOC_VENDOR
-	#define IOC_VENDOR 0x18000000
-	#endif
-	#ifndef _WSAIOW
-	#define _WSAIOW(x,y) (IOC_IN|(x)|(y))
-	#endif
-	#ifndef SIO_UDP_CONNRESET
-	#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR,12)
-	#endif
-	#ifndef AI_ADDRCONFIG
-	#define AI_ADDRCONFIG 0x00000400
-	#endif
-	#ifndef STATUS_INVALID_PARAMETER
-	#define STATUS_INVALID_PARAMETER 0xC000000D
-	#endif
-#endif
 
-#ifdef __DJGPP__
-#ifdef WATTCP // Alam_GBC: Wattcp may need this
-#include <tcp.h>
-#define strerror strerror_s
-#else // wattcp
-#include <lsck/lsck.h>
-#endif // libsocket
-#endif // djgpp
-
-typedef union
-{
-	struct sockaddr     any;
-	struct sockaddr_in  ip4;
-#ifdef HAVE_IPV6
-	struct sockaddr_in6 ip6;
-#endif
-} mysockaddr_t;
-
-#ifdef HAVE_MINIUPNPC
-#ifdef STATIC_MINIUPNPC
-#define STATICLIB
-#endif
-#include "miniupnpc/miniwget.h"
-#include "miniupnpc/miniupnpc.h"
-#include "miniupnpc/upnpcommands.h"
-#undef STATICLIB
-static UINT8 UPNP_support = TRUE;
-#endif
+	#ifdef USE_WINSOCK
+		// some undefined under win32
+		#undef errno
+		//#define errno WSAGetLastError() //Alam_GBC: this is the correct way, right?
+		#define errno h_errno // some very strange things happen when not using h_error?!?
+		#ifdef EWOULDBLOCK
+		#undef EWOULDBLOCK
+		#endif
+		#define EWOULDBLOCK WSAEWOULDBLOCK
+		#ifdef EMSGSIZE
+		#undef EMSGSIZE
+		#endif
+		#define EMSGSIZE WSAEMSGSIZE
+		#ifdef ECONNREFUSED
+		#undef ECONNREFUSED
+		#endif
+		#define ECONNREFUSED WSAECONNREFUSED
+		#ifdef ETIMEDOUT
+		#undef ETIMEDOUT
+		#endif
+		#define ETIMEDOUT WSAETIMEDOUT
+		#ifndef IOC_VENDOR
+		#define IOC_VENDOR 0x18000000
+		#endif
+		#ifndef _WSAIOW
+		#define _WSAIOW(x,y) (IOC_IN|(x)|(y))
+		#endif
+		#ifndef SIO_UDP_CONNRESET
+		#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR,12)
+		#endif
+		#ifndef AI_ADDRCONFIG
+		#define AI_ADDRCONFIG 0x00000400
+		#endif
+		#ifndef STATUS_INVALID_PARAMETER
+		#define STATUS_INVALID_PARAMETER 0xC000000D
+		#endif
+	#endif // USE_WINSOCK
+
+	typedef union
+	{
+		struct sockaddr     any;
+		struct sockaddr_in  ip4;
+	#ifdef HAVE_IPV6
+		struct sockaddr_in6 ip6;
+	#endif
+	} mysockaddr_t;
+
+	#ifdef HAVE_MINIUPNPC
+		#ifdef STATIC_MINIUPNPC
+			#define STATICLIB
+		#endif
+		#include "miniupnpc/miniwget.h"
+		#include "miniupnpc/miniupnpc.h"
+		#include "miniupnpc/upnpcommands.h"
+		#undef STATICLIB
+		static UINT8 UPNP_support = TRUE;
+	#endif // HAVE_MINIUPNC
 
 #endif // !NONET
 
@@ -155,54 +140,44 @@ static UINT8 UPNP_support = TRUE;
 
 #include "doomstat.h"
 
-// win32 or djgpp
-#if defined (USE_WINSOCK) || defined (__DJGPP__)
+// win32
+#ifdef USE_WINSOCK
 	// winsock stuff (in winsock a socket is not a file)
 	#define ioctl ioctlsocket
 	#define close closesocket
 #endif
 
 #include "i_addrinfo.h"
-
-#ifdef __DJGPP__
-
-#ifdef WATTCP
-#define SELECTTEST
-#endif
-
-#else
 #define SELECTTEST
-#endif
-
 #define DEFAULTPORT "5029"
 
 #if defined (USE_WINSOCK) && !defined (NONET)
-typedef SOCKET SOCKET_TYPE;
-#define ERRSOCKET (SOCKET_ERROR)
+	typedef SOCKET SOCKET_TYPE;
+	#define ERRSOCKET (SOCKET_ERROR)
 #else
-#if (defined (__unix__) && !defined (MSDOS)) || defined (__APPLE__) || defined (__HAIKU__)
-typedef int SOCKET_TYPE;
-#else
-typedef unsigned long SOCKET_TYPE;
-#endif
-#define ERRSOCKET (-1)
-#endif
-
-#if (defined (WATTCP) && !defined (__libsocket_socklen_t)) || defined (USE_WINSOCK1)
-typedef int socklen_t;
+	#if defined (__unix__) || defined (__APPLE__) || defined (__HAIKU__)
+		typedef int SOCKET_TYPE;
+	#else
+		typedef unsigned long SOCKET_TYPE;
+	#endif
+	#define ERRSOCKET (-1)
 #endif
 
 #ifndef NONET
-static SOCKET_TYPE mysockets[MAXNETNODES+1] = {ERRSOCKET};
-static size_t mysocketses = 0;
-static int myfamily[MAXNETNODES+1] = {0};
-static SOCKET_TYPE nodesocket[MAXNETNODES+1] = {ERRSOCKET};
-static mysockaddr_t clientaddress[MAXNETNODES+1];
-static mysockaddr_t broadcastaddress[MAXNETNODES+1];
-static size_t broadcastaddresses = 0;
-static boolean nodeconnected[MAXNETNODES+1];
-static mysockaddr_t banned[MAXBANS];
-static UINT8 bannedmask[MAXBANS];
+	// define socklen_t in DOS/Windows if it is not already defined
+	#ifdef USE_WINSOCK1
+		typedef int socklen_t;
+	#endif
+	static SOCKET_TYPE mysockets[MAXNETNODES+1] = {ERRSOCKET};
+	static size_t mysocketses = 0;
+	static int myfamily[MAXNETNODES+1] = {0};
+	static SOCKET_TYPE nodesocket[MAXNETNODES+1] = {ERRSOCKET};
+	static mysockaddr_t clientaddress[MAXNETNODES+1];
+	static mysockaddr_t broadcastaddress[MAXNETNODES+1];
+	static size_t broadcastaddresses = 0;
+	static boolean nodeconnected[MAXNETNODES+1];
+	static mysockaddr_t banned[MAXBANS];
+	static UINT8 bannedmask[MAXBANS];
 #endif
 
 static size_t numbans = 0;
@@ -213,19 +188,6 @@ static const char *serverport_name = DEFAULTPORT;
 static const char *clientport_name;/* any port */
 
 #ifndef NONET
-
-#ifdef WATTCP
-static void wattcp_outch(char s)
-{
-	static char old = '\0';
-	char pr[2] = {s,0};
-	if (s == old && old == ' ') return;
-	else old = s;
-	if (s == '\r') CONS_Printf("\n");
-	else if (s != '\n') CONS_Printf(pr);
-}
-#endif
-
 #ifdef USE_WINSOCK
 // stupid microsoft makes things complicated
 static char *get_WSAErrorStr(int e)
@@ -770,11 +732,7 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 	int opt;
 	socklen_t opts;
 #ifdef FIONBIO
-#ifdef WATTCP
-	char trueval = true;
-#else
 	unsigned long trueval = true;
-#endif
 #endif
 	mysockaddr_t straddr;
 	struct sockaddr_in sin;
@@ -1144,61 +1102,7 @@ boolean I_InitTcpDriver(void)
 		CONS_Debug(DBG_NETPLAY, "WinSock description: %s\n",WSAData.szDescription);
 		CONS_Debug(DBG_NETPLAY, "WinSock System Status: %s\n",WSAData.szSystemStatus);
 #endif
-#ifdef __DJGPP__
-#ifdef WATTCP // Alam_GBC: survive bootp, dhcp, rarp and wattcp/pktdrv from failing to load
-		survive_eth   = 1; // would be needed to not exit if pkt_eth_init() fails
-		survive_bootp = 1; // ditto for BOOTP
-		survive_dhcp  = 1; // ditto for DHCP/RARP
-		survive_rarp  = 1;
-		//_watt_do_exit = false;
-		//_watt_handle_cbreak = false;
-		//_watt_no_config = true;
-		_outch = wattcp_outch;
-		init_misc();
-//#ifdef DEBUGFILE
-		dbug_init();
-//#endif
-		switch (sock_init())
-		{
-			case 0:
-				init_tcp_driver = true;
-				break;
-			case 3:
-				CONS_Debug(DBG_NETPLAY, "No packet driver detected\n");
-				break;
-			case 4:
-				CONS_Debug(DBG_NETPLAY, "Error while talking to packet driver\n");
-				break;
-			case 5:
-				CONS_Debug(DBG_NETPLAY, "BOOTP failed\n");
-				break;
-			case 6:
-				CONS_Debug(DBG_NETPLAY, "DHCP failed\n");
-				break;
-			case 7:
-				CONS_Debug(DBG_NETPLAY, "RARP failed\n");
-				break;
-			case 8:
-				CONS_Debug(DBG_NETPLAY, "TCP/IP failed\n");
-				break;
-			case 9:
-				CONS_Debug(DBG_NETPLAY, "PPPoE login/discovery failed\n");
-				break;
-			default:
-				CONS_Debug(DBG_NETPLAY, "Unknown error with TCP/IP stack\n");
-				break;
-		}
-		hires_timer(0);
-#else // wattcp
-		if (__lsck_init())
-			init_tcp_driver = true;
-		else
-			CONS_Debug(DBG_NETPLAY, "No TCP/IP driver detected\n");
-#endif // libsocket
-#endif // __DJGPP__
-#ifndef __DJGPP__
 		init_tcp_driver = true;
-#endif
 	}
 #endif
 	if (!tcp_was_up && init_tcp_driver)
@@ -1223,10 +1127,8 @@ static void SOCK_CloseSocket(void)
 		if (mysockets[i] != (SOCKET_TYPE)ERRSOCKET
 		 && FD_ISSET(mysockets[i], &masterset))
 		{
-#if !defined (__DJGPP__) || defined (WATTCP)
 			FD_CLR(mysockets[i], &masterset);
 			close(mysockets[i]);
-#endif
 		}
 		mysockets[i] = ERRSOCKET;
 	}
@@ -1243,14 +1145,6 @@ void I_ShutdownTcpDriver(void)
 	WS_addrinfocleanup();
 	WSACleanup();
 #endif
-#ifdef __DJGPP__
-#ifdef WATTCP // wattcp
-	//_outch = NULL;
-	sock_exit();
-#else
-	__lsck_uninit();
-#endif // libsocket
-#endif // __DJGPP__
 	CONS_Printf("shut down\n");
 	init_tcp_driver = false;
 #endif
diff --git a/src/i_tcp.h b/src/i_tcp.h
index 738b8b4d14c9cbd13d8cd8299d1dc560e7cadc78..7857344156448712b934b0989b242e2e6fc554da 100644
--- a/src/i_tcp.h
+++ b/src/i_tcp.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/i_threads.h b/src/i_threads.h
index ecb9fce6715f3b8c40cc2bd37a0b3caa0d07101b..bc752181f521c447d736368cb0c17b09ae998572 100644
--- a/src/i_threads.h
+++ b/src/i_threads.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020 by James R.
+// Copyright (C) 2020-2021 by James R.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/i_video.h b/src/i_video.h
index ab48881d4405036b515ff65988a81bab89e7236a..2d07fcf10700973e4f3aad0b15e9566101ef9ec3 100644
--- a/src/i_video.h
+++ b/src/i_video.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/info.c b/src/info.c
index 29a79b1d6e28ab0e9b1352b0338d9dacc2a06d32..f56e5d78e3e786b33806a1a596f0051a182144fd 100644
--- a/src/info.c
+++ b/src/info.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -150,6 +150,7 @@ char sprnames[NUMSPRITES + 1][5] =
 	"SIGN", // Level end sign
 	"SPIK", // Spike Ball
 	"SFLM", // Spin fire
+	"TFLM", // Spin fire (team)
 	"USPK", // Floor spike
 	"WSPK", // Wall spike
 	"WSPB", // Wall spike base
@@ -1894,6 +1895,13 @@ state_t states[NUMSTATES] =
 	{SPR_SFLM, FF_FULLBRIGHT|4, 2, {NULL}, 0, 0, S_SPINFIRE6}, // S_SPINFIRE5
 	{SPR_SFLM, FF_FULLBRIGHT|5, 2, {NULL}, 0, 0, S_SPINFIRE1}, // S_SPINFIRE6
 
+	{SPR_TFLM, FF_FULLBRIGHT,   2, {NULL}, 0, 0, S_TEAM_SPINFIRE2}, // S_TEAM_SPINFIRE1
+	{SPR_TFLM, FF_FULLBRIGHT|1, 2, {NULL}, 0, 0, S_TEAM_SPINFIRE3}, // S_TEAM_SPINFIRE2
+	{SPR_TFLM, FF_FULLBRIGHT|2, 2, {NULL}, 0, 0, S_TEAM_SPINFIRE4}, // S_TEAM_SPINFIRE3
+	{SPR_TFLM, FF_FULLBRIGHT|3, 2, {NULL}, 0, 0, S_TEAM_SPINFIRE5}, // S_TEAM_SPINFIRE4
+	{SPR_TFLM, FF_FULLBRIGHT|4, 2, {NULL}, 0, 0, S_TEAM_SPINFIRE6}, // S_TEAM_SPINFIRE5
+	{SPR_TFLM, FF_FULLBRIGHT|5, 2, {NULL}, 0, 0, S_TEAM_SPINFIRE1}, // S_TEAM_SPINFIRE6
+
 	// Floor Spike
 	{SPR_USPK, 0,-1, {A_SpikeRetract}, 1, 0, S_SPIKE2}, // S_SPIKE1 -- Fully extended
 	{SPR_USPK, 1, 2, {A_Pain},         0, 0, S_SPIKE3}, // S_SPIKE2
@@ -2061,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
@@ -3291,18 +3299,18 @@ state_t states[NUMSTATES] =
 	{SPR_WZAP, FF_TRANS10|FF_ANIMATE|FF_RANDOMANIM, 4, {NULL}, 3, 2, S_NULL},  // S_WATERZAP
 
 	// Spindash dust
-	{SPR_DUST,            0, 7, {NULL}, 0, 0, S_SPINDUST2}, // S_SPINDUST1
-	{SPR_DUST,            1, 6, {NULL}, 0, 0, S_SPINDUST3}, // S_SPINDUST2
-	{SPR_DUST, FF_TRANS30|2, 4, {NULL}, 0, 0, S_SPINDUST4}, // S_SPINDUST3
-	{SPR_DUST, FF_TRANS60|3, 3, {NULL}, 0, 0, S_NULL}, // S_SPINDUST4
-	{SPR_BUBL,            0, 7, {NULL}, 0, 0, S_SPINDUST_BUBBLE2}, // S_SPINDUST_BUBBLE1
-	{SPR_BUBL,            0, 6, {NULL}, 0, 0, S_SPINDUST_BUBBLE3}, // S_SPINDUST_BUBBLE2
-	{SPR_BUBL, FF_TRANS30|0, 4, {NULL}, 0, 0, S_SPINDUST_BUBBLE4}, // S_SPINDUST_BUBBLE3
-	{SPR_BUBL, FF_TRANS60|0, 3, {NULL}, 0, 0, S_NULL}, // S_SPINDUST_BUBBLE4
-	{SPR_FPRT,            0, 7, {NULL}, 0, 0, S_SPINDUST_FIRE2}, // S_SPINDUST_FIRE1
-	{SPR_FPRT,            0, 6, {NULL}, 0, 0, S_SPINDUST_FIRE3}, // S_SPINDUST_FIRE2
-	{SPR_FPRT, FF_TRANS30|0, 4, {NULL}, 0, 0, S_SPINDUST_FIRE4}, // S_SPINDUST_FIRE3
-	{SPR_FPRT, FF_TRANS60|0, 3, {NULL}, 0, 0, S_NULL}, // S_SPINDUST_FIRE4
+	{SPR_DUST,                          0, 7, {NULL}, 0, 0, S_SPINDUST2}, // S_SPINDUST1
+	{SPR_DUST,                          1, 6, {NULL}, 0, 0, S_SPINDUST3}, // S_SPINDUST2
+	{SPR_DUST,               FF_TRANS30|2, 4, {NULL}, 0, 0, S_SPINDUST4}, // S_SPINDUST3
+	{SPR_DUST,               FF_TRANS60|3, 3, {NULL}, 0, 0, S_NULL}, // S_SPINDUST4
+	{SPR_BUBL,                          0, 7, {NULL}, 0, 0, S_SPINDUST_BUBBLE2}, // S_SPINDUST_BUBBLE1
+	{SPR_BUBL,                          0, 6, {NULL}, 0, 0, S_SPINDUST_BUBBLE3}, // S_SPINDUST_BUBBLE2
+	{SPR_BUBL,               FF_TRANS30|0, 4, {NULL}, 0, 0, S_SPINDUST_BUBBLE4}, // S_SPINDUST_BUBBLE3
+	{SPR_BUBL,               FF_TRANS60|0, 3, {NULL}, 0, 0, S_NULL}, // S_SPINDUST_BUBBLE4
+	{SPR_FPRT,            FF_FULLBRIGHT|0, 7, {NULL}, 0, 0, S_SPINDUST_FIRE2}, // S_SPINDUST_FIRE1
+	{SPR_FPRT,            FF_FULLBRIGHT|0, 6, {NULL}, 0, 0, S_SPINDUST_FIRE3}, // S_SPINDUST_FIRE2
+	{SPR_FPRT, FF_FULLBRIGHT|FF_TRANS30|0, 4, {NULL}, 0, 0, S_SPINDUST_FIRE4}, // S_SPINDUST_FIRE3
+	{SPR_FPRT, FF_FULLBRIGHT|FF_TRANS60|0, 3, {NULL}, 0, 0, S_NULL}, // S_SPINDUST_FIRE4
 
 
 	{SPR_TFOG, FF_FULLBRIGHT|FF_TRANS50,    2, {NULL}, 0, 0, S_FOG2},  // S_FOG1
@@ -3924,9 +3932,7 @@ state_t states[NUMSTATES] =
 	{SPR_BRIB, FF_ANIMATE|FF_RANDOMANIM, -1, {NULL}, 31, 1, S_NULL}, // S_BLUEBRICKDEBRIS
 	{SPR_BRIY, FF_ANIMATE|FF_RANDOMANIM, -1, {NULL}, 31, 1, S_NULL}, // S_YELLOWBRICKDEBRIS
 
-#ifdef SEENAMES
 	{SPR_NULL, 0, 1, {NULL}, 0, 0, S_NULL}, // S_NAMECHECK
-#endif
 };
 
 mobjinfo_t mobjinfo[NUMMOBJTYPES] =
@@ -5193,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
@@ -6458,8 +6464,8 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL,         // xdeathstate
 		sfx_fizzle,     // deathsound
 		10*FRACUNIT,    // speed
-		48*FRACUNIT,    // radius
-		160*FRACUNIT,   // height
+		24*FRACUNIT,    // radius
+		80*FRACUNIT,    // height
 		0,              // display offset
 		DMG_ELECTRIC,   // mass
 		1,              // damage
@@ -7968,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
@@ -7995,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
@@ -11424,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
@@ -11451,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
@@ -13395,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
@@ -13475,7 +13481,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		32*FRACUNIT,    // speed
 		30*FRACUNIT,    // radius
 		60*FRACUNIT,    // height
-		0,              // display offset
+		-1,             // display offset
 		100,            // mass
 		0,              // damage
 		sfx_None,       // activesound
@@ -13800,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
@@ -20374,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
@@ -21653,7 +21659,6 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL          // raisestate
 	},
 
-#ifdef SEENAMES
 	{           // MT_NAMECHECK
 		-1,             // doomednum
 		S_NAMECHECK,    // spawnstate
@@ -21680,7 +21685,6 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		MF_NOBLOCKMAP|MF_MISSILE|MF_NOGRAVITY|MF_NOSECTOR, // flags
 		S_NULL          // raisestate
 	},
-#endif
 };
 
 skincolor_t skincolors[MAXSKINCOLORS] = {
@@ -21757,7 +21761,7 @@ skincolor_t skincolors[MAXSKINCOLORS] = {
 	{"Violet",     {0xd0, 0xd1, 0xd2, 0xca, 0xcc, 0xb8, 0xb9, 0xb9, 0xba, 0xa8, 0xa8, 0xa9, 0xa9, 0xfd, 0xfe, 0xfe}, SKINCOLOR_MINT,       6,  V_MAGENTAMAP, true}, // SKINCOLOR_VIOLET
 	{"Lilac",      {0x00, 0xd0, 0xd1, 0xd2, 0xd3, 0xc1, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc5, 0xc6, 0xc6, 0xfe, 0x1f}, SKINCOLOR_VAPOR,      4,  V_ROSYMAP,    true}, // SKINCOLOR_LILAC
 	{"Plum",       {0xc8, 0xd3, 0xd5, 0xd6, 0xd7, 0xce, 0xcf, 0xb9, 0xb9, 0xba, 0xba, 0xa9, 0xa9, 0xa9, 0xfd, 0xfe}, SKINCOLOR_MINT,       7,  V_ROSYMAP,    true}, // SKINCOLOR_PLUM
-	{"Raspberry",  {0xc8, 0xc9, 0xca, 0xcb, 0xcb, 0xcc, 0xcd, 0xcd, 0xce, 0xb9, 0xb9, 0xba, 0xba, 0xbb, 0xfe, 0xfe}, SKINCOLOR_APPLE,      13, V_MAGENTAMAP, true}, // SKINCOLOR_RASPBERRY
+	{"Raspberry",  {0xc8, 0xc9, 0xca, 0xcb, 0xcb, 0xcc, 0xcd, 0xcd, 0xce, 0xb9, 0xb9, 0xba, 0xba, 0xbb, 0xfe, 0xfe}, SKINCOLOR_APPLE,      13, V_ROSYMAP,    true}, // SKINCOLOR_RASPBERRY
 	{"Rosy",       {0xfc, 0xc8, 0xc8, 0xc9, 0xc9, 0xca, 0xca, 0xcb, 0xcb, 0xcc, 0xcc, 0xcd, 0xcd, 0xce, 0xce, 0xcf}, SKINCOLOR_AQUA,       1,  V_ROSYMAP,    true}, // SKINCOLOR_ROSY
 
 	// super
diff --git a/src/info.h b/src/info.h
index 604922bebff2190e3c09296c5fff11772ba3b3e7..031a08b4316a00d135cf45a45827ff117181373e 100644
--- a/src/info.h
+++ b/src/info.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -684,6 +684,7 @@ typedef enum sprite
 	SPR_SIGN, // Level end sign
 	SPR_SPIK, // Spike Ball
 	SPR_SFLM, // Spin fire
+	SPR_TFLM, // Spin fire (team)
 	SPR_USPK, // Floor spike
 	SPR_WSPK, // Wall spike
 	SPR_WSPB, // Wall spike base
@@ -2324,6 +2325,13 @@ typedef enum state
 	S_SPINFIRE5,
 	S_SPINFIRE6,
 
+	S_TEAM_SPINFIRE1,
+	S_TEAM_SPINFIRE2,
+	S_TEAM_SPINFIRE3,
+	S_TEAM_SPINFIRE4,
+	S_TEAM_SPINFIRE5,
+	S_TEAM_SPINFIRE6,
+
 	// Spikes
 	S_SPIKE1,
 	S_SPIKE2,
@@ -4280,9 +4288,7 @@ typedef enum state
 	S_BLUEBRICKDEBRIS, // for CEZ3
 	S_YELLOWBRICKDEBRIS, // for CEZ3
 
-#ifdef SEENAMES
 	S_NAMECHECK,
-#endif
 
 	S_FIRSTFREESLOT,
 	S_LASTFREESLOT = S_FIRSTFREESLOT + NUMSTATEFREESLOTS - 1,
@@ -5082,9 +5088,7 @@ typedef enum mobj_type
 	MT_BLUEBRICKDEBRIS, // for CEZ3
 	MT_YELLOWBRICKDEBRIS, // for CEZ3
 
-#ifdef SEENAMES
 	MT_NAMECHECK,
-#endif
 
 	MT_FIRSTFREESLOT,
 	MT_LASTFREESLOT = MT_FIRSTFREESLOT + NUMMOBJFREESLOTS - 1,
diff --git a/src/keys.h b/src/keys.h
index 6cdd7956c4f28d7da6141f0744733778b00a2e2e..b19259320e59574effaa3b48ba7a713ac7af0c86 100644
--- a/src/keys.h
+++ b/src/keys.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/libdivide.h b/src/libdivide.h
new file mode 100644
index 0000000000000000000000000000000000000000..1a589c7e5508957b0c4a8e15d37cd02c0402f349
--- /dev/null
+++ b/src/libdivide.h
@@ -0,0 +1,2111 @@
+// libdivide.h - Optimized integer division
+// https://libdivide.com
+//
+// Copyright (C) 2010 - 2019 ridiculous_fish, <libdivide@ridiculousfish.com>
+// Copyright (C) 2016 - 2019 Kim Walisch, <kim.walisch@gmail.com>
+//
+// libdivide is dual-licensed under the Boost or zlib licenses.
+// You may use libdivide under the terms of either of these.
+// See LICENSE.txt in the libdivide source code repository for more details.
+
+
+// NOTICE: This is an altered source version of libdivide.
+// Libdivide is used here under the terms of the zlib license.
+// Here is the zlib license text from https://github.com/ridiculousfish/libdivide/blob/master/LICENSE.txt
+/*
+  zlib License
+  ------------
+
+  Copyright (C) 2010 - 2019 ridiculous_fish, <libdivide@ridiculousfish.com>
+  Copyright (C) 2016 - 2019 Kim Walisch, <kim.walisch@gmail.com>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+
+// This version of libdivide has been modified for use with SRB2.
+// Changes made include:
+//     - unused parts commented out (to avoid the need to fix C90 compilation issues with them)
+//     - C90 compilation issues fixed with used parts
+//     - use I_Error for errors
+
+#ifndef LIBDIVIDE_H
+#define LIBDIVIDE_H
+
+#define LIBDIVIDE_VERSION "3.0"
+#define LIBDIVIDE_VERSION_MAJOR 3
+#define LIBDIVIDE_VERSION_MINOR 0
+
+#include <stdint.h>
+
+#if defined(__cplusplus)
+    #include <cstdlib>
+    #include <cstdio>
+    #include <type_traits>
+#else
+    #include <stdlib.h>
+    #include <stdio.h>
+#endif
+
+#if defined(LIBDIVIDE_AVX512)
+    #include <immintrin.h>
+#elif defined(LIBDIVIDE_AVX2)
+    #include <immintrin.h>
+#elif defined(LIBDIVIDE_SSE2)
+    #include <emmintrin.h>
+#endif
+
+#if defined(_MSC_VER)
+    #include <intrin.h>
+    // disable warning C4146: unary minus operator applied
+    // to unsigned type, result still unsigned
+    #pragma warning(disable: 4146)
+    #define LIBDIVIDE_VC
+#endif
+
+#if !defined(__has_builtin)
+    #define __has_builtin(x) 0
+#endif
+
+#if defined(__SIZEOF_INT128__)
+    #define HAS_INT128_T
+    // clang-cl on Windows does not yet support 128-bit division
+    #if !(defined(__clang__) && defined(LIBDIVIDE_VC))
+        #define HAS_INT128_DIV
+    #endif
+#endif
+
+#if defined(__x86_64__) || defined(_M_X64)
+    #define LIBDIVIDE_X86_64
+#endif
+
+#if defined(__i386__)
+    #define LIBDIVIDE_i386
+#endif
+
+#if defined(__GNUC__) || defined(__clang__)
+    #define LIBDIVIDE_GCC_STYLE_ASM
+#endif
+
+#if defined(__cplusplus) || defined(LIBDIVIDE_VC)
+    #define LIBDIVIDE_FUNCTION __FUNCTION__
+#else
+    #define LIBDIVIDE_FUNCTION __func__
+#endif
+
+#define LIBDIVIDE_ERROR(msg) \
+    I_Error("libdivide.h:%d: %s(): Error: %s\n", \
+        __LINE__, LIBDIVIDE_FUNCTION, msg);
+
+#if defined(LIBDIVIDE_ASSERTIONS_ON)
+    #define LIBDIVIDE_ASSERT(x) \
+        if (!(x)) { \
+            I_Error("libdivide.h:%d: %s(): Assertion failed: %s\n", \
+                __LINE__, LIBDIVIDE_FUNCTION, #x); \
+        }
+#else
+    #define LIBDIVIDE_ASSERT(x)
+#endif
+
+#ifdef __cplusplus
+namespace libdivide {
+#endif
+
+// pack divider structs to prevent compilers from padding.
+// This reduces memory usage by up to 43% when using a large
+// array of libdivide dividers and improves performance
+// by up to 10% because of reduced memory bandwidth.
+#pragma pack(push, 1)
+
+struct libdivide_u32_t {
+    uint32_t magic;
+    uint8_t more;
+};
+
+struct libdivide_s32_t {
+    int32_t magic;
+    uint8_t more;
+};
+
+struct libdivide_u64_t {
+    uint64_t magic;
+    uint8_t more;
+};
+
+struct libdivide_s64_t {
+    int64_t magic;
+    uint8_t more;
+};
+
+struct libdivide_u32_branchfree_t {
+    uint32_t magic;
+    uint8_t more;
+};
+
+struct libdivide_s32_branchfree_t {
+    int32_t magic;
+    uint8_t more;
+};
+
+struct libdivide_u64_branchfree_t {
+    uint64_t magic;
+    uint8_t more;
+};
+
+struct libdivide_s64_branchfree_t {
+    int64_t magic;
+    uint8_t more;
+};
+
+#pragma pack(pop)
+
+// Explanation of the "more" field:
+//
+// * Bits 0-5 is the shift value (for shift path or mult path).
+// * Bit 6 is the add indicator for mult path.
+// * Bit 7 is set if the divisor is negative. We use bit 7 as the negative
+//   divisor indicator so that we can efficiently use sign extension to
+//   create a bitmask with all bits set to 1 (if the divisor is negative)
+//   or 0 (if the divisor is positive).
+//
+// u32: [0-4] shift value
+//      [5] ignored
+//      [6] add indicator
+//      magic number of 0 indicates shift path
+//
+// s32: [0-4] shift value
+//      [5] ignored
+//      [6] add indicator
+//      [7] indicates negative divisor
+//      magic number of 0 indicates shift path
+//
+// u64: [0-5] shift value
+//      [6] add indicator
+//      magic number of 0 indicates shift path
+//
+// s64: [0-5] shift value
+//      [6] add indicator
+//      [7] indicates negative divisor
+//      magic number of 0 indicates shift path
+//
+// In s32 and s64 branchfree modes, the magic number is negated according to
+// whether the divisor is negated. In branchfree strategy, it is not negated.
+
+enum {
+    LIBDIVIDE_32_SHIFT_MASK = 0x1F,
+    LIBDIVIDE_64_SHIFT_MASK = 0x3F,
+    LIBDIVIDE_ADD_MARKER = 0x40,
+    LIBDIVIDE_NEGATIVE_DIVISOR = 0x80
+};
+
+//static inline struct libdivide_s32_t libdivide_s32_gen(int32_t d);
+static inline struct libdivide_u32_t libdivide_u32_gen(uint32_t d);
+//static inline struct libdivide_s64_t libdivide_s64_gen(int64_t d);
+//static inline struct libdivide_u64_t libdivide_u64_gen(uint64_t d);
+
+/*static inline struct libdivide_s32_branchfree_t libdivide_s32_branchfree_gen(int32_t d);
+static inline struct libdivide_u32_branchfree_t libdivide_u32_branchfree_gen(uint32_t d);
+static inline struct libdivide_s64_branchfree_t libdivide_s64_branchfree_gen(int64_t d);
+static inline struct libdivide_u64_branchfree_t libdivide_u64_branchfree_gen(uint64_t d);*/
+
+//static inline int32_t  libdivide_s32_do(int32_t numer, const struct libdivide_s32_t *denom);
+static inline uint32_t libdivide_u32_do(uint32_t numer, const struct libdivide_u32_t *denom);
+//static inline int64_t  libdivide_s64_do(int64_t numer, const struct libdivide_s64_t *denom);
+//static inline uint64_t libdivide_u64_do(uint64_t numer, const struct libdivide_u64_t *denom);
+
+/*static inline int32_t  libdivide_s32_branchfree_do(int32_t numer, const struct libdivide_s32_branchfree_t *denom);
+static inline uint32_t libdivide_u32_branchfree_do(uint32_t numer, const struct libdivide_u32_branchfree_t *denom);
+static inline int64_t  libdivide_s64_branchfree_do(int64_t numer, const struct libdivide_s64_branchfree_t *denom);
+static inline uint64_t libdivide_u64_branchfree_do(uint64_t numer, const struct libdivide_u64_branchfree_t *denom);*/
+
+/*static inline int32_t  libdivide_s32_recover(const struct libdivide_s32_t *denom);
+static inline uint32_t libdivide_u32_recover(const struct libdivide_u32_t *denom);
+static inline int64_t  libdivide_s64_recover(const struct libdivide_s64_t *denom);
+static inline uint64_t libdivide_u64_recover(const struct libdivide_u64_t *denom);*/
+
+/*static inline int32_t  libdivide_s32_branchfree_recover(const struct libdivide_s32_branchfree_t *denom);
+static inline uint32_t libdivide_u32_branchfree_recover(const struct libdivide_u32_branchfree_t *denom);
+static inline int64_t  libdivide_s64_branchfree_recover(const struct libdivide_s64_branchfree_t *denom);
+static inline uint64_t libdivide_u64_branchfree_recover(const struct libdivide_u64_branchfree_t *denom);*/
+
+//////// Internal Utility Functions
+
+static inline uint32_t libdivide_mullhi_u32(uint32_t x, uint32_t y) {
+    uint64_t xl = x, yl = y;
+    uint64_t rl = xl * yl;
+    return (uint32_t)(rl >> 32);
+}
+
+static inline int32_t libdivide_mullhi_s32(int32_t x, int32_t y) {
+    int64_t xl = x, yl = y;
+    int64_t rl = xl * yl;
+    // needs to be arithmetic shift
+    return (int32_t)(rl >> 32);
+}
+
+static inline uint64_t libdivide_mullhi_u64(uint64_t x, uint64_t y) {
+#if defined(LIBDIVIDE_VC) && \
+    defined(LIBDIVIDE_X86_64)
+    return __umulh(x, y);
+#elif defined(HAS_INT128_T)
+    __uint128_t xl = x, yl = y;
+    __uint128_t rl = xl * yl;
+    return (uint64_t)(rl >> 64);
+#else
+    // full 128 bits are x0 * y0 + (x0 * y1 << 32) + (x1 * y0 << 32) + (x1 * y1 << 64)
+    uint32_t mask = 0xFFFFFFFF;
+    uint32_t x0 = (uint32_t)(x & mask);
+    uint32_t x1 = (uint32_t)(x >> 32);
+    uint32_t y0 = (uint32_t)(y & mask);
+    uint32_t y1 = (uint32_t)(y >> 32);
+    uint32_t x0y0_hi = libdivide_mullhi_u32(x0, y0);
+    uint64_t x0y1 = x0 * (uint64_t)y1;
+    uint64_t x1y0 = x1 * (uint64_t)y0;
+    uint64_t x1y1 = x1 * (uint64_t)y1;
+    uint64_t temp = x1y0 + x0y0_hi;
+    uint64_t temp_lo = temp & mask;
+    uint64_t temp_hi = temp >> 32;
+
+    return x1y1 + temp_hi + ((temp_lo + x0y1) >> 32);
+#endif
+}
+
+static inline int64_t libdivide_mullhi_s64(int64_t x, int64_t y) {
+#if defined(LIBDIVIDE_VC) && \
+    defined(LIBDIVIDE_X86_64)
+    return __mulh(x, y);
+#elif defined(HAS_INT128_T)
+    __int128_t xl = x, yl = y;
+    __int128_t rl = xl * yl;
+    return (int64_t)(rl >> 64);
+#else
+    // full 128 bits are x0 * y0 + (x0 * y1 << 32) + (x1 * y0 << 32) + (x1 * y1 << 64)
+    uint32_t mask = 0xFFFFFFFF;
+    uint32_t x0 = (uint32_t)(x & mask);
+    uint32_t y0 = (uint32_t)(y & mask);
+    int32_t x1 = (int32_t)(x >> 32);
+    int32_t y1 = (int32_t)(y >> 32);
+    uint32_t x0y0_hi = libdivide_mullhi_u32(x0, y0);
+    int64_t t = x1 * (int64_t)y0 + x0y0_hi;
+    int64_t w1 = x0 * (int64_t)y1 + (t & mask);
+
+    return x1 * (int64_t)y1 + (t >> 32) + (w1 >> 32);
+#endif
+}
+
+static inline int32_t libdivide_count_leading_zeros32(uint32_t val) {
+#if defined(__GNUC__) || \
+    __has_builtin(__builtin_clz)
+    // Fast way to count leading zeros
+    return __builtin_clz(val);
+#elif defined(LIBDIVIDE_VC)
+    unsigned long result;
+    if (_BitScanReverse(&result, val)) {
+        return 31 - result;
+    }
+    return 0;
+#else
+    if (val == 0)
+        return 32;
+    int32_t result = 8;
+    uint32_t hi = 0xFFU << 24;
+    while ((val & hi) == 0) {
+        hi >>= 8;
+        result += 8;
+    }
+    while (val & hi) {
+        result -= 1;
+        hi <<= 1;
+    }
+    return result;
+#endif
+}
+
+static inline int32_t libdivide_count_leading_zeros64(uint64_t val) {
+#if defined(__GNUC__) || \
+    __has_builtin(__builtin_clzll)
+    // Fast way to count leading zeros
+    return __builtin_clzll(val);
+#elif defined(LIBDIVIDE_VC) && defined(_WIN64)
+    unsigned long result;
+    if (_BitScanReverse64(&result, val)) {
+        return 63 - result;
+    }
+    return 0;
+#else
+    uint32_t hi = val >> 32;
+    uint32_t lo = val & 0xFFFFFFFF;
+    if (hi != 0) return libdivide_count_leading_zeros32(hi);
+    return 32 + libdivide_count_leading_zeros32(lo);
+#endif
+}
+
+// libdivide_64_div_32_to_32: divides a 64-bit uint {u1, u0} by a 32-bit
+// uint {v}. The result must fit in 32 bits.
+// Returns the quotient directly and the remainder in *r
+static inline uint32_t libdivide_64_div_32_to_32(uint32_t u1, uint32_t u0, uint32_t v, uint32_t *r) {
+#if (defined(LIBDIVIDE_i386) || defined(LIBDIVIDE_X86_64)) && \
+     defined(LIBDIVIDE_GCC_STYLE_ASM)
+    uint32_t result;
+    __asm__("divl %[v]"
+            : "=a"(result), "=d"(*r)
+            : [v] "r"(v), "a"(u0), "d"(u1)
+            );
+    return result;
+#else
+    uint64_t n = ((uint64_t)u1 << 32) | u0;
+    uint32_t result = (uint32_t)(n / v);
+    *r = (uint32_t)(n - result * (uint64_t)v);
+    return result;
+#endif
+}
+
+// libdivide_128_div_64_to_64: divides a 128-bit uint {u1, u0} by a 64-bit
+// uint {v}. The result must fit in 64 bits.
+// Returns the quotient directly and the remainder in *r
+/*static uint64_t libdivide_128_div_64_to_64(uint64_t u1, uint64_t u0, uint64_t v, uint64_t *r) {
+#if defined(LIBDIVIDE_X86_64) && \
+    defined(LIBDIVIDE_GCC_STYLE_ASM)
+    uint64_t result;
+    __asm__("divq %[v]"
+            : "=a"(result), "=d"(*r)
+            : [v] "r"(v), "a"(u0), "d"(u1)
+            );
+    return result;
+#elif defined(HAS_INT128_T) && \
+      defined(HAS_INT128_DIV)
+    __uint128_t n = ((__uint128_t)u1 << 64) | u0;
+    uint64_t result = (uint64_t)(n / v);
+    *r = (uint64_t)(n - result * (__uint128_t)v);
+    return result;
+#else
+    // Code taken from Hacker's Delight:
+    // http://www.hackersdelight.org/HDcode/divlu.c.
+    // License permits inclusion here per:
+    // http://www.hackersdelight.org/permissions.htm
+
+    const uint64_t b = (1ULL << 32); // Number base (32 bits)
+    uint64_t un1, un0; // Norm. dividend LSD's
+    uint64_t vn1, vn0; // Norm. divisor digits
+    uint64_t q1, q0; // Quotient digits
+    uint64_t un64, un21, un10; // Dividend digit pairs
+    uint64_t rhat; // A remainder
+    int32_t s; // Shift amount for norm
+
+    // If overflow, set rem. to an impossible value,
+    // and return the largest possible quotient
+    if (u1 >= v) {
+        *r = (uint64_t) -1;
+        return (uint64_t) -1;
+    }
+
+    // count leading zeros
+    s = libdivide_count_leading_zeros64(v);
+    if (s > 0) {
+        // Normalize divisor
+        v = v << s;
+        un64 = (u1 << s) | (u0 >> (64 - s));
+        un10 = u0 << s; // Shift dividend left
+    } else {
+        // Avoid undefined behavior of (u0 >> 64).
+        // The behavior is undefined if the right operand is
+        // negative, or greater than or equal to the length
+        // in bits of the promoted left operand.
+        un64 = u1;
+        un10 = u0;
+    }
+
+    // Break divisor up into two 32-bit digits
+    vn1 = v >> 32;
+    vn0 = v & 0xFFFFFFFF;
+
+    // Break right half of dividend into two digits
+    un1 = un10 >> 32;
+    un0 = un10 & 0xFFFFFFFF;
+
+    // Compute the first quotient digit, q1
+    q1 = un64 / vn1;
+    rhat = un64 - q1 * vn1;
+
+    while (q1 >= b || q1 * vn0 > b * rhat + un1) {
+        q1 = q1 - 1;
+        rhat = rhat + vn1;
+        if (rhat >= b)
+            break;
+    }
+
+     // Multiply and subtract
+    un21 = un64 * b + un1 - q1 * v;
+
+    // Compute the second quotient digit
+    q0 = un21 / vn1;
+    rhat = un21 - q0 * vn1;
+
+    while (q0 >= b || q0 * vn0 > b * rhat + un0) {
+        q0 = q0 - 1;
+        rhat = rhat + vn1;
+        if (rhat >= b)
+            break;
+    }
+
+    *r = (un21 * b + un0 - q0 * v) >> s;
+    return q1 * b + q0;
+#endif
+}*/
+
+// Bitshift a u128 in place, left (signed_shift > 0) or right (signed_shift < 0)
+static inline void libdivide_u128_shift(uint64_t *u1, uint64_t *u0, int32_t signed_shift) {
+    if (signed_shift > 0) {
+        uint32_t shift = signed_shift;
+        *u1 <<= shift;
+        *u1 |= *u0 >> (64 - shift);
+        *u0 <<= shift;
+    }
+    else if (signed_shift < 0) {
+        uint32_t shift = -signed_shift;
+        *u0 >>= shift;
+        *u0 |= *u1 << (64 - shift);
+        *u1 >>= shift;
+    }
+}
+
+// Computes a 128 / 128 -> 64 bit division, with a 128 bit remainder.
+/*static uint64_t libdivide_128_div_128_to_64(uint64_t u_hi, uint64_t u_lo, uint64_t v_hi, uint64_t v_lo, uint64_t *r_hi, uint64_t *r_lo) {
+#if defined(HAS_INT128_T) && \
+    defined(HAS_INT128_DIV)
+    __uint128_t ufull = u_hi;
+    __uint128_t vfull = v_hi;
+    ufull = (ufull << 64) | u_lo;
+    vfull = (vfull << 64) | v_lo;
+    uint64_t res = (uint64_t)(ufull / vfull);
+    __uint128_t remainder = ufull - (vfull * res);
+    *r_lo = (uint64_t)remainder;
+    *r_hi = (uint64_t)(remainder >> 64);
+    return res;
+#else
+    // Adapted from "Unsigned Doubleword Division" in Hacker's Delight
+    // We want to compute u / v
+    typedef struct { uint64_t hi; uint64_t lo; } u128_t;
+    u128_t u = {u_hi, u_lo};
+    u128_t v = {v_hi, v_lo};
+
+    if (v.hi == 0) {
+        // divisor v is a 64 bit value, so we just need one 128/64 division
+        // Note that we are simpler than Hacker's Delight here, because we know
+        // the quotient fits in 64 bits whereas Hacker's Delight demands a full
+        // 128 bit quotient
+        *r_hi = 0;
+        return libdivide_128_div_64_to_64(u.hi, u.lo, v.lo, r_lo);
+    }
+    // Here v >= 2**64
+    // We know that v.hi != 0, so count leading zeros is OK
+    // We have 0 <= n <= 63
+    uint32_t n = libdivide_count_leading_zeros64(v.hi);
+
+    // Normalize the divisor so its MSB is 1
+    u128_t v1t = v;
+    libdivide_u128_shift(&v1t.hi, &v1t.lo, n);
+    uint64_t v1 = v1t.hi; // i.e. v1 = v1t >> 64
+
+    // To ensure no overflow
+    u128_t u1 = u;
+    libdivide_u128_shift(&u1.hi, &u1.lo, -1);
+
+    // Get quotient from divide unsigned insn.
+    uint64_t rem_ignored;
+    uint64_t q1 = libdivide_128_div_64_to_64(u1.hi, u1.lo, v1, &rem_ignored);
+
+    // Undo normalization and division of u by 2.
+    u128_t q0 = {0, q1};
+    libdivide_u128_shift(&q0.hi, &q0.lo, n);
+    libdivide_u128_shift(&q0.hi, &q0.lo, -63);
+
+    // Make q0 correct or too small by 1
+    // Equivalent to `if (q0 != 0) q0 = q0 - 1;`
+    if (q0.hi != 0 || q0.lo != 0) {
+        q0.hi -= (q0.lo == 0); // borrow
+        q0.lo -= 1;
+    }
+
+    // Now q0 is correct.
+    // Compute q0 * v as q0v
+    // = (q0.hi << 64 + q0.lo) * (v.hi << 64 + v.lo)
+    // = (q0.hi * v.hi << 128) + (q0.hi * v.lo << 64) +
+    //   (q0.lo * v.hi <<  64) + q0.lo * v.lo)
+    // Each term is 128 bit
+    // High half of full product (upper 128 bits!) are dropped
+    u128_t q0v = {0, 0};
+    q0v.hi = q0.hi*v.lo + q0.lo*v.hi + libdivide_mullhi_u64(q0.lo, v.lo);
+    q0v.lo = q0.lo*v.lo;
+
+    // Compute u - q0v as u_q0v
+    // This is the remainder
+    u128_t u_q0v = u;
+    u_q0v.hi -= q0v.hi + (u.lo < q0v.lo); // second term is borrow
+    u_q0v.lo -= q0v.lo;
+
+    // Check if u_q0v >= v
+    // This checks if our remainder is larger than the divisor
+    if ((u_q0v.hi > v.hi) ||
+        (u_q0v.hi == v.hi && u_q0v.lo >= v.lo)) {
+        // Increment q0
+        q0.lo += 1;
+        q0.hi += (q0.lo == 0); // carry
+
+        // Subtract v from remainder
+        u_q0v.hi -= v.hi + (u_q0v.lo < v.lo);
+        u_q0v.lo -= v.lo;
+    }
+
+    *r_hi = u_q0v.hi;
+    *r_lo = u_q0v.lo;
+
+    LIBDIVIDE_ASSERT(q0.hi == 0);
+    return q0.lo;
+#endif
+}*/
+
+////////// UINT32
+
+static inline struct libdivide_u32_t libdivide_internal_u32_gen(uint32_t d, int branchfree) {
+    struct libdivide_u32_t result;
+    uint32_t floor_log_2_d;
+
+    if (d == 0) {
+        LIBDIVIDE_ERROR("divider must be != 0");
+    }
+
+    floor_log_2_d = 31 - libdivide_count_leading_zeros32(d);
+
+    // Power of 2
+    if ((d & (d - 1)) == 0) {
+        // We need to subtract 1 from the shift value in case of an unsigned
+        // branchfree divider because there is a hardcoded right shift by 1
+        // in its division algorithm. Because of this we also need to add back
+        // 1 in its recovery algorithm.
+        result.magic = 0;
+        result.more = (uint8_t)(floor_log_2_d - (branchfree != 0));
+    } else {
+        uint8_t more;
+        uint32_t rem, proposed_m;
+        uint32_t e;
+        proposed_m = libdivide_64_div_32_to_32(1U << floor_log_2_d, 0, d, &rem);
+
+        LIBDIVIDE_ASSERT(rem > 0 && rem < d);
+        e = d - rem;
+
+        // This power works if e < 2**floor_log_2_d.
+        if (!branchfree && (e < (1U << floor_log_2_d))) {
+            // This power works
+            more = floor_log_2_d;
+        } else {
+            // We have to use the general 33-bit algorithm.  We need to compute
+            // (2**power) / d. However, we already have (2**(power-1))/d and
+            // its remainder.  By doubling both, and then correcting the
+            // remainder, we can compute the larger division.
+            // don't care about overflow here - in fact, we expect it
+            const uint32_t twice_rem = rem + rem;
+            proposed_m += proposed_m;
+            if (twice_rem >= d || twice_rem < rem) proposed_m += 1;
+            more = floor_log_2_d | LIBDIVIDE_ADD_MARKER;
+        }
+        result.magic = 1 + proposed_m;
+        result.more = more;
+        // result.more's shift should in general be ceil_log_2_d. But if we
+        // used the smaller power, we subtract one from the shift because we're
+        // using the smaller power. If we're using the larger power, we
+        // subtract one from the shift because it's taken care of by the add
+        // indicator. So floor_log_2_d happens to be correct in both cases.
+    }
+    return result;
+}
+
+struct libdivide_u32_t libdivide_u32_gen(uint32_t d) {
+    return libdivide_internal_u32_gen(d, 0);
+}
+
+/*struct libdivide_u32_branchfree_t libdivide_u32_branchfree_gen(uint32_t d) {
+    if (d == 1) {
+        LIBDIVIDE_ERROR("branchfree divider must be != 1");
+    }
+    struct libdivide_u32_t tmp = libdivide_internal_u32_gen(d, 1);
+    struct libdivide_u32_branchfree_t ret = {tmp.magic, (uint8_t)(tmp.more & LIBDIVIDE_32_SHIFT_MASK)};
+    return ret;
+}*/
+
+uint32_t libdivide_u32_do(uint32_t numer, const struct libdivide_u32_t *denom) {
+    uint8_t more = denom->more;
+    if (!denom->magic) {
+        return numer >> more;
+    }
+    else {
+        uint32_t q = libdivide_mullhi_u32(denom->magic, numer);
+        if (more & LIBDIVIDE_ADD_MARKER) {
+            uint32_t t = ((numer - q) >> 1) + q;
+            return t >> (more & LIBDIVIDE_32_SHIFT_MASK);
+        }
+        else {
+            // All upper bits are 0,
+            // don't need to mask them off.
+            return q >> more;
+        }
+    }
+}
+
+/*uint32_t libdivide_u32_branchfree_do(uint32_t numer, const struct libdivide_u32_branchfree_t *denom) {
+    uint32_t q = libdivide_mullhi_u32(denom->magic, numer);
+    uint32_t t = ((numer - q) >> 1) + q;
+    return t >> denom->more;
+}
+
+uint32_t libdivide_u32_recover(const struct libdivide_u32_t *denom) {
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+
+    if (!denom->magic) {
+        return 1U << shift;
+    } else if (!(more & LIBDIVIDE_ADD_MARKER)) {
+        // We compute q = n/d = n*m / 2^(32 + shift)
+        // Therefore we have d = 2^(32 + shift) / m
+        // We need to ceil it.
+        // We know d is not a power of 2, so m is not a power of 2,
+        // so we can just add 1 to the floor
+        uint32_t hi_dividend = 1U << shift;
+        uint32_t rem_ignored;
+        return 1 + libdivide_64_div_32_to_32(hi_dividend, 0, denom->magic, &rem_ignored);
+    } else {
+        // Here we wish to compute d = 2^(32+shift+1)/(m+2^32).
+        // Notice (m + 2^32) is a 33 bit number. Use 64 bit division for now
+        // Also note that shift may be as high as 31, so shift + 1 will
+        // overflow. So we have to compute it as 2^(32+shift)/(m+2^32), and
+        // then double the quotient and remainder.
+        uint64_t half_n = 1ULL << (32 + shift);
+        uint64_t d = (1ULL << 32) | denom->magic;
+        // Note that the quotient is guaranteed <= 32 bits, but the remainder
+        // may need 33!
+        uint32_t half_q = (uint32_t)(half_n / d);
+        uint64_t rem = half_n % d;
+        // We computed 2^(32+shift)/(m+2^32)
+        // Need to double it, and then add 1 to the quotient if doubling th
+        // remainder would increase the quotient.
+        // Note that rem<<1 cannot overflow, since rem < d and d is 33 bits
+        uint32_t full_q = half_q + half_q + ((rem<<1) >= d);
+
+        // We rounded down in gen (hence +1)
+        return full_q + 1;
+    }
+}
+
+uint32_t libdivide_u32_branchfree_recover(const struct libdivide_u32_branchfree_t *denom) {
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+
+    if (!denom->magic) {
+        return 1U << (shift + 1);
+    } else {
+        // Here we wish to compute d = 2^(32+shift+1)/(m+2^32).
+        // Notice (m + 2^32) is a 33 bit number. Use 64 bit division for now
+        // Also note that shift may be as high as 31, so shift + 1 will
+        // overflow. So we have to compute it as 2^(32+shift)/(m+2^32), and
+        // then double the quotient and remainder.
+        uint64_t half_n = 1ULL << (32 + shift);
+        uint64_t d = (1ULL << 32) | denom->magic;
+        // Note that the quotient is guaranteed <= 32 bits, but the remainder
+        // may need 33!
+        uint32_t half_q = (uint32_t)(half_n / d);
+        uint64_t rem = half_n % d;
+        // We computed 2^(32+shift)/(m+2^32)
+        // Need to double it, and then add 1 to the quotient if doubling th
+        // remainder would increase the quotient.
+        // Note that rem<<1 cannot overflow, since rem < d and d is 33 bits
+        uint32_t full_q = half_q + half_q + ((rem<<1) >= d);
+
+        // We rounded down in gen (hence +1)
+        return full_q + 1;
+    }
+}*/
+
+/////////// UINT64
+
+/*static inline struct libdivide_u64_t libdivide_internal_u64_gen(uint64_t d, int branchfree) {
+    if (d == 0) {
+        LIBDIVIDE_ERROR("divider must be != 0");
+    }
+
+    struct libdivide_u64_t result;
+    uint32_t floor_log_2_d = 63 - libdivide_count_leading_zeros64(d);
+
+    // Power of 2
+    if ((d & (d - 1)) == 0) {
+        // We need to subtract 1 from the shift value in case of an unsigned
+        // branchfree divider because there is a hardcoded right shift by 1
+        // in its division algorithm. Because of this we also need to add back
+        // 1 in its recovery algorithm.
+        result.magic = 0;
+        result.more = (uint8_t)(floor_log_2_d - (branchfree != 0));
+    } else {
+        uint64_t proposed_m, rem;
+        uint8_t more;
+        // (1 << (64 + floor_log_2_d)) / d
+        proposed_m = libdivide_128_div_64_to_64(1ULL << floor_log_2_d, 0, d, &rem);
+
+        LIBDIVIDE_ASSERT(rem > 0 && rem < d);
+        const uint64_t e = d - rem;
+
+        // This power works if e < 2**floor_log_2_d.
+        if (!branchfree && e < (1ULL << floor_log_2_d)) {
+            // This power works
+            more = floor_log_2_d;
+        } else {
+            // We have to use the general 65-bit algorithm.  We need to compute
+            // (2**power) / d. However, we already have (2**(power-1))/d and
+            // its remainder. By doubling both, and then correcting the
+            // remainder, we can compute the larger division.
+            // don't care about overflow here - in fact, we expect it
+            proposed_m += proposed_m;
+            const uint64_t twice_rem = rem + rem;
+            if (twice_rem >= d || twice_rem < rem) proposed_m += 1;
+                more = floor_log_2_d | LIBDIVIDE_ADD_MARKER;
+        }
+        result.magic = 1 + proposed_m;
+        result.more = more;
+        // result.more's shift should in general be ceil_log_2_d. But if we
+        // used the smaller power, we subtract one from the shift because we're
+        // using the smaller power. If we're using the larger power, we
+        // subtract one from the shift because it's taken care of by the add
+        // indicator. So floor_log_2_d happens to be correct in both cases,
+        // which is why we do it outside of the if statement.
+    }
+    return result;
+}
+
+struct libdivide_u64_t libdivide_u64_gen(uint64_t d) {
+    return libdivide_internal_u64_gen(d, 0);
+}
+
+struct libdivide_u64_branchfree_t libdivide_u64_branchfree_gen(uint64_t d) {
+    if (d == 1) {
+        LIBDIVIDE_ERROR("branchfree divider must be != 1");
+    }
+    struct libdivide_u64_t tmp = libdivide_internal_u64_gen(d, 1);
+    struct libdivide_u64_branchfree_t ret = {tmp.magic, (uint8_t)(tmp.more & LIBDIVIDE_64_SHIFT_MASK)};
+    return ret;
+}
+
+uint64_t libdivide_u64_do(uint64_t numer, const struct libdivide_u64_t *denom) {
+    uint8_t more = denom->more;
+    if (!denom->magic) {
+        return numer >> more;
+    }
+    else {
+        uint64_t q = libdivide_mullhi_u64(denom->magic, numer);
+        if (more & LIBDIVIDE_ADD_MARKER) {
+            uint64_t t = ((numer - q) >> 1) + q;
+            return t >> (more & LIBDIVIDE_64_SHIFT_MASK);
+        }
+        else {
+             // All upper bits are 0,
+             // don't need to mask them off.
+            return q >> more;
+        }
+    }
+}
+
+uint64_t libdivide_u64_branchfree_do(uint64_t numer, const struct libdivide_u64_branchfree_t *denom) {
+    uint64_t q = libdivide_mullhi_u64(denom->magic, numer);
+    uint64_t t = ((numer - q) >> 1) + q;
+    return t >> denom->more;
+}
+
+uint64_t libdivide_u64_recover(const struct libdivide_u64_t *denom) {
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+
+    if (!denom->magic) {
+        return 1ULL << shift;
+    } else if (!(more & LIBDIVIDE_ADD_MARKER)) {
+        // We compute q = n/d = n*m / 2^(64 + shift)
+        // Therefore we have d = 2^(64 + shift) / m
+        // We need to ceil it.
+        // We know d is not a power of 2, so m is not a power of 2,
+        // so we can just add 1 to the floor
+        uint64_t hi_dividend = 1ULL << shift;
+        uint64_t rem_ignored;
+        return 1 + libdivide_128_div_64_to_64(hi_dividend, 0, denom->magic, &rem_ignored);
+    } else {
+        // Here we wish to compute d = 2^(64+shift+1)/(m+2^64).
+        // Notice (m + 2^64) is a 65 bit number. This gets hairy. See
+        // libdivide_u32_recover for more on what we do here.
+        // TODO: do something better than 128 bit math
+
+        // Full n is a (potentially) 129 bit value
+        // half_n is a 128 bit value
+        // Compute the hi half of half_n. Low half is 0.
+        uint64_t half_n_hi = 1ULL << shift, half_n_lo = 0;
+        // d is a 65 bit value. The high bit is always set to 1.
+        const uint64_t d_hi = 1, d_lo = denom->magic;
+        // Note that the quotient is guaranteed <= 64 bits,
+        // but the remainder may need 65!
+        uint64_t r_hi, r_lo;
+        uint64_t half_q = libdivide_128_div_128_to_64(half_n_hi, half_n_lo, d_hi, d_lo, &r_hi, &r_lo);
+        // We computed 2^(64+shift)/(m+2^64)
+        // Double the remainder ('dr') and check if that is larger than d
+        // Note that d is a 65 bit value, so r1 is small and so r1 + r1
+        // cannot overflow
+        uint64_t dr_lo = r_lo + r_lo;
+        uint64_t dr_hi = r_hi + r_hi + (dr_lo < r_lo); // last term is carry
+        int dr_exceeds_d = (dr_hi > d_hi) || (dr_hi == d_hi && dr_lo >= d_lo);
+        uint64_t full_q = half_q + half_q + (dr_exceeds_d ? 1 : 0);
+        return full_q + 1;
+    }
+}
+
+uint64_t libdivide_u64_branchfree_recover(const struct libdivide_u64_branchfree_t *denom) {
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+
+    if (!denom->magic) {
+        return 1ULL << (shift + 1);
+    } else {
+        // Here we wish to compute d = 2^(64+shift+1)/(m+2^64).
+        // Notice (m + 2^64) is a 65 bit number. This gets hairy. See
+        // libdivide_u32_recover for more on what we do here.
+        // TODO: do something better than 128 bit math
+
+        // Full n is a (potentially) 129 bit value
+        // half_n is a 128 bit value
+        // Compute the hi half of half_n. Low half is 0.
+        uint64_t half_n_hi = 1ULL << shift, half_n_lo = 0;
+        // d is a 65 bit value. The high bit is always set to 1.
+        const uint64_t d_hi = 1, d_lo = denom->magic;
+        // Note that the quotient is guaranteed <= 64 bits,
+        // but the remainder may need 65!
+        uint64_t r_hi, r_lo;
+        uint64_t half_q = libdivide_128_div_128_to_64(half_n_hi, half_n_lo, d_hi, d_lo, &r_hi, &r_lo);
+        // We computed 2^(64+shift)/(m+2^64)
+        // Double the remainder ('dr') and check if that is larger than d
+        // Note that d is a 65 bit value, so r1 is small and so r1 + r1
+        // cannot overflow
+        uint64_t dr_lo = r_lo + r_lo;
+        uint64_t dr_hi = r_hi + r_hi + (dr_lo < r_lo); // last term is carry
+        int dr_exceeds_d = (dr_hi > d_hi) || (dr_hi == d_hi && dr_lo >= d_lo);
+        uint64_t full_q = half_q + half_q + (dr_exceeds_d ? 1 : 0);
+        return full_q + 1;
+    }
+}*/
+
+/////////// SINT32
+
+/*static inline struct libdivide_s32_t libdivide_internal_s32_gen(int32_t d, int branchfree) {
+    if (d == 0) {
+        LIBDIVIDE_ERROR("divider must be != 0");
+    }
+
+    struct libdivide_s32_t result;
+
+    // If d is a power of 2, or negative a power of 2, we have to use a shift.
+    // This is especially important because the magic algorithm fails for -1.
+    // To check if d is a power of 2 or its inverse, it suffices to check
+    // whether its absolute value has exactly one bit set. This works even for
+    // INT_MIN, because abs(INT_MIN) == INT_MIN, and INT_MIN has one bit set
+    // and is a power of 2.
+    uint32_t ud = (uint32_t)d;
+    uint32_t absD = (d < 0) ? -ud : ud;
+    uint32_t floor_log_2_d = 31 - libdivide_count_leading_zeros32(absD);
+    // check if exactly one bit is set,
+    // don't care if absD is 0 since that's divide by zero
+    if ((absD & (absD - 1)) == 0) {
+        // Branchfree and normal paths are exactly the same
+        result.magic = 0;
+        result.more = floor_log_2_d | (d < 0 ? LIBDIVIDE_NEGATIVE_DIVISOR : 0);
+    } else {
+        LIBDIVIDE_ASSERT(floor_log_2_d >= 1);
+
+        uint8_t more;
+        // the dividend here is 2**(floor_log_2_d + 31), so the low 32 bit word
+        // is 0 and the high word is floor_log_2_d - 1
+        uint32_t rem, proposed_m;
+        proposed_m = libdivide_64_div_32_to_32(1U << (floor_log_2_d - 1), 0, absD, &rem);
+        const uint32_t e = absD - rem;
+
+        // We are going to start with a power of floor_log_2_d - 1.
+        // This works if works if e < 2**floor_log_2_d.
+        if (!branchfree && e < (1U << floor_log_2_d)) {
+            // This power works
+            more = floor_log_2_d - 1;
+        } else {
+            // We need to go one higher. This should not make proposed_m
+            // overflow, but it will make it negative when interpreted as an
+            // int32_t.
+            proposed_m += proposed_m;
+            const uint32_t twice_rem = rem + rem;
+            if (twice_rem >= absD || twice_rem < rem) proposed_m += 1;
+            more = floor_log_2_d | LIBDIVIDE_ADD_MARKER;
+        }
+
+        proposed_m += 1;
+        int32_t magic = (int32_t)proposed_m;
+
+        // Mark if we are negative. Note we only negate the magic number in the
+        // branchfull case.
+        if (d < 0) {
+            more |= LIBDIVIDE_NEGATIVE_DIVISOR;
+            if (!branchfree) {
+                magic = -magic;
+            }
+        }
+
+        result.more = more;
+        result.magic = magic;
+    }
+    return result;
+}
+
+struct libdivide_s32_t libdivide_s32_gen(int32_t d) {
+    return libdivide_internal_s32_gen(d, 0);
+}
+
+struct libdivide_s32_branchfree_t libdivide_s32_branchfree_gen(int32_t d) {
+    struct libdivide_s32_t tmp = libdivide_internal_s32_gen(d, 1);
+    struct libdivide_s32_branchfree_t result = {tmp.magic, tmp.more};
+    return result;
+}
+
+int32_t libdivide_s32_do(int32_t numer, const struct libdivide_s32_t *denom) {
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+
+    if (!denom->magic) {
+        uint32_t sign = (int8_t)more >> 7;
+        uint32_t mask = (1U << shift) - 1;
+        uint32_t uq = numer + ((numer >> 31) & mask);
+        int32_t q = (int32_t)uq;
+        q >>= shift;
+        q = (q ^ sign) - sign;
+        return q;
+    } else {
+        uint32_t uq = (uint32_t)libdivide_mullhi_s32(denom->magic, numer);
+        if (more & LIBDIVIDE_ADD_MARKER) {
+            // must be arithmetic shift and then sign extend
+            int32_t sign = (int8_t)more >> 7;
+            // q += (more < 0 ? -numer : numer)
+            // cast required to avoid UB
+            uq += ((uint32_t)numer ^ sign) - sign;
+        }
+        int32_t q = (int32_t)uq;
+        q >>= shift;
+        q += (q < 0);
+        return q;
+    }
+}
+
+int32_t libdivide_s32_branchfree_do(int32_t numer, const struct libdivide_s32_branchfree_t *denom) {
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+    // must be arithmetic shift and then sign extend
+    int32_t sign = (int8_t)more >> 7;
+    int32_t magic = denom->magic;
+    int32_t q = libdivide_mullhi_s32(magic, numer);
+    q += numer;
+
+    // If q is non-negative, we have nothing to do
+    // If q is negative, we want to add either (2**shift)-1 if d is a power of
+    // 2, or (2**shift) if it is not a power of 2
+    uint32_t is_power_of_2 = (magic == 0);
+    uint32_t q_sign = (uint32_t)(q >> 31);
+    q += q_sign & ((1U << shift) - is_power_of_2);
+
+    // Now arithmetic right shift
+    q >>= shift;
+    // Negate if needed
+    q = (q ^ sign) - sign;
+
+    return q;
+}
+
+int32_t libdivide_s32_recover(const struct libdivide_s32_t *denom) {
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+    if (!denom->magic) {
+        uint32_t absD = 1U << shift;
+        if (more & LIBDIVIDE_NEGATIVE_DIVISOR) {
+            absD = -absD;
+        }
+        return (int32_t)absD;
+    } else {
+        // Unsigned math is much easier
+        // We negate the magic number only in the branchfull case, and we don't
+        // know which case we're in. However we have enough information to
+        // determine the correct sign of the magic number. The divisor was
+        // negative if LIBDIVIDE_NEGATIVE_DIVISOR is set. If ADD_MARKER is set,
+        // the magic number's sign is opposite that of the divisor.
+        // We want to compute the positive magic number.
+        int negative_divisor = (more & LIBDIVIDE_NEGATIVE_DIVISOR);
+        int magic_was_negated = (more & LIBDIVIDE_ADD_MARKER)
+            ? denom->magic > 0 : denom->magic < 0;
+
+        // Handle the power of 2 case (including branchfree)
+        if (denom->magic == 0) {
+            int32_t result = 1U << shift;
+            return negative_divisor ? -result : result;
+        }
+
+        uint32_t d = (uint32_t)(magic_was_negated ? -denom->magic : denom->magic);
+        uint64_t n = 1ULL << (32 + shift); // this shift cannot exceed 30
+        uint32_t q = (uint32_t)(n / d);
+        int32_t result = (int32_t)q;
+        result += 1;
+        return negative_divisor ? -result : result;
+    }
+}
+
+int32_t libdivide_s32_branchfree_recover(const struct libdivide_s32_branchfree_t *denom) {
+    return libdivide_s32_recover((const struct libdivide_s32_t *)denom);
+}*/
+
+///////////// SINT64
+
+/*static inline struct libdivide_s64_t libdivide_internal_s64_gen(int64_t d, int branchfree) {
+    if (d == 0) {
+        LIBDIVIDE_ERROR("divider must be != 0");
+    }
+
+    struct libdivide_s64_t result;
+
+    // If d is a power of 2, or negative a power of 2, we have to use a shift.
+    // This is especially important because the magic algorithm fails for -1.
+    // To check if d is a power of 2 or its inverse, it suffices to check
+    // whether its absolute value has exactly one bit set.  This works even for
+    // INT_MIN, because abs(INT_MIN) == INT_MIN, and INT_MIN has one bit set
+    // and is a power of 2.
+    uint64_t ud = (uint64_t)d;
+    uint64_t absD = (d < 0) ? -ud : ud;
+    uint32_t floor_log_2_d = 63 - libdivide_count_leading_zeros64(absD);
+    // check if exactly one bit is set,
+    // don't care if absD is 0 since that's divide by zero
+    if ((absD & (absD - 1)) == 0) {
+        // Branchfree and non-branchfree cases are the same
+        result.magic = 0;
+        result.more = floor_log_2_d | (d < 0 ? LIBDIVIDE_NEGATIVE_DIVISOR : 0);
+    } else {
+        // the dividend here is 2**(floor_log_2_d + 63), so the low 64 bit word
+        // is 0 and the high word is floor_log_2_d - 1
+        uint8_t more;
+        uint64_t rem, proposed_m;
+        proposed_m = libdivide_128_div_64_to_64(1ULL << (floor_log_2_d - 1), 0, absD, &rem);
+        const uint64_t e = absD - rem;
+
+        // We are going to start with a power of floor_log_2_d - 1.
+        // This works if works if e < 2**floor_log_2_d.
+        if (!branchfree && e < (1ULL << floor_log_2_d)) {
+            // This power works
+            more = floor_log_2_d - 1;
+        } else {
+            // We need to go one higher. This should not make proposed_m
+            // overflow, but it will make it negative when interpreted as an
+            // int32_t.
+            proposed_m += proposed_m;
+            const uint64_t twice_rem = rem + rem;
+            if (twice_rem >= absD || twice_rem < rem) proposed_m += 1;
+            // note that we only set the LIBDIVIDE_NEGATIVE_DIVISOR bit if we
+            // also set ADD_MARKER this is an annoying optimization that
+            // enables algorithm #4 to avoid the mask. However we always set it
+            // in the branchfree case
+            more = floor_log_2_d | LIBDIVIDE_ADD_MARKER;
+        }
+        proposed_m += 1;
+        int64_t magic = (int64_t)proposed_m;
+
+        // Mark if we are negative
+        if (d < 0) {
+            more |= LIBDIVIDE_NEGATIVE_DIVISOR;
+            if (!branchfree) {
+                magic = -magic;
+            }
+        }
+
+        result.more = more;
+        result.magic = magic;
+    }
+    return result;
+}
+
+struct libdivide_s64_t libdivide_s64_gen(int64_t d) {
+    return libdivide_internal_s64_gen(d, 0);
+}
+
+struct libdivide_s64_branchfree_t libdivide_s64_branchfree_gen(int64_t d) {
+    struct libdivide_s64_t tmp = libdivide_internal_s64_gen(d, 1);
+    struct libdivide_s64_branchfree_t ret = {tmp.magic, tmp.more};
+    return ret;
+}
+
+int64_t libdivide_s64_do(int64_t numer, const struct libdivide_s64_t *denom) {
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+
+    if (!denom->magic) { // shift path
+        uint64_t mask = (1ULL << shift) - 1;
+        uint64_t uq = numer + ((numer >> 63) & mask);
+        int64_t q = (int64_t)uq;
+        q >>= shift;
+        // must be arithmetic shift and then sign-extend
+        int64_t sign = (int8_t)more >> 7;
+        q = (q ^ sign) - sign;
+        return q;
+    } else {
+        uint64_t uq = (uint64_t)libdivide_mullhi_s64(denom->magic, numer);
+        if (more & LIBDIVIDE_ADD_MARKER) {
+            // must be arithmetic shift and then sign extend
+            int64_t sign = (int8_t)more >> 7;
+            // q += (more < 0 ? -numer : numer)
+            // cast required to avoid UB
+            uq += ((uint64_t)numer ^ sign) - sign;
+        }
+        int64_t q = (int64_t)uq;
+        q >>= shift;
+        q += (q < 0);
+        return q;
+    }
+}
+
+int64_t libdivide_s64_branchfree_do(int64_t numer, const struct libdivide_s64_branchfree_t *denom) {
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+    // must be arithmetic shift and then sign extend
+    int64_t sign = (int8_t)more >> 7;
+    int64_t magic = denom->magic;
+    int64_t q = libdivide_mullhi_s64(magic, numer);
+    q += numer;
+
+    // If q is non-negative, we have nothing to do.
+    // If q is negative, we want to add either (2**shift)-1 if d is a power of
+    // 2, or (2**shift) if it is not a power of 2.
+    uint64_t is_power_of_2 = (magic == 0);
+    uint64_t q_sign = (uint64_t)(q >> 63);
+    q += q_sign & ((1ULL << shift) - is_power_of_2);
+
+    // Arithmetic right shift
+    q >>= shift;
+    // Negate if needed
+    q = (q ^ sign) - sign;
+
+    return q;
+}
+
+int64_t libdivide_s64_recover(const struct libdivide_s64_t *denom) {
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+    if (denom->magic == 0) { // shift path
+        uint64_t absD = 1ULL << shift;
+        if (more & LIBDIVIDE_NEGATIVE_DIVISOR) {
+            absD = -absD;
+        }
+        return (int64_t)absD;
+    } else {
+        // Unsigned math is much easier
+        int negative_divisor = (more & LIBDIVIDE_NEGATIVE_DIVISOR);
+        int magic_was_negated = (more & LIBDIVIDE_ADD_MARKER)
+            ? denom->magic > 0 : denom->magic < 0;
+
+        uint64_t d = (uint64_t)(magic_was_negated ? -denom->magic : denom->magic);
+        uint64_t n_hi = 1ULL << shift, n_lo = 0;
+        uint64_t rem_ignored;
+        uint64_t q = libdivide_128_div_64_to_64(n_hi, n_lo, d, &rem_ignored);
+        int64_t result = (int64_t)(q + 1);
+        if (negative_divisor) {
+            result = -result;
+        }
+        return result;
+    }
+}
+
+int64_t libdivide_s64_branchfree_recover(const struct libdivide_s64_branchfree_t *denom) {
+    return libdivide_s64_recover((const struct libdivide_s64_t *)denom);
+}*/
+
+#if defined(LIBDIVIDE_AVX512)
+
+static inline __m512i libdivide_u32_do_vector(__m512i numers, const struct libdivide_u32_t *denom);
+static inline __m512i libdivide_s32_do_vector(__m512i numers, const struct libdivide_s32_t *denom);
+static inline __m512i libdivide_u64_do_vector(__m512i numers, const struct libdivide_u64_t *denom);
+static inline __m512i libdivide_s64_do_vector(__m512i numers, const struct libdivide_s64_t *denom);
+
+static inline __m512i libdivide_u32_branchfree_do_vector(__m512i numers, const struct libdivide_u32_branchfree_t *denom);
+static inline __m512i libdivide_s32_branchfree_do_vector(__m512i numers, const struct libdivide_s32_branchfree_t *denom);
+static inline __m512i libdivide_u64_branchfree_do_vector(__m512i numers, const struct libdivide_u64_branchfree_t *denom);
+static inline __m512i libdivide_s64_branchfree_do_vector(__m512i numers, const struct libdivide_s64_branchfree_t *denom);
+
+//////// Internal Utility Functions
+
+static inline __m512i libdivide_s64_signbits(__m512i v) {;
+    return _mm512_srai_epi64(v, 63);
+}
+
+static inline __m512i libdivide_s64_shift_right_vector(__m512i v, int amt) {
+    return _mm512_srai_epi64(v, amt);
+}
+
+// Here, b is assumed to contain one 32-bit value repeated.
+static inline __m512i libdivide_mullhi_u32_vector(__m512i a, __m512i b) {
+    __m512i hi_product_0Z2Z = _mm512_srli_epi64(_mm512_mul_epu32(a, b), 32);
+    __m512i a1X3X = _mm512_srli_epi64(a, 32);
+    __m512i mask = _mm512_set_epi32(-1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0);
+    __m512i hi_product_Z1Z3 = _mm512_and_si512(_mm512_mul_epu32(a1X3X, b), mask);
+    return _mm512_or_si512(hi_product_0Z2Z, hi_product_Z1Z3);
+}
+
+// b is one 32-bit value repeated.
+static inline __m512i libdivide_mullhi_s32_vector(__m512i a, __m512i b) {
+    __m512i hi_product_0Z2Z = _mm512_srli_epi64(_mm512_mul_epi32(a, b), 32);
+    __m512i a1X3X = _mm512_srli_epi64(a, 32);
+    __m512i mask = _mm512_set_epi32(-1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0);
+    __m512i hi_product_Z1Z3 = _mm512_and_si512(_mm512_mul_epi32(a1X3X, b), mask);
+    return _mm512_or_si512(hi_product_0Z2Z, hi_product_Z1Z3);
+}
+
+// Here, y is assumed to contain one 64-bit value repeated.
+// https://stackoverflow.com/a/28827013
+static inline __m512i libdivide_mullhi_u64_vector(__m512i x, __m512i y) {
+    __m512i lomask = _mm512_set1_epi64(0xffffffff);
+    __m512i xh = _mm512_shuffle_epi32(x, (_MM_PERM_ENUM) 0xB1);
+    __m512i yh = _mm512_shuffle_epi32(y, (_MM_PERM_ENUM) 0xB1);
+    __m512i w0 = _mm512_mul_epu32(x, y);
+    __m512i w1 = _mm512_mul_epu32(x, yh);
+    __m512i w2 = _mm512_mul_epu32(xh, y);
+    __m512i w3 = _mm512_mul_epu32(xh, yh);
+    __m512i w0h = _mm512_srli_epi64(w0, 32);
+    __m512i s1 = _mm512_add_epi64(w1, w0h);
+    __m512i s1l = _mm512_and_si512(s1, lomask);
+    __m512i s1h = _mm512_srli_epi64(s1, 32);
+    __m512i s2 = _mm512_add_epi64(w2, s1l);
+    __m512i s2h = _mm512_srli_epi64(s2, 32);
+    __m512i hi = _mm512_add_epi64(w3, s1h);
+            hi = _mm512_add_epi64(hi, s2h);
+
+    return hi;
+}
+
+// y is one 64-bit value repeated.
+static inline __m512i libdivide_mullhi_s64_vector(__m512i x, __m512i y) {
+    __m512i p = libdivide_mullhi_u64_vector(x, y);
+    __m512i t1 = _mm512_and_si512(libdivide_s64_signbits(x), y);
+    __m512i t2 = _mm512_and_si512(libdivide_s64_signbits(y), x);
+    p = _mm512_sub_epi64(p, t1);
+    p = _mm512_sub_epi64(p, t2);
+    return p;
+}
+
+////////// UINT32
+
+__m512i libdivide_u32_do_vector(__m512i numers, const struct libdivide_u32_t *denom) {
+    uint8_t more = denom->more;
+    if (!denom->magic) {
+        return _mm512_srli_epi32(numers, more);
+    }
+    else {
+        __m512i q = libdivide_mullhi_u32_vector(numers, _mm512_set1_epi32(denom->magic));
+        if (more & LIBDIVIDE_ADD_MARKER) {
+            // uint32_t t = ((numer - q) >> 1) + q;
+            // return t >> denom->shift;
+            uint32_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+            __m512i t = _mm512_add_epi32(_mm512_srli_epi32(_mm512_sub_epi32(numers, q), 1), q);
+            return _mm512_srli_epi32(t, shift);
+        }
+        else {
+            return _mm512_srli_epi32(q, more);
+        }
+    }
+}
+
+__m512i libdivide_u32_branchfree_do_vector(__m512i numers, const struct libdivide_u32_branchfree_t *denom) {
+    __m512i q = libdivide_mullhi_u32_vector(numers, _mm512_set1_epi32(denom->magic));
+    __m512i t = _mm512_add_epi32(_mm512_srli_epi32(_mm512_sub_epi32(numers, q), 1), q);
+    return _mm512_srli_epi32(t, denom->more);
+}
+
+////////// UINT64
+
+__m512i libdivide_u64_do_vector(__m512i numers, const struct libdivide_u64_t *denom) {
+    uint8_t more = denom->more;
+    if (!denom->magic) {
+        return _mm512_srli_epi64(numers, more);
+    }
+    else {
+        __m512i q = libdivide_mullhi_u64_vector(numers, _mm512_set1_epi64(denom->magic));
+        if (more & LIBDIVIDE_ADD_MARKER) {
+            // uint32_t t = ((numer - q) >> 1) + q;
+            // return t >> denom->shift;
+            uint32_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+            __m512i t = _mm512_add_epi64(_mm512_srli_epi64(_mm512_sub_epi64(numers, q), 1), q);
+            return _mm512_srli_epi64(t, shift);
+        }
+        else {
+            return _mm512_srli_epi64(q, more);
+        }
+    }
+}
+
+__m512i libdivide_u64_branchfree_do_vector(__m512i numers, const struct libdivide_u64_branchfree_t *denom) {
+    __m512i q = libdivide_mullhi_u64_vector(numers, _mm512_set1_epi64(denom->magic));
+    __m512i t = _mm512_add_epi64(_mm512_srli_epi64(_mm512_sub_epi64(numers, q), 1), q);
+    return _mm512_srli_epi64(t, denom->more);
+}
+
+////////// SINT32
+
+__m512i libdivide_s32_do_vector(__m512i numers, const struct libdivide_s32_t *denom) {
+    uint8_t more = denom->more;
+    if (!denom->magic) {
+        uint32_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+        uint32_t mask = (1U << shift) - 1;
+        __m512i roundToZeroTweak = _mm512_set1_epi32(mask);
+        // q = numer + ((numer >> 31) & roundToZeroTweak);
+        __m512i q = _mm512_add_epi32(numers, _mm512_and_si512(_mm512_srai_epi32(numers, 31), roundToZeroTweak));
+        q = _mm512_srai_epi32(q, shift);
+        __m512i sign = _mm512_set1_epi32((int8_t)more >> 7);
+        // q = (q ^ sign) - sign;
+        q = _mm512_sub_epi32(_mm512_xor_si512(q, sign), sign);
+        return q;
+    }
+    else {
+        __m512i q = libdivide_mullhi_s32_vector(numers, _mm512_set1_epi32(denom->magic));
+        if (more & LIBDIVIDE_ADD_MARKER) {
+             // must be arithmetic shift
+            __m512i sign = _mm512_set1_epi32((int8_t)more >> 7);
+             // q += ((numer ^ sign) - sign);
+            q = _mm512_add_epi32(q, _mm512_sub_epi32(_mm512_xor_si512(numers, sign), sign));
+        }
+        // q >>= shift
+        q = _mm512_srai_epi32(q, more & LIBDIVIDE_32_SHIFT_MASK);
+        q = _mm512_add_epi32(q, _mm512_srli_epi32(q, 31)); // q += (q < 0)
+        return q;
+    }
+}
+
+__m512i libdivide_s32_branchfree_do_vector(__m512i numers, const struct libdivide_s32_branchfree_t *denom) {
+    int32_t magic = denom->magic;
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+     // must be arithmetic shift
+    __m512i sign = _mm512_set1_epi32((int8_t)more >> 7);
+    __m512i q = libdivide_mullhi_s32_vector(numers, _mm512_set1_epi32(magic));
+    q = _mm512_add_epi32(q, numers); // q += numers
+
+    // If q is non-negative, we have nothing to do
+    // If q is negative, we want to add either (2**shift)-1 if d is
+    // a power of 2, or (2**shift) if it is not a power of 2
+    uint32_t is_power_of_2 = (magic == 0);
+    __m512i q_sign = _mm512_srai_epi32(q, 31); // q_sign = q >> 31
+    __m512i mask = _mm512_set1_epi32((1U << shift) - is_power_of_2);
+    q = _mm512_add_epi32(q, _mm512_and_si512(q_sign, mask)); // q = q + (q_sign & mask)
+    q = _mm512_srai_epi32(q, shift); // q >>= shift
+    q = _mm512_sub_epi32(_mm512_xor_si512(q, sign), sign); // q = (q ^ sign) - sign
+    return q;
+}
+
+////////// SINT64
+
+__m512i libdivide_s64_do_vector(__m512i numers, const struct libdivide_s64_t *denom) {
+    uint8_t more = denom->more;
+    int64_t magic = denom->magic;
+    if (magic == 0) { // shift path
+        uint32_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+        uint64_t mask = (1ULL << shift) - 1;
+        __m512i roundToZeroTweak = _mm512_set1_epi64(mask);
+        // q = numer + ((numer >> 63) & roundToZeroTweak);
+        __m512i q = _mm512_add_epi64(numers, _mm512_and_si512(libdivide_s64_signbits(numers), roundToZeroTweak));
+        q = libdivide_s64_shift_right_vector(q, shift);
+        __m512i sign = _mm512_set1_epi32((int8_t)more >> 7);
+         // q = (q ^ sign) - sign;
+        q = _mm512_sub_epi64(_mm512_xor_si512(q, sign), sign);
+        return q;
+    }
+    else {
+        __m512i q = libdivide_mullhi_s64_vector(numers, _mm512_set1_epi64(magic));
+        if (more & LIBDIVIDE_ADD_MARKER) {
+            // must be arithmetic shift
+            __m512i sign = _mm512_set1_epi32((int8_t)more >> 7);
+            // q += ((numer ^ sign) - sign);
+            q = _mm512_add_epi64(q, _mm512_sub_epi64(_mm512_xor_si512(numers, sign), sign));
+        }
+        // q >>= denom->mult_path.shift
+        q = libdivide_s64_shift_right_vector(q, more & LIBDIVIDE_64_SHIFT_MASK);
+        q = _mm512_add_epi64(q, _mm512_srli_epi64(q, 63)); // q += (q < 0)
+        return q;
+    }
+}
+
+__m512i libdivide_s64_branchfree_do_vector(__m512i numers, const struct libdivide_s64_branchfree_t *denom) {
+    int64_t magic = denom->magic;
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+    // must be arithmetic shift
+    __m512i sign = _mm512_set1_epi32((int8_t)more >> 7);
+
+     // libdivide_mullhi_s64(numers, magic);
+    __m512i q = libdivide_mullhi_s64_vector(numers, _mm512_set1_epi64(magic));
+    q = _mm512_add_epi64(q, numers); // q += numers
+
+    // If q is non-negative, we have nothing to do.
+    // If q is negative, we want to add either (2**shift)-1 if d is
+    // a power of 2, or (2**shift) if it is not a power of 2.
+    uint32_t is_power_of_2 = (magic == 0);
+    __m512i q_sign = libdivide_s64_signbits(q); // q_sign = q >> 63
+    __m512i mask = _mm512_set1_epi64((1ULL << shift) - is_power_of_2);
+    q = _mm512_add_epi64(q, _mm512_and_si512(q_sign, mask)); // q = q + (q_sign & mask)
+    q = libdivide_s64_shift_right_vector(q, shift); // q >>= shift
+    q = _mm512_sub_epi64(_mm512_xor_si512(q, sign), sign); // q = (q ^ sign) - sign
+    return q;
+}
+
+#elif defined(LIBDIVIDE_AVX2)
+
+static inline __m256i libdivide_u32_do_vector(__m256i numers, const struct libdivide_u32_t *denom);
+static inline __m256i libdivide_s32_do_vector(__m256i numers, const struct libdivide_s32_t *denom);
+static inline __m256i libdivide_u64_do_vector(__m256i numers, const struct libdivide_u64_t *denom);
+static inline __m256i libdivide_s64_do_vector(__m256i numers, const struct libdivide_s64_t *denom);
+
+static inline __m256i libdivide_u32_branchfree_do_vector(__m256i numers, const struct libdivide_u32_branchfree_t *denom);
+static inline __m256i libdivide_s32_branchfree_do_vector(__m256i numers, const struct libdivide_s32_branchfree_t *denom);
+static inline __m256i libdivide_u64_branchfree_do_vector(__m256i numers, const struct libdivide_u64_branchfree_t *denom);
+static inline __m256i libdivide_s64_branchfree_do_vector(__m256i numers, const struct libdivide_s64_branchfree_t *denom);
+
+//////// Internal Utility Functions
+
+// Implementation of _mm256_srai_epi64(v, 63) (from AVX512).
+static inline __m256i libdivide_s64_signbits(__m256i v) {
+    __m256i hiBitsDuped = _mm256_shuffle_epi32(v, _MM_SHUFFLE(3, 3, 1, 1));
+    __m256i signBits = _mm256_srai_epi32(hiBitsDuped, 31);
+    return signBits;
+}
+
+// Implementation of _mm256_srai_epi64 (from AVX512).
+static inline __m256i libdivide_s64_shift_right_vector(__m256i v, int amt) {
+    const int b = 64 - amt;
+    __m256i m = _mm256_set1_epi64x(1ULL << (b - 1));
+    __m256i x = _mm256_srli_epi64(v, amt);
+    __m256i result = _mm256_sub_epi64(_mm256_xor_si256(x, m), m);
+    return result;
+}
+
+// Here, b is assumed to contain one 32-bit value repeated.
+static inline __m256i libdivide_mullhi_u32_vector(__m256i a, __m256i b) {
+    __m256i hi_product_0Z2Z = _mm256_srli_epi64(_mm256_mul_epu32(a, b), 32);
+    __m256i a1X3X = _mm256_srli_epi64(a, 32);
+    __m256i mask = _mm256_set_epi32(-1, 0, -1, 0, -1, 0, -1, 0);
+    __m256i hi_product_Z1Z3 = _mm256_and_si256(_mm256_mul_epu32(a1X3X, b), mask);
+    return _mm256_or_si256(hi_product_0Z2Z, hi_product_Z1Z3);
+}
+
+// b is one 32-bit value repeated.
+static inline __m256i libdivide_mullhi_s32_vector(__m256i a, __m256i b) {
+    __m256i hi_product_0Z2Z = _mm256_srli_epi64(_mm256_mul_epi32(a, b), 32);
+    __m256i a1X3X = _mm256_srli_epi64(a, 32);
+    __m256i mask = _mm256_set_epi32(-1, 0, -1, 0, -1, 0, -1, 0);
+    __m256i hi_product_Z1Z3 = _mm256_and_si256(_mm256_mul_epi32(a1X3X, b), mask);
+    return _mm256_or_si256(hi_product_0Z2Z, hi_product_Z1Z3);
+}
+
+// Here, y is assumed to contain one 64-bit value repeated.
+// https://stackoverflow.com/a/28827013
+static inline __m256i libdivide_mullhi_u64_vector(__m256i x, __m256i y) {
+    __m256i lomask = _mm256_set1_epi64x(0xffffffff);
+    __m256i xh = _mm256_shuffle_epi32(x, 0xB1);        // x0l, x0h, x1l, x1h
+    __m256i yh = _mm256_shuffle_epi32(y, 0xB1);        // y0l, y0h, y1l, y1h
+    __m256i w0 = _mm256_mul_epu32(x, y);               // x0l*y0l, x1l*y1l
+    __m256i w1 = _mm256_mul_epu32(x, yh);              // x0l*y0h, x1l*y1h
+    __m256i w2 = _mm256_mul_epu32(xh, y);              // x0h*y0l, x1h*y0l
+    __m256i w3 = _mm256_mul_epu32(xh, yh);             // x0h*y0h, x1h*y1h
+    __m256i w0h = _mm256_srli_epi64(w0, 32);
+    __m256i s1 = _mm256_add_epi64(w1, w0h);
+    __m256i s1l = _mm256_and_si256(s1, lomask);
+    __m256i s1h = _mm256_srli_epi64(s1, 32);
+    __m256i s2 = _mm256_add_epi64(w2, s1l);
+    __m256i s2h = _mm256_srli_epi64(s2, 32);
+    __m256i hi = _mm256_add_epi64(w3, s1h);
+            hi = _mm256_add_epi64(hi, s2h);
+
+    return hi;
+}
+
+// y is one 64-bit value repeated.
+static inline __m256i libdivide_mullhi_s64_vector(__m256i x, __m256i y) {
+    __m256i p = libdivide_mullhi_u64_vector(x, y);
+    __m256i t1 = _mm256_and_si256(libdivide_s64_signbits(x), y);
+    __m256i t2 = _mm256_and_si256(libdivide_s64_signbits(y), x);
+    p = _mm256_sub_epi64(p, t1);
+    p = _mm256_sub_epi64(p, t2);
+    return p;
+}
+
+////////// UINT32
+
+__m256i libdivide_u32_do_vector(__m256i numers, const struct libdivide_u32_t *denom) {
+    uint8_t more = denom->more;
+    if (!denom->magic) {
+        return _mm256_srli_epi32(numers, more);
+    }
+    else {
+        __m256i q = libdivide_mullhi_u32_vector(numers, _mm256_set1_epi32(denom->magic));
+        if (more & LIBDIVIDE_ADD_MARKER) {
+            // uint32_t t = ((numer - q) >> 1) + q;
+            // return t >> denom->shift;
+            uint32_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+            __m256i t = _mm256_add_epi32(_mm256_srli_epi32(_mm256_sub_epi32(numers, q), 1), q);
+            return _mm256_srli_epi32(t, shift);
+        }
+        else {
+            return _mm256_srli_epi32(q, more);
+        }
+    }
+}
+
+__m256i libdivide_u32_branchfree_do_vector(__m256i numers, const struct libdivide_u32_branchfree_t *denom) {
+    __m256i q = libdivide_mullhi_u32_vector(numers, _mm256_set1_epi32(denom->magic));
+    __m256i t = _mm256_add_epi32(_mm256_srli_epi32(_mm256_sub_epi32(numers, q), 1), q);
+    return _mm256_srli_epi32(t, denom->more);
+}
+
+////////// UINT64
+
+__m256i libdivide_u64_do_vector(__m256i numers, const struct libdivide_u64_t *denom) {
+    uint8_t more = denom->more;
+    if (!denom->magic) {
+        return _mm256_srli_epi64(numers, more);
+    }
+    else {
+        __m256i q = libdivide_mullhi_u64_vector(numers, _mm256_set1_epi64x(denom->magic));
+        if (more & LIBDIVIDE_ADD_MARKER) {
+            // uint32_t t = ((numer - q) >> 1) + q;
+            // return t >> denom->shift;
+            uint32_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+            __m256i t = _mm256_add_epi64(_mm256_srli_epi64(_mm256_sub_epi64(numers, q), 1), q);
+            return _mm256_srli_epi64(t, shift);
+        }
+        else {
+            return _mm256_srli_epi64(q, more);
+        }
+    }
+}
+
+__m256i libdivide_u64_branchfree_do_vector(__m256i numers, const struct libdivide_u64_branchfree_t *denom) {
+    __m256i q = libdivide_mullhi_u64_vector(numers, _mm256_set1_epi64x(denom->magic));
+    __m256i t = _mm256_add_epi64(_mm256_srli_epi64(_mm256_sub_epi64(numers, q), 1), q);
+    return _mm256_srli_epi64(t, denom->more);
+}
+
+////////// SINT32
+
+__m256i libdivide_s32_do_vector(__m256i numers, const struct libdivide_s32_t *denom) {
+    uint8_t more = denom->more;
+    if (!denom->magic) {
+        uint32_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+        uint32_t mask = (1U << shift) - 1;
+        __m256i roundToZeroTweak = _mm256_set1_epi32(mask);
+        // q = numer + ((numer >> 31) & roundToZeroTweak);
+        __m256i q = _mm256_add_epi32(numers, _mm256_and_si256(_mm256_srai_epi32(numers, 31), roundToZeroTweak));
+        q = _mm256_srai_epi32(q, shift);
+        __m256i sign = _mm256_set1_epi32((int8_t)more >> 7);
+        // q = (q ^ sign) - sign;
+        q = _mm256_sub_epi32(_mm256_xor_si256(q, sign), sign);
+        return q;
+    }
+    else {
+        __m256i q = libdivide_mullhi_s32_vector(numers, _mm256_set1_epi32(denom->magic));
+        if (more & LIBDIVIDE_ADD_MARKER) {
+             // must be arithmetic shift
+            __m256i sign = _mm256_set1_epi32((int8_t)more >> 7);
+             // q += ((numer ^ sign) - sign);
+            q = _mm256_add_epi32(q, _mm256_sub_epi32(_mm256_xor_si256(numers, sign), sign));
+        }
+        // q >>= shift
+        q = _mm256_srai_epi32(q, more & LIBDIVIDE_32_SHIFT_MASK);
+        q = _mm256_add_epi32(q, _mm256_srli_epi32(q, 31)); // q += (q < 0)
+        return q;
+    }
+}
+
+__m256i libdivide_s32_branchfree_do_vector(__m256i numers, const struct libdivide_s32_branchfree_t *denom) {
+    int32_t magic = denom->magic;
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+     // must be arithmetic shift
+    __m256i sign = _mm256_set1_epi32((int8_t)more >> 7);
+    __m256i q = libdivide_mullhi_s32_vector(numers, _mm256_set1_epi32(magic));
+    q = _mm256_add_epi32(q, numers); // q += numers
+
+    // If q is non-negative, we have nothing to do
+    // If q is negative, we want to add either (2**shift)-1 if d is
+    // a power of 2, or (2**shift) if it is not a power of 2
+    uint32_t is_power_of_2 = (magic == 0);
+    __m256i q_sign = _mm256_srai_epi32(q, 31); // q_sign = q >> 31
+    __m256i mask = _mm256_set1_epi32((1U << shift) - is_power_of_2);
+    q = _mm256_add_epi32(q, _mm256_and_si256(q_sign, mask)); // q = q + (q_sign & mask)
+    q = _mm256_srai_epi32(q, shift); // q >>= shift
+    q = _mm256_sub_epi32(_mm256_xor_si256(q, sign), sign); // q = (q ^ sign) - sign
+    return q;
+}
+
+////////// SINT64
+
+__m256i libdivide_s64_do_vector(__m256i numers, const struct libdivide_s64_t *denom) {
+    uint8_t more = denom->more;
+    int64_t magic = denom->magic;
+    if (magic == 0) { // shift path
+        uint32_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+        uint64_t mask = (1ULL << shift) - 1;
+        __m256i roundToZeroTweak = _mm256_set1_epi64x(mask);
+        // q = numer + ((numer >> 63) & roundToZeroTweak);
+        __m256i q = _mm256_add_epi64(numers, _mm256_and_si256(libdivide_s64_signbits(numers), roundToZeroTweak));
+        q = libdivide_s64_shift_right_vector(q, shift);
+        __m256i sign = _mm256_set1_epi32((int8_t)more >> 7);
+         // q = (q ^ sign) - sign;
+        q = _mm256_sub_epi64(_mm256_xor_si256(q, sign), sign);
+        return q;
+    }
+    else {
+        __m256i q = libdivide_mullhi_s64_vector(numers, _mm256_set1_epi64x(magic));
+        if (more & LIBDIVIDE_ADD_MARKER) {
+            // must be arithmetic shift
+            __m256i sign = _mm256_set1_epi32((int8_t)more >> 7);
+            // q += ((numer ^ sign) - sign);
+            q = _mm256_add_epi64(q, _mm256_sub_epi64(_mm256_xor_si256(numers, sign), sign));
+        }
+        // q >>= denom->mult_path.shift
+        q = libdivide_s64_shift_right_vector(q, more & LIBDIVIDE_64_SHIFT_MASK);
+        q = _mm256_add_epi64(q, _mm256_srli_epi64(q, 63)); // q += (q < 0)
+        return q;
+    }
+}
+
+__m256i libdivide_s64_branchfree_do_vector(__m256i numers, const struct libdivide_s64_branchfree_t *denom) {
+    int64_t magic = denom->magic;
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+    // must be arithmetic shift
+    __m256i sign = _mm256_set1_epi32((int8_t)more >> 7);
+
+     // libdivide_mullhi_s64(numers, magic);
+    __m256i q = libdivide_mullhi_s64_vector(numers, _mm256_set1_epi64x(magic));
+    q = _mm256_add_epi64(q, numers); // q += numers
+
+    // If q is non-negative, we have nothing to do.
+    // If q is negative, we want to add either (2**shift)-1 if d is
+    // a power of 2, or (2**shift) if it is not a power of 2.
+    uint32_t is_power_of_2 = (magic == 0);
+    __m256i q_sign = libdivide_s64_signbits(q); // q_sign = q >> 63
+    __m256i mask = _mm256_set1_epi64x((1ULL << shift) - is_power_of_2);
+    q = _mm256_add_epi64(q, _mm256_and_si256(q_sign, mask)); // q = q + (q_sign & mask)
+    q = libdivide_s64_shift_right_vector(q, shift); // q >>= shift
+    q = _mm256_sub_epi64(_mm256_xor_si256(q, sign), sign); // q = (q ^ sign) - sign
+    return q;
+}
+
+#elif defined(LIBDIVIDE_SSE2)
+
+static inline __m128i libdivide_u32_do_vector(__m128i numers, const struct libdivide_u32_t *denom);
+static inline __m128i libdivide_s32_do_vector(__m128i numers, const struct libdivide_s32_t *denom);
+static inline __m128i libdivide_u64_do_vector(__m128i numers, const struct libdivide_u64_t *denom);
+static inline __m128i libdivide_s64_do_vector(__m128i numers, const struct libdivide_s64_t *denom);
+
+static inline __m128i libdivide_u32_branchfree_do_vector(__m128i numers, const struct libdivide_u32_branchfree_t *denom);
+static inline __m128i libdivide_s32_branchfree_do_vector(__m128i numers, const struct libdivide_s32_branchfree_t *denom);
+static inline __m128i libdivide_u64_branchfree_do_vector(__m128i numers, const struct libdivide_u64_branchfree_t *denom);
+static inline __m128i libdivide_s64_branchfree_do_vector(__m128i numers, const struct libdivide_s64_branchfree_t *denom);
+
+//////// Internal Utility Functions
+
+// Implementation of _mm_srai_epi64(v, 63) (from AVX512).
+static inline __m128i libdivide_s64_signbits(__m128i v) {
+    __m128i hiBitsDuped = _mm_shuffle_epi32(v, _MM_SHUFFLE(3, 3, 1, 1));
+    __m128i signBits = _mm_srai_epi32(hiBitsDuped, 31);
+    return signBits;
+}
+
+// Implementation of _mm_srai_epi64 (from AVX512).
+static inline __m128i libdivide_s64_shift_right_vector(__m128i v, int amt) {
+    const int b = 64 - amt;
+    __m128i m = _mm_set1_epi64x(1ULL << (b - 1));
+    __m128i x = _mm_srli_epi64(v, amt);
+    __m128i result = _mm_sub_epi64(_mm_xor_si128(x, m), m);
+    return result;
+}
+
+// Here, b is assumed to contain one 32-bit value repeated.
+static inline __m128i libdivide_mullhi_u32_vector(__m128i a, __m128i b) {
+    __m128i hi_product_0Z2Z = _mm_srli_epi64(_mm_mul_epu32(a, b), 32);
+    __m128i a1X3X = _mm_srli_epi64(a, 32);
+    __m128i mask = _mm_set_epi32(-1, 0, -1, 0);
+    __m128i hi_product_Z1Z3 = _mm_and_si128(_mm_mul_epu32(a1X3X, b), mask);
+    return _mm_or_si128(hi_product_0Z2Z, hi_product_Z1Z3);
+}
+
+// SSE2 does not have a signed multiplication instruction, but we can convert
+// unsigned to signed pretty efficiently. Again, b is just a 32 bit value
+// repeated four times.
+static inline __m128i libdivide_mullhi_s32_vector(__m128i a, __m128i b) {
+    __m128i p = libdivide_mullhi_u32_vector(a, b);
+    // t1 = (a >> 31) & y, arithmetic shift
+    __m128i t1 = _mm_and_si128(_mm_srai_epi32(a, 31), b);
+    __m128i t2 = _mm_and_si128(_mm_srai_epi32(b, 31), a);
+    p = _mm_sub_epi32(p, t1);
+    p = _mm_sub_epi32(p, t2);
+    return p;
+}
+
+// Here, y is assumed to contain one 64-bit value repeated.
+// https://stackoverflow.com/a/28827013
+static inline __m128i libdivide_mullhi_u64_vector(__m128i x, __m128i y) {
+    __m128i lomask = _mm_set1_epi64x(0xffffffff);
+    __m128i xh = _mm_shuffle_epi32(x, 0xB1);        // x0l, x0h, x1l, x1h
+    __m128i yh = _mm_shuffle_epi32(y, 0xB1);        // y0l, y0h, y1l, y1h
+    __m128i w0 = _mm_mul_epu32(x, y);               // x0l*y0l, x1l*y1l
+    __m128i w1 = _mm_mul_epu32(x, yh);              // x0l*y0h, x1l*y1h
+    __m128i w2 = _mm_mul_epu32(xh, y);              // x0h*y0l, x1h*y0l
+    __m128i w3 = _mm_mul_epu32(xh, yh);             // x0h*y0h, x1h*y1h
+    __m128i w0h = _mm_srli_epi64(w0, 32);
+    __m128i s1 = _mm_add_epi64(w1, w0h);
+    __m128i s1l = _mm_and_si128(s1, lomask);
+    __m128i s1h = _mm_srli_epi64(s1, 32);
+    __m128i s2 = _mm_add_epi64(w2, s1l);
+    __m128i s2h = _mm_srli_epi64(s2, 32);
+    __m128i hi = _mm_add_epi64(w3, s1h);
+            hi = _mm_add_epi64(hi, s2h);
+
+    return hi;
+}
+
+// y is one 64-bit value repeated.
+static inline __m128i libdivide_mullhi_s64_vector(__m128i x, __m128i y) {
+    __m128i p = libdivide_mullhi_u64_vector(x, y);
+    __m128i t1 = _mm_and_si128(libdivide_s64_signbits(x), y);
+    __m128i t2 = _mm_and_si128(libdivide_s64_signbits(y), x);
+    p = _mm_sub_epi64(p, t1);
+    p = _mm_sub_epi64(p, t2);
+    return p;
+}
+
+////////// UINT32
+
+__m128i libdivide_u32_do_vector(__m128i numers, const struct libdivide_u32_t *denom) {
+    uint8_t more = denom->more;
+    if (!denom->magic) {
+        return _mm_srli_epi32(numers, more);
+    }
+    else {
+        __m128i q = libdivide_mullhi_u32_vector(numers, _mm_set1_epi32(denom->magic));
+        if (more & LIBDIVIDE_ADD_MARKER) {
+            // uint32_t t = ((numer - q) >> 1) + q;
+            // return t >> denom->shift;
+            uint32_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+            __m128i t = _mm_add_epi32(_mm_srli_epi32(_mm_sub_epi32(numers, q), 1), q);
+            return _mm_srli_epi32(t, shift);
+        }
+        else {
+            return _mm_srli_epi32(q, more);
+        }
+    }
+}
+
+__m128i libdivide_u32_branchfree_do_vector(__m128i numers, const struct libdivide_u32_branchfree_t *denom) {
+    __m128i q = libdivide_mullhi_u32_vector(numers, _mm_set1_epi32(denom->magic));
+    __m128i t = _mm_add_epi32(_mm_srli_epi32(_mm_sub_epi32(numers, q), 1), q);
+    return _mm_srli_epi32(t, denom->more);
+}
+
+////////// UINT64
+
+__m128i libdivide_u64_do_vector(__m128i numers, const struct libdivide_u64_t *denom) {
+    uint8_t more = denom->more;
+    if (!denom->magic) {
+        return _mm_srli_epi64(numers, more);
+    }
+    else {
+        __m128i q = libdivide_mullhi_u64_vector(numers, _mm_set1_epi64x(denom->magic));
+        if (more & LIBDIVIDE_ADD_MARKER) {
+            // uint32_t t = ((numer - q) >> 1) + q;
+            // return t >> denom->shift;
+            uint32_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+            __m128i t = _mm_add_epi64(_mm_srli_epi64(_mm_sub_epi64(numers, q), 1), q);
+            return _mm_srli_epi64(t, shift);
+        }
+        else {
+            return _mm_srli_epi64(q, more);
+        }
+    }
+}
+
+__m128i libdivide_u64_branchfree_do_vector(__m128i numers, const struct libdivide_u64_branchfree_t *denom) {
+    __m128i q = libdivide_mullhi_u64_vector(numers, _mm_set1_epi64x(denom->magic));
+    __m128i t = _mm_add_epi64(_mm_srli_epi64(_mm_sub_epi64(numers, q), 1), q);
+    return _mm_srli_epi64(t, denom->more);
+}
+
+////////// SINT32
+
+__m128i libdivide_s32_do_vector(__m128i numers, const struct libdivide_s32_t *denom) {
+    uint8_t more = denom->more;
+    if (!denom->magic) {
+        uint32_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+        uint32_t mask = (1U << shift) - 1;
+        __m128i roundToZeroTweak = _mm_set1_epi32(mask);
+        // q = numer + ((numer >> 31) & roundToZeroTweak);
+        __m128i q = _mm_add_epi32(numers, _mm_and_si128(_mm_srai_epi32(numers, 31), roundToZeroTweak));
+        q = _mm_srai_epi32(q, shift);
+        __m128i sign = _mm_set1_epi32((int8_t)more >> 7);
+        // q = (q ^ sign) - sign;
+        q = _mm_sub_epi32(_mm_xor_si128(q, sign), sign);
+        return q;
+    }
+    else {
+        __m128i q = libdivide_mullhi_s32_vector(numers, _mm_set1_epi32(denom->magic));
+        if (more & LIBDIVIDE_ADD_MARKER) {
+             // must be arithmetic shift
+            __m128i sign = _mm_set1_epi32((int8_t)more >> 7);
+             // q += ((numer ^ sign) - sign);
+            q = _mm_add_epi32(q, _mm_sub_epi32(_mm_xor_si128(numers, sign), sign));
+        }
+        // q >>= shift
+        q = _mm_srai_epi32(q, more & LIBDIVIDE_32_SHIFT_MASK);
+        q = _mm_add_epi32(q, _mm_srli_epi32(q, 31)); // q += (q < 0)
+        return q;
+    }
+}
+
+__m128i libdivide_s32_branchfree_do_vector(__m128i numers, const struct libdivide_s32_branchfree_t *denom) {
+    int32_t magic = denom->magic;
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_32_SHIFT_MASK;
+     // must be arithmetic shift
+    __m128i sign = _mm_set1_epi32((int8_t)more >> 7);
+    __m128i q = libdivide_mullhi_s32_vector(numers, _mm_set1_epi32(magic));
+    q = _mm_add_epi32(q, numers); // q += numers
+
+    // If q is non-negative, we have nothing to do
+    // If q is negative, we want to add either (2**shift)-1 if d is
+    // a power of 2, or (2**shift) if it is not a power of 2
+    uint32_t is_power_of_2 = (magic == 0);
+    __m128i q_sign = _mm_srai_epi32(q, 31); // q_sign = q >> 31
+    __m128i mask = _mm_set1_epi32((1U << shift) - is_power_of_2);
+    q = _mm_add_epi32(q, _mm_and_si128(q_sign, mask)); // q = q + (q_sign & mask)
+    q = _mm_srai_epi32(q, shift); // q >>= shift
+    q = _mm_sub_epi32(_mm_xor_si128(q, sign), sign); // q = (q ^ sign) - sign
+    return q;
+}
+
+////////// SINT64
+
+__m128i libdivide_s64_do_vector(__m128i numers, const struct libdivide_s64_t *denom) {
+    uint8_t more = denom->more;
+    int64_t magic = denom->magic;
+    if (magic == 0) { // shift path
+        uint32_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+        uint64_t mask = (1ULL << shift) - 1;
+        __m128i roundToZeroTweak = _mm_set1_epi64x(mask);
+        // q = numer + ((numer >> 63) & roundToZeroTweak);
+        __m128i q = _mm_add_epi64(numers, _mm_and_si128(libdivide_s64_signbits(numers), roundToZeroTweak));
+        q = libdivide_s64_shift_right_vector(q, shift);
+        __m128i sign = _mm_set1_epi32((int8_t)more >> 7);
+         // q = (q ^ sign) - sign;
+        q = _mm_sub_epi64(_mm_xor_si128(q, sign), sign);
+        return q;
+    }
+    else {
+        __m128i q = libdivide_mullhi_s64_vector(numers, _mm_set1_epi64x(magic));
+        if (more & LIBDIVIDE_ADD_MARKER) {
+            // must be arithmetic shift
+            __m128i sign = _mm_set1_epi32((int8_t)more >> 7);
+            // q += ((numer ^ sign) - sign);
+            q = _mm_add_epi64(q, _mm_sub_epi64(_mm_xor_si128(numers, sign), sign));
+        }
+        // q >>= denom->mult_path.shift
+        q = libdivide_s64_shift_right_vector(q, more & LIBDIVIDE_64_SHIFT_MASK);
+        q = _mm_add_epi64(q, _mm_srli_epi64(q, 63)); // q += (q < 0)
+        return q;
+    }
+}
+
+__m128i libdivide_s64_branchfree_do_vector(__m128i numers, const struct libdivide_s64_branchfree_t *denom) {
+    int64_t magic = denom->magic;
+    uint8_t more = denom->more;
+    uint8_t shift = more & LIBDIVIDE_64_SHIFT_MASK;
+    // must be arithmetic shift
+    __m128i sign = _mm_set1_epi32((int8_t)more >> 7);
+
+     // libdivide_mullhi_s64(numers, magic);
+    __m128i q = libdivide_mullhi_s64_vector(numers, _mm_set1_epi64x(magic));
+    q = _mm_add_epi64(q, numers); // q += numers
+
+    // If q is non-negative, we have nothing to do.
+    // If q is negative, we want to add either (2**shift)-1 if d is
+    // a power of 2, or (2**shift) if it is not a power of 2.
+    uint32_t is_power_of_2 = (magic == 0);
+    __m128i q_sign = libdivide_s64_signbits(q); // q_sign = q >> 63
+    __m128i mask = _mm_set1_epi64x((1ULL << shift) - is_power_of_2);
+    q = _mm_add_epi64(q, _mm_and_si128(q_sign, mask)); // q = q + (q_sign & mask)
+    q = libdivide_s64_shift_right_vector(q, shift); // q >>= shift
+    q = _mm_sub_epi64(_mm_xor_si128(q, sign), sign); // q = (q ^ sign) - sign
+    return q;
+}
+
+#endif
+
+/////////// C++ stuff
+
+#ifdef __cplusplus
+
+// The C++ divider class is templated on both an integer type
+// (like uint64_t) and an algorithm type.
+// * BRANCHFULL is the default algorithm type.
+// * BRANCHFREE is the branchfree algorithm type.
+enum {
+    BRANCHFULL,
+    BRANCHFREE
+};
+
+#if defined(LIBDIVIDE_AVX512)
+    #define LIBDIVIDE_VECTOR_TYPE __m512i
+#elif defined(LIBDIVIDE_AVX2)
+    #define LIBDIVIDE_VECTOR_TYPE __m256i
+#elif defined(LIBDIVIDE_SSE2)
+    #define LIBDIVIDE_VECTOR_TYPE __m128i
+#endif
+
+#if !defined(LIBDIVIDE_VECTOR_TYPE)
+    #define LIBDIVIDE_DIVIDE_VECTOR(ALGO)
+#else
+    #define LIBDIVIDE_DIVIDE_VECTOR(ALGO) \
+        LIBDIVIDE_VECTOR_TYPE divide(LIBDIVIDE_VECTOR_TYPE n) const { \
+            return libdivide_##ALGO##_do_vector(n, &denom); \
+        }
+#endif
+
+// The DISPATCHER_GEN() macro generates C++ methods (for the given integer
+// and algorithm types) that redirect to libdivide's C API.
+#define DISPATCHER_GEN(T, ALGO) \
+    libdivide_##ALGO##_t denom; \
+    dispatcher() { } \
+    dispatcher(T d) \
+        : denom(libdivide_##ALGO##_gen(d)) \
+    { } \
+    T divide(T n) const { \
+        return libdivide_##ALGO##_do(n, &denom); \
+    } \
+    LIBDIVIDE_DIVIDE_VECTOR(ALGO) \
+    T recover() const { \
+        return libdivide_##ALGO##_recover(&denom); \
+    }
+
+// The dispatcher selects a specific division algorithm for a given
+// type and ALGO using partial template specialization.
+template<bool IS_INTEGRAL, bool IS_SIGNED, int SIZEOF, int ALGO> struct dispatcher { };
+
+template<> struct dispatcher<true, true, sizeof(int32_t), BRANCHFULL> { DISPATCHER_GEN(int32_t, s32) };
+template<> struct dispatcher<true, true, sizeof(int32_t), BRANCHFREE> { DISPATCHER_GEN(int32_t, s32_branchfree) };
+template<> struct dispatcher<true, false, sizeof(uint32_t), BRANCHFULL> { DISPATCHER_GEN(uint32_t, u32) };
+template<> struct dispatcher<true, false, sizeof(uint32_t), BRANCHFREE> { DISPATCHER_GEN(uint32_t, u32_branchfree) };
+template<> struct dispatcher<true, true, sizeof(int64_t), BRANCHFULL> { DISPATCHER_GEN(int64_t, s64) };
+template<> struct dispatcher<true, true, sizeof(int64_t), BRANCHFREE> { DISPATCHER_GEN(int64_t, s64_branchfree) };
+template<> struct dispatcher<true, false, sizeof(uint64_t), BRANCHFULL> { DISPATCHER_GEN(uint64_t, u64) };
+template<> struct dispatcher<true, false, sizeof(uint64_t), BRANCHFREE> { DISPATCHER_GEN(uint64_t, u64_branchfree) };
+
+// This is the main divider class for use by the user (C++ API).
+// The actual division algorithm is selected using the dispatcher struct
+// based on the integer and algorithm template parameters.
+template<typename T, int ALGO = BRANCHFULL>
+class divider {
+public:
+    // We leave the default constructor empty so that creating
+    // an array of dividers and then initializing them
+    // later doesn't slow us down.
+    divider() { }
+
+    // Constructor that takes the divisor as a parameter
+    divider(T d) : div(d) { }
+
+    // Divides n by the divisor
+    T divide(T n) const {
+        return div.divide(n);
+    }
+
+    // Recovers the divisor, returns the value that was
+    // used to initialize this divider object.
+    T recover() const {
+        return div.recover();
+    }
+
+    bool operator==(const divider<T, ALGO>& other) const {
+        return div.denom.magic == other.denom.magic &&
+               div.denom.more == other.denom.more;
+    }
+
+    bool operator!=(const divider<T, ALGO>& other) const {
+        return !(*this == other);
+    }
+
+#if defined(LIBDIVIDE_VECTOR_TYPE)
+    // Treats the vector as packed integer values with the same type as
+    // the divider (e.g. s32, u32, s64, u64) and divides each of
+    // them by the divider, returning the packed quotients.
+    LIBDIVIDE_VECTOR_TYPE divide(LIBDIVIDE_VECTOR_TYPE n) const {
+        return div.divide(n);
+    }
+#endif
+
+private:
+    // Storage for the actual divisor
+    dispatcher<std::is_integral<T>::value,
+               std::is_signed<T>::value, sizeof(T), ALGO> div;
+};
+
+// Overload of operator / for scalar division
+template<typename T, int ALGO>
+T operator/(T n, const divider<T, ALGO>& div) {
+    return div.divide(n);
+}
+
+// Overload of operator /= for scalar division
+template<typename T, int ALGO>
+T& operator/=(T& n, const divider<T, ALGO>& div) {
+    n = div.divide(n);
+    return n;
+}
+
+#if defined(LIBDIVIDE_VECTOR_TYPE)
+    // Overload of operator / for vector division
+    template<typename T, int ALGO>
+    LIBDIVIDE_VECTOR_TYPE operator/(LIBDIVIDE_VECTOR_TYPE n, const divider<T, ALGO>& div) {
+        return div.divide(n);
+    }
+    // Overload of operator /= for vector division
+    template<typename T, int ALGO>
+    LIBDIVIDE_VECTOR_TYPE& operator/=(LIBDIVIDE_VECTOR_TYPE& n, const divider<T, ALGO>& div) {
+        n = div.divide(n);
+        return n;
+    }
+#endif
+
+// libdivdie::branchfree_divider<T>
+template <typename T>
+using branchfree_divider = divider<T, BRANCHFREE>;
+
+} // namespace libdivide
+
+#endif // __cplusplus
+
+#endif // LIBDIVIDE_H
diff --git a/src/locale/en.po b/src/locale/en.po
index 30ebe4368fe45ddfa5f5be5989f13a6c00e0fcaf..8dd08173d7869cd799490b473c2629bb35306e3f 100644
--- a/src/locale/en.po
+++ b/src/locale/en.po
@@ -466,7 +466,7 @@ msgid ""
 msgstr ""
 
 #: d_clisrv.c:1764
-msgid "has been kicked (Go away)\n"
+msgid "has been kicked (No reason given)\n"
 msgstr ""
 
 #: d_clisrv.c:1768
@@ -474,7 +474,7 @@ msgid "left the game (Broke ping limit)\n"
 msgstr ""
 
 #: d_clisrv.c:1772
-msgid "left the game (Consistency failure)\n"
+msgid "left the game (Synch failure)\n"
 msgstr ""
 
 #: d_clisrv.c:1778
@@ -501,7 +501,7 @@ msgid "left the game\n"
 msgstr ""
 
 #: d_clisrv.c:1798
-msgid "has been banned (Don't come back)\n"
+msgid "has been banned (No reason given)\n"
 msgstr ""
 
 #: d_clisrv.c:1802
diff --git a/src/locale/srb2.pot b/src/locale/srb2.pot
index 960c36dbe8e523d31c666faf2bc4beee3830c0df..cd2db750de93d1a2b3dda973a36fba5492700e1a 100644
--- a/src/locale/srb2.pot
+++ b/src/locale/srb2.pot
@@ -459,7 +459,7 @@ msgid ""
 msgstr ""
 
 #: d_clisrv.c:1889
-msgid "has been kicked (Go away)\n"
+msgid "has been kicked (No reason given)\n"
 msgstr ""
 
 #: d_clisrv.c:1893
@@ -467,7 +467,7 @@ msgid "left the game (Broke ping limit)\n"
 msgstr ""
 
 #: d_clisrv.c:1897
-msgid "left the game (Consistency failure)\n"
+msgid "left the game (Synch failure)\n"
 msgstr ""
 
 #: d_clisrv.c:1903
@@ -494,7 +494,7 @@ msgid "left the game\n"
 msgstr ""
 
 #: d_clisrv.c:1923
-msgid "has been banned (Don't come back)\n"
+msgid "has been banned (No reason given)\n"
 msgstr ""
 
 #: d_clisrv.c:1927
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 4667fdbf4a7549ae226075ff8d5f09feeb9cc05f..12ad4fee0549bbaad9d5ea2dd3106c9bd9b8f955 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -28,6 +28,10 @@
 #include "console.h"
 #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"
@@ -155,6 +159,8 @@ static const struct {
 	{META_PIVOTLIST,    "spriteframepivot_t[]"},
 	{META_FRAMEPIVOT,   "spriteframepivot_t"},
 
+	{META_TAGLIST,      "taglist"},
+
 	{META_MOBJ,         "mobj_t"},
 	{META_MAPTHING,     "mapthing_t"},
 
@@ -163,6 +169,8 @@ static const struct {
 	{META_SKIN,         "skin_t"},
 	{META_POWERS,       "player_t.powers"},
 	{META_SOUNDSID,     "skin_t.soundsid"},
+	{META_SKINSPRITES,  "skin_t.sprites"},
+	{META_SKINSPRITESLIST,  "skin_t.sprites[]"},
 
 	{META_VERTEX,       "vertex_t"},
 	{META_LINE,         "line_t"},
@@ -180,10 +188,15 @@ 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"},
 
 	{META_SECTORLINES,  "sector_t.lines"},
+#ifdef MUTABLE_TAGS
+	{META_SECTORTAGLIST, "sector_t.taglist"},
+#endif
 	{META_SIDENUM,      "line_t.sidenum"},
 	{META_LINEARGS,     "line_t.args"},
 	{META_LINESTRINGARGS, "line_t.stringargs"},
@@ -205,6 +218,9 @@ static const struct {
 	{META_ACTION,       "action"},
 
 	{META_LUABANKS,     "luabanks[]"},
+
+	{META_KEYEVENT,     "keyevent_t"},
+	{META_MOUSE,        "mouse_t"},
 	{NULL,              NULL}
 };
 
@@ -235,16 +251,10 @@ static const char *GetUserdataUType(lua_State *L)
 //   or players[0].powers -> "player_t.powers"
 static int lib_userdataType(lua_State *L)
 {
-	int type;
 	lua_settop(L, 1); // pop everything except arg 1 (in case somebody decided to add more)
-	type = lua_type(L, 1);
-	if (type == LUA_TLIGHTUSERDATA || type == LUA_TUSERDATA)
-	{
-		lua_pushstring(L, GetUserdataUType(L));
-		return 1;
-	}
-	else
-		return luaL_typerror(L, 1, "userdata");
+	luaL_checktype(L, 1, LUA_TUSERDATA);
+	lua_pushstring(L, GetUserdataUType(L));
+	return 1;
 }
 
 // Takes a metatable as first and only argument
@@ -356,6 +366,23 @@ static int lib_pGetColorAfter(lua_State *L)
 	return 1;
 }
 
+// M_MISC
+//////////////
+
+static int lib_mMapNumber(lua_State *L)
+{
+	const char *arg = luaL_checkstring(L, 1);
+	size_t len = strlen(arg);
+	if (len == 2 || len == 5) {
+		char first = arg[len-2];
+		char second = arg[len-1];
+		lua_pushinteger(L, M_MapNumber(first, second));
+	} else {
+		lua_pushinteger(L, 0);
+	}
+	return 1;
+}
+
 // M_RANDOM
 //////////////
 
@@ -1043,48 +1070,56 @@ static int lib_pSceneryXYMovement(lua_State *L)
 static int lib_pZMovement(lua_State *L)
 {
 	mobj_t *actor = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+	mobj_t *ptmthing = tmthing;
 	NOHUD
 	INLEVEL
 	if (!actor)
 		return LUA_ErrInvalid(L, "mobj_t");
 	lua_pushboolean(L, P_ZMovement(actor));
 	P_CheckPosition(actor, actor->x, actor->y);
+	P_SetTarget(&tmthing, ptmthing);
 	return 1;
 }
 
 static int lib_pRingZMovement(lua_State *L)
 {
 	mobj_t *actor = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+	mobj_t *ptmthing = tmthing;
 	NOHUD
 	INLEVEL
 	if (!actor)
 		return LUA_ErrInvalid(L, "mobj_t");
 	P_RingZMovement(actor);
 	P_CheckPosition(actor, actor->x, actor->y);
+	P_SetTarget(&tmthing, ptmthing);
 	return 0;
 }
 
 static int lib_pSceneryZMovement(lua_State *L)
 {
 	mobj_t *actor = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+	mobj_t *ptmthing = tmthing;
 	NOHUD
 	INLEVEL
 	if (!actor)
 		return LUA_ErrInvalid(L, "mobj_t");
 	lua_pushboolean(L, P_SceneryZMovement(actor));
 	P_CheckPosition(actor, actor->x, actor->y);
+	P_SetTarget(&tmthing, ptmthing);
 	return 1;
 }
 
 static int lib_pPlayerZMovement(lua_State *L)
 {
 	mobj_t *actor = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+	mobj_t *ptmthing = tmthing;
 	NOHUD
 	INLEVEL
 	if (!actor)
 		return LUA_ErrInvalid(L, "mobj_t");
 	P_PlayerZMovement(actor);
 	P_CheckPosition(actor, actor->x, actor->y);
+	P_SetTarget(&tmthing, ptmthing);
 	return 0;
 }
 
@@ -1471,11 +1506,13 @@ static int lib_pSpawnSkidDust(lua_State *L)
 static int lib_pMovePlayer(lua_State *L)
 {
 	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	mobj_t *ptmthing = tmthing;
 	NOHUD
 	INLEVEL
 	if (!player)
 		return LUA_ErrInvalid(L, "player_t");
 	P_MovePlayer(player);
+	P_SetTarget(&tmthing, ptmthing);
 	return 0;
 }
 
@@ -1664,6 +1701,26 @@ static int lib_pSwitchShield(lua_State *L)
 	return 0;
 }
 
+static int lib_pPlayerCanEnterSpinGaps(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	lua_pushboolean(L, P_PlayerCanEnterSpinGaps(player));
+	return 1;
+}
+
+static int lib_pPlayerShouldUseSpinHeight(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	lua_pushboolean(L, P_PlayerShouldUseSpinHeight(player));
+	return 1;
+}
+
 // P_MAP
 ///////////
 
@@ -1832,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
 ////////////
 
@@ -2487,6 +2575,17 @@ static int lib_pGetZAt(lua_State *L)
 	return 1;
 }
 
+static int lib_pButteredSlope(lua_State *L)
+{
+	mobj_t *mobj = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+	NOHUD
+	INLEVEL
+	if (!mobj)
+		return LUA_ErrInvalid(L, "mobj_t");
+	P_ButteredSlope(mobj);
+	return 0;
+}
+
 // R_DEFS
 ////////////
 
@@ -2801,46 +2900,13 @@ static int lib_sStopSoundByID(lua_State *L)
 
 static int lib_sChangeMusic(lua_State *L)
 {
-#ifdef MUSICSLOT_COMPATIBILITY
-	const char *music_name;
-	UINT32 music_num, position, prefadems, fadeinms;
-	char music_compat_name[7];
+	UINT32 position, prefadems, fadeinms;
 
-	boolean looping;
-	player_t *player = NULL;
-	UINT16 music_flags = 0;
-	//NOHUD
-
-	if (lua_isnumber(L, 1))
-	{
-		music_num = (UINT32)luaL_checkinteger(L, 1);
-		music_flags = (UINT16)(music_num & 0x0000FFFF);
-		if (music_flags && music_flags <= 1035)
-			snprintf(music_compat_name, 7, "%sM", G_BuildMapName((INT32)music_flags));
-		else if (music_flags && music_flags <= 1050)
-			strncpy(music_compat_name, compat_special_music_slots[music_flags - 1036], 7);
-		else
-			music_compat_name[0] = 0; // becomes empty string
-		music_compat_name[6] = 0;
-		music_name = (const char *)&music_compat_name;
-		music_flags = 0;
-	}
-	else
-	{
-		music_num = 0;
-		music_name = luaL_checkstring(L, 1);
-	}
-
-	looping = (boolean)lua_opttrueboolean(L, 2);
-
-#else
 	const char *music_name = luaL_checkstring(L, 1);
 	boolean looping = (boolean)lua_opttrueboolean(L, 2);
 	player_t *player = NULL;
 	UINT16 music_flags = 0;
-	//NOHUD
 
-#endif
 	if (!lua_isnone(L, 3) && lua_isuserdata(L, 3))
 	{
 		player = *((player_t **)luaL_checkudata(L, 3, META_PLAYER));
@@ -2848,13 +2914,7 @@ static int lib_sChangeMusic(lua_State *L)
 			return LUA_ErrInvalid(L, "player_t");
 	}
 
-#ifdef MUSICSLOT_COMPATIBILITY
-	if (music_num)
-		music_flags = (UINT16)((music_num & 0x7FFF0000) >> 16);
-	else
-#endif
 	music_flags = (UINT16)luaL_optinteger(L, 4, 0);
-
 	position = (UINT32)luaL_optinteger(L, 5, 0);
 	prefadems = (UINT32)luaL_optinteger(L, 6, 0);
 	fadeinms = (UINT32)luaL_optinteger(L, 7, 0);
@@ -3151,33 +3211,7 @@ static int lib_sMusicExists(lua_State *L)
 {
 	boolean checkMIDI = lua_opttrueboolean(L, 2);
 	boolean checkDigi = lua_opttrueboolean(L, 3);
-#ifdef MUSICSLOT_COMPATIBILITY
-	const char *music_name;
-	UINT32 music_num;
-	char music_compat_name[7];
-	UINT16 music_flags = 0;
-	NOHUD
-	if (lua_isnumber(L, 1))
-	{
-		music_num = (UINT32)luaL_checkinteger(L, 1);
-		music_flags = (UINT16)(music_num & 0x0000FFFF);
-		if (music_flags && music_flags <= 1035)
-			snprintf(music_compat_name, 7, "%sM", G_BuildMapName((INT32)music_flags));
-		else if (music_flags && music_flags <= 1050)
-			strncpy(music_compat_name, compat_special_music_slots[music_flags - 1036], 7);
-		else
-			music_compat_name[0] = 0; // becomes empty string
-		music_compat_name[6] = 0;
-		music_name = (const char *)&music_compat_name;
-	}
-	else
-	{
-		music_num = 0;
-		music_name = luaL_checkstring(L, 1);
-	}
-#else
 	const char *music_name = luaL_checkstring(L, 1);
-#endif
 	NOHUD
 	lua_pushboolean(L, S_MusicExists(music_name, checkMIDI, checkDigi));
 	return 1;
@@ -3400,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)
@@ -3741,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},
@@ -3757,6 +3902,9 @@ static luaL_Reg lib[] = {
 	{"M_GetColorAfter",lib_pGetColorAfter},
 	{"M_GetColorBefore",lib_pGetColorBefore},
 
+	// m_misc
+	{"M_MapNumber",lib_mMapNumber},
+
 	// m_random
 	{"P_RandomFixed",lib_pRandomFixed},
 	{"P_RandomByte",lib_pRandomByte},
@@ -3865,6 +4013,8 @@ static luaL_Reg lib[] = {
 	{"P_SpawnSpinMobj",lib_pSpawnSpinMobj},
 	{"P_Telekinesis",lib_pTelekinesis},
 	{"P_SwitchShield",lib_pSwitchShield},
+	{"P_PlayerCanEnterSpinGaps",lib_pPlayerCanEnterSpinGaps},
+	{"P_PlayerShouldUseSpinHeight",lib_pPlayerShouldUseSpinHeight},
 
 	// p_map
 	{"P_CheckPosition",lib_pCheckPosition},
@@ -3879,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},
@@ -3925,6 +4077,7 @@ static luaL_Reg lib[] = {
 
 	// p_slopes
 	{"P_GetZAt",lib_pGetZAt},
+	{"P_ButteredSlope",lib_pButteredSlope},
 
 	// r_defs
 	{"R_PointToAngle",lib_rPointToAngle},
@@ -3980,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},
@@ -4005,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_blockmaplib.c b/src/lua_blockmaplib.c
index 1949d56bb56fdca88c46005ae3118affc4e9c1df..9089d19b6a02c5126aeefde5e74aa1ec3ff8511e 100644
--- a/src/lua_blockmaplib.c
+++ b/src/lua_blockmaplib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2016-2020 by Iestyn "Monster Iestyn" Jealous.
-// Copyright (C) 2016-2020 by Sonic Team Junior.
+// Copyright (C) 2016-2021 by Iestyn "Monster Iestyn" Jealous.
+// Copyright (C) 2016-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/lua_consolelib.c b/src/lua_consolelib.c
index 84bfeaee2c07dbbae2257b083635eb9bb39e94fb..2b8cad69b8b5fb920294bdc4e1c0475fa6a59da0 100644
--- a/src/lua_consolelib.c
+++ b/src/lua_consolelib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -28,7 +28,7 @@ return luaL_error(L, "HUD rendering code should not call this function!");
 #define NOHOOK if (!lua_lumploading)\
 		return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
 
-static const char *cvname = NULL;
+static consvar_t *this_cvar;
 
 void Got_Luacmd(UINT8 **cp, INT32 playernum)
 {
@@ -273,34 +273,29 @@ static int lib_comBufInsertText(lua_State *L)
 	return 0;
 }
 
-void LUA_CVarChanged(const char *name)
+void LUA_CVarChanged(void *cvar)
 {
-	cvname = name;
+	this_cvar = cvar;
 }
 
 static void Lua_OnChange(void)
 {
-	I_Assert(gL != NULL);
-	I_Assert(cvname != NULL);
-
 	/// \todo Network this! XD_LUAVAR
 
-	lua_settop(gL, 0); // Just in case...
 	lua_pushcfunction(gL, LUA_GetErrorMessage);
+	lua_insert(gL, 1); // Because LUA_Call wants it at index 1.
 
 	// From CV_OnChange registry field, get the function for this cvar by name.
 	lua_getfield(gL, LUA_REGISTRYINDEX, "CV_OnChange");
 	I_Assert(lua_istable(gL, -1));
-	lua_getfield(gL, -1, cvname); // get function
+	lua_pushlightuserdata(gL, this_cvar);
+	lua_rawget(gL, -2); // get function
 
-	// From the CV_Vars registry field, get the cvar's userdata by name.
-	lua_getfield(gL, LUA_REGISTRYINDEX, "CV_Vars");
-	I_Assert(lua_istable(gL, -1));
-	lua_getfield(gL, -1, cvname); // get consvar_t* userdata.
-	lua_remove(gL, -2); // pop the CV_Vars table.
+	LUA_RawPushUserdata(gL, this_cvar);
 
 	LUA_Call(gL, 1, 0, 1); // call function(cvar)
 	lua_pop(gL, 1); // pop CV_OnChange table
+	lua_remove(gL, 1); // remove LUA_GetErrorMessage
 }
 
 static int lib_cvRegisterVar(lua_State *L)
@@ -311,15 +306,12 @@ static int lib_cvRegisterVar(lua_State *L)
 	luaL_checktype(L, 1, LUA_TTABLE);
 	lua_settop(L, 1); // Clear out all other possible arguments, leaving only the first one.
 	NOHOOK
-	cvar = lua_newuserdata(L, sizeof(consvar_t));
-	luaL_getmetatable(L, META_CVAR);
-	lua_setmetatable(L, -2);
+	cvar = ZZ_Calloc(sizeof(consvar_t));
+	LUA_PushUserdata(L, cvar, META_CVAR);
 
 #define FIELDERROR(f, e) luaL_error(L, "bad value for " LUA_QL(f) " in table passed to " LUA_QL("CV_RegisterVar") " (%s)", e);
 #define TYPEERROR(f, t) FIELDERROR(f, va("%s expected, got %s", lua_typename(L, t), luaL_typename(L, -1)))
 
-	memset(cvar, 0x00, sizeof(consvar_t)); // zero everything by default
-
 	lua_pushnil(L);
 	while (lua_next(L, 1)) {
 		// stack: cvar table, cvar userdata, key/index, value
@@ -368,7 +360,7 @@ static int lib_cvRegisterVar(lua_State *L)
 
 				lua_getfield(L, LUA_REGISTRYINDEX, "CV_PossibleValue");
 				I_Assert(lua_istable(L, 5));
-				lua_pushvalue(L, 2); // cvar userdata
+				lua_pushlightuserdata(L, cvar);
 				cvpv = lua_newuserdata(L, sizeof(CV_PossibleValue_t) * (count+1));
 				lua_rawset(L, 5);
 				lua_pop(L, 1); // pop CV_PossibleValue registry table
@@ -396,8 +388,9 @@ static int lib_cvRegisterVar(lua_State *L)
 				TYPEERROR("func", LUA_TFUNCTION)
 			lua_getfield(L, LUA_REGISTRYINDEX, "CV_OnChange");
 			I_Assert(lua_istable(L, 5));
+			lua_pushlightuserdata(L, cvar);
 			lua_pushvalue(L, 4);
-			lua_setfield(L, 5, cvar->name);
+			lua_rawset(L, 5);
 			lua_pop(L, 1);
 			cvar->func = Lua_OnChange;
 		}
@@ -414,19 +407,6 @@ static int lib_cvRegisterVar(lua_State *L)
 	if ((cvar->flags & CV_CALL) && !cvar->func)
 		return luaL_error(L, M_GetText("Variable %s has CV_CALL without a function\n"), cvar->name);
 
-	// stack: cvar table, cvar userdata
-	lua_getfield(L, LUA_REGISTRYINDEX, "CV_Vars");
-	I_Assert(lua_istable(L, 3));
-
-	lua_getfield(L, 3, cvar->name);
-	if (lua_type(L, -1) != LUA_TNIL)
-		return luaL_error(L, M_GetText("Variable %s is already defined\n"), cvar->name);
-	lua_pop(L, 1);
-
-	lua_pushvalue(L, 2);
-	lua_setfield(L, 3, cvar->name);
-	lua_pop(L, 1);
-
 	// actually time to register it to the console now! Finally!
 	cvar->flags |= CV_MODIFIED;
 	CV_RegisterVar(cvar);
@@ -439,7 +419,8 @@ static int lib_cvRegisterVar(lua_State *L)
 
 static int lib_cvFindVar(lua_State *L)
 {
-	LUA_PushLightUserdata(L, CV_FindVar(luaL_checkstring(L,1)), META_CVAR);
+	const char *name = luaL_checkstring(L, 1);
+	LUA_PushUserdata(L, CV_FindVar(name), META_CVAR);
 	return 1;
 }
 
@@ -449,10 +430,10 @@ static int CVarSetFunction
 		void (*Set)(consvar_t *, const char *),
 		void (*SetValue)(consvar_t *, INT32)
 ){
-	consvar_t *cvar = (consvar_t *)luaL_checkudata(L, 1, META_CVAR);
+	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))
 	{
@@ -481,7 +462,7 @@ static int lib_cvStealthSet(lua_State *L)
 
 static int lib_cvAddValue(lua_State *L)
 {
-	consvar_t *cvar = (consvar_t *)luaL_checkudata(L, 1, META_CVAR);
+	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);
@@ -540,7 +521,7 @@ static luaL_Reg lib[] = {
 
 static int cvar_get(lua_State *L)
 {
-	consvar_t *cvar = (consvar_t *)luaL_checkudata(L, 1, META_CVAR);
+	consvar_t *cvar = *(consvar_t **)luaL_checkudata(L, 1, META_CVAR);
 	const char *field = luaL_checkstring(L, 2);
 
 	if(fastcmp(field,"name"))
diff --git a/src/lua_hook.h b/src/lua_hook.h
index 796f3a9d287dcf0e3997d6963949b33111dceb0d..531d16288ab507e19dc6f47e92e0157772306aa8 100644
--- a/src/lua_hook.h
+++ b/src/lua_hook.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -12,111 +12,139 @@
 
 #include "r_defs.h"
 #include "d_player.h"
+#include "s_sound.h"
+#include "d_event.h"
 
-enum hook {
-	hook_NetVars=0,
-	hook_MapChange,
-	hook_MapLoad,
-	hook_PlayerJoin,
-	hook_PreThinkFrame,
-	hook_ThinkFrame,
-	hook_PostThinkFrame,
-	hook_MobjSpawn,
-	hook_MobjCollide,
-	hook_MobjLineCollide,
-	hook_MobjMoveCollide,
-	hook_TouchSpecial,
-	hook_MobjFuse,
-	hook_MobjThinker,
-	hook_BossThinker,
-	hook_ShouldDamage,
-	hook_MobjDamage,
-	hook_MobjDeath,
-	hook_BossDeath,
-	hook_MobjRemoved,
-	hook_JumpSpecial,
-	hook_AbilitySpecial,
-	hook_SpinSpecial,
-	hook_JumpSpinSpecial,
-	hook_BotTiccmd,
-	hook_BotAI,
-	hook_BotRespawn,
-	hook_LinedefExecute,
-	hook_PlayerMsg,
-	hook_HurtMsg,
-	hook_PlayerSpawn,
-	hook_ShieldSpawn,
-	hook_ShieldSpecial,
-	hook_MobjMoveBlocked,
-	hook_MapThingSpawn,
-	hook_FollowMobj,
-	hook_PlayerCanDamage,
-	hook_PlayerQuit,
-	hook_IntermissionThinker,
-	hook_TeamSwitch,
-	hook_ViewpointSwitch,
-	hook_SeenPlayer,
-	hook_PlayerThink,
-	hook_ShouldJingleContinue,
-	hook_GameQuit,
-	hook_PlayerCmd,
-	hook_MusicChange,
-
-	hook_MAX // last hook
-};
-extern const char *const hookNames[];
+/*
+Do you know what an 'X Macro' is? Such a macro is called over each element of
+a list and expands the input. I use it for the hook lists because both an enum
+and array of hook names need to be kept in order. The X Macro handles this
+automatically.
+*/
+
+#define MOBJ_HOOK_LIST(X) \
+	X (MobjSpawn),/* P_SpawnMobj */\
+	X (MobjCollide),/* PIT_CheckThing */\
+	X (MobjLineCollide),/* ditto */\
+	X (MobjMoveCollide),/* tritto */\
+	X (TouchSpecial),/* P_TouchSpecialThing */\
+	X (MobjFuse),/* when mobj->fuse runs out */\
+	X (MobjThinker),/* P_MobjThinker, P_SceneryThinker */\
+	X (BossThinker),/* P_GenericBossThinker */\
+	X (ShouldDamage),/* P_DamageMobj (Should mobj take damage?) */\
+	X (MobjDamage),/* P_DamageMobj (Mobj actually takes damage!) */\
+	X (MobjDeath),/* P_KillMobj */\
+	X (BossDeath),/* A_BossDeath */\
+	X (MobjRemoved),/* P_RemoveMobj */\
+	X (BotRespawn),/* B_CheckRespawn */\
+	X (MobjMoveBlocked),/* P_XYMovement (when movement is blocked) */\
+	X (MapThingSpawn),/* P_SpawnMapThing */\
+	X (FollowMobj),/* P_PlayerAfterThink Smiles mobj-following */\
+
+#define HOOK_LIST(X) \
+	X (NetVars),/* add to archive table (netsave) */\
+	X (MapChange),/* (before map load) */\
+	X (MapLoad),\
+	X (PlayerJoin),/* Got_AddPlayer */\
+	X (PreThinkFrame)/* frame (before mobj and player thinkers) */,\
+	X (ThinkFrame),/* frame (after mobj and player thinkers) */\
+	X (PostThinkFrame),/* frame (at end of tick, ie after overlays, precipitation, specials) */\
+	X (JumpSpecial),/* P_DoJumpStuff (Any-jumping) */\
+	X (AbilitySpecial),/* P_DoJumpStuff (Double-jumping) */\
+	X (SpinSpecial),/* P_DoSpinAbility (Spin button effect) */\
+	X (JumpSpinSpecial),/* P_DoJumpStuff (Spin button effect (mid-air)) */\
+	X (BotTiccmd),/* B_BuildTiccmd */\
+	X (PlayerMsg),/* chat messages */\
+	X (HurtMsg),/* imhurttin */\
+	X (PlayerSpawn),/* G_SpawnPlayer */\
+	X (ShieldSpawn),/* P_SpawnShieldOrb */\
+	X (ShieldSpecial),/* shield abilities */\
+	X (PlayerCanDamage),/* P_PlayerCanDamage */\
+	X (PlayerQuit),\
+	X (IntermissionThinker),/* Y_Ticker */\
+	X (TeamSwitch),/* team switching in... uh... *what* speak, spit it the fuck out */\
+	X (ViewpointSwitch),/* spy mode (no trickstabs) */\
+	X (SeenPlayer),/* MT_NAMECHECK */\
+	X (PlayerThink),/* P_PlayerThink */\
+	X (GameQuit),\
+	X (PlayerCmd),/* building the player's ticcmd struct (Ported from SRB2Kart) */\
+	X (MusicChange),\
+	X (PlayerHeight),/* override player height */\
+	X (PlayerCanEnterSpinGaps),\
+	X (KeyDown),\
+	X (KeyUp),\
+
+#define STRING_HOOK_LIST(X) \
+	X (BotAI),/* B_BuildTailsTiccmd by skin name */\
+	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.
+(Since each enumeration is not defined in the source code, but by the list
+macros above, it is not greppable.) The name passed to the macro can also be
+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
+
+#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 LUAh_MapChange(INT16 mapnumber); // Hook for map change (before load)
-void LUAh_MapLoad(void); // Hook for map load
-void LUAh_PlayerJoin(int playernum); // Hook for Got_AddPlayer
-void LUAh_PreThinkFrame(void); // Hook for frame (before mobj and player thinkers)
-void LUAh_ThinkFrame(void); // Hook for frame (after mobj and player thinkers)
-void LUAh_PostThinkFrame(void); // Hook for frame (at end of tick, ie after overlays, precipitation, specials)
-boolean LUAh_MobjHook(mobj_t *mo, enum hook which);
-boolean LUAh_PlayerHook(player_t *plr, enum hook which);
-#define LUAh_MobjSpawn(mo) LUAh_MobjHook(mo, hook_MobjSpawn) // Hook for P_SpawnMobj by mobj type
-UINT8 LUAh_MobjCollideHook(mobj_t *thing1, mobj_t *thing2, enum hook which);
-UINT8 LUAh_MobjLineCollideHook(mobj_t *thing, line_t *line, enum hook which);
-#define LUAh_MobjCollide(thing1, thing2) LUAh_MobjCollideHook(thing1, thing2, hook_MobjCollide) // Hook for PIT_CheckThing by (thing) mobj type
-#define LUAh_MobjLineCollide(thing, line) LUAh_MobjLineCollideHook(thing, line, hook_MobjLineCollide) // Hook for PIT_CheckThing by (thing) mobj type
-#define LUAh_MobjMoveCollide(thing1, thing2) LUAh_MobjCollideHook(thing1, thing2, hook_MobjMoveCollide) // Hook for PIT_CheckThing by (tmthing) mobj type
-boolean LUAh_TouchSpecial(mobj_t *special, mobj_t *toucher); // Hook for P_TouchSpecialThing by mobj type
-#define LUAh_MobjFuse(mo) LUAh_MobjHook(mo, hook_MobjFuse) // Hook for mobj->fuse == 0 by mobj type
-boolean LUAh_MobjThinker(mobj_t *mo); // Hook for P_MobjThinker or P_SceneryThinker by mobj type
-#define LUAh_BossThinker(mo) LUAh_MobjHook(mo, hook_BossThinker) // Hook for P_GenericBossThinker by mobj type
-UINT8 LUAh_ShouldDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype); // Hook for P_DamageMobj by mobj type (Should mobj take damage?)
-boolean LUAh_MobjDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype); // Hook for P_DamageMobj by mobj type (Mobj actually takes damage!)
-boolean LUAh_MobjDeath(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damagetype); // Hook for P_KillMobj by mobj type
-#define LUAh_BossDeath(mo) LUAh_MobjHook(mo, hook_BossDeath) // Hook for A_BossDeath by mobj type
-#define LUAh_MobjRemoved(mo) LUAh_MobjHook(mo, hook_MobjRemoved) // Hook for P_RemoveMobj by mobj type
-#define LUAh_JumpSpecial(player) LUAh_PlayerHook(player, hook_JumpSpecial) // Hook for P_DoJumpStuff (Any-jumping)
-#define LUAh_AbilitySpecial(player) LUAh_PlayerHook(player, hook_AbilitySpecial) // Hook for P_DoJumpStuff (Double-jumping)
-#define LUAh_SpinSpecial(player) LUAh_PlayerHook(player, hook_SpinSpecial) // Hook for P_DoSpinAbility (Spin button effect)
-#define LUAh_JumpSpinSpecial(player) LUAh_PlayerHook(player, hook_JumpSpinSpecial) // Hook for P_DoJumpStuff (Spin button effect (mid-air))
-boolean LUAh_BotTiccmd(player_t *bot, ticcmd_t *cmd); // Hook for B_BuildTiccmd
-boolean LUAh_BotAI(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd); // Hook for B_BuildTailsTiccmd by skin name
-boolean LUAh_BotRespawn(mobj_t *sonic, mobj_t *tails); // Hook for B_CheckRespawn
-boolean LUAh_LinedefExecute(line_t *line, mobj_t *mo, sector_t *sector); // Hook for linedef executors
-boolean LUAh_PlayerMsg(int source, int target, int flags, char *msg); // Hook for chat messages
-boolean LUAh_HurtMsg(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 damagetype); // Hook for hurt messages
-#define LUAh_PlayerSpawn(player) LUAh_PlayerHook(player, hook_PlayerSpawn) // Hook for G_SpawnPlayer
-#define LUAh_ShieldSpawn(player) LUAh_PlayerHook(player, hook_ShieldSpawn) // Hook for P_SpawnShieldOrb
-#define LUAh_ShieldSpecial(player) LUAh_PlayerHook(player, hook_ShieldSpecial) // Hook for shield abilities
-#define LUAh_MobjMoveBlocked(mo) LUAh_MobjHook(mo, hook_MobjMoveBlocked) // Hook for P_XYMovement (when movement is blocked)
-boolean LUAh_MapThingSpawn(mobj_t *mo, mapthing_t *mthing); // Hook for P_SpawnMapThing by mobj type
-boolean LUAh_FollowMobj(player_t *player, mobj_t *mobj); // Hook for P_PlayerAfterThink Smiles mobj-following
-UINT8 LUAh_PlayerCanDamage(player_t *player, mobj_t *mobj); // Hook for P_PlayerCanDamage
-void LUAh_PlayerQuit(player_t *plr, kickreason_t reason); // Hook for player quitting
-void LUAh_IntermissionThinker(void); // Hook for Y_Ticker
-boolean LUAh_TeamSwitch(player_t *player, int newteam, boolean fromspectators, boolean tryingautobalance, boolean tryingscramble); // Hook for team switching in... uh....
-UINT8 LUAh_ViewpointSwitch(player_t *player, player_t *newdisplayplayer, boolean forced); // Hook for spy mode
-#ifdef SEENAMES
-boolean LUAh_SeenPlayer(player_t *player, player_t *seenfriend); // Hook for MT_NAMECHECK
-#endif
-#define LUAh_PlayerThink(player) LUAh_PlayerHook(player, hook_PlayerThink) // Hook for P_PlayerThink
-boolean LUAh_ShouldJingleContinue(player_t *player, const char *musname); // Hook for whether a jingle of the given music should continue playing
-void LUAh_GameQuit(void); // Hook for game quitting
-boolean LUAh_PlayerCmd(player_t *player, ticcmd_t *cmd); // Hook for building player's ticcmd struct (Ported from SRB2Kart)
-boolean LUAh_MusicChange(const char *oldname, char *newname, UINT16 *mflags, boolean *looping, UINT32 *position, UINT32 *prefadems, UINT32 *fadeinms); // Hook for music changes
\ No newline at end of file
+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);
+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 *);
+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);
+int  LUA_HookHurtMsg(player_t *, mobj_t *inflictor, mobj_t *source, UINT8 damagetype);
+int  LUA_HookMapThingSpawn(mobj_t *, mapthing_t *);
+int  LUA_HookFollowMobj(player_t *, mobj_t *);
+int  LUA_HookPlayerCanDamage(player_t *, mobj_t *);
+void LUA_HookPlayerQuit(player_t *, kickreason_t);
+int  LUA_HookTeamSwitch(player_t *, int newteam, boolean fromspectators, boolean tryingautobalance, boolean tryingscramble);
+int  LUA_HookViewpointSwitch(player_t *player, player_t *newdisplayplayer, boolean forced);
+int  LUA_HookSeenPlayer(player_t *player, player_t *seenfriend);
+int  LUA_HookShouldJingleContinue(player_t *, const char *musname);
+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);
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 117aa48a303e7ba7945f2b970cb20d0292cced2a..a72b22b5a62b953bbccde13f271c76fb6e24cad1 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -25,1950 +25,1150 @@
 
 #include "m_perfstats.h"
 #include "d_netcmd.h" // for cv_perfstats
-#include "i_system.h" // I_GetTimeMicros
-
-static UINT8 hooksAvailable[(hook_MAX/8)+1];
-
-const char *const hookNames[hook_MAX+1] = {
-	"NetVars",
-	"MapChange",
-	"MapLoad",
-	"PlayerJoin",
-	"PreThinkFrame",
-	"ThinkFrame",
-	"PostThinkFrame",
-	"MobjSpawn",
-	"MobjCollide",
-	"MobjLineCollide",
-	"MobjMoveCollide",
-	"TouchSpecial",
-	"MobjFuse",
-	"MobjThinker",
-	"BossThinker",
-	"ShouldDamage",
-	"MobjDamage",
-	"MobjDeath",
-	"BossDeath",
-	"MobjRemoved",
-	"JumpSpecial",
-	"AbilitySpecial",
-	"SpinSpecial",
-	"JumpSpinSpecial",
-	"BotTiccmd",
-	"BotAI",
-	"BotRespawn",
-	"LinedefExecute",
-	"PlayerMsg",
-	"HurtMsg",
-	"PlayerSpawn",
-	"ShieldSpawn",
-	"ShieldSpecial",
-	"MobjMoveBlocked",
-	"MapThingSpawn",
-	"FollowMobj",
-	"PlayerCanDamage",
-	"PlayerQuit",
-	"IntermissionThinker",
-	"TeamSwitch",
-	"ViewpointSwitch",
-	"SeenPlayer",
-	"PlayerThink",
-	"ShouldJingleContinue",
-	"GameQuit",
-	"PlayerCmd",
-	"MusicChange",
-	NULL
-};
+#include "i_system.h" // I_GetPreciseTime
 
-// Hook metadata
-struct hook_s
-{
-	struct hook_s *next;
-	enum hook type;
-	UINT16 id;
-	union {
-		mobjtype_t mt;
-		char *str;
-	} s;
-	boolean error;
-};
-typedef struct hook_s* hook_p;
+/* =========================================================================
+                                  ABSTRACTION
+   ========================================================================= */
 
-#define FMT_HOOKID "hook_%d"
+#define LIST(id, M) \
+	static const char * const id [] = { M (TOSTR)  NULL }
 
-// For each mobj type, a linked list to its thinker and collision hooks.
-// That way, we don't have to iterate through all the hooks.
-// We could do that with all other mobj hooks, but it would probably just be
-// a waste of memory since they are only called occasionally. Probably...
-static hook_p mobjthinkerhooks[NUMMOBJTYPES];
-static hook_p mobjcollidehooks[NUMMOBJTYPES];
+LIST   (mobjHookNames,   MOBJ_HOOK_LIST);
+LIST       (hookNames,        HOOK_LIST);
+LIST    (hudHookNames,    HUD_HOOK_LIST);
+LIST (stringHookNames, STRING_HOOK_LIST);
 
-// For each mobj type, a linked list for other mobj hooks
-static hook_p mobjhooks[NUMMOBJTYPES];
+#undef LIST
 
-// A linked list for player hooks
-static hook_p playerhooks;
+typedef struct {
+	int numHooks;
+	int *ids;
+} hook_t;
 
-// A linked list for linedef executor hooks
-static hook_p linedefexecutorhooks;
+typedef struct {
+	int numGeneric;
+	int ref;
+} stringhook_t;
 
-// For other hooks, a unique linked list
-hook_p roothook;
+static hook_t hookIds[HOOK(MAX)];
+static hook_t hudHookIds[HUD_HOOK(MAX)];
+static hook_t mobjHookIds[NUMMOBJTYPES][MOBJ_HOOK(MAX)];
 
-static void PushHook(lua_State *L, hook_p hookp)
-{
-	lua_pushfstring(L, FMT_HOOKID, hookp->id);
-	lua_gettable(L, LUA_REGISTRYINDEX);
-}
+// Lua tables are used to lookup string hook ids.
+static stringhook_t stringHooks[STRING_HOOK(MAX)];
 
-// Takes hook, function, and additional arguments (mobj type to act on, etc.)
-static int lib_addHook(lua_State *L)
-{
-	static struct hook_s hook = {NULL, 0, 0, {0}, false};
-	static UINT32 nextid;
-	hook_p hookp, *lastp;
+// This will be indexed by hook id, the value of which fetches the registry.
+static int * hookRefs;
+static int   nextid;
 
-	hook.type = luaL_checkoption(L, 1, NULL, hookNames);
-	lua_remove(L, 1);
+// After a hook errors once, don't print the error again.
+static UINT8 * hooksErrored;
 
-	luaL_checktype(L, 1, LUA_TFUNCTION);
+static int errorRef;
 
-	if (!lua_lumploading)
-		return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
+static boolean mobj_hook_available(int hook_type, mobjtype_t mobj_type)
+{
+	return
+		(
+				mobjHookIds [MT_NULL] [hook_type].numHooks > 0 ||
+				mobjHookIds[mobj_type][hook_type].numHooks > 0
+		);
+}
 
-	switch(hook.type)
+static int hook_in_list
+(
+		const char * const         name,
+		const char * const * const list
+){
+	int type;
+
+	for (type = 0; list[type] != NULL; ++type)
 	{
-	// Take a mobjtype enum which this hook is specifically for.
-	case hook_MobjSpawn:
-	case hook_MobjCollide:
-	case hook_MobjLineCollide:
-	case hook_MobjMoveCollide:
-	case hook_TouchSpecial:
-	case hook_MobjFuse:
-	case hook_MobjThinker:
-	case hook_BossThinker:
-	case hook_ShouldDamage:
-	case hook_MobjDamage:
-	case hook_MobjDeath:
-	case hook_BossDeath:
-	case hook_MobjRemoved:
-	case hook_HurtMsg:
-	case hook_MobjMoveBlocked:
-	case hook_MapThingSpawn:
-	case hook_FollowMobj:
-		hook.s.mt = MT_NULL;
-		if (lua_isnumber(L, 2))
-			hook.s.mt = lua_tonumber(L, 2);
-		luaL_argcheck(L, hook.s.mt < NUMMOBJTYPES, 2, "invalid mobjtype_t");
-		break;
-	case hook_BotAI:
-	case hook_ShouldJingleContinue:
-		hook.s.str = NULL;
-		if (lua_isstring(L, 2))
-		{ // lowercase copy
-			hook.s.str = Z_StrDup(lua_tostring(L, 2));
-			strlwr(hook.s.str);
-		}
-		break;
-	case hook_LinedefExecute: // Linedef executor functions
-		hook.s.str = Z_StrDup(luaL_checkstring(L, 2));
-		strupr(hook.s.str);
-		break;
-	default:
-		break;
+		if (strcmp(name, list[type]) == 0)
+			break;
 	}
-	lua_settop(L, 1); // lua stack contains only the function now.
 
-	hooksAvailable[hook.type/8] |= 1<<(hook.type%8);
+	return type;
+}
 
-	// set hook.id to the highest id + 1
-	hook.id = nextid++;
+static void get_table(lua_State *L)
+{
+	lua_pushvalue(L, -1);
+	lua_rawget(L, -3);
 
-	// Special cases for some hook types (see the comments above mobjthinkerhooks declaration)
-	switch(hook.type)
+	if (lua_isnil(L, -1))
 	{
-	case hook_MobjThinker:
-		lastp = &mobjthinkerhooks[hook.s.mt];
-		break;
-	case hook_MobjCollide:
-	case hook_MobjLineCollide:
-	case hook_MobjMoveCollide:
-		lastp = &mobjcollidehooks[hook.s.mt];
-		break;
-	case hook_MobjSpawn:
-	case hook_TouchSpecial:
-	case hook_MobjFuse:
-	case hook_BossThinker:
-	case hook_ShouldDamage:
-	case hook_MobjDamage:
-	case hook_MobjDeath:
-	case hook_BossDeath:
-	case hook_MobjRemoved:
-	case hook_MobjMoveBlocked:
-	case hook_MapThingSpawn:
-	case hook_FollowMobj:
-		lastp = &mobjhooks[hook.s.mt];
-		break;
-	case hook_JumpSpecial:
-	case hook_AbilitySpecial:
-	case hook_SpinSpecial:
-	case hook_JumpSpinSpecial:
-	case hook_PlayerSpawn:
-	case hook_PlayerCanDamage:
-	case hook_TeamSwitch:
-	case hook_ViewpointSwitch:
-	case hook_SeenPlayer:
-	case hook_ShieldSpawn:
-	case hook_ShieldSpecial:
-	case hook_PlayerThink:
-		lastp = &playerhooks;
-		break;
-	case hook_LinedefExecute:
-		lastp = &linedefexecutorhooks;
-		break;
-	default:
-		lastp = &roothook;
-		break;
+		lua_pop(L, 1);
+		lua_createtable(L, 1, 0);
+		lua_pushvalue(L, -2);
+		lua_pushvalue(L, -2);
+		lua_rawset(L, -5);
 	}
 
-	// iterate the hook metadata structs
-	// set lastp to the last hook struct's "next" pointer.
-	for (hookp = *lastp; hookp; hookp = hookp->next)
-		lastp = &hookp->next;
-	// allocate a permanent memory struct to stuff hook.
-	hookp = ZZ_Alloc(sizeof(struct hook_s));
-	memcpy(hookp, &hook, sizeof(struct hook_s));
-	// tack it onto the end of the linked list.
-	*lastp = hookp;
-
-	// set the hook function in the registry.
-	lua_pushfstring(L, FMT_HOOKID, hook.id);
-	lua_pushvalue(L, 1);
-	lua_settable(L, LUA_REGISTRYINDEX);
-	return 0;
+	lua_remove(L, -2);
 }
 
-int LUA_HookLib(lua_State *L)
+static void add_hook_to_table(lua_State *L, int n)
 {
-	memset(hooksAvailable,0,sizeof(UINT8[(hook_MAX/8)+1]));
-	roothook = NULL;
-	lua_register(L, "addHook", lib_addHook);
-	return 0;
+	lua_pushnumber(L, nextid);
+	lua_rawseti(L, -2, n);
 }
 
-boolean LUAh_MobjHook(mobj_t *mo, enum hook which)
+static void add_string_hook(lua_State *L, int type)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[which/8] & (1<<(which%8))))
-		return false;
+	stringhook_t * hook = &stringHooks[type];
 
-	I_Assert(mo->type < NUMMOBJTYPES);
+	char * string = NULL;
 
-	if (!(mobjhooks[MT_NULL] || mobjhooks[mo->type]))
-		return false;
+	switch (type)
+	{
+		case STRING_HOOK(BotAI):
+		case STRING_HOOK(ShouldJingleContinue):
+			if (lua_isstring(L, 3))
+			{ // lowercase copy
+				string = Z_StrDup(lua_tostring(L, 3));
+				strlwr(string);
+			}
+			break;
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+		case STRING_HOOK(LinedefExecute):
+			string = Z_StrDup(luaL_checkstring(L, 3));
+			strupr(string);
+			break;
+	}
 
-	// Look for all generic mobj hooks
-	for (hookp = mobjhooks[MT_NULL]; hookp; hookp = hookp->next)
+	if (hook->ref > 0)
+		lua_getref(L, hook->ref);
+	else
 	{
-		if (hookp->type != which)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-			LUA_PushUserdata(gL, mo, META_MOBJ);
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -2);
-		if (lua_pcall(gL, 1, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		lua_newtable(L);
+		lua_pushvalue(L, -1);
+		hook->ref = luaL_ref(L, LUA_REGISTRYINDEX);
 	}
 
-	for (hookp = mobjhooks[mo->type]; hookp; hookp = hookp->next)
+	if (string)
 	{
-		if (hookp->type != which)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-			LUA_PushUserdata(gL, mo, META_MOBJ);
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -2);
-		if (lua_pcall(gL, 1, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		lua_pushstring(L, string);
+		get_table(L);
+		add_hook_to_table(L, 1 + lua_objlen(L, -1));
 	}
-
-	lua_settop(gL, 0);
-	return hooked;
+	else
+		add_hook_to_table(L, ++hook->numGeneric);
 }
 
-boolean LUAh_PlayerHook(player_t *plr, enum hook which)
+static void add_hook(hook_t *map)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[which/8] & (1<<(which%8))))
-		return false;
+	Z_Realloc(map->ids, (map->numHooks + 1) * sizeof *map->ids,
+			PU_STATIC, &map->ids);
+	map->ids[map->numHooks++] = nextid;
+}
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+static void add_mobj_hook(lua_State *L, int hook_type)
+{
+	mobjtype_t   mobj_type = luaL_optnumber(L, 3, MT_NULL);
 
-	for (hookp = playerhooks; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != which)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-			LUA_PushUserdata(gL, plr, META_PLAYER);
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -2);
-		if (lua_pcall(gL, 1, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
-	}
+	luaL_argcheck(L, mobj_type < NUMMOBJTYPES, 3, "invalid mobjtype_t");
 
-	lua_settop(gL, 0);
-	return hooked;
+	add_hook(&mobjHookIds[mobj_type][hook_type]);
 }
 
-// Hook for map change (before load)
-void LUAh_MapChange(INT16 mapnumber)
+static void add_hud_hook(lua_State *L, int idx)
 {
-	hook_p hookp;
-	if (!gL || !(hooksAvailable[hook_MapChange/8] & (1<<(hook_MapChange%8))))
-		return;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-	lua_pushinteger(gL, mapnumber);
+	add_hook(&hudHookIds[luaL_checkoption(L,
+				idx, "game", hudHookNames)]);
+}
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
+static void add_hook_ref(lua_State *L, int idx)
+{
+	if (!(nextid & 7))
 	{
-		if (hookp->type != hook_MapChange)
-			continue;
-
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -2);
-		if (lua_pcall(gL, 1, 0, 1)) {
-			CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-		}
+		Z_Realloc(hooksErrored,
+				BIT_ARRAY_SIZE (nextid + 1) * sizeof *hooksErrored,
+				PU_STATIC, &hooksErrored);
+		hooksErrored[nextid >> 3] = 0;
 	}
 
-	lua_settop(gL, 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);
 }
 
-// Hook for map load
-void LUAh_MapLoad(void)
+// Takes hook, function, and additional arguments (mobj type to act on, etc.)
+static int lib_addHook(lua_State *L)
 {
-	hook_p hookp;
-	if (!gL || !(hooksAvailable[hook_MapLoad/8] & (1<<(hook_MapLoad%8))))
-		return;
+	const char * name;
+	int type;
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-	lua_pushinteger(gL, gamemap);
+	if (!lua_lumploading)
+		return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
+
+	name = luaL_checkstring(L, 1);
+	luaL_checktype(L, 2, LUA_TFUNCTION);
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	/* this is a very special case */
+	if (( type = hook_in_list(name, stringHookNames) ) < STRING_HOOK(MAX))
 	{
-		if (hookp->type != hook_MapLoad)
-			continue;
-
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -2);
-		if (lua_pcall(gL, 1, 0, 1)) {
-			CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-		}
+		add_string_hook(L, type);
+	}
+	else if (( type = hook_in_list(name, mobjHookNames) ) < MOBJ_HOOK(MAX))
+	{
+		add_mobj_hook(L, type);
+	}
+	else if (( type = hook_in_list(name, hookNames) ) < HOOK(MAX))
+	{
+		add_hook(&hookIds[type]);
+	}
+	else if (strcmp(name, "HUD") == 0)
+	{
+		add_hud_hook(L, 3);
+	}
+	else
+	{
+		return luaL_argerror(L, 1, lua_pushfstring(L, "invalid hook " LUA_QS, name));
 	}
 
-	lua_settop(gL, 0);
+	add_hook_ref(L, 2);/* the function */
+
+	return 0;
 }
 
-// Hook for Got_AddPlayer
-void LUAh_PlayerJoin(int playernum)
+int LUA_HookLib(lua_State *L)
 {
-	hook_p hookp;
-	if (!gL || !(hooksAvailable[hook_PlayerJoin/8] & (1<<(hook_PlayerJoin%8))))
-		return;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-	lua_pushinteger(gL, playernum);
+	lua_pushcfunction(L, LUA_GetErrorMessage);
+	errorRef = luaL_ref(L, LUA_REGISTRYINDEX);
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != hook_PlayerJoin)
-			continue;
-
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -2);
-		if (lua_pcall(gL, 1, 0, 1)) {
-			CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-		}
-	}
+	lua_register(L, "addHook", lib_addHook);
 
-	lua_settop(gL, 0);
+	return 0;
 }
 
-// Hook for frame (before mobj and player thinkers)
-void LUAh_PreThinkFrame(void)
+/* TODO: remove in next backwards incompatible release */
+#if MODID == 18
+int lib_hudadd(lua_State *L);/* yeah compiler */
+int lib_hudadd(lua_State *L)
 {
-	hook_p hookp;
-	if (!gL || !(hooksAvailable[hook_PreThinkFrame/8] & (1<<(hook_PreThinkFrame%8))))
-		return;
+	if (!lua_lumploading)
+		return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
 
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+	luaL_checktype(L, 1, LUA_TFUNCTION);
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != hook_PreThinkFrame)
-			continue;
-
-		PushHook(gL, hookp);
-		if (lua_pcall(gL, 0, 0, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-		}
-	}
+	add_hud_hook(L, 2);
+	add_hook_ref(L, 1);
 
-	lua_pop(gL, 1); // Pop error handler
+	return 0;
 }
+#endif
+
+typedef struct Hook_State Hook_State;
+typedef void (*Hook_Callback)(Hook_State *);
+
+struct Hook_State {
+	INT32        status;/* return status to calling function */
+	void       * userdata;
+	int          hook_type;
+	mobjtype_t   mobj_type;/* >0 if mobj hook */
+	const char * string;/* used to fetch table, ran first if set */
+	int          top;/* index of last argument passed to hook */
+	int          id;/* id to fetch ref */
+	int          values;/* num arguments passed to hook */
+	int          results;/* num values returned by hook */
+	Hook_Callback results_handler;/* callback when hook successfully returns */
+};
+
+enum {
+	EINDEX = 1,/* error handler */
+	SINDEX = 2,/* string itself is pushed in case of string hook */
+};
 
-// Hook for frame (after mobj and player thinkers)
-void LUAh_ThinkFrame(void)
+static void push_error_handler(void)
 {
-	hook_p hookp;
-	// variables used by perf stats
-	int hook_index = 0;
-	int time_taken = 0;
-	if (!gL || !(hooksAvailable[hook_ThinkFrame/8] & (1<<(hook_ThinkFrame%8))))
-		return;
+	lua_getref(gL, errorRef);
+}
 
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+/* repush hook string */
+static void push_string(void)
+{
+	lua_pushvalue(gL, SINDEX);
+}
+
+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();
+}
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
+static boolean init_hook_type
+(
+		Hook_State * hook,
+		int          status,
+		int          hook_type,
+		mobjtype_t   mobj_type,
+		const char * string,
+		int          nonzero
+){
+	hook->status = status;
+
+	if (nonzero)
 	{
-		if (hookp->type != hook_ThinkFrame)
-			continue;
-
-		if (cv_perfstats.value == 3)
-			time_taken = I_GetTimeMicros();
-		PushHook(gL, hookp);
-		if (lua_pcall(gL, 0, 0, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-		}
-		if (cv_perfstats.value == 3)
-		{
-			lua_Debug ar;
-			time_taken = I_GetTimeMicros() - time_taken;
-			// we need the function, let's just retrieve it again
-			PushHook(gL, hookp);
-			lua_getinfo(gL, ">S", &ar);
-			PS_SetThinkFrameHookInfo(hook_index, time_taken, ar.short_src);
-			hook_index++;
-		}
+		start_hook_stack();
+		hook->hook_type = hook_type;
+		hook->mobj_type = mobj_type;
+		hook->string = string;
+		return begin_hook_values(hook);
 	}
-
-	lua_pop(gL, 1); // Pop error handler
+	else
+		return false;
 }
 
-// Hook for frame (at end of tick, ie after overlays, precipitation, specials)
-void LUAh_PostThinkFrame(void)
-{
-	hook_p hookp;
-	if (!gL || !(hooksAvailable[hook_PostThinkFrame/8] & (1<<(hook_PostThinkFrame%8))))
-		return;
+static boolean prepare_hook
+(
+		Hook_State * hook,
+		int default_status,
+		int hook_type
+){
+	return init_hook_type(hook, default_status,
+			hook_type, 0, NULL,
+			hookIds[hook_type].numHooks);
+}
 
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+static boolean prepare_mobj_hook
+(
+		Hook_State * hook,
+		int          default_status,
+		int          hook_type,
+		mobjtype_t   mobj_type
+){
+	return init_hook_type(hook, default_status,
+			hook_type, mobj_type, NULL,
+			mobj_hook_available(hook_type, mobj_type));
+}
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
+static boolean prepare_string_hook
+(
+		Hook_State * hook,
+		int          default_status,
+		int          hook_type,
+		const char * string
+){
+	if (init_hook_type(hook, default_status,
+				hook_type, 0, string,
+				stringHooks[hook_type].ref))
 	{
-		if (hookp->type != hook_PostThinkFrame)
-			continue;
-
-		PushHook(gL, hookp);
-		if (lua_pcall(gL, 0, 0, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-		}
+		lua_pushstring(gL, string);
+		return begin_hook_values(hook);
 	}
+	else
+		return false;
+}
 
-	lua_pop(gL, 1); // Pop error handler
+static void init_hook_call
+(
+		Hook_State * hook,
+		int    results,
+		Hook_Callback results_handler
+){
+	const int top = lua_gettop(gL);
+	hook->values = (top - hook->top);
+	hook->top = top;
+	hook->results = results;
+	hook->results_handler = results_handler;
 }
 
-// Hook for mobj collisions
-UINT8 LUAh_MobjCollideHook(mobj_t *thing1, mobj_t *thing2, enum hook which)
+static void get_hook(Hook_State *hook, const int *ids, int n)
 {
-	hook_p hookp;
-	UINT8 shouldCollide = 0; // 0 = default, 1 = force yes, 2 = force no.
-	if (!gL || !(hooksAvailable[which/8] & (1<<(which%8))))
-		return 0;
-
-	I_Assert(thing1->type < NUMMOBJTYPES);
-
-	if (!(mobjcollidehooks[MT_NULL] || mobjcollidehooks[thing1->type]))
-		return 0;
+	hook->id = ids[n];
+	lua_getref(gL, hookRefs[hook->id]);
+}
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+static void get_hook_from_table(Hook_State *hook, int n)
+{
+	lua_rawgeti(gL, -1, n);
+	hook->id = lua_tonumber(gL, -1);
+	lua_pop(gL, 1);
+	lua_getref(gL, hookRefs[hook->id]);
+}
 
-	// Look for all generic mobj collision hooks
-	for (hookp = mobjcollidehooks[MT_NULL]; hookp; hookp = hookp->next)
+static int call_single_hook_no_copy(Hook_State *hook)
+{
+	if (lua_pcall(gL, hook->values, hook->results, EINDEX) == 0)
 	{
-		if (hookp->type != which)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
+		if (hook->results > 0)
 		{
-			LUA_PushUserdata(gL, thing1, META_MOBJ);
-			LUA_PushUserdata(gL, thing2, META_MOBJ);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
+			(*hook->results_handler)(hook);
+			lua_pop(gL, hook->results);
 		}
-		if (!lua_isnil(gL, -1))
-		{ // if nil, leave shouldCollide = 0.
-			if (lua_toboolean(gL, -1))
-				shouldCollide = 1; // Force yes
-			else
-				shouldCollide = 2; // Force no
-		}
-		lua_pop(gL, 1);
 	}
-
-	for (hookp = mobjcollidehooks[thing1->type]; hookp; hookp = hookp->next)
+	else
 	{
-		if (hookp->type != which)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
+		/* print the error message once */
+		if (cv_debug & DBG_LUA || !in_bit_array(hooksErrored, hook->id))
 		{
-			LUA_PushUserdata(gL, thing1, META_MOBJ);
-			LUA_PushUserdata(gL, thing2, META_MOBJ);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (!lua_isnil(gL, -1))
-		{ // if nil, leave shouldCollide = 0.
-			if (lua_toboolean(gL, -1))
-				shouldCollide = 1; // Force yes
-			else
-				shouldCollide = 2; // Force no
+			CONS_Alert(CONS_WARNING, "%s\n", lua_tostring(gL, -1));
+			set_bit_array(hooksErrored, hook->id);
 		}
 		lua_pop(gL, 1);
 	}
 
-	lua_settop(gL, 0);
-	return shouldCollide;
+	return 1;
 }
 
-UINT8 LUAh_MobjLineCollideHook(mobj_t *thing, line_t *line, enum hook which)
+static int call_single_hook(Hook_State *hook)
 {
-	hook_p hookp;
-	UINT8 shouldCollide = 0; // 0 = default, 1 = force yes, 2 = force no.
-	if (!gL || !(hooksAvailable[which/8] & (1<<(which%8))))
-		return 0;
+	int i;
 
-	I_Assert(thing->type < NUMMOBJTYPES);
+	for (i = -(hook->values) + 1; i <= 0; ++i)
+		lua_pushvalue(gL, hook->top + i);
 
-	if (!(mobjcollidehooks[MT_NULL] || mobjcollidehooks[thing->type]))
-		return 0;
+	return call_single_hook_no_copy(hook);
+}
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+static int call_hook_table_for(Hook_State *hook, int n)
+{
+	int k;
 
-	// Look for all generic mobj collision hooks
-	for (hookp = mobjcollidehooks[MT_NULL]; hookp; hookp = hookp->next)
+	for (k = 1; k <= n; ++k)
 	{
-		if (hookp->type != which)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, thing, META_MOBJ);
-			LUA_PushUserdata(gL, line, META_LINE);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (!lua_isnil(gL, -1))
-		{ // if nil, leave shouldCollide = 0.
-			if (lua_toboolean(gL, -1))
-				shouldCollide = 1; // Force yes
-			else
-				shouldCollide = 2; // Force no
-		}
-		lua_pop(gL, 1);
+		get_hook_from_table(hook, k);
+		call_single_hook(hook);
 	}
 
-	for (hookp = mobjcollidehooks[thing->type]; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != which)
-			continue;
+	return n;
+}
 
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, thing, META_MOBJ);
-			LUA_PushUserdata(gL, line, META_LINE);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (!lua_isnil(gL, -1))
-		{ // if nil, leave shouldCollide = 0.
-			if (lua_toboolean(gL, -1))
-				shouldCollide = 1; // Force yes
-			else
-				shouldCollide = 2; // Force no
-		}
-		lua_pop(gL, 1);
+static int call_hook_table(Hook_State *hook)
+{
+	return call_hook_table_for(hook, lua_objlen(gL, -1));
+}
+
+static int call_mapped(Hook_State *hook, const hook_t *map)
+{
+	int k;
+
+	for (k = 0; k < map->numHooks; ++k)
+	{
+		get_hook(hook, map->ids, k);
+		call_single_hook(hook);
 	}
 
-	lua_settop(gL, 0);
-	return shouldCollide;
+	return map->numHooks;
 }
 
-// Hook for mobj thinkers
-boolean LUAh_MobjThinker(mobj_t *mo)
+static int call_string_hooks(Hook_State *hook)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_MobjThinker/8] & (1<<(hook_MobjThinker%8))))
-		return false;
+	const stringhook_t *map = &stringHooks[hook->hook_type];
 
-	I_Assert(mo->type < NUMMOBJTYPES);
+	int calls = 0;
 
-	if (!(mobjthinkerhooks[MT_NULL] || mobjthinkerhooks[mo->type]))
-		return false;
+	lua_getref(gL, map->ref);
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+	/* call generic string hooks first */
+	calls += call_hook_table_for(hook, map->numGeneric);
 
-	// Look for all generic mobj thinker hooks
-	for (hookp = mobjthinkerhooks[MT_NULL]; hookp; hookp = hookp->next)
-	{
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-			LUA_PushUserdata(gL, mo, META_MOBJ);
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -2);
-		if (lua_pcall(gL, 1, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
-	}
-
-	for (hookp = mobjthinkerhooks[mo->type]; hookp; hookp = hookp->next)
-	{
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-			LUA_PushUserdata(gL, mo, META_MOBJ);
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -2);
-		if (lua_pcall(gL, 1, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
-	}
+	push_string();
+	lua_rawget(gL, -2);
+	calls += call_hook_table(hook);
 
-	lua_settop(gL, 0);
-	return hooked;
+	return calls;
 }
 
-// Hook for P_TouchSpecialThing by mobj type
-boolean LUAh_TouchSpecial(mobj_t *special, mobj_t *toucher)
+static int call_mobj_type_hooks(Hook_State *hook, mobjtype_t mobj_type)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_TouchSpecial/8] & (1<<(hook_TouchSpecial%8))))
-		return false;
-
-	I_Assert(special->type < NUMMOBJTYPES);
+	return call_mapped(hook, &mobjHookIds[mobj_type][hook->hook_type]);
+}
 
-	if (!(mobjhooks[MT_NULL] || mobjhooks[special->type]))
-		return false;
+static int call_hooks
+(
+		Hook_State * hook,
+		int        results,
+		Hook_Callback results_handler
+){
+	int calls = 0;
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+	init_hook_call(hook, results, results_handler);
 
-	// Look for all generic touch special hooks
-	for (hookp = mobjhooks[MT_NULL]; hookp; hookp = hookp->next)
+	if (hook->string)
 	{
-		if (hookp->type != hook_TouchSpecial)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, special, META_MOBJ);
-			LUA_PushUserdata(gL, toucher, META_MOBJ);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		calls += call_string_hooks(hook);
 	}
-
-	for (hookp = mobjhooks[special->type]; hookp; hookp = hookp->next)
+	else if (hook->mobj_type > 0)
 	{
-		if (hookp->type != hook_TouchSpecial)
-			continue;
+		/* call generic mobj hooks first */
+		calls += call_mobj_type_hooks(hook, MT_NULL);
+		calls += call_mobj_type_hooks(hook, hook->mobj_type);
 
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, special, META_MOBJ);
-			LUA_PushUserdata(gL, toucher, META_MOBJ);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		ps_lua_mobjhooks.value.i += calls;
 	}
+	else
+		calls += call_mapped(hook, &hookIds[hook->hook_type]);
 
 	lua_settop(gL, 0);
-	return hooked;
+
+	return calls;
 }
 
-// Hook for P_DamageMobj by mobj type (Should mobj take damage?)
-UINT8 LUAh_ShouldDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
-{
-	hook_p hookp;
-	UINT8 shouldDamage = 0; // 0 = default, 1 = force yes, 2 = force no.
-	if (!gL || !(hooksAvailable[hook_ShouldDamage/8] & (1<<(hook_ShouldDamage%8))))
-		return 0;
+/* =========================================================================
+                            COMMON RESULT HANDLERS
+   ========================================================================= */
 
-	I_Assert(target->type < NUMMOBJTYPES);
+#define res_none NULL
 
-	if (!(mobjhooks[MT_NULL] || mobjhooks[target->type]))
-		return 0;
+static void res_true(Hook_State *hook)
+{
+	if (lua_toboolean(gL, -1))
+		hook->status = true;
+}
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+static void res_false(Hook_State *hook)
+{
+	if (!lua_isnil(gL, -1) && !lua_toboolean(gL, -1))
+		hook->status = false;
+}
 
-	// Look for all generic should damage hooks
-	for (hookp = mobjhooks[MT_NULL]; hookp; hookp = hookp->next)
+static void res_force(Hook_State *hook)
+{
+	if (!lua_isnil(gL, -1))
 	{
-		if (hookp->type != hook_ShouldDamage)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, target, META_MOBJ);
-			LUA_PushUserdata(gL, inflictor, META_MOBJ);
-			LUA_PushUserdata(gL, source, META_MOBJ);
-			lua_pushinteger(gL, damage);
-			lua_pushinteger(gL, damagetype);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		if (lua_pcall(gL, 5, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (!lua_isnil(gL, -1))
-		{
-			if (lua_toboolean(gL, -1))
-				shouldDamage = 1; // Force yes
-			else
-				shouldDamage = 2; // Force no
-		}
-		lua_pop(gL, 1);
+		if (lua_toboolean(gL, -1))
+			hook->status = 1; // Force yes
+		else
+			hook->status = 2; // Force no
 	}
+}
 
-	for (hookp = mobjhooks[target->type]; hookp; hookp = hookp->next)
+/* =========================================================================
+                               GENERALISED HOOKS
+   ========================================================================= */
+
+int LUA_HookMobj(mobj_t *mobj, int hook_type)
+{
+	Hook_State hook;
+	if (prepare_mobj_hook(&hook, false, hook_type, mobj->type))
 	{
-		if (hookp->type != hook_ShouldDamage)
-			continue;
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, target, META_MOBJ);
-			LUA_PushUserdata(gL, inflictor, META_MOBJ);
-			LUA_PushUserdata(gL, source, META_MOBJ);
-			lua_pushinteger(gL, damage);
-			lua_pushinteger(gL, damagetype);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		if (lua_pcall(gL, 5, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (!lua_isnil(gL, -1))
-		{
-			if (lua_toboolean(gL, -1))
-				shouldDamage = 1; // Force yes
-			else
-				shouldDamage = 2; // Force no
-		}
-		lua_pop(gL, 1);
+		LUA_PushUserdata(gL, mobj, META_MOBJ);
+		call_hooks(&hook, 1, res_true);
 	}
-
-	lua_settop(gL, 0);
-	return shouldDamage;
+	return hook.status;
 }
 
-// Hook for P_DamageMobj by mobj type (Mobj actually takes damage!)
-boolean LUAh_MobjDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
+int LUA_Hook2Mobj(mobj_t *t1, mobj_t *t2, int hook_type)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_MobjDamage/8] & (1<<(hook_MobjDamage%8))))
-		return false;
-
-	I_Assert(target->type < NUMMOBJTYPES);
-
-	if (!(mobjhooks[MT_NULL] || mobjhooks[target->type]))
-		return false;
+	Hook_State hook;
+	if (prepare_mobj_hook(&hook, 0, hook_type, t1->type))
+	{
+		LUA_PushUserdata(gL, t1, META_MOBJ);
+		LUA_PushUserdata(gL, t2, META_MOBJ);
+		call_hooks(&hook, 1, res_force);
+	}
+	return hook.status;
+}
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+void LUA_HookVoid(int type)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, 0, type))
+		call_hooks(&hook, 0, res_none);
+}
 
-	// Look for all generic mobj damage hooks
-	for (hookp = mobjhooks[MT_NULL]; hookp; hookp = hookp->next)
+void LUA_HookInt(INT32 number, int hook_type)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, 0, hook_type))
 	{
-		if (hookp->type != hook_MobjDamage)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, target, META_MOBJ);
-			LUA_PushUserdata(gL, inflictor, META_MOBJ);
-			LUA_PushUserdata(gL, source, META_MOBJ);
-			lua_pushinteger(gL, damage);
-			lua_pushinteger(gL, damagetype);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		if (lua_pcall(gL, 5, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		lua_pushinteger(gL, number);
+		call_hooks(&hook, 0, res_none);
 	}
+}
 
-	for (hookp = mobjhooks[target->type]; hookp; hookp = hookp->next)
+void LUA_HookBool(boolean value, int hook_type)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, 0, hook_type))
 	{
-		if (hookp->type != hook_MobjDamage)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, target, META_MOBJ);
-			LUA_PushUserdata(gL, inflictor, META_MOBJ);
-			LUA_PushUserdata(gL, source, META_MOBJ);
-			lua_pushinteger(gL, damage);
-			lua_pushinteger(gL, damagetype);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		if (lua_pcall(gL, 5, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		lua_pushboolean(gL, value);
+		call_hooks(&hook, 0, res_none);
 	}
-
-	lua_settop(gL, 0);
-	return hooked;
 }
 
-// Hook for P_KillMobj by mobj type
-boolean LUAh_MobjDeath(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damagetype)
+int LUA_HookPlayer(player_t *player, int hook_type)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_MobjDeath/8] & (1<<(hook_MobjDeath%8))))
-		return false;
+	Hook_State hook;
+	if (prepare_hook(&hook, false, hook_type))
+	{
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		call_hooks(&hook, 1, res_true);
+	}
+	return hook.status;
+}
 
-	I_Assert(target->type < NUMMOBJTYPES);
+int LUA_HookTiccmd(player_t *player, ticcmd_t *cmd, int hook_type)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, false, hook_type))
+	{
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		LUA_PushUserdata(gL, cmd, META_TICCMD);
 
-	if (!(mobjhooks[MT_NULL] || mobjhooks[target->type]))
-		return false;
+		if (hook_type == HOOK(PlayerCmd))
+			hook_cmd_running = true;
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+		call_hooks(&hook, 1, res_true);
 
-	// Look for all generic mobj death hooks
-	for (hookp = mobjhooks[MT_NULL]; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != hook_MobjDeath)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, target, META_MOBJ);
-			LUA_PushUserdata(gL, inflictor, META_MOBJ);
-			LUA_PushUserdata(gL, source, META_MOBJ);
-			lua_pushinteger(gL, damagetype);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -5);
-		lua_pushvalue(gL, -5);
-		lua_pushvalue(gL, -5);
-		lua_pushvalue(gL, -5);
-		if (lua_pcall(gL, 4, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		if (hook_type == HOOK(PlayerCmd))
+			hook_cmd_running = false;
 	}
+	return hook.status;
+}
 
-	for (hookp = mobjhooks[target->type]; hookp; hookp = hookp->next)
+int LUA_HookKey(event_t *event, int hook_type)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, false, hook_type))
 	{
-		if (hookp->type != hook_MobjDeath)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, target, META_MOBJ);
-			LUA_PushUserdata(gL, inflictor, META_MOBJ);
-			LUA_PushUserdata(gL, source, META_MOBJ);
-			lua_pushinteger(gL, damagetype);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -5);
-		lua_pushvalue(gL, -5);
-		lua_pushvalue(gL, -5);
-		lua_pushvalue(gL, -5);
-		if (lua_pcall(gL, 4, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		LUA_PushUserdata(gL, event, META_KEYEVENT);
+		call_hooks(&hook, 1, res_true);
 	}
-
-	lua_settop(gL, 0);
-	return hooked;
+	return hook.status;
 }
 
-// Hook for B_BuildTiccmd
-boolean LUAh_BotTiccmd(player_t *bot, ticcmd_t *cmd)
+void LUA_HookHUD(int hook_type)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_BotTiccmd/8] & (1<<(hook_BotTiccmd%8))))
-		return false;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	const hook_t * map = &hudHookIds[hook_type];
+	Hook_State hook;
+	if (map->numHooks > 0)
 	{
-		if (hookp->type != hook_BotTiccmd)
-			continue;
+		start_hook_stack();
+		begin_hook_values(&hook);
 
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, bot, META_PLAYER);
-			LUA_PushUserdata(gL, cmd, META_TICCMD);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
-	}
+		LUA_SetHudHook(hook_type);
 
-	lua_settop(gL, 0);
-	return hooked;
+		hud_running = true; // local hook
+		init_hook_call(&hook, 0, res_none);
+		call_mapped(&hook, map);
+		hud_running = false;
+	}
 }
 
-// Hook for B_BuildTailsTiccmd by skin name
-boolean LUAh_BotAI(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
+/* =========================================================================
+                               SPECIALIZED HOOKS
+   ========================================================================= */
+
+void LUA_HookThinkFrame(void)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_BotAI/8] & (1<<(hook_BotAI%8))))
-		return false;
+	const int type = HOOK(ThinkFrame);
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+	// variables used by perf stats
+	int hook_index = 0;
+	precise_t time_taken = 0;
+
+	Hook_State hook;
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	const hook_t * map = &hookIds[type];
+	int k;
+
+	if (prepare_hook(&hook, 0, type))
 	{
-		if (hookp->type != hook_BotAI
-		|| (hookp->s.str && strcmp(hookp->s.str, ((skin_t*)tails->skin)->name)))
-			continue;
+		init_hook_call(&hook, 0, res_none);
 
-		if (lua_gettop(gL) == 1)
+		for (k = 0; k < map->numHooks; ++k)
 		{
-			LUA_PushUserdata(gL, sonic, META_MOBJ);
-			LUA_PushUserdata(gL, tails, META_MOBJ);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 8, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
+			get_hook(&hook, map->ids, k);
+
+			if (cv_perfstats.value == 3)
+			{
+				lua_pushvalue(gL, -1);/* need the function again */
+				time_taken = I_GetPreciseTime();
+			}
+
+			call_single_hook(&hook);
+
+			if (cv_perfstats.value == 3)
+			{
+				lua_Debug ar;
+				time_taken = I_GetPreciseTime() - time_taken;
+				lua_getinfo(gL, ">S", &ar);
+				PS_SetThinkFrameHookInfo(hook_index, time_taken, ar.short_src);
+				hook_index++;
+			}
 		}
 
-		// This turns forward, backward, left, right, jump, and spin into a proper ticcmd for tails.
-		if (lua_istable(gL, 2+1)) {
-			boolean forward=false, backward=false, left=false, right=false, strafeleft=false, straferight=false, jump=false, spin=false;
-#define CHECKFIELD(field) \
-			lua_getfield(gL, 2+1, #field);\
-			if (lua_toboolean(gL, -1))\
-				field = true;\
-			lua_pop(gL, 1);
-
-			CHECKFIELD(forward)
-			CHECKFIELD(backward)
-			CHECKFIELD(left)
-			CHECKFIELD(right)
-			CHECKFIELD(strafeleft)
-			CHECKFIELD(straferight)
-			CHECKFIELD(jump)
-			CHECKFIELD(spin)
-#undef CHECKFIELD
-			B_KeysToTiccmd(tails, cmd, forward, backward, left, right, strafeleft, straferight, jump, spin);
-		} else
-			B_KeysToTiccmd(tails, cmd, lua_toboolean(gL, 2+1), lua_toboolean(gL, 2+2), lua_toboolean(gL, 2+3), lua_toboolean(gL, 2+4), lua_toboolean(gL, 2+5), lua_toboolean(gL, 2+6), lua_toboolean(gL, 2+7), lua_toboolean(gL, 2+8));
-
-		lua_pop(gL, 8);
-		hooked = true;
+		lua_settop(gL, 0);
 	}
-
-	lua_settop(gL, 0);
-	return hooked;
 }
 
-// Hook for B_CheckRespawn
-boolean LUAh_BotRespawn(mobj_t *sonic, mobj_t *tails)
+int LUA_HookMobjLineCollide(mobj_t *mobj, line_t *line)
 {
-	hook_p hookp;
-	UINT8 shouldRespawn = 0; // 0 = default, 1 = force yes, 2 = force no.
-	if (!gL || !(hooksAvailable[hook_BotRespawn/8] & (1<<(hook_BotRespawn%8))))
-		return false;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	Hook_State hook;
+	if (prepare_mobj_hook(&hook, 0, MOBJ_HOOK(MobjLineCollide), mobj->type))
 	{
-		if (hookp->type != hook_BotRespawn)
-			continue;
-
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, sonic, META_MOBJ);
-			LUA_PushUserdata(gL, tails, META_MOBJ);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (!lua_isnil(gL, -1))
-		{
-			if (lua_toboolean(gL, -1))
-				shouldRespawn = 1; // Force yes
-			else
-				shouldRespawn = 2; // Force no
-		}
-		lua_pop(gL, 1);
+		LUA_PushUserdata(gL, mobj, META_MOBJ);
+		LUA_PushUserdata(gL, line, META_LINE);
+		call_hooks(&hook, 1, res_force);
 	}
-
-	lua_settop(gL, 0);
-	return shouldRespawn;
+	return hook.status;
 }
 
-// Hook for linedef executors
-boolean LUAh_LinedefExecute(line_t *line, mobj_t *mo, sector_t *sector)
+int LUA_HookTouchSpecial(mobj_t *special, mobj_t *toucher)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_LinedefExecute/8] & (1<<(hook_LinedefExecute%8))))
-		return 0;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	for (hookp = linedefexecutorhooks; hookp; hookp = hookp->next)
+	Hook_State hook;
+	if (prepare_mobj_hook(&hook, false, MOBJ_HOOK(TouchSpecial), special->type))
 	{
-		if (strcmp(hookp->s.str, line->stringargs[0]))
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, line, META_LINE);
-			LUA_PushUserdata(gL, mo, META_MOBJ);
-			LUA_PushUserdata(gL, sector, META_SECTOR);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -4);
-		lua_pushvalue(gL, -4);
-		lua_pushvalue(gL, -4);
-		if (lua_pcall(gL, 3, 0, 1)) {
-			CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-		}
-		hooked = true;
+		LUA_PushUserdata(gL, special, META_MOBJ);
+		LUA_PushUserdata(gL, toucher, META_MOBJ);
+		call_hooks(&hook, 1, res_true);
 	}
+	return hook.status;
+}
 
-	lua_settop(gL, 0);
-	return hooked;
+static int damage_hook
+(
+		mobj_t *target,
+		mobj_t *inflictor,
+		mobj_t *source,
+		INT32   damage,
+		UINT8   damagetype,
+		int     hook_type,
+		Hook_Callback results_handler
+){
+	Hook_State hook;
+	if (prepare_mobj_hook(&hook, 0, hook_type, target->type))
+	{
+		LUA_PushUserdata(gL, target, META_MOBJ);
+		LUA_PushUserdata(gL, inflictor, META_MOBJ);
+		LUA_PushUserdata(gL, source, META_MOBJ);
+		if (hook_type != MOBJ_HOOK(MobjDeath))
+			lua_pushinteger(gL, damage);
+		lua_pushinteger(gL, damagetype);
+		call_hooks(&hook, 1, results_handler);
+	}
+	return hook.status;
 }
 
+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), res_force);
+}
 
-boolean LUAh_PlayerMsg(int source, int target, int flags, char *msg)
+int LUA_HookMobjDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_PlayerMsg/8] & (1<<(hook_PlayerMsg%8))))
-		return false;
+	return damage_hook(target, inflictor, source, damage, damagetype,
+			MOBJ_HOOK(MobjDamage), res_true);
+}
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+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), res_true);
+}
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
+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))
 	{
-		if (hookp->type != hook_PlayerMsg)
-			continue;
-
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, &players[source], META_PLAYER); // Source player
-			if (flags & 2 /*HU_CSAY*/) { // csay TODO: make HU_CSAY accessible outside hu_stuff.c
-				lua_pushinteger(gL, 3); // type
-				lua_pushnil(gL); // target
-			} else if (target == -1) { // sayteam
-				lua_pushinteger(gL, 1); // type
-				lua_pushnil(gL); // target
-			} else if (target == 0) { // say
-				lua_pushinteger(gL, 0); // type
-				lua_pushnil(gL); // target
-			} else { // sayto
-				lua_pushinteger(gL, 2); // type
-				LUA_PushUserdata(gL, &players[target-1], META_PLAYER); // target
-			}
-			lua_pushstring(gL, msg); // msg
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -5);
-		lua_pushvalue(gL, -5);
-		lua_pushvalue(gL, -5);
-		lua_pushvalue(gL, -5);
-		if (lua_pcall(gL, 4, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		LUA_PushUserdata(gL, t1, META_MOBJ);
+		LUA_PushUserdata(gL, t2, META_MOBJ);
+		LUA_PushUserdata(gL, line, META_LINE);
+		call_hooks(&hook, 1, res_true);
 	}
-
-	lua_settop(gL, 0);
-	return hooked;
+	return hook.status;
 }
 
+typedef struct {
+	mobj_t   * tails;
+	ticcmd_t * cmd;
+} BotAI_State;
 
-// Hook for hurt messages
-boolean LUAh_HurtMsg(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 damagetype)
+static boolean checkbotkey(const char *field)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_HurtMsg/8] & (1<<(hook_HurtMsg%8))))
-		return false;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+	return lua_toboolean(gL, -1) && strcmp(lua_tostring(gL, -2), field) == 0;
+}
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != hook_HurtMsg
-		|| (hookp->s.mt && !(inflictor && hookp->s.mt == inflictor->type)))
-			continue;
+static void res_botai(Hook_State *hook)
+{
+	BotAI_State *botai = hook->userdata;
+
+	int k[8];
+
+	int fields = 0;
+
+	// This turns forward, backward, left, right, jump, and spin into a proper ticcmd for tails.
+	if (lua_istable(gL, -8)) {
+		lua_pushnil(gL); // key
+		while (lua_next(gL, -9)) {
+#define CHECK(n, f) (checkbotkey(f) ? (k[(n)-1] = 1) : 0)
+			if (
+					CHECK(1,    "forward") || CHECK(2,    "backward") ||
+					CHECK(3,       "left") || CHECK(4,       "right") ||
+					CHECK(5, "strafeleft") || CHECK(6, "straferight") ||
+					CHECK(7,       "jump") || CHECK(8,        "spin")
+			){
+				if (8 <= ++fields)
+				{
+					lua_pop(gL, 2); // pop key and value
+					break;
+				}
+			}
 
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, player, META_PLAYER);
-			LUA_PushUserdata(gL, inflictor, META_MOBJ);
-			LUA_PushUserdata(gL, source, META_MOBJ);
-			lua_pushinteger(gL, damagetype);
+			lua_pop(gL, 1); // pop value
+#undef CHECK
 		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -5);
-		lua_pushvalue(gL, -5);
-		lua_pushvalue(gL, -5);
-		lua_pushvalue(gL, -5);
-		if (lua_pcall(gL, 4, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
+	} else {
+		while (fields < 8)
+		{
+			k[fields] = lua_toboolean(gL, -8 + fields);
+			fields++;
 		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
 	}
 
-	lua_settop(gL, 0);
-	return hooked;
+	B_KeysToTiccmd(botai->tails, botai->cmd,
+			k[0],k[1],k[2],k[3],k[4],k[5],k[6],k[7]);
+
+	hook->status = true;
 }
 
-void LUAh_NetArchiveHook(lua_CFunction archFunc)
+int LUA_HookBotAI(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 {
-	hook_p hookp;
-	int errorhandlerindex;
-	if (!gL || !(hooksAvailable[hook_NetVars/8] & (1<<(hook_NetVars%8))))
-		return;
+	const char *skin = ((skin_t *)tails->skin)->name;
+
+	Hook_State hook;
+	BotAI_State botai;
 
-	// stack: tables
-	I_Assert(lua_gettop(gL) > 0);
-	I_Assert(lua_istable(gL, -1));
+	if (prepare_string_hook(&hook, false, STRING_HOOK(BotAI), skin))
+	{
+		LUA_PushUserdata(gL, sonic, META_MOBJ);
+		LUA_PushUserdata(gL, tails, META_MOBJ);
 
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-	errorhandlerindex = lua_gettop(gL);
+		botai.tails = tails;
+		botai.cmd   = cmd;
 
-	// tables becomes an upvalue of archFunc
-	lua_pushvalue(gL, -2);
-	lua_pushcclosure(gL, archFunc, 1);
-	// stack: tables, archFunc
+		hook.userdata = &botai;
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != hook_NetVars)
-			continue;
-
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -2); // archFunc
-		if (lua_pcall(gL, 1, 0, errorhandlerindex)) {
-			CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-		}
+		call_hooks(&hook, 8, res_botai);
 	}
 
-	lua_pop(gL, 2); // Pop archFunc and error handler
-	// stack: tables
+	return hook.status;
 }
 
-boolean LUAh_MapThingSpawn(mobj_t *mo, mapthing_t *mthing)
+void LUA_HookLinedefExecute(line_t *line, mobj_t *mo, sector_t *sector)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_MapThingSpawn/8] & (1<<(hook_MapThingSpawn%8))))
-		return false;
-
-	if (!(mobjhooks[MT_NULL] || mobjhooks[mo->type]))
-		return false;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	// Look for all generic mobj map thing spawn hooks
-	for (hookp = mobjhooks[MT_NULL]; hookp; hookp = hookp->next)
+	Hook_State hook;
+	if (prepare_string_hook
+			(&hook, 0, STRING_HOOK(LinedefExecute), line->stringargs[0]))
 	{
-		if (hookp->type != hook_MapThingSpawn)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, mo, META_MOBJ);
-			LUA_PushUserdata(gL, mthing, META_MAPTHING);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		LUA_PushUserdata(gL, line, META_LINE);
+		LUA_PushUserdata(gL, mo, META_MOBJ);
+		LUA_PushUserdata(gL, sector, META_SECTOR);
+		ps_lua_mobjhooks.value.i += call_hooks(&hook, 0, res_none);
 	}
+}
 
-	for (hookp = mobjhooks[mo->type]; hookp; hookp = hookp->next)
+int LUA_HookPlayerMsg(int source, int target, int flags, char *msg)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, false, HOOK(PlayerMsg)))
 	{
-		if (hookp->type != hook_MapThingSpawn)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, mo, META_MOBJ);
-			LUA_PushUserdata(gL, mthing, META_MAPTHING);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		LUA_PushUserdata(gL, &players[source], META_PLAYER); // Source player
+		if (flags & 2 /*HU_CSAY*/) { // csay TODO: make HU_CSAY accessible outside hu_stuff.c
+			lua_pushinteger(gL, 3); // type
+			lua_pushnil(gL); // target
+		} else if (target == -1) { // sayteam
+			lua_pushinteger(gL, 1); // type
+			lua_pushnil(gL); // target
+		} else if (target == 0) { // say
+			lua_pushinteger(gL, 0); // type
+			lua_pushnil(gL); // target
+		} else { // sayto
+			lua_pushinteger(gL, 2); // type
+			LUA_PushUserdata(gL, &players[target-1], META_PLAYER); // target
+		}
+		lua_pushstring(gL, msg); // msg
+		call_hooks(&hook, 1, res_true);
 	}
+	return hook.status;
+}
 
-	lua_settop(gL, 0);
-	return hooked;
+int LUA_HookHurtMsg(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 damagetype)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, false, HOOK(HurtMsg)))
+	{
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		LUA_PushUserdata(gL, inflictor, META_MOBJ);
+		LUA_PushUserdata(gL, source, META_MOBJ);
+		lua_pushinteger(gL, damagetype);
+		call_hooks(&hook, 1, res_true);
+	}
+	return hook.status;
 }
 
-// Hook for P_PlayerAfterThink Smiles mobj-following
-boolean LUAh_FollowMobj(player_t *player, mobj_t *mobj)
+void LUA_HookNetArchive(lua_CFunction archFunc)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_FollowMobj/8] & (1<<(hook_FollowMobj%8))))
-		return 0;
+	const hook_t * map = &hookIds[HOOK(NetVars)];
+	Hook_State hook;
+	/* this is a remarkable case where the stack isn't reset */
+	if (map->numHooks > 0)
+	{
+		// stack: tables
+		I_Assert(lua_gettop(gL) > 0);
+		I_Assert(lua_istable(gL, -1));
 
-	if (!(mobjhooks[MT_NULL] || mobjhooks[mobj->type]))
-		return 0;
+		push_error_handler();
+		lua_insert(gL, EINDEX);
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+		begin_hook_values(&hook);
 
-	// Look for all generic mobj follow item hooks
-	for (hookp = mobjhooks[MT_NULL]; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != hook_FollowMobj)
-			continue;
+		// tables becomes an upvalue of archFunc
+		lua_pushvalue(gL, -1);
+		lua_pushcclosure(gL, archFunc, 1);
+		// stack: tables, archFunc
 
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, player, META_PLAYER);
-			LUA_PushUserdata(gL, mobj, META_MOBJ);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		init_hook_call(&hook, 0, res_none);
+		call_mapped(&hook, map);
+
+		lua_pop(gL, 1); // pop archFunc
+		lua_remove(gL, EINDEX); // pop error handler
+		// stack: tables
 	}
+}
 
-	for (hookp = mobjhooks[mobj->type]; hookp; hookp = hookp->next)
+int LUA_HookMapThingSpawn(mobj_t *mobj, mapthing_t *mthing)
+{
+	Hook_State hook;
+	if (prepare_mobj_hook(&hook, false, MOBJ_HOOK(MapThingSpawn), mobj->type))
 	{
-		if (hookp->type != hook_FollowMobj)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, player, META_PLAYER);
-			LUA_PushUserdata(gL, mobj, META_MOBJ);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		LUA_PushUserdata(gL, mobj, META_MOBJ);
+		LUA_PushUserdata(gL, mthing, META_MAPTHING);
+		call_hooks(&hook, 1, res_true);
 	}
-
-	lua_settop(gL, 0);
-	return hooked;
+	return hook.status;
 }
 
-// Hook for P_PlayerCanDamage
-UINT8 LUAh_PlayerCanDamage(player_t *player, mobj_t *mobj)
+int LUA_HookFollowMobj(player_t *player, mobj_t *mobj)
 {
-	hook_p hookp;
-	UINT8 shouldCollide = 0; // 0 = default, 1 = force yes, 2 = force no.
-	if (!gL || !(hooksAvailable[hook_PlayerCanDamage/8] & (1<<(hook_PlayerCanDamage%8))))
-		return 0;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	for (hookp = playerhooks; hookp; hookp = hookp->next)
+	Hook_State hook;
+	if (prepare_mobj_hook(&hook, false, MOBJ_HOOK(FollowMobj), mobj->type))
 	{
-		if (hookp->type != hook_PlayerCanDamage)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, player, META_PLAYER);
-			LUA_PushUserdata(gL, mobj, META_MOBJ);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (!lua_isnil(gL, -1))
-		{ // if nil, leave shouldCollide = 0.
-			if (lua_toboolean(gL, -1))
-				shouldCollide = 1; // Force yes
-			else
-				shouldCollide = 2; // Force no
-		}
-		lua_pop(gL, 1);
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		LUA_PushUserdata(gL, mobj, META_MOBJ);
+		call_hooks(&hook, 1, res_true);
 	}
-
-	lua_settop(gL, 0);
-	return shouldCollide;
+	return hook.status;
 }
 
-void LUAh_PlayerQuit(player_t *plr, kickreason_t reason)
+int LUA_HookPlayerCanDamage(player_t *player, mobj_t *mobj)
 {
-	hook_p hookp;
-	if (!gL || !(hooksAvailable[hook_PlayerQuit/8] & (1<<(hook_PlayerQuit%8))))
-		return;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	Hook_State hook;
+	if (prepare_hook(&hook, 0, HOOK(PlayerCanDamage)))
 	{
-		if (hookp->type != hook_PlayerQuit)
-			continue;
-
-	    if (lua_gettop(gL) == 1)
-	    {
-	        LUA_PushUserdata(gL, plr, META_PLAYER); // Player that quit
-	        lua_pushinteger(gL, reason); // Reason for quitting
-	    }
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 0, 1)) {
-			CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-		}
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		LUA_PushUserdata(gL, mobj, META_MOBJ);
+		call_hooks(&hook, 1, res_force);
 	}
-
-	lua_settop(gL, 0);
+	return hook.status;
 }
 
-// Hook for Y_Ticker
-void LUAh_IntermissionThinker(void)
+void LUA_HookPlayerQuit(player_t *plr, kickreason_t reason)
 {
-	hook_p hookp;
-	if (!gL || !(hooksAvailable[hook_IntermissionThinker/8] & (1<<(hook_IntermissionThinker%8))))
-		return;
-
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	Hook_State hook;
+	if (prepare_hook(&hook, 0, HOOK(PlayerQuit)))
 	{
-		if (hookp->type != hook_IntermissionThinker)
-			continue;
-
-		PushHook(gL, hookp);
-		if (lua_pcall(gL, 0, 0, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-		}
+		LUA_PushUserdata(gL, plr, META_PLAYER); // Player that quit
+		lua_pushinteger(gL, reason); // Reason for quitting
+		call_hooks(&hook, 0, res_none);
 	}
-
-	lua_pop(gL, 1); // Pop error handler
 }
 
-// Hook for team switching
-// It's just an edit of LUAh_ViewpointSwitch.
-boolean LUAh_TeamSwitch(player_t *player, int newteam, boolean fromspectators, boolean tryingautobalance, boolean tryingscramble)
+int LUA_HookTeamSwitch(player_t *player, int newteam, boolean fromspectators, boolean tryingautobalance, boolean tryingscramble)
 {
-	hook_p hookp;
-	boolean canSwitchTeam = true;
-	if (!gL || !(hooksAvailable[hook_TeamSwitch/8] & (1<<(hook_TeamSwitch%8))))
-		return true;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	for (hookp = playerhooks; hookp; hookp = hookp->next)
+	Hook_State hook;
+	if (prepare_hook(&hook, true, HOOK(TeamSwitch)))
 	{
-		if (hookp->type != hook_TeamSwitch)
-			continue;
-
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, player, META_PLAYER);
-			lua_pushinteger(gL, newteam);
-			lua_pushboolean(gL, fromspectators);
-			lua_pushboolean(gL, tryingautobalance);
-			lua_pushboolean(gL, tryingscramble);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		lua_pushvalue(gL, -6);
-		if (lua_pcall(gL, 5, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (!lua_isnil(gL, -1) && !lua_toboolean(gL, -1))
-			canSwitchTeam = false; // Can't switch team
-		lua_pop(gL, 1);
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		lua_pushinteger(gL, newteam);
+		lua_pushboolean(gL, fromspectators);
+		lua_pushboolean(gL, tryingautobalance);
+		lua_pushboolean(gL, tryingscramble);
+		call_hooks(&hook, 1, res_false);
 	}
-
-	lua_settop(gL, 0);
-	return canSwitchTeam;
+	return hook.status;
 }
 
-// Hook for spy mode
-UINT8 LUAh_ViewpointSwitch(player_t *player, player_t *newdisplayplayer, boolean forced)
+int LUA_HookViewpointSwitch(player_t *player, player_t *newdisplayplayer, boolean forced)
 {
-	hook_p hookp;
-	UINT8 canSwitchView = 0; // 0 = default, 1 = force yes, 2 = force no.
-	if (!gL || !(hooksAvailable[hook_ViewpointSwitch/8] & (1<<(hook_ViewpointSwitch%8))))
-		return 0;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	hud_running = true; // local hook
-
-	for (hookp = playerhooks; hookp; hookp = hookp->next)
+	Hook_State hook;
+	if (prepare_hook(&hook, 0, HOOK(ViewpointSwitch)))
 	{
-		if (hookp->type != hook_ViewpointSwitch)
-			continue;
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		LUA_PushUserdata(gL, newdisplayplayer, META_PLAYER);
+		lua_pushboolean(gL, forced);
 
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, player, META_PLAYER);
-			LUA_PushUserdata(gL, newdisplayplayer, META_PLAYER);
-			lua_pushboolean(gL, forced);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -4);
-		lua_pushvalue(gL, -4);
-		lua_pushvalue(gL, -4);
-		if (lua_pcall(gL, 3, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (!lua_isnil(gL, -1))
-		{ // if nil, leave canSwitchView = 0.
-			if (lua_toboolean(gL, -1))
-				canSwitchView = 1; // Force viewpoint switch
-			else
-				canSwitchView = 2; // Skip viewpoint switch
-		}
-		lua_pop(gL, 1);
+		hud_running = true; // local hook
+		call_hooks(&hook, 1, res_force);
+		hud_running = false;
 	}
+	return hook.status;
+}
 
-	lua_settop(gL, 0);
-
-	hud_running = false;
+int LUA_HookSeenPlayer(player_t *player, player_t *seenfriend)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, true, HOOK(SeenPlayer)))
+	{
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		LUA_PushUserdata(gL, seenfriend, META_PLAYER);
 
-	return canSwitchView;
+		hud_running = true; // local hook
+		call_hooks(&hook, 1, res_false);
+		hud_running = false;
+	}
+	return hook.status;
 }
 
-// Hook for MT_NAMECHECK
-#ifdef SEENAMES
-boolean LUAh_SeenPlayer(player_t *player, player_t *seenfriend)
+int LUA_HookShouldJingleContinue(player_t *player, const char *musname)
 {
-	hook_p hookp;
-	boolean hasSeenPlayer = true;
-	if (!gL || !(hooksAvailable[hook_SeenPlayer/8] & (1<<(hook_SeenPlayer%8))))
-		return true;
+	Hook_State hook;
+	if (prepare_string_hook
+			(&hook, false, STRING_HOOK(ShouldJingleContinue), musname))
+	{
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		push_string();
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+		hud_running = true; // local hook
+		call_hooks(&hook, 1, res_true);
+		hud_running = false;
+	}
+	return hook.status;
+}
+
+boolean hook_cmd_running = false;
 
-	hud_running = true; // local hook
+static void update_music_name(struct MusicChange *musicchange)
+{
+	size_t length;
+	const char * new = lua_tolstring(gL, -6, &length);
 
-	for (hookp = playerhooks; hookp; hookp = hookp->next)
+	if (length < 7)
 	{
-		if (hookp->type != hook_SeenPlayer)
-			continue;
-
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, player, META_PLAYER);
-			LUA_PushUserdata(gL, seenfriend, META_PLAYER);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (!lua_isnil(gL, -1) && !lua_toboolean(gL, -1))
-			hasSeenPlayer = false; // Hasn't seen player
-		lua_pop(gL, 1);
+		strcpy(musicchange->newname, new);
+		lua_pushvalue(gL, -6);/* may as well keep it for next call */
+	}
+	else
+	{
+		memcpy(musicchange->newname, new, 6);
+		musicchange->newname[6] = '\0';
+		lua_pushlstring(gL, new, 6);
 	}
 
-	lua_settop(gL, 0);
-
-	hud_running = false;
+	lua_replace(gL, -7);
+}
 
-	return hasSeenPlayer;
+static void res_musicchange(Hook_State *hook)
+{
+	struct MusicChange *musicchange = hook->userdata;
+
+	// output 1: true, false, or string musicname override
+	if (lua_isstring(gL, -6))
+		update_music_name(musicchange);
+	else if (lua_isboolean(gL, -6) && lua_toboolean(gL, -6))
+		hook->status = true;
+
+	// output 2: mflags override
+	if (lua_isnumber(gL, -5))
+		*musicchange->mflags = lua_tonumber(gL, -5);
+	// output 3: looping override
+	if (lua_isboolean(gL, -4))
+		*musicchange->looping = lua_toboolean(gL, -4);
+	// output 4: position override
+	if (lua_isnumber(gL, -3))
+		*musicchange->position = lua_tonumber(gL, -3);
+	// output 5: prefadems override
+	if (lua_isnumber(gL, -2))
+		*musicchange->prefadems = lua_tonumber(gL, -2);
+	// output 6: fadeinms override
+	if (lua_isnumber(gL, -1))
+		*musicchange->fadeinms = lua_tonumber(gL, -1);
 }
-#endif // SEENAMES
 
-boolean LUAh_ShouldJingleContinue(player_t *player, const char *musname)
+int LUA_HookMusicChange(const char *oldname, struct MusicChange *param)
 {
-	hook_p hookp;
-	boolean keepplaying = false;
-	if (!gL || !(hooksAvailable[hook_ShouldJingleContinue/8] & (1<<(hook_ShouldJingleContinue%8))))
-		return true;
+	const int type = HOOK(MusicChange);
+	const hook_t * map = &hookIds[type];
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+	Hook_State hook;
 
-	hud_running = true; // local hook
+	int k;
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	if (prepare_hook(&hook, false, type))
 	{
-		if (hookp->type != hook_ShouldJingleContinue
-			|| (hookp->s.str && strcmp(hookp->s.str, musname)))
-			continue;
+		init_hook_call(&hook, 6, res_musicchange);
+		hook.values = 7;/* values pushed later */
+		hook.userdata = param;
 
-		if (lua_gettop(gL) == 1)
+		lua_pushstring(gL, oldname);/* the only constant value */
+		lua_pushstring(gL, param->newname);/* semi constant */
+
+		for (k = 0; k <= map->numHooks; ++k)
 		{
-			LUA_PushUserdata(gL, player, META_PLAYER);
-			lua_pushstring(gL, musname);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (!lua_isnil(gL, -1) && lua_toboolean(gL, -1))
-			keepplaying = true; // Keep playing this boolean
-		lua_pop(gL, 1);
-	}
+			get_hook(&hook, map->ids, k);
 
-	lua_settop(gL, 0);
+			lua_pushvalue(gL, -3);
+			lua_pushvalue(gL, -3);
+			lua_pushinteger(gL, *param->mflags);
+			lua_pushboolean(gL, *param->looping);
+			lua_pushinteger(gL, *param->position);
+			lua_pushinteger(gL, *param->prefadems);
+			lua_pushinteger(gL, *param->fadeinms);
 
-	hud_running = false;
+			call_single_hook_no_copy(&hook);
+		}
+
+		lua_settop(gL, 0);
+	}
 
-	return keepplaying;
+	return hook.status;
 }
 
-// Hook for game quitting
-void LUAh_GameQuit(void)
+static void res_playerheight(Hook_State *hook)
 {
-	hook_p hookp;
-	if (!gL || !(hooksAvailable[hook_GameQuit/8] & (1<<(hook_GameQuit%8))))
-		return;
-
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	if (!lua_isnil(gL, -1))
 	{
-		if (hookp->type != hook_GameQuit)
-			continue;
-
-		PushHook(gL, hookp);
-		if (lua_pcall(gL, 0, 0, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-		}
+		fixed_t returnedheight = lua_tonumber(gL, -1);
+		// 0 height has... strange results, but it's not problematic like negative heights are.
+		// when an object's height is set to a negative number directly with lua, it's forced to 0 instead.
+		// here, I think it's better to ignore negatives so that they don't replace any results of previous hooks!
+		if (returnedheight >= 0)
+			hook->status = returnedheight;
 	}
-
-	lua_pop(gL, 1); // Pop error handler
 }
 
-// Hook for building player's ticcmd struct (Ported from SRB2Kart)
-boolean hook_cmd_running = false;
-boolean LUAh_PlayerCmd(player_t *player, ticcmd_t *cmd)
+fixed_t LUA_HookPlayerHeight(player_t *player)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_PlayerCmd/8] & (1<<(hook_PlayerCmd%8))))
-		return false;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	hook_cmd_running = true;
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	Hook_State hook;
+	if (prepare_hook(&hook, -1, HOOK(PlayerHeight)))
 	{
-		if (hookp->type != hook_PlayerCmd)
-			continue;
-
-		if (lua_gettop(gL) == 1)
-		{
-			LUA_PushUserdata(gL, player, META_PLAYER);
-			LUA_PushUserdata(gL, cmd, META_TICCMD);
-		}
-		PushHook(gL, hookp);
-		lua_pushvalue(gL, -3);
-		lua_pushvalue(gL, -3);
-		if (lua_pcall(gL, 2, 1, 1)) {
-			if (!hookp->error || cv_debug & DBG_LUA)
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL, -1));
-			lua_pop(gL, 1);
-			hookp->error = true;
-			continue;
-		}
-		if (lua_toboolean(gL, -1))
-			hooked = true;
-		lua_pop(gL, 1);
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		call_hooks(&hook, 1, res_playerheight);
 	}
-
-	lua_settop(gL, 0);
-	hook_cmd_running = false;
-	return hooked;
+	return hook.status;
 }
 
-// Hook for music changes
-boolean LUAh_MusicChange(const char *oldname, char *newname, UINT16 *mflags, boolean *looping,
-	UINT32 *position, UINT32 *prefadems, UINT32 *fadeinms)
+int LUA_HookPlayerCanEnterSpinGaps(player_t *player)
 {
-	hook_p hookp;
-	boolean hooked = false;
-
-	if (!gL || !(hooksAvailable[hook_MusicChange/8] & (1<<(hook_MusicChange%8))))
-		return false;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	for (hookp = roothook; hookp; hookp = hookp->next)
-		if (hookp->type == hook_MusicChange)
-		{
-			PushHook(gL, hookp);
-			lua_pushstring(gL, oldname);
-			lua_pushstring(gL, newname);
-			lua_pushinteger(gL, *mflags);
-			lua_pushboolean(gL, *looping);
-			lua_pushinteger(gL, *position);
-			lua_pushinteger(gL, *prefadems);
-			lua_pushinteger(gL, *fadeinms);
-			if (lua_pcall(gL, 7, 6, 1)) {
-				CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL,-1));
-				lua_pop(gL, 1);
-				continue;
-			}
-
-			// output 1: true, false, or string musicname override
-			if (lua_isboolean(gL, -6) && lua_toboolean(gL, -6))
-				hooked = true;
-			else if (lua_isstring(gL, -6))
-				strncpy(newname, lua_tostring(gL, -6), 7);
-			// output 2: mflags override
-			if (lua_isnumber(gL, -5))
-				*mflags = lua_tonumber(gL, -5);
-			// output 3: looping override
-			if (lua_isboolean(gL, -4))
-				*looping = lua_toboolean(gL, -4);
-			// output 4: position override
-			if (lua_isboolean(gL, -3))
-				*position = lua_tonumber(gL, -3);
-			// output 5: prefadems override
-			if (lua_isboolean(gL, -2))
-				*prefadems = lua_tonumber(gL, -2);
-			// output 6: fadeinms override
-			if (lua_isboolean(gL, -1))
-				*fadeinms = lua_tonumber(gL, -1);
-
-			lua_pop(gL, 7);  // Pop returned values and error handler
-		}
-
-	lua_settop(gL, 0);
-	newname[6] = 0;
-	return hooked;
-}
\ No newline at end of file
+	Hook_State hook;
+	if (prepare_hook(&hook, 0, HOOK(PlayerCanEnterSpinGaps)))
+	{
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		call_hooks(&hook, 1, res_force);
+	}
+	return hook.status;
+}
diff --git a/src/lua_hud.h b/src/lua_hud.h
index 4a7c596c8a95e7d343fd9da73b8aa50bcaa344b0..c1d2d164b97d6952612e088d80f0bd256a117d4b 100644
--- a/src/lua_hud.h
+++ b/src/lua_hud.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2014-2016 by John "JTE" Muniz.
-// Copyright (C) 2014-2020 by Sonic Team Junior.
+// Copyright (C) 2014-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -13,6 +13,7 @@
 enum hud {
 	hud_stagetitle = 0,
 	hud_textspectator,
+	hud_crosshair,
 	// Singleplayer / Co-op
 	hud_score,
 	hud_time,
@@ -36,7 +37,9 @@ enum hud {
 	hud_tabemblems,
 	// Intermission
 	hud_intermissiontally,
+	hud_intermissiontitletext,
 	hud_intermissionmessages,
+	hud_intermissionemeralds,
 	hud_MAX
 };
 
@@ -44,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(void);
+void LUA_SetHudHook(int hook);
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index 684e47c381d63dbcd7dee7c376f82ba0777a33a3..0dd951efd964e28a04074d311215d059564d88a7 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2014-2016 by John "JTE" Muniz.
-// Copyright (C) 2014-2020 by Sonic Team Junior.
+// Copyright (C) 2014-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -23,22 +23,23 @@
 #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",
 	"textspectator",
+	"crosshair",
 
 	"score",
 	"time",
@@ -62,7 +63,9 @@ static const char *const hud_disable_options[] = {
 	"tabemblems",
 
 	"intermissiontally",
+	"intermissiontitletext",
 	"intermissionmessages",
+	"intermissionemeralds",
 	NULL};
 
 enum hudinfo {
@@ -92,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,
@@ -381,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
 //
@@ -660,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;
@@ -856,6 +951,26 @@ static int libd_drawScaledNameTag(lua_State *L)
 	return 0;
 }
 
+static int libd_drawLevelTitle(lua_State *L)
+{
+	INT32 x;
+	INT32 y;
+	const char *str;
+	INT32 flags;
+
+	HUDONLY
+
+	x = luaL_checkinteger(L, 1);
+	y = luaL_checkinteger(L, 2);
+	str = luaL_checkstring(L, 3);
+	flags = luaL_optinteger(L, 4, 0);
+
+	flags &= ~V_PARAMMASK; // Don't let crashes happen.
+
+	V_DrawLevelTitle(x, y, flags, str);
+	return 0;
+}
+
 static int libd_stringWidth(lua_State *L)
 {
 	const char *str = luaL_checkstring(L, 1);
@@ -885,6 +1000,20 @@ static int libd_nameTagWidth(lua_State *L)
 	return 1;
 }
 
+static int libd_levelTitleWidth(lua_State *L)
+{
+	HUDONLY
+	lua_pushinteger(L, V_LevelNameWidth(luaL_checkstring(L, 1)));
+	return 1;
+}
+
+static int libd_levelTitleHeight(lua_State *L)
+{
+	HUDONLY
+	lua_pushinteger(L, V_LevelNameHeight(luaL_checkstring(L, 1)));
+	return 1;
+}
+
 static int libd_getColormap(lua_State *L)
 {
 	INT32 skinnum = TC_DEFAULT;
@@ -896,8 +1025,10 @@ static int libd_getColormap(lua_State *L)
 	else if (lua_type(L, 1) == LUA_TNUMBER) // skin number
 	{
 		skinnum = (INT32)luaL_checkinteger(L, 1);
-		if (skinnum < TC_BLINK || skinnum >= MAXSKINS)
-			return luaL_error(L, "skin number %d is out of range (%d - %d)", skinnum, TC_BLINK, MAXSKINS-1);
+		if (skinnum >= MAXSKINS)
+			return luaL_error(L, "skin number %d is out of range (>%d)", skinnum, MAXSKINS-1);
+		else if (skinnum < 0 && skinnum > TC_DEFAULT)
+			return luaL_error(L, "translation colormap index is out of range");
 	}
 	else // skin name
 	{
@@ -1082,16 +1213,20 @@ 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},
 	{"drawString", libd_drawString},
 	{"drawNameTag", libd_drawNameTag},
 	{"drawScaledNameTag", libd_drawScaledNameTag},
+	{"drawLevelTitle", libd_drawLevelTitle},
 	{"fadeScreen", libd_fadeScreen},
 	// misc
 	{"stringWidth", libd_stringWidth},
 	{"nameTagWidth", libd_nameTagWidth},
+	{"levelTitleWidth", libd_levelTitleWidth},
+	{"levelTitleHeight", libd_levelTitleHeight},
 	// m_random
 	{"RandomFixed",libd_RandomFixed},
 	{"RandomByte",libd_RandomByte},
@@ -1110,6 +1245,8 @@ static luaL_Reg lib_draw[] = {
 	{NULL, NULL}
 };
 
+static int lib_draw_ref;
+
 //
 // lib_hud
 //
@@ -1144,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},
@@ -1183,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);
@@ -1241,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);
@@ -1254,156 +1356,29 @@ boolean LUA_HudEnabled(enum hud option)
 	return false;
 }
 
-// Hook for HUD rendering
-void LUAh_GameHUD(player_t *stplayr)
-{
-	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_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)
+void LUA_SetHudHook(int hook)
 {
-	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_getref(gL, lib_draw_ref);
 
-	lua_settop(gL, 0);
-	hud_running = false;
-}
-
-void LUAh_IntermissionHUD(void)
-{
-	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_pushnil(gL);
-	while (lua_next(gL, -3) != 0) {
-		lua_pushvalue(gL, -3); // graphics library (HUD[1])
-		LUA_Call(gL, 1, 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_infolib.c b/src/lua_infolib.c
index 4c6ef35287500d973d85af6d76412a0f4bc12202..af2d99a0c015cf05001ccaa31b3c54fffa09ea04 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -1635,8 +1635,10 @@ static int skincolor_get(lua_State *L)
 		lua_pushinteger(L, info->chatcolor);
 	else if (fastcmp(field,"accessible"))
 		lua_pushboolean(L, info->accessible);
-	else
+	else {
 		CONS_Debug(DBG_LUA, M_GetText("'%s' has no field named '%s'; returning nil.\n"), "skincolor_t", field);
+		return 0;
+	}
 	return 1;
 }
 
diff --git a/src/lua_inputlib.c b/src/lua_inputlib.c
new file mode 100644
index 0000000000000000000000000000000000000000..661d9364166f4e2eec28d281dd0711dfbd45480a
--- /dev/null
+++ b/src/lua_inputlib.c
@@ -0,0 +1,270 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2021 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  lua_inputlib.c
+/// \brief input library for Lua scripting
+
+#include "doomdef.h"
+#include "fastcmp.h"
+#include "g_input.h"
+#include "g_game.h"
+#include "hu_stuff.h"
+#include "i_system.h"
+
+#include "lua_script.h"
+#include "lua_libs.h"
+
+boolean mousegrabbedbylua = true;
+
+///////////////
+// FUNCTIONS //
+///////////////
+
+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);
+	lua_pushinteger(L, PLAYER1INPUTDOWN(i));
+	return 1;
+}
+
+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);
+	lua_pushinteger(L, PLAYER2INPUTDOWN(i));
+	return 1;
+}
+
+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);
+	lua_pushinteger(L, gamecontrol[i][0]);
+	lua_pushinteger(L, gamecontrol[i][1]);
+	return 2;
+}
+
+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);
+	lua_pushinteger(L, gamecontrolbis[i][0]);
+	lua_pushinteger(L, gamecontrolbis[i][1]);
+	return 2;
+}
+
+static int lib_joyAxis(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 1);
+	lua_pushinteger(L, JoyAxis(i));
+	return 1;
+}
+
+static int lib_joy2Axis(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 1);
+	lua_pushinteger(L, Joy2Axis(i));
+	return 1;
+}
+
+static int lib_keyNumToName(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 1);
+	lua_pushstring(L, G_KeyNumToName(i));
+	return 1;
+}
+
+static int lib_keyNameToNum(lua_State *L)
+{
+	const char *str = luaL_checkstring(L, 1);
+	lua_pushinteger(L, G_KeyNameToNum(str));
+	return 1;
+}
+
+static int lib_keyNumPrintable(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 1);
+	lua_pushboolean(L, i >= 32 && i <= 127);
+	return 1;
+}
+
+static int lib_shiftKeyNum(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 1);
+	if (i >= 32 && i <= 127)
+		lua_pushinteger(L, shiftxform[i]);
+	return 1;
+}
+
+static int lib_getMouseGrab(lua_State *L)
+{
+	lua_pushboolean(L, mousegrabbedbylua);
+	return 1;
+}
+
+static int lib_setMouseGrab(lua_State *L)
+{
+	mousegrabbedbylua = luaL_checkboolean(L, 1);
+	I_UpdateMouseGrab();
+	return 0;
+}
+
+static int lib_getCursorPosition(lua_State *L)
+{
+	int x, y;
+	I_GetCursorPosition(&x, &y);
+	lua_pushinteger(L, x);
+	lua_pushinteger(L, y);
+	return 2;
+}
+
+static luaL_Reg lib[] = {
+	{"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}
+};
+
+///////////////////
+// gamekeydown[] //
+///////////////////
+
+static int lib_getGameKeyDown(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 2);
+	if (i < 0 || i >= NUMINPUTS)
+		return luaL_error(L, "gamekeydown[] index %d out of range (0 - %d)", i, NUMINPUTS-1);
+	lua_pushboolean(L, gamekeydown[i]);
+	return 1;
+}
+
+static int lib_setGameKeyDown(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 2);
+	boolean j = luaL_checkboolean(L, 3);
+	if (i < 0 || i >= NUMINPUTS)
+		return luaL_error(L, "gamekeydown[] index %d out of range (0 - %d)", i, NUMINPUTS-1);
+	gamekeydown[i] = j;
+	return 0;
+}
+
+static int lib_lenGameKeyDown(lua_State *L)
+{
+	lua_pushinteger(L, NUMINPUTS);
+	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 //
+///////////
+
+static int mouse_get(lua_State *L)
+{
+	mouse_t *m = *((mouse_t **)luaL_checkudata(L, 1, META_MOUSE));
+	const char *field = luaL_checkstring(L, 2);
+
+	I_Assert(m != NULL);
+
+	if (fastcmp(field,"dx"))
+		lua_pushinteger(L, m->dx);
+	else if (fastcmp(field,"dy"))
+		lua_pushinteger(L, m->dy);
+	else if (fastcmp(field,"mlookdy"))
+		lua_pushinteger(L, m->mlookdy);
+	else if (fastcmp(field,"rdx"))
+		lua_pushinteger(L, m->rdx);
+	else if (fastcmp(field,"rdy"))
+		lua_pushinteger(L, m->rdy);
+	else if (fastcmp(field,"buttons"))
+		lua_pushinteger(L, m->buttons);
+	else
+		return luaL_error(L, "mouse_t has no field named %s", field);
+
+	return 1;
+}
+
+// #mouse -> 1 or 2
+static int mouse_num(lua_State *L)
+{
+	mouse_t *m = *((mouse_t **)luaL_checkudata(L, 1, META_MOUSE));
+
+	I_Assert(m != NULL);
+
+	lua_pushinteger(L, m == &mouse ? 1 : 2);
+	return 1;
+}
+
+int LUA_InputLib(lua_State *L)
+{
+	lua_newuserdata(L, 0);
+		lua_createtable(L, 0, 2);
+			lua_pushcfunction(L, lib_getGameKeyDown);
+			lua_setfield(L, -2, "__index");
+
+			lua_pushcfunction(L, lib_setGameKeyDown);
+			lua_setfield(L, -2, "__newindex");
+
+			lua_pushcfunction(L, lib_lenGameKeyDown);
+			lua_setfield(L, -2, "__len");
+		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");
+
+		lua_pushcfunction(L, mouse_num);
+		lua_setfield(L, -2, "__len");
+	lua_pop(L, 1);
+
+	luaL_register(L, "input", lib);
+	return 0;
+}
diff --git a/src/lua_libs.h b/src/lua_libs.h
index 062a3fe5009fb2beda2c2a5545243ab350a2cdf5..8903834e861c32c72dc9eaf2f22c864cefc218a7 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -12,6 +12,10 @@
 
 extern lua_State *gL;
 
+extern boolean mousegrabbedbylua;
+
+#define MUTABLE_TAGS
+
 #define LREG_VALID "VALID_USERDATA"
 #define LREG_EXTVARS "LUA_VARS"
 #define LREG_STATEACTION "STATE_ACTION"
@@ -27,6 +31,8 @@ extern lua_State *gL;
 #define META_PIVOTLIST "SPRITEFRAMEPIVOT_T[]"
 #define META_FRAMEPIVOT "SPRITEFRAMEPIVOT_T*"
 
+#define META_TAGLIST "TAGLIST"
+
 #define META_MOBJ "MOBJ_T*"
 #define META_MAPTHING "MAPTHING_T*"
 
@@ -35,6 +41,8 @@ extern lua_State *gL;
 #define META_SKIN "SKIN_T*"
 #define META_POWERS "PLAYER_T*POWERS"
 #define META_SOUNDSID "SKIN_T*SOUNDSID"
+#define META_SKINSPRITES "SKIN_T*SPRITES"
+#define META_SKINSPRITESLIST "SKIN_T*SPRITES[]"
 
 #define META_VERTEX "VERTEX_T*"
 #define META_LINE "LINE_T*"
@@ -56,6 +64,9 @@ extern lua_State *gL;
 #define META_CVAR "CONSVAR_T*"
 
 #define META_SECTORLINES "SECTOR_T*LINES"
+#ifdef MUTABLE_TAGS
+#define META_SECTORTAGLIST "sector_t.taglist"
+#endif
 #define META_SIDENUM "LINE_T*SIDENUM"
 #define META_LINEARGS "LINE_T*ARGS"
 #define META_LINESTRINGARGS "LINE_T*STRINGARGS"
@@ -79,6 +90,9 @@ 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);
 
 int LUA_EnumLib(lua_State *L);
@@ -93,6 +107,8 @@ int LUA_PlayerLib(lua_State *L);
 int LUA_SkinLib(lua_State *L);
 int LUA_ThinkerLib(lua_State *L);
 int LUA_MapLib(lua_State *L);
+int LUA_TagLib(lua_State *L);
 int LUA_PolyObjLib(lua_State *L);
 int LUA_BlockmapLib(lua_State *L);
 int LUA_HudLib(lua_State *L);
+int LUA_InputLib(lua_State *L);
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 95cc8c1019e88de52213a0fac5ebff9d42ffa8d0..9031c99f13a981fc6c235caea12a0fbfb9201e49 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -37,6 +37,7 @@ enum sector_e {
 	sector_lightlevel,
 	sector_special,
 	sector_tag,
+	sector_taglist,
 	sector_thinglist,
 	sector_heightsec,
 	sector_camsec,
@@ -55,6 +56,7 @@ static const char *const sector_opt[] = {
 	"lightlevel",
 	"special",
 	"tag",
+	"taglist",
 	"thinglist",
 	"heightsec",
 	"camsec",
@@ -89,6 +91,7 @@ enum line_e {
 	line_flags,
 	line_special,
 	line_tag,
+	line_taglist,
 	line_args,
 	line_stringargs,
 	line_sidenum,
@@ -113,6 +116,7 @@ static const char *const line_opt[] = {
 	"flags",
 	"special",
 	"tag",
+	"taglist",
 	"args",
 	"stringargs",
 	"sidenum",
@@ -579,7 +583,10 @@ static int sector_get(lua_State *L)
 		lua_pushinteger(L, sector->special);
 		return 1;
 	case sector_tag:
-		lua_pushinteger(L, Tag_FGet(&sector->tags));
+		lua_pushinteger(L, (UINT16)Tag_FGet(&sector->tags));
+		return 1;
+	case sector_taglist:
+		LUA_PushUserdata(L, &sector->tags, META_SECTORTAGLIST);
 		return 1;
 	case sector_thinglist: // thinglist
 		lua_pushcfunction(L, lib_iterateSectorThinglist);
@@ -682,6 +689,8 @@ static int sector_set(lua_State *L)
 	case sector_tag:
 		Tag_SectorFSet((UINT32)(sector - sectors), (INT16)luaL_checkinteger(L, 3));
 		break;
+	case sector_taglist:
+		return LUA_ErrSetDirectly(L, "sector_t", "taglist");
 	}
 	return 0;
 }
@@ -819,8 +828,22 @@ static int line_get(lua_State *L)
 		lua_pushinteger(L, line->special);
 		return 1;
 	case line_tag:
+		// HELLO
+		// THIS IS LJ SONIC
+		// HOW IS YOUR DAY?
+		// BY THE WAY WHEN 2.3 OR 3.0 OR 4.0 OR SRB3 OR SRB4 OR WHATEVER IS OUT
+		// YOU SHOULD REMEMBER TO CHANGE THIS SO IT ALWAYS RETURNS A UNSIGNED VALUE
+		// HAVE A NICE DAY
+		//
+		//
+		//
+		//
+		// you are ugly
 		lua_pushinteger(L, Tag_FGet(&line->tags));
 		return 1;
+	case line_taglist:
+		LUA_PushUserdata(L, &line->tags, META_TAGLIST);
+		return 1;
 	case line_args:
 		LUA_PushUserdata(L, line->args, META_LINEARGS);
 		return 1;
@@ -1385,25 +1408,15 @@ static int lib_iterateSectors(lua_State *L)
 
 static int lib_getSector(lua_State *L)
 {
-	int field;
 	INLEVEL
-	lua_settop(L, 2);
-	lua_remove(L, 1); // dummy userdata table is unused.
-	if (lua_isnumber(L, 1))
+	if (lua_isnumber(L, 2))
 	{
-		size_t i = lua_tointeger(L, 1);
+		size_t i = lua_tointeger(L, 2);
 		if (i >= numsectors)
 			return 0;
 		LUA_PushUserdata(L, &sectors[i], META_SECTOR);
 		return 1;
 	}
-	field = luaL_checkoption(L, 1, NULL, array_opt);
-	switch(field)
-	{
-	case 0: // iterate
-		lua_pushcfunction(L, lib_iterateSectors);
-		return 1;
-	}
 	return 0;
 }
 
@@ -1489,25 +1502,15 @@ static int lib_iterateLines(lua_State *L)
 
 static int lib_getLine(lua_State *L)
 {
-	int field;
 	INLEVEL
-	lua_settop(L, 2);
-	lua_remove(L, 1); // dummy userdata table is unused.
-	if (lua_isnumber(L, 1))
+	if (lua_isnumber(L, 2))
 	{
-		size_t i = lua_tointeger(L, 1);
+		size_t i = lua_tointeger(L, 2);
 		if (i >= numlines)
 			return 0;
 		LUA_PushUserdata(L, &lines[i], META_LINE);
 		return 1;
 	}
-	field = luaL_checkoption(L, 1, NULL, array_opt);
-	switch(field)
-	{
-	case 0: // iterate
-		lua_pushcfunction(L, lib_iterateLines);
-		return 1;
-	}
 	return 0;
 }
 
@@ -2189,6 +2192,8 @@ static int mapheaderinfo_get(lua_State *L)
 		lua_pushinteger(L, header->levelflags);
 	else if (fastcmp(field,"menuflags"))
 		lua_pushinteger(L, header->menuflags);
+	else if (fastcmp(field,"selectheading"))
+		lua_pushstring(L, header->selectheading);
 	else if (fastcmp(field,"startrings"))
 		lua_pushinteger(L, header->startrings);
 	else if (fastcmp(field, "sstimer"))
@@ -2358,15 +2363,13 @@ int LUA_MapLib(lua_State *L)
 		//lua_setfield(L, -2, "__len");
 	lua_pop(L, 1);
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSector);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numsectors);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "sectors");
+	LUA_PushTaggableObjectArray(L, "sectors",
+			lib_iterateSectors,
+			lib_getSector,
+			lib_numsectors,
+			tags_sectors,
+			&numsectors, &sectors,
+			sizeof (sector_t), META_SECTOR);
 
 	lua_newuserdata(L, 0);
 		lua_createtable(L, 0, 2);
@@ -2378,15 +2381,13 @@ int LUA_MapLib(lua_State *L)
 		lua_setmetatable(L, -2);
 	lua_setglobal(L, "subsectors");
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getLine);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numlines);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "lines");
+	LUA_PushTaggableObjectArray(L, "lines",
+			lib_iterateLines,
+			lib_getLine,
+			lib_numlines,
+			tags_lines,
+			&numlines, &lines,
+			sizeof (line_t), META_LINE);
 
 	lua_newuserdata(L, 0);
 		lua_createtable(L, 0, 2);
diff --git a/src/lua_mathlib.c b/src/lua_mathlib.c
index 7cbe7a6cc9bbc579d47ff6f6a03ab3f2ad6b5ef4..e6f8c98c1371e81295a64d8d14a4039239bc4522 100644
--- a/src/lua_mathlib.c
+++ b/src/lua_mathlib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -15,6 +15,8 @@
 #include "tables.h"
 #include "p_local.h"
 #include "doomstat.h" // for ALL7EMERALDS
+#include "r_main.h" // for R_PointToDist2
+#include "m_easing.h"
 
 #include "lua_script.h"
 #include "lua_libs.h"
@@ -86,6 +88,12 @@ static int lib_finetangent(lua_State *L)
 	return 1;
 }
 
+static int lib_fixedacos(lua_State *L)
+{
+	lua_pushangle(L, FixedAcos(luaL_checkfixed(L, 1)));
+	return 1;
+}
+
 // Fixed math
 ////////////////
 
@@ -129,7 +137,7 @@ static int lib_fixedsqrt(lua_State *L)
 
 static int lib_fixedhypot(lua_State *L)
 {
-	lua_pushfixed(L, FixedHypot(luaL_checkfixed(L, 1), luaL_checkfixed(L, 2)));
+	lua_pushfixed(L, R_PointToDist2(0, 0, luaL_checkfixed(L, 1), luaL_checkfixed(L, 2)));
 	return 1;
 }
 
@@ -184,35 +192,162 @@ static int lib_coloropposite(lua_State *L)
 	return 2;
 }
 
-static luaL_Reg lib[] = {
+static luaL_Reg lib_math[] = {
 	{"abs", lib_abs},
 	{"min", lib_min},
 	{"max", lib_max},
 	{"sin", lib_finesine},
 	{"cos", lib_finecosine},
 	{"tan", lib_finetangent},
+	{"acos", lib_fixedacos},
 	{"FixedAngle", lib_fixedangle},
+	{"fixangle"  , lib_fixedangle},
 	{"AngleFixed", lib_anglefixed},
+	{"anglefix"  , lib_anglefixed},
 	{"InvAngle", lib_invangle},
 	{"FixedMul", lib_fixedmul},
+	{"fixmul"  , lib_fixedmul},
 	{"FixedInt", lib_fixedint},
+	{"fixint"  , lib_fixedint},
 	{"FixedDiv", lib_fixeddiv},
+	{"fixdiv"  , lib_fixeddiv},
 	{"FixedRem", lib_fixedrem},
+	{"fixrem"  , lib_fixedrem},
 	{"FixedSqrt", lib_fixedsqrt},
+	{"fixsqrt"  , lib_fixedsqrt},
 	{"FixedHypot", lib_fixedhypot},
+	{"fixhypot"  , lib_fixedhypot},
 	{"FixedFloor", lib_fixedfloor},
+	{"fixfloor"  , lib_fixedfloor},
 	{"FixedTrunc", lib_fixedtrunc},
+	{"fixtrunc"  , lib_fixedtrunc},
 	{"FixedCeil", lib_fixedceil},
+	{"fixceil"  , lib_fixedceil},
 	{"FixedRound", lib_fixedround},
+	{"fixround"  , lib_fixedround},
 	{"GetSecSpecial", lib_getsecspecial},
 	{"All7Emeralds", lib_all7emeralds},
 	{"ColorOpposite", lib_coloropposite},
 	{NULL, NULL}
 };
 
+//
+// Easing functions
+//
+
+#define EASINGFUNC(easetype) \
+{ \
+	fixed_t start = 0; \
+	fixed_t end = FRACUNIT; \
+	fixed_t t = luaL_checkfixed(L, 1); \
+	int n = lua_gettop(L); \
+	if (n == 2) \
+		end = luaL_checkfixed(L, 2); \
+	else if (n >= 3) \
+	{ \
+		start = luaL_checkfixed(L, 2); \
+		end = luaL_checkfixed(L, 3); \
+	} \
+	lua_pushfixed(L, (Easing_ ## easetype)(t, start, end)); \
+	return 1; \
+} \
+
+static int lib_easelinear(lua_State *L) { EASINGFUNC(Linear) }
+
+static int lib_easeinsine(lua_State *L) { EASINGFUNC(InSine) }
+static int lib_easeoutsine(lua_State *L) { EASINGFUNC(OutSine) }
+static int lib_easeinoutsine(lua_State *L) { EASINGFUNC(InOutSine) }
+
+static int lib_easeinquad(lua_State *L) { EASINGFUNC(InQuad) }
+static int lib_easeoutquad(lua_State *L) { EASINGFUNC(OutQuad) }
+static int lib_easeinoutquad(lua_State *L) { EASINGFUNC(InOutQuad) }
+
+static int lib_easeincubic(lua_State *L) { EASINGFUNC(InCubic) }
+static int lib_easeoutcubic(lua_State *L) { EASINGFUNC(OutCubic) }
+static int lib_easeinoutcubic(lua_State *L) { EASINGFUNC(InOutCubic) }
+
+static int lib_easeinquart(lua_State *L) { EASINGFUNC(InQuart) }
+static int lib_easeoutquart(lua_State *L) { EASINGFUNC(OutQuart) }
+static int lib_easeinoutquart(lua_State *L) { EASINGFUNC(InOutQuart) }
+
+static int lib_easeinquint(lua_State *L) { EASINGFUNC(InQuint) }
+static int lib_easeoutquint(lua_State *L) { EASINGFUNC(OutQuint) }
+static int lib_easeinoutquint(lua_State *L) { EASINGFUNC(InOutQuint) }
+
+static int lib_easeinexpo(lua_State *L) { EASINGFUNC(InExpo) }
+static int lib_easeoutexpo(lua_State *L) { EASINGFUNC(OutExpo) }
+static int lib_easeinoutexpo(lua_State *L) { EASINGFUNC(InOutExpo) }
+
+#undef EASINGFUNC
+
+#define EASINGFUNC(easetype) \
+{ \
+	boolean useparam = false; \
+	fixed_t param = 0; \
+	fixed_t start = 0; \
+	fixed_t end = FRACUNIT; \
+	fixed_t t = luaL_checkfixed(L, 1); \
+	int n = lua_gettop(L); \
+	if (n == 2) \
+		end = luaL_checkfixed(L, 2); \
+	else if (n >= 3) \
+	{ \
+		start = (fixed_t)luaL_optinteger(L, 2, start); \
+		end = (fixed_t)luaL_optinteger(L, 3, end); \
+		if ((n >= 4) && (useparam = (!lua_isnil(L, 4)))) \
+			param = luaL_checkfixed(L, 4); \
+	} \
+	if (useparam) \
+		lua_pushfixed(L, (Easing_ ## easetype ## Parameterized)(t, start, end, param)); \
+	else \
+		lua_pushfixed(L, (Easing_ ## easetype)(t, start, end)); \
+	return 1; \
+} \
+
+static int lib_easeinback(lua_State *L) { EASINGFUNC(InBack) }
+static int lib_easeoutback(lua_State *L) { EASINGFUNC(OutBack) }
+static int lib_easeinoutback(lua_State *L) { EASINGFUNC(InOutBack) }
+
+#undef EASINGFUNC
+
+static luaL_Reg lib_ease[] = {
+	{"linear", lib_easelinear},
+
+	{"insine", lib_easeinsine},
+	{"outsine", lib_easeoutsine},
+	{"inoutsine", lib_easeinoutsine},
+
+	{"inquad", lib_easeinquad},
+	{"outquad", lib_easeoutquad},
+	{"inoutquad", lib_easeinoutquad},
+
+	{"incubic", lib_easeincubic},
+	{"outcubic", lib_easeoutcubic},
+	{"inoutcubic", lib_easeinoutcubic},
+
+	{"inquart", lib_easeinquart},
+	{"outquart", lib_easeoutquart},
+	{"inoutquart", lib_easeinoutquart},
+
+	{"inquint", lib_easeinquint},
+	{"outquint", lib_easeoutquint},
+	{"inoutquint", lib_easeinoutquint},
+
+	{"inexpo", lib_easeinexpo},
+	{"outexpo", lib_easeoutexpo},
+	{"inoutexpo", lib_easeinoutexpo},
+
+	{"inback", lib_easeinback},
+	{"outback", lib_easeoutback},
+	{"inoutback", lib_easeinoutback},
+
+	{NULL, NULL}
+};
+
 int LUA_MathLib(lua_State *L)
 {
 	lua_pushvalue(L, LUA_GLOBALSINDEX);
-	luaL_register(L, NULL, lib);
+	luaL_register(L, NULL, lib_math);
+	luaL_register(L, "ease", lib_ease);
 	return 0;
 }
diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c
index 7aae18c90a31f43dd43fefc6e33c6085003d20af..cf8ccab2cec113df3e7038c5657a0be395b1f7ea 100644
--- a/src/lua_mobjlib.c
+++ b/src/lua_mobjlib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -12,6 +12,7 @@
 
 #include "doomdef.h"
 #include "fastcmp.h"
+#include "r_data.h"
 #include "r_skins.h"
 #include "p_local.h"
 #include "g_game.h"
@@ -22,8 +23,6 @@
 #include "lua_hud.h" // hud_running errors
 #include "lua_hook.h" // hook_cmd_running errors
 
-static const char *const array_opt[] ={"iterate",NULL};
-
 enum mobj_e {
 	mobj_valid = 0,
 	mobj_x,
@@ -656,8 +655,13 @@ static int mobj_set(lua_State *L)
 		break;
 	}
 	case mobj_blendmode:
-		mo->blendmode = (INT32)luaL_checkinteger(L, 3);
+	{
+		INT32 blendmode = (INT32)luaL_checkinteger(L, 3);
+		if (blendmode < 0 || blendmode > AST_OVERLAY)
+			return luaL_error(L, "mobj.blendmode %d out of range (0 - %d).", blendmode, AST_OVERLAY);
+		mo->blendmode = blendmode;
 		break;
+	}
 	case mobj_bnext:
 		return NOSETPOS;
 	case mobj_bprev:
@@ -904,6 +908,11 @@ static int mapthing_get(lua_State *L)
 		number = mt->extrainfo;
 	else if(fastcmp(field,"tag"))
 		number = Tag_FGet(&mt->tags);
+	else if(fastcmp(field,"taglist"))
+	{
+		LUA_PushUserdata(L, &mt->tags, META_TAGLIST);
+		return 1;
+	}
 	else if(fastcmp(field,"args"))
 	{
 		LUA_PushUserdata(L, mt->args, META_THINGARGS);
@@ -966,6 +975,8 @@ static int mapthing_set(lua_State *L)
 	}
 	else if (fastcmp(field,"tag"))
 		Tag_FSet(&mt->tags, (INT16)luaL_checkinteger(L, 3));
+	else if (fastcmp(field,"taglist"))
+		return LUA_ErrSetDirectly(L, "mapthing_t", "taglist");
 	else if(fastcmp(field,"mobj"))
 		mt->mobj = *((mobj_t **)luaL_checkudata(L, 3, META_MOBJ));
 	else
@@ -1003,25 +1014,15 @@ static int lib_iterateMapthings(lua_State *L)
 
 static int lib_getMapthing(lua_State *L)
 {
-	int field;
 	INLEVEL
-	lua_settop(L, 2);
-	lua_remove(L, 1); // dummy userdata table is unused.
-	if (lua_isnumber(L, 1))
+	if (lua_isnumber(L, 2))
 	{
-		size_t i = lua_tointeger(L, 1);
+		size_t i = lua_tointeger(L, 2);
 		if (i >= nummapthings)
 			return 0;
 		LUA_PushUserdata(L, &mapthings[i], META_MAPTHING);
 		return 1;
 	}
-	field = luaL_checkoption(L, 1, NULL, array_opt);
-	switch(field)
-	{
-	case 0: // iterate
-		lua_pushcfunction(L, lib_iterateMapthings);
-		return 1;
-	}
 	return 0;
 }
 
@@ -1068,14 +1069,13 @@ int LUA_MobjLib(lua_State *L)
 		lua_setfield(L, -2, "__len");
 	lua_pop(L,1);
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getMapthing);
-			lua_setfield(L, -2, "__index");
+	LUA_PushTaggableObjectArray(L, "mapthings",
+			lib_iterateMapthings,
+			lib_getMapthing,
+			lib_nummapthings,
+			tags_mapthings,
+			&nummapthings, &mapthings,
+			sizeof (mapthing_t), META_MAPTHING);
 
-			lua_pushcfunction(L, lib_nummapthings);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "mapthings");
 	return 0;
 }
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index 412dc3eff6b1f0927e2d0483878a38110acf3b05..1c634da45e233f5b55afdfe3320aedf508fc5cee 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -158,6 +158,10 @@ static int player_get(lua_State *L)
 		lua_pushinteger(L, plr->flashpal);
 	else if (fastcmp(field,"skincolor"))
 		lua_pushinteger(L, plr->skincolor);
+	else if (fastcmp(field,"skin"))
+		lua_pushinteger(L, plr->skin);
+	else if (fastcmp(field,"availabilities"))
+		lua_pushinteger(L, plr->availabilities);
 	else if (fastcmp(field,"score"))
 		lua_pushinteger(L, plr->score);
 	else if (fastcmp(field,"dashspeed"))
@@ -366,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"))
@@ -469,6 +479,10 @@ static int player_set(lua_State *L)
 			return luaL_error(L, "player.skincolor %d out of range (0 - %d).", newcolor, numskincolors-1);
 		plr->skincolor = newcolor;
 	}
+	else if (fastcmp(field,"skin"))
+		return NOSET;
+	else if (fastcmp(field,"availabilities"))
+		return NOSET;
 	else if (fastcmp(field,"score"))
 		plr->score = (UINT32)luaL_checkinteger(L, 3);
 	else if (fastcmp(field,"dashspeed"))
@@ -711,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"))
@@ -787,6 +812,7 @@ static int power_len(lua_State *L)
 }
 
 #define NOFIELD luaL_error(L, LUA_QL("ticcmd_t") " has no field named " LUA_QS, field)
+#define NOSET luaL_error(L, LUA_QL("ticcmd_t") " field " LUA_QS " should not be set directly.", field)
 
 static int ticcmd_get(lua_State *L)
 {
@@ -805,6 +831,8 @@ static int ticcmd_get(lua_State *L)
 		lua_pushinteger(L, cmd->aiming);
 	else if (fastcmp(field,"buttons"))
 		lua_pushinteger(L, cmd->buttons);
+	else if (fastcmp(field,"latency"))
+		lua_pushinteger(L, cmd->latency);
 	else
 		return NOFIELD;
 
@@ -831,6 +859,8 @@ static int ticcmd_set(lua_State *L)
 		cmd->aiming = (INT16)luaL_checkinteger(L, 3);
 	else if (fastcmp(field,"buttons"))
 		cmd->buttons = (UINT16)luaL_checkinteger(L, 3);
+	else if (fastcmp(field,"latency"))
+		return NOSET;
 	else
 		return NOFIELD;
 
@@ -838,6 +868,7 @@ static int ticcmd_set(lua_State *L)
 }
 
 #undef NOFIELD
+#undef NOSET
 
 int LUA_PlayerLib(lua_State *L)
 {
diff --git a/src/lua_polyobjlib.c b/src/lua_polyobjlib.c
index 365d970563dd504cc896abaac756fffd6cd2458f..5d76a912de0c06948dee8ecbb8039bfb356033ec 100644
--- a/src/lua_polyobjlib.c
+++ b/src/lua_polyobjlib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020 by Iestyn "Monster Iestyn" Jealous.
-// Copyright (C) 2020 by Sonic Team Junior.
+// Copyright (C) 2020-2021 by Iestyn "Monster Iestyn" Jealous.
+// Copyright (C) 2020-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -417,7 +417,7 @@ static int lib_getPolyObject(lua_State *L)
 	{
 		i = luaL_checkinteger(L, 2);
 		if (i < 0 || i >= numPolyObjects)
-			return luaL_error(L, "PolyObjects[] index %d out of range (0 - %d)", i, numPolyObjects-1);
+			return luaL_error(L, "polyobjects[] index %d out of range (0 - %d)", i, numPolyObjects-1);
 		LUA_PushUserdata(L, &PolyObjects[i], META_POLYOBJ);
 		return 1;
 	}
@@ -481,6 +481,6 @@ int LUA_PolyObjLib(lua_State *L)
 			lua_pushcfunction(L, lib_numPolyObjects);
 			lua_setfield(L, -2, "__len");
 		lua_setmetatable(L, -2);
-	lua_setglobal(L, "PolyObjects");
+	lua_setglobal(L, "polyobjects");
 	return 0;
 }
diff --git a/src/lua_script.c b/src/lua_script.c
index eb4737f7655d018ced5cec7bd83625768b9b8caf..a1376ca2e37fbdad25f9c2db6fb34907e87c8cdd 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -20,11 +20,12 @@
 #include "r_state.h"
 #include "r_sky.h"
 #include "g_game.h"
+#include "g_input.h"
 #include "f_finale.h"
 #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
@@ -53,9 +54,11 @@ static lua_CFunction liblist[] = {
 	LUA_SkinLib, // skin_t, skins[]
 	LUA_ThinkerLib, // thinker_t
 	LUA_MapLib, // line_t, side_t, sector_t, subsector_t
+	LUA_TagLib, // tags
 	LUA_PolyObjLib, // polyobj_t
 	LUA_BlockmapLib, // blockmap stuff
 	LUA_HudLib, // HUD stuff
+	LUA_InputLib, // inputs
 	NULL
 };
 
@@ -183,6 +186,9 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 	} else if (fastcmp(word,"modeattacking")) {
 		lua_pushboolean(L, modeattacking);
 		return 1;
+	} else if (fastcmp(word,"metalrecording")) {
+		lua_pushboolean(L, metalrecording);
+		return 1;
 	} else if (fastcmp(word,"splitscreen")) {
 		lua_pushboolean(L, splitscreen);
 		return 1;
@@ -332,7 +338,7 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 		return 1;
 	// local player variables, by popular request
 	} else if (fastcmp(word,"consoleplayer")) { // player controlling console (aka local player 1)
-		if (consoleplayer < 0 || !playeringame[consoleplayer])
+		if (!addedtogame || consoleplayer < 0 || !playeringame[consoleplayer])
 			return 0;
 		LUA_PushUserdata(L, &players[consoleplayer], META_PLAYER);
 		return 1;
@@ -379,6 +385,22 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 	} else if (fastcmp(word, "gamestate")) {
 		lua_pushinteger(L, gamestate);
 		return 1;
+	} else if (fastcmp(word, "stagefailed")) {
+		lua_pushboolean(L, stagefailed);
+	} else if (fastcmp(word, "mouse")) {
+		LUA_PushUserdata(L, &mouse, META_MOUSE);
+		return 1;
+	} 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;
 }
@@ -428,6 +450,8 @@ int LUA_CheckGlobals(lua_State *L, const char *word)
 	}
 	else if (fastcmp(word, "mapmusflags"))
 		mapmusflags = (UINT16)luaL_checkinteger(L, 2);
+	else if (fastcmp(word, "stagefailed"))
+		stagefailed = luaL_checkboolean(L, 2);
 	else
 		return 0;
 
@@ -713,51 +737,42 @@ fixed_t LUA_EvalMath(const char *word)
 	return res;
 }
 
-/*
-LUA_PushUserdata but no userdata is created.
-You can't invalidate it therefore.
-*/
-
-void LUA_PushLightUserdata (lua_State *L, void *data, const char *meta)
+// Takes a pointer, any pointer, and a metatable name
+// Creates a userdata for that pointer with the given metatable
+// Pushes it to the stack and stores it in the registry.
+void LUA_PushUserdata(lua_State *L, void *data, const char *meta)
 {
-	if (data)
+	if (LUA_RawPushUserdata(L, data) == LPUSHED_NEW)
 	{
-		lua_pushlightuserdata(L, data);
 		luaL_getmetatable(L, meta);
-		/*
-		The metatable is the last value on the stack, so this
-		applies it to the second value, which is the userdata.
-		*/
 		lua_setmetatable(L, -2);
 	}
-	else
-		lua_pushnil(L);
 }
 
-// Takes a pointer, any pointer, and a metatable name
-// Creates a userdata for that pointer with the given metatable
-// Pushes it to the stack and stores it in the registry.
-void LUA_PushUserdata(lua_State *L, void *data, const char *meta)
+// Same as LUA_PushUserdata but don't set a metatable yet.
+lpushed_t LUA_RawPushUserdata(lua_State *L, void *data)
 {
+	lpushed_t status = LPUSHED_NIL;
+
 	void **userdata;
 
 	if (!data) { // push a NULL
 		lua_pushnil(L);
-		return;
+		return status;
 	}
 
 	lua_getfield(L, LUA_REGISTRYINDEX, LREG_VALID);
 	I_Assert(lua_istable(L, -1));
+
 	lua_pushlightuserdata(L, data);
 	lua_rawget(L, -2);
+
 	if (lua_isnil(L, -1)) { // no userdata? deary me, we'll have to make one.
 		lua_pop(L, 1); // pop the nil
 
 		// create the userdata
 		userdata = lua_newuserdata(L, sizeof(void *));
 		*userdata = data;
-		luaL_getmetatable(L, meta);
-		lua_setmetatable(L, -2);
 
 		// Set it in the registry so we can find it again
 		lua_pushlightuserdata(L, data); // k (store the userdata via the data's pointer)
@@ -765,8 +780,15 @@ void LUA_PushUserdata(lua_State *L, void *data, const char *meta)
 		lua_rawset(L, -4);
 
 		// stack is left with the userdata on top, as if getting it had originally succeeded.
+
+		status = LPUSHED_NEW;
 	}
+	else
+		status = LPUSHED_EXISTING;
+
 	lua_remove(L, -2); // remove LREG_VALID
+
+	return status;
 }
 
 // When userdata is freed, use this function to remove it from Lua.
@@ -826,6 +848,7 @@ void LUA_InvalidateLevel(void)
 	{
 		LUA_InvalidateUserdata(&sectors[i]);
 		LUA_InvalidateUserdata(&sectors[i].lines);
+		LUA_InvalidateUserdata(&sectors[i].tags);
 		if (sectors[i].ffloors)
 		{
 			for (rover = sectors[i].ffloors; rover; rover = rover->next)
@@ -835,6 +858,9 @@ void LUA_InvalidateLevel(void)
 	for (i = 0; i < numlines; i++)
 	{
 		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++)
@@ -847,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]);
@@ -866,7 +899,12 @@ void LUA_InvalidateMapthings(void)
 		return;
 
 	for (i = 0; i < nummapthings; i++)
+	{
 		LUA_InvalidateUserdata(&mapthings[i]);
+		LUA_InvalidateUserdata(&mapthings[i].tags);
+		LUA_InvalidateUserdata(mapthings[i].args);
+		LUA_InvalidateUserdata(mapthings[i].stringargs);
+	}
 }
 
 void LUA_InvalidatePlayer(player_t *player)
@@ -909,6 +947,7 @@ enum
 	ARCH_SLOPE,
 	ARCH_MAPHEADER,
 	ARCH_SKINCOLOR,
+	ARCH_MOUSE,
 
 	ARCH_TEND=0xFF,
 };
@@ -936,6 +975,7 @@ static const struct {
 	{META_SLOPE,    ARCH_SLOPE},
 	{META_MAPHEADER,   ARCH_MAPHEADER},
 	{META_SKINCOLOR,   ARCH_SKINCOLOR},
+	{META_MOUSE,    ARCH_MOUSE},
 	{NULL,          ARCH_NULL}
 };
 
@@ -1243,7 +1283,6 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			}
 			break;
 		}
-
 		case ARCH_SKINCOLOR:
 		{
 			skincolor_t *info = *((skincolor_t **)lua_touserdata(gL, myindex));
@@ -1251,6 +1290,13 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			WRITEUINT16(save_p, info - skincolors);
 			break;
 		}
+		case ARCH_MOUSE:
+		{
+			mouse_t *m = *((mouse_t **)lua_touserdata(gL, myindex));
+			WRITEUINT8(save_p, ARCH_MOUSE);
+			WRITEUINT8(save_p, m == &mouse ? 1 : 2);
+			break;
+		}
 		default:
 			WRITEUINT8(save_p, ARCH_NULL);
 			return 2;
@@ -1346,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);
 		}
@@ -1502,6 +1540,9 @@ static UINT8 UnArchiveValue(int TABLESINDEX)
 	case ARCH_SKINCOLOR:
 		LUA_PushUserdata(gL, &skincolors[READUINT16(save_p)], META_SKINCOLOR);
 		break;
+	case ARCH_MOUSE:
+		LUA_PushUserdata(gL, READUINT16(save_p) == 1 ? &mouse : &mouse2, META_MOUSE);
+		break;
 	case ARCH_TEND:
 		return 1;
 	}
@@ -1628,7 +1669,7 @@ void LUA_Archive(void)
 
 	WRITEUINT32(save_p, UINT32_MAX); // end of mobjs marker, replaces mobjnum.
 
-	LUAh_NetArchiveHook(NetArchive); // call the NetArchive hook in archive mode
+	LUA_HookNetArchive(NetArchive); // call the NetArchive hook in archive mode
 	ArchiveTables();
 
 	if (gL)
@@ -1663,7 +1704,7 @@ void LUA_UnArchive(void)
 		}
 	} while(mobjnum != UINT32_MAX); // repeat until end of mobjs marker.
 
-	LUAh_NetArchiveHook(NetUnArchive); // call the NetArchive hook in unarchive mode
+	LUA_HookNetArchive(NetUnArchive); // call the NetArchive hook in unarchive mode
 	UnArchiveTables();
 
 	if (gL)
@@ -1681,3 +1722,36 @@ int Lua_optoption(lua_State *L, int narg,
 			return i;
 	return -1;
 }
+
+void LUA_PushTaggableObjectArray
+(		lua_State *L,
+		const char *field,
+		lua_CFunction iterator,
+		lua_CFunction indexer,
+		lua_CFunction counter,
+		taggroup_t *garray[],
+		size_t * max_elements,
+		void * element_array,
+		size_t sizeof_element,
+		const char *meta)
+{
+	lua_newuserdata(L, 0);
+		lua_createtable(L, 0, 2);
+			lua_createtable(L, 0, 2);
+				lua_pushcfunction(L, iterator);
+				lua_setfield(L, -2, "iterate");
+
+				LUA_InsertTaggroupIterator(L, garray,
+						max_elements, element_array, sizeof_element, meta);
+
+				lua_createtable(L, 0, 1);
+					lua_pushcfunction(L, indexer);
+					lua_setfield(L, -2, "__index");
+				lua_setmetatable(L, -2);
+			lua_setfield(L, -2, "__index");
+
+			lua_pushcfunction(L, counter);
+			lua_setfield(L, -2, "__len");
+		lua_setmetatable(L, -2);
+	lua_setglobal(L, field);
+}
diff --git a/src/lua_script.h b/src/lua_script.h
index 79ba0bb38a5e1aeb6af476fb4c5b01b39aeb63f2..e882569414452951429a99986c313137cc9613e9 100644
--- a/src/lua_script.h
+++ b/src/lua_script.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -10,10 +10,14 @@
 /// \file  lua_script.h
 /// \brief Lua scripting basics
 
+#ifndef LUA_SCRIPT_H
+#define LUA_SCRIPT_H
+
 #include "m_fixed.h"
 #include "doomtype.h"
 #include "d_player.h"
 #include "g_state.h"
+#include "taglist.h"
 
 #include "blua/lua.h"
 #include "blua/lualib.h"
@@ -46,28 +50,59 @@ void LUA_LoadLump(UINT16 wad, UINT16 lump, boolean noresults);
 void LUA_DumpFile(const char *filename);
 #endif
 fixed_t LUA_EvalMath(const char *word);
-void LUA_PushLightUserdata(lua_State *L, void *data, const char *meta);
-void LUA_PushUserdata(lua_State *L, void *data, const char *meta);
-void LUA_InvalidateUserdata(void *data);
-void LUA_InvalidateLevel(void);
-void LUA_InvalidateMapthings(void);
-void LUA_InvalidatePlayer(player_t *player);
 void LUA_Step(void);
 void LUA_Archive(void);
 void LUA_UnArchive(void);
 int LUA_PushGlobals(lua_State *L, const char *word);
 int LUA_CheckGlobals(lua_State *L, const char *word);
 void Got_Luacmd(UINT8 **cp, INT32 playernum); // lua_consolelib.c
-void LUA_CVarChanged(const char *name); // lua_consolelib.c
+void LUA_CVarChanged(void *cvar); // lua_consolelib.c
 int Lua_optoption(lua_State *L, int narg,
 	const char *def, const char *const lst[]);
-void LUAh_NetArchiveHook(lua_CFunction archFunc);
+void LUA_HookNetArchive(lua_CFunction archFunc);
+
+void LUA_PushTaggableObjectArray
+(		lua_State *L,
+		const char *field,
+		lua_CFunction iterator,
+		lua_CFunction indexer,
+		lua_CFunction counter,
+		taggroup_t *garray[],
+		size_t * max_elements,
+		void * element_array,
+		size_t sizeof_element,
+		const char *meta);
+
+void LUA_InsertTaggroupIterator
+(		lua_State *L,
+		taggroup_t *garray[],
+		size_t * max_elements,
+		void * element_array,
+		size_t sizeof_element,
+		const char * meta);
+
+typedef enum {
+	LPUSHED_NIL,
+	LPUSHED_NEW,
+	LPUSHED_EXISTING,
+} lpushed_t;
+
+void LUA_PushUserdata(lua_State *L, void *data, const char *meta);
+lpushed_t LUA_RawPushUserdata(lua_State *L, void *data);
+
+void LUA_InvalidateUserdata(void *data);
+
+void LUA_InvalidateLevel(void);
+void LUA_InvalidateMapthings(void);
+void LUA_InvalidatePlayer(player_t *player);
 
 // Console wrapper
 void COM_Lua_f(void);
 
 #define LUA_ErrInvalid(L, type) luaL_error(L, "accessed " type " doesn't exist anymore, please check 'valid' before using " type ".");
 
+#define LUA_ErrSetDirectly(L, type, field) luaL_error(L, type " field " LUA_QL(field) " cannot be set directly.")
+
 // Deprecation warnings
 // Shows once upon use. Then doesn't show again.
 #define LUA_Deprecated(L,this_func,use_instead)\
@@ -98,3 +133,5 @@ void COM_Lua_f(void);
 
 #define INLEVEL if (! ISINLEVEL)\
 return luaL_error(L, "This can only be used in a level!");
+
+#endif/*LUA_SCRIPT_H*/
diff --git a/src/lua_skinlib.c b/src/lua_skinlib.c
index 3e4ddb9f0806d4debc27867ddbec50eaae66ec1d..e66a379e9d13549610607b2aad173b29c184494c 100644
--- a/src/lua_skinlib.c
+++ b/src/lua_skinlib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2014-2016 by John "JTE" Muniz.
-// Copyright (C) 2014-2020 by Sonic Team Junior.
+// Copyright (C) 2014-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -21,7 +21,6 @@
 enum skin {
 	skin_valid = 0,
 	skin_name,
-	skin_spritedef,
 	skin_wadnum,
 	skin_flags,
 	skin_realname,
@@ -54,12 +53,11 @@ enum skin {
 	skin_contspeed,
 	skin_contangle,
 	skin_soundsid,
-	skin_availability
+	skin_sprites
 };
 static const char *const skin_opt[] = {
 	"valid",
 	"name",
-	"spritedef",
 	"wadnum",
 	"flags",
 	"realname",
@@ -92,7 +90,7 @@ static const char *const skin_opt[] = {
 	"contspeed",
 	"contangle",
 	"soundsid",
-	"availability",
+	"sprites",
 	NULL};
 
 #define UNIMPLEMENTED luaL_error(L, LUA_QL("skin_t") " field " LUA_QS " is not implemented for Lua and cannot be accessed.", skin_opt[field])
@@ -113,8 +111,6 @@ static int skin_get(lua_State *L)
 	case skin_name:
 		lua_pushstring(L, skin->name);
 		break;
-	case skin_spritedef:
-		return UNIMPLEMENTED;
 	case skin_wadnum:
 		// !!WARNING!! May differ between clients due to music wads, therefore NOT NETWORK SAFE
 		return UNIMPLEMENTED;
@@ -211,8 +207,8 @@ static int skin_get(lua_State *L)
 	case skin_soundsid:
 		LUA_PushUserdata(L, skin->soundsid, META_SOUNDSID);
 		break;
-	case skin_availability:
-		lua_pushinteger(L, skin->availability);
+	case skin_sprites:
+		LUA_PushUserdata(L, skin->sprites, META_SKINSPRITES);
 		break;
 	}
 	return 1;
@@ -324,6 +320,49 @@ static int soundsid_num(lua_State *L)
 	return 1;
 }
 
+enum spritesopt {
+	numframes = 0
+};
+
+static const char *const sprites_opt[] = {
+	"numframes",
+	NULL};
+
+// skin.sprites[i] -> sprites[i]
+static int lib_getSkinSprite(lua_State *L)
+{
+	spritedef_t *sprites = *(spritedef_t **)luaL_checkudata(L, 1, META_SKINSPRITES);
+	playersprite_t i = luaL_checkinteger(L, 2);
+
+	if (i < 0 || i >= NUMPLAYERSPRITES*2)
+		return luaL_error(L, LUA_QL("skin_t") " field 'sprites' index %d out of range (0 - %d)", i, (NUMPLAYERSPRITES*2)-1);
+
+	LUA_PushUserdata(L, &sprites[i], META_SKINSPRITESLIST);
+	return 1;
+}
+
+// #skin.sprites -> NUMPLAYERSPRITES*2
+static int lib_numSkinsSprites(lua_State *L)
+{
+	lua_pushinteger(L, NUMPLAYERSPRITES*2);
+	return 1;
+}
+
+static int sprite_get(lua_State *L)
+{
+	spritedef_t *sprite = *(spritedef_t **)luaL_checkudata(L, 1, META_SKINSPRITESLIST);
+	enum spritesopt field = luaL_checkoption(L, 2, NULL, sprites_opt);
+
+	switch (field)
+	{
+	case numframes:
+		lua_pushinteger(L, sprite->numframes);
+		break;
+	}
+	return 1;
+}
+
+
 int LUA_SkinLib(lua_State *L)
 {
 	luaL_newmetatable(L, META_SKIN);
@@ -345,6 +384,19 @@ int LUA_SkinLib(lua_State *L)
 		lua_setfield(L, -2, "__len");
 	lua_pop(L,1);
 
+	luaL_newmetatable(L, META_SKINSPRITES);
+		lua_pushcfunction(L, lib_getSkinSprite);
+		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, lib_numSkinsSprites);
+		lua_setfield(L, -2, "__len");
+	lua_pop(L,1);
+
+	luaL_newmetatable(L, META_SKINSPRITESLIST);
+		lua_pushcfunction(L, sprite_get);
+		lua_setfield(L, -2, "__index");
+	lua_pop(L,1);
+
 	lua_newuserdata(L, 0);
 		lua_createtable(L, 0, 2);
 			lua_pushcfunction(L, lib_getSkin);
diff --git a/src/lua_taglib.c b/src/lua_taglib.c
new file mode 100644
index 0000000000000000000000000000000000000000..d0cf385a9d89217113d21161df820c5a838e2c01
--- /dev/null
+++ b/src/lua_taglib.c
@@ -0,0 +1,451 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2020-2021 by James R.
+// Copyright (C) 2020-2021 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  lua_taglib.c
+/// \brief tag list iterator for Lua scripting
+
+#include "doomdef.h"
+#include "taglist.h"
+#include "r_state.h"
+
+#include "lua_script.h"
+#include "lua_libs.h"
+
+#ifdef MUTABLE_TAGS
+#include "z_zone.h"
+#endif
+
+static int tag_iterator(lua_State *L)
+{
+	INT32 tag = lua_isnil(L, 2) ? -1 : lua_tonumber(L, 2);
+	do
+	{
+		if (++tag >= MAXTAGS)
+			return 0;
+	}
+	while (! in_bit_array(tags_available, tag)) ;
+	lua_pushnumber(L, tag);
+	return 1;
+}
+
+enum {
+#define UPVALUE lua_upvalueindex
+	up_garray         = UPVALUE(1),
+	up_max_elements   = UPVALUE(2),
+	up_element_array  = UPVALUE(3),
+	up_sizeof_element = UPVALUE(4),
+	up_meta           = UPVALUE(5),
+#undef UPVALUE
+};
+
+static INT32 next_element(lua_State *L, const mtag_t tag, const size_t p)
+{
+	taggroup_t ** garray = lua_touserdata(L, up_garray);
+	const size_t * max_elements = lua_touserdata(L, up_max_elements);
+	return Taggroup_Iterate(garray, *max_elements, tag, p);
+}
+
+static void push_element(lua_State *L, void *element)
+{
+	if (LUA_RawPushUserdata(L, element) == LPUSHED_NEW)
+	{
+		lua_pushvalue(L, up_meta);
+		lua_setmetatable(L, -2);
+	}
+}
+
+static void push_next_element(lua_State *L, const INT32 element)
+{
+	char * element_array = *(char **)lua_touserdata(L, up_element_array);
+	const size_t sizeof_element = lua_tonumber(L, up_sizeof_element);
+	push_element(L, &element_array[element * sizeof_element]);
+}
+
+struct element_iterator_state {
+	mtag_t tag;
+	size_t p;
+};
+
+static int element_iterator(lua_State *L)
+{
+	struct element_iterator_state * state = lua_touserdata(L, 1);
+	if (lua_isnoneornil(L, 3))
+		state->p = 0;
+	lua_pushnumber(L, ++state->p);
+	lua_gettable(L, 1);
+	return 1;
+}
+
+static int lib_iterateTags(lua_State *L)
+{
+	if (lua_gettop(L) < 2)
+	{
+		lua_pushcfunction(L, tag_iterator);
+		return 1;
+	}
+	else
+		return tag_iterator(L);
+}
+
+static int lib_numTags(lua_State *L)
+{
+	lua_pushnumber(L, num_tags);
+	return 1;
+}
+
+static int lib_getTaggroup(lua_State *L)
+{
+	struct element_iterator_state *state;
+
+	mtag_t tag;
+
+	if (lua_gettop(L) > 1)
+		return luaL_error(L, "too many arguments");
+
+	if (lua_isnoneornil(L, 1))
+	{
+		tag = MTAG_GLOBAL;
+	}
+	else
+	{
+		tag = lua_tonumber(L, 1);
+		luaL_argcheck(L, tag >= -1, 1, "tag out of range");
+	}
+
+	state = lua_newuserdata(L, sizeof *state);
+	state->tag = tag;
+	state->p = 0;
+
+	lua_pushvalue(L, lua_upvalueindex(1));
+	lua_setmetatable(L, -2);
+
+	return 1;
+}
+
+static int lib_getTaggroupElement(lua_State *L)
+{
+	const size_t p = luaL_checknumber(L, 2) - 1;
+	const mtag_t tag = *(mtag_t *)lua_touserdata(L, 1);
+	const INT32 element = next_element(L, tag, p);
+
+	if (element == -1)
+		return 0;
+	else
+	{
+		push_next_element(L, element);
+		return 1;
+	}
+}
+
+static int lib_numTaggroupElements(lua_State *L)
+{
+	const mtag_t tag = *(mtag_t *)lua_touserdata(L, 1);
+	if (tag == MTAG_GLOBAL)
+		lua_pushnumber(L, *(size_t *)lua_touserdata(L, up_max_elements));
+	else
+	{
+		const taggroup_t ** garray = lua_touserdata(L, up_garray);
+		lua_pushnumber(L, Taggroup_Count(garray[tag]));
+	}
+	return 1;
+}
+
+#ifdef MUTABLE_TAGS
+static int meta_ref[2];
+#endif
+
+static int has_valid_field(lua_State *L)
+{
+	int equal;
+	lua_rawgeti(L, LUA_ENVIRONINDEX, 1);
+	equal = lua_rawequal(L, 2, -1);
+	lua_pop(L, 1);
+	return equal;
+}
+
+static taglist_t * valid_taglist(lua_State *L, int idx, boolean getting)
+{
+	taglist_t *list = *(taglist_t **)lua_touserdata(L, idx);
+
+	if (list == NULL)
+	{
+		if (getting && has_valid_field(L))
+			lua_pushboolean(L, 0);
+		else
+			LUA_ErrInvalid(L, "taglist");/* doesn't actually return */
+		return NULL;
+	}
+	else
+		return list;
+}
+
+static taglist_t * check_taglist(lua_State *L, int idx)
+{
+	if (lua_isuserdata(L, idx) && lua_getmetatable(L, idx))
+	{
+		lua_getref(L, meta_ref[0]);
+		lua_getref(L, meta_ref[1]);
+
+		if (lua_rawequal(L, -3, -2) || lua_rawequal(L, -3, -1))
+		{
+			lua_pop(L, 3);
+			return valid_taglist(L, idx, false);
+		}
+	}
+
+	return luaL_argerror(L, idx, "must be a tag list"), NULL;
+}
+
+static int taglist_get(lua_State *L)
+{
+	const taglist_t *list = valid_taglist(L, 1, true);
+
+	if (list == NULL)/* valid check */
+		return 1;
+
+	if (lua_isnumber(L, 2))
+	{
+		const size_t i = lua_tonumber(L, 2);
+
+		if (list && i <= list->count)
+		{
+			lua_pushnumber(L, list->tags[i - 1]);
+			return 1;
+		}
+		else
+			return 0;
+	}
+	else if (has_valid_field(L))
+	{
+		lua_pushboolean(L, 1);
+		return 1;
+	}
+	else
+	{
+		lua_getmetatable(L, 1);
+		lua_replace(L, 1);
+		lua_rawget(L, 1);
+		return 1;
+	}
+}
+
+static int taglist_len(lua_State *L)
+{
+	const taglist_t *list = valid_taglist(L, 1, false);
+	lua_pushnumber(L, list->count);
+	return 1;
+}
+
+static int taglist_equal(lua_State *L)
+{
+	const taglist_t *lhs = check_taglist(L, 1);
+	const taglist_t *rhs = check_taglist(L, 2);
+	lua_pushboolean(L, Tag_Compare(lhs, rhs));
+	return 1;
+}
+
+static int taglist_iterator(lua_State *L)
+{
+	const taglist_t *list = valid_taglist(L, 1, false);
+	const size_t i = 1 + lua_tonumber(L, lua_upvalueindex(1));
+	if (i <= list->count)
+	{
+		lua_pushnumber(L, list->tags[i - 1]);
+		/* watch me exploit an upvalue as a control because
+			I want to use the control as the value */
+		lua_pushnumber(L, i);
+		lua_replace(L, lua_upvalueindex(1));
+		return 1;
+	}
+	else
+		return 0;
+}
+
+static int taglist_iterate(lua_State *L)
+{
+	check_taglist(L, 1);
+	lua_pushnumber(L, 0);
+	lua_pushcclosure(L, taglist_iterator, 1);
+	lua_pushvalue(L, 1);
+	return 2;
+}
+
+static int taglist_find(lua_State *L)
+{
+	const taglist_t *list = check_taglist(L, 1);
+	const mtag_t tag = luaL_checknumber(L, 2);
+	lua_pushboolean(L, Tag_Find(list, tag));
+	return 1;
+}
+
+static int taglist_shares(lua_State *L)
+{
+	const taglist_t *lhs = check_taglist(L, 1);
+	const taglist_t *rhs = check_taglist(L, 2);
+	lua_pushboolean(L, Tag_Share(lhs, rhs));
+	return 1;
+}
+
+/* only sector tags are mutable... */
+
+#ifdef MUTABLE_TAGS
+static size_t sector_of_taglist(taglist_t *list)
+{
+	return (sector_t *)((char *)list - offsetof (sector_t, tags)) - sectors;
+}
+
+static int this_taglist(lua_State *L)
+{
+	lua_settop(L, 1);
+	return 1;
+}
+
+static int taglist_add(lua_State *L)
+{
+	taglist_t *list = *(taglist_t **)luaL_checkudata(L, 1, META_SECTORTAGLIST);
+	const mtag_t tag = luaL_checknumber(L, 2);
+
+	if (! Tag_Find(list, tag))
+	{
+		Taggroup_Add(tags_sectors, tag, sector_of_taglist(list));
+		Tag_Add(list, tag);
+	}
+
+	return this_taglist(L);
+}
+
+static int taglist_remove(lua_State *L)
+{
+	taglist_t *list = *(taglist_t **)luaL_checkudata(L, 1, META_SECTORTAGLIST);
+	const mtag_t tag = luaL_checknumber(L, 2);
+
+	size_t i;
+
+	for (i = 0; i < list->count; ++i)
+	{
+		if (list->tags[i] == tag)
+		{
+			if (list->count > 1)
+			{
+				memmove(&list->tags[i], &list->tags[i + 1],
+						(list->count - 1 - i) * sizeof (mtag_t));
+				list->tags = Z_Realloc(list->tags,
+						(--list->count) * sizeof (mtag_t), PU_LEVEL, NULL);
+				Taggroup_Remove(tags_sectors, tag, sector_of_taglist(list));
+			}
+			else/* reset to default tag */
+				Tag_SectorFSet(sector_of_taglist(list), 0);
+			break;
+		}
+	}
+
+	return this_taglist(L);
+}
+#endif/*MUTABLE_TAGS*/
+
+void LUA_InsertTaggroupIterator
+(		lua_State *L,
+		taggroup_t *garray[],
+		size_t * max_elements,
+		void * element_array,
+		size_t sizeof_element,
+		const char * meta)
+{
+	lua_createtable(L, 0, 3);
+		lua_pushlightuserdata(L, garray);
+		lua_pushlightuserdata(L, max_elements);
+
+		lua_pushvalue(L, -2);
+		lua_pushvalue(L, -2);
+		lua_pushlightuserdata(L, element_array);
+		lua_pushnumber(L, sizeof_element);
+		luaL_getmetatable(L, meta);
+		lua_pushcclosure(L, lib_getTaggroupElement, 5);
+		lua_setfield(L, -4, "__index");
+
+		lua_pushcclosure(L, lib_numTaggroupElements, 2);
+		lua_setfield(L, -2, "__len");
+
+		lua_pushcfunction(L, element_iterator);
+		lua_setfield(L, -2, "__call");
+	lua_pushcclosure(L, lib_getTaggroup, 1);
+	lua_setfield(L, -2, "tagged");
+}
+
+static luaL_Reg taglist_lib[] = {
+	{"iterate", taglist_iterate},
+	{"find", taglist_find},
+	{"shares", taglist_shares},
+#ifdef MUTABLE_TAGS
+	{"add", taglist_add},
+	{"remove", taglist_remove},
+#endif
+	{0}
+};
+
+static void open_taglist(lua_State *L)
+{
+	luaL_register(L, "taglist", taglist_lib);
+
+	lua_getfield(L, -1, "find");
+	lua_setfield(L, -2, "has");
+}
+
+#define new_literal(L, s) \
+	(lua_pushliteral(L, s), luaL_ref(L, -2))
+
+#ifdef MUTABLE_TAGS
+static int
+#else
+static void
+#endif
+set_taglist_metatable(lua_State *L, const char *meta)
+{
+	luaL_newmetatable(L, meta);
+		lua_pushcfunction(L, taglist_get);
+		lua_createtable(L, 0, 1);
+			new_literal(L, "valid");
+		lua_setfenv(L, -2);
+		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, taglist_len);
+		lua_setfield(L, -2, "__len");
+
+		lua_pushcfunction(L, taglist_equal);
+		lua_setfield(L, -2, "__eq");
+#ifdef MUTABLE_TAGS
+	return luaL_ref(L, LUA_REGISTRYINDEX);
+#endif
+}
+
+int LUA_TagLib(lua_State *L)
+{
+	lua_newuserdata(L, 0);
+		lua_createtable(L, 0, 2);
+			lua_createtable(L, 0, 1);
+				lua_pushcfunction(L, lib_iterateTags);
+				lua_setfield(L, -2, "iterate");
+			lua_setfield(L, -2, "__index");
+
+			lua_pushcfunction(L, lib_numTags);
+			lua_setfield(L, -2, "__len");
+		lua_setmetatable(L, -2);
+	lua_setglobal(L, "tags");
+
+	open_taglist(L);
+
+#ifdef MUTABLE_TAGS
+	meta_ref[0] = set_taglist_metatable(L, META_TAGLIST);
+	meta_ref[1] = set_taglist_metatable(L, META_SECTORTAGLIST);
+#else
+	set_taglist_metatable(L, META_TAGLIST);
+#endif
+
+	return 0;
+}
diff --git a/src/lua_thinkerlib.c b/src/lua_thinkerlib.c
index 82baa64693472908fb22029a7838ee6f2228e7e3..65bf8c313b14c942e77454f4890a81df62cdfcec 100644
--- a/src/lua_thinkerlib.c
+++ b/src/lua_thinkerlib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by John "JTE" Muniz.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_aatree.c b/src/m_aatree.c
index c0bb739f8f1c75adbe5c99ef55b59ccf958d1690..b228ed63de0fd6d29d2cfe027a96f9c8eefd2e46 100644
--- a/src/m_aatree.c
+++ b/src/m_aatree.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_aatree.h b/src/m_aatree.h
index b784eb17af61267a681942f34b32bf5d43c91168..5a240394f77920845e53b023f314d223f27076b4 100644
--- a/src/m_aatree.h
+++ b/src/m_aatree.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_anigif.c b/src/m_anigif.c
index dbc8d3422366f4c8cf255cde5e66ea55cb9cbddf..fe04a5cb41fd42141295001d1860161a16c35f3d 100644
--- a/src/m_anigif.c
+++ b/src/m_anigif.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 2013-2016 by Matthew "Kaito Sinclaire" Walsh.
 // Copyright (C) 2013      by "Ninji".
-// Copyright (C) 2013-2020 by Sonic Team Junior.
+// Copyright (C) 2013-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -18,7 +18,7 @@
 #include "z_zone.h"
 #include "v_video.h"
 #include "i_video.h"
-#include "i_system.h" // I_GetTimeMicros
+#include "i_system.h" // I_GetPreciseTime
 #include "m_misc.h"
 #include "st_stuff.h" // st_palette
 
@@ -53,8 +53,8 @@ static RGBA_t *gif_framepalette = NULL;
 
 static FILE *gif_out = NULL;
 static INT32 gif_frames = 0;
-static UINT32 gif_prevframeus = 0; // "us" is microseconds
-static UINT32 gif_delayus = 0;
+static precise_t gif_prevframetime = 0;
+static UINT32 gif_delayus = 0; // "us" is microseconds
 static UINT8 gif_writeover = 0;
 
 
@@ -608,7 +608,7 @@ static void GIF_framewrite(void)
 		{
 			// golden's attempt at creating a "dynamic delay"
 			UINT16 mingifdelay = 10; // minimum gif delay in milliseconds (keep at 10 because gifs can't get more precise).
-			gif_delayus += (I_GetTimeMicros() - gif_prevframeus); // increase delay by how much time was spent between last measurement
+			gif_delayus += I_PreciseToMicros(I_GetPreciseTime() - gif_prevframetime); // increase delay by how much time was spent between last measurement
 
 			if (gif_delayus/1000 >= mingifdelay) // delay is big enough to be able to effect gif frame delay?
 			{
@@ -621,7 +621,7 @@ static void GIF_framewrite(void)
 		{
 			float delayf = ceil(100.0f/NEWTICRATE);
 
-			delay = (UINT16)((I_GetTimeMicros() - gif_prevframeus)/10/1000);
+			delay = (UINT16)I_PreciseToMicros((I_GetPreciseTime() - gif_prevframetime))/10/1000;
 
 			if (delay < (UINT16)(delayf))
 				delay = (UINT16)(delayf);
@@ -711,7 +711,7 @@ static void GIF_framewrite(void)
 	}
 	fwrite(gifframe_data, 1, (p - gifframe_data), gif_out);
 	++gif_frames;
-	gif_prevframeus = I_GetTimeMicros();
+	gif_prevframetime = I_GetPreciseTime();
 }
 
 
@@ -739,7 +739,7 @@ INT32 GIF_open(const char *filename)
 
 	GIF_headwrite();
 	gif_frames = 0;
-	gif_prevframeus = I_GetTimeMicros();
+	gif_prevframetime = I_GetPreciseTime();
 	gif_delayus = 0;
 	return 1;
 }
diff --git a/src/m_anigif.h b/src/m_anigif.h
index abe05dd963019c38d23228180ed7495f779ad562..ca7563b1e9c6de3b537e287220cd83fc775d17ae 100644
--- a/src/m_anigif.h
+++ b/src/m_anigif.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2013-2016 by Matthew "Kaito Sinclaire" Walsh.
-// Copyright (C) 2013-2020 by Sonic Team Junior.
+// Copyright (C) 2013-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_argv.c b/src/m_argv.c
index 7d43d96bc62f0c20c2b1f1485809defab2bdda0a..453d6e45c1ff9e8ee53c6a44bfe2e774a0efdc6b 100644
--- a/src/m_argv.c
+++ b/src/m_argv.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_argv.h b/src/m_argv.h
index 92770f4e9e9b0c58b3a32ad8b752e62cca65b236..f39db513ffdb0d371e06916a51bd964a2cbf821e 100644
--- a/src/m_argv.h
+++ b/src/m_argv.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_bbox.c b/src/m_bbox.c
index 02d5341643938f4db4134c5324479c03e4859857..e0505fd95a485a4f1fad9eb5e3b004fd22fa6fa2 100644
--- a/src/m_bbox.c
+++ b/src/m_bbox.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_bbox.h b/src/m_bbox.h
index 9b63c61b6de97470a6744af9bd532242ba58abab..c56bd22c06714a7eb93c5cf533f3ea8ed7b342a4 100644
--- a/src/m_bbox.h
+++ b/src/m_bbox.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_cheat.c b/src/m_cheat.c
index 6e0fb8c5c0784f66be508981aa33d75f9a661d89..ef896c9911975550905069813394f7c4a4e654d1 100644
--- a/src/m_cheat.c
+++ b/src/m_cheat.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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_cheat.h b/src/m_cheat.h
index ac2540408d481f0745a756929dbeea219ecbc9bd..ee4ba5f557eafe7c98b85605c08394f5bd28144d 100644
--- a/src/m_cheat.h
+++ b/src/m_cheat.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_cond.c b/src/m_cond.c
index 36fcd7cf295aff241e7637d906381422121bd60a..85d732a48d3d7dea1a8c4289a8acaf24ce859e64 100644
--- a/src/m_cond.c
+++ b/src/m_cond.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -496,6 +496,64 @@ UINT8 M_GotHighEnoughRings(INT32 trings)
 	return false;
 }
 
+// Gets the skin number for a SECRET_SKIN unlockable.
+INT32 M_UnlockableSkinNum(unlockable_t *unlock)
+{
+	if (unlock->type != SECRET_SKIN)
+	{
+		// This isn't a skin unlockable...
+		return -1;
+	}
+
+	if (unlock->stringVar && strcmp(unlock->stringVar, ""))
+	{
+		// Get the skin from the string.
+		INT32 skinnum = R_SkinAvailable(unlock->stringVar);
+		if (skinnum != -1)
+		{
+			return skinnum;
+		}
+	}
+
+	if (unlock->variable >= 0 && unlock->variable < numskins)
+	{
+		// Use the number directly.
+		return unlock->variable;
+	}
+
+	// Invalid skin unlockable.
+	return -1;
+}
+
+// Gets the skin number for a ET_SKIN emblem.
+INT32 M_EmblemSkinNum(emblem_t *emblem)
+{
+	if (emblem->type != ET_SKIN)
+	{
+		// This isn't a skin emblem...
+		return -1;
+	}
+
+	if (emblem->stringVar && strcmp(emblem->stringVar, ""))
+	{
+		// Get the skin from the string.
+		INT32 skinnum = R_SkinAvailable(emblem->stringVar);
+		if (skinnum != -1)
+		{
+			return skinnum;
+		}
+	}
+
+	if (emblem->var >= 0 && emblem->var < numskins)
+	{
+		// Use the number directly.
+		return emblem->var;
+	}
+
+	// Invalid skin emblem.
+	return -1;
+}
+
 // ----------------
 // Misc Emblem shit
 // ----------------
diff --git a/src/m_cond.h b/src/m_cond.h
index 9bb162ff317a34f40d69cc1ec37230e1d818af27..b2c6d65e6046b030a44721568ae429baece6909a 100644
--- a/src/m_cond.h
+++ b/src/m_cond.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh.
-// Copyright (C) 2012-2020 by Sonic Team Junior.
+// Copyright (C) 2012-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -92,6 +92,7 @@ typedef struct
 	UINT8 sprite;    ///< emblem sprite to use, 0 - 25
 	UINT16 color;    ///< skincolor to use
 	INT32 var;       ///< If needed, specifies information on the target amount to achieve (or target skin)
+	char *stringVar; ///< String version
 	char hint[110];  ///< Hint for emblem hints menu
 	UINT8 collected; ///< Do you have this emblem?
 } emblem_t;
@@ -116,6 +117,7 @@ typedef struct
 	UINT8 showconditionset;
 	INT16 type;
 	INT16 variable;
+	char *stringVar;
 	UINT8 nocecho;
 	UINT8 nochecklist;
 	UINT8 unlocked;
@@ -132,6 +134,7 @@ typedef struct
 #define SECRET_WARP			 2 // Selectable warp
 #define SECRET_SOUNDTEST	 3 // Sound Test
 #define SECRET_CREDITS		 4 // Enables Credits
+#define SECRET_SKIN			 5 // Unlocks a skin
 
 // If you have more secrets than these variables allow in your game,
 // you seriously need to get a life.
@@ -185,4 +188,7 @@ UINT8 M_GotHighEnoughScore(INT32 tscore);
 UINT8 M_GotLowEnoughTime(INT32 tictime);
 UINT8 M_GotHighEnoughRings(INT32 trings);
 
+INT32 M_UnlockableSkinNum(unlockable_t *unlock);
+INT32 M_EmblemSkinNum(emblem_t *emblem);
+
 #define M_Achieved(a) ((a) >= MAXCONDITIONSETS || conditionSets[a].achieved)
diff --git a/src/m_dllist.h b/src/m_dllist.h
index 680c2cd80085f0e7501e714e20c749e0fb371f39..65303b4a339fb81ec9f7a2c9eb70f2b8cd55a20a 100644
--- a/src/m_dllist.h
+++ b/src/m_dllist.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2005      by James Haley
-// Copyright (C) 2005-2020 by Sonic Team Junior.
+// Copyright (C) 2005-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_easing.c b/src/m_easing.c
new file mode 100644
index 0000000000000000000000000000000000000000..c871d3106e53c2e62216a8a731dab50cf3c6fb15
--- /dev/null
+++ b/src/m_easing.c
@@ -0,0 +1,430 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2020-2021 by Jaime "Lactozilla" Passos.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  m_easing.c
+/// \brief Easing functions
+///        Referenced from https://easings.net/
+
+#include "m_easing.h"
+#include "tables.h"
+#include "doomdef.h"
+
+/*
+	For the computation of the logarithm, we choose, by trial and error, from among
+	a sequence of particular factors those, that when multiplied with the function
+	argument, normalize it to unity. For every factor chosen, we add up the
+	corresponding logarithm value stored in a table. The sum then corresponds to
+	the logarithm of the function argument.
+
+	For the integer portion, we would want to choose
+		2^i, i = 1, 2, 4, 8, ...
+	and for the factional part we choose
+		1+2^-i, i = 1, 2, 3, 4, 5 ...
+
+	The algorithm for the exponential is closely related and quite literally the inverse
+	of the logarithm algorithm. From among the sequence of tabulated logarithms for our
+	chosen factors, we pick those that when subtracted from the function argument ultimately
+	reduce it to zero. Starting with unity, we multiply with all the factors whose logarithms
+	we have subtracted in the process. The resulting product corresponds to the result of the exponentiation.
+
+	Logarithms of values greater than unity can be computed by applying the algorithm to the reciprocal
+	of the function argument (with the negation of the result as appropriate), likewise exponentiation with
+	negative function arguments requires us negate the function argument and compute the reciprocal at the end.
+*/
+
+static fixed_t logtabdec[FRACBITS] =
+{
+	0x95c1, 0x526a, 0x2b80, 0x1663,
+	0xb5d, 0x5b9, 0x2e0, 0x170,
+	0xb8, 0x5c, 0x2e, 0x17,
+	0x0b, 0x06, 0x03, 0x01
+};
+
+static fixed_t fixlog2(fixed_t a)
+{
+	UINT32 x = a, y = 0;
+	INT32 t, i, shift = 8;
+
+	if (x > FRACUNIT)
+		x = FixedDiv(FRACUNIT, x);
+
+	// Integer part
+	//   1<<19 = 0x80000
+	//   1<<18 = 0x40000
+	//   1<<17 = 0x20000
+	//   1<<16 = 0x10000
+
+#define dologtab(i) \
+	t = (x << shift); \
+	if (t < FRACUNIT) \
+	{ \
+		x = t; \
+		y += (1 << (19 - i)); \
+	} \
+	shift /= 2;
+
+	dologtab(0)
+	dologtab(1)
+	dologtab(2)
+	dologtab(3)
+
+#undef dologtab
+
+	// Decimal part
+	for (i = 0; i < FRACBITS; i++)
+	{
+		t = x + (x >> (i + 1));
+		if (t < FRACUNIT)
+		{
+			x = t;
+			y += logtabdec[i];
+		}
+	}
+
+	if (a <= FRACUNIT)
+		return -y;
+
+	return y;
+}
+
+// Notice how this is symmetric to fixlog2.
+static INT32 fixexp(fixed_t a)
+{
+	UINT32 x, y;
+	fixed_t t, i, shift = 8;
+
+	// Underflow prevention.
+	if (a <= -15 * FRACUNIT)
+		return 0;
+
+	x = (a < 0) ? (-a) : (a);
+	y = FRACUNIT;
+
+	// Integer part (see fixlog2)
+#define dologtab(i) \
+	t = x - (1 << (19 - i)); \
+	if (t >= 0) \
+	{ \
+		x = t; \
+		y <<= shift; \
+	} \
+	shift /= 2;
+
+	dologtab(0)
+	dologtab(1)
+	dologtab(2)
+	dologtab(3)
+
+#undef dologtab
+
+	// Decimal part
+	for (i = 0; i < FRACBITS; i++)
+	{
+		t = (x - logtabdec[i]);
+		if (t >= 0)
+		{
+			x = t;
+			y += (y >> (i + 1));
+		}
+	}
+
+	if (a < 0)
+		return FixedDiv(FRACUNIT, y);
+
+	return y;
+}
+
+#define fixpow(x, y) fixexp(FixedMul((y), fixlog2(x)))
+#define fixintmul(x, y) FixedMul((x) * FRACUNIT, y)
+#define fixintdiv(x, y) FixedDiv(x, (y) * FRACUNIT)
+#define fixinterp(start, end, t) FixedMul((FRACUNIT - (t)), start) + FixedMul(t, end)
+
+// ==================
+//  EASING FUNCTIONS
+// ==================
+
+#define EASINGFUNC(type) fixed_t Easing_ ## type (fixed_t t, fixed_t start, fixed_t end)
+
+//
+// Linear
+//
+
+EASINGFUNC(Linear)
+{
+	return fixinterp(start, end, t);
+}
+
+//
+// Sine
+//
+
+// This is equivalent to calculating (x * pi) and converting the result from radians into degrees.
+#define fixang(x) FixedMul((x), 180*FRACUNIT)
+
+EASINGFUNC(InSine)
+{
+	fixed_t c = fixang(t / 2);
+	fixed_t x = FRACUNIT - FINECOSINE(FixedAngle(c)>>ANGLETOFINESHIFT);
+	return fixinterp(start, end, x);
+}
+
+EASINGFUNC(OutSine)
+{
+	fixed_t c = fixang(t / 2);
+	fixed_t x = FINESINE(FixedAngle(c)>>ANGLETOFINESHIFT);
+	return fixinterp(start, end, x);
+}
+
+EASINGFUNC(InOutSine)
+{
+	fixed_t c = fixang(t);
+	fixed_t x = -(FINECOSINE(FixedAngle(c)>>ANGLETOFINESHIFT) - FRACUNIT) / 2;
+	return fixinterp(start, end, x);
+}
+
+#undef fixang
+
+//
+// Quad
+//
+
+EASINGFUNC(InQuad)
+{
+	return fixinterp(start, end, FixedMul(t, t));
+}
+
+EASINGFUNC(OutQuad)
+{
+	return fixinterp(start, end, FRACUNIT - FixedMul(FRACUNIT - t, FRACUNIT - t));
+}
+
+EASINGFUNC(InOutQuad)
+{
+	fixed_t x = t < (FRACUNIT/2)
+	? fixintmul(2, FixedMul(t, t))
+	: FRACUNIT - fixpow(FixedMul(-2*FRACUNIT, t) + 2*FRACUNIT, 2*FRACUNIT) / 2;
+	return fixinterp(start, end, x);
+}
+
+//
+// Cubic
+//
+
+EASINGFUNC(InCubic)
+{
+	fixed_t x = FixedMul(t, FixedMul(t, t));
+	return fixinterp(start, end, x);
+}
+
+EASINGFUNC(OutCubic)
+{
+	return fixinterp(start, end, FRACUNIT - fixpow(FRACUNIT - t, 3*FRACUNIT));
+}
+
+EASINGFUNC(InOutCubic)
+{
+	fixed_t x = t < (FRACUNIT/2)
+	? fixintmul(4, FixedMul(t, FixedMul(t, t)))
+	: FRACUNIT - fixpow(fixintmul(-2, t) + 2*FRACUNIT, 3*FRACUNIT) / 2;
+	return fixinterp(start, end, x);
+}
+
+//
+// "Quart"
+//
+
+EASINGFUNC(InQuart)
+{
+	fixed_t x = FixedMul(FixedMul(t, t), FixedMul(t, t));
+	return fixinterp(start, end, x);
+}
+
+EASINGFUNC(OutQuart)
+{
+	fixed_t x = FRACUNIT - fixpow(FRACUNIT - t, 4 * FRACUNIT);
+	return fixinterp(start, end, x);
+}
+
+EASINGFUNC(InOutQuart)
+{
+	fixed_t x = t < (FRACUNIT/2)
+	? fixintmul(8, FixedMul(FixedMul(t, t), FixedMul(t, t)))
+	: FRACUNIT - fixpow(fixintmul(-2, t) + 2*FRACUNIT, 4*FRACUNIT) / 2;
+	return fixinterp(start, end, x);
+}
+
+//
+// "Quint"
+//
+
+EASINGFUNC(InQuint)
+{
+	fixed_t x = FixedMul(t, FixedMul(FixedMul(t, t), FixedMul(t, t)));
+	return fixinterp(start, end, x);
+}
+
+EASINGFUNC(OutQuint)
+{
+	fixed_t x = FRACUNIT - fixpow(FRACUNIT - t, 5 * FRACUNIT);
+	return fixinterp(start, end, x);
+}
+
+EASINGFUNC(InOutQuint)
+{
+	fixed_t x = t < (FRACUNIT/2)
+	? FixedMul(16*FRACUNIT, FixedMul(t, FixedMul(FixedMul(t, t), FixedMul(t, t))))
+	: FRACUNIT - fixpow(fixintmul(-2, t) + 2*FRACUNIT, 5*FRACUNIT) / 2;
+	return fixinterp(start, end, x);
+}
+
+//
+// Exponential
+//
+
+EASINGFUNC(InExpo)
+{
+	fixed_t x = (!t) ? 0 : fixpow(2*FRACUNIT, fixintmul(10, t) - 10*FRACUNIT);
+	return fixinterp(start, end, x);
+}
+
+EASINGFUNC(OutExpo)
+{
+	fixed_t x = (t >= FRACUNIT) ? FRACUNIT
+	: FRACUNIT - fixpow(2*FRACUNIT, fixintmul(-10, t));
+	return fixinterp(start, end, x);
+}
+
+EASINGFUNC(InOutExpo)
+{
+	fixed_t x;
+
+	if (!t)
+		x = 0;
+	else if (t >= FRACUNIT)
+		x = FRACUNIT;
+	else
+	{
+		if (t < FRACUNIT / 2)
+		{
+			x = fixpow(2*FRACUNIT, fixintmul(20, t) - 10*FRACUNIT);
+			x = fixintdiv(x, 2);
+		}
+		else
+		{
+			x = fixpow(2*FRACUNIT, fixintmul(-20, t) + 10*FRACUNIT);
+			x = fixintdiv((2*FRACUNIT) - x, 2);
+		}
+	}
+
+	return fixinterp(start, end, x);
+}
+
+//
+// "Back"
+//
+
+#define EASEBACKCONST1 111514 // 1.70158
+#define EASEBACKCONST2 99942 // 1.525
+
+static fixed_t EaseInBack(fixed_t t, fixed_t start, fixed_t end, fixed_t c1)
+{
+	const fixed_t c3 = c1 + FRACUNIT;
+	fixed_t x = FixedMul(FixedMul(t, t), FixedMul(c3, t) - c1);
+	return fixinterp(start, end, x);
+}
+
+EASINGFUNC(InBack)
+{
+	return EaseInBack(t, start, end, EASEBACKCONST1);
+}
+
+static fixed_t EaseOutBack(fixed_t t, fixed_t start, fixed_t end, fixed_t c1)
+{
+	const fixed_t c3 = c1 + FRACUNIT;
+	fixed_t x;
+	t -= FRACUNIT;
+	x = FRACUNIT + FixedMul(FixedMul(t, t), FixedMul(c3, t) + c1);
+	return fixinterp(start, end, x);
+}
+
+EASINGFUNC(OutBack)
+{
+	return EaseOutBack(t, start, end, EASEBACKCONST1);
+}
+
+static fixed_t EaseInOutBack(fixed_t t, fixed_t start, fixed_t end, fixed_t c2)
+{
+	fixed_t x, y;
+	const fixed_t f2 = 2*FRACUNIT;
+
+	if (t < FRACUNIT / 2)
+	{
+		x = fixpow(FixedMul(t, f2), f2);
+		y = FixedMul(c2 + FRACUNIT, FixedMul(t, f2));
+		x = FixedMul(x, y - c2);
+	}
+	else
+	{
+		x = fixpow(-(FixedMul(t, f2) - f2), f2);
+		y = FixedMul(c2 + FRACUNIT, FixedMul(t, f2) - f2);
+		x = FixedMul(x, y + c2);
+		x += f2;
+	}
+
+	x /= 2;
+
+	return fixinterp(start, end, x);
+}
+
+EASINGFUNC(InOutBack)
+{
+	return EaseInOutBack(t, start, end, EASEBACKCONST2);
+}
+
+#undef EASINGFUNC
+#define EASINGFUNC(type) fixed_t Easing_ ## type (fixed_t t, fixed_t start, fixed_t end, fixed_t param)
+
+EASINGFUNC(InBackParameterized)
+{
+	return EaseInBack(t, start, end, param);
+}
+
+EASINGFUNC(OutBackParameterized)
+{
+	return EaseOutBack(t, start, end, param);
+}
+
+EASINGFUNC(InOutBackParameterized)
+{
+	return EaseInOutBack(t, start, end, param);
+}
+
+#undef EASINGFUNC
+
+// Function list
+
+#define EASINGFUNC(type) Easing_ ## type
+#define COMMA ,
+
+easingfunc_t easing_funclist[EASE_MAX] =
+{
+	EASINGFUNCLIST(COMMA)
+};
+
+// Function names
+
+#undef EASINGFUNC
+#define EASINGFUNC(type) #type
+
+const char *easing_funcnames[EASE_MAX] =
+{
+	EASINGFUNCLIST(COMMA)
+};
+
+#undef COMMA
+#undef EASINGFUNC
diff --git a/src/m_easing.h b/src/m_easing.h
new file mode 100644
index 0000000000000000000000000000000000000000..435ad35e7a0b6b2302ce5256713155bf18c4258f
--- /dev/null
+++ b/src/m_easing.h
@@ -0,0 +1,101 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2020-2021 by Jaime "Lactozilla" Passos.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  m_easing.h
+/// \brief Easing functions
+
+#ifndef __M_EASING_H__
+#define __M_EASING_H__
+
+#include "doomtype.h"
+#include "m_fixed.h"
+
+typedef enum
+{
+	EASE_LINEAR = 0,
+
+	EASE_INSINE,
+	EASE_OUTSINE,
+	EASE_INOUTSINE,
+
+	EASE_INQUAD,
+	EASE_OUTQUAD,
+	EASE_INOUTQUAD,
+
+	EASE_INCUBIC,
+	EASE_OUTCUBIC,
+	EASE_INOUTCUBIC,
+
+	EASE_INQUART,
+	EASE_OUTQUART,
+	EASE_INOUTQUART,
+
+	EASE_INQUINT,
+	EASE_OUTQUINT,
+	EASE_INOUTQUINT,
+
+	EASE_INEXPO,
+	EASE_OUTEXPO,
+	EASE_INOUTEXPO,
+
+	EASE_INBACK,
+	EASE_OUTBACK,
+	EASE_INOUTBACK,
+
+	EASE_MAX,
+} easing_t;
+
+typedef fixed_t (*easingfunc_t)(fixed_t, fixed_t, fixed_t);
+
+extern easingfunc_t easing_funclist[EASE_MAX];
+extern const char *easing_funcnames[EASE_MAX];
+
+#define EASINGFUNCLIST(sep) \
+	EASINGFUNC(Linear) sep /* Easing_Linear */ \
+ \
+	EASINGFUNC(InSine) sep /* Easing_InSine */ \
+	EASINGFUNC(OutSine) sep /* Easing_OutSine */ \
+	EASINGFUNC(InOutSine) sep /* Easing_InOutSine */ \
+ \
+	EASINGFUNC(InQuad) sep /* Easing_InQuad */ \
+	EASINGFUNC(OutQuad) sep /* Easing_OutQuad */ \
+	EASINGFUNC(InOutQuad) sep /* Easing_InOutQuad */ \
+ \
+	EASINGFUNC(InCubic) sep /* Easing_InCubic */ \
+	EASINGFUNC(OutCubic) sep /* Easing_OutCubic */ \
+	EASINGFUNC(InOutCubic) sep /* Easing_InOutCubic */ \
+ \
+	EASINGFUNC(InQuart) sep /* Easing_InQuart */ \
+	EASINGFUNC(OutQuart) sep /* Easing_OutQuart */ \
+	EASINGFUNC(InOutQuart) sep /* Easing_InOutQuart */ \
+ \
+	EASINGFUNC(InQuint) sep /* Easing_InQuint */ \
+	EASINGFUNC(OutQuint) sep /* Easing_OutQuint */ \
+	EASINGFUNC(InOutQuint) sep /* Easing_InOutQuint */ \
+ \
+	EASINGFUNC(InExpo) sep /* Easing_InExpo */ \
+	EASINGFUNC(OutExpo) sep /* Easing_OutExpo */ \
+	EASINGFUNC(InOutExpo) sep /* Easing_InOutExpo */ \
+ \
+	EASINGFUNC(InBack) sep /* Easing_InBack */ \
+	EASINGFUNC(OutBack) sep /* Easing_OutBack */ \
+	EASINGFUNC(InOutBack) sep /* Easing_InOutBack */
+
+#define EASINGFUNC(type) fixed_t Easing_ ## type (fixed_t t, fixed_t start, fixed_t end);
+
+EASINGFUNCLIST()
+
+#undef EASINGFUNC
+#define EASINGFUNC(type) fixed_t Easing_ ## type (fixed_t t, fixed_t start, fixed_t end, fixed_t param);
+
+EASINGFUNC(InBackParameterized) /* Easing_InBackParameterized */
+EASINGFUNC(OutBackParameterized) /* Easing_OutBackParameterized */
+EASINGFUNC(InOutBackParameterized) /* Easing_InOutBackParameterized */
+
+#undef EASINGFUNC
+#endif
diff --git a/src/m_fixed.c b/src/m_fixed.c
index eb10fd5f801825dc82462d67de67110b65edf285..d40ccd98e35604a706dd7fa39f16234e1eb28f5a 100644
--- a/src/m_fixed.c
+++ b/src/m_fixed.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_fixed.h b/src/m_fixed.h
index 289ca442a03e7740a7e1844303a8d84c97f9aa22..1cf2f00d1e482d8fae5c77ea48a47b6940826297 100644
--- a/src/m_fixed.h
+++ b/src/m_fixed.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -71,7 +71,7 @@ FUNCMATH FUNCINLINE static ATTRINLINE fixed_t FloatToFixed(float f)
 		value   [eax]       \
 		modify exact [eax edx]
 #elif defined (__GNUC__) && defined (__i386__) && !defined (NOASM)
-	// DJGPP, i386 linux, cygwin or mingw
+	// i386 linux, cygwin or mingw
 	FUNCMATH FUNCINLINE static inline fixed_t FixedMul(fixed_t a, fixed_t b) // asm
 	{
 		fixed_t ret;
diff --git a/src/m_menu.c b/src/m_menu.c
index 77648f877c7adc9ff2b3b5c4fc9f0be7eb532a05..fc1e33b67dc16c6e8898a1df754119ecf8cf86ed 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -3,7 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 2011-2016 by Matthew "Kaito Sinclaire" Walsh.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -62,6 +62,8 @@
 
 #include "i_joy.h" // for joystick menu controls
 
+#include "p_saveg.h" // Only for NEWSKINSAVES
+
 // Condition Sets
 #include "m_cond.h"
 
@@ -1104,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[] =
@@ -1322,7 +1324,7 @@ static menuitem_t OP_Camera2ExtendedOptionsMenu[] =
 enum
 {
 	op_video_resolution = 1,
-#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
+#if defined (__unix__) || defined (UNIXCOMMON) || defined (HAVE_SDL)
 	op_video_fullscreen,
 #endif
 	op_video_vsync,
@@ -1334,7 +1336,7 @@ static menuitem_t OP_VideoOptionsMenu[] =
 	{IT_HEADER, NULL, "Screen", NULL, 0},
 	{IT_STRING | IT_CALL,  NULL, "Set Resolution...",       M_VideoModeMenu,          6},
 
-#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
+#if defined (__unix__) || defined (UNIXCOMMON) || defined (HAVE_SDL)
 	{IT_STRING|IT_CVAR,      NULL, "Fullscreen",             &cv_fullscreen,         11},
 #endif
 	{IT_STRING | IT_CVAR, NULL, "Vertical Sync",                &cv_vidwait,         16},
@@ -1356,9 +1358,7 @@ static menuitem_t OP_VideoOptionsMenu[] =
 	{IT_STRING | IT_CVAR, NULL, "Score/Time/Rings",          &cv_timetic,          71},
 	{IT_STRING | IT_CVAR, NULL, "Show Powerups",             &cv_powerupdisplay,   76},
 	{IT_STRING | IT_CVAR, NULL, "Local ping display",		&cv_showping,			81}, // shows ping next to framerate if we want to.
-#ifdef SEENAMES
 	{IT_STRING | IT_CVAR, NULL, "Show player names",         &cv_seenames,         86},
-#endif
 
 	{IT_HEADER, NULL, "Console", NULL, 95},
 	{IT_STRING | IT_CVAR, NULL, "Background color",          &cons_backcolor,      101},
@@ -1455,7 +1455,7 @@ static menuitem_t OP_OpenGLOptionsMenu[] =
 #ifdef ALAM_LIGHTING
 	{IT_SUBMENU|IT_STRING,      NULL, "Lighting...",         &OP_OpenGLLightingDef,   144},
 #endif
-#if defined (_WINDOWS) && (!((defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)))
+#if defined (_WINDOWS) && (!(defined (__unix__) || defined (UNIXCOMMON) || defined (HAVE_SDL)))
 	{IT_STRING|IT_CVAR,         NULL, "Fullscreen",          &cv_fullscreen,          154},
 #endif
 };
@@ -1551,18 +1551,19 @@ static menuitem_t OP_ScreenshotOptionsMenu[] =
 	{IT_STRING|IT_CVAR, NULL, "Window Size",       &cv_zlib_window_bits,           57},
 
 	{IT_HEADER, NULL, "Movie Mode (F9)", NULL, 64},
-	{IT_STRING|IT_CVAR, NULL, "Storage Location",  &cv_movie_option,              70},
-	{IT_STRING|IT_CVAR|IT_CV_STRING, NULL, "Custom Folder", &cv_movie_folder, 	  75},
-	{IT_STRING|IT_CVAR, NULL, "Capture Mode",      &cv_moviemode,                 90},
+	{IT_STRING|IT_CVAR, NULL, "Storage Location",  &cv_movie_option,               70},
+	{IT_STRING|IT_CVAR|IT_CV_STRING, NULL, "Custom Folder", &cv_movie_folder, 	   75},
+	{IT_STRING|IT_CVAR, NULL, "Capture Mode",      &cv_moviemode,                  90},
 
-	{IT_STRING|IT_CVAR, NULL, "Region Optimizing", &cv_gif_optimize,              95},
-	{IT_STRING|IT_CVAR, NULL, "Downscaling",       &cv_gif_downscale,             100},
+	{IT_STRING|IT_CVAR, NULL, "Downscaling",       &cv_gif_downscale,              95},
+	{IT_STRING|IT_CVAR, NULL, "Region Optimizing", &cv_gif_optimize,              100},
 	{IT_STRING|IT_CVAR, NULL, "Local Color Table", &cv_gif_localcolortable,       105},
 
-	{IT_STRING|IT_CVAR, NULL, "Memory Level",      &cv_zlib_memorya,              95},
-	{IT_STRING|IT_CVAR, NULL, "Compression Level", &cv_zlib_levela,               100},
-	{IT_STRING|IT_CVAR, NULL, "Strategy",          &cv_zlib_strategya,            105},
-	{IT_STRING|IT_CVAR, NULL, "Window Size",       &cv_zlib_window_bitsa,         110},
+	{IT_STRING|IT_CVAR, NULL, "Downscaling",       &cv_apng_downscale,             95},
+	{IT_STRING|IT_CVAR, NULL, "Memory Level",      &cv_zlib_memorya,              100},
+	{IT_STRING|IT_CVAR, NULL, "Compression Level", &cv_zlib_levela,               105},
+	{IT_STRING|IT_CVAR, NULL, "Strategy",          &cv_zlib_strategya,            110},
+	{IT_STRING|IT_CVAR, NULL, "Window Size",       &cv_zlib_window_bitsa,         115},
 };
 
 enum
@@ -1575,7 +1576,7 @@ enum
 	op_screenshot_gif_start = 13,
 	op_screenshot_gif_end = 15,
 	op_screenshot_apng_start = 16,
-	op_screenshot_apng_end = 19,
+	op_screenshot_apng_end = 20,
 };
 
 static menuitem_t OP_EraseDataMenu[] =
@@ -1613,53 +1614,54 @@ static menuitem_t OP_ServerOptionsMenu[] =
 	{IT_STRING | IT_CVAR,    NULL, "Max Players",                      &cv_maxplayers,          21},
 	{IT_STRING | IT_CVAR,    NULL, "Allow Add-on Downloading",         &cv_downloading,         26},
 	{IT_STRING | IT_CVAR,    NULL, "Allow players to join",            &cv_allownewplayer,      31},
+	{IT_STRING | IT_CVAR,    NULL, "Minutes for reconnecting",         &cv_rejointimeout,       36},
 #endif
-	{IT_STRING | IT_CVAR,    NULL, "Map progression",                  &cv_advancemap,          36},
-	{IT_STRING | IT_CVAR,    NULL, "Intermission Timer",               &cv_inttime,             41},
+	{IT_STRING | IT_CVAR,    NULL, "Map progression",                  &cv_advancemap,          41},
+	{IT_STRING | IT_CVAR,    NULL, "Intermission Timer",               &cv_inttime,             46},
 
-	{IT_HEADER, NULL, "Characters", NULL, 50},
-	{IT_STRING | IT_CVAR,    NULL, "Force a character",                &cv_forceskin,           56},
-	{IT_STRING | IT_CVAR,    NULL, "Restrict character changes",       &cv_restrictskinchange,  61},
+	{IT_HEADER, NULL, "Characters", NULL, 55},
+	{IT_STRING | IT_CVAR,    NULL, "Force a character",                &cv_forceskin,           61},
+	{IT_STRING | IT_CVAR,    NULL, "Restrict character changes",       &cv_restrictskinchange,  66},
 
-	{IT_HEADER, NULL, "Items", NULL, 70},
-	{IT_STRING | IT_CVAR,    NULL, "Item respawn delay",               &cv_itemrespawntime,     76},
-	{IT_STRING | IT_SUBMENU, NULL, "Mystery Item Monitor Toggles...",  &OP_MonitorToggleDef,    81},
+	{IT_HEADER, NULL, "Items", NULL, 75},
+	{IT_STRING | IT_CVAR,    NULL, "Item respawn delay",               &cv_itemrespawntime,     81},
+	{IT_STRING | IT_SUBMENU, NULL, "Mystery Item Monitor Toggles...",  &OP_MonitorToggleDef,    86},
 
-	{IT_HEADER, NULL, "Cooperative", NULL, 90},
-	{IT_STRING | IT_CVAR,    NULL, "Players required for exit",        &cv_playersforexit,      96},
-	{IT_STRING | IT_CVAR,    NULL, "Starposts",                        &cv_coopstarposts,      101},
-	{IT_STRING | IT_CVAR,    NULL, "Life sharing",                     &cv_cooplives,          106},
-	{IT_STRING | IT_CVAR,    NULL, "Post-goal free roaming",           &cv_exitmove,           111},
+	{IT_HEADER, NULL, "Cooperative", NULL, 95},
+	{IT_STRING | IT_CVAR,    NULL, "Players required for exit",        &cv_playersforexit,     101},
+	{IT_STRING | IT_CVAR,    NULL, "Starposts",                        &cv_coopstarposts,      106},
+	{IT_STRING | IT_CVAR,    NULL, "Life sharing",                     &cv_cooplives,          111},
+	{IT_STRING | IT_CVAR,    NULL, "Post-goal free roaming",           &cv_exitmove,           116},
 
-	{IT_HEADER, NULL, "Race, Competition", NULL, 120},
-	{IT_STRING | IT_CVAR,    NULL, "Level completion countdown",       &cv_countdowntime,      126},
-	{IT_STRING | IT_CVAR,    NULL, "Item Monitors",                    &cv_competitionboxes,   131},
+	{IT_HEADER, NULL, "Race, Competition", NULL, 125},
+	{IT_STRING | IT_CVAR,    NULL, "Level completion countdown",       &cv_countdowntime,      131},
+	{IT_STRING | IT_CVAR,    NULL, "Item Monitors",                    &cv_competitionboxes,   136},
 
-	{IT_HEADER, NULL, "Ringslinger (Match, CTF, Tag, H&S)", NULL, 140},
-	{IT_STRING | IT_CVAR,    NULL, "Time Limit",                       &cv_timelimit,          146},
-	{IT_STRING | IT_CVAR,    NULL, "Score Limit",                      &cv_pointlimit,         151},
-	{IT_STRING | IT_CVAR,    NULL, "Overtime on Tie",                  &cv_overtime,           156},
-	{IT_STRING | IT_CVAR,    NULL, "Player respawn delay",             &cv_respawntime,        161},
+	{IT_HEADER, NULL, "Ringslinger (Match, CTF, Tag, H&S)", NULL, 145},
+	{IT_STRING | IT_CVAR,    NULL, "Time Limit",                       &cv_timelimit,          151},
+	{IT_STRING | IT_CVAR,    NULL, "Score Limit",                      &cv_pointlimit,         156},
+	{IT_STRING | IT_CVAR,    NULL, "Overtime on Tie",                  &cv_overtime,           161},
+	{IT_STRING | IT_CVAR,    NULL, "Player respawn delay",             &cv_respawntime,        166},
 
-	{IT_STRING | IT_CVAR,    NULL, "Item Monitors",                    &cv_matchboxes,         171},
-	{IT_STRING | IT_CVAR,    NULL, "Weapon Rings",                     &cv_specialrings,       176},
-	{IT_STRING | IT_CVAR,    NULL, "Power Stones",                     &cv_powerstones,        181},
+	{IT_STRING | IT_CVAR,    NULL, "Item Monitors",                    &cv_matchboxes,         176},
+	{IT_STRING | IT_CVAR,    NULL, "Weapon Rings",                     &cv_specialrings,       181},
+	{IT_STRING | IT_CVAR,    NULL, "Power Stones",                     &cv_powerstones,        186},
 
-	{IT_STRING | IT_CVAR,    NULL, "Flag respawn delay",               &cv_flagtime,           191},
-	{IT_STRING | IT_CVAR,    NULL, "Hiding time",                      &cv_hidetime,           196},
+	{IT_STRING | IT_CVAR,    NULL, "Flag respawn delay",               &cv_flagtime,           196},
+	{IT_STRING | IT_CVAR,    NULL, "Hiding time",                      &cv_hidetime,           201},
 
-	{IT_HEADER, NULL, "Teams", NULL, 205},
-	{IT_STRING | IT_CVAR,    NULL, "Autobalance sizes",                &cv_autobalance,        211},
-	{IT_STRING | IT_CVAR,    NULL, "Scramble on Map Change",           &cv_scrambleonchange,   216},
+	{IT_HEADER, NULL, "Teams", NULL, 210},
+	{IT_STRING | IT_CVAR,    NULL, "Autobalance sizes",                &cv_autobalance,        216},
+	{IT_STRING | IT_CVAR,    NULL, "Scramble on Map Change",           &cv_scrambleonchange,   221},
 
 #ifndef NONET
-	{IT_HEADER, NULL, "Advanced", NULL, 225},
-	{IT_STRING | IT_CVAR | IT_CV_STRING, NULL, "Master server",        &cv_masterserver,       231},
+	{IT_HEADER, NULL, "Advanced", NULL, 230},
+	{IT_STRING | IT_CVAR | IT_CV_STRING, NULL, "Master server",        &cv_masterserver,       236},
 
-	{IT_STRING | IT_CVAR,    NULL, "Join delay",                       &cv_joindelay,          246},
-	{IT_STRING | IT_CVAR,    NULL, "Attempts to resynchronise",        &cv_resynchattempts,    251},
+	{IT_STRING | IT_CVAR,    NULL, "Join delay",                       &cv_joindelay,          251},
+	{IT_STRING | IT_CVAR,    NULL, "Attempts to resynchronise",        &cv_resynchattempts,    256},
 
-	{IT_STRING | IT_CVAR,    NULL, "Show IP Address of Joiners",       &cv_showjoinaddress,    256},
+	{IT_STRING | IT_CVAR,    NULL, "Show IP Address of Joiners",       &cv_showjoinaddress,    261},
 #endif
 };
 
@@ -3213,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)
@@ -3227,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)
@@ -3260,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;
@@ -3305,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;
@@ -3319,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;
@@ -3337,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
@@ -4060,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);
 
@@ -4103,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));
+	}
 }
 
 //
@@ -4183,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
 
@@ -5166,34 +5170,75 @@ static boolean M_GametypeHasLevels(INT32 gt)
 
 static INT32 M_CountRowsToShowOnPlatter(INT32 gt)
 {
-	INT32 mapnum = 0, prevmapnum = 0, col = 0, rows = 0;
+	INT32 col = 0, rows = 0;
+	INT32 mapIterate = 0;
+	INT32 headingIterate = 0;
+	boolean mapAddedAlready[NUMMAPS];
 
-	while (mapnum < NUMMAPS)
+	memset(mapAddedAlready, 0, sizeof mapAddedAlready);
+
+	for (mapIterate = 0; mapIterate < NUMMAPS; mapIterate++)
 	{
-		if (M_CanShowLevelOnPlatter(mapnum, gt))
+		boolean forceNewRow = true;
+
+		if (mapAddedAlready[mapIterate] == true)
 		{
-			if (rows == 0)
+			// Already added under another heading
+			continue;
+		}
+
+		if (M_CanShowLevelOnPlatter(mapIterate, gt) == false)
+		{
+			// Don't show this one
+			continue;
+		}
+
+		for (headingIterate = mapIterate; headingIterate < NUMMAPS; headingIterate++)
+		{
+			boolean wide = false;
+
+			if (mapAddedAlready[headingIterate] == true)
+			{
+				// Already added under another heading
+				continue;
+			}
+
+			if (M_CanShowLevelOnPlatter(headingIterate, gt) == false)
+			{
+				// Don't show this one
+				continue;
+			}
+
+			if (!fastcmp(mapheaderinfo[mapIterate]->selectheading, mapheaderinfo[headingIterate]->selectheading))
+			{
+				// Headers don't match
+				continue;
+			}
+
+			wide = (mapheaderinfo[headingIterate]->menuflags & LF2_WIDEICON);
+
+			// preparing next position to drop mapnum into
+			if (col == 2 // no more space on the row?
+				|| wide || forceNewRow)
+			{
+				col = 0;
 				rows++;
+			}
 			else
 			{
-				if (col == 2
-				|| (mapheaderinfo[prevmapnum]->menuflags & LF2_WIDEICON)
-				|| (mapheaderinfo[mapnum]->menuflags & LF2_WIDEICON)
-				|| !(fastcmp(mapheaderinfo[mapnum]->selectheading, mapheaderinfo[prevmapnum]->selectheading)))
-				{
-					col = 0;
-					rows++;
-				}
-				else
-					col++;
+				col++;
 			}
-			prevmapnum = mapnum;
+
+			// Done adding this one
+			mapAddedAlready[headingIterate] = true;
+			forceNewRow = wide;
 		}
-		mapnum++;
 	}
 
 	if (levellistmode == LLM_CREATESERVER)
+	{
 		rows++;
+	}
 
 	return rows;
 }
@@ -5223,7 +5268,10 @@ static void M_CacheLevelPlatter(void)
 static boolean M_PrepareLevelPlatter(INT32 gt, boolean nextmappick)
 {
 	INT32 numrows = M_CountRowsToShowOnPlatter(gt);
-	INT32 mapnum = 0, prevmapnum = 0, col = 0, row = 0, startrow = 0;
+	INT32 col = 0, row = 0, startrow = 0;
+	INT32 mapIterate = 0; // First level of map loop -- find starting points for select headings
+	INT32 headingIterate = 0; // Second level of map loop -- finding maps that match mapIterate's heading.
+	boolean mapAddedAlready[NUMMAPS];
 
 	if (!numrows)
 		return false;
@@ -5240,6 +5288,8 @@ static boolean M_PrepareLevelPlatter(INT32 gt, boolean nextmappick)
 	// done here so lsrow and lscol can be set if cv_nextmap is on the platter
 	lsrow = lscol = lshli = lsoffs[0] = lsoffs[1] = 0;
 
+	memset(mapAddedAlready, 0, sizeof mapAddedAlready);
+
 	if (levellistmode == LLM_CREATESERVER)
 	{
 		sprintf(levelselect.rows[0].header, "Gametype");
@@ -5251,31 +5301,75 @@ static boolean M_PrepareLevelPlatter(INT32 gt, boolean nextmappick)
 		char_notes = NULL;
 	}
 
-	while (mapnum < NUMMAPS)
+	for (mapIterate = 0; mapIterate < NUMMAPS; mapIterate++)
 	{
-		if (M_CanShowLevelOnPlatter(mapnum, gt))
+		INT32 headerRow = -1;
+		boolean anyAvailable = false;
+		boolean forceNewRow = true;
+
+		if (mapAddedAlready[mapIterate] == true)
+		{
+			// Already added under another heading
+			continue;
+		}
+
+		if (M_CanShowLevelOnPlatter(mapIterate, gt) == false)
 		{
-			const UINT8 actnum = mapheaderinfo[mapnum]->actnum;
-			const boolean headingisname = (fastcmp(mapheaderinfo[mapnum]->selectheading, mapheaderinfo[mapnum]->lvlttl));
-			const boolean wide = (mapheaderinfo[mapnum]->menuflags & LF2_WIDEICON);
+			// Don't show this one
+			continue;
+		}
+
+		for (headingIterate = mapIterate; headingIterate < NUMMAPS; headingIterate++)
+		{
+			UINT8 actnum = 0;
+			boolean headingisname = false;
+			boolean wide = false;
+
+			if (mapAddedAlready[headingIterate] == true)
+			{
+				// Already added under another heading
+				continue;
+			}
+
+			if (M_CanShowLevelOnPlatter(headingIterate, gt) == false)
+			{
+				// Don't show this one
+				continue;
+			}
+
+			if (!fastcmp(mapheaderinfo[mapIterate]->selectheading, mapheaderinfo[headingIterate]->selectheading))
+			{
+				// Headers don't match
+				continue;
+			}
+
+			actnum = mapheaderinfo[headingIterate]->actnum;
+			headingisname = (fastcmp(mapheaderinfo[headingIterate]->selectheading, mapheaderinfo[headingIterate]->lvlttl));
+			wide = (mapheaderinfo[headingIterate]->menuflags & LF2_WIDEICON);
 
 			// preparing next position to drop mapnum into
 			if (levelselect.rows[startrow].maplist[0])
 			{
 				if (col == 2 // no more space on the row?
-				|| wide
-				|| (mapheaderinfo[prevmapnum]->menuflags & LF2_WIDEICON)
-				|| !(fastcmp(mapheaderinfo[mapnum]->selectheading, mapheaderinfo[prevmapnum]->selectheading))) // a new heading is starting?
+					|| wide || forceNewRow)
 				{
 					col = 0;
 					row++;
 				}
 				else
+				{
 					col++;
+				}
+			}
+
+			if (headerRow == -1)
+			{
+				// Set where the header row is meant to be
+				headerRow = row;
 			}
 
-			levelselect.rows[row].maplist[col] = mapnum+1; // putting the map on the platter
-			levelselect.rows[row].mapavailable[col] = M_LevelAvailableOnPlatter(mapnum);
+			levelselect.rows[row].maplist[col] = headingIterate+1; // putting the map on the platter
+			levelselect.rows[row].mapavailable[col] = M_LevelAvailableOnPlatter(headingIterate);
 
 			if ((lswide(row) = wide)) // intentionally assignment
 			{
@@ -5283,7 +5377,7 @@ static boolean M_PrepareLevelPlatter(INT32 gt, boolean nextmappick)
 				levelselect.rows[row].mapavailable[2] = levelselect.rows[row].mapavailable[1] = levelselect.rows[row].mapavailable[0];
 			}
 
-			if (nextmappick && cv_nextmap.value == mapnum+1) // A little quality of life improvement.
+			if (nextmappick && cv_nextmap.value == headingIterate+1) // A little quality of life improvement.
 			{
 				lsrow = row;
 				lscol = col;
@@ -5292,6 +5386,8 @@ static boolean M_PrepareLevelPlatter(INT32 gt, boolean nextmappick)
 			// individual map name
 			if (levelselect.rows[row].mapavailable[col])
 			{
+				anyAvailable = true;
+
 				if (headingisname)
 				{
 					if (actnum)
@@ -5302,7 +5398,7 @@ static boolean M_PrepareLevelPlatter(INT32 gt, boolean nextmappick)
 				else if (wide)
 				{
 					// Yes, with LF2_WIDEICON it'll continue on over into the next 17+1 char block. That's alright; col is always zero, the string is contiguous, and the maximum length is lvlttl[22] + ' ' + ZONE + ' ' + INT32, which is about 39 or so - barely crossing into the third column.
-					char* mapname = G_BuildMapTitle(mapnum+1);
+					char* mapname = G_BuildMapTitle(headingIterate+1);
 					strcpy(levelselect.rows[row].mapnames[col], (const char *)mapname);
 					Z_Free(mapname);
 				}
@@ -5311,9 +5407,9 @@ static boolean M_PrepareLevelPlatter(INT32 gt, boolean nextmappick)
 					char mapname[22+1+11]; // lvlttl[22] + ' ' + INT32
 
 					if (actnum)
-						sprintf(mapname, "%s %d", mapheaderinfo[mapnum]->lvlttl, actnum);
+						sprintf(mapname, "%s %d", mapheaderinfo[headingIterate]->lvlttl, actnum);
 					else
-						strcpy(mapname, mapheaderinfo[mapnum]->lvlttl);
+						strcpy(mapname, mapheaderinfo[headingIterate]->lvlttl);
 
 					if (strlen(mapname) >= 17)
 						strcpy(mapname+17-3, "...");
@@ -5322,27 +5418,36 @@ static boolean M_PrepareLevelPlatter(INT32 gt, boolean nextmappick)
 				}
 			}
 			else
-				sprintf(levelselect.rows[row].mapnames[col], "???");
-
-			// creating header text
-			if (!col && ((row == startrow) || !(fastcmp(mapheaderinfo[mapnum]->selectheading, mapheaderinfo[levelselect.rows[row-1].maplist[0]-1]->selectheading))))
 			{
-				if (!levelselect.rows[row].mapavailable[col])
-					sprintf(levelselect.rows[row].header, "???");
-				else
-				{
-					sprintf(levelselect.rows[row].header, "%s", mapheaderinfo[mapnum]->selectheading);
-					if (!(mapheaderinfo[mapnum]->levelflags & LF_NOZONE) && headingisname)
-					{
-						sprintf(levelselect.rows[row].header + strlen(levelselect.rows[row].header), " ZONE");
-					}
-				}
+				sprintf(levelselect.rows[row].mapnames[col], "???");
 			}
 
-			prevmapnum = mapnum;
+			// Done adding this one
+			mapAddedAlready[headingIterate] = true;
+			forceNewRow = wide;
 		}
 
-		mapnum++;
+		if (headerRow == -1)
+		{
+			// Shouldn't happen
+			continue;
+		}
+
+		// creating header text
+		if (anyAvailable == false)
+		{
+			sprintf(levelselect.rows[headerRow].header, "???");
+		}
+		else
+		{
+			sprintf(levelselect.rows[headerRow].header, "%s", mapheaderinfo[mapIterate]->selectheading);
+
+			if (!(mapheaderinfo[mapIterate]->levelflags & LF_NOZONE)
+				&& fastcmp(mapheaderinfo[mapIterate]->selectheading, mapheaderinfo[mapIterate]->lvlttl))
+			{
+				sprintf(levelselect.rows[headerRow].header + strlen(levelselect.rows[headerRow].header), " ZONE");
+			}
+		}
 	}
 
 #ifdef SYMMETRICAL_PLATTER
@@ -6234,8 +6339,8 @@ static void M_AddonsOptions(INT32 choice)
 	M_SetupNextMenu(&OP_AddonsOptionsDef);
 }
 
-#define LOCATIONSTRING1 "Visit \x83SRB2.ORG/MODS\x80 to get & make add-ons!"
-//#define LOCATIONSTRING2 "Visit \x88SRB2.ORG/MODS\x80 to get & make add-ons!"
+#define LOCATIONSTRING1 "Visit \x83SRB2.ORG/ADDONS\x80 to get & make addons!"
+//#define LOCATIONSTRING2 "Visit \x88SRB2.ORG/ADDONS\x80 to get & make addons!"
 
 static void M_LoadAddonsPatches(void)
 {
@@ -6307,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))
@@ -6354,6 +6460,7 @@ static void M_DrawTemperature(INT32 x, fixed_t t)
 #undef vpadding
 #undef h
 #undef NUMCOLOURS
+#endif
 
 static char *M_AddonsHeaderPath(void)
 {
@@ -6447,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;
@@ -6937,8 +7043,7 @@ static void M_SelectableClearMenus(INT32 choice)
 static void M_UltimateCheat(INT32 choice)
 {
 	(void)choice;
-	if (Playing())
-		LUAh_GameQuit();
+	LUA_HookBool(true, HOOK(GameQuit));
 	I_Quit();
 }
 
@@ -7085,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");
 
@@ -8423,7 +8535,7 @@ static void M_DrawLoadGameData(void)
 				sprdef = &charbotskin->sprites[SPR2_SIGN];
 				if (!sprdef->numframes)
 					goto skipbot;
-				colormap = R_GetTranslationColormap(savegameinfo[savetodraw].botskin-1, charbotskin->prefcolor, 0);
+				colormap = R_GetTranslationColormap(savegameinfo[savetodraw].botskin-1, charbotskin->prefcolor, GTC_CACHE);
 				sprframe = &sprdef->spriteframes[0];
 				patch = W_CachePatchNum(sprframe->lumppat[0], PU_PATCH);
 
@@ -8433,8 +8545,6 @@ static void M_DrawLoadGameData(void)
 					charbotskin->highresscale,
 					0, patch, colormap);
 
-				Z_Free(colormap);
-
 				tempx -= (20<<FRACBITS);
 				//flip = V_FLIP;
 			}
@@ -8443,7 +8553,7 @@ skipbot:
 			if (!charskin) // shut up compiler
 				goto skipsign;
 			sprdef = &charskin->sprites[SPR2_SIGN];
-			colormap = R_GetTranslationColormap(savegameinfo[savetodraw].skinnum, charskin->prefcolor, 0);
+			colormap = R_GetTranslationColormap(savegameinfo[savetodraw].skinnum, charskin->prefcolor, GTC_CACHE);
 			if (!sprdef->numframes)
 				goto skipsign;
 			sprframe = &sprdef->spriteframes[0];
@@ -8483,8 +8593,6 @@ skipsign:
 				charskin->highresscale/2,
 				0, patch, colormap);
 skiplife:
-			if (colormap)
-				Z_Free(colormap);
 
 			patch = W_CachePatchName("STLIVEX", PU_PATCH);
 
@@ -8554,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.");
+	}
 }
 
 //
@@ -8585,7 +8699,7 @@ static void M_LoadSelect(INT32 choice)
 
 #define VERSIONSIZE 16
 #define BADSAVE { savegameinfo[slot].lives = -666; Z_Free(savebuffer); return; }
-#define CHECKPOS if (save_p >= end_p) BADSAVE
+#define CHECKPOS if (sav_p >= end_p) BADSAVE
 // Reads the save file to list lives, level, player, etc.
 // Tails 05-29-2003
 static void M_ReadSavegameInfo(UINT32 slot)
@@ -8594,10 +8708,13 @@ static void M_ReadSavegameInfo(UINT32 slot)
 	char savename[255];
 	UINT8 *savebuffer;
 	UINT8 *end_p; // buffer end point, don't read past here
-	UINT8 *save_p;
+	UINT8 *sav_p;
 	INT32 fake; // Dummy variable
 	char temp[sizeof(timeattackfolder)];
 	char vcheck[VERSIONSIZE];
+#ifdef NEWSKINSAVES
+	INT16 backwardsCompat = 0;
+#endif
 
 	sprintf(savename, savegamename, slot);
 
@@ -8613,19 +8730,19 @@ static void M_ReadSavegameInfo(UINT32 slot)
 	end_p = savebuffer + length;
 
 	// skip the description field
-	save_p = savebuffer;
+	sav_p = savebuffer;
 
 	// Version check
 	memset(vcheck, 0, sizeof (vcheck));
 	sprintf(vcheck, "version %d", VERSION);
-	if (strcmp((const char *)save_p, (const char *)vcheck)) BADSAVE
-	save_p += VERSIONSIZE;
+	if (strcmp((const char *)sav_p, (const char *)vcheck)) BADSAVE
+	sav_p += VERSIONSIZE;
 
 	// dearchive all the modifications
 	// P_UnArchiveMisc()
 
 	CHECKPOS
-	fake = READINT16(save_p);
+	fake = READINT16(sav_p);
 
 	if (((fake-1) & 8191) >= NUMMAPS) BADSAVE
 
@@ -8642,54 +8759,84 @@ static void M_ReadSavegameInfo(UINT32 slot)
 	savegameinfo[slot].gamemap = fake;
 
 	CHECKPOS
-	savegameinfo[slot].numemeralds = READUINT16(save_p)-357; // emeralds
+	savegameinfo[slot].numemeralds = READUINT16(sav_p)-357; // emeralds
 
 	CHECKPOS
-	READSTRINGN(save_p, temp, sizeof(temp)); // mod it belongs to
+	READSTRINGN(sav_p, temp, sizeof(temp)); // mod it belongs to
 
 	if (strcmp(temp, timeattackfolder)) BADSAVE
 
 	// P_UnArchivePlayer()
+#ifdef NEWSKINSAVES
 	CHECKPOS
-	fake = READUINT16(save_p);
-	savegameinfo[slot].skinnum = fake & ((1<<5) - 1);
-	if (savegameinfo[slot].skinnum >= numskins
-	|| !R_SkinUsable(-1, savegameinfo[slot].skinnum))
-		BADSAVE
-	savegameinfo[slot].botskin = fake >> 5;
-	if (savegameinfo[slot].botskin-1 >= numskins
-	|| !R_SkinUsable(-1, savegameinfo[slot].botskin-1))
-		BADSAVE
+	backwardsCompat = READUINT16(sav_p);
+
+	if (backwardsCompat != NEWSKINSAVES)
+	{
+		// Backwards compat
+		savegameinfo[slot].skinnum = backwardsCompat & ((1<<5) - 1);
+
+		if (savegameinfo[slot].skinnum >= numskins
+		|| !R_SkinUsable(-1, savegameinfo[slot].skinnum))
+			BADSAVE
+
+		savegameinfo[slot].botskin = backwardsCompat >> 5;
+		if (savegameinfo[slot].botskin-1 >= numskins
+		|| !R_SkinUsable(-1, savegameinfo[slot].botskin-1))
+			BADSAVE
+	}
+	else
+#endif
+	{
+		char ourSkinName[SKINNAMESIZE+1];
+		char botSkinName[SKINNAMESIZE+1];
+
+		CHECKPOS
+		READSTRINGN(sav_p, ourSkinName, SKINNAMESIZE);
+		savegameinfo[slot].skinnum = R_SkinAvailable(ourSkinName);
+
+		if (savegameinfo[slot].skinnum >= numskins
+		|| !R_SkinUsable(-1, savegameinfo[slot].skinnum))
+			BADSAVE
+
+		CHECKPOS
+		READSTRINGN(sav_p, botSkinName, SKINNAMESIZE);
+		savegameinfo[slot].botskin = (R_SkinAvailable(botSkinName) + 1);
+
+		if (savegameinfo[slot].botskin-1 >= numskins
+		|| !R_SkinUsable(-1, savegameinfo[slot].botskin-1))
+			BADSAVE
+	}
 
 	CHECKPOS
-	savegameinfo[slot].numgameovers = READUINT8(save_p); // numgameovers
+	savegameinfo[slot].numgameovers = READUINT8(sav_p); // numgameovers
 	CHECKPOS
-	savegameinfo[slot].lives = READSINT8(save_p); // lives
+	savegameinfo[slot].lives = READSINT8(sav_p); // lives
 	CHECKPOS
-	savegameinfo[slot].continuescore = READINT32(save_p); // score
+	savegameinfo[slot].continuescore = READINT32(sav_p); // score
 	CHECKPOS
-	fake = READINT32(save_p); // continues
+	fake = READINT32(sav_p); // continues
 	if (useContinues)
 		savegameinfo[slot].continuescore = fake;
 
 	// File end marker check
 	CHECKPOS
-	switch (READUINT8(save_p))
+	switch (READUINT8(sav_p))
 	{
 		case 0xb7:
 			{
 				UINT8 i, banksinuse;
 				CHECKPOS
-				banksinuse = READUINT8(save_p);
+				banksinuse = READUINT8(sav_p);
 				CHECKPOS
 				if (banksinuse > NUM_LUABANKS)
 					BADSAVE
 				for (i = 0; i < banksinuse; i++)
 				{
-					(void)READINT32(save_p);
+					(void)READINT32(sav_p);
 					CHECKPOS
 				}
-				if (READUINT8(save_p) != 0x1d)
+				if (READUINT8(sav_p) != 0x1d)
 					BADSAVE
 			}
 		case 0x1d:
@@ -8822,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);
@@ -8968,7 +9115,7 @@ static void M_CacheCharacterSelectEntry(INT32 i, INT32 skinnum)
 
 static UINT8 M_SetupChoosePlayerDirect(INT32 choice)
 {
-	INT32 skinnum;
+	INT32 skinnum, botskinnum;
 	UINT8 i;
 	UINT8 firstvalid = 255, lastvalid = 255;
 	boolean allowed = false;
@@ -9000,6 +9147,13 @@ static UINT8 M_SetupChoosePlayerDirect(INT32 choice)
 				skinnum = description[i].skinnum[0];
 				if ((skinnum != -1) && (R_SkinUsable(-1, skinnum)))
 				{
+					botskinnum = description[i].skinnum[1];
+					if ((botskinnum != -1) && (!R_SkinUsable(-1, botskinnum)))
+					{
+						// Bot skin isn't unlocked
+						continue;
+					}
+
 					// Handling order.
 					if (firstvalid == 255)
 						firstvalid = i;
@@ -11424,9 +11578,9 @@ static void M_ServerOptions(INT32 choice)
 		OP_ServerOptionsMenu[ 2].status = IT_GRAYEDOUT; // Max players
 		OP_ServerOptionsMenu[ 3].status = IT_GRAYEDOUT; // Allow add-on downloading
 		OP_ServerOptionsMenu[ 4].status = IT_GRAYEDOUT; // Allow players to join
-		OP_ServerOptionsMenu[35].status = IT_GRAYEDOUT; // Master server
-		OP_ServerOptionsMenu[36].status = IT_GRAYEDOUT; // Minimum delay between joins
-		OP_ServerOptionsMenu[37].status = IT_GRAYEDOUT; // Attempts to resynchronise
+		OP_ServerOptionsMenu[36].status = IT_GRAYEDOUT; // Master server
+		OP_ServerOptionsMenu[37].status = IT_GRAYEDOUT; // Minimum delay between joins
+		OP_ServerOptionsMenu[38].status = IT_GRAYEDOUT; // Attempts to resynchronise
 	}
 	else
 	{
@@ -11434,11 +11588,11 @@ static void M_ServerOptions(INT32 choice)
 		OP_ServerOptionsMenu[ 2].status = IT_STRING | IT_CVAR;
 		OP_ServerOptionsMenu[ 3].status = IT_STRING | IT_CVAR;
 		OP_ServerOptionsMenu[ 4].status = IT_STRING | IT_CVAR;
-		OP_ServerOptionsMenu[35].status = (netgame
+		OP_ServerOptionsMenu[36].status = (netgame
 			? IT_GRAYEDOUT
 			: (IT_STRING | IT_CVAR | IT_CV_STRING));
-		OP_ServerOptionsMenu[36].status = IT_STRING | IT_CVAR;
 		OP_ServerOptionsMenu[37].status = IT_STRING | IT_CVAR;
+		OP_ServerOptionsMenu[38].status = IT_STRING | IT_CVAR;
 	}
 #endif
 
@@ -11755,7 +11909,7 @@ static void M_DrawSetupMultiPlayerMenu(void)
 		goto faildraw;
 
 	// ok, draw player sprite for sure now
-	colormap = R_GetTranslationColormap(setupm_fakeskin, setupm_fakecolor->color, 0);
+	colormap = R_GetTranslationColormap(setupm_fakeskin, setupm_fakecolor->color, GTC_CACHE);
 
 	if (multi_frame >= sprdef->numframes)
 		multi_frame = 0;
@@ -11773,7 +11927,6 @@ static void M_DrawSetupMultiPlayerMenu(void)
 		FixedDiv(skins[setupm_fakeskin].highresscale, skins[setupm_fakeskin].shieldscale),
 		flags, patch, colormap);
 
-	Z_Free(colormap);
 	goto colordraw;
 
 faildraw:
@@ -12689,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]));
 
 
 			}
@@ -12722,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)
@@ -12741,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:
@@ -12792,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
@@ -12929,7 +13082,7 @@ static void M_VideoModeMenu(INT32 choice)
 
 	memset(modedescs, 0, sizeof(modedescs));
 
-#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
+#if defined (__unix__) || defined (UNIXCOMMON) || defined (HAVE_SDL)
 	VID_PrepareModeList(); // FIXME: hack
 #endif
 	vidm_nummodes = 0;
@@ -13372,8 +13525,7 @@ void M_QuitResponse(INT32 ch)
 
 	if (ch != 'y' && ch != KEY_ENTER)
 		return;
-	if (Playing())
-		LUAh_GameQuit();
+	LUA_HookBool(true, HOOK(GameQuit));
 	if (!(netgame || cv_debug))
 	{
 		S_ResetCaptions();
diff --git a/src/m_menu.h b/src/m_menu.h
index 0465128ef75063b57cd3849bb6faab251e45e139..ba9c326a00eb42d0e9f9dfa5330ad69b4e886ddb 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -3,7 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 2011-2016 by Matthew "Kaito Sinclaire" Walsh.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_misc.c b/src/m_misc.c
index d97d8f94be36972a82880f79316ac8d2f7149131..59783d5d30dc8d195d5b732d2de474833459f816 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -64,8 +64,6 @@ typedef off_t off64_t;
 #define PRIdS "u"
 #elif defined (_WIN32)
 #define PRIdS "Iu"
-#elif defined (DJGPP)
-#define PRIdS "u"
 #else
 #define PRIdS "zu"
 #endif
@@ -163,6 +161,11 @@ consvar_t cv_zlib_levela = CVAR_INIT ("apng_compress_level", "4", CV_SAVE, zlib_
 consvar_t cv_zlib_strategya = CVAR_INIT ("apng_strategy", "RLE", CV_SAVE, zlib_strategy_t, NULL);
 consvar_t cv_zlib_window_bitsa = CVAR_INIT ("apng_window_size", "32k", CV_SAVE, zlib_window_bits_t, NULL);
 consvar_t cv_apng_delay = CVAR_INIT ("apng_speed", "1x", CV_SAVE, apng_delay_t, NULL);
+consvar_t cv_apng_downscale = CVAR_INIT ("apng_downscale", "On", CV_SAVE, CV_OnOff, NULL);
+
+#ifdef USE_APNG
+static boolean apng_downscale = false; // So nobody can do something dumb like changing cvars mid output
+#endif
 
 boolean takescreenshot = false; // Take a screenshot this tic
 
@@ -981,25 +984,38 @@ static inline boolean M_PNGLib(void)
 
 static void M_PNGFrame(png_structp png_ptr, png_infop png_info_ptr, png_bytep png_buf)
 {
+	png_uint_16 downscale = apng_downscale ? vid.dupx : 1;
+
 	png_uint_32 pitch = png_get_rowbytes(png_ptr, png_info_ptr);
-	PNG_CONST png_uint_32 height = vid.height;
-	png_bytepp row_pointers = png_malloc(png_ptr, height* sizeof (png_bytep));
-	png_uint_32 y;
+	PNG_CONST png_uint_32 width = vid.width / downscale;
+	PNG_CONST png_uint_32 height = vid.height / downscale;
+	png_bytepp row_pointers = png_malloc(png_ptr, height * sizeof (png_bytep));
+	png_uint_32 x, y;
 	png_uint_16 framedelay = (png_uint_16)cv_apng_delay.value;
 
 	apng_frames++;
 
 	for (y = 0; y < height; y++)
 	{
-		row_pointers[y] = png_buf;
-		png_buf += pitch;
+		row_pointers[y] = malloc(pitch * sizeof(png_byte));
+		for (x = 0; x < width; x++)
+			row_pointers[y][x] = png_buf[x * downscale];
+		png_buf += pitch * (downscale * downscale);
 	}
+		//for (x = 0; x < width; x++)
+		//{
+		//	printf("%d", x);
+		//	row_pointers[y][x] = 0;
+		//}
+	/*	row_pointers[y] = calloc(1, sizeof(png_bytep));
+		png_buf += pitch * 2;
+	}*/
 
 #ifndef PNG_STATIC
 	if (aPNG_write_frame_head)
 #endif
 		aPNG_write_frame_head(apng_ptr, apng_info_ptr, row_pointers,
-			vid.width, /* width */
+			width,     /* width */
 			height,    /* height */
 			0,         /* x offset */
 			0,         /* y offset */
@@ -1030,6 +1046,12 @@ static void M_PNGfix_acTL(png_structp png_ptr, png_infop png_info_ptr,
 
 static boolean M_SetupaPNG(png_const_charp filename, png_bytep pal)
 {
+	png_uint_16 downscale;
+
+	apng_downscale = (!!cv_apng_downscale.value);
+
+	downscale = apng_downscale ? vid.dupx : 1;
+
 	apng_FILE = fopen(filename,"wb+"); // + mode for reading
 	if (!apng_FILE)
 	{
@@ -1080,7 +1102,7 @@ static boolean M_SetupaPNG(png_const_charp filename, png_bytep pal)
 	png_set_compression_strategy(apng_ptr, cv_zlib_strategya.value);
 	png_set_compression_window_bits(apng_ptr, cv_zlib_window_bitsa.value);
 
-	M_PNGhdr(apng_ptr, apng_info_ptr, vid.width, vid.height, pal);
+	M_PNGhdr(apng_ptr, apng_info_ptr, vid.width / downscale, vid.height / downscale, pal);
 
 	M_PNGText(apng_ptr, apng_info_ptr, true);
 
@@ -1609,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;
@@ -2666,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 dbded37d0a47fdc81c0488098e5bf7ac8e677143..82ccd58c75738ef121e0697ba0a2dc6177906e83 100644
--- a/src/m_misc.h
+++ b/src/m_misc.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -33,7 +33,7 @@ extern consvar_t cv_screenshot_option, cv_screenshot_folder, cv_screenshot_color
 extern consvar_t cv_moviemode, cv_movie_folder, cv_movie_option;
 extern consvar_t cv_zlib_memory, cv_zlib_level, cv_zlib_strategy, cv_zlib_window_bits;
 extern consvar_t cv_zlib_memorya, cv_zlib_levela, cv_zlib_strategya, cv_zlib_window_bitsa;
-extern consvar_t cv_apng_delay;
+extern consvar_t cv_apng_delay, cv_apng_downscale;
 
 void M_StartMovie(void);
 void M_SaveFrame(void);
@@ -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 085adda80dac925917e2f86ec3a14ec4ec5c0590..439a9da1cd009106bc80fa4f3c81e3337a0acbdf 100644
--- a/src/m_perfstats.c
+++ b/src/m_perfstats.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020 by Sonic Team Junior.
+// Copyright (C) 2020-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -22,519 +22,802 @@
 #include "hardware/hw_main.h"
 #endif
 
-int ps_tictime = 0;
+struct perfstatrow;
 
-int ps_playerthink_time = 0;
-int ps_thinkertime = 0;
+typedef struct perfstatrow perfstatrow_t;
 
-int ps_thlist_times[NUM_THINKERLISTS];
-static const char* thlist_names[] = {
-	"Polyobjects:     %d",
-	"Main:            %d",
-	"Mobjs:           %d",
-	"Dynamic slopes:  %d",
-	"Precipitation:   %d",
-	NULL
+struct perfstatrow {
+	const char  * lores_label;
+	const char  * hires_label;
+	ps_metric_t * metric;
+	UINT8         flags;
 };
-static const char* thlist_shortnames[] = {
-	"plyobjs %d",
-	"main    %d",
-	"mobjs   %d",
-	"dynslop %d",
-	"precip  %d",
-	NULL
+
+// 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}
+};
+
+perfstatrow_t gamelogicbrief_row[] = {
+	{"logic  ", "Game logic:    ", &ps_tictime, PS_TIME},
+	{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}
+};
+
+#ifdef HWRENDER
+perfstatrow_t batchcount_rows[] = {
+	{"polygon", "Polygons:  ", &ps_hw_numpolys, 0},
+	{"vertex ", "Vertices:  ", &ps_hw_numverts, 0},
+	{0}
+};
+
+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
+
+// 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}
+};
+
+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}
 };
 
-int ps_checkposition_calls = 0;
+perfstatrow_t misc_calls_rows[] = {
+	{"lmhook", "Lua mobj hooks: ", &ps_lua_mobjhooks, PS_LEVEL},
+	{"chkpos", "P_CheckPosition:", &ps_checkposition_calls, PS_LEVEL},
+	{0}
+};
 
-int ps_lua_thinkframe_time = 0;
-int ps_lua_mobjhooks = 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;
 
-void PS_SetThinkFrameHookInfo(int index, UINT32 time_taken, char* short_src)
+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;
 }
 
-void M_DrawPerfStats(void)
+static boolean PS_HighResolution(void)
 {
-	char s[100];
-	int currenttime = I_GetTimeMicros();
-	int frametime = currenttime - ps_prevframetime;
-	ps_prevframetime = currenttime;
+	return (vid.width >= 640 && vid.height >= 400);
+}
 
-	if (cv_perfstats.value == 1) // rendering
+static boolean PS_IsLevelActive(void)
+{
+	return gamestate == GS_LEVEL ||
+			(gamestate == GS_TITLESCREEN && titlemapinaction);
+}
+
+// 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));
+}
+
+// Should the row be visible on the screen?
+static boolean PS_IsRowVisible(perfstatrow_t *row)
+{
+	boolean value_is_zero;
+
+	if (row->flags & PS_TIME)
+		value_is_zero = row->metric->value.p == 0;
+	else
+		value_is_zero = row->metric->value.i == 0;
+
+	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 (vid.width < 640 || vid.height < 400) // low resolution
+		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))
 		{
-			snprintf(s, sizeof s - 1, "frmtime %d", frametime);
-			V_DrawThinString(20, 10, V_MONOSPACE | V_YELLOWMAP, s);
-			if (!(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
-			{
-				snprintf(s, sizeof s - 1, "ui      %d", ps_uitime);
-				V_DrawThinString(20, 18, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "finupdt %d", ps_swaptime);
-				V_DrawThinString(20, 26, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "logic   %d", ps_tictime);
-				V_DrawThinString(20, 38, V_MONOSPACE | V_GRAYMAP, s);
-				return;
-			}
-			snprintf(s, sizeof s - 1, "drwtime %d", ps_rendercalltime);
-			V_DrawThinString(20, 18, V_MONOSPACE | V_YELLOWMAP, s);
-			snprintf(s, sizeof s - 1, "bspcall %d", ps_numbspcalls);
-			V_DrawThinString(90, 10, V_MONOSPACE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "sprites %d", ps_numsprites);
-			V_DrawThinString(90, 18, V_MONOSPACE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "drwnode %d", ps_numdrawnodes);
-			V_DrawThinString(90, 26, V_MONOSPACE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "plyobjs %d", ps_numpolyobjects);
-			V_DrawThinString(90, 34, V_MONOSPACE | V_BLUEMAP, s);
-#ifdef HWRENDER
-			if (rendermode == render_opengl) // OpenGL specific stats
-			{
-				snprintf(s, sizeof s - 1, "skybox  %d", ps_hw_skyboxtime);
-				V_DrawThinString(24, 26, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "bsptime %d", ps_bsptime);
-				V_DrawThinString(24, 34, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "nodesrt %d", ps_hw_nodesorttime);
-				V_DrawThinString(24, 42, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "nodedrw %d", ps_hw_nodedrawtime);
-				V_DrawThinString(24, 50, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "sprsort %d", ps_hw_spritesorttime);
-				V_DrawThinString(24, 58, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "sprdraw %d", ps_hw_spritedrawtime);
-				V_DrawThinString(24, 66, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "other   %d",
-					ps_rendercalltime - ps_hw_skyboxtime - ps_bsptime - ps_hw_nodesorttime
-					- ps_hw_nodedrawtime - ps_hw_spritesorttime - ps_hw_spritedrawtime
-					- ps_hw_batchsorttime - ps_hw_batchdrawtime);
-				V_DrawThinString(24, 74, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "ui      %d", ps_uitime);
-				V_DrawThinString(20, 82, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "finupdt %d", ps_swaptime);
-				V_DrawThinString(20, 90, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "logic   %d", ps_tictime);
-				V_DrawThinString(20, 102, V_MONOSPACE | V_GRAYMAP, s);
-				if (cv_glbatching.value)
-				{
-					snprintf(s, sizeof s - 1, "batsort %d", ps_hw_batchsorttime);
-					V_DrawThinString(90, 46, V_MONOSPACE | V_REDMAP, s);
-					snprintf(s, sizeof s - 1, "batdraw %d", ps_hw_batchdrawtime);
-					V_DrawThinString(90, 54, V_MONOSPACE | V_REDMAP, s);
-
-					snprintf(s, sizeof s - 1, "polygon %d", ps_hw_numpolys);
-					V_DrawThinString(155, 10, V_MONOSPACE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "drwcall %d", ps_hw_numcalls);
-					V_DrawThinString(155, 18, V_MONOSPACE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "shaders %d", ps_hw_numshaders);
-					V_DrawThinString(155, 26, V_MONOSPACE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "vertex  %d", ps_hw_numverts);
-					V_DrawThinString(155, 34, V_MONOSPACE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "texture %d", ps_hw_numtextures);
-					V_DrawThinString(220, 10, V_MONOSPACE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "polyflg %d", ps_hw_numpolyflags);
-					V_DrawThinString(220, 18, V_MONOSPACE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "colors  %d", ps_hw_numcolors);
-					V_DrawThinString(220, 26, V_MONOSPACE | V_PURPLEMAP, s);
-				}
-				else
-				{
-					// reset these vars so the "other" measurement isn't off
-					ps_hw_batchsorttime = 0;
-					ps_hw_batchdrawtime = 0;
-				}
-			}
-			else // software specific stats
-#endif
-			{
-				snprintf(s, sizeof s - 1, "bsptime %d", ps_bsptime);
-				V_DrawThinString(24, 26, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "sprclip %d", ps_sw_spritecliptime);
-				V_DrawThinString(24, 34, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "portals %d", ps_sw_portaltime);
-				V_DrawThinString(24, 42, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "planes  %d", ps_sw_planetime);
-				V_DrawThinString(24, 50, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "masked  %d", ps_sw_maskedtime);
-				V_DrawThinString(24, 58, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "other   %d",
-					ps_rendercalltime - ps_bsptime - ps_sw_spritecliptime
-					- ps_sw_portaltime - ps_sw_planetime - ps_sw_maskedtime);
-				V_DrawThinString(24, 66, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "ui      %d", ps_uitime);
-				V_DrawThinString(20, 74, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "finupdt %d", ps_swaptime);
-				V_DrawThinString(20, 82, V_MONOSPACE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "logic   %d", ps_tictime);
-				V_DrawThinString(20, 94, V_MONOSPACE | V_GRAYMAP, s);
-			}
+			found_value = value;
 		}
-		else // high resolution
+		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));
+}
+
+// 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 = rows; row->lores_label; ++row)
+	{
+		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)
 		{
-			snprintf(s, sizeof s - 1, "Frame time:     %d", frametime);
-			V_DrawSmallString(20, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-			if (!(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
-			{
-				snprintf(s, sizeof s - 1, "UI render:      %d", ps_uitime);
-				V_DrawSmallString(20, 15, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "I_FinishUpdate: %d", ps_swaptime);
-				V_DrawSmallString(20, 20, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "Game logic:     %d", ps_tictime);
-				V_DrawSmallString(20, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
-				return;
-			}
-			snprintf(s, sizeof s - 1, "3d rendering:   %d", ps_rendercalltime);
-			V_DrawSmallString(20, 15, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-			snprintf(s, sizeof s - 1, "BSP calls:    %d", ps_numbspcalls);
-			V_DrawSmallString(115, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "Sprites:      %d", ps_numsprites);
-			V_DrawSmallString(115, 15, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "Drawnodes:    %d", ps_numdrawnodes);
-			V_DrawSmallString(115, 20, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "Polyobjects:  %d", ps_numpolyobjects);
-			V_DrawSmallString(115, 25, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
+			V_DrawSmallString(x, draw_y, draw_flags, final_str);
+			draw_y += 5;
+		}
+		else
+		{
+			V_DrawThinString(x, draw_y, draw_flags, final_str);
+			draw_y += 8;
+		}
+	}
+
+	return draw_y;
+}
+
+static void PS_UpdateMetricHistory(ps_metric_t *metric, boolean time_metric, boolean frame_metric, boolean set_user)
+{
+	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 PS_UpdateRowHistories(perfstatrow_t *rows, boolean frame_metric)
+{
+	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);
+	}
+}
+
+// Update all metrics that are calculated on every frame.
+static void PS_UpdateFrameStats(void)
+{
+	// update frame time
+	precise_t currenttime = I_GetPreciseTime();
+	ps_frametime.value.p = currenttime - ps_prevframetime;
+	ps_prevframetime = currenttime;
+
+	// update 3d rendering stats
+	if (PS_IsLevelActive())
+	{
+		// Remember to update this calculation when adding more 3d rendering stats!
+		ps_otherrendertime.value.p = ps_rendercalltime.value.p - ps_bsptime.value.p;
+
 #ifdef HWRENDER
-			if (rendermode == render_opengl) // OpenGL specific stats
+		if (rendermode == render_opengl)
+		{
+			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)
 			{
-				snprintf(s, sizeof s - 1, "Skybox render:  %d", ps_hw_skyboxtime);
-				V_DrawSmallString(24, 20, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "RenderBSPNode:  %d", ps_bsptime);
-				V_DrawSmallString(24, 25, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "Drwnode sort:   %d", ps_hw_nodesorttime);
-				V_DrawSmallString(24, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "Drwnode render: %d", ps_hw_nodedrawtime);
-				V_DrawSmallString(24, 35, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "Sprite sort:    %d", ps_hw_spritesorttime);
-				V_DrawSmallString(24, 40, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "Sprite render:  %d", ps_hw_spritedrawtime);
-				V_DrawSmallString(24, 45, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				// Remember to update this calculation when adding more 3d rendering stats!
-				snprintf(s, sizeof s - 1, "Other:          %d",
-					ps_rendercalltime - ps_hw_skyboxtime - ps_bsptime - ps_hw_nodesorttime
-					- ps_hw_nodedrawtime - ps_hw_spritesorttime - ps_hw_spritedrawtime
-					- ps_hw_batchsorttime - ps_hw_batchdrawtime);
-				V_DrawSmallString(24, 50, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "UI render:      %d", ps_uitime);
-				V_DrawSmallString(20, 55, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "I_FinishUpdate: %d", ps_swaptime);
-				V_DrawSmallString(20, 60, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "Game logic:     %d", ps_tictime);
-				V_DrawSmallString(20, 70, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
-				if (cv_glbatching.value)
-				{
-					snprintf(s, sizeof s - 1, "Batch sort:   %d", ps_hw_batchsorttime);
-					V_DrawSmallString(115, 35, V_MONOSPACE | V_ALLOWLOWERCASE | V_REDMAP, s);
-					snprintf(s, sizeof s - 1, "Batch render: %d", ps_hw_batchdrawtime);
-					V_DrawSmallString(115, 40, V_MONOSPACE | V_ALLOWLOWERCASE | V_REDMAP, s);
-
-					snprintf(s, sizeof s - 1, "Polygons:   %d", ps_hw_numpolys);
-					V_DrawSmallString(200, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "Vertices:   %d", ps_hw_numverts);
-					V_DrawSmallString(200, 15, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "Draw calls: %d", ps_hw_numcalls);
-					V_DrawSmallString(200, 25, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "Shaders:    %d", ps_hw_numshaders);
-					V_DrawSmallString(200, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "Textures:   %d", ps_hw_numtextures);
-					V_DrawSmallString(200, 35, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "Polyflags:  %d", ps_hw_numpolyflags);
-					V_DrawSmallString(200, 40, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
-					snprintf(s, sizeof s - 1, "Colors:     %d", ps_hw_numcolors);
-					V_DrawSmallString(200, 45, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
-				}
-				else
-				{
-					// reset these vars so the "other" measurement isn't off
-					ps_hw_batchsorttime = 0;
-					ps_hw_batchdrawtime = 0;
-				}
+				ps_otherrendertime.value.p -=
+					ps_hw_batchsorttime.value.p +
+					ps_hw_batchdrawtime.value.p;
 			}
-			else // software specific stats
+		}
+		else
 #endif
-			{
-				snprintf(s, sizeof s - 1, "RenderBSPNode:  %d", ps_bsptime);
-				V_DrawSmallString(24, 20, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "R_ClipSprites:  %d", ps_sw_spritecliptime);
-				V_DrawSmallString(24, 25, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "Portals+Skybox: %d", ps_sw_portaltime);
-				V_DrawSmallString(24, 30, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "R_DrawPlanes:   %d", ps_sw_planetime);
-				V_DrawSmallString(24, 35, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "R_DrawMasked:   %d", ps_sw_maskedtime);
-				V_DrawSmallString(24, 40, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				// Remember to update this calculation when adding more 3d rendering stats!
-				snprintf(s, sizeof s - 1, "Other:          %d",
-					ps_rendercalltime - ps_bsptime - ps_sw_spritecliptime
-					- ps_sw_portaltime - ps_sw_planetime - ps_sw_maskedtime);
-				V_DrawSmallString(24, 45, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "UI render:      %d", ps_uitime);
-				V_DrawSmallString(20, 50, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "I_FinishUpdate: %d", ps_swaptime);
-				V_DrawSmallString(20, 55, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-				snprintf(s, sizeof s - 1, "Game logic:     %d", ps_tictime);
-				V_DrawSmallString(20, 65, V_MONOSPACE | V_ALLOWLOWERCASE | V_GRAYMAP, s);
-			}
+		{
+			ps_otherrendertime.value.p -=
+				ps_sw_spritecliptime.value.p +
+				ps_sw_portaltime.value.p +
+				ps_sw_planetime.value.p +
+				ps_sw_maskedtime.value.p;
 		}
 	}
-	else if (cv_perfstats.value == 2) // logic
+
+	if (cv_ps_samplesize.value > 1)
+	{
+		PS_UpdateRowHistories(rendertime_rows, true);
+		if (PS_IsLevelActive())
+			PS_UpdateRowHistories(commoncounter_rows, true);
+
+#ifdef HWRENDER
+		if (rendermode == render_opengl && cv_glbatching.value)
+		{
+			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--;
+	}
+}
+
+// Update thinker counters by iterating the thinker lists.
+static void PS_CountThinkers(void)
+{
+	int i;
+	thinker_t *thinker;
+
+	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++)
 	{
-		int i = 0;
-		thinker_t *thinker;
-		int thinkercount = 0;
-		int polythcount = 0;
-		int mainthcount = 0;
-		int mobjcount = 0;
-		int nothinkcount = 0;
-		int scenerycount = 0;
-		int dynslopethcount = 0;
-		int precipcount = 0;
-		int removecount = 0;
-		// y offset for drawing columns
-		int yoffset1 = 0;
-		int yoffset2 = 0;
-
-		for (i = 0; i < NUM_THINKERLISTS; i++)
+		for (thinker = thlist[i].next; thinker != &thlist[i]; thinker = thinker->next)
 		{
-			for (thinker = thlist[i].next; thinker != &thlist[i]; thinker = thinker->next)
+			ps_thinkercount.value.i++;
+			if (thinker->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+				ps_removecount.value.i++;
+			else if (i == THINK_POLYOBJ)
+				ps_polythcount.value.i++;
+			else if (i == THINK_MAIN)
+				ps_mainthcount.value.i++;
+			else if (i == THINK_MOBJ)
 			{
-				thinkercount++;
-				if (thinker->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
-					removecount++;
-				else if (i == THINK_POLYOBJ)
-					polythcount++;
-				else if (i == THINK_MAIN)
-					mainthcount++;
-				else if (i == THINK_MOBJ)
+				if (thinker->function.acp1 == (actionf_p1)P_MobjThinker)
 				{
-					if (thinker->function.acp1 == (actionf_p1)P_MobjThinker)
-					{
-						mobj_t *mobj = (mobj_t*)thinker;
-						mobjcount++;
-						if (mobj->flags & MF_NOTHINK)
-							nothinkcount++;
-						else if (mobj->flags & MF_SCENERY)
-							scenerycount++;
-					}
+					mobj_t *mobj = (mobj_t*)thinker;
+					ps_mobjcount.value.i++;
+					if (mobj->flags & MF_NOTHINK)
+						ps_nothinkcount.value.i++;
+					else if (mobj->flags & MF_SCENERY)
+						ps_scenerycount.value.i++;
+					else
+						ps_regularcount.value.i++;
 				}
-				else if (i == THINK_DYNSLOPE)
-					dynslopethcount++;
-				else if (i == THINK_PRECIP)
-					precipcount++;
 			}
+			else if (i == THINK_DYNSLOPE)
+				ps_dynslopethcount.value.i++;
+			else if (i == THINK_PRECIP)
+				ps_precipcount.value.i++;
 		}
+	}
+}
 
-		if (vid.width < 640 || vid.height < 400) // low resolution
+// 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())
 		{
-			snprintf(s, sizeof s - 1, "logic   %d", ps_tictime);
-			V_DrawThinString(20, 10, V_MONOSPACE | V_YELLOWMAP, s);
-			if (!(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
-				return;
-			snprintf(s, sizeof s - 1, "plrthnk %d", ps_playerthink_time);
-			V_DrawThinString(24, 18, V_MONOSPACE | V_YELLOWMAP, s);
-			snprintf(s, sizeof s - 1, "thnkers %d", ps_thinkertime);
-			V_DrawThinString(24, 26, V_MONOSPACE | V_YELLOWMAP, s);
-			for (i = 0; i < NUM_THINKERLISTS; i++)
-			{
-				yoffset1 += 8;
-				snprintf(s, sizeof s - 1, thlist_shortnames[i], ps_thlist_times[i]);
-				V_DrawThinString(28, 26+yoffset1, V_MONOSPACE | V_YELLOWMAP, s);
-			}
-			snprintf(s, sizeof s - 1, "lthinkf %d", ps_lua_thinkframe_time);
-			V_DrawThinString(24, 34+yoffset1, V_MONOSPACE | V_YELLOWMAP, s);
-			snprintf(s, sizeof s - 1, "other   %d",
-				ps_tictime - ps_playerthink_time - ps_thinkertime - ps_lua_thinkframe_time);
-			V_DrawThinString(24, 42+yoffset1, V_MONOSPACE | V_YELLOWMAP, s);
-
-			snprintf(s, sizeof s - 1, "thnkers %d", thinkercount);
-			V_DrawThinString(90, 10, V_MONOSPACE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "plyobjs %d", polythcount);
-			V_DrawThinString(94, 18, V_MONOSPACE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "main    %d", mainthcount);
-			V_DrawThinString(94, 26, V_MONOSPACE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "mobjs   %d", mobjcount);
-			V_DrawThinString(94, 34, V_MONOSPACE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "regular %d", mobjcount - scenerycount - nothinkcount);
-			V_DrawThinString(98, 42, V_MONOSPACE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "scenery %d", scenerycount);
-			V_DrawThinString(98, 50, V_MONOSPACE | V_BLUEMAP, s);
-			if (nothinkcount)
-			{
-				snprintf(s, sizeof s - 1, "nothink %d", nothinkcount);
-				V_DrawThinString(98, 58, V_MONOSPACE | V_BLUEMAP, s);
-				yoffset2 += 8;
-			}
-			snprintf(s, sizeof s - 1, "dynslop %d", dynslopethcount);
-			V_DrawThinString(94, 58+yoffset2, V_MONOSPACE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "precip  %d", precipcount);
-			V_DrawThinString(94, 66+yoffset2, V_MONOSPACE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "remove  %d", removecount);
-			V_DrawThinString(94, 74+yoffset2, V_MONOSPACE | V_BLUEMAP, s);
-
-			snprintf(s, sizeof s - 1, "lmhooks %d", ps_lua_mobjhooks);
-			V_DrawThinString(170, 10, V_MONOSPACE | V_PURPLEMAP, s);
-			snprintf(s, sizeof s - 1, "chkpos  %d", ps_checkposition_calls);
-			V_DrawThinString(170, 18, V_MONOSPACE | V_PURPLEMAP, s);
+			ps_otherlogictime.value.p =
+				ps_tictime.value.p -
+				ps_playerthink_time.value.p -
+				ps_thinkertime.value.p -
+				ps_lua_thinkframe_time.value.p;
+
+			PS_CountThinkers();
 		}
-		else // high resolution
+
+		if (cv_ps_samplesize.value > 1)
 		{
-			snprintf(s, sizeof s - 1, "Game logic:      %d", ps_tictime);
-			V_DrawSmallString(20, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-			if (!(gamestate == GS_LEVEL || (gamestate == GS_TITLESCREEN && titlemapinaction)))
-				return;
-			snprintf(s, sizeof s - 1, "P_PlayerThink:   %d", ps_playerthink_time);
-			V_DrawSmallString(24, 15, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-			snprintf(s, sizeof s - 1, "P_RunThinkers:   %d", ps_thinkertime);
-			V_DrawSmallString(24, 20, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-			for (i = 0; i < NUM_THINKERLISTS; i++)
-			{
-				yoffset1 += 5;
-				snprintf(s, sizeof s - 1, thlist_names[i], ps_thlist_times[i]);
-				V_DrawSmallString(28, 20+yoffset1, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-			}
-			snprintf(s, sizeof s - 1, "LUAh_ThinkFrame: %d", ps_lua_thinkframe_time);
-			V_DrawSmallString(24, 25+yoffset1, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-			snprintf(s, sizeof s - 1, "Other:           %d",
-				ps_tictime - ps_playerthink_time - ps_thinkertime - ps_lua_thinkframe_time);
-			V_DrawSmallString(24, 30+yoffset1, V_MONOSPACE | V_ALLOWLOWERCASE | V_YELLOWMAP, s);
-
-			snprintf(s, sizeof s - 1, "Thinkers:        %d", thinkercount);
-			V_DrawSmallString(115, 10+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "Polyobjects:     %d", polythcount);
-			V_DrawSmallString(119, 15+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "Main:            %d", mainthcount);
-			V_DrawSmallString(119, 20+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "Mobjs:           %d", mobjcount);
-			V_DrawSmallString(119, 25+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "Regular:         %d", mobjcount - scenerycount - nothinkcount);
-			V_DrawSmallString(123, 30+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "Scenery:         %d", scenerycount);
-			V_DrawSmallString(123, 35+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
-			if (nothinkcount)
+			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--;
+	}
+}
+
+static void PS_DrawDescriptorHeader(void)
+{
+	if (cv_ps_samplesize.value > 1)
+	{
+		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;
+		}
+
+		if (hires)
+			V_DrawSmallString(x, y, flags, str);
+		else
+			V_DrawThinString(x, y, flags, str);
+	}
+}
+
+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())
+	{
+		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);
+
+	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);
+}
+
+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++)
+	{
+
+#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)
 			{
-				snprintf(s, sizeof s - 1, "Nothink:         %d", nothinkcount);
-				V_DrawSmallString(123, 40+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
-				yoffset2 += 5;
+				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()
 			}
-			snprintf(s, sizeof s - 1, "Dynamic slopes:  %d", dynslopethcount);
-			V_DrawSmallString(119, 40+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "Precipitation:   %d", precipcount);
-			V_DrawSmallString(119, 45+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
-			snprintf(s, sizeof s - 1, "Pending removal: %d", removecount);
-			V_DrawSmallString(119, 50+yoffset2, V_MONOSPACE | V_ALLOWLOWERCASE | V_BLUEMAP, s);
-
-			snprintf(s, sizeof s - 1, "Calls:");
-			V_DrawSmallString(212, 10, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
-			snprintf(s, sizeof s - 1, "Lua mobj hooks:  %d", ps_lua_mobjhooks);
-			V_DrawSmallString(216, 15, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
-			snprintf(s, sizeof s - 1, "P_CheckPosition: %d", ps_checkposition_calls);
-			V_DrawSmallString(216, 20, V_MONOSPACE | V_ALLOWLOWERCASE | V_PURPLEMAP, s);
+			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
+	{
+		PS_UpdateFrameStats();
+		PS_DrawRenderStats();
+	}
+	else if (cv_perfstats.value == 2) // logic
+	{
+		// 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: %u", str, 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 01a818c1c360c3cdc6231069686edcb7bf3544ee..3ff0e6c6b014b4c24099be602e507d3c07cfcc43 100644
--- a/src/m_perfstats.h
+++ b/src/m_perfstats.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020 by Sonic Team Junior.
+// Copyright (C) 2020-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -16,26 +16,45 @@
 #include "lua_script.h"
 #include "p_local.h"
 
-extern int ps_tictime;
-
-extern int ps_playerthink_time;
-extern int ps_thinkertime;
-
-extern int ps_thlist_times[];
-
-extern int ps_checkposition_calls;
-
-extern int 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
 {
-	UINT32 time_taken;
+	ps_metric_t time_taken;
 	char short_src[LUA_IDSIZE];
 } ps_hookinfo_t;
 
-void PS_SetThinkFrameHookInfo(int index, UINT32 time_taken, char* short_src);
+#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/m_queue.c b/src/m_queue.c
index 8603ab20215cd3496d223acc2589dd88fd4073e5..a337ca4ce9b283afad602a6d06376136bb010aad 100644
--- a/src/m_queue.c
+++ b/src/m_queue.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2003      by James Haley
-// Copyright (C) 2003-2020 by Sonic Team Junior.
+// Copyright (C) 2003-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_queue.h b/src/m_queue.h
index 3e9579e11395b3aea2679e17b877fa4591db485e..cc64b8dd7917acdd136ddc7f832ef2737904e66f 100644
--- a/src/m_queue.h
+++ b/src/m_queue.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2003      by James Haley
-// Copyright (C) 2003-2020 by Sonic Team Junior.
+// Copyright (C) 2003-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_random.c b/src/m_random.c
index 481fdb72b06dfd3ee7c3ef520035b69667f480fe..2e6213e1277ce027fc04cd6e7435dbaa8e49963d 100644
--- a/src/m_random.c
+++ b/src/m_random.c
@@ -3,7 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -60,7 +60,7 @@ UINT8 M_RandomByte(void)
   */
 INT32 M_RandomKey(INT32 a)
 {
-	return (INT32)((rand()/((unsigned)RAND_MAX+1.0f))*a);
+	return (INT32)((rand()/((float)RAND_MAX+1.0f))*a);
 }
 
 /** Provides a random integer in a given range.
@@ -73,7 +73,7 @@ INT32 M_RandomKey(INT32 a)
   */
 INT32 M_RandomRange(INT32 a, INT32 b)
 {
-	return (INT32)((rand()/((unsigned)RAND_MAX+1.0f))*(b-a+1))+a;
+	return (INT32)((rand()/((float)RAND_MAX+1.0f))*(b-a+1))+a;
 }
 
 
diff --git a/src/m_random.h b/src/m_random.h
index 01190e0466a95aa59e9ef5845d83fbf15b085177..df10b4bb371ed14394b897bc07bb9abe78c0f096 100644
--- a/src/m_random.h
+++ b/src/m_random.h
@@ -3,7 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/m_swap.h b/src/m_swap.h
index b44d6de8c1ebe42a8bbb9f18bfce261dd3faf08f..6aa347d97d146bc9c33d1d69cde0ff296b04a8a5 100644
--- a/src/m_swap.h
+++ b/src/m_swap.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/mserv.c b/src/mserv.c
index dfb4174156978b35f6d29bebee1738eac0f2d446..f64c7bea91b6487efca07f01ba7bb41ad786358c 100644
--- a/src/mserv.c
+++ b/src/mserv.c
@@ -1,8 +1,8 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
-// Copyright (C)      2020 by James R.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
+// Copyright (C) 2020-2021 by James R.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/mserv.h b/src/mserv.h
index d0d5e49dfe9519fa5d0f1c2904fd0f2849e50100..7a3b3d8ec8461d9b4734353a23e37e587369be86 100644
--- a/src/mserv.h
+++ b/src/mserv.h
@@ -1,8 +1,8 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
-// Copyright (C)      2020 by James R.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
+// Copyright (C) 2020-2021 by James R.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/p_ceilng.c b/src/p_ceilng.c
index f12499d5ce6315e1a8baf70b43f454443ae0bef8..e28f9b5b10ce215422e3d11ec90847c1833c801a 100644
--- a/src/p_ceilng.c
+++ b/src/p_ceilng.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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);
@@ -395,9 +397,8 @@ INT32 EV_DoCeiling(line_t *line, ceiling_e type)
 	sector_t *sec;
 	ceiling_t *ceiling;
 	mtag_t tag = Tag_FGet(&line->tags);
-	TAG_ITER_DECLARECOUNTER(0);
 
-	TAG_ITER_SECTORS(0, tag, secnum)
+	TAG_ITER_SECTORS(tag, secnum)
 	{
 		sec = &sectors[secnum];
 
@@ -513,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:
@@ -617,9 +621,8 @@ INT32 EV_DoCrush(line_t *line, ceiling_e type)
 	sector_t *sec;
 	ceiling_t *ceiling;
 	mtag_t tag = Tag_FGet(&line->tags);
-	TAG_ITER_DECLARECOUNTER(0);
 
-	TAG_ITER_SECTORS(0, tag, secnum)
+	TAG_ITER_SECTORS(tag, secnum)
 	{
 		sec = &sectors[secnum];
 
diff --git a/src/p_enemy.c b/src/p_enemy.c
index 22de9bc67325b1b2b85d076808d24345d04c27de..9d51aced50899c8574997fa1d18b187db9e20371 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -25,6 +25,7 @@
 #include "i_video.h"
 #include "z_zone.h"
 #include "lua_hook.h"
+#include "m_cond.h" // SECRET_SKIN
 
 #ifdef HW3SOUND
 #include "hardware/hw3sound.h"
@@ -743,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
@@ -1708,7 +1709,7 @@ void A_HoodThink(mobj_t *actor)
 	dx = (actor->target->x - actor->x), dy = (actor->target->y - actor->y), dz = (actor->target->z - actor->z);
 	dm = P_AproxDistance(dx, dy);
 	// Target dangerously close to robohood, retreat then.
-	if ((dm < 256<<FRACBITS) && (abs(dz) < 128<<FRACBITS))
+	if ((dm < 256<<FRACBITS) && (abs(dz) < 128<<FRACBITS) && !(actor->flags2 & MF2_AMBUSH))
 	{
 		S_StartSound(actor, actor->info->attacksound);
 		P_SetMobjState(actor, actor->info->raisestate);
@@ -3517,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);
 }
 
@@ -3590,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)
@@ -3924,6 +3923,10 @@ void A_BossDeath(mobj_t *mo)
 	}
 	else
 	{
+		// Initialize my junk
+		junk.tags.tags = NULL;
+		junk.tags.count = 0;
+
 		// Bring the egg trap up to the surface
 		// Incredibly shitty code ahead
 		Tag_FSet(&junk.tags, LE_CAPSULE0);
@@ -3957,7 +3960,7 @@ void A_BossDeath(mobj_t *mo)
 	}
 
 bossjustdie:
-	if (LUAh_BossDeath(mo))
+	if (LUA_HookMobj(mo, MOBJ_HOOK(BossDeath)))
 		return;
 	else if (P_MobjWasRemoved(mo))
 		return;
@@ -4053,6 +4056,10 @@ bossjustdie:
 		}
 		case MT_KOOPA:
 		{
+			// Initialize my junk
+			junk.tags.tags = NULL;
+			junk.tags.count = 0;
+
 			Tag_FSet(&junk.tags, LE_KOOPA);
 			EV_DoCeiling(&junk, raiseToHighest);
 			return;
@@ -4193,7 +4200,7 @@ void A_CustomPower(mobj_t *actor)
 		return;
 	}
 
-	if (locvar1 >= NUMPOWERS)
+	if (locvar1 >= NUMPOWERS || locvar1 < 0)
 	{
 		CONS_Debug(DBG_GAMELOGIC, "Power #%d out of range!\n", locvar1);
 		return;
@@ -4904,7 +4911,7 @@ void A_ThrownRing(mobj_t *actor)
 		}
 
 		if (actor->tracer && (actor->tracer->health)
-			&& (actor->tracer->player->powers[pw_shield] & SH_PROTECTELECTRIC))// Already found someone to follow.
+			&& (actor->tracer->player && actor->tracer->player->powers[pw_shield] & SH_PROTECTELECTRIC))// Already found someone to follow.
 		{
 			const INT32 temp = actor->threshold;
 			actor->threshold = 32000;
@@ -5093,6 +5100,33 @@ void A_SignSpin(mobj_t *actor)
 	}
 }
 
+static boolean SignSkinCheck(player_t *player, INT32 num)
+{
+	INT32 i;
+
+	if (player != NULL)
+	{
+		// Use player's availabilities
+		return R_SkinUsable(player - players, num);
+	}
+
+	// Player invalid, only show characters that are unlocked from the start.
+	for (i = 0; i < MAXUNLOCKABLES; i++)
+	{
+		if (unlockables[i].type == SECRET_SKIN)
+		{
+			INT32 lockedSkin = M_UnlockableSkinNum(&unlockables[i]);
+
+			if (lockedSkin == num)
+			{
+				return false;
+			}
+		}
+	}
+
+	return true;
+}
+
 // Function: A_SignPlayer
 //
 // Description: Changes the state of a level end sign to reflect the player that hit it.
@@ -5153,23 +5187,21 @@ void A_SignPlayer(mobj_t *actor)
 		// I turned this function into a fucking mess. I'm so sorry. -Lach
 		if (locvar1 == -2) // random skin
 		{
-#define skincheck(num) (player ? !R_SkinUsable(player-players, num) : skins[num].availability > 0)
 			player_t *player = actor->target ? actor->target->player : NULL;
 			UINT8 skinnum;
 			UINT8 skincount = 0;
 			for (skinnum = 0; skinnum < numskins; skinnum++)
-				if (!skincheck(skinnum))
+				if (SignSkinCheck(player, skinnum))
 					skincount++;
 			skinnum = P_RandomKey(skincount);
 			for (skincount = 0; skincount < numskins; skincount++)
 			{
 				if (skincount > skinnum)
 					break;
-				if (skincheck(skincount))
+				if (!SignSkinCheck(player, skincount))
 					skinnum++;
 			}
 			skin = &skins[skinnum];
-#undef skincheck
 		}
 		else // specific skin
 			skin = &skins[locvar1];
@@ -5263,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;
@@ -5912,13 +5944,18 @@ void A_DetonChase(mobj_t *actor)
 
 	if (actor->reactiontime == -42)
 	{
-		fixed_t xyspeed;
+		fixed_t xyspeed, speed;
+
+		if (actor->target->player)
+			speed = actor->target->player->normalspeed;
+		else
+			speed = actor->target->info->speed;
 
 		actor->reactiontime = -42;
 
 		exact = actor->movedir>>ANGLETOFINESHIFT;
-		xyspeed = FixedMul(FixedMul(actor->tracer->player->normalspeed,3*FRACUNIT/4), FINECOSINE(exact));
-		actor->momz = FixedMul(FixedMul(actor->tracer->player->normalspeed,3*FRACUNIT/4), FINESINE(exact));
+		xyspeed = FixedMul(FixedMul(speed,3*FRACUNIT/4), FINECOSINE(exact));
+		actor->momz = FixedMul(FixedMul(speed,3*FRACUNIT/4), FINESINE(exact));
 
 		exact = actor->angle>>ANGLETOFINESHIFT;
 		actor->momx = FixedMul(xyspeed, FINECOSINE(exact));
@@ -7513,7 +7550,7 @@ void A_Boss2PogoTarget(mobj_t *actor)
 	}
 
 	// Target hit, retreat!
-	if (actor->target->player->powers[pw_flashing] > TICRATE || actor->flags2 & MF2_FRET)
+	if ((actor->target->player && actor->target->player->powers[pw_flashing] > TICRATE) || actor->flags2 & MF2_FRET)
 	{
 		UINT8 prandom = P_RandomByte();
 		actor->z++; // unstick from the floor
@@ -7524,7 +7561,7 @@ void A_Boss2PogoTarget(mobj_t *actor)
 	// Try to land on top of the player.
 	else if (P_AproxDistance(actor->x-actor->target->x, actor->y-actor->target->y) < FixedMul(512*FRACUNIT, actor->scale))
 	{
-		fixed_t airtime, gravityadd, zoffs;
+		fixed_t airtime, gravityadd, zoffs, height;
 
 		// check gravity in the sector (for later math)
 		P_CheckGravity(actor, true);
@@ -7546,7 +7583,13 @@ void A_Boss2PogoTarget(mobj_t *actor)
 		// Remember, kids!
 		// Reduced down Calculus lets you avoid bad 'logic math' loops!
 		//airtime = FixedDiv(-actor->momz<<1, gravityadd)<<1; // going from 0 to 0 is much simpler
-		zoffs = (P_GetPlayerHeight(actor->target->player)>>1) + (actor->target->floorz - actor->floorz); // offset by the difference in floor height plus half the player height,
+
+		if (actor->target->player)
+			height = P_GetPlayerHeight(actor->target->player) >> 1;
+		else
+			height = actor->target->height >> 1;
+
+		zoffs = height + (actor->target->floorz - actor->floorz); // offset by the difference in floor height plus half the player height,
 		airtime = FixedDiv((-actor->momz - FixedSqrt(FixedMul(actor->momz,actor->momz)+zoffs)), gravityadd)<<1; // to try and land on their head rather than on their feet
 
 		actor->angle = R_PointToAngle2(actor->x, actor->y, actor->target->x, actor->target->y);
@@ -8227,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;
@@ -8238,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);
@@ -9854,28 +9903,29 @@ void A_Custom3DRotate(mobj_t *actor)
 
 	const fixed_t radius = FixedMul(loc1lw*FRACUNIT, actor->scale);
 	const fixed_t hOff = FixedMul(loc1up*FRACUNIT, actor->scale);
-	const fixed_t hspeed = FixedMul(loc2up*FRACUNIT/10, actor->scale);
-	const fixed_t vspeed = FixedMul(loc2lw*FRACUNIT/10, actor->scale);
+	const fixed_t hspeed = loc2up*FRACUNIT/10; // Monster's note (29/05/21): DO NOT SCALE, this is an angular speed!
+	const fixed_t vspeed = loc2lw*FRACUNIT/10; // ditto
 
 	if (LUA_CallAction(A_CUSTOM3DROTATE, actor))
 		return;
 
-	if (actor->target->health == 0)
+	if (!actor->target) // Ensure we actually have a target first.
 	{
+		CONS_Printf("Error: A_Custom3DRotate: Object has no target.\n");
 		P_RemoveMobj(actor);
 		return;
 	}
 
-	if (!actor->target) // This should NEVER happen.
+	if (actor->target->health == 0)
 	{
-		if (cv_debug)
-			CONS_Printf("Error: Object has no target\n");
 		P_RemoveMobj(actor);
 		return;
 	}
+
 	if (hspeed==0 && vspeed==0)
 	{
-		CONS_Printf("Error: A_Custom3DRotate: Object has no speed.\n");
+		if (cv_debug)
+			CONS_Printf("Error: A_Custom3DRotate: Object has no speed.\n");
 		return;
 	}
 
@@ -14300,6 +14350,14 @@ void A_RolloutRock(mobj_t *actor)
 	if (LUA_CallAction(A_ROLLOUTROCK, actor))
 		return;
 
+	if (!actor->tracer || P_MobjWasRemoved(actor->tracer) || !actor->tracer->health)
+		actor->flags |= MF_PUSHABLE;
+	else
+	{
+		actor->flags2 = (actor->flags2 & ~MF2_OBJECTFLIP) | (actor->tracer->flags2 & MF2_OBJECTFLIP);
+		actor->eflags = (actor->eflags & ~MFE_VERTICALFLIP) | (actor->tracer->eflags & MFE_VERTICALFLIP);
+	}
+
 	actor->friction = FRACUNIT; // turns out riding on solids sucks, so let's just make it easier on ourselves
 
 	if (actor->eflags & MFE_JUSTHITFLOOR)
@@ -14338,7 +14396,8 @@ void A_RolloutRock(mobj_t *actor)
 
 	speed = P_AproxDistance(actor->momx, actor->momy); // recalculate speed for visual rolling
 
-	if (speed < actor->scale >> 1) // stop moving if speed is insignificant
+	if (((actor->flags & MF_PUSHABLE) || !(actor->flags2 & MF2_STRONGBOX))
+		&& speed < actor->scale) // stop moving if speed is insignificant
 	{
 		actor->momx = 0;
 		actor->momy = 0;
@@ -14358,9 +14417,6 @@ void A_RolloutRock(mobj_t *actor)
 
 	actor->frame = actor->reactiontime % maxframes; // set frame
 
-	if (!actor->tracer || P_MobjWasRemoved(actor->tracer) || !actor->tracer->health)
-		actor->flags |= MF_PUSHABLE;
-
 	if (!(actor->flags & MF_PUSHABLE) || (actor->movecount != 1)) // if being ridden or haven't moved, don't disappear
 		actor->fuse = actor->info->painchance;
 	else if (actor->fuse < 2*TICRATE)
diff --git a/src/p_floor.c b/src/p_floor.c
index ed49b03a38ef8b4381ad968a4540c09952d1d018..263644f702bb7a8a26f66c6989b57deb2b225ccc 100644
--- a/src/p_floor.c
+++ b/src/p_floor.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -635,7 +635,6 @@ void T_BounceCheese(bouncecheese_t *bouncer)
 	boolean remove;
 	INT32 i;
 	mtag_t tag = Tag_FGet(&bouncer->sourceline->tags);
-	TAG_ITER_DECLARECOUNTER(0);
 
 	if (bouncer->sector->crumblestate == CRUMBLE_RESTORE || bouncer->sector->crumblestate == CRUMBLE_WAIT
 		|| bouncer->sector->crumblestate == CRUMBLE_ACTIVATED) // Oops! Crumbler says to remove yourself!
@@ -650,7 +649,7 @@ void T_BounceCheese(bouncecheese_t *bouncer)
 	}
 
 	// You can use multiple target sectors, but at your own risk!!!
-	TAG_ITER_SECTORS(0, tag, i)
+	TAG_ITER_SECTORS(tag, i)
 	{
 		actionsector = &sectors[i];
 		actionsector->moved = true;
@@ -775,7 +774,6 @@ void T_StartCrumble(crumble_t *crumble)
 	sector_t *sector;
 	INT32 i;
 	mtag_t tag = Tag_FGet(&crumble->sourceline->tags);
-	TAG_ITER_DECLARECOUNTER(0);
 
 	// Once done, the no-return thinker just sits there,
 	// constantly 'returning'... kind of an oxymoron, isn't it?
@@ -804,7 +802,7 @@ void T_StartCrumble(crumble_t *crumble)
 		}
 		else if (++crumble->timer == 0) // Reposition back to original spot
 		{
-			TAG_ITER_SECTORS(0, tag, i)
+			TAG_ITER_SECTORS(tag, i)
 			{
 				sector = &sectors[i];
 
@@ -840,7 +838,7 @@ void T_StartCrumble(crumble_t *crumble)
 		// Flash to indicate that the platform is about to return.
 		if (crumble->timer > -224 && (leveltime % ((abs(crumble->timer)/8) + 1) == 0))
 		{
-			TAG_ITER_SECTORS(0, tag, i)
+			TAG_ITER_SECTORS(tag, i)
 			{
 				sector = &sectors[i];
 
@@ -932,7 +930,7 @@ void T_StartCrumble(crumble_t *crumble)
 		P_RemoveThinker(&crumble->thinker);
 	}
 
-	TAG_ITER_SECTORS(0, tag, i)
+	TAG_ITER_SECTORS(tag, i)
 	{
 		sector = &sectors[i];
 		sector->moved = true;
@@ -948,7 +946,6 @@ void T_StartCrumble(crumble_t *crumble)
 void T_MarioBlock(mariothink_t *block)
 {
 	INT32 i;
-	TAG_ITER_DECLARECOUNTER(0);
 
 	T_MovePlane
 	(
@@ -983,7 +980,7 @@ void T_MarioBlock(mariothink_t *block)
 		block->sector->ceilspeed = 0;
 		block->direction = 0;
 	}
-	TAG_ITER_SECTORS(0, (INT16)block->tag, i)
+	TAG_ITER_SECTORS((INT16)block->tag, i)
 		P_RecalcPrecipInSector(&sectors[i]);
 }
 
@@ -1045,6 +1042,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:
@@ -1064,9 +1062,7 @@ static mobj_t *SearchMarioNode(msecnode_t *node)
 		case MT_HOOP:
 		case MT_HOOPCOLLIDE:
 		case MT_NIGHTSCORE:
-#ifdef SEENAMES
 		case MT_NAMECHECK: // DEFINITELY not this, because it is client-side.
-#endif
 			continue;
 		default:
 			break;
@@ -1295,9 +1291,8 @@ void T_NoEnemiesSector(noenemies_t *nobaddies)
 	INT32 secnum = -1;
 	boolean FOFsector = false;
 	mtag_t tag = Tag_FGet(&nobaddies->sourceline->tags);
-	TAG_ITER_DECLARECOUNTER(0);
 
-	TAG_ITER_SECTORS(0, tag, secnum)
+	TAG_ITER_SECTORS(tag, secnum)
 	{
 		sec = &sectors[secnum];
 
@@ -1308,14 +1303,13 @@ void T_NoEnemiesSector(noenemies_t *nobaddies)
 		{
 			INT32 targetsecnum = -1;
 			mtag_t tag2 = Tag_FGet(&sec->lines[i]->tags);
-			TAG_ITER_DECLARECOUNTER(1);
 
 			if (sec->lines[i]->special < 100 || sec->lines[i]->special >= 300)
 				continue;
 
 			FOFsector = true;
 
-			TAG_ITER_SECTORS(1, tag2, targetsecnum)
+			TAG_ITER_SECTORS(tag2, targetsecnum)
 			{
 				if (T_SectorHasEnemies(&sectors[targetsecnum]))
 					return;
@@ -1402,7 +1396,6 @@ void T_EachTimeThinker(eachtime_t *eachtime)
 	fixed_t bottomheight, topheight;
 	ffloor_t *rover;
 	mtag_t tag = Tag_FGet(&eachtime->sourceline->tags);
-	TAG_ITER_DECLARECOUNTER(0);
 
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
@@ -1412,7 +1405,7 @@ void T_EachTimeThinker(eachtime_t *eachtime)
 		eachtime->playersOnArea[i] = false;
 	}
 
-	TAG_ITER_SECTORS(0, tag, secnum)
+	TAG_ITER_SECTORS(tag, secnum)
 	{
 		sec = &sectors[secnum];
 
@@ -1430,14 +1423,13 @@ void T_EachTimeThinker(eachtime_t *eachtime)
 		{
 			INT32 targetsecnum = -1;
 			mtag_t tag2 = Tag_FGet(&sec->lines[i]->tags);
-			TAG_ITER_DECLARECOUNTER(1);
 
 			if (sec->lines[i]->special < 100 || sec->lines[i]->special >= 300)
 				continue;
 
 			FOFsector = true;
 
-			TAG_ITER_SECTORS(1, tag2, targetsecnum)
+			TAG_ITER_SECTORS(tag2, targetsecnum)
 			{
 				targetsec = &sectors[targetsecnum];
 
@@ -1572,12 +1564,11 @@ void T_RaiseSector(raise_t *raise)
 	INT32 direction;
 	result_e res = 0;
 	mtag_t tag = raise->tag;
-	TAG_ITER_DECLARECOUNTER(0);
 
 	if (raise->sector->crumblestate >= CRUMBLE_FALL || raise->sector->ceilingdata)
 		return;
 
-	TAG_ITER_SECTORS(0, tag, i)
+	TAG_ITER_SECTORS(tag, i)
 	{
 		sector = &sectors[i];
 
@@ -1704,7 +1695,7 @@ void T_RaiseSector(raise_t *raise)
 	raise->sector->ceilspeed = 42;
 	raise->sector->floorspeed = speed*direction;
 
-	TAG_ITER_SECTORS(0, tag, i)
+	TAG_ITER_SECTORS(tag, i)
 		P_RecalcPrecipInSector(&sectors[i]);
 }
 
@@ -1822,9 +1813,8 @@ void EV_DoFloor(line_t *line, floor_e floortype)
 	sector_t *sec;
 	floormove_t *dofloor;
 	mtag_t tag = Tag_FGet(&line->tags);
-	TAG_ITER_DECLARECOUNTER(0);
 
-	TAG_ITER_SECTORS(0, tag, secnum)
+	TAG_ITER_SECTORS(tag, secnum)
 	{
 		sec = &sectors[secnum];
 
@@ -2039,10 +2029,9 @@ void EV_DoElevator(line_t *line, elevator_e elevtype, boolean customspeed)
 	sector_t *sec;
 	elevator_t *elevator;
 	mtag_t tag = Tag_FGet(&line->tags);
-	TAG_ITER_DECLARECOUNTER(0);
 
 	// act on all sectors with the same tag as the triggering linedef
-	TAG_ITER_SECTORS(0, tag, secnum)
+	TAG_ITER_SECTORS(tag, secnum)
 	{
 		sec = &sectors[secnum];
 
@@ -2339,7 +2328,6 @@ INT32 EV_StartCrumble(sector_t *sec, ffloor_t *rover, boolean floating,
 	sector_t *foundsec;
 	INT32 i;
 	mtag_t tag = Tag_FGet(&rover->master->tags);
-	TAG_ITER_DECLARECOUNTER(0);
 
 	// If floor is already activated, skip it
 	if (sec->floordata)
@@ -2382,7 +2370,7 @@ INT32 EV_StartCrumble(sector_t *sec, ffloor_t *rover, boolean floating,
 
 	crumble->sector->crumblestate = CRUMBLE_ACTIVATED;
 
-	TAG_ITER_SECTORS(0, tag, i)
+	TAG_ITER_SECTORS(tag, i)
 	{
 		foundsec = &sectors[i];
 
diff --git a/src/p_inter.c b/src/p_inter.c
index 415c679e4922b1d55920bc5f937c39fee1927e30..b37689fd8f080bb85e790da77c105c5e550dcaf4 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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...
@@ -365,7 +365,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 	if (special->flags & (MF_ENEMY|MF_BOSS) && special->flags2 & MF2_FRET)
 		return;
 
-	if (LUAh_TouchSpecial(special, toucher) || P_MobjWasRemoved(special))
+	if (LUA_HookTouchSpecial(special, toucher) || P_MobjWasRemoved(special))
 		return;
 
 	// 0 = none, 1 = elemental pierce, 2 = bubble bounce
@@ -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,8 +1385,12 @@ 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;
 
 				Tag_FSet(&junk.tags, LE_AXE);
 				EV_DoElevator(&junk, bridgeFall, false);
@@ -1419,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);
@@ -1681,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);
@@ -1692,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);
@@ -1700,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;
@@ -1732,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);
@@ -1785,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;
@@ -1809,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
@@ -1935,7 +1939,7 @@ static void P_HitDeathMessages(player_t *player, mobj_t *inflictor, mobj_t *sour
 	if (!netgame)
 		return; // Presumably it's obvious what's happening in splitscreen.
 
-	if (LUAh_HurtMsg(player, inflictor, source, damagetype))
+	if (LUA_HookHurtMsg(player, inflictor, source, damagetype))
 		return;
 
 	deadtarget = (player->mo->health <= 0);
@@ -2387,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.
@@ -2409,7 +2413,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 	target->flags2 &= ~(MF2_SKULLFLY|MF2_NIGHTSPULL);
 	target->health = 0; // This makes it easy to check if something's dead elsewhere.
 
-	if (LUAh_MobjDeath(target, inflictor, source, damagetype) || P_MobjWasRemoved(target))
+	if (LUA_HookMobjDeath(target, inflictor, source, damagetype) || P_MobjWasRemoved(target))
 		return;
 
 	// Let EVERYONE know what happened to a player! 01-29-2002 Tails
@@ -2551,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))
@@ -3471,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.
@@ -3544,7 +3548,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 	// Everything above here can't be forced.
 	if (!metalrecording)
 	{
-		UINT8 shouldForce = LUAh_ShouldDamage(target, inflictor, source, damage, damagetype);
+		UINT8 shouldForce = LUA_HookShouldDamage(target, inflictor, source, damage, damagetype);
 		if (P_MobjWasRemoved(target))
 			return (shouldForce == 1); // mobj was removed
 		if (shouldForce == 1)
@@ -3562,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;
 	}
@@ -3585,7 +3589,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 		if (!force && target->flags2 & MF2_FRET) // Currently flashing from being hit
 			return false;
 
-		if (LUAh_MobjDamage(target, inflictor, source, damage, damagetype) || P_MobjWasRemoved(target))
+		if (LUA_HookMobjDamage(target, inflictor, source, damage, damagetype) || P_MobjWasRemoved(target))
 			return true;
 
 		if (target->health > 1)
@@ -3635,7 +3639,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 				|| (G_GametypeHasTeams() && player->ctfteam == source->player->ctfteam)))
 					return false; // Don't run eachother over in special stages and team games and such
 			}
-			if (LUAh_MobjDamage(target, inflictor, source, damage, damagetype))
+			if (LUA_HookMobjDamage(target, inflictor, source, damage, damagetype))
 				return true;
 			P_NiGHTSDamage(target, source); // -5s :(
 			return true;
@@ -3647,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
@@ -3689,15 +3693,15 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 			if (force
 			|| (inflictor && inflictor->flags & MF_MISSILE && inflictor->flags2 & MF2_SUPERFIRE)) // Super Sonic is stunned!
 			{
-				if (!LUAh_MobjDamage(target, inflictor, source, damage, damagetype))
+				if (!LUA_HookMobjDamage(target, inflictor, source, damage, damagetype))
 					P_SuperDamage(player, inflictor, source, damage);
 				return true;
 			}
 			return false;
 		}
-		else if (LUAh_MobjDamage(target, inflictor, source, damage, damagetype))
+		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_lights.c b/src/p_lights.c
index d396e92d3dedff6ac56f40901335ec701d6625e9..1e41146da682a2ed5d93166974f1b04843bd40ed 100644
--- a/src/p_lights.c
+++ b/src/p_lights.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -374,10 +374,9 @@ void P_FadeLightBySector(sector_t *sector, INT32 destvalue, INT32 speed, boolean
 void P_FadeLight(INT16 tag, INT32 destvalue, INT32 speed, boolean ticbased, boolean force)
 {
 	INT32 i;
-	TAG_ITER_DECLARECOUNTER(0);
 
 	// search all sectors for ones with tag
-	TAG_ITER_SECTORS(0, tag, i)
+	TAG_ITER_SECTORS(tag, i)
 	{
 		if (!force && ticbased // always let speed fader execute
 			&& sectors[i].lightingdata
diff --git a/src/p_local.h b/src/p_local.h
index 8a508496208b82699b4bdde458f7639f7823805e..1fcd3050d92fde6fd1056f86d81137be89b93902 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -143,6 +143,8 @@ angle_t P_GetLocalAngle(player_t *player);
 void P_SetLocalAngle(player_t *player, angle_t angle);
 void P_ForceLocalAngle(player_t *player, angle_t angle);
 boolean P_PlayerFullbright(player_t *player);
+boolean P_PlayerCanEnterSpinGaps(player_t *player);
+boolean P_PlayerShouldUseSpinHeight(player_t *player);
 
 boolean P_IsObjectInGoop(mobj_t *mo);
 boolean P_IsObjectOnGround(mobj_t *mo);
@@ -326,9 +328,7 @@ mobj_t *P_SpawnPointMissile(mobj_t *source, fixed_t xa, fixed_t ya, fixed_t za,
 mobj_t *P_SpawnAlteredDirectionMissile(mobj_t *source, mobjtype_t type, fixed_t x, fixed_t y, fixed_t z, INT32 shiftingAngle);
 mobj_t *P_SPMAngle(mobj_t *source, mobjtype_t type, angle_t angle, UINT8 aimtype, UINT32 flags2);
 #define P_SpawnPlayerMissile(s,t,f) P_SPMAngle(s,t,s->angle,true,f)
-#ifdef SEENAMES
 #define P_SpawnNameFinder(s,t) P_SPMAngle(s,t,s->angle,true,0)
-#endif
 void P_ColorTeamMissile(mobj_t *missile, player_t *source);
 SINT8 P_MobjFlip(mobj_t *mobj);
 fixed_t P_GetMobjGravity(mobj_t *mo);
diff --git a/src/p_map.c b/src/p_map.c
index fbc7447ce0dd555f24f8b0ef9d8b58e05b36383e..0c9479e5c2d6c8c51c2d16ab4b8f521120ce4755 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -419,16 +419,23 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object)
 			}
 			else if (object->player->dashmode >= DASHMODE_THRESHOLD)
 				P_SetPlayerMobjState(object, S_PLAY_DASH);
-			else if (P_IsObjectOnGround(object) && horizspeed >= FixedMul(object->player->runspeed, object->scale))
-				P_SetPlayerMobjState(object, S_PLAY_RUN);
+			else if (P_IsObjectOnGround(object))
+				P_SetPlayerMobjState(object, (horizspeed >= FixedMul(object->player->runspeed, object->scale)) ? S_PLAY_RUN : S_PLAY_WALK);
 			else
-				P_SetPlayerMobjState(object, S_PLAY_WALK);
+				P_SetPlayerMobjState(object, (object->momz > 0) ? S_PLAY_SPRING : S_PLAY_FALL);
 		}
 		else if (P_MobjFlip(object)*vertispeed > 0)
 			P_SetPlayerMobjState(object, S_PLAY_SPRING);
 		else
 			P_SetPlayerMobjState(object, S_PLAY_FALL);
 	}
+	else if (horizspeed
+		&& object->tracer
+		&& object->tracer->player
+		&& object->tracer->player->powers[pw_carry] != CR_NONE
+		&& object->tracer->tracer == object
+		&& (!demoplayback || P_ControlStyle(object->tracer->player) == CS_LMAOGALOG))
+			P_SetPlayerAngle(object->tracer->player, spring->angle);
 
 	object->standingslope = NULL; // And again.
 
@@ -511,6 +518,7 @@ static void P_DoFanAndGasJet(mobj_t *spring, mobj_t *object)
 			if (spring->state != &states[S_STEAM1]) // Only when it bursts
 				break;
 
+			object->eflags |= MFE_SPRUNG;
 			object->momz = flipval*FixedMul(speed, FixedSqrt(FixedMul(spring->scale, object->scale))); // scale the speed with both objects' scales, just like with springs!
 
 			if (p)
@@ -727,9 +735,8 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	|| (thing->player && thing->player->spectator))
 		return true;
 
-#ifdef SEENAMES
-  // Do name checks all the way up here
-  // So that NOTHING ELSE can see MT_NAMECHECK because it is client-side.
+	// Do name checks all the way up here
+	// So that NOTHING ELSE can see MT_NAMECHECK because it is client-side.
 	if (tmthing->type == MT_NAMECHECK)
 	{
 		// Ignore things that aren't players, ignore spectators, ignore yourself.
@@ -747,13 +754,12 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			return true; // underneath
 
 		// REX HAS SEEN YOU
-		if (!LUAh_SeenPlayer(tmthing->target->player, thing->player))
+		if (!LUA_HookSeenPlayer(tmthing->target->player, thing->player))
 			return false;
 
 		seenplayer = thing->player;
 		return false;
 	}
-#endif
 
 	// Metal Sonic destroys tiny baby objects.
 	if (tmthing->type == MT_METALSONIC_RACE
@@ -937,7 +943,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	}
 
 	{
-		UINT8 shouldCollide = LUAh_MobjCollide(thing, tmthing); // checks hook for thing's type
+		UINT8 shouldCollide = LUA_Hook2Mobj(thing, tmthing, MOBJ_HOOK(MobjCollide)); // checks hook for thing's type
 		if (P_MobjWasRemoved(tmthing) || P_MobjWasRemoved(thing))
 			return true; // one of them was removed???
 		if (shouldCollide == 1)
@@ -945,7 +951,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		else if (shouldCollide == 2)
 			return true; // force no collide
 
-		shouldCollide = LUAh_MobjMoveCollide(tmthing, thing); // checks hook for tmthing's type
+		shouldCollide = LUA_Hook2Mobj(tmthing, thing, MOBJ_HOOK(MobjMoveCollide)); // checks hook for tmthing's type
 		if (P_MobjWasRemoved(tmthing) || P_MobjWasRemoved(thing))
 			return true; // one of them was removed???
 		if (shouldCollide == 1)
@@ -982,7 +988,8 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	if (thing->type == MT_SALOONDOOR && tmthing->player)
 	{
 		mobj_t *ref = (tmthing->player->powers[pw_carry] == CR_MINECART && tmthing->tracer && !P_MobjWasRemoved(tmthing->tracer)) ? tmthing->tracer : tmthing;
-		if ((thing->flags2 & MF2_AMBUSH) || ref != tmthing)
+		if (((thing->flags2 & MF2_AMBUSH) && (tmthing->z <= thing->z + thing->height) && (tmthing->z + tmthing->height >= thing->z))
+			|| ref != tmthing)
 		{
 			fixed_t dm = min(FixedHypot(ref->momx, ref->momy), 16*FRACUNIT);
 			angle_t ang = R_PointToAngle2(0, 0, ref->momx, ref->momy) - thing->angle;
@@ -995,7 +1002,8 @@ static boolean PIT_CheckThing(mobj_t *thing)
 
 	if (thing->type == MT_SALOONDOORCENTER && tmthing->player)
 	{
-		if ((thing->flags2 & MF2_AMBUSH) || (tmthing->player->powers[pw_carry] == CR_MINECART && tmthing->tracer && !P_MobjWasRemoved(tmthing->tracer)))
+		if (((thing->flags2 & MF2_AMBUSH) && (tmthing->z <= thing->z + thing->height) && (tmthing->z + tmthing->height >= thing->z))
+			|| (tmthing->player->powers[pw_carry] == CR_MINECART && tmthing->tracer && !P_MobjWasRemoved(tmthing->tracer)))
 			return true;
 	}
 
@@ -1148,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;
 	}
 
@@ -1683,7 +1691,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 					if (!(player->charability2 == CA2_MELEE && player->panim == PA_ABILITY2))
 					{
 						fixed_t setmomz = -*momz; // Store this, momz get changed by P_DoJump within P_DoBubbleBounce
-					
+
 						if (elementalpierce == 2) // Reset bubblewrap, part 1
 							P_DoBubbleBounce(player);
 						*momz = setmomz; // Therefore, you should be thrust in the opposite direction, vertically.
@@ -1692,7 +1700,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 						if (elementalpierce == 2) // Reset bubblewrap, part 2
 						{
 							boolean underwater = tmthing->eflags & MFE_UNDERWATER;
-							
+
 							if (underwater)
 								*momz /= 2;
 							*momz -= (*momz/(underwater ? 8 : 4)); // Cap the height!
@@ -1927,7 +1935,7 @@ static boolean PIT_CheckLine(line_t *ld)
 	blockingline = ld;
 
 	{
-		UINT8 shouldCollide = LUAh_MobjLineCollide(tmthing, blockingline); // checks hook for thing's type
+		UINT8 shouldCollide = LUA_HookMobjLineCollide(tmthing, blockingline); // checks hook for thing's type
 		if (P_MobjWasRemoved(tmthing))
 			return true; // one of them was removed???
 		if (shouldCollide == 1)
@@ -2021,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
@@ -2257,6 +2265,8 @@ boolean P_CheckPosition(mobj_t *thing, fixed_t x, fixed_t y)
 			{
 				if (!P_BlockThingsIterator(bx, by, PIT_CheckThing))
 					blockval = false;
+				else
+					tmhitthing = tmfloorthing;
 				if (P_MobjWasRemoved(tmthing))
 					return false;
 			}
@@ -2731,7 +2741,10 @@ boolean P_TryMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff)
 			if (thing->type == MT_SKIM)
 				maxstep = 0;
 
-			if (tmceilingz - tmfloorz < thing->height)
+			if (tmceilingz - tmfloorz < thing->height
+				|| (thing->player
+					&& tmceilingz - tmfloorz < P_GetPlayerHeight(thing->player)
+					&& !P_PlayerCanEnterSpinGaps(thing->player)))
 			{
 				if (tmfloorthing)
 					tmhitthing = tmfloorthing;
@@ -3339,6 +3352,11 @@ static boolean PTR_LineIsBlocking(line_t *li)
 	if (openbottom - slidemo->z > FixedMul(MAXSTEPMOVE, slidemo->scale))
 		return true; // too big a step up
 
+	if (slidemo->player
+		&& openrange < P_GetPlayerHeight(slidemo->player)
+		&& !P_PlayerCanEnterSpinGaps(slidemo->player))
+			return true; // nonspin character should not take this path
+
 	return false;
 }
 
@@ -3453,9 +3471,17 @@ static boolean PTR_SlideTraverse(intercept_t *in)
 			P_ProcessSpecialSector(slidemo->player, slidemo->subsector->sector, li->polyobj->lines[0]->backsector);
 	}
 
-	if (slidemo->player && slidemo->player->charability == CA_GLIDEANDCLIMB
-		&& (slidemo->player->pflags & PF_GLIDING || slidemo->player->climbing))
-		PTR_GlideClimbTraverse(li);
+	if (slidemo->player)
+	{
+		if (slidemo->player->charability == CA_GLIDEANDCLIMB
+			&& (slidemo->player->pflags & PF_GLIDING || slidemo->player->climbing))
+			PTR_GlideClimbTraverse(li);
+		else
+		{
+			slidemo->player->lastsidehit = li->sidenum[P_PointOnLineSide(slidemo->x, slidemo->y, li)];
+			slidemo->player->lastlinehit = (INT16)(li - lines);
+		}
+	}
 
 	if (in->frac < bestslidefrac && (!slidemo->player || !slidemo->player->climbing))
 	{
diff --git a/src/p_maputl.c b/src/p_maputl.c
index 90718a41cbe700621c191994401925ca5ab360e3..efcebe7363741a3ff1bb3a851dfc7e8653e6f3c8 100644
--- a/src/p_maputl.c
+++ b/src/p_maputl.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/p_maputl.h b/src/p_maputl.h
index 08b606833cd7895e11211de6eb94b4742f13125e..cec344d03df8b810f4a952dabb69de2746dccacd 100644
--- a/src/p_maputl.h
+++ b/src/p_maputl.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 7ba6d1fad979ff68c563f00693c1ba94b5fbdf93..83f9ebf3c7a5f27d18a65416900be70df38e911b 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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
@@ -325,9 +325,7 @@ boolean P_SetPlayerMobjState(mobj_t *mobj, statenum_t state)
 		mobj->tics = st->tics;
 
 		// Adjust the player's animation speed to match their velocity.
-		if (state == S_PLAY_STND && player->powers[pw_super] && skins[player->skin].sprites[SPR2_WAIT|FF_SPR2SUPER].numframes == 0) // if no super wait, don't wait at all
-			mobj->tics = -1;
-		else if (player->panim == PA_EDGE && (player->charflags & SF_FASTEDGE))
+		if (player->panim == PA_EDGE && (player->charflags & SF_FASTEDGE))
 			mobj->tics = 2;
 		else if (!(disableSpeedAdjust || player->charflags & SF_NOSPEEDADJUST))
 		{
@@ -396,8 +394,28 @@ boolean P_SetPlayerMobjState(mobj_t *mobj, statenum_t state)
 
 			if (skin)
 			{
-				spr2 = P_GetSkinSprite2(skin, (((player->powers[pw_super] && !(player->charflags & SF_NOSUPERSPRITES)) ? FF_SPR2SUPER : 0)|st->frame) & FF_FRAMEMASK, mobj->player);
+				UINT16 stateframe = st->frame;
+				
+				// Add/Remove FF_SPR2SUPER based on certain conditions
+				if (player->charflags & SF_NOSUPERSPRITES)
+					stateframe = stateframe & ~FF_SPR2SUPER;
+				else if (player->powers[pw_super])
+					stateframe = stateframe | FF_SPR2SUPER;
+				
+				if (stateframe & FF_SPR2SUPER)
+				{
+					if (mobj->eflags & MFE_FORCENOSUPER)
+						stateframe = stateframe & ~FF_SPR2SUPER;
+				}
+				else if (mobj->eflags & MFE_FORCESUPER)
+					stateframe = stateframe | FF_SPR2SUPER;
+					
+				// Get the sprite2 and frame number
+				spr2 = P_GetSkinSprite2(skin, (stateframe & FF_FRAMEMASK), mobj->player);
 				numframes = skin->sprites[spr2].numframes;
+				
+				if (state == S_PLAY_STND && (spr2 & FF_SPR2SUPER) && skin->sprites[SPR2_WAIT|FF_SPR2SUPER].numframes == 0)
+					mobj->tics = -1;	// If no super wait, don't wait at all
 			}
 			else
 			{
@@ -522,8 +540,23 @@ boolean P_SetMobjState(mobj_t *mobj, statenum_t state)
 
 			if (skin)
 			{
-				spr2 = P_GetSkinSprite2(skin, st->frame & FF_FRAMEMASK, mobj->player);
+				UINT16 stateframe = st->frame;
+				
+				// Add/Remove FF_SPR2SUPER based on certain conditions
+				if (stateframe & FF_SPR2SUPER)
+				{
+					if (mobj->eflags & MFE_FORCENOSUPER)
+						stateframe = stateframe & ~FF_SPR2SUPER;
+				}
+				else if (mobj->eflags & MFE_FORCESUPER)
+					stateframe = stateframe | FF_SPR2SUPER;
+					
+				// Get the sprite2 and frame number
+				spr2 = P_GetSkinSprite2(skin, (stateframe & FF_FRAMEMASK), NULL);
 				numframes = skin->sprites[spr2].numframes;
+				
+				if (state == S_PLAY_STND && (spr2 & FF_SPR2SUPER) && skin->sprites[SPR2_WAIT|FF_SPR2SUPER].numframes == 0)
+					mobj->tics = -1;	// If no super wait, don't wait at all
 			}
 			else
 			{
@@ -1839,12 +1872,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 (LUAh_MobjMoveBlocked(mo))
+		if (LUA_HookMobjMoveBlocked(mo, tmhitthing, blockingline))
 		{
 			if (P_MobjWasRemoved(mo))
 				return;
@@ -2549,6 +2580,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;
@@ -3188,13 +3223,16 @@ boolean P_SceneryZMovement(mobj_t *mo)
 //
 boolean P_CanRunOnWater(player_t *player, ffloor_t *rover)
 {
-	fixed_t topheight = P_GetFFloorTopZAt(rover, player->mo->x, player->mo->y);
+	boolean flip = player->mo->eflags & MFE_VERTICALFLIP;
+	fixed_t surfaceheight = flip ? P_GetFFloorBottomZAt(rover, player->mo->x, player->mo->y) : P_GetFFloorTopZAt(rover, player->mo->x, player->mo->y);
+	fixed_t playerbottom = flip ? (player->mo->z + player->mo->height) : player->mo->z;
+	boolean doifit = flip ? (surfaceheight - player->mo->floorz >= player->mo->height) : (player->mo->ceilingz - surfaceheight >= player->mo->height);
 
 	if (!player->powers[pw_carry] && !player->homing
-		&& ((player->powers[pw_super] || player->charflags & SF_RUNONWATER || player->dashmode >= DASHMODE_THRESHOLD) && player->mo->ceilingz-topheight >= player->mo->height)
+		&& ((player->powers[pw_super] || player->charflags & SF_RUNONWATER || player->dashmode >= DASHMODE_THRESHOLD) && doifit)
 		&& (rover->flags & FF_SWIMMABLE) && !(player->pflags & PF_SPINNING) && player->speed > FixedMul(player->runspeed, player->mo->scale)
 		&& !(player->pflags & PF_SLIDING)
-		&& abs(player->mo->z - topheight) < FixedMul(30*FRACUNIT, player->mo->scale))
+		&& abs(playerbottom - surfaceheight) < FixedMul(30*FRACUNIT, player->mo->scale))
 		return true;
 
 	return false;
@@ -3231,8 +3269,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)
 		{
@@ -3283,11 +3321,9 @@ void P_MobjCheckWater(mobj_t *mobj)
 			boolean electric = !!(p->powers[pw_shield] & SH_PROTECTELECTRIC);
 			if (electric || ((p->powers[pw_shield] & SH_PROTECTFIRE) && !(p->powers[pw_shield] & SH_PROTECTWATER) && !(mobj->eflags & MFE_TOUCHLAVA)))
 			{ // Water removes electric and non-water fire shields...
-				P_FlashPal(p,
-				electric
-				? PAL_WHITE
-				: PAL_NUKE,
-				1);
+			    if (electric)
+				    P_FlashPal(p, PAL_WHITE, 1);
+				
 				p->powers[pw_shield] = p->powers[pw_shield] & SH_STACK;
 			}
 		}
@@ -3366,7 +3402,7 @@ void P_MobjCheckWater(mobj_t *mobj)
 			}
 
 			// skipping stone!
-			if (p && (p->charability2 == CA2_SPINDASH) && p->speed/2 > abs(mobj->momz)
+			if (p && p->speed/2 > abs(mobj->momz)
 				&& ((p->pflags & (PF_SPINNING|PF_JUMPED)) == PF_SPINNING)
 				&& ((!(mobj->eflags & MFE_VERTICALFLIP) && thingtop - mobj->momz > mobj->watertop)
 				|| ((mobj->eflags & MFE_VERTICALFLIP) && mobj->z - mobj->momz < mobj->waterbottom)))
@@ -4135,7 +4171,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))
@@ -4176,7 +4212,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)
@@ -4604,9 +4640,8 @@ static boolean P_Boss4MoveCage(mobj_t *mobj, fixed_t delta)
 	INT32 snum;
 	sector_t *sector;
 	boolean gotcage = false;
-	TAG_ITER_DECLARECOUNTER(0);
 
-	TAG_ITER_SECTORS(0, tag, snum)
+	TAG_ITER_SECTORS(tag, snum)
 	{
 		sector = &sectors[snum];
 		sector->floorheight += delta;
@@ -4690,9 +4725,8 @@ static void P_Boss4DestroyCage(mobj_t *mobj)
 	size_t a;
 	sector_t *sector, *rsec;
 	ffloor_t *rover;
-	TAG_ITER_DECLARECOUNTER(0);
 
-	TAG_ITER_SECTORS(0, tag, snum)
+	TAG_ITER_SECTORS(tag, snum)
 	{
 		sector = &sectors[snum];
 
@@ -5654,14 +5688,10 @@ static void P_Boss9Thinker(mobj_t *mobj)
 				if (P_RandomRange(1,(dist>>FRACBITS)/16) == 1)
 					break;
 			}
-			if (spawner)
+			if (spawner && dist)
 			{
 				mobj_t *missile = P_SpawnMissile(spawner, mobj, MT_MSGATHER);
-
-				if (dist == 0)
-					missile->fuse = 0;
-				else
-					missile->fuse = (dist/P_AproxDistance(missile->momx, missile->momy));
+				missile->fuse = (dist/P_AproxDistance(missile->momx, missile->momy));
 
 				if (missile->fuse > mobj->fuse)
 					P_RemoveMobj(missile);
@@ -6840,7 +6870,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 +7341,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;
@@ -7513,7 +7543,7 @@ static void P_RosySceneryThink(mobj_t *mobj)
 
 static void P_MobjSceneryThink(mobj_t *mobj)
 {
-	if (LUAh_MobjThinker(mobj))
+	if (LUA_HookMobj(mobj, MOBJ_HOOK(MobjThinker)))
 		return;
 	if (P_MobjWasRemoved(mobj))
 		return;
@@ -7861,7 +7891,7 @@ static void P_MobjSceneryThink(mobj_t *mobj)
 
 		if (!mobj->fuse)
 		{
-			if (!LUAh_MobjFuse(mobj))
+			if (!LUA_HookMobj(mobj, MOBJ_HOOK(MobjFuse)))
 				P_RemoveMobj(mobj);
 			return;
 		}
@@ -7920,7 +7950,7 @@ static void P_MobjSceneryThink(mobj_t *mobj)
 			mobj->fuse--;
 			if (!mobj->fuse)
 			{
-				if (!LUAh_MobjFuse(mobj))
+				if (!LUA_HookMobj(mobj, MOBJ_HOOK(MobjFuse)))
 					P_RemoveMobj(mobj);
 				return;
 			}
@@ -7937,7 +7967,7 @@ static boolean P_MobjPushableThink(mobj_t *mobj)
 	P_PushableThinker(mobj);
 
 	// Extinguish fire objects in water. (Yes, it's extraordinarily rare to have a pushable flame object, but Brak uses such a case.)
-	if (mobj->flags & MF_FIRE && mobj->type != MT_PUMA && mobj->type != MT_FIREBALL
+	if ((mobj->flags & MF_FIRE) && !(mobj->eflags & MFE_TOUCHLAVA)
 		&& (mobj->eflags & (MFE_UNDERWATER | MFE_TOUCHWATER)))
 	{
 		P_KillMobj(mobj, NULL, NULL, 0);
@@ -7949,7 +7979,7 @@ static boolean P_MobjPushableThink(mobj_t *mobj)
 
 static boolean P_MobjBossThink(mobj_t *mobj)
 {
-	if (LUAh_BossThinker(mobj))
+	if (LUA_HookMobj(mobj, MOBJ_HOOK(BossThinker)))
 	{
 		if (P_MobjWasRemoved(mobj))
 			return false;
@@ -9651,6 +9681,12 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
 		break;
 	}
 	case MT_SALOONDOOR:
+		if (!mobj->tracer) // Door center is gone or not spawned?
+		{
+			P_RemoveMobj(mobj); // Die
+			return false;
+		}
+
 		P_SaloonDoorThink(mobj);
 		break;
 	case MT_MINECARTSPAWNER:
@@ -9705,7 +9741,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
 		P_MobjCheckWater(mobj);
 
 		// Extinguish fire objects in water
-		if (mobj->flags & MF_FIRE && mobj->type != MT_PUMA && mobj->type != MT_FIREBALL
+		if ((mobj->flags & MF_FIRE) && !(mobj->eflags & MFE_TOUCHLAVA)
 			&& (mobj->eflags & (MFE_UNDERWATER|MFE_TOUCHWATER)))
 		{
 			P_KillMobj(mobj, NULL, NULL, 0);
@@ -9835,7 +9871,7 @@ static void P_FlagFuseThink(mobj_t *mobj)
 	if (mobj->type == MT_REDFLAG)
 	{
 		if (!(mobj->flags2 & MF2_JUSTATTACKED))
-			CONS_Printf(M_GetText("The %c%s%c has returned to base.\n"), 0x85, M_GetText("Red flag"), 0x80);
+			CONS_Printf(M_GetText("The \205Red flag\200 has returned to base.\n"));
 
 		// Assumedly in splitscreen players will be on opposing teams
 		if (players[consoleplayer].ctfteam == 1 || splitscreen)
@@ -9848,7 +9884,7 @@ static void P_FlagFuseThink(mobj_t *mobj)
 	else // MT_BLUEFLAG
 	{
 		if (!(mobj->flags2 & MF2_JUSTATTACKED))
-			CONS_Printf(M_GetText("The %c%s%c has returned to base.\n"), 0x84, M_GetText("Blue flag"), 0x80);
+			CONS_Printf(M_GetText("The \204Blue flag\200 has returned to base.\n"));
 
 		// Assumedly in splitscreen players will be on opposing teams
 		if (players[consoleplayer].ctfteam == 2 || splitscreen)
@@ -9870,7 +9906,7 @@ static boolean P_FuseThink(mobj_t *mobj)
 	if (mobj->fuse)
 		return true;
 
-	if (LUAh_MobjFuse(mobj) || P_MobjWasRemoved(mobj))
+	if (LUA_HookMobj(mobj, MOBJ_HOOK(MobjFuse)) || P_MobjWasRemoved(mobj))
 		;
 	else if (mobj->info->flags & MF_MONITOR)
 	{
@@ -10046,13 +10082,13 @@ void P_MobjThinker(mobj_t *mobj)
 	// Check for a Lua thinker first
 	if (!mobj->player)
 	{
-		if (LUAh_MobjThinker(mobj) || P_MobjWasRemoved(mobj))
+		if (LUA_HookMobj(mobj, MOBJ_HOOK(MobjThinker)) || P_MobjWasRemoved(mobj))
 			return;
 	}
 	else if (!mobj->player->spectator)
 	{
 		// You cannot short-circuit the player thinker like you can other thinkers.
-		LUAh_MobjThinker(mobj);
+		LUA_HookMobj(mobj, MOBJ_HOOK(MobjThinker));
 		if (P_MobjWasRemoved(mobj))
 			return;
 	}
@@ -10386,6 +10422,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:
@@ -10481,9 +10520,6 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 	P_SetThingPosition(mobj);
 	I_Assert(mobj->subsector != NULL);
 
-	// Make sure scale matches destscale immediately when spawned
-	P_SetScale(mobj, mobj->destscale);
-
 	mobj->floorz   = P_GetSectorFloorZAt  (mobj->subsector->sector, x, y);
 	mobj->ceilingz = P_GetSectorCeilingZAt(mobj->subsector->sector, x, y);
 
@@ -10523,7 +10559,7 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 
 	// DANGER! This can cause P_SpawnMobj to return NULL!
 	// Avoid using P_RemoveMobj on the newly created mobj in "MobjSpawn" Lua hooks!
-	if (LUAh_MobjSpawn(mobj))
+	if (LUA_HookMobj(mobj, MOBJ_HOOK(MobjSpawn)))
 	{
 		if (P_MobjWasRemoved(mobj))
 			return NULL;
@@ -10910,7 +10946,7 @@ void P_RemoveMobj(mobj_t *mobj)
 		return; // something already removing this mobj.
 
 	mobj->thinker.function.acp1 = (actionf_p1)P_RemoveThinkerDelayed; // shh. no recursing.
-	LUAh_MobjRemoved(mobj);
+	LUA_HookMobj(mobj, MOBJ_HOOK(MobjRemoved));
 	mobj->thinker.function.acp1 = (actionf_p1)P_MobjThinker; // needed for P_UnsetThingPosition, etc. to work.
 
 	// Rings only, please!
@@ -11053,7 +11089,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
@@ -11099,22 +11135,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);
-	}
 }
 
 //
@@ -11397,6 +11425,10 @@ void P_SpawnPlayer(INT32 playernum)
 		p->jumpfactor = skins[p->skin].jumpfactor;
 	}
 
+	// Clear lastlinehit and lastsidehit
+	p->lastsidehit = -1;
+	p->lastlinehit = -1;
+
 	//awayview stuff
 	p->awayviewmobj = NULL;
 	p->awayviewtics = 0;
@@ -11792,7 +11824,7 @@ static boolean P_AllowMobjSpawn(mapthing_t* mthing, mobjtype_t i)
 		if (!(G_CoopGametype() || (mthing->options & MTF_EXTRA)))
 			return false; // she doesn't hang out here
 
-		if (!mariomode && !(netgame || multiplayer) && players[consoleplayer].skin == 3)
+		if (!(netgame || multiplayer) && players[consoleplayer].skin == 3)
 			return false; // no doubles
 
 		break;
@@ -11958,6 +11990,7 @@ static boolean P_SetupEmblem(mapthing_t *mthing, mobj_t *mobj)
 	INT32 j;
 	emblem_t* emblem = M_GetLevelEmblems(gamemap);
 	skincolornum_t emcolor;
+	boolean validEmblem = true;
 
 	while (emblem)
 	{
@@ -11982,8 +12015,19 @@ static boolean P_SetupEmblem(mapthing_t *mthing, mobj_t *mobj)
 	emcolor = M_GetEmblemColor(&emblemlocations[j]); // workaround for compiler complaint about bad function casting
 	mobj->color = (UINT16)emcolor;
 
-	if (emblemlocations[j].collected
-		|| (emblemlocations[j].type == ET_SKIN && emblemlocations[j].var != players[0].skin))
+	validEmblem = !emblemlocations[j].collected;
+
+	if (emblemlocations[j].type == ET_SKIN)
+	{
+		INT32 skinnum = M_EmblemSkinNum(&emblemlocations[j]);
+
+		if (players[0].skin != skinnum)
+		{
+			validEmblem = false;
+		}
+	}
+
+	if (validEmblem == false)
 	{
 		P_UnsetThingPosition(mobj);
 		mobj->flags |= MF_NOCLIP;
@@ -12556,7 +12600,7 @@ static boolean P_SetupBooster(mapthing_t* mthing, mobj_t* mobj, boolean strong)
 
 static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean *doangle)
 {
-	boolean override = LUAh_MapThingSpawn(mobj, mthing);
+	boolean override = LUA_HookMapThingSpawn(mobj, mthing);
 
 	if (P_MobjWasRemoved(mobj))
 		return false;
diff --git a/src/p_mobj.h b/src/p_mobj.h
index 5bb7c908e85463020507f1e02dbb7059d904ddf6..2d096385bd1689f2fb3402a5ca1489cbfd7bca40 100644
--- a/src/p_mobj.h
+++ b/src/p_mobj.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -218,33 +218,40 @@ typedef enum
 typedef enum
 {
 	// The mobj stands on solid floor (not on another mobj or in air)
-	MFE_ONGROUND          = 1,
+	MFE_ONGROUND			= 1,
 	// The mobj just hit the floor while falling, this is cleared on next frame
 	// (instant damage in lava/slime sectors to prevent jump cheat..)
-	MFE_JUSTHITFLOOR      = 1<<1,
+	MFE_JUSTHITFLOOR		= 1<<1,
 	// The mobj stands in a sector with water, and touches the surface
 	// this bit is set once and for all at the start of mobjthinker
-	MFE_TOUCHWATER        = 1<<2,
+	MFE_TOUCHWATER			= 1<<2,
 	// The mobj stands in a sector with water, and his waist is BELOW the water surface
 	// (for player, allows swimming up/down)
-	MFE_UNDERWATER        = 1<<3,
+	MFE_UNDERWATER			= 1<<3,
 	// used for ramp sectors
-	MFE_JUSTSTEPPEDDOWN   = 1<<4,
+	MFE_JUSTSTEPPEDDOWN		= 1<<4,
 	// Vertically flip sprite/allow upside-down physics
-	MFE_VERTICALFLIP      = 1<<5,
+	MFE_VERTICALFLIP		= 1<<5,
 	// Goo water
-	MFE_GOOWATER          = 1<<6,
+	MFE_GOOWATER			= 1<<6,
 	// The mobj is touching a lava block
-	MFE_TOUCHLAVA         = 1<<7,
+	MFE_TOUCHLAVA			= 1<<7,
 	// Mobj was already pushed this tic
-	MFE_PUSHED            = 1<<8,
+	MFE_PUSHED				= 1<<8,
 	// Mobj was already sprung this tic
-	MFE_SPRUNG            = 1<<9,
+	MFE_SPRUNG				= 1<<9,
 	// Platform movement
-	MFE_APPLYPMOMZ        = 1<<10,
+	MFE_APPLYPMOMZ			= 1<<10,
 	// Compute and trigger on mobj angle relative to tracer
 	// See Linedef Exec 457 (Track mobj angle to point)
-	MFE_TRACERANGLE       = 1<<11,
+	MFE_TRACERANGLE			= 1<<11,
+	// Forces an object to use super sprites with SPR_PLAY.
+	MFE_FORCESUPER			= 1<<12,
+	// Forces an object to NOT use super sprites with SPR_PLAY.
+	MFE_FORCENOSUPER		= 1<<13,
+	// Makes an object use super sprites where they wouldn't have otherwise and vice-versa
+	MFE_REVERSESUPER		= MFE_FORCESUPER|MFE_FORCENOSUPER
+	
 	// free: to and including 1<<15
 } mobjeflag_t;
 
diff --git a/src/p_polyobj.c b/src/p_polyobj.c
index 874edbd50cfb60d6ff0308748dd149c96f8480c3..6431e46248f0f426de801e2c2e95d9835ab07ff0 100644
--- a/src/p_polyobj.c
+++ b/src/p_polyobj.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2006      by James Haley
-// Copyright (C) 2006-2020 by Sonic Team Junior.
+// Copyright (C) 2006-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/p_polyobj.h b/src/p_polyobj.h
index 8c29469653aa6207731fe7dcccbebba313d345b7..7c814e0bf14766bf6f6cf2af134675389d0f8797 100644
--- a/src/p_polyobj.h
+++ b/src/p_polyobj.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2006      by James Haley
-// Copyright (C) 2006-2020 by Sonic Team Junior.
+// Copyright (C) 2006-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/p_pspr.h b/src/p_pspr.h
index 231262beb3aa204b331103a42342369efb624259..4525ba14cc9f4844573a22e5df2f36418799efa2 100644
--- a/src/p_pspr.h
+++ b/src/p_pspr.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/p_saveg.c b/src/p_saveg.c
index adedea049108ecbd1d44b8931bb0854365128005..1270064c01f1494abc582fc7a61c8a80791f4635 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -64,12 +64,29 @@ typedef enum
 static inline void P_ArchivePlayer(void)
 {
 	const player_t *player = &players[consoleplayer];
-	INT16 skininfo = player->skin + (botskin<<5);
 	SINT8 pllives = player->lives;
 	if (pllives < startinglivesbalance[numgameovers]) // Bump up to 3 lives if the player
 		pllives = startinglivesbalance[numgameovers]; // has less than that.
 
-	WRITEUINT16(save_p, skininfo);
+#ifdef NEWSKINSAVES
+	// Write a specific value into the old skininfo location.
+	// If we read something other than this, it's an older save file that used skin numbers.
+	WRITEUINT16(save_p, NEWSKINSAVES);
+#endif
+
+	// Write skin names, so that loading skins in different orders
+	// doesn't change who the save file is for!
+	WRITESTRINGN(save_p, skins[player->skin].name, SKINNAMESIZE);
+
+	if (botskin != 0)
+	{
+		WRITESTRINGN(save_p, skins[botskin-1].name, SKINNAMESIZE);
+	}
+	else
+	{
+		WRITESTRINGN(save_p, "\0", SKINNAMESIZE);
+	}
+
 	WRITEUINT8(save_p, numgameovers);
 	WRITESINT8(save_p, pllives);
 	WRITEUINT32(save_p, player->score);
@@ -78,9 +95,27 @@ static inline void P_ArchivePlayer(void)
 
 static inline void P_UnArchivePlayer(void)
 {
-	INT16 skininfo = READUINT16(save_p);
-	savedata.skin = skininfo & ((1<<5) - 1);
-	savedata.botskin = skininfo >> 5;
+#ifdef NEWSKINSAVES
+	INT16 backwardsCompat = READUINT16(save_p);
+
+	if (backwardsCompat != NEWSKINSAVES)
+	{
+		// This is an older save file, which used direct skin numbers.
+		savedata.skin = backwardsCompat & ((1<<5) - 1);
+		savedata.botskin = backwardsCompat >> 5;
+	}
+	else
+#endif
+	{
+		char ourSkinName[SKINNAMESIZE+1];
+		char botSkinName[SKINNAMESIZE+1];
+
+		READSTRINGN(save_p, ourSkinName, SKINNAMESIZE);
+		savedata.skin = R_SkinAvailable(ourSkinName);
+
+		READSTRINGN(save_p, botSkinName, SKINNAMESIZE);
+		savedata.botskin = R_SkinAvailable(botSkinName) + 1;
+	}
 
 	savedata.numgameovers = READUINT8(save_p);
 	savedata.lives = READSINT8(save_p);
@@ -158,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 //
 		////////////////////////////
@@ -372,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 //
 		////////////////////////////
@@ -1506,7 +1568,7 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 {
 	const mobj_t *mobj = (const mobj_t *)th;
 	UINT32 diff;
-	UINT16 diff2;
+	UINT32 diff2;
 
 	// Ignore stationary hoops - these will be respawned from mapthings.
 	if (mobj->type == MT_HOOP)
@@ -1638,7 +1700,7 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 		diff2 |= MD2_SHADOWSCALE;
 	if (mobj->renderflags)
 		diff2 |= MD2_RENDERFLAGS;
-	if (mobj->renderflags)
+	if (mobj->blendmode != AST_TRANSLUCENT)
 		diff2 |= MD2_BLENDMODE;
 	if (mobj->spritexscale != FRACUNIT)
 		diff2 |= MD2_SPRITEXSCALE;
@@ -1646,6 +1708,8 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 		diff2 |= MD2_SPRITEYSCALE;
 	if (mobj->spritexoffset)
 		diff2 |= MD2_SPRITEXOFFSET;
+	if (mobj->spriteyoffset)
+		diff2 |= MD2_SPRITEYOFFSET;
 	if (mobj->floorspriteslope)
 	{
 		pslope_t *slope = mobj->floorspriteslope;
@@ -1667,7 +1731,7 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 	WRITEUINT8(save_p, type);
 	WRITEUINT32(save_p, diff);
 	if (diff & MD_MORE)
-		WRITEUINT16(save_p, diff2);
+		WRITEUINT32(save_p, diff2);
 
 	// save pointer, at load time we will search this pointer to reinitilize pointers
 	WRITEUINT32(save_p, (size_t)mobj);
@@ -2615,14 +2679,14 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 	thinker_t *next;
 	mobj_t *mobj;
 	UINT32 diff;
-	UINT16 diff2;
+	UINT32 diff2;
 	INT32 i;
 	fixed_t z, floorz, ceilingz;
 	ffloor_t *floorrover = NULL, *ceilingrover = NULL;
 
 	diff = READUINT32(save_p);
 	if (diff & MD_MORE)
-		diff2 = READUINT16(save_p);
+		diff2 = READUINT32(save_p);
 	else
 		diff2 = 0;
 
@@ -2843,10 +2907,16 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 		mobj->renderflags = READUINT32(save_p);
 	if (diff2 & MD2_BLENDMODE)
 		mobj->blendmode = READINT32(save_p);
+	else
+		mobj->blendmode = AST_TRANSLUCENT;
 	if (diff2 & MD2_SPRITEXSCALE)
 		mobj->spritexscale = READFIXED(save_p);
+	else
+		mobj->spritexscale = FRACUNIT;
 	if (diff2 & MD2_SPRITEYSCALE)
 		mobj->spriteyscale = READFIXED(save_p);
+	else
+		mobj->spriteyscale = FRACUNIT;
 	if (diff2 & MD2_SPRITEXOFFSET)
 		mobj->spritexoffset = READFIXED(save_p);
 	if (diff2 & MD2_SPRITEYOFFSET)
@@ -4182,7 +4252,10 @@ static inline boolean P_NetUnArchiveMisc(boolean reloading)
 	tokenlist = READUINT32(save_p);
 
 	if (!P_LoadLevel(true, reloading))
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("Can't load the level!\n"));
 		return false;
+	}
 
 	// get the time
 	leveltime = READUINT32(save_p);
@@ -4260,19 +4333,26 @@ static inline boolean P_UnArchiveLuabanksAndConsistency(void)
 {
 	switch (READUINT8(save_p))
 	{
-		case 0xb7:
+		case 0xb7: // luabanks marker
 			{
 				UINT8 i, banksinuse = READUINT8(save_p);
 				if (banksinuse > NUM_LUABANKS)
+				{
+					CONS_Alert(CONS_ERROR, M_GetText("Corrupt Luabanks! (Too many banks in use)\n"));
 					return false;
+				}
 				for (i = 0; i < banksinuse; i++)
 					luabanks[i] = READINT32(save_p);
-				if (READUINT8(save_p) != 0x1d)
+				if (READUINT8(save_p) != 0x1d) // consistency marker
+				{
+					CONS_Alert(CONS_ERROR, M_GetText("Corrupt Luabanks! (Failed consistency check)\n"));
 					return false;
+				}
 			}
-		case 0x1d:
+		case 0x1d: // consistency marker
 			break;
-		default:
+		default: // anything else is nonsense
+			CONS_Alert(CONS_ERROR, M_GetText("Failed consistency check (???)\n"));
 			return false;
 	}
 
diff --git a/src/p_saveg.h b/src/p_saveg.h
index be98953eb232198e88fb757840f9e0021fa48f2d..a909282fe55e85ce1077d43fa6e69621e2da8a02 100644
--- a/src/p_saveg.h
+++ b/src/p_saveg.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -18,6 +18,8 @@
 #pragma interface
 #endif
 
+#define NEWSKINSAVES (INT16_MAX) // Purely for backwards compatibility, remove this for 2.3
+
 // Persistent storage/archiving.
 // These are the load / save game routines.
 
diff --git a/src/p_setup.c b/src/p_setup.c
index 918ffbd4e6955348e4597963520245fd2ef353a6..588815aeead2d932e22fea91951d094de04bc7f1 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -65,7 +65,7 @@
 
 #include "md5.h" // map MD5
 
-// for LUAh_MapLoad
+// for MapLoad hook
 #include "lua_script.h"
 #include "lua_hook.h"
 
@@ -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;
@@ -1621,6 +1677,14 @@ static void ParseTextmapLinedefParameter(UINT32 i, char *param, char *val)
 		P_SetLinedefV1(i, atol(val));
 	else if (fastcmp(param, "v2"))
 		P_SetLinedefV2(i, atol(val));
+	else if (strlen(param) == 7 && fastncmp(param, "arg", 3) && fastncmp(param + 4, "str", 3))
+	{
+		size_t argnum = param[3] - '0';
+		if (argnum >= NUMLINESTRINGARGS)
+			return;
+		lines[i].stringargs[argnum] = Z_Malloc(strlen(val) + 1, PU_LEVEL, NULL);
+		M_Memcpy(lines[i].stringargs[argnum], val, strlen(val) + 1);
+	}
 	else if (fastncmp(param, "arg", 3) && strlen(param) > 3)
 	{
 		size_t argnum = atol(param + 3);
@@ -1628,14 +1692,6 @@ static void ParseTextmapLinedefParameter(UINT32 i, char *param, char *val)
 			return;
 		lines[i].args[argnum] = atol(val);
 	}
-	else if (fastncmp(param, "stringarg", 9) && strlen(param) > 9)
-	{
-		size_t argnum = param[9] - '0';
-		if (argnum >= NUMLINESTRINGARGS)
-			return;
-		lines[i].stringargs[argnum] = Z_Malloc(strlen(val) + 1, PU_LEVEL, NULL);
-		M_Memcpy(lines[i].stringargs[argnum], val, strlen(val) + 1);
-	}
 	else if (fastcmp(param, "sidefront"))
 		lines[i].sidenum[0] = atol(val);
 	else if (fastcmp(param, "sideback"))
@@ -1720,6 +1776,14 @@ static void ParseTextmapThingParameter(UINT32 i, char *param, char *val)
 	else if (fastcmp(param, "ambush") && fastcmp("true", val))
 		mapthings[i].options |= MTF_AMBUSH;
 
+	else if (strlen(param) == 7 && fastncmp(param, "arg", 3) && fastncmp(param + 4, "str", 3))
+	{
+		size_t argnum = param[3] - '0';
+		if (argnum >= NUMMAPTHINGSTRINGARGS)
+			return;
+		mapthings[i].stringargs[argnum] = Z_Malloc(strlen(val) + 1, PU_LEVEL, NULL);
+		M_Memcpy(mapthings[i].stringargs[argnum], val, strlen(val) + 1);
+	}
 	else if (fastncmp(param, "arg", 3) && strlen(param) > 3)
 	{
 		size_t argnum = atol(param + 3);
@@ -1727,14 +1791,6 @@ static void ParseTextmapThingParameter(UINT32 i, char *param, char *val)
 			return;
 		mapthings[i].args[argnum] = atol(val);
 	}
-	else if (fastncmp(param, "stringarg", 9) && strlen(param) > 9)
-	{
-		size_t argnum = param[9] - '0';
-		if (argnum >= NUMMAPTHINGSTRINGARGS)
-			return;
-		mapthings[i].stringargs[argnum] = Z_Malloc(strlen(val) + 1, PU_LEVEL, NULL);
-		M_Memcpy(mapthings[i].stringargs[argnum], val, strlen(val) + 1);
-	}
 }
 
 /** From a given position table, run a specified parser function through a {}-encapsuled text.
@@ -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);
 	}
 
@@ -2486,7 +2559,7 @@ static void P_LoadMapBSP(const virtres_t *virt)
 		if (numsubsectors <= 0)
 			I_Error("Level has no subsectors (did you forget to run it through a nodesbuilder?)");
 		if (numnodes <= 0)
-			I_Error("Level has no nodes");
+			I_Error("Level has no nodes (does your map have at least 2 sectors?)");
 		if (numsegs <= 0)
 			I_Error("Level has no segs");
 
@@ -2950,6 +3023,75 @@ static void P_LinkMapData(void)
 	}
 }
 
+// For maps in binary format, add multi-tags from linedef specials. This must be done
+// before any linedef specials have been processed.
+static void P_AddBinaryMapTagsFromLine(sector_t *sector, line_t *line)
+{
+	Tag_Add(&sector->tags, Tag_FGet(&line->tags));
+	if (line->flags & ML_EFFECT6) {
+		if (sides[line->sidenum[0]].textureoffset)
+			Tag_Add(&sector->tags, (INT32)sides[line->sidenum[0]].textureoffset / FRACUNIT);
+		if (sides[line->sidenum[0]].rowoffset)
+			Tag_Add(&sector->tags, (INT32)sides[line->sidenum[0]].rowoffset / FRACUNIT);
+	}
+	if (line->flags & ML_TFERLINE) {
+		if (sides[line->sidenum[1]].textureoffset)
+			Tag_Add(&sector->tags, (INT32)sides[line->sidenum[1]].textureoffset / FRACUNIT);
+		if (sides[line->sidenum[1]].rowoffset)
+			Tag_Add(&sector->tags, (INT32)sides[line->sidenum[1]].rowoffset / FRACUNIT);
+	}
+}
+
+static void P_AddBinaryMapTags(void)
+{
+	size_t i;
+
+	for (i = 0; i < numlines; i++)
+	{
+		// 96: Apply Tag to Tagged Sectors
+		// 97: Apply Tag to Front Sector
+		// 98: Apply Tag to Back Sector
+		// 99: Apply Tag to Front and Back Sectors
+		if (lines[i].special == 96) {
+			size_t j;
+			mtag_t tag = Tag_FGet(&lines[i].frontsector->tags);
+			mtag_t target_tag = Tag_FGet(&lines[i].tags);
+			mtag_t offset_tags[4];
+			memset(offset_tags, 0, sizeof(mtag_t)*4);
+			if (lines[i].flags & ML_EFFECT6) {
+				offset_tags[0] = (INT32)sides[lines[i].sidenum[0]].textureoffset / FRACUNIT;
+				offset_tags[1] = (INT32)sides[lines[i].sidenum[0]].rowoffset / FRACUNIT;
+			}
+			if (lines[i].flags & ML_TFERLINE) {
+				offset_tags[2] = (INT32)sides[lines[i].sidenum[1]].textureoffset / FRACUNIT;
+				offset_tags[3] = (INT32)sides[lines[i].sidenum[1]].rowoffset / FRACUNIT;
+			}
+
+			for (j = 0; j < numsectors; j++) {
+				boolean matches_target_tag = target_tag && Tag_Find(&sectors[j].tags, target_tag);
+				size_t k; for (k = 0; k < 4; k++) {
+					if (lines[i].flags & ML_EFFECT5) {
+						if (matches_target_tag || (offset_tags[k] && Tag_Find(&sectors[j].tags, offset_tags[k]))) {
+							Tag_Add(&sectors[j].tags, tag);
+							break;
+						}
+					} else if (matches_target_tag) {
+						if (k == 0)
+							Tag_Add(&sectors[j].tags, tag);
+						if (offset_tags[k])
+							Tag_Add(&sectors[j].tags, offset_tags[k]);
+					}
+				}
+			}
+		} else {
+			if (lines[i].special == 97 || lines[i].special == 99)
+				P_AddBinaryMapTagsFromLine(lines[i].frontsector, &lines[i]);
+			if (lines[i].special == 98 || lines[i].special == 99)
+				P_AddBinaryMapTagsFromLine(lines[i].backsector, &lines[i]);
+		}
+	}
+}
+
 //For maps in binary format, converts setup of specials to UDMF format.
 static void P_ConvertBinaryMap(void)
 {
@@ -2966,9 +3108,7 @@ static void P_ConvertBinaryMap(void)
 			INT32 check = -1;
 			INT32 paramline = -1;
 
-			TAG_ITER_DECLARECOUNTER(0);
-
-			TAG_ITER_LINES(0, tag, check)
+			TAG_ITER_LINES(tag, check)
 			{
 				if (lines[check].special == 22)
 				{
@@ -3087,6 +3227,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;
 		}
@@ -3141,6 +3287,34 @@ static void P_ConvertBinaryMap(void)
 				lines[i].args[1] = tag;
 			lines[i].special = 720;
 			break;
+		case 723: //Copy back side floor slope
+		case 724: //Copy back side ceiling slope
+		case 725: //Copy back side floor and ceiling slope
+			if (lines[i].special != 724)
+				lines[i].args[2] = tag;
+			if (lines[i].special != 723)
+				lines[i].args[3] = tag;
+			lines[i].special = 720;
+			break;
+		case 730: //Copy front side floor slope to back side
+		case 731: //Copy front side ceiling slope to back side
+		case 732: //Copy front side floor and ceiling slope to back side
+			if (lines[i].special != 731)
+				lines[i].args[4] |= TMSC_FRONTTOBACKFLOOR;
+			if (lines[i].special != 730)
+				lines[i].args[4] |= TMSC_FRONTTOBACKCEILING;
+			lines[i].special = 720;
+			break;
+		case 733: //Copy back side floor slope to front side
+		case 734: //Copy back side ceiling slope to front side
+		case 735: //Copy back side floor and ceiling slope to front side
+			if (lines[i].special != 734)
+				lines[i].args[4] |= TMSC_BACKTOFRONTFLOOR;
+			if (lines[i].special != 733)
+				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%)
@@ -3171,7 +3345,7 @@ static void P_ConvertBinaryMap(void)
 		switch (mapthings[i].type)
 		{
 		case 750:
-			Tag_Add(&mapthings[i].tags, mapthings[i].angle);
+			Tag_FSet(&mapthings[i].tags, mapthings[i].angle);
 			break;
 		case 760:
 		case 761:
@@ -3183,11 +3357,9 @@ static void P_ConvertBinaryMap(void)
 			INT32 firstline = -1;
 			mtag_t tag = mapthings[i].angle;
 
-			TAG_ITER_DECLARECOUNTER(0);
-
 			Tag_FSet(&mapthings[i].tags, tag);
 
-			TAG_ITER_LINES(0, tag, check)
+			TAG_ITER_LINES(tag, check)
 			{
 				if (lines[check].special == 20)
 				{
@@ -3287,6 +3459,9 @@ static boolean P_LoadMapFromFile(void)
 
 	P_LinkMapData();
 
+	if (!udmf)
+		P_AddBinaryMapTags();
+
 	Taglist_InitGlobalTables();
 
 	if (!udmf)
@@ -3394,8 +3569,10 @@ static void P_InitLevelSettings(void)
 	numstarposts = 0;
 	ssspheres = timeinmap = 0;
 
-	// special stage
-	stagefailed = true; // assume failed unless proven otherwise - P_GiveEmerald or emerald touchspecial
+	// Assume Special Stages were failed in unless proven otherwise - via P_GiveEmerald or emerald touchspecial
+	// Normal stages will default to be OK, until a Lua script / linedef executor says otherwise.
+	stagefailed = G_IsSpecialStage(gamemap);
+
 	// Reset temporary record data
 	memset(&ntemprecords, 0, sizeof(nightsdata_t));
 
@@ -4135,7 +4312,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 
 #ifdef HWRENDER
 	// Free GPU textures before freeing patches.
-	if (vid.glstate == VID_GL_LIBRARY_LOADED)
+	if (rendermode == render_opengl && (vid.glstate == VID_GL_LIBRARY_LOADED))
 		HWR_ClearAllTextures();
 #endif
 
@@ -4174,7 +4351,9 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 
 	P_ResetWaypoints();
 
-	P_MapStart();
+	P_MapStart(); // tmthing can be used starting from this point
+
+	P_InitSlopes();
 
 	if (!P_LoadMapFromFile())
 		return false;
@@ -4227,8 +4406,6 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	// clear special respawning que
 	iquehead = iquetail = 0;
 
-	P_MapEnd();
-
 	// Remove the loading shit from the screen
 	if (rendermode != render_none && !(titlemapinaction || reloadinggamestate))
 		F_WipeColorFill(levelfadecol);
@@ -4248,6 +4425,8 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 
 	P_RunCachedActions();
 
+	P_MapEnd(); // tmthing is no longer needed from this point onwards
+
 	// Took me 3 hours to figure out why my progression kept on getting overwritten with the titlemap...
 	if (!titlemapinaction)
 	{
@@ -4271,7 +4450,9 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 				G_CopyTiccmd(&players[i].cmd, &netcmds[buf][i], 1);
 		}
 		P_PreTicker(2);
-		LUAh_MapLoad();
+		P_MapStart(); // just in case MapLoad modifies tmthing
+		LUA_HookInt(gamemap, HOOK(MapLoad));
+		P_MapEnd(); // just in case MapLoad modifies tmthing
 	}
 
 	// No render mode or reloading gamestate, stop here.
@@ -4385,10 +4566,9 @@ static lumpinfo_t* FindFolder(const char *folName, UINT16 *start, UINT16 *end, l
 // Add a wadfile to the active wad files,
 // replace sounds, musics, patches, textures, sprites and maps
 //
-boolean P_AddWadFile(const char *wadfilename)
+static boolean P_LoadAddon(UINT16 wadnum, UINT16 numlumps)
 {
 	size_t i, j, sreplaces = 0, mreplaces = 0, digmreplaces = 0;
-	UINT16 numlumps, wadnum;
 	char *name;
 	lumpinfo_t *lumpinfo;
 
@@ -4409,18 +4589,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++)
@@ -4500,7 +4672,7 @@ boolean P_AddWadFile(const char *wadfilename)
 
 #ifdef HWRENDER
 	// Free GPU textures before freeing patches.
-	if (vid.glstate == VID_GL_LIBRARY_LOADED)
+	if (rendermode == render_opengl && (vid.glstate == VID_GL_LIBRARY_LOADED))
 		HWR_ClearAllTextures();
 #endif
 
@@ -4527,8 +4699,8 @@ boolean P_AddWadFile(const char *wadfilename)
 	//
 	// look for skins
 	//
-	R_AddSkins(wadnum); // faB: wadfile index in wadfiles[]
-	R_PatchSkins(wadnum); // toast: PATCH PATCH
+	R_AddSkins(wadnum, false); // faB: wadfile index in wadfiles[]
+	R_PatchSkins(wadnum, false); // toast: PATCH PATCH
 	ST_ReloadSkinFaceGraphics();
 
 	//
@@ -4584,3 +4756,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 34de9c93da1c4a91f7c46cc25af8107136df530e..c3c680fdd3176392580373f8aedec1d154987006 100644
--- a/src/p_setup.h
+++ b/src/p_setup.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -80,6 +80,7 @@ typedef struct
 	UINT8 *picture;
 #ifdef HWRENDER
 	void *mipmap;
+	void *mippic;
 #endif
 } levelflat_t;
 
@@ -102,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_sight.c b/src/p_sight.c
index 2e1e499970418d23f5129e9271199592dbb9ce23..706745f35b45aa4f1439d8e415cc2171d32083a1 100644
--- a/src/p_sight.c
+++ b/src/p_sight.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -307,7 +307,7 @@ static boolean P_CrossSubsector(size_t num, register los_t *los)
 			for (rover = front->ffloors; rover; rover = rover->next)
 			{
 				if (!(rover->flags & FF_EXISTS)
-					|| !(rover->flags & FF_RENDERSIDES) || rover->flags & FF_TRANSLUCENT)
+					|| !(rover->flags & FF_RENDERSIDES) || (rover->flags & (FF_TRANSLUCENT|FF_FOG)))
 				{
 					continue;
 				}
@@ -323,7 +323,7 @@ static boolean P_CrossSubsector(size_t num, register los_t *los)
 			for (rover = back->ffloors; rover; rover = rover->next)
 			{
 				if (!(rover->flags & FF_EXISTS)
-					|| !(rover->flags & FF_RENDERSIDES) || rover->flags & FF_TRANSLUCENT)
+					|| !(rover->flags & FF_RENDERSIDES) || (rover->flags & (FF_TRANSLUCENT|FF_FOG)))
 				{
 					continue;
 				}
@@ -452,7 +452,7 @@ boolean P_CheckSight(mobj_t *t1, mobj_t *t2)
 			/// \todo Improve by checking fog density/translucency
 			/// and setting a sight limit.
 			if (!(rover->flags & FF_EXISTS)
-				|| !(rover->flags & FF_RENDERPLANES) || rover->flags & FF_TRANSLUCENT)
+				|| !(rover->flags & FF_RENDERPLANES) || (rover->flags & (FF_TRANSLUCENT|FF_FOG)))
 			{
 				continue;
 			}
diff --git a/src/p_slopes.c b/src/p_slopes.c
index aa46a84024d459e2c3dab0164d4122b59f30b126..bfca153a628c8246fb5c6a2629e6d609115b7a13 100644
--- a/src/p_slopes.c
+++ b/src/p_slopes.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2004      by Stephen McGranahan
-// Copyright (C) 2015-2020 by Sonic Team Junior.
+// Copyright (C) 2015-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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)
 {
@@ -546,11 +576,10 @@ static boolean P_SetSlopeFromTag(sector_t *sec, INT32 tag, boolean ceiling)
 {
 	INT32 i;
 	pslope_t **secslope = ceiling ? &sec->c_slope : &sec->f_slope;
-	TAG_ITER_DECLARECOUNTER(0);
 
 	if (!tag || *secslope)
 		return false;
-	TAG_ITER_SECTORS(0, tag, i)
+	TAG_ITER_SECTORS(tag, i)
 	{
 		pslope_t *srcslope = ceiling ? sectors[i].c_slope : sectors[i].f_slope;
 		if (srcslope)
@@ -632,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();
 
@@ -665,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:
@@ -672,6 +711,13 @@ void P_SpawnSlopes(const boolean fromsave) {
 		}
 }
 
+/// Initializes slopes.
+void P_InitSlopes(void)
+{
+	slopelist = NULL;
+	slopecount = 0;
+}
+
 // ============================================================================
 //
 // Various utilities related to slopes
@@ -774,13 +820,13 @@ 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;
 	}
 
 	//CONS_Printf("Launched off of slope.\n");
 	mo->standingslope = NULL;
-
-	if (mo->player)
-		mo->player->powers[pw_justlaunched] = 1;
 }
 
 //
diff --git a/src/p_slopes.h b/src/p_slopes.h
index 46e8dc1e7e730dec73cfc8ffcd3aff4c4acdfeb4..43cd3edb0d9009f341b9ab82c168c3429c3521a5 100644
--- a/src/p_slopes.h
+++ b/src/p_slopes.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2004      by Stephen McGranahan
-// Copyright (C) 2015-2020 by Sonic Team Junior.
+// Copyright (C) 2015-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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 a1afdd00ddb12c4c4253e4dfe4547a8e3391734d..07410efa22de11856486facf0ff1dff56f91a436 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -35,7 +35,7 @@
 #include "v_video.h" // V_AUTOFADEOUT|V_ALLOWLOWERCASE
 #include "m_misc.h"
 #include "m_cond.h" //unlock triggers
-#include "lua_hook.h" // LUAh_LinedefExecute
+#include "lua_hook.h" // LUA_HookLinedefExecute
 #include "f_finale.h" // control text prompt
 #include "r_skins.h" // skins
 
@@ -1981,6 +1981,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
 //
@@ -1988,53 +2004,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)
 	{
@@ -2051,7 +2028,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;
@@ -2063,7 +2040,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];
@@ -2078,7 +2055,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;
 
@@ -2103,7 +2080,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;
 
@@ -2116,49 +2093,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;
@@ -2166,14 +2128,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;
@@ -2223,7 +2182,6 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 	INT32 secnum = -1;
 	mobj_t *bot = NULL;
 	mtag_t tag = Tag_FGet(&line->tags);
-	TAG_ITER_DECLARECOUNTER(0);
 
 	I_Assert(!mo || !P_MobjWasRemoved(mo)); // If mo is there, mo must be valid!
 
@@ -2251,7 +2209,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				newceilinglightsec = line->frontsector->ceilinglightsec;
 
 				// act on all sectors with the same tag as the triggering linedef
-				TAG_ITER_SECTORS(0, tag, secnum)
+				TAG_ITER_SECTORS(tag, secnum)
 				{
 					if (sectors[secnum].lightingdata)
 					{
@@ -2306,7 +2264,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 		case 409: // Change tagged sectors' tag
 		// (formerly "Change calling sectors' tag", but behavior was changed)
 		{
-			TAG_ITER_SECTORS(0, tag, secnum)
+			TAG_ITER_SECTORS(tag, secnum)
 				Tag_SectorFSet(secnum,(INT16)(sides[line->sidenum[0]].textureoffset>>FRACBITS));
 			break;
 		}
@@ -2316,7 +2274,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 
 		case 411: // Stop floor/ceiling movement in tagged sector(s)
-			TAG_ITER_SECTORS(0, tag, secnum)
+			TAG_ITER_SECTORS(tag, secnum)
 			{
 				if (sectors[secnum].floordata)
 				{
@@ -2501,7 +2459,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 						// Additionally play the sound from tagged sectors' soundorgs
 						sector_t *sec;
 
-						TAG_ITER_SECTORS(0, tag, secnum)
+						TAG_ITER_SECTORS(tag, secnum)
 						{
 							sec = &sectors[secnum];
 							S_StartSound(&sec->soundorg, sfxnum);
@@ -2616,7 +2574,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 
 		case 416: // Spawn adjustable fire flicker
-			TAG_ITER_SECTORS(0, tag, secnum)
+			TAG_ITER_SECTORS(tag, secnum)
 			{
 				if (line->flags & ML_NOCLIMB && line->backsector)
 				{
@@ -2650,7 +2608,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 
 		case 417: // Spawn adjustable glowing light
-			TAG_ITER_SECTORS(0, tag, secnum)
+			TAG_ITER_SECTORS(tag, secnum)
 			{
 				if (line->flags & ML_NOCLIMB && line->backsector)
 				{
@@ -2684,7 +2642,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 
 		case 418: // Spawn adjustable strobe flash (unsynchronized)
-			TAG_ITER_SECTORS(0, tag, secnum)
+			TAG_ITER_SECTORS(tag, secnum)
 			{
 				if (line->flags & ML_NOCLIMB && line->backsector)
 				{
@@ -2718,7 +2676,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 
 		case 419: // Spawn adjustable strobe flash (synchronized)
-			TAG_ITER_SECTORS(0, tag, secnum)
+			TAG_ITER_SECTORS(tag, secnum)
 			{
 				if (line->flags & ML_NOCLIMB && line->backsector)
 				{
@@ -2766,7 +2724,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 
 		case 421: // Stop lighting effect in tagged sectors
-			TAG_ITER_SECTORS(0, tag, secnum)
+			TAG_ITER_SECTORS(tag, secnum)
 				if (sectors[secnum].lightingdata)
 				{
 					P_RemoveThinker(&((elevator_t *)sectors[secnum].lightingdata)->thinker);
@@ -2980,7 +2938,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				ffloor_t *rover; // FOF that we are going to crumble
 				boolean foundrover = false; // for debug, "Can't find a FOF" message
 
-				TAG_ITER_SECTORS(0, sectag, secnum)
+				TAG_ITER_SECTORS(sectag, secnum)
 				{
 					sec = sectors + secnum;
 
@@ -3105,7 +3063,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			if (line->sidenum[1] != 0xffff)
 				state = (statenum_t)sides[line->sidenum[1]].toptexture;
 
-			TAG_ITER_SECTORS(0, tag, secnum)
+			TAG_ITER_SECTORS(tag, secnum)
 			{
 				boolean tryagain;
 				sec = sectors + secnum;
@@ -3135,7 +3093,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 		case 443: // Calls a named Lua function
 			if (line->stringargs[0])
-				LUAh_LinedefExecute(line, mo, callsec);
+				LUA_HookLinedefExecute(line, mo, callsec);
 			else
 				CONS_Alert(CONS_WARNING, "Linedef %s is missing the hook name of the Lua function to call! (This should be given in arg0str)\n", sizeu1(line-lines));
 			break;
@@ -3165,7 +3123,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				boolean foundrover = false; // for debug, "Can't find a FOF" message
 				ffloortype_e oldflags; // store FOF's old flags
 
-				TAG_ITER_SECTORS(0, sectag, secnum)
+				TAG_ITER_SECTORS(sectag, secnum)
 				{
 					sec = sectors + secnum;
 
@@ -3223,7 +3181,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				if (line->flags & ML_NOCLIMB) // don't respawn!
 					respawn = false;
 
-				TAG_ITER_SECTORS(0, sectag, secnum)
+				TAG_ITER_SECTORS(sectag, secnum)
 				{
 					sec = sectors + secnum;
 
@@ -3279,7 +3237,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					source = sectors[sourcesec].extra_colormap;
 				}
 			}
-			TAG_ITER_SECTORS(0, line->args[0], secnum)
+			TAG_ITER_SECTORS(line->args[0], secnum)
 			{
 				if (sectors[secnum].colormap_protected)
 					continue;
@@ -3414,7 +3372,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			ffloor_t *rover; // FOF that we are going to operate
 			boolean foundrover = false; // for debug, "Can't find a FOF" message
 
-			TAG_ITER_SECTORS(0, sectag, secnum)
+			TAG_ITER_SECTORS(sectag, secnum)
 			{
 				sec = sectors + secnum;
 
@@ -3478,7 +3436,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			boolean foundrover = false; // for debug, "Can't find a FOF" message
 			size_t j = 0; // sec->ffloors is saved as ffloor #0, ss->ffloors->next is #1, etc
 
-			TAG_ITER_SECTORS(0, sectag, secnum)
+			TAG_ITER_SECTORS(sectag, secnum)
 			{
 				sec = sectors + secnum;
 
@@ -3563,7 +3521,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			ffloor_t *rover; // FOF that we are going to operate
 			boolean foundrover = false; // for debug, "Can't find a FOF" message
 
-			TAG_ITER_SECTORS(0, sectag, secnum)
+			TAG_ITER_SECTORS(sectag, secnum)
 			{
 				sec = sectors + secnum;
 
@@ -3614,7 +3572,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				}
 			}
 
-			TAG_ITER_SECTORS(0, line->args[0], secnum)
+			TAG_ITER_SECTORS(line->args[0], secnum)
 			{
 				extracolormap_t *source_exc, *dest_exc, *exc;
 
@@ -3694,7 +3652,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 		}
 		case 456: // Stop fade colormap
-			TAG_ITER_SECTORS(0, line->args[0], secnum)
+			TAG_ITER_SECTORS(line->args[0], secnum)
 				P_ResetColormapFader(&sectors[secnum]);
 			break;
 
@@ -3887,12 +3845,11 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 		case 465: // Set linedef executor delay
 			{
 				INT32 linenum;
-				TAG_ITER_DECLARECOUNTER(1);
 
 				if (!udmf)
 					break;
 
-				TAG_ITER_LINES(1, line->args[0], linenum)
+				TAG_ITER_LINES(line->args[0], linenum)
 				{
 					if (line->args[2])
 						lines[linenum].executordelay += line->args[1];
@@ -3902,6 +3859,21 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			}
 			break;
 
+		case 466: // Set level failure state
+			{
+				if (line->flags & ML_NOCLIMB)
+				{
+					stagefailed = false;
+					CONS_Debug(DBG_GAMELOGIC, "Stage can be completed successfully!\n");
+				}
+				else
+				{
+					stagefailed = true;
+					CONS_Debug(DBG_GAMELOGIC, "Stage will end in failure...\n");
+				}
+			}
+			break;
+
 		case 480: // Polyobj_DoorSlide
 		case 481: // Polyobj_DoorSwing
 			PolyDoor(line);
@@ -4304,7 +4276,7 @@ void P_ProcessSpecialSector(player_t *player, sector_t *sector, sector_t *rovers
 			if (leveltime % (TICRATE/2) == 0 && player->rings > 0)
 			{
 				player->rings--;
-				S_StartSound(player->mo, sfx_itemup);
+				S_StartSound(player->mo, sfx_antiri);
 			}
 			break;
 		case 11: // Special Stage Damage
@@ -4420,15 +4392,19 @@ void P_ProcessSpecialSector(player_t *player, sector_t *sector, sector_t *rovers
 			// clear the special so you can't push the button twice.
 			sector->special = 0;
 
+			// Initialize my junk
+			junk.tags.tags = NULL;
+			junk.tags.count = 0;
+
 			// Move the button down
-			Tag_FSet(&junk.tags, 680);
+			Tag_FSet(&junk.tags, LE_CAPSULE0);
 			EV_DoElevator(&junk, elevateDown, false);
 
 			// Open the top FOF
-			Tag_FSet(&junk.tags, 681);
+			Tag_FSet(&junk.tags, LE_CAPSULE1);
 			EV_DoFloor(&junk, raiseFloorToNearestFast);
 			// Open the bottom FOF
-			Tag_FSet(&junk.tags, 682);
+			Tag_FSet(&junk.tags, LE_CAPSULE2);
 			EV_DoCeiling(&junk, lowerToLowestFast);
 
 			// Mark all players with the time to exit thingy!
@@ -4503,7 +4479,7 @@ DoneSection2:
 
 				P_InstaThrust(player->mo, player->mo->angle, linespeed);
 
-				if ((lines[i].flags & ML_EFFECT5) && (player->charability2 == CA2_SPINDASH)) // Roll!
+				if (lines[i].flags & ML_EFFECT5) // Roll!
 				{
 					if (!(player->pflags & PF_SPINNING))
 						player->pflags |= PF_SPINNING;
@@ -4600,7 +4576,7 @@ DoneSection2:
 
 					HU_SetCEchoFlags(V_AUTOFADEOUT|V_ALLOWLOWERCASE);
 					HU_SetCEchoDuration(5);
-					HU_DoCEcho(va(M_GetText("%s%s%s\\CAPTURED THE %sBLUE FLAG%s.\\\\\\\\"), "\x85", player_names[player-players], "\x80", "\x84", "\x80"));
+					HU_DoCEcho(va(M_GetText("\205%s\200\\CAPTURED THE \204BLUE FLAG\200.\\\\\\\\"), player_names[player-players]));
 
 					if (splitscreen || players[consoleplayer].ctfteam == 1)
 						S_StartSound(NULL, sfx_flgcap);
@@ -4633,7 +4609,7 @@ DoneSection2:
 
 					HU_SetCEchoFlags(V_AUTOFADEOUT|V_ALLOWLOWERCASE);
 					HU_SetCEchoDuration(5);
-					HU_DoCEcho(va(M_GetText("%s%s%s\\CAPTURED THE %sRED FLAG%s.\\\\\\\\"), "\x84", player_names[player-players], "\x80", "\x85", "\x80"));
+					HU_DoCEcho(va(M_GetText("\204%s\200\\CAPTURED THE \205RED FLAG\200.\\\\\\\\"), player_names[player-players]));
 
 					if (splitscreen || players[consoleplayer].ctfteam == 2)
 						S_StartSound(NULL, sfx_flgcap);
@@ -4669,7 +4645,7 @@ DoneSection2:
 			break;
 
 		case 7: // Make player spin
-			if (!(player->pflags & PF_SPINNING) && P_IsObjectOnGround(player->mo) && (player->charability2 == CA2_SPINDASH))
+			if (!(player->pflags & PF_SPINNING))
 			{
 				player->pflags |= PF_SPINNING;
 				P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
@@ -4823,6 +4799,8 @@ DoneSection2:
 
 					if (player->laps >= (UINT8)cv_numlaps.value)
 						CONS_Printf(M_GetText("%s has finished the race.\n"), player_names[player-players]);
+					else if (player->laps == (UINT8)cv_numlaps.value-1)
+						CONS_Printf(M_GetText("%s started the \205final lap\200!\n"), player_names[player-players]);
 					else
 						CONS_Printf(M_GetText("%s started lap %u\n"), player_names[player-players], (UINT32)player->laps+1);
 
@@ -5922,9 +5900,8 @@ void T_LaserFlash(laserthink_t *flash)
 	sector_t *sector;
 	sector_t *sourcesec = flash->sourceline->frontsector;
 	fixed_t top, bottom;
-	TAG_ITER_DECLARECOUNTER(0);
 
-	TAG_ITER_SECTORS(0, flash->tag, s)
+	TAG_ITER_SECTORS(flash->tag, s)
 	{
 		sector = &sectors[s];
 		for (fflr = sector->ffloors; fflr; fflr = fflr->next)
@@ -6204,11 +6181,10 @@ void P_SpawnSpecials(boolean fromnetsave)
 			INT32 s;
 			size_t sec;
 			ffloortype_e ffloorflags;
-			TAG_ITER_DECLARECOUNTER(0);
 
 			case 1: // Definable gravity per sector
 				sec = sides[*lines[i].sidenum].sector - sectors;
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 				{
 					sectors[s].gravity = &sectors[sec].floorheight; // This allows it to change in realtime!
 
@@ -6232,7 +6208,7 @@ void P_SpawnSpecials(boolean fromnetsave)
 
 			case 5: // Change camera info
 				sec = sides[*lines[i].sidenum].sector - sectors;
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					P_AddCameraScanner(&sectors[sec], &sectors[s], R_PointToAngle2(lines[i].v2->x, lines[i].v2->y, lines[i].v1->x, lines[i].v1->y));
 				break;
 
@@ -6259,7 +6235,7 @@ void P_SpawnSpecials(boolean fromnetsave)
 						P_ApplyFlatAlignment(lines + i, lines[i].frontsector, flatangle, xoffs, yoffs);
 					else
 					{
-						TAG_ITER_SECTORS(0, tag, s)
+						TAG_ITER_SECTORS(tag, s)
 							P_ApplyFlatAlignment(lines + i, sectors + s, flatangle, xoffs, yoffs);
 					}
 				}
@@ -6270,7 +6246,7 @@ void P_SpawnSpecials(boolean fromnetsave)
 				break;
 
 			case 8: // Sector Parameters
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 				{
 					if (lines[i].flags & ML_NOCLIMB)
 					{
@@ -6297,7 +6273,7 @@ void P_SpawnSpecials(boolean fromnetsave)
 				break;
 
 			case 10: // Vertical culling plane for sprites and FOFs
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					sectors[s].cullheight = &lines[i]; // This allows it to change in realtime!
 				break;
 
@@ -6358,19 +6334,19 @@ void P_SpawnSpecials(boolean fromnetsave)
 
 			case 63: // support for drawn heights coming from different sector
 				sec = sides[*lines[i].sidenum].sector-sectors;
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					sectors[s].heightsec = (INT32)sec;
 				break;
 
 			case 64: // Appearing/Disappearing FOF option
 				if (lines[i].flags & ML_BLOCKMONSTERS) { // Find FOFs by control sector tag
-					TAG_ITER_SECTORS(0, tag, s)
+					TAG_ITER_SECTORS(tag, s)
 						for (j = 0; (unsigned)j < sectors[s].linecount; j++)
 							if (sectors[s].lines[j]->special >= 100 && sectors[s].lines[j]->special < 300)
 								Add_MasterDisappearer(abs(lines[i].dx>>FRACBITS), abs(lines[i].dy>>FRACBITS), abs(sides[lines[i].sidenum[0]].sector->floorheight>>FRACBITS), (INT32)(sectors[s].lines[j]-lines), (INT32)i);
 				} else // Find FOFs by effect sector tag
 				{
-					TAG_ITER_LINES(0, tag, s)
+					TAG_ITER_LINES(tag, s)
 					{
 						if ((size_t)s == i)
 							continue;
@@ -6381,15 +6357,15 @@ void P_SpawnSpecials(boolean fromnetsave)
 				break;
 
 			case 66: // Displace floor by front sector
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					P_AddPlaneDisplaceThinker(pd_floor, P_AproxDistance(lines[i].dx, lines[i].dy)>>8, sides[lines[i].sidenum[0]].sector-sectors, s, !!(lines[i].flags & ML_NOCLIMB));
 				break;
 			case 67: // Displace ceiling by front sector
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					P_AddPlaneDisplaceThinker(pd_ceiling, P_AproxDistance(lines[i].dx, lines[i].dy)>>8, sides[lines[i].sidenum[0]].sector-sectors, s, !!(lines[i].flags & ML_NOCLIMB));
 				break;
 			case 68: // Displace both floor AND ceiling by front sector
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					P_AddPlaneDisplaceThinker(pd_both, P_AproxDistance(lines[i].dx, lines[i].dy)>>8, sides[lines[i].sidenum[0]].sector-sectors, s, !!(lines[i].flags & ML_NOCLIMB));
 				break;
 
@@ -6985,46 +6961,46 @@ void P_SpawnSpecials(boolean fromnetsave)
 
 			case 600: // floor lighting independently (e.g. lava)
 				sec = sides[*lines[i].sidenum].sector-sectors;
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					sectors[s].floorlightsec = (INT32)sec;
 				break;
 
 			case 601: // ceiling lighting independently
 				sec = sides[*lines[i].sidenum].sector-sectors;
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					sectors[s].ceilinglightsec = (INT32)sec;
 				break;
 
 			case 602: // Adjustable pulsating light
 				sec = sides[*lines[i].sidenum].sector - sectors;
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					P_SpawnAdjustableGlowingLight(&sectors[sec], &sectors[s],
 						P_AproxDistance(lines[i].dx, lines[i].dy)>>FRACBITS);
 				break;
 
 			case 603: // Adjustable flickering light
 				sec = sides[*lines[i].sidenum].sector - sectors;
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					P_SpawnAdjustableFireFlicker(&sectors[sec], &sectors[s],
 						P_AproxDistance(lines[i].dx, lines[i].dy)>>FRACBITS);
 				break;
 
 			case 604: // Adjustable Blinking Light (unsynchronized)
 				sec = sides[*lines[i].sidenum].sector - sectors;
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					P_SpawnAdjustableStrobeFlash(&sectors[sec], &sectors[s],
 						abs(lines[i].dx)>>FRACBITS, abs(lines[i].dy)>>FRACBITS, false);
 				break;
 
 			case 605: // Adjustable Blinking Light (synchronized)
 				sec = sides[*lines[i].sidenum].sector - sectors;
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					P_SpawnAdjustableStrobeFlash(&sectors[sec], &sectors[s],
 						abs(lines[i].dx)>>FRACBITS, abs(lines[i].dy)>>FRACBITS, true);
 				break;
 
 			case 606: // HACK! Copy colormaps. Just plain colormaps.
-				TAG_ITER_SECTORS(0, lines[i].args[0], s)
+				TAG_ITER_SECTORS(lines[i].args[0], s)
 				{
 					extracolormap_t *exc;
 
@@ -7085,7 +7061,8 @@ void P_SpawnSpecials(boolean fromnetsave)
 		}
 	}
 
-	P_RunLevelLoadExecutors();
+	if (!fromnetsave)
+		P_RunLevelLoadExecutors();
 }
 
 /** Adds 3Dfloors as appropriate based on a common control linedef.
@@ -7098,13 +7075,12 @@ void P_SpawnSpecials(boolean fromnetsave)
   */
 static void P_AddFakeFloorsByLine(size_t line, ffloortype_e ffloorflags, thinkerlist_t *secthinkers)
 {
-	TAG_ITER_DECLARECOUNTER(0);
 	INT32 s;
 	mtag_t tag = Tag_FGet(&lines[line].tags);
 	size_t sec = sides[*lines[line].sidenum].sector-sectors;
 
 	line_t* li = lines + line;
-	TAG_ITER_SECTORS(0, tag, s)
+	TAG_ITER_SECTORS(tag, s)
 		P_AddFakeFloor(&sectors[s], &sectors[sec], li, ffloorflags, secthinkers);
 }
 
@@ -7214,7 +7190,6 @@ void T_Scroll(scroll_t *s)
 		size_t i;
 		INT32 sect;
 		ffloor_t *rover;
-		TAG_ITER_DECLARECOUNTER(0);
 
 		case sc_side: // scroll wall texture
 			side = sides + s->affectee;
@@ -7251,7 +7226,7 @@ void T_Scroll(scroll_t *s)
 				if (!is3dblock)
 					continue;
 
-				TAG_ITER_SECTORS(0, Tag_FGet(&line->tags), sect)
+				TAG_ITER_SECTORS(Tag_FGet(&line->tags), sect)
 				{
 					sector_t *psec;
 					psec = sectors + sect;
@@ -7326,7 +7301,7 @@ void T_Scroll(scroll_t *s)
 
 				if (!is3dblock)
 					continue;
-				TAG_ITER_SECTORS(0, Tag_FGet(&line->tags), sect)
+				TAG_ITER_SECTORS(Tag_FGet(&line->tags), sect)
 				{
 					sector_t *psec;
 					psec = sectors + sect;
@@ -7466,11 +7441,10 @@ static void P_SpawnScrollers(void)
 		switch (special)
 		{
 			register INT32 s;
-			TAG_ITER_DECLARECOUNTER(0);
 
 			case 513: // scroll effect ceiling
 			case 533: // scroll and carry objects on ceiling
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					Add_Scroller(sc_ceiling, -dx, dy, control, s, accel, l->flags & ML_NOCLIMB);
 				if (special != 533)
 					break;
@@ -7479,13 +7453,13 @@ static void P_SpawnScrollers(void)
 			case 523:	// carry objects on ceiling
 				dx = FixedMul(dx, CARRYFACTOR);
 				dy = FixedMul(dy, CARRYFACTOR);
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					Add_Scroller(sc_carry_ceiling, dx, dy, control, s, accel, l->flags & ML_NOCLIMB);
 				break;
 
 			case 510: // scroll effect floor
 			case 530: // scroll and carry objects on floor
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					Add_Scroller(sc_floor, -dx, dy, control, s, accel, l->flags & ML_NOCLIMB);
 				if (special != 530)
 					break;
@@ -7494,7 +7468,7 @@ static void P_SpawnScrollers(void)
 			case 520:	// carry objects on floor
 				dx = FixedMul(dx, CARRYFACTOR);
 				dy = FixedMul(dy, CARRYFACTOR);
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					Add_Scroller(sc_carry, dx, dy, control, s, accel, l->flags & ML_NOCLIMB);
 				break;
 
@@ -7502,7 +7476,7 @@ static void P_SpawnScrollers(void)
 			// (same direction and speed as scrolling floors)
 			case 502:
 			{
-				TAG_ITER_LINES(0, tag, s)
+				TAG_ITER_LINES(tag, s)
 					if (s != (INT32)i)
 					{
 						if (l->flags & ML_EFFECT2) // use texture offsets instead
@@ -7604,9 +7578,8 @@ void T_Disappear(disappear_t *d)
 		ffloor_t *rover;
 		register INT32 s;
 		mtag_t afftag = Tag_FGet(&lines[d->affectee].tags);
-		TAG_ITER_DECLARECOUNTER(0);
 
-		TAG_ITER_SECTORS(0, afftag, s)
+		TAG_ITER_SECTORS(afftag, s)
 		{
 			for (rover = sectors[s].ffloors; rover; rover = rover->next)
 			{
@@ -8337,7 +8310,6 @@ static void P_SpawnFriction(void)
 	fixed_t strength; // frontside texture offset controls magnitude
 	fixed_t friction; // friction value to be applied during movement
 	INT32 movefactor; // applied to each player move to simulate inertia
-	TAG_ITER_DECLARECOUNTER(0);
 
 	for (i = 0; i < numlines; i++, l++)
 		if (l->special == 540)
@@ -8363,7 +8335,7 @@ static void P_SpawnFriction(void)
 			else
 				movefactor = FRACUNIT;
 
-			TAG_ITER_SECTORS(0, tag, s)
+			TAG_ITER_SECTORS(tag, s)
 				Add_Friction(friction, movefactor, s, -1);
 		}
 }
@@ -8452,6 +8424,9 @@ static inline boolean PIT_PushThing(mobj_t *thing)
 	if (thing->player && thing->player->powers[pw_carry] == CR_ROPEHANG)
 		return false;
 
+	if (!tmpusher->source)
+		return false;
+
 	// Allow this to affect pushable objects at some point?
 	if (thing->player && (!(thing->flags & (MF_NOGRAVITY | MF_NOCLIP)) || thing->player->powers[pw_carry] == CR_NIGHTSMODE))
 	{
@@ -8882,7 +8857,6 @@ static void P_SpawnPushers(void)
 	mtag_t tag;
 	register INT32 s;
 	mobj_t *thing;
-	TAG_ITER_DECLARECOUNTER(0);
 
 	for (i = 0; i < numlines; i++, l++)
 	{
@@ -8890,15 +8864,15 @@ static void P_SpawnPushers(void)
 		switch (l->special)
 		{
 			case 541: // wind
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					Add_Pusher(p_wind, l->dx, l->dy, NULL, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
 				break;
 			case 544: // current
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					Add_Pusher(p_current, l->dx, l->dy, NULL, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
 				break;
 			case 547: // push/pull
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 				{
 					thing = P_GetPushThing(s);
 					if (thing) // No MT_P* means no effect
@@ -8906,19 +8880,19 @@ static void P_SpawnPushers(void)
 				}
 				break;
 			case 545: // current up
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					Add_Pusher(p_upcurrent, l->dx, l->dy, NULL, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
 				break;
 			case 546: // current down
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					Add_Pusher(p_downcurrent, l->dx, l->dy, NULL, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
 				break;
 			case 542: // wind up
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					Add_Pusher(p_upwind, l->dx, l->dy, NULL, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
 				break;
 			case 543: // wind down
-				TAG_ITER_SECTORS(0, tag, s)
+				TAG_ITER_SECTORS(tag, s)
 					Add_Pusher(p_downwind, l->dx, l->dy, NULL, s, -1, l->flags & ML_NOCLIMB, l->flags & ML_EFFECT4);
 				break;
 		}
diff --git a/src/p_spec.h b/src/p_spec.h
index bba7c4a40a090084cce1f9738dc414c5a0c1c013..3b8abfcf8a099ee3bcb4f442d016f83c315b065f 100644
--- a/src/p_spec.h
+++ b/src/p_spec.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/p_telept.c b/src/p_telept.c
index f6feddf4b513a2cb356baca7d60c160c465d9605..6bac5ad208e54f795288925342adb1becb05e021 100644
--- a/src/p_telept.c
+++ b/src/p_telept.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/p_tick.c b/src/p_tick.c
index da2a980c480a54f22d2dc2c3ef2904e96c07a1e8..55a16fd81cb8b582fd8ab53c2d64c4f18809b8e2 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -22,7 +22,7 @@
 #include "lua_script.h"
 #include "lua_hook.h"
 #include "m_perfstats.h"
-#include "i_system.h" // I_GetTimeMicros
+#include "i_system.h" // I_GetPreciseTime
 
 // Object place
 #include "m_cheat.h"
@@ -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_GetTimeMicros();
+		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_GetTimeMicros() - 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;
 
-		LUAh_PreThinkFrame();
+		LUA_HOOK(PreThinkFrame);
 
-		ps_playerthink_time = I_GetTimeMicros();
+		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_GetTimeMicros() - 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_GetTimeMicros();
+		PS_START_TIMING(ps_thinkertime);
 		P_RunThinkers();
-		ps_thinkertime = I_GetTimeMicros() - 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_GetTimeMicros();
-		LUAh_ThinkFrame();
-		ps_lua_thinkframe_time = I_GetTimeMicros() - ps_lua_thinkframe_time;
+		PS_START_TIMING(ps_lua_thinkframe_time);
+		LUA_HookThinkFrame();
+		PS_STOP_TIMING(ps_lua_thinkframe_time);
 	}
 
 	// Run shield positioning
@@ -760,7 +760,7 @@ void P_Ticker(boolean run)
 		if (modeattacking)
 			G_GhostTicker();
 
-		LUAh_PostThinkFrame();
+		LUA_HOOK(PostThinkFrame);
 	}
 
 	P_MapEnd();
@@ -783,7 +783,7 @@ void P_PreTicker(INT32 frames)
 	{
 		P_MapStart();
 
-		LUAh_PreThinkFrame();
+		LUA_HOOK(PreThinkFrame);
 
 		for (i = 0; i < MAXPLAYERS; i++)
 			if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
@@ -810,7 +810,7 @@ void P_PreTicker(INT32 frames)
 			if (playeringame[i] && players[i].mo && !P_MobjWasRemoved(players[i].mo))
 				P_PlayerAfterThink(&players[i]);
 
-		LUAh_ThinkFrame();
+		LUA_HookThinkFrame();
 
 		// Run shield positioning
 		P_RunShields();
@@ -819,7 +819,7 @@ void P_PreTicker(INT32 frames)
 		P_UpdateSpecials();
 		P_RespawnSpecials();
 
-		LUAh_PostThinkFrame();
+		LUA_HOOK(PostThinkFrame);
 
 		P_MapEnd();
 	}
diff --git a/src/p_tick.h b/src/p_tick.h
index 1fb88f3f20a33f3319fd29af2634c2dd46a88417..ae481c6a2aa10df64f680f33aca97b43b5b13bc6 100644
--- a/src/p_tick.h
+++ b/src/p_tick.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/p_user.c b/src/p_user.c
index c5f919c78ec4f6ee73f8535f730ef7883e549cc3..f21118a81fb2f4f088758a9b92ac52444d289e77 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -190,7 +190,7 @@ fixed_t P_ReturnThrustY(mobj_t *mo, angle_t angle, fixed_t move)
 boolean P_AutoPause(void)
 {
 	// Don't pause even on menu-up or focus-lost in netgames or record attack
-	if (netgame || modeattacking || gamestate == GS_TITLESCREEN)
+	if (netgame || modeattacking || gamestate == GS_TITLESCREEN || (marathonmode && gamestate == GS_INTERMISSION))
 		return false;
 
 	return (menuactive || ( window_notinfocus && cv_pauseifunfocused.value ));
@@ -777,7 +777,7 @@ void P_NightserizePlayer(player_t *player, INT32 nighttime)
 	UINT8 oldmare, oldmarelap, oldmarebonuslap;
 
 	// Bots can't be NiGHTSerized, silly!1 :P
-	if (player->bot)
+	if (player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN)
 		return;
 
 	if (player->powers[pw_carry] != CR_NIGHTSMODE)
@@ -969,6 +969,9 @@ pflags_t P_GetJumpFlags(player_t *player)
 //
 boolean P_PlayerInPain(player_t *player)
 {
+	// If the player doesn't have a mobj, it can't be in pain.
+	if (!player->mo)
+		return false;
 	// no silly, sliding isn't pain
 	if (!(player->pflags & PF_SLIDING) && player->mo->state == &states[player->mo->info->painstate] && player->powers[pw_flashing])
 		return true;
@@ -1111,7 +1114,7 @@ boolean P_PlayerCanDamage(player_t *player, mobj_t *thing)
 		return false;
 
 	{
-		UINT8 shouldCollide = LUAh_PlayerCanDamage(player, thing);
+		UINT8 shouldCollide = LUA_HookPlayerCanDamage(player, thing);
 		if (P_MobjWasRemoved(thing))
 			return false; // removed???
 		if (shouldCollide == 1)
@@ -1188,9 +1191,9 @@ void P_GivePlayerRings(player_t *player, INT32 num_rings)
 {
 	if (!player)
 		return;
-
-	if (player->bot)
-		player = &players[consoleplayer];
+	
+	if ((player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN) && player->botleader)
+		player = player->botleader;
 
 	if (!player->mo)
 		return;
@@ -1234,8 +1237,8 @@ void P_GivePlayerSpheres(player_t *player, INT32 num_spheres)
 	if (!player)
 		return;
 
-	if (player->bot)
-		player = &players[consoleplayer];
+	if ((player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN) && player->botleader)
+		player = player->botleader;
 
 	if (!player->mo)
 		return;
@@ -1261,8 +1264,8 @@ void P_GivePlayerLives(player_t *player, INT32 numlives)
 	if (!player)
 		return;
 
-	if (player->bot)
-		player = &players[consoleplayer];
+	if ((player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN) && player->botleader)
+		player = player->botleader;
 
 	if (gamestate == GS_LEVEL)
 	{
@@ -1341,7 +1344,7 @@ void P_DoSuperTransformation(player_t *player, boolean giverings)
 	// Transformation animation
 	P_SetPlayerMobjState(player->mo, S_PLAY_SUPER_TRANS1);
 
-	if (giverings)
+	if (giverings && player->rings < 50)
 		player->rings = 50;
 
 	// Just in case.
@@ -1367,8 +1370,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)
@@ -1491,10 +1494,10 @@ void P_PlayLivesJingle(player_t *player)
 	if (player && !P_IsLocalPlayer(player))
 		return;
 
-	if (use1upSound || cv_1upsound.value)
-		S_StartSound(NULL, sfx_oneup);
-	else if (mariomode)
+	if (mariomode)
 		S_StartSound(NULL, sfx_marioa);
+	else if (use1upSound || cv_1upsound.value)
+		S_StartSound(NULL, sfx_oneup);
 	else
 	{
 		P_PlayJingle(player, JT_1UP);
@@ -1594,7 +1597,7 @@ boolean P_EvaluateMusicStatus(UINT16 status, const char *musname)
 				break;
 
 			case JT_OTHER:  // Other state
-				result = LUAh_ShouldJingleContinue(&players[i], musname);
+				result = LUA_HookShouldJingleContinue(&players[i], musname);
 				break;
 
 			case JT_NONE:   // Null state
@@ -1860,7 +1863,7 @@ void P_SpawnShieldOrb(player_t *player)
 		I_Error("P_SpawnShieldOrb: player->mo is NULL!\n");
 #endif
 
-	if (LUAh_ShieldSpawn(player))
+	if (LUA_HookPlayer(player, HOOK(ShieldSpawn)))
 		return;
 
 	if (player->powers[pw_shield] & SH_FORCE)
@@ -2016,6 +2019,8 @@ mobj_t *P_SpawnGhostMobj(mobj_t *mobj)
 {
 	mobj_t *ghost = P_SpawnMobj(mobj->x, mobj->y, mobj->z, MT_GHOST);
 
+	P_SetTarget(&ghost->target, mobj);
+
 	P_SetScale(ghost, mobj->scale);
 	ghost->destscale = mobj->scale;
 
@@ -2329,7 +2334,8 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 			P_MobjCheckWater(player->mo);
 			if (player->pflags & PF_SPINNING)
 			{
-				if (player->mo->state-states != S_PLAY_ROLL && !(player->pflags & PF_STARTDASH))
+				if (!(player->pflags & PF_STARTDASH) && player->panim != PA_ROLL && player->panim != PA_ETC
+				&& player->panim != PA_ABILITY && player->panim != PA_ABILITY2)
 				{
 					P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
 					S_StartSound(player->mo, sfx_spin);
@@ -2612,10 +2618,10 @@ static void P_CheckBustableBlocks(player_t *player)
 
 	if ((netgame || multiplayer) && player->spectator)
 		return;
-	
+
 	oldx = player->mo->x;
 	oldy = player->mo->y;
-	
+
 	if (!(player->pflags & PF_BOUNCING)) // Bouncers only get to break downwards, not sideways
 	{
 		P_UnsetThingPosition(player->mo);
@@ -2634,7 +2640,7 @@ static void P_CheckBustableBlocks(player_t *player)
 
 		if (!node->m_sector->ffloors)
 			continue;
-		
+
 		for (rover = node->m_sector->ffloors; rover; rover = rover->next)
 		{
 			if (!P_PlayerCanBust(player, rover))
@@ -2771,16 +2777,9 @@ static void P_CheckBouncySectors(player_t *player)
 				player->mo->momx = -FixedMul(player->mo->momx,bouncestrength);
 				player->mo->momy = -FixedMul(player->mo->momy,bouncestrength);
 
-				if (player->pflags & PF_SPINNING)
-				{
-					player->pflags &= ~PF_SPINNING;
-					player->pflags |= P_GetJumpFlags(player);
-					player->pflags |= PF_THOKKED;
-				}
 			}
 			else
 			{
-				fixed_t newmom;
 				pslope_t *slope = (abs(oldz - topheight) < abs(oldz + player->mo->height - bottomheight)) ? *rover->t_slope : *rover->b_slope;
 
 				momentum.x = player->mo->momx;
@@ -2790,53 +2789,28 @@ static void P_CheckBouncySectors(player_t *player)
 				if (slope)
 					P_ReverseQuantizeMomentumToSlope(&momentum, slope);
 
-				newmom = momentum.z = -FixedMul(momentum.z,bouncestrength)/2;
+				momentum.z = -FixedMul(momentum.z,bouncestrength)/2;
 
-				if (abs(newmom) < (bouncestrength*2))
+				if (abs(momentum.z) < (bouncestrength*2))
 					goto bouncydone;
 
-				if (!(rover->master->flags & ML_BOUNCY))
-				{
-					if (newmom > 0)
-					{
-						if (newmom < 8*FRACUNIT)
-							newmom = 8*FRACUNIT;
-					}
-					else if (newmom < 0)
-					{
-						if (newmom > -8*FRACUNIT)
-							newmom = -8*FRACUNIT;
-					}
-				}
-
-				if (newmom > P_GetPlayerHeight(player)/2)
-					newmom = P_GetPlayerHeight(player)/2;
-				else if (newmom < -P_GetPlayerHeight(player)/2)
-					newmom = -P_GetPlayerHeight(player)/2;
-
-				momentum.z = newmom*2;
+				if (momentum.z > FixedMul(24*FRACUNIT, player->mo->scale)) //half of the default player height
+					momentum.z = FixedMul(24*FRACUNIT, player->mo->scale);
+				else if (momentum.z < -FixedMul(24*FRACUNIT, player->mo->scale))
+					momentum.z = -FixedMul(24*FRACUNIT, player->mo->scale);
 
 				if (slope)
 					P_QuantizeMomentumToSlope(&momentum, slope);
 
 				player->mo->momx = momentum.x;
 				player->mo->momy = momentum.y;
-				player->mo->momz = momentum.z/2;
+				player->mo->momz = momentum.z;
 
 				if (player->pflags & PF_SPINNING)
 				{
-					player->pflags &= ~PF_SPINNING;
-					player->pflags |= P_GetJumpFlags(player);
 					player->pflags |= PF_THOKKED;
 				}
 			}
-
-			if ((player->pflags & PF_SPINNING) && player->speed < FixedMul(1<<FRACBITS, player->mo->scale) && player->mo->momz)
-			{
-				player->pflags &= ~PF_SPINNING;
-				player->pflags |= P_GetJumpFlags(player);
-			}
-
 			goto bouncydone;
 		}
 	}
@@ -4498,7 +4472,7 @@ void P_DoJump(player_t *player, boolean soundandstate)
 	if (twodlevel || (player->mo->flags2 & MF2_TWOD))
 		factor += player->jumpfactor / 10;
 
-	if (player->charflags & SF_MULTIABILITY && player->charability == CA_DOUBLEJUMP)
+	if (player->charflags & SF_MULTIABILITY && player->charability == CA_DOUBLEJUMP && (player->actionspd >> FRACBITS) != -1)
 		factor -= max(0, player->secondjump * player->jumpfactor / ((player->actionspd >> FRACBITS) + 1)); // Reduce the jump height each time
 
 	//if (maptol & TOL_NIGHTS)
@@ -4525,6 +4499,9 @@ void P_DoJump(player_t *player, boolean soundandstate)
 
 	player->pflags |= P_GetJumpFlags(player);;
 
+	if (player->charflags & SF_NOJUMPDAMAGE)
+		player->pflags &= ~PF_SPINNING;
+
 	if (soundandstate)
 	{
 		if (!player->spectator)
@@ -4577,7 +4554,7 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 
 	if (cmd->buttons & BT_SPIN)
 	{
-		if (LUAh_SpinSpecial(player))
+		if (LUA_HookPlayer(player, HOOK(SpinSpecial)))
 			return;
 	}
 
@@ -4876,22 +4853,28 @@ void P_DoBubbleBounce(player_t *player)
 //
 void P_DoAbilityBounce(player_t *player, boolean changemomz)
 {
-	fixed_t prevmomz;
 	if (player->mo->state-states == S_PLAY_BOUNCE_LANDING)
 		return;
+
 	if (changemomz)
 	{
-		fixed_t minmomz;
-		prevmomz = player->mo->momz;
+		fixed_t prevmomz = player->mo->momz, minmomz;
+
 		if (P_MobjFlip(player->mo)*prevmomz < 0)
 			prevmomz = 0;
 		else if (player->mo->eflags & MFE_UNDERWATER)
 			prevmomz /= 2;
+
 		P_DoJump(player, false);
 		player->pflags &= ~(PF_STARTJUMP|PF_JUMPED);
 		minmomz = FixedMul(player->mo->momz, 3*FRACUNIT/2);
-		player->mo->momz = max(minmomz, (minmomz + prevmomz)/2);
+
+		if (player->mo->eflags & MFE_VERTICALFLIP) // Use "min" or "max" depending on if the player is flipped
+			player->mo->momz = min(minmomz, (minmomz + prevmomz)/2);
+		else
+			player->mo->momz = max(minmomz, (minmomz + prevmomz)/2);
 	}
+
 	S_StartSound(player->mo, sfx_boingf);
 	P_SetPlayerMobjState(player->mo, S_PLAY_BOUNCE_LANDING);
 	player->pflags |= PF_BOUNCING|PF_THOKKED;
@@ -5020,7 +5003,7 @@ static boolean P_PlayerShieldThink(player_t *player, ticcmd_t *cmd, mobj_t *lock
 	if ((player->powers[pw_shield] & SH_NOSTACK) && !player->powers[pw_super] && !(player->pflags & PF_SPINDOWN)
 		&& ((!(player->pflags & PF_THOKKED) || (((player->powers[pw_shield] & SH_NOSTACK) == SH_BUBBLEWRAP || (player->powers[pw_shield] & SH_NOSTACK) == SH_ATTRACT) && player->secondjump == UINT8_MAX) ))) // thokked is optional if you're bubblewrapped / 3dblasted
 	{
-		if ((player->powers[pw_shield] & SH_NOSTACK) == SH_ATTRACT)
+		if ((player->powers[pw_shield] & SH_NOSTACK) == SH_ATTRACT && !(player->charflags & SF_NOSHIELDABILITY))
 		{
 			if ((lockonshield = P_LookForEnemies(player, false, false)))
 			{
@@ -5043,7 +5026,7 @@ static boolean P_PlayerShieldThink(player_t *player, ticcmd_t *cmd, mobj_t *lock
 				}
 			}
 		}
-		if (cmd->buttons & BT_SPIN && !LUAh_ShieldSpecial(player)) // Spin button effects
+		if ((!(player->charflags & SF_NOSHIELDABILITY)) && (cmd->buttons & BT_SPIN && !LUA_HookPlayer(player, HOOK(ShieldSpecial)))) // Spin button effects
 		{
 			// Force stop
 			if ((player->powers[pw_shield] & ~(SH_FORCEHP|SH_STACK)) == SH_FORCE)
@@ -5167,7 +5150,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 				// and you don't have a shield, do it!
 				P_DoSuperTransformation(player, false);
 			}
-			else if (!LUAh_JumpSpinSpecial(player))
+			else if (!LUA_HookPlayer(player, HOOK(JumpSpinSpecial)))
 				switch (player->charability)
 				{
 					case CA_THOK:
@@ -5240,7 +5223,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 
 	if (cmd->buttons & BT_JUMP && !player->exiting && !P_PlayerInPain(player))
 	{
-		if (LUAh_JumpSpecial(player))
+		if (LUA_HookPlayer(player, HOOK(JumpSpecial)))
 			;
 		// all situations below this require jump button not to be pressed already
 		else if (player->pflags & PF_JUMPDOWN)
@@ -5275,7 +5258,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 		}*/
 		else if (player->pflags & PF_JUMPED)
 		{
-			if (!LUAh_AbilitySpecial(player))
+			if (!LUA_HookPlayer(player, HOOK(AbilitySpecial)))
 			switch (player->charability)
 			{
 				case CA_THOK:
@@ -5289,7 +5272,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 						fixed_t actionspd = player->actionspd;
 
 						if (player->charflags & SF_DASHMODE)
-							actionspd = max(player->normalspeed, FixedDiv(player->speed, player->mo->scale));
+							actionspd = max(player->actionspd, FixedDiv(player->speed, player->mo->scale));
 
 						if (player->mo->eflags & MFE_UNDERWATER)
 							actionspd >>= 1;
@@ -5345,9 +5328,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);
 						}*/
 					}
@@ -5366,7 +5349,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);
@@ -5468,7 +5451,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 		}
 		else if (player->pflags & PF_THOKKED)
 		{
-			if (!LUAh_AbilitySpecial(player))
+			if (!LUA_HookPlayer(player, HOOK(AbilitySpecial)))
 				switch (player->charability)
 				{
 					case CA_FLY:
@@ -5491,7 +5474,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 						break;
 				}
 		}
-		else if ((player->powers[pw_shield] & SH_NOSTACK) == SH_WHIRLWIND && !player->powers[pw_super])
+		else if ((!(player->charflags & SF_NOSHIELDABILITY)) && ((player->powers[pw_shield] & SH_NOSTACK) == SH_WHIRLWIND && !player->powers[pw_super] && !LUA_HookPlayer(player, HOOK(ShieldSpecial))))
 			P_DoJumpShield(player);
 	}
 
@@ -5612,16 +5595,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;
 
@@ -5637,17 +5614,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);
 	}
 
@@ -5953,22 +5928,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])
@@ -7752,6 +7711,11 @@ void P_ElementalFire(player_t *player, boolean cropcircle)
 			flame->eflags = (flame->eflags & ~MFE_VERTICALFLIP)|(player->mo->eflags & MFE_VERTICALFLIP);
 			P_InstaThrust(flame, flame->angle, FixedMul(3*FRACUNIT, flame->scale));
 			P_SetObjectMomZ(flame, 3*FRACUNIT, false);
+			if (!(gametyperules & GTR_FRIENDLY))
+			{
+				P_SetMobjState(flame, S_TEAM_SPINFIRE1);
+				flame->color = player->mo->color;
+			}
 		}
 #undef limitangle
 #undef numangles
@@ -7779,6 +7743,11 @@ void P_ElementalFire(player_t *player, boolean cropcircle)
 			flame->destscale = player->mo->scale;
 			P_SetScale(flame, player->mo->scale);
 			flame->eflags = (flame->eflags & ~MFE_VERTICALFLIP)|(player->mo->eflags & MFE_VERTICALFLIP);
+			if (!(gametyperules & GTR_FRIENDLY))
+			{
+				P_SetMobjState(flame, S_TEAM_SPINFIRE1);
+				flame->color = player->mo->color;
+			}
 
 			flame->momx = 8; // this is a hack which is used to ensure it still behaves as a missile and can damage others
 			P_XYMovement(flame);
@@ -8605,12 +8574,6 @@ void P_MovePlayer(player_t *player)
 		player->climbing--;
 	}
 
-	if (!player->climbing)
-	{
-		player->lastsidehit = -1;
-		player->lastlinehit = -1;
-	}
-
 	// Make sure you're not teetering when you shouldn't be.
 	if (player->panim == PA_EDGE
 	&& (player->mo->momx || player->mo->momy || player->mo->momz))
@@ -8635,41 +8598,47 @@ void P_MovePlayer(player_t *player)
 		P_DoFiring(player, cmd);
 
 	{
+		boolean atspinheight = false;
 		fixed_t oldheight = player->mo->height;
+		fixed_t luaheight = LUA_HookPlayerHeight(player);
 
+		if (luaheight != -1)
+		{
+			player->mo->height = luaheight;
+			if (luaheight <= P_GetPlayerSpinHeight(player))
+				atspinheight = true; // spinning will not save you from being crushed
+		}
 		// Less height while spinning. Good for spinning under things...?
-		if ((player->mo->state == &states[player->mo->info->painstate])
-		|| ((player->pflags & PF_JUMPED) && !(player->pflags & PF_NOJUMPDAMAGE))
-		|| (player->pflags & PF_SPINNING)
-		|| player->powers[pw_tailsfly] || player->pflags & PF_GLIDING
-		|| (player->charability == CA_GLIDEANDCLIMB && player->mo->state-states == S_PLAY_GLIDE_LANDING)
-		|| (player->charability == CA_FLY && player->mo->state-states == S_PLAY_FLY_TIRED))
+		else if (P_PlayerShouldUseSpinHeight(player))
+		{
 			player->mo->height = P_GetPlayerSpinHeight(player);
+			atspinheight = true;
+		}
 		else
 			player->mo->height = P_GetPlayerHeight(player);
 
 		if (player->mo->eflags & MFE_VERTICALFLIP && player->mo->height != oldheight) // adjust z height for reverse gravity, similar to how it's done for scaling
 			player->mo->z -= player->mo->height - oldheight;
-	}
 
-	// Crush test...
-	if ((player->mo->ceilingz - player->mo->floorz < player->mo->height)
-		&& !(player->mo->flags & MF_NOCLIP))
-	{
-		if ((player->charability2 == CA2_SPINDASH) && !(player->pflags & PF_SPINNING))
+		// Crush test...
+		if ((player->mo->ceilingz - player->mo->floorz < player->mo->height)
+			&& !(player->mo->flags & MF_NOCLIP))
 		{
-			player->pflags |= PF_SPINNING;
-			P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
-		}
-		else if (player->mo->ceilingz - player->mo->floorz < player->mo->height)
-		{
-			if ((netgame || multiplayer) && player->spectator)
-				P_DamageMobj(player->mo, NULL, NULL, 1, DMG_SPECTATOR); // Respawn crushed spectators
-			else
-				P_DamageMobj(player->mo, NULL, NULL, 1, DMG_CRUSHED);
+			if (!atspinheight)
+			{
+				player->pflags |= PF_SPINNING;
+				P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+			}
+			else if (player->mo->ceilingz - player->mo->floorz < player->mo->height)
+			{
+				if ((netgame || multiplayer) && player->spectator)
+					P_DamageMobj(player->mo, NULL, NULL, 1, DMG_SPECTATOR); // Respawn crushed spectators
+				else
+					P_DamageMobj(player->mo, NULL, NULL, 1, DMG_CRUSHED);
 
-			if (player->playerstate == PST_DEAD)
-				return;
+				if (player->playerstate == PST_DEAD)
+					return;
+			}
 		}
 	}
 
@@ -8997,8 +8966,11 @@ void P_NukeEnemies(mobj_t *inflictor, mobj_t *source, fixed_t radius)
 		if (mo->type == MT_MINUS && !(mo->flags & (MF_SPECIAL|MF_SHOOTABLE)))
 			mo->flags = (mo->flags & ~MF_NOCLIPTHING)|MF_SPECIAL|MF_SHOOTABLE;
 
-		if (mo->type == MT_EGGGUARD && mo->tracer) //nuke Egg Guard's shield!
+		if (mo->type == MT_EGGGUARD && mo->tracer) // Egg Guard's shield needs to be removed if it has one!
+		{
 			P_KillMobj(mo->tracer, inflictor, source, DMG_NUKE);
+			P_KillMobj(mo, inflictor, source, DMG_NUKE);
+		}
 
 		if (mo->flags & MF_BOSS || mo->type == MT_PLAYER) //don't OHKO bosses nor players!
 			P_DamageMobj(mo, inflictor, source, 1, DMG_NUKE);
@@ -9485,11 +9457,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();
@@ -10490,7 +10462,7 @@ boolean P_SpectatorJoinGame(player_t *player)
 		else
 			changeto = (P_RandomFixed() & 1) + 1;
 
-		if (!LUAh_TeamSwitch(player, changeto, true, false, false))
+		if (!LUA_HookTeamSwitch(player, changeto, true, false, false))
 			return false;
 
 		if (player->mo)
@@ -10507,7 +10479,7 @@ boolean P_SpectatorJoinGame(player_t *player)
 		{
 			// Call ViewpointSwitch hooks here.
 			// The viewpoint was forcibly changed.
-			LUAh_ViewpointSwitch(player, &players[consoleplayer], true);
+			LUA_HookViewpointSwitch(player, &players[consoleplayer], true);
 			displayplayer = consoleplayer;
 		}
 
@@ -10525,7 +10497,7 @@ boolean P_SpectatorJoinGame(player_t *player)
 		// respawn in place and sit there for the rest of the round.
 		if (!((gametyperules & GTR_HIDEFROZEN) && leveltime > (hidetime * TICRATE)))
 		{
-			if (!LUAh_TeamSwitch(player, 3, true, false, false))
+			if (!LUA_HookTeamSwitch(player, 3, true, false, false))
 				return false;
 			if (player->mo)
 			{
@@ -10552,7 +10524,7 @@ boolean P_SpectatorJoinGame(player_t *player)
 			{
 				// Call ViewpointSwitch hooks here.
 				// The viewpoint was forcibly changed.
-				LUAh_ViewpointSwitch(player, &players[consoleplayer], true);
+				LUA_HookViewpointSwitch(player, &players[consoleplayer], true);
 				displayplayer = consoleplayer;
 			}
 
@@ -11343,8 +11315,9 @@ static void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 	mobj_t *mo = player->mo;
 	angle_t angle = player->drawangle;
 	fixed_t dist;
+	fixed_t heightoffset = ((mo->eflags & MFE_VERTICALFLIP) ? mo->height - (P_GetPlayerHeight(player) >> 1) : (P_GetPlayerHeight(player) >> 1));
 	panim_t panim = player->panim;
-	tic_t dashmode = player->dashmode;
+	tic_t dashmode = min(player->dashmode, DASHMODE_MAX);
 	boolean underwater = mo->eflags & MFE_UNDERWATER;
 	statenum_t stat = fume->state-states;
 
@@ -11376,7 +11349,7 @@ static void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 				offsetV = i*P_ReturnThrustY(fume, fume->movedir, radiusV);
 				x = mo->x + radiusX + FixedMul(offsetH, factorX);
 				y = mo->y + radiusY + FixedMul(offsetH, factorY);
-				z = mo->z + (mo->height >> 1) + offsetV;
+				z = mo->z + heightoffset + offsetV;
 				P_SpawnMobj(x, y, z, MT_SMALLBUBBLE)->scale = mo->scale >> 1;
 			}
 
@@ -11439,7 +11412,7 @@ static void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 	P_UnsetThingPosition(fume);
 	fume->x = mo->x + P_ReturnThrustX(fume, angle, dist);
 	fume->y = mo->y + P_ReturnThrustY(fume, angle, dist);
-	fume->z = mo->z + ((mo->height - fume->height) >> 1);
+	fume->z = mo->z + heightoffset - (fume->height >> 1);
 	P_SetThingPosition(fume);
 
 	// If dashmode is high enough, spawn a trail
@@ -11461,6 +11434,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!
 	{
@@ -11468,7 +11444,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)
 		{
@@ -11477,12 +11453,11 @@ void P_PlayerThink(player_t *player)
 		}
 		if (player->playerstate == PST_REBORN)
 		{
-			LUAh_PlayerThink(player);
+			LUA_HookPlayer(player, HOOK(PlayerThink));
 			return;
 		}
 	}
 
-#ifdef SEENAMES
 	if (netgame && player == &players[displayplayer] && !(leveltime % (TICRATE/5)))
 	{
 		seenplayer = NULL;
@@ -11507,7 +11482,6 @@ void P_PlayerThink(player_t *player)
 			}
 		}
 	}
-#endif
 
 	if (player->awayviewmobj && P_MobjWasRemoved(player->awayviewmobj))
 	{
@@ -11581,7 +11555,7 @@ void P_PlayerThink(player_t *player)
 
 			if (player->playerstate == PST_DEAD)
 			{
-				LUAh_PlayerThink(player);
+				LUA_HookPlayer(player, HOOK(PlayerThink));
 				return;
 			}
 		}
@@ -11614,7 +11588,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 +11619,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;
@@ -11702,7 +11676,7 @@ void P_PlayerThink(player_t *player)
 	{
 		player->mo->flags2 &= ~MF2_SHADOW;
 		P_DeathThink(player);
-		LUAh_PlayerThink(player);
+		LUA_HookPlayer(player, HOOK(PlayerThink));
 		return;
 	}
 
@@ -11744,7 +11718,7 @@ void P_PlayerThink(player_t *player)
 	{
 		if (P_SpectatorJoinGame(player))
 		{
-			LUAh_PlayerThink(player);
+			LUA_HookPlayer(player, HOOK(PlayerThink));
 			return; // player->mo was removed.
 		}
 	}
@@ -11849,7 +11823,7 @@ void P_PlayerThink(player_t *player)
 
 	if (!player->mo)
 	{
-		LUAh_PlayerThink(player);
+		LUA_HookPlayer(player, HOOK(PlayerThink));
 		return; // P_MovePlayer removed player->mo.
 	}
 
@@ -12303,7 +12277,7 @@ void P_PlayerThink(player_t *player)
 	}
 #undef dashmode
 
-	LUAh_PlayerThink(player);
+	LUA_HookPlayer(player, HOOK(PlayerThink));
 
 /*
 //	Colormap verification
@@ -12578,13 +12552,16 @@ void P_PlayerAfterThink(player_t *player)
 					player->powers[pw_carry] = CR_NONE;
 				else
 				{
-					P_TryMove(player->mo, tails->x + P_ReturnThrustX(tails, tails->player->drawangle, 4*FRACUNIT), tails->y + P_ReturnThrustY(tails, tails->player->drawangle, 4*FRACUNIT), true);
+					if (tails->player)
+						P_TryMove(player->mo, tails->x + P_ReturnThrustX(tails, tails->player->drawangle, 4*FRACUNIT), tails->y + P_ReturnThrustY(tails, tails->player->drawangle, 4*FRACUNIT), true);
+					else
+						P_TryMove(player->mo, tails->x + P_ReturnThrustX(tails, tails->angle, 4*FRACUNIT), tails->y + P_ReturnThrustY(tails, tails->angle, 4*FRACUNIT), true);
 					player->mo->momx = tails->momx;
 					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;
 
@@ -12595,14 +12572,14 @@ void P_PlayerAfterThink(player_t *player)
 				if (P_AproxDistance(player->mo->x - tails->x, player->mo->y - tails->y) > player->mo->radius)
 					player->powers[pw_carry] = CR_NONE;
 
-				if (player->powers[pw_carry] != CR_NONE)
+				if (player->powers[pw_carry] == CR_PLAYER)
 				{
 					if (player->mo->state-states != S_PLAY_RIDE)
 						P_SetPlayerMobjState(player->mo, S_PLAY_RIDE);
-					if ((tails->skin && ((skin_t *)(tails->skin))->sprites[SPR2_SWIM].numframes) && (tails->eflags & MFE_UNDERWATER))
+					if (tails->player && (tails->skin && ((skin_t *)(tails->skin))->sprites[SPR2_SWIM].numframes) && (tails->eflags & MFE_UNDERWATER))
 						tails->player->powers[pw_tailsfly] = 0;
 				}
-				else
+				else if (player->powers[pw_carry] == CR_NONE)
 					P_SetTarget(&player->mo->tracer, NULL);
 
 				if (player-players == consoleplayer && botingame)
@@ -12705,9 +12682,15 @@ void P_PlayerAfterThink(player_t *player)
 
 				if (player->cmd.forwardmove || player->cmd.sidemove)
 				{
-					rock->movedir = (player->cmd.angleturn << FRACBITS) + R_PointToAngle2(0, 0, player->cmd.forwardmove << FRACBITS, -player->cmd.sidemove << FRACBITS);
+					rock->flags2 |= MF2_STRONGBOX; // signifies the rock should not slow to a halt
+					if (twodlevel || (mo->flags2 & MF2_TWOD))
+						rock->movedir = mo->angle;
+					else
+						rock->movedir = (player->cmd.angleturn << FRACBITS) + R_PointToAngle2(0, 0, player->cmd.forwardmove << FRACBITS, -player->cmd.sidemove << FRACBITS);
 					P_Thrust(rock, rock->movedir, rock->scale >> 1);
 				}
+				else
+					rock->flags2 &= ~MF2_STRONGBOX;
 
 				mo->momx = rock->momx;
 				mo->momy = rock->momy;
@@ -12723,7 +12706,7 @@ void P_PlayerAfterThink(player_t *player)
 					mo->tics = walktics;
 				}
 
-				P_TeleportMove(player->mo, rock->x, rock->y, rock->z + rock->height);
+				P_TeleportMove(player->mo, rock->x, rock->y, rock->z + ((mo->eflags & MFE_VERTICALFLIP) ? -mo->height : rock->height));
 				break;
 			}
 			case CR_PTERABYTE: // being carried by a Pterabyte
@@ -12863,7 +12846,7 @@ void P_PlayerAfterThink(player_t *player)
 
 		if (player->followmobj)
 		{
-			if (LUAh_FollowMobj(player, player->followmobj) || P_MobjWasRemoved(player->followmobj))
+			if (LUA_HookFollowMobj(player, player->followmobj) || P_MobjWasRemoved(player->followmobj))
 				{;}
 			else
 			{
@@ -12934,3 +12917,37 @@ boolean P_PlayerFullbright(player_t *player)
 			|| !(player->mo->state >= &states[S_PLAY_NIGHTS_TRANS1]
 			&& player->mo->state < &states[S_PLAY_NIGHTS_TRANS6])))); // Note the < instead of <=
 }
+
+#define JUMPCURLED(player) ((player->pflags & PF_JUMPED)\
+	&& (!(player->charflags & SF_NOJUMPSPIN))\
+	&& (player->panim == PA_JUMP || player->panim == PA_ROLL))\
+
+// returns true if the player can enter a sector that they could not if standing at their skin's full height
+boolean P_PlayerCanEnterSpinGaps(player_t *player)
+{
+	UINT8 canEnter = LUA_HookPlayerCanEnterSpinGaps(player);
+	if (canEnter == 1)
+		return true;
+	else if (canEnter == 2)
+		return false;
+
+	return ((player->pflags & (PF_SPINNING|PF_SLIDING|PF_GLIDING)) // players who are spinning, sliding, or gliding
+		|| (player->charability == CA_GLIDEANDCLIMB && player->mo->state-states == S_PLAY_GLIDE_LANDING) // players who are landing from a glide
+		|| ((player->charflags & (SF_DASHMODE|SF_MACHINE)) == (SF_DASHMODE|SF_MACHINE)
+			&& player->dashmode >= DASHMODE_THRESHOLD && player->mo->state-states == S_PLAY_DASH) // machine players in dashmode
+		|| JUMPCURLED(player)); // players who are jumpcurled, but only if they would normally jump that way
+}
+
+// returns true if the player should use their skin's spinheight instead of their skin's height
+boolean P_PlayerShouldUseSpinHeight(player_t *player)
+{
+	return ((player->pflags & (PF_SPINNING|PF_SLIDING|PF_GLIDING))
+		|| (player->mo->state == &states[player->mo->info->painstate])
+		|| (player->panim == PA_ROLL)
+		|| ((player->powers[pw_tailsfly] || (player->charability == CA_FLY && player->mo->state-states == S_PLAY_FLY_TIRED))
+			&& !(player->charflags & SF_NOJUMPSPIN))
+		|| (player->charability == CA_GLIDEANDCLIMB && player->mo->state-states == S_PLAY_GLIDE_LANDING)
+		|| ((player->charflags & (SF_DASHMODE|SF_MACHINE)) == (SF_DASHMODE|SF_MACHINE)
+			&& player->dashmode >= DASHMODE_THRESHOLD && player->mo->state-states == S_PLAY_DASH)
+		|| JUMPCURLED(player));
+}
diff --git a/src/r_bsp.c b/src/r_bsp.c
index 6f2a90d2d5e6c8642b4dc0eded0d47afb189012b..b8559d39e54cb5debb00878487bae7b9b7a5a29c 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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_bsp.h b/src/r_bsp.h
index e2da8ebaf54e2226140ee008897d0eefb2432751..40d24ffece796beb2fcdb1cd2cb5289ad30206ed 100644
--- a/src/r_bsp.h
+++ b/src/r_bsp.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_data.c b/src/r_data.c
index af672f6dc024ee2c6840818982d586b17ceacb07..2cfe9cb7ace3139d40a32a9d8dd0ba21afa9b794 100644
--- a/src/r_data.c
+++ b/src/r_data.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -30,10 +30,6 @@
 #include "byteptr.h"
 #include "dehacked.h"
 
-#ifdef _WIN32
-#include <malloc.h> // alloca(sizeof)
-#endif
-
 //
 // Graphics.
 // SRB2 graphics for walls and sprites
diff --git a/src/r_data.h b/src/r_data.h
index aec52b54b654bb874215097e3e508b9ea5609bab..571fdc54f0a20e795f97704e6a2ef173bb6fdb82 100644
--- a/src/r_data.h
+++ b/src/r_data.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_defs.h b/src/r_defs.h
index 9c649fbc4508bf148787566eb6692f6111d24706..1be3a1b8cdce5f365acc957c7087ab7a7cc9c331 100644
--- a/src/r_defs.h
+++ b/src/r_defs.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_draw.c b/src/r_draw.c
index d9ea942a2f22b301bdbd1762e0635f31ba085d6e..f0a19a462848d02c54b07a8a481f11e0969ebef0 100644
--- a/src/r_draw.c
+++ b/src/r_draw.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -25,6 +25,7 @@
 #include "w_wad.h"
 #include "z_zone.h"
 #include "console.h" // Until buffering gets finished
+#include "libdivide.h" // used by NPO2 tilted span functions
 
 #ifdef HWRENDER
 #include "hardware/hw_main.h"
@@ -134,19 +135,51 @@ UINT32 nflatxshift, nflatyshift, nflatshiftup, nflatmask;
 #define DEFAULT_STARTTRANSCOLOR 96
 #define NUM_PALETTE_ENTRIES 256
 
-static UINT8** translationtablecache[MAXSKINS + 7] = {NULL};
+static UINT8 **translationtablecache[MAXSKINS + 7] = {NULL};
 UINT8 skincolor_modified[MAXSKINCOLORS];
 
-CV_PossibleValue_t Color_cons_t[MAXSKINCOLORS+1];
+static INT32 SkinToCacheIndex(INT32 skinnum)
+{
+	switch (skinnum)
+	{
+		case TC_DEFAULT:    return DEFAULT_TT_CACHE_INDEX;
+		case TC_BOSS:       return BOSS_TT_CACHE_INDEX;
+		case TC_METALSONIC: return METALSONIC_TT_CACHE_INDEX;
+		case TC_ALLWHITE:   return ALLWHITE_TT_CACHE_INDEX;
+		case TC_RAINBOW:    return RAINBOW_TT_CACHE_INDEX;
+		case TC_BLINK:      return BLINK_TT_CACHE_INDEX;
+		case TC_DASHMODE:   return DASHMODE_TT_CACHE_INDEX;
+		     default:       break;
+	}
 
-#define TRANSTAB_AMTMUL10 (256.0f / 10.0f)
+	return skinnum;
+}
+
+static INT32 CacheIndexToSkin(INT32 ttc)
+{
+	switch (ttc)
+	{
+		case DEFAULT_TT_CACHE_INDEX:    return TC_DEFAULT;
+		case BOSS_TT_CACHE_INDEX:       return TC_BOSS;
+		case METALSONIC_TT_CACHE_INDEX: return TC_METALSONIC;
+		case ALLWHITE_TT_CACHE_INDEX:   return TC_ALLWHITE;
+		case RAINBOW_TT_CACHE_INDEX:    return TC_RAINBOW;
+		case BLINK_TT_CACHE_INDEX:      return TC_BLINK;
+		case DASHMODE_TT_CACHE_INDEX:   return TC_DASHMODE;
+		     default:                   break;
+	}
+
+	return ttc;
+}
+
+CV_PossibleValue_t Color_cons_t[MAXSKINCOLORS+1];
 
 /** \brief Initializes the translucency tables used by the Software renderer.
 */
 void R_InitTranslucencyTables(void)
 {
-	// Load here the transparency lookup tables 'TINTTAB'
-	// NOTE: the TINTTAB resource MUST BE aligned on 64k for the asm
+	// Load here the transparency lookup tables 'TRANSx0'
+	// NOTE: the TRANSx0 resources MUST BE aligned on 64k for the asm
 	// optimised code (in other words, transtables pointer low word is 0)
 	transtables = Z_MallocAlign(NUMTRANSTABLES*0x10000, PU_STATIC,
 		NULL, 16);
@@ -164,42 +197,43 @@ void R_InitTranslucencyTables(void)
 	R_GenerateBlendTables();
 }
 
-void R_GenerateBlendTables(void)
+static colorlookup_t transtab_lut;
+
+static void BlendTab_Translucent(UINT8 *table, int style, UINT8 blendamt)
 {
-	INT32 i;
+	INT16 bg, fg;
 
-	for (i = 0; i < NUMBLENDMAPS; i++)
-	{
-		if (i == blendtab_modulate)
-			continue;
-		blendtables[i] = Z_MallocAlign((NUMTRANSTABLES + 1) * 0x10000, PU_STATIC, NULL, 16);
-	}
+	if (table == NULL)
+		I_Error("BlendTab_Translucent: input table was NULL!");
 
-	for (i = 0; i <= 9; i++)
+	for (bg = 0; bg < 0xFF; bg++)
 	{
-		const size_t offs = (0x10000 * i);
-		const UINT8 alpha = TRANSTAB_AMTMUL10 * i;
+		for (fg = 0; fg < 0xFF; fg++)
+		{
+			RGBA_t backrgba = V_GetMasterColor(bg);
+			RGBA_t frontrgba = V_GetMasterColor(fg);
+			RGBA_t result;
 
-		R_GenerateTranslucencyTable(blendtables[blendtab_add] + offs, AST_ADD, alpha);
-		R_GenerateTranslucencyTable(blendtables[blendtab_subtract] + offs, AST_SUBTRACT, alpha);
-		R_GenerateTranslucencyTable(blendtables[blendtab_reversesubtract] + offs, AST_REVERSESUBTRACT, alpha);
-	}
+			result.rgba = ASTBlendPixel(backrgba, frontrgba, style, 0xFF);
+			result.rgba = ASTBlendPixel(result, frontrgba, AST_TRANSLUCENT, blendamt);
 
-	// Modulation blending only requires a single table
-	blendtables[blendtab_modulate] = Z_MallocAlign(0x10000, PU_STATIC, NULL, 16);
-	R_GenerateTranslucencyTable(blendtables[blendtab_modulate], AST_MODULATE, 0);
+			table[((bg * 0x100) + fg)] = GetColorLUT(&transtab_lut, result.s.red, result.s.green, result.s.blue);
+		}
+	}
 }
 
-static colorlookup_t transtab_lut;
-
-void R_GenerateTranslucencyTable(UINT8 *table, int style, UINT8 blendamt)
+static void BlendTab_Subtractive(UINT8 *table, int style, UINT8 blendamt)
 {
 	INT16 bg, fg;
 
 	if (table == NULL)
-		I_Error("R_GenerateTranslucencyTable: input table was NULL!");
+		I_Error("BlendTab_Subtractive: input table was NULL!");
 
-	InitColorLUT(&transtab_lut, pMasterPalette, false);
+	if (blendamt == 0xFF)
+	{
+		memset(table, GetColorLUT(&transtab_lut, 0, 0, 0), 0x10000);
+		return;
+	}
 
 	for (bg = 0; bg < 0xFF; bg++)
 	{
@@ -209,12 +243,94 @@ void R_GenerateTranslucencyTable(UINT8 *table, int style, UINT8 blendamt)
 			RGBA_t frontrgba = V_GetMasterColor(fg);
 			RGBA_t result;
 
-			result.rgba = ASTBlendPixel(backrgba, frontrgba, style, blendamt);
+			result.rgba = ASTBlendPixel(backrgba, frontrgba, style, 0xFF);
+			result.s.red = max(0, result.s.red - blendamt);
+			result.s.green = max(0, result.s.green - blendamt);
+			result.s.blue = max(0, result.s.blue - blendamt);
+
+			table[((bg * 0x100) + fg)] = GetColorLUT(&transtab_lut, result.s.red, result.s.green, result.s.blue);
+		}
+	}
+}
+
+static void BlendTab_Modulative(UINT8 *table)
+{
+	INT16 bg, fg;
+
+	if (table == NULL)
+		I_Error("BlendTab_Modulative: input table was NULL!");
+
+	for (bg = 0; bg < 0xFF; bg++)
+	{
+		for (fg = 0; fg < 0xFF; fg++)
+		{
+			RGBA_t backrgba = V_GetMasterColor(bg);
+			RGBA_t frontrgba = V_GetMasterColor(fg);
+			RGBA_t result;
+			result.rgba = ASTBlendPixel(backrgba, frontrgba, AST_MODULATE, 0);
 			table[((bg * 0x100) + fg)] = GetColorLUT(&transtab_lut, result.s.red, result.s.green, result.s.blue);
 		}
 	}
 }
 
+static INT32 BlendTab_Count[NUMBLENDMAPS] =
+{
+	NUMTRANSTABLES+1, // blendtab_add
+	NUMTRANSTABLES+1, // blendtab_subtract
+	NUMTRANSTABLES+1, // blendtab_reversesubtract
+	1                 // blendtab_modulate
+};
+
+static INT32 BlendTab_FromStyle[] =
+{
+	0,                        // AST_COPY
+	0,                        // AST_TRANSLUCENT
+	blendtab_add,             // AST_ADD
+	blendtab_subtract,        // AST_SUBTRACT
+	blendtab_reversesubtract, // AST_REVERSESUBTRACT
+	blendtab_modulate,        // AST_MODULATE
+	0                         // AST_OVERLAY
+};
+
+static void BlendTab_GenerateMaps(INT32 tab, INT32 style, void (*genfunc)(UINT8 *, int, UINT8))
+{
+	INT32 i = 0, num = BlendTab_Count[tab];
+	const float amtmul = (256.0f / (float)(NUMTRANSTABLES + 1));
+	for (; i < num; i++)
+	{
+		const size_t offs = (0x10000 * i);
+		const UINT16 alpha = min(amtmul * i, 0xFF);
+		genfunc(blendtables[tab] + offs, style, alpha);
+	}
+}
+
+void R_GenerateBlendTables(void)
+{
+	INT32 i;
+
+	for (i = 0; i < NUMBLENDMAPS; i++)
+		blendtables[i] = Z_MallocAlign(BlendTab_Count[i] * 0x10000, PU_STATIC, NULL, 16);
+
+	InitColorLUT(&transtab_lut, pMasterPalette, false);
+
+	// Additive
+	BlendTab_GenerateMaps(blendtab_add, AST_ADD, BlendTab_Translucent);
+
+	// Subtractive
+#if 1
+	BlendTab_GenerateMaps(blendtab_subtract, AST_SUBTRACT, BlendTab_Subtractive);
+#else
+	BlendTab_GenerateMaps(blendtab_subtract, AST_SUBTRACT, BlendTab_Translucent);
+#endif
+
+	// Reverse subtractive
+	BlendTab_GenerateMaps(blendtab_reversesubtract, AST_REVERSESUBTRACT, BlendTab_Translucent);
+
+	// Modulative blending only requires a single table
+	BlendTab_Modulative(blendtables[blendtab_modulate]);
+}
+
+#define ClipBlendLevel(style, trans) max(min((trans), BlendTab_Count[BlendTab_FromStyle[style]]-1), 0)
 #define ClipTransLevel(trans) max(min((trans), NUMTRANSMAPS-2), 0)
 
 UINT8 *R_GetTranslucencyTable(INT32 alphalevel)
@@ -224,7 +340,12 @@ UINT8 *R_GetTranslucencyTable(INT32 alphalevel)
 
 UINT8 *R_GetBlendTable(int style, INT32 alphalevel)
 {
-	size_t offs = (ClipTransLevel(alphalevel) << FF_TRANSSHIFT);
+	size_t offs;
+
+	if (style <= AST_COPY || style >= AST_OVERLAY)
+		return NULL;
+
+	offs = (ClipBlendLevel(style, alphalevel) << FF_TRANSSHIFT);
 
 	// Lactozilla: Returns the equivalent to AST_TRANSLUCENT
 	// if no alpha style matches any of the blend tables.
@@ -249,6 +370,14 @@ UINT8 *R_GetBlendTable(int style, INT32 alphalevel)
 		return NULL;
 }
 
+boolean R_BlendLevelVisible(INT32 blendmode, INT32 alphalevel)
+{
+	if (blendmode <= AST_COPY || blendmode == AST_SUBTRACT || blendmode == AST_MODULATE || blendmode >= AST_OVERLAY)
+		return true;
+
+	return (alphalevel < BlendTab_Count[BlendTab_FromStyle[blendmode]]);
+}
+
 // Define for getting accurate color brightness readings according to how the human eye sees them.
 // https://en.wikipedia.org/wiki/Relative_luminance
 // 0.2126 to red
@@ -308,7 +437,7 @@ static void R_RainbowColormap(UINT8 *dest_colormap, UINT16 skincolor)
 /**	\brief	Generates a translation colormap.
 
 	\param	dest_colormap	colormap to populate
-	\param	skinnum		number of skin, TC_DEFAULT or TC_BOSS
+	\param	skinnum		skin number, or a translation mode
 	\param	color		translation color
 
 	\return	void
@@ -353,8 +482,12 @@ static void R_GenerateTranslationColormap(UINT8 *dest_colormap, INT32 skinnum, U
 		// White!
 		if (skinnum == TC_BOSS)
 		{
+			UINT8 *originalColormap = R_GetTranslationColormap(TC_DEFAULT, (skincolornum_t)color, GTC_CACHE);
 			for (i = 0; i < 16; i++)
+			{
+				dest_colormap[DEFAULT_STARTTRANSCOLOR + i] = originalColormap[DEFAULT_STARTTRANSCOLOR + i];
 				dest_colormap[31-i] = i;
+			}
 		}
 		else if (skinnum == TC_METALSONIC)
 		{
@@ -412,6 +545,9 @@ static void R_GenerateTranslationColormap(UINT8 *dest_colormap, INT32 skinnum, U
 	if (color >= numskincolors)
 		I_Error("Invalid skin color #%hu.", (UINT16)color);
 
+	if (skinnum < 0 && skinnum > TC_DEFAULT)
+		I_Error("Invalid translation colormap index %d.", skinnum);
+
 	starttranscolor = (skinnum != TC_DEFAULT) ? skins[skinnum].starttranscolor : DEFAULT_STARTTRANSCOLOR;
 
 	if (starttranscolor >= NUM_PALETTE_ENTRIES)
@@ -448,25 +584,11 @@ static void R_GenerateTranslationColormap(UINT8 *dest_colormap, INT32 skinnum, U
 UINT8* R_GetTranslationColormap(INT32 skinnum, skincolornum_t color, UINT8 flags)
 {
 	UINT8* ret;
-	INT32 skintableindex;
+	INT32 skintableindex = SkinToCacheIndex(skinnum); // Adjust if we want the default colormap
 	INT32 i;
 
-	// Adjust if we want the default colormap
-	switch (skinnum)
-	{
-		case TC_DEFAULT:    skintableindex = DEFAULT_TT_CACHE_INDEX; break;
-		case TC_BOSS:       skintableindex = BOSS_TT_CACHE_INDEX; break;
-		case TC_METALSONIC: skintableindex = METALSONIC_TT_CACHE_INDEX; break;
-		case TC_ALLWHITE:   skintableindex = ALLWHITE_TT_CACHE_INDEX; break;
-		case TC_RAINBOW:    skintableindex = RAINBOW_TT_CACHE_INDEX; break;
-		case TC_BLINK:      skintableindex = BLINK_TT_CACHE_INDEX; break;
-		case TC_DASHMODE:   skintableindex = DASHMODE_TT_CACHE_INDEX; break;
-		     default:       skintableindex = skinnum; break;
-	}
-
 	if (flags & GTC_CACHE)
 	{
-
 		// Allocate table for skin if necessary
 		if (!translationtablecache[skintableindex])
 			translationtablecache[skintableindex] = Z_Calloc(MAXSKINCOLORS * sizeof(UINT8**), PU_STATIC, NULL);
@@ -479,7 +601,8 @@ UINT8* R_GetTranslationColormap(INT32 skinnum, skincolornum_t color, UINT8 flags
 		{
 			for (i = 0; i < (INT32)(sizeof(translationtablecache) / sizeof(translationtablecache[0])); i++)
 				if (translationtablecache[i] && translationtablecache[i][color])
-					R_GenerateTranslationColormap(translationtablecache[i][color], i>=MAXSKINS ? MAXSKINS-i-1 : i, color);
+					R_GenerateTranslationColormap(translationtablecache[i][color], CacheIndexToSkin(i), color);
+
 			skincolor_modified[color] = false;
 		}
 	}
diff --git a/src/r_draw.h b/src/r_draw.h
index 9957541ca33bcfd84c37b03f4375184a722da459..2173c7a5a36e5c9b92063657aa833dbb5b457726 100644
--- a/src/r_draw.h
+++ b/src/r_draw.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -106,13 +106,17 @@ extern lumpnum_t viewborderlump[8];
 
 #define GTC_CACHE 1
 
-#define TC_DEFAULT    -1
-#define TC_BOSS       -2
-#define TC_METALSONIC -3 // For Metal Sonic battle
-#define TC_ALLWHITE   -4 // For Cy-Brak-demon
-#define TC_RAINBOW    -5 // For single colour
-#define TC_BLINK      -6 // For item blinking, according to kart
-#define TC_DASHMODE   -7 // For Metal Sonic's dashmode
+enum
+{
+	TC_BOSS       = INT8_MIN,
+	TC_METALSONIC, // For Metal Sonic battle
+	TC_ALLWHITE,   // For Cy-Brak-demon
+	TC_RAINBOW,    // For single colour
+	TC_BLINK,      // For item blinking, according to kart
+	TC_DASHMODE,   // For Metal Sonic's dashmode
+
+	TC_DEFAULT
+};
 
 // Custom player skin translation
 // Initialize color translation tables, for player rendering etc.
@@ -136,11 +140,12 @@ extern UINT8 *blendtables[NUMBLENDMAPS];
 
 void R_InitTranslucencyTables(void);
 void R_GenerateBlendTables(void);
-void R_GenerateTranslucencyTable(UINT8 *table, int style, UINT8 blendamt);
 
 UINT8 *R_GetTranslucencyTable(INT32 alphalevel);
 UINT8 *R_GetBlendTable(int style, INT32 alphalevel);
 
+boolean R_BlendLevelVisible(INT32 blendmode, INT32 alphalevel);
+
 // Color ramp modification should force a recache
 extern UINT8 skincolor_modified[];
 
@@ -172,7 +177,7 @@ void R_Draw2sMultiPatchTranslucentColumn_8(void);
 void R_DrawFogColumn_8(void);
 void R_DrawColumnShadowed_8(void);
 
-#define PLANELIGHTFLOAT (BASEVIDWIDTH * BASEVIDWIDTH / vid.width / (zeroheight - FIXED_TO_FLOAT(viewz)) / 21.0f * FIXED_TO_FLOAT(fovtan))
+#define PLANELIGHTFLOAT (BASEVIDWIDTH * BASEVIDWIDTH / vid.width / zeroheight / 21.0f * FIXED_TO_FLOAT(fovtan))
 
 void R_DrawSpan_8(void);
 void R_DrawTranslucentSpan_8(void);
diff --git a/src/r_draw16.c b/src/r_draw16.c
index 8b1d29e8d6557dd8f7e556e68964ba67e77ac3b0..1a2fed77316b6fd80c6b91f5d1357e1b47eee99f 100644
--- a/src/r_draw16.c
+++ b/src/r_draw16.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_draw8.c b/src/r_draw8.c
index e78ba8a6c49b8f39a9c54fe0af814340c9462641..b8a63d5c042d7ce49f8480bac269b763c43f5dd6 100644
--- a/src/r_draw8.c
+++ b/src/r_draw8.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -693,8 +693,8 @@ void R_DrawTiltedSpan_8(void)
 	do
 	{
 		double z = 1.f/iz;
-		u = (INT64)(uz*z) + viewx;
-		v = (INT64)(vz*z) + viewy;
+		u = (INT64)(uz*z);
+		v = (INT64)(vz*z);
 
 		colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 
@@ -726,8 +726,8 @@ void R_DrawTiltedSpan_8(void)
 		endv = vz*endz;
 		stepu = (INT64)((endu - startu) * INVSPAN);
 		stepv = (INT64)((endv - startv) * INVSPAN);
-		u = (INT64)(startu) + viewx;
-		v = (INT64)(startv) + viewy;
+		u = (INT64)(startu);
+		v = (INT64)(startv);
 
 		for (i = SPANSIZE-1; i >= 0; i--)
 		{
@@ -763,8 +763,8 @@ void R_DrawTiltedSpan_8(void)
 			left = 1.f/left;
 			stepu = (INT64)((endu - startu) * left);
 			stepv = (INT64)((endv - startv) * left);
-			u = (INT64)(startu) + viewx;
-			v = (INT64)(startv) + viewy;
+			u = (INT64)(startu);
+			v = (INT64)(startv);
 
 			for (; width != 0; width--)
 			{
@@ -826,8 +826,8 @@ void R_DrawTiltedTranslucentSpan_8(void)
 	do
 	{
 		double z = 1.f/iz;
-		u = (INT64)(uz*z) + viewx;
-		v = (INT64)(vz*z) + viewy;
+		u = (INT64)(uz*z);
+		v = (INT64)(vz*z);
 
 		colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 		*dest = *(ds_transmap + (colormap[source[((v >> nflatyshift) & nflatmask) | (u >> nflatxshift)]] << 8) + *dest);
@@ -858,8 +858,8 @@ void R_DrawTiltedTranslucentSpan_8(void)
 		endv = vz*endz;
 		stepu = (INT64)((endu - startu) * INVSPAN);
 		stepv = (INT64)((endv - startv) * INVSPAN);
-		u = (INT64)(startu) + viewx;
-		v = (INT64)(startv) + viewy;
+		u = (INT64)(startu);
+		v = (INT64)(startv);
 
 		for (i = SPANSIZE-1; i >= 0; i--)
 		{
@@ -895,8 +895,8 @@ void R_DrawTiltedTranslucentSpan_8(void)
 			left = 1.f/left;
 			stepu = (INT64)((endu - startu) * left);
 			stepv = (INT64)((endv - startv) * left);
-			u = (INT64)(startu) + viewx;
-			v = (INT64)(startv) + viewy;
+			u = (INT64)(startu);
+			v = (INT64)(startv);
 
 			for (; width != 0; width--)
 			{
@@ -960,8 +960,8 @@ void R_DrawTiltedTranslucentWaterSpan_8(void)
 	do
 	{
 		double z = 1.f/iz;
-		u = (INT64)(uz*z) + viewx;
-		v = (INT64)(vz*z) + viewy;
+		u = (INT64)(uz*z);
+		v = (INT64)(vz*z);
 
 		colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 		*dest = *(ds_transmap + (colormap[source[((v >> nflatyshift) & nflatmask) | (u >> nflatxshift)]] << 8) + *dsrc++);
@@ -992,8 +992,8 @@ void R_DrawTiltedTranslucentWaterSpan_8(void)
 		endv = vz*endz;
 		stepu = (INT64)((endu - startu) * INVSPAN);
 		stepv = (INT64)((endv - startv) * INVSPAN);
-		u = (INT64)(startu) + viewx;
-		v = (INT64)(startv) + viewy;
+		u = (INT64)(startu);
+		v = (INT64)(startv);
 
 		for (i = SPANSIZE-1; i >= 0; i--)
 		{
@@ -1029,8 +1029,8 @@ void R_DrawTiltedTranslucentWaterSpan_8(void)
 			left = 1.f/left;
 			stepu = (INT64)((endu - startu) * left);
 			stepv = (INT64)((endv - startv) * left);
-			u = (INT64)(startu) + viewx;
-			v = (INT64)(startv) + viewy;
+			u = (INT64)(startu);
+			v = (INT64)(startv);
 
 			for (; width != 0; width--)
 			{
@@ -1091,8 +1091,8 @@ void R_DrawTiltedSplat_8(void)
 	do
 	{
 		double z = 1.f/iz;
-		u = (INT64)(uz*z) + viewx;
-		v = (INT64)(vz*z) + viewy;
+		u = (INT64)(uz*z);
+		v = (INT64)(vz*z);
 
 		colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 
@@ -1127,8 +1127,8 @@ void R_DrawTiltedSplat_8(void)
 		endv = vz*endz;
 		stepu = (INT64)((endu - startu) * INVSPAN);
 		stepv = (INT64)((endv - startv) * INVSPAN);
-		u = (INT64)(startu) + viewx;
-		v = (INT64)(startv) + viewy;
+		u = (INT64)(startu);
+		v = (INT64)(startv);
 
 		for (i = SPANSIZE-1; i >= 0; i--)
 		{
@@ -1168,8 +1168,8 @@ void R_DrawTiltedSplat_8(void)
 			left = 1.f/left;
 			stepu = (INT64)((endu - startu) * left);
 			stepv = (INT64)((endv - startv) * left);
-			u = (INT64)(startu) + viewx;
-			v = (INT64)(startv) + viewy;
+			u = (INT64)(startu);
+			v = (INT64)(startv);
 
 			for (; width != 0; width--)
 			{
@@ -1227,8 +1227,9 @@ void R_DrawSplat_8 (void)
 		// need!
 		//
 		// <Callum> 4194303 = (2048x2048)-1 (2048x2048 is maximum flat size)
+		// Why decimal? 0x3FFFFF == 4194303... ~Golden
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
+		val &= 0x3FFFFF;
 		val = source[val];
 		if (val != TRANSPARENTPIXEL)
 			dest[0] = colormap[val];
@@ -1236,7 +1237,7 @@ void R_DrawSplat_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
+		val &= 0x3FFFFF;
 		val = source[val];
 		if (val != TRANSPARENTPIXEL)
 			dest[1] = colormap[val];
@@ -1244,7 +1245,7 @@ void R_DrawSplat_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
+		val &= 0x3FFFFF;
 		val = source[val];
 		if (val != TRANSPARENTPIXEL)
 			dest[2] = colormap[val];
@@ -1252,7 +1253,7 @@ void R_DrawSplat_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
+		val &= 0x3FFFFF;
 		val = source[val];
 		if (val != TRANSPARENTPIXEL)
 			dest[3] = colormap[val];
@@ -1260,7 +1261,7 @@ void R_DrawSplat_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
+		val &= 0x3FFFFF;
 		val = source[val];
 		if (val != TRANSPARENTPIXEL)
 			dest[4] = colormap[val];
@@ -1268,7 +1269,7 @@ void R_DrawSplat_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
+		val &= 0x3FFFFF;
 		val = source[val];
 		if (val != TRANSPARENTPIXEL)
 			dest[5] = colormap[val];
@@ -1276,7 +1277,7 @@ void R_DrawSplat_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
+		val &= 0x3FFFFF;
 		val = source[val];
 		if (val != TRANSPARENTPIXEL)
 			dest[6] = colormap[val];
@@ -1284,7 +1285,7 @@ void R_DrawSplat_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
+		val &= 0x3FFFFF;
 		val = source[val];
 		if (val != TRANSPARENTPIXEL)
 			dest[7] = colormap[val];
@@ -1447,10 +1448,7 @@ void R_DrawFloorSprite_8 (void)
 		// SoM: Why didn't I see this earlier? the spot variable is a waste now because we don't
 		// have the uber complicated math to calculate it now, so that was a memory write we didn't
 		// need!
-		//
-		// <Callum> 4194303 = (2048x2048)-1 (2048x2048 is maximum flat size)
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
 		val = source[val];
 		if (val & 0xFF00)
 			dest[0] = colormap[translation[val & 0xFF]];
@@ -1458,7 +1456,6 @@ void R_DrawFloorSprite_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
 		val = source[val];
 		if (val & 0xFF00)
 			dest[1] = colormap[translation[val & 0xFF]];
@@ -1466,7 +1463,6 @@ void R_DrawFloorSprite_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
 		val = source[val];
 		if (val & 0xFF00)
 			dest[2] = colormap[translation[val & 0xFF]];
@@ -1474,7 +1470,6 @@ void R_DrawFloorSprite_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
 		val = source[val];
 		if (val & 0xFF00)
 			dest[3] = colormap[translation[val & 0xFF]];
@@ -1482,7 +1477,6 @@ void R_DrawFloorSprite_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
 		val = source[val];
 		if (val & 0xFF00)
 			dest[4] = colormap[translation[val & 0xFF]];
@@ -1490,7 +1484,6 @@ void R_DrawFloorSprite_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
 		val = source[val];
 		if (val & 0xFF00)
 			dest[5] = colormap[translation[val & 0xFF]];
@@ -1498,7 +1491,6 @@ void R_DrawFloorSprite_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
 		val = source[val];
 		if (val & 0xFF00)
 			dest[6] = colormap[translation[val & 0xFF]];
@@ -1506,7 +1498,6 @@ void R_DrawFloorSprite_8 (void)
 		yposition += ystep;
 
 		val = (((UINT32)yposition >> nflatyshift) & nflatmask) | ((UINT32)xposition >> nflatxshift);
-		val &= 4194303;
 		val = source[val];
 		if (val & 0xFF00)
 			dest[7] = colormap[translation[val & 0xFF]];
@@ -1682,8 +1673,8 @@ void R_DrawTiltedFloorSprite_8(void)
 		endv = vz*endz;
 		stepu = (INT64)((endu - startu) * INVSPAN);
 		stepv = (INT64)((endv - startv) * INVSPAN);
-		u = (INT64)(startu) + viewx;
-		v = (INT64)(startv) + viewy;
+		u = (INT64)(startu);
+		v = (INT64)(startv);
 
 		for (i = SPANSIZE-1; i >= 0; i--)
 		{
@@ -1722,8 +1713,8 @@ void R_DrawTiltedFloorSprite_8(void)
 			left = 1.f/left;
 			stepu = (INT64)((endu - startu) * left);
 			stepv = (INT64)((endv - startv) * left);
-			u = (INT64)(startu) + viewx;
-			v = (INT64)(startv) + viewy;
+			u = (INT64)(startu);
+			v = (INT64)(startv);
 
 			for (; width != 0; width--)
 			{
@@ -1791,8 +1782,8 @@ void R_DrawTiltedTranslucentFloorSprite_8(void)
 		endv = vz*endz;
 		stepu = (INT64)((endu - startu) * INVSPAN);
 		stepv = (INT64)((endv - startv) * INVSPAN);
-		u = (INT64)(startu) + viewx;
-		v = (INT64)(startv) + viewy;
+		u = (INT64)(startu);
+		v = (INT64)(startv);
 
 		for (i = SPANSIZE-1; i >= 0; i--)
 		{
@@ -1831,8 +1822,8 @@ void R_DrawTiltedTranslucentFloorSprite_8(void)
 			left = 1.f/left;
 			stepu = (INT64)((endu - startu) * left);
 			stepv = (INT64)((endv - startv) * left);
-			u = (INT64)(startu) + viewx;
-			v = (INT64)(startv) + viewy;
+			u = (INT64)(startu);
+			v = (INT64)(startv);
 
 			for (; width != 0; width--)
 			{
diff --git a/src/r_draw8_npo2.c b/src/r_draw8_npo2.c
index a34a20e9a9737241bbc183d71f8bae01a982cb7a..2433cb4024295401017fae6b08394a7db7d0d0df 100644
--- a/src/r_draw8_npo2.c
+++ b/src/r_draw8_npo2.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -106,6 +106,9 @@ void R_DrawTiltedSpan_NPO2_8(void)
 	double endz, endu, endv;
 	UINT32 stepu, stepv;
 
+	struct libdivide_u32_t x_divider = libdivide_u32_gen(ds_flatwidth);
+	struct libdivide_u32_t y_divider = libdivide_u32_gen(ds_flatheight);
+
 	iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
 
 	// Lighting is simple. It's just linear interpolation from start to end
@@ -133,24 +136,25 @@ void R_DrawTiltedSpan_NPO2_8(void)
 	do
 	{
 		double z = 1.f/iz;
-		u = (INT64)(uz*z) + viewx;
-		v = (INT64)(vz*z) + viewy;
+		u = (INT64)(uz*z);
+		v = (INT64)(vz*z);
 
 		colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 
 		// Lactozilla: Non-powers-of-two
 		{
-			fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-			fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+			fixed_t x = (((fixed_t)u) >> FRACBITS);
+			fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 			// Carefully align all of my Friends.
 			if (x < 0)
-				x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+				x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+			else
+				x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 			if (y < 0)
-				y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-			x %= ds_flatwidth;
-			y %= ds_flatheight;
+				y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+			else
+				y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 			*dest = colormap[source[((y * ds_flatwidth) + x)]];
 		}
@@ -181,25 +185,26 @@ void R_DrawTiltedSpan_NPO2_8(void)
 		endv = vz*endz;
 		stepu = (INT64)((endu - startu) * INVSPAN);
 		stepv = (INT64)((endv - startv) * INVSPAN);
-		u = (INT64)(startu) + viewx;
-		v = (INT64)(startv) + viewy;
+		u = (INT64)(startu);
+		v = (INT64)(startv);
 
 		for (i = SPANSIZE-1; i >= 0; i--)
 		{
 			colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 			// Lactozilla: Non-powers-of-two
 			{
-				fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-				fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 				// Carefully align all of my Friends.
 				if (x < 0)
-					x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+					x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+				else
+					x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 				if (y < 0)
-					y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-				x %= ds_flatwidth;
-				y %= ds_flatheight;
+					y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+				else
+					y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 				*dest = colormap[source[((y * ds_flatwidth) + x)]];
 			}
@@ -220,17 +225,18 @@ void R_DrawTiltedSpan_NPO2_8(void)
 			colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 			// Lactozilla: Non-powers-of-two
 			{
-				fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-				fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 				// Carefully align all of my Friends.
 				if (x < 0)
-					x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+					x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+				else
+					x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 				if (y < 0)
-					y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-				x %= ds_flatwidth;
-				y %= ds_flatheight;
+					y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+				else
+					y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 				*dest = colormap[source[((y * ds_flatwidth) + x)]];
 			}
@@ -248,25 +254,26 @@ void R_DrawTiltedSpan_NPO2_8(void)
 			left = 1.f/left;
 			stepu = (INT64)((endu - startu) * left);
 			stepv = (INT64)((endv - startv) * left);
-			u = (INT64)(startu) + viewx;
-			v = (INT64)(startv) + viewy;
+			u = (INT64)(startu);
+			v = (INT64)(startv);
 
 			for (; width != 0; width--)
 			{
 				colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 				// Lactozilla: Non-powers-of-two
 				{
-					fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-					fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+					fixed_t x = (((fixed_t)u) >> FRACBITS);
+					fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 					// Carefully align all of my Friends.
 					if (x < 0)
-						x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+						x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+					else
+						x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 					if (y < 0)
-						y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-					x %= ds_flatwidth;
-					y %= ds_flatheight;
+						y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+					else
+						y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 					*dest = colormap[source[((y * ds_flatwidth) + x)]];
 				}
@@ -299,6 +306,9 @@ void R_DrawTiltedTranslucentSpan_NPO2_8(void)
 	double endz, endu, endv;
 	UINT32 stepu, stepv;
 
+	struct libdivide_u32_t x_divider = libdivide_u32_gen(ds_flatwidth);
+	struct libdivide_u32_t y_divider = libdivide_u32_gen(ds_flatheight);
+
 	iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
 
 	// Lighting is simple. It's just linear interpolation from start to end
@@ -326,23 +336,24 @@ void R_DrawTiltedTranslucentSpan_NPO2_8(void)
 	do
 	{
 		double z = 1.f/iz;
-		u = (INT64)(uz*z) + viewx;
-		v = (INT64)(vz*z) + viewy;
+		u = (INT64)(uz*z);
+		v = (INT64)(vz*z);
 
 		colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 		// Lactozilla: Non-powers-of-two
 		{
-			fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-			fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+			fixed_t x = (((fixed_t)u) >> FRACBITS);
+			fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 			// Carefully align all of my Friends.
 			if (x < 0)
-				x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+				x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+			else
+				x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 			if (y < 0)
-				y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-			x %= ds_flatwidth;
-			y %= ds_flatheight;
+				y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+			else
+				y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 			*dest = *(ds_transmap + (colormap[source[((y * ds_flatwidth) + x)]] << 8) + *dest);
 		}
@@ -373,25 +384,26 @@ void R_DrawTiltedTranslucentSpan_NPO2_8(void)
 		endv = vz*endz;
 		stepu = (INT64)((endu - startu) * INVSPAN);
 		stepv = (INT64)((endv - startv) * INVSPAN);
-		u = (INT64)(startu) + viewx;
-		v = (INT64)(startv) + viewy;
+		u = (INT64)(startu);
+		v = (INT64)(startv);
 
 		for (i = SPANSIZE-1; i >= 0; i--)
 		{
 			colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 			// Lactozilla: Non-powers-of-two
 			{
-				fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-				fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 				// Carefully align all of my Friends.
 				if (x < 0)
-					x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+					x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+				else
+					x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 				if (y < 0)
-					y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-				x %= ds_flatwidth;
-				y %= ds_flatheight;
+					y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+				else
+					y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 				*dest = *(ds_transmap + (colormap[source[((y * ds_flatwidth) + x)]] << 8) + *dest);
 			}
@@ -412,17 +424,18 @@ void R_DrawTiltedTranslucentSpan_NPO2_8(void)
 			colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 			// Lactozilla: Non-powers-of-two
 			{
-				fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-				fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 				// Carefully align all of my Friends.
 				if (x < 0)
-					x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+					x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+				else
+					x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 				if (y < 0)
-					y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-				x %= ds_flatwidth;
-				y %= ds_flatheight;
+					y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+				else
+					y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 				*dest = *(ds_transmap + (colormap[source[((y * ds_flatwidth) + x)]] << 8) + *dest);
 			}
@@ -440,25 +453,26 @@ void R_DrawTiltedTranslucentSpan_NPO2_8(void)
 			left = 1.f/left;
 			stepu = (INT64)((endu - startu) * left);
 			stepv = (INT64)((endv - startv) * left);
-			u = (INT64)(startu) + viewx;
-			v = (INT64)(startv) + viewy;
+			u = (INT64)(startu);
+			v = (INT64)(startv);
 
 			for (; width != 0; width--)
 			{
 				colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 				// Lactozilla: Non-powers-of-two
 				{
-					fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-					fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+					fixed_t x = (((fixed_t)u) >> FRACBITS);
+					fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 					// Carefully align all of my Friends.
 					if (x < 0)
-						x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+						x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+					else
+						x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 					if (y < 0)
-						y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-					x %= ds_flatwidth;
-					y %= ds_flatheight;
+						y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+					else
+						y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 					*dest = *(ds_transmap + (colormap[source[((y * ds_flatwidth) + x)]] << 8) + *dest);
 				}
@@ -490,6 +504,9 @@ void R_DrawTiltedSplat_NPO2_8(void)
 	double endz, endu, endv;
 	UINT32 stepu, stepv;
 
+	struct libdivide_u32_t x_divider = libdivide_u32_gen(ds_flatwidth);
+	struct libdivide_u32_t y_divider = libdivide_u32_gen(ds_flatheight);
+
 	iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
 
 	// Lighting is simple. It's just linear interpolation from start to end
@@ -517,24 +534,25 @@ void R_DrawTiltedSplat_NPO2_8(void)
 	do
 	{
 		double z = 1.f/iz;
-		u = (INT64)(uz*z) + viewx;
-		v = (INT64)(vz*z) + viewy;
+		u = (INT64)(uz*z);
+		v = (INT64)(vz*z);
 
 		colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 
 		// Lactozilla: Non-powers-of-two
 		{
-			fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-			fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+			fixed_t x = (((fixed_t)u) >> FRACBITS);
+			fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 			// Carefully align all of my Friends.
 			if (x < 0)
-				x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+				x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+			else
+				x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 			if (y < 0)
-				y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-			x %= ds_flatwidth;
-			y %= ds_flatheight;
+				y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+			else
+				y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 			val = source[((y * ds_flatwidth) + x)];
 		}
@@ -569,25 +587,26 @@ void R_DrawTiltedSplat_NPO2_8(void)
 		endv = vz*endz;
 		stepu = (INT64)((endu - startu) * INVSPAN);
 		stepv = (INT64)((endv - startv) * INVSPAN);
-		u = (INT64)(startu) + viewx;
-		v = (INT64)(startv) + viewy;
+		u = (INT64)(startu);
+		v = (INT64)(startv);
 
 		for (i = SPANSIZE-1; i >= 0; i--)
 		{
 			colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 			// Lactozilla: Non-powers-of-two
 			{
-				fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-				fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 				// Carefully align all of my Friends.
 				if (x < 0)
-					x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+					x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+				else
+					x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 				if (y < 0)
-					y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-				x %= ds_flatwidth;
-				y %= ds_flatheight;
+					y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+				else
+					y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 				val = source[((y * ds_flatwidth) + x)];
 			}
@@ -610,17 +629,18 @@ void R_DrawTiltedSplat_NPO2_8(void)
 			colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 			// Lactozilla: Non-powers-of-two
 			{
-				fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-				fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 				// Carefully align all of my Friends.
 				if (x < 0)
-					x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+					x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+				else
+					x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 				if (y < 0)
-					y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-				x %= ds_flatwidth;
-				y %= ds_flatheight;
+					y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+				else
+					y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 				val = source[((y * ds_flatwidth) + x)];
 			}
@@ -640,26 +660,26 @@ void R_DrawTiltedSplat_NPO2_8(void)
 			left = 1.f/left;
 			stepu = (INT64)((endu - startu) * left);
 			stepv = (INT64)((endv - startv) * left);
-			u = (INT64)(startu) + viewx;
-			v = (INT64)(startv) + viewy;
+			u = (INT64)(startu);
+			v = (INT64)(startv);
 
 			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-viewx) >> FRACBITS);
-					fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+					fixed_t x = (((fixed_t)u) >> FRACBITS);
+					fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 					// Carefully align all of my Friends.
 					if (x < 0)
-						x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+						x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+					else
+						x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 					if (y < 0)
-						y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-					x %= ds_flatwidth;
-					y %= ds_flatheight;
+						y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+					else
+						y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 					val = source[((y * ds_flatwidth) + x)];
 				}
@@ -1002,14 +1022,14 @@ void R_DrawTiltedFloorSprite_NPO2_8(void)
 		endv = vz*endz;
 		stepu = (INT64)((endu - startu) * INVSPAN);
 		stepv = (INT64)((endv - startv) * INVSPAN);
-		u = (INT64)(startu) + viewx;
-		v = (INT64)(startv) + viewy;
+		u = (INT64)(startu);
+		v = (INT64)(startv);
 
 		for (i = SPANSIZE-1; i >= 0; i--)
 		{
 			// Lactozilla: Non-powers-of-two
-			fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-			fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+			fixed_t x = (((fixed_t)u) >> FRACBITS);
+			fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 			// Carefully align all of my Friends.
 			if (x < 0)
@@ -1040,8 +1060,8 @@ void R_DrawTiltedFloorSprite_NPO2_8(void)
 			v = (INT64)(startv);
 			// Lactozilla: Non-powers-of-two
 			{
-				fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-				fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 				// Carefully align all of my Friends.
 				if (x < 0)
@@ -1070,14 +1090,14 @@ void R_DrawTiltedFloorSprite_NPO2_8(void)
 			left = 1.f/left;
 			stepu = (INT64)((endu - startu) * left);
 			stepv = (INT64)((endv - startv) * left);
-			u = (INT64)(startu) + viewx;
-			v = (INT64)(startv) + viewy;
+			u = (INT64)(startu);
+			v = (INT64)(startv);
 
 			for (; width != 0; width--)
 			{
 				// Lactozilla: Non-powers-of-two
-				fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-				fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 				// Carefully align all of my Friends.
 				if (x < 0)
@@ -1152,14 +1172,14 @@ void R_DrawTiltedTranslucentFloorSprite_NPO2_8(void)
 		endv = vz*endz;
 		stepu = (INT64)((endu - startu) * INVSPAN);
 		stepv = (INT64)((endv - startv) * INVSPAN);
-		u = (INT64)(startu) + viewx;
-		v = (INT64)(startv) + viewy;
+		u = (INT64)(startu);
+		v = (INT64)(startv);
 
 		for (i = SPANSIZE-1; i >= 0; i--)
 		{
 			// Lactozilla: Non-powers-of-two
-			fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-			fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+			fixed_t x = (((fixed_t)u) >> FRACBITS);
+			fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 			// Carefully align all of my Friends.
 			if (x < 0)
@@ -1190,8 +1210,8 @@ void R_DrawTiltedTranslucentFloorSprite_NPO2_8(void)
 			v = (INT64)(startv);
 			// Lactozilla: Non-powers-of-two
 			{
-				fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-				fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 				// Carefully align all of my Friends.
 				if (x < 0)
@@ -1220,14 +1240,14 @@ void R_DrawTiltedTranslucentFloorSprite_NPO2_8(void)
 			left = 1.f/left;
 			stepu = (INT64)((endu - startu) * left);
 			stepv = (INT64)((endv - startv) * left);
-			u = (INT64)(startu) + viewx;
-			v = (INT64)(startv) + viewy;
+			u = (INT64)(startu);
+			v = (INT64)(startv);
 
 			for (; width != 0; width--)
 			{
 				// Lactozilla: Non-powers-of-two
-				fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-				fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 				// Carefully align all of my Friends.
 				if (x < 0)
@@ -1401,6 +1421,9 @@ void R_DrawTiltedTranslucentWaterSpan_NPO2_8(void)
 	double endz, endu, endv;
 	UINT32 stepu, stepv;
 
+	struct libdivide_u32_t x_divider = libdivide_u32_gen(ds_flatwidth);
+	struct libdivide_u32_t y_divider = libdivide_u32_gen(ds_flatheight);
+
 	iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
 
 	// Lighting is simple. It's just linear interpolation from start to end
@@ -1429,23 +1452,24 @@ void R_DrawTiltedTranslucentWaterSpan_NPO2_8(void)
 	do
 	{
 		double z = 1.f/iz;
-		u = (INT64)(uz*z) + viewx;
-		v = (INT64)(vz*z) + viewy;
+		u = (INT64)(uz*z);
+		v = (INT64)(vz*z);
 
 		colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 		// Lactozilla: Non-powers-of-two
 		{
-			fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-			fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+			fixed_t x = (((fixed_t)u) >> FRACBITS);
+			fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 			// Carefully align all of my Friends.
 			if (x < 0)
-				x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+				x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+			else
+				x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 			if (y < 0)
-				y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-			x %= ds_flatwidth;
-			y %= ds_flatheight;
+				y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+			else
+				y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 			*dest = *(ds_transmap + (colormap[source[((y * ds_flatwidth) + x)]] << 8) + *dsrc++);
 		}
@@ -1476,25 +1500,26 @@ void R_DrawTiltedTranslucentWaterSpan_NPO2_8(void)
 		endv = vz*endz;
 		stepu = (INT64)((endu - startu) * INVSPAN);
 		stepv = (INT64)((endv - startv) * INVSPAN);
-		u = (INT64)(startu) + viewx;
-		v = (INT64)(startv) + viewy;
+		u = (INT64)(startu);
+		v = (INT64)(startv);
 
 		for (i = SPANSIZE-1; i >= 0; i--)
 		{
 			colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 			// Lactozilla: Non-powers-of-two
 			{
-				fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-				fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 				// Carefully align all of my Friends.
 				if (x < 0)
-					x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+					x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+				else
+					x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 				if (y < 0)
-					y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-				x %= ds_flatwidth;
-				y %= ds_flatheight;
+					y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+				else
+					y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 				*dest = *(ds_transmap + (colormap[source[((y * ds_flatwidth) + x)]] << 8) + *dsrc++);
 			}
@@ -1515,17 +1540,18 @@ void R_DrawTiltedTranslucentWaterSpan_NPO2_8(void)
 			colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 			// Lactozilla: Non-powers-of-two
 			{
-				fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-				fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+				fixed_t x = (((fixed_t)u) >> FRACBITS);
+				fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 				// Carefully align all of my Friends.
 				if (x < 0)
-					x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+					x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+				else
+					x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 				if (y < 0)
-					y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-				x %= ds_flatwidth;
-				y %= ds_flatheight;
+					y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+				else
+					y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 				*dest = *(ds_transmap + (colormap[source[((y * ds_flatwidth) + x)]] << 8) + *dsrc++);
 			}
@@ -1543,25 +1569,26 @@ void R_DrawTiltedTranslucentWaterSpan_NPO2_8(void)
 			left = 1.f/left;
 			stepu = (INT64)((endu - startu) * left);
 			stepv = (INT64)((endv - startv) * left);
-			u = (INT64)(startu) + viewx;
-			v = (INT64)(startv) + viewy;
+			u = (INT64)(startu);
+			v = (INT64)(startv);
 
 			for (; width != 0; width--)
 			{
 				colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 				// Lactozilla: Non-powers-of-two
 				{
-					fixed_t x = (((fixed_t)u-viewx) >> FRACBITS);
-					fixed_t y = (((fixed_t)v-viewy) >> FRACBITS);
+					fixed_t x = (((fixed_t)u) >> FRACBITS);
+					fixed_t y = (((fixed_t)v) >> FRACBITS);
 
 					// Carefully align all of my Friends.
 					if (x < 0)
-						x = ds_flatwidth - ((UINT32)(ds_flatwidth - x) % ds_flatwidth);
+						x += (libdivide_u32_do((UINT32)(-x-1), &x_divider) + 1) * ds_flatwidth;
+					else
+						x -= libdivide_u32_do((UINT32)x, &x_divider) * ds_flatwidth;
 					if (y < 0)
-						y = ds_flatheight - ((UINT32)(ds_flatheight - y) % ds_flatheight);
-
-					x %= ds_flatwidth;
-					y %= ds_flatheight;
+						y += (libdivide_u32_do((UINT32)(-y-1), &y_divider) + 1) * ds_flatheight;
+					else
+						y -= libdivide_u32_do((UINT32)y, &y_divider) * ds_flatheight;
 
 					*dest = *(ds_transmap + (colormap[source[((y * ds_flatwidth) + x)]] << 8) + *dsrc++);
 				}
diff --git a/src/r_local.h b/src/r_local.h
index 4ccb766cf72c903a952b75c85d2a1f313c42ebd5..ba78ea87dbae64b69864e8afcbd05915113e6af5 100644
--- a/src/r_local.h
+++ b/src/r_local.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_main.c b/src/r_main.c
index 165f74a7975515011d81b4ad8a0d2346b0ad4600..8729b5dcb36ccedb8aed999dbf3afcb60e0faf04 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -34,7 +34,7 @@
 #include "m_random.h" // quake camera shake
 #include "r_portal.h"
 #include "r_main.h"
-#include "i_system.h" // I_GetTimeMicros
+#include "i_system.h" // I_GetPreciseTime
 
 #ifdef HWRENDER
 #include "hardware/hw_main.h"
@@ -100,22 +100,23 @@ lighttable_t *zlight[LIGHTLEVELS][MAXLIGHTZ];
 extracolormap_t *extra_colormaps = NULL;
 
 // Render stats
-int ps_prevframetime = 0;
-int ps_rendercalltime = 0;
-int ps_uitime = 0;
-int ps_swaptime = 0;
+precise_t ps_prevframetime = 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};
 
-int ps_bsptime = 0;
+ps_metric_t ps_bsptime = {0};
 
-int ps_sw_spritecliptime = 0;
-int ps_sw_portaltime = 0;
-int ps_sw_planetime = 0;
-int 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"},
@@ -955,7 +956,7 @@ void R_ExecuteSetViewSize(void)
 		j = viewheight*16;
 		for (i = 0; i < j; i++)
 		{
-			dy = ((i - viewheight*8)<<FRACBITS) + FRACUNIT/2;
+			dy = (i - viewheight*8)<<FRACBITS;
 			dy = FixedMul(abs(dy), fovtan);
 			yslopetab[i] = FixedDiv(centerx*FRACUNIT, dy);
 		}
@@ -1089,8 +1090,6 @@ subsector_t *R_PointInSubsectorOrNull(fixed_t x, fixed_t y)
 // 18/08/18: (No it's actually 16*viewheight, thanks Jimita for finding this out)
 static void R_SetupFreelook(player_t *player, boolean skybox)
 {
-	INT32 dy = 0;
-
 #ifndef HWRENDER
 	(void)player;
 	(void)skybox;
@@ -1109,14 +1108,15 @@ static void R_SetupFreelook(player_t *player, boolean skybox)
 		G_SoftwareClipAimingPitch((INT32 *)&aimingangle);
 	}
 
+	centeryfrac = (viewheight/2)<<FRACBITS;
+
 	if (rendermode == render_soft)
-	{
-		dy = (AIMINGTODY(aimingangle)>>FRACBITS) * viewwidth/BASEVIDWIDTH;
-		yslope = &yslopetab[viewheight*8 - (viewheight/2 + dy)];
-	}
+		centeryfrac += FixedMul(AIMINGTODY(aimingangle), FixedDiv(viewwidth<<FRACBITS, BASEVIDWIDTH<<FRACBITS));
 
-	centery = (viewheight/2) + dy;
-	centeryfrac = centery<<FRACBITS;
+	centery = FixedInt(FixedRound(centeryfrac));
+
+	if (rendermode == render_soft)
+		yslope = &yslopetab[viewheight*8 - centery];
 }
 
 void R_SetupFrame(player_t *player)
@@ -1497,11 +1497,11 @@ void R_RenderPlayerView(player_t *player)
 	mytotal = 0;
 	ProfZeroTimer();
 #endif
-	ps_numbspcalls = ps_numpolyobjects = ps_numdrawnodes = 0;
-	ps_bsptime = I_GetTimeMicros();
+	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_GetTimeMicros() - ps_bsptime;
-	ps_numsprites = visspritecount;
+	PS_STOP_TIMING(ps_bsptime);
+	ps_numsprites.value.i = visspritecount;
 #ifdef TIMING
 	RDMSR(0x10, &mycount);
 	mytotal += mycount; // 64bit add
@@ -1511,9 +1511,9 @@ void R_RenderPlayerView(player_t *player)
 //profile stuff ---------------------------------------------------------
 	Mask_Post(&masks[nummasks - 1]);
 
-	ps_sw_spritecliptime = I_GetTimeMicros();
+	PS_START_TIMING(ps_sw_spritecliptime);
 	R_ClipSprites(drawsegs, NULL);
-	ps_sw_spritecliptime = I_GetTimeMicros() - ps_sw_spritecliptime;
+	PS_STOP_TIMING(ps_sw_spritecliptime);
 
 
 	// Add skybox portals caused by sky visplanes.
@@ -1521,7 +1521,7 @@ void R_RenderPlayerView(player_t *player)
 		Portal_AddSkyboxPortals();
 
 	// Portal rendering. Hijacks the BSP traversal.
-	ps_sw_portaltime = I_GetTimeMicros();
+	PS_START_TIMING(ps_sw_portaltime);
 	if (portal_base)
 	{
 		portal_t *portal;
@@ -1561,17 +1561,17 @@ void R_RenderPlayerView(player_t *player)
 			Portal_Remove(portal);
 		}
 	}
-	ps_sw_portaltime = I_GetTimeMicros() - ps_sw_portaltime;
+	PS_STOP_TIMING(ps_sw_portaltime);
 
-	ps_sw_planetime = I_GetTimeMicros();
+	PS_START_TIMING(ps_sw_planetime);
 	R_DrawPlanes();
-	ps_sw_planetime = I_GetTimeMicros() - ps_sw_planetime;
+	PS_STOP_TIMING(ps_sw_planetime);
 
 	// draw mid texture and sprite
 	// And now 3D floors/sides!
-	ps_sw_maskedtime = I_GetTimeMicros();
+	PS_START_TIMING(ps_sw_maskedtime);
 	R_DrawMasked(masks, nummasks);
-	ps_sw_maskedtime = I_GetTimeMicros() - ps_sw_maskedtime;
+	PS_STOP_TIMING(ps_sw_maskedtime);
 
 	free(masks);
 }
diff --git a/src/r_main.h b/src/r_main.h
index f1cc9621f00f320ffdbb363b08b783af89fda75b..5f3bed9803102cb7592ac755ef09f30870cc3f9f 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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.
@@ -78,22 +79,23 @@ boolean R_DoCulling(line_t *cullheight, line_t *viewcullheight, fixed_t vz, fixe
 
 // Render stats
 
-extern int ps_prevframetime;// time when previous frame was rendered
-extern int ps_rendercalltime;
-extern int ps_uitime;
-extern int ps_swaptime;
+extern precise_t ps_prevframetime;// time when previous frame was rendered
+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 int ps_bsptime;
+extern ps_metric_t ps_bsptime;
 
-extern int ps_sw_spritecliptime;
-extern int ps_sw_portaltime;
-extern int ps_sw_planetime;
-extern int ps_sw_maskedtime;
+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 int ps_numbspcalls;
-extern int ps_numsprites;
-extern int ps_numdrawnodes;
-extern int ps_numpolyobjects;
+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_patch.c b/src/r_patch.c
index 1a08d1892d5e13d6b36ebfe59945bce9a58b75f6..6827cd12c75d397fdaa771f3373c5d4812a83952 100644
--- a/src/r_patch.c
+++ b/src/r_patch.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020 by Jaime "Lactozilla" Passos.
+// Copyright (C) 2020-2021 by Jaime "Lactozilla" Passos.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_patch.h b/src/r_patch.h
index 32bcb3909efe057af98d54cd151f56414c71deb1..96fbb0e28c11d3bbd91f84edc999a7d6d5223c70 100644
--- a/src/r_patch.h
+++ b/src/r_patch.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020 by Jaime "Lactozilla" Passos.
+// Copyright (C) 2020-2021 by Jaime "Lactozilla" Passos.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_patchrotation.c b/src/r_patchrotation.c
index 123c4eef229a20fa554094bf44a2cc3853e72dc8..dae3a7b53a6cab88c151e7d1605d99fc5972c321 100644
--- a/src/r_patchrotation.c
+++ b/src/r_patchrotation.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020 by Jaime "Lactozilla" Passos.
+// Copyright (C) 2020-2021 by Jaime "Lactozilla" Passos.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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_patchrotation.h b/src/r_patchrotation.h
index 2744f71d25380469b30b1fdcf8b5112578a2abd8..689b7d411b63d0143804d789da0501fb1f0d1415 100644
--- a/src/r_patchrotation.h
+++ b/src/r_patchrotation.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020 by Jaime "Lactozilla" Passos.
+// Copyright (C) 2020-2021 by Jaime "Lactozilla" Passos.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_picformats.c b/src/r_picformats.c
index 02f1de4ab20d1f0edaeb8753459eacc8c452de23..5c81d1e02186902818415c84088fcfc182235681 100644
--- a/src/r_picformats.c
+++ b/src/r_picformats.c
@@ -2,8 +2,8 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 2005-2009 by Andrey "entryway" Budko.
-// Copyright (C) 2018-2020 by Jaime "Lactozilla" Passos.
-// Copyright (C) 2019-2020 by Sonic Team Junior.
+// Copyright (C) 2018-2021 by Jaime "Lactozilla" Passos.
+// Copyright (C) 2019-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -540,55 +540,60 @@ void *Picture_GetPatchPixel(
 {
 	fixed_t ofs;
 	column_t *column;
-	UINT8 *s8 = NULL;
-	UINT16 *s16 = NULL;
-	UINT32 *s32 = NULL;
+	INT32 inbpp = Picture_FormatBPP(informat);
 	softwarepatch_t *doompatch = (softwarepatch_t *)patch;
+	boolean isdoompatch = Picture_IsDoomPatchFormat(informat);
 	INT16 width;
 
 	if (patch == NULL)
 		I_Error("Picture_GetPatchPixel: patch == NULL");
 
-	width = (Picture_IsDoomPatchFormat(informat) ? patch->width : SHORT(patch->width));
+	width = (isdoompatch ? SHORT(doompatch->width) : patch->width);
 
 	if (x >= 0 && x < width)
 	{
 		INT32 colx = (flags & PICFLAGS_XFLIP) ? (width-1)-x : x;
 		INT32 topdelta, prevdelta = -1;
-		INT32 colofs = (Picture_IsDoomPatchFormat(informat) ? LONG(patch->columnofs[colx]) : patch->columnofs[colx]);
+		INT32 colofs = (isdoompatch ? LONG(doompatch->columnofs[colx]) : patch->columnofs[colx]);
 
-		// Column offsets are pointers so no casting required
-		if (Picture_IsDoomPatchFormat(informat))
+		// Column offsets are pointers, so no casting is required.
+		if (isdoompatch)
 			column = (column_t *)((UINT8 *)doompatch + colofs);
 		else
 			column = (column_t *)((UINT8 *)patch->columns + colofs);
 
 		while (column->topdelta != 0xff)
 		{
+			UINT8 *s8 = NULL;
+			UINT16 *s16 = NULL;
+			UINT32 *s32 = NULL;
+
 			topdelta = column->topdelta;
 			if (topdelta <= prevdelta)
 				topdelta += prevdelta;
 			prevdelta = topdelta;
-			s8 = (UINT8 *)(column) + 3;
-			if (Picture_FormatBPP(informat) == PICDEPTH_32BPP)
-				s32 = (UINT32 *)s8;
-			else if (Picture_FormatBPP(informat) == PICDEPTH_16BPP)
-				s16 = (UINT16 *)s8;
-			for (ofs = 0; ofs < column->length; ofs++)
+
+			ofs = (y - topdelta);
+
+			if (y >= topdelta && ofs < column->length)
 			{
-				if ((topdelta + ofs) == y)
+				s8 = (UINT8 *)(column) + 3;
+				switch (inbpp)
 				{
-					if (Picture_FormatBPP(informat) == PICDEPTH_32BPP)
+					case PICDEPTH_32BPP:
+						s32 = (UINT32 *)s8;
 						return &s32[ofs];
-					else if (Picture_FormatBPP(informat) == PICDEPTH_16BPP)
+					case PICDEPTH_16BPP:
+						s16 = (UINT16 *)s8;
 						return &s16[ofs];
-					else // PICDEPTH_8BPP
+					default: // PICDEPTH_8BPP
 						return &s8[ofs];
 				}
 			}
-			if (Picture_FormatBPP(informat) == PICDEPTH_32BPP)
+
+			if (inbpp == PICDEPTH_32BPP)
 				column = (column_t *)((UINT32 *)column + column->length);
-			else if (Picture_FormatBPP(informat) == PICDEPTH_16BPP)
+			else if (inbpp == PICDEPTH_16BPP)
 				column = (column_t *)((UINT16 *)column + column->length);
 			else
 				column = (column_t *)((UINT8 *)column + column->length);
@@ -896,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
@@ -978,8 +982,8 @@ static png_bytep *PNG_Read(
 
 				for (i = 0; i < 256; i++)
 				{
-					UINT32 rgb = R_PutRgbaRGBA(pal->red, pal->green, pal->blue, 0xFF);
-					if (rgb != pMasterPalette[i].rgba)
+					byteColor_t *curpal = &(pMasterPalette[i].s);
+					if (pal->red != curpal->red || pal->green != curpal->green || pal->blue != curpal->blue)
 					{
 						usepal = false;
 						break;
@@ -993,14 +997,14 @@ 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 == 256)
+			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 RGB even if
+					// libpng will transform this image into RGBA even if
 					// the transparent index does not exist in the image,
 					// and there is no way around that.
 					if (trans[i] < 0xFF)
diff --git a/src/r_picformats.h b/src/r_picformats.h
index 8d3999013475f23b9428e0e252148d91c88c8ea2..c74f8a13a60a2c2602d656c5dad6920360f87d9c 100644
--- a/src/r_picformats.h
+++ b/src/r_picformats.h
@@ -1,8 +1,8 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
-// Copyright (C) 2018-2020 by Jaime "Lactozilla" Passos.
-// Copyright (C) 2019-2020 by Sonic Team Junior.
+// Copyright (C) 2018-2021 by Jaime "Lactozilla" Passos.
+// Copyright (C) 2019-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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 c54b32382eb178f86b8d19a5a36d6dbe07189a39..45719ce58c502d0bbc9ec5820d31d500b5d76022 100644
--- a/src/r_plane.c
+++ b/src/r_plane.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -31,13 +31,6 @@
 #include "z_zone.h"
 #include "p_tick.h"
 
-#ifdef TIMING
-#include "p5prof.h"
-	INT64 mycount;
-	INT64 mytotal = 0;
-	UINT32 nombre = 100000;
-#endif
-
 //
 // opening
 //
@@ -96,14 +89,13 @@ 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];
 fixed_t cachedystep[MAXVIDHEIGHT];
 
 static fixed_t xoffs, yoffs;
+static floatv3_t ds_slope_origin, ds_slope_u, ds_slope_v;
 
 //
 // R_InitPlanes
@@ -120,28 +112,27 @@ 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;
 	boolean active;
 } planeripple;
 
-static void R_CalculatePlaneRipple(visplane_t *plane, INT32 y, fixed_t plheight, boolean calcfrac)
+// ripples da water texture
+static fixed_t R_CalculateRippleOffset(INT32 y)
 {
-	fixed_t distance = FixedMul(plheight, yslope[y]);
+	fixed_t distance = FixedMul(planeheight, yslope[y]);
 	const INT32 yay = (planeripple.offset + (distance>>9)) & 8191;
+	return FixedDiv(FINESINE(yay), (1<<12) + (distance>>11));
+}
 
-	// ripples da water texture
-	ds_bgofs = FixedDiv(FINESINE(yay), (1<<12) + (distance>>11))>>FRACBITS;
-
-	if (calcfrac)
-	{
-		angle_t angle = (plane->viewangle + plane->plangle)>>ANGLETOFINESHIFT;
-		angle = (angle + 2048) & 8191; // 90 degrees
-		planeripple.xfrac = FixedMul(FINECOSINE(angle), (ds_bgofs<<FRACBITS));
-		planeripple.yfrac = FixedMul(FINESINE(angle), (ds_bgofs<<FRACBITS));
-	}
+static void R_CalculatePlaneRipple(angle_t angle)
+{
+	angle >>= ANGLETOFINESHIFT;
+	angle = (angle + 2048) & 8191; // 90 degrees
+	planeripple.xfrac = FixedMul(FINECOSINE(angle), ds_bgofs);
+	planeripple.yfrac = FixedMul(FINESINE(angle), ds_bgofs);
 }
 
 static void R_UpdatePlaneRipple(void)
@@ -150,20 +141,7 @@ static void R_UpdatePlaneRipple(void)
 	planeripple.offset = (leveltime * 140);
 }
 
-//
-// R_MapPlane
-//
-// Uses global vars:
-//  basexscale
-//  baseyscale
-//  centerx
-//  viewx
-//  viewy
-//  viewsin
-//  viewcos
-//  viewheight
-
-void R_MapPlane(INT32 y, INT32 x1, INT32 x2)
+static void R_MapPlane(INT32 y, INT32 x1, INT32 x2)
 {
 	angle_t angle, planecos, planesin;
 	fixed_t distance = 0, span;
@@ -177,60 +155,52 @@ void R_MapPlane(INT32 y, INT32 x1, INT32 x2)
 	if (x1 >= vid.width)
 		x1 = vid.width - 1;
 
-	if (!currentplane->slope)
+	angle = (currentplane->viewangle + currentplane->plangle)>>ANGLETOFINESHIFT;
+	planecos = FINECOSINE(angle);
+	planesin = FINESINE(angle);
+
+	if (planeheight != cachedheight[y])
 	{
-		angle = (currentplane->viewangle + currentplane->plangle)>>ANGLETOFINESHIFT;
-		planecos = FINECOSINE(angle);
-		planesin = FINESINE(angle);
+		cachedheight[y] = planeheight;
+		cacheddistance[y] = distance = FixedMul(planeheight, yslope[y]);
+		span = abs(centery - y);
 
-		if (planeheight != cachedheight[y])
+		if (span) // Don't divide by zero
 		{
-			cachedheight[y] = planeheight;
-			cacheddistance[y] = distance = FixedMul(planeheight, yslope[y]);
-			span = abs(centery - y);
-
-			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);
-			}
-
-			cachedxstep[y] = ds_xstep;
-			cachedystep[y] = ds_ystep;
+			ds_xstep = FixedMul(planesin, planeheight) / span;
+			ds_ystep = FixedMul(planecos, planeheight) / span;
 		}
 		else
-		{
-			distance = cacheddistance[y];
-			ds_xstep = cachedxstep[y];
-			ds_ystep = cachedystep[y];
-		}
+			ds_xstep = ds_ystep = FRACUNIT;
 
-		ds_xfrac = xoffs + FixedMul(planecos, distance) + (x1 - centerx) * ds_xstep;
-		ds_yfrac = yoffs - FixedMul(planesin, distance) + (x1 - centerx) * ds_ystep;
+		cachedxstep[y] = ds_xstep;
+		cachedystep[y] = ds_ystep;
 	}
+	else
+	{
+		distance = cacheddistance[y];
+		ds_xstep = cachedxstep[y];
+		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;
 
 	// Water ripple effect
 	if (planeripple.active)
 	{
-		// Needed for ds_bgofs
-		R_CalculatePlaneRipple(currentplane, y, planeheight, (!currentplane->slope));
+		ds_bgofs = R_CalculateRippleOffset(y);
 
-		if (currentplane->slope)
-		{
-			ds_sup = &ds_su[y];
-			ds_svp = &ds_sv[y];
-			ds_szp = &ds_sz[y];
-		}
-		else
-		{
-			ds_xfrac += planeripple.xfrac;
-			ds_yfrac += planeripple.yfrac;
-		}
+		R_CalculatePlaneRipple(currentplane->viewangle + currentplane->plangle);
+
+		ds_xfrac += planeripple.xfrac;
+		ds_yfrac += planeripple.yfrac;
+		ds_bgofs >>= FRACBITS;
 
 		if ((y + ds_bgofs) >= viewheight)
 			ds_bgofs = viewheight-y-1;
@@ -238,16 +208,11 @@ void R_MapPlane(INT32 y, INT32 x1, INT32 x2)
 			ds_bgofs = -y;
 	}
 
-	if (currentplane->slope)
-		ds_colormap = colormaps;
-	else
-	{
-		pindex = distance >> LIGHTZSHIFT;
-		if (pindex >= MAXLIGHTZ)
-			pindex = MAXLIGHTZ - 1;
-		ds_colormap = planezlight[pindex];
-	}
+	pindex = distance >> LIGHTZSHIFT;
+	if (pindex >= MAXLIGHTZ)
+		pindex = MAXLIGHTZ - 1;
 
+	ds_colormap = planezlight[pindex];
 	if (currentplane->extra_colormap)
 		ds_colormap = currentplane->extra_colormap->colormap + (ds_colormap - colormaps);
 
@@ -255,19 +220,46 @@ void R_MapPlane(INT32 y, INT32 x1, INT32 x2)
 	ds_x1 = x1;
 	ds_x2 = x2;
 
-	// profile drawer
-#ifdef TIMING
-	ProfZeroTimer();
-#endif
-
 	spanfunc();
+}
 
-#ifdef TIMING
-	RDMSR(0x10, &mycount);
-	mytotal += mycount; // 64bit add
-	if (!(nombre--))
-	I_Error("spanfunc() CPU Spy reports: 0x%d %d\n", *((INT32 *)&mytotal+1), (INT32)mytotal);
+static void R_MapTiltedPlane(INT32 y, INT32 x1, INT32 x2)
+{
+#ifdef RANGECHECK
+	if (x2 < x1 || x1 < 0 || x2 >= viewwidth || y > viewheight)
+		I_Error("R_MapTiltedPlane: %d, %d at %d", x1, x2, y);
 #endif
+
+	if (x1 >= vid.width)
+		x1 = vid.width - 1;
+
+	// Water ripple effect
+	if (planeripple.active)
+	{
+		ds_bgofs = R_CalculateRippleOffset(y);
+
+		ds_sup = &ds_su[y];
+		ds_svp = &ds_sv[y];
+		ds_szp = &ds_sz[y];
+
+		ds_bgofs >>= FRACBITS;
+
+		if ((y + ds_bgofs) >= viewheight)
+			ds_bgofs = viewheight-y-1;
+		if ((y + ds_bgofs) < 0)
+			ds_bgofs = -y;
+	}
+
+	if (currentplane->extra_colormap)
+		ds_colormap = currentplane->extra_colormap->colormap;
+	else
+		ds_colormap = colormaps;
+
+	ds_y = y;
+	ds_x1 = x1;
+	ds_x2 = x2;
+
+	spanfunc();
 }
 
 void R_ClearFFloorClips (void)
@@ -294,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++)
@@ -320,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)
@@ -334,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
@@ -367,11 +351,11 @@ visplane_t *R_FindPlane(fixed_t height, INT32 picnum, INT32 lightlevel,
 		if (plangle != 0)
 		{
 			// Add the view offset, rotated by the plane angle.
-			fixed_t cosinecomponent = FINECOSINE(plangle>>ANGLETOFINESHIFT);
-			fixed_t sinecomponent = FINESINE(plangle>>ANGLETOFINESHIFT);
-			fixed_t oldxoff = xoff;
-			xoff = FixedMul(xoff,cosinecomponent)+FixedMul(yoff,sinecomponent);
-			yoff = -FixedMul(oldxoff,sinecomponent)+FixedMul(yoff,cosinecomponent);
+			float ang = ANG2RAD(plangle);
+			float x = FixedToFloat(xoff);
+			float y = FixedToFloat(yoff);
+			xoff = FloatToFixed(x * cos(ang) + y * sin(ang));
+			yoff = FloatToFixed(-x * sin(ang) + y * cos(ang));
 		}
 	}
 
@@ -379,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
 		{
@@ -529,58 +515,24 @@ 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;
-*/
-
 }
 
-//
-// R_MakeSpans
-//
-void R_MakeSpans(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_RenderFloorSplat
+	//    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;
@@ -589,12 +541,12 @@ void R_MakeSpans(INT32 x, INT32 t1, INT32 b1, INT32 t2, INT32 b2)
 
 	while (t1 < t2 && t1 <= b1)
 	{
-		R_MapPlane(t1, spanstart[t1], x - 1);
+		mapfunc(t1, spanstart[t1], x - 1);
 		t1++;
 	}
 	while (b1 > b2 && b1 >= t1)
 	{
-		R_MapPlane(b1, spanstart[b1], x - 1);
+		mapfunc(b1, spanstart[b1], x - 1);
 		b1--;
 	}
 
@@ -666,69 +618,109 @@ static void R_DrawSkyPlane(visplane_t *pl)
 	}
 }
 
-// Potentially override other stuff for now cus we're mean. :< But draw a slope plane!
-// I copied ZDoom's code and adapted it to SRB2... -Red
-void R_CalculateSlopeVectors(pslope_t *slope, fixed_t planeviewx, fixed_t planeviewy, fixed_t planeviewz, fixed_t planexscale, fixed_t planeyscale, fixed_t planexoffset, fixed_t planeyoffset, angle_t planeviewangle, angle_t planeangle, float fudge)
+// Returns the height of the sloped plane at (x, y) as a 32.16 fixed_t
+static INT64 R_GetSlopeZAt(const pslope_t *slope, fixed_t x, fixed_t y)
 {
-	floatv3_t p, m, n;
-	float ang;
-	float vx, vy, vz;
-	float xscale = FIXED_TO_FLOAT(planexscale);
-	float yscale = FIXED_TO_FLOAT(planeyscale);
-	// compiler complains when P_GetSlopeZAt is used in FLOAT_TO_FIXED directly
-	// use this as a temp var to store P_GetSlopeZAt's return value each time
-	fixed_t temp;
+	INT64 x64 = ((INT64)x - (INT64)slope->o.x);
+	INT64 y64 = ((INT64)y - (INT64)slope->o.y);
+
+	x64 = (x64 * (INT64)slope->d.x) / FRACUNIT;
+	y64 = (y64 * (INT64)slope->d.y) / FRACUNIT;
+
+	return (INT64)slope->o.z + ((x64 + y64) * (INT64)slope->zdelta) / FRACUNIT;
+}
+
+// Sets the texture origin vector of the sloped plane.
+static void R_SetSlopePlaneOrigin(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, fixed_t xoff, fixed_t yoff, fixed_t angle)
+{
+	floatv3_t *p = &ds_slope_origin;
 
-	vx = FIXED_TO_FLOAT(planeviewx+planexoffset);
-	vy = FIXED_TO_FLOAT(planeviewy-planeyoffset);
-	vz = FIXED_TO_FLOAT(planeviewz);
+	INT64 vx = (INT64)xpos + (INT64)xoff;
+	INT64 vy = (INT64)ypos - (INT64)yoff;
 
-	temp = P_GetSlopeZAt(slope, planeviewx, planeviewy);
-	zeroheight = FIXED_TO_FLOAT(temp);
+	float vxf = vx / (float)FRACUNIT;
+	float vyf = vy / (float)FRACUNIT;
+	float ang = ANG2RAD(ANGLE_270 - angle);
 
 	// p is the texture origin in view space
 	// Don't add in the offsets at this stage, because doing so can result in
 	// errors if the flat is rotated.
-	ang = ANG2RAD(ANGLE_270 - planeviewangle);
-	p.x = vx * cos(ang) - vy * sin(ang);
-	p.z = vx * sin(ang) + vy * cos(ang);
-	temp = P_GetSlopeZAt(slope, -planexoffset, planeyoffset);
-	p.y = FIXED_TO_FLOAT(temp) - vz;
+	p->x = vxf * cos(ang) - vyf * sin(ang);
+	p->z = vxf * sin(ang) + vyf * cos(ang);
+	p->y = (R_GetSlopeZAt(slope, -xoff, yoff) - zpos) / (float)FRACUNIT;
+}
+
+// This function calculates all of the vectors necessary for drawing a sloped plane.
+void R_SetSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, fixed_t xoff, fixed_t yoff, angle_t angle, angle_t plangle)
+{
+	// Potentially override other stuff for now cus we're mean. :< But draw a slope plane!
+	// I copied ZDoom's code and adapted it to SRB2... -Red
+	floatv3_t *m = &ds_slope_v, *n = &ds_slope_u;
+	fixed_t height, temp;
+	float ang;
+
+	R_SetSlopePlaneOrigin(slope, xpos, ypos, zpos, xoff, yoff, angle);
+	height = P_GetSlopeZAt(slope, xpos, ypos);
+	zeroheight = FixedToFloat(height - zpos);
 
 	// m is the v direction vector in view space
-	ang = ANG2RAD(ANGLE_180 - (planeviewangle + planeangle));
-	m.x = yscale * cos(ang);
-	m.z = yscale * sin(ang);
+	ang = ANG2RAD(ANGLE_180 - (angle + plangle));
+	m->x = cos(ang);
+	m->z = sin(ang);
 
 	// n is the u direction vector in view space
-	n.x = xscale * sin(ang);
-	n.z = -xscale * cos(ang);
+	n->x = sin(ang);
+	n->z = -cos(ang);
+
+	plangle >>= ANGLETOFINESHIFT;
+	temp = P_GetSlopeZAt(slope, xpos + FINESINE(plangle), ypos + FINECOSINE(plangle));
+	m->y = FixedToFloat(temp - height);
+	temp = P_GetSlopeZAt(slope, xpos + FINECOSINE(plangle), ypos - FINESINE(plangle));
+	n->y = FixedToFloat(temp - height);
+}
 
-	ang = ANG2RAD(planeangle);
-	temp = P_GetSlopeZAt(slope, planeviewx + yscale * FLOAT_TO_FIXED(sin(ang)), planeviewy + yscale * FLOAT_TO_FIXED(cos(ang)));
-	m.y = FIXED_TO_FLOAT(temp) - zeroheight;
-	temp = P_GetSlopeZAt(slope, planeviewx + xscale * FLOAT_TO_FIXED(cos(ang)), planeviewy - xscale * FLOAT_TO_FIXED(sin(ang)));
-	n.y = FIXED_TO_FLOAT(temp) - zeroheight;
+// This function calculates all of the vectors necessary for drawing a sloped and scaled plane.
+void R_SetScaledSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, fixed_t xs, fixed_t ys, fixed_t xoff, fixed_t yoff, angle_t angle, angle_t plangle)
+{
+	floatv3_t *m = &ds_slope_v, *n = &ds_slope_u;
+	fixed_t height, temp;
 
-	if (ds_powersoftwo)
-	{
-		m.x /= fudge;
-		m.y /= fudge;
-		m.z /= fudge;
+	float xscale = FixedToFloat(xs);
+	float yscale = FixedToFloat(ys);
+	float ang;
 
-		n.x *= fudge;
-		n.y *= fudge;
-		n.z *= fudge;
-	}
+	R_SetSlopePlaneOrigin(slope, xpos, ypos, zpos, xoff, yoff, angle);
+	height = P_GetSlopeZAt(slope, xpos, ypos);
+	zeroheight = FixedToFloat(height - zpos);
+
+	// m is the v direction vector in view space
+	ang = ANG2RAD(ANGLE_180 - (angle + plangle));
+	m->x = yscale * cos(ang);
+	m->z = yscale * sin(ang);
+
+	// n is the u direction vector in view space
+	n->x = xscale * sin(ang);
+	n->z = -xscale * cos(ang);
+
+	ang = ANG2RAD(plangle);
+	temp = P_GetSlopeZAt(slope, xpos + FloatToFixed(yscale * sin(ang)), ypos + FloatToFixed(yscale * cos(ang)));
+	m->y = FixedToFloat(temp - height);
+	temp = P_GetSlopeZAt(slope, xpos + FloatToFixed(xscale * cos(ang)), ypos - FloatToFixed(xscale * sin(ang)));
+	n->y = FixedToFloat(temp - height);
+}
+
+void R_CalculateSlopeVectors(void)
+{
+	float sfmult = 65536.f;
 
 	// Eh. I tried making this stuff fixed-point and it exploded on me. Here's a macro for the only floating-point vector function I recall using.
 #define CROSS(d, v1, v2) \
 d->x = (v1.y * v2.z) - (v1.z * v2.y);\
 d->y = (v1.z * v2.x) - (v1.x * v2.z);\
 d->z = (v1.x * v2.y) - (v1.y * v2.x)
-		CROSS(ds_sup, p, m);
-		CROSS(ds_svp, p, n);
-		CROSS(ds_szp, m, n);
+	CROSS(ds_sup, ds_slope_origin, ds_slope_v);
+	CROSS(ds_svp, ds_slope_origin, ds_slope_u);
+	CROSS(ds_szp, ds_slope_v, ds_slope_u);
 #undef CROSS
 
 	ds_sup->z *= focallengthf;
@@ -736,27 +728,15 @@ d->z = (v1.x * v2.y) - (v1.y * v2.x)
 	ds_szp->z *= focallengthf;
 
 	// Premultiply the texture vectors with the scale factors
-#define SFMULT 65536.f
 	if (ds_powersoftwo)
-	{
-		ds_sup->x *= (SFMULT * (1<<nflatshiftup));
-		ds_sup->y *= (SFMULT * (1<<nflatshiftup));
-		ds_sup->z *= (SFMULT * (1<<nflatshiftup));
-		ds_svp->x *= (SFMULT * (1<<nflatshiftup));
-		ds_svp->y *= (SFMULT * (1<<nflatshiftup));
-		ds_svp->z *= (SFMULT * (1<<nflatshiftup));
-	}
-	else
-	{
-		// Lactozilla: I'm essentially multiplying the vectors by FRACUNIT...
-		ds_sup->x *= SFMULT;
-		ds_sup->y *= SFMULT;
-		ds_sup->z *= SFMULT;
-		ds_svp->x *= SFMULT;
-		ds_svp->y *= SFMULT;
-		ds_svp->z *= SFMULT;
-	}
-#undef SFMULT
+		sfmult *= (1 << nflatshiftup);
+
+	ds_sup->x *= sfmult;
+	ds_sup->y *= sfmult;
+	ds_sup->z *= sfmult;
+	ds_svp->x *= sfmult;
+	ds_svp->y *= sfmult;
+	ds_svp->z *= sfmult;
 }
 
 void R_SetTiltedSpan(INT32 span)
@@ -773,22 +753,50 @@ void R_SetTiltedSpan(INT32 span)
 	ds_szp = &ds_sz[span];
 }
 
-static void R_SetSlopePlaneVectors(visplane_t *pl, INT32 y, fixed_t xoff, fixed_t yoff, float fudge)
+static void R_SetSlopePlaneVectors(visplane_t *pl, INT32 y, fixed_t xoff, fixed_t yoff)
 {
 	R_SetTiltedSpan(y);
-	R_CalculateSlopeVectors(pl->slope, pl->viewx, pl->viewy, pl->viewz, FRACUNIT, FRACUNIT, xoff, yoff, pl->viewangle, pl->plangle, fudge);
+	R_SetSlopePlane(pl->slope, pl->viewx, pl->viewy, pl->viewz, xoff, yoff, pl->viewangle, pl->plangle);
+	R_CalculateSlopeVectors();
+}
+
+static inline void R_AdjustSlopeCoordinates(vector3_t *origin)
+{
+	const fixed_t modmask = ((1 << (32-nflatshiftup)) - 1);
+
+	fixed_t ox = (origin->x & modmask);
+	fixed_t oy = -(origin->y & modmask);
+
+	xoffs &= modmask;
+	yoffs &= modmask;
+
+	xoffs -= (origin->x - ox);
+	yoffs += (origin->y + oy);
+}
+
+static inline void R_AdjustSlopeCoordinatesNPO2(vector3_t *origin)
+{
+	const fixed_t modmaskw = (ds_flatwidth << FRACBITS);
+	const fixed_t modmaskh = (ds_flatheight << FRACBITS);
+
+	fixed_t ox = (origin->x % modmaskw);
+	fixed_t oy = -(origin->y % modmaskh);
+
+	xoffs %= modmaskw;
+	yoffs %= modmaskh;
+
+	xoffs -= (origin->x - ox);
+	yoffs += (origin->y + oy);
 }
 
 void R_DrawSinglePlane(visplane_t *pl)
 {
 	levelflat_t *levelflat;
 	INT32 light = 0;
-	INT32 x;
-	INT32 stop, angle;
+	INT32 x, stop;
 	ffloor_t *rover;
-	int type;
-	int spanfunctype = BASEDRAWFUNC;
-	angle_t viewang = viewangle;
+	INT32 type, spanfunctype = BASEDRAWFUNC;
+	void (*mapfunc)(INT32, INT32, INT32) = R_MapPlane;
 
 	if (!(pl->minx <= pl->maxx))
 		return;
@@ -940,15 +948,11 @@ 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;
 	}
 
 	xoffs = pl->xoffs;
 	yoffs = pl->yoffs;
-	planeheight = abs(pl->height - pl->viewz);
 
 	if (light >= LIGHTLEVELS)
 		light = LIGHTLEVELS-1;
@@ -958,76 +962,31 @@ void R_DrawSinglePlane(visplane_t *pl)
 
 	if (pl->slope)
 	{
-		float fudgecanyon = 0;
-		angle_t hack = (pl->plangle & (ANGLE_90-1));
-
-		yoffs *= 1;
+		mapfunc = R_MapTiltedPlane;
 
-		if (ds_powersoftwo)
+		if (!pl->plangle)
 		{
-			fixed_t temp;
-			// Okay, look, don't ask me why this works, but without this setup there's a disgusting-looking misalignment with the textures. -Red
-			fudgecanyon = ((1<<nflatshiftup)+1.0f)/(1<<nflatshiftup);
-			if (hack)
-			{
-				/*
-				Essentially: We can't & the components along the regular axes when the plane is rotated.
-				This is because the distance on each regular axis in order to loop is different.
-				We rotate them, & the components, add them together, & them again, and then rotate them back.
-				These three seperate & operations are done per axis in order to prevent overflows.
-				toast 10/04/17
-				*/
-				const fixed_t cosinecomponent = FINECOSINE(hack>>ANGLETOFINESHIFT);
-				const fixed_t sinecomponent = FINESINE(hack>>ANGLETOFINESHIFT);
-
-				const fixed_t modmask = ((1 << (32-nflatshiftup)) - 1);
-
-				fixed_t ox = (FixedMul(pl->slope->o.x,cosinecomponent) & modmask) - (FixedMul(pl->slope->o.y,sinecomponent) & modmask);
-				fixed_t oy = (-FixedMul(pl->slope->o.x,sinecomponent) & modmask) - (FixedMul(pl->slope->o.y,cosinecomponent) & modmask);
-
-				temp = ox & modmask;
-				oy &= modmask;
-				ox = FixedMul(temp,cosinecomponent)+FixedMul(oy,-sinecomponent); // negative sine for opposite direction
-				oy = -FixedMul(temp,-sinecomponent)+FixedMul(oy,cosinecomponent);
-
-				temp = xoffs;
-				xoffs = (FixedMul(temp,cosinecomponent) & modmask) + (FixedMul(yoffs,sinecomponent) & modmask);
-				yoffs = (-FixedMul(temp,sinecomponent) & modmask) + (FixedMul(yoffs,cosinecomponent) & modmask);
-
-				temp = xoffs & modmask;
-				yoffs &= modmask;
-				xoffs = FixedMul(temp,cosinecomponent)+FixedMul(yoffs,-sinecomponent); // ditto
-				yoffs = -FixedMul(temp,-sinecomponent)+FixedMul(yoffs,cosinecomponent);
-
-				xoffs -= (pl->slope->o.x - ox);
-				yoffs += (pl->slope->o.y + oy);
-			}
+			if (ds_powersoftwo)
+				R_AdjustSlopeCoordinates(&pl->slope->o);
 			else
-			{
-				xoffs &= ((1 << (32-nflatshiftup))-1);
-				yoffs &= ((1 << (32-nflatshiftup))-1);
-				xoffs -= (pl->slope->o.x + (1 << (31-nflatshiftup))) & ~((1 << (32-nflatshiftup))-1);
-				yoffs += (pl->slope->o.y + (1 << (31-nflatshiftup))) & ~((1 << (32-nflatshiftup))-1);
-			}
-
-			xoffs = (fixed_t)(xoffs*fudgecanyon);
-			yoffs = (fixed_t)(yoffs/fudgecanyon);
+				R_AdjustSlopeCoordinatesNPO2(&pl->slope->o);
 		}
 
 		if (planeripple.active)
 		{
-			fixed_t plheight = abs(P_GetSlopeZAt(pl->slope, pl->viewx, pl->viewy) - pl->viewz);
+			planeheight = abs(P_GetSlopeZAt(pl->slope, pl->viewx, pl->viewy) - pl->viewz);
 
 			R_PlaneBounds(pl);
 
 			for (x = pl->high; x < pl->low; x++)
 			{
-				R_CalculatePlaneRipple(pl, x, plheight, true);
-				R_SetSlopePlaneVectors(pl, x, (xoffs + planeripple.xfrac), (yoffs + planeripple.yfrac), fudgecanyon);
+				ds_bgofs = R_CalculateRippleOffset(x);
+				R_CalculatePlaneRipple(pl->viewangle + pl->plangle);
+				R_SetSlopePlaneVectors(pl, x, (xoffs + planeripple.xfrac), (yoffs + planeripple.yfrac));
 			}
 		}
 		else
-			R_SetSlopePlaneVectors(pl, 0, xoffs, yoffs, fudgecanyon);
+			R_SetSlopePlaneVectors(pl, 0, xoffs, yoffs);
 
 		switch (spanfunctype)
 		{
@@ -1048,7 +1007,10 @@ void R_DrawSinglePlane(visplane_t *pl)
 		planezlight = scalelight[light];
 	}
 	else
+	{
+		planeheight = abs(pl->height - pl->viewz);
 		planezlight = zlight[light];
+	}
 
 	// Use the correct span drawer depending on the powers-of-twoness
 	if (!ds_powersoftwo)
@@ -1069,19 +1031,8 @@ void R_DrawSinglePlane(visplane_t *pl)
 
 	stop = pl->maxx + 1;
 
-	if (viewx != pl->viewx || viewy != pl->viewy)
-	{
-		viewx = pl->viewx;
-		viewy = pl->viewy;
-	}
-	if (viewz != pl->viewz)
-		viewz = pl->viewz;
-
 	for (x = pl->minx; x <= stop; x++)
-	{
-		R_MakeSpans(x, pl->top[x-1], pl->bottom[x-1],
-			pl->top[x], pl->bottom[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)
@@ -1148,13 +1099,11 @@ 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]);
 		}
 	}
 #endif
-
-	viewangle = viewang;
 }
 
 void R_PlaneBounds(visplane_t *plane)
diff --git a/src/r_plane.h b/src/r_plane.h
index 0d11c5b721c2ffadcaee26f4fbd830a6b2698c0a..862b95069ddacdd85382ef7f55c476a592b4177b 100644
--- a/src/r_plane.h
+++ b/src/r_plane.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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;
@@ -78,8 +77,6 @@ void R_InitPlanes(void);
 void R_ClearPlanes(void);
 void R_ClearFFloorClips (void);
 
-void R_MapPlane(INT32 y, INT32 x1, INT32 x2);
-void R_MakeSpans(INT32 x, INT32 t1, INT32 b1, INT32 t2, INT32 b2);
 void R_DrawPlanes(void);
 visplane_t *R_FindPlane(fixed_t height, INT32 picnum, INT32 lightlevel, fixed_t xoff, fixed_t yoff, angle_t plangle,
 	extracolormap_t *planecolormap, ffloor_t *ffloor, polyobj_t *polyobj, pslope_t *slope);
@@ -94,7 +91,9 @@ boolean R_CheckPowersOfTwo(void);
 void R_DrawSinglePlane(visplane_t *pl);
 
 // Calculates the slope vectors needed for tilted span drawing.
-void R_CalculateSlopeVectors(pslope_t *slope, fixed_t planeviewx, fixed_t planeviewy, fixed_t planeviewz, fixed_t planexscale, fixed_t planeyscale, fixed_t planexoffset, fixed_t planeyoffset, angle_t planeviewangle, angle_t planeangle, float fudge);
+void R_SetSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, fixed_t xoff, fixed_t yoff, angle_t angle, angle_t plangle);
+void R_SetScaledSlopePlane(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t zpos, fixed_t xs, fixed_t ys, fixed_t xoff, fixed_t yoff, angle_t angle, angle_t plangle);
+void R_CalculateSlopeVectors(void);
 
 // Sets the slope vector pointers for the current tilted span.
 void R_SetTiltedSpan(INT32 span);
diff --git a/src/r_portal.c b/src/r_portal.c
index 1aca145ec9bc89e265884d2c7f6f1446a6a5a011..3026f4e4c0a99ea9cde1503eb90952e06b49a26b 100644
--- a/src/r_portal.c
+++ b/src/r_portal.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_portal.h b/src/r_portal.h
index e665a26e63d46cf0431c6e46a52cb64bddd0bbd3..0effd07b5b272e5f799dc04848c6f66ef8dbdede 100644
--- a/src/r_portal.h
+++ b/src/r_portal.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_segs.c b/src/r_segs.c
index 1ed1f0285f785465ea3a307c0a6250b4db0d5c49..a8c85ec33bade941039e1c88fea53906d37a3ee7 100644
--- a/src/r_segs.c
+++ b/src/r_segs.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -540,7 +540,7 @@ static boolean R_IsFFloorTranslucent(visffloor_t *pfloor)
 
 	// Polyobjects have no ffloors, and they're handled in the conditional above.
 	if (pfloor->ffloor != NULL)
-		return (pfloor->ffloor->flags & FF_TRANSLUCENT);
+		return (pfloor->ffloor->flags & (FF_TRANSLUCENT|FF_FOG));
 
 	return false;
 }
@@ -1191,7 +1191,7 @@ static void R_RenderSegLoop (void)
 
 							// Lactozilla: Cull part of the column by the 3D floor if it can't be seen
 							// "bottom" is the top pixel of the floor column
-							if (ffbottom >= bottom-1 && R_FFloorCanClip(&ffloor[i]))
+							if (ffbottom >= bottom-1 && R_FFloorCanClip(&ffloor[i]) && !curline->polyseg)
 							{
 								rw_floormarked = true;
 								floorclip[rw_x] = fftop;
@@ -1239,7 +1239,7 @@ static void R_RenderSegLoop (void)
 
 							// Lactozilla: Cull part of the column by the 3D floor if it can't be seen
 							// "top" is the height of the ceiling column
-							if (fftop <= top+1 && R_FFloorCanClip(&ffloor[i]))
+							if (fftop <= top+1 && R_FFloorCanClip(&ffloor[i]) && !curline->polyseg)
 							{
 								rw_ceilingmarked = true;
 								ceilingclip[rw_x] = ffbottom;
@@ -1477,10 +1477,18 @@ static void R_RenderSegLoop (void)
 		}
 
 		for (i = 0; i < numffloors; i++)
+		{
+			if (curline->polyseg && (ffloor[i].polyobj != curline->polyseg))
+				continue;
+
 			ffloor[i].f_frac += ffloor[i].f_step;
+		}
 
 		for (i = 0; i < numbackffloors; i++)
 		{
+			if (curline->polyseg && (ffloor[i].polyobj != curline->polyseg))
+				continue;
+
 			ffloor[i].f_clip[rw_x] = ffloor[i].c_clip[rw_x] = (INT16)((ffloor[i].b_frac >> HEIGHTBITS) & 0xFFFF);
 			ffloor[i].b_frac += ffloor[i].b_step;
 		}
@@ -1649,23 +1657,26 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 		// left
 		temp = xtoviewangle[start]+viewangle;
 
+#define FIXED_TO_DOUBLE(x) (((double)(x)) / ((double)FRACUNIT))
+#define DOUBLE_TO_FIXED(x) (fixed_t)((x) * ((double)FRACUNIT))
+
 		{
 			// Both lines can be written in slope-intercept form, so figure out line intersection
-			float a1, b1, c1, a2, b2, c2, det; // 1 is the seg, 2 is the view angle vector...
-			///TODO: convert to FPU
+			double a1, b1, c1, a2, b2, c2, det; // 1 is the seg, 2 is the view angle vector...
+			///TODO: convert to fixed point
 
-			a1 = FIXED_TO_FLOAT(curline->v2->y-curline->v1->y);
-			b1 = FIXED_TO_FLOAT(curline->v1->x-curline->v2->x);
-			c1 = a1*FIXED_TO_FLOAT(curline->v1->x) + b1*FIXED_TO_FLOAT(curline->v1->y);
+			a1 = FIXED_TO_DOUBLE(curline->v2->y-curline->v1->y);
+			b1 = FIXED_TO_DOUBLE(curline->v1->x-curline->v2->x);
+			c1 = a1*FIXED_TO_DOUBLE(curline->v1->x) + b1*FIXED_TO_DOUBLE(curline->v1->y);
 
-			a2 = -FIXED_TO_FLOAT(FINESINE(temp>>ANGLETOFINESHIFT));
-			b2 = FIXED_TO_FLOAT(FINECOSINE(temp>>ANGLETOFINESHIFT));
-			c2 = a2*FIXED_TO_FLOAT(viewx) + b2*FIXED_TO_FLOAT(viewy);
+			a2 = -FIXED_TO_DOUBLE(FINESINE(temp>>ANGLETOFINESHIFT));
+			b2 = FIXED_TO_DOUBLE(FINECOSINE(temp>>ANGLETOFINESHIFT));
+			c2 = a2*FIXED_TO_DOUBLE(viewx) + b2*FIXED_TO_DOUBLE(viewy);
 
 			det = a1*b2 - a2*b1;
 
-			ds_p->leftpos.x = segleft.x = FLOAT_TO_FIXED((b2*c1 - b1*c2)/det);
-			ds_p->leftpos.y = segleft.y = FLOAT_TO_FIXED((a1*c2 - a2*c1)/det);
+			ds_p->leftpos.x = segleft.x = DOUBLE_TO_FIXED((b2*c1 - b1*c2)/det);
+			ds_p->leftpos.y = segleft.y = DOUBLE_TO_FIXED((a1*c2 - a2*c1)/det);
 		}
 
 		// right
@@ -1673,22 +1684,26 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 
 		{
 			// Both lines can be written in slope-intercept form, so figure out line intersection
-			float a1, b1, c1, a2, b2, c2, det; // 1 is the seg, 2 is the view angle vector...
-			///TODO: convert to FPU
+			double a1, b1, c1, a2, b2, c2, det; // 1 is the seg, 2 is the view angle vector...
+			///TODO: convert to fixed point
 
-			a1 = FIXED_TO_FLOAT(curline->v2->y-curline->v1->y);
-			b1 = FIXED_TO_FLOAT(curline->v1->x-curline->v2->x);
-			c1 = a1*FIXED_TO_FLOAT(curline->v1->x) + b1*FIXED_TO_FLOAT(curline->v1->y);
+			a1 = FIXED_TO_DOUBLE(curline->v2->y-curline->v1->y);
+			b1 = FIXED_TO_DOUBLE(curline->v1->x-curline->v2->x);
+			c1 = a1*FIXED_TO_DOUBLE(curline->v1->x) + b1*FIXED_TO_DOUBLE(curline->v1->y);
 
-			a2 = -FIXED_TO_FLOAT(FINESINE(temp>>ANGLETOFINESHIFT));
-			b2 = FIXED_TO_FLOAT(FINECOSINE(temp>>ANGLETOFINESHIFT));
-			c2 = a2*FIXED_TO_FLOAT(viewx) + b2*FIXED_TO_FLOAT(viewy);
+			a2 = -FIXED_TO_DOUBLE(FINESINE(temp>>ANGLETOFINESHIFT));
+			b2 = FIXED_TO_DOUBLE(FINECOSINE(temp>>ANGLETOFINESHIFT));
+			c2 = a2*FIXED_TO_DOUBLE(viewx) + b2*FIXED_TO_DOUBLE(viewy);
 
 			det = a1*b2 - a2*b1;
 
-			ds_p->rightpos.x = segright.x = FLOAT_TO_FIXED((b2*c1 - b1*c2)/det);
-			ds_p->rightpos.y = segright.y = FLOAT_TO_FIXED((a1*c2 - a2*c1)/det);
+			ds_p->rightpos.x = segright.x = DOUBLE_TO_FIXED((b2*c1 - b1*c2)/det);
+			ds_p->rightpos.y = segright.y = DOUBLE_TO_FIXED((a1*c2 - a2*c1)/det);
 		}
+
+#undef FIXED_TO_DOUBLE
+#undef DOUBLE_TO_FIXED
+
 	}
 
 
diff --git a/src/r_segs.h b/src/r_segs.h
index ace5711d493da30102c791764b78b0f1ba5ff85c..da7d44ad4689f0c28da9e0554caba54e7b8aa0ba 100644
--- a/src/r_segs.h
+++ b/src/r_segs.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_skins.c b/src/r_skins.c
index 25904e95e3ff2c1c89e5fcd5c60c564efedaf84b..86c0bbc544b7907f30b6ed4ef6c07326f8669a98 100644
--- a/src/r_skins.c
+++ b/src/r_skins.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -148,8 +148,6 @@ static void Sk_SetDefaultValue(skin_t *skin)
 	skin->contspeed = 17;
 	skin->contangle = 0;
 
-	skin->availability = 0;
-
 	for (i = 0; i < sfx_skinsoundslot0; i++)
 		if (S_sfx[i].skinsound != -1)
 			skin->soundsid[S_sfx[i].skinsound] = i;
@@ -176,14 +174,34 @@ void R_InitSkins(void)
 
 UINT32 R_GetSkinAvailabilities(void)
 {
-	INT32 s;
 	UINT32 response = 0;
+	UINT32 unlockShift = 0;
+	INT32 i;
 
-	for (s = 0; s < MAXSKINS; s++)
+	for (i = 0; i < MAXUNLOCKABLES; i++)
 	{
-		if (skins[s].availability && unlockables[skins[s].availability - 1].unlocked)
-			response |= (1 << s);
+		if (unlockables[i].type != SECRET_SKIN)
+		{
+			continue;
+		}
+
+		if (unlockShift >= 32)
+		{
+			// This crash is impossible to trigger as is,
+			// but it could happen if MAXUNLOCKABLES is ever made higher than 32,
+			// and someone makes a mod that has 33+ unlockable characters. :V
+			I_Error("Too many unlockable characters\n");
+			return 0;
+		}
+
+		if (unlockables[i].unlocked)
+		{
+			response |= (1 << unlockShift);
+		}
+
+		unlockShift++;
 	}
+
 	return response;
 }
 
@@ -191,14 +209,83 @@ UINT32 R_GetSkinAvailabilities(void)
 // warning don't use with an invalid skinnum other than -1 which always returns true
 boolean R_SkinUsable(INT32 playernum, INT32 skinnum)
 {
-	return ((skinnum == -1) // Simplifies things elsewhere, since there's already plenty of checks for less-than-0...
-		|| (!skins[skinnum].availability)
-		|| (((netgame || multiplayer) && playernum != -1) ? (players[playernum].availabilities & (1 << skinnum)) : (unlockables[skins[skinnum].availability - 1].unlocked))
-		|| (modeattacking) // If you have someone else's run you might as well take a look
-		|| (Playing() && (R_SkinAvailable(mapheaderinfo[gamemap-1]->forcecharacter) == skinnum)) // Force 1.
-		|| (netgame && (cv_forceskin.value == skinnum)) // Force 2.
-		|| (metalrecording && skinnum == 5) // Force 3.
-		);
+	INT32 unlockID = -1;
+	UINT32 unlockShift = 0;
+	INT32 i;
+
+	if (skinnum == -1)
+	{
+		// Simplifies things elsewhere, since there's already plenty of checks for less-than-0...
+		return true;
+	}
+
+	if (modeattacking)
+	{
+		// If you have someone else's run you might as well take a look
+		return true;
+	}
+
+	if (Playing() && (R_SkinAvailable(mapheaderinfo[gamemap-1]->forcecharacter) == skinnum))
+	{
+		// Force 1.
+		return true;
+	}
+
+	if (netgame && (cv_forceskin.value == skinnum))
+	{
+		// Force 2.
+		return true;
+	}
+	
+	if (metalrecording && skinnum == 5)
+	{
+		// 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.
+
+	for (i = 0; i < MAXUNLOCKABLES; i++)
+	{
+		INT32 unlockSkin = -1;
+
+		if (unlockables[i].type != SECRET_SKIN)
+		{
+			continue;
+		}
+
+		unlockSkin = M_UnlockableSkinNum(&unlockables[i]);
+
+		if (unlockSkin == skinnum)
+		{
+			unlockID = i;
+			break;
+		}
+
+		unlockShift++;
+	}
+
+	if (unlockID == -1)
+	{
+		// This skin isn't locked at all, we're good.
+		return true;
+	}
+
+	if ((netgame || multiplayer) && playernum != -1)
+	{
+		// We want to check per-player unlockables.
+		return (players[playernum].availabilities & (1 << unlockShift));
+	}
+	else
+	{
+		// We want to check our global unlockables.
+		return (unlockables[unlockID].unlocked);
+	}
 }
 
 // returns true if the skin name is found (loaded from pwad)
@@ -216,6 +303,103 @@ INT32 R_SkinAvailable(const char *name)
 	return -1;
 }
 
+// Auxillary function that actually sets the skin
+static void SetSkin(player_t *player, INT32 skinnum)
+{
+	skin_t *skin = &skins[skinnum];
+	UINT16 newcolor = 0;
+
+	player->skin = skinnum;
+
+	player->camerascale = skin->camerascale;
+	player->shieldscale = skin->shieldscale;
+
+	player->charability = (UINT8)skin->ability;
+	player->charability2 = (UINT8)skin->ability2;
+
+	player->charflags = (UINT32)skin->flags;
+
+	player->thokitem = skin->thokitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].painchance : (UINT32)skin->thokitem;
+	player->spinitem = skin->spinitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].damage : (UINT32)skin->spinitem;
+	player->revitem = skin->revitem < 0 ? (mobjtype_t)mobjinfo[MT_PLAYER].raisestate : (UINT32)skin->revitem;
+	player->followitem = skin->followitem;
+
+	if (((player->powers[pw_shield] & SH_NOSTACK) == SH_PINK) && (player->revitem == MT_LHRT || player->spinitem == MT_LHRT || player->thokitem == MT_LHRT)) // Healers can't keep their buff.
+		player->powers[pw_shield] &= SH_STACK;
+
+	player->actionspd = skin->actionspd;
+	player->mindash = skin->mindash;
+	player->maxdash = skin->maxdash;
+
+	player->normalspeed = skin->normalspeed;
+	player->runspeed = skin->runspeed;
+	player->thrustfactor = skin->thrustfactor;
+	player->accelstart = skin->accelstart;
+	player->acceleration = skin->acceleration;
+
+	player->jumpfactor = skin->jumpfactor;
+
+	player->height = skin->height;
+	player->spinheight = skin->spinheight;
+
+	if (!(cv_debug || devparm) && !(netgame || multiplayer || demoplayback))
+	{
+		if (player == &players[consoleplayer])
+			CV_StealthSetValue(&cv_playercolor, skin->prefcolor);
+		else if (player == &players[secondarydisplayplayer])
+			CV_StealthSetValue(&cv_playercolor2, skin->prefcolor);
+		player->skincolor = newcolor = skin->prefcolor;
+		if (player->bot && botingame)
+		{
+			botskin = (UINT8)(skinnum + 1);
+			botcolor = skin->prefcolor;
+		}
+	}
+
+	if (player->followmobj)
+	{
+		P_RemoveMobj(player->followmobj);
+		P_SetTarget(&player->followmobj, NULL);
+	}
+
+	if (player->mo)
+	{
+		fixed_t radius = FixedMul(skin->radius, player->mo->scale);
+		if ((player->powers[pw_carry] == CR_NIGHTSMODE) && (skin->sprites[SPR2_NFLY].numframes == 0)) // If you don't have a sprite for flying horizontally, use the default NiGHTS skin.
+		{
+			skin = &skins[DEFAULTNIGHTSSKIN];
+			player->followitem = skin->followitem;
+			if (!(cv_debug || devparm) && !(netgame || multiplayer || demoplayback))
+				newcolor = skin->prefcolor; // will be updated in thinker to flashing
+		}
+		player->mo->skin = skin;
+		if (newcolor)
+			player->mo->color = newcolor;
+		P_SetScale(player->mo, player->mo->scale);
+		player->mo->radius = radius;
+
+		P_SetPlayerMobjState(player->mo, player->mo->state-states); // Prevent visual errors when switching between skins with differing number of frames
+	}
+}
+
+// Gets the player to the first usuable skin in the game.
+// (If your mod locked them all, then you kinda stupid)
+INT32 GetPlayerDefaultSkin(INT32 playernum)
+{
+	INT32 i;
+
+	for (i = 0; i < numskins; i++)
+	{
+		if (R_SkinUsable(playernum, i))
+		{
+			return i;
+		}
+	}
+
+	I_Error("All characters are locked!");
+	return 0;
+}
+
 // network code calls this when a 'skin change' is received
 void SetPlayerSkin(INT32 playernum, const char *skinname)
 {
@@ -224,16 +408,16 @@ void SetPlayerSkin(INT32 playernum, const char *skinname)
 
 	if ((i != -1) && R_SkinUsable(playernum, i))
 	{
-		SetPlayerSkinByNum(playernum, i);
+		SetSkin(player, i);
 		return;
 	}
 
 	if (P_IsLocalPlayer(player))
 		CONS_Alert(CONS_WARNING, M_GetText("Skin '%s' not found.\n"), skinname);
-	else if(server || IsPlayerAdmin(consoleplayer))
+	else if (server || IsPlayerAdmin(consoleplayer))
 		CONS_Alert(CONS_WARNING, M_GetText("Player %d (%s) skin '%s' not found\n"), playernum, player_names[playernum], skinname);
 
-	SetPlayerSkinByNum(playernum, 0);
+	SetSkin(player, GetPlayerDefaultSkin(playernum));
 }
 
 // Same as SetPlayerSkin, but uses the skin #.
@@ -241,90 +425,19 @@ void SetPlayerSkin(INT32 playernum, const char *skinname)
 void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum)
 {
 	player_t *player = &players[playernum];
-	skin_t *skin = &skins[skinnum];
-	UINT16 newcolor = 0;
 
 	if (skinnum >= 0 && skinnum < numskins && R_SkinUsable(playernum, skinnum)) // Make sure it exists!
 	{
-		player->skin = skinnum;
-
-		player->camerascale = skin->camerascale;
-		player->shieldscale = skin->shieldscale;
-
-		player->charability = (UINT8)skin->ability;
-		player->charability2 = (UINT8)skin->ability2;
-
-		player->charflags = (UINT32)skin->flags;
-
-		player->thokitem = skin->thokitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].painchance : (UINT32)skin->thokitem;
-		player->spinitem = skin->spinitem < 0 ? (UINT32)mobjinfo[MT_PLAYER].damage : (UINT32)skin->spinitem;
-		player->revitem = skin->revitem < 0 ? (mobjtype_t)mobjinfo[MT_PLAYER].raisestate : (UINT32)skin->revitem;
-		player->followitem = skin->followitem;
-
-		if (((player->powers[pw_shield] & SH_NOSTACK) == SH_PINK) && (player->revitem == MT_LHRT || player->spinitem == MT_LHRT || player->thokitem == MT_LHRT)) // Healers can't keep their buff.
-			player->powers[pw_shield] &= SH_STACK;
-
-		player->actionspd = skin->actionspd;
-		player->mindash = skin->mindash;
-		player->maxdash = skin->maxdash;
-
-		player->normalspeed = skin->normalspeed;
-		player->runspeed = skin->runspeed;
-		player->thrustfactor = skin->thrustfactor;
-		player->accelstart = skin->accelstart;
-		player->acceleration = skin->acceleration;
-
-		player->jumpfactor = skin->jumpfactor;
-
-		player->height = skin->height;
-		player->spinheight = skin->spinheight;
-
-		if (!(cv_debug || devparm) && !(netgame || multiplayer || demoplayback))
-		{
-			if (playernum == consoleplayer)
-				CV_StealthSetValue(&cv_playercolor, skin->prefcolor);
-			else if (playernum == secondarydisplayplayer)
-				CV_StealthSetValue(&cv_playercolor2, skin->prefcolor);
-			player->skincolor = newcolor = skin->prefcolor;
-			if (player->bot && botingame)
-			{
-				botskin = (UINT8)(skinnum + 1);
-				botcolor = skin->prefcolor;
-			}
-		}
-
-		if (player->followmobj)
-		{
-			P_RemoveMobj(player->followmobj);
-			P_SetTarget(&player->followmobj, NULL);
-		}
-
-		if (player->mo)
-		{
-			fixed_t radius = FixedMul(skin->radius, player->mo->scale);
-			if ((player->powers[pw_carry] == CR_NIGHTSMODE) && (skin->sprites[SPR2_NFLY].numframes == 0)) // If you don't have a sprite for flying horizontally, use the default NiGHTS skin.
-			{
-				skin = &skins[DEFAULTNIGHTSSKIN];
-				player->followitem = skin->followitem;
-				if (!(cv_debug || devparm) && !(netgame || multiplayer || demoplayback))
-					newcolor = skin->prefcolor; // will be updated in thinker to flashing
-			}
-			player->mo->skin = skin;
-			if (newcolor)
-				player->mo->color = newcolor;
-			P_SetScale(player->mo, player->mo->scale);
-			player->mo->radius = radius;
-
-			P_SetPlayerMobjState(player->mo, player->mo->state-states); // Prevent visual errors when switching between skins with differing number of frames
-		}
+		SetSkin(player, skinnum);
 		return;
 	}
 
 	if (P_IsLocalPlayer(player))
 		CONS_Alert(CONS_WARNING, M_GetText("Requested skin %d not found\n"), skinnum);
-	else if(server || IsPlayerAdmin(consoleplayer))
+	else if (server || IsPlayerAdmin(consoleplayer))
 		CONS_Alert(CONS_WARNING, "Player %d (%s) skin %d not found\n", playernum, player_names[playernum], skinnum);
-	SetPlayerSkinByNum(playernum, 0); // not found put the sonic skin
+
+	SetSkin(player, GetPlayerDefaultSkin(playernum));
 }
 
 //
@@ -511,6 +624,10 @@ static boolean R_ProcessPatchableFields(skin_t *skin, char *stoken, char *value)
 	GETFLAG(MULTIABILITY)
 	GETFLAG(NONIGHTSROTATION)
 	GETFLAG(NONIGHTSSUPER)
+	GETFLAG(NOSUPERSPRITES)
+	GETFLAG(NOSUPERJUMPBOOST)
+	GETFLAG(CANBUSTWALLS)
+	GETFLAG(NOSHIELDABILITY)
 #undef GETFLAG
 
 	else // let's check if it's a sound, otherwise error out
@@ -554,7 +671,7 @@ static boolean R_ProcessPatchableFields(skin_t *skin, char *stoken, char *value)
 //
 // Find skin sprites, sounds & optional status bar face, & add them
 //
-void R_AddSkins(UINT16 wadnum)
+void R_AddSkins(UINT16 wadnum, boolean mainfile)
 {
 	UINT16 lump, lastlump = 0;
 	char *buf;
@@ -669,12 +786,6 @@ void R_AddSkins(UINT16 wadnum)
 				if (!realname)
 					STRBUFCPY(skin->realname, skin->hudname);
 			}
-			else if (!stricmp(stoken, "availability"))
-			{
-				skin->availability = atoi(value);
-				if (skin->availability >= MAXUNLOCKABLES)
-					skin->availability = 0;
-			}
 			else if (!R_ProcessPatchableFields(skin, stoken, value))
 				CONS_Debug(DBG_SETUP, "R_AddSkins: Unknown keyword '%s' in S_SKIN lump #%d (WAD %s)\n", stoken, lump, wadfiles[wadnum]->filename);
 
@@ -689,7 +800,7 @@ next_token:
 
 		R_FlushTranslationColormapCache();
 
-		if (!skin->availability) // Safe to print...
+		if (mainfile == false)
 			CONS_Printf(M_GetText("Added skin '%s'\n"), skin->name);
 #ifdef SKINVALUES
 		skin_cons_t[numskins].value = numskins;
@@ -709,7 +820,7 @@ next_token:
 //
 // Patch skin sprites
 //
-void R_PatchSkins(UINT16 wadnum)
+void R_PatchSkins(UINT16 wadnum, boolean mainfile)
 {
 	UINT16 lump, lastlump = 0;
 	char *buf;
@@ -822,7 +933,7 @@ next_token:
 
 		R_FlushTranslationColormapCache();
 
-		if (!skin->availability) // Safe to print...
+		if (mainfile == false)
 			CONS_Printf(M_GetText("Patched skin '%s'\n"), skin->name);
 	}
 	return;
diff --git a/src/r_skins.h b/src/r_skins.h
index fbbb38743d84704d3373aafd9e5cc1a7135a46d2..a38997f4dd623aad8dc7cdf21aff1283f8c8aa93 100644
--- a/src/r_skins.h
+++ b/src/r_skins.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -80,8 +80,6 @@ typedef struct
 	// contains super versions too
 	spritedef_t sprites[NUMPLAYERSPRITES*2];
 	spriteinfo_t sprinfo[NUMPLAYERSPRITES*2];
-
-	UINT8 availability; // lock?
 } skin_t;
 
 /// Externs
@@ -91,13 +89,14 @@ extern skin_t skins[MAXSKINS];
 /// Function prototypes
 void R_InitSkins(void);
 
+INT32 GetPlayerDefaultSkin(INT32 playernum);
 void SetPlayerSkin(INT32 playernum,const char *skinname);
 void SetPlayerSkinByNum(INT32 playernum,INT32 skinnum); // Tails 03-16-2002
 boolean R_SkinUsable(INT32 playernum, INT32 skinnum);
 UINT32 R_GetSkinAvailabilities(void);
 INT32 R_SkinAvailable(const char *name);
-void R_PatchSkins(UINT16 wadnum);
-void R_AddSkins(UINT16 wadnum);
+void R_AddSkins(UINT16 wadnum, boolean mainfile);
+void R_PatchSkins(UINT16 wadnum, boolean mainfile);
 
 UINT8 P_GetSkinSprite2(skin_t *skin, UINT8 spr2, player_t *player);
 
diff --git a/src/r_sky.c b/src/r_sky.c
index 7cdcfa44d2e7c74bd1d2a6204ef75cd749b6abd8..041cccfc5546f679894a7d7ab2e6cc93c0d8c8c4 100644
--- a/src/r_sky.c
+++ b/src/r_sky.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_sky.h b/src/r_sky.h
index 55d866b86a52a5151b0c0decc4e6d587edd129a4..f4356dcfae3f4f6e47248bb7d6ef46e21495fa31 100644
--- a/src/r_sky.h
+++ b/src/r_sky.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_splats.c b/src/r_splats.c
index a3fad82d81730ff1d0ff967f0a4419c60791e59a..c554e9b1f002937671e19b1e043460d3168c3e2e 100644
--- a/src/r_splats.c
+++ b/src/r_splats.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -28,11 +28,12 @@ static void prepare_rastertab(void);
 //                                                               FLOOR SPLATS
 // ==========================================================================
 
+static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, vissprite_t *vis);
+
 #ifdef USEASM
 void ASMCALL rasterize_segment_tex_asm(INT32 x1, INT32 y1, INT32 x2, INT32 y2, INT32 tv1, INT32 tv2, INT32 tc, INT32 dir);
 #endif
 
-// Lactozilla
 static void rasterize_segment_tex(INT32 x1, INT32 y1, INT32 x2, INT32 y2, INT32 tv1, INT32 tv2, INT32 tc, INT32 dir)
 {
 #ifdef USEASM
@@ -137,7 +138,7 @@ static void rasterize_segment_tex(INT32 x1, INT32 y1, INT32 x2, INT32 y2, INT32
 	}
 }
 
-void R_DrawFloorSprite(vissprite_t *spr)
+void R_DrawFloorSplat(vissprite_t *spr)
 {
 	floorsplat_t splat;
 	mobj_t *mobj = spr->mobj;
@@ -154,7 +155,6 @@ void R_DrawFloorSprite(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);
@@ -187,7 +187,7 @@ void R_DrawFloorSprite(vissprite_t *spr)
 	if (spr->rotateflags & SRF_3D || renderflags & RF_NOSPLATBILLBOARD)
 		splatangle = mobj->angle;
 	else
-		splatangle = viewangle;
+		splatangle = spr->viewpoint.angle;
 
 	if (!(spr->cut & SC_ISROTATED))
 		splatangle += mobj->rollangle;
@@ -217,7 +217,7 @@ void R_DrawFloorSprite(vissprite_t *spr)
 	splat.x = x;
 	splat.y = y;
 	splat.z = mobj->z;
-	splat.tilted = false;
+	splat.slope = NULL;
 
 	// Set positions
 
@@ -237,9 +237,9 @@ void R_DrawFloorSprite(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++)
@@ -254,37 +254,10 @@ void R_DrawFloorSprite(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)
-	{
-		// Lactozilla: Just copy the entire slope LMFAOOOO
-		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_DrawFloorSprite(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_DrawFloorSprite(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;
@@ -330,7 +308,7 @@ void R_DrawFloorSprite(vissprite_t *spr)
 		v2d[i].y = (centeryfrac + FixedMul(rot_z, yscale))>>FRACBITS;
 	}
 
-	R_RenderFloorSplat(&splat, v2d, spr);
+	R_RasterizeFloorSplat(&splat, v2d, spr);
 }
 
 // --------------------------------------------------------------------------
@@ -338,7 +316,7 @@ void R_DrawFloorSprite(vissprite_t *spr)
 // fill the polygon with linear interpolation, call span drawer for each
 // scan line
 // --------------------------------------------------------------------------
-void R_RenderFloorSplat(floorsplat_t *pSplat, vector2_t *verts, vissprite_t *vis)
+static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, vissprite_t *vis)
 {
 	// rasterizing
 	INT32 miny = viewheight + 1, maxy = 0;
@@ -416,31 +394,32 @@ void R_RenderFloorSplat(floorsplat_t *pSplat, vector2_t *verts, vissprite_t *vis
 	if (R_CheckPowersOfTwo())
 		R_CheckFlatLength(ds_flatwidth * ds_flatheight);
 
-	// Lactozilla: I don't know what I'm doing
-	if (pSplat->tilted)
+	if (pSplat->slope)
 	{
 		R_SetTiltedSpan(0);
-		R_CalculateSlopeVectors(&pSplat->slope, viewx, viewy, viewz, pSplat->xscale, pSplat->yscale, -pSplat->verts[0].x, pSplat->verts[0].y, viewangle, pSplat->angle, 1.0f);
+		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 @@ void R_RenderFloorSplat(floorsplat_t *pSplat, vector2_t *verts, vissprite_t *vis
 	{
 		ds_transmap = vis->transmap;
 
-		if (pSplat->tilted)
+		if (pSplat->slope)
 			spanfunctype = SPANDRAWFUNC_TILTEDTRANSSPRITE;
 		else
 			spanfunctype = SPANDRAWFUNC_TRANSSPRITE;
@@ -528,12 +507,12 @@ void R_RenderFloorSplat(floorsplat_t *pSplat, vector2_t *verts, vissprite_t *vis
 		if (x2 < x1)
 			continue;
 
-		if (!pSplat->tilted)
+		if (!pSplat->slope)
 		{
 			fixed_t xstep, ystep;
 			fixed_t distance, span;
 
-			angle_t angle = (viewangle + pSplat->angle)>>ANGLETOFINESHIFT;
+			angle_t angle = (vis->viewpoint.angle + pSplat->angle)>>ANGLETOFINESHIFT;
 			angle_t planecos = FINECOSINE(angle);
 			angle_t planesin = FINESINE(angle);
 
@@ -543,17 +522,13 @@ void R_RenderFloorSplat(floorsplat_t *pSplat, vector2_t *verts, vissprite_t *vis
 				distance = cacheddistance[y] = FixedMul(planeheight, yslope[y]);
 				span = abs(centery - y);
 
-				if (span) // don't divide by zero
+				if (span) // Don't divide by zero
 				{
 					xstep = FixedMul(planesin, planeheight) / span;
 					ystep = FixedMul(planecos, planeheight) / span;
 				}
 				else
-				{
-					// ah
-					xstep = FRACUNIT;
-					ystep = FRACUNIT;
-				}
+					xstep = ystep = FRACUNIT;
 
 				cachedxstep[y] = xstep;
 				cachedystep[y] = ystep;
@@ -581,7 +556,7 @@ void R_RenderFloorSplat(floorsplat_t *pSplat, vector2_t *verts, vissprite_t *vis
 		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 e1f836f489bab54513dafd5b867ebfd7dbc79f44..7e31406d1290e94861a1f660299a05be3dfc8670 100644
--- a/src/r_splats.h
+++ b/src/r_splats.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -34,15 +34,13 @@ 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
 	mobj_t *mobj; // Mobj it is tied to
 } floorsplat_t;
 
-void R_DrawFloorSprite(vissprite_t *spr);
-void R_RenderFloorSplat(floorsplat_t *pSplat, vector2_t *verts, vissprite_t *vis);
+void R_DrawFloorSplat(vissprite_t *spr);
 
 #endif /*__R_SPLATS_H__*/
diff --git a/src/r_state.h b/src/r_state.h
index 25aa697024c87ceeee88d0b1f1312d7270a502b8..5a606ed8c9fa2804f5802d8834a1f7d85b963260 100644
--- a/src/r_state.h
+++ b/src/r_state.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_textures.c b/src/r_textures.c
index 9de9649e222a9628f0917592570c899917e62722..793e5237f62e64e937c32f97d4d01bbb08a6c065 100644
--- a/src/r_textures.c
+++ b/src/r_textures.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -28,11 +28,6 @@
 #include "byteptr.h"
 #include "dehacked.h"
 
-// I don't know what this is even for, but r_data.c had it.
-#ifdef _WIN32
-#include <malloc.h> // alloca(sizeof)
-#endif
-
 #ifdef HWRENDER
 #include "hardware/hw_glob.h" // HWR_LoadMapTextures
 #endif
@@ -604,7 +599,7 @@ void *R_GetLevelFlat(levelflat_t *levelflat)
 				levelflat->height = ds_flatheight = SHORT(patch->height);
 
 				levelflat->picture = Z_Malloc(levelflat->width * levelflat->height, PU_LEVEL, NULL);
-				converted = Picture_FlatConvert(PICFMT_DOOMPATCH, patch, PICFMT_FLAT, 0, &size, levelflat->width, levelflat->height, patch->topoffset, patch->leftoffset, 0);
+				converted = Picture_FlatConvert(PICFMT_DOOMPATCH, patch, PICFMT_FLAT, 0, &size, levelflat->width, levelflat->height, SHORT(patch->topoffset), SHORT(patch->leftoffset), 0);
 				M_Memcpy(levelflat->picture, converted, size);
 				Z_Free(converted);
 			}
@@ -626,7 +621,7 @@ void *R_GetLevelFlat(levelflat_t *levelflat)
 //
 // R_CheckPowersOfTwo
 //
-// Self-explanatory?
+// Sets ds_powersoftwo true if the flat's dimensions are powers of two, and returns that.
 //
 boolean R_CheckPowersOfTwo(void)
 {
@@ -732,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);
@@ -754,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
@@ -844,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);
@@ -875,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
@@ -964,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);
@@ -978,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++)
 				{
@@ -1002,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);
@@ -1017,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++)
 			{
@@ -1558,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_textures.h b/src/r_textures.h
index 74a94a9ededc42ca2a7a66413141de9d8f98535d..dd286b6ac57c7082f57bece9445d9c1695958d13 100644
--- a/src/r_textures.h
+++ b/src/r_textures.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/r_things.c b/src/r_things.c
index 08337392742fe3775f3faa2d1f0a073937736fc6..bed71a6d791f1c5dbc41a0f517560dc5387c1b08 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -230,7 +230,7 @@ boolean R_AddSingleSpriteDef(const char *sprname, spritedef_t *spritedef, UINT16
 	UINT8 rotation;
 	lumpinfo_t *lumpinfo;
 	softwarepatch_t patch;
-	UINT8 numadded = 0;
+	UINT16 numadded = 0;
 
 	memset(sprtemp,0xFF, sizeof (sprtemp));
 	maxframe = (size_t)-1;
@@ -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;
@@ -547,8 +548,8 @@ void R_InitSprites(void)
 	R_InitSkins();
 	for (i = 0; i < numwadfiles; i++)
 	{
-		R_AddSkins((UINT16)i);
-		R_PatchSkins((UINT16)i);
+		R_AddSkins((UINT16)i, true);
+		R_PatchSkins((UINT16)i, true);
 		R_LoadSpriteInfoLumps(i, wadfiles[i]->numlumps);
 	}
 	ST_ReloadSkinFaceGraphics();
@@ -753,7 +754,7 @@ UINT8 *R_GetSpriteTranslation(vissprite_t *vis)
 		else if (vis->mobj->type == MT_METALSONIC_BATTLE)
 			return R_GetTranslationColormap(TC_METALSONIC, 0, GTC_CACHE);
 		else
-			return R_GetTranslationColormap(TC_BOSS, 0, GTC_CACHE);
+			return R_GetTranslationColormap(TC_BOSS, vis->mobj->color, GTC_CACHE);
 	}
 	else if (vis->mobj->color)
 	{
@@ -796,7 +797,7 @@ static void R_DrawVisSprite(vissprite_t *vis)
 	INT32 pwidth;
 	fixed_t frac;
 	patch_t *patch = vis->patch;
-	fixed_t this_scale = vis->mobj->scale;
+	fixed_t this_scale = vis->thingscale;
 	INT32 x1, x2;
 	INT64 overflow_test;
 
@@ -1078,6 +1079,14 @@ static void R_SplitSprite(vissprite_t *sprite)
 		sprite->sz = cutfrac;
 		newsprite->szt = (INT16)(sprite->sz - 1);
 
+		if (testheight < sprite->pzt && testheight > sprite->pz)
+			sprite->pz = newsprite->pzt = testheight;
+		else
+		{
+			newsprite->pz = newsprite->gz;
+			newsprite->pzt = newsprite->gzt;
+		}
+
 		newsprite->szt -= 8;
 
 		newsprite->cut |= SC_TOP;
@@ -1300,12 +1309,16 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 	shadow->patch = patch;
 	shadow->heightsec = vis->heightsec;
 
+	shadow->thingheight = FRACUNIT;
+	shadow->pz = groundz + (isflipped ? -shadow->thingheight : 0);
+	shadow->pzt = shadow->pz + shadow->thingheight;
+
 	shadow->mobjflags = 0;
 	shadow->sortscale = vis->sortscale;
 	shadow->dispoffset = vis->dispoffset - 5;
 	shadow->gx = thing->x;
 	shadow->gy = thing->y;
-	shadow->gzt = groundz + patch->height * shadowyscale / 2;
+	shadow->gzt = (isflipped ? shadow->pzt : shadow->pz) + patch->height * shadowyscale / 2;
 	shadow->gz = shadow->gzt - patch->height * shadowyscale;
 	shadow->texturemid = FixedMul(thing->scale, FixedDiv(shadow->gzt - viewz, shadowyscale));
 	if (thing->skin && ((skin_t *)thing->skin)->flags & SF_HIRES)
@@ -1320,6 +1333,7 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 
 	shadow->xscale = FixedMul(xscale, shadowxscale); //SoM: 4/17/2000
 	shadow->scale = FixedMul(yscale, shadowyscale);
+	shadow->thingscale = thing->scale;
 	shadow->sector = vis->sector;
 	shadow->szt = (INT16)((centeryfrac - FixedMul(shadow->gzt - viewz, yscale))>>FRACBITS);
 	shadow->sz = (INT16)((centeryfrac - FixedMul(shadow->gz - viewz, yscale))>>FRACBITS);
@@ -1411,7 +1425,7 @@ static void R_ProjectSprite(mobj_t *thing)
 
 	fixed_t sheartan = 0;
 	fixed_t shadowscale = FRACUNIT;
-	fixed_t basetx; // drop shadows
+	fixed_t basetx, basetz; // drop shadows
 
 	boolean shadowdraw, shadoweffects, shadowskew;
 	boolean splat = R_ThingIsFloorSprite(thing);
@@ -1441,7 +1455,7 @@ static void R_ProjectSprite(mobj_t *thing)
 	tr_x = thing->x - viewx;
 	tr_y = thing->y - viewy;
 
-	tz = FixedMul(tr_x, viewcos) + FixedMul(tr_y, viewsin); // near/far distance
+	basetz = tz = FixedMul(tr_x, viewcos) + FixedMul(tr_y, viewsin); // near/far distance
 
 	// thing is behind view plane?
 	if (!papersprite && (tz < FixedMul(MINZ, this_scale))) // papersprite clipping is handled later
@@ -1790,7 +1804,7 @@ static void R_ProjectSprite(mobj_t *thing)
 	else if (oldthing->frame & FF_TRANSMASK)
 	{
 		trans = (oldthing->frame & FF_TRANSMASK) >> FF_TRANSSHIFT;
-		if (oldthing->blendmode == AST_TRANSLUCENT && trans >= NUMTRANSMAPS)
+		if (!R_BlendLevelVisible(oldthing->blendmode, trans))
 			return;
 	}
 	else
@@ -1935,6 +1949,9 @@ static void R_ProjectSprite(mobj_t *thing)
 	vis->gy = thing->y;
 	vis->gz = gz;
 	vis->gzt = gzt;
+	vis->thingheight = thing->height;
+	vis->pz = thing->z;
+	vis->pzt = vis->pz + vis->thingheight;
 	vis->texturemid = FixedDiv(gzt - viewz, spriteyscale);
 	vis->scalestep = scalestep;
 	vis->paperoffset = paperoffset;
@@ -1942,6 +1959,10 @@ static void R_ProjectSprite(mobj_t *thing)
 	vis->centerangle = centerangle;
 	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
 
@@ -1960,6 +1981,7 @@ static void R_ProjectSprite(mobj_t *thing)
 
 	vis->xscale = FixedMul(spritexscale, xscale); //SoM: 4/17/2000
 	vis->scale = FixedMul(spriteyscale, yscale); //<<detailshift;
+	vis->thingscale = oldthing->scale;
 
 	vis->spritexscale = spritexscale;
 	vis->spriteyscale = spriteyscale;
@@ -2037,7 +2059,7 @@ static void R_ProjectSprite(mobj_t *thing)
 		R_SplitSprite(vis);
 
 	if (oldthing->shadowscale && cv_shadow.value)
-		R_ProjectDropShadow(oldthing, vis, oldthing->shadowscale, basetx, tz);
+		R_ProjectDropShadow(oldthing, vis, oldthing->shadowscale, basetx, basetz);
 
 	// Debug
 	++objectsdrawn;
@@ -2151,6 +2173,9 @@ static void R_ProjectPrecipitationSprite(precipmobj_t *thing)
 	vis->gy = thing->y;
 	vis->gz = gz;
 	vis->gzt = gzt;
+	vis->thingheight = 4*FRACUNIT;
+	vis->pz = thing->z;
+	vis->pzt = vis->pz + vis->thingheight;
 	vis->texturemid = vis->gzt - viewz;
 	vis->scalestep = 0;
 	vis->paperdistance = 0;
@@ -2179,7 +2204,7 @@ static void R_ProjectPrecipitationSprite(precipmobj_t *thing)
 
 	// specific translucency
 	if (thing->frame & FF_TRANSMASK)
-		vis->transmap = (thing->frame & FF_TRANSMASK) - 0x10000 + transtables;
+		vis->transmap = R_GetTranslucencyTable((thing->frame & FF_TRANSMASK) >> FF_TRANSSHIFT);
 	else
 		vis->transmap = NULL;
 
@@ -2544,15 +2569,19 @@ static void R_CreateDrawNodes(maskcount_t* mask, drawnode_t* head, boolean temps
 				planeobjectz = P_GetZAt(r2->plane->slope, rover->gx, rover->gy, r2->plane->height);
 				planecameraz = P_GetZAt(r2->plane->slope,     viewx,     viewy, r2->plane->height);
 
-				// bird: if any part of the sprite peeks in front the plane
-				if (planecameraz < viewz)
+				if (rover->mobjflags & MF_NOCLIPHEIGHT)
 				{
-					if (rover->gzt >= planeobjectz)
+					//Objects with NOCLIPHEIGHT can appear halfway in.
+					if (planecameraz < viewz && rover->pz+(rover->thingheight/2) >= planeobjectz)
+						continue;
+					if (planecameraz > viewz && rover->pzt-(rover->thingheight/2) <= planeobjectz)
 						continue;
 				}
-				else if (planecameraz > viewz)
+				else
 				{
-					if (rover->gz <= planeobjectz)
+					if (planecameraz < viewz && rover->pz >= planeobjectz)
+						continue;
+					if (planecameraz > viewz && rover->pzt <= planeobjectz)
 						continue;
 				}
 
@@ -2585,7 +2614,7 @@ static void R_CreateDrawNodes(maskcount_t* mask, drawnode_t* head, boolean temps
 			}
 			else if (r2->thickseg)
 			{
-				//fixed_t topplaneobjectz, topplanecameraz, botplaneobjectz, botplanecameraz;
+				fixed_t topplaneobjectz, topplanecameraz, botplaneobjectz, botplanecameraz;
 				if (rover->x1 > r2->thickseg->x2 || rover->x2 < r2->thickseg->x1)
 					continue;
 
@@ -2596,11 +2625,6 @@ static void R_CreateDrawNodes(maskcount_t* mask, drawnode_t* head, boolean temps
 				if (scale <= rover->sortscale)
 					continue;
 
-				// bird: Always sort sprites behind segs. This helps the plane
-				// sorting above too. Basically if the sprite gets sorted behind
-				// the seg here, it will be behind the plane too, since planes
-				// are added after segs in the list.
-#if 0
 				topplaneobjectz = P_GetFFloorTopZAt   (r2->ffloor, rover->gx, rover->gy);
 				topplanecameraz = P_GetFFloorTopZAt   (r2->ffloor,     viewx,     viewy);
 				botplaneobjectz = P_GetFFloorBottomZAt(r2->ffloor, rover->gx, rover->gy);
@@ -2609,7 +2633,6 @@ static void R_CreateDrawNodes(maskcount_t* mask, drawnode_t* head, boolean temps
 				if ((topplanecameraz > viewz && botplanecameraz < viewz) ||
 				    (topplanecameraz < viewz && rover->gzt < topplaneobjectz) ||
 				    (botplanecameraz > viewz && rover->gz > botplaneobjectz))
-#endif
 				{
 					entry = R_CreateDrawNode(NULL);
 					(entry->prev = r2->prev)->next = entry;
@@ -2650,11 +2673,23 @@ static void R_CreateDrawNodes(maskcount_t* mask, drawnode_t* head, boolean temps
 
 					if (!behind)
 					{
-						// FIXME: calculate gz and gzt for splats properly and use that
-						if (rover->mobj->z < viewz)
-							infront = (r2->sprite->mobj->z >= rover->mobj->z);
+						fixed_t z1 = 0, z2 = 0;
+
+						if (rover->mobj->z - viewz > 0)
+						{
+							z1 = rover->pz;
+							z2 = r2->sprite->pz;
+						}
 						else
-							infront = (r2->sprite->mobj->z <= rover->mobj->z);
+						{
+							z1 = r2->sprite->pz;
+							z2 = rover->pz;
+						}
+
+						z1 -= viewz;
+						z2 -= viewz;
+
+						infront = (z1 >= z2);
 					}
 				}
 				else
@@ -2710,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;
 }
 
@@ -2753,7 +2788,7 @@ static void R_DrawSprite(vissprite_t *spr)
 	mceilingclip = spr->cliptop;
 
 	if (spr->cut & SC_SPLAT)
-		R_DrawFloorSprite(spr);
+		R_DrawFloorSplat(spr);
 	else
 		R_DrawVisSprite(spr);
 }
diff --git a/src/r_things.h b/src/r_things.h
index d15ae818c4c273dee396fe9c6b7919a95c87b98b..79dc80d94a2a15691056e34716f038a491cf54ba 100644
--- a/src/r_things.h
+++ b/src/r_things.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -151,10 +151,12 @@ typedef struct vissprite_s
 	INT32 x1, x2;
 
 	fixed_t gx, gy; // for line side calculation
-	fixed_t gz, gzt; // global bottom/top for silhouette clipping and sorting with 3D floors
+	fixed_t gz, gzt; // global bottom/top for silhouette clipping
+	fixed_t pz, pzt; // physical bottom/top for sorting with 3D floors
 
 	fixed_t startfrac; // horizontal position of x1
-	fixed_t scale;
+	fixed_t xscale, scale; // projected horizontal and vertical scales
+	fixed_t thingscale; // the object's scale
 	fixed_t sortscale; // sortscale only differs from scale for paper sprites, floor sprites, and MF2_LINKDRAW
 	fixed_t sortsplat; // the sortscale from behind the floor sprite
 	fixed_t scalestep; // only for paper sprites, 0 otherwise
@@ -163,6 +165,12 @@ typedef struct vissprite_s
 
 	angle_t centerangle; // for paper sprites
 
+	// 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
 		INT32 offset; // The center of the shearing location offset from x1
@@ -182,10 +190,10 @@ typedef struct vissprite_s
 
 	extracolormap_t *extra_colormap; // global colormaps
 
-	fixed_t xscale;
+	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.
-	sector_t *sector; // The sector containing the thing.
 	INT16 sz, szt;
 
 	spritecut_e cut;
diff --git a/src/s_sound.c b/src/s_sound.c
index 36bd454c104b02867c29d3f16f22f639ed06dc97..30f24236923a45200f40ccddd5eff680f4e98c99 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -1033,11 +1033,9 @@ void S_SetSfxVolume(INT32 volume)
 
 void S_ClearSfx(void)
 {
-#ifndef DJGPPDOS
 	size_t i;
 	for (i = 1; i < NUMSFX; i++)
 		I_FreeSfx(S_sfx + i);
-#endif
 }
 
 static void S_StopChannel(INT32 cnum)
@@ -1354,28 +1352,6 @@ void S_InitSfxChannels(INT32 sfxVolume)
 /// Music
 /// ------------------------
 
-#ifdef MUSICSLOT_COMPATIBILITY
-const char *compat_special_music_slots[16] =
-{
-	"_title", // 1036  title screen
-	"_intro", // 1037  intro
-	"_clear", // 1038  level clear
-	"_inv", // 1039  invincibility
-	"_shoes",  // 1040  super sneakers
-	"_minv", // 1041  Mario invincibility
-	"_drown",  // 1042  drowning
-	"_gover", // 1043  game over
-	"_1up", // 1044  extra life
-	"_conti", // 1045  continue screen
-	"_super", // 1046  Super Sonic
-	"_chsel", // 1047  character select
-	"_creds", // 1048  credits
-	"_inter", // 1049  Race Results
-	"_stjr",   // 1050  Sonic Team Jr. Presents
-	""
-};
-#endif
-
 static char      music_name[7]; // up to 6-character name
 static void      *music_data;
 static UINT16    music_flags;
@@ -2143,7 +2119,7 @@ boolean S_RecallMusic(UINT16 status, boolean fromfirst)
 static lumpnum_t S_GetMusicLumpNum(const char *mname)
 {
 	boolean midipref = cv_musicpref.value;
-	
+
 	if (S_PrefAvailable(midipref, mname))
 		return W_GetNumForName(va(midipref ? "d_%s":"o_%s", mname));
 	else if (S_PrefAvailable(!midipref, mname))
@@ -2262,6 +2238,16 @@ static void S_ChangeMusicToQueue(void)
 void S_ChangeMusicEx(const char *mmusic, UINT16 mflags, boolean looping, UINT32 position, UINT32 prefadems, UINT32 fadeinms)
 {
 	char newmusic[7];
+
+	struct MusicChange hook_param = {
+		newmusic,
+		&mflags,
+		&looping,
+		&position,
+		&prefadems,
+		&fadeinms
+	};
+
 	boolean currentmidi = (I_SongType() == MU_MID || I_SongType() == MU_MID_EX);
 	boolean midipref = cv_musicpref.value;
 
@@ -2269,7 +2255,7 @@ void S_ChangeMusicEx(const char *mmusic, UINT16 mflags, boolean looping, UINT32
 		return;
 
 	strncpy(newmusic, mmusic, 7);
-	if (LUAh_MusicChange(music_name, newmusic, &mflags, &looping, &position, &prefadems, &fadeinms))
+	if (LUA_HookMusicChange(music_name, &hook_param))
 		return;
 	newmusic[6] = 0;
 
@@ -2291,7 +2277,7 @@ void S_ChangeMusicEx(const char *mmusic, UINT16 mflags, boolean looping, UINT32
 		I_FadeSong(0, prefadems, S_ChangeMusicToQueue);
 		return;
 	}
-	else if (strnicmp(music_name, newmusic, 6) || (mflags & MUSIC_FORCERESET) || 
+	else if (strnicmp(music_name, newmusic, 6) || (mflags & MUSIC_FORCERESET) ||
 		(midipref != currentmidi && S_PrefAvailable(midipref, newmusic)))
  	{
 		CONS_Debug(DBG_DETAILED, "Now playing song %s\n", newmusic);
@@ -2465,7 +2451,7 @@ void S_StartEx(boolean reset)
 static void Command_Tunes_f(void)
 {
 	const char *tunearg;
-	UINT16 tunenum, track = 0;
+	UINT16 track = 0;
 	UINT32 position = 0;
 	const size_t argc = COM_Argc();
 
@@ -2481,7 +2467,6 @@ static void Command_Tunes_f(void)
 	}
 
 	tunearg = COM_Argv(1);
-	tunenum = (UINT16)atoi(tunearg);
 	track = 0;
 
 	if (!strcasecmp(tunearg, "-show"))
@@ -2500,24 +2485,14 @@ static void Command_Tunes_f(void)
 		tunearg = mapheaderinfo[gamemap-1]->musname;
 		track = mapheaderinfo[gamemap-1]->mustrack;
 	}
-	else if (!tunearg[2] && toupper(tunearg[0]) >= 'A' && toupper(tunearg[0]) <= 'Z')
-		tunenum = (UINT16)M_MapNumber(tunearg[0], tunearg[1]);
 
-	if (tunenum && tunenum >= 1036)
-	{
-		CONS_Alert(CONS_NOTICE, M_GetText("Valid music slots are 1 to 1035.\n"));
-		return;
-	}
-	if (!tunenum && strlen(tunearg) > 6) // This is automatic -- just show the error just in case
+	if (strlen(tunearg) > 6) // This is automatic -- just show the error just in case
 		CONS_Alert(CONS_NOTICE, M_GetText("Music name too long - truncated to six characters.\n"));
 
 	if (argc > 2)
 		track = (UINT16)atoi(COM_Argv(2))-1;
 
-	if (tunenum)
-		snprintf(mapmusname, 7, "%sM", G_BuildMapName(tunenum));
-	else
-		strncpy(mapmusname, tunearg, 7);
+	strncpy(mapmusname, tunearg, 7);
 
 	if (argc > 4)
 		position = (UINT32)atoi(COM_Argv(4));
diff --git a/src/s_sound.h b/src/s_sound.h
index 4ac3c70bf0d4a76f759e166ee0db3c682894ee5f..8fcb816d906accd7f63b6756d8915327a97c4bc0 100644
--- a/src/s_sound.h
+++ b/src/s_sound.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -265,6 +265,16 @@ boolean S_RecallMusic(UINT16 status, boolean fromfirst);
 // Music Playback
 //
 
+/* this is for the sake of the hook */
+struct MusicChange {
+	char    * newname;
+	UINT16  * mflags;
+	boolean * looping;
+	UINT32  * position;
+	UINT32  * prefadems;
+	UINT32  * fadeinms;
+};
+
 // Start music track, arbitrary, given its name, and set whether looping
 // note: music flags 12 bits for tracknum (gme, other formats with more than one track)
 //       13-15 aren't used yet
@@ -319,10 +329,4 @@ void S_StopSoundByNum(sfxenum_t sfxnum);
 #define S_StartScreamSound S_StartSound
 #endif
 
-#ifdef MUSICSLOT_COMPATIBILITY
-// For compatibility with code/scripts relying on older versions
-// This is a list of all the "special" slot names and their associated numbers
-extern const char *compat_special_music_slots[16];
-#endif
-
 #endif
diff --git a/src/screen.c b/src/screen.c
index 9d36eee39cb1da8c2ce14e5fc1392344a9dbefc9..770f1c8026aaf4fcb5dd9df97da55271f717b547 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -33,6 +33,11 @@
 #include "s_sound.h" // ditto
 #include "g_game.h" // ditto
 #include "p_local.h" // P_AutoPause()
+#ifdef HWRENDER
+#include "hardware/hw_main.h"
+#include "hardware/hw_light.h"
+#include "hardware/hw_model.h"
+#endif
 
 
 #if defined (USEASM) && !defined (NORUSEASM)//&& (!defined (_MSC_VER) || (_MSC_VER <= 1200))
@@ -217,7 +222,7 @@ void SCR_SetMode(void)
 
 	// Set the video mode in the video interface.
 	if (setmodeneeded)
-		VID_SetMode(--setmodeneeded);
+		VID_SetMode(setmodeneeded - 1);
 
 	V_SetPalette(0);
 
@@ -423,6 +428,10 @@ void SCR_ChangeRenderer(void)
 			CONS_Alert(CONS_ERROR, "OpenGL never loaded\n");
 		return;
 	}
+
+	if (rendermode == render_opengl && (vid.glstate == VID_GL_LIBRARY_LOADED)) // Clear these out before switching to software
+		HWR_ClearAllTextures();
+
 #endif
 
 	// Set the new render mode
diff --git a/src/screen.h b/src/screen.h
index e4944775d952249c785c14262960daa8f58bc796..67880e2b964dc16a7693d754a6646bd031f14c04 100644
--- a/src/screen.h
+++ b/src/screen.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/sdl/CMakeLists.txt b/src/sdl/CMakeLists.txt
index a7f015c869c783537dccfff26bad85de9c68f5f5..4f19d93dff20c791d6b031fa18c1ab9f5efe0c71 100644
--- a/src/sdl/CMakeLists.txt
+++ b/src/sdl/CMakeLists.txt
@@ -21,46 +21,25 @@ if(${SRB2_CONFIG_SDL2_USEMIXER})
 	endif()
 	if(${SDL2_MIXER_FOUND})
 		set(SRB2_HAVE_MIXER ON)
-		set(SRB2_SDL2_SOUNDIMPL mixer_sound.c)
+		target_sources(SRB2SDL2 PRIVATE mixer_sound.c)
 	else()
 		message(WARNING "You specified that SDL2_mixer is available, but it was not found. Falling back to sdl sound.")
-		set(SRB2_SDL2_SOUNDIMPL sdl_sound.c)
+		target_sources(SRB2SDL2 PRIVATE sdl_sound.c)
 	endif()
 elseif(${MIXERX_FOUND})
-	set(SRB2_SDL2_SOUNDIMPL mixer_sound.c)
+	target_sources(SRB2SDL2 PRIVATE mixer_sound.c)
 else()
-	set(SRB2_SDL2_SOUNDIMPL sdl_sound.c)
+	target_sources(SRB2SDL2 PRIVATE sdl_sound.c)
 endif()
 
-set(SRB2_SDL2_SOURCES
-	dosstr.c
-	endtxt.c
-	hwsym_sdl.c
-	i_main.c
-	i_net.c
-	i_system.c
-	i_ttf.c
-	i_video.c
-	#IMG_xpm.c
-	ogl_sdl.c
+target_sourcefile(c)
 
-	${SRB2_SDL2_SOUNDIMPL}
-)
-
-set(SRB2_SDL2_HEADERS
-	endtxt.h
-	hwsym_sdl.h
-	i_ttf.h
-	ogl_sdl.h
-	sdlmain.h
-)
+target_sources(SRB2SDL2 PRIVATE ogl_sdl.c)
 
 if(${SRB2_CONFIG_HAVE_THREADS})
-	set(SRB2_SDL2_SOURCES ${SRB2_SDL2_SOURCES} i_threads.c)
+	target_sources(SRB2SDL2 PRIVATE i_threads.c)
 endif()
 
-source_group("Interface Code" FILES ${SRB2_SDL2_SOURCES} ${SRB2_SDL2_HEADERS})
-
 # Dependency
 if(${SRB2_CONFIG_USE_INTERNAL_LIBRARIES})
 	set(SDL2_FOUND ON)
@@ -76,79 +55,29 @@ else()
 endif()
 
 if(${SDL2_FOUND})
-	set(SRB2_SDL2_TOTAL_SOURCES
-		${SRB2_CORE_SOURCES}
-		${SRB2_CORE_HEADERS}
-		${SRB2_PNG_SOURCES}
-		${SRB2_PNG_HEADERS}
-		${SRB2_CORE_RENDER_SOURCES}
-		${SRB2_CORE_GAME_SOURCES}
-		${SRB2_LUA_SOURCES}
-		${SRB2_LUA_HEADERS}
-		${SRB2_BLUA_SOURCES}
-		${SRB2_BLUA_HEADERS}
-		${SRB2_SDL2_SOURCES}
-		${SRB2_SDL2_HEADERS}
-	)
-
-	source_group("Main" FILES ${SRB2_CORE_SOURCES} ${SRB2_CORE_HEADERS}
-		${SRB2_PNG_SOURCES} ${SRB2_PNG_HEADERS})
-	source_group("Renderer" FILES ${SRB2_CORE_RENDER_SOURCES})
-	source_group("Game" FILES ${SRB2_CORE_GAME_SOURCES})
-	source_group("Assembly" FILES ${SRB2_ASM_SOURCES} ${SRB2_NASM_SOURCES})
-	source_group("LUA" FILES ${SRB2_LUA_SOURCES} ${SRB2_LUA_HEADERS})
-	source_group("LUA\\Interpreter" FILES ${SRB2_BLUA_SOURCES} ${SRB2_BLUA_HEADERS})
-
-	if(${SRB2_CONFIG_HWRENDER})
-		set(SRB2_SDL2_TOTAL_SOURCES ${SRB2_SDL2_TOTAL_SOURCES}
-			${SRB2_HWRENDER_SOURCES}
-			${SRB2_HWRENDER_HEADERS}
-			${SRB2_R_OPENGL_SOURCES}
-			${SRB2_R_OPENGL_HEADERS}
-		)
-
-		source_group("Hardware" FILES ${SRB2_HWRENDER_SOURCES} ${SRB2_HWRENDER_HEADERS})
-		source_group("Hardware\\OpenGL Renderer" FILES ${SRB2_R_OPENGL_SOURCES} ${SRB2_R_OPENGL_HEADERS})
-	endif()
-
 	if(${SRB2_USEASM})
-		set(SRB2_SDL2_TOTAL_SOURCES ${SRB2_SDL2_TOTAL_SOURCES}
-			${SRB2_NASM_SOURCES}
-		)
-		if(MSVC)
-			set(SRB2_SDL2_TOTAL_SOURCES ${SRB2_SDL2_TOTAL_SOURCES}
-				${SRB2_NASM_OBJECTS}
-			)
-			set_source_files_properties(${SRB2_NASM_OBJECTS} PROPERTIES GENERATED ON)
-		else()
-			list(APPEND SRB2_SDL2_TOTAL_SOURCES ${SRB2_ASM_SOURCES})
-			set_source_files_properties(${SRB2_ASM_SOURCES} PROPERTIES LANGUAGE C)
-			set_source_files_properties(${SRB2_ASM_SOURCES} PROPERTIES COMPILE_FLAGS "-x assembler-with-cpp")
-		endif()
+		set_source_files_properties(${SRB2_ASM_SOURCES} PROPERTIES LANGUAGE C)
+		set_source_files_properties(${SRB2_ASM_SOURCES} PROPERTIES COMPILE_FLAGS "-x assembler-with-cpp")
 	endif()
 
 	if(${CMAKE_SYSTEM} MATCHES Windows)
-		set(SRB2_SDL2_TOTAL_SOURCES ${SRB2_SDL2_TOTAL_SOURCES}
-			${CMAKE_SOURCE_DIR}/src/win32/win_dbg.c
-			${CMAKE_SOURCE_DIR}/src/win32/Srb2win.rc
-		)
+		target_sources(SRB2SDL2 PRIVATE
+			../win32/win_dbg.c
+			../win32/Srb2win.rc)
 	endif()
 
 	if(${CMAKE_SYSTEM} MATCHES Darwin)
 		set(MACOSX_BUNDLE_ICON_FILE Srb2mac.icns)
 		set_source_files_properties(macosx/Srb2mac.icns PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
-		set(SRB2_SDL2_MAC_SOURCES
+		target_sources(SRB2SDL2 PRIVATE
 			macosx/mac_alert.c
 			macosx/mac_alert.h
 			macosx/mac_resources.c
 			macosx/mac_resources.h
 			macosx/Srb2mac.icns
 		)
-		source_group("Interface Code\\OSX Compatibility" FILES ${SRB2_SDL2_MAC_SOURCES})
-		set(SRB2_SDL2_TOTAL_SOURCES ${SRB2_SDL2_TOTAL_SOURCES} ${SRB2_SDL2_MAC_SOURCES})
 	endif()
 
-	add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32 ${SRB2_SDL2_TOTAL_SOURCES})
 	if(${CMAKE_SYSTEM} MATCHES Windows)
 		set_target_properties(SRB2SDL2 PROPERTIES OUTPUT_NAME srb2win)
 	elseif(${CMAKE_SYSTEM} MATCHES Linux)
@@ -205,18 +134,6 @@ if(${SDL2_FOUND})
 			set(ASM_ASSEMBLER_OBJFORMAT ${CMAKE_ASM_NASM_OBJECT_FORMAT})
 			set_source_files_properties(${SRB2_NASM_SOURCES} LANGUAGE ASM_NASM)
 		endif()
-
-		if(MSVC)
-			# using assembler with msvc doesn't work, must do it manually
-			foreach(ASMFILE ${SRB2_NASM_SOURCES})
-				get_filename_component(ASMFILE_NAME ${ASMFILE} NAME_WE)
-				set(ASMFILE_NAME ${ASMFILE_NAME}.obj)
-				add_custom_command(TARGET SRB2SDL2 PRE_LINK
-					COMMAND ${ASM_ASSEMBLER_TEMP} ARGS -f ${ASM_ASSEMBLER_OBJFORMAT} -o ${CMAKE_CURRENT_BINARY_DIR}/${ASMFILE_NAME} ${ASMFILE}
-					COMMENT "assemble ${ASMFILE_NAME}."
-				)
-			endforeach()
-		endif()
 	endif()
 
 	set_target_properties(SRB2SDL2 PROPERTIES VERSION ${SRB2_VERSION})
@@ -230,31 +147,6 @@ if(${SDL2_FOUND})
 		)
 	endif()
 
-	if(MSVC)
-		if(${SRB2_CONFIG_USE_INTERNAL_LIBRARIES})
-			set(SDL2_MAIN_FOUND ON)
-			if(${SRB2_SYSTEM_BITS} EQUAL 64)
-				set(SDL2_MAIN_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/libs/SDL2/x86_64-w64-mingw32/include/SDL2)
-				set(SDL2_MAIN_LIBRARIES "-L${CMAKE_SOURCE_DIR}/libs/SDL2/x86_64-w64-mingw32/lib -lSDL2main")
-			else() # 32-bit
-				set(SDL2_MAIN_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/libs/SDL2/i686-w64-mingw32/include/SDL2)
-				set(SDL2_MAIN_LIBRARIES "-L${CMAKE_SOURCE_DIR}/libs/SDL2/i686-w64-mingw32/lib -lSDL2main")
-			endif()
-		else()
-			find_package(SDL2_MAIN REQUIRED)
-		endif()
-		target_link_libraries(SRB2SDL2 PRIVATE
-			${SDL2_MAIN_LIBRARIES}
-		)
-		target_compile_options(SRB2SDL2 PRIVATE
-			/Umain
-			/D_CRT_SECURE_NO_WARNINGS # something about string functions.
-			/D_CRT_NONSTDC_NO_DEPRECATE
-			/DSDLMAIN
-			/D_WINSOCK_DEPRECATED_NO_WARNINGS # Don't care
-		)
-	endif()
-
 	target_include_directories(SRB2SDL2 PRIVATE
 		${SDL2_INCLUDE_DIRS}
 		${SDL2_MIXER_INCLUDE_DIRS}
diff --git a/src/sdl/MakeNIX.cfg b/src/sdl/MakeNIX.cfg
deleted file mode 100644
index 47c944eb5f21e8451c55374b212220fa09072360..0000000000000000000000000000000000000000
--- a/src/sdl/MakeNIX.cfg
+++ /dev/null
@@ -1,74 +0,0 @@
-#
-# sdl/makeNIX.cfg for SRB2/?nix
-#
-
-#Valgrind support
-ifdef VALGRIND
-VALGRIND_PKGCONFIG?=valgrind
-VALGRIND_CFLAGS?=$(shell $(PKG_CONFIG) $(VALGRIND_PKGCONFIG) --cflags)
-VALGRIND_LDFLAGS?=$(shell $(PKG_CONFIG) $(VALGRIND_PKGCONFIG) --libs)
-ZDEBUG=1
-LIBS+=$(VALGRIND_LDFLAGS)
-ifdef GCC46
-WFLAGS+=-Wno-error=unused-but-set-variable
-WFLAGS+=-Wno-unused-but-set-variable
-endif
-endif
-
-#
-#here is GNU/Linux and other
-#
-
-	OPTS=-DUNIXCOMMON
-
-	#LDFLAGS = -L/usr/local/lib
-	LIBS=-lm
-ifdef LINUX
-	LIBS+=-lrt
-ifdef NOTERMIOS
-	OPTS+=-DNOTERMIOS
-endif
-endif
-
-ifdef LINUX64
-	OPTS+=-DLINUX64
-endif
-
-#
-#here is Solaris
-#
-ifdef SOLARIS
-	NOIPX=1
-	NOASM=1
-	OPTS+=-DSOLARIS -DINADDR_NONE=INADDR_ANY -DBSD_COMP
-	OPTS+=-I/usr/local/include -I/opt/sfw/include
-	LDFLAGS+=-L/opt/sfw/lib
-	LIBS+=-lsocket -lnsl
-endif
-
-#
-#here is FreeBSD
-#
-ifdef FREEBSD
-	OPTS+=-DLINUX -DFREEBSD -I/usr/X11R6/include
-	SDL_CONFIG?=sdl11-config
-	LDFLAGS+=-L/usr/X11R6/lib
-	LIBS+=-lipx -lkvm
-endif
-
-#
-#here is Mac OS X
-#
-ifdef MACOSX
-	OBJS+=$(OBJDIR)/mac_resources.o
-	OBJS+=$(OBJDIR)/mac_alert.o
-	LIBS+=-framework CoreFoundation
-endif
-
-ifndef NOHW
-	OPTS+=-I/usr/X11R6/include
-	LDFLAGS+=-L/usr/X11R6/lib
-endif
-
-	# name of the exefile
-	EXENAME?=lsdl2srb2
diff --git a/src/sdl/Makefile.cfg b/src/sdl/Makefile.cfg
deleted file mode 100644
index 45d0d6ba75a666cba5e4e2c3a3f9704987705cb6..0000000000000000000000000000000000000000
--- a/src/sdl/Makefile.cfg
+++ /dev/null
@@ -1,125 +0,0 @@
-#
-# sdl/makefile.cfg for SRB2/SDL
-#
-
-#
-#SDL...., *looks at Alam*, THIS IS A MESS!
-#
-
-ifdef UNIXCOMMON
-include sdl/MakeNIX.cfg
-endif
-
-ifdef PANDORA
-include sdl/SRB2Pandora/Makefile.cfg
-endif #ifdef PANDORA
-
-ifdef CYGWIN32
-include sdl/MakeCYG.cfg
-endif #ifdef CYGWIN32
-
-ifdef SDL_PKGCONFIG
-SDL_CFLAGS?=$(shell $(PKG_CONFIG) $(SDL_PKGCONFIG) --cflags)
-SDL_LDFLAGS?=$(shell $(PKG_CONFIG) $(SDL_PKGCONFIG) --libs)
-else
-ifdef PREFIX
-	SDL_CONFIG?=$(PREFIX)-sdl2-config
-else
-	SDL_CONFIG?=sdl2-config
-endif
-
-ifdef STATIC
-	SDL_CFLAGS?=$(shell $(SDL_CONFIG) --cflags)
-	SDL_LDFLAGS?=$(shell $(SDL_CONFIG) --static-libs)
-else
-	SDL_CFLAGS?=$(shell $(SDL_CONFIG) --cflags)
-	SDL_LDFLAGS?=$(shell $(SDL_CONFIG) --libs)
-endif
-endif
-
-
-	#use the x86 asm code
-ifndef CYGWIN32
-ifndef NOASM
-	USEASM=1
-endif
-endif
-
-	OBJS+=$(OBJDIR)/i_video.o $(OBJDIR)/dosstr.o $(OBJDIR)/endtxt.o $(OBJDIR)/hwsym_sdl.o
-
-	OPTS+=-DDIRECTFULLSCREEN -DHAVE_SDL
-
-ifndef NOHW
-	OBJS+=$(OBJDIR)/r_opengl.o $(OBJDIR)/ogl_sdl.o
-endif
-
-ifdef NOMIXER
-	i_sound_o=$(OBJDIR)/sdl_sound.o
-else
-	i_sound_o=$(OBJDIR)/mixer_sound.o
-	OPTS+=-DHAVE_MIXER
-ifdef HAVE_MIXERX
-	OPTS+=-DHAVE_MIXERX
-	SDL_LDFLAGS+=-lSDL2_mixer_ext
-else
-	SDL_LDFLAGS+=-lSDL2_mixer
-endif
-endif
-
-ifndef NOTHREADS
-	OPTS+=-DHAVE_THREADS
-	OBJS+=$(OBJDIR)/i_threads.o
-endif
-
-ifdef SDL_TTF
-	OPTS+=-DHAVE_TTF
-	SDL_LDFLAGS+=-lSDL2_ttf -lfreetype -lz
-	OBJS+=$(OBJDIR)/i_ttf.o
-endif
-
-ifdef SDL_IMAGE
-	OPTS+=-DHAVE_IMAGE
-	SDL_LDFLAGS+=-lSDL2_image
-endif
-
-ifdef SDL_NET
-	OPTS+=-DHAVE_SDLNET
-	SDL_LDFLAGS+=-lSDL2_net
-endif
-
-ifdef MINGW
-ifndef NOSDLMAIN
-	SDLMAIN=1
-endif
-endif
-
-ifdef SDLMAIN
-	OPTS+=-DSDLMAIN
-else
-ifdef MINGW
-	SDL_CFLAGS+=-Umain
-	SDL_LDFLAGS+=-mconsole
-endif
-endif
-
-ifndef NOHW
-ifdef OPENAL
-ifdef MINGW
-	LIBS:=-lopenal32 $(LIBS)
-else
-	LIBS:=-lopenal $(LIBS)
-endif
-else
-ifdef MINGW
-ifdef DS3D
-	LIBS:=-ldsound -luuid $(LIBS)
-endif
-endif
-endif
-endif
-
-CFLAGS+=$(SDL_CFLAGS)
-LIBS:=$(SDL_LDFLAGS) $(LIBS)
-ifdef STATIC
-	LIBS+=$(shell $(SDL_CONFIG) --static-libs)
-endif
diff --git a/src/sdl/Sourcefile b/src/sdl/Sourcefile
new file mode 100644
index 0000000000000000000000000000000000000000..82d5ce0734eb30684cee1ee875f8e94e481bd5ad
--- /dev/null
+++ b/src/sdl/Sourcefile
@@ -0,0 +1,7 @@
+i_net.c
+i_system.c
+i_main.c
+i_video.c
+dosstr.c
+endtxt.c
+hwsym_sdl.c
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index d46a4af2b0d89ea1dc93b0573e4ef1d6a32665b4..105e1def868b96e66c05302674a9a93e4f83e159 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -247,6 +247,7 @@
     <ClInclude Include="..\i_tcp.h" />
     <ClInclude Include="..\i_video.h" />
     <ClInclude Include="..\keys.h" />
+    <ClInclude Include="..\libdivide.h" />
     <ClInclude Include="..\lua_hook.h" />
     <ClInclude Include="..\lua_hud.h" />
     <ClInclude Include="..\lua_libs.h" />
@@ -406,6 +407,7 @@
     <ClCompile Include="..\lua_hooklib.c" />
     <ClCompile Include="..\lua_hudlib.c" />
     <ClCompile Include="..\lua_infolib.c" />
+    <ClCompile Include="..\lua_inputlib.c" />
     <ClCompile Include="..\lua_maplib.c" />
     <ClCompile Include="..\lua_mathlib.c" />
     <ClCompile Include="..\lua_mobjlib.c" />
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj.filters b/src/sdl/Srb2SDL-vc10.vcxproj.filters
index adae2f446dbde8267e375bb794eacc50bed9c663..4048903976b57317d6be3ac77ff0f87a38b17f8c 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj.filters
+++ b/src/sdl/Srb2SDL-vc10.vcxproj.filters
@@ -402,6 +402,9 @@
     <ClInclude Include="..\tables.h">
       <Filter>P_Play</Filter>
     </ClInclude>
+    <ClInclude Include="..\libdivide.h">
+      <Filter>R_Rend</Filter>
+    </ClInclude>
     <ClInclude Include="..\r_bsp.h">
       <Filter>R_Rend</Filter>
     </ClInclude>
@@ -720,6 +723,9 @@
     <ClCompile Include="..\lua_infolib.c">
       <Filter>LUA</Filter>
     </ClCompile>
+    <ClCompile Include="..\lua_inputlib.c">
+      <Filter>LUA</Filter>
+    </ClCompile>
     <ClCompile Include="..\lua_maplib.c">
       <Filter>LUA</Filter>
     </ClCompile>
diff --git a/src/sdl/hwsym_sdl.c b/src/sdl/hwsym_sdl.c
index 3985086626c4d17f157a63668627d210737d1546..96e3d7d6926ef23771c8dcf489b4d8d2a16c0a1c 100644
--- a/src/sdl/hwsym_sdl.c
+++ b/src/sdl/hwsym_sdl.c
@@ -90,7 +90,6 @@ void *hwSym(const char *funcName,void *handle)
 	GETFUNC(ReadRect);
 	GETFUNC(GClipRect);
 	GETFUNC(ClearMipMapCache);
-	GETFUNC(ClearCacheList);
 	GETFUNC(SetSpecialState);
 	GETFUNC(GetTextureUsed);
 	GETFUNC(DrawModel);
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index 10c0747bf18b4e8fc725f4cfd3984a9183e79c56..ccec37093698edbcc7865702fea0138db5996749 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -5,7 +5,7 @@
 //
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Portions Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 2014-2020 by Sonic Team Junior.
+// Copyright (C) 2014-2021 by Sonic Team Junior.
 //
 // This program is free software; you can redistribute it and/or
 // modify it under the terms of the GNU General Public License
@@ -54,12 +54,6 @@ typedef LPVOID (WINAPI *p_MapViewOfFile) (HANDLE, DWORD, DWORD, DWORD, SIZE_T);
 #include <fcntl.h>
 #endif
 
-#if defined (_WIN32)
-DWORD TimeFunction(int requested_frequency);
-#else
-int TimeFunction(int requested_frequency);
-#endif
-
 #include <stdio.h>
 #ifdef _WIN32
 #include <conio.h>
@@ -108,7 +102,7 @@ int TimeFunction(int requested_frequency);
 #endif
 #endif
 
-#if (defined (__unix__) && !defined (_MSDOS)) || (defined (UNIXCOMMON) && !defined(__APPLE__))
+#if defined (__unix__) || (defined (UNIXCOMMON) && !defined (__APPLE__))
 #include <errno.h>
 #include <sys/wait.h>
 #define NEWSIGNALHANDLER
@@ -143,6 +137,12 @@ int TimeFunction(int requested_frequency);
 #include <errno.h>
 #endif
 
+#if defined (__unix__) || defined(__APPLE__) || defined (UNIXCOMMON)
+#include <execinfo.h>
+#include <time.h>
+#define UNIXBACKTRACE
+#endif
+
 // Locations for searching the srb2.pk3
 #if defined (__unix__) || defined(__APPLE__) || defined (UNIXCOMMON)
 #define DEFAULTWADLOCATION1 "/usr/local/share/games/SRB2"
@@ -244,6 +244,71 @@ SDL_bool framebuffer = SDL_FALSE;
 
 UINT8 keyboard_started = false;
 
+#ifdef UNIXBACKTRACE
+#define STDERR_WRITE(string) if (fd != -1) I_OutputMsg("%s", string)
+#define CRASHLOG_WRITE(string) if (fd != -1) write(fd, string, strlen(string))
+#define CRASHLOG_STDERR_WRITE(string) \
+	if (fd != -1)\
+		write(fd, string, strlen(string));\
+	I_OutputMsg("%s", string)
+
+static void write_backtrace(INT32 signal)
+{
+	int fd = -1;
+	size_t size;
+	time_t rawtime;
+	struct tm timeinfo;
+
+	enum { BT_SIZE = 1024, STR_SIZE = 32 };
+	void *array[BT_SIZE];
+	char timestr[STR_SIZE];
+
+	const char *error = "An error occurred within SRB2! Send this stack trace to someone who can help!\n";
+	const char *error2 = "(Or find crash-log.txt in your SRB2 directory.)\n"; // Shown only to stderr.
+
+	fd = open(va("%s" PATHSEP "%s", srb2home, "crash-log.txt"), O_CREAT|O_APPEND|O_RDWR, S_IRUSR|S_IWUSR);
+
+	if (fd == -1)
+		I_OutputMsg("\nWARNING: Couldn't open crash log for writing! Make sure your permissions are correct. Please save the below report!\n");
+
+	// Get the current time as a string.
+	time(&rawtime);
+	localtime_r(&rawtime, &timeinfo);
+	strftime(timestr, STR_SIZE, "%a, %d %b %Y %T %z", &timeinfo);
+
+	CRASHLOG_WRITE("------------------------\n"); // Nice looking seperator
+
+	CRASHLOG_STDERR_WRITE("\n"); // Newline to look nice for both outputs.
+	CRASHLOG_STDERR_WRITE(error); // "Oops, SRB2 crashed" message
+	STDERR_WRITE(error2); // Tell the user where the crash log is.
+
+	// Tell the log when we crashed.
+	CRASHLOG_WRITE("Time of crash: ");
+	CRASHLOG_WRITE(timestr);
+	CRASHLOG_WRITE("\n");
+
+	// Give the crash log the cause and a nice 'Backtrace:' thing
+	// The signal is given to the user when the parent process sees we crashed.
+	CRASHLOG_WRITE("Cause: ");
+	CRASHLOG_WRITE(strsignal(signal));
+	CRASHLOG_WRITE("\n"); // Newline for the signal name
+
+	CRASHLOG_STDERR_WRITE("\nBacktrace:\n");
+
+	// Flood the output and log with the backtrace
+	size = backtrace(array, BT_SIZE);
+	backtrace_symbols_fd(array, size, fd);
+	backtrace_symbols_fd(array, size, STDERR_FILENO);
+
+	CRASHLOG_WRITE("\n"); // Write another newline to the log so it looks nice :)
+
+	close(fd);
+}
+#undef STDERR_WRITE
+#undef CRASHLOG_WRITE
+#undef CRASHLOG_STDERR_WRITE
+#endif // UNIXBACKTRACE
+
 static void I_ReportSignal(int num, int coredumped)
 {
 	//static char msg[] = "oh no! back to reality!\r\n";
@@ -303,6 +368,9 @@ FUNCNORETURN static ATTRNORETURN void signal_handler(INT32 num)
 {
 	D_QuitNetGame(); // Fix server freezes
 	CL_AbortDownloadResume();
+#ifdef UNIXBACKTRACE
+	write_backtrace(num);
+#endif
 	I_ReportSignal(num, 0);
 	I_ShutdownSystem();
 	signal(num, SIG_DFL);               //default signal action
@@ -482,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;
 
@@ -504,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
 	{
@@ -512,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;
 }
@@ -558,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);
@@ -584,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)
@@ -693,6 +761,28 @@ static void I_RegisterSignals (void)
 #endif
 }
 
+#ifdef NEWSIGNALHANDLER
+static void signal_handler_child(INT32 num)
+{
+#ifdef UNIXBACKTRACE
+	write_backtrace(num);
+#endif
+
+	signal(num, SIG_DFL);               //default signal action
+	raise(num);
+}
+
+static void I_RegisterChildSignals(void)
+{
+	// If these defines don't exist,
+	// then compilation would have failed above us...
+	signal(SIGILL , signal_handler_child);
+	signal(SIGSEGV , signal_handler_child);
+	signal(SIGABRT , signal_handler_child);
+	signal(SIGFPE , signal_handler_child);
+}
+#endif
+
 //
 //I_OutputMsg
 //
@@ -827,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;
 		}
 	}
@@ -887,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);
 	}
 
@@ -910,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);
 	}
 
@@ -922,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
@@ -959,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);
 			}
 		}
@@ -990,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);
 			}
 		}
@@ -1002,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;
@@ -1020,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
 		{
@@ -1042,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);
 	}
@@ -1157,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);
 	}
 
@@ -1180,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);
 	}
 
@@ -1192,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
@@ -1231,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);
 			}
 		}
@@ -1262,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);
 			}
 		}
@@ -1274,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;
@@ -1290,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
 		{
@@ -1314,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);
 	}
@@ -1714,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;
 					}
@@ -1724,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);
 			}
 		}
@@ -1777,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);
 	}
 
@@ -1868,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);
 			}
 	}
@@ -1876,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;
 
@@ -2044,112 +2134,42 @@ ticcmd_t *I_BaseTiccmd2(void)
 	return &emptycmd2;
 }
 
-#if defined (_WIN32)
-static HMODULE winmm = NULL;
-static DWORD starttickcount = 0; // hack for win2k time bug
-static p_timeGetTime pfntimeGetTime = NULL;
-
-// ---------
-// I_GetTime
-// Use the High Resolution Timer if available,
-// else use the multimedia timer which has 1 millisecond precision on Windowz 95,
-// but lower precision on Windows NT
-// ---------
-
-DWORD TimeFunction(int requested_frequency)
-{
-	DWORD newtics = 0;
-	// this var acts as a multiplier if sub-millisecond precision is asked but is not available
-	int excess_frequency = requested_frequency / 1000;
-
-	if (!starttickcount) // high precision timer
-	{
-		LARGE_INTEGER currtime; // use only LowPart if high resolution counter is not available
-		static LARGE_INTEGER basetime = {{0, 0}};
-
-		// use this if High Resolution timer is found
-		static LARGE_INTEGER frequency;
-
-		if (!basetime.LowPart)
-		{
-			if (!QueryPerformanceFrequency(&frequency))
-				frequency.QuadPart = 0;
-			else
-				QueryPerformanceCounter(&basetime);
-		}
-
-		if (frequency.LowPart && QueryPerformanceCounter(&currtime))
-		{
-			newtics = (INT32)((currtime.QuadPart - basetime.QuadPart) * requested_frequency
-				/ frequency.QuadPart);
-		}
-		else if (pfntimeGetTime)
-		{
-			currtime.LowPart = pfntimeGetTime();
-			if (!basetime.LowPart)
-				basetime.LowPart = currtime.LowPart;
-			if (requested_frequency > 1000)
-				newtics = currtime.LowPart - basetime.LowPart * excess_frequency;
-			else
-				newtics = (currtime.LowPart - basetime.LowPart)/(1000/requested_frequency);
-		}
-	}
-	else
-	{
-		if (requested_frequency > 1000)
-			newtics = (GetTickCount() - starttickcount) * excess_frequency;
-		else
-			newtics = (GetTickCount() - starttickcount)/(1000/requested_frequency);
-	}
-
-	return newtics;
-}
-
-static void I_ShutdownTimer(void)
-{
-	pfntimeGetTime = NULL;
-	if (winmm)
-	{
-		p_timeEndPeriod pfntimeEndPeriod = (p_timeEndPeriod)(LPVOID)GetProcAddress(winmm, "timeEndPeriod");
-		if (pfntimeEndPeriod)
-			pfntimeEndPeriod(1);
-		FreeLibrary(winmm);
-		winmm = NULL;
-	}
-}
-#else
 //
 // I_GetTime
 // returns time in 1/TICRATE second tics
 //
 
-// millisecond precision only
-int TimeFunction(int requested_frequency)
-{
-	static Uint64 basetime = 0;
-		   Uint64 ticks = SDL_GetTicks();
+static Uint64 timer_frequency;
 
-	if (!basetime)
-		basetime = ticks;
+static double tic_frequency;
+static Uint64 tic_epoch;
 
-	ticks -= basetime;
+tic_t I_GetTime(void)
+{
+	static double elapsed;
 
-	ticks = (ticks*requested_frequency);
+	const Uint64 now = SDL_GetPerformanceCounter();
 
-	ticks = (ticks/1000);
+	elapsed += (now - tic_epoch) / tic_frequency;
+	tic_epoch = now; // moving epoch
 
-	return ticks;
+	return (tic_t)elapsed;
 }
-#endif
 
-tic_t I_GetTime(void)
+precise_t I_GetPreciseTime(void)
 {
-	return TimeFunction(NEWTICRATE);
+	return SDL_GetPerformanceCounter();
 }
 
-int I_GetTimeMicros(void)
+int I_PreciseToMicros(precise_t d)
 {
-	return TimeFunction(1000000);
+	// 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));
 }
 
 //
@@ -2157,26 +2177,11 @@ int I_GetTimeMicros(void)
 //
 void I_StartupTimer(void)
 {
-#ifdef _WIN32
-	// for win2k time bug
-	if (M_CheckParm("-gettickcount"))
-	{
-		starttickcount = GetTickCount();
-		CONS_Printf("%s", M_GetText("Using GetTickCount()\n"));
-	}
-	winmm = LoadLibraryA("winmm.dll");
-	if (winmm)
-	{
-		p_timeEndPeriod pfntimeBeginPeriod = (p_timeEndPeriod)(LPVOID)GetProcAddress(winmm, "timeBeginPeriod");
-		if (pfntimeBeginPeriod)
-			pfntimeBeginPeriod(1);
-		pfntimeGetTime = (p_timeGetTime)(LPVOID)GetProcAddress(winmm, "timeGetTime");
-	}
-	I_AddExitFunc(I_ShutdownTimer);
-#endif
-}
-
+	timer_frequency = SDL_GetPerformanceFrequency();
+	tic_epoch       = SDL_GetPerformanceCounter();
 
+	tic_frequency   = timer_frequency / (double)NEWTICRATE;
+}
 
 void I_Sleep(void)
 {
@@ -2220,6 +2225,7 @@ static void I_Fork(void)
 			newsignalhandler_Warn("fork()");
 			break;
 		case 0:
+			I_RegisterChildSignals();
 			break;
 		default:
 			if (logstream)
diff --git a/src/sdl/i_threads.c b/src/sdl/i_threads.c
index 3b1c20b9a3cbb79038253b4bd5b7dbec3df001d7..f73d00bcfc2ee70eba47f432812ec8cb4db7ec39 100644
--- a/src/sdl/i_threads.c
+++ b/src/sdl/i_threads.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020 by James R.
+// Copyright (C) 2020-2021 by James R.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index b8b3b9d34e8ae97648d3f1f6178511c251b9253e..ed766ff23dfb79395bbb5648211be73c45fc34b6 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -4,7 +4,7 @@
 //
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Portions Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 2014-2020 by Sonic Team Junior.
+// Copyright (C) 2014-2021 by Sonic Team Junior.
 //
 // This program is free software; you can redistribute it and/or
 // modify it under the terms of the GNU General Public License
@@ -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,6 +409,14 @@ void I_UpdateMouseGrab(void)
 		SDLdoGrabMouse();
 }
 
+void I_SetMouseGrab(boolean grab)
+{
+	if (grab)
+		SDLdoGrabMouse();
+	else
+		SDLdoUngrabMouse();
+}
+
 static void VID_Command_NumModes_f (void)
 {
 	CONS_Printf(M_GetText("%d video mode(s) available(s)\n"), VID_NumModes());
@@ -650,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)
@@ -673,8 +686,8 @@ static void Impl_HandleMouseMotionEvent(SDL_MouseMotionEvent evt)
 		{
 			if (SDL_GetMouseFocus() == window && SDL_GetKeyboardFocus() == window)
 			{
-				mousemovex +=  evt.xrel;
-				mousemovey += -evt.yrel;
+				mousemovex += evt.xrel;
+				mousemovey += evt.yrel;
 				SDL_SetWindowGrab(window, SDL_TRUE);
 			}
 			firstmove = false;
@@ -729,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);
@@ -753,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)
@@ -782,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])
 	{
@@ -799,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);
 }
@@ -826,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;
 
@@ -849,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)
@@ -867,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;
 
@@ -1057,8 +1070,7 @@ void I_GetEvent(void)
 					M_SetupJoystickMenu(0);
 			 	break;
 			case SDL_QUIT:
-				if (Playing())
-					LUAh_GameQuit();
+				LUA_HookBool(true, HOOK(GameQuit));
 				I_Quit();
 				break;
 		}
@@ -1072,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);
 	}
 
@@ -1863,7 +1875,6 @@ void VID_StartupOpenGL(void)
 		HWD.pfnReadRect         = hwSym("ReadRect",NULL);
 		HWD.pfnGClipRect        = hwSym("GClipRect",NULL);
 		HWD.pfnClearMipMapCache = hwSym("ClearMipMapCache",NULL);
-		HWD.pfnClearCacheList   = hwSym("ClearCacheList",NULL);
 		HWD.pfnSetSpecialState  = hwSym("SetSpecialState",NULL);
 		HWD.pfnSetPalette       = hwSym("SetPalette",NULL);
 		HWD.pfnGetTextureUsed   = hwSym("GetTextureUsed",NULL);
@@ -1940,3 +1951,8 @@ void I_ShutdownGraphics(void)
 	framebuffer = SDL_FALSE;
 }
 #endif
+
+void I_GetCursorPosition(INT32 *x, INT32 *y)
+{
+	SDL_GetMouseState(x, y);
+}
diff --git a/src/sdl/mixer_sound.c b/src/sdl/mixer_sound.c
index c64164caafce5b52698e564a2514d57fa7a7eeda..35a79acc0b16ece0a457018db2df9d7a94b2781b 100644
--- a/src/sdl/mixer_sound.c
+++ b/src/sdl/mixer_sound.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2014-2020 by Sonic Team Junior.
+// Copyright (C) 2014-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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,10 +1294,14 @@ 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};
+#if defined (GME_VERSION) && GME_VERSION >= 0x000603
+		if (looping)
+			gme_set_autoload_playback_limit(gme, 0);
+#endif
 		gme_set_equalizer(gme, &eq);
 		gme_start_track(gme, 0);
 		current_track = 0;
@@ -1356,7 +1360,7 @@ void I_StopSong(void)
 	if (!fading_nocleanup)
 		I_StopFadingSong();
 
-#ifdef HAVE_LIBGME
+#ifdef HAVE_GME
 	if (gme)
 	{
 		Mix_HookMusic(NULL, NULL);
@@ -1429,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/sdl/ogl_sdl.c b/src/sdl/ogl_sdl.c
index 52727c05600a5f33221e729d51263b08fcdde30b..c426e6792f6c8116c52615a27f997e63e9f2b275 100644
--- a/src/sdl/ogl_sdl.c
+++ b/src/sdl/ogl_sdl.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 //
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 2014-2020 by Sonic Team Junior.
+// Copyright (C) 2014-2021 by Sonic Team Junior.
 //
 // This program is free software; you can redistribute it and/or
 // modify it under the terms of the GNU General Public License
diff --git a/src/sdl/ogl_sdl.h b/src/sdl/ogl_sdl.h
index 748e30bae06036c2785cb249e92f6798dc67f4f1..8f87f688e36a4b897268c1f0fb3f55747779b908 100644
--- a/src/sdl/ogl_sdl.h
+++ b/src/sdl/ogl_sdl.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 //
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 2014-2020 by Sonic Team Junior.
+// Copyright (C) 2014-2021 by Sonic Team Junior.
 //
 // This program is free software; you can redistribute it and/or
 // modify it under the terms of the GNU General Public License
diff --git a/src/sdl/sdl_sound.c b/src/sdl/sdl_sound.c
index 86e294fb57457d09539834ab268d0c9f62dc5360..058b601c350072e93f41bae736a89f432d5d79cf 100644
--- a/src/sdl/sdl_sound.c
+++ b/src/sdl/sdl_sound.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 //
 // Copyright (C) 1993-1996 by id Software, Inc.
-// Copyright (C) 2014-2020 by Sonic Team Junior.
+// Copyright (C) 2014-2021 by Sonic Team Junior.
 //
 // This program is free software; you can redistribute it and/or
 // modify it under the terms of the GNU General Public License
diff --git a/src/sdl/sdlmain.h b/src/sdl/sdlmain.h
index e35506114e2ebdd0c5f32fccc8c765e9cbe9bcb7..a9676b5c2f1261bbc7aab49d0ada71e4cba50f91 100644
--- a/src/sdl/sdlmain.h
+++ b/src/sdl/sdlmain.h
@@ -1,7 +1,7 @@
 // Emacs style mode select   -*- C++ -*-
 //-----------------------------------------------------------------------------
 //
-// Copyright (C) 2006-2020 by Sonic Team Junior.
+// Copyright (C) 2006-2021 by Sonic Team Junior.
 //
 // This program is free software; you can redistribute it and/or
 // modify it under the terms of the GNU General Public License
diff --git a/src/sounds.c b/src/sounds.c
index 092bda21f2f3fa28ab3fc1dd5e14716981506e56..4c5b11ee98294cf21662512c87eb2c566cae7069 100644
--- a/src/sounds.c
+++ b/src/sounds.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/sounds.h b/src/sounds.h
index e49dd2f3e3e9eac78401c48e2ed5f00d59dfb8a7..2dd37953c5750e87f2f904ffa11dc17d413611af 100644
--- a/src/sounds.h
+++ b/src/sounds.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/st_stuff.c b/src/st_stuff.c
index b25538d883276cdac46784489462b2c32f349fc9..a328d669e51169ba18d8ae8d046774d3b298eed3 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -43,6 +43,7 @@
 #endif
 
 #include "lua_hud.h"
+#include "lua_hook.h"
 
 UINT16 objectsdrawn = 0;
 
@@ -299,10 +300,6 @@ void ST_LoadGraphics(void)
 	gravboots = W_CachePatchName("TVGVICON", PU_HUDGFX);
 
 	tagico = W_CachePatchName("TAGICO", PU_HUDGFX);
-	rflagico = W_CachePatchName("RFLAGICO", PU_HUDGFX);
-	bflagico = W_CachePatchName("BFLAGICO", PU_HUDGFX);
-	rmatcico = W_CachePatchName("RMATCICO", PU_HUDGFX);
-	bmatcico = W_CachePatchName("BMATCICO", PU_HUDGFX);
 	gotrflag = W_CachePatchName("GOTRFLAG", PU_HUDGFX);
 	gotbflag = W_CachePatchName("GOTBFLAG", PU_HUDGFX);
 	fnshico = W_CachePatchName("FNSHICO", PU_HUDGFX);
@@ -1371,7 +1368,7 @@ void ST_drawTitleCard(void)
 		zzticker = lt_ticker;
 		V_DrawMappedPatch(FixedInt(lt_zigzag), (-zzticker) % zigzag->height, V_SNAPTOTOP|V_SNAPTOLEFT, zigzag, colormap);
 		V_DrawMappedPatch(FixedInt(lt_zigzag), (zigzag->height-zzticker) % zigzag->height, V_SNAPTOTOP|V_SNAPTOLEFT, zigzag, colormap);
-		V_DrawMappedPatch(FixedInt(lt_zigzag), (-zigzag->height+zzticker) % zztext->height, V_SNAPTOTOP|V_SNAPTOLEFT, zztext, colormap);
+		V_DrawMappedPatch(FixedInt(lt_zigzag), (-zztext->height+zzticker) % zztext->height, V_SNAPTOTOP|V_SNAPTOLEFT, zztext, colormap);
 		V_DrawMappedPatch(FixedInt(lt_zigzag), (zzticker) % zztext->height, V_SNAPTOTOP|V_SNAPTOLEFT, zztext, colormap);
 	}
 
@@ -1395,7 +1392,7 @@ void ST_drawTitleCard(void)
 	lt_lasttic = lt_ticker;
 
 luahook:
-	LUAh_TitleCardHUD(stplyr);
+	LUA_HUDHOOK(titlecard);
 }
 
 //
@@ -2040,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);
@@ -2191,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);
 		}
 
 	}
@@ -2363,27 +2359,29 @@ static inline void ST_drawRaceHUD(void)
 
 static void ST_drawTeamHUD(void)
 {
-	patch_t *p;
 #define SEP 20
 
 	if (F_GetPromptHideHud(0)) // y base is 0
 		return;
 
-	if (gametyperules & GTR_TEAMFLAGS)
-		p = bflagico;
-	else
-		p = bmatcico;
-
-	if (LUA_HudEnabled(hud_teamscores))
-		V_DrawSmallScaledPatch(BASEVIDWIDTH/2 - SEP - (p->width / 4), 4, V_HUDTRANS|V_PERPLAYER|V_SNAPTOTOP, p);
-
-	if (gametyperules & GTR_TEAMFLAGS)
-		p = rflagico;
-	else
-		p = rmatcico;
+	rflagico = W_CachePatchName("RFLAGICO", PU_HUDGFX);
+	bflagico = W_CachePatchName("BFLAGICO", PU_HUDGFX);
+	rmatcico = W_CachePatchName("RMATCICO", PU_HUDGFX);
+	bmatcico = W_CachePatchName("BMATCICO", PU_HUDGFX);
 
 	if (LUA_HudEnabled(hud_teamscores))
-		V_DrawSmallScaledPatch(BASEVIDWIDTH/2 + SEP - (p->width / 4), 4, V_HUDTRANS|V_PERPLAYER|V_SNAPTOTOP, p);
+	{
+		if (gametyperules & GTR_TEAMFLAGS)
+		{
+			V_DrawSmallScaledPatch(BASEVIDWIDTH/2 - SEP - (bflagico->width / 4), 4, V_HUDTRANS|V_PERPLAYER|V_SNAPTOTOP, bflagico);
+			V_DrawSmallScaledPatch(BASEVIDWIDTH/2 + SEP - (rflagico->width / 4), 4, V_HUDTRANS|V_PERPLAYER|V_SNAPTOTOP, rflagico);
+		}
+		else
+		{
+			V_DrawSmallScaledPatch(BASEVIDWIDTH/2 - SEP - (bmatcico->width / 4), 4, V_HUDTRANS|V_PERPLAYER|V_SNAPTOTOP, bmatcico);
+			V_DrawSmallScaledPatch(BASEVIDWIDTH/2 + SEP - (rmatcico->width / 4), 4, V_HUDTRANS|V_PERPLAYER|V_SNAPTOTOP, rmatcico);
+		}
+	}
 
 	if (!(gametyperules & GTR_TEAMFLAGS))
 		goto num;
@@ -2734,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))
@@ -2751,7 +2749,6 @@ static void ST_overlayDrawer(void)
 
 void ST_Drawer(void)
 {
-#ifdef SEENAMES
 	if (cv_seenames.value && cv_allowseenames.value && displayplayer == consoleplayer && seenplayer && seenplayer->mo)
 	{
 		INT32 c = 0;
@@ -2775,7 +2772,6 @@ void ST_Drawer(void)
 
 		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT/2 + 15, V_HUDTRANSHALF|c, player_names[seenplayer-players]);
 	}
-#endif
 
 	// Doom's status bar only updated if necessary.
 	// However, ours updates every frame regardless, so the "refresh" param was removed
diff --git a/src/st_stuff.h b/src/st_stuff.h
index 4ea307d2b45c6cdcd91dd345755f9b18029237a0..b1ea2942d3be73188a1a8a4905f27b5a38b5758f 100644
--- a/src/st_stuff.h
+++ b/src/st_stuff.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/strcasestr.c b/src/strcasestr.c
index b266278ede5a7f338d3b7cb77f068671309b2a6c..1cbee286a433a722dcb0b95a25fa5dccf64485c9 100644
--- a/src/strcasestr.c
+++ b/src/strcasestr.c
@@ -2,7 +2,7 @@
 strcasestr -- case insensitive substring searching function.
 */
 /*
-Copyright 2019-2020 James R.
+Copyright 2019-2021 James R.
 All rights reserved.
 
 Redistribution and use in source forms, with or without modification, is
diff --git a/src/string.c b/src/string.c
index e430c5cc340ba3a1a98dd1a99ee661ecd52b333f..f32025612283c02e7da2522be5c94fe8596efc56 100644
--- a/src/string.c
+++ b/src/string.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2006      by Graue.
-// Copyright (C) 2006-2020 by Sonic Team Junior.
+// Copyright (C) 2006-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/tables.c b/src/tables.c
index 70a1ecd0addf7fae640d44847f7ffec8c70d8278..9263f42d327f679e10a9a3899acd81eee4171ddf 100644
--- a/src/tables.c
+++ b/src/tables.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/tables.h b/src/tables.h
index 953d891ce8b1c519ca0db3143a20f2645d747eee..baa3adf36de62eb88cb31dfed7e2dcd535205f50 100644
--- a/src/tables.h
+++ b/src/tables.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/taglist.c b/src/taglist.c
index b11216b6cf7b3e7c709b73750c2d46e8e9155c40..ad1b9dc4b52e2a6a8cf6d117a0bbff722ec07ccf 100644
--- a/src/taglist.c
+++ b/src/taglist.c
@@ -1,8 +1,8 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
-// Copyright (C)      2020 by Nev3r.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
+// Copyright (C) 2020-2021 by Nev3r.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -15,6 +15,11 @@
 #include "z_zone.h"
 #include "r_data.h"
 
+// Bit array of whether a tag exists for sectors/lines/things.
+bitarray_t tags_available[BIT_ARRAY_SIZE (MAXTAGS)];
+
+size_t num_tags;
+
 // Taggroups are used to list elements of the same tag, for iteration.
 // Since elements can now have multiple tags, it means an element may appear
 // in several taggroups at the same time. These are built on level load.
@@ -22,11 +27,13 @@ taggroup_t* tags_sectors[MAXTAGS + 1];
 taggroup_t* tags_lines[MAXTAGS + 1];
 taggroup_t* tags_mapthings[MAXTAGS + 1];
 
-/// Adds a tag to a given element's taglist.
+/// Adds a tag to a given element's taglist. It will not add a duplicate.
 /// \warning This does not rebuild the global taggroups, which are used for iteration.
 void Tag_Add (taglist_t* list, const mtag_t tag)
 {
-	list->tags = Z_Realloc(list->tags, (list->count + 1) * sizeof(list->tags), PU_LEVEL, NULL);
+	if (Tag_Find(list, tag))
+		return;
+	list->tags = Z_Realloc(list->tags, (list->count + 1) * sizeof(mtag_t), PU_LEVEL, NULL);
 	list->tags[list->count++] = tag;
 }
 
@@ -105,6 +112,39 @@ size_t Taggroup_Find (const taggroup_t *group, const size_t id)
 	return -1;
 }
 
+/// group->count, but also checks for NULL
+size_t Taggroup_Count (const taggroup_t *group)
+{
+	return group ? group->count : 0;
+}
+
+/// Iterate thru elements in a global taggroup.
+INT32 Taggroup_Iterate
+(		taggroup_t *garray[],
+		const size_t max_elements,
+		const mtag_t tag,
+		const size_t p)
+{
+	const taggroup_t *group;
+
+	if (tag == MTAG_GLOBAL)
+	{
+		if (p < max_elements)
+			return p;
+		return -1;
+	}
+
+	group = garray[(UINT16)tag];
+
+	if (group)
+	{
+		if (p < group->count)
+			return group->elements[p];
+		return -1;
+	}
+	return -1;
+}
+
 /// Add an element to a global taggroup.
 void Taggroup_Add (taggroup_t *garray[], const mtag_t tag, size_t id)
 {
@@ -120,6 +160,12 @@ void Taggroup_Add (taggroup_t *garray[], const mtag_t tag, size_t id)
 	if (Taggroup_Find(group, id) != (size_t)-1)
 		return;
 
+	if (! in_bit_array(tags_available, tag))
+	{
+		num_tags++;
+		set_bit_array(tags_available, tag);
+	}
+
 	// Create group if empty.
 	if (!group)
 	{
@@ -133,25 +179,34 @@ void Taggroup_Add (taggroup_t *garray[], const mtag_t tag, size_t id)
 		for (i = 0; i < group->count; i++)
 			if (group->elements[i] > id)
 				break;
+	}
 
-		group->elements = Z_Realloc(group->elements, (group->count + 1) * sizeof(size_t), PU_LEVEL, NULL);
+	group->elements = Z_Realloc(group->elements, (group->count + 1) * sizeof(size_t), PU_LEVEL, NULL);
 
-		// Offset existing elements to make room for the new one.
-		if (i < group->count)
-			memmove(&group->elements[i + 1], &group->elements[i], group->count - i);
-	}
+	// Offset existing elements to make room for the new one.
+	if (i < group->count)
+		memmove(&group->elements[i + 1], &group->elements[i], group->count - i);
 
 	group->count++;
-	group->elements = Z_Realloc(group->elements, group->count * sizeof(size_t), PU_LEVEL, NULL);
 	group->elements[i] = id;
 }
 
+static size_t total_elements_with_tag (const mtag_t tag)
+{
+	return
+		(
+				Taggroup_Count(tags_sectors[tag]) +
+				Taggroup_Count(tags_lines[tag]) +
+				Taggroup_Count(tags_mapthings[tag])
+		);
+}
+
 /// Remove an element from a global taggroup.
 void Taggroup_Remove (taggroup_t *garray[], const mtag_t tag, size_t id)
 {
 	taggroup_t *group;
 	size_t rempos;
-	size_t newcount;
+	size_t oldcount;
 
 	if (tag == MTAG_GLOBAL)
 		return;
@@ -161,8 +216,14 @@ void Taggroup_Remove (taggroup_t *garray[], const mtag_t tag, size_t id)
 	if ((rempos = Taggroup_Find(group, id)) == (size_t)-1)
 		return;
 
+	if (group->count == 1 && total_elements_with_tag(tag) == 1)
+	{
+		num_tags--;
+		unset_bit_array(tags_available, tag);
+	}
+
 	// Strip away taggroup if no elements left.
-	if (!(newcount = --group->count))
+	if (!(oldcount = group->count--))
 	{
 		Z_Free(group->elements);
 		Z_Free(group);
@@ -170,19 +231,18 @@ void Taggroup_Remove (taggroup_t *garray[], const mtag_t tag, size_t id)
 	}
 	else
 	{
-		size_t *newelements = Z_Malloc(newcount * sizeof(size_t), PU_LEVEL, NULL);
+		size_t *newelements = Z_Malloc(group->count * sizeof(size_t), PU_LEVEL, NULL);
 		size_t i;
 
 		// Copy the previous entries save for the one to remove.
 		for (i = 0; i < rempos; i++)
 			newelements[i] = group->elements[i];
 
-		for (i = rempos + 1; i < group->count; i++)
+		for (i = rempos + 1; i < oldcount; i++)
 			newelements[i - 1] = group->elements[i];
 
 		Z_Free(group->elements);
 		group->elements = newelements;
-		group->count = newcount;
 	}
 }
 
@@ -209,6 +269,9 @@ void Taglist_InitGlobalTables(void)
 {
 	size_t i, j;
 
+	memset(tags_available, 0, sizeof tags_available);
+	num_tags = 0;
+
 	for (i = 0; i < MAXTAGS; i++)
 	{
 		tags_sectors[i] = NULL;
@@ -236,56 +299,17 @@ void Taglist_InitGlobalTables(void)
 
 INT32 Tag_Iterate_Sectors (const mtag_t tag, const size_t p)
 {
-	if (tag == MTAG_GLOBAL)
-	{
-		if (p < numsectors)
-			return p;
-		return -1;
-	}
-
-	if (tags_sectors[(UINT16)tag])
-	{
-		if (p < tags_sectors[(UINT16)tag]->count)
-			return tags_sectors[(UINT16)tag]->elements[p];
-		return -1;
-	}
-	return -1;
+	return Taggroup_Iterate(tags_sectors, numsectors, tag, p);
 }
 
 INT32 Tag_Iterate_Lines (const mtag_t tag, const size_t p)
 {
-	if (tag == MTAG_GLOBAL)
-	{
-		if (p < numlines)
-			return p;
-		return -1;
-	}
-
-	if (tags_lines[(UINT16)tag])
-	{
-		if (p < tags_lines[(UINT16)tag]->count)
-			return tags_lines[(UINT16)tag]->elements[p];
-		return -1;
-	}
-	return -1;
+	return Taggroup_Iterate(tags_lines, numlines, tag, p);
 }
 
 INT32 Tag_Iterate_Things (const mtag_t tag, const size_t p)
 {
-	if (tag == MTAG_GLOBAL)
-	{
-		if (p < nummapthings)
-			return p;
-		return -1;
-	}
-
-	if (tags_mapthings[(UINT16)tag])
-	{
-		if (p < tags_mapthings[(UINT16)tag]->count)
-			return tags_mapthings[(UINT16)tag]->elements[p];
-		return -1;
-	}
-	return -1;
+	return Taggroup_Iterate(tags_mapthings, nummapthings, tag, p);
 }
 
 INT32 Tag_FindLineSpecial(const INT16 special, const mtag_t tag)
diff --git a/src/taglist.h b/src/taglist.h
index 0e6d9f8422bbc150dbbabe3c6048d6e66c448f05..d045eb8276011c22d049111fca82869f0b3b857b 100644
--- a/src/taglist.h
+++ b/src/taglist.h
@@ -1,8 +1,8 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
-// Copyright (C)      2020 by Nev3r.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
+// Copyright (C) 2020-2021 by Nev3r.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -43,6 +43,10 @@ typedef struct
 	size_t count;
 } taggroup_t;
 
+extern bitarray_t tags_available[];
+
+extern size_t num_tags;
+
 extern taggroup_t* tags_sectors[];
 extern taggroup_t* tags_lines[];
 extern taggroup_t* tags_mapthings[];
@@ -50,6 +54,13 @@ extern taggroup_t* tags_mapthings[];
 void Taggroup_Add (taggroup_t *garray[], const mtag_t tag, size_t id);
 void Taggroup_Remove (taggroup_t *garray[], const mtag_t tag, size_t id);
 size_t Taggroup_Find (const taggroup_t *group, const size_t id);
+size_t Taggroup_Count (const taggroup_t *group);
+
+INT32 Taggroup_Iterate
+(		taggroup_t *garray[],
+		const size_t max_elements,
+		const mtag_t tag,
+		const size_t p);
 
 void Taglist_InitGlobalTables(void);
 
@@ -60,25 +71,16 @@ INT32 Tag_Iterate_Things (const mtag_t tag, const size_t p);
 INT32 Tag_FindLineSpecial(const INT16 special, const mtag_t tag);
 INT32 P_FindSpecialLineFromTag(INT16 special, INT16 tag, INT32 start);
 
-// Use this macro to declare an iterator position variable.
-#define TAG_ITER_DECLARECOUNTER(level) size_t ICNT_##level
-
-#define TAG_ITER(level, fn, tag, return_varname) for(ICNT_##level = 0; (return_varname = fn(tag, ICNT_##level)) >= 0; ICNT_##level++)
+#define ICNAME2(id) ICNT_##id
+#define ICNAME(id) ICNAME2(id)
+#define TAG_ITER(fn, tag, return_varname) for(size_t ICNAME(__LINE__) = 0; (return_varname = fn(tag, ICNAME(__LINE__))) >= 0; ICNAME(__LINE__)++)
 
 // Use these macros as wrappers for a taglist iteration.
-#define TAG_ITER_SECTORS(level, tag, return_varname) TAG_ITER(level, Tag_Iterate_Sectors, tag, return_varname)
-#define TAG_ITER_LINES(level, tag, return_varname)   TAG_ITER(level, Tag_Iterate_Lines, tag, return_varname)
-#define TAG_ITER_THINGS(level, tag, return_varname)  TAG_ITER(level, Tag_Iterate_Things, tag, return_varname)
+#define TAG_ITER_SECTORS(tag, return_varname) TAG_ITER(Tag_Iterate_Sectors, tag, return_varname)
+#define TAG_ITER_LINES(tag, return_varname)   TAG_ITER(Tag_Iterate_Lines, tag, return_varname)
+#define TAG_ITER_THINGS(tag, return_varname)  TAG_ITER(Tag_Iterate_Things, tag, return_varname)
 
 /* ITERATION MACROS
-TAG_ITER_DECLARECOUNTER must be used before using the iterators.
-
-'level':
-For each nested iteration, an additional TAG_ITER_DECLARECOUNTER
-must be used with a different level number to avoid conflict with
-the outer iterations.
-Most cases don't have nested iterations and thus the level is just 0.
-
 'tag':
 Pretty much the elements' tag to iterate through.
 
@@ -88,17 +90,12 @@ Target variable's name to return the iteration results to.
 
 EXAMPLE:
 {
-	TAG_ITER_DECLARECOUNTER(0);
-	TAG_ITER_DECLARECOUNTER(1); // For the nested iteration.
-
 	size_t li;
-	size_t sec;
-
 	INT32 tag1 = 4;
 
 	...
 
-	TAG_ITER_LINES(0, tag1, li)
+	TAG_ITER_LINES(tag1, li)
 	{
 		line_t *line = lines + li;
 
@@ -106,11 +103,11 @@ EXAMPLE:
 
 		if (something)
 		{
+			size_t sec;
 			mtag_t tag2 = 8;
 
-			// Nested iteration; just make sure the level is higher
-			// and that it has its own counter declared in scope.
-			TAG_ITER_SECTORS(1, tag2, sec)
+			// Nested iteration.
+			TAG_ITER_SECTORS(tag2, sec)
 			{
 				sector_t *sector = sectors + sec;
 
diff --git a/src/tmap.nas b/src/tmap.nas
index 69282d0b471dd2c86802df544f4a346e4b96baa9..5bf28359e6b75b1753e94eea8d0fa77b077978b8 100644
--- a/src/tmap.nas
+++ b/src/tmap.nas
@@ -1,7 +1,7 @@
 ;; SONIC ROBO BLAST 2
 ;;-----------------------------------------------------------------------------
 ;; Copyright (C) 1998-2000 by DooM Legacy Team.
-;; Copyright (C) 1999-2020 by Sonic Team Junior.
+;; Copyright (C) 1999-2021 by Sonic Team Junior.
 ;;
 ;; This program is free software distributed under the
 ;; terms of the GNU General Public License, version 2.
diff --git a/src/tmap.s b/src/tmap.s
index 3a4cf2e1a1bcca125f3745ef252d83075ae2dce4..62dcf85dcc00ed350e3bf145e03eedb885e4ee6a 100644
--- a/src/tmap.s
+++ b/src/tmap.s
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/tmap_asm.s b/src/tmap_asm.s
index 3cd0f87cc5c58e430663d6cc95d0467f76272588..b5a0a51e91dd00f330b33d2cf0ca9cee60373323 100644
--- a/src/tmap_asm.s
+++ b/src/tmap_asm.s
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/tmap_mmx.nas b/src/tmap_mmx.nas
index 15b97499de530b346f85a2b6545a2f1d2843c2a2..8b6ef91a60aeff852abee8824eb8f49a6572df92 100644
--- a/src/tmap_mmx.nas
+++ b/src/tmap_mmx.nas
@@ -1,7 +1,7 @@
 ;; SONIC ROBO BLAST 2
 ;;-----------------------------------------------------------------------------
 ;; Copyright (C) 1998-2000 by DOSDOOM.
-;; Copyright (C) 2010-2020 by Sonic Team Junior.
+;; Copyright (C) 2010-2021 by Sonic Team Junior.
 ;;
 ;; This program is free software distributed under the
 ;; terms of the GNU General Public License, version 2.
diff --git a/src/tmap_vc.nas b/src/tmap_vc.nas
index 49eb21a6d3c7823921bc67e7a951d410e6af56c8..b6ee26e6b8f22d481b419dac46227b010f78058f 100644
--- a/src/tmap_vc.nas
+++ b/src/tmap_vc.nas
@@ -1,7 +1,7 @@
 ;; SONIC ROBO BLAST 2
 ;;-----------------------------------------------------------------------------
 ;; Copyright (C) 1998-2000 by DooM Legacy Team.
-;; Copyright (C) 1999-2020 by Sonic Team Junior.
+;; Copyright (C) 1999-2021 by Sonic Team Junior.
 ;;
 ;; This program is free software distributed under the
 ;; terms of the GNU General Public License, version 2.
diff --git a/src/v_video.c b/src/v_video.c
index 4713db0d89dda23a656f3df9ace63db9fba52902..c3993854403fe87db28c06e18705a69f01932a84 100644
--- a/src/v_video.c
+++ b/src/v_video.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -418,7 +418,7 @@ void V_SetPalette(INT32 palettenum)
 #ifdef HWRENDER
 	if (rendermode == render_opengl)
 		HWR_SetPalette(&pLocalPalette[palettenum*256]);
-#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
+#if defined (__unix__) || defined (UNIXCOMMON) || defined (HAVE_SDL)
 	else
 #endif
 #endif
@@ -432,7 +432,7 @@ void V_SetPaletteLump(const char *pal)
 #ifdef HWRENDER
 	if (rendermode == render_opengl)
 		HWR_SetPalette(pLocalPalette);
-#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
+#if defined (__unix__) || defined (UNIXCOMMON) || defined (HAVE_SDL)
 	else
 #endif
 #endif
@@ -455,7 +455,8 @@ void VID_BlitLinearScreen_ASM(const UINT8 *srcptr, UINT8 *destptr, INT32 width,
 
 static void CV_constextsize_OnChange(void)
 {
-	con_recalc = true;
+	if (!con_refresh)
+		con_recalc = true;
 }
 
 
@@ -808,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;
@@ -829,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
@@ -856,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)))
@@ -896,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])
@@ -906,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])
@@ -916,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;
 			}
 		}
@@ -937,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;
 			}
 		}
@@ -950,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;
@@ -998,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)
@@ -1015,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 8a18f82ad7ab834988e672e3f5c21189764876b6..c10ab22cea8f56497f998aa449fb672b59f62f84 100644
--- a/src/v_video.h
+++ b/src/v_video.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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 ece084beb2ddaed925f436d78fcfd290d94c57c5..28fc71c36fca9dceea7f7aecc31f6436028b8d84 100644
--- a/src/version.h
+++ b/src/version.h
@@ -1,6 +1,6 @@
-#define SRB2VERSION "2.2.8"/* this must be the first line, for cmake !! */
+#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
@@ -9,7 +9,7 @@
 // it's only for detection of the version the player is using so the MS can alert them of an update.
 // Only set it higher, not lower, obviously.
 // Note that we use this to help keep internal testing in check; this is why v2.2.0 is not version "1".
-#define MODVERSION 49
+#define MODVERSION 50
 
 // Define this as a prerelease version suffix
 // #define BETAVERSION "RC1"
diff --git a/src/vid_copy.s b/src/vid_copy.s
index eae435ea4cd2ea512ad82c5d7aef29abfc2ce7aa..6a37883565f57023f5687d8c31e2de56d72b288a 100644
--- a/src/vid_copy.s
+++ b/src/vid_copy.s
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
diff --git a/src/w_wad.c b/src/w_wad.c
index aca530fa518c7134636349b769f6760590cc79e3..e49e0ce82f9ffe24c06d757b61b2a69bbc10ee2a 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -50,22 +50,24 @@
 
 #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
 #include "p_setup.h" // P_ScanThings
 #endif
 #include "m_misc.h" // M_MapNumber
+#include "g_game.h" // G_SetGameModified
 
 #ifdef HWRENDER
 #include "hardware/hw_main.h"
@@ -103,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
@@ -116,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);
 		}
@@ -127,6 +134,8 @@ void W_Shutdown(void)
 		Z_Free(wad->lumpinfo);
 		Z_Free(wad);
 	}
+
+	Z_Free(wadfiles);
 }
 
 //===========================================================================
@@ -420,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;
@@ -601,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);
@@ -678,14 +689,122 @@ 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)
 	{
 #ifdef _DEBUG
-		CONS_Error("A WAD file was not found or not valid.\nCheck the log to see which ones.\n");
+		CONS_Error(va("%s was not found or not valid.\nCheck the log for more details.\n", filename));
 #else
-		I_Error("A WAD file was not found or not valid.\nCheck the log to see which ones.\n");
+		I_Error("%s was not found or not valid.\nCheck the log for more details.\n", filename);
 #endif
 	}
 	else
@@ -693,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
 //
@@ -714,9 +846,8 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 #ifndef NOMD5
 	size_t i;
 #endif
-	size_t packetsize;
 	UINT8 md5sum[16];
-	boolean important;
+	int important;
 
 	if (!(refreshdirmenu & REFRESHDIR_ADDFILE))
 		refreshdirmenu = REFRESHDIR_NORMAL|REFRESHDIR_ADDFILE; // clean out cons_alerts that happened earlier
@@ -732,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"));
@@ -746,25 +876,16 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	if ((handle = W_OpenWadFile(&filename, true)) == NULL)
 		return W_InitFileError(filename, startup);
 
-	// 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 = !W_VerifyNMUSlumps(filename)))
-	{
-		packetsize = packetsizetally + nameonlylength(filename) + 22;
+	important = W_VerifyNMUSlumps(filename, startup);
 
-		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;
+	if (important == -1)
+	{
+		fclose(handle);
+		return INT16_MAX;
 	}
 
+	important = !important;
+
 #ifndef NOMD5
 	//
 	// w-waiiiit!
@@ -775,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);
@@ -811,14 +933,22 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 		return W_InitFileError(filename, startup);
 	}
 
+	if (important && !mainfile)
+	{
+		//G_SetGameModified(true);
+		modifiedgame = true; // avoid savemoddata being set to false
+	}
+
 	//
 	// link wad file to search files
 	//
 	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);
@@ -838,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)
@@ -874,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".
   *
@@ -883,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);
 	}
 }
 
@@ -898,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;
 
@@ -1163,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)
@@ -1261,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.
@@ -1278,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;
 
@@ -1291,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
@@ -1350,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)
@@ -1368,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
@@ -1670,26 +2034,12 @@ void *W_CacheSoftwarePatchNumPwad(UINT16 wad, UINT16 lump, INT32 tag)
 
 		// read the lump in full
 		W_ReadLumpHeaderPwad(wad, lump, lumpdata, 0, 0);
+		ptr = lumpdata;
 
 #ifndef NO_PNG_LUMPS
-		// lump is a png so convert it
 		if (Picture_IsLumpPNG((UINT8 *)lumpdata, len))
-		{
-			size_t newlen;
-			void *converted = Picture_PNGConvert((UINT8 *)lumpdata, PICFMT_DOOMPATCH, NULL, NULL, NULL, NULL, len, &newlen, 0);
-			ptr = Z_Malloc(newlen, PU_STATIC, NULL);
-			M_Memcpy(ptr, converted, newlen);
-			Z_Free(converted);
-			len = newlen;
-		}
-		else // just copy it into the patch cache
+			ptr = Picture_PNGConvert((UINT8 *)lumpdata, PICFMT_DOOMPATCH, NULL, NULL, NULL, NULL, len, &len, 0);
 #endif
-		{
-			ptr = Z_Malloc(len, PU_STATIC, NULL);
-			M_Memcpy(ptr, lumpdata, len);
-		}
-
-		Z_Free(lumpdata);
 
 		dest = Z_Calloc(sizeof(patch_t), tag, &lumpcache[lump]);
 		Patch_Create(ptr, len, dest);
@@ -1735,6 +2085,9 @@ void *W_CachePatchNum(lumpnum_t lumpnum, INT32 tag)
 
 void W_UnlockCachedPatch(void *patch)
 {
+	if (!patch)
+		return;
+
 	// The hardware code does its own memory management, as its patches
 	// have different lifetimes from software's.
 #ifdef HWRENDER
@@ -1834,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
 }
@@ -1919,8 +2272,16 @@ static lumpchecklist_t folderblacklist[] =
 static int
 W_VerifyPK3 (FILE *fp, lumpchecklist_t *checklist, boolean status)
 {
+	int verified = true;
+
     zend_t zend;
     zentry_t zentry;
+    zlentry_t zlentry;
+
+	long file_size;/* size of zip file */
+	long data_size;/* size of data inside zip file */
+
+	long old_position;
 
 	UINT16 numlumps;
 	size_t i;
@@ -1936,6 +2297,8 @@ W_VerifyPK3 (FILE *fp, lumpchecklist_t *checklist, boolean status)
 	// Central directory bullshit
 
 	fseek(fp, 0, SEEK_END);
+	file_size = ftell(fp);
+
 	if (!ResFindSignature(fp, pat_end, max(0, ftell(fp) - (22 + 65536))))
 		return true;
 
@@ -1943,6 +2306,8 @@ W_VerifyPK3 (FILE *fp, lumpchecklist_t *checklist, boolean status)
 	if (fread(&zend, 1, sizeof zend, fp) < sizeof zend)
 		return true;
 
+	data_size = sizeof zend;
+
 	numlumps = zend.entries;
 
 	fseek(fp, zend.cdiroffset, SEEK_SET);
@@ -1957,40 +2322,79 @@ W_VerifyPK3 (FILE *fp, lumpchecklist_t *checklist, boolean status)
 		if (memcmp(zentry.signature, pat_central, 4))
 			return true;
 
-		fullname = malloc(zentry.namelen + 1);
-		if (fgets(fullname, zentry.namelen + 1, fp) != fullname)
-			return true;
+		if (verified == true)
+		{
+			fullname = malloc(zentry.namelen + 1);
+			if (fgets(fullname, zentry.namelen + 1, fp) != fullname)
+				return true;
 
-		// Strip away file address and extension for the 8char name.
-		if ((trimname = strrchr(fullname, '/')) != 0)
-			trimname++;
-		else
-			trimname = fullname; // Care taken for root files.
+			// Strip away file address and extension for the 8char name.
+			if ((trimname = strrchr(fullname, '/')) != 0)
+				trimname++;
+			else
+				trimname = fullname; // Care taken for root files.
 
-		if (*trimname) // Ignore directories, well kinda
-		{
-			if ((dotpos = strrchr(trimname, '.')) == 0)
-				dotpos = fullname + strlen(fullname); // Watch for files without extension.
+			if (*trimname) // Ignore directories, well kinda
+			{
+				if ((dotpos = strrchr(trimname, '.')) == 0)
+					dotpos = fullname + strlen(fullname); // Watch for files without extension.
 
-			memset(lumpname, '\0', 9); // Making sure they're initialized to 0. Is it necessary?
-			strncpy(lumpname, trimname, min(8, dotpos - trimname));
+				memset(lumpname, '\0', 9); // Making sure they're initialized to 0. Is it necessary?
+				strncpy(lumpname, trimname, min(8, dotpos - trimname));
 
-			if (! W_VerifyName(lumpname, checklist, status))
-				return false;
+				if (! W_VerifyName(lumpname, checklist, status))
+					verified = false;
+
+				// Check for directories next, if it's blacklisted it will return false
+				else if (W_VerifyName(fullname, folderblacklist, status))
+					verified = false;
+			}
+
+			free(fullname);
 
-			// Check for directories next, if it's blacklisted it will return false
-			if (W_VerifyName(fullname, folderblacklist, status))
-				return false;
+			// skip and ignore comments/extra fields
+			if (fseek(fp, zentry.xtralen + zentry.commlen, SEEK_CUR) != 0)
+				return true;
+		}
+		else
+		{
+			if (fseek(fp, zentry.namelen + zentry.xtralen + zentry.commlen, SEEK_CUR) != 0)
+				return true;
 		}
 
-		free(fullname);
+		data_size +=
+			sizeof zentry + zentry.namelen + zentry.xtralen + zentry.commlen;
 
-		// skip and ignore comments/extra fields
-		if (fseek(fp, zentry.xtralen + zentry.commlen, SEEK_CUR) != 0)
+		old_position = ftell(fp);
+
+		if (fseek(fp, zentry.offset, SEEK_SET) != 0)
 			return true;
+
+		if (fread(&zlentry, 1, sizeof(zlentry_t), fp) < sizeof (zlentry_t))
+			return true;
+
+		data_size +=
+			sizeof zlentry + zlentry.namelen + zlentry.xtralen + zlentry.compsize;
+
+		fseek(fp, old_position, SEEK_SET);
 	}
 
-	return true;
+	if (data_size < file_size)
+	{
+		const char * error = "ZIP file has holes (%ld extra bytes)\n";
+		CONS_Alert(CONS_ERROR, error, (file_size - data_size));
+		return -1;
+	}
+	else if (data_size > file_size)
+	{
+		const char * error = "Reported size of ZIP file contents exceeds file size (%ld extra bytes)\n";
+		CONS_Alert(CONS_ERROR, error, (data_size - file_size));
+		return -1;
+	}
+	else
+	{
+		return verified;
+	}
 }
 
 // Note: This never opens lumps themselves and therefore doesn't have to
@@ -2029,12 +2433,13 @@ static int W_VerifyFile(const char *filename, lumpchecklist_t *checklist,
   * be sent.
   *
   * \param filename Filename of the wad to check.
+  * \param exit_on_error Whether to exit upon file error.
   * \return 1 if file contains only music/sound lumps, 0 if it contains other
   *         stuff (maps, sprites, dehacked lumps, and so on). -1 if there no
   *         file exists with that filename
   * \author Alam Arias
   */
-int W_VerifyNMUSlumps(const char *filename)
+int W_VerifyNMUSlumps(const char *filename, boolean exit_on_error)
 {
 	// MIDI, MOD/S3M/IT/XM/OGG/MP3/WAV, WAVE SFX
 	// ENDOOM text and palette lumps
@@ -2080,7 +2485,7 @@ int W_VerifyNMUSlumps(const char *filename)
 		{"LT", 2}, // Titlecard changes
 
 		{"SLID", 4}, // Continue
-		{"CONT", 4}, 
+		{"CONT", 4},
 
 		{"MINICAPS", 8}, // NiGHTS graphics here and below
 		{"BLUESTAT", 8}, // Sphere status
@@ -2108,7 +2513,13 @@ int W_VerifyNMUSlumps(const char *filename)
 
 		{NULL, 0},
 	};
-	return W_VerifyFile(filename, NMUSlist, false);
+
+	int status = W_VerifyFile(filename, NMUSlist, false);
+
+	if (status == -1)
+		W_InitFileError(filename, exit_on_error);
+
+	return status;
 }
 
 /** \brief Generates a virtual resource used for level data loading.
diff --git a/src/w_wad.h b/src/w_wad.h
index 1e86eea5a6b2ac991d26d47b98cf3416f4de5b2b..a41ba1724a93efad3d583a1ea0d4b065f5dd3798 100644
--- a/src/w_wad.h
+++ b/src/w_wad.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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);
@@ -206,6 +228,6 @@ void W_UnlockCachedPatch(void *patch);
 
 void W_VerifyFileMD5(UINT16 wadfilenum, const char *matchmd5);
 
-int W_VerifyNMUSlumps(const char *filename);
+int W_VerifyNMUSlumps(const char *filename, boolean exit_on_error);
 
 #endif // __W_WAD__
diff --git a/src/win32/Makefile.cfg b/src/win32/Makefile.cfg
deleted file mode 100644
index 486616f2d79b477ec7474c5f9982b72d8d663ae3..0000000000000000000000000000000000000000
--- a/src/win32/Makefile.cfg
+++ /dev/null
@@ -1,164 +0,0 @@
-#
-# win32/Makefile.cfg for SRB2/Minwgw
-#
-
-#
-#Mingw, if you don't know, that's Win32/Win64
-#
-
-ifdef MINGW64
-	HAVE_LIBGME=1
-	LIBGME_CFLAGS=-I../libs/gme/include
-	LIBGME_LDFLAGS=-L../libs/gme/win64 -lgme
-ifdef HAVE_OPENMPT
-	LIBOPENMPT_CFLAGS?=-I../libs/libopenmpt/inc
-	LIBOPENMPT_LDFLAGS?=-L../libs/libopenmpt/lib/x86_64/mingw -lopenmpt
-endif
-ifndef NOMIXERX
-	HAVE_MIXERX=1
-	SDL_CFLAGS?=-I../libs/SDL2/x86_64-w64-mingw32/include/SDL2 -I../libs/SDLMixerX/x86_64-w64-mingw32/include/SDL2 -Dmain=SDL_main
-	SDL_LDFLAGS?=-L../libs/SDL2/x86_64-w64-mingw32/lib -L../libs/SDLMixerX/x86_64-w64-mingw32/lib -lmingw32 -lSDL2main -lSDL2 -mwindows
-else
-	SDL_CFLAGS?=-I../libs/SDL2/x86_64-w64-mingw32/include/SDL2 -I../libs/SDL2_mixer/x86_64-w64-mingw32/include/SDL2 -Dmain=SDL_main
-	SDL_LDFLAGS?=-L../libs/SDL2/x86_64-w64-mingw32/lib -L../libs/SDL2_mixer/x86_64-w64-mingw32/lib -lmingw32 -lSDL2main -lSDL2 -mwindows
-endif
-else
-	HAVE_LIBGME=1
-	LIBGME_CFLAGS=-I../libs/gme/include
-	LIBGME_LDFLAGS=-L../libs/gme/win32 -lgme
-ifdef HAVE_OPENMPT
-	LIBOPENMPT_CFLAGS?=-I../libs/libopenmpt/inc
-	LIBOPENMPT_LDFLAGS?=-L../libs/libopenmpt/lib/x86/mingw -lopenmpt
-endif
-ifndef NOMIXERX
-	HAVE_MIXERX=1
-	SDL_CFLAGS?=-I../libs/SDL2/i686-w64-mingw32/include/SDL2 -I../libs/SDLMixerX/i686-w64-mingw32/include/SDL2 -Dmain=SDL_main
-	SDL_LDFLAGS?=-L../libs/SDL2/i686-w64-mingw32/lib -L../libs/SDLMixerX/i686-w64-mingw32/lib -lmingw32 -lSDL2main -lSDL2 -mwindows
-else
-	SDL_CFLAGS?=-I../libs/SDL2/i686-w64-mingw32/include/SDL2 -I../libs/SDL2_mixer/i686-w64-mingw32/include/SDL2 -Dmain=SDL_main
-	SDL_LDFLAGS?=-L../libs/SDL2/i686-w64-mingw32/lib -L../libs/SDL2_mixer/i686-w64-mingw32/lib -lmingw32 -lSDL2main -lSDL2 -mwindows
-endif
-endif
-
-ifndef NOASM
-	USEASM=1
-endif
-
-ifndef NONET
-ifndef MINGW64 #miniupnc is broken with MINGW64
-	HAVE_MINIUPNPC=1
-endif
-endif
-
-	OPTS=-DSTDC_HEADERS
-
-ifndef GCC44
-	#OPTS+=-mms-bitfields
-endif
-
-ifndef SDL
-	OPTS+=-D_WINDOWS
-endif
-
-ifndef SDL
-	LIBS+=-lmingw32 -mwindows -ldinput -ldxguid -lgdi32 -lwinmm
-endif
-
-	LIBS+=-ladvapi32 -lkernel32 -lmsvcrt -luser32
-ifdef MINGW64
-	LIBS+=-lws2_32
-else
-ifdef NO_IPV6
-	LIBS+=-lwsock32
-else
-	LIBS+=-lws2_32
-endif
-endif
-
-	# name of the exefile
-ifdef SDL
-	EXENAME?=srb2win.exe
-else
-	EXENAME?=srb2dd.exe
-endif
-
-ifdef SDL
-	i_system_o+=$(OBJDIR)/SRB2.res
-	#i_main_o+=$(OBJDIR)/win_dbg.o
-ifndef NOHW
-	OPTS+=-DUSE_WGL_SWAP
-endif
-else
-	D_FILES+=$(D_DIR)/fmodex.dll
-	CFLAGS+=-I../libs/fmodex/inc
-	LDFLAGS+=-L../libs/fmodex/lib
-ifdef MINGW64
-	LIBS+=-lfmodex64_vc
-else
-	LIBS+=-lfmodex_vc
-endif
-	i_cdmus_o=$(OBJDIR)/win_cd.o
-	i_net_o=$(OBJDIR)/win_net.o
-	i_system_o=$(OBJDIR)/win_sys.o $(OBJDIR)/SRB2.res
-	i_sound_o=$(OBJDIR)/win_snd.o
-	i_main_o=$(OBJDIR)/win_main.o
-	#i_main_o+=$(OBJDIR)/win_dbg.o
-	OBJS=$(OBJDIR)/dx_error.o $(OBJDIR)/fabdxlib.o $(OBJDIR)/win_vid.o $(OBJDIR)/win_dll.o
-endif
-
-
-ZLIB_CFLAGS?=-I../libs/zlib
-ifdef MINGW64
-ZLIB_LDFLAGS?=-L../libs/zlib/win32 -lz64
-else
-ZLIB_LDFLAGS?=-L../libs/zlib/win32 -lz32
-endif
-
-ifndef NOPNG
-ifndef PNG_CONFIG
-	PNG_CFLAGS?=-I../libs/libpng-src
-ifdef MINGW64
-	PNG_LDFLAGS?=-L../libs/libpng-src/projects -lpng64
-else
-	PNG_LDFLAGS?=-L../libs/libpng-src/projects -lpng32
-endif #MINGW64
-endif #PNG_CONFIG
-endif #NOPNG
-
-ifdef GETTEXT
-ifndef CCBS
-	MSGFMT?=../libs/gettext/bin32/msgfmt.exe
-endif
-ifdef MINGW64
-	CPPFLAGS+=-I../libs/gettext/include64
-	LDFLAGS+=-L../libs/gettext/lib64
-	LIBS+=-lmingwex
-else
-	CPPFLAGS+=-I../libs/gettext/include32
-	LDFLAGS+=-L../libs/gettext/lib32
-	STATIC_GETTEXT=1
-endif #MINGW64
-ifdef STATIC_GETTEXT
-	LIBS+=-lasprintf -lintl
-else
-	LIBS+=-lintl.dll
-endif #STATIC_GETTEXT
-endif #GETTEXT
-
-ifdef HAVE_MINIUPNPC
-	CPPFLAGS+=-I../libs/ -DSTATIC_MINIUPNPC
-ifdef MINGW64
-	LDFLAGS+=-L../libs/miniupnpc/mingw64
-else
-	LDFLAGS+=-L../libs/miniupnpc/mingw32
-endif #MINGW64
-endif
-
-ifndef NOCURL
-	CURL_CFLAGS+=-I../libs/curl/include
-ifdef MINGW64
-	CURL_LDFLAGS+=-L../libs/curl/lib64 -lcurl
-else
-	CURL_LDFLAGS+=-L../libs/curl/lib32 -lcurl
-endif #MINGW64
-endif
\ No newline at end of file
diff --git a/src/win32/Srb2win.rc b/src/win32/Srb2win.rc
index d5d59922c113a29af52c673700d8600b8be7804f..0a280448b48b13d4dc5c09cc674e4213f87a9995 100644
--- a/src/win32/Srb2win.rc
+++ b/src/win32/Srb2win.rc
@@ -76,8 +76,8 @@ END
 #include "../doomdef.h" // Needed for version string
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 2,2,8,0
- PRODUCTVERSION 2,2,8,0
+ FILEVERSION 2,2,9,0
+ PRODUCTVERSION 2,2,9,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
@@ -97,7 +97,7 @@ BEGIN
             VALUE "FileDescription", "Sonic Robo Blast 2\0"
             VALUE "FileVersion", VERSIONSTRING_RC
             VALUE "InternalName", "srb2\0"
-            VALUE "LegalCopyright", "Copyright 1998-2020 by Sonic Team Junior\0"
+            VALUE "LegalCopyright", "Copyright 1998-2021 by Sonic Team Junior\0"
             VALUE "LegalTrademarks", "Sonic the Hedgehog and related characters are trademarks of Sega.\0"
             VALUE "OriginalFilename", "srb2win.exe\0"
             VALUE "PrivateBuild", "\0"
diff --git a/src/win32/win_dll.c b/src/win32/win_dll.c
index d942d8cd406ad55b85b0e1bdd768631588f244ec..4743cec34b2e6af738caeec60d7c179e58ec14d1 100644
--- a/src/win32/win_dll.c
+++ b/src/win32/win_dll.c
@@ -111,7 +111,6 @@ static loadfunc_t hwdFuncTable[] = {
 	{"ReadRect@24",         &hwdriver.pfnReadRect},
 	{"GClipRect@20",        &hwdriver.pfnGClipRect},
 	{"ClearMipMapCache@0",  &hwdriver.pfnClearMipMapCache},
-	{"ClearCacheList@0",    &hwdriver.pfnClearCacheList},
 	{"SetSpecialState@8",   &hwdriver.pfnSetSpecialState},
 	{"DrawModel@16",        &hwdriver.pfnDrawModel},
 	{"SetTransform@4",      &hwdriver.pfnSetTransform},
@@ -145,7 +144,6 @@ static loadfunc_t hwdFuncTable[] = {
 	{"ReadRect",            &hwdriver.pfnReadRect},
 	{"GClipRect",           &hwdriver.pfnGClipRect},
 	{"ClearMipMapCache",    &hwdriver.pfnClearMipMapCache},
-	{"ClearCacheList",      &hwdriver.pfnClearCacheList},
 	{"SetSpecialState",     &hwdriver.pfnSetSpecialState},
 	{"DrawModel",           &hwdriver.pfnDrawModel},
 	{"SetTransform",        &hwdriver.pfnSetTransform},
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 061cbb5e1d45ed5659593b3f88944bba74e82e4d..f24436d4082e645fbbd9fe103a7ed33e741a18b7 100644
--- a/src/y_inter.c
+++ b/src/y_inter.c
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2004-2020 by Sonic Team Junior.
+// Copyright (C) 2004-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -212,7 +212,89 @@ 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);
+}
+
+
+//
+// Y_LoadIntermissionData
+//
+// Load patches for drawing the intermission, if acceptable
+//
+void Y_LoadIntermissionData(void)
+{
+	INT32 i;
+
+	if (dedicated)
+		return;
+
+	switch (intertype)
+	{
+		case int_coop:
+		{
+			for (i = 0; i < 4; ++i)
+			{
+				if (strlen(data.coop.bonuses[i].patch))
+					data.coop.bonuspatches[i] = W_CachePatchName(data.coop.bonuses[i].patch, PU_PATCH);
+			}
+			data.coop.ptotal = W_CachePatchName("YB_TOTAL", 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:
+		{
+			for (i = 0; i < 2; ++i)
+				data.spec.bonuspatches[i] = W_CachePatchName(data.spec.bonuses[i].patch, PU_PATCH);
+
+			data.spec.pscore = W_CachePatchName("YB_SCORE", PU_PATCH);
+			data.spec.pcontinues = W_CachePatchName("YB_CONTI", 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:
+		case int_teammatch:
+		{
+			if (!rflagico) //prevent a crash if we haven't cached our team graphics yet
+			{
+				rflagico = W_CachePatchName("RFLAGICO", PU_HUDGFX);
+				bflagico = W_CachePatchName("BFLAGICO", PU_HUDGFX);
+				rmatcico = W_CachePatchName("RMATCICO", PU_HUDGFX);
+				bmatcico = W_CachePatchName("BMATCICO", PU_HUDGFX);
+			}
+
+			data.match.redflag = (intertype == int_ctf) ? rflagico : rmatcico;
+			data.match.blueflag = (intertype == int_ctf) ? bflagico : bmatcico;
+		}
+		/* FALLTHRU */
+		case int_match:
+		case int_race:
+		case int_comp:
+		{
+			if (intertype == int_match || intertype == int_race)
+			{
+				// get RESULT header
+				data.match.result = W_CachePatchName("RESULT", PU_PATCH);
+			}
+
+			// get background tile
+			bgtile = W_CachePatchName("SRB2BACK", PU_PATCH);
+			break;
+		}
+		case int_none:
+		default:
+			break;
+	}
 }
 
 //
@@ -347,7 +429,7 @@ void Y_IntermissionDrawer(void)
 	else if (bgtile)
 		V_DrawPatchFill(bgtile);
 
-	LUAh_IntermissionHUD();
+	LUA_HUDHOOK(intermission);
 	if (!LUA_HudEnabled(hud_intermissiontally))
 		goto skiptallydrawer;
 
@@ -388,14 +470,17 @@ void Y_IntermissionDrawer(void)
 			}
 		}
 
-		// draw the "got through act" lines and act number
-		V_DrawLevelTitle(data.coop.passedx1, 49, 0, data.coop.passed1);
+		if (LUA_HudEnabled(hud_intermissiontitletext))
 		{
-			INT32 h = V_LevelNameHeight(data.coop.passed2);
-			V_DrawLevelTitle(data.coop.passedx2, 49+h+2, 0, data.coop.passed2);
+			// draw the "got through act" lines and act number
+			V_DrawLevelTitle(data.coop.passedx1, 49, 0, data.coop.passed1);
+			{
+				INT32 h = V_LevelNameHeight(data.coop.passed2);
+				V_DrawLevelTitle(data.coop.passedx2, 49+h+2, 0, data.coop.passed2);
 
-			if (data.coop.actnum)
-				V_DrawLevelActNum(244, 42+h, 0, data.coop.actnum);
+				if (data.coop.actnum)
+					V_DrawLevelActNum(244, 42+h, 0, data.coop.actnum);
+			}
 		}
 
 		bonusy = 150;
@@ -479,37 +564,44 @@ void Y_IntermissionDrawer(void)
 
 		if (drawsection == 1)
 		{
-			const char *ringtext = "\x82" "50 rings, no shield";
-			const char *tut1text = "\x82" "press " "\x80" "spin";
-			const char *tut2text = "\x82" "mid-" "\x80" "jump";
-			ttheight = 8;
-			V_DrawLevelTitle(data.spec.passedx1 + xoffset1, ttheight, 0, data.spec.passed1);
-			ttheight += V_LevelNameHeight(data.spec.passed3) + 2;
-			V_DrawLevelTitle(data.spec.passedx3 + xoffset2, ttheight, 0, data.spec.passed3);
-			ttheight += V_LevelNameHeight(data.spec.passed4) + 2;
-			V_DrawLevelTitle(data.spec.passedx4 + xoffset3, ttheight, 0, data.spec.passed4);
-
-			ttheight = 108;
-			V_DrawLevelTitle(BASEVIDWIDTH/2 + xoffset4 - (V_LevelNameWidth(ringtext)/2), ttheight, 0, ringtext);
-			ttheight += V_LevelNameHeight(tut1text) + 2;
-			V_DrawLevelTitle(BASEVIDWIDTH/2 + xoffset5 - (V_LevelNameWidth(tut1text)/2), ttheight, 0, tut1text);
-			ttheight += V_LevelNameHeight(tut2text) + 2;
-			V_DrawLevelTitle(BASEVIDWIDTH/2 + xoffset6 - (V_LevelNameWidth(tut2text)/2), ttheight, 0, tut2text);
+			if (LUA_HudEnabled(hud_intermissiontitletext))
+			{
+				const char *ringtext = "\x82" "50 rings, no shield";
+				const char *tut1text = "\x82" "press " "\x80" "spin";
+				const char *tut2text = "\x82" "mid-" "\x80" "jump";
+				ttheight = 8;
+				V_DrawLevelTitle(data.spec.passedx1 + xoffset1, ttheight, 0, data.spec.passed1);
+				ttheight += V_LevelNameHeight(data.spec.passed3) + 2;
+				V_DrawLevelTitle(data.spec.passedx3 + xoffset2, ttheight, 0, data.spec.passed3);
+				ttheight += V_LevelNameHeight(data.spec.passed4) + 2;
+				V_DrawLevelTitle(data.spec.passedx4 + xoffset3, ttheight, 0, data.spec.passed4);
+
+				ttheight = 108;
+				V_DrawLevelTitle(BASEVIDWIDTH/2 + xoffset4 - (V_LevelNameWidth(ringtext)/2), ttheight, 0, ringtext);
+				ttheight += V_LevelNameHeight(tut1text) + 2;
+				V_DrawLevelTitle(BASEVIDWIDTH/2 + xoffset5 - (V_LevelNameWidth(tut1text)/2), ttheight, 0, tut1text);
+				ttheight += V_LevelNameHeight(tut2text) + 2;
+				V_DrawLevelTitle(BASEVIDWIDTH/2 + xoffset6 - (V_LevelNameWidth(tut2text)/2), ttheight, 0, tut2text);
+			}
 		}
 		else
 		{
 			INT32 yoffset = 0;
-			if (data.spec.passed1[0] != '\0')
-			{
-				ttheight = 24;
-				V_DrawLevelTitle(data.spec.passedx1 + xoffset1, ttheight, 0, data.spec.passed1);
-				ttheight += V_LevelNameHeight(data.spec.passed2) + 2;
-				V_DrawLevelTitle(data.spec.passedx2 + xoffset2, ttheight, 0, data.spec.passed2);
-			}
-			else
+
+			if (LUA_HudEnabled(hud_intermissiontitletext))
 			{
-				ttheight = 24 + (V_LevelNameHeight(data.spec.passed2)/2) + 2;
-				V_DrawLevelTitle(data.spec.passedx2 + xoffset1, ttheight, 0, data.spec.passed2);
+				if (data.spec.passed1[0] != '\0')
+				{
+					ttheight = 24;
+					V_DrawLevelTitle(data.spec.passedx1 + xoffset1, ttheight, 0, data.spec.passed1);
+					ttheight += V_LevelNameHeight(data.spec.passed2) + 2;
+					V_DrawLevelTitle(data.spec.passedx2 + xoffset2, ttheight, 0, data.spec.passed2);
+				}
+				else
+				{
+					ttheight = 24 + (V_LevelNameHeight(data.spec.passed2)/2) + 2;
+					V_DrawLevelTitle(data.spec.passedx2 + xoffset1, ttheight, 0, data.spec.passed2);
+				}
 			}
 
 			V_DrawScaledPatch(152 + xoffset3, 108, 0, data.spec.bonuspatches[0]);
@@ -555,6 +647,7 @@ void Y_IntermissionDrawer(void)
 
 		// draw the emeralds
 		//if (intertic & 1)
+		if (LUA_HudEnabled(hud_intermissionemeralds))
 		{
 			boolean drawthistic = !(ALL7EMERALDS(emeralds) && (intertic & 1));
 			INT32 emeraldx = 152 - 3*28;
@@ -927,7 +1020,8 @@ void Y_Ticker(void)
 	if (paused || P_AutoPause())
 		return;
 
-	LUAh_IntermissionThinker();
+	LUA_HookBool(intertype == int_spec && stagefailed,
+			HOOK(IntermissionThinker));
 
 	intertic++;
 
@@ -1181,10 +1275,9 @@ void Y_DetermineIntermissionType(void)
 //
 // Called by G_DoCompleted. Sets up data for intermission drawer/ticker.
 //
+//
 void Y_StartIntermission(void)
 {
-	INT32 i;
-
 	intertic = -1;
 
 #ifdef PARANOIA
@@ -1228,20 +1321,12 @@ void Y_StartIntermission(void)
 			// setup time data
 			data.coop.tics = players[consoleplayer].realtime;
 
-			for (i = 0; i < 4; ++i)
-				data.coop.bonuspatches[i] = W_CachePatchName(data.coop.bonuses[i].patch, PU_PATCH);
-			data.coop.ptotal = W_CachePatchName("YB_TOTAL", PU_PATCH);
-
 			// get act number
 			data.coop.actnum = mapheaderinfo[gamemap-1]->actnum;
 
-			// 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);
 				useinterpic = true;
 				usebuffer = false;
 			}
@@ -1256,24 +1341,40 @@ void Y_StartIntermission(void)
 			usetile = false;
 
 			// set up the "got through act" message according to skin name
-			// too long so just show "YOU GOT THROUGH THE ACT"
-			if (strlen(skins[players[consoleplayer].skin].realname) > 13)
-			{
-				strcpy(data.coop.passed1, "you got");
-				strcpy(data.coop.passed2, (mapheaderinfo[gamemap-1]->actnum) ? "through act" : "through the act");
-			}
-			// long enough that "X GOT" won't fit so use "X PASSED THE ACT"
-			else if (strlen(skins[players[consoleplayer].skin].realname) > 8)
+			if (stagefailed)
 			{
-				strcpy(data.coop.passed1, skins[players[consoleplayer].skin].realname);
-				strcpy(data.coop.passed2, (mapheaderinfo[gamemap-1]->actnum) ? "passed act" : "passed the act");
+				strcpy(data.coop.passed1, mapheaderinfo[gamemap-1]->lvlttl);
+
+				if (mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE)
+				{
+					data.spec.passed2[0] = '\0';
+				}
+				else
+				{
+					strcpy(data.coop.passed2, "Zone");
+				}
 			}
-			// length is okay for normal use
 			else
 			{
-				snprintf(data.coop.passed1, sizeof data.coop.passed1, "%s got",
-					skins[players[consoleplayer].skin].realname);
-				strcpy(data.coop.passed2, (mapheaderinfo[gamemap-1]->actnum) ? "through act" : "through the act");
+				// too long so just show "YOU GOT THROUGH THE ACT"
+				if (strlen(skins[players[consoleplayer].skin].realname) > 13)
+				{
+					strcpy(data.coop.passed1, "you got");
+					strcpy(data.coop.passed2, (mapheaderinfo[gamemap-1]->actnum) ? "through act" : "through the act");
+				}
+				// long enough that "X GOT" won't fit so use "X PASSED THE ACT"
+				else if (strlen(skins[players[consoleplayer].skin].realname) > 8)
+				{
+					strcpy(data.coop.passed1, skins[players[consoleplayer].skin].realname);
+					strcpy(data.coop.passed2, (mapheaderinfo[gamemap-1]->actnum) ? "passed act" : "passed the act");
+				}
+				// length is okay for normal use
+				else
+				{
+					snprintf(data.coop.passed1, sizeof data.coop.passed1, "%s got",
+						skins[players[consoleplayer].skin].realname);
+					strcpy(data.coop.passed2, (mapheaderinfo[gamemap-1]->actnum) ? "through act" : "through the act");
+				}
 			}
 
 			// set X positions
@@ -1290,6 +1391,13 @@ void Y_StartIntermission(void)
 			// The above value is not precalculated because it needs only be computed once
 			// at the start of intermission, and precalculating it would preclude mods
 			// changing the font to one of a slightly different width.
+
+			if ((stagefailed) && !(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
+			{
+				// Bit of a hack, offset so that the "Zone" text is right aligned like title cards.
+				data.coop.passedx2 = (data.coop.passedx1 + V_LevelNameWidth(data.coop.passed1)) - V_LevelNameWidth(data.coop.passed2);
+			}
+
 			break;
 		}
 
@@ -1298,21 +1406,9 @@ void Y_StartIntermission(void)
 			// give out ring bonuses
 			Y_AwardSpecialStageBonus();
 
-			for (i = 0; i < 2; ++i)
-				data.spec.bonuspatches[i] = W_CachePatchName(data.spec.bonuses[i].patch, PU_PATCH);
-
-			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);
 				useinterpic = true;
-			}
 			else
 				useinterpic = false;
 
@@ -1405,11 +1501,6 @@ void Y_StartIntermission(void)
 
 			data.match.levelstring[sizeof data.match.levelstring - 1] = '\0';
 
-			// get RESULT header
-			data.match.result =
-				W_CachePatchName("RESULT", PU_PATCH);
-
-			bgtile = W_CachePatchName("SRB2BACK", PU_PATCH);
 			usetile = true;
 			useinterpic = false;
 			break;
@@ -1434,10 +1525,6 @@ void Y_StartIntermission(void)
 
 			data.match.levelstring[sizeof data.match.levelstring - 1] = '\0';
 
-			// get RESULT header
-			data.match.result = W_CachePatchName("RESULT", PU_PATCH);
-
-			bgtile = W_CachePatchName("SRB2BACK", PU_PATCH);
 			usetile = true;
 			useinterpic = false;
 			break;
@@ -1463,18 +1550,6 @@ void Y_StartIntermission(void)
 
 			data.match.levelstring[sizeof data.match.levelstring - 1] = '\0';
 
-			if (intertype == int_ctf)
-			{
-				data.match.redflag = rflagico;
-				data.match.blueflag = bflagico;
-			}
-			else // team match
-			{
-				data.match.redflag = rmatcico;
-				data.match.blueflag = bmatcico;
-			}
-
-			bgtile = W_CachePatchName("SRB2BACK", PU_PATCH);
 			usetile = true;
 			useinterpic = false;
 			break;
@@ -1499,8 +1574,6 @@ void Y_StartIntermission(void)
 
 			data.competition.levelstring[sizeof data.competition.levelstring - 1] = '\0';
 
-			// get background tile
-			bgtile = W_CachePatchName("SRB2BACK", PU_PATCH);
 			usetile = true;
 			useinterpic = false;
 			break;
@@ -1733,7 +1806,6 @@ static void Y_SetNullBonus(player_t *player, y_bonus_t *bstruct)
 {
 	(void)player;
 	memset(bstruct, 0, sizeof(y_bonus_t));
-	strncpy(bstruct->patch, "MISSING", sizeof(bstruct->patch));
 }
 
 //
@@ -1746,21 +1818,30 @@ static void Y_SetTimeBonus(player_t *player, y_bonus_t *bstruct)
 	strncpy(bstruct->patch, "YB_TIME", sizeof(bstruct->patch));
 	bstruct->display = true;
 
-	// calculate time bonus
-	secs = player->realtime / TICRATE;
-	if      (secs <  30) /*   :30 */ bonus = 50000;
-	else if (secs <  60) /*  1:00 */ bonus = 10000;
-	else if (secs <  90) /*  1:30 */ bonus = 5000;
-	else if (secs < 120) /*  2:00 */ bonus = 4000;
-	else if (secs < 180) /*  3:00 */ bonus = 3000;
-	else if (secs < 240) /*  4:00 */ bonus = 2000;
-	else if (secs < 300) /*  5:00 */ bonus = 1000;
-	else if (secs < 360) /*  6:00 */ bonus = 500;
-	else if (secs < 420) /*  7:00 */ bonus = 400;
-	else if (secs < 480) /*  8:00 */ bonus = 300;
-	else if (secs < 540) /*  9:00 */ bonus = 200;
-	else if (secs < 600) /* 10:00 */ bonus = 100;
-	else  /* TIME TAKEN: TOO LONG */ bonus = 0;
+	if (stagefailed == true)
+	{
+		// Time Bonus would be very easy to cheese by failing immediately.
+		bonus = 0;
+	}
+	else
+	{
+		// calculate time bonus
+		secs = player->realtime / TICRATE;
+		if      (secs <  30) /*   :30 */ bonus = 50000;
+		else if (secs <  60) /*  1:00 */ bonus = 10000;
+		else if (secs <  90) /*  1:30 */ bonus = 5000;
+		else if (secs < 120) /*  2:00 */ bonus = 4000;
+		else if (secs < 180) /*  3:00 */ bonus = 3000;
+		else if (secs < 240) /*  4:00 */ bonus = 2000;
+		else if (secs < 300) /*  5:00 */ bonus = 1000;
+		else if (secs < 360) /*  6:00 */ bonus = 500;
+		else if (secs < 420) /*  7:00 */ bonus = 400;
+		else if (secs < 480) /*  8:00 */ bonus = 300;
+		else if (secs < 540) /*  9:00 */ bonus = 200;
+		else if (secs < 600) /* 10:00 */ bonus = 100;
+		else  /* TIME TAKEN: TOO LONG */ bonus = 0;
+	}
+
 	bstruct->points = bonus;
 }
 
@@ -1813,12 +1894,21 @@ static void Y_SetGuardBonus(player_t *player, y_bonus_t *bstruct)
 	strncpy(bstruct->patch, "YB_GUARD", sizeof(bstruct->patch));
 	bstruct->display = true;
 
-	if      (player->timeshit == 0) bonus = 10000;
-	else if (player->timeshit == 1) bonus = 5000;
-	else if (player->timeshit == 2) bonus = 1000;
-	else if (player->timeshit == 3) bonus = 500;
-	else if (player->timeshit == 4) bonus = 100;
-	else                            bonus = 0;
+	if (stagefailed == true)
+	{
+		// "No-hit" runs would be very easy to cheese by failing immediately.
+		bonus = 0;
+	}
+	else
+	{
+		if      (player->timeshit == 0) bonus = 10000;
+		else if (player->timeshit == 1) bonus = 5000;
+		else if (player->timeshit == 2) bonus = 1000;
+		else if (player->timeshit == 3) bonus = 500;
+		else if (player->timeshit == 4) bonus = 100;
+		else                            bonus = 0;
+	}
+
 	bstruct->points = bonus;
 }
 
@@ -2031,7 +2121,8 @@ static void Y_AwardSpecialStageBonus(void)
 //
 void Y_EndIntermission(void)
 {
-	Y_UnloadData();
+	if (!dedicated)
+		Y_UnloadData();
 
 	endtic = -1;
 	intertype = int_none;
diff --git a/src/y_inter.h b/src/y_inter.h
index 859144b1d4ad71f7a98a797327537a213e0970b2..871142858ba1df05f0163b19a9cf9efeeafe560a 100644
--- a/src/y_inter.h
+++ b/src/y_inter.h
@@ -1,6 +1,6 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2004-2020 by Sonic Team Junior.
+// Copyright (C) 2004-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -14,6 +14,7 @@ extern boolean usebuffer;
 void Y_IntermissionDrawer(void);
 void Y_Ticker(void);
 
+void Y_LoadIntermissionData(void);
 void Y_StartIntermission(void);
 void Y_EndIntermission(void);
 
diff --git a/src/z_zone.c b/src/z_zone.c
index ad64a3a07f04f01d40ac291cfa4e77f26bd37e88..34ff3d37ef3a025beaa925228d5c6a18c68f2228 100644
--- a/src/z_zone.c
+++ b/src/z_zone.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 2006      by Graue.
-// Copyright (C) 2006-2020 by Sonic Team Junior.
+// Copyright (C) 2006-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -813,12 +813,12 @@ static void Command_Memfree_f(void)
 #ifdef HWRENDER
 	if (rendermode == render_opengl)
 	{
-		CONS_Printf(M_GetText("Patch info headers: %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRPATCHINFO)>>10));
-		CONS_Printf(M_GetText("Mipmap patches    : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRPATCHCOLMIPMAP)>>10));
-		CONS_Printf(M_GetText("HW Texture cache  : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRCACHE)>>10));
-		CONS_Printf(M_GetText("Plane polygons    : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRPLANE)>>10));
-		CONS_Printf(M_GetText("HW model textures : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRMODELTEXTURE)>>10));
-		CONS_Printf(M_GetText("HW Texture used   : %7d KB\n"), HWR_GetTextureUsed()>>10);
+		CONS_Printf(M_GetText("Patch info headers     : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRPATCHINFO)>>10));
+		CONS_Printf(M_GetText("Cached textures        : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRCACHE)>>10));
+		CONS_Printf(M_GetText("Texture colormaps      : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRPATCHCOLMIPMAP)>>10));
+		CONS_Printf(M_GetText("Model textures         : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRMODELTEXTURE)>>10));
+		CONS_Printf(M_GetText("Plane polygons         : %7s KB\n"), sizeu1(Z_TagUsage(PU_HWRPLANE)>>10));
+		CONS_Printf(M_GetText("All GPU textures       : %7d KB\n"), HWR_GetTextureUsed()>>10);
 	}
 #endif
 
diff --git a/src/z_zone.h b/src/z_zone.h
index e80a45e7fb4f2ed6223868fc78d18649e75ab4ad..17f572a905b56597bf955ee28b53814c6108bf04 100644
--- a/src/z_zone.h
+++ b/src/z_zone.h
@@ -2,7 +2,7 @@
 //-----------------------------------------------------------------------------
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2020 by Sonic Team Junior.
+// Copyright (C) 1999-2021 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -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
@@ -68,8 +69,7 @@ enum
 	PU_HWRCACHE_UNLOCKED     = 102, // 'unlocked' PU_HWRCACHE memory:
 									// 'second-level' cache for graphics
                                     // stored in hardware format and downloaded as needed
-	PU_HWRPATCHINFO_UNLOCKED    = 103, // 'unlocked' PU_HWRPATCHINFO memory
-	PU_HWRMODELTEXTURE_UNLOCKED = 104, // 'unlocked' PU_HWRMODELTEXTURE memory
+	PU_HWRMODELTEXTURE_UNLOCKED = 103, // 'unlocked' PU_HWRMODELTEXTURE memory
 };
 
 //