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/dep/.gitignore b/dep/.gitignore
deleted file mode 100644
index fb941664fc5718a31185a0cf67ccd2cfd68a2be8..0000000000000000000000000000000000000000
--- a/dep/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-#All folders
-*.d
diff --git a/dep/FreeBSD/SDL/Debug/.gitignore b/dep/FreeBSD/SDL/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/FreeBSD/SDL/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/FreeBSD/SDL/Release/.gitignore b/dep/FreeBSD/SDL/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/FreeBSD/SDL/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/Linux/SDL/Debug/.gitignore b/dep/Linux/SDL/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/Linux/SDL/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/Linux/SDL/Release/.gitignore b/dep/Linux/SDL/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/Linux/SDL/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/Linux64/SDL/Debug/.gitignore b/dep/Linux64/SDL/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/Linux64/SDL/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/Linux64/SDL/Release/.gitignore b/dep/Linux64/SDL/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/Linux64/SDL/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/MasterClient/.gitignore b/dep/MasterClient/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/MasterClient/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/MasterServer/.gitignore b/dep/MasterServer/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/MasterServer/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/Mingw/Debug/.gitignore b/dep/Mingw/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/Mingw/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/Mingw/Release/.gitignore b/dep/Mingw/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/Mingw/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/Mingw/SDL/Debug/.gitignore b/dep/Mingw/SDL/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/Mingw/SDL/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/Mingw/SDL/Release/.gitignore b/dep/Mingw/SDL/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/Mingw/SDL/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/Mingw64/Debug/.gitignore b/dep/Mingw64/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/Mingw64/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/Mingw64/Release/.gitignore b/dep/Mingw64/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/Mingw64/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/Mingw64/SDL/Debug/.gitignore b/dep/Mingw64/SDL/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/Mingw64/SDL/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/Mingw64/SDL/Release/.gitignore b/dep/Mingw64/SDL/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/Mingw64/SDL/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/SDL/Release/.gitignore b/dep/SDL/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/SDL/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/VC/.gitignore b/dep/VC/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/VC/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/VC9/.gitignore b/dep/VC9/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/VC9/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/cygwin/Debug/.gitignore b/dep/cygwin/Debug/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/cygwin/Debug/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/cygwin/Release/.gitignore b/dep/cygwin/Release/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/cygwin/Release/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/dep/dummy/.gitignore b/dep/dummy/.gitignore
deleted file mode 100644
index 42c6dc2c662642792a8860e166dfd81126695e8f..0000000000000000000000000000000000000000
--- a/dep/dummy/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# DON'T REMOVE
-# This keeps the folder from disappearing
diff --git a/extras/conf/SRB2-22.cfg b/extras/conf/SRB2-22.cfg
index 3fd4b6ccdc0f6b24f1533d912daf234042dd870e..f457fe9721000f0efc7b690b13250387a42cb355 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";
@@ -3059,6 +3092,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";
@@ -3484,6 +3589,7 @@ thingtypes
 			sprite = "ARCHA1";
 			width = 24;
 			height = 32;
+			flags8text = "[8] Don't jump away";
 		}
 		118
 		{
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 87a0499b6d088b5bb51cf36658de9238397b3402..721cd6dca4d04be6d6153a00043ea5029d653116 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,92 +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_taglib.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})
@@ -369,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_LIBGME)
 	else()
 		message(WARNING "You have specified that GME is available but it was not found.")
 	endif()
@@ -389,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()
@@ -412,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()
@@ -433,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()
@@ -454,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()
@@ -482,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()
@@ -490,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()
@@ -563,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
@@ -604,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 a4c3c4fdbaea6992f150d85f495c2becd328008b..9659a4994c1dce6e94981ff090b0b4f1e9a171bf 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -1,824 +1,416 @@
-
-#     GNU Make makefile for SRB2
-#############################################################################
-# Copyright (C) 1998-2000 by DooM Legacy Team.
-# Copyright (C) 2003-2021 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 SDL/Mingw version with 'make MINGW=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 dependency files
-#     distclean
-#       Remove autogenerated files
-#     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.
 #
-#############################################################################
-
-,=,
-
-ifeq (,$(filter-out cleandep clean distclean,$(or $(MAKECMDGOALS),all)))
-CLEANONLY=1
-else ifndef SILENT
-echo=@echo "$(1)"
-ifndef MAKE_RESTARTS
-print=$(info $(1))
-endif
-endif
-
-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...
-
- $(call print,Detected a Windows system$(,) compiling for 32-bit MinGW SDL2...)
-
- # go for a 32-bit sdl mingw exe by default
- MINGW=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
-
- $(call print,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
+# 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 PANDORA
-LINUX=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 LINUX64
-LINUX=1
-NONX86=1
-# LINUX64 does not imply X86_64=1; could mean ARM64 or Itanium
-endif
+clean_targets=cleandep clean distclean info
 
-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
+.PHONY : $(clean_targets) all
 
-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
+goals:=$(or $(MAKECMDGOALS),all)
+cleanonly:=$(filter $(clean_targets),$(goals))
+destructive:=$(filter-out info,$(cleanonly))
 
-ifdef HAIKU
-NOIPX=1
-NOASM=1
-ifndef NONET
-LIBS=-lnetwork
-endif
-CFLAGS+=-DUNIXCOMMON
-PNG_CFLAGS?=
-PNG_LDFLAGS?=-lpng
+ifndef cleanonly
+include Makefile.d/old.mk
 endif
 
-ifdef PANDORA
-NONX86=1
-NOHW=1
-endif
+include Makefile.d/util.mk
 
-ifndef NOOPENMPT
-HAVE_OPENMPT=1
-endif
-
-ifdef MINGW
-include win32/Makefile.cfg
-endif #ifdef MINGW
-
-ifdef UNIX
-UNIXCOMMON=1
+ifdef PREFIX
+CC:=$(PREFIX)-gcc
 endif
 
-ifdef LINUX
-UNIXCOMMON=1
-ifndef NOGME
-HAVE_LIBGME=1
-endif
-endif
+OBJDUMP_OPTS?=--wide --source --line-numbers
 
-ifdef SOLARIS
-UNIXCOMMON=1
-endif
+OBJCOPY:=$(call Prefix,objcopy)
+OBJDUMP:=$(call Prefix,objdump)
+WINDRES:=$(call Prefix,windres)
 
-ifdef FREEBSD
-UNIXCOMMON=1
-endif
-
-ifdef MACOSX
-UNIXCOMMON=1
+ifdef YASM
+NASM?=yasm
+else
+NASM?=nasm
 endif
 
-ifdef SDL
-	include sdl/Makefile.cfg
-endif #ifdef SDL
-
-ifdef DISTCC
-        CC:=distcc $(CC)
+ifdef YASM
+ifdef STABS
+NASMOPTS?=-g stabs
+else
+NASMOPTS?=-g dwarf2
 endif
-
-ifdef CCACHE
-        CC:=ccache $(CC)
+else
+NASMOPTS?=-g
 endif
 
-MSGFMT?=msgfmt
-
+GZIP?=gzip
+GZIP_OPTS?=-9 -f -n
 ifdef WINDOWSHELL
-	COMPTIME=-..\comptime.bat
-else
-	COMPTIME=-../comptime.sh
+GZIP_OPTS+=--rsyncable
 endif
 
+UPX_OPTS?=--best --preserve-build-id
 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
-	COMPTIME:=@$(COMPTIME)
-endif
-
-ifdef NONET
-	OPTS+=-DNONET
-	NOCURL=1
-else
-ifdef NO_IPV6
-	OPTS+=-DNO_IPV6
-endif
-endif
-
-ifdef NOHW
-	OPTS+=-DNOHW
-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
+UPX_OPTS+=-qq
 endif
 
-OPTS += -DCOMPVERSION
-
-ifndef NONX86
-ifndef GCC29
-	ARCHOPTS?=-msse3 -mfpmath=sse
-else
-	ARCHOPTS?=-mpentium
-endif
-else
-ifdef X86_64
-	ARCHOPTS?=-march=nocona
-endif
-endif
+include Makefile.d/detect.mk
 
-ifndef NOASM
-ifndef NONX86
-	OBJS+=$(OBJDIR)/tmap.o $(OBJDIR)/tmap_mmx.o
-	OPTS+=-DUSEASM
-endif
-endif
+# make would try to remove the implicitly made directories
+.PRECIOUS : %/ comptime.c
 
-ifndef NOPNG
-OPTS+=-DHAVE_PNG
+sources:=
+makedir:=../make
 
-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
+# -DCOMPVERSION: flag to use comptime.h
+opts:=-DCOMPVERSION -g
+libs:=
 
-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
-endif
+nasm_format:=
 
-ifdef LINUX
-PNG_CFLAGS+=-D_LARGEFILE64_SOURCE
-endif
+# This is a list of variables names, of which if defined,
+# also defines the name as a macro to the compiler.
+passthru_opts:=
 
-LIBS+=$(PNG_LDFLAGS)
-CFLAGS+=$(PNG_CFLAGS)
+include Makefile.d/platform.mk
+include Makefile.d/features.mk
+include Makefile.d/versions.mk
 
-OBJS+=$(OBJDIR)/apng.o
+ifdef DEBUGMODE
+makedir:=$(makedir)/debug
 endif
 
-ifdef HAVE_LIBGME
-OPTS+=-DHAVE_LIBGME
+depdir:=$(makedir)/deps
+objdir:=$(makedir)/objs
 
-LIBGME_PKGCONFIG?=libgme
-LIBGME_CFLAGS?=$(shell $(PKG_CONFIG) $(LIBGME_PKGCONFIG) --cflags)
-LIBGME_LDFLAGS?=$(shell $(PKG_CONFIG) $(LIBGME_PKGCONFIG) --libs)
+# very sophisticated dependency
+sources+=\
+	$(call List,Sourcefile)\
+	$(call List,blua/Sourcefile)\
 
-LIBS+=$(LIBGME_LDFLAGS)
-CFLAGS+=$(LIBGME_CFLAGS)
-endif
+depends:=$(basename $(filter %.c %.s,$(sources)))
+objects:=$(basename $(filter %.c %.s %.nas,$(sources)))
 
-ifdef HAVE_OPENMPT
-OPTS+=-DHAVE_OPENMPT
+depends:=$(depends:%=$(depdir)/%.d)
 
-LIBOPENMPT_PKGCONFIG?=libopenmpt
-LIBOPENMPT_CFLAGS?=$(shell $(PKG_CONFIG) $(LIBOPENMPT_PKGCONFIG) --cflags)
-LIBOPENMPT_LDFLAGS?=$(shell $(PKG_CONFIG) $(LIBOPENMPT_PKGCONFIG) --libs)
+# 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
 
-LIBS+=$(LIBOPENMPT_LDFLAGS)
-CFLAGS+=$(LIBOPENMPT_CFLAGS)
+# windows resource file
+rc_file:=$(basename $(filter %.rc,$(sources)))
+ifdef rc_file
+objects+=$(rc_file:=.res)
 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)
+objects:=$(addprefix $(objdir)/,$(objects))
 
-LIBS+=$(ZLIB_LDFLAGS)
-CFLAGS+=$(ZLIB_CFLAGS)
+ifdef DEBUGMODE
+bin:=../bin/debug
 else
-NOPNG=1
+bin:=../bin
 endif
 
-ifndef NOCURL
-OPTS+=-DHAVE_CURL
-CURLCONFIG?=curl-config
-CURL_CFLAGS?=$(shell $(CURLCONFIG) --cflags)
-CURL_LDFLAGS?=$(shell $(CURLCONFIG) --libs)
-
-LIBS+=$(CURL_LDFLAGS)
-CFLAGS+=$(CURL_CFLAGS)
-endif
+# default EXENAME (usually set by platform)
+EXENAME?=srb2
+DBGNAME?=$(EXENAME).debug
 
-ifdef STATIC
-LIBS:=-static $(LIBS)
-endif
+exe:=$(bin)/$(EXENAME)
+dbg:=$(bin)/$(DBGNAME)
 
-ifdef HAVE_MINIUPNPC
-ifdef NONET
-HAVE_MINIUPNPC=''
-else
-LIBS+=-lminiupnpc
-ifdef MINGW
-LIBS+=-lws2_32 -liphlpapi
-endif
-CFLAGS+=-DHAVE_MINIUPNPC
-endif
-endif
+build_done==== Build is done, look for \
+           $(<F) at $(abspath $(<D)) ===
 
-include blua/Makefile.cfg
+all : $(exe)
+	$(call Echo,$(build_done))
 
-ifdef NOMD5
-	OPTS+=-DNOMD5
-else
-	OBJS:=$(OBJDIR)/md5.o $(OBJS)
+ifndef VALGRIND
+dump : $(dbg).txt
 endif
 
-ifdef NOPOSTPROCESSING
-	OPTS+=-DNOPOSTPROCESSING
+ifdef STATIC
+libs+=-static
 endif
 
-	OPTS:=-fno-exceptions $(OPTS)
-
-ifdef MOBJCONSISTANCY
-	OPTS+=-DMOBJCONSISTANCY
+# build with profiling information
+ifdef PROFILEMODE
+opts+=-pg
+libs+=-pg
 endif
 
-ifdef PACKETDROP
-	OPTS+=-DPACKETDROP
+ifdef DEBUGMODE
+debug_opts=-D_DEBUG
+else # build a normal optimized version
+debug_opts=-DNDEBUG
+opts+=-O3
 endif
 
-ifdef DEBUGMODE
+# debug_opts also get passed to windres
+opts+=$(debug_opts)
 
-	# build with debugging information
-	WINDRESFLAGS = -D_DEBUG
-ifdef GCC48
-	CFLAGS+=-Og
-else
-	CFLAGS+=-O0
-endif
-	CFLAGS+= -Wall -DPARANOIA -DRANGECHECK -DPACKETDROP
-else
+opts+=$(foreach v,$(passthru_opts),$(if $($(v)),-D$(v)))
 
+opts+=$(WFLAGS) $(CPPFLAGS) $(CFLAGS)
+libs+=$(LDFLAGS)
+asflags:=$(ASFLAGS) -x assembler-with-cpp
 
-	# build a normal optimised version
-	WINDRESFLAGS = -DNDEBUG
-	CFLAGS+=-O3
-endif
-	CFLAGS+=-g $(OPTS) $(ARCHOPTS) $(WINDRESFLAGS)
+cc=$(CC)
 
-ifdef YASM
-ifdef STABS
-	NASMOPTS?= -g stabs
-else
-	NASMOPTS?= -g dwarf2
-endif
-else
-	NASMOPTS?= -g
+ifdef DISTCC
+cc=distcc $(CC)
 endif
 
-ifdef PROFILEMODE
-	# build with profiling information
-	CFLAGS+=-pg
-	LDFLAGS+=-pg
+ifdef CCACHE
+cc=ccache $(CC)
 endif
 
-ifdef ZDEBUG
-	CPPFLAGS+=-DZDEBUG
-endif
+ifndef SILENT
+# makefile will 'restart' when it finishes including the
+# dependencies.
+ifndef MAKE_RESTARTS
+ifndef destructive
+$(shell $(CC) -v)
+define flags =
 
-OPTS+=$(CPPFLAGS)
+SHELL ..... $(SHELL)
 
-# default EXENAME if all else fails
-EXENAME?=srb2
-DBGNAME?=$(EXENAME).debug
+CC ........ $(cc)
 
-# $(OBJDIR)/dstrings.o \
-
-# not too sophisticated dependency
-OBJS:=$(i_main_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)
-
-DEPS:=$(patsubst $(OBJDIR)/%.o,$(DEPDIR)/%.d,$(filter %.o,$(OBJS)))
-OBJS+=$(OBJDIR)/comptime.o
+CFLAGS .... $(opts)
+
+LDFLAGS ... $(libs)
 
-ifndef SILENT
-ifndef ECHO
-ifndef NOECHOFILENAMES
-define echoName =
-	@echo -- $< ...
 endef
+$(info $(flags))
 endif
+# don't generate dependency files if only cleaning
+ifndef cleanonly
+$(info Checking dependency files...)
+include $(depends)
 endif
 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
-
-OPTS+=-DGETTEXT
 endif
 
-ifdef PANDORA
-all: $(BIN)/$(PNDNAME)
+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
 
+%/ :
+	$(.)$(mkdir) $(call Windows_path,$@)
 
-ifdef SDL
-all: $(BIN)/$(EXENAME)
-endif
-
-ifdef DUMMY
-all:	$(BIN)/$(EXENAME)
-endif
-
-cleandep:
-	$(REMOVE) $(DEPS)
-	$(REMOVE) comptime.h
+# this is needed so the target can be referenced in the
+# prerequisites
+.SECONDEXPANSION :
 
-clean:
-	$(REMOVE) *~ *.flc
-	$(REMOVE) $(OBJDIR)/*.o
+# 'UPX' is also recognized in the environment by upx
+unexport UPX
 
-distclean: clean cleandep
-
-ifdef MINGW
-	$(REMOVE) $(OBJDIR)/*.res
-endif
-
-ifdef CYGWIN32
-	$(REMOVE) $(OBJDIR)/*.res
+# executable stripped of debugging symbols
+$(exe) : $(dbg) | $$(@D)/
+	$(.)$(OBJCOPY) --strip-debug $< $@
+	$(.)-$(OBJCOPY) --add-gnu-debuglink=$< $@
+ifdef UPX
+	$(call Echo,Compressing final executable...)
+	$(.)-$(upx) $@
 endif
 
-#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
+# original executable with debugging symbols
+$(dbg) : $(objects) | $$(@D)/
+	$(call Echo,Linking $(@F)...)
+	$(.)$(LD) -o $@ $^ $(libs)
 
-# executable
-# NOTE: DJGPP's objcopy do not have --add-gnu-debuglink
-
-$(BIN)/$(EXENAME): $(POS) $(OBJS)
-	-$(MKDIR) $(BIN)
-	$(call echo,Linking $(EXENAME)...)
-	$(LD) $(LDFLAGS) $(OBJS) -o $(BIN)/$(EXENAME) $(LIBS)
-ifndef VALGRIND
-ifndef NOOBJDUMP
-	$(call 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
-
-# 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)
-endif
-endif
-	$(call echo,Build is done$(,) please look for $(EXENAME) in $(BIN)$(,) (checking for post steps))
+# disassembly of executable
+$(dbg).txt : $(dbg)
+	$(call Echo,Dumping debugging info...)
+	$(.)$(OBJDUMP) $(OBJDUMP_OPTS) $< > $@
+	$(.)$(GZIP) $(GZIP_OPTS) $@
 
-reobjdump:
-	$(call echo,Redumping debugging info)
-	$(OBJDUMP) $(OBJDUMP_OPTS) $(BIN)/$(DBGNAME) > $(BIN)/$(DBGNAME).txt
+# '::' means run unconditionally
+# this really updates comptime.h
+comptime.c ::
 ifdef WINDOWSHELL
-	-$(GZIP) $(GZIP_OPTS) $(BIN)/$(DBGNAME).txt
+	$(.)..\comptime.bat .
 else
-	-$(GZIP) $(GZIP_OPT2) $(BIN)/$(DBGNAME).txt
+	$(.)../comptime.sh .
 endif
 
-$(OBJDIR):
-	-$(MKDIR) $(OBJDIR)
-
-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 $@
-endif
-endif
+# I wish I could make dependencies out of rc files :(
+$(objdir)/win32/Srb2win.res : \
+	win32/afxres.h win32/resource.h
 
-#dependecy made by gcc itself !
-ifndef DUMMY
-ifndef CLEANONLY
-$(call print,Checking dependency files...)
--include $(DEPS)
-endif
-endif
-
-undefine deps_rule
-
-# windows makes it too hard !
+# dependency recipe template
+# 1: source file suffix
+# 2: extra flags to gcc
+define _recipe =
+$(depdir)/%.d : %.$(1) | $$$$(@D)/
 ifndef WINDOWSHELL
-ifdef echoName
-define deps_rule =
-	@printf "%-20.20s\r" $<
-
-endef
+ifdef Echo_name
+	@printf '%-20.20s\r' $$<
 endif
 endif
-
-define deps_rule +=
-	$(CC) $(CFLAGS) -M -MF $@ -MT $(OBJDIR)/$(<:.c=.o) $<
+	$(.)$(cc) -MM -MF $$@ -MT $(objdir)/$$*.o $(2) $$<
 endef
 
-$(DEPDIR)/%.d: %.c
-	$(deps_rule)
-
-$(DEPDIR)/%.d: $(INTERFACE)/%.c
-	$(deps_rule)
-
-$(DEPDIR)/%.d: hardware/%.c
-	$(deps_rule)
-
-$(DEPDIR)/%.d: blua/%.c
-	$(deps_rule)
-
-ifdef VALGRIND
-$(OBJDIR)/z_zone.o: z_zone.c
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -DHAVE_VALGRIND $(VALGRIND_CFLAGS) -c $< -o $@
-endif
-
-$(OBJDIR)/comptime.o::
-ifdef echoName
-	@echo -- comptime.c ...
-endif
-	$(COMPTIME) .
-	$(CC) $(CFLAGS) $(WFLAGS) -c comptime.c -o $@
-
-$(BIN)/%.mo: locale/%.po
-	-$(MKDIR) $(BIN)
-	$(echoName)
-	$(MSGFMT) -f -o $@ $<
-
-$(OBJDIR)/%.o: %.c
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
-
-$(OBJDIR)/%.o: $(INTERFACE)/%.c
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
-
-ifdef MACOSX
-$(OBJDIR)/%.o: sdl/macosx/%.c
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
-endif
-
-$(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 $@
+$(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: %.s
-	$(echoName)
-	$(CC) $(OPTS) -x assembler-with-cpp -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)/SRB2.res: win32/Srb2win.rc win32/afxres.h win32/resource.h
-	$(echoName)
-	$(WINDRES) -i $< -O rc $(WINDRESFLAGS) --include-dir=win32 -o $@ -O coff
+_rm=$(.)$(rmrf) $(call Windows_path,$(1))
 
+cleandep :
+	$(call _rm,$(depends) comptime.h)
 
-ifdef SDL
+clean :
+	$(call _rm,$(exe) $(dbg) $(dbg).txt $(objects))
 
-ifdef MINGW
-$(OBJDIR)/win_dbg.o: win32/win_dbg.c
-	$(echoName)
-	$(CC) $(CFLAGS) $(WFLAGS) -c $< -o $@
-endif
+distclean :
+	$(call _rm,../bin ../objs ../dep ../make comptime.h)
 
-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 075cd2d3a8defa3fd7d11feb431d2ad9cc67a704..0000000000000000000000000000000000000000
--- a/src/Makefile.cfg
+++ /dev/null
@@ -1,478 +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
-# And don't bother if this is a clean only run
-ifeq   (,$(filter GCC% CLEANONLY,$(.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)))
-	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
-
-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
-DEPDIR=../dep
-#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
-	DEPDIR:=$(DEPDIR)/dummy
-else
-ifdef LINUX
-	NASMFORMAT=elf -DLINUX
-	SDL=1
-ifdef LINUX64
-	OBJDIR:=$(OBJDIR)/Linux64
-	BIN:=$(BIN)/Linux64
-	DEPDIR:=$(DEPDIR)/Linux64
-else
-	OBJDIR:=$(OBJDIR)/Linux
-	BIN:=$(BIN)/Linux
-	DEPDIR:=$(DEPDIR)/Linux
-endif
-else
-ifdef FREEBSD
-	INTERFACE=sdl
-	NASMFORMAT=elf -DLINUX
-	SDL=1
-
-	OBJDIR:=$(OBJDIR)/FreeBSD
-	BIN:=$(BIN)/FreeBSD
-	DEPDIR:=$(DEPDIR)/Linux
-else
-ifdef SOLARIS
-	INTERFACE=sdl
-	NASMFORMAT=elf -DLINUX
-	SDL=1
-
-	OBJDIR:=$(OBJDIR)/Solaris
-	BIN:=$(BIN)/Solaris
-	DEPDIR:=$(DEPDIR)/Solaris
-else
-ifdef CYGWIN32
-	INTERFACE=sdl
-	NASMFORMAT=win32
-	SDL=1
-
-	OBJDIR:=$(OBJDIR)/cygwin
-	BIN:=$(BIN)/Cygwin
-	DEPDIR:=$(DEPDIR)/Cygwin
-else
-ifdef MINGW64
-	#NASMFORMAT=win64
-	SDL=1
-	OBJDIR:=$(OBJDIR)/Mingw64
-	BIN:=$(BIN)/Mingw64
-	DEPDIR:=$(DEPDIR)/Mingw64
-else
-ifdef MINGW
-	NASMFORMAT=win32
-	SDL=1
-	OBJDIR:=$(OBJDIR)/Mingw
-	BIN:=$(BIN)/Mingw
-	DEPDIR:=$(DEPDIR)/Mingw
-endif
-endif
-endif
-endif
-endif
-endif
-endif
-
-ifdef ARCHNAME
-	OBJDIR:=$(OBJDIR)/$(ARCHNAME)
-	BIN:=$(BIN)/$(ARCHNAME)
-	DEPDIR:=$(DEPDIR)/$(ARCHNAME)
-endif
-
-OBJDUMP_OPTS?=--wide --source --line-numbers
-LD=$(CC)
-
-ifdef SDL
-	INTERFACE=sdl
-	OBJDIR:=$(OBJDIR)/SDL
-	DEPDIR:=$(DEPDIR)/SDL
-endif
-
-ifndef DUMMY
-ifdef DEBUGMODE
-	OBJDIR:=$(OBJDIR)/Debug
-	BIN:=$(BIN)/Debug
-	DEPDIR:=$(DEPDIR)/Debug
-else
-	OBJDIR:=$(OBJDIR)/Release
-	BIN:=$(BIN)/Release
-	DEPDIR:=$(DEPDIR)/Release
-endif
-endif
diff --git a/src/Makefile.d/detect.mk b/src/Makefile.d/detect.mk
new file mode 100644
index 0000000000000000000000000000000000000000..9c8a0a227d8a74b6e780234068885a8ab8c3a85c
--- /dev/null
+++ b/src/Makefile.d/detect.mk
@@ -0,0 +1,103 @@
+#
+# 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))
+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),$(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..d7d0c3dd1ef59f5ca3d9b6b6d7e6191af9f671d8
--- /dev/null
+++ b/src/Makefile.d/versions.mk
@@ -0,0 +1,177 @@
+#
+# 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+=-Wdeclaration-after-statement
+ WFLAGS+=-Wno-error=declaration-after-statement
+ 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..0c671b26876e1740c1ab101a711b561f9ad2dd7a
--- /dev/null
+++ b/src/Makefile.d/win32.mk
@@ -0,0 +1,99 @@
+#
+# Mingw, if you don't know, that's Win32/Win64
+#
+
+ifndef MINGW64
+EXENAME?=srb2win.exe
+else
+EXENAME?=srb2win64.exe
+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..ef0ebb88cd33e3d865095015cfd8be09a8487388 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.
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 3a2962e659e24cbb34f9690afe52348393a31ed3..0000000000000000000000000000000000000000
--- a/src/blua/Makefile.cfg
+++ /dev/null
@@ -1,53 +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_taglib.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/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 d4033e6efe963db1523b709de045012f3c381900..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.
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 1560220f6ecc257cd6cacd20cc25fb83534e152c..95d6f5fd1ebecaab1b9f5636c4d33b4826a680f8 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_KeyNumToString(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_KeyStringToNum(COM_Argv(1));
 	if (key <= 0 || key >= NUMINPUTS)
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("Invalid key name\n"));
@@ -1697,7 +1697,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
@@ -1756,8 +1759,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);
@@ -1818,7 +1821,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..28f40d308270cc279bc70b5995b6f3fdf378536c 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.
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 77176e5c8d377558779820d28912681e0b277dfb..2e5da5abace374caa999bbfc12bf461021c8782e 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.
@@ -1100,7 +1100,7 @@ static inline void CL_DrawConnectionStatus(void)
 				break;
 			case CL_ASKFULLFILELIST:
 			case CL_CHECKFILES:
-				cltext = M_GetText("Checking server addon list ...");
+				cltext = M_GetText("Checking server addon list...");
 				break;
 			case CL_CONFIRMCONNECT:
 				cltext = "";
@@ -1235,15 +1235,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)
@@ -1286,6 +1285,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;
@@ -1304,14 +1318,7 @@ 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);
@@ -1429,9 +1436,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);
@@ -1652,15 +1656,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);
@@ -1763,20 +1758,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++;
 	}
@@ -2040,18 +2039,14 @@ static boolean CL_FinishedFileList(void)
 
 		if (fileneeded == NULL)
 			I_Error("CL_FinishedFileList: fileneeded == NULL");
-#endif
 
 		for (i = 0; i < fileneedednum; i++)
 			if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
 			{
-#ifndef NONET
 				totalfilesrequestednum++;
 				totalfilesrequestedsize += fileneeded[i].totalsize;
-#endif
 			}
 
-#ifndef NONET
 		if (totalfilesrequestedsize>>20 >= 100)
 			downloadsize = Z_StrDup(va("%uM",totalfilesrequestedsize>>20));
 		else
@@ -2081,6 +2076,70 @@ static boolean CL_FinishedFileList(void)
 	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.
@@ -2111,26 +2170,33 @@ static boolean CL_ServerConnectionSearchTicker(tic_t *asksent)
 				return true;
 		}
 
-		// Quit here rather than downloading files and being refused later.
-		if (serverlist[i].info.refusereason == 2)
-			serverisfull = true;
-		else if (serverlist[i].info.refusereason)
+		if (client)
 		{
-			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);
+			serverinfo_pak *info = &serverlist[i].info;
+
+			if (info->refusereason == REFUSE_SLOTS_FULL)
+				serverisfull = true;
 			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;
-		}
+			{
+				const char *reason = InvalidServerReason(info);
 
-		if (client)
-		{
-			D_ParseFileneeded(serverlist[i].info.fileneedednum, serverlist[i].info.fileneeded, 0);
+				// 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(message, NULL, MM_NOTHING);
+					Z_Free(message);
+					return false;
+				}
+			}
+
+			D_ParseFileneeded(info->fileneedednum, info->fileneeded, 0);
 
-			if (serverlist[i].info.flags & SV_LOTSOFADDONS)
+			if (info->flags & SV_LOTSOFADDONS)
 			{
 				cl_mode = CL_ASKFULLFILELIST;
 				cl_lastcheckedfilecount = 0;
@@ -2725,7 +2791,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.
@@ -2798,14 +2864,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;
 	}
 
@@ -2861,7 +2927,6 @@ 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();
@@ -3222,7 +3287,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:
@@ -3230,7 +3295,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
@@ -3276,7 +3341,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:
@@ -3293,7 +3358,7 @@ static void Got_KickCmd(UINT8 **p, INT32 playernum)
 
 	if (pnum == consoleplayer)
 	{
-		LUAh_GameQuit(false);
+		LUA_HookBool(false, HOOK(GameQuit));
 #ifdef DUMPCONSISTENCY
 		if (msg == KICK_MSG_CON_FAIL) SV_SavedGame();
 #endif
@@ -3501,6 +3566,8 @@ void SV_ResetServer(void)
 	// clear server_context
 	memset(server_context, '-', 8);
 
+	CV_RevertNetVars();
+
 	DEBFILE("\n-=-=-=-=-=-=-= Server Reset =-=-=-=-=-=-=-\n\n");
 }
 
@@ -3713,7 +3780,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)
@@ -3890,6 +3957,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
@@ -3900,33 +4039,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
@@ -3993,7 +4113,7 @@ static void HandleConnect(SINT8 node)
 static void HandleShutdown(SINT8 node)
 {
 	(void)node;
-	LUAh_GameQuit(false);
+	LUA_HookBool(false, HOOK(GameQuit));
 	D_QuitNetGame();
 	CL_Reset();
 	D_StartTitle();
@@ -4008,7 +4128,7 @@ static void HandleShutdown(SINT8 node)
 static void HandleTimeout(SINT8 node)
 {
 	(void)node;
-	LUAh_GameQuit(false);
+	LUA_HookBool(false, HOOK(GameQuit));
 	D_QuitNetGame();
 	CL_Reset();
 	D_StartTitle();
@@ -4549,7 +4669,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:
diff --git a/src/d_clisrv.h b/src/d_clisrv.h
index 9d1d63a09da3d9ab690f00a10396f21e76efe974..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,9 +22,13 @@
 #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 4
 
@@ -144,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.
@@ -193,11 +194,8 @@ 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];
@@ -206,6 +204,12 @@ typedef struct
 #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
@@ -223,7 +227,7 @@ 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;
@@ -417,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..1fd2e3824251082d6c059fffef8ddbe76f67a15c 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.
diff --git a/src/d_main.c b/src/d_main.c
index ef47699e4a76c77fdb615ae262549d3deac3c289..1228de775b1b166a0a0cded956fbaa7f40d4ac3b 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
@@ -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->data1 - KEY_MOUSE1) < MOUSEBUTTONS)
+			{
+				if (ev->type == ev_keydown)
+					mouse.buttons |= 1 << (ev->data1 - KEY_MOUSE1);
+				else
+					mouse.buttons &= ~(1 << (ev->data1 - KEY_MOUSE1));
+			}
+			else if ((UINT32)(ev->data1 - KEY_2MOUSE1) < MOUSEBUTTONS)
+			{
+				if (ev->type == ev_keydown)
+					mouse2.buttons |= 1 << (ev->data1 - KEY_2MOUSE1);
+				else
+					mouse2.buttons &= ~(1 << (ev->data1 - KEY_2MOUSE1));
+			}
+			// Scroll (has no keyup event)
+			else switch (ev->data1) {
+				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 && 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);
 }
 
 //
@@ -860,33 +923,54 @@ void D_StartTitle(void)
 	tutorialmode = false;
 }
 
+#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)
 {
 	char *newfile;
 	size_t index = 0;
 
-	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("D_AddFile: No more free memory to add file %s (list reallocation)", file);
-	}
+	REALLOC_FILE_LIST
 
 	newfile = malloc(strlen(file) + 1);
 	if (!newfile)
-		I_Error("D_AddFile: No more free memory to add file %s (filename allocation)", 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 separator + NULL terminator
+	if (!newfile)
+		I_Error("D_AddFolder: No more free memory to add folder %s", file);
+
+	strcpy(newfile, file);
+	strcat(newfile, PATHSEP);
 
 	list->files[index] = newfile;
-	list->files[index + 1] = NULL;
 }
 
+#undef REALLOC_FILE_LIST
+
 static inline void D_CleanFile(addfilelist_t *list)
 {
 	if (list->files)
@@ -946,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
@@ -1057,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"
@@ -1119,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)
@@ -1186,21 +1270,25 @@ void D_SRB2Main(void)
 	// Do this up here so that WADs loaded through the command line can use ExecCfg
 	COM_Init();
 
-	// add any files specified on the command line with -file wadfile
-	// to the wad list
+	// Add any files specified on the command line with
+	// "-file <file>" or "-folder <folder>" to the add-on list
 	if (!((M_GetUrlProtocolArg() || M_CheckParm("-connect")) && !M_CheckParm("-server")))
 	{
-		if (M_CheckParm("-file"))
-		{
-			// the parms after p are wadfile/lump names,
-			// until end of parms or another - preceded parm
-			while (M_IsNextParm())
-			{
-				const char *s = M_GetNextParm();
+		INT32 addontype = 0;
+		INT32 i;
 
-				if (s) // Check for NULL?
-					D_AddFile(&startuppwads, s);
-			}
+		for (i = 1; i < myargc; i++)
+		{
+			if (!strcasecmp(myargv[i], "-file"))
+				addontype = 1;
+			else if (!strcasecmp(myargv[i], "-folder"))
+				addontype = 2;
+			else if (myargv[i][0] == '-' || myargv[i][0] == '+')
+				addontype = 0;
+			else if (addontype == 1)
+				D_AddFile(&startuppwads, myargv[i]);
+			else if (addontype == 2)
+				D_AddFolder(&startuppwads, myargv[i]);
 		}
 	}
 
@@ -1300,7 +1388,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
 
@@ -1566,7 +1654,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 d8e38c954679a404e2e785a25fa1604ab50cba3e..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.
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 f96b036ae192e365ac9f4177db071c0130b4fe90..0956abb7254c23a39c511029068e4b0e1af9bb98 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.
@@ -63,7 +63,9 @@ static void Got_WeaponPref(UINT8 **cp, INT32 playernum);
 static void Got_Mapcmd(UINT8 **cp, INT32 playernum);
 static void Got_ExitLevelcmd(UINT8 **cp, INT32 playernum);
 static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum);
+static void Got_RequestAddfoldercmd(UINT8 **cp, INT32 playernum);
 static void Got_Addfilecmd(UINT8 **cp, INT32 playernum);
+static void Got_Addfoldercmd(UINT8 **cp, INT32 playernum);
 static void Got_Pause(UINT8 **cp, INT32 playernum);
 static void Got_Suicide(UINT8 **cp, INT32 playernum);
 static void Got_RandomSeed(UINT8 **cp, INT32 playernum);
@@ -115,6 +117,7 @@ static void Command_Map_f(void);
 static void Command_ResetCamera_f(void);
 
 static void Command_Addfile(void);
+static void Command_Addfolder(void);
 static void Command_ListWADS_f(void);
 static void Command_RunSOC(void);
 static void Command_Pause(void);
@@ -168,7 +171,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
@@ -255,7 +258,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
@@ -398,16 +401,16 @@ const char *netxcmdnames[MAXNETXCMD - 1] =
 	"MAP",
 	"EXITLEVEL",
 	"ADDFILE",
+	"ADDFOLDER",
 	"PAUSE",
 	"ADDPLAYER",
 	"TEAMCHANGE",
 	"CLEARSCORES",
-	"LOGIN",
 	"VERIFIED",
 	"RANDOMSEED",
 	"RUNSOC",
 	"REQADDFILE",
-	"DELFILE", // replace next time we add an XD
+	"REQADDFOLDER",
 	"SETMOTD",
 	"SUICIDE",
 	"LUACMD",
@@ -441,7 +444,9 @@ void D_RegisterServerCommands(void)
 	RegisterNetXCmd(XD_MAP, Got_Mapcmd);
 	RegisterNetXCmd(XD_EXITLEVEL, Got_ExitLevelcmd);
 	RegisterNetXCmd(XD_ADDFILE, Got_Addfilecmd);
+	RegisterNetXCmd(XD_ADDFOLDER, Got_Addfoldercmd);
 	RegisterNetXCmd(XD_REQADDFILE, Got_RequestAddfilecmd);
+	RegisterNetXCmd(XD_REQADDFOLDER, Got_RequestAddfoldercmd);
 	RegisterNetXCmd(XD_PAUSE, Got_Pause);
 	RegisterNetXCmd(XD_SUICIDE, Got_Suicide);
 	RegisterNetXCmd(XD_RUNSOC, Got_RunSOCcmd);
@@ -472,6 +477,7 @@ void D_RegisterServerCommands(void)
 	COM_AddCommand("showmap", Command_Showmap_f);
 	COM_AddCommand("mapmd5", Command_Mapmd5_f);
 
+	COM_AddCommand("addfolder", Command_Addfolder);
 	COM_AddCommand("addfile", Command_Addfile);
 	COM_AddCommand("listwad", Command_ListWADS_f);
 
@@ -788,7 +794,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);
@@ -1313,8 +1319,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.
@@ -1475,7 +1482,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())
@@ -1491,12 +1499,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 (!skins[s].availability && (p->availabilities & (1 << s)))
+			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
+		{
+			UINT32 illegalMask = UINT32_MAX;
+
+			for (i = 0; i < unlockShift; i++)
+			{
+				illegalMask &= ~(1 << i);
+			}
+
+			if ((p->availabilities & illegalMask) != 0)
 			{
 				kick = true;
-				break;
 			}
 		}
 
@@ -2098,7 +2123,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)
@@ -2683,7 +2708,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
@@ -2844,7 +2869,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;
 	}
 
@@ -3198,7 +3223,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))
 	{
@@ -3353,6 +3378,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 +3400,92 @@ static void Command_Addfile(void)
 	}
 }
 
+static void Command_Addfolder(void)
+{
+	size_t argc = COM_Argc(); // amount of arguments total
+	size_t curarg; // current argument index
+
+	const char *addedfolders[argc]; // list of filenames already processed
+	size_t numfoldersadded = 0; // the amount of filenames processed
+
+	if (argc < 2)
+	{
+		CONS_Printf(M_GetText("addfolder <path> [path2...] [...]: Load add-ons\n"));
+		return;
+	}
+
+	// start at one to skip command name
+	for (curarg = 1; curarg < argc; curarg++)
+	{
+		const char *fn, *p;
+		char buf[256];
+		char *buf_p = buf;
+		INT32 i;
+		size_t ii;
+		boolean folderadded = false;
+
+		fn = COM_Argv(curarg);
+
+		// For the amount of filenames previously processed...
+		for (ii = 0; ii < numfoldersadded; ii++)
+		{
+			// If this is one of them, don't try to add it.
+			if (!strcmp(fn, addedfolders[ii]))
+			{
+				folderadded = true;
+				break;
+			}
+		}
+
+		// If we've added this one, skip to the next one.
+		if (folderadded)
+		{
+			CONS_Alert(CONS_WARNING, M_GetText("Already processed %s, skipping\n"), fn);
+			continue;
+		}
+
+		// Disallow non-printing characters and semicolons.
+		for (i = 0; fn[i] != '\0'; i++)
+			if (!isprint(fn[i]) || fn[i] == ';')
+				return;
+
+		// Add file on your client directly if you aren't in a netgame.
+		if (!(netgame || multiplayer))
+		{
+			P_AddFolder(fn);
+			addedfolders[numfoldersadded++] = fn;
+			continue;
+		}
+
+		p = fn+strlen(fn);
+		while(--p >= fn)
+			if (*p == '\\' || *p == '/' || *p == ':')
+				break;
+		++p;
+
+		// check 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;
+		}
+
+		WRITESTRINGN(buf_p,p,240);
+
+		addedfolders[numfoldersadded++] = fn;
+
+		if (IsPlayerAdmin(consoleplayer) && (!server)) // Request to add file
+			SendNetXCmd(XD_REQADDFOLDER, buf, buf_p - buf);
+		else
+			SendNetXCmd(XD_ADDFOLDER, buf, buf_p - buf);
+	}
+}
+
 static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
 {
 	char filename[241];
-	filestatus_t ncs = FS_NOTFOUND;
+	filestatus_t ncs = FS_NOTCHECKED;
 	UINT8 md5sum[16];
 	boolean kick = false;
 	boolean toomany = false;
@@ -3430,10 +3540,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);
@@ -3478,6 +3644,49 @@ static void Got_Addfilecmd(UINT8 **cp, INT32 playernum)
 	G_SetGameModified(true);
 }
 
+static void Got_Addfoldercmd(UINT8 **cp, INT32 playernum)
+{
+	char path[241];
+	filestatus_t ncs = FS_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;
@@ -3498,6 +3707,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);
 	}
@@ -3610,7 +3821,7 @@ static void Command_Playintro_f(void)
   */
 FUNCNORETURN static ATTRNORETURN void Command_Quit_f(void)
 {
-	LUAh_GameQuit(true);
+	LUA_HookBool(true, HOOK(GameQuit));
 	I_Quit();
 }
 
@@ -4272,7 +4483,7 @@ void Command_ExitGame_f(void)
 {
 	INT32 i;
 
-	LUAh_GameQuit(false);
+	LUA_HookBool(false, HOOK(GameQuit));
 
 	D_QuitNetGame();
 	CL_Reset();
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index cb8eda5c99e302106388bccb5129fb186faf99e8..efe00552a38e35fb7c13ac0b83872544f04fcf49 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.
@@ -45,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
 
@@ -129,16 +129,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 0016cb180abafede34cea8412832c3c86873a56e..22d3549dc8d5dc73f0b13b0a6852c0d7aae44054 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"
@@ -124,7 +120,7 @@ char luafiledir[256 + 16] = "luafiles";
 /** Fills a serverinfo packet with information about wad files loaded.
   *
   * \todo Give this function a better name since it is in global scope.
-  * Used to have size limiting built in - now handled via W_LoadWadFile in w_wad.c
+  * Used to have size limiting built in - now handled via W_InitFile in w_wad.c
   *
   */
 UINT8 *PutFileNeeded(UINT16 firstfile)
@@ -134,7 +130,7 @@ UINT8 *PutFileNeeded(UINT16 firstfile)
 	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 = mainwads+1; i < numwadfiles; i++) //mainwads+1, otherwise we start on the first mainwad
 	{
@@ -162,9 +158,10 @@ UINT8 *PutFileNeeded(UINT16 firstfile)
 		}
 
 		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
@@ -172,6 +169,7 @@ UINT8 *PutFileNeeded(UINT16 firstfile)
 			// filestatus += (0 << 4); -- Won't send, too big
 
 		WRITEUINT8(p, filestatus);
+		WRITEUINT8(p, folder);
 
 		count++;
 		WRITEUINT32(p, wadfiles[i]->filesize);
@@ -195,8 +193,7 @@ void AllocFileNeeded(INT32 size)
 
 void FreeFileNeeded(void)
 {
-	if (fileneeded)
-		Z_Free(fileneeded);
+	Z_Free(fileneeded);
 	fileneeded = NULL;
 }
 
@@ -223,6 +220,7 @@ void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 fi
 		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
@@ -417,6 +415,8 @@ 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)
@@ -485,7 +485,11 @@ INT32 CL_CheckFiles(void)
 			}
 		}
 
-		fileneeded[i].status = FS_NOTFOUND; //findfile(fileneeded[i].filename, fileneeded[i].md5sum, true);
+		if (fileneeded[i].folder)
+			fileneeded[i].status = findfolder(fileneeded[i].filename);
+		else
+			fileneeded[i].status = findfile(fileneeded[i].filename, fileneeded[i].md5sum, true);
+
 		CONS_Debug(DBG_NETPLAY, "found %d\n", fileneeded[i].status);
 		return 4;
 	}
@@ -510,7 +514,10 @@ boolean CL_LoadServerFiles(void)
 			continue; // Already loaded
 		else if (fileneeded[i].status == FS_FOUND)
 		{
-			P_AddWadFile(fileneeded[i].filename);
+			if (fileneeded[i].folder)
+				P_AddFolder(fileneeded[i].filename);
+			else
+				P_AddWadFile(fileneeded[i].filename);
 			G_SetGameModified(true);
 			fileneeded[i].status = FS_OPEN;
 			return false;
@@ -1624,3 +1631,23 @@ filestatus_t findfile(char *filename, const UINT8 *wantedmd5sum, boolean complet
 
 	return (badmd5 ? FS_MD5SUMBAD : FS_NOTFOUND); // md5 sum bad or file not found
 }
+
+filestatus_t findfolder(const char *path)
+{
+	// Check the path by itself first.
+	if (checkfolderpath(path, NULL, true))
+		return FS_FOUND;
+
+#define checkpath(startpath) { \
+	if (checkfolderpath(path, startpath, true)) \
+		return FS_FOUND; \
+	}
+
+	checkpath(srb2home) // Then, look in srb2home.
+	checkpath(srb2path) // Now, look in srb2path.
+	checkpath(".") // Finally, look in ".".
+
+#undef checkpath
+
+	return FS_NOTFOUND;
+}
diff --git a/src/d_netfil.h b/src/d_netfil.h
index c79d7e4b643bc3d6c1df381da88ab3957c7f6431..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.
@@ -49,6 +49,7 @@ typedef struct
 	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
 
@@ -63,6 +64,8 @@ typedef struct
 	UINT32 ackresendposition; // Used when resuming downloads
 } fileneeded_t;
 
+#define FILENEEDEDSIZE 23
+
 extern INT32 fileneedednum;
 extern fileneeded_t *fileneeded;
 extern char downloaddir[512];
@@ -150,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 2e7afed882ccf4b1ca1966b7439785aadf4a993a..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.
@@ -313,9 +313,43 @@ typedef enum
 	RW_RAIL    = 32
 } ringweapons_t;
 
+//Bot types
+typedef enum
+{
+	BOT_NONE = 0,
+	BOT_2PAI,
+	BOT_2PHUMAN,
+	BOT_MPAI
+} bottype_t;
+
+//AI states
+typedef enum
+{
+	AI_STANDBY = 0,
+	AI_FOLLOW,
+	AI_CATCHUP,
+	AI_THINKFLY,
+	AI_FLYSTANDBY,
+	AI_FLYCARRY,
+	AI_SPINFOLLOW
+} aistatetype_t;
+
+
 // ========================================================================
 //                          PLAYER STRUCTURE
 // ========================================================================
+
+//Bot memory struct
+typedef struct botmem_s
+{
+	boolean lastForward;
+	boolean lastBlocked;
+	boolean blocked;	
+	UINT8 catchup_tics;
+	UINT8 thinkstate;
+} botmem_t;
+
+//Main struct
 typedef struct player_s
 {
 	mobj_t *mo;
@@ -525,8 +559,13 @@ typedef struct player_s
 
 	boolean spectator;
 	boolean outofcoop;
+	boolean removing;
 	UINT8 bot;
-
+	struct player_s *botleader;
+	UINT16 lastbuttons;
+	botmem_t botmem;
+	boolean blocked;
+	
 	tic_t jointime; // Timer when player joins game to change skin/color
 	tic_t quittime; // Time elapsed since user disconnected, zero if connected
 #ifdef HWRENDER
diff --git a/src/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..fbeaae08c5fc11f8e339be44ad62ed1000b085b1 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.
@@ -430,29 +426,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 dd6d7d69ff722bedd843456a4b5a7963889a1269..63e273a6c1f4bc4d605978a577db79550f8340ac 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"
 
@@ -5167,6 +5170,12 @@ struct int_const_s const INT_CONST[] = {
 	{"GF_REDFLAG",GF_REDFLAG},
 	{"GF_BLUEFLAG",GF_BLUEFLAG},
 
+	// Bot types
+	{"BOT_NONE",BOT_NONE},
+	{"BOT_2PAI",BOT_2PAI},
+	{"BOT_2PHUMAN",BOT_2PHUMAN},
+	{"BOT_MPAI",BOT_MPAI},
+
 	// Customisable sounds for Skins, from sounds.h
 	{"SKSSPIN",SKSSPIN},
 	{"SKSPUTPUT",SKSPUTPUT},
@@ -5455,6 +5464,76 @@ 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}
 };
 
diff --git a/src/deh_tables.h b/src/deh_tables.h
index d094bcbad4e74b736aa6aa423cfe94a408dc18f2..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.
diff --git a/src/dehacked.c b/src/dehacked.c
index c2ea28d27cef95d623c480b0259940e390f7cd2f..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.
@@ -188,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')
@@ -562,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"))
 					{
diff --git a/src/dehacked.h b/src/dehacked.h
index 1620314caaba5bbabafded19233b0c162f148f84..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.
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 11bf27ab0b5511fc337416499dbef7435241c6fd..4558e1181a7f4b2bce0de9a2e79e5fb4e50c29e9 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
 
@@ -418,7 +418,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"
@@ -607,10 +607,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 950f50856b7679e035c268e036660fe505d8dbab..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)
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 fdcfad2795f94f5c656345caa0921191084c135f..acad7b2481a2a5cf8e57a1c9f4f6a52863d838c6 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.
@@ -1063,7 +1063,7 @@ boolean F_IntroResponder(event_t *event)
 //  CREDITS
 // =========
 static const char *credits[] = {
-	"\1Sonic Robo Blast II",
+	"\1Sonic Robo Blast 2",
 	"\1Credits",
 	"",
 	"\1Game Design",
@@ -1089,7 +1089,6 @@ 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\"",
@@ -1102,6 +1101,7 @@ static const char *credits[] = {
 	"Louis-Antoine \"LJ Sonic\" de Moulins", // de Rochefort doesn't quite fit on the screen sorry lol
 	"John \"JTE\" Muniz",
 	"Colin \"Sonict\" Pfaff",
+	"James \"james\" Robert Roman",
 	"Sean \"Sryder13\" Ryder",
 	"Ehab \"Wolfy\" Saeed",
 	"Tasos \"tatokis\" Sahanidis", // Corrected C FixedMul, making 64-bit builds netplay compatible
@@ -1166,7 +1166,6 @@ 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.",
 	"\"SSNTails\"",
@@ -1191,7 +1190,6 @@ static const char *credits[] = {
 	"\"Revan\"",
 	"Anna \"QueenDelta\" Sandlin",
 	"Wessel \"sphere\" Smit",
-	"\"Spazzo\"",
 	"\"SSNTails\"",
 	"Rob Tisdell",
 	"\"Torgo\"",
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 3201028d5141430cea4e23c3470eed6498656132..e5f85e46da2b1d7684d97fe900fc103436369469 100644
--- a/src/filesrch.c
+++ b/src/filesrch.c
@@ -29,6 +29,7 @@
 #include "m_misc.h"
 #include "z_zone.h"
 #include "m_menu.h" // Addons_option_Onchange
+#include "w_wad.h"
 
 #if defined (_WIN32) && defined (_MSC_VER)
 
@@ -337,6 +338,11 @@ size_t dir_on[menudepth];
 UINT8 refreshdirmenu = 0;
 char *refreshdirname = NULL;
 
+#define folderpathlen 1024
+#define maxfolderdepth 48
+
+#define isuptree(dirent) ((dirent)[0]=='.' && ((dirent)[1]=='\0' || ((dirent)[1]=='.' && (dirent)[2]=='\0')))
+
 filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *wantedmd5sum, boolean completepath, int maxsearchdepth)
 {
 	filestatus_t retval = FS_NOTFOUND;
@@ -384,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;
@@ -442,6 +445,329 @@ filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *want
 	return retval;
 }
 
+// Called from findfolder and ResGetLumpsFolder in w_wad.c.
+// Call with cleanup true if the path has to be verified.
+boolean checkfolderpath(const char *path, const char *startpath, boolean cleanup)
+{
+	char folderpath[folderpathlen], basepath[folderpathlen], *fn = NULL;
+	DIR *dirhandle;
+
+	// Remove path separators from the filename, and don't try adding "/".
+	// See also the same code in W_InitFolder.
+	if (cleanup)
+	{
+		const char *p = path + strlen(path);
+		size_t len;
+
+		--p;
+		while (*p == '\\' || *p == '/' || *p == ':')
+		{
+			p--;
+			if (p < path)
+				return false;
+		}
+		++p;
+
+		// Allocate the new path name.
+		len = (p - path) + 1;
+		fn = ZZ_Alloc(len);
+		strlcpy(fn, path, len);
+	}
+
+	if (startpath)
+	{
+		snprintf(basepath, sizeof basepath, "%s" PATHSEP, startpath);
+
+		if (cleanup)
+		{
+			snprintf(folderpath, sizeof folderpath, "%s%s", basepath, fn);
+			Z_Free(fn); // Don't need this anymore.
+		}
+		else
+			snprintf(folderpath, sizeof folderpath, "%s%s", basepath, path);
+
+		// Home path and folder path are the same? Not valid.
+		if (!strcmp(basepath, folderpath))
+			return false;
+	}
+	else if (cleanup)
+	{
+		snprintf(folderpath, sizeof folderpath, "%s", fn);
+		Z_Free(fn); // Don't need this anymore.
+	}
+	else
+		snprintf(folderpath, sizeof folderpath, "%s", path);
+
+	dirhandle = opendir(folderpath);
+	if (dirhandle == NULL)
+		return false;
+	else
+		closedir(dirhandle);
+
+	return true;
+}
+
+INT32 pathisfolder(const char *path)
+{
+	struct stat fsstat;
+
+	if (stat(path, &fsstat) < 0)
+		return -1;
+	else if (S_ISDIR(fsstat.st_mode))
+		return 1;
+
+	return 0;
+}
+
+INT32 samepaths(const char *path1, const char *path2)
+{
+	struct stat stat1;
+	struct stat stat2;
+
+	if (stat(path1, &stat1) < 0)
+		return -1;
+	if (stat(path2, &stat2) < 0)
+		return -1;
+
+	if (stat1.st_dev == stat2.st_dev)
+	{
+#if !defined(_WIN32)
+		return (stat1.st_ino == stat2.st_ino);
+#else
+		HANDLE file1 = CreateFileA(path1, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
+		HANDLE file2 = CreateFileA(path2, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
+		BY_HANDLE_FILE_INFORMATION file1info, file2info;
+		boolean ok = false;
+
+		if (file1 != INVALID_HANDLE_VALUE && file2 != INVALID_HANDLE_VALUE)
+		{
+			if (GetFileInformationByHandle(file1, &file1info) && GetFileInformationByHandle(file2, &file2info))
+			{
+				if (file1info.dwVolumeSerialNumber == file2info.dwVolumeSerialNumber
+				&& file1info.nFileIndexLow == file2info.nFileIndexLow
+				&& file1info.nFileIndexHigh == file2info.nFileIndexHigh)
+					ok = true;
+			}
+		}
+
+		if (file1 != INVALID_HANDLE_VALUE)
+			CloseHandle(file1);
+		if (file2 != INVALID_HANDLE_VALUE)
+			CloseHandle(file2);
+
+		return ok;
+#endif
+	}
+
+	return false;
+}
+
+//
+// Folder loading
+//
+
+static void initfolderpath(char *folderpath, size_t *folderpathindex, int depthleft)
+{
+	folderpathindex[depthleft] = strlen(folderpath) + 1;
+
+	if (folderpath[folderpathindex[depthleft]-2] != PATHSEP[0])
+	{
+		folderpath[folderpathindex[depthleft]-1] = PATHSEP[0];
+		folderpath[folderpathindex[depthleft]] = 0;
+	}
+	else
+		folderpathindex[depthleft]--;
+}
+
+lumpinfo_t *getfolderfiles(const char *path, UINT16 *nlmp, UINT16 *nfiles, UINT16 *nfolders)
+{
+	DIR **dirhandle;
+	struct dirent *dent;
+	struct stat fsstat;
+
+	int rootfolder = (maxfolderdepth - 1);
+	int depthleft = rootfolder;
+
+	char folderpath[folderpathlen];
+	size_t *folderpathindex;
+
+	lumpinfo_t *lumpinfo, *lump_p;
+	UINT16 i = 0, numlumps = (*nlmp);
+
+	dirhandle = (DIR **)malloc(maxfolderdepth * sizeof (DIR*));
+	folderpathindex = (size_t *)malloc(maxfolderdepth * sizeof(size_t));
+
+	// Open the root directory
+	strlcpy(folderpath, path, folderpathlen);
+	dirhandle[depthleft] = opendir(folderpath);
+
+	if (dirhandle[depthleft] == NULL)
+	{
+		free(dirhandle);
+		free(folderpathindex);
+		return NULL;
+	}
+
+	initfolderpath(folderpath, folderpathindex, depthleft);
+	(*nfiles) = 0;
+	(*nfolders) = 0;
+
+	// Count files and directories
+	while (depthleft < maxfolderdepth)
+	{
+		folderpath[folderpathindex[depthleft]] = 0;
+		dent = readdir(dirhandle[depthleft]);
+
+		if (!dent)
+		{
+			if (depthleft != rootfolder) // Don't close the root directory
+				closedir(dirhandle[depthleft]);
+			depthleft++;
+			continue;
+		}
+		else if (isuptree(dent->d_name))
+			continue;
+
+		strcpy(&folderpath[folderpathindex[depthleft]], dent->d_name);
+
+		if (stat(folderpath, &fsstat) < 0)
+			;
+		else if (S_ISDIR(fsstat.st_mode) && depthleft)
+		{
+			folderpathindex[--depthleft] = strlen(folderpath) + 1;
+			dirhandle[depthleft] = opendir(folderpath);
+
+			if (dirhandle[depthleft])
+			{
+				numlumps++;
+				(*nfolders)++;
+			}
+			else
+				depthleft++;
+
+			folderpath[folderpathindex[depthleft]-1] = '/';
+			folderpath[folderpathindex[depthleft]] = 0;
+		}
+		else
+		{
+			numlumps++;
+			(*nfiles)++;
+		}
+
+		if (numlumps == (UINT16_MAX-1))
+			break;
+	}
+
+	// Failure: No files have been found.
+	if (!(*nfiles))
+	{
+		(*nfiles) = UINT16_MAX;
+		free(folderpathindex);
+		free(dirhandle);
+		for (; depthleft < maxfolderdepth; closedir(dirhandle[depthleft++])); // Close any open directories.
+		return NULL;
+	}
+
+	// Create the files and directories as lump entries
+	// It's possible to create lumps and count files at the same time,
+	// but I didn't to constantly have to reallocate memory for every lump.
+	rewinddir(dirhandle[rootfolder]);
+	depthleft = rootfolder;
+
+	strlcpy(folderpath, path, folderpathlen);
+	initfolderpath(folderpath, folderpathindex, depthleft);
+
+	lump_p = lumpinfo = Z_Calloc(numlumps * sizeof(lumpinfo_t), PU_STATIC, NULL);
+
+	while (depthleft < maxfolderdepth)
+	{
+		char *fullname, *trimname;
+
+		folderpath[folderpathindex[depthleft]] = 0;
+		dent = readdir(dirhandle[depthleft]);
+
+		if (!dent)
+		{
+			closedir(dirhandle[depthleft++]);
+			continue;
+		}
+		else if (isuptree(dent->d_name))
+			continue;
+
+		strcpy(&folderpath[folderpathindex[depthleft]], dent->d_name);
+
+		if (stat(folderpath, &fsstat) < 0)
+			continue;
+		else if (S_ISDIR(fsstat.st_mode) && depthleft)
+		{
+			folderpathindex[--depthleft] = strlen(folderpath) + 1;
+			dirhandle[depthleft] = opendir(folderpath);
+
+			if (!dirhandle[depthleft])
+			{
+				depthleft++;
+				continue;
+			}
+
+			folderpath[folderpathindex[depthleft]-1] = '/';
+			folderpath[folderpathindex[depthleft]] = 0;
+		}
+
+		lump_p->diskpath = Z_StrDup(folderpath); // Path in the filesystem to the file
+		lump_p->compression = CM_NOCOMPRESSION; // Lump is uncompressed
+
+		// Remove the folder path.
+		fullname = lump_p->diskpath;
+		if (strstr(fullname, path))
+			fullname += strlen(path) + 1;
+
+		// Get the 8-character long lump name.
+		trimname = strrchr(fullname, '/');
+		if (trimname)
+			trimname++;
+		else
+			trimname = fullname;
+
+		if (trimname[0])
+		{
+			char *dotpos = strrchr(trimname, '.');
+			if (dotpos == NULL)
+				dotpos = fullname + strlen(fullname);
+
+			strncpy(lump_p->name, trimname, min(8, dotpos - trimname));
+
+			// The name of the file, without the extension.
+			lump_p->longname = Z_Calloc(dotpos - trimname + 1, PU_STATIC, NULL);
+			strlcpy(lump_p->longname, trimname, dotpos - trimname + 1);
+		}
+		else
+			lump_p->longname = Z_Calloc(1, PU_STATIC, NULL);
+
+		// The complete name of the file, with its extension,
+		// excluding the path of the folder where it resides.
+		lump_p->fullname = Z_StrDup(fullname);
+
+		lump_p++;
+		i++;
+
+		if (i > numlumps || i == (UINT16_MAX-1))
+		{
+			for (; depthleft < maxfolderdepth; closedir(dirhandle[depthleft++])); // Close any open directories.
+			break;
+		}
+	}
+
+	free(folderpathindex);
+	free(dirhandle);
+
+	(*nlmp) = numlumps;
+	return lumpinfo;
+}
+
+//
+// Addons menu
+//
+
 char exttable[NUM_EXT_TABLE][7] = { // maximum extension length (currently 4) plus 3 (null terminator, stop, and length including previous two)
 	"\5.txt", "\5.cfg", // exec
 	"\5.wad",
@@ -636,10 +962,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);
@@ -700,10 +1023,7 @@ boolean preparefilemenu(boolean samedepth)
 
 		if (!dent)
 			break;
-		else if (dent->d_name[0]=='.' &&
-				(dent->d_name[1]=='\0' ||
-					(dent->d_name[1]=='.' &&
-						dent->d_name[2]=='\0')))
+		else if (isuptree(dent->d_name))
 			continue; // we don't want to scan uptree
 
 		strcpy(&menupath[menupathindex[menudepthleft]],dent->d_name);
diff --git a/src/filesrch.h b/src/filesrch.h
index cdb3de2440593b9e2e68f3c3f22e8894bf4d992f..3ae847056d5c71ace189a2ddd10b52de3633f3c7 100644
--- a/src/filesrch.h
+++ b/src/filesrch.h
@@ -7,6 +7,7 @@
 #include "doomdef.h"
 #include "d_netfil.h"
 #include "m_menu.h" // MAXSTRINGLENGTH
+#include "w_wad.h"
 
 extern consvar_t cv_addons_option, cv_addons_folder, cv_addons_md5, cv_addons_showall, cv_addons_search_case, cv_addons_search_type;
 
@@ -28,6 +29,12 @@ extern consvar_t cv_addons_option, cv_addons_folder, cv_addons_md5, cv_addons_sh
 filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *wantedmd5sum,
 	boolean completepath, int maxsearchdepth);
 
+INT32 pathisfolder(const char *path);
+boolean checkfolderpath(const char *path, const char *startpath, boolean cleanup);
+INT32 samepaths(const char *path1, const char *path2);
+
+lumpinfo_t *getfolderfiles(const char *path, UINT16 *nlmp, UINT16 *nfiles, UINT16 *nfolders);
+
 #define menudepth 20
 
 extern char menupath[1024];
@@ -91,5 +98,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 593fd77234a329ba89140e9ad82b98a5e993c22d..701f930e511a8234aa00ca82ca3b055085fabbb9 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);
@@ -1956,7 +1968,7 @@ void G_DoPlayDemo(char *defdemoname)
 	// Set skin
 	SetPlayerSkin(0, skin);
 
-	LUAh_MapChange(gamemap);
+	LUA_HookInt(gamemap, HOOK(MapChange));
 	displayplayer = consoleplayer = 0;
 	memset(playeringame,0,sizeof(playeringame));
 	playeringame[0] = true;
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 13423ce77d5b65ea73730d00696dfb63ba49c779..8cdeaf079eb125b344801cca9c92e3606689e98e 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.
@@ -169,7 +169,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 +406,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);
@@ -841,7 +825,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;
@@ -850,28 +834,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:
@@ -903,7 +887,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)
@@ -914,7 +898,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;
@@ -923,28 +907,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:
@@ -978,7 +962,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)
@@ -1087,14 +1071,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
@@ -1117,9 +1101,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
@@ -1131,12 +1115,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;
@@ -1179,10 +1166,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);
@@ -1261,8 +1248,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);
@@ -1318,12 +1305,12 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		}
 
 	// fire with any button/key
-	axis = PlayerJoyAxis(ssplayer, AXISFIRE);
+	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);
+	axis = PlayerJoyAxis(ssplayer, JA_FIRENORMAL);
 	if (PLAYERINPUTDOWN(ssplayer, gc_firenormal) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_FIRENORMAL;
 
@@ -1339,7 +1326,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 		cmd->buttons |= BT_CUSTOM3;
 
 	// use with any button/key
-	axis = PlayerJoyAxis(ssplayer, AXISSPIN);
+	axis = PlayerJoyAxis(ssplayer, JA_SPIN);
 	if (PLAYERINPUTDOWN(ssplayer, gc_spin) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_SPIN;
 
@@ -1457,7 +1444,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 
 
 	// jump button
-	axis = PlayerJoyAxis(ssplayer, AXISJUMP);
+	axis = PlayerJoyAxis(ssplayer, JA_JUMP);
 	if (PLAYERINPUTDOWN(ssplayer, gc_jump) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_JUMP;
 
@@ -1476,7 +1463,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)
@@ -1510,24 +1497,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;
@@ -1560,23 +1545,14 @@ 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)
+	// Note: Majority of botstuffs are handled in G_Ticker now.
+	if (player->bot == BOT_2PHUMAN) //Player-controlled bot
+	{
+		G_CopyTiccmd(cmd,  I_BaseTiccmd2(), 1); // empty, or external driver
 		// Fix offset angle for P2-controlled Tailsbot when P2's controls are set to non-Legacy
 		cmd->angleturn = (INT16)((localangle - *myangle) >> 16);
-
+	}	
+	
 	*myangle += (cmd->angleturn<<16);
 
 	if (controlstyle == CS_LMAOGALOG) {
@@ -1687,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.
@@ -1701,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;
 	}
 
@@ -1724,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;
 }
@@ -1873,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();
@@ -2074,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
@@ -2198,6 +2179,16 @@ boolean G_Responder(event_t *ev)
 	return false;
 }
 
+//
+// G_LuaResponder
+// Let Lua handle key events.
+//
+boolean G_LuaResponder(event_t *ev)
+{
+	return (ev->type == ev_keydown && LUA_HookKey(ev->data1, HOOK(KeyDown))) ||
+		(ev->type == ev_keyup && LUA_HookKey(ev->data1, HOOK(KeyUp)));
+}
+
 //
 // G_Ticker
 // Make ticcmd_ts for the players.
@@ -2207,6 +2198,23 @@ void G_Ticker(boolean run)
 	UINT32 i;
 	INT32 buf;
 
+	// Bot players queued for removal
+	for (i = MAXPLAYERS-1; i != UINT32_MAX; i--)
+	{
+		if (playeringame[i] && players[i].removing)
+		{
+			CL_RemovePlayer(i, i);
+			if (netgame)
+			{
+				char kickmsg[256];
+
+				strcpy(kickmsg, M_GetText("\x82*Bot %s has been removed"));
+				strcpy(kickmsg, va(kickmsg, player_names[i], i));
+				HU_AddChatText(kickmsg, false);
+			}
+		}
+	}
+
 	// see also SCR_DisplayMarathonInfo
 	if ((marathonmode & (MA_INIT|MA_INGAME)) == MA_INGAME && gamestate == GS_LEVEL)
 		marathontime++;
@@ -2292,19 +2300,57 @@ void G_Ticker(boolean run)
 		if (playeringame[i])
 		{
 			INT16 received;
+			// Save last frame's button readings
+			players[i].lastbuttons = players[i].cmd.buttons;
 
 			G_CopyTiccmd(&players[i].cmd, &netcmds[buf][i], 1);
-
-			received = (players[i].cmd.angleturn & TICCMD_RECEIVED);
-
-			players[i].angleturn += players[i].cmd.angleturn - players[i].oldrelangleturn;
-			players[i].oldrelangleturn = players[i].cmd.angleturn;
-			if (P_ControlStyle(&players[i]) == CS_LMAOGALOG)
-				P_ForceLocalAngle(&players[i], players[i].angleturn << 16);
-			else
-				players[i].cmd.angleturn = players[i].angleturn;
-
-			players[i].cmd.angleturn &= ~TICCMD_RECEIVED;
+			// Bot ticcmd handling
+			// Yes, ordinarily this would be handled in G_BuildTiccmd...
+			// ...however, bot players won't have a corresponding consoleplayer or splitscreen player 2 to send that information.
+			// Therefore, this has to be done after ticcmd sends are received.
+			if (players[i].bot == BOT_2PAI) { // Tailsbot for P2
+				if (!players[i].powers[pw_tailsfly] && (players[i].cmd.forwardmove || players[i].cmd.sidemove || players[i].cmd.buttons))
+				{
+					players[i].bot = BOT_2PHUMAN; // A player-controlled bot. Returns to AI when it respawns.
+					CV_SetValue(&cv_analog[1], true);
+				}
+				else
+				{
+					B_BuildTiccmd(&players[i], &players[i].cmd);
+				}
+				B_HandleFlightIndicator(&players[i]);
+			}
+			else if (players[i].bot == BOT_MPAI) {
+				B_BuildTiccmd(&players[i], &players[i].cmd);
+			}
+			
+			// Do angle adjustments.
+			if (players[i].bot == BOT_NONE || players[i].bot == BOT_2PHUMAN)
+			{
+				received = (players[i].cmd.angleturn & TICCMD_RECEIVED);
+				players[i].angleturn += players[i].cmd.angleturn - players[i].oldrelangleturn;
+				players[i].oldrelangleturn = players[i].cmd.angleturn;
+				if (P_ControlStyle(&players[i]) == CS_LMAOGALOG)
+					P_ForceLocalAngle(&players[i], players[i].angleturn << 16);
+				else
+					players[i].cmd.angleturn = players[i].angleturn;
+    			if (P_ControlStyle(&players[i]) == CS_LMAOGALOG)
+    				P_ForceLocalAngle(&players[i], players[i].angleturn << 16);
+    			else
+    				players[i].cmd.angleturn = players[i].angleturn;
+    
+    			players[i].cmd.angleturn &= ~TICCMD_RECEIVED;
+				// Use the leveltime sent in the player's ticcmd to determine control lag
+    			players[i].cmd.latency = min(((leveltime & 0xFF) - players[i].cmd.latency) & 0xFF, MAXPREDICTTICS-1);
+			}
+			else // Less work is required if we're building a bot ticcmd.
+			{
+    			// Since bot TicCmd is pre-determined for both the client and server, the latency and packet checks are simplified.
+    			received = 1;
+    			players[i].cmd.latency = 0;
+				players[i].angleturn = players[i].cmd.angleturn;
+				players[i].oldrelangleturn = players[i].cmd.angleturn;
+			}
 			players[i].cmd.angleturn |= received;
 		}
 	}
@@ -2491,6 +2537,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	tic_t quittime;
 	boolean spectator;
 	boolean outofcoop;
+	boolean removing;
 	INT16 bot;
 	SINT8 pity;
 	INT16 rings;
@@ -2507,6 +2554,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;
@@ -2583,6 +2631,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;
 
@@ -2627,8 +2676,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;
@@ -2745,7 +2796,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)
@@ -2974,7 +3025,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;
 
@@ -2992,6 +3044,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;
@@ -3095,8 +3169,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();
@@ -3124,7 +3198,7 @@ void G_DoReborn(INT32 playernum)
 		}
 		else
 		{
-			LUAh_MapChange(gamemap);
+			LUA_HookInt(gamemap, HOOK(MapChange));
 			titlecardforreload = true;
 			G_DoLoadLevel(true);
 			titlecardforreload = false;
@@ -3173,7 +3247,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++;
@@ -3214,7 +3288,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;
@@ -3507,6 +3581,7 @@ tolinfo_t TYPEOFLEVEL[NUMTOLNAMES] = {
 	{"MARIO",TOL_MARIO},
 	{"NIGHTS",TOL_NIGHTS},
 	{"OLDBRAK",TOL_ERZ3},
+	{"ERZ3",TOL_ERZ3},
 
 	{"XMAS",TOL_XMAS},
 	{"CHRISTMAS",TOL_XMAS},
@@ -3742,7 +3817,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;
 
@@ -3927,12 +4002,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)
 		{
@@ -3963,7 +4039,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();
@@ -3995,8 +4071,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)
@@ -4618,6 +4701,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
@@ -4636,9 +4722,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
 
diff --git a/src/g_game.h b/src/g_game.h
index 744d6755abd35dc0765ec2174e1cfed94c2b15b8..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.
@@ -85,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)
@@ -204,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..2f7980c647d1eba7b794274200e9f9a11477c8b3 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];
@@ -140,11 +138,8 @@ void G_MapEventsToControls(event_t *ev)
 			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->data2;
+			mouse.rdy = ev->data3;
 			break;
 
 		case ev_joystick: // buttons are virtual keys
@@ -166,9 +161,8 @@ void G_MapEventsToControls(event_t *ev)
 		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->data2;
+			mouse2.rdy = ev->data3;
 			break;
 
 		default:
@@ -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_KeyNumToString(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_KeyStringToNum(const char *keystr)
 {
 	UINT32 j;
 
@@ -817,10 +811,10 @@ void G_SaveKeySetting(FILE *f, INT32 (*fromcontrols)[2], INT32 (*fromcontrolsbis
 	for (i = 1; i < num_gamecontrols; i++)
 	{
 		fprintf(f, "setcontrol \"%s\" \"%s\"", gamecontrolname[i],
-			G_KeynumToString(fromcontrols[i][0]));
+			G_KeyNumToString(fromcontrols[i][0]));
 
 		if (fromcontrols[i][1])
-			fprintf(f, " \"%s\"\n", G_KeynumToString(fromcontrols[i][1]));
+			fprintf(f, " \"%s\"\n", G_KeyNumToString(fromcontrols[i][1]));
 		else
 			fprintf(f, "\n");
 	}
@@ -828,10 +822,10 @@ void G_SaveKeySetting(FILE *f, INT32 (*fromcontrols)[2], INT32 (*fromcontrolsbis
 	for (i = 1; i < num_gamecontrols; i++)
 	{
 		fprintf(f, "setcontrol2 \"%s\" \"%s\"", gamecontrolname[i],
-			G_KeynumToString(fromcontrolsbis[i][0]));
+			G_KeyNumToString(fromcontrolsbis[i][0]));
 
 		if (fromcontrolsbis[i][1])
-			fprintf(f, " \"%s\"\n", G_KeynumToString(fromcontrolsbis[i][1]));
+			fprintf(f, " \"%s\"\n", G_KeyNumToString(fromcontrolsbis[i][1]));
 		else
 			fprintf(f, "\n");
 	}
@@ -1007,8 +1001,8 @@ static void setcontrol(INT32 (*gc)[2])
 		CONS_Printf(M_GetText("Control '%s' unknown\n"), namectrl);
 		return;
 	}
-	keynum1 = G_KeyStringtoNum(COM_Argv(2));
-	keynum2 = G_KeyStringtoNum(COM_Argv(3));
+	keynum1 = G_KeyStringToNum(COM_Argv(2));
+	keynum2 = G_KeyStringToNum(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..ffd0cb560b5f0921fb3595c970f93b7e5d1ff5f0 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.
@@ -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];
 
@@ -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_KeyNumToString(INT32 keynum);
+INT32 G_KeyStringToNum(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 fb3417158a76a53abd38698da07425541ffddb02..0ac33d1361d2a4bf92070a9d944c1bfeab9cd980 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;
diff --git a/src/hardware/hw_batching.h b/src/hardware/hw_batching.h
index 42291a0dfd261731ce6d40c24e06de63260dc132..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.
diff --git a/src/hardware/hw_cache.c b/src/hardware/hw_cache.c
index 83a4e2e03d9b56cd503092762b7ac48fe52a5611..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.
diff --git a/src/hardware/hw_data.h b/src/hardware/hw_data.h
index 7e56a14d0f71b9d575046ccb9877487169c3877d..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.
diff --git a/src/hardware/hw_defs.h b/src/hardware/hw_defs.h
index 4bff8fc6a6866f896fca62d62de808e516630df4..8df9b8916b2e563d7c1eebb16511b222a636a312 100644
--- a/src/hardware/hw_defs.h
+++ b/src/hardware/hw_defs.h
@@ -229,14 +229,15 @@ enum EPolyFlags
 	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.
 };
 
 
@@ -265,7 +266,6 @@ struct FTextureInfo
 };
 typedef struct FTextureInfo FTextureInfo;
 
-// jimita 14032019
 struct FLightInfo
 {
 	FUINT			light_level;
@@ -281,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 ba4923d10e76149cf0dd088aa7633db43e4a8ff0..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))
 	{
@@ -447,6 +521,10 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx));
 				else if (!(option & V_SNAPTOLEFT))
 					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx))/2;
+				if (perplayershuffle & 4)
+					cx -= ((float)vid.width - ((float)BASEVIDWIDTH * dupx))/4;
+				else if (perplayershuffle & 8)
+					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx))/4;
 			}
 			if (fabsf((float)vid.height - (float)BASEVIDHEIGHT * dupy) > 1.0E-36f)
 			{
@@ -454,23 +532,27 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy));
 				else if (!(option & V_SNAPTOTOP))
 					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy))/2;
+				if (perplayershuffle & 1)
+					cy -= ((float)vid.height - ((float)BASEVIDHEIGHT * dupy))/4;
+				else if (perplayershuffle & 2)
+					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy))/4;
 			}
 		}
 	}
 
-	fwidth = w;
-	fheight = h;
+	fwidth = FIXED_TO_FLOAT(w);
+	fheight = FIXED_TO_FLOAT(h);
 
-	if (sx + w > gpatch->width)
-		fwidth = gpatch->width - sx;
+	if (sx + w > gpatch->width<<FRACBITS)
+		fwidth = FIXED_TO_FLOAT((gpatch->width<<FRACBITS) - sx);
 
-	if (sy + h > gpatch->height)
-		fheight = gpatch->height - sy;
+	if (sy + h > gpatch->height<<FRACBITS)
+		fheight = FIXED_TO_FLOAT((gpatch->height<<FRACBITS) - sy);
 
-	if (pscale != FRACUNIT)
+	if (pscale != FRACUNIT || vscale != FRACUNIT || (splitscreen && option & V_PERPLAYER))
 	{
-		fwidth *=  fscale * dupx;
-		fheight *=  fscale * dupy;
+		fwidth *= fscalew * dupx;
+		fheight *= fscaleh * dupy;
 	}
 	else
 	{
@@ -495,17 +577,17 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 
 	v[0].z = v[1].z = v[2].z = v[3].z = 1.0f;
 
-	v[0].s = v[3].s = ((sx)/(float)(gpatch->width))*hwrPatch->max_s;
-	if (sx + w > gpatch->width)
+	v[0].s = v[3].s = (FIXED_TO_FLOAT(sx)/(float)(gpatch->width))*hwrPatch->max_s;
+	if (sx + w > gpatch->width<<FRACBITS)
 		v[2].s = v[1].s = hwrPatch->max_s;
 	else
-		v[2].s = v[1].s = ((sx+w)/(float)(gpatch->width))*hwrPatch->max_s;
+		v[2].s = v[1].s = (FIXED_TO_FLOAT(sx+w)/(float)(gpatch->width))*hwrPatch->max_s;
 
-	v[0].t = v[1].t = ((sy)/(float)(gpatch->height))*hwrPatch->max_t;
-	if (sy + h > gpatch->height)
+	v[0].t = v[1].t = (FIXED_TO_FLOAT(sy)/(float)(gpatch->height))*hwrPatch->max_t;
+	if (sy + h > gpatch->height<<FRACBITS)
 		v[2].t = v[3].t = hwrPatch->max_t;
 	else
-		v[2].t = v[3].t = ((sy+h)/(float)(gpatch->height))*hwrPatch->max_t;
+		v[2].t = v[3].t = (FIXED_TO_FLOAT(sy+h)/(float)(gpatch->height))*hwrPatch->max_t;
 
 	flags = PF_Translucent|PF_NoDepthTest;
 
@@ -514,6 +596,76 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 	if (option & V_WRAPY)
 		flags |= PF_ForceWrapY;
 
+	// Auto-crop at splitscreen borders!
+	if (splitscreen && (option & V_PERPLAYER))
+	{
+#define flerp(a,b,amount) (((a) * (1.0f - (amount))) + ((b) * (amount))) // Float lerp
+
+#ifdef QUADS
+		if (splitscreen > 1) // 3 or 4 players
+		{
+			#error Auto-cropping doesnt take quadscreen into account! Fix it!
+			// Hint: For player 1/2, copy player 1's code below. For player 3/4, copy player 2's code below
+			// For player 1/3 and 2/4, mangle the below code to apply horizontally instead of vertically
+		}
+		else
+#endif
+		// 2 players
+		{
+			if (stplyr == &players[displayplayer]) // Player 1's screen, crop at the bottom
+			{
+				if ((cy - fheight) < 0) // If the bottom is below the border
+				{
+					if (cy <= 0) // If the whole patch is beyond the border...
+						return; // ...crop away the entire patch, don't draw anything
+
+					if (fheight <= 0) // Don't divide by zero
+						return;
+
+					v[2].y = v[3].y = 0; // Clamp the polygon edge vertex position
+					// Now for the UV-map... Uh-oh, math time!
+
+					// On second thought, a basic linear interpolation suffices
+					//float full_height = fheight;
+					//float cropped_height = fheight - cy;
+					//float remaining_height = cy;
+					//float cropped_percentage = (fheight - cy) / fheight;
+					//float remaining_percentage = cy / fheight;
+					//v[2].t = v[3].t = lerp(v[2].t, v[0].t, cropped_percentage);
+					// By swapping v[2] and v[0], we can use remaining_percentage for less operations
+					//v[2].t = v[3].t = lerp(v[0].t, v[2].t, remaining_percentage);
+
+					v[2].t = v[3].t = flerp(v[0].t, v[2].t, cy/fheight);
+				}
+			}
+			else //if (stplyr == &players[secondarydisplayplayer]) // Player 2's screen, crop at the top
+			{
+				if (cy > 0) // If the top is above the border
+				{
+					if ((cy - fheight) >= 0) // If the whole patch is beyond the border...
+						return; // ...crop away the entire patch, don't draw anything
+
+					if (fheight <= 0) // Don't divide by zero
+						return;
+
+					v[0].y = v[1].y = 0; // Clamp the polygon edge vertex position
+					// Now for the UV-map... Uh-oh, math time!
+
+					// On second thought, a basic linear interpolation suffices
+					//float full_height = fheight;
+					//float cropped_height = cy;
+					//float remaining_height = fheight - cy;
+					//float cropped_percentage = cy / fheight;
+					//float remaining_percentage = (fheight - cy) / fheight;
+					//v[0].t = v[1].t = lerp(v[0].t, v[2].t, cropped_percentage);
+
+					v[0].t = v[1].t = flerp(v[0].t, v[2].t, cy/fheight);
+				}
+			}
+		}
+#undef flerp
+	}
+
 	// clip it since it is used for bunny scroll in doom I
 	if (alphalevel)
 	{
diff --git a/src/hardware/hw_drv.h b/src/hardware/hw_drv.h
index da4ee861435dee90e9cf7dc87ec22009b66128ff..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.
@@ -68,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);
diff --git a/src/hardware/hw_glob.h b/src/hardware/hw_glob.h
index 2aba622481d618a4e8f5d2d499e420e74123b9ce..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.
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 f2af1cc4010a0de3efa8f23e758e3293a642bc53..e0851af8518e5146e8fac17af3f56a102799b7e9 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -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,13 +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_COPY:
-		case AST_OVERLAY:
-			return PF_Masked;
+		case AST_TRANSLUCENT:
+			return PF_Translucent;
 		case AST_ADD:
 			return PF_Additive;
 		case AST_SUBTRACT:
@@ -718,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)
@@ -747,7 +745,7 @@ UINT8 HWR_GetTranstableAlpha(INT32 transtablenum)
 
 FBITFIELD HWR_SurfaceBlend(INT32 style, INT32 transtablenum, FSurfaceInfo *pSurf)
 {
-	if (!transtablenum || style == AST_COPY || style == AST_OVERLAY)
+	if (!transtablenum || style <= AST_COPY || style >= AST_OVERLAY)
 	{
 		pSurf->PolyColor.s.alpha = 0xff;
 		return PF_Masked;
@@ -788,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);
 }
 
 // ==========================================================================
@@ -834,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
@@ -972,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;
@@ -1001,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
@@ -1107,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
@@ -1186,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
@@ -1252,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
@@ -1468,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))
@@ -1557,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)
@@ -1592,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)
 		{
@@ -1621,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);
@@ -1637,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
@@ -1720,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);
 				}
@@ -1735,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)
@@ -1767,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);
@@ -1782,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
@@ -1832,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);
 				}
@@ -1847,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)
@@ -1859,6 +1895,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 			}
 		}
 	}
+#undef SLOPEPARAMS
 //Hurdler: end of 3d-floors test
 }
 
@@ -2662,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;
@@ -2837,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)
@@ -2848,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)
@@ -3569,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);
 
@@ -3661,7 +3705,13 @@ 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
@@ -3709,6 +3759,7 @@ 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;
 	UINT8 alpha;
 
@@ -3829,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
@@ -3937,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);
@@ -3966,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);
@@ -4138,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;
@@ -4157,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
@@ -4216,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;
@@ -4266,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);
@@ -4277,6 +4341,7 @@ 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;
@@ -4365,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
 
@@ -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)
 	{
@@ -5658,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
 
@@ -6461,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);
 }
@@ -6654,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;
@@ -6692,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 4ad09aa3d63b1612239243ca15a0a2d878ea7c8d..b751b2a6e1c7698663e353c56d49e778e81fb7f0 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.
@@ -39,7 +39,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 +69,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);
 
diff --git a/src/hardware/hw_md2.c b/src/hardware/hw_md2.c
index 5caf344f75a3859751919338d8cea8b6badf1f92..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.
@@ -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);
+					}
+				}
 			}
 		}
 
@@ -1533,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 aee0ffc45576308e5eacbae3dcad2a99fa7a4d32..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.
@@ -62,6 +62,9 @@ static  FBITFIELD   CurrentPolyFlags;
 static FTextureInfo *TexCacheTail = NULL;
 static FTextureInfo *TexCacheHead = NULL;
 
+static RGBA_t *textureBuffer = NULL;
+static size_t textureBufferSize = 0;
+
 RGBA_t  myPaletteData[256];
 GLint   screen_width    = 0;               // used by Draw2DLine()
 GLint   screen_height   = 0;
@@ -131,7 +134,6 @@ static const GLfloat byte2float[256] = {
 // -----------------+
 // GL_DBG_Printf    : Output debug messages to debug log if DEBUG_TO_FILE is defined,
 //                  : else do nothing
-// Returns          :
 // -----------------+
 
 #ifdef DEBUG_TO_FILE
@@ -159,8 +161,6 @@ FUNCPRINTF void GL_DBG_Printf(const char *format, ...)
 
 // -----------------+
 // GL_MSG_Warning   : Raises a warning.
-//                  :
-// Returns          :
 // -----------------+
 
 static void GL_MSG_Warning(const char *format, ...)
@@ -184,8 +184,6 @@ static void GL_MSG_Warning(const char *format, ...)
 
 // -----------------+
 // GL_MSG_Error     : Raises an error.
-//                  :
-// Returns          :
 // -----------------+
 
 static void GL_MSG_Error(const char *format, ...)
@@ -910,7 +908,6 @@ void SetupGLFunc4(void)
 	pgluBuild2DMipmaps = GetGLFunc("gluBuild2DMipmaps");
 }
 
-// jimita
 EXPORT boolean HWRAPI(CompileShaders) (void)
 {
 #ifdef GL_SHADERS
@@ -1346,6 +1343,10 @@ void Flush(void)
 
 	TexCacheTail = TexCacheHead = NULL; //Hurdler: well, TexCacheHead is already NULL
 	tex_downloaded = 0;
+
+	free(textureBuffer);
+	textureBuffer = NULL;
+	textureBufferSize = 0;
 }
 
 
@@ -1379,7 +1380,6 @@ INT32 isExtAvailable(const char *extension, const GLubyte *start)
 
 // -----------------+
 // Init             : Initialise the OpenGL interface API
-// Returns          :
 // -----------------+
 EXPORT boolean HWRAPI(Init) (void)
 {
@@ -1739,37 +1739,48 @@ EXPORT void HWRAPI(SetBlend) (FBITFIELD PolyFlags)
 	CurrentPolyFlags = PolyFlags;
 }
 
+static void AllocTextureBuffer(GLMipmap_t *pTexInfo)
+{
+	size_t size = pTexInfo->width * pTexInfo->height;
+	if (size > textureBufferSize)
+	{
+		textureBuffer = realloc(textureBuffer, size * sizeof(RGBA_t));
+		if (textureBuffer == NULL)
+			I_Error("AllocTextureBuffer: out of memory allocating %s bytes", sizeu1(size * sizeof(RGBA_t)));
+		textureBufferSize = size;
+	}
+}
+
 // -----------------+
-// UpdateTexture    : Updates the texture data.
+// UpdateTexture    : Updates texture data.
 // -----------------+
 EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 {
-	// Download a mipmap
-	boolean updatemipmap = true;
-	static RGBA_t   tex[2048*2048];
-	const GLvoid   *ptex = tex;
-	INT32             w, h;
-	GLuint texnum = 0;
+	// Upload a texture
+	GLuint num = pTexInfo->downloaded;
+	boolean update = true;
+
+	INT32 w = pTexInfo->width, h = pTexInfo->height;
+	INT32 i, j;
 
-	if (!pTexInfo->downloaded)
+	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++)
 		{
@@ -1800,20 +1811,18 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 						tex[w*j+i].s.alpha = *pImgData;
 					pImgData++;
 				}
-
 			}
 		}
 	}
 	else if (pTexInfo->format == GL_TEXFMT_RGBA)
 	{
-		// corona test : passed as ARGB 8888, which is not in glide formats
-		// Hurdler: not used for coronas anymore, just for dynamic lighting
-		ptex = pTexInfo->data;
+		// Directly upload the texture data without any kind of conversion.
+		ptex = pImgData;
 	}
 	else if (pTexInfo->format == GL_TEXFMT_ALPHA_INTENSITY_88)
 	{
-		const GLubyte *pImgData = (const GLubyte *)pTexInfo->data;
-		INT32 i, j;
+		AllocTextureBuffer(pTexInfo);
+		ptex = tex = textureBuffer;
 
 		for (j = 0; j < h; j++)
 		{
@@ -1830,8 +1839,8 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 	}
 	else if (pTexInfo->format == GL_TEXFMT_ALPHA_8) // Used for fade masks
 	{
-		const GLubyte *pImgData = (const GLubyte *)pTexInfo->data;
-		INT32 i, j;
+		AllocTextureBuffer(pTexInfo);
+		ptex = tex = textureBuffer;
 
 		for (j = 0; j < h; j++)
 		{
@@ -1846,11 +1855,10 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 		}
 	}
 	else
-		GL_MSG_Warning ("SetTexture(bad format) %ld\n", pTexInfo->format);
+		GL_MSG_Warning("UpdateTexture: bad format %d\n", pTexInfo->format);
 
-	// the texture number was already generated by pglGenTextures
-	pglBindTexture(GL_TEXTURE_2D, texnum);
-	tex_downloaded = texnum;
+	pglBindTexture(GL_TEXTURE_2D, num);
+	tex_downloaded = num;
 
 	// disable texture filtering on any texture that has holes so there's no dumb borders or blending issues
 	if (pTexInfo->flags & TF_TRANSPARENT)
@@ -1879,7 +1887,7 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 		}
 		else
 		{
-			if (updatemipmap)
+			if (update)
 				pglTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
 			else
 				pglTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
@@ -1900,7 +1908,7 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 		}
 		else
 		{
-			if (updatemipmap)
+			if (update)
 				pglTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
 			else
 				pglTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
@@ -1920,7 +1928,7 @@ EXPORT void HWRAPI(UpdateTexture) (GLMipmap_t *pTexInfo)
 		}
 		else
 		{
-			if (updatemipmap)
+			if (update)
 				pglTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
 			else
 				pglTexImage2D(GL_TEXTURE_2D, 0, textureformatGL, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ptex);
@@ -2176,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)
@@ -3015,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)
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 7c4f1acf1124b1087dcd7a90cf61b2f883434f18..e0eaf8fb178f1252b60b93e329d2f14d05858e53 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.
@@ -686,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)
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 12f0d751d14eec081175d3a01fdb8a1d03647bb4..e046fd620114161ddf49e7bba50685547cd7d32a 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.
@@ -314,4 +314,16 @@ 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 Returns whether the mouse is grabbed
+*/
+boolean I_GetMouseGrab(void);
+
+/** \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 ab8a69a9fad3c300c92fa540fa60ae68345aff82..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.
@@ -64,7 +64,7 @@
 		#include <errno.h>
 		#include <time.h>
 
-		#if (defined (__unix__) && !defined (MSDOS)) || defined(__APPLE__) || defined (UNIXCOMMON)
+		#if defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)
 			#include <sys/time.h>
 		#endif // UNIXCOMMON
 	#endif
@@ -107,15 +107,6 @@
 		#endif
 	#endif // USE_WINSOCK
 
-	#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;
@@ -149,32 +140,22 @@
 
 #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)
 #else
-	#if (defined (__unix__) && !defined (MSDOS)) || defined (__APPLE__) || defined (__HAIKU__)
+	#if defined (__unix__) || defined (__APPLE__) || defined (__HAIKU__)
 		typedef int SOCKET_TYPE;
 	#else
 		typedef unsigned long SOCKET_TYPE;
@@ -184,7 +165,7 @@
 
 #ifndef NONET
 	// define socklen_t in DOS/Windows if it is not already defined
-	#if (defined (WATTCP) && !defined (__libsocket_socklen_t)) || defined (USE_WINSOCK1)
+	#ifdef USE_WINSOCK1
 		typedef int socklen_t;
 	#endif
 	static SOCKET_TYPE mysockets[MAXNETNODES+1] = {ERRSOCKET};
@@ -207,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)
@@ -764,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;
@@ -1138,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)
@@ -1217,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;
 	}
@@ -1237,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 ee836a3728e4bb0ccea8352b790b7dfdb053136e..efcf1c044141225bf9fd64e1af619548df2c7298 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.
@@ -13481,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
@@ -21761,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 60e9702463fb50db9934d6aea9e6eab28a562983..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.
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 a59ba546e9bd5e4d9da6206759d05788f456a0ff..1c3b483fa1d702cac697c3c1665c97133b2b6b6b 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,9 @@
 #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 "lua_script.h"
 #include "lua_libs.h"
@@ -212,6 +215,8 @@ static const struct {
 	{META_ACTION,       "action"},
 
 	{META_LUABANKS,     "luabanks[]"},
+
+	{META_MOUSE,        "mouse_t"},
 	{NULL,              NULL}
 };
 
@@ -242,16 +247,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
@@ -363,6 +362,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
 //////////////
 
@@ -432,7 +448,7 @@ static int lib_pAproxDistance(lua_State *L)
 	fixed_t dx = luaL_checkfixed(L, 1);
 	fixed_t dy = luaL_checkfixed(L, 2);
 	//HUDSAFE
-	lua_pushfixed(L, R_PointToDist2(0, 0, dx, dy));
+	lua_pushfixed(L, P_AproxDistance(dx, dy));
 	return 1;
 }
 
@@ -1050,48 +1066,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;
 }
 
@@ -1478,11 +1502,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;
 }
 
@@ -2514,6 +2540,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
 ////////////
 
@@ -2828,46 +2865,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];
-
-	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);
-	}
+	UINT32 position, prefadems, fadeinms;
 
-	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));
@@ -2875,13 +2879,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);
@@ -3178,33 +3176,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;
@@ -3427,6 +3399,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)
@@ -3784,6 +3861,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},
@@ -3954,6 +4034,7 @@ static luaL_Reg lib[] = {
 
 	// p_slopes
 	{"P_GetZAt",lib_pGetZAt},
+	{"P_ButteredSlope",lib_pButteredSlope},
 
 	// r_defs
 	{"R_PointToAngle",lib_rPointToAngle},
@@ -4009,6 +4090,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},
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 5344fee7617aacf789e577eed98de26b3296818a..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,16 +273,13 @@ 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_pushcfunction(gL, LUA_GetErrorMessage);
@@ -291,13 +288,10 @@ static void Lua_OnChange(void)
 	// 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
@@ -312,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
@@ -369,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
@@ -397,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;
 		}
@@ -415,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);
@@ -440,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;
 }
 
@@ -450,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))
 	{
@@ -482,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);
@@ -541,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 0d631aa4e275259c5bda256e2dcc0e5824283ea4..223b83c61f7d9c35a451a3ea217344a341fe7126 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,113 +12,122 @@
 
 #include "r_defs.h"
 #include "d_player.h"
+#include "s_sound.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_PlayerHeight,
-	hook_PlayerCanEnterSpinGaps,
+/*
+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.
+*/
 
-	hook_MAX // last hook
-};
-extern const char *const hookNames[];
+#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 */\
+
+/*
+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 STRING_HOOK(name) stringhook_ ## name
+
+enum {   MOBJ_HOOK_LIST   (MOBJ_HOOK)    MOBJ_HOOK(MAX) };
+enum {        HOOK_LIST        (HOOK)         HOOK(MAX) };
+enum { STRING_HOOK_LIST (STRING_HOOK)  STRING_HOOK(MAX) };
+
+/* dead simple, LUA_HOOK(GameQuit) */
+#define LUA_HOOK(type) LUA_HookVoid(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
-boolean LUAh_SeenPlayer(player_t *player, player_t *seenfriend); // Hook for MT_NAMECHECK
-#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(boolean quitting); // 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
-fixed_t LUAh_PlayerHeight(player_t *player);
-UINT8 LUAh_PlayerCanEnterSpinGaps(player_t *player);
+void LUA_HookVoid(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(INT32 keycode, 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_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 637809fd84811f75b5e379c7de6c85d76dca9e96..d1b0d3bdd21b26184f49079c622ae16ec23f5f87 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.
@@ -27,2037 +27,1075 @@
 #include "d_netcmd.h" // for cv_perfstats
 #include "i_system.h" // I_GetPreciseTime
 
-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",
-	"PlayerHeight",
-	"PlayerCanEnterSpinGaps",
-	NULL
-};
+/* =========================================================================
+                                  ABSTRACTION
+   ========================================================================= */
 
-// Hook metadata
-struct hook_s
-{
-	struct hook_s *next;
-	enum hook type;
-	UINT16 id;
-	union {
-		mobjtype_t mt;
-		char *str;
-	} s;
-	boolean error;
+static const char * const mobjHookNames[] = { MOBJ_HOOK_LIST (TOSTR)  NULL };
+static const char * const     hookNames[] = {      HOOK_LIST (TOSTR)  NULL };
+
+static const char * const stringHookNames[] = {
+	STRING_HOOK_LIST (TOSTR)  NULL
 };
-typedef struct hook_s* hook_p;
 
-#define FMT_HOOKID "hook_%d"
+typedef struct {
+	int numHooks;
+	int *ids;
+} hook_t;
 
-// 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];
+typedef struct {
+	int numGeneric;
+	int ref;
+} stringhook_t;
 
-// For each mobj type, a linked list for other mobj hooks
-static hook_p mobjhooks[NUMMOBJTYPES];
+static hook_t hookIds[HOOK(MAX)];
+static hook_t mobjHookIds[NUMMOBJTYPES][MOBJ_HOOK(MAX)];
 
-// A linked list for player hooks
-static hook_p playerhooks;
+// Lua tables are used to lookup string hook ids.
+static stringhook_t stringHooks[STRING_HOOK(MAX)];
 
-// A linked list for linedef executor hooks
-static hook_p linedefexecutorhooks;
+// This will be indexed by hook id, the value of which fetches the registry.
+static int * hookRefs;
 
-// For other hooks, a unique linked list
-hook_p roothook;
+// After a hook errors once, don't print the error again.
+static UINT8 * hooksErrored;
 
-static void PushHook(lua_State *L, hook_p hookp)
-{
-	lua_pushfstring(L, FMT_HOOKID, hookp->id);
-	lua_gettable(L, LUA_REGISTRYINDEX);
-}
+static int errorRef;
 
-// Takes hook, function, and additional arguments (mobj type to act on, etc.)
-static int lib_addHook(lua_State *L)
+static boolean mobj_hook_available(int hook_type, mobjtype_t mobj_type)
 {
-	static struct hook_s hook = {NULL, 0, 0, {0}, false};
-	static UINT32 nextid;
-	hook_p hookp, *lastp;
-
-	hook.type = luaL_checkoption(L, 1, NULL, hookNames);
-	lua_remove(L, 1);
-
-	luaL_checktype(L, 1, LUA_TFUNCTION);
+	return
+		(
+				mobjHookIds [MT_NULL] [hook_type].numHooks > 0 ||
+				mobjHookIds[mobj_type][hook_type].numHooks > 0
+		);
+}
 
-	if (!lua_lumploading)
-		return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
+static int hook_in_list
+(
+		const char * const         name,
+		const char * const * const list
+){
+	int type;
 
-	switch(hook.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:
-	case hook_PlayerHeight:
-	case hook_PlayerCanEnterSpinGaps:
-		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 id, int n)
 {
-	memset(hooksAvailable,0,sizeof(UINT8[(hook_MAX/8)+1]));
-	roothook = NULL;
-	lua_register(L, "addHook", lib_addHook);
-	return 0;
+	lua_pushnumber(L, id);
+	lua_rawseti(L, -2, n);
 }
 
-boolean LUAh_MobjHook(mobj_t *mo, enum hook which)
+static void add_string_hook(lua_State *L, int type, int id)
 {
-	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, id, 1 + lua_objlen(L, -1));
 	}
-
-	lua_settop(gL, 0);
-	return hooked;
+	else
+		add_hook_to_table(L, id, ++hook->numGeneric);
 }
 
-boolean LUAh_PlayerHook(player_t *plr, enum hook which)
+static void add_hook(hook_t *map, int id)
 {
-	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++] = id;
+}
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+static void add_mobj_hook(lua_State *L, int hook_type, int id)
+{
+	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], id);
 }
 
-// Hook for map change (before load)
-void LUAh_MapChange(INT16 mapnumber)
+// 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_MapChange/8] & (1<<(hook_MapChange%8))))
-		return;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-	lua_pushinteger(gL, mapnumber);
+	static int nextid;
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
-	{
-		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);
-		}
-	}
+	const char * name;
+	int type;
 
-	lua_settop(gL, 0);
-}
+	if (!lua_lumploading)
+		return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
 
-// Hook for map load
-void LUAh_MapLoad(void)
-{
-	hook_p hookp;
-	if (!gL || !(hooksAvailable[hook_MapLoad/8] & (1<<(hook_MapLoad%8))))
-		return;
+	name = luaL_checkstring(L, 1);
+	luaL_checktype(L, 2, LUA_TFUNCTION);
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-	lua_pushinteger(gL, gamemap);
+	/* this is a very special case */
+	if (( type = hook_in_list(name, stringHookNames) ) < STRING_HOOK(MAX))
+	{
+		add_string_hook(L, type, nextid);
+	}
+	else if (( type = hook_in_list(name, mobjHookNames) ) < MOBJ_HOOK(MAX))
+	{
+		add_mobj_hook(L, type, nextid);
+	}
+	else if (( type = hook_in_list(name, hookNames) ) < HOOK(MAX))
+	{
+		add_hook(&hookIds[type], nextid);
+	}
+	else
+	{
+		return luaL_argerror(L, 1, lua_pushfstring(L, "invalid hook " LUA_QS, name));
+	}
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	if (!(nextid & 7))
 	{
-		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);
-		}
+		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, 2);/* the function */
+	hookRefs[nextid++] = luaL_ref(L, LUA_REGISTRYINDEX);
+
+	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)
-{
-	hook_p hookp;
-	if (!gL || !(hooksAvailable[hook_PreThinkFrame/8] & (1<<(hook_PreThinkFrame%8))))
-		return;
-
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+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 */
+};
 
-	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;
-		}
-	}
+enum {
+	EINDEX = 1,/* error handler */
+	SINDEX = 2,/* string itself is pushed in case of string hook */
+};
 
-	lua_pop(gL, 1); // Pop error handler
+static void push_error_handler(void)
+{
+	lua_getref(gL, errorRef);
 }
 
-// Hook for frame (after mobj and player thinkers)
-void LUAh_ThinkFrame(void)
+/* repush hook string */
+static void push_string(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_pushvalue(gL, SINDEX);
+}
 
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+static boolean start_hook_stack(void)
+{
+	lua_settop(gL, 0);
+	push_error_handler();
+	return true;
+}
 
-	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_GetPreciseTime();
-		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_GetPreciseTime() - 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++;
-		}
+		hook->hook_type = hook_type;
+		hook->mobj_type = mobj_type;
+		hook->string = string;
+		return start_hook_stack();
 	}
-
-	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 true;
 	}
+	else
+		return false;
+}
 
-	lua_pop(gL, 1); // Pop error handler
+static void init_hook_call
+(
+		Hook_State * hook,
+		int    values,
+		int    results,
+		Hook_Callback results_handler
+){
+	hook->top = lua_gettop(gL);
+	hook->values = values;
+	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;
-
-	I_Assert(thing->type < NUMMOBJTYPES);
+	int i;
 
-	if (!(mobjcollidehooks[MT_NULL] || mobjcollidehooks[thing->type]))
-		return 0;
+	for (i = -(hook->values) + 1; i <= 0; ++i)
+		lua_pushvalue(gL, hook->top + i);
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	// Look for all generic mobj collision hooks
-	for (hookp = mobjcollidehooks[MT_NULL]; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != which)
-			continue;
+	return call_single_hook_no_copy(hook);
+}
 
-		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_for(Hook_State *hook, int n)
+{
+	int k;
 
-	for (hookp = mobjcollidehooks[thing->type]; 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);
 	}
 
-	lua_settop(gL, 0);
-	return shouldCollide;
+	return n;
 }
 
-// Hook for mobj thinkers
-boolean LUAh_MobjThinker(mobj_t *mo)
+static int call_hook_table(Hook_State *hook)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_MobjThinker/8] & (1<<(hook_MobjThinker%8))))
-		return false;
-
-	I_Assert(mo->type < NUMMOBJTYPES);
-
-	if (!(mobjthinkerhooks[MT_NULL] || mobjthinkerhooks[mo->type]))
-		return false;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+	return call_hook_table_for(hook, lua_objlen(gL, -1));
+}
 
-	// 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);
-	}
+static int call_mapped(Hook_State *hook, const hook_t *map)
+{
+	int k;
 
-	for (hookp = mobjthinkerhooks[mo->type]; hookp; hookp = hookp->next)
+	for (k = 0; k < map->numHooks; ++k)
 	{
-		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);
+		get_hook(hook, map->ids, k);
+		call_single_hook(hook);
 	}
 
-	lua_settop(gL, 0);
-	return hooked;
+	return map->numHooks;
 }
 
-// Hook for P_TouchSpecialThing by mobj type
-boolean LUAh_TouchSpecial(mobj_t *special, mobj_t *toucher)
+static int call_string_hooks(Hook_State *hook)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_TouchSpecial/8] & (1<<(hook_TouchSpecial%8))))
-		return false;
-
-	I_Assert(special->type < NUMMOBJTYPES);
-
-	if (!(mobjhooks[MT_NULL] || mobjhooks[special->type]))
-		return false;
+	const stringhook_t *map = &stringHooks[hook->hook_type];
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	// Look for all generic touch special hooks
-	for (hookp = mobjhooks[MT_NULL]; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != hook_TouchSpecial)
-			continue;
+	int calls = 0;
 
-		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);
-	}
+	lua_getref(gL, map->ref);
 
-	for (hookp = mobjhooks[special->type]; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != hook_TouchSpecial)
-			continue;
+	/* call generic string hooks first */
+	calls += call_hook_table_for(hook, map->numGeneric);
 
-		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);
-	}
+	push_string();
+	lua_rawget(gL, -2);
+	calls += call_hook_table(hook);
 
-	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)
+static int call_mobj_type_hooks(Hook_State *hook, mobjtype_t mobj_type)
 {
-	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;
-
-	I_Assert(target->type < NUMMOBJTYPES);
+	return call_mapped(hook, &mobjHookIds[mobj_type][hook->hook_type]);
+}
 
-	if (!(mobjhooks[MT_NULL] || mobjhooks[target->type]))
-		return 0;
+static int call_hooks
+(
+		Hook_State * hook,
+		int        values,
+		int        results,
+		Hook_Callback results_handler
+){
+	int calls = 0;
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+	init_hook_call(hook, values, results, results_handler);
 
-	// Look for all generic should damage hooks
-	for (hookp = mobjhooks[MT_NULL]; hookp; hookp = hookp->next)
+	if (hook->string)
 	{
-		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);
+		calls += call_string_hooks(hook);
 	}
-
-	for (hookp = mobjhooks[target->type]; hookp; hookp = hookp->next)
+	else if (hook->mobj_type > 0)
 	{
-		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);
+		/* 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 += calls;
 	}
+	else
+		calls += call_mapped(hook, &hookIds[hook->hook_type]);
 
 	lua_settop(gL, 0);
-	return shouldDamage;
+
+	return calls;
 }
 
-// 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)
-{
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_MobjDamage/8] & (1<<(hook_MobjDamage%8))))
-		return false;
+/* =========================================================================
+                            COMMON RESULT HANDLERS
+   ========================================================================= */
 
-	I_Assert(target->type < NUMMOBJTYPES);
+#define res_none NULL
 
-	if (!(mobjhooks[MT_NULL] || mobjhooks[target->type]))
-		return false;
+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 mobj 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_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);
+			hook->status = 1; // Force yes
+		else
+			hook->status = 2; // Force no
 	}
+}
 
-	for (hookp = mobjhooks[target->type]; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != hook_MobjDamage)
-			continue;
+/* =========================================================================
+                               GENERALISED HOOKS
+   ========================================================================= */
 
-		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);
+int LUA_HookMobj(mobj_t *mobj, int hook_type)
+{
+	Hook_State hook;
+	if (prepare_mobj_hook(&hook, false, hook_type, mobj->type))
+	{
+		LUA_PushUserdata(gL, mobj, META_MOBJ);
+		call_hooks(&hook, 1, 1, res_true);
 	}
-
-	lua_settop(gL, 0);
-	return hooked;
+	return hook.status;
 }
 
-// Hook for P_KillMobj by mobj type
-boolean LUAh_MobjDeath(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damagetype)
+int LUA_Hook2Mobj(mobj_t *t1, mobj_t *t2, int hook_type)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_MobjDeath/8] & (1<<(hook_MobjDeath%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, 2, 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, 0, res_none);
+}
 
-	// Look for all generic mobj death 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_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_pushinteger(gL, number);
+		call_hooks(&hook, 1, 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_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_pushboolean(gL, value);
+		call_hooks(&hook, 1, 0, res_none);
 	}
+}
 
-	lua_settop(gL, 0);
-	return hooked;
+int LUA_HookPlayer(player_t *player, int hook_type)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, false, hook_type))
+	{
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		call_hooks(&hook, 1, 1, res_true);
+	}
+	return hook.status;
 }
 
-// Hook for B_BuildTiccmd
-boolean LUAh_BotTiccmd(player_t *bot, ticcmd_t *cmd)
+int LUA_HookTiccmd(player_t *player, ticcmd_t *cmd, int hook_type)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_BotTiccmd/8] & (1<<(hook_BotTiccmd%8))))
-		return false;
+	Hook_State hook;
+	if (prepare_hook(&hook, false, hook_type))
+	{
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		LUA_PushUserdata(gL, cmd, META_TICCMD);
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+		if (hook_type == HOOK(PlayerCmd))
+			hook_cmd_running = true;
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != hook_BotTiccmd)
-			continue;
+		call_hooks(&hook, 2, 1, res_true);
 
-		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);
+		if (hook_type == HOOK(PlayerCmd))
+			hook_cmd_running = false;
 	}
+	return hook.status;
+}
 
-	lua_settop(gL, 0);
-	return hooked;
+int LUA_HookKey(INT32 keycode, int hook_type)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, false, hook_type))
+	{
+		lua_pushinteger(gL, keycode);
+		call_hooks(&hook, 1, 1, res_true);
+	}
+	return hook.status;
 }
 
-// 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, 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, 2, 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;
+		LUA_PushUserdata(gL, special, META_MOBJ);
+		LUA_PushUserdata(gL, toucher, META_MOBJ);
+		call_hooks(&hook, 2, 1, res_true);
+	}
+	return hook.status;
+}
 
-		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;
+static int damage_hook
+(
+		mobj_t *target,
+		mobj_t *inflictor,
+		mobj_t *source,
+		INT32   damage,
+		UINT8   damagetype,
+		int     hook_type,
+		int     values,
+		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 (values == 5)
+			lua_pushinteger(gL, damage);
+		lua_pushinteger(gL, damagetype);
+		call_hooks(&hook, values, 1, results_handler);
 	}
+	return hook.status;
+}
 
-	lua_settop(gL, 0);
-	return hooked;
+int LUA_HookShouldDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
+{
+	return damage_hook(target, inflictor, source, damage, damagetype,
+			MOBJ_HOOK(ShouldDamage), 5, res_force);
 }
 
+int LUA_HookMobjDamage(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 damage, UINT8 damagetype)
+{
+	return damage_hook(target, inflictor, source, damage, damagetype,
+			MOBJ_HOOK(MobjDamage), 5, res_true);
+}
 
-boolean LUAh_PlayerMsg(int source, int target, int flags, char *msg)
+int LUA_HookMobjDeath(mobj_t *target, mobj_t *inflictor, mobj_t *source, 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, 0, damagetype,
+			MOBJ_HOOK(MobjDeath), 4, res_true);
+}
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+typedef struct {
+	mobj_t   * tails;
+	ticcmd_t * cmd;
+} BotAI_State;
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != hook_PlayerMsg)
-			continue;
+static boolean checkbotkey(const char *field)
+{
+	return lua_toboolean(gL, -1) && strcmp(lua_tostring(gL, -2), field) == 0;
+}
 
-		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
+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;
+				}
 			}
-			lua_pushstring(gL, msg); // msg
+
+			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;
+}
 
-// Hook for hurt messages
-boolean LUAh_HurtMsg(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 damagetype)
+int LUA_HookBotAI(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 {
-	hook_p hookp;
-	boolean hooked = false;
-	if (!gL || !(hooksAvailable[hook_HurtMsg/8] & (1<<(hook_HurtMsg%8))))
-		return false;
+	const char *skin = ((skin_t *)tails->skin)->name;
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+	Hook_State hook;
+	BotAI_State botai;
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	if (prepare_string_hook(&hook, false, STRING_HOOK(BotAI), skin))
 	{
-		if (hookp->type != hook_HurtMsg
-		|| (hookp->s.mt && !(inflictor && hookp->s.mt == inflictor->type)))
-			continue;
+		LUA_PushUserdata(gL, sonic, META_MOBJ);
+		LUA_PushUserdata(gL, tails, META_MOBJ);
 
-		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);
-		}
-		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);
+		botai.tails = tails;
+		botai.cmd   = cmd;
+
+		hook.userdata = &botai;
+
+		call_hooks(&hook, 2, 8, res_botai);
 	}
 
-	lua_settop(gL, 0);
-	return hooked;
+	return hook.status;
 }
 
-void LUAh_NetArchiveHook(lua_CFunction archFunc)
+void LUA_HookLinedefExecute(line_t *line, mobj_t *mo, sector_t *sector)
 {
-	hook_p hookp;
-	int errorhandlerindex;
-	if (!gL || !(hooksAvailable[hook_NetVars/8] & (1<<(hook_NetVars%8))))
-		return;
-
-	// stack: tables
-	I_Assert(lua_gettop(gL) > 0);
-	I_Assert(lua_istable(gL, -1));
-
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-	errorhandlerindex = lua_gettop(gL);
-
-	// tables becomes an upvalue of archFunc
-	lua_pushvalue(gL, -2);
-	lua_pushcclosure(gL, archFunc, 1);
-	// stack: tables, archFunc
-
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	Hook_State hook;
+	if (prepare_string_hook
+			(&hook, 0, STRING_HOOK(LinedefExecute), line->stringargs[0]))
 	{
-		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);
-		}
+		LUA_PushUserdata(gL, line, META_LINE);
+		LUA_PushUserdata(gL, mo, META_MOBJ);
+		LUA_PushUserdata(gL, sector, META_SECTOR);
+		ps_lua_mobjhooks += call_hooks(&hook, 3, 0, res_none);
 	}
-
-	lua_pop(gL, 2); // Pop archFunc and error handler
-	// stack: tables
 }
 
-boolean LUAh_MapThingSpawn(mobj_t *mo, mapthing_t *mthing)
+int LUA_HookPlayerMsg(int source, int target, int flags, char *msg)
 {
-	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_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, 4, 1, res_true);
 	}
+	return hook.status;
+}
 
-	for (hookp = mobjhooks[mo->type]; hookp; hookp = hookp->next)
+int LUA_HookHurtMsg(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 damagetype)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, false, HOOK(HurtMsg)))
 	{
-		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, player, META_PLAYER);
+		LUA_PushUserdata(gL, inflictor, META_MOBJ);
+		LUA_PushUserdata(gL, source, META_MOBJ);
+		lua_pushinteger(gL, damagetype);
+		call_hooks(&hook, 4, 1, res_true);
 	}
-
-	lua_settop(gL, 0);
-	return hooked;
+	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);
+		// tables becomes an upvalue of archFunc
+		lua_pushvalue(gL, -1);
+		lua_pushcclosure(gL, archFunc, 1);
+		// stack: tables, archFunc
 
-	// Look for all generic mobj follow item hooks
-	for (hookp = mobjhooks[MT_NULL]; hookp; hookp = hookp->next)
-	{
-		if (hookp->type != hook_FollowMobj)
-			continue;
+		init_hook_call(&hook, 1, 0, res_none);
+		call_mapped(&hook, map);
 
-		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_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, 2, 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, 2, 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, 2, 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, 2, 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, 5, 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, 3, 1, res_force);
+		hud_running = false;
 	}
-
-	lua_settop(gL, 0);
-
-	hud_running = false;
-
-	return canSwitchView;
+	return hook.status;
 }
 
-// Hook for MT_NAMECHECK
-boolean LUAh_SeenPlayer(player_t *player, player_t *seenfriend)
+int LUA_HookSeenPlayer(player_t *player, player_t *seenfriend)
 {
-	hook_p hookp;
-	boolean hasSeenPlayer = true;
-	if (!gL || !(hooksAvailable[hook_SeenPlayer/8] & (1<<(hook_SeenPlayer%8))))
-		return true;
-
-	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, true, HOOK(SeenPlayer)))
 	{
-		if (hookp->type != hook_SeenPlayer)
-			continue;
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		LUA_PushUserdata(gL, seenfriend, META_PLAYER);
 
-		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);
+		hud_running = true; // local hook
+		call_hooks(&hook, 2, 1, res_false);
+		hud_running = false;
 	}
-
-	lua_settop(gL, 0);
-
-	hud_running = false;
-
-	return hasSeenPlayer;
+	return hook.status;
 }
 
-boolean LUAh_ShouldJingleContinue(player_t *player, const char *musname)
+int LUA_HookShouldJingleContinue(player_t *player, const char *musname)
 {
-	hook_p hookp;
-	boolean keepplaying = false;
-	if (!gL || !(hooksAvailable[hook_ShouldJingleContinue/8] & (1<<(hook_ShouldJingleContinue%8))))
-		return true;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	hud_running = true; // local hook
-
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	Hook_State hook;
+	if (prepare_string_hook
+			(&hook, false, STRING_HOOK(ShouldJingleContinue), musname))
 	{
-		if (hookp->type != hook_ShouldJingleContinue
-			|| (hookp->s.str && strcmp(hookp->s.str, musname)))
-			continue;
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		push_string();
 
-		if (lua_gettop(gL) == 1)
-		{
-			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);
+		hud_running = true; // local hook
+		call_hooks(&hook, 2, 1, res_true);
+		hud_running = false;
 	}
-
-	lua_settop(gL, 0);
-
-	hud_running = false;
-
-	return keepplaying;
+	return hook.status;
 }
 
-// Hook for game quitting
-void LUAh_GameQuit(boolean quitting)
-{
-	hook_p hookp;
-	if (!gL || !(hooksAvailable[hook_GameQuit/8] & (1<<(hook_GameQuit%8))))
-		return;
+boolean hook_cmd_running = false;
 
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+static void update_music_name(struct MusicChange *musicchange)
+{
+	size_t length;
+	const char * new = lua_tolstring(gL, -6, &length);
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
+	if (length < 7)
 	{
-		if (hookp->type != hook_GameQuit)
-			continue;
-
-		PushHook(gL, hookp);
-		lua_pushboolean(gL, quitting);
-		if (lua_pcall(gL, 1, 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;
-		}
+		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_pop(gL, 1); // Pop error handler
+	lua_replace(gL, -7);
 }
 
-// Hook for building player's ticcmd struct (Ported from SRB2Kart)
-boolean hook_cmd_running = false;
-boolean LUAh_PlayerCmd(player_t *player, ticcmd_t *cmd)
+static void res_musicchange(Hook_State *hook)
 {
-	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)
-	{
-		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_settop(gL, 0);
-	hook_cmd_running = false;
-	return hooked;
+	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);
 }
 
-// Hook for music changes
-boolean LUAh_MusicChange(const char *oldname, char *newname, UINT16 *mflags, boolean *looping,
-	UINT32 *position, UINT32 *prefadems, UINT32 *fadeinms)
+int LUA_HookMusicChange(const char *oldname, struct MusicChange *param)
 {
-	hook_p hookp;
-	boolean hooked = false;
+	const int type = HOOK(MusicChange);
+	const hook_t * map = &hookIds[type];
 
-	if (!gL || !(hooksAvailable[hook_MusicChange/8] & (1<<(hook_MusicChange%8))))
-		return false;
+	Hook_State hook;
 
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
+	int k;
+
+	if (prepare_hook(&hook, false, type))
+	{
+		init_hook_call(&hook, 7, 6, res_musicchange);
+		hook.userdata = param;
+
+		lua_pushstring(gL, oldname);/* the only constant value */
+		lua_pushstring(gL, param->newname);/* semi constant */
 
-	for (hookp = roothook; hookp; hookp = hookp->next)
-		if (hookp->type == hook_MusicChange)
+		for (k = 0; k <= map->numHooks; ++k)
 		{
-			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;
-			}
+			get_hook(&hook, map->ids, k);
+
+			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);
 
-			// 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
+			call_single_hook_no_copy(&hook);
 		}
 
-	lua_settop(gL, 0);
-	newname[6] = 0;
-	return hooked;
+		lua_settop(gL, 0);
+	}
+
+	return hook.status;
 }
 
-// Hook for determining player height
-fixed_t LUAh_PlayerHeight(player_t *player)
+static void res_playerheight(Hook_State *hook)
 {
-	hook_p hookp;
-	fixed_t newheight = -1;
-	if (!gL || !(hooksAvailable[hook_PlayerHeight/8] & (1<<(hook_PlayerHeight%8))))
-		return newheight;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	for (hookp = playerhooks; hookp; hookp = hookp->next)
+	if (!lua_isnil(gL, -1))
 	{
-		if (hookp->type != hook_PlayerHeight)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-			LUA_PushUserdata(gL, player, 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_isnil(gL, -1))
-		{
-			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)
-				newheight = returnedheight;
-		}
-		lua_pop(gL, 1);
+		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_settop(gL, 0);
-	return newheight;
 }
 
-// Hook for determining whether players are allowed passage through spin gaps
-UINT8 LUAh_PlayerCanEnterSpinGaps(player_t *player)
+fixed_t LUA_HookPlayerHeight(player_t *player)
 {
-	hook_p hookp;
-	UINT8 canEnter = 0; // 0 = default, 1 = force yes, 2 = force no.
-	if (!gL || !(hooksAvailable[hook_PlayerCanEnterSpinGaps/8] & (1<<(hook_PlayerCanEnterSpinGaps%8))))
-		return 0;
-
-	lua_settop(gL, 0);
-	lua_pushcfunction(gL, LUA_GetErrorMessage);
-
-	for (hookp = playerhooks; hookp; hookp = hookp->next)
+	Hook_State hook;
+	if (prepare_hook(&hook, -1, HOOK(PlayerHeight)))
 	{
-		if (hookp->type != hook_PlayerCanEnterSpinGaps)
-			continue;
-
-		ps_lua_mobjhooks++;
-		if (lua_gettop(gL) == 1)
-			LUA_PushUserdata(gL, player, 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_isnil(gL, -1))
-		{ // if nil, leave canEnter = 0.
-			if (lua_toboolean(gL, -1))
-				canEnter = 1; // Force yes
-			else
-				canEnter = 2; // Force no
-		}
-		lua_pop(gL, 1);
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		call_hooks(&hook, 1, 1, res_playerheight);
 	}
+	return hook.status;
+}
 
-	lua_settop(gL, 0);
-	return canEnter;
+int LUA_HookPlayerCanEnterSpinGaps(player_t *player)
+{
+	Hook_State hook;
+	if (prepare_hook(&hook, 0, HOOK(PlayerCanEnterSpinGaps)))
+	{
+		LUA_PushUserdata(gL, player, META_PLAYER);
+		call_hooks(&hook, 1, 1, res_force);
+	}
+	return hook.status;
 }
diff --git a/src/lua_hud.h b/src/lua_hud.h
index 1e9dca00b921abe7e3c0baa2413fc015668e5fa8..d2f5bceca6f0d56745d2a40c3826ea1e594b9718 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.
@@ -37,7 +37,9 @@ enum hud {
 	hud_tabemblems,
 	// Intermission
 	hud_intermissiontally,
+	hud_intermissiontitletext,
 	hud_intermissionmessages,
+	hud_intermissionemeralds,
 	hud_MAX
 };
 
@@ -49,4 +51,4 @@ 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 LUAh_IntermissionHUD(boolean failedstage);
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index 8d451e99c5100bc2370974ea977ea055fed04595..62b25b74202186b6fc8ad8ced5c18f6e091667ca 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.
@@ -63,7 +63,9 @@ static const char *const hud_disable_options[] = {
 	"tabemblems",
 
 	"intermissiontally",
+	"intermissiontitletext",
 	"intermissionmessages",
+	"intermissionemeralds",
 	NULL};
 
 enum hudinfo {
@@ -278,7 +280,7 @@ static int hudinfo_num(lua_State *L)
 
 static int colormap_get(lua_State *L)
 {
-	const UINT8 *colormap = *((UINT8 **)luaL_checkudata(L, 1, META_COLORMAP));
+	UINT8 *colormap = *((UINT8 **)luaL_checkudata(L, 1, META_COLORMAP));
 	UINT32 i = luaL_checkinteger(L, 2);
 	if (i >= 256)
 		return luaL_error(L, "colormap index %d out of range (0 - %d)", i, 255);
@@ -286,6 +288,23 @@ static int colormap_get(lua_State *L)
 	return 1;
 }
 
+static int colormap_set(lua_State *L)
+{
+	UINT8 *colormap = *((UINT8 **)luaL_checkudata(L, 1, META_COLORMAP));
+	UINT32 i = luaL_checkinteger(L, 2);
+	if (i >= 256)
+		return luaL_error(L, "colormap index %d out of range (0 - %d)", i, 255);
+	colormap[i] = (UINT8)luaL_checkinteger(L, 3);
+	return 0;
+}
+
+static int colormap_free(lua_State *L)
+{
+	UINT8 *colormap = *((UINT8 **)luaL_checkudata(L, 1, META_COLORMAP));
+	Z_Free(colormap);
+	return 0;
+}
+
 static int patch_get(lua_State *L)
 {
 	patch_t *patch = *((patch_t **)luaL_checkudata(L, 1, META_PATCH));
@@ -661,6 +680,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;
@@ -857,6 +915,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);
@@ -886,6 +964,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;
@@ -912,7 +1004,7 @@ static int libd_getColormap(lua_State *L)
 
 	// all was successful above, now we generate the colormap at last!
 
-	colormap = R_GetTranslationColormap(skinnum, color, GTC_CACHE);
+	colormap = R_GetTranslationColormap(skinnum, color, 0);
 	LUA_PushUserdata(L, colormap, META_COLORMAP); // push as META_COLORMAP userdata, specifically for patches to use!
 	return 1;
 }
@@ -921,10 +1013,14 @@ static int libd_getStringColormap(lua_State *L)
 {
 	INT32 flags = luaL_checkinteger(L, 1);
 	UINT8* colormap = NULL;
+	UINT8* lua_colormap = NULL;
 	HUDONLY
 	colormap = V_GetStringColormap(flags & V_CHARCOLORMASK);
 	if (colormap) {
-		LUA_PushUserdata(L, colormap, META_COLORMAP); // push as META_COLORMAP userdata, specifically for patches to use!
+		lua_colormap = Z_Malloc(256 * sizeof(UINT8), PU_LUA, NULL);
+		memcpy(lua_colormap, colormap, 256 * sizeof(UINT8));
+
+		LUA_PushUserdata(L, lua_colormap, META_COLORMAP); // push as META_COLORMAP userdata, specifically for patches to use!
 		return 1;
 	}
 	return 0;
@@ -1085,16 +1181,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},
@@ -1231,6 +1331,12 @@ int LUA_HudLib(lua_State *L)
 	luaL_newmetatable(L, META_COLORMAP);
 		lua_pushcfunction(L, colormap_get);
 		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, colormap_set);
+		lua_setfield(L, -2, "__newindex");
+
+		lua_pushcfunction(L, colormap_free);
+		lua_setfield(L, -2, "__gc");
 	lua_pop(L,1);
 
 	luaL_newmetatable(L, META_PATCH);
@@ -1384,7 +1490,7 @@ void LUAh_TitleCardHUD(player_t *stplayr)
 	hud_running = false;
 }
 
-void LUAh_IntermissionHUD(void)
+void LUAh_IntermissionHUD(boolean failedstage)
 {
 	if (!gL || !(hudAvailable & (1<<hudhook_intermission)))
 		return;
@@ -1402,10 +1508,14 @@ void LUAh_IntermissionHUD(void)
 	lua_rawgeti(gL, -2, 1); // HUD[1] = lib_draw
 	I_Assert(lua_istable(gL, -1));
 	lua_remove(gL, -3); // pop HUD
+
+	lua_pushboolean(gL, failedstage); // stagefailed
 	lua_pushnil(gL);
-	while (lua_next(gL, -3) != 0) {
-		lua_pushvalue(gL, -3); // graphics library (HUD[1])
-		LUA_Call(gL, 1, 0, 1);
+
+	while (lua_next(gL, -4) != 0) {
+		lua_pushvalue(gL, -4); // graphics library (HUD[1])
+		lua_pushvalue(gL, -4); // stagefailed
+		LUA_Call(gL, 2, 0, 1);
 	}
 	lua_settop(gL, 0);
 	hud_running = false;
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index 6e86f47b7272701efb42497a8fde70692681911a..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.
diff --git a/src/lua_inputlib.c b/src/lua_inputlib.c
new file mode 100644
index 0000000000000000000000000000000000000000..71eb1033f7fa8467f54fa9d936665d192051dd8b
--- /dev/null
+++ b/src/lua_inputlib.c
@@ -0,0 +1,242 @@
+// 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"
+
+///////////////
+// 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_keyNumToString(lua_State *L)
+{
+	int i = luaL_checkinteger(L, 1);
+	lua_pushstring(L, G_KeyNumToString(i));
+	return 1;
+}
+
+static int lib_keyStringToNum(lua_State *L)
+{
+	const char *str = luaL_checkstring(L, 1);
+	lua_pushinteger(L, G_KeyStringToNum(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, I_GetMouseGrab());
+	return 1;
+}
+
+static int lib_setMouseGrab(lua_State *L)
+{
+	boolean grab = luaL_checkboolean(L, 1);
+	I_SetMouseGrab(grab);
+	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[] = {
+	{"G_GameControlDown", lib_gameControlDown},
+	{"G_GameControl2Down", lib_gameControl2Down},
+	{"G_GameControlToKeyNum", lib_gameControlToKeyNum},
+	{"G_GameControl2ToKeyNum", lib_gameControl2ToKeyNum},
+	{"G_JoyAxis", lib_joyAxis},
+	{"G_Joy2Axis", lib_joy2Axis},
+	{"G_KeyNumToString", lib_keyNumToString},
+	{"G_KeyStringToNum", lib_keyStringToNum},
+	{"HU_KeyNumPrintable", lib_keyNumPrintable},
+	{"HU_ShiftKeyNum", lib_shiftKeyNum},
+	{"I_GetMouseGrab", lib_getMouseGrab},
+	{"I_SetMouseGrab", lib_setMouseGrab},
+	{"I_GetCursorPosition", lib_getCursorPosition},
+	{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;
+}
+
+///////////
+// 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_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);
+
+	// Set global functions
+	lua_pushvalue(L, LUA_GLOBALSINDEX);
+	luaL_register(L, NULL, lib);
+	return 0;
+}
diff --git a/src/lua_libs.h b/src/lua_libs.h
index fbe8d48780f35009dfb0ca9b8f7209664297017c..668eb99b001f4cb0607351969028392ee2e9cded 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.
@@ -88,6 +88,8 @@ extern lua_State *gL;
 
 #define META_LUABANKS "LUABANKS[]*"
 
+#define META_MOUSE "MOUSE_T*"
+
 boolean luaL_checkboolean(lua_State *L, int narg);
 
 int LUA_EnumLib(lua_State *L);
@@ -106,3 +108,4 @@ 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 0161417960fc30556b1fdf11e112c4a7c6f5215f..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.
diff --git a/src/lua_mathlib.c b/src/lua_mathlib.c
index b6046ab53b2d37948bc6fdb40c540a49d148be57..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.
@@ -16,6 +16,7 @@
 #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"
@@ -87,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
 ////////////////
 
@@ -185,13 +192,14 @@ 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},
@@ -223,9 +231,123 @@ static luaL_Reg lib[] = {
 	{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 65adceb154cb66424c73b3415253891307a9f706..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"
@@ -654,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:
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index 0eb54808fb21a90de8ee5b6aaa3d046eff8cfdd1..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.
@@ -370,6 +370,12 @@ static int player_get(lua_State *L)
 		lua_pushboolean(L, plr->outofcoop);
 	else if (fastcmp(field,"bot"))
 		lua_pushinteger(L, plr->bot);
+	else if (fastcmp(field,"botleader"))
+		LUA_PushUserdata(L, plr->botleader, META_PLAYER);
+	else if (fastcmp(field,"lastbuttons"))
+		lua_pushinteger(L, plr->lastbuttons);
+	else if (fastcmp(field,"blocked"))
+		lua_pushboolean(L, plr->blocked);
 	else if (fastcmp(field,"jointime"))
 		lua_pushinteger(L, plr->jointime);
 	else if (fastcmp(field,"quittime"))
@@ -719,6 +725,17 @@ static int player_set(lua_State *L)
 		plr->outofcoop = lua_toboolean(L, 3);
 	else if (fastcmp(field,"bot"))
 		return NOSET;
+	else if (fastcmp(field,"botleader"))
+	{
+		player_t *player = NULL;
+		if (!lua_isnil(L, 3))
+			player = *((player_t **)luaL_checkudata(L, 3, META_PLAYER));
+		plr->botleader = player;
+	}
+	else if (fastcmp(field,"lastbuttons"))
+		plr->lastbuttons = (UINT16)luaL_checkinteger(L, 3);
+	else if (fastcmp(field,"blocked"))
+		plr->blocked = (UINT8)luaL_checkinteger(L, 3);
 	else if (fastcmp(field,"jointime"))
 		plr->jointime = (tic_t)luaL_checkinteger(L, 3);
 	else if (fastcmp(field,"quittime"))
@@ -795,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)
 {
@@ -813,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;
 
@@ -839,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;
 
@@ -846,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 2a5bcfbf19a54458e63960d5b06c1466f96487de..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.
diff --git a/src/lua_script.c b/src/lua_script.c
index 7fd5a98e6f71620ad4c89eff58e71e222773b622..9eb1912b3209977657b986307e550dd9df2512aa 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
@@ -57,6 +58,7 @@ static lua_CFunction liblist[] = {
 	LUA_PolyObjLib, // polyobj_t
 	LUA_BlockmapLib, // blockmap stuff
 	LUA_HudLib, // HUD stuff
+	LUA_InputLib, // inputs
 	NULL
 };
 
@@ -184,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;
@@ -380,6 +385,14 @@ 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;
 	}
 	return 0;
 }
@@ -429,6 +442,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;
 
@@ -714,27 +729,6 @@ 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)
-{
-	if (data)
-	{
-		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.
@@ -857,6 +851,8 @@ void LUA_InvalidateLevel(void)
 	{
 		LUA_InvalidateUserdata(&lines[i]);
 		LUA_InvalidateUserdata(&lines[i].tags);
+		LUA_InvalidateUserdata(lines[i].args);
+		LUA_InvalidateUserdata(lines[i].stringargs);
 		LUA_InvalidateUserdata(lines[i].sidenum);
 	}
 	for (i = 0; i < numsides; i++)
@@ -869,6 +865,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]);
@@ -891,6 +894,8 @@ void LUA_InvalidateMapthings(void)
 	{
 		LUA_InvalidateUserdata(&mapthings[i]);
 		LUA_InvalidateUserdata(&mapthings[i].tags);
+		LUA_InvalidateUserdata(mapthings[i].args);
+		LUA_InvalidateUserdata(mapthings[i].stringargs);
 	}
 }
 
@@ -934,6 +939,7 @@ enum
 	ARCH_SLOPE,
 	ARCH_MAPHEADER,
 	ARCH_SKINCOLOR,
+	ARCH_MOUSE,
 
 	ARCH_TEND=0xFF,
 };
@@ -961,6 +967,7 @@ static const struct {
 	{META_SLOPE,    ARCH_SLOPE},
 	{META_MAPHEADER,   ARCH_MAPHEADER},
 	{META_SKINCOLOR,   ARCH_SKINCOLOR},
+	{META_MOUSE,    ARCH_MOUSE},
 	{NULL,          ARCH_NULL}
 };
 
@@ -1268,7 +1275,6 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			}
 			break;
 		}
-
 		case ARCH_SKINCOLOR:
 		{
 			skincolor_t *info = *((skincolor_t **)lua_touserdata(gL, myindex));
@@ -1276,6 +1282,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;
@@ -1527,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;
 	}
@@ -1653,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)
@@ -1688,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)
diff --git a/src/lua_script.h b/src/lua_script.h
index 77fbb7c1d125f682db270b7d4f051d6673290d43..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.
@@ -56,10 +56,10 @@ 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,
@@ -87,7 +87,6 @@ typedef enum {
 	LPUSHED_EXISTING,
 } lpushed_t;
 
-void LUA_PushLightUserdata(lua_State *L, void *data, const char *meta);
 void LUA_PushUserdata(lua_State *L, void *data, const char *meta);
 lpushed_t LUA_RawPushUserdata(lua_State *L, void *data);
 
diff --git a/src/lua_skinlib.c b/src/lua_skinlib.c
index 56be6bf4f40504420e7e67cb0c6b60c739a6dfc7..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.
@@ -53,7 +53,6 @@ enum skin {
 	skin_contspeed,
 	skin_contangle,
 	skin_soundsid,
-	skin_availability,
 	skin_sprites
 };
 static const char *const skin_opt[] = {
@@ -91,7 +90,6 @@ static const char *const skin_opt[] = {
 	"contspeed",
 	"contangle",
 	"soundsid",
-	"availability",
 	"sprites",
 	NULL};
 
@@ -209,11 +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);
-		break;
 	case skin_sprites:
-		LUA_PushLightUserdata(L, skin->sprites, META_SKINSPRITES);
+		LUA_PushUserdata(L, skin->sprites, META_SKINSPRITES);
 		break;
 	}
 	return 1;
@@ -336,13 +331,13 @@ static const char *const sprites_opt[] = {
 // skin.sprites[i] -> sprites[i]
 static int lib_getSkinSprite(lua_State *L)
 {
-	spritedef_t *sprites = (spritedef_t *)luaL_checkudata(L, 1, META_SKINSPRITES);
+	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_PushLightUserdata(L, &sprites[i], META_SKINSPRITESLIST);
+	LUA_PushUserdata(L, &sprites[i], META_SKINSPRITESLIST);
 	return 1;
 }
 
@@ -355,7 +350,7 @@ static int lib_numSkinsSprites(lua_State *L)
 
 static int sprite_get(lua_State *L)
 {
-	spritedef_t *sprite = (spritedef_t *)luaL_checkudata(L, 1, META_SKINSPRITESLIST);
+	spritedef_t *sprite = *(spritedef_t **)luaL_checkudata(L, 1, META_SKINSPRITESLIST);
 	enum spritesopt field = luaL_checkoption(L, 2, NULL, sprites_opt);
 
 	switch (field)
diff --git a/src/lua_taglib.c b/src/lua_taglib.c
index c9f320fe8f230524b0b42b10b04022b8d79a5d10..d0cf385a9d89217113d21161df820c5a838e2c01 100644
--- a/src/lua_taglib.c
+++ b/src/lua_taglib.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
-// Copyright (C) 2020 by James R.
-// Copyright (C) 2020 by Sonic Team Junior.
+// 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.
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 41f99254eff59fea3d80f7592168723d9de7a975..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.
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..c958bb4a4de31450276067e730c8c77442b0c800 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.
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 148473378f789e3937e25675b173b450cc3ea274..57371da52b565d621bac7103ad95a91362667a12 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"
 
@@ -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},
@@ -1453,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
 };
@@ -3305,7 +3307,7 @@ boolean M_Responder(event_t *ev)
 		}
 		else if (ev->type == ev_mouse && mousewait < I_GetTime())
 		{
-			pmousey += ev->data3;
+			pmousey -= ev->data3;
 			if (pmousey < lasty-30)
 			{
 				ch = KEY_DOWNARROW;
@@ -4183,7 +4185,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 +5168,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 +5266,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 +5286,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 +5299,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)
 		{
-			const UINT8 actnum = mapheaderinfo[mapnum]->actnum;
-			const boolean headingisname = (fastcmp(mapheaderinfo[mapnum]->selectheading, mapheaderinfo[mapnum]->lvlttl));
-			const boolean wide = (mapheaderinfo[mapnum]->menuflags & LF2_WIDEICON);
+			// Already added under another heading
+			continue;
+		}
+
+		if (M_CanShowLevelOnPlatter(mapIterate, gt) == false)
+		{
+			// 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++;
+				}
 			}
 
-			levelselect.rows[row].maplist[col] = mapnum+1; // putting the map on the platter
-			levelselect.rows[row].mapavailable[col] = M_LevelAvailableOnPlatter(mapnum);
+			if (headerRow == -1)
+			{
+				// Set where the header row is meant to be
+				headerRow = row;
+			}
+
+			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 +5375,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 +5384,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 +5396,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 +5405,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 +5416,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 +6337,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)
 {
@@ -6938,7 +7041,7 @@ static void M_SelectableClearMenus(INT32 choice)
 static void M_UltimateCheat(INT32 choice)
 {
 	(void)choice;
-	LUAh_GameQuit(true);
+	LUA_HookBool(true, HOOK(GameQuit));
 	I_Quit();
 }
 
@@ -8581,7 +8684,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)
@@ -8590,10 +8693,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);
 
@@ -8609,19 +8715,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
 
@@ -8638,54 +8744,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:
@@ -8964,7 +9100,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;
@@ -8996,6 +9132,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;
@@ -12684,13 +12827,13 @@ static void M_DrawControl(void)
 			else
 			{
 				if (keys[0] != KEY_NULL)
-					strcat (tmp, G_KeynumToString (keys[0]));
+					strcat (tmp, G_KeyNumToString (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_KeyNumToString (keys[1]));
 
 
 			}
@@ -12924,7 +13067,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;
@@ -13367,7 +13510,7 @@ void M_QuitResponse(INT32 ch)
 
 	if (ch != 'y' && ch != KEY_ENTER)
 		return;
-	LUAh_GameQuit(true);
+	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 17a398b83f9b26a19d756a5fdfafe8b8a63652c5..4100a8f170fa48f022771abe9982f919cdfe5095 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
@@ -2690,3 +2688,22 @@ const char * M_Ftrim (double f)
 		return &dig[1];/* skip the 0 */
 	}
 }
+
+// Returns true if the string is empty.
+boolean M_IsStringEmpty(const char *s)
+{
+	const char *ch = s;
+
+	if (ch == NULL || (ch && strlen(ch) < 1))
+		return true;
+
+	for (;;ch++)
+	{
+		if (!(*ch))
+			break;
+		if (!isspace((*ch)))
+			return false;
+	}
+
+	return true;
+}
diff --git a/src/m_misc.h b/src/m_misc.h
index c5ef9f9f2548e339ef76e96ae02f835b82dec23c..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.
@@ -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 1596a87e5a841d019513c706b13608259334d7e7..8a99312e6a3d987de4ef6ab7965e31cbdc220458 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.
@@ -62,7 +62,7 @@ int thinkframe_hooks_capacity = 16;
 
 static INT32 draw_row;
 
-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)
 	{
@@ -565,7 +565,7 @@ void M_DrawPerfStats(void)
 				len = (int)strlen(str);
 				if (len > 20)
 					str += len - 20;
-				snprintf(s, sizeof s - 1, "%20s: %u", str, thinkframe_hooks[i].time_taken);
+				snprintf(s, sizeof s - 1, "%20s: %d", str, I_PreciseToMicros(thinkframe_hooks[i].time_taken));
 				V_DrawSmallString(x, y, V_MONOSPACE | V_ALLOWLOWERCASE | text_color, s);
 				y += 4; // repeated code!
 				if (y > 192)
diff --git a/src/m_perfstats.h b/src/m_perfstats.h
index 132bea38c696acaf9a7093cc81f795918452356d..71208fbc19f95d886e7cce17c1982ff61de0089f 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.
@@ -30,11 +30,11 @@ extern int       ps_lua_mobjhooks;
 
 typedef struct
 {
-	UINT32 time_taken;
+	precise_t time_taken;
 	char short_src[LUA_IDSIZE];
 } ps_hookinfo_t;
 
-void PS_SetThinkFrameHookInfo(int index, UINT32 time_taken, char* short_src);
+void PS_SetThinkFrameHookInfo(int index, precise_t time_taken, char* short_src);
 
 void M_DrawPerfStats(void);
 
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..43f3cc1d52400ef4086a09503d4039d9df336c5c 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.
@@ -395,9 +395,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];
 
@@ -617,9 +616,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 59176d6cc8ba44cc31d710ef6a87da801a1e62d8..7db2ace398138b99e71bd2e38c695ad60392bc53 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);
@@ -3590,7 +3591,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)
@@ -3961,7 +3962,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;
@@ -4201,7 +4202,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;
@@ -5101,6 +5102,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.
@@ -5161,23 +5189,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];
@@ -9873,8 +9899,8 @@ 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;
@@ -14320,6 +14346,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)
@@ -14358,7 +14392,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;
@@ -14378,9 +14413,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 7c26065b59b6e9ef0f002bf20907e3eae75856cc..d81a022e50d1b38895fceb5f0d73440410f25450 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]);
 }
 
@@ -1293,9 +1290,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];
 
@@ -1306,14 +1302,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;
@@ -1400,7 +1395,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++)
 	{
@@ -1410,7 +1404,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];
 
@@ -1428,14 +1422,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];
 
@@ -1570,12 +1563,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];
 
@@ -1702,7 +1694,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]);
 }
 
@@ -1820,9 +1812,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];
 
@@ -2037,10 +2028,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];
 
@@ -2337,7 +2327,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)
@@ -2380,7 +2369,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 e9a16a3dd143128a06fa5658cb8ad2d7fb35f4c2..21e3bfa3de4ee75e653ca479e743f3ef5cc0aeeb 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
@@ -470,14 +470,14 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				if (!(player->charability2 == CA2_MELEE && player->panim == PA_ABILITY2))
 				{
 					fixed_t setmomz = -toucher->momz; // Store this, momz get changed by P_DoJump within P_DoBubbleBounce
-
+					
 					if (elementalpierce == 2) // Reset bubblewrap, part 1
 						P_DoBubbleBounce(player);
 					toucher->momz = setmomz;
 					if (elementalpierce == 2) // Reset bubblewrap, part 2
 					{
 						boolean underwater = toucher->eflags & MFE_UNDERWATER;
-
+							
 						if (underwater)
 							toucher->momz /= 2;
 						toucher->momz -= (toucher->momz/(underwater ? 8 : 4)); // Cap the height!
@@ -630,7 +630,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 // ***************************** //
 		// Special Stage Token
 		case MT_TOKEN:
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 
 			P_AddPlayerScore(player, 1000);
@@ -670,7 +670,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 
 		// Emerald Hunt
 		case MT_EMERHUNT:
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 
 			if (hunt1 == special)
@@ -701,7 +701,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 		case MT_EMERALD5:
 		case MT_EMERALD6:
 		case MT_EMERALD7:
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 
 			if (special->threshold)
@@ -738,7 +738,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 		// Secret emblem thingy
 		case MT_EMBLEM:
 			{
-				if (demoplayback || player->bot)
+				if (demoplayback || (player->bot && player->bot != BOT_MPAI))
 					return;
 				emblemlocations[special->health-1].collected = true;
 
@@ -751,7 +751,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 		// CTF Flags
 		case MT_REDFLAG:
 		case MT_BLUEFLAG:
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 			if (player->powers[pw_flashing] || player->tossdelay)
 				return;
@@ -826,7 +826,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			{
 				boolean spec = G_IsSpecialStage(gamemap);
 				boolean cangiveemmy = false;
-				if (player->bot)
+				if (player->bot && player->bot != BOT_MPAI)
 					return;
 				if (player->exiting)
 					return;
@@ -1072,7 +1072,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			return;
 		case MT_EGGCAPSULE:
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 
 			// make sure everything is as it should be, THEN take rings from players in special stages
@@ -1164,7 +1164,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			return;
 		case MT_NIGHTSSUPERLOOP:
-			if (player->bot || !(player->powers[pw_carry] == CR_NIGHTSMODE))
+			if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
 				return;
 			if (!G_IsSpecialStage(gamemap))
 				player->powers[pw_nights_superloop] = (UINT16)special->info->speed;
@@ -1186,7 +1186,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			break;
 		case MT_NIGHTSDRILLREFILL:
-			if (player->bot || !(player->powers[pw_carry] == CR_NIGHTSMODE))
+			if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
 				return;
 			if (!G_IsSpecialStage(gamemap))
 				player->drillmeter = special->info->speed;
@@ -1208,7 +1208,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			break;
 		case MT_NIGHTSHELPER:
-			if (player->bot || !(player->powers[pw_carry] == CR_NIGHTSMODE))
+			if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
 				return;
 			if (!G_IsSpecialStage(gamemap))
 			{
@@ -1240,7 +1240,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			break;
 		case MT_NIGHTSEXTRATIME:
-			if (player->bot || !(player->powers[pw_carry] == CR_NIGHTSMODE))
+			if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
 				return;
 			if (!G_IsSpecialStage(gamemap))
 			{
@@ -1272,7 +1272,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			break;
 		case MT_NIGHTSLINKFREEZE:
-			if (player->bot || !(player->powers[pw_carry] == CR_NIGHTSMODE))
+			if ((player->bot && player->bot != BOT_MPAI) || !(player->powers[pw_carry] == CR_NIGHTSMODE))
 				return;
 			if (!G_IsSpecialStage(gamemap))
 			{
@@ -1332,7 +1332,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 					if (playeringame[i] && players[i].powers[pw_carry] == CR_NIGHTSMODE)
 						players[i].drillmeter += TICRATE/2;
 			}
-			else if (player->bot)
+			else if (player->bot && player->bot != BOT_MPAI)
 				players[consoleplayer].drillmeter += TICRATE/2;
 			else
 				player->drillmeter += TICRATE/2;
@@ -1385,9 +1385,9 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				thinker_t  *th;
 				mobj_t *mo2;
 
-				if (player->bot)
+				if (player->bot && player->bot != BOT_MPAI)
 					return;
-
+					
 				// Initialize my junk
 				junk.tags.tags = NULL;
 				junk.tags.count = 0;
@@ -1423,7 +1423,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			return;
 		}
 		case MT_FIREFLOWER:
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 
 			S_StartSound(toucher, sfx_mario3);
@@ -1617,7 +1617,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 
 				if (special->tracer && !(special->tracer->flags2 & MF2_STRONGBOX))
 					macespin = true;
-
+				
 				if (macespin ? (player->powers[pw_ignorelatch] & (1<<15)) : (player->powers[pw_ignorelatch]))
 					return;
 
@@ -1685,7 +1685,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				return; // Only go in the mouth
 
 			// Eaten by player!
-			if ((!player->bot) && (player->powers[pw_underwater] && player->powers[pw_underwater] <= 12*TICRATE + 1))
+			if ((!player->bot || player->bot == BOT_MPAI) && (player->powers[pw_underwater] && player->powers[pw_underwater] <= 12*TICRATE + 1))
 			{
 				player->powers[pw_underwater] = underwatertics + 1;
 				P_RestoreMusic(player);
@@ -1696,7 +1696,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 
 			if (!player->climbing)
 			{
-				if (player->bot && toucher->state-states != S_PLAY_GASP)
+				if (player->bot && player->bot != BOT_MPAI && toucher->state-states != S_PLAY_GASP)
 					S_StartSound(toucher, special->info->deathsound); // Force it to play a sound for bots
 				P_SetPlayerMobjState(toucher, S_PLAY_GASP);
 				P_ResetPlayer(player);
@@ -1704,7 +1704,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 
 			toucher->momx = toucher->momy = toucher->momz = 0;
 
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 			else
 				break;
@@ -1736,7 +1736,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			return;
 
 		case MT_MINECARTSPAWNER:
-			if (!player->bot && special->fuse <= TICRATE && player->powers[pw_carry] != CR_MINECART && !(player->powers[pw_ignorelatch] & (1<<15)))
+			if (!player->bot && player->bot != BOT_MPAI && special->fuse <= TICRATE && player->powers[pw_carry] != CR_MINECART && !(player->powers[pw_ignorelatch] & (1<<15)))
 			{
 				mobj_t *mcart = P_SpawnMobj(special->x, special->y, special->z, MT_MINECART);
 				P_SetTarget(&mcart->target, toucher);
@@ -1789,7 +1789,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			}
 			return;
 		default: // SOC or script pickup
-			if (player->bot)
+			if (player->bot && player->bot != BOT_MPAI)
 				return;
 			P_SetTarget(&special->target, toucher);
 			break;
@@ -1813,7 +1813,7 @@ void P_TouchStarPost(mobj_t *post, player_t *player, boolean snaptopost)
 	mobj_t *toucher = player->mo;
 	mobj_t *checkbase = snaptopost ? post : toucher;
 
-	if (player->bot)
+	if (player->bot && player->bot != BOT_MPAI)
 		return;
 	// In circuit, player must have touched all previous starposts
 	if (circuitmap
@@ -1939,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);
@@ -2413,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
@@ -2555,7 +2555,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 
 		if ((target->player->lives <= 1) && (netgame || multiplayer) && G_GametypeUsesCoopLives() && (cv_cooplives.value == 0))
 			;
-		else if (!target->player->bot && !target->player->spectator && (target->player->lives != INFLIVES)
+		else if ((!target->player->bot || target->player->bot == BOT_MPAI) && !target->player->spectator && (target->player->lives != INFLIVES)
 		 && G_GametypeUsesLives())
 		{
 			if (!(target->player->pflags & PF_FINISHED))
@@ -3475,7 +3475,7 @@ void P_SpecialStageDamage(player_t *player, mobj_t *inflictor, mobj_t *source)
 	if (inflictor && inflictor->type == MT_LHRT)
 		return;
 
-	if (player->powers[pw_shield] || player->bot)  //If One-Hit Shield
+	if (player->powers[pw_shield] || (player->bot && player->bot != BOT_MPAI))  //If One-Hit Shield
 	{
 		P_RemoveShield(player);
 		S_StartSound(player->mo, sfx_shldls); // Ba-Dum! Shield loss.
@@ -3548,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)
@@ -3566,7 +3566,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 			return false;
 
 		// Make sure that boxes cannot be popped by enemies, red rings, etc.
-		if (target->flags & MF_MONITOR && ((!source || !source->player || source->player->bot)
+		if (target->flags & MF_MONITOR && ((!source || !source->player || (source->player->bot && source->player->bot != BOT_MPAI))
 		|| (inflictor && (inflictor->type == MT_REDRING || (inflictor->type >= MT_THROWNBOUNCE && inflictor->type <= MT_THROWNGRENADE)))))
 			return false;
 	}
@@ -3589,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)
@@ -3639,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;
@@ -3693,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 8568dd4f8c2d58481c1b15e7b5a9e65a864f77bc..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.
diff --git a/src/p_map.c b/src/p_map.c
index bf668ba3e8562db44f524066553350306f206ae1..e55bebb9a74d9a3428b270398570373b593d8fc8 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)
@@ -746,7 +754,7 @@ 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;
@@ -935,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)
@@ -943,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)
@@ -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)
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 49db6daee9916632abb3a9fedb3c18a6f6871e82..6e5ccdea4dbeae9a16b1fc1a9cbced5875333955 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.
@@ -1839,12 +1839,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_HookMobj(mo, MOBJ_HOOK(MobjMoveBlocked)))
 		{
 			if (P_MobjWasRemoved(mo))
 				return;
@@ -2549,6 +2547,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;
@@ -3286,11 +3288,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;
 			}
 		}
@@ -4138,7 +4138,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))
@@ -4179,7 +4179,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)
@@ -4607,9 +4607,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;
@@ -4693,9 +4692,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];
 
@@ -7310,7 +7308,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;
@@ -7512,7 +7510,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;
@@ -7860,7 +7858,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;
 		}
@@ -7919,7 +7917,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;
 			}
@@ -7948,7 +7946,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;
@@ -9875,7 +9873,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)
 	{
@@ -10051,13 +10049,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;
 	}
@@ -10391,6 +10389,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:
@@ -10525,7 +10526,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;
@@ -10912,7 +10913,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!
@@ -11055,7 +11056,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
@@ -11101,22 +11102,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);
-	}
 }
 
 //
@@ -11964,6 +11957,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)
 	{
@@ -11988,8 +11982,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;
@@ -12562,7 +12567,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..82cd056a876d429d8cc58b1bd5ec0e7f99185644 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.
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 03229e740bc0b5cddc3940ee8df0b96d1843347d..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 //
 		////////////////////////////
@@ -4190,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);
@@ -4268,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 40dd1a28477a50c3216372314facfd5efdab8984..e10ba774e65ce28c5a302329b5fa88bdfc6d31e9 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;
@@ -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)
 				{
@@ -3141,6 +3281,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%)
@@ -3183,11 +3351,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 +3453,9 @@ static boolean P_LoadMapFromFile(void)
 
 	P_LinkMapData();
 
+	if (!udmf)
+		P_AddBinaryMapTags();
+
 	Taglist_InitGlobalTables();
 
 	if (!udmf)
@@ -3394,8 +3563,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));
 
@@ -4174,7 +4345,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 +4400,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 +4419,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 +4444,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 +4560,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 +4583,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++)
@@ -4527,8 +4693,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 +4750,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 5d13ae7d429d94d5c1fed55b08ff275fe3d3b353..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.
@@ -103,6 +103,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate);
 void HWR_LoadLevel(void);
 #endif
 boolean P_AddWadFile(const char *wadfilename);
+boolean P_AddFolder(const char *folderpath);
 boolean P_RunSOC(const char *socfilename);
 void P_LoadSoundsRange(UINT16 wadnum, UINT16 first, UINT16 num);
 void P_LoadMusicsRange(UINT16 wadnum, UINT16 first, UINT16 num);
diff --git a/src/p_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..41cfbf3373dac2494addbd35ce7f14aab4012030 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();
 
@@ -672,6 +708,13 @@ void P_SpawnSlopes(const boolean fromsave) {
 		}
 }
 
+/// Initializes slopes.
+void P_InitSlopes(void)
+{
+	slopelist = NULL;
+	slopecount = 0;
+}
+
 // ============================================================================
 //
 // Various utilities related to slopes
@@ -774,13 +817,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 226e58d154423037c980835f61447a2ca8bb500d..90dca25afaa6528649c58b31270a05b58f05f490 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
 
@@ -1988,53 +1988,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;
+	boolean raintype = (PRECIP_SNOW || PRECIP_RAIN || PRECIP_STORM || PRECIP_STORM_NOSTRIKES || PRECIP_BLANK);
 
-	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 (weathernum == raintype && curWeather == raintype)
+		purge = false;
 
 	if (purge)
 	{
@@ -2051,7 +2012,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 +2024,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 || PRECIP_STORM || PRECIP_STORM_NOSTRIKES)) // Snow To Rain
 			{
 				precipmobj->flags = mobjinfo[MT_RAIN].flags;
 				st = &states[mobjinfo[MT_RAIN].spawnstate];
@@ -2078,7 +2039,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 +2064,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 +2077,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 +2112,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 +2166,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 +2193,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 +2248,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 +2258,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 +2443,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 +2558,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 +2592,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 +2626,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 +2660,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 +2708,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 +2922,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 +3047,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 +3077,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 +3107,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 +3165,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 +3221,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 +3356,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 +3420,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 +3505,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 +3556,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 +3636,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 +3829,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 +3843,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);
@@ -5928,9 +5884,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)
@@ -6210,11 +6165,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!
 
@@ -6238,7 +6192,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;
 
@@ -6265,7 +6219,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);
 					}
 				}
@@ -6276,7 +6230,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)
 					{
@@ -6303,7 +6257,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;
 
@@ -6364,19 +6318,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;
@@ -6387,15 +6341,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;
 
@@ -6991,46 +6945,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;
 
@@ -7104,13 +7058,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);
 }
 
@@ -7220,7 +7173,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;
@@ -7257,7 +7209,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;
@@ -7332,7 +7284,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;
@@ -7472,11 +7424,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;
@@ -7485,13 +7436,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;
@@ -7500,7 +7451,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;
 
@@ -7508,7 +7459,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
@@ -7610,9 +7561,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)
 			{
@@ -8343,7 +8293,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)
@@ -8369,7 +8318,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);
 		}
 }
@@ -8458,6 +8407,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))
 	{
@@ -8888,7 +8840,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++)
 	{
@@ -8896,15 +8847,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
@@ -8912,19 +8863,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 c0a1c5700727eecd19ce386d7ea0e708cb28a8bb..d7357eb828508b49cc60b9e6d80beedab282982c 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.
@@ -656,7 +656,7 @@ void P_Ticker(boolean run)
 		ps_lua_mobjhooks = 0;
 		ps_checkposition_calls = 0;
 
-		LUAh_PreThinkFrame();
+		LUA_HOOK(PreThinkFrame);
 
 		ps_playerthink_time = I_GetPreciseTime();
 		for (i = 0; i < MAXPLAYERS; i++)
@@ -687,7 +687,7 @@ void P_Ticker(boolean run)
 				P_PlayerAfterThink(&players[i]);
 
 		ps_lua_thinkframe_time = I_GetPreciseTime();
-		LUAh_ThinkFrame();
+		LUA_HookThinkFrame();
 		ps_lua_thinkframe_time = I_GetPreciseTime() - ps_lua_thinkframe_time;
 	}
 
@@ -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 4413cc6cdfe583ddf18c3ada79ed8c4a6a39bc66..198db44066bd08ab5bc9f3d9b01c2d612cf85005 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.
@@ -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)
@@ -1111,7 +1111,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 +1188,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 +1234,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 +1261,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)
 	{
@@ -1367,8 +1367,8 @@ void P_AddPlayerScore(player_t *player, UINT32 amount)
 {
 	UINT32 oldscore;
 
-	if (player->bot)
-		player = &players[consoleplayer];
+	if ((player->bot == BOT_2PAI || player->bot == BOT_2PHUMAN) && player->botleader)
+		player = player->botleader;
 
 	// NiGHTS does it different!
 	if (gamestate == GS_LEVEL && mapheaderinfo[gamemap-1]->typeoflevel & TOL_NIGHTS)
@@ -1594,7 +1594,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 +1860,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 +2016,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;
 
@@ -4581,7 +4583,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;
 	}
 
@@ -5053,7 +5055,7 @@ static boolean P_PlayerShieldThink(player_t *player, ticcmd_t *cmd, mobj_t *lock
 				}
 			}
 		}
-		if ((!(player->charflags & SF_NOSHIELDABILITY)) && (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)
@@ -5177,7 +5179,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:
@@ -5250,7 +5252,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)
@@ -5285,7 +5287,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:
@@ -5299,7 +5301,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;
@@ -5376,7 +5378,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);
@@ -5478,7 +5480,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:
@@ -5501,7 +5503,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 						break;
 				}
 		}
-		else if ((!(player->charflags & SF_NOSHIELDABILITY)) && ((player->powers[pw_shield] & SH_NOSTACK) == SH_WHIRLWIND && !player->powers[pw_super] && !LUAh_ShieldSpecial(player)))
+		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);
 	}
 
@@ -5963,22 +5965,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])
@@ -8651,7 +8637,7 @@ void P_MovePlayer(player_t *player)
 	{
 		boolean atspinheight = false;
 		fixed_t oldheight = player->mo->height;
-		fixed_t luaheight = LUAh_PlayerHeight(player);
+		fixed_t luaheight = LUA_HookPlayerHeight(player);
 
 		if (luaheight != -1)
 		{
@@ -9017,8 +9003,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);
@@ -9505,11 +9494,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();
@@ -10510,7 +10499,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)
@@ -10527,7 +10516,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;
 		}
 
@@ -10545,7 +10534,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)
 			{
@@ -10572,7 +10561,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;
 			}
 
@@ -11363,6 +11352,7 @@ 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 = min(player->dashmode, DASHMODE_MAX);
 	boolean underwater = mo->eflags & MFE_UNDERWATER;
@@ -11396,7 +11386,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;
 			}
 
@@ -11459,7 +11449,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
@@ -11481,6 +11471,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!
 	{
@@ -11488,7 +11481,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)
 		{
@@ -11497,7 +11490,7 @@ void P_PlayerThink(player_t *player)
 		}
 		if (player->playerstate == PST_REBORN)
 		{
-			LUAh_PlayerThink(player);
+			LUA_HookPlayer(player, HOOK(PlayerThink));
 			return;
 		}
 	}
@@ -11599,7 +11592,7 @@ void P_PlayerThink(player_t *player)
 
 			if (player->playerstate == PST_DEAD)
 			{
-				LUAh_PlayerThink(player);
+				LUA_HookPlayer(player, HOOK(PlayerThink));
 				return;
 			}
 		}
@@ -11632,7 +11625,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;
@@ -11663,8 +11656,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;
@@ -11720,7 +11713,7 @@ void P_PlayerThink(player_t *player)
 	{
 		player->mo->flags2 &= ~MF2_SHADOW;
 		P_DeathThink(player);
-		LUAh_PlayerThink(player);
+		LUA_HookPlayer(player, HOOK(PlayerThink));
 		return;
 	}
 
@@ -11762,7 +11755,7 @@ void P_PlayerThink(player_t *player)
 	{
 		if (P_SpectatorJoinGame(player))
 		{
-			LUAh_PlayerThink(player);
+			LUA_HookPlayer(player, HOOK(PlayerThink));
 			return; // player->mo was removed.
 		}
 	}
@@ -11867,7 +11860,7 @@ void P_PlayerThink(player_t *player)
 
 	if (!player->mo)
 	{
-		LUAh_PlayerThink(player);
+		LUA_HookPlayer(player, HOOK(PlayerThink));
 		return; // P_MovePlayer removed player->mo.
 	}
 
@@ -12321,7 +12314,7 @@ void P_PlayerThink(player_t *player)
 	}
 #undef dashmode
 
-	LUAh_PlayerThink(player);
+	LUA_HookPlayer(player, HOOK(PlayerThink));
 
 /*
 //	Colormap verification
@@ -12604,8 +12597,8 @@ void P_PlayerAfterThink(player_t *player)
 					player->mo->momy = tails->momy;
 					player->mo->momz = tails->momz;
 				}
-
-				if (G_CoopGametype() && tails->player && tails->player->bot != 1)
+				
+				if (G_CoopGametype() && tails->player && tails->player->bot != BOT_2PAI)
 				{
 					player->mo->angle = tails->angle;
 
@@ -12616,14 +12609,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->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)
@@ -12726,9 +12719,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;
@@ -12744,7 +12743,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
@@ -12884,7 +12883,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
 			{
@@ -12963,25 +12962,29 @@ boolean P_PlayerFullbright(player_t *player)
 // 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 = LUAh_PlayerCanEnterSpinGaps(player);
+	UINT8 canEnter = LUA_HookPlayerCanEnterSpinGaps(player);
 	if (canEnter == 1)
 		return true;
 	else if (canEnter == 2)
 		return false;
 
-	return ((player->pflags & (PF_SPINNING|PF_GLIDING)) // players who are spinning or gliding
+	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_GLIDING))
+	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..5acd4e70c5fa083eaf675055be9d623e15f014e3 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.
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.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 8625fbab2d8374dddf6629df536bda75f10b4c47..f0a19a462848d02c54b07a8a481f11e0969ebef0 100644
--- a/src/r_draw.c
+++ b/src/r_draw.c
@@ -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"
@@ -274,7 +275,7 @@ static void BlendTab_Modulative(UINT8 *table)
 
 static INT32 BlendTab_Count[NUMBLENDMAPS] =
 {
-	NUMTRANSTABLES,   // blendtab_add
+	NUMTRANSTABLES+1, // blendtab_add
 	NUMTRANSTABLES+1, // blendtab_subtract
 	NUMTRANSTABLES+1, // blendtab_reversesubtract
 	1                 // blendtab_modulate
@@ -294,7 +295,7 @@ static INT32 BlendTab_FromStyle[] =
 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));
+	const float amtmul = (256.0f / (float)(NUMTRANSTABLES + 1));
 	for (; i < num; i++)
 	{
 		const size_t offs = (0x10000 * i);
@@ -341,7 +342,7 @@ UINT8 *R_GetBlendTable(int style, INT32 alphalevel)
 {
 	size_t offs;
 
-	if (style == AST_COPY || style == AST_OVERLAY)
+	if (style <= AST_COPY || style >= AST_OVERLAY)
 		return NULL;
 
 	offs = (ClipBlendLevel(style, alphalevel) << FF_TRANSSHIFT);
@@ -371,7 +372,7 @@ UINT8 *R_GetBlendTable(int style, INT32 alphalevel)
 
 boolean R_BlendLevelVisible(INT32 blendmode, INT32 alphalevel)
 {
-	if (blendmode == AST_COPY || blendmode == AST_SUBTRACT || blendmode == AST_MODULATE || blendmode == AST_OVERLAY)
+	if (blendmode <= AST_COPY || blendmode == AST_SUBTRACT || blendmode == AST_MODULATE || blendmode >= AST_OVERLAY)
 		return true;
 
 	return (alphalevel < BlendTab_Count[BlendTab_FromStyle[blendmode]]);
@@ -481,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)
 		{
diff --git a/src/r_draw.h b/src/r_draw.h
index caf43fd89d7ba5451eed376ef08591c71a25efa8..2173c7a5a36e5c9b92063657aa833dbb5b457726 100644
--- a/src/r_draw.h
+++ b/src/r_draw.h
@@ -177,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 1f451115eba2108c59b6188232d5b88bd3aacb74..b8a63d5c042d7ce49f8480bac269b763c43f5dd6 100644
--- a/src/r_draw8.c
+++ b/src/r_draw8.c
@@ -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];
@@ -1672,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--)
 		{
@@ -1712,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--)
 			{
@@ -1781,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--)
 		{
@@ -1821,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..71ec999486497c562214620fcd0510219daf2ce0 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,8 +660,8 @@ 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--)
 			{
@@ -649,17 +669,18 @@ void R_DrawTiltedSplat_NPO2_8(void)
 				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 +1023,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 +1061,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 +1091,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 +1173,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 +1211,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 +1241,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 +1422,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 +1453,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 +1501,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 +1541,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 +1570,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 04bdebc58282642cdca3329e9b3f801c10f4c6c3..17e124cb9271c714a115e37705ee6ec11ed60faa 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.
@@ -955,7 +955,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);
 		}
diff --git a/src/r_main.h b/src/r_main.h
index 2ac6abf5a1e45ca7b0062a1e663e221ef9825931..f81447c456b8866cf9a0c1347c32a20ca7b7f057 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.
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..a9b4a2b8fb701d45f7f21deefee1f75c768ee797 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.
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 f87362c76ef895097b7e21329367c6b082fa705e..59b1d16c5bdadbf0de6454f40f87815aee19582d 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,9 +540,7 @@ 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;
@@ -566,30 +564,36 @@ void *Picture_GetPatchPixel(
 
 		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);
@@ -979,8 +983,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;
@@ -996,12 +1000,12 @@ static png_bytep *PNG_Read(
 		{
 			png_get_tRNS(png_ptr, png_info_ptr, &trans, &trans_num, &trans_values);
 
-			if (trans && trans_num == 256)
+			if (trans && trans_num > 0)
 			{
 				INT32 i;
 				for (i = 0; i < trans_num; 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 ea4dfa4e8a8c13d4aad4ed8260d6da37e8fea00a..818770906956b2b0ed9ce172888ceb28e31c9077 100644
--- a/src/r_plane.c
+++ b/src/r_plane.c
@@ -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
 //
@@ -104,6 +97,7 @@ 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
@@ -127,21 +121,20 @@ struct
 	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)
@@ -159,7 +152,7 @@ static void R_UpdatePlaneRipple(void)
 //  baseyscale
 //  centerx
 
-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;
@@ -173,60 +166,50 @@ 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 = FixedMul(distance, basexscale);
+			ds_ystep = FixedMul(distance, baseyscale);
 		}
 
-		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];
 	}
 
+	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;
@@ -234,16 +217,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);
 
@@ -251,19 +229,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)
@@ -363,11 +368,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));
 		}
 	}
 
@@ -375,9 +380,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
 		{
@@ -571,10 +578,7 @@ void R_ExpandPlane(visplane_t *pl, INT32 start, INT32 stop)
 
 }
 
-//
-// R_MakeSpans
-//
-void R_MakeSpans(INT32 x, INT32 t1, INT32 b1, INT32 t2, INT32 b2)
+static void R_MakeSpans(INT32 x, INT32 t1, INT32 b1, INT32 t2, INT32 b2)
 {
 	//    Alam: from r_splats's R_RasterizeFloorSplat
 	if (t1 >= vid.height) t1 = vid.height-1;
@@ -600,6 +604,32 @@ void R_MakeSpans(INT32 x, INT32 t1, INT32 b1, INT32 t2, INT32 b2)
 		spanstart[b2--] = x;
 }
 
+static void R_MakeTiltedSpans(INT32 x, INT32 t1, INT32 b1, INT32 t2, INT32 b2)
+{
+	//    Alam: from r_splats's R_RasterizeFloorSplat
+	if (t1 >= vid.height) t1 = vid.height-1;
+	if (b1 >= vid.height) b1 = vid.height-1;
+	if (t2 >= vid.height) t2 = vid.height-1;
+	if (b2 >= vid.height) b2 = vid.height-1;
+	if (x-1 >= vid.width) x = vid.width;
+
+	while (t1 < t2 && t1 <= b1)
+	{
+		R_MapTiltedPlane(t1, spanstart[t1], x - 1);
+		t1++;
+	}
+	while (b1 > b2 && b1 >= t1)
+	{
+		R_MapTiltedPlane(b1, spanstart[b1], x - 1);
+		b1--;
+	}
+
+	while (t2 < t1 && t2 <= b2)
+		spanstart[t2++] = x;
+	while (b2 > b1 && b2 >= t2)
+		spanstart[b2--] = x;
+}
+
 void R_DrawPlanes(void)
 {
 	visplane_t *pl;
@@ -662,69 +692,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);
 
-	vx = FIXED_TO_FLOAT(planeviewx+planexoffset);
-	vy = FIXED_TO_FLOAT(planeviewy-planeyoffset);
-	vz = FIXED_TO_FLOAT(planeviewz);
+	x64 = (x64 * (INT64)slope->d.x) / FRACUNIT;
+	y64 = (y64 * (INT64)slope->d.y) / FRACUNIT;
 
-	temp = P_GetSlopeZAt(slope, planeviewx, planeviewy);
-	zeroheight = FIXED_TO_FLOAT(temp);
+	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;
+
+	INT64 vx = (INT64)xpos + (INT64)xoff;
+	INT64 vy = (INT64)ypos - (INT64)yoff;
+
+	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 + FLOAT_TO_FIXED(yscale * sin(ang)), planeviewy + FLOAT_TO_FIXED(yscale * cos(ang)));
-	m.y = FIXED_TO_FLOAT(temp) - zeroheight;
-	temp = P_GetSlopeZAt(slope, planeviewx + FLOAT_TO_FIXED(xscale * cos(ang)), planeviewy - FLOAT_TO_FIXED(xscale * 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;
@@ -732,27 +802,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)
@@ -769,10 +827,40 @@ 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)
@@ -782,8 +870,8 @@ void R_DrawSinglePlane(visplane_t *pl)
 	INT32 x;
 	INT32 stop, angle;
 	ffloor_t *rover;
-	int type;
-	int spanfunctype = BASEDRAWFUNC;
+	INT32 type;
+	INT32 spanfunctype = BASEDRAWFUNC;
 
 	if (!(pl->minx <= pl->maxx))
 		return;
@@ -943,7 +1031,6 @@ void R_DrawSinglePlane(visplane_t *pl)
 
 	xoffs = pl->xoffs;
 	yoffs = pl->yoffs;
-	planeheight = abs(pl->height - pl->viewz);
 
 	if (light >= LIGHTLEVELS)
 		light = LIGHTLEVELS-1;
@@ -953,76 +1040,29 @@ void R_DrawSinglePlane(visplane_t *pl)
 
 	if (pl->slope)
 	{
-		float fudgecanyon = 0;
-		angle_t hack = (pl->plangle & (ANGLE_90-1));
-
-		yoffs *= 1;
-
-		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)
 		{
@@ -1043,7 +1083,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)
@@ -1064,18 +1107,15 @@ void R_DrawSinglePlane(visplane_t *pl)
 
 	stop = pl->maxx + 1;
 
-	if (viewx != pl->viewx || viewy != pl->viewy)
+	if (pl->slope)
 	{
-		viewx = pl->viewx;
-		viewy = pl->viewy;
+		for (x = pl->minx; x <= stop; x++)
+			R_MakeTiltedSpans(x, pl->top[x-1], pl->bottom[x-1], pl->top[x], pl->bottom[x]);
 	}
-	if (viewz != pl->viewz)
-		viewz = pl->viewz;
-
-	for (x = pl->minx; x <= stop; x++)
+	else
 	{
-		R_MakeSpans(x, pl->top[x-1], pl->bottom[x-1],
-			pl->top[x], pl->bottom[x]);
+		for (x = pl->minx; x <= stop; x++)
+			R_MakeSpans(x, pl->top[x-1], pl->bottom[x-1], pl->top[x], pl->bottom[x]);
 	}
 
 /*
diff --git a/src/r_plane.h b/src/r_plane.h
index 0d11c5b721c2ffadcaee26f4fbd830a6b2698c0a..bdad77930af31d3448ea98208e128dfa25a99502 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.
@@ -78,8 +78,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 +92,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 a6772f9646af9451e4cdeb2f2cdb9f7b4bdd2749..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.
@@ -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;
 		}
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 6f150f234ed9908f9c58bd42ecbe65ac5e5a534a..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));
 }
 
 //
@@ -558,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;
@@ -673,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);
 
@@ -693,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;
@@ -713,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;
@@ -826,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 72cac9fd937f6bc35e98a343bb4bf98c4a6a4b8a..4783fb6408d560d7dc9578a09e09d6350227fdb5 100644
--- a/src/r_splats.c
+++ b/src/r_splats.c
@@ -419,7 +419,8 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 	if (pSplat->tilted)
 	{
 		R_SetTiltedSpan(0);
-		R_CalculateSlopeVectors(&pSplat->slope, viewx, viewy, viewz, pSplat->xscale, pSplat->yscale, -pSplat->verts[0].x, pSplat->verts[0].y, vis->viewangle, pSplat->angle, 1.0f);
+		R_SetScaledSlopePlane(&pSplat->slope, viewx, viewy, viewz, pSplat->xscale, pSplat->yscale, -pSplat->verts[0].x, pSplat->verts[0].y, vis->viewangle, pSplat->angle);
+		R_CalculateSlopeVectors();
 		spanfunctype = SPANDRAWFUNC_TILTEDSPRITE;
 	}
 	else
diff --git a/src/r_splats.h b/src/r_splats.h
index 05d8b66b06a8b04f569ae1739861660d425df92c..cab3d63b66bf8f2bca5324dcd2f677253e7dee7d 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.
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 d5da69018cb39c81a1dcac1fe01736ee67abab65..793e5237f62e64e937c32f97d4d01bbb08a6c065 100644
--- a/src/r_textures.c
+++ b/src/r_textures.c
@@ -727,7 +727,7 @@ Rloadflats (INT32 i, INT32 w)
 	texpatch_t *patch;
 
 	// Yes
-	if (wadfiles[w]->type == RET_PK3)
+	if (W_FileHasFolders(wadfiles[w]))
 	{
 		texstart = W_CheckNumForFolderStartPK3("flats/", (UINT16)w, 0);
 		texend = W_CheckNumForFolderEndPK3("flats/", (UINT16)w, texstart);
@@ -749,7 +749,7 @@ Rloadflats (INT32 i, INT32 w)
 			size_t lumplength;
 			size_t flatsize = 0;
 
-			if (wadfiles[w]->type == RET_PK3)
+			if (W_FileHasFolders(wadfiles[w]))
 			{
 				if (W_IsLumpFolder(wadnum, lumpnum)) // Check if lump is a folder
 					continue; // If it is then SKIP IT
@@ -839,7 +839,7 @@ Rloadtextures (INT32 i, INT32 w)
 	texpatch_t *patch;
 
 	// Get the lump numbers for the markers in the WAD, if they exist.
-	if (wadfiles[w]->type == RET_PK3)
+	if (W_FileHasFolders(wadfiles[w]))
 	{
 		texstart = W_CheckNumForFolderStartPK3("textures/", (UINT16)w, 0);
 		texend = W_CheckNumForFolderEndPK3("textures/", (UINT16)w, texstart);
@@ -870,7 +870,7 @@ Rloadtextures (INT32 i, INT32 w)
 			size_t lumplength;
 #endif
 
-			if (wadfiles[w]->type == RET_PK3)
+			if (W_FileHasFolders(wadfiles[w]))
 			{
 				if (W_IsLumpFolder(wadnum, lumpnum)) // Check if lump is a folder
 					continue; // If it is then SKIP IT
@@ -959,7 +959,7 @@ void R_LoadTextures(void)
 	{
 #ifdef WALLFLATS
 		// Count flats
-		if (wadfiles[w]->type == RET_PK3)
+		if (W_FileHasFolders(wadfiles[w]))
 		{
 			texstart = W_CheckNumForFolderStartPK3("flats/", (UINT16)w, 0);
 			texend = W_CheckNumForFolderEndPK3("flats/", (UINT16)w, texstart);
@@ -973,7 +973,7 @@ void R_LoadTextures(void)
 		if (!( texstart == INT16_MAX || texend == INT16_MAX ))
 		{
 			// PK3s have subfolders, so we can't just make a simple sum
-			if (wadfiles[w]->type == RET_PK3)
+			if (W_FileHasFolders(wadfiles[w]))
 			{
 				for (j = texstart; j < texend; j++)
 				{
@@ -997,7 +997,7 @@ void R_LoadTextures(void)
 		}
 
 		// Count single-patch textures
-		if (wadfiles[w]->type == RET_PK3)
+		if (W_FileHasFolders(wadfiles[w]))
 		{
 			texstart = W_CheckNumForFolderStartPK3("textures/", (UINT16)w, 0);
 			texend = W_CheckNumForFolderEndPK3("textures/", (UINT16)w, texstart);
@@ -1012,7 +1012,7 @@ void R_LoadTextures(void)
 			continue;
 
 		// PK3s have subfolders, so we can't just make a simple sum
-		if (wadfiles[w]->type == RET_PK3)
+		if (W_FileHasFolders(wadfiles[w]))
 		{
 			for (j = texstart; j < texend; j++)
 			{
@@ -1553,6 +1553,7 @@ lumpnum_t R_GetFlatNumForName(const char *name)
 					continue;
 			break;
 		case RET_PK3:
+		case RET_FOLDER:
 			if ((start = W_CheckNumForFolderStartPK3("Flats/", i, 0)) == INT16_MAX)
 				continue;
 			if ((end = W_CheckNumForFolderEndPK3("Flats/", i, start)) == INT16_MAX)
diff --git a/src/r_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 a7c44c237539bed2e80bf1b7a355de03f1836989..b636a970184a6d4acc595c0a807f967288257999 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -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)
 	{
diff --git a/src/r_things.h b/src/r_things.h
index 95b4215afa132ede0a11346036a3fffb4d6e7da9..9315b36e946c35020d88a78049e18a284d9a3790 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.
diff --git a/src/s_sound.c b/src/s_sound.c
index 392a5b45328abd8c9667e745050ce2aa732d000a..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;
@@ -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;
 
@@ -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 d37724390dfa2148fe31c863a72c32a55552e596..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.
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/i_system.c b/src/sdl/i_system.c
index a0dd6e1da707a1e521f53623d2de42b01e6f7dfa..d68e3e435bb6c749b8e770e0cc604aba96206185 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
@@ -102,7 +102,7 @@ typedef LPVOID (WINAPI *p_MapViewOfFile) (HANDLE, DWORD, DWORD, DWORD, SIZE_T);
 #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
@@ -1969,7 +1969,7 @@ void I_GetMouseEvents(void)
 		event.data1 = 0;
 //		event.data1 = buttons; // not needed
 		event.data2 = handlermouse2x << 1;
-		event.data3 = -handlermouse2y << 1;
+		event.data3 = handlermouse2y << 1;
 		handlermouse2x = 0;
 		handlermouse2y = 0;
 
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 0ed10463fc79734e004abdfdb960d56630168abf..819589eaff1a5d856da1cf76d5aaaf47352f8ab1 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
@@ -405,6 +405,19 @@ void I_UpdateMouseGrab(void)
 		SDLdoGrabMouse();
 }
 
+boolean I_GetMouseGrab(void)
+{
+	return (boolean)SDL_GetWindowGrab(window);
+}
+
+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());
@@ -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;
@@ -1057,7 +1070,7 @@ void I_GetEvent(void)
 					M_SetupJoystickMenu(0);
 			 	break;
 			case SDL_QUIT:
-				LUAh_GameQuit(true);
+				LUA_HookBool(true, HOOK(GameQuit));
 				I_Quit();
 				break;
 		}
@@ -1938,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 412a21ea0098415b46eea3565630f6f9ce99bfe8..2f1a872669443c033d882940fafd43689518265e 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.
@@ -1301,7 +1301,7 @@ boolean I_PlaySong(boolean looping)
 #if defined (GME_VERSION) && GME_VERSION >= 0x000603
 		if (looping)
 			gme_set_autoload_playback_limit(gme, 0);
-#endif        
+#endif
 		gme_set_equalizer(gme, &eq);
 		gme_start_track(gme, 0);
 		current_track = 0;
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 a1fbbec03ae7321455d7fe93ff8545d6aa542e7d..af14118e36cb87bb0475982345bba8bd1943306f 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.
@@ -1367,7 +1367,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);
 	}
 
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 a759f4d02bfc0658ea68a6b6becd20dffaad7700..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.
@@ -27,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;
 }
 
diff --git a/src/taglist.h b/src/taglist.h
index a0529ab6b6afae4fe4252e61fd5848e2bd875ef6..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.
@@ -71,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.
 
@@ -99,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;
 
@@ -117,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 695b392cda63a7497d55026abf39bd87c3b8157e..5514e5df8705ed71360320b7b15bf328db8a1da3 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,16 +50,17 @@
 
 #include "filesrch.h"
 
-#include "i_video.h" // rendermode
+#include "d_main.h"
 #include "d_netfil.h"
-#include "dehacked.h"
 #include "d_clisrv.h"
+#include "dehacked.h"
 #include "r_defs.h"
 #include "r_data.h"
 #include "r_textures.h"
 #include "r_patch.h"
 #include "r_picformats.h"
 #include "i_system.h"
+#include "i_video.h" // rendermode
 #include "md5.h"
 #include "lua_script.h"
 #ifdef SCANTHINGS
@@ -117,10 +118,15 @@ void W_Shutdown(void)
 	{
 		wadfile_t *wad = wadfiles[numwadfiles];
 
-		fclose(wad->handle);
+		if (wad->handle)
+			fclose(wad->handle);
 		Z_Free(wad->filename);
+		if (wad->path)
+			Z_Free(wad->path);
 		while (wad->numlumps--)
 		{
+			if (wad->lumpinfo[wad->numlumps].diskpath)
+				Z_Free(wad->lumpinfo[wad->numlumps].diskpath);
 			Z_Free(wad->lumpinfo[wad->numlumps].longname);
 			Z_Free(wad->lumpinfo[wad->numlumps].fullname);
 		}
@@ -423,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;
@@ -604,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);
@@ -681,6 +689,58 @@ static lumpinfo_t* ResGetLumpsZip (FILE* handle, UINT16* nlmp)
 	return lumpinfo;
 }
 
+// Checks if the combination of the first path and the second path are valid.
+// If they are, the concatenated path is returned.
+static char *W_CheckFolderPath(const char *startpath, const char *path)
+{
+	if (checkfolderpath(path, startpath, false))
+	{
+		char *fn;
+
+		if (startpath)
+		{
+			size_t len = strlen(startpath) + strlen(path) + strlen(PATHSEP) + 1;
+			fn = ZZ_Alloc(len);
+			snprintf(fn, len, "%s" PATHSEP "%s", startpath, path);
+		}
+		else
+			fn = Z_StrDup(path);
+
+		return fn;
+	}
+
+	return NULL;
+}
+
+// Returns the first valid path for a folder.
+static char *W_GetFullFolderPath(const char *path)
+{
+	// Check the path by itself first.
+	char *fn = W_CheckFolderPath(NULL, path);
+	if (fn)
+		return fn;
+
+#define checkpath(startpath) { \
+	fn = W_CheckFolderPath(startpath, path); \
+	if (fn) \
+		return fn; \
+} \
+
+	checkpath(srb2home) // Then, look in srb2home.
+	checkpath(srb2path) // Now, look in srb2path.
+	checkpath(".") // Finally, look in ".".
+
+#undef checkpath
+
+	return NULL;
+}
+
+// Loads files from a folder into a lumpinfo structure.
+static lumpinfo_t *ResGetLumpsFolder(const char *path, UINT16 *nlmp, UINT16 *nfiles, UINT16 *nfolders)
+{
+	return getfolderfiles(path, nlmp, nfiles, nfolders);
+}
+
 static UINT16 W_InitFileError (const char *filename, boolean exitworthy)
 {
 	if (exitworthy)
@@ -696,6 +756,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
 //
@@ -765,17 +838,17 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	//
 	W_MakeFileMD5(filename, md5sum);
 
-	if (wadfiles)
+	for (i = 0; i < numwadfiles; i++)
 	{
-		for (i = 0; i < numwadfiles; i++)
+		if (wadfiles[i]->type == RET_FOLDER)
+			continue;
+
+		if (!memcmp(wadfiles[i]->md5sum, md5sum, 16))
 		{
-			if (!memcmp(wadfiles[i]->md5sum, md5sum, 16))
-			{
-				CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), filename);
-				if (handle)
-					fclose(handle);
-				return W_InitFileError(filename, false);
-			}
+			CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), filename);
+			if (handle)
+				fclose(handle);
+			return W_InitFileError(filename, false);
 		}
 	}
 #endif
@@ -815,9 +888,11 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	//
 	wadfile = Z_Malloc(sizeof (*wadfile), PU_STATIC, NULL);
 	wadfile->filename = Z_StrDup(filename);
+	wadfile->path = NULL;
 	wadfile->type = type;
 	wadfile->handle = handle;
-	wadfile->numlumps = (UINT16)numlumps;
+	wadfile->numlumps = numlumps;
+	wadfile->filecount = wadfile->foldercount = 0;
 	wadfile->lumpinfo = lumpinfo;
 	wadfile->important = important;
 	fseek(handle, 0, SEEK_END);
@@ -841,14 +916,8 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	wadfiles[numwadfiles] = wadfile;
 	numwadfiles++; // must come BEFORE W_LoadDehackedLumps, so any addfile called by COM_BufInsertText called by Lua doesn't overwrite what we just loaded
 
-#ifdef HWRENDER
 	// Read shaders from file
-	if (rendermode == render_opengl && (vid.glstate == VID_GL_LIBRARY_LOADED))
-	{
-		HWR_LoadCustomShadersFromFile(numwadfiles - 1, (type == RET_PK3));
-		HWR_CompileShaders();
-	}
-#endif // HWRENDER
+	W_ReadFileShaders(wadfile);
 
 	// TODO: HACK ALERT - Load Lua & SOC stuff right here. I feel like this should be out of this place, but... Let's stick with this for now.
 	switch (wadfile->type)
@@ -874,6 +943,136 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	return wadfile->numlumps;
 }
 
+//
+// Loads a folder as a WAD.
+//
+UINT16 W_InitFolder(const char *path, boolean mainfile, boolean startup)
+{
+	lumpinfo_t *lumpinfo = NULL;
+	wadfile_t *wadfile;
+	UINT16 numlumps = 0;
+	UINT16 filecount, foldercount;
+	size_t i;
+	char *fn, *fullpath;
+	const char *p;
+	int important;
+
+	if (!(refreshdirmenu & REFRESHDIR_ADDFILE))
+		refreshdirmenu = REFRESHDIR_NORMAL|REFRESHDIR_ADDFILE; // clean out cons_alerts that happened earlier
+
+	if (refreshdirname)
+		Z_Free(refreshdirname);
+	if (dirmenu)
+		refreshdirname = Z_StrDup(path);
+	else
+		refreshdirname = NULL;
+
+	if (numwadfiles >= MAX_WADFILES)
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("Maximum wad files reached\n"));
+		refreshdirmenu |= REFRESHDIR_MAX;
+		return W_InitFileError(path, startup);
+	}
+
+	important = 0; /// \todo Implement a W_VerifyFolder.
+
+	// Remove path separators from the filename, and don't try adding "/".
+	p = path+strlen(path);
+	--p;
+
+	while (*p == '\\' || *p == '/' || *p == ':')
+	{
+		p--;
+		if (p < path)
+		{
+			CONS_Alert(CONS_ERROR, M_GetText("Path %s is prohibited\n"), path);
+			return W_InitFileError(path, startup);
+		}
+	}
+	p++;
+
+	// Allocate the new path name.
+	i = (p - path) + 1;
+	fn = ZZ_Alloc(i);
+	strlcpy(fn, path, i);
+
+	if (M_IsStringEmpty(fn))
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("Folder name is empty\n"));
+		Z_Free(fn);
+
+		if (startup)
+			return W_InitFileError("A folder", true);
+		else
+			return W_InitFileError("a folder", false);
+	}
+
+	// Get the full path for this filename.
+	fullpath = W_GetFullFolderPath(fn);
+	if (fullpath == NULL)
+	{
+		Z_Free(fn);
+		return W_InitFileError(path, false);
+	}
+
+	for (i = 0; i < numwadfiles; i++)
+	{
+		if (wadfiles[i]->type != RET_FOLDER)
+			continue;
+
+		if (samepaths(wadfiles[i]->path, fullpath) > 0)
+		{
+			CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), path);
+			Z_Free(fn);
+			Z_Free(fullpath);
+			return W_InitFileError(path, false);
+		}
+	}
+
+	lumpinfo = ResGetLumpsFolder(fullpath, &numlumps, &filecount, &foldercount);
+	if (lumpinfo == NULL)
+	{
+		if (filecount == UINT16_MAX)
+			CONS_Alert(CONS_ERROR, M_GetText("Folder %s is empty\n"), path);
+
+		Z_Free(fn);
+		Z_Free(fullpath);
+
+		return W_InitFileError(path, startup);
+	}
+
+	if (important && !mainfile)
+		G_SetGameModified(true);
+
+	wadfile = Z_Malloc(sizeof (*wadfile), PU_STATIC, NULL);
+	wadfile->filename = fn;
+	wadfile->path = fullpath;
+	wadfile->type = RET_FOLDER;
+	wadfile->handle = NULL;
+	wadfile->numlumps = numlumps;
+	wadfile->filecount = filecount;
+	wadfile->foldercount = foldercount;
+	wadfile->lumpinfo = lumpinfo;
+	wadfile->important = important;
+
+	// Irrelevant.
+	wadfile->filesize = 0;
+	memset(wadfile->md5sum, 0x00, 16);
+
+	Z_Calloc(numlumps * sizeof (*wadfile->lumpcache), PU_STATIC, &wadfile->lumpcache);
+	Z_Calloc(numlumps * sizeof (*wadfile->patchcache), PU_STATIC, &wadfile->patchcache);
+
+	CONS_Printf(M_GetText("Added folder %s (%u files, %u folders)\n"), fn, filecount, foldercount);
+	wadfiles[numwadfiles] = wadfile;
+	numwadfiles++;
+
+	W_ReadFileShaders(wadfile);
+	W_LoadDehackedLumpsPK3(numwadfiles - 1, mainfile);
+	W_InvalidateLumpnumCache();
+
+	return wadfile->numlumps;
+}
+
 /** Tries to load a series of files.
   * All files are wads unless they have an extension of ".soc" or ".lua".
   *
@@ -889,8 +1088,16 @@ void W_InitMultipleFiles(addfilelist_t *list)
 
 	for (; i < list->numfiles; i++)
 	{
-		//CONS_Debug(DBG_SETUP, "Loading %s\n", *filenames);
-		W_InitFile(list->files[i], 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);
 	}
 }
 
@@ -1164,7 +1371,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)
@@ -1262,9 +1469,34 @@ UINT8 W_LumpExists(const char *name)
 
 size_t W_LumpLengthPwad(UINT16 wad, UINT16 lump)
 {
+	lumpinfo_t *l;
+
 	if (!TestValidLump(wad, lump))
 		return 0;
-	return wadfiles[wad]->lumpinfo[lump].size;
+
+	l = wadfiles[wad]->lumpinfo + lump;
+
+	if (wadfiles[wad]->type == RET_FOLDER)
+	{
+		INT32 stat = pathisfolder(l->diskpath);
+
+		if (stat < 0)
+			I_Error("W_LumpLengthPwad: could not stat %s", l->diskpath);
+		else if (stat == 1) // Path is a folder.
+			return 0;
+		else
+		{
+			FILE *handle = fopen(l->diskpath, "rb");
+			if (handle == NULL)
+				I_Error("W_LumpLengthPwad: could not open file %s", l->diskpath);
+
+			fseek(handle, 0, SEEK_END);
+			l->size = l->disksize = ftell(handle);
+			fclose(handle);
+		}
+	}
+
+	return l->size;
 }
 
 /** Returns the buffer size needed to load the given lump.
@@ -1279,11 +1511,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;
 
@@ -1292,23 +1524,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
@@ -1351,17 +1583,44 @@ void zerr(int ret)
   */
 size_t W_ReadLumpHeaderPwad(UINT16 wad, UINT16 lump, void *dest, size_t size, size_t offset)
 {
-	size_t lumpsize;
+	size_t lumpsize, bytesread;
 	lumpinfo_t *l;
-	FILE *handle;
+	FILE *handle = NULL;
 
 	if (!TestValidLump(wad,lump))
 		return 0;
 
+	l = wadfiles[wad]->lumpinfo + lump;
+
+	// Open the external file for this lump, if the WAD is a folder.
+	if (wadfiles[wad]->type == RET_FOLDER)
+	{
+		INT32 stat = pathisfolder(l->diskpath);
+
+		if (stat < 0)
+			I_Error("W_ReadLumpHeaderPwad: could not stat %s", l->diskpath);
+		else if (stat == 1) // Path is a folder.
+			return 0;
+		else
+		{
+			handle = fopen(l->diskpath, "rb");
+			if (handle == NULL)
+				I_Error("W_ReadLumpHeaderPwad: could not open file %s", l->diskpath);
+
+			// Find length of file
+			fseek(handle, 0, SEEK_END);
+			l->size = l->disksize = ftell(handle);
+		}
+	}
+
 	lumpsize = wadfiles[wad]->lumpinfo[lump].size;
 	// empty resource (usually markers like S_START, F_END ..)
 	if (!lumpsize || lumpsize<offset)
+	{
+		if (wadfiles[wad]->type == RET_FOLDER)
+			fclose(handle);
 		return 0;
+	}
 
 	// zero size means read all the lump
 	if (!size || size+offset > lumpsize)
@@ -1369,24 +1628,22 @@ size_t W_ReadLumpHeaderPwad(UINT16 wad, UINT16 lump, void *dest, size_t size, si
 
 	// Let's get the raw lump data.
 	// We setup the desired file handle to read the lump data.
-	l = wadfiles[wad]->lumpinfo + lump;
-	handle = wadfiles[wad]->handle;
+	if (wadfiles[wad]->type != RET_FOLDER)
+		handle = wadfiles[wad]->handle;
 	fseek(handle, (long)(l->position + offset), SEEK_SET);
 
 	// But let's not copy it yet. We support different compression formats on lumps, so we need to take that into account.
 	switch(wadfiles[wad]->lumpinfo[lump].compression)
 	{
 	case CM_NOCOMPRESSION:		// If it's uncompressed, we directly write the data into our destination, and return the bytes read.
+		bytesread = fread(dest, 1, size, handle);
+		if (wadfiles[wad]->type == RET_FOLDER)
+			fclose(handle);
 #ifdef NO_PNG_LUMPS
-		{
-			size_t bytesread = fread(dest, 1, size, handle);
-			if (Picture_IsLumpPNG((UINT8 *)dest, bytesread))
-				Picture_ThrowPNGError(l->fullname, wadfiles[wad]->filename);
-			return bytesread;
-		}
-#else
-		return fread(dest, 1, size, handle);
+		if (Picture_IsLumpPNG((UINT8 *)dest, bytesread))
+			Picture_ThrowPNGError(l->fullname, wadfiles[wad]->filename);
 #endif
+		return bytesread;
 	case CM_LZF:		// Is it LZF compressed? Used by ZWADs.
 		{
 #ifdef ZWAD
diff --git a/src/w_wad.h b/src/w_wad.h
index bd7809b8bfe56b525c7a2cfe663167b587e1ba9a..efe0513725c8ec7657ca14e5ea3e07d238a34c3a 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;
@@ -115,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 filecount, foldercount; // file and folder count
 	FILE *handle;
 	UINT32 filesize; // for network
 	UINT8 md5sum[16];
@@ -153,10 +156,14 @@ void W_Shutdown(void);
 FILE *W_OpenWadFile(const char **filename, boolean useerrors);
 // Load and add a wadfile to the active wad files, returns numbers of lumps, INT16_MAX on error
 UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup);
+// Adds a folder as a file
+UINT16 W_InitFolder(const char *path, boolean mainfile, boolean startup);
 
 // W_InitMultipleFiles exits if a file was not found, but not if all is okay.
 void W_InitMultipleFiles(addfilelist_t *list);
 
+#define W_FileHasFolders(wadfile) ((wadfile)->type == RET_PK3 || (wadfile)->type == RET_FOLDER)
+
 const char *W_CheckNameForNumPwad(UINT16 wad, UINT16 lump);
 const char *W_CheckNameForNum(lumpnum_t lumpnum);
 
diff --git a/src/win32/Makefile.cfg b/src/win32/Makefile.cfg
deleted file mode 100644
index 702ae3765ce52effaccb48b7d3c3e196a7ac9214..0000000000000000000000000000000000000000
--- a/src/win32/Makefile.cfg
+++ /dev/null
@@ -1,136 +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
-
-	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
-	EXENAME?=srb2win.exe
-
-ifdef SDL
-	i_system_o+=$(OBJDIR)/SRB2.res
-	#i_main_o+=$(OBJDIR)/win_dbg.o
-ifndef NOHW
-	OPTS+=-DUSE_WGL_SWAP
-endif
-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
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/y_inter.c b/src/y_inter.c
index 6833ca2b5aeb1e62d45d4561ea38d0ccc8d52832..6e1bc2e61b305cdbcbfa9532b628ff65e7b1f65f 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,7 @@ static void Y_IntermissionTokenDrawer(void)
 	calc = (lowy - y)*2;
 
 	if (calc > 0)
-		V_DrawCroppedPatch(32<<FRACBITS, y<<FRACBITS, FRACUNIT/2, 0, tokenicon, 0, 0, tokenicon->width, calc);
+		V_DrawCroppedPatch(32<<FRACBITS, y<<FRACBITS, FRACUNIT/2, FRACUNIT/2, 0, tokenicon, NULL, 0, 0, tokenicon->width<<FRACBITS, calc<<FRACBITS);
 }
 
 
@@ -266,6 +266,14 @@ void Y_LoadIntermissionData(void)
 		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;
 		}
@@ -422,7 +430,7 @@ void Y_IntermissionDrawer(void)
 	else if (bgtile)
 		V_DrawPatchFill(bgtile);
 
-	LUAh_IntermissionHUD();
+	LUAh_IntermissionHUD(intertype == int_spec && stagefailed);
 	if (!LUA_HudEnabled(hud_intermissiontally))
 		goto skiptallydrawer;
 
@@ -463,14 +471,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;
@@ -554,37 +565,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]);
@@ -630,6 +648,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;
@@ -1002,7 +1021,8 @@ void Y_Ticker(void)
 	if (paused || P_AutoPause())
 		return;
 
-	LUAh_IntermissionThinker();
+	LUA_HookBool(intertype == int_spec && stagefailed,
+			HOOK(IntermissionThinker));
 
 	intertic++;
 
@@ -1322,24 +1342,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
@@ -1356,6 +1392,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;
 		}
 
@@ -1776,21 +1819,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;
 }
 
@@ -1843,12 +1895,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;
 }
 
@@ -2061,7 +2122,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 7268b1a473f14764701cd3a13a91f459447e6bf6..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.
diff --git a/src/z_zone.c b/src/z_zone.c
index d7da17e51d1608cf068a0c929bbdbd982d3a935c..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.
diff --git a/src/z_zone.h b/src/z_zone.h
index 7b58be8f3fce52c01642e20bee0de00c1795bece..be55bf9940ead1a1385f92ca50d296b0bd4fbdf1 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.