diff --git a/.gitattributes b/.gitattributes
index c2e507352e6419be54e8866cc5bb40f5570a0fc7..9f08509300772556a9466ca012bd087bf7350393 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -10,6 +10,7 @@
 /src/Make*.cfg text=auto
 /src/CMakeLists.txt text=auto
 *.mk -whitespace text=auto
+/comptime.sh text eol=lf
 # Windows EOL
 *.cs -crlf -whitespace
 *.bat -crlf -whitespace
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b292bfc06c21f5ef02d6bb155ae4ab5e4ee6533c..46fa6584d456dcfa698b547436a79fd78125650e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,7 +3,7 @@ variables:
   GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_PATH
 
 default:
-  image: debian:stable-slim
+  image: git.do.srb2.org:5050/stjr/srb2ci/srb2ci:stable
 
   cache:
     - key: ccache-$CI_PROJECT_PATH_SLUG-$CI_JOB_NAME_SLUG
@@ -19,6 +19,11 @@ default:
         - apt-cache
       unprotect: true
 
+    - key: apk-$CI_JOB_IMAGE
+      paths:
+        - apk-cache
+      unprotect: true
+
   before_script:
     - - |
          # debconf
@@ -159,11 +164,18 @@ stages:
 
 Debian testing GCC:
   stage: build
+
+  when: manual
+
   image: debian:testing-slim
 
   allow_failure: true
 
   artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "testing-gcc"
     name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-testing-gcc"
 
   variables:
@@ -198,6 +210,8 @@ Debian testing GCC:
 Windows x86:
   stage: build
 
+  when: on_success
+
   artifacts:
     paths:
       - "bin/"
@@ -228,6 +242,8 @@ Windows x86:
 Debian stable:amd64:
   stage: build
 
+  when: on_success
+
   artifacts:
     paths:
       - "bin/"
@@ -270,6 +286,8 @@ Debian stable:amd64:
 Debian stable:i386:
   stage: build
 
+  when: manual
+
   artifacts:
     paths:
       - "bin/"
@@ -311,6 +329,8 @@ Debian stable:i386:
 Debian stable:arm64:
   stage: build
 
+  when: manual
+
   artifacts:
     paths:
       - "bin/"
@@ -345,7 +365,7 @@ Debian stable:arm64:
     - - |
           # make
           echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
-      - make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1 ERRORMODE=1 NONX86=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1 NONX86=1
+      - make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1 ERRORMODE=1 NONX86=1 ARM64=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1 NONX86=1 ARM64=1
       - |
           # make
           echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
@@ -353,6 +373,8 @@ Debian stable:arm64:
 Windows x64:
   stage: build
 
+  when: manual
+
   artifacts:
     paths:
       - "bin/"
@@ -383,9 +405,15 @@ Windows x64:
 Debian stable Clang:
   stage: build
 
+  when: manual
+
   allow_failure: true
 
   artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "clang"
     name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-clang"
 
   variables:
@@ -419,12 +447,61 @@ Debian stable Clang:
           # make
           echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
 
+Debian stable musl:
+  stage: build
+
+  when: manual
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "musl"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-musl"
+
+  variables:
+    CC: musl-gcc
+    LDD: musl-ldd
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install gcc
+      - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_end:`date +%s`:apt_toolchain\r\e[0K"
+
+    - - |
+          # apt_development
+          echo -e "\e[0Ksection_start:`date +%s`:apt_development[collapsed=true]\r\e[0KInstalling development packages"
+      - apt-get install musl-tools
+      - |
+          # apt_development
+          echo -e "\e[0Ksection_end:`date +%s`:apt_development\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1 SDL=0 NOHW=1 NOZLIB=1 NOCURL=1 NOGME=1 NOOPENMPT=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1 SDL=0 NOHW=1 NOZLIB=1 NOCURL=1 NOGME=1 NOOPENMPT=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
 Debian testing Clang:
   extends: Debian stable Clang
 
+  when: manual
+
   image: debian:testing-slim
 
   artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "testing-clang"
     name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-testing-clang"
 
   variables:
@@ -432,3 +509,152 @@ Debian testing Clang:
     WFLAGS: -Wno-cast-align -Wno-deprecated-non-prototype
     CFLAGS: -Wno-cast-align -Wno-deprecated-non-prototype
     LDFLAGS: -Wl,-fuse-ld=gold
+
+Debian testing musl:
+  extends: Debian stable musl
+
+  when: manual
+
+  image: debian:testing-slim
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "testing-musl"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-testing-musl"
+
+  variables:
+    CC: musl-gcc
+    LDD: musl-ldd
+    LDFLAGS: -Wl,-fuse-ld=gold
+
+Alpine 3 GCC:
+  stage: build
+
+  when: on_success
+
+  image: alpine:3
+
+  allow_failure: true
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Apline-3"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-Apline-3"
+
+  before_script:
+    - - |
+          # apk_cache
+          echo -e "\e[0Ksection_start:`date +%s`:apk_cache[collapsed=true]\r\e[0KUpdating APK listing"
+      - export APK_CACHE_DIR=`pwd`/apk-cache
+      - mkdir --parents --verbose $APK_CACHE_DIR/
+      - ln -sf /etc/apk/cache $APK_CACHE_DIR
+      - |
+          # apk_cache
+          echo -e "\e[0Ksection_end:`date +%s`:apk_cache\r\e[0K"
+
+    - - |
+          # apk_update
+          echo -e "\e[0Ksection_start:`date +%s`:apk_update[collapsed=true]\r\e[0KUpdating APK listing"
+      - apk update
+      - |
+          # apk_update
+          echo -e "\e[0Ksection_end:`date +%s`:apk_update\r\e[0K"
+
+    - - |
+          # apk_upgrade
+          echo -e "\e[0Ksection_start:`date +%s`:apk_upgrade[collapsed=true]\r\e[0KUpdating existing packages"
+      - apk upgrade
+      - |
+          # apk_update
+          echo -e "\e[0Ksection_end:`date +%s`:apk_upgrade\r\e[0K"
+
+    - - |
+          # apk_common
+          echo -e "\e[0Ksection_start:`date +%s`:apk_common[collapsed=true]\r\e[0KInstalling common packages"
+      - apk add make git ccache nasm
+      - |
+          # apk_common
+          echo -e "\e[0Ksection_end:`date +%s`:apk_common\r\e[0K"
+
+    - - |
+          # ccache_config
+          echo -e "\e[0Ksection_start:`date +%s`:ccache_config[collapsed=true]\r\e[0KSetting up ccache config"
+      - mkdir --parents --verbose ~/.ccache/
+      - touch ~/.ccache/ccache.conf
+      - |
+          # cache.conf
+          echo Adding ccache configution option
+      - |
+          # base_dir
+          echo base_dir = $PWD                  | tee -a ~/.ccache/ccache.conf
+      - |
+          # cache_dir
+          echo cache_dir = $PWD/ccache          | tee -a ~/.ccache/ccache.conf
+      - |
+          # compiler_check
+          echo compiler_check = content         | tee -a ~/.ccache/ccache.conf
+      - |
+          # stats_log
+          echo stats_log = $PWD/ccache_statslog | tee -a ~/.ccache/ccache.conf
+      - |
+          # max_size
+          echo max_size = 50M                   | tee -a ~/.ccache/ccache.conf
+      - |
+          # ccache_config
+          echo -e "\e[0Ksection_end:`date +%s`:ccache_config\r\e[0K"
+
+    - - |
+          # cache_reset
+          echo -e "\e[0Ksection_start:`date +%s`:ccache_reset[collapsed=true]\r\e[0KResetting ccache statistics"
+      - ccache --zero-stats
+      - ccache --show-stats
+      - |
+          # ccache_reset
+          echo -e "\e[0Ksection_end:`date +%s`:ccache_reset\r\e[0K"
+
+  script:
+    - - |
+          # apk_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apk_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apk add gcc
+      - |
+          # apk_toolchain
+          echo -e "\e[0Ksection_end:`date +%s`:apk_toolchain\r\e[0K"
+
+    - - |
+          # apk_development
+          echo -e "\e[0Ksection_start:`date +%s`:apk_development[collapsed=true]\r\e[0KInstalling development packages"
+      - apk add musl-dev sdl2_mixer-dev libpng-dev curl-dev libgme-dev libopenmpt-dev
+      - |
+          # apk_development
+          echo -e "\e[0Ksection_end:`date +%s`:apk_development\r\e[0K"
+
+    - - |
+          # make
+          echo -e "\e[0Ksection_start:`date +%s`:make[collapsed=false]\r\e[0KCompiling SRB2"
+      - make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1 NOEXECINFO=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1 NOEXECINFO=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+  after_script:
+    - - |
+           # apk_clean
+           echo -e "\e[0Ksection_start:`date +%s`:apk_clean[collapsed=true]\r\e[0KCleaning of unneeded APK packages"
+      - apk cache clean
+      - |
+          # apk_clean
+          echo -e "\e[0Ksection_end:`date +%s`:apk_clean\r\e[0K"
+
+    - - |
+          # ccache_stats
+          echo -e "\e[0Ksection_start:`date +%s`:ccache_stats[collapsed=true]\r\e[0Kccache statistics:"
+      - ccache --show-stats --verbose
+      - ccache --show-log-stats --verbose
+      - |
+          # ccahe_stats
+          echo -e "\e[0Ksection_end:`date +%s`:ccache_stats\r\e[0K"
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 80a3bdcd6798a48d10ea928d60ca3c4fb77bf353..8803620e7a9305cb28359aa31c973223d4df247f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -75,6 +75,7 @@ option(SRB2_CONFIG_ERRORMODE "Compile C code with warnings treated as errors." O
 option(SRB2_CONFIG_DEBUGMODE "Compile with PARANOIA, ZDEBUG, RANGECHECK and PACKETDROP defined." OFF)
 option(SRB2_CONFIG_MOBJCONSISTANCY "Compile with MOBJCONSISTANCY defined." OFF)
 option(SRB2_CONFIG_PACKETDROP "Compile with PACKETDROP defined." OFF)
+option(SRB2_CONFIG_EXECINFO "Enable stack trace dump support." ON)
 option(SRB2_CONFIG_ZDEBUG "Compile with ZDEBUG defined." OFF)
 # SRB2_CONFIG_PROFILEMODE is probably superceded by some CMake setting.
 option(SRB2_CONFIG_PROFILEMODE "Compile for profiling (GCC only)." OFF)
diff --git a/appveyor.yml b/appveyor.yml
index 63d801b734719bf4ba47dfb42b1b7849c3a23189..d0d94b9828b56bc70f2e60bb18064bf9c933a099 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 2.2.13.{branch}-{build}
+version: 2.2.14.{branch}-{build}
 os: MinGW
 
 environment:
diff --git a/comptime.bat b/comptime.bat
index 0c7ea06d60955ec1cd5015a9b660625dae0b6d2c..c03f088b23b23ba43abc9839e39a99c78317a618 100644
--- a/comptime.bat
+++ b/comptime.bat
@@ -1,6 +1,7 @@
 @echo off
 set BRA=Unknown
 set REV=illegal
+set GL1=Dummy
 
 copy nul: /b +%1\comptime.c tmp.$$$ > nul
 move tmp.$$$ %1\comptime.c > nul
@@ -13,8 +14,9 @@ goto filwri
 :gitrev
 set GIT=%2
 if "%GIT%"=="" set GIT=git
-for /f "usebackq" %%s in (`%GIT% rev-parse --abbrev-ref HEAD`) do @set BRA=%%s
-for /f "usebackq" %%s in (`%GIT% rev-parse HEAD`) do @set REV=%%s
+for /f "tokens=* usebackq" %%s in (`%GIT% rev-parse --abbrev-ref HEAD`) do @set BRA=%%s
+for /f "tokens=* usebackq" %%s in (`%GIT% rev-parse HEAD`) do @set REV=%%s
+for /f "tokens=* usebackq" %%s in (`%GIT% log -1 --format^=%%f`) do @set GL1=%%s
 set REV=%REV:~0,8%
 goto filwri
 
@@ -30,3 +32,4 @@ echo // by the %0 batch file >> %1\comptime.h
 echo // >> %1\comptime.h
 echo const char* compbranch = "%BRA%"; >> %1\comptime.h
 echo const char* comprevision = "%REV%"; >> %1\comptime.h
+echo const char* compnote = "%GL1%"; >> %1\comptime.h
diff --git a/comptime.sh b/comptime.sh
index ce771f631781c153dd87e60a20fdad8064d347e9..3338ecc35506d8b9e9b001679da881b21e81772c 100755
--- a/comptime.sh
+++ b/comptime.sh
@@ -12,24 +12,26 @@ version() {
 //
 const char* compbranch = "$1";
 const char* comprevision = "$2";
+const char* compnote = "$3";
 EOF
 }
 
 versiongit() {
 	gitbranch="$(git rev-parse --abbrev-ref HEAD)"
 	gitversion="$(git rev-parse HEAD | cut -c -8)"
-	version "$gitbranch" "$gitversion";
+	gitsubject="$(git log -1 --format=%f)"
+	version "$gitbranch" "$gitversion" "$gitsubject";
 	exit 0
 }
 
 versionsvn() {
 	svnrevision="$(svnversion -n "$1")"
-	version "Subversion" "r$svnrevision";
+	version "Subversion" "r$svnrevision" "dummy";
 	exit 0
 }
 
 versionfake() {
-	version "Unknown" "illegal";
+	version "Unknown" "illegal" "dummy";
 }
 
 compversion() {
diff --git a/src/Android.mk b/src/Android.mk
index 035d48887727c2a6d6b63a6acb38fc7ec65a9342..235c4b81e5b4603cecffe56e962a973a7469977a 100644
--- a/src/Android.mk
+++ b/src/Android.mk
@@ -8,11 +8,7 @@ LOCAL_SRC_FILES :=      am_map.c \
                         command.c \
                         comptime.c \
                         console.c \
-                        d_clisrv.c \
                         d_main.c \
-                        d_net.c \
-                        d_netcmd.c \
-                        d_netfil.c \
                         dehacked.c \
                         f_finale.c \
                         f_wipe.c \
@@ -20,7 +16,6 @@ LOCAL_SRC_FILES :=      am_map.c \
                         g_game.c \
                         g_input.c \
                         hu_stuff.c \
-                        i_tcp.c \
                         info.c \
                         lzf.c \
                         m_argv.c \
@@ -32,7 +27,6 @@ LOCAL_SRC_FILES :=      am_map.c \
                         m_queue.c \
                         m_random.c \
                         md5.c \
-                        mserv.c \
                         p_ceilng.c \
                         p_enemy.c \
                         p_fab.c \
@@ -61,6 +55,7 @@ LOCAL_SRC_FILES :=      am_map.c \
                         r_things.c \
                         s_sound.c \
                         screen.c \
+                        snake.c \
                         sounds.c \
                         st_stuff.c \
                         string.c \
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index b926b3b7a3372d206df781fd65bbf7a947ddaef3..49f783722ab429986fb45b317a2d00c36a4b5f94 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -6,10 +6,6 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
 	config.h.in
 	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
@@ -83,12 +79,10 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
 	sounds.c
 	w_wad.c
 	filesrch.c
-	mserv.c
-	http-mserv.c
-	i_tcp.c
 	lzf.c
 	b_bot.c
 	u_list.c
+	snake.c
 	lua_script.c
 	lua_baselib.c
 	lua_mathlib.c
@@ -105,6 +99,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
 	lua_blockmaplib.c
 	lua_hudlib.c
 	lua_hudlib_drawlist.c
+	lua_colorlib.c
 	lua_inputlib.c
 )
 
@@ -149,6 +144,7 @@ set(SRB2_CONFIG_DEV_BUILD OFF CACHE BOOL
 	"Compile a development build of SRB2.")
 
 add_subdirectory(blua)
+add_subdirectory(netcode)
 
 # OS macros
 if (UNIX)
@@ -258,6 +254,7 @@ target_compile_options(SRB2SDL2 PRIVATE
 		-Winline
 		-Wformat-y2k
 		-Wformat-security
+		-fwrapv
 
 		$<$<VERSION_LESS:$<C_COMPILER_VERSION>,2.9.5>:
 			-Wno-div-by-zero
@@ -281,7 +278,7 @@ target_compile_options(SRB2SDL2 PRIVATE
 
 		$<$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,4.5.0>:
 			-Wlogical-op
-			-Wno-error=array-bounds
+			#-Wno-error=array-bounds
 		>
 
 		$<$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,4.6.0>:
@@ -327,6 +324,7 @@ target_compile_options(SRB2SDL2 PRIVATE
 		-Wno-error=non-literal-null-conversion
 		-Wno-error=constant-conversion
 		-Wno-error=unused-but-set-variable
+		-fwrapv
 	>
 
 	# C, MSVC
@@ -380,6 +378,11 @@ endif()
 if(SRB2_CONFIG_PACKETDROP)
 	target_compile_definitions(SRB2SDL2 PRIVATE -DPACKETDROP)
 endif()
+if(SRB2_CONFIG_EXECINFO)
+else()
+	target_compile_definitions(SRB2SDL2 PRIVATE -DNOEXECINFO)
+	message(STATUS "You have disabled stack trace dump support")
+endif()
 if(SRB2_CONFIG_ZDEBUG)
 	target_compile_definitions(SRB2SDL2 PRIVATE -DZDEBUG)
 endif()
diff --git a/src/Makefile b/src/Makefile
index 539c2fa743cc46071c94b8736c49802e6034e62c..6dba19c2418d9ee465b435b40332245762ecd21f 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -62,11 +62,11 @@
 #
 # Netplay incompatible
 # --------------------
-# NONET=1 - Disable online capability.
 # NOMD5=1 - Disable MD5 checksum (validation tool).
 # NOPOSTPROCESSING=1 - ?
 # MOBJCONSISTANCY=1 - ??
 # PACKETDROP=1 - ??
+# NOEXECINFO=1 - Disable stack trace dump support
 # DEBUGMODE=1 - Enable various debugging capabilities.
 #               Also disables optimizations.
 # NOZLIB=1 - Disable some compression capability. Implies
@@ -165,7 +165,7 @@ sources:=
 makedir:=../make
 
 # -DCOMPVERSION: flag to use comptime.h
-opts:=-DCOMPVERSION -g
+opts:=-DCOMPVERSION -g -fwrapv
 libs:=
 
 # This is a list of variables names, of which if defined,
@@ -187,6 +187,7 @@ objdir:=$(makedir)/objs
 sources+=\
 	$(call List,Sourcefile)\
 	$(call List,blua/Sourcefile)\
+	$(call List,netcode/Sourcefile)\
 
 depends:=$(basename $(filter %.c %.s,$(sources)))
 objects:=$(basename $(filter %.c %.s %.nas,$(sources)))
diff --git a/src/Makefile.d/features.mk b/src/Makefile.d/features.mk
index a66779c07a77c72a4ae0c085ff738ce58e62cf73..29587302f5fdc7758834a3e74ba792164a0beb77 100644
--- a/src/Makefile.d/features.mk
+++ b/src/Makefile.d/features.mk
@@ -3,9 +3,9 @@
 #
 
 passthru_opts+=\
-	NONET NO_IPV6 NOHW NOMD5 NOPOSTPROCESSING\
+	NO_IPV6 NOHW NOMD5 NOPOSTPROCESSING\
 	MOBJCONSISTANCY PACKETDROP ZDEBUG\
-	HAVE_MINIUPNPC\
+	HAVE_MINIUPNPC NOEXECINFO\
 
 # build with debugging information
 ifdef DEBUGMODE
@@ -18,10 +18,6 @@ opts+=-DHWRENDER
 sources+=$(call List,hardware/Sourcefile)
 endif
 
-ifdef NONET
-NOCURL=1
-endif
-
 ifndef NOMD5
 sources+=md5.c
 endif
@@ -43,13 +39,11 @@ 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
diff --git a/src/Makefile.d/versions.mk b/src/Makefile.d/versions.mk
index d2877b374346115621773c822af00a92dd4de72c..2523d7f3c25f6c519e67d728417a40e9e052e883 100644
--- a/src/Makefile.d/versions.mk
+++ b/src/Makefile.d/versions.mk
@@ -116,7 +116,7 @@ ifdef GCC43
  #WFLAGS+=-Wno-error=clobbered
 endif
 ifdef GCC44
- WFLAGS+=-Wno-error=array-bounds
+#WFLAGS+=-Wno-error=array-bounds
 endif
 ifdef GCC46
  WFLAGS+=-Wno-error=suggest-attribute=noreturn
diff --git a/src/Makefile.d/win32.mk b/src/Makefile.d/win32.mk
index 73a3d9e453ecaa0a01e32e15f71326bd7be57920..a858881c0a0508c0eb3addfc456bdd042656108d 100644
--- a/src/Makefile.d/win32.mk
+++ b/src/Makefile.d/win32.mk
@@ -33,12 +33,10 @@ 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
diff --git a/src/Sourcefile b/src/Sourcefile
index f2b408c665d28306ce9c778646f6139b6874099a..7beb98c9e313286506c55d359bb19b38b013bfb0 100644
--- a/src/Sourcefile
+++ b/src/Sourcefile
@@ -1,9 +1,5 @@
 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
@@ -77,12 +73,10 @@ s_sound.c
 sounds.c
 w_wad.c
 filesrch.c
-mserv.c
-http-mserv.c
-i_tcp.c
 lzf.c
 b_bot.c
 u_list.c
+snake.c
 lua_script.c
 lua_baselib.c
 lua_mathlib.c
@@ -100,3 +94,4 @@ lua_blockmaplib.c
 lua_hudlib.c
 lua_hudlib_drawlist.c
 lua_inputlib.c
+lua_colorlib.c
diff --git a/src/am_map.c b/src/am_map.c
index 331d1a5e0db4c669791b71f2984dfe208598b8f4..df3a45cfff504e046ddaa6ecc03cf9f9d548e45f 100644
--- a/src/am_map.c
+++ b/src/am_map.c
@@ -1071,7 +1071,6 @@ static inline void AM_drawPlayers(void)
 		return;
 	}
 
-	// multiplayer (how??)
 	for (i = 0; i < MAXPLAYERS; i++)
 	{
 		if (!playeringame[i] || players[i].spectator)
diff --git a/src/android/i_net.c b/src/android/i_net.c
index f6e642022e4a54360d68004f69d61eaa5e3acb19..4c30dc767a04615b75e6badf68d3603bc475a575 100644
--- a/src/android/i_net.c
+++ b/src/android/i_net.c
@@ -1,4 +1,4 @@
-#include "../i_net.h"
+#include "../netcode/i_net.h"
 
 boolean I_InitNetwork(void)
 {
diff --git a/src/b_bot.c b/src/b_bot.c
index 57f7623042d318df981af823ac59d1865c44fb75..c8228e840f751b676a00e920cde61dcb8f06311c 100644
--- a/src/b_bot.c
+++ b/src/b_bot.c
@@ -239,7 +239,8 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 	// SPINNING
 	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)
+	else if ((mem->thinkstate == AI_FOLLOW || mem->thinkstate == AI_SPINFOLLOW)
+		&& bot->charability2 == CA2_SPINDASH)
 	{
 		if (!_2d)
 		{
@@ -329,7 +330,7 @@ static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
 	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)
+		if (bot->charability == CA_FLY && bot->pflags & PF_THOKKED)
 		{
 			cmd->forwardmove = min(MAXPLMOVE, (dist/scale)>>3);
 			if (zdist < -64*scale)
@@ -583,11 +584,11 @@ void B_RespawnBot(INT32 playernum)
 	P_SetOrigin(tails, x, y, z);
 	if (player->charability == CA_FLY)
 	{
-		P_SetPlayerMobjState(tails, S_PLAY_FLY);
+		P_SetMobjState(tails, S_PLAY_FLY);
 		tails->player->powers[pw_tailsfly] = (UINT16)-1;
 	}
 	else
-		P_SetPlayerMobjState(tails, S_PLAY_FALL);
+		P_SetMobjState(tails, S_PLAY_FALL);
 	P_SetScale(tails, sonic->scale);
 	tails->destscale = sonic->destscale;
 }
@@ -613,6 +614,9 @@ void B_HandleFlightIndicator(player_t *player)
 
 		// otherwise, spawn it
 		P_SetTarget(&tails->hnext, P_SpawnMobjFromMobj(tails, 0, 0, 0, MT_OVERLAY));
+		if (P_MobjWasRemoved(tails->hnext))
+			return;  // we can't spawn one, so it can't exist
+
 		P_SetTarget(&tails->hnext->target, tails);
 		P_SetTarget(&tails->hnext->hprev, tails);
 		P_SetMobjState(tails->hnext, S_FLIGHTINDICATOR);
diff --git a/src/blua/lbaselib.c b/src/blua/lbaselib.c
index 0fc222038dd97dbc4306018a88f87b69b612c167..2bb3d9cf09d030e439231c08a591141518c62b25 100644
--- a/src/blua/lbaselib.c
+++ b/src/blua/lbaselib.c
@@ -275,18 +275,36 @@ static int luaB_dofile (lua_State *L) {
 	int n = lua_gettop(L);
 
 	if (!W_FileHasFolders(wadfiles[numwadfiles - 1]))
-		luaL_error(L, "dofile() only works with PK3 files");
+		luaL_error(L, "dofile() only works with PK3 files and folders");
 
 	snprintf(fullfilename, sizeof(fullfilename), "Lua/%s", filename);
 	lumpnum = W_CheckNumForFullNamePK3(fullfilename, numwadfiles - 1, 0);
 	if (lumpnum == INT16_MAX)
 		luaL_error(L, "can't find script " LUA_QS, fullfilename);
 
-	LUA_LoadLump(numwadfiles - 1, lumpnum, false);
+	LUA_DoLump(numwadfiles - 1, lumpnum, false);
 
 	return lua_gettop(L) - n;
 }
 
+// Edited to load PK3 entries instead
+static int luaB_loadfile (lua_State *L) {
+  const char *filename = luaL_checkstring(L, 1);
+  char fullfilename[256];
+  UINT16 lumpnum;
+
+	if (!W_FileHasFolders(wadfiles[numwadfiles - 1]))
+    luaL_error(L, "loadfile() only works with PK3 files and folders");
+
+  snprintf(fullfilename, sizeof(fullfilename), "Lua/%s", filename);
+  lumpnum = W_CheckNumForFullNamePK3(fullfilename, numwadfiles - 1, 0);
+  if (lumpnum == INT16_MAX)
+    luaL_error(L, "can't find script " LUA_QS, fullfilename);
+
+  LUA_LoadLump(numwadfiles - 1, lumpnum);
+
+  return 1;
+}
 
 static int luaB_assert (lua_State *L) {
   luaL_checkany(L, 1);
@@ -406,6 +424,7 @@ static const luaL_Reg base_funcs[] = {
   {"collectgarbage", luaB_collectgarbage},
   {"error", luaB_error},
   {"dofile", luaB_dofile},
+  {"loadfile", luaB_loadfile},
   {"gcinfo", luaB_gcinfo},
   {"getfenv", luaB_getfenv},
   {"getmetatable", luaB_getmetatable},
diff --git a/src/blua/liolib.c b/src/blua/liolib.c
index 545f9c144c3ba798c96609e4bcb0e6ca22f1f79e..6b44e2eb8d484597588736e1e074a946be8c12ca 100644
--- a/src/blua/liolib.c
+++ b/src/blua/liolib.c
@@ -19,7 +19,8 @@
 #include "lualib.h"
 #include "../i_system.h"
 #include "../g_game.h"
-#include "../d_netfil.h"
+#include "../netcode/d_netfil.h"
+#include "../netcode/net_command.h"
 #include "../lua_libs.h"
 #include "../byteptr.h"
 #include "../lua_script.h"
diff --git a/src/command.c b/src/command.c
index e1a43522da9946fe73b10efcf4f250008318fb50..d41a551538fa222c85b0919829987f36c9810846 100644
--- a/src/command.c
+++ b/src/command.c
@@ -28,11 +28,12 @@
 #include "byteptr.h"
 #include "p_saveg.h"
 #include "g_game.h" // for player_names
-#include "d_netcmd.h"
+#include "netcode/d_netcmd.h"
+#include "netcode/net_command.h"
 #include "hu_stuff.h"
 #include "p_setup.h"
 #include "lua_script.h"
-#include "d_netfil.h" // findfile
+#include "netcode/d_netfil.h" // findfile
 #include "r_data.h" // Color_cons_t
 #include "d_main.h" // D_IsPathAllowed
 
@@ -50,9 +51,11 @@ static void COM_CEchoDuration_f(void);
 static void COM_Exec_f(void);
 static void COM_Wait_f(void);
 static void COM_Help_f(void);
+static void COM_Find_f(void);
 static void COM_Toggle_f(void);
 static void COM_Add_f(void);
 
+
 static void CV_EnforceExecVersion(void);
 static boolean CV_FilterVarByVersion(consvar_t *v, const char *valstr);
 static boolean CV_Command(void);
@@ -343,6 +346,7 @@ void COM_Init(void)
 	COM_AddCommand("exec", COM_Exec_f, 0);
 	COM_AddCommand("wait", COM_Wait_f, 0);
 	COM_AddCommand("help", COM_Help_f, COM_LUA);
+	COM_AddCommand("find", COM_Find_f, COM_LUA);
 	COM_AddCommand("toggle", COM_Toggle_f, COM_LUA);
 	COM_AddCommand("add", COM_Add_f, COM_LUA);
 	RegisterNetXCmd(XD_NETVAR, Got_NetVar);
@@ -878,7 +882,7 @@ static void COM_Help_f(void)
 			boolean floatmode = false;
 			const char *cvalue = NULL;
 			CONS_Printf("\x82""Variable %s:\n", cvar->name);
-			CONS_Printf(M_GetText("  flags :"));
+			CONS_Printf(M_GetText("  flags: "));
 			if (cvar->flags & CV_SAVE)
 				CONS_Printf("AUTOSAVE ");
 			if (cvar->flags & CV_FLOAT)
@@ -975,31 +979,8 @@ static void COM_Help_f(void)
 				return;
 			}
 
-			CONS_Printf("No exact match, searching...\n");
-
-			// variables
-			CONS_Printf("\x82""Variables:\n");
-			for (cvar = consvar_vars; cvar; cvar = cvar->next)
-			{
-				if ((cvar->flags & CV_NOSHOWHELP) || (!strstr(cvar->name, help)))
-					continue;
-				CONS_Printf("%s ", cvar->name);
-				i++;
-			}
-
-			// commands
-			CONS_Printf("\x82""\nCommands:\n");
-			for (cmd = com_commands; cmd; cmd = cmd->next)
-			{
-				if (!strstr(cmd->name, help))
-					continue;
-				CONS_Printf("%s ",cmd->name);
-				i++;
-			}
-
-			CONS_Printf("\x82""\nCheck wiki.srb2.org for more or type help <command or variable>\n");
-
-			CONS_Debug(DBG_GAMELOGIC, "\x87Total : %d\n", i);
+			CONS_Printf("No variable or command named %s", help);
+			CONS_Printf("\x82""\nCheck wiki.srb2.org for more or try typing help without arguments\n");
 		}
 		return;
 	}
@@ -1029,6 +1010,76 @@ static void COM_Help_f(void)
 	}
 }
 
+static void COM_Find_f(void)
+{
+	static char prefix[80];
+	xcommand_t *cmd;
+	consvar_t *cvar;
+	cmdalias_t *alias;
+	const char *match;
+	const char *help;
+	size_t helplen;
+	boolean matchesany;
+
+	if (COM_Argc() != 2)
+	{
+		CONS_Printf(M_GetText("find <text>: Search for variables, commands and aliases containing <text>\n"));
+		return;
+	}
+
+	help = COM_Argv(1);
+	helplen = strlen(help);
+	CONS_Printf("\x82""Variables:\n");
+	matchesany = false;
+	for (cvar = consvar_vars; cvar; cvar = cvar->next)
+	{
+		if (cvar->flags & CV_NOSHOWHELP)
+			continue;
+		match = strstr(cvar->name, help);
+		if (match != NULL)
+		{
+			memcpy(prefix, cvar->name, match - cvar->name);
+			prefix[match - cvar->name] = '\0';
+			CONS_Printf("  %s\x83%s\x80%s\n", prefix, help, &match[helplen]);
+			matchesany = true;
+		}
+	}
+	if (!matchesany)
+		CONS_Printf("  (none)\n");
+
+	CONS_Printf("\x82""Commands:\n");
+	matchesany = false;
+	for (cmd = com_commands; cmd; cmd = cmd->next)
+	{
+		match = strstr(cmd->name, help);
+		if (match != NULL)
+		{
+			memcpy(prefix, cmd->name, match - cmd->name);
+			prefix[match - cmd->name] = '\0';
+			CONS_Printf("  %s\x83%s\x80%s\n", prefix, help, &match[helplen]);
+			matchesany = true;
+		}
+	}
+	if (!matchesany)
+		CONS_Printf("  (none)\n");
+
+	CONS_Printf("\x82""Aliases:\n");
+	matchesany = false;
+	for (alias = com_alias; alias; alias = alias->next)
+	{
+		match = strstr(alias->name, help);
+		if (match != NULL)
+		{
+			memcpy(prefix, alias->name, match - alias->name);
+			prefix[match - alias->name] = '\0';
+			CONS_Printf("  %s\x83%s\x80%s\n", prefix, help, &match[helplen]);
+			matchesany = true;
+		}
+	}
+	if (!matchesany)
+		CONS_Printf("  (none)\n");
+}
+
 /** Toggles a console variable. Useful for on/off values.
   *
   * This works on on/off, yes/no values only
diff --git a/src/console.c b/src/console.c
index 21b608ce4df88ac5719cb8c43bb739e314450cbd..7747cc3f0df03cb1a7ba6b9509e28c16f1579524 100644
--- a/src/console.c
+++ b/src/console.c
@@ -220,13 +220,16 @@ static char *bindtable[NUMINPUTS];
 static void CONS_Bind_f(void)
 {
 	size_t na;
+	char *newcmd;
+	//size_t newlen = 0;
+	unsigned int i;
 	INT32 key;
 
 	na = COM_Argc();
 
-	if (na != 2 && na != 3)
+	if (na < 2)
 	{
-		CONS_Printf(M_GetText("bind <keyname> [<command>]: create shortcut keys to command(s)\n"));
+		CONS_Printf(M_GetText("bind <keyname> [<command>] [<arg1>] [...]: create shortcut keys to command(s)\n"));
 		CONS_Printf("\x82%s", M_GetText("Bind table :\n"));
 		na = 0;
 		for (key = 0; key < NUMINPUTS; key++)
@@ -250,8 +253,36 @@ static void CONS_Bind_f(void)
 	Z_Free(bindtable[key]);
 	bindtable[key] = NULL;
 
-	if (na == 3)
-		bindtable[key] = Z_StrDup(COM_Argv(2));
+	if (na < 3)
+		return;
+
+	for (i = 2; i < na; ++i)
+	{
+		const char *arg = COM_Argv(i);
+
+		// on the second iteration, and after
+		if (i > 2)
+		{
+			size_t newlen = strlen(bindtable[key]) + strlen(arg) + 1; // new length, allow space for ' ' and '\0'
+			size_t curpos = newcmd - bindtable[key]; // offset from newcmd to original pointer
+
+			newcmd = bindtable[key] = Z_Realloc(bindtable[key], newlen, PU_STATIC, NULL);
+			newcmd += curpos; // reapply offset
+
+			newcmd[0] = ' '; // replace previous '\0' w/ ' '
+			++newcmd; // make sure later strcpy doesnt overwrite ' '
+		}
+		// first iteration
+		else
+			// allocate space for argument and a ' ' or '\0'
+			newcmd = bindtable[key] = Z_Calloc(strlen(arg) + 1, PU_STATIC, NULL);
+
+		// the copy
+		strcpy(newcmd, arg);
+
+		// move window past copied argument for next iteration
+		newcmd += strlen(arg);
+	}
 }
 
 //======================================================================
@@ -543,13 +574,13 @@ static void CON_RecalcSize(void)
 		con_scalefactor = 1;
 		break;
 	case V_SMALLSCALEPATCH:
-		con_scalefactor = vid.smalldupx;
+		con_scalefactor = vid.smalldup;
 		break;
 	case V_MEDSCALEPATCH:
-		con_scalefactor = vid.meddupx;
+		con_scalefactor = vid.meddup;
 		break;
 	default:	// Full scaling
-		con_scalefactor = vid.dupx;
+		con_scalefactor = vid.dup;
 		break;
 	}
 
@@ -667,7 +698,7 @@ static void CON_MoveConsole(void)
 	}
 
 	// Not instant - Increment fracmovement fractionally
-	fracmovement += FixedMul(cons_speed.value*vid.fdupy, renderdeltatics);
+	fracmovement += FixedMul(cons_speed.value*vid.fdup, renderdeltatics);
 
 	if (con_curlines < con_destlines) // Move the console downwards
 	{
@@ -921,7 +952,8 @@ boolean CON_Responder(event_t *ev)
 	static UINT8 consdown = false; // console is treated differently due to rare usage
 
 	// sequential completions a la 4dos
-	static char completion[80];
+	static char completioncmd[80 + sizeof("find ")] = "find ";
+	static char *completion = &completioncmd[sizeof("find ")-1];
 
 	static INT32 skips;
 
@@ -936,7 +968,7 @@ boolean CON_Responder(event_t *ev)
 		return false;
 
 	// let go keyup events, don't eat them
-	if (ev->type != ev_keydown && ev->type != ev_console)
+	if (ev->type != ev_keydown && ev->type != ev_text && ev->type != ev_console)
 	{
 		if (ev->key == gamecontrol[GC_CONSOLE][0] || ev->key == gamecontrol[GC_CONSOLE][1])
 			consdown = false;
@@ -951,7 +983,7 @@ boolean CON_Responder(event_t *ev)
 		if (modeattacking || metalrecording || marathonmode)
 			return false;
 
-		if (key == gamecontrol[GC_CONSOLE][0] || key == gamecontrol[GC_CONSOLE][1])
+		if ((key == gamecontrol[GC_CONSOLE][0] || key == gamecontrol[GC_CONSOLE][1]) && !shiftdown)
 		{
 			if (consdown) // ignore repeat
 				return true;
@@ -963,7 +995,7 @@ boolean CON_Responder(event_t *ev)
 		// check other keys only if console prompt is active
 		if (!consoleready && key < NUMINPUTS) // metzgermeister: boundary check!!
 		{
-			if (! menuactive && bindtable[key])
+			if (ev->type == ev_keydown && !menuactive && bindtable[key])
 			{
 				COM_BufAddText(bindtable[key]);
 				COM_BufAddText("\n");
@@ -980,6 +1012,13 @@ boolean CON_Responder(event_t *ev)
 		}
 	}
 
+	if (ev->type == ev_text)
+	{
+		if (!consoletoggle)
+			CON_InputAddChar(key);
+		return true;
+	}
+
 	// Always eat ctrl/shift/alt if console open, so the menu doesn't get ideas
 	if (key == KEY_LSHIFT || key == KEY_RSHIFT
 	 || key == KEY_LCTRL || key == KEY_RCTRL
@@ -1057,36 +1096,14 @@ boolean CON_Responder(event_t *ev)
 		// show all cvars/commands that match what we have inputted
 		if (key == KEY_TAB)
 		{
-			size_t i, len;
-
 			if (!completion[0])
 			{
 				if (!input_len || input_len >= 40 || strchr(inputlines[inputline], ' '))
 					return true;
 				strcpy(completion, inputlines[inputline]);
 			}
-			len = strlen(completion);
-
-			//first check commands
-			CONS_Printf("\nCommands:\n");
-			for (i = 0, cmd = COM_CompleteCommand(completion, i); cmd; cmd = COM_CompleteCommand(completion, ++i))
-				CONS_Printf("  \x83" "%s" "\x80" "%s\n", completion, cmd+len);
-			if (i == 0) CONS_Printf("  (none)\n");
-
-			//now we move on to CVARs
-			CONS_Printf("Variables:\n");
-			for (i = 0, cmd = CV_CompleteVar(completion, i); cmd; cmd = CV_CompleteVar(completion, ++i))
-				CONS_Printf("  \x83" "%s" "\x80" "%s\n", completion, cmd+len);
-			if (i == 0) CONS_Printf("  (none)\n");
-
-			//and finally aliases
-			CONS_Printf("Aliases:\n");
-			for (i = 0, cmd = COM_CompleteAlias(completion, i); cmd; cmd = COM_CompleteAlias(completion, ++i))
-				CONS_Printf("  \x83" "%s" "\x80" "%s\n", completion, cmd+len);
-			if (i == 0) CONS_Printf("  (none)\n");
-
+			COM_BufInsertText(completioncmd);
 			completion[0] = 0;
-
 			return true;
 		}
 		// ---
@@ -1316,21 +1333,12 @@ boolean CON_Responder(event_t *ev)
 	else if (key == KEY_KPADSLASH)
 		key = '/';
 
-	if (key >= 'a' && key <= 'z')
-	{
-		if (capslock ^ shiftdown)
-			key = shiftxform[key];
-	}
-	else if (shiftdown)
-		key = shiftxform[key];
-
 	// enter a char into the command prompt
 	if (key < 32 || key > 127)
 		return true;
 
 	if (input_sel != input_cur)
 		CON_InputDelSelection();
-	CON_InputAddChar(key);
 
 	return true;
 }
@@ -1764,9 +1772,9 @@ static void CON_DrawBackpic(void)
 	con_backpic = W_CachePatchNum(piclump, PU_PATCH);
 
 	// Center the backpic, and draw a vertically cropped patch.
-	w = (con_backpic->width * vid.dupx);
+	w = con_backpic->width * vid.dup;
 	x = (vid.width / 2) - (w / 2);
-	h = con_curlines/vid.dupy;
+	h = con_curlines/vid.dup;
 
 	// If the patch doesn't fill the entire screen,
 	// then fill the sides with a solid color.
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
deleted file mode 100755
index 1ec9cf1e94b9fcbe2c135109cd4fb52031945620..0000000000000000000000000000000000000000
--- a/src/d_clisrv.c
+++ /dev/null
@@ -1,5840 +0,0 @@
-// SONIC ROBO BLAST 2
-//-----------------------------------------------------------------------------
-// Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 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  d_clisrv.c
-/// \brief SRB2 Network game communication and protocol, all OS independent parts.
-
-#include <time.h>
-#ifdef __GNUC__
-#include <unistd.h> //for unlink
-#endif
-
-#include "i_time.h"
-#include "i_net.h"
-#include "i_system.h"
-#include "i_video.h"
-#include "d_net.h"
-#include "d_main.h"
-#include "g_game.h"
-#include "st_stuff.h"
-#include "hu_stuff.h"
-#include "keys.h"
-#include "g_input.h" // JOY1
-#include "m_menu.h"
-#include "console.h"
-#include "d_netfil.h"
-#include "byteptr.h"
-#include "p_saveg.h"
-#include "z_zone.h"
-#include "p_local.h"
-#include "m_misc.h"
-#include "am_map.h"
-#include "m_random.h"
-#include "mserv.h"
-#include "y_inter.h"
-#include "r_local.h"
-#include "m_argv.h"
-#include "p_setup.h"
-#include "lzf.h"
-#include "lua_script.h"
-#include "lua_hook.h"
-#include "lua_libs.h"
-#include "md5.h"
-#include "m_perfstats.h"
-
-// aaaaaa
-#include "i_joy.h"
-
-#ifndef NONET
-// cl loading screen
-#include "v_video.h"
-#include "f_finale.h"
-#endif
-
-//
-// NETWORKING
-//
-// gametic is the tic about to (or currently being) run
-// Server:
-//   maketic is the tic that hasn't had control made for it yet
-//   nettics is the tic for each node
-//   firstticstosend is the lowest value of nettics
-// Client:
-//   neededtic is the tic needed by the client to run the game
-//   firstticstosend is used to optimize a condition
-// Normally maketic >= gametic > 0
-
-#define PREDICTIONQUEUE BACKUPTICS
-#define PREDICTIONMASK (PREDICTIONQUEUE-1)
-#define MAX_REASONLENGTH 30
-
-boolean server = true; // true or false but !server == client
-#define client (!server)
-boolean nodownload = false;
-boolean serverrunning = false;
-INT32 serverplayer = 0;
-char motd[254], server_context[8]; // Message of the Day, Unique Context (even without Mumble support)
-
-// Server specific vars
-UINT8 playernode[MAXPLAYERS];
-char playeraddress[MAXPLAYERS][64];
-
-// Minimum timeout for sending the savegame
-// The actual timeout will be longer depending on the savegame length
-tic_t jointimeout = (10*TICRATE);
-static boolean sendingsavegame[MAXNETNODES]; // Are we sending the savegame?
-static boolean resendingsavegame[MAXNETNODES]; // Are we resending the savegame?
-static tic_t savegameresendcooldown[MAXNETNODES]; // How long before we can resend again?
-static tic_t freezetimeout[MAXNETNODES]; // Until when can this node freeze the server before getting a timeout?
-
-// Incremented by cv_joindelay when a client joins, decremented each tic.
-// If higher than cv_joindelay * 2 (3 joins in a short timespan), joins are temporarily disabled.
-static tic_t joindelay = 0;
-
-UINT16 pingmeasurecount = 1;
-UINT32 realpingtable[MAXPLAYERS]; //the base table of ping where an average will be sent to everyone.
-UINT32 playerpingtable[MAXPLAYERS]; //table of player latency values.
-SINT8 nodetoplayer[MAXNETNODES];
-SINT8 nodetoplayer2[MAXNETNODES]; // say the numplayer for this node if any (splitscreen)
-UINT8 playerpernode[MAXNETNODES]; // used specialy for scplitscreen
-boolean nodeingame[MAXNETNODES]; // set false as nodes leave game
-tic_t servermaxping = 800; // server's max ping. Defaults to 800
-static tic_t nettics[MAXNETNODES]; // what tic the client have received
-static tic_t supposedtics[MAXNETNODES]; // nettics prevision for smaller packet
-static UINT8 nodewaiting[MAXNETNODES];
-static tic_t firstticstosend; // min of the nettics
-static tic_t tictoclear = 0; // optimize d_clearticcmd
-static tic_t maketic;
-
-static INT16 consistancy[BACKUPTICS];
-
-static UINT8 player_joining = false;
-UINT8 hu_redownloadinggamestate = 0;
-
-// true when a player is connecting or disconnecting so that the gameplay has stopped in its tracks
-boolean hu_stopped = false;
-
-consvar_t cv_dedicatedidletime = CVAR_INIT ("dedicatedidletime", "10", CV_SAVE, CV_Unsigned, NULL);
-
-UINT8 adminpassmd5[16];
-boolean adminpasswordset = false;
-
-// Client specific
-static ticcmd_t localcmds;
-static ticcmd_t localcmds2;
-static boolean cl_packetmissed;
-// here it is for the secondary local player (splitscreen)
-static UINT8 mynode; // my address pointofview server
-static boolean cl_redownloadinggamestate = false;
-
-static UINT8 localtextcmd[MAXTEXTCMD];
-static UINT8 localtextcmd2[MAXTEXTCMD]; // splitscreen
-static tic_t neededtic;
-SINT8 servernode = 0; // the number of the server node
-
-/// \brief do we accept new players?
-/// \todo WORK!
-boolean acceptnewnode = true;
-
-static boolean serverisfull = false; //lets us be aware if the server was full after we check files, but before downloading, so we can ask if the user still wants to download or not
-static tic_t firstconnectattempttime = 0;
-
-// engine
-
-// Must be a power of two
-#define TEXTCMD_HASH_SIZE 4
-
-typedef struct textcmdplayer_s
-{
-	INT32 playernum;
-	UINT8 cmd[MAXTEXTCMD];
-	struct textcmdplayer_s *next;
-} textcmdplayer_t;
-
-typedef struct textcmdtic_s
-{
-	tic_t tic;
-	textcmdplayer_t *playercmds[TEXTCMD_HASH_SIZE];
-	struct textcmdtic_s *next;
-} textcmdtic_t;
-
-ticcmd_t netcmds[BACKUPTICS][MAXPLAYERS];
-static textcmdtic_t *textcmds[TEXTCMD_HASH_SIZE] = {NULL};
-
-
-consvar_t cv_showjoinaddress = CVAR_INIT ("showjoinaddress", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
-
-static CV_PossibleValue_t playbackspeed_cons_t[] = {{1, "MIN"}, {10, "MAX"}, {0, NULL}};
-consvar_t cv_playbackspeed = CVAR_INIT ("playbackspeed", "1", 0, playbackspeed_cons_t, NULL);
-
-static inline void *G_DcpyTiccmd(void* dest, const ticcmd_t* src, const size_t n)
-{
-	const size_t d = n / sizeof(ticcmd_t);
-	const size_t r = n % sizeof(ticcmd_t);
-	UINT8 *ret = dest;
-
-	if (r)
-		M_Memcpy(dest, src, n);
-	else if (d)
-		G_MoveTiccmd(dest, src, d);
-	return ret+n;
-}
-
-static inline void *G_ScpyTiccmd(ticcmd_t* dest, void* src, const size_t n)
-{
-	const size_t d = n / sizeof(ticcmd_t);
-	const size_t r = n % sizeof(ticcmd_t);
-	UINT8 *ret = src;
-
-	if (r)
-		M_Memcpy(dest, src, n);
-	else if (d)
-		G_MoveTiccmd(dest, src, d);
-	return ret+n;
-}
-
-
-
-// Some software don't support largest packet
-// (original sersetup, not exactely, but the probability of sending a packet
-// of 512 bytes is like 0.1)
-UINT16 software_MAXPACKETLENGTH;
-
-/** Guesses the full value of a tic from its lowest byte, for a specific node
-  *
-  * \param low The lowest byte of the tic value
-  * \param node The node to deduce the tic for
-  * \return The full tic value
-  *
-  */
-tic_t ExpandTics(INT32 low, INT32 node)
-{
-	INT32 delta;
-
-	delta = low - (nettics[node] & UINT8_MAX);
-
-	if (delta >= -64 && delta <= 64)
-		return (nettics[node] & ~UINT8_MAX) + low;
-	else if (delta > 64)
-		return (nettics[node] & ~UINT8_MAX) - 256 + low;
-	else //if (delta < -64)
-		return (nettics[node] & ~UINT8_MAX) + 256 + low;
-}
-
-// -----------------------------------------------------------------
-// Some extra data function for handle textcmd buffer
-// -----------------------------------------------------------------
-
-static void (*listnetxcmd[MAXNETXCMD])(UINT8 **p, INT32 playernum);
-
-void RegisterNetXCmd(netxcmd_t id, void (*cmd_f)(UINT8 **p, INT32 playernum))
-{
-#ifdef PARANOIA
-	if (id >= MAXNETXCMD)
-		I_Error("Command id %d too big", id);
-	if (listnetxcmd[id] != 0)
-		I_Error("Command id %d already used", id);
-#endif
-	listnetxcmd[id] = cmd_f;
-}
-
-void SendNetXCmd(netxcmd_t id, const void *param, size_t nparam)
-{
-	if (localtextcmd[0]+2+nparam > MAXTEXTCMD)
-	{
-		// for future reference: if (cv_debug) != debug disabled.
-		CONS_Alert(CONS_ERROR, M_GetText("NetXCmd buffer full, cannot add netcmd %d! (size: %d, needed: %s)\n"), id, localtextcmd[0], sizeu1(nparam));
-		return;
-	}
-	localtextcmd[0]++;
-	localtextcmd[localtextcmd[0]] = (UINT8)id;
-	if (param && nparam)
-	{
-		M_Memcpy(&localtextcmd[localtextcmd[0]+1], param, nparam);
-		localtextcmd[0] = (UINT8)(localtextcmd[0] + (UINT8)nparam);
-	}
-}
-
-// splitscreen player
-void SendNetXCmd2(netxcmd_t id, const void *param, size_t nparam)
-{
-	if (localtextcmd2[0]+2+nparam > MAXTEXTCMD)
-	{
-		I_Error("No more place in the buffer for netcmd %d\n",id);
-		return;
-	}
-	localtextcmd2[0]++;
-	localtextcmd2[localtextcmd2[0]] = (UINT8)id;
-	if (param && nparam)
-	{
-		M_Memcpy(&localtextcmd2[localtextcmd2[0]+1], param, nparam);
-		localtextcmd2[0] = (UINT8)(localtextcmd2[0] + (UINT8)nparam);
-	}
-}
-
-UINT8 GetFreeXCmdSize(void)
-{
-	// -1 for the size and another -1 for the ID.
-	return (UINT8)(localtextcmd[0] - 2);
-}
-
-// Frees all textcmd memory for the specified tic
-static void D_FreeTextcmd(tic_t tic)
-{
-	textcmdtic_t **tctprev = &textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
-	textcmdtic_t *textcmdtic = *tctprev;
-
-	while (textcmdtic && textcmdtic->tic != tic)
-	{
-		tctprev = &textcmdtic->next;
-		textcmdtic = textcmdtic->next;
-	}
-
-	if (textcmdtic)
-	{
-		INT32 i;
-
-		// Remove this tic from the list.
-		*tctprev = textcmdtic->next;
-
-		// Free all players.
-		for (i = 0; i < TEXTCMD_HASH_SIZE; i++)
-		{
-			textcmdplayer_t *textcmdplayer = textcmdtic->playercmds[i];
-
-			while (textcmdplayer)
-			{
-				textcmdplayer_t *tcpnext = textcmdplayer->next;
-				Z_Free(textcmdplayer);
-				textcmdplayer = tcpnext;
-			}
-		}
-
-		// Free this tic's own memory.
-		Z_Free(textcmdtic);
-	}
-}
-
-// Gets the buffer for the specified ticcmd, or NULL if there isn't one
-static UINT8* D_GetExistingTextcmd(tic_t tic, INT32 playernum)
-{
-	textcmdtic_t *textcmdtic = textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
-	while (textcmdtic && textcmdtic->tic != tic) textcmdtic = textcmdtic->next;
-
-	// Do we have an entry for the tic? If so, look for player.
-	if (textcmdtic)
-	{
-		textcmdplayer_t *textcmdplayer = textcmdtic->playercmds[playernum & (TEXTCMD_HASH_SIZE - 1)];
-		while (textcmdplayer && textcmdplayer->playernum != playernum) textcmdplayer = textcmdplayer->next;
-
-		if (textcmdplayer) return textcmdplayer->cmd;
-	}
-
-	return NULL;
-}
-
-// Gets the buffer for the specified ticcmd, creating one if necessary
-static UINT8* D_GetTextcmd(tic_t tic, INT32 playernum)
-{
-	textcmdtic_t *textcmdtic = textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
-	textcmdtic_t **tctprev = &textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
-	textcmdplayer_t *textcmdplayer, **tcpprev;
-
-	// Look for the tic.
-	while (textcmdtic && textcmdtic->tic != tic)
-	{
-		tctprev = &textcmdtic->next;
-		textcmdtic = textcmdtic->next;
-	}
-
-	// If we don't have an entry for the tic, make it.
-	if (!textcmdtic)
-	{
-		textcmdtic = *tctprev = Z_Calloc(sizeof (textcmdtic_t), PU_STATIC, NULL);
-		textcmdtic->tic = tic;
-	}
-
-	tcpprev = &textcmdtic->playercmds[playernum & (TEXTCMD_HASH_SIZE - 1)];
-	textcmdplayer = *tcpprev;
-
-	// Look for the player.
-	while (textcmdplayer && textcmdplayer->playernum != playernum)
-	{
-		tcpprev = &textcmdplayer->next;
-		textcmdplayer = textcmdplayer->next;
-	}
-
-	// If we don't have an entry for the player, make it.
-	if (!textcmdplayer)
-	{
-		textcmdplayer = *tcpprev = Z_Calloc(sizeof (textcmdplayer_t), PU_STATIC, NULL);
-		textcmdplayer->playernum = playernum;
-	}
-
-	return textcmdplayer->cmd;
-}
-
-static void ExtraDataTicker(void)
-{
-	INT32 i;
-
-	for (i = 0; i < MAXPLAYERS; i++)
-		if (playeringame[i] || i == 0)
-		{
-			UINT8 *bufferstart = D_GetExistingTextcmd(gametic, i);
-
-			if (bufferstart)
-			{
-				UINT8 *curpos = bufferstart;
-				UINT8 *bufferend = &curpos[curpos[0]+1];
-
-				curpos++;
-				while (curpos < bufferend)
-				{
-					if (*curpos < MAXNETXCMD && listnetxcmd[*curpos])
-					{
-						const UINT8 id = *curpos;
-						curpos++;
-						DEBFILE(va("executing x_cmd %s ply %u ", netxcmdnames[id - 1], i));
-						(listnetxcmd[id])(&curpos, i);
-						DEBFILE("done\n");
-					}
-					else
-					{
-						if (server)
-						{
-							SendKick(i, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
-							DEBFILE(va("player %d kicked [gametic=%u] reason as follows:\n", i, gametic));
-						}
-						CONS_Alert(CONS_WARNING, M_GetText("Got unknown net command [%s]=%d (max %d)\n"), sizeu1(curpos - bufferstart), *curpos, bufferstart[0]);
-						break;
-					}
-				}
-			}
-		}
-
-	// If you are a client, you can safely forget the net commands for this tic
-	// If you are the server, you need to remember them until every client has been acknowledged,
-	// because if you need to resend a PT_SERVERTICS packet, you will need to put the commands in it
-	if (client)
-		D_FreeTextcmd(gametic);
-}
-
-static void D_Clearticcmd(tic_t tic)
-{
-	INT32 i;
-
-	D_FreeTextcmd(tic);
-
-	for (i = 0; i < MAXPLAYERS; i++)
-		netcmds[tic%BACKUPTICS][i].angleturn = 0;
-
-	DEBFILE(va("clear tic %5u (%2u)\n", tic, tic%BACKUPTICS));
-}
-
-void D_ResetTiccmds(void)
-{
-	INT32 i;
-
-	memset(&localcmds, 0, sizeof(ticcmd_t));
-	memset(&localcmds2, 0, sizeof(ticcmd_t));
-
-	// Reset the net command list
-	for (i = 0; i < TEXTCMD_HASH_SIZE; i++)
-		while (textcmds[i])
-			D_Clearticcmd(textcmds[i]->tic);
-}
-
-void SendKick(UINT8 playernum, UINT8 msg)
-{
-	UINT8 buf[2];
-
-	if (!(server && cv_rejointimeout.value))
-		msg &= ~KICK_MSG_KEEP_BODY;
-
-	buf[0] = playernum;
-	buf[1] = msg;
-	SendNetXCmd(XD_KICK, &buf, 2);
-}
-
-// -----------------------------------------------------------------
-// end of extra data function
-// -----------------------------------------------------------------
-
-// -----------------------------------------------------------------
-// extra data function for lmps
-// -----------------------------------------------------------------
-
-// if extradatabit is set, after the ziped tic you find this:
-//
-//   type   |  description
-// ---------+--------------
-//   byte   | size of the extradata
-//   byte   | the extradata (xd) bits: see XD_...
-//            with this byte you know what parameter folow
-// if (xd & XDNAMEANDCOLOR)
-//   byte   | color
-//   char[MAXPLAYERNAME] | name of the player
-// endif
-// if (xd & XD_WEAPON_PREF)
-//   byte   | original weapon switch: boolean, true if use the old
-//          | weapon switch methode
-//   char[NUMWEAPONS] | the weapon switch priority
-//   byte   | autoaim: true if use the old autoaim system
-// endif
-/*boolean AddLmpExtradata(UINT8 **demo_point, INT32 playernum)
-{
-	UINT8 *textcmd = D_GetExistingTextcmd(gametic, playernum);
-
-	if (!textcmd)
-		return false;
-
-	M_Memcpy(*demo_point, textcmd, textcmd[0]+1);
-	*demo_point += textcmd[0]+1;
-	return true;
-}
-
-void ReadLmpExtraData(UINT8 **demo_pointer, INT32 playernum)
-{
-	UINT8 nextra;
-	UINT8 *textcmd;
-
-	if (!demo_pointer)
-		return;
-
-	textcmd = D_GetTextcmd(gametic, playernum);
-	nextra = **demo_pointer;
-	M_Memcpy(textcmd, *demo_pointer, nextra + 1);
-	// increment demo pointer
-	*demo_pointer += nextra + 1;
-}*/
-
-// -----------------------------------------------------------------
-// end extra data function for lmps
-// -----------------------------------------------------------------
-
-static INT16 Consistancy(void);
-
-typedef enum
-{
-	CL_SEARCHING,
-	CL_CHECKFILES,
-	CL_DOWNLOADFILES,
-	CL_ASKJOIN,
-	CL_LOADFILES,
-	CL_WAITJOINRESPONSE,
-	CL_DOWNLOADSAVEGAME,
-	CL_CONNECTED,
-	CL_ABORTED,
-	CL_ASKFULLFILELIST,
-	CL_CONFIRMCONNECT
-} cl_mode_t;
-
-static void GetPackets(void);
-
-static cl_mode_t cl_mode = CL_SEARCHING;
-
-static UINT16 cl_lastcheckedfilecount = 0;	// used for full file list
-
-#ifndef NONET
-#define SNAKE_SPEED 5
-
-#define SNAKE_NUM_BLOCKS_X 20
-#define SNAKE_NUM_BLOCKS_Y 10
-#define SNAKE_BLOCK_SIZE 12
-#define SNAKE_BORDER_SIZE 12
-
-#define SNAKE_MAP_WIDTH  (SNAKE_NUM_BLOCKS_X * SNAKE_BLOCK_SIZE)
-#define SNAKE_MAP_HEIGHT (SNAKE_NUM_BLOCKS_Y * SNAKE_BLOCK_SIZE)
-
-#define SNAKE_LEFT_X ((BASEVIDWIDTH - SNAKE_MAP_WIDTH) / 2 - SNAKE_BORDER_SIZE)
-#define SNAKE_RIGHT_X (SNAKE_LEFT_X + SNAKE_MAP_WIDTH + SNAKE_BORDER_SIZE * 2 - 1)
-#define SNAKE_BOTTOM_Y (BASEVIDHEIGHT - 48)
-#define SNAKE_TOP_Y (SNAKE_BOTTOM_Y - SNAKE_MAP_HEIGHT - SNAKE_BORDER_SIZE * 2 + 1)
-
-enum snake_bonustype_s {
-	SNAKE_BONUS_NONE = 0,
-	SNAKE_BONUS_SLOW,
-	SNAKE_BONUS_FAST,
-	SNAKE_BONUS_GHOST,
-	SNAKE_BONUS_NUKE,
-	SNAKE_BONUS_SCISSORS,
-	SNAKE_BONUS_REVERSE,
-	SNAKE_BONUS_EGGMAN,
-	SNAKE_NUM_BONUSES,
-};
-
-static const char *snake_bonuspatches[] = {
-	NULL,
-	"DL_SLOW",
-	"TVSSC0",
-	"TVIVC0",
-	"TVARC0",
-	"DL_SCISSORS",
-	"TVRCC0",
-	"TVEGC0",
-};
-
-static const char *snake_backgrounds[] = {
-	"RVPUMICF",
-	"FRSTRCKF",
-	"TAR",
-	"MMFLRB4",
-	"RVDARKF1",
-	"RVZWALF1",
-	"RVZWALF4",
-	"RVZWALF5",
-	"RVZGRS02",
-	"RVZGRS04",
-};
-
-typedef struct snake_s
-{
-	boolean paused;
-	boolean pausepressed;
-	tic_t time;
-	tic_t nextupdate;
-	boolean gameover;
-	UINT8 background;
-
-	UINT16 snakelength;
-	enum snake_bonustype_s snakebonus;
-	tic_t snakebonustime;
-	UINT8 snakex[SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y];
-	UINT8 snakey[SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y];
-	UINT8 snakedir[SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y];
-
-	UINT8 applex;
-	UINT8 appley;
-
-	enum snake_bonustype_s bonustype;
-	UINT8 bonusx;
-	UINT8 bonusy;
-} snake_t;
-
-static snake_t *snake = NULL;
-
-static void Snake_Initialise(void)
-{
-	if (!snake)
-		snake = malloc(sizeof(snake_t));
-
-	snake->paused = false;
-	snake->pausepressed = false;
-	snake->time = 0;
-	snake->nextupdate = SNAKE_SPEED;
-	snake->gameover = false;
-	snake->background = M_RandomKey(sizeof(snake_backgrounds) / sizeof(*snake_backgrounds));
-
-	snake->snakelength = 1;
-	snake->snakebonus = SNAKE_BONUS_NONE;
-	snake->snakex[0] = M_RandomKey(SNAKE_NUM_BLOCKS_X);
-	snake->snakey[0] = M_RandomKey(SNAKE_NUM_BLOCKS_Y);
-	snake->snakedir[0] = 0;
-	snake->snakedir[1] = 0;
-
-	snake->applex = M_RandomKey(SNAKE_NUM_BLOCKS_X);
-	snake->appley = M_RandomKey(SNAKE_NUM_BLOCKS_Y);
-
-	snake->bonustype = SNAKE_BONUS_NONE;
-}
-
-static UINT8 Snake_GetOppositeDir(UINT8 dir)
-{
-	if (dir == 1 || dir == 3)
-		return dir + 1;
-	else if (dir == 2 || dir == 4)
-		return dir - 1;
-	else
-		return 12 + 5 - dir;
-}
-
-event_t *snakejoyevents[MAXEVENTS];
-UINT16 joyeventcount = 0;
-
-// I'm screaming the hack is clean - ashi
-static boolean Snake_Joy_Grabber(event_t *ev)
-{
-	if (ev->type == ev_joystick  && ev->key == 0)
-	{
-		snakejoyevents[joyeventcount] = ev;
-		joyeventcount++;
-		return true;
-	}
-	else
-		return false;
-}
-
-static void Snake_FindFreeSlot(UINT8 *freex, UINT8 *freey, UINT8 headx, UINT8 heady)
-{
-	UINT8 x, y;
-	UINT16 i;
-
-	do
-	{
-		x = M_RandomKey(SNAKE_NUM_BLOCKS_X);
-		y = M_RandomKey(SNAKE_NUM_BLOCKS_Y);
-
-		for (i = 0; i < snake->snakelength; i++)
-			if (x == snake->snakex[i] && y == snake->snakey[i])
-				break;
-	} while (i < snake->snakelength || (x == headx && y == heady)
-		|| (x == snake->applex && y == snake->appley)
-		|| (snake->bonustype != SNAKE_BONUS_NONE && x == snake->bonusx && y == snake->bonusy));
-
-	*freex = x;
-	*freey = y;
-}
-
-static void Snake_Handle(void)
-{
-	UINT8 x, y;
-	UINT8 oldx, oldy;
-	UINT16 i;
-	UINT16 j;
-	UINT16 joystate = 0;
-	static INT32 pjoyx = 0, pjoyy = 0;
-
-	// Handle retry
-	if (snake->gameover && (PLAYER1INPUTDOWN(GC_JUMP) || gamekeydown[KEY_ENTER]))
-	{
-		Snake_Initialise();
-		snake->pausepressed = true; // Avoid accidental pause on respawn
-	}
-
-	// Handle pause
-	if (PLAYER1INPUTDOWN(GC_PAUSE) || gamekeydown[KEY_ENTER])
-	{
-		if (!snake->pausepressed)
-			snake->paused = !snake->paused;
-		snake->pausepressed = true;
-	}
-	else
-		snake->pausepressed = false;
-
-	if (snake->paused)
-		return;
-
-	snake->time++;
-
-	x = snake->snakex[0];
-	y = snake->snakey[0];
-	oldx = snake->snakex[1];
-	oldy = snake->snakey[1];
-
-	// process the input events in here dear lord
-	for (j = 0; j < joyeventcount; j++)
-	{
-		event_t *ev = snakejoyevents[j];
-		const INT32 jdeadzone = (JOYAXISRANGE * cv_digitaldeadzone.value) / FRACUNIT;
-		if (ev->y != INT32_MAX)
-		{
-			if (Joystick.bGamepadStyle || abs(ev->y) > jdeadzone)
-			{
-				if (ev->y < 0 && pjoyy >= 0)
-					joystate = 1;
-				else if (ev->y > 0 && pjoyy <= 0)
-					joystate = 2;
-				pjoyy = ev->y;
-			}
-			else
-				pjoyy = 0;
-		}
-
-		if (ev->x != INT32_MAX)
-		{
-			if (Joystick.bGamepadStyle || abs(ev->x) > jdeadzone)
-			{
-				if (ev->x < 0 && pjoyx >= 0)
-					joystate = 3;
-				else if (ev->x > 0 && pjoyx <= 0)
-					joystate = 4;
-				pjoyx = ev->x;
-			}
-			else
-				pjoyx = 0;
-		}
-	}
-	joyeventcount = 0;
-
-	// Update direction
-	if (PLAYER1INPUTDOWN(GC_STRAFELEFT) || gamekeydown[KEY_LEFTARROW] || joystate == 3)
-	{
-		if (snake->snakelength < 2 || x <= oldx)
-			snake->snakedir[0] = 1;
-	}
-	else if (PLAYER1INPUTDOWN(GC_STRAFERIGHT) || gamekeydown[KEY_RIGHTARROW] || joystate == 4)
-	{
-		if (snake->snakelength < 2 || x >= oldx)
-			snake->snakedir[0] = 2;
-	}
-	else if (PLAYER1INPUTDOWN(GC_FORWARD) || gamekeydown[KEY_UPARROW] || joystate == 1)
-	{
-		if (snake->snakelength < 2 || y <= oldy)
-			snake->snakedir[0] = 3;
-	}
-	else if (PLAYER1INPUTDOWN(GC_BACKWARD) || gamekeydown[KEY_DOWNARROW] || joystate == 2)
-	{
-		if (snake->snakelength < 2 || y >= oldy)
-			snake->snakedir[0] = 4;
-	}
-
-	if (snake->snakebonustime)
-	{
-		snake->snakebonustime--;
-		if (!snake->snakebonustime)
-			snake->snakebonus = SNAKE_BONUS_NONE;
-	}
-
-	snake->nextupdate--;
-	if (snake->nextupdate)
-		return;
-	if (snake->snakebonus == SNAKE_BONUS_SLOW)
-		snake->nextupdate = SNAKE_SPEED * 2;
-	else if (snake->snakebonus == SNAKE_BONUS_FAST)
-		snake->nextupdate = SNAKE_SPEED * 2 / 3;
-	else
-		snake->nextupdate = SNAKE_SPEED;
-
-	if (snake->gameover)
-		return;
-
-	// Find new position
-	switch (snake->snakedir[0])
-	{
-		case 1:
-			if (x > 0)
-				x--;
-			else
-				snake->gameover = true;
-			break;
-		case 2:
-			if (x < SNAKE_NUM_BLOCKS_X - 1)
-				x++;
-			else
-				snake->gameover = true;
-			break;
-		case 3:
-			if (y > 0)
-				y--;
-			else
-				snake->gameover = true;
-			break;
-		case 4:
-			if (y < SNAKE_NUM_BLOCKS_Y - 1)
-				y++;
-			else
-				snake->gameover = true;
-			break;
-	}
-
-	// Check collision with snake
-	if (snake->snakebonus != SNAKE_BONUS_GHOST)
-		for (i = 1; i < snake->snakelength - 1; i++)
-			if (x == snake->snakex[i] && y == snake->snakey[i])
-			{
-				if (snake->snakebonus == SNAKE_BONUS_SCISSORS)
-				{
-					snake->snakebonus = SNAKE_BONUS_NONE;
-					snake->snakelength = i;
-					S_StartSound(NULL, sfx_adderr);
-				}
-				else
-					snake->gameover = true;
-			}
-
-	if (snake->gameover)
-	{
-		S_StartSound(NULL, sfx_lose);
-		return;
-	}
-
-	// Check collision with apple
-	if (x == snake->applex && y == snake->appley)
-	{
-		if (snake->snakelength + 3 < SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y)
-		{
-			snake->snakelength++;
-			snake->snakex  [snake->snakelength - 1] = snake->snakex  [snake->snakelength - 2];
-			snake->snakey  [snake->snakelength - 1] = snake->snakey  [snake->snakelength - 2];
-			snake->snakedir[snake->snakelength - 1] = snake->snakedir[snake->snakelength - 2];
-		}
-
-		// Spawn new apple
-		Snake_FindFreeSlot(&snake->applex, &snake->appley, x, y);
-
-		// Spawn new bonus
-		if (!(snake->snakelength % 5))
-		{
-			do
-			{
-				snake->bonustype = M_RandomKey(SNAKE_NUM_BONUSES - 1) + 1;
-			} while (snake->snakelength > SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y * 3 / 4
-				&& (snake->bonustype == SNAKE_BONUS_EGGMAN || snake->bonustype == SNAKE_BONUS_FAST || snake->bonustype == SNAKE_BONUS_REVERSE));
-
-			Snake_FindFreeSlot(&snake->bonusx, &snake->bonusy, x, y);
-		}
-
-		S_StartSound(NULL, sfx_s3k6b);
-	}
-
-	if (snake->snakelength > 1 && snake->snakedir[0])
-	{
-		UINT8 dir = snake->snakedir[0];
-
-		oldx = snake->snakex[1];
-		oldy = snake->snakey[1];
-
-		// Move
-		for (i = snake->snakelength - 1; i > 0; i--)
-		{
-			snake->snakex[i] = snake->snakex[i - 1];
-			snake->snakey[i] = snake->snakey[i - 1];
-			snake->snakedir[i] = snake->snakedir[i - 1];
-		}
-
-		// Handle corners
-		if      (x < oldx && dir == 3)
-			dir = 5;
-		else if (x > oldx && dir == 3)
-			dir = 6;
-		else if (x < oldx && dir == 4)
-			dir = 7;
-		else if (x > oldx && dir == 4)
-			dir = 8;
-		else if (y < oldy && dir == 1)
-			dir = 9;
-		else if (y < oldy && dir == 2)
-			dir = 10;
-		else if (y > oldy && dir == 1)
-			dir = 11;
-		else if (y > oldy && dir == 2)
-			dir = 12;
-		snake->snakedir[1] = dir;
-	}
-
-	snake->snakex[0] = x;
-	snake->snakey[0] = y;
-
-	// Check collision with bonus
-	if (snake->bonustype != SNAKE_BONUS_NONE && x == snake->bonusx && y == snake->bonusy)
-	{
-		S_StartSound(NULL, sfx_ncchip);
-
-		switch (snake->bonustype)
-		{
-		case SNAKE_BONUS_SLOW:
-			snake->snakebonus = SNAKE_BONUS_SLOW;
-			snake->snakebonustime = 20 * TICRATE;
-			break;
-		case SNAKE_BONUS_FAST:
-			snake->snakebonus = SNAKE_BONUS_FAST;
-			snake->snakebonustime = 20 * TICRATE;
-			break;
-		case SNAKE_BONUS_GHOST:
-			snake->snakebonus = SNAKE_BONUS_GHOST;
-			snake->snakebonustime = 10 * TICRATE;
-			break;
-		case SNAKE_BONUS_NUKE:
-			for (i = 0; i < snake->snakelength; i++)
-			{
-				snake->snakex  [i] = snake->snakex  [0];
-				snake->snakey  [i] = snake->snakey  [0];
-				snake->snakedir[i] = snake->snakedir[0];
-			}
-
-			S_StartSound(NULL, sfx_bkpoof);
-			break;
-		case SNAKE_BONUS_SCISSORS:
-			snake->snakebonus = SNAKE_BONUS_SCISSORS;
-			snake->snakebonustime = 60 * TICRATE;
-			break;
-		case SNAKE_BONUS_REVERSE:
-			for (i = 0; i < (snake->snakelength + 1) / 2; i++)
-			{
-				UINT16 i2 = snake->snakelength - 1 - i;
-				UINT8 tmpx   = snake->snakex  [i];
-				UINT8 tmpy   = snake->snakey  [i];
-				UINT8 tmpdir = snake->snakedir[i];
-
-				// Swap first segment with last segment
-				snake->snakex  [i] = snake->snakex  [i2];
-				snake->snakey  [i] = snake->snakey  [i2];
-				snake->snakedir[i] = Snake_GetOppositeDir(snake->snakedir[i2]);
-				snake->snakex  [i2] = tmpx;
-				snake->snakey  [i2] = tmpy;
-				snake->snakedir[i2] = Snake_GetOppositeDir(tmpdir);
-			}
-
-			snake->snakedir[0] = 0;
-
-			S_StartSound(NULL, sfx_gravch);
-			break;
-		default:
-			if (snake->snakebonus != SNAKE_BONUS_GHOST)
-			{
-				snake->gameover = true;
-				S_StartSound(NULL, sfx_lose);
-			}
-		}
-
-		snake->bonustype = SNAKE_BONUS_NONE;
-	}
-}
-
-static void Snake_Draw(void)
-{
-	INT16 i;
-
-	// Background
-	V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
-
-	V_DrawFlatFill(
-		SNAKE_LEFT_X + SNAKE_BORDER_SIZE,
-		SNAKE_TOP_Y  + SNAKE_BORDER_SIZE,
-		SNAKE_MAP_WIDTH,
-		SNAKE_MAP_HEIGHT,
-		W_GetNumForName(snake_backgrounds[snake->background])
-	);
-
-	// Borders
-	V_DrawFill(SNAKE_LEFT_X, SNAKE_TOP_Y, SNAKE_BORDER_SIZE + SNAKE_MAP_WIDTH, SNAKE_BORDER_SIZE, 242); // Top
-	V_DrawFill(SNAKE_LEFT_X + SNAKE_BORDER_SIZE + SNAKE_MAP_WIDTH, SNAKE_TOP_Y, SNAKE_BORDER_SIZE, SNAKE_BORDER_SIZE + SNAKE_MAP_HEIGHT, 242); // Right
-	V_DrawFill(SNAKE_LEFT_X + SNAKE_BORDER_SIZE, SNAKE_TOP_Y + SNAKE_BORDER_SIZE + SNAKE_MAP_HEIGHT, SNAKE_BORDER_SIZE + SNAKE_MAP_WIDTH, SNAKE_BORDER_SIZE, 242); // Bottom
-	V_DrawFill(SNAKE_LEFT_X, SNAKE_TOP_Y + SNAKE_BORDER_SIZE, SNAKE_BORDER_SIZE, SNAKE_BORDER_SIZE + SNAKE_MAP_HEIGHT, 242); // Left
-
-	// Apple
-	V_DrawFixedPatch(
-		(SNAKE_LEFT_X + SNAKE_BORDER_SIZE + snake->applex * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2) * FRACUNIT,
-		(SNAKE_TOP_Y  + SNAKE_BORDER_SIZE + snake->appley * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2) * FRACUNIT,
-		FRACUNIT / 4,
-		0,
-		W_CachePatchLongName("DL_APPLE", PU_HUDGFX),
-		NULL
-	);
-
-	// Bonus
-	if (snake->bonustype != SNAKE_BONUS_NONE)
-		V_DrawFixedPatch(
-			(SNAKE_LEFT_X + SNAKE_BORDER_SIZE + snake->bonusx * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2    ) * FRACUNIT,
-			(SNAKE_TOP_Y  + SNAKE_BORDER_SIZE + snake->bonusy * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2 + 4) * FRACUNIT,
-			FRACUNIT / 2,
-			0,
-			W_CachePatchLongName(snake_bonuspatches[snake->bonustype], PU_HUDGFX),
-			NULL
-		);
-
-	// Snake
-	if (!snake->gameover || snake->time % 8 < 8 / 2) // Blink if game over
-	{
-		for (i = snake->snakelength - 1; i >= 0; i--)
-		{
-			const char *patchname;
-			UINT8 dir = snake->snakedir[i];
-
-			if (i == 0) // Head
-			{
-				switch (dir)
-				{
-					case  1: patchname = "DL_SNAKEHEAD_L"; break;
-					case  2: patchname = "DL_SNAKEHEAD_R"; break;
-					case  3: patchname = "DL_SNAKEHEAD_T"; break;
-					case  4: patchname = "DL_SNAKEHEAD_B"; break;
-					default: patchname = "DL_SNAKEHEAD_M";
-				}
-			}
-			else // Body
-			{
-				switch (dir)
-				{
-					case  1: patchname = "DL_SNAKEBODY_L"; break;
-					case  2: patchname = "DL_SNAKEBODY_R"; break;
-					case  3: patchname = "DL_SNAKEBODY_T"; break;
-					case  4: patchname = "DL_SNAKEBODY_B"; break;
-					case  5: patchname = "DL_SNAKEBODY_LT"; break;
-					case  6: patchname = "DL_SNAKEBODY_RT"; break;
-					case  7: patchname = "DL_SNAKEBODY_LB"; break;
-					case  8: patchname = "DL_SNAKEBODY_RB"; break;
-					case  9: patchname = "DL_SNAKEBODY_TL"; break;
-					case 10: patchname = "DL_SNAKEBODY_TR"; break;
-					case 11: patchname = "DL_SNAKEBODY_BL"; break;
-					case 12: patchname = "DL_SNAKEBODY_BR"; break;
-					default: patchname = "DL_SNAKEBODY_B";
-				}
-			}
-
-			V_DrawFixedPatch(
-				(SNAKE_LEFT_X + SNAKE_BORDER_SIZE + snake->snakex[i] * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2) * FRACUNIT,
-				(SNAKE_TOP_Y  + SNAKE_BORDER_SIZE + snake->snakey[i] * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2) * FRACUNIT,
-				i == 0 && dir == 0 ? FRACUNIT / 5 : FRACUNIT / 2,
-				snake->snakebonus == SNAKE_BONUS_GHOST ? V_TRANSLUCENT : 0,
-				W_CachePatchLongName(patchname, PU_HUDGFX),
-				NULL
-			);
-		}
-	}
-
-	// Length
-	V_DrawString(SNAKE_RIGHT_X + 4, SNAKE_TOP_Y, V_MONOSPACE, va("%u", snake->snakelength));
-
-	// Bonus
-	if (snake->snakebonus != SNAKE_BONUS_NONE
-	&& (snake->snakebonustime >= 3 * TICRATE || snake->time % 4 < 4 / 2))
-		V_DrawFixedPatch(
-			(SNAKE_RIGHT_X + 10) * FRACUNIT,
-			(SNAKE_TOP_Y + 24) * FRACUNIT,
-			FRACUNIT / 2,
-			0,
-			W_CachePatchLongName(snake_bonuspatches[snake->snakebonus], PU_HUDGFX),
-			NULL
-		);
-}
-
-static void CL_DrawConnectionStatusBox(void)
-{
-	M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
-	if (cl_mode != CL_CONFIRMCONNECT)
-		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
-}
-
-//
-// CL_DrawConnectionStatus
-//
-// Keep the local client informed of our status.
-//
-static inline void CL_DrawConnectionStatus(void)
-{
-	INT32 ccstime = I_GetTime();
-
-	// Draw background fade
-	V_DrawFadeScreen(0xFF00, 16); // force default
-
-	if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_LOADFILES)
-	{
-		INT32 i, animtime = ((ccstime / 4) & 15) + 16;
-		UINT8 palstart;
-		const char *cltext;
-
-		// Draw the bottom box.
-		CL_DrawConnectionStatusBox();
-
-		if (cl_mode == CL_SEARCHING)
-			palstart = 32; // Red
-		else if (cl_mode == CL_CONFIRMCONNECT)
-			palstart = 48; // Orange
-		else
-			palstart = 96; // Green
-
-		if (!(cl_mode == CL_DOWNLOADSAVEGAME && lastfilenum != -1))
-			for (i = 0; i < 16; ++i) // 15 pal entries total.
-				V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-16, 16, 8, palstart + ((animtime - i) & 15));
-
-		switch (cl_mode)
-		{
-			case CL_DOWNLOADSAVEGAME:
-				if (fileneeded && lastfilenum != -1)
-				{
-					UINT32 currentsize = fileneeded[lastfilenum].currentsize;
-					UINT32 totalsize = fileneeded[lastfilenum].totalsize;
-					INT32 dldlength;
-
-					cltext = M_GetText("Downloading game state...");
-					Net_GetNetStat();
-
-					dldlength = (INT32)((currentsize/(double)totalsize) * 256);
-					if (dldlength > 256)
-						dldlength = 256;
-					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
-					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
-
-					V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-						va(" %4uK/%4uK",currentsize>>10,totalsize>>10));
-
-					V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-						va("%3.1fK/s ", ((double)getbps)/1024));
-				}
-				else
-					cltext = M_GetText("Waiting to download game state...");
-				break;
-			case CL_ASKFULLFILELIST:
-			case CL_CHECKFILES:
-				cltext = M_GetText("Checking server addon list...");
-				break;
-			case CL_CONFIRMCONNECT:
-				cltext = "";
-				break;
-			case CL_LOADFILES:
-				cltext = M_GetText("Loading server addons...");
-				break;
-			case CL_ASKJOIN:
-			case CL_WAITJOINRESPONSE:
-				if (serverisfull)
-					cltext = M_GetText("Server full, waiting for a slot...");
-				else
-					cltext = M_GetText("Requesting to join...");
-				break;
-			default:
-				cltext = M_GetText("Connecting to server...");
-				break;
-		}
-		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, cltext);
-	}
-	else
-	{
-		if (cl_mode == CL_LOADFILES)
-		{
-			INT32 totalfileslength;
-			INT32 loadcompletednum = 0;
-			INT32 i;
-
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
-
-			//ima just count files here
-			if (fileneeded)
-			{
-				for (i = 0; i < fileneedednum; i++)
-					if (fileneeded[i].status == FS_OPEN)
-						loadcompletednum++;
-			}
-
-			// Loading progress
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, "Loading server addons...");
-			totalfileslength = (INT32)((loadcompletednum/(double)(fileneedednum)) * 256);
-			M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, totalfileslength, 8, 96);
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-				va(" %2u/%2u Files",loadcompletednum,fileneedednum));
-		}
-		else if (lastfilenum != -1)
-		{
-			INT32 dldlength;
-			static char tempname[28];
-			fileneeded_t *file;
-			char *filename;
-
-			if (snake)
-				Snake_Draw();
-
-			// Draw the bottom box.
-			CL_DrawConnectionStatusBox();
-
-			if (fileneeded)
-			{
-				file = &fileneeded[lastfilenum];
-				filename = file->filename;
-			}
-			else
-				return;
-
-			Net_GetNetStat();
-			dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256);
-			if (dldlength > 256)
-				dldlength = 256;
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
-
-			memset(tempname, 0, sizeof(tempname));
-			// offset filename to just the name only part
-			filename += strlen(filename) - nameonlylength(filename);
-
-			if (strlen(filename) > sizeof(tempname)-1) // too long to display fully
-			{
-				size_t endhalfpos = strlen(filename)-10;
-				// display as first 14 chars + ... + last 10 chars
-				// which should add up to 27 if our math(s) is correct
-				snprintf(tempname, sizeof(tempname), "%.14s...%.10s", filename, filename+endhalfpos);
-			}
-			else // we can copy the whole thing in safely
-			{
-				strncpy(tempname, filename, sizeof(tempname)-1);
-			}
-
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
-				va(M_GetText("Downloading \"%s\""), tempname));
-			V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-				va(" %4uK/%4uK",fileneeded[lastfilenum].currentsize>>10,file->totalsize>>10));
-			V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-				va("%3.1fK/s ", ((double)getbps)/1024));
-		}
-		else
-		{
-			if (snake)
-				Snake_Draw();
-
-			CL_DrawConnectionStatusBox();
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
-				M_GetText("Waiting to download files..."));
-		}
-	}
-}
-#endif
-
-static boolean CL_AskFileList(INT32 firstfile)
-{
-	netbuffer->packettype = PT_TELLFILESNEEDED;
-	netbuffer->u.filesneedednum = firstfile;
-
-	return HSendPacket(servernode, false, 0, sizeof (INT32));
-}
-
-/** Sends a special packet to declare how many players in local
-  * Used only in arbitratrenetstart()
-  * Sends a PT_CLIENTJOIN packet to the server
-  *
-  * \return True if the packet was successfully sent
-  * \todo Improve the description...
-  *
-  */
-static boolean CL_SendJoin(void)
-{
-	UINT8 localplayers = 1;
-	char const *player2name;
-	if (netgame)
-		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;
-
-	CleanupPlayerName(consoleplayer, cv_playername.zstring);
-	if (splitscreen)
-		CleanupPlayerName(1, cv_playername2.zstring);/* 1 is a HACK? oh no */
-	// Avoid empty string on bots to avoid softlocking in singleplayer
-	if (botingame)
-		player2name = strcmp(cv_playername.zstring, "Tails") == 0 ? "Tail" : "Tails";
-	else
-		player2name = cv_playername2.zstring;
-
-	strncpy(netbuffer->u.clientcfg.names[0], cv_playername.zstring, MAXPLAYERNAME);
-	strncpy(netbuffer->u.clientcfg.names[1], player2name, MAXPLAYERNAME);
-
-	return HSendPacket(servernode, true, 0, sizeof (clientconfig_pak));
-}
-
-static INT32 FindRejoinerNum(SINT8 node)
-{
-	char addressbuffer[64];
-	const char *nodeaddress;
-	const char *strippednodeaddress;
-	INT32 i;
-
-	// Make sure there is no dead dress before proceeding to the stripping
-	if (!I_GetNodeAddress)
-		return -1;
-	nodeaddress = I_GetNodeAddress(node);
-	if (!nodeaddress)
-		return -1;
-
-	// Strip the address of its port
-	strcpy(addressbuffer, nodeaddress);
-	strippednodeaddress = I_NetSplitAddress(addressbuffer, NULL);
-
-	// Check if any player matches the stripped address
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		if (playeringame[i] && playeraddress[i][0] && playernode[i] == UINT8_MAX
-		&& !strcmp(playeraddress[i], strippednodeaddress))
-			return i;
-	}
-
-	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;
-
-	netbuffer->packettype = PT_SERVERINFO;
-	netbuffer->u.serverinfo._255 = 255;
-	netbuffer->u.serverinfo.packetversion = PACKETVERSION;
-	netbuffer->u.serverinfo.version = VERSION;
-	netbuffer->u.serverinfo.subversion = SUBVERSION;
-	strncpy(netbuffer->u.serverinfo.application, SRB2APPLICATION,
-			sizeof netbuffer->u.serverinfo.application);
-	// return back the time value so client can compute their ping
-	netbuffer->u.serverinfo.time = (tic_t)LONG(servertime);
-	netbuffer->u.serverinfo.leveltime = (tic_t)LONG(leveltime);
-
-	// Exclude bots from both counts
-	netbuffer->u.serverinfo.numberofplayer = (UINT8)(D_NumPlayers() - D_NumBots());
-	netbuffer->u.serverinfo.maxplayer = (UINT8)(cv_maxplayers.value - D_NumBots());
-
-	netbuffer->u.serverinfo.refusereason = GetRefuseReason(node);
-
-	strncpy(netbuffer->u.serverinfo.gametypename, Gametype_Names[gametype],
-			sizeof netbuffer->u.serverinfo.gametypename);
-	netbuffer->u.serverinfo.modifiedgame = (UINT8)modifiedgame;
-	netbuffer->u.serverinfo.cheatsenabled = CV_CheatsEnabled();
-	netbuffer->u.serverinfo.flags = (dedicated ? SV_DEDICATED : 0);
-	strncpy(netbuffer->u.serverinfo.servername, cv_servername.string,
-		MAXSERVERNAME);
-	strncpy(netbuffer->u.serverinfo.mapname, G_BuildMapName(gamemap), 7);
-
-	M_Memcpy(netbuffer->u.serverinfo.mapmd5, mapmd5, 16);
-
-	memset(netbuffer->u.serverinfo.maptitle, 0, sizeof netbuffer->u.serverinfo.maptitle);
-
-	if (mapheaderinfo[gamemap-1] && *mapheaderinfo[gamemap-1]->lvlttl)
-	{
-		char *read = mapheaderinfo[gamemap-1]->lvlttl, *writ = netbuffer->u.serverinfo.maptitle;
-		while (writ < (netbuffer->u.serverinfo.maptitle+32) && *read != '\0')
-		{
-			if (!(*read & 0x80))
-			{
-				*writ = toupper(*read);
-				writ++;
-			}
-			read++;
-		}
-		*writ = '\0';
-		//strncpy(netbuffer->u.serverinfo.maptitle, (char *)mapheaderinfo[gamemap-1]->lvlttl, 33);
-	}
-	else
-		strncpy(netbuffer->u.serverinfo.maptitle, "UNKNOWN", 32);
-
-	if (mapheaderinfo[gamemap-1] && !(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
-		netbuffer->u.serverinfo.iszone = 1;
-	else
-		netbuffer->u.serverinfo.iszone = 0;
-
-	if (mapheaderinfo[gamemap-1])
-		netbuffer->u.serverinfo.actnum = mapheaderinfo[gamemap-1]->actnum;
-
-	p = PutFileNeeded(0);
-
-	HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
-}
-
-static void SV_SendPlayerInfo(INT32 node)
-{
-	UINT8 i;
-	netbuffer->packettype = PT_PLAYERINFO;
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		if (!playeringame[i])
-		{
-			netbuffer->u.playerinfo[i].num = 255; // This slot is empty.
-			continue;
-		}
-
-		netbuffer->u.playerinfo[i].num = i;
-		strncpy(netbuffer->u.playerinfo[i].name, (const char *)&player_names[i], MAXPLAYERNAME+1);
-		netbuffer->u.playerinfo[i].name[MAXPLAYERNAME] = '\0';
-
-		//fetch IP address
-		//No, don't do that, you fuckface.
-		memset(netbuffer->u.playerinfo[i].address, 0, 4);
-
-		if (G_GametypeHasTeams())
-		{
-			if (!players[i].ctfteam)
-				netbuffer->u.playerinfo[i].team = 255;
-			else
-				netbuffer->u.playerinfo[i].team = (UINT8)players[i].ctfteam;
-		}
-		else
-		{
-			if (players[i].spectator)
-				netbuffer->u.playerinfo[i].team = 255;
-			else
-				netbuffer->u.playerinfo[i].team = 0;
-		}
-
-		netbuffer->u.playerinfo[i].score = LONG(players[i].score);
-		netbuffer->u.playerinfo[i].timeinserver = SHORT((UINT16)(players[i].jointime / TICRATE));
-		netbuffer->u.playerinfo[i].skin = (UINT8)(players[i].skin
-#ifdef DEVELOP // it's safe to do this only because PLAYERINFO isn't read by the game itself
-		% 3
-#endif
-		);
-
-		// Extra data
-		netbuffer->u.playerinfo[i].data = 0; //players[i].skincolor;
-
-		if (players[i].pflags & PF_TAGIT)
-			netbuffer->u.playerinfo[i].data |= 0x20;
-
-		if (players[i].gotflag)
-			netbuffer->u.playerinfo[i].data |= 0x40;
-
-		if (players[i].powers[pw_super])
-			netbuffer->u.playerinfo[i].data |= 0x80;
-	}
-
-	HSendPacket(node, false, 0, sizeof(plrinfo) * MAXPLAYERS);
-}
-
-/** Sends a PT_SERVERCFG packet
-  *
-  * \param node The destination
-  * \return True if the packet was successfully sent
-  *
-  */
-static boolean SV_SendServerConfig(INT32 node)
-{
-	boolean waspacketsent;
-
-	netbuffer->packettype = PT_SERVERCFG;
-
-	netbuffer->u.servercfg.serverplayer = (UINT8)serverplayer;
-	netbuffer->u.servercfg.totalslotnum = (UINT8)(doomcom->numslots);
-	netbuffer->u.servercfg.gametic = (tic_t)LONG(gametic);
-	netbuffer->u.servercfg.clientnode = (UINT8)node;
-	netbuffer->u.servercfg.gamestate = (UINT8)gamestate;
-	netbuffer->u.servercfg.gametype = (UINT8)gametype;
-	netbuffer->u.servercfg.modifiedgame = (UINT8)modifiedgame;
-	netbuffer->u.servercfg.usedCheats = (UINT8)usedCheats;
-
-	memcpy(netbuffer->u.servercfg.server_context, server_context, 8);
-
-	{
-		const size_t len = sizeof (serverconfig_pak);
-
-#ifdef DEBUGFILE
-		if (debugfile)
-		{
-			fprintf(debugfile, "ServerConfig Packet about to be sent, size of packet:%s to node:%d\n",
-				sizeu1(len), node);
-		}
-#endif
-
-		waspacketsent = HSendPacket(node, true, 0, len);
-	}
-
-#ifdef DEBUGFILE
-	if (debugfile)
-	{
-		if (waspacketsent)
-		{
-			fprintf(debugfile, "ServerConfig Packet was sent\n");
-		}
-		else
-		{
-			fprintf(debugfile, "ServerConfig Packet could not be sent right now\n");
-		}
-	}
-#endif
-
-	return waspacketsent;
-}
-
-#ifndef NONET
-#define SAVEGAMESIZE (768*1024)
-
-static boolean SV_ResendingSavegameToAnyone(void)
-{
-	INT32 i;
-
-	for (i = 0; i < MAXNETNODES; i++)
-		if (resendingsavegame[i])
-			return true;
-	return false;
-}
-
-static void SV_SendSaveGame(INT32 node, boolean resending)
-{
-	size_t length, compressedlen;
-	UINT8 *savebuffer;
-	UINT8 *compressedsave;
-	UINT8 *buffertosend;
-
-	// first save it in a malloced buffer
-	savebuffer = (UINT8 *)malloc(SAVEGAMESIZE);
-	if (!savebuffer)
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
-		return;
-	}
-
-	// Leave room for the uncompressed length.
-	save_p = savebuffer + sizeof(UINT32);
-
-	P_SaveNetGame(resending);
-
-	length = save_p - savebuffer;
-	if (length > SAVEGAMESIZE)
-	{
-		free(savebuffer);
-		save_p = NULL;
-		I_Error("Savegame buffer overrun");
-	}
-
-	// Allocate space for compressed save: one byte fewer than for the
-	// uncompressed data to ensure that the compression is worthwhile.
-	compressedsave = malloc(length - 1);
-	if (!compressedsave)
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
-		return;
-	}
-
-	// Attempt to compress it.
-	if((compressedlen = lzf_compress(savebuffer + sizeof(UINT32), length - sizeof(UINT32), compressedsave + sizeof(UINT32), length - sizeof(UINT32) - 1)))
-	{
-		// Compressing succeeded; send compressed data
-
-		free(savebuffer);
-
-		// State that we're compressed.
-		buffertosend = compressedsave;
-		WRITEUINT32(compressedsave, length - sizeof(UINT32));
-		length = compressedlen + sizeof(UINT32);
-	}
-	else
-	{
-		// Compression failed to make it smaller; send original
-
-		free(compressedsave);
-
-		// State that we're not compressed
-		buffertosend = savebuffer;
-		WRITEUINT32(savebuffer, 0);
-	}
-
-	AddRamToSendQueue(node, buffertosend, length, SF_RAM, 0);
-	save_p = NULL;
-
-	// Remember when we started sending the savegame so we can handle timeouts
-	sendingsavegame[node] = true;
-	freezetimeout[node] = I_GetTime() + jointimeout + length / 1024; // 1 extra tic for each kilobyte
-}
-
-#ifdef DUMPCONSISTENCY
-#define TMPSAVENAME "badmath.sav"
-static consvar_t cv_dumpconsistency = CVAR_INIT ("dumpconsistency", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
-
-static void SV_SavedGame(void)
-{
-	size_t length;
-	UINT8 *savebuffer;
-	char tmpsave[256];
-
-	if (!cv_dumpconsistency.value)
-		return;
-
-	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
-
-	// first save it in a malloced buffer
-	save_p = savebuffer = (UINT8 *)malloc(SAVEGAMESIZE);
-	if (!save_p)
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
-		return;
-	}
-
-	P_SaveNetGame(false);
-
-	length = save_p - savebuffer;
-	if (length > SAVEGAMESIZE)
-	{
-		free(savebuffer);
-		save_p = NULL;
-		I_Error("Savegame buffer overrun");
-	}
-
-	// then save it!
-	if (!FIL_WriteFile(tmpsave, savebuffer, length))
-		CONS_Printf(M_GetText("Didn't save %s for netgame"), tmpsave);
-
-	free(savebuffer);
-	save_p = NULL;
-}
-
-#undef  TMPSAVENAME
-#endif
-#define TMPSAVENAME "$$$.sav"
-
-
-static void CL_LoadReceivedSavegame(boolean reloading)
-{
-	UINT8 *savebuffer = NULL;
-	size_t length, decompressedlen;
-	char tmpsave[256];
-
-	FreeFileNeeded();
-
-	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
-
-	length = FIL_ReadFile(tmpsave, &savebuffer);
-
-	CONS_Printf(M_GetText("Loading savegame length %s\n"), sizeu1(length));
-	if (!length)
-	{
-		I_Error("Can't read savegame sent");
-		return;
-	}
-
-	save_p = savebuffer;
-
-	// Decompress saved game if necessary.
-	decompressedlen = READUINT32(save_p);
-	if(decompressedlen > 0)
-	{
-		UINT8 *decompressedbuffer = Z_Malloc(decompressedlen, PU_STATIC, NULL);
-		lzf_decompress(save_p, length - sizeof(UINT32), decompressedbuffer, decompressedlen);
-		Z_Free(savebuffer);
-		save_p = savebuffer = decompressedbuffer;
-	}
-
-	paused = false;
-	demoplayback = false;
-	titlemapinaction = TITLEMAP_OFF;
-	titledemo = false;
-	automapactive = false;
-
-	// load a base level
-	if (P_LoadNetGame(reloading))
-	{
-		const UINT8 actnum = mapheaderinfo[gamemap-1]->actnum;
-		CONS_Printf(M_GetText("Map is now \"%s"), G_BuildMapName(gamemap));
-		if (strcmp(mapheaderinfo[gamemap-1]->lvlttl, ""))
-		{
-			CONS_Printf(": %s", mapheaderinfo[gamemap-1]->lvlttl);
-			if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
-				CONS_Printf(M_GetText(" Zone"));
-			if (actnum > 0)
-				CONS_Printf(" %2d", actnum);
-		}
-		CONS_Printf("\"\n");
-	}
-
-	// done
-	Z_Free(savebuffer);
-	save_p = NULL;
-	if (unlink(tmpsave) == -1)
-		CONS_Alert(CONS_ERROR, M_GetText("Can't delete %s\n"), tmpsave);
-	consistancy[gametic%BACKUPTICS] = Consistancy();
-	CON_ToggleOff();
-
-	// Tell the server we have received and reloaded the gamestate
-	// so they know they can resume the game
-	netbuffer->packettype = PT_RECEIVEDGAMESTATE;
-	HSendPacket(servernode, true, 0, 0);
-}
-
-static void CL_ReloadReceivedSavegame(void)
-{
-	INT32 i;
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		LUA_InvalidatePlayer(&players[i]);
-		sprintf(player_names[i], "Player %d", i + 1);
-	}
-
-	CL_LoadReceivedSavegame(true);
-
-	if (neededtic < gametic)
-		neededtic = gametic;
-	maketic = neededtic;
-
-	ticcmd_oldangleturn[0] = players[consoleplayer].oldrelangleturn;
-	P_ForceLocalAngle(&players[consoleplayer], (angle_t)(players[consoleplayer].angleturn << 16));
-	if (splitscreen)
-	{
-		ticcmd_oldangleturn[1] = players[secondarydisplayplayer].oldrelangleturn;
-		P_ForceLocalAngle(&players[secondarydisplayplayer], (angle_t)(players[secondarydisplayplayer].angleturn << 16));
-	}
-
-	camera.subsector = R_PointInSubsector(camera.x, camera.y);
-	camera2.subsector = R_PointInSubsector(camera2.x, camera2.y);
-
-	cl_redownloadinggamestate = false;
-
-	CONS_Printf(M_GetText("Game state reloaded\n"));
-}
-#endif
-
-#ifndef NONET
-static void SendAskInfo(INT32 node)
-{
-	const tic_t asktime = I_GetTime();
-	netbuffer->packettype = PT_ASKINFO;
-	netbuffer->u.askinfo.version = VERSION;
-	netbuffer->u.askinfo.time = (tic_t)LONG(asktime);
-
-	// Even if this never arrives due to the host being firewalled, we've
-	// now allowed traffic from the host to us in, so once the MS relays
-	// our address to the host, it'll be able to speak to us.
-	HSendPacket(node, false, 0, sizeof (askinfo_pak));
-}
-
-serverelem_t serverlist[MAXSERVERLIST];
-UINT32 serverlistcount = 0;
-
-#define FORCECLOSE 0x8000
-
-static void SL_ClearServerList(INT32 connectedserver)
-{
-	UINT32 i;
-
-	for (i = 0; i < serverlistcount; i++)
-		if (connectedserver != serverlist[i].node)
-		{
-			Net_CloseConnection(serverlist[i].node|FORCECLOSE);
-			serverlist[i].node = 0;
-		}
-	serverlistcount = 0;
-}
-
-static UINT32 SL_SearchServer(INT32 node)
-{
-	UINT32 i;
-	for (i = 0; i < serverlistcount; i++)
-		if (serverlist[i].node == node)
-			return i;
-
-	return UINT32_MAX;
-}
-
-static void SL_InsertServer(serverinfo_pak* info, SINT8 node)
-{
-	UINT32 i;
-
-	// search if not already on it
-	i = SL_SearchServer(node);
-	if (i == UINT32_MAX)
-	{
-		// not found add it
-		if (serverlistcount >= MAXSERVERLIST)
-			return; // list full
-
-		/* 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->version != VERSION)
-				return; // Not same version.
-
-			if (info->subversion != SUBVERSION)
-				return; // Close, but no cigar.
-
-			if (strcmp(info->application, SRB2APPLICATION))
-				return;/* that's a different mod */
-		}
-
-		i = serverlistcount++;
-	}
-
-	serverlist[i].info = *info;
-	serverlist[i].node = node;
-
-	// resort server list
-	M_SortServerList();
-}
-
-#if defined (MASTERSERVER) && defined (HAVE_THREADS)
-struct Fetch_servers_ctx
-{
-	int room;
-	int id;
-};
-
-static void
-Fetch_servers_thread (struct Fetch_servers_ctx *ctx)
-{
-	msg_server_t *server_list;
-
-	server_list = GetShortServersList(ctx->room, ctx->id);
-
-	if (server_list)
-	{
-		I_lock_mutex(&ms_QueryId_mutex);
-		{
-			if (ctx->id != ms_QueryId)
-			{
-				free(server_list);
-				server_list = NULL;
-			}
-		}
-		I_unlock_mutex(ms_QueryId_mutex);
-
-		if (server_list)
-		{
-			I_lock_mutex(&m_menu_mutex);
-			{
-				if (m_waiting_mode == M_WAITING_SERVERS)
-					m_waiting_mode = M_NOT_WAITING;
-			}
-			I_unlock_mutex(m_menu_mutex);
-
-			I_lock_mutex(&ms_ServerList_mutex);
-			{
-				ms_ServerList = server_list;
-			}
-			I_unlock_mutex(ms_ServerList_mutex);
-		}
-	}
-
-	free(ctx);
-}
-#endif/*defined (MASTERSERVER) && defined (HAVE_THREADS)*/
-
-void CL_QueryServerList (msg_server_t *server_list)
-{
-	INT32 i;
-
-	for (i = 0; server_list[i].header.buffer[0]; i++)
-	{
-		// Make sure MS version matches our own, to
-		// thwart nefarious servers who lie to the MS.
-
-		/* lol bruh, that version COMES from the servers */
-		//if (strcmp(version, server_list[i].version) == 0)
-		{
-			INT32 node = I_NetMakeNodewPort(server_list[i].ip, server_list[i].port);
-			if (node == -1)
-				break; // no more node free
-			SendAskInfo(node);
-			// Force close the connection so that servers can't eat
-			// up nodes forever if we never get a reply back from them
-			// (usually when they've not forwarded their ports).
-			//
-			// Don't worry, we'll get in contact with the working
-			// servers again when they send SERVERINFO to us later!
-			//
-			// (Note: as a side effect this probably means every
-			// server in the list will probably be using the same node (e.g. node 1),
-			// not that it matters which nodes they use when
-			// the connections are closed afterwards anyway)
-			// -- Monster Iestyn 12/11/18
-			Net_CloseConnection(node|FORCECLOSE);
-		}
-	}
-}
-
-void CL_UpdateServerList(boolean internetsearch, INT32 room)
-{
-	(void)internetsearch;
-	(void)room;
-
-	SL_ClearServerList(0);
-
-	if (!netgame && I_NetOpenSocket)
-	{
-		if (I_NetOpenSocket())
-		{
-			netgame = true;
-			multiplayer = true;
-		}
-	}
-
-	// search for local servers
-	if (netgame)
-		SendAskInfo(BROADCASTADDR);
-
-#ifdef MASTERSERVER
-	if (internetsearch)
-	{
-#ifdef HAVE_THREADS
-		struct Fetch_servers_ctx *ctx;
-
-		ctx = malloc(sizeof *ctx);
-
-		/* This called from M_Refresh so I don't use a mutex */
-		m_waiting_mode = M_WAITING_SERVERS;
-
-		I_lock_mutex(&ms_QueryId_mutex);
-		{
-			ctx->id = ms_QueryId;
-		}
-		I_unlock_mutex(ms_QueryId_mutex);
-
-		ctx->room = room;
-
-		I_spawn_thread("fetch-servers", (I_thread_fn)Fetch_servers_thread, ctx);
-#else
-		msg_server_t *server_list;
-
-		server_list = GetShortServersList(room, 0);
-
-		if (server_list)
-		{
-			CL_QueryServerList(server_list);
-			free(server_list);
-		}
-#endif
-	}
-#endif/*MASTERSERVER*/
-}
-
-#endif // ifndef NONET
-
-static void M_ConfirmConnect(event_t *ev)
-{
-#ifndef NONET
-	if (ev->type == ev_keydown)
-	{
-		if (ev->key == ' ' || ev->key == 'y' || ev->key == KEY_ENTER || ev->key == KEY_JOY1)
-		{
-			if (totalfilesrequestednum > 0)
-			{
-				if (CL_SendFileRequest())
-				{
-					cl_mode = CL_DOWNLOADFILES;
-					Snake_Initialise();
-				}
-			}
-			else
-				cl_mode = CL_LOADFILES;
-
-			M_ClearMenus(true);
-		}
-		else if (ev->key == 'n' || ev->key == KEY_ESCAPE || ev->key == KEY_JOY1 + 3)
-		{
-			cl_mode = CL_ABORTED;
-			M_ClearMenus(true);
-		}
-	}
-#else
-	(void)ev;
-#endif
-}
-
-static boolean CL_FinishedFileList(void)
-{
-	INT32 i;
-	char *downloadsize = NULL;
-
-	//CONS_Printf(M_GetText("Checking files...\n"));
-	i = CL_CheckFiles();
-	if (i == 4) // still checking ...
-	{
-		return true;
-	}
-	else if (i == 3) // too many files
-	{
-		D_QuitNetGame();
-		CL_Reset();
-		D_StartTitle();
-		M_StartMessage(M_GetText(
-			"You have too many WAD files loaded\n"
-			"to add ones the server is using.\n"
-			"Please restart SRB2 before connecting.\n\n"
-			"Press ESC\n"
-		), NULL, MM_NOTHING);
-		return false;
-	}
-	else if (i == 2) // cannot join for some reason
-	{
-		D_QuitNetGame();
-		CL_Reset();
-		D_StartTitle();
-		M_StartMessage(M_GetText(
-			"You have the wrong addons loaded.\n\n"
-			"To play on this server, restart\n"
-			"the game and don't load any addons.\n"
-			"SRB2 will automatically add\n"
-			"everything you need when you join.\n\n"
-			"Press ESC\n"
-		), NULL, MM_NOTHING);
-		return false;
-	}
-	else if (i == 1)
-	{
-		if (serverisfull)
-		{
-			M_StartMessage(M_GetText(
-				"This server is full!\n"
-				"\n"
-				"You may load server addons (if any), and wait for a slot.\n"
-				"\n"
-				"Press ENTER to continue\nor ESC to cancel.\n\n"
-			), M_ConfirmConnect, MM_EVENTHANDLER);
-			cl_mode = CL_CONFIRMCONNECT;
-			curfadevalue = 0;
-		}
-		else
-			cl_mode = CL_LOADFILES;
-	}
-	else
-	{
-		// must download something
-		// can we, though?
-		if (!CL_CheckDownloadable()) // nope!
-		{
-			D_QuitNetGame();
-			CL_Reset();
-			D_StartTitle();
-			M_StartMessage(M_GetText(
-				"An error occured when trying to\n"
-				"download missing addons.\n"
-				"(This is almost always a problem\n"
-				"with the server, not your game.)\n\n"
-				"See the console or log file\n"
-				"for additional details.\n\n"
-				"Press ESC\n"
-			), NULL, MM_NOTHING);
-			return false;
-		}
-
-#ifndef NONET
-		downloadcompletednum = 0;
-		downloadcompletedsize = 0;
-		totalfilesrequestednum = 0;
-		totalfilesrequestedsize = 0;
-
-		if (fileneeded == NULL)
-			I_Error("CL_FinishedFileList: fileneeded == NULL");
-
-		for (i = 0; i < fileneedednum; i++)
-			if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
-			{
-				totalfilesrequestednum++;
-				totalfilesrequestedsize += fileneeded[i].totalsize;
-			}
-
-		if (totalfilesrequestedsize>>20 >= 100)
-			downloadsize = Z_StrDup(va("%uM",totalfilesrequestedsize>>20));
-		else
-			downloadsize = Z_StrDup(va("%uK",totalfilesrequestedsize>>10));
-#endif
-
-		if (serverisfull)
-			M_StartMessage(va(M_GetText(
-				"This server is full!\n"
-				"Download of %s additional content\nis required to join.\n"
-				"\n"
-				"You may download, load server addons,\nand wait for a slot.\n"
-				"\n"
-				"Press ENTER to continue\nor ESC to cancel.\n"
-			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
-		else
-			M_StartMessage(va(M_GetText(
-				"Download of %s additional content\nis required to join.\n"
-				"\n"
-				"Press ENTER to continue\nor ESC to cancel.\n"
-			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
-
-		Z_Free(downloadsize);
-		cl_mode = CL_CONFIRMCONNECT;
-		curfadevalue = 0;
-	}
-	return true;
-}
-
-#ifndef NONET
-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
-}
-#endif // ifndef NONET
-
-/** 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.
-  * \return False if the connection was aborted
-  * \sa CL_ServerConnectionTicker
-  * \sa CL_ConnectToServer
-  *
-  */
-static boolean CL_ServerConnectionSearchTicker(tic_t *asksent)
-{
-#ifndef NONET
-	INT32 i;
-
-	// serverlist is updated by GetPacket function
-	if (serverlistcount > 0)
-	{
-		// this can be a responce to our broadcast request
-		if (servernode == -1 || servernode >= MAXNETNODES)
-		{
-			i = 0;
-			servernode = serverlist[i].node;
-			CONS_Printf(M_GetText("Found, "));
-		}
-		else
-		{
-			i = SL_SearchServer(servernode);
-			if (i < 0)
-				return true;
-		}
-
-		if (client)
-		{
-			serverinfo_pak *info = &serverlist[i].info;
-
-			if (info->refusereason == REFUSE_SLOTS_FULL)
-				serverisfull = true;
-			else
-			{
-				const char *reason = InvalidServerReason(info);
-
-				// Quit here rather than downloading files
-				// and being refused later.
-				if (reason)
-				{
-					char *message = Z_StrDup(reason);
-					D_QuitNetGame();
-					CL_Reset();
-					D_StartTitle();
-					M_StartMessage(message, NULL, MM_NOTHING);
-					Z_Free(message);
-					return false;
-				}
-			}
-
-			D_ParseFileneeded(info->fileneedednum, info->fileneeded, 0);
-
-			if (info->flags & SV_LOTSOFADDONS)
-			{
-				cl_mode = CL_ASKFULLFILELIST;
-				cl_lastcheckedfilecount = 0;
-				return true;
-			}
-
-			cl_mode = CL_CHECKFILES;
-		}
-		else
-		{
-			cl_mode = CL_ASKJOIN; // files need not be checked for the server.
-			*asksent = 0;
-		}
-
-		return true;
-	}
-
-	// Ask the info to the server (askinfo packet)
-	if (*asksent + NEWTICRATE < I_GetTime())
-	{
-		SendAskInfo(servernode);
-		*asksent = I_GetTime();
-	}
-#else
-	(void)asksent;
-	// No netgames, so we skip this state.
-	cl_mode = CL_ASKJOIN;
-#endif // ifndef NONET/else
-
-	return true;
-}
-
-/** Called by CL_ConnectToServer
-  *
-  * \param tmpsave The name of the gamestate file???
-  * \param oldtic Used for knowing when to poll events and redraw
-  * \param asksent ???
-  * \return False if the connection was aborted
-  * \sa CL_ServerConnectionSearchTicker
-  * \sa CL_ConnectToServer
-  *
-  */
-static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic_t *asksent)
-{
-	boolean waitmore;
-	INT32 i;
-
-#ifdef NONET
-	(void)tmpsave;
-#endif
-
-	switch (cl_mode)
-	{
-		case CL_SEARCHING:
-			if (!CL_ServerConnectionSearchTicker(asksent))
-				return false;
-			break;
-
-		case CL_ASKFULLFILELIST:
-			if (cl_lastcheckedfilecount == UINT16_MAX) // All files retrieved
-				cl_mode = CL_CHECKFILES;
-			else if (fileneedednum != cl_lastcheckedfilecount || I_GetTime() >= *asksent)
-			{
-				if (CL_AskFileList(fileneedednum))
-				{
-					cl_lastcheckedfilecount = fileneedednum;
-					*asksent = I_GetTime() + NEWTICRATE;
-				}
-			}
-			break;
-		case CL_CHECKFILES:
-			if (!CL_FinishedFileList())
-				return false;
-			break;
-		case CL_DOWNLOADFILES:
-			waitmore = false;
-			for (i = 0; i < fileneedednum; i++)
-				if (fileneeded[i].status == FS_DOWNLOADING
-					|| fileneeded[i].status == FS_REQUESTED)
-				{
-					waitmore = true;
-					break;
-				}
-			if (waitmore)
-				break; // exit the case
-
-#ifndef NONET
-			if (snake)
-			{
-				free(snake);
-				snake = NULL;
-			}
-#endif
-
-			cl_mode = CL_LOADFILES;
-			break;
-		case CL_LOADFILES:
-			if (CL_LoadServerFiles())
-			{
-				FreeFileNeeded();
-				*asksent = 0; //This ensure the first join ask is right away
-				firstconnectattempttime = I_GetTime();
-				cl_mode = CL_ASKJOIN;
-			}
-			break;
-		case CL_ASKJOIN:
-			if (firstconnectattempttime + NEWTICRATE*300 < I_GetTime() && !server)
-			{
-				CONS_Printf(M_GetText("5 minute wait time exceeded.\n"));
-				CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
-				M_StartMessage(M_GetText(
-					"5 minute wait time exceeded.\n"
-					"You may retry connection.\n"
-					"\n"
-					"Press ESC\n"
-				), NULL, MM_NOTHING);
-				return false;
-			}
-#ifndef NONET
-			// prepare structures to save the file
-			// WARNING: this can be useless in case of server not in GS_LEVEL
-			// but since the network layer doesn't provide ordered packets...
-			CL_PrepareDownloadSaveGame(tmpsave);
-#endif
-			if (I_GetTime() >= *asksent && CL_SendJoin())
-			{
-				*asksent = I_GetTime() + NEWTICRATE*3;
-				cl_mode = CL_WAITJOINRESPONSE;
-			}
-			break;
-		case CL_WAITJOINRESPONSE:
-			if (I_GetTime() >= *asksent)
-			{
-				cl_mode = CL_ASKJOIN;
-			}
-			break;
-#ifndef NONET
-		case CL_DOWNLOADSAVEGAME:
-			// At this state, the first (and only) needed file is the gamestate
-			if (fileneeded[0].status == FS_FOUND)
-			{
-				// Gamestate is now handled within CL_LoadReceivedSavegame()
-				CL_LoadReceivedSavegame(false);
-				cl_mode = CL_CONNECTED;
-			} // don't break case continue to CL_CONNECTED
-			else
-				break;
-#endif
-
-		case CL_CONNECTED:
-		case CL_CONFIRMCONNECT: //logic is handled by M_ConfirmConnect
-		default:
-			break;
-
-		// Connection closed by cancel, timeout or refusal.
-		case CL_ABORTED:
-			cl_mode = CL_SEARCHING;
-			return false;
-	}
-
-	GetPackets();
-	Net_AckTicker();
-
-	// Call it only once by tic
-	if (*oldtic != I_GetTime())
-	{
-		I_OsPolling();
-
-		if (cl_mode == CL_CONFIRMCONNECT)
-			D_ProcessEvents(); //needed for menu system to receive inputs
-		else
-		{
-			// my hand has been forced and I am dearly sorry for this awful hack :vomit:
-			for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
-			{
-#ifndef NONET
-				if (!Snake_Joy_Grabber(&events[eventtail]))
-#endif
-					G_MapEventsToControls(&events[eventtail]);
-			}
-		}
-
-		if (gamekeydown[KEY_ESCAPE] || gamekeydown[KEY_JOY1+1] || cl_mode == CL_ABORTED)
-		{
-			CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
-			M_StartMessage(M_GetText("Network game synchronization aborted.\n\nPress ESC\n"), NULL, MM_NOTHING);
-
-#ifndef NONET
-			if (snake)
-			{
-				free(snake);
-				snake = NULL;
-			}
-#endif
-
-			D_QuitNetGame();
-			CL_Reset();
-			D_StartTitle();
-			memset(gamekeydown, 0, NUMKEYS);
-			return false;
-		}
-#ifndef NONET
-		else if (cl_mode == CL_DOWNLOADFILES && snake)
-			Snake_Handle();
-#endif
-
-		if (client && (cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADSAVEGAME))
-			FileReceiveTicker();
-
-		// why are these here? this is for servers, we're a client
-		//if (key == 's' && server)
-		//	doomcom->numnodes = (INT16)pnumnodes;
-		//FileSendTicker();
-		*oldtic = I_GetTime();
-
-#ifndef NONET
-		if (client && cl_mode != CL_CONNECTED && cl_mode != CL_ABORTED)
-		{
-			if (!snake)
-			{
-				F_MenuPresTicker(); // title sky
-				F_TitleScreenTicker(true);
-				F_TitleScreenDrawer();
-			}
-			CL_DrawConnectionStatus();
-#ifdef HAVE_THREADS
-			I_lock_mutex(&m_menu_mutex);
-#endif
-			M_Drawer(); //Needed for drawing messageboxes on the connection screen
-#ifdef HAVE_THREADS
-			I_unlock_mutex(m_menu_mutex);
-#endif
-			I_UpdateNoVsync(); // page flip or blit buffer
-			if (moviemode)
-				M_SaveFrame();
-			S_UpdateSounds();
-			S_UpdateClosedCaptions();
-		}
-#else
-		CON_Drawer();
-		I_UpdateNoVsync();
-#endif
-	}
-	else
-	{
-		I_Sleep(cv_sleep.value);
-		I_UpdateTime(cv_timescale.value);
-	}
-
-	return true;
-}
-
-/** Use adaptive send using net_bandwidth and stat.sendbytes
-  *
-  * \todo Better description...
-  *
-  */
-static void CL_ConnectToServer(void)
-{
-	INT32 pnumnodes, nodewaited = doomcom->numnodes, i;
-	tic_t oldtic;
-#ifndef NONET
-	tic_t asksent;
-	char tmpsave[256];
-
-	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
-
-	lastfilenum = -1;
-#endif
-
-	cl_mode = CL_SEARCHING;
-
-#ifndef NONET
-	// Don't get a corrupt savegame error because tmpsave already exists
-	if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1)
-		I_Error("Can't delete %s\n", tmpsave);
-#endif
-
-	if (netgame)
-	{
-		if (servernode < 0 || servernode >= MAXNETNODES)
-			CONS_Printf(M_GetText("Searching for a server...\n"));
-		else
-			CONS_Printf(M_GetText("Contacting the server...\n"));
-	}
-
-	if (gamestate == GS_INTERMISSION)
-		Y_EndIntermission(); // clean up intermission graphics etc
-
-	DEBFILE(va("waiting %d nodes\n", doomcom->numnodes));
-	G_SetGamestate(GS_WAITINGPLAYERS);
-	wipegamestate = GS_WAITINGPLAYERS;
-
-	ClearAdminPlayers();
-	pnumnodes = 1;
-	oldtic = I_GetTime() - 1;
-
-#ifndef NONET
-	asksent = (tic_t) - TICRATE;
-	firstconnectattempttime = I_GetTime();
-
-	i = SL_SearchServer(servernode);
-
-	if (i != -1)
-	{
-		char *gametypestr = serverlist[i].info.gametypename;
-		CONS_Printf(M_GetText("Connecting to: %s\n"), serverlist[i].info.servername);
-		gametypestr[sizeof serverlist[i].info.gametypename - 1] = '\0';
-		CONS_Printf(M_GetText("Gametype: %s\n"), gametypestr);
-		CONS_Printf(M_GetText("Version: %d.%d.%u\n"), serverlist[i].info.version/100,
-		 serverlist[i].info.version%100, serverlist[i].info.subversion);
-	}
-	SL_ClearServerList(servernode);
-#endif
-
-	do
-	{
-		// If the connection was aborted for some reason, leave
-#ifndef NONET
-		if (!CL_ServerConnectionTicker(tmpsave, &oldtic, &asksent))
-#else
-		if (!CL_ServerConnectionTicker((char*)NULL, &oldtic, (tic_t *)NULL))
-#endif
-			return;
-
-		if (server)
-		{
-			pnumnodes = 0;
-			for (i = 0; i < MAXNETNODES; i++)
-				if (nodeingame[i])
-					pnumnodes++;
-		}
-	}
-	while (!(cl_mode == CL_CONNECTED && (client || (server && nodewaited <= pnumnodes))));
-
-	if (netgame)
-		F_StartWaitingPlayers();
-	DEBFILE(va("Synchronisation Finished\n"));
-
-	displayplayer = consoleplayer;
-}
-
-#ifndef NONET
-typedef struct banreason_s
-{
-	char *reason;
-	struct banreason_s *prev; //-1
-	struct banreason_s *next; //+1
-} banreason_t;
-
-static banreason_t *reasontail = NULL; //last entry, use prev
-static banreason_t *reasonhead = NULL; //1st entry, use next
-
-static void Command_ShowBan(void) //Print out ban list
-{
-	size_t i;
-	const char *address, *mask;
-	banreason_t *reasonlist = reasonhead;
-
-	if (I_GetBanAddress)
-		CONS_Printf(M_GetText("Ban List:\n"));
-	else
-		return;
-
-	for (i = 0;(address = I_GetBanAddress(i)) != NULL;i++)
-	{
-		if (!I_GetBanMask || (mask = I_GetBanMask(i)) == NULL)
-			CONS_Printf("%s: %s ", sizeu1(i+1), address);
-		else
-			CONS_Printf("%s: %s/%s ", sizeu1(i+1), address, mask);
-
-		if (reasonlist && reasonlist->reason)
-			CONS_Printf("(%s)\n", reasonlist->reason);
-		else
-			CONS_Printf("\n");
-
-		if (reasonlist) reasonlist = reasonlist->next;
-	}
-
-	if (i == 0 && !address)
-		CONS_Printf(M_GetText("(empty)\n"));
-}
-
-void D_SaveBan(void)
-{
-	FILE *f;
-	size_t i;
-	banreason_t *reasonlist = reasonhead;
-	const char *address, *mask;
-	const char *path = va("%s"PATHSEP"%s", srb2home, "ban.txt");
-
-	if (!reasonhead)
-	{
-		remove(path);
-		return;
-	}
-
-	f = fopen(path, "w");
-
-	if (!f)
-	{
-		CONS_Alert(CONS_WARNING, M_GetText("Could not save ban list into ban.txt\n"));
-		return;
-	}
-
-	for (i = 0;(address = I_GetBanAddress(i)) != NULL;i++)
-	{
-		if (!I_GetBanMask || (mask = I_GetBanMask(i)) == NULL)
-			fprintf(f, "%s 0", address);
-		else
-			fprintf(f, "%s %s", address, mask);
-
-		if (reasonlist && reasonlist->reason)
-			fprintf(f, " %s\n", reasonlist->reason);
-		else
-			fprintf(f, " %s\n", "NA");
-
-		if (reasonlist) reasonlist = reasonlist->next;
-	}
-
-	fclose(f);
-}
-
-static void Ban_Add(const char *reason)
-{
-	banreason_t *reasonlist = malloc(sizeof(*reasonlist));
-
-	if (!reasonlist)
-		return;
-	if (!reason)
-		reason = "NA";
-
-	reasonlist->next = NULL;
-	reasonlist->reason = Z_StrDup(reason);
-	if ((reasonlist->prev = reasontail) == NULL)
-		reasonhead = reasonlist;
-	else
-		reasontail->next = reasonlist;
-	reasontail = reasonlist;
-}
-
-static void Ban_Clear(void)
-{
-	banreason_t *temp;
-
-	I_ClearBans();
-
-	reasontail = NULL;
-
-	while (reasonhead)
-	{
-		temp = reasonhead->next;
-		Z_Free(reasonhead->reason);
-		free(reasonhead);
-		reasonhead = temp;
-	}
-}
-
-static void Command_ClearBans(void)
-{
-	if (!I_ClearBans)
-		return;
-
-	Ban_Clear();
-	D_SaveBan();
-}
-
-static void Ban_Load_File(boolean warning)
-{
-	FILE *f;
-	const char *address, *mask;
-	char buffer[MAX_WADPATH];
-
-	if (!I_ClearBans)
-		return;
-
-	f = fopen(va("%s"PATHSEP"%s", srb2home, "ban.txt"), "r");
-
-	if (!f)
-	{
-		if (warning)
-			CONS_Alert(CONS_WARNING, M_GetText("Could not open ban.txt for ban list\n"));
-		return;
-	}
-
-	Ban_Clear();
-
-	for (; fgets(buffer, (int)sizeof(buffer), f);)
-	{
-		address = strtok(buffer, " \t\r\n");
-		mask = strtok(NULL, " \t\r\n");
-
-		I_SetBanAddress(address, mask);
-
-		Ban_Add(strtok(NULL, "\r\n"));
-	}
-
-	fclose(f);
-}
-
-static void Command_ReloadBan(void)  //recheck ban.txt
-{
-	Ban_Load_File(true);
-}
-
-static void Command_connect(void)
-{
-	if (COM_Argc() < 2 || *COM_Argv(1) == 0)
-	{
-		CONS_Printf(M_GetText(
-			"Connect <serveraddress> (port): connect to a server\n"
-			"Connect ANY: connect to the first lan server found\n"
-			//"Connect SELF: connect to your own server.\n"
-			));
-		return;
-	}
-
-	if (Playing() || titledemo)
-	{
-		CONS_Printf(M_GetText("You cannot connect while in a game. End this game first.\n"));
-		return;
-	}
-
-	// modified game check: no longer handled
-	// we don't request a restart unless the filelist differs
-
-	server = false;
-/*
-	if (!stricmp(COM_Argv(1), "self"))
-	{
-		servernode = 0;
-		server = true;
-		/// \bug should be but...
-		//SV_SpawnServer();
-	}
-	else
-*/
-	{
-		// used in menu to connect to a server in the list
-		if (netgame && !stricmp(COM_Argv(1), "node"))
-		{
-			servernode = (SINT8)atoi(COM_Argv(2));
-		}
-		else if (netgame)
-		{
-			CONS_Printf(M_GetText("You cannot connect while in a game. End this game first.\n"));
-			return;
-		}
-		else if (I_NetOpenSocket)
-		{
-			I_NetOpenSocket();
-			netgame = true;
-			multiplayer = true;
-
-			if (!stricmp(COM_Argv(1), "any"))
-				servernode = BROADCASTADDR;
-			else if (I_NetMakeNodewPort)
-			{
-				if (COM_Argc() >= 3) // address AND port
-					servernode = I_NetMakeNodewPort(COM_Argv(1), COM_Argv(2));
-				else // address only, or address:port
-					servernode = I_NetMakeNode(COM_Argv(1));
-			}
-			else
-			{
-				CONS_Alert(CONS_ERROR, M_GetText("There is no server identification with this network driver\n"));
-				D_CloseConnection();
-				return;
-			}
-		}
-		else
-			CONS_Alert(CONS_ERROR, M_GetText("There is no network driver\n"));
-	}
-
-	splitscreen = false;
-	SplitScreen_OnChange();
-	botingame = false;
-	botskin = 0;
-	CL_ConnectToServer();
-}
-#endif
-
-static void ResetNode(INT32 node);
-
-//
-// CL_ClearPlayer
-//
-// Clears the player data so that a future client can use this slot
-//
-void CL_ClearPlayer(INT32 playernum)
-{
-	if (players[playernum].mo)
-		P_RemoveMobj(players[playernum].mo);
-	memset(&players[playernum], 0, sizeof (player_t));
-	memset(playeraddress[playernum], 0, sizeof(*playeraddress));
-}
-
-//
-// CL_RemovePlayer
-//
-// Removes a player from the current game
-//
-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.
-	if (!playeringame[playernum])
-		return;
-
-	if (server && !demoplayback && playernode[playernum] != UINT8_MAX)
-	{
-		INT32 node = playernode[playernum];
-		playerpernode[node]--;
-		if (playerpernode[node] <= 0)
-		{
-			nodeingame[node] = false;
-			Net_CloseConnection(node);
-			ResetNode(node);
-		}
-	}
-
-	if (gametyperules & GTR_TEAMFLAGS)
-		P_PlayerFlagBurst(&players[playernum], false); // Don't take the flag with you!
-
-	// If in a special stage, redistribute the player's spheres across
-	// the remaining players.
-	if (G_IsSpecialStage(gamemap))
-	{
-		INT32 i, count, sincrement, spheres, rincrement, rings;
-
-		for (i = 0, count = 0; i < MAXPLAYERS; i++)
-		{
-			if (playeringame[i])
-				count++;
-		}
-
-		count--;
-		sincrement = spheres = players[playernum].spheres;
-		rincrement = rings = players[playernum].rings;
-
-		if (count)
-		{
-			sincrement /= count;
-			rincrement /= count;
-		}
-
-		for (i = 0; i < MAXPLAYERS; i++)
-		{
-			if (playeringame[i] && i != playernum)
-			{
-				if (spheres < 2*sincrement)
-				{
-					P_GivePlayerSpheres(&players[i], spheres);
-					spheres = 0;
-				}
-				else
-				{
-					P_GivePlayerSpheres(&players[i], sincrement);
-					spheres -= sincrement;
-				}
-
-				if (rings < 2*rincrement)
-				{
-					P_GivePlayerRings(&players[i], rings);
-					rings = 0;
-				}
-				else
-				{
-					P_GivePlayerRings(&players[i], rincrement);
-					rings -= rincrement;
-				}
-			}
-		}
-	}
-
-	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.
-		LUA_HookViewpointSwitch(&players[consoleplayer], &players[consoleplayer], true);
-		displayplayer = consoleplayer;
-	}
-
-	// Reset player data
-	CL_ClearPlayer(playernum);
-
-	// remove avatar of player
-	playeringame[playernum] = false;
-	playernode[playernum] = UINT8_MAX;
-	while (!playeringame[doomcom->numslots-1] && doomcom->numslots > 1)
-		doomcom->numslots--;
-
-	// Reset the name
-	sprintf(player_names[playernum], "Player %d", playernum+1);
-
-	player_name_changes[playernum] = 0;
-
-	if (IsPlayerAdmin(playernum))
-	{
-		RemoveAdminPlayer(playernum); // don't stay admin after you're gone
-	}
-
-	LUA_InvalidatePlayer(&players[playernum]);
-
-	if (G_TagGametype()) //Check if you still have a game. Location flexible. =P
-		P_CheckSurvivors();
-	else if (gametyperules & GTR_RACE)
-		P_CheckRacers();
-}
-
-void CL_Reset(void)
-{
-	if (metalrecording)
-		G_StopMetalRecording(false);
-	if (metalplayback)
-		G_StopMetalDemo();
-	if (demorecording)
-		G_CheckDemoStatus();
-
-	// reset client/server code
-	DEBFILE(va("\n-=-=-=-=-=-=-= Client reset =-=-=-=-=-=-=-\n\n"));
-
-	if (servernode > 0 && servernode < MAXNETNODES)
-	{
-		nodeingame[(UINT8)servernode] = false;
-		Net_CloseConnection(servernode);
-	}
-	D_CloseConnection(); // netgame = false
-	multiplayer = false;
-	servernode = 0;
-	server = true;
-	doomcom->numnodes = 1;
-	doomcom->numslots = 1;
-	SV_StopServer();
-	SV_ResetServer();
-
-	// make sure we don't leave any fileneeded gunk over from a failed join
-	FreeFileNeeded();
-	fileneedednum = 0;
-
-#ifndef NONET
-	totalfilesrequestednum = 0;
-	totalfilesrequestedsize = 0;
-#endif
-	firstconnectattempttime = 0;
-	serverisfull = false;
-	connectiontimeout = (tic_t)cv_nettimeout.value; //reset this temporary hack
-
-	// D_StartTitle should get done now, but the calling function will handle it
-}
-
-#ifndef NONET
-static void Command_GetPlayerNum(void)
-{
-	INT32 i;
-
-	for (i = 0; i < MAXPLAYERS; i++)
-		if (playeringame[i])
-		{
-			if (serverplayer == i)
-				CONS_Printf(M_GetText("num:%2d  node:%2d  %s\n"), i, playernode[i], player_names[i]);
-			else
-				CONS_Printf(M_GetText("\x82num:%2d  node:%2d  %s\n"), i, playernode[i], player_names[i]);
-		}
-}
-
-SINT8 nametonum(const char *name)
-{
-	INT32 playernum, i;
-
-	if (!strcmp(name, "0"))
-		return 0;
-
-	playernum = (SINT8)atoi(name);
-
-	if (playernum < 0 || playernum >= MAXPLAYERS)
-		return -1;
-
-	if (playernum)
-	{
-		if (playeringame[playernum])
-			return (SINT8)playernum;
-		else
-			return -1;
-	}
-
-	for (i = 0; i < MAXPLAYERS; i++)
-		if (playeringame[i] && !stricmp(player_names[i], name))
-			return (SINT8)i;
-
-	CONS_Printf(M_GetText("There is no player named \"%s\"\n"), name);
-
-	return -1;
-}
-
-/** Lists all players and their player numbers.
-  *
-  * \sa Command_GetPlayerNum
-  */
-static void Command_Nodes(void)
-{
-	INT32 i;
-	size_t maxlen = 0;
-	const char *address;
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		const size_t plen = strlen(player_names[i]);
-		if (playeringame[i] && plen > maxlen)
-			maxlen = plen;
-	}
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		if (playeringame[i])
-		{
-			CONS_Printf("%.2u: %*s", i, (int)maxlen, player_names[i]);
-
-			if (playernode[i] != UINT8_MAX)
-			{
-				CONS_Printf(" - node %.2d", playernode[i]);
-				if (I_GetNodeAddress && (address = I_GetNodeAddress(playernode[i])) != NULL)
-					CONS_Printf(" - %s", address);
-			}
-
-			if (IsPlayerAdmin(i))
-				CONS_Printf(M_GetText(" (verified admin)"));
-
-			if (players[i].spectator)
-				CONS_Printf(M_GetText(" (spectator)"));
-
-			CONS_Printf("\n");
-		}
-	}
-}
-
-static void Command_Ban(void)
-{
-	if (COM_Argc() < 2)
-	{
-		CONS_Printf(M_GetText("Ban <playername/playernum> <reason>: ban and kick a player\n"));
-		return;
-	}
-
-	if (!netgame) // Don't kick Tails in splitscreen!
-	{
-		CONS_Printf(M_GetText("This only works in a netgame.\n"));
-		return;
-	}
-
-	if (server || IsPlayerAdmin(consoleplayer))
-	{
-		UINT8 buf[3 + MAX_REASONLENGTH];
-		UINT8 *p = buf;
-		const SINT8 pn = nametonum(COM_Argv(1));
-		const INT32 node = playernode[(INT32)pn];
-
-		if (pn == -1 || pn == 0)
-			return;
-
-		WRITEUINT8(p, pn);
-
-		if (server && I_Ban && !I_Ban(node)) // only the server is allowed to do this right now
-		{
-			CONS_Alert(CONS_WARNING, M_GetText("Too many bans! Geez, that's a lot of people you're excluding...\n"));
-			WRITEUINT8(p, KICK_MSG_GO_AWAY);
-			SendNetXCmd(XD_KICK, &buf, 2);
-		}
-		else
-		{
-			if (server) // only the server is allowed to do this right now
-			{
-				Ban_Add(COM_Argv(2));
-				D_SaveBan(); // save the ban list
-			}
-
-			if (COM_Argc() == 2)
-			{
-				WRITEUINT8(p, KICK_MSG_BANNED);
-				SendNetXCmd(XD_KICK, &buf, 2);
-			}
-			else
-			{
-				size_t i, j = COM_Argc();
-				char message[MAX_REASONLENGTH];
-
-				//Steal from the motd code so you don't have to put the reason in quotes.
-				strlcpy(message, COM_Argv(2), sizeof message);
-				for (i = 3; i < j; i++)
-				{
-					strlcat(message, " ", sizeof message);
-					strlcat(message, COM_Argv(i), sizeof message);
-				}
-
-				WRITEUINT8(p, KICK_MSG_CUSTOM_BAN);
-				WRITESTRINGN(p, message, MAX_REASONLENGTH);
-				SendNetXCmd(XD_KICK, &buf, p - buf);
-			}
-		}
-	}
-	else
-		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
-
-}
-
-static void Command_BanIP(void)
-{
-	if (COM_Argc() < 2)
-	{
-		CONS_Printf(M_GetText("banip <ip> <reason>: ban an ip address\n"));
-		return;
-	}
-
-	if (server) // Only the server can use this, otherwise does nothing.
-	{
-		const char *address = (COM_Argv(1));
-		const char *reason;
-
-		if (COM_Argc() == 2)
-			reason = NULL;
-		else
-			reason = COM_Argv(2);
-
-
-		if (I_SetBanAddress && I_SetBanAddress(address, NULL))
-		{
-			if (reason)
-				CONS_Printf("Banned IP address %s for: %s\n", address, reason);
-			else
-				CONS_Printf("Banned IP address %s\n", address);
-
-			Ban_Add(reason);
-			D_SaveBan();
-		}
-		else
-		{
-			return;
-		}
-	}
-}
-
-static void Command_Kick(void)
-{
-	if (COM_Argc() < 2)
-	{
-		CONS_Printf(M_GetText("kick <playername/playernum> <reason>: kick a player\n"));
-		return;
-	}
-
-	if (!netgame) // Don't kick Tails in splitscreen!
-	{
-		CONS_Printf(M_GetText("This only works in a netgame.\n"));
-		return;
-	}
-
-	if (server || IsPlayerAdmin(consoleplayer))
-	{
-		UINT8 buf[3 + MAX_REASONLENGTH];
-		UINT8 *p = buf;
-		const SINT8 pn = nametonum(COM_Argv(1));
-
-		if (pn == -1 || pn == 0)
-			return;
-
-		// Special case if we are trying to kick a player who is downloading the game state:
-		// trigger a timeout instead of kicking them, because a kick would only
-		// take effect after they have finished downloading
-		if (server && playernode[pn] != UINT8_MAX && sendingsavegame[playernode[pn]])
-		{
-			Net_ConnectionTimeout(playernode[pn]);
-			return;
-		}
-
-		WRITESINT8(p, pn);
-
-		if (COM_Argc() == 2)
-		{
-			WRITEUINT8(p, KICK_MSG_GO_AWAY);
-			SendNetXCmd(XD_KICK, &buf, 2);
-		}
-		else
-		{
-			size_t i, j = COM_Argc();
-			char message[MAX_REASONLENGTH];
-
-			//Steal from the motd code so you don't have to put the reason in quotes.
-			strlcpy(message, COM_Argv(2), sizeof message);
-			for (i = 3; i < j; i++)
-			{
-				strlcat(message, " ", sizeof message);
-				strlcat(message, COM_Argv(i), sizeof message);
-			}
-
-			WRITEUINT8(p, KICK_MSG_CUSTOM_KICK);
-			WRITESTRINGN(p, message, MAX_REASONLENGTH);
-			SendNetXCmd(XD_KICK, &buf, p - buf);
-		}
-	}
-	else
-		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
-}
-
-static void Command_ResendGamestate(void)
-{
-	SINT8 playernum;
-
-	if (COM_Argc() == 1)
-	{
-		CONS_Printf(M_GetText("resendgamestate <playername/playernum>: resend the game state to a player\n"));
-		return;
-	}
-	else if (client)
-	{
-		CONS_Printf(M_GetText("Only the server can use this.\n"));
-		return;
-	}
-
-	playernum = nametonum(COM_Argv(1));
-	if (playernum == -1 || playernum == 0)
-		return;
-
-	// Send a PT_WILLRESENDGAMESTATE packet to the client so they know what's going on
-	netbuffer->packettype = PT_WILLRESENDGAMESTATE;
-	if (!HSendPacket(playernode[playernum], true, 0, 0))
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("A problem occured, please try again.\n"));
-		return;
-	}
-}
-#endif
-
-static void Got_KickCmd(UINT8 **p, INT32 playernum)
-{
-	INT32 pnum, msg;
-	char buf[3 + MAX_REASONLENGTH];
-	char *reason = buf;
-	kickreason_t kickreason = KR_KICK;
-	boolean keepbody;
-
-	pnum = READUINT8(*p);
-	msg = READUINT8(*p);
-	keepbody = (msg & KICK_MSG_KEEP_BODY) != 0;
-	msg &= ~KICK_MSG_KEEP_BODY;
-
-	if (pnum == serverplayer && IsPlayerAdmin(playernum))
-	{
-		CONS_Printf(M_GetText("Server is being shut down remotely. Goodbye!\n"));
-
-		if (server)
-			COM_BufAddText("quit\n");
-
-		return;
-	}
-
-	// Is playernum authorized to make this kick?
-	if (playernum != serverplayer && !IsPlayerAdmin(playernum)
-		&& !(playernode[playernum] != UINT8_MAX && playerpernode[playernode[playernum]] == 2
-		&& nodetoplayer2[playernode[playernum]] == pnum))
-	{
-		// We received a kick command from someone who isn't the
-		// server or admin, and who isn't in splitscreen removing
-		// player 2. Thus, it must be someone with a modified
-		// binary, trying to kick someone but without having
-		// authorization.
-
-		// We deal with this by changing the kick reason to
-		// "consistency failure" and kicking the offending user
-		// instead.
-
-		// Note: Splitscreen in netgames is broken because of
-		// this. Only the server has any idea of which players
-		// are using splitscreen on the same computer, so
-		// clients cannot always determine if a kick is
-		// legitimate.
-
-		CONS_Alert(CONS_WARNING, M_GetText("Illegal kick command received from %s for player %d\n"), player_names[playernum], pnum);
-
-		// In debug, print a longer message with more details.
-		// TODO Callum: Should we translate this?
-/*
-		CONS_Debug(DBG_NETPLAY,
-			"So, you must be asking, why is this an illegal kick?\n"
-			"Well, let's take a look at the facts, shall we?\n"
-			"\n"
-			"playernum (this is the guy who did it), he's %d.\n"
-			"pnum (the guy he's trying to kick) is %d.\n"
-			"playernum's node is %d.\n"
-			"That node has %d players.\n"
-			"Player 2 on that node is %d.\n"
-			"pnum's node is %d.\n"
-			"That node has %d players.\n"
-			"Player 2 on that node is %d.\n"
-			"\n"
-			"If you think this is a bug, please report it, including all of the details above.\n",
-				playernum, pnum,
-				playernode[playernum], playerpernode[playernode[playernum]],
-				nodetoplayer2[playernode[playernum]],
-				playernode[pnum], playerpernode[playernode[pnum]],
-				nodetoplayer2[playernode[pnum]]);
-*/
-		pnum = playernum;
-		msg = KICK_MSG_CON_FAIL;
-		keepbody = true;
-	}
-
-	//CONS_Printf("\x82%s ", player_names[pnum]);
-
-	// If a verified admin banned someone, the server needs to know about it.
-	// If the playernum isn't zero (the server) then the server needs to record the ban.
-	if (server && playernum && (msg == KICK_MSG_BANNED || msg == KICK_MSG_CUSTOM_BAN))
-	{
-		if (I_Ban && !I_Ban(playernode[(INT32)pnum]))
-			CONS_Alert(CONS_WARNING, M_GetText("Too many bans! Geez, that's a lot of people you're excluding...\n"));
-#ifndef NONET
-		else
-			Ban_Add(reason);
-#endif
-	}
-
-	switch (msg)
-	{
-		case KICK_MSG_GO_AWAY:
-			if (!players[pnum].quittime)
-				HU_AddChatText(va("\x82*%s has been kicked (No reason given)", player_names[pnum]), false);
-			kickreason = KR_KICK;
-			break;
-		case KICK_MSG_PING_HIGH:
-			HU_AddChatText(va("\x82*%s left the game (Broke ping limit)", player_names[pnum]), false);
-			kickreason = KR_PINGLIMIT;
-			break;
-		case KICK_MSG_CON_FAIL:
-			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
-			{
-				INT32 i;
-
-				CONS_Printf(M_GetText("Player kicked is #%d, dumping consistency...\n"), pnum);
-
-				for (i = 0; i < MAXPLAYERS; i++)
-				{
-					if (!playeringame[i])
-						continue;
-					CONS_Printf("-------------------------------------\n");
-					CONS_Printf("Player %d: %s\n", i, player_names[i]);
-					CONS_Printf("Skin: %d\n", players[i].skin);
-					CONS_Printf("Color: %d\n", players[i].skincolor);
-					CONS_Printf("Speed: %d\n",players[i].speed>>FRACBITS);
-					if (players[i].mo)
-					{
-						if (!players[i].mo->skin)
-							CONS_Printf("Mobj skin: NULL!\n");
-						else
-							CONS_Printf("Mobj skin: %s\n", ((skin_t *)players[i].mo->skin)->name);
-						CONS_Printf("Position: %d, %d, %d\n", players[i].mo->x, players[i].mo->y, players[i].mo->z);
-						if (!players[i].mo->state)
-							CONS_Printf("State: S_NULL\n");
-						else
-							CONS_Printf("State: %d\n", (statenum_t)(players[i].mo->state-states));
-					}
-					else
-						CONS_Printf("Mobj: NULL\n");
-					CONS_Printf("-------------------------------------\n");
-				}
-			}
-			break;
-		case KICK_MSG_TIMEOUT:
-			HU_AddChatText(va("\x82*%s left the game (Connection timeout)", player_names[pnum]), false);
-			kickreason = KR_TIMEOUT;
-			break;
-		case KICK_MSG_PLAYER_QUIT:
-			if (netgame && !players[pnum].quittime) // not splitscreen/bots or soulless body
-				HU_AddChatText(va("\x82*%s left the game", player_names[pnum]), false);
-			kickreason = KR_LEAVE;
-			break;
-		case KICK_MSG_BANNED:
-			HU_AddChatText(va("\x82*%s has been banned (No reason given)", player_names[pnum]), false);
-			kickreason = KR_BAN;
-			break;
-		case KICK_MSG_CUSTOM_KICK:
-			READSTRINGN(*p, reason, MAX_REASONLENGTH+1);
-			HU_AddChatText(va("\x82*%s has been kicked (%s)", player_names[pnum], reason), false);
-			kickreason = KR_KICK;
-			break;
-		case KICK_MSG_CUSTOM_BAN:
-			READSTRINGN(*p, reason, MAX_REASONLENGTH+1);
-			HU_AddChatText(va("\x82*%s has been banned (%s)", player_names[pnum], reason), false);
-			kickreason = KR_BAN;
-			break;
-	}
-
-	if (pnum == consoleplayer)
-	{
-		LUA_HookBool(false, HOOK(GameQuit));
-#ifdef DUMPCONSISTENCY
-		if (msg == KICK_MSG_CON_FAIL) SV_SavedGame();
-#endif
-		D_QuitNetGame();
-		CL_Reset();
-		D_StartTitle();
-		if (msg == KICK_MSG_CON_FAIL)
-			M_StartMessage(M_GetText("Server closed connection\n(synch failure)\nPress ESC\n"), NULL, MM_NOTHING);
-		else if (msg == KICK_MSG_PING_HIGH)
-			M_StartMessage(M_GetText("Server closed connection\n(Broke ping limit)\nPress ESC\n"), NULL, MM_NOTHING);
-		else if (msg == KICK_MSG_BANNED)
-			M_StartMessage(M_GetText("You have been banned by the server\n\nPress ESC\n"), NULL, MM_NOTHING);
-		else if (msg == KICK_MSG_CUSTOM_KICK)
-			M_StartMessage(va(M_GetText("You have been kicked\n(%s)\nPress ESC\n"), reason), NULL, MM_NOTHING);
-		else if (msg == KICK_MSG_CUSTOM_BAN)
-			M_StartMessage(va(M_GetText("You have been banned\n(%s)\nPress ESC\n"), reason), NULL, MM_NOTHING);
-		else
-			M_StartMessage(M_GetText("You have been kicked by the server\n\nPress ESC\n"), NULL, MM_NOTHING);
-	}
-	else if (keepbody)
-	{
-		if (server && !demoplayback && playernode[pnum] != UINT8_MAX)
-		{
-			INT32 node = playernode[pnum];
-			playerpernode[node]--;
-			if (playerpernode[node] <= 0)
-			{
-				nodeingame[node] = false;
-				Net_CloseConnection(node);
-				ResetNode(node);
-			}
-		}
-
-		playernode[pnum] = UINT8_MAX;
-
-		players[pnum].quittime = 1;
-	}
-	else
-		CL_RemovePlayer(pnum, kickreason);
-}
-
-static CV_PossibleValue_t netticbuffer_cons_t[] = {{0, "MIN"}, {3, "MAX"}, {0, NULL}};
-consvar_t cv_netticbuffer = CVAR_INIT ("netticbuffer", "1", CV_SAVE, netticbuffer_cons_t, NULL);
-
-consvar_t cv_allownewplayer = CVAR_INIT ("allowjoin", "On", CV_SAVE|CV_NETVAR|CV_ALLOWLUA, CV_OnOff, NULL);
-consvar_t cv_joinnextround = CVAR_INIT ("joinnextround", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL); /// \todo not done
-static CV_PossibleValue_t maxplayers_cons_t[] = {{2, "MIN"}, {32, "MAX"}, {0, NULL}};
-consvar_t cv_maxplayers = CVAR_INIT ("maxplayers", "8", CV_SAVE|CV_NETVAR|CV_ALLOWLUA, maxplayers_cons_t, NULL);
-static CV_PossibleValue_t joindelay_cons_t[] = {{1, "MIN"}, {3600, "MAX"}, {0, "Off"}, {0, NULL}};
-consvar_t cv_joindelay = CVAR_INIT ("joindelay", "10", CV_SAVE|CV_NETVAR, joindelay_cons_t, NULL);
-static CV_PossibleValue_t rejointimeout_cons_t[] = {{1, "MIN"}, {60 * FRACUNIT, "MAX"}, {0, "Off"}, {0, NULL}};
-consvar_t cv_rejointimeout = CVAR_INIT ("rejointimeout", "2", CV_SAVE|CV_NETVAR|CV_FLOAT, rejointimeout_cons_t, NULL);
-
-static CV_PossibleValue_t resynchattempts_cons_t[] = {{1, "MIN"}, {20, "MAX"}, {0, "No"}, {0, NULL}};
-consvar_t cv_resynchattempts = CVAR_INIT ("resynchattempts", "10", CV_SAVE|CV_NETVAR, resynchattempts_cons_t, NULL);
-consvar_t cv_blamecfail = CVAR_INIT ("blamecfail", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
-
-// max file size to send to a player (in kilobytes)
-static CV_PossibleValue_t maxsend_cons_t[] = {{0, "MIN"}, {204800, "MAX"}, {0, NULL}};
-consvar_t cv_maxsend = CVAR_INIT ("maxsend", "4096", CV_SAVE|CV_NETVAR, maxsend_cons_t, NULL);
-consvar_t cv_noticedownload = CVAR_INIT ("noticedownload", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
-
-// Speed of file downloading (in packets per tic)
-static CV_PossibleValue_t downloadspeed_cons_t[] = {{1, "MIN"}, {300, "MAX"}, {0, NULL}};
-consvar_t cv_downloadspeed = CVAR_INIT ("downloadspeed", "16", CV_SAVE|CV_NETVAR, downloadspeed_cons_t, NULL);
-
-static void Got_AddPlayer(UINT8 **p, INT32 playernum);
-
-// called one time at init
-void D_ClientServerInit(void)
-{
-	DEBFILE(va("- - -== SRB2 v%d.%.2d.%d "VERSIONSTRING" debugfile ==- - -\n",
-		VERSION/100, VERSION%100, SUBVERSION));
-
-#ifndef NONET
-	COM_AddCommand("getplayernum", Command_GetPlayerNum, COM_LUA);
-	COM_AddCommand("kick", Command_Kick, COM_LUA);
-	COM_AddCommand("ban", Command_Ban, COM_LUA);
-	COM_AddCommand("banip", Command_BanIP, COM_LUA);
-	COM_AddCommand("clearbans", Command_ClearBans, COM_LUA);
-	COM_AddCommand("showbanlist", Command_ShowBan, COM_LUA);
-	COM_AddCommand("reloadbans", Command_ReloadBan, COM_LUA);
-	COM_AddCommand("connect", Command_connect, COM_LUA);
-	COM_AddCommand("nodes", Command_Nodes, COM_LUA);
-	COM_AddCommand("resendgamestate", Command_ResendGamestate, COM_LUA);
-#ifdef PACKETDROP
-	COM_AddCommand("drop", Command_Drop, COM_LUA);
-	COM_AddCommand("droprate", Command_Droprate, COM_LUA);
-#endif
-#ifdef _DEBUG
-	COM_AddCommand("numnodes", Command_Numnodes, COM_LUA);
-#endif
-#endif
-
-	RegisterNetXCmd(XD_KICK, Got_KickCmd);
-	RegisterNetXCmd(XD_ADDPLAYER, Got_AddPlayer);
-#ifndef NONET
-#ifdef DUMPCONSISTENCY
-	CV_RegisterVar(&cv_dumpconsistency);
-#endif
-	Ban_Load_File(false);
-#endif
-
-	gametic = 0;
-	localgametic = 0;
-
-	// do not send anything before the real begin
-	SV_StopServer();
-	SV_ResetServer();
-	if (dedicated)
-		SV_SpawnServer();
-}
-
-static void ResetNode(INT32 node)
-{
-	nodeingame[node] = false;
-	nodewaiting[node] = 0;
-
-	nettics[node] = gametic;
-	supposedtics[node] = gametic;
-
-	nodetoplayer[node] = -1;
-	nodetoplayer2[node] = -1;
-	playerpernode[node] = 0;
-
-	sendingsavegame[node] = false;
-	resendingsavegame[node] = false;
-	savegameresendcooldown[node] = 0;
-}
-
-void SV_ResetServer(void)
-{
-	INT32 i;
-
-	// +1 because this command will be executed in com_executebuffer in
-	// tryruntic so gametic will be incremented, anyway maketic > gametic
-	// is not an issue
-
-	maketic = gametic + 1;
-	neededtic = maketic;
-	tictoclear = maketic;
-
-	joindelay = 0;
-
-	for (i = 0; i < MAXNETNODES; i++)
-		ResetNode(i);
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		LUA_InvalidatePlayer(&players[i]);
-		playeringame[i] = false;
-		playernode[i] = UINT8_MAX;
-		memset(playeraddress[i], 0, sizeof(*playeraddress));
-		sprintf(player_names[i], "Player %d", i + 1);
-		adminplayers[i] = -1; // Populate the entire adminplayers array with -1.
-	}
-
-	memset(player_name_changes, 0, sizeof player_name_changes);
-
-	mynode = 0;
-	cl_packetmissed = false;
-	cl_redownloadinggamestate = false;
-
-	if (dedicated)
-	{
-		nodeingame[0] = true;
-		serverplayer = 0;
-	}
-	else
-		serverplayer = consoleplayer;
-
-	if (server)
-		servernode = 0;
-
-	doomcom->numslots = 0;
-
-	// clear server_context
-	memset(server_context, '-', 8);
-
-	CV_RevertNetVars();
-
-	// Ensure synched when creating a new server
-	M_CopyGameData(serverGamedata, clientGamedata);
-
-	DEBFILE("\n-=-=-=-=-=-=-= Server Reset =-=-=-=-=-=-=-\n\n");
-}
-
-static inline void SV_GenContext(void)
-{
-	UINT8 i;
-	// generate server_context, as exactly 8 bytes of randomly mixed A-Z and a-z
-	// (hopefully M_Random is initialized!! if not this will be awfully silly!)
-	for (i = 0; i < 8; i++)
-	{
-		const char a = M_RandomKey(26*2);
-		if (a < 26) // uppercase
-			server_context[i] = 'A'+a;
-		else // lowercase
-			server_context[i] = 'a'+(a-26);
-	}
-}
-
-//
-// D_QuitNetGame
-// Called before quitting to leave a net game
-// without hanging the other players
-//
-void D_QuitNetGame(void)
-{
-	mousegrabbedbylua = true;
-	I_UpdateMouseGrab();
-
-	if (!netgame || !netbuffer)
-		return;
-
-	DEBFILE("===========================================================================\n"
-	        "                  Quitting Game, closing connection\n"
-	        "===========================================================================\n");
-
-	// abort send/receive of files
-	CloseNetFile();
-	RemoveAllLuaFileTransfers();
-	waitingforluafiletransfer = false;
-	waitingforluafilecommand = false;
-
-	if (server)
-	{
-		INT32 i;
-
-		netbuffer->packettype = PT_SERVERSHUTDOWN;
-		for (i = 0; i < MAXNETNODES; i++)
-			if (nodeingame[i])
-				HSendPacket(i, true, 0, 0);
-#ifdef MASTERSERVER
-		if (serverrunning && ms_RoomId > 0)
-			UnregisterServer();
-#endif
-	}
-	else if (servernode > 0 && servernode < MAXNETNODES && nodeingame[(UINT8)servernode])
-	{
-		netbuffer->packettype = PT_CLIENTQUIT;
-		HSendPacket(servernode, true, 0, 0);
-	}
-
-	D_CloseConnection();
-	ClearAdminPlayers();
-
-	DEBFILE("===========================================================================\n"
-	        "                         Log finish\n"
-	        "===========================================================================\n");
-#ifdef DEBUGFILE
-	if (debugfile)
-	{
-		fclose(debugfile);
-		debugfile = NULL;
-	}
-#endif
-}
-
-// Adds a node to the game (player will follow at map change or at savegame....)
-static inline void SV_AddNode(INT32 node)
-{
-	nettics[node] = gametic;
-	supposedtics[node] = gametic;
-	// little hack because the server connects to itself and puts
-	// nodeingame when connected not here
-	if (node)
-		nodeingame[node] = true;
-}
-
-// Xcmd XD_ADDPLAYER
-static void Got_AddPlayer(UINT8 **p, INT32 playernum)
-{
-	INT16 node, newplayernum;
-	boolean splitscreenplayer;
-	boolean rejoined;
-	player_t *newplayer;
-
-	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
-	{
-		// protect against hacked/buggy client
-		CONS_Alert(CONS_WARNING, M_GetText("Illegal add player command received from %s\n"), player_names[playernum]);
-		if (server)
-			SendKick(playernum, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
-		return;
-	}
-
-	node = READUINT8(*p);
-	newplayernum = READUINT8(*p);
-	splitscreenplayer = newplayernum & 0x80;
-	newplayernum &= ~0x80;
-
-	rejoined = playeringame[newplayernum];
-
-	if (!rejoined)
-	{
-		// Clear player before joining, lest some things get set incorrectly
-		// HACK: don't do this for splitscreen, it relies on preset values
-		if (!splitscreen && !botingame)
-			CL_ClearPlayer(newplayernum);
-		playeringame[newplayernum] = true;
-		G_AddPlayer(newplayernum);
-		if (newplayernum+1 > doomcom->numslots)
-			doomcom->numslots = (INT16)(newplayernum+1);
-
-		if (server && I_GetNodeAddress)
-		{
-			char addressbuffer[64];
-			const char *address = I_GetNodeAddress(node);
-			if (address) // MI: fix msvcrt.dll!_mbscat crash?
-			{
-				strcpy(addressbuffer, address);
-				strcpy(playeraddress[newplayernum],
-						I_NetSplitAddress(addressbuffer, NULL));
-			}
-		}
-	}
-
-	newplayer = &players[newplayernum];
-
-	newplayer->jointime = 0;
-	newplayer->quittime = 0;
-
-	READSTRINGN(*p, player_names[newplayernum], MAXPLAYERNAME);
-
-	// the server is creating my player
-	if (node == mynode)
-	{
-		playernode[newplayernum] = 0; // for information only
-		if (!splitscreenplayer)
-		{
-			consoleplayer = newplayernum;
-			displayplayer = newplayernum;
-			secondarydisplayplayer = newplayernum;
-			DEBFILE("spawning me\n");
-			ticcmd_oldangleturn[0] = newplayer->oldrelangleturn;
-		}
-		else
-		{
-			secondarydisplayplayer = newplayernum;
-			DEBFILE("spawning my brother\n");
-			if (botingame)
-				newplayer->bot = 1;
-			ticcmd_oldangleturn[1] = newplayer->oldrelangleturn;
-		}
-		P_ForceLocalAngle(newplayer, (angle_t)(newplayer->angleturn << 16));
-		D_SendPlayerConfig();
-		addedtogame = true;
-
-		if (rejoined)
-		{
-			if (newplayer->mo)
-			{
-				newplayer->viewheight = 41*newplayer->height/48;
-
-				if (newplayer->mo->eflags & MFE_VERTICALFLIP)
-					newplayer->viewz = newplayer->mo->z + newplayer->mo->height - newplayer->viewheight;
-				else
-					newplayer->viewz = newplayer->mo->z + newplayer->viewheight;
-			}
-
-			// wake up the status bar
-			ST_Start();
-			// wake up the heads up text
-			HU_Start();
-
-			if (camera.chase && !splitscreenplayer)
-				P_ResetCamera(newplayer, &camera);
-			if (camera2.chase && splitscreenplayer)
-				P_ResetCamera(newplayer, &camera2);
-		}
-	}
-
-	if (netgame)
-	{
-		char joinmsg[256];
-
-		if (rejoined)
-			strcpy(joinmsg, M_GetText("\x82*%s has rejoined the game (player %d)"));
-		else
-			strcpy(joinmsg, M_GetText("\x82*%s has joined the game (player %d)"));
-		strcpy(joinmsg, va(joinmsg, player_names[newplayernum], newplayernum));
-
-		// Merge join notification + IP to avoid clogging console/chat
-		if (server && cv_showjoinaddress.value && I_GetNodeAddress)
-		{
-			const char *address = I_GetNodeAddress(node);
-			if (address)
-				strcat(joinmsg, va(" (%s)", address));
-		}
-
-		HU_AddChatText(joinmsg, false);
-	}
-
-	if (server && multiplayer && motd[0] != '\0')
-		COM_BufAddText(va("sayto %d %s\n", newplayernum, motd));
-
-	if (!rejoined)
-		LUA_HookInt(newplayernum, HOOK(PlayerJoin));
-}
-
-static boolean SV_AddWaitingPlayers(const char *name, const char *name2)
-{
-	INT32 node, n, newplayer = false;
-	UINT8 buf[2 + MAXPLAYERNAME];
-	UINT8 *p;
-	INT32 newplayernum;
-
-	for (node = 0; node < MAXNETNODES; node++)
-	{
-		// splitscreen can allow 2 player in one node
-		for (; nodewaiting[node] > 0; nodewaiting[node]--)
-		{
-			newplayer = true;
-
-			newplayernum = FindRejoinerNum(node);
-			if (newplayernum == -1)
-			{
-				// search for a free playernum
-				// we can't use playeringame since it is not updated here
-				for (newplayernum = dedicated ? 1 : 0; newplayernum < MAXPLAYERS; newplayernum++)
-				{
-					if (playeringame[newplayernum])
-						continue;
-					for (n = 0; n < MAXNETNODES; n++)
-						if (nodetoplayer[n] == newplayernum || nodetoplayer2[n] == newplayernum)
-							break;
-					if (n == MAXNETNODES)
-						break;
-				}
-			}
-
-			// should never happen since we check the playernum
-			// before accepting the join
-			I_Assert(newplayernum < MAXPLAYERS);
-
-			playernode[newplayernum] = (UINT8)node;
-
-			p = buf + 2;
-			buf[0] = (UINT8)node;
-			buf[1] = newplayernum;
-			if (playerpernode[node] < 1)
-			{
-				nodetoplayer[node] = newplayernum;
-				WRITESTRINGN(p, name, MAXPLAYERNAME);
-			}
-			else
-			{
-				nodetoplayer2[node] = newplayernum;
-				buf[1] |= 0x80;
-				WRITESTRINGN(p, name2, MAXPLAYERNAME);
-			}
-			playerpernode[node]++;
-
-			SendNetXCmd(XD_ADDPLAYER, &buf, p - buf);
-
-			DEBFILE(va("Server added player %d node %d\n", newplayernum, node));
-		}
-	}
-
-	return newplayer;
-}
-
-void CL_AddSplitscreenPlayer(void)
-{
-	if (cl_mode == CL_CONNECTED)
-		CL_SendJoin();
-}
-
-void CL_RemoveSplitscreenPlayer(void)
-{
-	if (cl_mode != CL_CONNECTED)
-		return;
-
-	SendKick(secondarydisplayplayer, KICK_MSG_PLAYER_QUIT);
-}
-
-// is there a game running
-boolean Playing(void)
-{
-	return (server && serverrunning) || (client && cl_mode == CL_CONNECTED);
-}
-
-boolean SV_SpawnServer(void)
-{
-	if (demoplayback)
-		G_StopDemo(); // reset engine parameter
-	if (metalplayback)
-		G_StopMetalDemo();
-
-	if (!serverrunning)
-	{
-		CONS_Printf(M_GetText("Starting Server....\n"));
-		serverrunning = true;
-		SV_ResetServer();
-		SV_GenContext();
-		if (netgame && I_NetOpenSocket)
-		{
-			I_NetOpenSocket();
-#ifdef MASTERSERVER
-			if (ms_RoomId > 0)
-				RegisterServer();
-#endif
-		}
-
-		// non dedicated server just connect to itself
-		if (!dedicated)
-			CL_ConnectToServer();
-		else doomcom->numslots = 1;
-	}
-
-	return SV_AddWaitingPlayers(cv_playername.zstring, cv_playername2.zstring);
-}
-
-void SV_StopServer(void)
-{
-	tic_t i;
-
-	if (gamestate == GS_INTERMISSION)
-		Y_EndIntermission();
-	gamestate = wipegamestate = GS_NULL;
-
-	localtextcmd[0] = 0;
-	localtextcmd2[0] = 0;
-
-	for (i = firstticstosend; i < firstticstosend + BACKUPTICS; i++)
-		D_Clearticcmd(i);
-
-	consoleplayer = 0;
-	cl_mode = CL_SEARCHING;
-	maketic = gametic+1;
-	neededtic = maketic;
-	serverrunning = false;
-}
-
-// called at singleplayer start and stopdemo
-void SV_StartSinglePlayerServer(void)
-{
-	server = true;
-	netgame = false;
-	multiplayer = false;
-	G_SetGametype(GT_COOP);
-
-	// no more tic the game with this settings!
-	SV_StopServer();
-
-	if (splitscreen)
-		multiplayer = true;
-}
-
-static void SV_SendRefuse(INT32 node, const char *reason)
-{
-	strcpy(netbuffer->u.serverrefuse.reason, reason);
-
-	netbuffer->packettype = PT_SERVERREFUSE;
-	HSendPacket(node, true, 0, strlen(netbuffer->u.serverrefuse.reason) + 1);
-	Net_CloseConnection(node);
-}
-
-// used at txtcmds received to check packetsize bound
-static size_t TotalTextCmdPerTic(tic_t tic)
-{
-	INT32 i;
-	size_t total = 1; // num of textcmds in the tic (ntextcmd byte)
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		UINT8 *textcmd = D_GetExistingTextcmd(tic, i);
-		if ((!i || playeringame[i]) && textcmd)
-			total += 2 + textcmd[0]; // "+2" for size and playernum
-	}
-
-	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 - D_NumBots());
-		}
-	}
-
-	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
-  *
-  */
-static void HandleConnect(SINT8 node)
-{
-	char names[MAXSPLITSCREENPLAYERS][MAXPLAYERNAME + 1];
-	INT32 rejoinernum;
-	INT32 i;
-	const char *refuse;
-
-	rejoinernum = FindRejoinerNum(node);
-
-	refuse = ConnectionRefused(node, rejoinernum);
-
-	if (refuse)
-		SV_SendRefuse(node, refuse);
-	else
-	{
-#ifndef NONET
-		boolean newnode = false;
-#endif
-
-		for (i = 0; i < netbuffer->u.clientcfg.localplayers - playerpernode[node]; i++)
-		{
-			strlcpy(names[i], netbuffer->u.clientcfg.names[i], MAXPLAYERNAME + 1);
-			if (!EnsurePlayerNameIsGood(names[i], rejoinernum))
-			{
-				SV_SendRefuse(node, "Bad player name");
-				return;
-			}
-		}
-
-		// client authorised to join
-		nodewaiting[node] = (UINT8)(netbuffer->u.clientcfg.localplayers - playerpernode[node]);
-		if (!nodeingame[node])
-		{
-			gamestate_t backupstate = gamestate;
-#ifndef NONET
-			newnode = true;
-#endif
-			SV_AddNode(node);
-
-			if (cv_joinnextround.value && gameaction == ga_nothing)
-				G_SetGamestate(GS_WAITINGPLAYERS);
-			if (!SV_SendServerConfig(node))
-			{
-				G_SetGamestate(backupstate);
-				/// \note Shouldn't SV_SendRefuse be called before ResetNode?
-				ResetNode(node);
-				SV_SendRefuse(node, M_GetText("Server couldn't send info, please try again"));
-				/// \todo fix this !!!
-				return; // restart the while
-			}
-			//if (gamestate != GS_LEVEL) // GS_INTERMISSION, etc?
-			//	SV_SendPlayerConfigs(node); // send bare minimum player info
-			G_SetGamestate(backupstate);
-			DEBFILE("new node joined\n");
-		}
-#ifndef NONET
-		if (nodewaiting[node])
-		{
-			if ((gamestate == GS_LEVEL || gamestate == GS_INTERMISSION) && newnode)
-			{
-				SV_SendSaveGame(node, false); // send a complete game state
-				DEBFILE("send savegame\n");
-			}
-			SV_AddWaitingPlayers(names[0], names[1]);
-			joindelay += cv_joindelay.value * TICRATE;
-			player_joining = true;
-		}
-#endif
-	}
-}
-
-/** Called when a PT_SERVERSHUTDOWN packet is received
-  *
-  * \param node The packet sender (should be the server)
-  *
-  */
-static void HandleShutdown(SINT8 node)
-{
-	(void)node;
-	LUA_HookBool(false, HOOK(GameQuit));
-	D_QuitNetGame();
-	CL_Reset();
-	D_StartTitle();
-	M_StartMessage(M_GetText("Server has shutdown\n\nPress Esc\n"), NULL, MM_NOTHING);
-}
-
-/** Called when a PT_NODETIMEOUT packet is received
-  *
-  * \param node The packet sender (should be the server)
-  *
-  */
-static void HandleTimeout(SINT8 node)
-{
-	(void)node;
-	LUA_HookBool(false, HOOK(GameQuit));
-	D_QuitNetGame();
-	CL_Reset();
-	D_StartTitle();
-	M_StartMessage(M_GetText("Server Timeout\n\nPress Esc\n"), NULL, MM_NOTHING);
-}
-
-#ifndef NONET
-/** Called when a PT_SERVERINFO packet is received
-  *
-  * \param node The packet sender
-  * \note What happens if the packet comes from a client or something like that?
-  *
-  */
-static void HandleServerInfo(SINT8 node)
-{
-	// compute ping in ms
-	const tic_t ticnow = I_GetTime();
-	const tic_t ticthen = (tic_t)LONG(netbuffer->u.serverinfo.time);
-	const tic_t ticdiff = (ticnow - ticthen)*1000/NEWTICRATE;
-	netbuffer->u.serverinfo.time = (tic_t)LONG(ticdiff);
-	netbuffer->u.serverinfo.servername[MAXSERVERNAME-1] = 0;
-	netbuffer->u.serverinfo.application
-		[sizeof netbuffer->u.serverinfo.application - 1] = '\0';
-	netbuffer->u.serverinfo.gametypename
-		[sizeof netbuffer->u.serverinfo.gametypename - 1] = '\0';
-
-	SL_InsertServer(&netbuffer->u.serverinfo, node);
-}
-#endif
-
-static void PT_WillResendGamestate(void)
-{
-#ifndef NONET
-	char tmpsave[256];
-
-	if (server || cl_redownloadinggamestate)
-		return;
-
-	// Send back a PT_CANRECEIVEGAMESTATE packet to the server
-	// so they know they can start sending the game state
-	netbuffer->packettype = PT_CANRECEIVEGAMESTATE;
-	if (!HSendPacket(servernode, true, 0, 0))
-		return;
-
-	CONS_Printf(M_GetText("Reloading game state...\n"));
-
-	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
-
-	// Don't get a corrupt savegame error because tmpsave already exists
-	if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1)
-		I_Error("Can't delete %s\n", tmpsave);
-
-	CL_PrepareDownloadSaveGame(tmpsave);
-
-	cl_redownloadinggamestate = true;
-#endif
-}
-
-static void PT_CanReceiveGamestate(SINT8 node)
-{
-#ifndef NONET
-	if (client || sendingsavegame[node])
-		return;
-
-	CONS_Printf(M_GetText("Resending game state to %s...\n"), player_names[nodetoplayer[node]]);
-
-	SV_SendSaveGame(node, true); // Resend a complete game state
-	resendingsavegame[node] = true;
-#else
-	(void)node;
-#endif
-}
-
-/** Handles a packet received from a node that isn't in game
-  *
-  * \param node The packet sender
-  * \todo Choose a better name, as the packet can also come from the server apparently?
-  * \sa HandlePacketFromPlayer
-  * \sa GetPackets
-  *
-  */
-static void HandlePacketFromAwayNode(SINT8 node)
-{
-	if (node != servernode)
-		DEBFILE(va("Received packet from unknown host %d\n", node));
-
-// macro for packets that should only be sent by the server
-// if it is NOT from the server, bail out and close the connection!
-#define SERVERONLY \
-			if (node != servernode) \
-			{ \
-				Net_CloseConnection(node); \
-				break; \
-			}
-	switch (netbuffer->packettype)
-	{
-		case PT_ASKINFOVIAMS:
-			Net_CloseConnection(node);
-			break;
-
-		case PT_TELLFILESNEEDED:
-			if (server && serverrunning)
-			{
-				UINT8 *p;
-				INT32 firstfile = netbuffer->u.filesneedednum;
-
-				netbuffer->packettype = PT_MOREFILESNEEDED;
-				netbuffer->u.filesneededcfg.first = firstfile;
-				netbuffer->u.filesneededcfg.more = 0;
-
-				p = PutFileNeeded(firstfile);
-
-				HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
-			}
-			else // Shouldn't get this if you aren't the server...?
-				Net_CloseConnection(node);
-			break;
-
-		case PT_MOREFILESNEEDED:
-			if (server && serverrunning)
-			{ // But wait I thought I'm the server?
-				Net_CloseConnection(node);
-				break;
-			}
-			SERVERONLY
-			if (cl_mode == CL_ASKFULLFILELIST && netbuffer->u.filesneededcfg.first == fileneedednum)
-			{
-				D_ParseFileneeded(netbuffer->u.filesneededcfg.num, netbuffer->u.filesneededcfg.files, netbuffer->u.filesneededcfg.first);
-				if (!netbuffer->u.filesneededcfg.more)
-					cl_lastcheckedfilecount = UINT16_MAX; // Got the whole file list
-			}
-			break;
-
-		case PT_ASKINFO:
-			if (server && serverrunning)
-			{
-				SV_SendServerInfo(node, (tic_t)LONG(netbuffer->u.askinfo.time));
-				SV_SendPlayerInfo(node); // Send extra info
-			}
-			Net_CloseConnection(node);
-			break;
-
-		case PT_SERVERREFUSE: // Negative response of client join request
-			if (server && serverrunning)
-			{ // But wait I thought I'm the server?
-				Net_CloseConnection(node);
-				break;
-			}
-			SERVERONLY
-			if (cl_mode == CL_WAITJOINRESPONSE)
-			{
-				// Save the reason so it can be displayed after quitting the netgame
-				char *reason = strdup(netbuffer->u.serverrefuse.reason);
-				if (!reason)
-					I_Error("Out of memory!\n");
-
-				if (strstr(reason, "Maximum players reached"))
-				{
-					serverisfull = true;
-					//Special timeout for when refusing due to player cap. The client will wait 3 seconds between join requests when waiting for a slot, so we need this to be much longer
-					//We set it back to the value of cv_nettimeout.value in CL_Reset
-					connectiontimeout = NEWTICRATE*7;
-					cl_mode = CL_ASKJOIN;
-					free(reason);
-					break;
-				}
-
-				M_StartMessage(va(M_GetText("Server refuses connection\n\nReason:\n%s"),
-					reason), NULL, MM_NOTHING);
-
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
-
-				free(reason);
-
-				// Will be reset by caller. Signals refusal.
-				cl_mode = CL_ABORTED;
-			}
-			break;
-
-		case PT_SERVERCFG: // Positive response of client join request
-		{
-			if (server && serverrunning && node != servernode)
-			{ // but wait I thought I'm the server?
-				Net_CloseConnection(node);
-				break;
-			}
-			SERVERONLY
-			/// \note how would this happen? and is it doing the right thing if it does?
-			if (cl_mode != CL_WAITJOINRESPONSE)
-				break;
-
-			if (client)
-			{
-				maketic = gametic = neededtic = (tic_t)LONG(netbuffer->u.servercfg.gametic);
-				G_SetGametype(netbuffer->u.servercfg.gametype);
-				modifiedgame = netbuffer->u.servercfg.modifiedgame;
-				if (netbuffer->u.servercfg.usedCheats)
-					G_SetUsedCheats(true);
-				memcpy(server_context, netbuffer->u.servercfg.server_context, 8);
-			}
-
-			nodeingame[(UINT8)servernode] = true;
-			serverplayer = netbuffer->u.servercfg.serverplayer;
-			doomcom->numslots = SHORT(netbuffer->u.servercfg.totalslotnum);
-			mynode = netbuffer->u.servercfg.clientnode;
-			if (serverplayer >= 0)
-				playernode[(UINT8)serverplayer] = servernode;
-
-			if (netgame)
-#ifndef NONET
-				CONS_Printf(M_GetText("Join accepted, waiting for complete game state...\n"));
-#else
-				CONS_Printf(M_GetText("Join accepted, waiting for next level change...\n"));
-#endif
-			DEBFILE(va("Server accept join gametic=%u mynode=%d\n", gametic, mynode));
-
-#ifndef NONET
-			/// \note Wait. What if a Lua script uses some global custom variables synched with the NetVars hook?
-			///       Shouldn't them be downloaded even at intermission time?
-			///       Also, according to HandleConnect, the server will send the savegame even during intermission...
-			if (netbuffer->u.servercfg.gamestate == GS_LEVEL/* ||
-				netbuffer->u.servercfg.gamestate == GS_INTERMISSION*/)
-				cl_mode = CL_DOWNLOADSAVEGAME;
-			else
-#endif
-				cl_mode = CL_CONNECTED;
-			break;
-		}
-
-		// Handled in d_netfil.c
-		case PT_FILEFRAGMENT:
-			if (server)
-			{ // But wait I thought I'm the server?
-				Net_CloseConnection(node);
-				break;
-			}
-			SERVERONLY
-			PT_FileFragment();
-			break;
-
-		case PT_FILEACK:
-			if (server)
-				PT_FileAck();
-			break;
-
-		case PT_FILERECEIVED:
-			if (server)
-				PT_FileReceived();
-			break;
-
-		case PT_REQUESTFILE:
-			if (server)
-			{
-				if (!cv_downloading.value || !PT_RequestFile(node))
-					Net_CloseConnection(node); // close connection if one of the requested files could not be sent, or you disabled downloading anyway
-			}
-			else
-				Net_CloseConnection(node); // nope
-			break;
-
-		case PT_NODETIMEOUT:
-		case PT_CLIENTQUIT:
-			if (server)
-				Net_CloseConnection(node);
-			break;
-
-		case PT_CLIENTCMD:
-			break; // This is not an "unknown packet"
-
-		case PT_SERVERTICS:
-			// Do not remove my own server (we have just get a out of order packet)
-			if (node == servernode)
-				break;
-			/* FALLTHRU */
-
-		default:
-			DEBFILE(va("unknown packet received (%d) from unknown host\n",netbuffer->packettype));
-			Net_CloseConnection(node);
-			break; // Ignore it
-
-	}
-#undef SERVERONLY
-}
-
-/** Handles a packet received from a node that is in game
-  *
-  * \param node The packet sender
-  * \todo Choose a better name
-  * \sa HandlePacketFromAwayNode
-  * \sa GetPackets
-  *
-  */
-static void HandlePacketFromPlayer(SINT8 node)
-{
-	INT32 netconsole;
-	tic_t realend, realstart;
-	UINT8 *pak, *txtpak, numtxtpak;
-#ifndef NOMD5
-	UINT8 finalmd5[16];/* Well, it's the cool thing to do? */
-#endif
-
-	txtpak = NULL;
-
-	if (dedicated && node == 0)
-		netconsole = 0;
-	else
-		netconsole = nodetoplayer[node];
-
-#ifdef PARANOIA
-	if (netconsole >= MAXPLAYERS)
-		I_Error("bad table nodetoplayer: node %d player %d", doomcom->remotenode, netconsole);
-#endif
-
-	switch (netbuffer->packettype)
-	{
-// -------------------------------------------- SERVER RECEIVE ----------
-		case PT_CLIENTCMD:
-		case PT_CLIENT2CMD:
-		case PT_CLIENTMIS:
-		case PT_CLIENT2MIS:
-		case PT_NODEKEEPALIVE:
-		case PT_NODEKEEPALIVEMIS:
-			if (client)
-				break;
-
-			// To save bytes, only the low byte of tic numbers are sent
-			// Use ExpandTics to figure out what the rest of the bytes are
-			realstart = ExpandTics(netbuffer->u.clientpak.client_tic, node);
-			realend = ExpandTics(netbuffer->u.clientpak.resendfrom, node);
-
-			if (netbuffer->packettype == PT_CLIENTMIS || netbuffer->packettype == PT_CLIENT2MIS
-				|| netbuffer->packettype == PT_NODEKEEPALIVEMIS
-				|| supposedtics[node] < realend)
-			{
-				supposedtics[node] = realend;
-			}
-			// Discard out of order packet
-			if (nettics[node] > realend)
-			{
-				DEBFILE(va("out of order ticcmd discarded nettics = %u\n", nettics[node]));
-				break;
-			}
-
-			// Update the nettics
-			nettics[node] = realend;
-
-			// This should probably still timeout though, as the node should always have a player 1 number
-			if (netconsole == -1)
-				break;
-
-			// As long as clients send valid ticcmds, the server can keep running, so reset the timeout
-			/// \todo Use a separate cvar for that kind of timeout?
-			freezetimeout[node] = I_GetTime() + connectiontimeout;
-
-			// Don't do anything for packets of type NODEKEEPALIVE?
-			// Sryder 2018/07/01: Update the freezetimeout still!
-			if (netbuffer->packettype == PT_NODEKEEPALIVE
-				|| netbuffer->packettype == PT_NODEKEEPALIVEMIS)
-				break;
-
-			// If we've alredy received a ticcmd for this tic, just submit it for the next one.
-			tic_t faketic = maketic;
-			if ((!!(netcmds[maketic % BACKUPTICS][netconsole].angleturn & TICCMD_RECEIVED))
-				&& (maketic - firstticstosend < BACKUPTICS - 1))
-				faketic++;
-
-			// Copy ticcmd
-			G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][netconsole], &netbuffer->u.clientpak.cmd, 1);
-
-			// Check ticcmd for "speed hacks"
-			if (netcmds[faketic%BACKUPTICS][netconsole].forwardmove > MAXPLMOVE || netcmds[faketic%BACKUPTICS][netconsole].forwardmove < -MAXPLMOVE
-				|| netcmds[faketic%BACKUPTICS][netconsole].sidemove > MAXPLMOVE || netcmds[faketic%BACKUPTICS][netconsole].sidemove < -MAXPLMOVE)
-			{
-				CONS_Alert(CONS_WARNING, M_GetText("Illegal movement value received from node %d\n"), netconsole);
-				//D_Clearticcmd(k);
-
-				SendKick(netconsole, KICK_MSG_CON_FAIL);
-				break;
-			}
-
-			// Splitscreen cmd
-			if ((netbuffer->packettype == PT_CLIENT2CMD || netbuffer->packettype == PT_CLIENT2MIS)
-				&& nodetoplayer2[node] >= 0)
-				G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][(UINT8)nodetoplayer2[node]],
-					&netbuffer->u.client2pak.cmd2, 1);
-
-
-			// Check player consistancy during the level
-			if (realstart <= gametic && realstart + BACKUPTICS - 1 > gametic && gamestate == GS_LEVEL
-				&& consistancy[realstart%BACKUPTICS] != SHORT(netbuffer->u.clientpak.consistancy)
-#ifndef NONET
-				&& !SV_ResendingSavegameToAnyone()
-#endif
-				&& !resendingsavegame[node] && savegameresendcooldown[node] <= I_GetTime())
-			{
-				if (cv_resynchattempts.value)
-				{
-					// Tell the client we are about to resend them the gamestate
-					netbuffer->packettype = PT_WILLRESENDGAMESTATE;
-					HSendPacket(node, true, 0, 0);
-
-					resendingsavegame[node] = true;
-
-					if (cv_blamecfail.value)
-						CONS_Printf(M_GetText("Synch failure for player %d (%s); expected %hd, got %hd\n"),
-							netconsole+1, player_names[netconsole],
-							consistancy[realstart%BACKUPTICS],
-							SHORT(netbuffer->u.clientpak.consistancy));
-					DEBFILE(va("Restoring player %d (synch failure) [%update] %d!=%d\n",
-						netconsole, realstart, consistancy[realstart%BACKUPTICS],
-						SHORT(netbuffer->u.clientpak.consistancy)));
-					break;
-				}
-				else
-				{
-					SendKick(netconsole, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
-					DEBFILE(va("player %d kicked (synch failure) [%u] %d!=%d\n",
-						netconsole, realstart, consistancy[realstart%BACKUPTICS],
-						SHORT(netbuffer->u.clientpak.consistancy)));
-					break;
-				}
-			}
-			break;
-		case PT_BASICKEEPALIVE:
-			if (client)
-				break;
-
-			// This should probably still timeout though, as the node should always have a player 1 number
-			if (netconsole == -1)
-				break;
-
-			// If a client sends this it should mean they are done receiving the savegame
-			sendingsavegame[node] = false;
-
-			// As long as clients send keep alives, the server can keep running, so reset the timeout
-			/// \todo Use a separate cvar for that kind of timeout?
-			freezetimeout[node] = I_GetTime() + connectiontimeout;
-			break;
-		case PT_TEXTCMD2: // splitscreen special
-			netconsole = nodetoplayer2[node];
-			/* FALLTHRU */
-		case PT_TEXTCMD:
-			if (client)
-				break;
-
-			if (netconsole < 0 || netconsole >= MAXPLAYERS)
-				Net_UnAcknowledgePacket(node);
-			else
-			{
-				size_t j;
-				tic_t tic = maketic;
-				UINT8 *textcmd;
-
-				// ignore if the textcmd has a reported size of zero
-				// this shouldn't be sent at all
-				if (!netbuffer->u.textcmd[0])
-				{
-					DEBFILE(va("GetPacket: Textcmd with size 0 detected! (node %u, player %d)\n",
-						node, netconsole));
-					Net_UnAcknowledgePacket(node);
-					break;
-				}
-
-				// ignore if the textcmd size var is actually larger than it should be
-				// BASEPACKETSIZE + 1 (for size) + textcmd[0] should == datalength
-				if (netbuffer->u.textcmd[0] > (size_t)doomcom->datalength-BASEPACKETSIZE-1)
-				{
-					DEBFILE(va("GetPacket: Bad Textcmd packet size! (expected %d, actual %s, node %u, player %d)\n",
-					netbuffer->u.textcmd[0], sizeu1((size_t)doomcom->datalength-BASEPACKETSIZE-1),
-						node, netconsole));
-					Net_UnAcknowledgePacket(node);
-					break;
-				}
-
-				// check if tic that we are making isn't too large else we cannot send it :(
-				// doomcom->numslots+1 "+1" since doomcom->numslots can change within this time and sent time
-				j = software_MAXPACKETLENGTH
-					- (netbuffer->u.textcmd[0]+2+BASESERVERTICSSIZE
-					+ (doomcom->numslots+1)*sizeof(ticcmd_t));
-
-				// search a tic that have enougth space in the ticcmd
-				while ((textcmd = D_GetExistingTextcmd(tic, netconsole)),
-					(TotalTextCmdPerTic(tic) > j || netbuffer->u.textcmd[0] + (textcmd ? textcmd[0] : 0) > MAXTEXTCMD)
-					&& tic < firstticstosend + BACKUPTICS)
-					tic++;
-
-				if (tic >= firstticstosend + BACKUPTICS)
-				{
-					DEBFILE(va("GetPacket: Textcmd too long (max %s, used %s, mak %d, "
-						"tosend %u, node %u, player %d)\n", sizeu1(j), sizeu2(TotalTextCmdPerTic(maketic)),
-						maketic, firstticstosend, node, netconsole));
-					Net_UnAcknowledgePacket(node);
-					break;
-				}
-
-				// Make sure we have a buffer
-				if (!textcmd) textcmd = D_GetTextcmd(tic, netconsole);
-
-				DEBFILE(va("textcmd put in tic %u at position %d (player %d) ftts %u mk %u\n",
-					tic, textcmd[0]+1, netconsole, firstticstosend, maketic));
-
-				M_Memcpy(&textcmd[textcmd[0]+1], netbuffer->u.textcmd+1, netbuffer->u.textcmd[0]);
-				textcmd[0] += (UINT8)netbuffer->u.textcmd[0];
-			}
-			break;
-		case PT_LOGIN:
-			if (client)
-				break;
-
-#ifndef NOMD5
-			if (doomcom->datalength < 16)/* ignore partial sends */
-				break;
-
-			if (!adminpasswordset)
-			{
-				CONS_Printf(M_GetText("Password from %s failed (no password set).\n"), player_names[netconsole]);
-				break;
-			}
-
-			// Do the final pass to compare with the sent md5
-			D_MD5PasswordPass(adminpassmd5, 16, va("PNUM%02d", netconsole), &finalmd5);
-
-			if (!memcmp(netbuffer->u.md5sum, finalmd5, 16))
-			{
-				CONS_Printf(M_GetText("%s passed authentication.\n"), player_names[netconsole]);
-				COM_BufInsertText(va("promote %d\n", netconsole)); // do this immediately
-			}
-			else
-				CONS_Printf(M_GetText("Password from %s failed.\n"), player_names[netconsole]);
-#endif
-			break;
-		case PT_NODETIMEOUT:
-		case PT_CLIENTQUIT:
-			if (client)
-				break;
-
-			// nodeingame will be put false in the execution of kick command
-			// this allow to send some packets to the quitting client to have their ack back
-			nodewaiting[node] = 0;
-			if (netconsole != -1 && playeringame[netconsole])
-			{
-				UINT8 kickmsg;
-
-				if (netbuffer->packettype == PT_NODETIMEOUT)
-					kickmsg = KICK_MSG_TIMEOUT;
-				else
-					kickmsg = KICK_MSG_PLAYER_QUIT;
-				kickmsg |= KICK_MSG_KEEP_BODY;
-
-				SendKick(netconsole, kickmsg);
-				nodetoplayer[node] = -1;
-
-				if (nodetoplayer2[node] != -1 && nodetoplayer2[node] >= 0
-					&& playeringame[(UINT8)nodetoplayer2[node]])
-				{
-					SendKick(nodetoplayer2[node], kickmsg);
-					nodetoplayer2[node] = -1;
-				}
-			}
-			Net_CloseConnection(node);
-			nodeingame[node] = false;
-			break;
-		case PT_CANRECEIVEGAMESTATE:
-			PT_CanReceiveGamestate(node);
-			break;
-		case PT_ASKLUAFILE:
-			if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_ASKED)
-				AddLuaFileToSendQueue(node, luafiletransfers->realfilename);
-			break;
-		case PT_HASLUAFILE:
-			if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_SENDING)
-				SV_HandleLuaFileSent(node);
-			break;
-		case PT_RECEIVEDGAMESTATE:
-			sendingsavegame[node] = false;
-			resendingsavegame[node] = false;
-			savegameresendcooldown[node] = I_GetTime() + 5 * TICRATE;
-			break;
-// -------------------------------------------- CLIENT RECEIVE ----------
-		case PT_SERVERTICS:
-			// Only accept PT_SERVERTICS from the server.
-			if (node != servernode)
-			{
-				CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_SERVERTICS", node);
-				if (server)
-					SendKick(netconsole, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
-				break;
-			}
-
-			realstart = netbuffer->u.serverpak.starttic;
-			realend = realstart + netbuffer->u.serverpak.numtics;
-
-			if (!txtpak)
-				txtpak = (UINT8 *)&netbuffer->u.serverpak.cmds[netbuffer->u.serverpak.numslots
-					* netbuffer->u.serverpak.numtics];
-
-			if (realend > gametic + CLIENTBACKUPTICS)
-				realend = gametic + CLIENTBACKUPTICS;
-			cl_packetmissed = realstart > neededtic;
-
-			if (realstart <= neededtic && realend > neededtic)
-			{
-				tic_t i, j;
-				pak = (UINT8 *)&netbuffer->u.serverpak.cmds;
-
-				for (i = realstart; i < realend; i++)
-				{
-					// clear first
-					D_Clearticcmd(i);
-
-					// copy the tics
-					pak = G_ScpyTiccmd(netcmds[i%BACKUPTICS], pak,
-						netbuffer->u.serverpak.numslots*sizeof (ticcmd_t));
-
-					// copy the textcmds
-					numtxtpak = *txtpak++;
-					for (j = 0; j < numtxtpak; j++)
-					{
-						INT32 k = *txtpak++; // playernum
-						const size_t txtsize = txtpak[0]+1;
-
-						if (i >= gametic) // Don't copy old net commands
-							M_Memcpy(D_GetTextcmd(i, k), txtpak, txtsize);
-						txtpak += txtsize;
-					}
-				}
-
-				neededtic = realend;
-			}
-			else
-			{
-				DEBFILE(va("frame not in bound: %u\n", neededtic));
-				/*if (realend < neededtic - 2 * TICRATE || neededtic + 2 * TICRATE < realstart)
-					I_Error("Received an out of order PT_SERVERTICS packet!\n"
-							"Got tics %d-%d, needed tic %d\n\n"
-							"Please report this crash on the Master Board,\n"
-							"IRC or Discord so it can be fixed.\n", (INT32)realstart, (INT32)realend, (INT32)neededtic);*/
-			}
-			break;
-		case PT_PING:
-			// Only accept PT_PING from the server.
-			if (node != servernode)
-			{
-				CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_PING", node);
-				if (server)
-					SendKick(netconsole, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
-				break;
-			}
-
-			//Update client ping table from the server.
-			if (client)
-			{
-				UINT8 i;
-				for (i = 0; i < MAXPLAYERS; i++)
-					if (playeringame[i])
-						playerpingtable[i] = (tic_t)netbuffer->u.pingtable[i];
-
-				servermaxping = (tic_t)netbuffer->u.pingtable[MAXPLAYERS];
-			}
-
-			break;
-		case PT_SERVERCFG:
-			break;
-		case PT_FILEFRAGMENT:
-			// Only accept PT_FILEFRAGMENT from the server.
-			if (node != servernode)
-			{
-				CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_FILEFRAGMENT", node);
-				if (server)
-					SendKick(netconsole, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
-				break;
-			}
-			if (client)
-				PT_FileFragment();
-			break;
-		case PT_FILEACK:
-			if (server)
-				PT_FileAck();
-			break;
-		case PT_FILERECEIVED:
-			if (server)
-				PT_FileReceived();
-			break;
-		case PT_WILLRESENDGAMESTATE:
-			PT_WillResendGamestate();
-			break;
-		case PT_SENDINGLUAFILE:
-			if (client)
-				CL_PrepareDownloadLuaFile();
-			break;
-		default:
-			DEBFILE(va("UNKNOWN PACKET TYPE RECEIVED %d from host %d\n",
-				netbuffer->packettype, node));
-	} // end switch
-}
-
-/**	Handles all received packets, if any
-  *
-  * \todo Add details to this description (lol)
-  *
-  */
-static void GetPackets(void)
-{
-	SINT8 node; // The packet sender
-
-	player_joining = false;
-
-	while (HGetPacket())
-	{
-		node = (SINT8)doomcom->remotenode;
-
-		if (netbuffer->packettype == PT_CLIENTJOIN && server)
-		{
-			HandleConnect(node);
-			continue;
-		}
-		if (node == servernode && client && cl_mode != CL_SEARCHING)
-		{
-			if (netbuffer->packettype == PT_SERVERSHUTDOWN)
-			{
-				HandleShutdown(node);
-				continue;
-			}
-			if (netbuffer->packettype == PT_NODETIMEOUT)
-			{
-				HandleTimeout(node);
-				continue;
-			}
-		}
-
-#ifndef NONET
-		if (netbuffer->packettype == PT_SERVERINFO)
-		{
-			HandleServerInfo(node);
-			continue;
-		}
-#endif
-
-		if (netbuffer->packettype == PT_PLAYERINFO)
-			continue; // We do nothing with PLAYERINFO, that's for the MS browser.
-
-		// Packet received from someone already playing
-		if (nodeingame[node])
-			HandlePacketFromPlayer(node);
-		// Packet received from someone not playing
-		else
-			HandlePacketFromAwayNode(node);
-	}
-}
-
-//
-// NetUpdate
-// Builds ticcmds for console player,
-// sends out a packet
-//
-// no more use random generator, because at very first tic isn't yet synchronized
-// Note: It is called consistAncy on purpose.
-//
-static INT16 Consistancy(void)
-{
-	INT32 i;
-	UINT32 ret = 0;
-#ifdef MOBJCONSISTANCY
-	thinker_t *th;
-	mobj_t *mo;
-#endif
-
-	DEBFILE(va("TIC %u ", gametic));
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		if (!playeringame[i])
-			ret ^= 0xCCCC;
-		else if (!players[i].mo);
-		else
-		{
-			ret += players[i].mo->x;
-			ret -= players[i].mo->y;
-			ret += players[i].powers[pw_shield];
-			ret *= i+1;
-		}
-	}
-	// I give up
-	// Coop desynching enemies is painful
-	if (!G_PlatformGametype())
-		ret += P_GetRandSeed();
-
-#ifdef MOBJCONSISTANCY
-	if (gamestate == GS_LEVEL)
-	{
-		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
-		{
-			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
-				continue;
-
-			mo = (mobj_t *)th;
-
-			if (mo->flags & (MF_SPECIAL | MF_SOLID | MF_PUSHABLE | MF_BOSS | MF_MISSILE | MF_SPRING | MF_MONITOR | MF_FIRE | MF_ENEMY | MF_PAIN | MF_STICKY))
-			{
-				ret -= mo->type;
-				ret += mo->x;
-				ret -= mo->y;
-				ret += mo->z;
-				ret -= mo->momx;
-				ret += mo->momy;
-				ret -= mo->momz;
-				ret += mo->angle;
-				ret -= mo->flags;
-				ret += mo->flags2;
-				ret -= mo->eflags;
-				if (mo->target)
-				{
-					ret += mo->target->type;
-					ret -= mo->target->x;
-					ret += mo->target->y;
-					ret -= mo->target->z;
-					ret += mo->target->momx;
-					ret -= mo->target->momy;
-					ret += mo->target->momz;
-					ret -= mo->target->angle;
-					ret += mo->target->flags;
-					ret -= mo->target->flags2;
-					ret += mo->target->eflags;
-					ret -= mo->target->state - states;
-					ret += mo->target->tics;
-					ret -= mo->target->sprite;
-					ret += mo->target->frame;
-				}
-				else
-					ret ^= 0x3333;
-				if (mo->tracer && mo->tracer->type != MT_OVERLAY)
-				{
-					ret += mo->tracer->type;
-					ret -= mo->tracer->x;
-					ret += mo->tracer->y;
-					ret -= mo->tracer->z;
-					ret += mo->tracer->momx;
-					ret -= mo->tracer->momy;
-					ret += mo->tracer->momz;
-					ret -= mo->tracer->angle;
-					ret += mo->tracer->flags;
-					ret -= mo->tracer->flags2;
-					ret += mo->tracer->eflags;
-					ret -= mo->tracer->state - states;
-					ret += mo->tracer->tics;
-					ret -= mo->tracer->sprite;
-					ret += mo->tracer->frame;
-				}
-				else
-					ret ^= 0xAAAA;
-				ret -= mo->state - states;
-				ret += mo->tics;
-				ret -= mo->sprite;
-				ret += mo->frame;
-			}
-		}
-	}
-#endif
-
-	DEBFILE(va("Consistancy = %u\n", (ret & 0xFFFF)));
-
-	return (INT16)(ret & 0xFFFF);
-}
-
-// confusing, but this DOESN'T send PT_NODEKEEPALIVE, it sends PT_BASICKEEPALIVE
-// used during wipes to tell the server that a node is still connected
-static void CL_SendClientKeepAlive(void)
-{
-	netbuffer->packettype = PT_BASICKEEPALIVE;
-
-	HSendPacket(servernode, false, 0, 0);
-}
-
-static void SV_SendServerKeepAlive(void)
-{
-	INT32 n;
-
-	for (n = 1; n < MAXNETNODES; n++)
-	{
-		if (nodeingame[n])
-		{
-			netbuffer->packettype = PT_BASICKEEPALIVE;
-			HSendPacket(n, false, 0, 0);
-		}
-	}
-}
-
-// send the client packet to the server
-static void CL_SendClientCmd(void)
-{
-	size_t packetsize = 0;
-	boolean mis = false;
-
-	netbuffer->packettype = PT_CLIENTCMD;
-
-	if (cl_packetmissed)
-	{
-		netbuffer->packettype = PT_CLIENTMIS;
-		mis = true;
-	}
-
-	netbuffer->u.clientpak.resendfrom = (UINT8)(neededtic & UINT8_MAX);
-	netbuffer->u.clientpak.client_tic = (UINT8)(gametic & UINT8_MAX);
-
-	if (gamestate == GS_WAITINGPLAYERS)
-	{
-		// Send PT_NODEKEEPALIVE packet
-		netbuffer->packettype = (mis ? PT_NODEKEEPALIVEMIS : PT_NODEKEEPALIVE);
-		packetsize = sizeof (clientcmd_pak) - sizeof (ticcmd_t) - sizeof (INT16);
-		HSendPacket(servernode, false, 0, packetsize);
-	}
-	else if (gamestate != GS_NULL && (addedtogame || dedicated))
-	{
-		packetsize = sizeof (clientcmd_pak);
-		G_MoveTiccmd(&netbuffer->u.clientpak.cmd, &localcmds, 1);
-		netbuffer->u.clientpak.consistancy = SHORT(consistancy[gametic%BACKUPTICS]);
-
-		// Send a special packet with 2 cmd for splitscreen
-		if (splitscreen || botingame)
-		{
-			netbuffer->packettype = (mis ? PT_CLIENT2MIS : PT_CLIENT2CMD);
-			packetsize = sizeof (client2cmd_pak);
-			G_MoveTiccmd(&netbuffer->u.client2pak.cmd2, &localcmds2, 1);
-		}
-
-		HSendPacket(servernode, false, 0, packetsize);
-	}
-
-	if (cl_mode == CL_CONNECTED || dedicated)
-	{
-		// Send extra data if needed
-		if (localtextcmd[0])
-		{
-			netbuffer->packettype = PT_TEXTCMD;
-			M_Memcpy(netbuffer->u.textcmd, localtextcmd, localtextcmd[0]+1);
-			// All extra data have been sent
-			if (HSendPacket(servernode, true, 0, localtextcmd[0]+1)) // Send can fail...
-				localtextcmd[0] = 0;
-		}
-
-		// Send extra data if needed for player 2 (splitscreen)
-		if (localtextcmd2[0])
-		{
-			netbuffer->packettype = PT_TEXTCMD2;
-			M_Memcpy(netbuffer->u.textcmd, localtextcmd2, localtextcmd2[0]+1);
-			// All extra data have been sent
-			if (HSendPacket(servernode, true, 0, localtextcmd2[0]+1)) // Send can fail...
-				localtextcmd2[0] = 0;
-		}
-	}
-}
-
-// send the server packet
-// send tic from firstticstosend to maketic-1
-static void SV_SendTics(void)
-{
-	tic_t realfirsttic, lasttictosend, i;
-	UINT32 n;
-	INT32 j;
-	size_t packsize;
-	UINT8 *bufpos;
-	UINT8 *ntextcmd;
-
-	// send to all client but not to me
-	// for each node create a packet with x tics and send it
-	// x is computed using supposedtics[n], max packet size and maketic
-	for (n = 1; n < MAXNETNODES; n++)
-		if (nodeingame[n])
-		{
-			// assert supposedtics[n]>=nettics[n]
-			realfirsttic = supposedtics[n];
-			lasttictosend = min(maketic, nettics[n] + CLIENTBACKUPTICS);
-
-			if (realfirsttic >= lasttictosend)
-			{
-				// well we have sent all tics we will so use extrabandwidth
-				// to resent packet that are supposed lost (this is necessary since lost
-				// packet detection work when we have received packet with firsttic > neededtic
-				// (getpacket servertics case)
-				DEBFILE(va("Nothing to send node %u mak=%u sup=%u net=%u \n",
-					n, maketic, supposedtics[n], nettics[n]));
-				realfirsttic = nettics[n];
-				if (realfirsttic >= lasttictosend || (I_GetTime() + n)&3)
-					// all tic are ok
-					continue;
-				DEBFILE(va("Sent %d anyway\n", realfirsttic));
-			}
-			if (realfirsttic < firstticstosend)
-				realfirsttic = firstticstosend;
-
-			// compute the length of the packet and cut it if too large
-			packsize = BASESERVERTICSSIZE;
-			for (i = realfirsttic; i < lasttictosend; i++)
-			{
-				packsize += sizeof (ticcmd_t) * doomcom->numslots;
-				packsize += TotalTextCmdPerTic(i);
-
-				if (packsize > software_MAXPACKETLENGTH)
-				{
-					DEBFILE(va("packet too large (%s) at tic %d (should be from %d to %d)\n",
-						sizeu1(packsize), i, realfirsttic, lasttictosend));
-					lasttictosend = i;
-
-					// too bad: too much player have send extradata and there is too
-					//          much data in one tic.
-					// To avoid it put the data on the next tic. (see getpacket
-					// textcmd case) but when numplayer changes the computation can be different
-					if (lasttictosend == realfirsttic)
-					{
-						if (packsize > MAXPACKETLENGTH)
-							I_Error("Too many players: can't send %s data for %d players to node %d\n"
-							        "Well sorry nobody is perfect....\n",
-							        sizeu1(packsize), doomcom->numslots, n);
-						else
-						{
-							lasttictosend++; // send it anyway!
-							DEBFILE("sending it anyway\n");
-						}
-					}
-					break;
-				}
-			}
-
-			// Send the tics
-			netbuffer->packettype = PT_SERVERTICS;
-			netbuffer->u.serverpak.starttic = realfirsttic;
-			netbuffer->u.serverpak.numtics = (UINT8)(lasttictosend - realfirsttic);
-			netbuffer->u.serverpak.numslots = (UINT8)SHORT(doomcom->numslots);
-			bufpos = (UINT8 *)&netbuffer->u.serverpak.cmds;
-
-			for (i = realfirsttic; i < lasttictosend; i++)
-			{
-				bufpos = G_DcpyTiccmd(bufpos, netcmds[i%BACKUPTICS], doomcom->numslots * sizeof (ticcmd_t));
-			}
-
-			// add textcmds
-			for (i = realfirsttic; i < lasttictosend; i++)
-			{
-				ntextcmd = bufpos++;
-				*ntextcmd = 0;
-				for (j = 0; j < MAXPLAYERS; j++)
-				{
-					UINT8 *textcmd = D_GetExistingTextcmd(i, j);
-					INT32 size = textcmd ? textcmd[0] : 0;
-
-					if ((!j || playeringame[j]) && size)
-					{
-						(*ntextcmd)++;
-						WRITEUINT8(bufpos, j);
-						M_Memcpy(bufpos, textcmd, size + 1);
-						bufpos += size + 1;
-					}
-				}
-			}
-			packsize = bufpos - (UINT8 *)&(netbuffer->u);
-
-			HSendPacket(n, false, 0, packsize);
-			// when tic are too large, only one tic is sent so don't go backward!
-			if (lasttictosend-doomcom->extratics > realfirsttic)
-				supposedtics[n] = lasttictosend-doomcom->extratics;
-			else
-				supposedtics[n] = lasttictosend;
-			if (supposedtics[n] < nettics[n]) supposedtics[n] = nettics[n];
-		}
-	// node 0 is me!
-	supposedtics[0] = maketic;
-}
-
-//
-// TryRunTics
-//
-static void Local_Maketic(INT32 realtics)
-{
-	I_OsPolling(); // I_Getevent
-	D_ProcessEvents(); // menu responder, cons responder,
-	                   // game responder calls HU_Responder, AM_Responder,
-	                   // and G_MapEventsToControls
-	if (!dedicated) rendergametic = gametic;
-	// translate inputs (keyboard/mouse/joystick) into game controls
-	G_BuildTiccmd(&localcmds, realtics, 1);
-	if (splitscreen || botingame)
-		G_BuildTiccmd(&localcmds2, realtics, 2);
-
-	localcmds.angleturn |= TICCMD_RECEIVED;
-	localcmds2.angleturn |= TICCMD_RECEIVED;
-}
-
-// create missed tic
-static void SV_Maketic(void)
-{
-	INT32 i;
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		if (!playeringame[i])
-			continue;
-
-		// We didn't receive this tic
-		if ((netcmds[maketic % BACKUPTICS][i].angleturn & TICCMD_RECEIVED) == 0)
-		{
-			ticcmd_t *    ticcmd = &netcmds[(maketic    ) % BACKUPTICS][i];
-			ticcmd_t *prevticcmd = &netcmds[(maketic - 1) % BACKUPTICS][i];
-
-			if (players[i].quittime)
-			{
-				// Copy the angle/aiming from the previous tic
-				// and empty the other inputs
-				memset(ticcmd, 0, sizeof(netcmds[0][0]));
-				ticcmd->angleturn = prevticcmd->angleturn | TICCMD_RECEIVED;
-				ticcmd->aiming = prevticcmd->aiming;
-			}
-			else
-			{
-				DEBFILE(va("MISS tic%4d for player %d\n", maketic, i));
-				// Copy the input from the previous tic
-				*ticcmd = *prevticcmd;
-				ticcmd->angleturn &= ~TICCMD_RECEIVED;
-			}
-		}
-	}
-
-	// all tic are now proceed make the next
-	maketic++;
-}
-
-boolean TryRunTics(tic_t realtics)
-{
-	boolean ticking;
-
-	// the machine has lagged but it is not so bad
-	if (realtics > TICRATE/7) // FIXME: consistency failure!!
-	{
-		if (server)
-			realtics = 1;
-		else
-			realtics = TICRATE/7;
-	}
-
-	if (singletics)
-		realtics = 1;
-
-	if (realtics >= 1)
-	{
-		COM_BufTicker();
-		if (mapchangepending)
-			D_MapChange(-1, 0, ultimatemode, false, 2, false, fromlevelselect); // finish the map change
-	}
-
-	NetUpdate();
-
-	if (demoplayback)
-	{
-		neededtic = gametic + realtics;
-		// start a game after a demo
-		maketic += realtics;
-		firstticstosend = maketic;
-		tictoclear = firstticstosend;
-	}
-
-	GetPackets();
-
-#ifdef DEBUGFILE
-	if (debugfile && (realtics || neededtic > gametic))
-	{
-		//SoM: 3/30/2000: Need long INT32 in the format string for args 4 & 5.
-		//Shut up stupid warning!
-		fprintf(debugfile, "------------ Tryruntic: REAL:%d NEED:%d GAME:%d LOAD: %d\n",
-			realtics, neededtic, gametic, debugload);
-		debugload = 100000;
-	}
-#endif
-
-	ticking = neededtic > gametic;
-
-	if (ticking)
-	{
-		if (realtics)
-			hu_stopped = false;
-	}
-
-	if (player_joining)
-	{
-		if (realtics)
-			hu_stopped = true;
-		return false;
-	}
-
-	if (ticking)
-	{
-		if (advancedemo)
-		{
-			if (timedemo_quit)
-				COM_ImmedExecute("quit");
-			else
-				D_StartTitle();
-		}
-		else
-			// run the count * tics
-			while (neededtic > gametic)
-			{
-				boolean update_stats = !(paused || P_AutoPause());
-
-				DEBFILE(va("============ Running tic %d (local %d)\n", gametic, localgametic));
-
-				if (update_stats)
-					PS_START_TIMING(ps_tictime);
-
-				G_Ticker((gametic % NEWTICRATERATIO) == 0);
-				ExtraDataTicker();
-				gametic++;
-				consistancy[gametic%BACKUPTICS] = Consistancy();
-
-				if (update_stats)
-				{
-					PS_STOP_TIMING(ps_tictime);
-					PS_UpdateTickStats();
-				}
-
-				// Leave a certain amount of tics present in the net buffer as long as we've ran at least one tic this frame.
-				if (client && gamestate == GS_LEVEL && leveltime > 3 && neededtic <= gametic + cv_netticbuffer.value)
-					break;
-			}
-	}
-	else
-	{
-		if (realtics)
-			hu_stopped = true;
-	}
-
-	return ticking;
-}
-
-/*
-Ping Update except better:
-We call this once per second and check for people's pings. If their ping happens to be too high, we increment some timer and kick them out.
-If they're not lagging, decrement the timer by 1. Of course, reset all of this if they leave.
-*/
-
-static INT32 pingtimeout[MAXPLAYERS];
-
-static inline void PingUpdate(void)
-{
-	INT32 i;
-	boolean laggers[MAXPLAYERS];
-	UINT8 numlaggers = 0;
-	memset(laggers, 0, sizeof(boolean) * MAXPLAYERS);
-
-	netbuffer->packettype = PT_PING;
-
-	//check for ping limit breakage.
-	if (cv_maxping.value)
-	{
-		for (i = 1; i < MAXPLAYERS; i++)
-		{
-			if (playeringame[i] && !players[i].quittime
-			&& (realpingtable[i] / pingmeasurecount > (unsigned)cv_maxping.value))
-			{
-				if (players[i].jointime > 30 * TICRATE)
-					laggers[i] = true;
-				numlaggers++;
-			}
-			else
-				pingtimeout[i] = 0;
-		}
-
-		//kick lagging players... unless everyone but the server's ping sucks.
-		//in that case, it is probably the server's fault.
-		if (numlaggers < D_NumPlayers() - 1)
-		{
-			for (i = 1; i < MAXPLAYERS; i++)
-			{
-				if (playeringame[i] && laggers[i])
-				{
-					pingtimeout[i]++;
-					// ok your net has been bad for too long, you deserve to die.
-					if (pingtimeout[i] > cv_pingtimeout.value)
-					{
-						pingtimeout[i] = 0;
-						SendKick(i, KICK_MSG_PING_HIGH | KICK_MSG_KEEP_BODY);
-					}
-				}
-				/*
-					you aren't lagging,
-					but you aren't free yet.
-					In case you'll keep spiking,
-					we just make the timer go back down. (Very unstable net must still get kicked).
-				*/
-				else
-					pingtimeout[i] = (pingtimeout[i] == 0 ? 0 : pingtimeout[i]-1);
-			}
-		}
-	}
-
-	//make the ping packet and clear server data for next one
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		netbuffer->u.pingtable[i] = realpingtable[i] / pingmeasurecount;
-		//server takes a snapshot of the real ping for display.
-		//otherwise, pings fluctuate a lot and would be odd to look at.
-		playerpingtable[i] = realpingtable[i] / pingmeasurecount;
-		realpingtable[i] = 0; //Reset each as we go.
-	}
-
-	// send the server's maxping as last element of our ping table. This is useful to let us know when we're about to get kicked.
-	netbuffer->u.pingtable[MAXPLAYERS] = cv_maxping.value;
-
-	//send out our ping packets
-	for (i = 0; i < MAXNETNODES; i++)
-		if (nodeingame[i])
-			HSendPacket(i, true, 0, sizeof(INT32) * (MAXPLAYERS+1));
-
-	pingmeasurecount = 1; //Reset count
-}
-
-static tic_t gametime = 0;
-
-static void UpdatePingTable(void)
-{
-	INT32 i;
-
-	if (server)
-	{
-		if (netgame && !(gametime % 35)) // update once per second.
-			PingUpdate();
-		// update node latency values so we can take an average later.
-		for (i = 0; i < MAXPLAYERS; i++)
-			if (playeringame[i] && playernode[i] != UINT8_MAX)
-				realpingtable[i] += G_TicsToMilliseconds(GetLag(playernode[i]));
-		pingmeasurecount++;
-	}
-}
-
-// Handle timeouts to prevent definitive freezes from happenning
-static void HandleNodeTimeouts(void)
-{
-	INT32 i;
-
-	if (server)
-	{
-		for (i = 1; i < MAXNETNODES; i++)
-			if (nodeingame[i] && freezetimeout[i] < I_GetTime())
-				Net_ConnectionTimeout(i);
-
-		// In case the cvar value was lowered
-		if (joindelay)
-			joindelay = min(joindelay - 1, 3 * (tic_t)cv_joindelay.value * TICRATE);
-	}
-}
-
-// Keep the network alive while not advancing tics!
-void NetKeepAlive(void)
-{
-	tic_t nowtime;
-	INT32 realtics;
-
-	nowtime = I_GetTime();
-	realtics = nowtime - gametime;
-
-	// return if there's no time passed since the last call
-	if (realtics <= 0) // nothing new to update
-		return;
-
-	UpdatePingTable();
-
-	GetPackets();
-
-#ifdef MASTERSERVER
-	MasterClient_Ticker();
-#endif
-
-	if (client)
-	{
-		// send keep alive
-		CL_SendClientKeepAlive();
-		// No need to check for resynch because we aren't running any tics
-	}
-	else
-	{
-		SV_SendServerKeepAlive();
-	}
-
-	// No else because no tics are being run and we can't resynch during this
-
-	Net_AckTicker();
-	HandleNodeTimeouts();
-	FileSendTicker();
-}
-
-void NetUpdate(void)
-{
-	static tic_t resptime = 0;
-	tic_t nowtime;
-	INT32 i;
-	INT32 realtics;
-
-	nowtime = I_GetTime();
-	realtics = nowtime - gametime;
-
-	if (realtics <= 0) // nothing new to update
-		return;
-
-	if (realtics > 5)
-	{
-		if (server)
-			realtics = 1;
-		else
-			realtics = 5;
-	}
-
-	if (server && dedicated && gamestate == GS_LEVEL)
-	{
-		const tic_t dedicatedidletime = cv_dedicatedidletime.value * TICRATE;
-		static tic_t dedicatedidletimeprev = 0;
-		static tic_t dedicatedidle = 0;
-
-		if (dedicatedidletime > 0)
-		{
-			for (i = 1; i < MAXNETNODES; ++i)
-				if (nodeingame[i])
-				{
-					if (dedicatedidle >= dedicatedidletime)
-					{
-						CONS_Printf("DEDICATED: Awakening from idle (Node %d detected...)\n", i);
-						dedicatedidle = 0;
-					}
-					break;
-				}
-
-			if (i == MAXNETNODES)
-			{
-				if (leveltime == 2)
-				{
-					// On next tick...
-					dedicatedidle = dedicatedidletime-1;
-				}
-				else if (dedicatedidle >= dedicatedidletime)
-				{
-					if (D_GetExistingTextcmd(gametic, 0) || D_GetExistingTextcmd(gametic+1, 0))
-					{
-						CONS_Printf("DEDICATED: Awakening from idle (Netxcmd detected...)\n");
-						dedicatedidle = 0;
-					}
-					else
-					{
-						realtics = 0;
-					}
-				}
-				else if ((dedicatedidle += realtics) >= dedicatedidletime)
-				{
-					const char *idlereason = "at round start";
-					if (leveltime > 3)
-						idlereason = va("for %d seconds", dedicatedidle/TICRATE);
-
-					CONS_Printf("DEDICATED: No nodes %s, idling...\n", idlereason);
-					realtics = 0;
-					dedicatedidle = dedicatedidletime;
-				}
-			}
-		}
-		else
-		{
-			if (dedicatedidletimeprev > 0 && dedicatedidle >= dedicatedidletimeprev)
-			{
-				CONS_Printf("DEDICATED: Awakening from idle (Idle disabled...)\n");
-			}
-			dedicatedidle = 0;
-		}
-
-		dedicatedidletimeprev = dedicatedidletime;
-	}
-
-	gametime = nowtime;
-
-	UpdatePingTable();
-
-	if (client)
-		maketic = neededtic;
-
-	Local_Maketic(realtics); // make local tic, and call menu?
-
-	if (server)
-		CL_SendClientCmd(); // send it
-
-	GetPackets(); // get packet from client or from server
-
-	// client send the command after a receive of the server
-	// the server send before because in single player is beter
-
-#ifdef MASTERSERVER
-	MasterClient_Ticker(); // Acking the Master Server
-#endif
-
-	if (client)
-	{
-#ifndef NONET
-		// If the client just finished redownloading the game state, load it
-		if (cl_redownloadinggamestate && fileneeded[0].status == FS_FOUND)
-			CL_ReloadReceivedSavegame();
-#endif
-
-		CL_SendClientCmd(); // Send tic cmd
-		hu_redownloadinggamestate = cl_redownloadinggamestate;
-	}
-	else
-	{
-		if (!demoplayback && realtics > 0)
-		{
-			INT32 counts;
-
-			hu_redownloadinggamestate = false;
-
-			// Don't erase tics not acknowledged
-			counts = realtics;
-
-			firstticstosend = gametic;
-			for (i = 0; i < MAXNETNODES; i++)
-			{
-				if (!nodeingame[i])
-					continue;
-				if (nettics[i] < firstticstosend)
-					firstticstosend = nettics[i];
-				if (maketic + counts >= nettics[i] + (BACKUPTICS - TICRATE))
-					Net_ConnectionTimeout(i);
-			}
-
-			if (maketic + counts >= firstticstosend + BACKUPTICS)
-				counts = firstticstosend+BACKUPTICS-maketic-1;
-
-			for (i = 0; i < counts; i++)
-				SV_Maketic(); // Create missed tics and increment maketic
-
-			for (; tictoclear < firstticstosend; tictoclear++) // Clear only when acknowledged
-				D_Clearticcmd(tictoclear);                    // Clear the maketic the new tic
-
-			SV_SendTics();
-
-			neededtic = maketic; // The server is a client too
-		}
-	}
-
-	Net_AckTicker();
-	HandleNodeTimeouts();
-
-	nowtime /= NEWTICRATERATIO;
-
-	if (nowtime > resptime)
-	{
-		resptime = nowtime;
-#ifdef HAVE_THREADS
-		I_lock_mutex(&m_menu_mutex);
-#endif
-		M_Ticker();
-#ifdef HAVE_THREADS
-		I_unlock_mutex(m_menu_mutex);
-#endif
-		CON_Ticker();
-	}
-
-	FileSendTicker();
-}
-
-/** Returns the number of players playing.
-  * \return Number of players. Can be zero if we're running a ::dedicated
-  *         server.
-  * \author Graue <graue@oceanbase.org>
-  */
-INT32 D_NumPlayers(void)
-{
-	INT32 num = 0, ix;
-	for (ix = 0; ix < MAXPLAYERS; ix++)
-		if (playeringame[ix])
-			num++;
-	return num;
-}
-
-/** Similar to the above, but counts only bots.
-  * Purpose is to remove bots from both the player count and the
-  * max player count on the server view
-*/
-INT32 D_NumBots(void)
-{
-	INT32 num = 0, ix;
-	for (ix = 0; ix < MAXPLAYERS; ix++)
-		if (playeringame[ix] && players[ix].bot)
-			num++;
-	return num;
-}
-
-tic_t GetLag(INT32 node)
-{
-	return gametic - nettics[node];
-}
-
-void D_MD5PasswordPass(const UINT8 *buffer, size_t len, const char *salt, void *dest)
-{
-#ifdef NOMD5
-	(void)buffer;
-	(void)len;
-	(void)salt;
-	memset(dest, 0, 16);
-#else
-	char tmpbuf[256];
-	const size_t sl = strlen(salt);
-
-	if (len > 256-sl)
-		len = 256-sl;
-
-	memcpy(tmpbuf, buffer, len);
-	memmove(&tmpbuf[len], salt, sl);
-	//strcpy(&tmpbuf[len], salt);
-	len += strlen(salt);
-	if (len < 256)
-		memset(&tmpbuf[len],0,256-len);
-
-	// Yes, we intentionally md5 the ENTIRE buffer regardless of size...
-	md5_buffer(tmpbuf, 256, dest);
-#endif
-}
diff --git a/src/d_event.h b/src/d_event.h
index 5aa4350600b85a2aafc5a59b87081492a134e048..7743d860947c8b5e2546f2df6a80cc3bdb9679a7 100644
--- a/src/d_event.h
+++ b/src/d_event.h
@@ -22,6 +22,7 @@ typedef enum
 {
 	ev_keydown,
 	ev_keyup,
+	ev_text,
 	ev_console,
 	ev_mouse,
 	ev_joystick,
diff --git a/src/d_main.c b/src/d_main.c
index 24c70843a3bd03d383651d7423914a7fd4d3f0e5..bc821cf71f19982ba725b9ee6a7b198baf8fcf75 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -34,7 +34,7 @@
 #include "doomdef.h"
 #include "am_map.h"
 #include "console.h"
-#include "d_net.h"
+#include "netcode/d_net.h"
 #include "f_finale.h"
 #include "g_game.h"
 #include "hu_stuff.h"
@@ -56,11 +56,11 @@
 #include "w_wad.h"
 #include "z_zone.h"
 #include "d_main.h"
-#include "d_netfil.h"
+#include "netcode/d_netfil.h"
 #include "m_cheat.h"
 #include "y_inter.h"
 #include "p_local.h" // chasecam
-#include "mserv.h" // ms_RoomId
+#include "netcode/mserv.h" // ms_RoomId
 #include "m_misc.h" // screenshot functionality
 #include "deh_tables.h" // Dehacked list test
 #include "m_cond.h" // condition initialization
@@ -192,19 +192,19 @@ void D_ProcessEvents(void)
 		ev = &events[eventtail];
 
 		// Set mouse buttons early in case event is eaten later
-		if (ev->type == ev_keydown || ev->type == ev_keyup)
+		if (ev->type == ev_keydown || ev->type == ev_keyup || ev->type == ev_text)
 		{
 			// Mouse buttons
 			if ((UINT32)(ev->key - KEY_MOUSE1) < MOUSEBUTTONS)
 			{
-				if (ev->type == ev_keydown)
+				if (ev->type == ev_keydown || ev->type == ev_text)
 					mouse.buttons |= 1 << (ev->key - KEY_MOUSE1);
 				else
 					mouse.buttons &= ~(1 << (ev->key - KEY_MOUSE1));
 			}
 			else if ((UINT32)(ev->key - KEY_2MOUSE1) < MOUSEBUTTONS)
 			{
-				if (ev->type == ev_keydown)
+				if (ev->type == ev_keydown || ev->type == ev_text)
 					mouse2.buttons |= 1 << (ev->key - KEY_2MOUSE1);
 				else
 					mouse2.buttons &= ~(1 << (ev->key - KEY_2MOUSE1));
@@ -981,6 +981,7 @@ void D_StartTitle(void)
 	emeralds = 0;
 	memset(&luabanks, 0, sizeof(luabanks));
 	lastmaploaded = 0;
+	pickedchar = R_SkinAvailable(cv_defaultskin.string);
 
 	// In case someone exits out at the same time they start a time attack run,
 	// reset modeattacking
@@ -1613,6 +1614,9 @@ void D_SRB2Main(void)
 	if (D_CheckNetGame())
 		autostart = true;
 
+	if (!dedicated)
+		pickedchar = R_SkinAvailable(cv_defaultskin.string);
+
 	// check for a driver that wants intermission stats
 	// start the apropriate game based on parms
 	if (M_CheckParm("-metal"))
diff --git a/src/d_player.h b/src/d_player.h
index 7ad5b9f811962cb56ffcf55030f2ff1d2aea331d..1409e28b3d5cb86537c8d576249881665b9b8e8e 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -158,6 +158,10 @@ typedef enum
 	PF_FORCESTRAFE = 1<<28, // Turning inputs are translated into strafing inputs
 	PF_CANCARRY    = 1<<29, // Can carry another player?
 	PF_FINISHED    = 1<<30, // The player finished the level. NOT the same as exiting
+	
+	// True if shield button down last tic
+	// This may be the final flag, but 2.3 could free up the others
+	PF_SHIELDDOWN    = 1<<31,
 
 	// up to 1<<31 is free
 } pflags_t;
@@ -607,6 +611,7 @@ typedef struct player_s
 
 	tic_t jointime; // Timer when player joins game to change skin/color
 	tic_t quittime; // Time elapsed since user disconnected, zero if connected
+	tic_t lastinputtime; // the last tic the player has made any input
 #ifdef HWRENDER
 	fixed_t fovadd; // adjust FOV for hw rendering
 #endif
diff --git a/src/d_think.h b/src/d_think.h
index efc1589bf62e277a67ab309f05d33d66af741865..58912458783a2f3b2187e50ebaf00a667a0e0a78 100644
--- a/src/d_think.h
+++ b/src/d_think.h
@@ -51,6 +51,7 @@ typedef struct thinker_s
 	// killough 11/98: count of how many other objects reference
 	// this one using pointers. Used for garbage collection.
 	INT32 references;
+	boolean cachable;
 
 #ifdef PARANOIA
 	INT32 debug_mobjtype;
diff --git a/src/d_ticcmd.h b/src/d_ticcmd.h
index 2481ed738b23769a546fef840500d10ba7b024f5..43eb0f00b8df58da69c2dc7123d274d69070c104 100644
--- a/src/d_ticcmd.h
+++ b/src/d_ticcmd.h
@@ -26,20 +26,23 @@
 // Button/action code definitions.
 typedef enum
 {
-	// First 4 bits are weapon change info, DO NOT USE!
-	BT_WEAPONMASK = 0x0F, //our first four bits.
+	// First 3 bits are weapon change info, DO NOT USE!
+	BT_WEAPONMASK = 0x07,  //our first three bits.
+	
+	BT_SHIELD     = 1<<3,  // shield or super action
 
-	BT_WEAPONNEXT = 1<<4,
-	BT_WEAPONPREV = 1<<5,
-
-	BT_ATTACK     = 1<<6, // shoot rings
-	BT_SPIN       = 1<<7,
-	BT_CAMLEFT    = 1<<8, // turn camera left
-	BT_CAMRIGHT   = 1<<9, // turn camera right
-	BT_TOSSFLAG   = 1<<10,
-	BT_JUMP       = 1<<11,
-	BT_FIRENORMAL = 1<<12, // Fire a normal ring no matter what
+	BT_WEAPONNEXT = 1<<4,  // select next weapon
+	BT_WEAPONPREV = 1<<5,  // select previous weapon
 
+	BT_ATTACK     = 1<<6,  // shoot rings
+	BT_SPIN       = 1<<7,  // spin action
+	BT_CAMLEFT    = 1<<8,  // turn camera left
+	BT_CAMRIGHT   = 1<<9,  // turn camera right
+	BT_TOSSFLAG   = 1<<10, // toss flag or emeralds
+	BT_JUMP       = 1<<11, // jump action
+	BT_FIRENORMAL = 1<<12, // fire a normal ring no matter what
+	
+	// custom lua buttons
 	BT_CUSTOM1    = 1<<13,
 	BT_CUSTOM2    = 1<<14,
 	BT_CUSTOM3    = 1<<15,
diff --git a/src/deh_lua.c b/src/deh_lua.c
index 2bdb51f2746bf51a6dd25643797ab12d4b6129f5..9679780d78f09982fac730208c442fdd7e1717e2 100644
--- a/src/deh_lua.c
+++ b/src/deh_lua.c
@@ -297,7 +297,8 @@ static int ScanConstants(lua_State *L, boolean mathlib, const char *word)
 			CacheAndPushConstant(L, word, (lua_Integer)PF_FULLSTASIS);
 			return 1;
 		}
-		else if (fastcmp(p, "USEDOWN")) // Remove case when 2.3 nears release...
+		// TODO: 2.3: Delete this alias
+		else if (fastcmp(p, "USEDOWN"))
 		{
 			CacheAndPushConstant(L, word, (lua_Integer)PF_SPINDOWN);
 			return 1;
@@ -583,11 +584,12 @@ static int ScanConstants(lua_State *L, boolean mathlib, const char *word)
 		return 0;
 	}
 
-	if (fastcmp(word, "BT_USE")) // Remove case when 2.3 nears release...
+	// TODO: 2.3: Delete this alias
+	if (fastcmp(word, "BT_USE"))
 	{
 		CacheAndPushConstant(L, word, (lua_Integer)BT_SPIN);
 		return 1;
-	}
+	} 
 
 	for (i = 0; INT_CONST[i].n; i++)
 		if (fastcmp(word,INT_CONST[i].n)) {
@@ -806,8 +808,7 @@ int LUA_SOCLib(lua_State *L)
 	lua_register(L,"getActionName",lib_getActionName);
 
 	luaL_newmetatable(L, META_ACTION);
-		lua_pushcfunction(L, action_call);
-		lua_setfield(L, -2, "__call");
+		LUA_SetCFunctionField(L, "__call", action_call);
 	lua_pop(L, 1);
 
 	// Allow access to constants without forcing the use of name comparison checks Lua-side
diff --git a/src/deh_soc.c b/src/deh_soc.c
index 2193cd875cd00898c035c0d47d030f189045572c..6162034de636e54ec8d35f5292720564de8de32d 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -34,7 +34,7 @@
 #include "r_sky.h"
 #include "fastcmp.h"
 #include "lua_script.h" // Reluctantly included for LUA_EvalMath
-#include "d_clisrv.h"
+#include "netcode/d_clisrv.h"
 
 #ifdef HWRENDER
 #include "hardware/hw_light.h"
@@ -911,6 +911,7 @@ static void readspriteframe(MYFILE *f, spriteinfo_t *sprinfo, UINT8 frame)
 				sprinfo->pivot[frame].x = value;
 			else if (fastcmp(word, "YPIVOT"))
 				sprinfo->pivot[frame].y = value;
+			// TODO: 2.3: Delete
 			else if (fastcmp(word, "ROTAXIS"))
 				deh_warning("SpriteInfo: ROTAXIS is deprecated and will be removed.");
 			else
@@ -1617,6 +1618,7 @@ void readlevelheader(MYFILE *f, INT32 num)
 						sizeof(mapheaderinfo[num-1]->musname), va("Level header %d: music", num));
 				}
 			}
+			// TODO: 2.3: Delete
 			else if (fastcmp(word, "MUSICSLOT"))
 				deh_warning("Level header %d: MusicSlot parameter is deprecated and will be removed.\nUse \"Music\" instead.", num);
 			else if (fastcmp(word, "MUSICTRACK"))
diff --git a/src/deh_soc.h b/src/deh_soc.h
index 0cab545f680d6ec86d6e4707b53c2a16291439ba..029390133761c597c88cefb679cc847f8ce5e520 100644
--- a/src/deh_soc.h
+++ b/src/deh_soc.h
@@ -35,7 +35,7 @@
 #include "r_sky.h"
 #include "fastcmp.h"
 #include "lua_script.h" // Reluctantly included for LUA_EvalMath
-#include "d_clisrv.h"
+#include "netcode/d_clisrv.h"
 
 #ifdef HWRENDER
 #include "hardware/hw_light.h"
diff --git a/src/deh_tables.c b/src/deh_tables.c
index 0801cf935a184d5a30d0edd215fe90e2b68db737..baa8ece163ecff24dc6392ad7f48e9477f7409bc 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -370,8 +370,9 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
 	"S_XDEATHSTATE",
 	"S_RAISESTATE",
 
-	// Thok
+	// Thok effect and spin trail
 	"S_THOK",
+	"S_THOKEFFECT",
 
 	// Player
 	"S_PLAY_STND",
@@ -1933,6 +1934,13 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
 	"S_SMALLGRABCHAIN",
 	"S_BIGGRABCHAIN",
 
+	// Blue spring on a ball
+	"S_BLUESPRINGBALL",
+	"S_BLUESPRINGBALL2",
+	"S_BLUESPRINGBALL3",
+	"S_BLUESPRINGBALL4",
+	"S_BLUESPRINGBALL5",
+
 	// Yellow spring on a ball
 	"S_YELLOWSPRINGBALL",
 	"S_YELLOWSPRINGBALL2",
@@ -3553,7 +3561,8 @@ const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for sanity t
 	"MT_NULL",
 	"MT_UNKNOWN",
 
-	"MT_THOK", // Thok! mobj
+	"MT_THOK", // Spin trail mobj
+	"MT_THOKEFFECT", // Thok boom effect
 	"MT_PLAYER",
 	"MT_TAILSOVERLAY", // c:
 	"MT_METALJETFUME",
@@ -3891,6 +3900,7 @@ const char *const MOBJTYPE_LIST[] = {  // array length left dynamic for sanity t
 	"MT_BIGMACE", // Big Mace
 	"MT_SMALLGRABCHAIN", // Small Grab Chain
 	"MT_BIGGRABCHAIN", // Big Grab Chain
+	"MT_BLUESPRINGBALL", // Blue spring on a ball
 	"MT_YELLOWSPRINGBALL", // Yellow spring on a ball
 	"MT_REDSPRINGBALL", // Red spring on a ball
 	"MT_SMALLFIREBAR", // Small Firebar
@@ -4883,7 +4893,7 @@ const char *const MENUTYPES_LIST[] = {
 	"MP_SERVER",
 	"MP_CONNECT",
 	"MP_ROOM",
-	"MP_PLAYERSETUP", // MP_PlayerSetupDef shared with SPLITSCREEN if #defined NONET
+	"MP_PLAYERSETUP",
 	"MP_SERVER_OPTIONS",
 
 	// Options
@@ -5568,7 +5578,8 @@ struct int_const_s const INT_CONST[] = {
 	{"ROTAXIS_Z",ROTAXIS_Z},
 
 	// Buttons (ticcmd_t)
-	{"BT_WEAPONMASK",BT_WEAPONMASK}, //our first four bits.
+	{"BT_WEAPONMASK",BT_WEAPONMASK}, //our first three bits.
+	{"BT_SHIELD",BT_SHIELD},
 	{"BT_WEAPONNEXT",BT_WEAPONNEXT},
 	{"BT_WEAPONPREV",BT_WEAPONPREV},
 	{"BT_ATTACK",BT_ATTACK}, // shoot rings
@@ -5748,9 +5759,7 @@ struct int_const_s const INT_CONST[] = {
 	{"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_SHIELD",GC_SHIELD},
 	{"GC_FIRE",GC_FIRE},
 	{"GC_FIRENORMAL",GC_FIRENORMAL},
 	{"GC_TOSSFLAG",GC_TOSSFLAG},
diff --git a/src/doomdef.h b/src/doomdef.h
index 45d6645faa0bc0ff884e11cb5722e4967f679560..4e08b11bfb0116c2f60fffc33582d6c9a56f3b27 100644
--- a/src/doomdef.h
+++ b/src/doomdef.h
@@ -56,7 +56,6 @@
 #endif
 
 #ifdef _WINDOWS
-#define NONET
 #if !defined (HWRENDER) && !defined (NOHW)
 #define HWRENDER
 #endif
@@ -534,7 +533,7 @@ extern char liveeventbackup[256];
 #define M_GetText(x) (x)
 #endif
 void M_StartupLocale(void);
-extern void *(*M_Memcpy)(void* dest, const void* src, size_t n) FUNCNONNULL;
+void *M_Memcpy(void* dest, const void* src, size_t n);
 char *va(const char *format, ...) FUNCPRINTF;
 char *M_GetToken(const char *inputString);
 void M_UnGetToken(void);
@@ -724,7 +723,7 @@ extern int
 /// Maintain compatibility with older 2.2 demos
 #define OLD22DEMOCOMPAT
 
-#if defined (HAVE_CURL) && ! defined (NONET)
+#ifdef HAVE_CURL
 #define MASTERSERVER
 #else
 #undef UPDATE_ALERT
diff --git a/src/doomstat.h b/src/doomstat.h
index fdd0d0b834ff58601cce67881e399709f9277d8b..6a2d6acf00f816804c1ec8830b69c9848deadcbe 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -634,7 +634,7 @@ extern boolean singletics;
 // Netgame stuff
 // =============
 
-#include "d_clisrv.h"
+#include "netcode/d_clisrv.h"
 
 extern consvar_t cv_timetic; // display high resolution timer
 extern consvar_t cv_powerupdisplay; // display powerups
diff --git a/src/dummy/i_net.c b/src/dummy/i_net.c
index f6e642022e4a54360d68004f69d61eaa5e3acb19..4c30dc767a04615b75e6badf68d3603bc475a575 100644
--- a/src/dummy/i_net.c
+++ b/src/dummy/i_net.c
@@ -1,4 +1,4 @@
-#include "../i_net.h"
+#include "../netcode/i_net.h"
 
 boolean I_InitNetwork(void)
 {
diff --git a/src/dummy/i_sound.c b/src/dummy/i_sound.c
index ba0fc64239267c0a5964e4b619eb367194a60912..436187805da2968c514f640404ffc4bc34f57c87 100644
--- a/src/dummy/i_sound.c
+++ b/src/dummy/i_sound.c
@@ -164,7 +164,7 @@ void I_SetMusicVolume(UINT8 volume)
 	(void)volume;
 }
 
-boolean I_SetSongTrack(int track)
+boolean I_SetSongTrack(INT32 track)
 {
 	(void)track;
 	return false;
diff --git a/src/dummy/i_system.c b/src/dummy/i_system.c
index 70e1ef4ec2b7aba9699731c0d743733750bc26fc..fe33cfe3ef46bdfecf38b3187d3946242b601551 100644
--- a/src/dummy/i_system.c
+++ b/src/dummy/i_system.c
@@ -1,6 +1,7 @@
 #include "../doomdef.h"
 #include "../doomtype.h"
 #include "../i_system.h"
+#include "../i_time.h"
 
 FILE *logstream = NULL;
 
@@ -19,6 +20,11 @@ void I_Sleep(UINT32 ms)
 	(void)ms;
 }
 
+void I_SleepDuration(precise_t duration)
+{
+	(void)duration;
+}
+
 precise_t I_GetPreciseTime(void)
 {
 	return 0;
diff --git a/src/f_finale.c b/src/f_finale.c
index 91c06b31652ab2902668fdb82009519ee6811108..5dc18115c9b07f37e3e36db788ed88489e011ef3 100644
--- a/src/f_finale.c
+++ b/src/f_finale.c
@@ -14,7 +14,7 @@
 #include "doomdef.h"
 #include "doomstat.h"
 #include "d_main.h"
-#include "d_netcmd.h"
+#include "netcode/d_netcmd.h"
 #include "f_finale.h"
 #include "g_game.h"
 #include "hu_stuff.h"
@@ -1327,7 +1327,7 @@ void F_CreditDrawer(void)
 			y += 12<<FRACBITS;
 			break;
 		}
-		if (FixedMul(y,vid.dupy) > vid.height)
+		if (FixedMul(y,vid.dup) > vid.height)
 			break;
 	}
 }
@@ -1362,7 +1362,7 @@ void F_CreditTicker(void)
 			case 1: y += 30<<FRACBITS; break;
 			default: y += 12<<FRACBITS; break;
 		}
-		if (FixedMul(y,vid.dupy) > vid.height)
+		if (FixedMul(y,vid.dup) > vid.height)
 			break;
 	}
 
@@ -2082,7 +2082,7 @@ void F_EndingDrawer(void)
 		if (goodending && finalecount >= TICRATE && finalecount < INFLECTIONPOINT)
 		{
 			INT32 workingtime = finalecount - TICRATE;
-			fixed_t radius = ((vid.width/vid.dupx)*(INFLECTIONPOINT - TICRATE - workingtime))/(INFLECTIONPOINT - TICRATE);
+			fixed_t radius = ((vid.width/vid.dup)*(INFLECTIONPOINT - TICRATE - workingtime))/(INFLECTIONPOINT - TICRATE);
 			angle_t fa;
 			INT32 eemeralds_cur[4];
 			char patchname[7] = "CEMGx0";
@@ -2287,7 +2287,6 @@ void F_InitMenuPresValues(void)
 void F_SkyScroll(const char *patchname)
 {
 	INT32 x, basey = 0;
-	INT32 dupz = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy);
 	patch_t *pat;
 
 	if (rendermode == render_none)
@@ -2315,17 +2314,17 @@ void F_SkyScroll(const char *patchname)
 	curbgy %= pat->height * 16;
 
 	// Ooh, fancy frame interpolation
-	x     = ((curbgx*dupz) + FixedInt((rendertimefrac-FRACUNIT) * curbgxspeed*dupz)) / 16;
-	basey = ((curbgy*dupz) + FixedInt((rendertimefrac-FRACUNIT) * curbgyspeed*dupz)) / 16;
+	x     = ((curbgx*vid.dup) + FixedInt((rendertimefrac-FRACUNIT) * curbgxspeed*vid.dup)) / 16;
+	basey = ((curbgy*vid.dup) + FixedInt((rendertimefrac-FRACUNIT) * curbgyspeed*vid.dup)) / 16;
 
 	if (x     > 0) // Make sure that we don't leave the left or top sides empty
-		x     -= pat->width  * dupz;
+		x     -= pat->width  * vid.dup;
 	if (basey > 0)
-		basey -= pat->height * dupz;
+		basey -= pat->height * vid.dup;
 
-	for (; x < vid.width; x += pat->width * dupz)
+	for (; x < vid.width; x += pat->width * vid.dup)
 	{
-		for (INT32 y = basey; y < vid.height; y += pat->height * dupz)
+		for (INT32 y = basey; y < vid.height; y += pat->height * vid.dup)
 			V_DrawScaledPatch(x, y, V_NOSCALESTART, pat);
 	}
 
@@ -2603,7 +2602,7 @@ static void F_LoadAlacroixGraphics(SINT8 newttscale)
 
 static void F_FigureActiveTtScale(void)
 {
-	SINT8 newttscale = max(1, min(6, vid.dupx));
+	SINT8 newttscale = max(1, min(6, vid.dup));
 	SINT8 oldttscale = activettscale;
 
 	if (newttscale == testttscale)
@@ -4095,7 +4094,7 @@ static fixed_t F_GetPromptHideHudBound(void)
 	F_GetPageTextGeometry(&pagelines, &rightside, &boxh, &texth, &texty, &namey, &chevrony, &textx, &textr);
 
 	// calc boxheight (see V_DrawPromptBack)
-	boxh *= vid.dupy;
+	boxh *= vid.dup;
 	boxh = (boxh * 4) + (boxh/2)*5; // 4 lines of space plus gaps between and some leeway
 
 	// return a coordinate to check
@@ -4525,7 +4524,7 @@ void F_TextPromptDrawer(void)
 			if (players[j].mo->state == states+S_PLAY_STND && players[j].mo->tics != -1)\
 				players[j].mo->tics++;\
 			else if (players[j].mo->state == states+S_PLAY_WAIT)\
-				P_SetPlayerMobjState(players[j].mo, S_PLAY_STND);\
+				P_SetMobjState(players[j].mo, S_PLAY_STND);\
 		}\
 	}
 
diff --git a/src/filesrch.c b/src/filesrch.c
index 9ee64f5ba658752e40a57c9ff0c0d2302ed85ecf..111dfd6e7a8be99e9f9b2764d773ec9d09dadd2a 100644
--- a/src/filesrch.c
+++ b/src/filesrch.c
@@ -26,7 +26,7 @@
 #include <string.h>
 
 #include "filesrch.h"
-#include "d_netfil.h"
+#include "netcode/d_netfil.h"
 #include "m_misc.h"
 #include "z_zone.h"
 #include "m_menu.h" // Addons_option_Onchange
@@ -433,9 +433,19 @@ filestatus_t filesearch(char *filename, const char *startpath, const UINT8 *want
 		// okay, now we actually want searchpath to incorporate d_name
 		strcpy(&searchpath[searchpathindex[depthleft]],dent->d_name);
 
+#if defined(__linux__) || defined(__FreeBSD__)
+		if (dent->d_type == DT_UNKNOWN)
+			if (lstat(searchpath,&fsstat) == 0 && S_ISDIR(fsstat.st_mode))
+				dent->d_type = DT_DIR;
+
+		// Linux and FreeBSD has a special field for file type on dirent, so use that to speed up lookups.
+		// FIXME: should we also follow symlinks?
+		if (dent->d_type == DT_DIR && depthleft)
+#else
 		if (stat(searchpath,&fsstat) < 0) // do we want to follow symlinks? if not: change it to lstat
 			; // was the file (re)moved? can't stat it
 		else if (S_ISDIR(fsstat.st_mode) && depthleft)
+#endif
 		{
 			searchpathindex[--depthleft] = strlen(searchpath) + 1;
 			dirhandle[depthleft] = opendir(searchpath);
diff --git a/src/filesrch.h b/src/filesrch.h
index 59ef5269b194f0918a14927a6fc1eaf003e1a40b..a934c48d61bc9cfa4e31f550c2001bd582908e31 100644
--- a/src/filesrch.h
+++ b/src/filesrch.h
@@ -5,7 +5,7 @@
 #define __FILESRCH_H__
 
 #include "doomdef.h"
-#include "d_netfil.h"
+#include "netcode/d_netfil.h"
 #include "m_menu.h" // MAXSTRINGLENGTH
 #include "w_wad.h"
 
diff --git a/src/g_demo.c b/src/g_demo.c
index 4b9ff56e80f5711faabd18b53a9474f2c0a60c5e..8a8ad1259f9c39d4666b64c0cbdd0108400a6dc9 100644
--- a/src/g_demo.c
+++ b/src/g_demo.c
@@ -15,7 +15,7 @@
 #include "console.h"
 #include "d_main.h"
 #include "d_player.h"
-#include "d_clisrv.h"
+#include "netcode/d_clisrv.h"
 #include "p_setup.h"
 #include "i_time.h"
 #include "i_system.h"
@@ -39,7 +39,7 @@
 #include "v_video.h"
 #include "lua_hook.h"
 #include "md5.h" // demo checksums
-#include "d_netfil.h" // G_CheckDemoExtraFiles
+#include "netcode/d_netfil.h" // G_CheckDemoExtraFiles
 
 boolean timingdemo; // if true, exit with report on completion
 boolean nodrawers; // for comparative timing purposes
@@ -798,32 +798,53 @@ void G_GhostTicker(void)
 					if (type == MT_GHOST)
 					{
 						mobj = P_SpawnGhostMobj(g->mo); // does a large portion of the work for us
-						mobj->frame = (mobj->frame & ~FF_FRAMEMASK)|tr_trans60<<FF_TRANSSHIFT; // P_SpawnGhostMobj sets trans50, we want trans60
+						if (!P_MobjWasRemoved(mobj))
+							mobj->frame = (mobj->frame & ~FF_FRAMEMASK)|tr_trans60<<FF_TRANSSHIFT; // P_SpawnGhostMobj sets trans50, we want trans60
 					}
-					else
+					else if (type == MT_THOKEFFECT)
 					{
-						mobj = P_SpawnMobjFromMobj(g->mo, 0, 0, -FixedDiv(FixedMul(g->mo->info->height, g->mo->scale) - g->mo->height,3*FRACUNIT), MT_THOK);
-						mobj->sprite = states[mobjinfo[type].spawnstate].sprite;
-						mobj->frame = (states[mobjinfo[type].spawnstate].frame & FF_FRAMEMASK) | tr_trans60<<FF_TRANSSHIFT;
+						mobj = P_SpawnMobjFromMobj(g->mo, 0, 0, FixedDiv(g->mo->height, g->mo->scale)*3/4, type);
+						mobj->angle = g->mo->angle + ANGLE_90;
+						mobj->fuse = 7;
+						mobj->scale = g->mo->scale / 3;
+						mobj->destscale = 10 * g->mo->scale;
+						mobj->colorized = true;
 						mobj->color = g->mo->color;
-						mobj->skin = g->mo->skin;
-						P_SetScale(mobj, (mobj->destscale = g->mo->scale));
+						mobj->momx = -g->mo->momx / 2;
+						mobj->momy = -g->mo->momy / 2;
+					}
 
-						if (type == MT_THOK) // spintrail-specific modification for MT_THOK
+					else
+					{
+						mobj = P_SpawnMobjFromMobj(g->mo, 0, 0, -FixedDiv(FixedMul(g->mo->info->height, g->mo->scale) - g->mo->height,3*FRACUNIT), MT_THOK);
+						if (!P_MobjWasRemoved(mobj))
 						{
-							mobj->frame = FF_TRANS80;
-							mobj->fuse = mobj->tics;
+							mobj->sprite = states[mobjinfo[type].spawnstate].sprite;
+							mobj->frame = (states[mobjinfo[type].spawnstate].frame & FF_FRAMEMASK) | tr_trans60<<FF_TRANSSHIFT;
+							mobj->color = g->mo->color;
+							mobj->skin = g->mo->skin;
+							P_SetScale(mobj, (mobj->destscale = g->mo->scale));
+
+							if (type == MT_THOK) // spintrail-specific modification for MT_THOK
+							{
+								mobj->frame = FF_TRANS80;
+								mobj->fuse = mobj->tics;
+							}
+							mobj->tics = -1; // nope.
 						}
-						mobj->tics = -1; // nope.
 					}
-					mobj->floorz = mobj->z;
-					mobj->ceilingz = mobj->z+mobj->height;
-					P_UnsetThingPosition(mobj);
-					mobj->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up...
-					P_SetThingPosition(mobj);
-					if (!mobj->fuse)
-						mobj->fuse = 8;
-					P_SetTarget(&mobj->target, g->mo);
+
+					if (!P_MobjWasRemoved(mobj))
+					{
+						mobj->floorz = mobj->z;
+						mobj->ceilingz = mobj->z+mobj->height;
+						P_UnsetThingPosition(mobj);
+						mobj->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up...
+						P_SetThingPosition(mobj);
+						if (!mobj->fuse)
+							mobj->fuse = 8;
+						P_SetTarget(&mobj->target, g->mo);
+					}
 				}
 			}
 			if (xziptic & EZT_HIT)
@@ -847,6 +868,8 @@ void G_GhostTicker(void)
 					|| health != 0 || i >= 4) // only spawn for the first 4 hits per frame, to prevent ghosts from splode-spamming too bad.
 						continue;
 					poof = P_SpawnMobj(x, y, z, MT_GHOST);
+					if (P_MobjWasRemoved(poof))
+						continue;
 					poof->angle = angle;
 					poof->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up...
 					poof->health = 0;
@@ -892,19 +915,22 @@ void G_GhostTicker(void)
 				if (follow)
 					P_RemoveMobj(follow);
 				P_SetTarget(&follow, P_SpawnMobjFromMobj(g->mo, 0, 0, 0, MT_GHOST));
-				P_SetTarget(&follow->tracer, g->mo);
-				follow->tics = -1;
-				temp = READINT16(g->p)<<FRACBITS;
-				follow->height = FixedMul(follow->scale, temp);
+				if (!P_MobjWasRemoved(follow))
+				{
+					P_SetTarget(&follow->tracer, g->mo);
+					follow->tics = -1;
+					temp = READINT16(g->p)<<FRACBITS;
+					follow->height = FixedMul(follow->scale, temp);
 
-				if (followtic & FZT_LINKDRAW)
-					follow->flags2 |= MF2_LINKDRAW;
+					if (followtic & FZT_LINKDRAW)
+						follow->flags2 |= MF2_LINKDRAW;
 
-				if (followtic & FZT_COLORIZED)
-					follow->colorized = true;
+					if (followtic & FZT_COLORIZED)
+						follow->colorized = true;
 
-				if (followtic & FZT_SKIN)
-					follow->skin = &skins[READUINT8(g->p)];
+					if (followtic & FZT_SKIN)
+						follow->skin = &skins[READUINT8(g->p)];
+				}
 			}
 			if (follow)
 			{
@@ -1094,31 +1120,50 @@ void G_ReadMetalTic(mobj_t *metal)
 				{
 					mobj = P_SpawnGhostMobj(metal); // does a large portion of the work for us
 				}
+				else if (type == MT_THOKEFFECT)
+				{
+					mobj = P_SpawnMobjFromMobj(metal, 0, 0, FixedDiv(metal->height, metal->scale)*3/4, type);
+					mobj->angle = metal->angle + ANGLE_90;
+					mobj->fuse = 7;
+					mobj->scale = metal->scale / 3;
+					mobj->destscale = 10 * metal->scale;
+					mobj->colorized = true;
+					mobj->color = metal->color;
+					mobj->momx = -metal->momx / 2;
+					mobj->momy = -metal->momy / 2;
+				}
 				else
 				{
 					mobj = P_SpawnMobjFromMobj(metal, 0, 0, -FixedDiv(FixedMul(metal->info->height, metal->scale) - metal->height,3*FRACUNIT), MT_THOK);
-					mobj->sprite = states[mobjinfo[type].spawnstate].sprite;
-					mobj->frame = states[mobjinfo[type].spawnstate].frame;
-					mobj->angle = metal->angle;
-					mobj->color = metal->color;
-					mobj->skin = metal->skin;
-					P_SetScale(mobj, (mobj->destscale = metal->scale));
-
-					if (type == MT_THOK) // spintrail-specific modification for MT_THOK
+					if (!P_MobjWasRemoved(mobj))
 					{
-						mobj->frame = FF_TRANS70;
-						mobj->fuse = mobj->tics;
+						mobj->sprite = states[mobjinfo[type].spawnstate].sprite;
+						mobj->frame = states[mobjinfo[type].spawnstate].frame;
+						mobj->angle = metal->angle;
+						mobj->color = metal->color;
+						mobj->skin = metal->skin;
+						P_SetScale(mobj, (mobj->destscale = metal->scale));
+
+						if (type == MT_THOK) // spintrail-specific modification for MT_THOK
+						{
+							mobj->frame = FF_TRANS70;
+							mobj->fuse = mobj->tics;
+						}
+						mobj->tics = -1; // nope.
 					}
-					mobj->tics = -1; // nope.
 				}
-				mobj->floorz = mobj->z;
-				mobj->ceilingz = mobj->z+mobj->height;
-				P_UnsetThingPosition(mobj);
-				mobj->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up...
-				P_SetThingPosition(mobj);
-				if (!mobj->fuse)
-					mobj->fuse = 8;
-				P_SetTarget(&mobj->target, metal);
+
+				if (!P_MobjWasRemoved(mobj))
+				{
+					mobj->floorz = mobj->z;
+					mobj->ceilingz = mobj->z+mobj->height;
+					P_UnsetThingPosition(mobj);
+					mobj->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up...
+					P_SetThingPosition(mobj);
+					if (!mobj->fuse)
+						mobj->fuse = 8;
+					P_SetTarget(&mobj->target, metal);
+				}
 			}
 		}
 		if (xziptic & EZT_SPRITE)
@@ -1140,19 +1185,22 @@ void G_ReadMetalTic(mobj_t *metal)
 				if (follow)
 					P_RemoveMobj(follow);
 				P_SetTarget(&follow, P_SpawnMobjFromMobj(metal, 0, 0, 0, MT_GHOST));
-				P_SetTarget(&follow->tracer, metal);
-				follow->tics = -1;
-				temp = READINT16(metal_p)<<FRACBITS;
-				follow->height = FixedMul(follow->scale, temp);
+				if (!P_MobjWasRemoved(follow))
+				{
+					P_SetTarget(&follow->tracer, metal);
+					follow->tics = -1;
+					temp = READINT16(metal_p)<<FRACBITS;
+					follow->height = FixedMul(follow->scale, temp);
 
-				if (followtic & FZT_LINKDRAW)
-					follow->flags2 |= MF2_LINKDRAW;
+					if (followtic & FZT_LINKDRAW)
+						follow->flags2 |= MF2_LINKDRAW;
 
-				if (followtic & FZT_COLORIZED)
-					follow->colorized = true;
+					if (followtic & FZT_COLORIZED)
+						follow->colorized = true;
 
-				if (followtic & FZT_SKIN)
-					follow->skin = &skins[READUINT8(metal_p)];
+					if (followtic & FZT_SKIN)
+						follow->skin = &skins[READUINT8(metal_p)];
+				}
 			}
 			if (follow)
 			{
@@ -1492,16 +1540,21 @@ void G_BeginRecording(void)
 	demo_p += 16;
 
 	// Skin
-	for (i = 0; i < 16 && cv_skin.string[i]; i++)
-		name[i] = cv_skin.string[i];
+	const char *skinname = skins[players[0].skin].name;
+	for (i = 0; i < 16 && skinname[i]; i++)
+		name[i] = skinname[i];
 	for (; i < 16; i++)
 		name[i] = '\0';
 	M_Memcpy(demo_p,name,16);
 	demo_p += 16;
 
 	// Color
-	for (i = 0; i < MAXCOLORNAME && cv_playercolor.string[i]; i++)
-		name[i] = cv_playercolor.string[i];
+	UINT16 skincolor = players[0].skincolor;
+	if (skincolor >= numskincolors)
+		skincolor = SKINCOLOR_NONE;
+	const char *skincolor_name = skincolors[skincolor].name;
+	for (i = 0; i < MAXCOLORNAME && skincolor_name[i]; i++)
+		name[i] = skincolor_name[i];
 	for (; i < MAXCOLORNAME; i++)
 		name[i] = '\0';
 	M_Memcpy(demo_p,name,MAXCOLORNAME);
@@ -1887,16 +1940,9 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
 	p++; // VERSION
 	p++; // SUBVERSION
 	oldversion = READUINT16(p);
-	switch(oldversion) // demoversion
+	if (oldversion < 0x000c || oldversion > DEMOVERSION)
 	{
-	case DEMOVERSION: // latest always supported
-	case 0x000f: // The previous demoversions also supported 
-	case 0x000e:
-	case 0x000d: // all that changed between then and now was longer color name
-	case 0x000c:
-		break;
-	// too old, cannot support.
-	default:
+		// too old (or new), cannot support
 		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname);
 		Z_Free(buffer);
 		return UINT8_MAX;
@@ -1969,14 +2015,11 @@ void G_DoPlayDemo(char *defdemoname)
 	UINT8 i;
 	lumpnum_t l;
 	char skin[17],color[MAXCOLORNAME+1],*n,*pdemoname;
-	UINT8 version,subversion,charability,charability2,thrustfactor,accelstart,acceleration,cnamelen;
+	UINT8 version,subversion,charability,charability2,thrustfactor,accelstart,acceleration;
 	pflags_t pflags;
 	UINT32 randseed, followitem;
 	fixed_t camerascale,shieldscale,actionspd,mindash,maxdash,normalspeed,runspeed,jumpfactor,height,spinheight;
 	char msg[1024];
-#ifdef OLD22DEMOCOMPAT
-	boolean use_old_demo_vars = false;
-#endif
 
 	skin[16] = '\0';
 	color[MAXCOLORNAME] = '\0';
@@ -2035,23 +2078,13 @@ void G_DoPlayDemo(char *defdemoname)
 	subversion = READUINT8(demo_p);
 	demoversion = READUINT16(demo_p);
 	demo_forwardmove_rng = (demoversion < 0x0010);
-	switch(demoversion)
-	{
-	case 0x000f:
-	case 0x000d:
-	case 0x000e:
-	case DEMOVERSION: // latest always supported
-		cnamelen = MAXCOLORNAME;
-		break;
 #ifdef OLD22DEMOCOMPAT
-	// all that changed between then and now was longer color name
-	case 0x000c:
-		cnamelen = 16;
-		use_old_demo_vars = true;
-		break;
+	if (demoversion < 0x000c || demoversion > DEMOVERSION)
+#else
+	if (demoversion < 0x000d || demoversion > DEMOVERSION)
 #endif
-	// too old, cannot support.
-	default:
+	{
+		// too old (or new), cannot support
 		snprintf(msg, 1024, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemoname);
 		CONS_Alert(CONS_ERROR, "%s", msg);
 		M_StartMessage(msg, NULL, MM_NOTHING);
@@ -2178,8 +2211,8 @@ void G_DoPlayDemo(char *defdemoname)
 	demo_p += 16;
 
 	// Color
-	M_Memcpy(color,demo_p,cnamelen);
-	demo_p += cnamelen;
+	M_Memcpy(color, demo_p, (demoversion < 0x000d) ? 16 : MAXCOLORNAME);
+	demo_p += (demoversion < 0x000d) ? 16 : MAXCOLORNAME;
 
 	charability = READUINT8(demo_p);
 	charability2 = READUINT8(demo_p);
@@ -2215,7 +2248,7 @@ void G_DoPlayDemo(char *defdemoname)
 
 	// net var data
 #ifdef OLD22DEMOCOMPAT
-	if (use_old_demo_vars)
+	if (demoversion < 0x000d)
 		CV_LoadOldDemoVars(&demo_p);
 	else
 #endif
@@ -2263,7 +2296,6 @@ void G_DoPlayDemo(char *defdemoname)
 			players[0].skincolor = i;
 			break;
 		}
-	CV_StealthSetValue(&cv_playercolor, players[0].skincolor);
 	if (players[0].mo)
 	{
 		players[0].mo->color = players[0].skincolor;
@@ -2349,19 +2381,13 @@ UINT8 G_CheckDemoForError(char *defdemoname)
 	demo_p++; // version
 	demo_p++; // subversion
 	our_demo_version = READUINT16(demo_p);
-	switch(our_demo_version)
-	{
-	case 0x000d:
-	case 0x000e:
-	case 0x000f:
-	case DEMOVERSION: // latest always supported
-		break;
 #ifdef OLD22DEMOCOMPAT
-	case 0x000c:
-		break;
+	if (our_demo_version < 0x000c || our_demo_version > DEMOVERSION)
+#else
+	if (our_demo_version < 0x000d || our_demo_version > DEMOVERSION)
 #endif
-	// too old, cannot support.
-	default:
+	{
+		// too old (or new), cannot support
 		return DFILE_ERROR_NOTDEMO;
 	}
 	demo_p += 16; // demo checksum
@@ -2383,7 +2409,6 @@ void G_AddGhost(char *defdemoname)
 	INT32 i;
 	lumpnum_t l;
 	char name[17],skin[17],color[MAXCOLORNAME+1],*n,*pdemoname,md5[16];
-	UINT8 cnamelen;
 	demoghost *gh;
 	UINT8 flags, subversion;
 	UINT8 *buffer,*p;
@@ -2435,20 +2460,9 @@ void G_AddGhost(char *defdemoname)
 	p++; // VERSION
 	subversion = READUINT8(p); // SUBVERSION
 	ghostversion = READUINT16(p);
-	switch(ghostversion)
+	if (ghostversion < 0x000c || ghostversion > DEMOVERSION)
 	{
-	case 0x000f:
-	case 0x000d:
-	case 0x000e:
-	case DEMOVERSION: // latest always supported
-		cnamelen = MAXCOLORNAME;
-		break;
-	// all that changed between then and now was longer color name
-	case 0x000c:
-		cnamelen = 16;
-		break;
-	// too old, cannot support.
-	default:
+		// too old (or new), cannot support
 		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo version incompatible.\n"), pdemoname);
 		Z_Free(pdemoname);
 		Z_Free(buffer);
@@ -2511,8 +2525,8 @@ void G_AddGhost(char *defdemoname)
 	p += 16;
 
 	// Color
-	M_Memcpy(color, p,cnamelen);
-	p += cnamelen;
+	M_Memcpy(color, p, (ghostversion < 0x000d) ? 16 : MAXCOLORNAME);
+	p += (ghostversion < 0x000d) ? 16 : MAXCOLORNAME;
 
 	// Ghosts do not have a player structure to put this in.
 	p++; // charability
@@ -2569,7 +2583,9 @@ void G_AddGhost(char *defdemoname)
 	{ // A bit more complex than P_SpawnPlayer because ghosts aren't solid and won't just push themselves out of the ceiling.
 		fixed_t z,f,c;
 		fixed_t offset = mthing->z << FRACBITS;
-		gh->mo = P_SpawnMobj(mthing->x << FRACBITS, mthing->y << FRACBITS, 0, MT_GHOST);
+		P_SetTarget(&gh->mo, P_SpawnMobj(mthing->x << FRACBITS, mthing->y << FRACBITS, 0, MT_GHOST));
+		if (P_MobjWasRemoved(gh->mo))
+			return;
 		gh->mo->angle = FixedAngle(mthing->angle << FRACBITS);
 		f = gh->mo->floorz;
 		c = gh->mo->ceilingz - mobjinfo[MT_PLAYER].height;
@@ -2695,16 +2711,9 @@ void G_DoPlayMetal(void)
 	metal_p++; // VERSION
 	metal_p++; // SUBVERSION
 	metalversion = READUINT16(metal_p);
-	switch(metalversion)
+	if (metalversion < 0x000c || metalversion > DEMOVERSION)
 	{
-	case DEMOVERSION: // latest always supported
-	case 0x000f:
-	case 0x000e: // There are checks wheter the momentum is from older demo versions or not
-	case 0x000d: // all that changed between then and now was longer color name
-	case 0x000c:
-		break;
-	// too old, cannot support.
-	default:
+		// too old (or new), cannot support
 		CONS_Alert(CONS_WARNING, M_GetText("Failed to load bot recording for this map, format version incompatible.\n"));
 		Z_Free(metalbuffer);
 		return;
diff --git a/src/g_game.c b/src/g_game.c
index 619ed8c89d722280375a30fe3059c58785ae61b2..121672fa7995fb55c355472bf43571a9e277377c 100644
--- a/src/g_game.c
+++ b/src/g_game.c
@@ -15,7 +15,8 @@
 #include "console.h"
 #include "d_main.h"
 #include "d_player.h"
-#include "d_clisrv.h"
+#include "netcode/d_clisrv.h"
+#include "netcode/net_command.h"
 #include "f_finale.h"
 #include "p_setup.h"
 #include "p_saveg.h"
@@ -50,11 +51,14 @@
 #include "r_fps.h" // frame interpolation/uncapped
 
 #include "lua_hud.h"
+#include "lua_libs.h"
 
 gameaction_t gameaction;
 gamestate_t gamestate = GS_NULL;
 UINT8 ultimatemode = false;
 
+INT32 pickedchar;
+
 boolean botingame;
 UINT8 botskin;
 UINT16 botcolor;
@@ -304,7 +308,9 @@ consvar_t cv_chatheight= CVAR_INIT ("chatheight", "8", CV_SAVE, chatheight_cons_
 consvar_t cv_chatnotifications= CVAR_INIT ("chatnotifications", "On", CV_SAVE, CV_OnOff, NULL);
 
 // chat spam protection (why would you want to disable that???)
-consvar_t cv_chatspamprotection= CVAR_INIT ("chatspamprotection", "On", CV_SAVE, CV_OnOff, NULL);
+consvar_t cv_chatspamprotection= CVAR_INIT ("chatspamprotection", "On", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
+consvar_t cv_chatspamspeed= CVAR_INIT ("chatspamspeed", "35", CV_SAVE|CV_NETVAR, CV_Unsigned, NULL);
+consvar_t cv_chatspamburst= CVAR_INIT ("chatspamburst", "3", CV_SAVE|CV_NETVAR, CV_Unsigned, NULL);
 
 // minichat text background
 consvar_t cv_chatbacktint = CVAR_INIT ("chatbacktint", "On", CV_SAVE, CV_OnOff, NULL);
@@ -1167,7 +1173,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	// why build a ticcmd if we're paused?
 	// Or, for that matter, if we're being reborn.
 	// ...OR if we're blindfolded. No looking into the floor.
-	if (paused || P_AutoPause() || (gamestate == GS_LEVEL && (player->playerstate == PST_REBORN || ((gametyperules & GTR_TAG)
+	if (ignoregameinputs || paused || P_AutoPause() || (gamestate == GS_LEVEL && (player->playerstate == PST_REBORN || ((gametyperules & GTR_TAG)
 	&& (leveltime < hidetime * TICRATE) && (player->pflags & PF_TAGIT)))))
 	{//@TODO splitscreen player
 		cmd->angleturn = ticcmd_oldangleturn[forplayer];
@@ -1331,7 +1337,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 #if NUM_WEAPONS > 10
 "Add extra inputs to g_input.h/gamecontrols_e"
 #endif
-	//use the four avaliable bits to determine the weapon.
+	//use the three avaliable bits to determine the weapon.
 	cmd->buttons &= ~BT_WEAPONMASK;
 	for (i = 0; i < NUM_WEAPONS; ++i)
 		if (PLAYERINPUTDOWN(ssplayer, GC_WEPSLOT1 + i))
@@ -1349,9 +1355,14 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 	axis = PlayerJoyAxis(ssplayer, JA_FIRENORMAL);
 	if (PLAYERINPUTDOWN(ssplayer, GC_FIRENORMAL) || (usejoystick && axis > 0))
 		cmd->buttons |= BT_FIRENORMAL;
-
+	
+	// Toss flag button
 	if (PLAYERINPUTDOWN(ssplayer, GC_TOSSFLAG))
 		cmd->buttons |= BT_TOSSFLAG;
+	
+	// Shield button
+	if (PLAYERINPUTDOWN(ssplayer, GC_SHIELD))
+		cmd->buttons |= BT_SHIELD;
 
 	// Lua scriptable buttons
 	if (PLAYERINPUTDOWN(ssplayer, GC_CUSTOM1))
@@ -2744,6 +2755,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	p->pflags |= PF_SPINDOWN;
 	p->pflags |= PF_ATTACKDOWN;
 	p->pflags |= PF_JUMPDOWN;
+	p->pflags |= PF_SHIELDDOWN;
 
 	p->playerstate = PST_LIVE;
 	p->panim = PA_IDLE; // standing animation
@@ -2751,25 +2763,6 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
 	//if ((netgame || multiplayer) && !p->spectator) -- moved into P_SpawnPlayer to account for forced changes there
 		//p->powers[pw_flashing] = flashingtics-1; // Babysitting deterrent
 
-	// Check to make sure their color didn't change somehow...
-	if (G_GametypeHasTeams())
-	{
-		if (p->ctfteam == 1 && p->skincolor != skincolor_redteam)
-		{
-			if (p == &players[consoleplayer])
-				CV_SetValue(&cv_playercolor, skincolor_redteam);
-			else if (p == &players[secondarydisplayplayer])
-				CV_SetValue(&cv_playercolor2, skincolor_redteam);
-		}
-		else if (p->ctfteam == 2 && p->skincolor != skincolor_blueteam)
-		{
-			if (p == &players[consoleplayer])
-				CV_SetValue(&cv_playercolor, skincolor_blueteam);
-			else if (p == &players[secondarydisplayplayer])
-				CV_SetValue(&cv_playercolor2, skincolor_blueteam);
-		}
-	}
-
 	if (betweenmaps)
 		return;
 
@@ -4362,7 +4355,7 @@ void G_LoadGameSettings(void)
 }
 
 #define GAMEDATA_ID 0x86E4A27C // Change every major version, as usual
-#define COMPAT_GAMEDATA_ID 0xFCAFE211 // Can be removed entirely for 2.3
+#define COMPAT_GAMEDATA_ID 0xFCAFE211 // TODO: 2.3: Delete
 
 // G_LoadGameData
 // Loads the main data file, which stores information such as emblems found, etc.
@@ -4805,12 +4798,9 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
 	Z_Free(savebuffer);
 	save_p = savebuffer = NULL;
 
-//	gameaction = ga_nothing;
-//	G_SetGamestate(GS_LEVEL);
 	displayplayer = consoleplayer;
 	multiplayer = splitscreen = false;
 
-//	G_DeferedInitNew(sk_medium, G_BuildMapName(1), 0, 0, 1);
 	if (setsizeneeded)
 		R_ExecuteSetViewSize();
 
@@ -5003,9 +4993,9 @@ cleanup:
 // Can be called by the startup code or the menu task,
 // consoleplayer, displayplayer, playeringame[] should be set.
 //
-void G_DeferedInitNew(boolean pultmode, const char *mapname, INT32 pickedchar, boolean SSSG, boolean FLS)
+void G_DeferedInitNew(boolean pultmode, const char *mapname, INT32 character, boolean SSSG, boolean FLS)
 {
-	UINT16 color = skins[pickedchar].prefcolor;
+	pickedchar = character;
 	paused = false;
 
 	if (demoplayback)
@@ -5026,10 +5016,7 @@ void G_DeferedInitNew(boolean pultmode, const char *mapname, INT32 pickedchar, b
 		SplitScreen_OnChange();
 	}
 
-	color = skins[pickedchar].prefcolor;
-	SetPlayerSkinByNum(consoleplayer, pickedchar);
-	CV_StealthSet(&cv_skin, skins[pickedchar].name);
-	CV_StealthSetValue(&cv_playercolor, color);
+	SetPlayerSkinByNum(consoleplayer, character);
 
 	if (mapname)
 		D_MapChange(M_MapNumber(mapname[3], mapname[4]), gametype, pultmode, true, 1, false, FLS);
@@ -5109,6 +5096,10 @@ void G_InitNew(UINT8 pultmode, const char *mapname, boolean resetplayer, boolean
 			CV_StealthSetValue(&cv_itemfinder, 0);
 	}
 
+	// Restore each player's skin if it was previously forced to be a specific one
+	// (Looks a bit silly, but it works.)
+	boolean reset_skin = netgame && mapheaderinfo[gamemap-1] && mapheaderinfo[gamemap-1]->forcecharacter[0] != '\0';
+
 	// internal game map
 	// well this check is useless because it is done before (d_netcmd.c::command_map_f)
 	// but in case of for demos....
@@ -5136,6 +5127,9 @@ void G_InitNew(UINT8 pultmode, const char *mapname, boolean resetplayer, boolean
 	automapactive = false;
 	imcontinuing = false;
 
+	if (reset_skin)
+		D_SendPlayerConfig();
+
 	// fetch saved data if available
 	if (savedata.lives > 0)
 	{
@@ -5388,16 +5382,29 @@ void G_FreeMapSearch(mapsearchfreq_t *freq, INT32 freqc)
 INT32 G_FindMapByNameOrCode(const char *mapname, char **realmapnamep)
 {
 	boolean usemapcode = false;
-
 	INT32 newmapnum;
-
-	size_t mapnamelen;
-
+	size_t mapnamelen = strlen(mapname);
 	char *p;
 
-	mapnamelen = strlen(mapname);
-
-	if (mapnamelen == 2)/* maybe two digit code */
+	if (mapnamelen == 1)
+	{
+		if (mapname[0] == '*') // current map
+		{
+			usemapcode = true;
+			newmapnum = gamemap;
+		}
+		else if (mapname[0] == '+' && mapheaderinfo[gamemap-1]) // next map
+		{
+			usemapcode = true;
+			newmapnum = mapheaderinfo[gamemap-1]->nextlevel;
+			if (newmapnum < 1 || newmapnum > NUMMAPS)
+			{
+				CONS_Alert(CONS_ERROR, M_GetText("NextLevel (%d) is not a valid map.\n"), newmapnum);
+				return 0;
+			}
+		}
+	}
+	else if (mapnamelen == 2)/* maybe two digit code */
 	{
 		if (( newmapnum = M_MapNumber(mapname[0], mapname[1]) ))
 			usemapcode = true;
diff --git a/src/g_game.h b/src/g_game.h
index 9873430b936fd4b79f89d0adbcf714a303fae2ce..2612224a1252266346c352aa0294fee28047fe15 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -53,7 +53,7 @@ extern consvar_t cv_instantretry;
 
 // used in game menu
 extern consvar_t cv_tutorialprompt;
-extern consvar_t cv_chatwidth, cv_chatnotifications, cv_chatheight, cv_chattime, cv_consolechat, cv_chatbacktint, cv_chatspamprotection, cv_compactscoreboard;
+extern consvar_t cv_chatwidth, cv_chatnotifications, cv_chatheight, cv_chattime, cv_consolechat, cv_chatbacktint, cv_chatspamprotection, cv_chatspamspeed, cv_chatspamburst, cv_compactscoreboard;
 extern consvar_t cv_crosshair, cv_crosshair2;
 extern consvar_t cv_invertmouse, cv_alwaysfreelook, cv_chasefreelook, cv_mousemove;
 extern consvar_t cv_invertmouse2, cv_alwaysfreelook2, cv_chasefreelook2, cv_mousemove2;
@@ -176,8 +176,7 @@ void G_SpawnPlayer(INT32 playernum);
 
 // Can be called by the startup code or M_Responder.
 // A normal game starts at map 1, but a warp test can start elsewhere
-void G_DeferedInitNew(boolean pultmode, const char *mapname, INT32 pickedchar,
-	boolean SSSG, boolean FLS);
+void G_DeferedInitNew(boolean pultmode, const char *mapname, INT32 character, boolean SSSG, boolean FLS);
 void G_DoLoadLevel(boolean resetplayer);
 void G_StartTitleCard(void);
 void G_PreLevelTitleCard(void);
diff --git a/src/g_input.c b/src/g_input.c
index 826dcecbdf22b6ff833fce6e65bfb71ef051d05d..3f1be37ba3f588fb2f3b2f6012ae7bf29c397b98 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -16,8 +16,10 @@
 #include "g_input.h"
 #include "keys.h"
 #include "hu_stuff.h" // need HUFONT start & end
-#include "d_net.h"
+#include "netcode/d_net.h"
 #include "console.h"
+#include "lua_script.h"
+#include "lua_libs.h"
 
 #define MAXMOUSESENSITIVITY 100 // sensitivity steps
 
@@ -116,7 +118,10 @@ void G_MapEventsToControls(event_t *ev)
 	{
 		case ev_keydown:
 			if (ev->key < NUMINPUTS)
-				gamekeydown[ev->key] = 1;
+			{
+				if (!ignoregameinputs)
+					gamekeydown[ev->key] = 1;
+			}
 #ifdef PARANOIA
 			else
 			{
@@ -144,7 +149,7 @@ void G_MapEventsToControls(event_t *ev)
 
 		case ev_joystick: // buttons are virtual keys
 			i = ev->key;
-			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on)
+			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on || ignoregameinputs)
 				break;
 			if (ev->x != INT32_MAX) joyxmove[i] = ev->x;
 			if (ev->y != INT32_MAX) joyymove[i] = ev->y;
@@ -152,7 +157,7 @@ void G_MapEventsToControls(event_t *ev)
 
 		case ev_joystick2: // buttons are virtual keys
 			i = ev->key;
-			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on)
+			if (i >= JOYAXISSET || menuactive || CON_Ready() || chat_on || ignoregameinputs)
 				break;
 			if (ev->x != INT32_MAX) joy2xmove[i] = ev->x;
 			if (ev->y != INT32_MAX) joy2ymove[i] = ev->y;
@@ -571,9 +576,7 @@ static const char *gamecontrolname[NUM_GAMECONTROLS] =
 	"weapon5",
 	"weapon6",
 	"weapon7",
-	"weapon8",
-	"weapon9",
-	"weapon10",
+	"shield",
 	"fire",
 	"firenormal",
 	"tossflag",
@@ -688,6 +691,7 @@ void G_DefineDefaultControls(void)
 	gamecontroldefault[gcs_fps][GC_CENTERVIEW ][0] = KEY_LCTRL;
 	gamecontroldefault[gcs_fps][GC_JUMP       ][0] = KEY_SPACE;
 	gamecontroldefault[gcs_fps][GC_SPIN       ][0] = KEY_LSHIFT;
+	gamecontroldefault[gcs_fps][GC_SHIELD     ][0] = KEY_LALT;
 	gamecontroldefault[gcs_fps][GC_FIRE       ][0] = KEY_RCTRL;
 	gamecontroldefault[gcs_fps][GC_FIRE       ][1] = KEY_MOUSE1+0;
 	gamecontroldefault[gcs_fps][GC_FIRENORMAL ][0] = KEY_RALT;
@@ -708,6 +712,7 @@ void G_DefineDefaultControls(void)
 	gamecontroldefault[gcs_platform][GC_CENTERVIEW ][0] = KEY_END;
 	gamecontroldefault[gcs_platform][GC_JUMP       ][0] = KEY_SPACE;
 	gamecontroldefault[gcs_platform][GC_SPIN       ][0] = KEY_LSHIFT;
+	gamecontroldefault[gcs_platform][GC_SHIELD     ][0] = KEY_LALT;
 	gamecontroldefault[gcs_platform][GC_FIRE       ][0] = 's';
 	gamecontroldefault[gcs_platform][GC_FIRE       ][1] = KEY_MOUSE1+0;
 	gamecontroldefault[gcs_platform][GC_FIRENORMAL ][0] = 'w';
@@ -723,9 +728,6 @@ void G_DefineDefaultControls(void)
 		gamecontroldefault[i][GC_WEPSLOT5     ][0] = '5';
 		gamecontroldefault[i][GC_WEPSLOT6     ][0] = '6';
 		gamecontroldefault[i][GC_WEPSLOT7     ][0] = '7';
-		gamecontroldefault[i][GC_WEPSLOT8     ][0] = '8';
-		gamecontroldefault[i][GC_WEPSLOT9     ][0] = '9';
-		gamecontroldefault[i][GC_WEPSLOT10    ][0] = '0';
 		gamecontroldefault[i][GC_TOSSFLAG     ][0] = '\'';
 		gamecontroldefault[i][GC_CAMTOGGLE    ][0] = 'v';
 		gamecontroldefault[i][GC_CAMRESET     ][0] = 'r';
@@ -744,15 +746,15 @@ void G_DefineDefaultControls(void)
 		gamecontroldefault[i][GC_CUSTOM1      ][1] = KEY_JOY1+1; // B
 		gamecontroldefault[i][GC_CUSTOM2      ][1] = KEY_JOY1+3; // Y
 		gamecontroldefault[i][GC_CUSTOM3      ][1] = KEY_JOY1+8; // Left Stick
-		gamecontroldefault[i][GC_CAMTOGGLE    ][1] = KEY_JOY1+4; // LB
+		gamecontroldefault[i][GC_SHIELD       ][1] = KEY_JOY1+4; // LB
 		gamecontroldefault[i][GC_CENTERVIEW   ][1] = KEY_JOY1+5; // RB
-		gamecontroldefault[i][GC_SCREENSHOT   ][1] = KEY_JOY1+6; // Back
+		gamecontroldefault[i][GC_SCORES       ][1] = KEY_JOY1+6; // Back
 		gamecontroldefault[i][GC_SYSTEMMENU   ][0] = KEY_JOY1+7; // Start
 		gamecontroldefault[i][GC_WEAPONPREV   ][1] = KEY_HAT1+2; // D-Pad Left
 		gamecontroldefault[i][GC_WEAPONNEXT   ][1] = KEY_HAT1+3; // D-Pad Right
 		gamecontroldefault[i][GC_VIEWPOINTNEXT][1] = KEY_JOY1+9; // Right Stick
 		gamecontroldefault[i][GC_TOSSFLAG     ][1] = KEY_HAT1+0; // D-Pad Up
-		gamecontroldefault[i][GC_SCORES       ][1] = KEY_HAT1+1; // D-Pad Down
+		gamecontroldefault[i][GC_CAMTOGGLE    ][1] = KEY_HAT1+1; // D-Pad Down
 
 		// Second player controls only have joypad defaults
 		gamecontrolbisdefault[i][GC_JUMP         ][1] = KEY_2JOY1+0; // A
@@ -760,15 +762,15 @@ void G_DefineDefaultControls(void)
 		gamecontrolbisdefault[i][GC_CUSTOM1      ][1] = KEY_2JOY1+1; // B
 		gamecontrolbisdefault[i][GC_CUSTOM2      ][1] = KEY_2JOY1+3; // Y
 		gamecontrolbisdefault[i][GC_CUSTOM3      ][1] = KEY_2JOY1+8; // Left Stick
-		gamecontrolbisdefault[i][GC_CAMTOGGLE    ][1] = KEY_2JOY1+4; // LB
+		gamecontrolbisdefault[i][GC_SHIELD       ][1] = KEY_2JOY1+4; // LB
 		gamecontrolbisdefault[i][GC_CENTERVIEW   ][1] = KEY_2JOY1+5; // RB
-		gamecontrolbisdefault[i][GC_SCREENSHOT   ][1] = KEY_2JOY1+6; // Back
+		//gamecontrolbisdefault[i][GC_SCORES       ][1] = KEY_2JOY1+6; // Back
 		//gamecontrolbisdefault[i][GC_SYSTEMMENU   ][0] = KEY_2JOY1+7; // Start
 		gamecontrolbisdefault[i][GC_WEAPONPREV   ][1] = KEY_2HAT1+2; // D-Pad Left
 		gamecontrolbisdefault[i][GC_WEAPONNEXT   ][1] = KEY_2HAT1+3; // D-Pad Right
 		gamecontrolbisdefault[i][GC_VIEWPOINTNEXT][1] = KEY_2JOY1+9; // Right Stick
 		gamecontrolbisdefault[i][GC_TOSSFLAG     ][1] = KEY_2HAT1+0; // D-Pad Up
-		//gamecontrolbisdefault[i][GC_SCORES       ][1] = KEY_2HAT1+1; // D-Pad Down
+		gamecontrolbisdefault[i][GC_CAMTOGGLE    ][1] = KEY_2HAT1+1; // D-Pad Down
 	}
 }
 
@@ -997,8 +999,9 @@ static void setcontrol(INT32 (*gc)[2])
 	INT32 player = ((void*)gc == (void*)&gamecontrolbis ? 1 : 0);
 	boolean nestedoverride = false;
 
-	// Update me for 2.3
+	// TODO: 2.3: Delete the "use" alias
 	namectrl = (stricmp(COM_Argv(1), "use")) ? COM_Argv(1) : "spin";
+		
 
 	for (numctrl = 0; numctrl < NUM_GAMECONTROLS && stricmp(namectrl, gamecontrolname[numctrl]);
 		numctrl++)
diff --git a/src/g_input.h b/src/g_input.h
index e9c909e6e2c222360086874ddb27e2495b4e0381..48c103076667df50884767685386b98bd4865a02 100644
--- a/src/g_input.h
+++ b/src/g_input.h
@@ -74,9 +74,7 @@ typedef enum
 	GC_WEPSLOT5,
 	GC_WEPSLOT6,
 	GC_WEPSLOT7,
-	GC_WEPSLOT8,
-	GC_WEPSLOT9,
-	GC_WEPSLOT10,
+	GC_SHIELD,
 	GC_FIRE,
 	GC_FIRENORMAL,
 	GC_TOSSFLAG,
diff --git a/src/g_state.h b/src/g_state.h
index 8f97930bb6d7b2ce736382d443f9d9f89d83b03a..4b07f3d903cd1c90efd3c28a68f1c9897061f4eb 100644
--- a/src/g_state.h
+++ b/src/g_state.h
@@ -53,9 +53,11 @@ typedef enum
 
 extern gamestate_t gamestate;
 extern UINT8 titlemapinaction;
-extern UINT8 ultimatemode; // was sk_insane
+extern UINT8 ultimatemode;
 extern gameaction_t gameaction;
 
+extern INT32 pickedchar;
+
 extern boolean botingame;
 extern UINT8 botskin;
 extern UINT16 botcolor;
diff --git a/src/hardware/hw_defs.h b/src/hardware/hw_defs.h
index 74c4ed7d2eec91d1c846ca0a43f23d636a1024f8..3b660cc70c36515dab60b4ae3eabb914b5de7d5d 100644
--- a/src/hardware/hw_defs.h
+++ b/src/hardware/hw_defs.h
@@ -276,9 +276,6 @@ struct FSurfaceInfo
 };
 typedef struct FSurfaceInfo FSurfaceInfo;
 
-#define GL_DEFAULTMIX 0x00000000
-#define GL_DEFAULTFOG 0xFF000000
-
 //Hurdler: added for backward compatibility
 enum hwdsetspecialstate
 {
@@ -286,6 +283,7 @@ enum hwdsetspecialstate
 	HWD_SET_SHADERS,
 	HWD_SET_TEXTUREFILTERMODE,
 	HWD_SET_TEXTUREANISOTROPICMODE,
+	HWD_SET_WIREFRAME,
 	HWD_NUMSTATE
 };
 
diff --git a/src/hardware/hw_draw.c b/src/hardware/hw_draw.c
index eb0b9e332297a0219c1ef19f922dea5b8547d868..ec9dc76130425df42ade0d53b9d597494d8bd7f5 100644
--- a/src/hardware/hw_draw.c
+++ b/src/hardware/hw_draw.c
@@ -78,10 +78,8 @@ void HWR_DrawPatch(patch_t *gpatch, INT32 x, INT32 y, INT32 option)
 //  | /|
 //  |/ |
 //  0--1
-	float sdupx = FIXED_TO_FLOAT(vid.fdupx)*2.0f;
-	float sdupy = FIXED_TO_FLOAT(vid.fdupy)*2.0f;
-	float pdupx = FIXED_TO_FLOAT(vid.fdupx)*2.0f;
-	float pdupy = FIXED_TO_FLOAT(vid.fdupy)*2.0f;
+	float sdup = FIXED_TO_FLOAT(vid.fdup)*2.0f;
+	float pdup = FIXED_TO_FLOAT(vid.fdup)*2.0f;
 
 	// make patch ready in hardware cache
 	HWR_GetPatch(gpatch);
@@ -90,25 +88,23 @@ void HWR_DrawPatch(patch_t *gpatch, INT32 x, INT32 y, INT32 option)
 	switch (option & V_SCALEPATCHMASK)
 	{
 	case V_NOSCALEPATCH:
-		pdupx = pdupy = 2.0f;
+		pdup = 2.0f;
 		break;
 	case V_SMALLSCALEPATCH:
-		pdupx = 2.0f * FIXED_TO_FLOAT(vid.fsmalldupx);
-		pdupy = 2.0f * FIXED_TO_FLOAT(vid.fsmalldupy);
+		pdup = 2.0f * FIXED_TO_FLOAT(vid.fsmalldup);
 		break;
 	case V_MEDSCALEPATCH:
-		pdupx = 2.0f * FIXED_TO_FLOAT(vid.fmeddupx);
-		pdupy = 2.0f * FIXED_TO_FLOAT(vid.fmeddupy);
+		pdup = 2.0f * FIXED_TO_FLOAT(vid.fmeddup);
 		break;
 	}
 
 	if (option & V_NOSCALESTART)
-		sdupx = sdupy = 2.0f;
+		sdup = 2.0f;
 
-	v[0].x = v[3].x = (x*sdupx-(gpatch->leftoffset)*pdupx)/vid.width - 1;
-	v[2].x = v[1].x = (x*sdupx+(gpatch->width-gpatch->leftoffset)*pdupx)/vid.width - 1;
-	v[0].y = v[1].y = 1-(y*sdupy-(gpatch->topoffset)*pdupy)/vid.height;
-	v[2].y = v[3].y = 1-(y*sdupy+(gpatch->height-gpatch->topoffset)*pdupy)/vid.height;
+	v[0].x = v[3].x = (x*sdup-(gpatch->leftoffset)*pdup)/vid.width - 1;
+	v[2].x = v[1].x = (x*sdup+(gpatch->width-gpatch->leftoffset)*pdup)/vid.width - 1;
+	v[0].y = v[1].y = 1-(y*sdup-(gpatch->topoffset)*pdup)/vid.height;
+	v[2].y = v[3].y = 1-(y*sdup+(gpatch->height-gpatch->topoffset)*pdup)/vid.height;
 
 	v[0].z = v[1].z = v[2].z = v[3].z = 1.0f;
 
@@ -137,7 +133,7 @@ void HWR_DrawStretchyFixedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t p
 //  | /|
 //  |/ |
 //  0--1
-	float dupx, dupy, fscalew, fscaleh, fwidth, fheight;
+	float dup, fscalew, fscaleh, fwidth, fheight;
 
 	UINT8 perplayershuffle = 0;
 
@@ -149,25 +145,21 @@ void HWR_DrawStretchyFixedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t p
 
 	hwrPatch = ((GLPatch_t *)gpatch->hardware);
 
-	dupx = (float)vid.dupx;
-	dupy = (float)vid.dupy;
+	dup = (float)vid.dup;
 
 	switch (option & V_SCALEPATCHMASK)
 	{
 	case V_NOSCALEPATCH:
-		dupx = dupy = 1.0f;
+		dup = 1.0f;
 		break;
 	case V_SMALLSCALEPATCH:
-		dupx = (float)vid.smalldupx;
-		dupy = (float)vid.smalldupy;
+		dup = (float)vid.smalldup;
 		break;
 	case V_MEDSCALEPATCH:
-		dupx = (float)vid.meddupx;
-		dupy = (float)vid.meddupy;
+		dup = (float)vid.meddup;
 		break;
 	}
 
-	dupx = dupy = (dupx < dupy ? dupx : dupy);
 	fscalew = fscaleh = FIXED_TO_FLOAT(pscale);
 	if (vscale != pscale)
 		fscaleh = FIXED_TO_FLOAT(vscale);
@@ -261,8 +253,8 @@ void HWR_DrawStretchyFixedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t p
 
 	if (!(option & V_NOSCALESTART))
 	{
-		cx = cx * dupx;
-		cy = cy * dupy;
+		cx = cx * dup;
+		cy = cy * dup;
 
 		if (!(option & V_SCALEPATCHMASK))
 		{
@@ -279,40 +271,40 @@ void HWR_DrawStretchyFixedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t p
 				}
 			}
 			// centre screen
-			if (fabsf((float)vid.width - (float)BASEVIDWIDTH * dupx) > 1.0E-36f)
+			if (fabsf((float)vid.width - (float)BASEVIDWIDTH * dup) > 1.0E-36f)
 			{
 				if (option & V_SNAPTORIGHT)
-					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx));
+					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dup));
 				else if (!(option & V_SNAPTOLEFT))
-					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx))/2;
+					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dup))/2;
 				if (perplayershuffle & 4)
-					cx -= ((float)vid.width - ((float)BASEVIDWIDTH * dupx))/4;
+					cx -= ((float)vid.width - ((float)BASEVIDWIDTH * dup))/4;
 				else if (perplayershuffle & 8)
-					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx))/4;
+					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dup))/4;
 			}
-			if (fabsf((float)vid.height - (float)BASEVIDHEIGHT * dupy) > 1.0E-36f)
+			if (fabsf((float)vid.height - (float)BASEVIDHEIGHT * dup) > 1.0E-36f)
 			{
 				if (option & V_SNAPTOBOTTOM)
-					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy));
+					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dup));
 				else if (!(option & V_SNAPTOTOP))
-					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy))/2;
+					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dup))/2;
 				if (perplayershuffle & 1)
-					cy -= ((float)vid.height - ((float)BASEVIDHEIGHT * dupy))/4;
+					cy -= ((float)vid.height - ((float)BASEVIDHEIGHT * dup))/4;
 				else if (perplayershuffle & 2)
-					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy))/4;
+					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dup))/4;
 			}
 		}
 	}
 
 	if (pscale != FRACUNIT || vscale != FRACUNIT || (splitscreen && option & V_PERPLAYER))
 	{
-		fwidth = (float)(gpatch->width) * fscalew * dupx;
-		fheight = (float)(gpatch->height) * fscaleh * dupy;
+		fwidth = (float)(gpatch->width) * fscalew * dup;
+		fheight = (float)(gpatch->height) * fscaleh * dup;
 	}
 	else
 	{
-		fwidth = (float)(gpatch->width) * dupx;
-		fheight = (float)(gpatch->height) * dupy;
+		fwidth = (float)(gpatch->width) * dup;
+		fheight = (float)(gpatch->height) * dup;
 	}
 
 	// positions of the cx, cy, are between 0 and vid.width/vid.height now, we need them to be between -1 and 1
@@ -379,7 +371,7 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 //  | /|
 //  |/ |
 //  0--1
-	float dupx, dupy, fscalew, fscaleh, fwidth, fheight;
+	float dup, fscalew, fscaleh, fwidth, fheight;
 
 	UINT8 perplayershuffle = 0;
 
@@ -391,25 +383,21 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 
 	hwrPatch = ((GLPatch_t *)gpatch->hardware);
 
-	dupx = (float)vid.dupx;
-	dupy = (float)vid.dupy;
+	dup = (float)vid.dup;
 
 	switch (option & V_SCALEPATCHMASK)
 	{
 	case V_NOSCALEPATCH:
-		dupx = dupy = 1.0f;
+		dup = 1.0f;
 		break;
 	case V_SMALLSCALEPATCH:
-		dupx = (float)vid.smalldupx;
-		dupy = (float)vid.smalldupy;
+		dup = (float)vid.smalldup;
 		break;
 	case V_MEDSCALEPATCH:
-		dupx = (float)vid.meddupx;
-		dupy = (float)vid.meddupy;
+		dup = (float)vid.meddup;
 		break;
 	}
 
-	dupx = dupy = (dupx < dupy ? dupx : dupy);
 	fscalew = fscaleh = FIXED_TO_FLOAT(pscale);
 	if (vscale != pscale)
 		fscaleh = FIXED_TO_FLOAT(vscale);
@@ -487,8 +475,8 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 
 	if (!(option & V_NOSCALESTART))
 	{
-		cx = cx * dupx;
-		cy = cy * dupy;
+		cx = cx * dup;
+		cy = cy * dup;
 
 		if (!(option & V_SCALEPATCHMASK))
 		{
@@ -496,27 +484,27 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 			// no the patch is cropped do not do this ever
 
 			// centre screen
-			if (fabsf((float)vid.width - (float)BASEVIDWIDTH * dupx) > 1.0E-36f)
+			if (fabsf((float)vid.width - (float)BASEVIDWIDTH * dup) > 1.0E-36f)
 			{
 				if (option & V_SNAPTORIGHT)
-					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx));
+					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dup));
 				else if (!(option & V_SNAPTOLEFT))
-					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx))/2;
+					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dup))/2;
 				if (perplayershuffle & 4)
-					cx -= ((float)vid.width - ((float)BASEVIDWIDTH * dupx))/4;
+					cx -= ((float)vid.width - ((float)BASEVIDWIDTH * dup))/4;
 				else if (perplayershuffle & 8)
-					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx))/4;
+					cx += ((float)vid.width - ((float)BASEVIDWIDTH * dup))/4;
 			}
-			if (fabsf((float)vid.height - (float)BASEVIDHEIGHT * dupy) > 1.0E-36f)
+			if (fabsf((float)vid.height - (float)BASEVIDHEIGHT * dup) > 1.0E-36f)
 			{
 				if (option & V_SNAPTOBOTTOM)
-					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy));
+					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dup));
 				else if (!(option & V_SNAPTOTOP))
-					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy))/2;
+					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dup))/2;
 				if (perplayershuffle & 1)
-					cy -= ((float)vid.height - ((float)BASEVIDHEIGHT * dupy))/4;
+					cy -= ((float)vid.height - ((float)BASEVIDHEIGHT * dup))/4;
 				else if (perplayershuffle & 2)
-					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy))/4;
+					cy += ((float)vid.height - ((float)BASEVIDHEIGHT * dup))/4;
 			}
 		}
 	}
@@ -532,13 +520,13 @@ void HWR_DrawCroppedPatch(patch_t *gpatch, fixed_t x, fixed_t y, fixed_t pscale,
 
 	if (pscale != FRACUNIT || vscale != FRACUNIT || (splitscreen && option & V_PERPLAYER))
 	{
-		fwidth *= fscalew * dupx;
-		fheight *= fscaleh * dupy;
+		fwidth *= fscalew * dup;
+		fheight *= fscaleh * dup;
 	}
 	else
 	{
-		fwidth *= dupx;
-		fheight *= dupy;
+		fwidth *= dup;
+		fheight *= dup;
 	}
 
 	// positions of the cx, cy, are between 0 and vid.width/vid.height now, we need them to be between -1 and 1
@@ -674,9 +662,9 @@ void HWR_DrawPic(INT32 x, INT32 y, lumpnum_t lumpnum)
 //  0--1
 
 	v[0].x = v[3].x = 2.0f * (float)x/vid.width - 1;
-	v[2].x = v[1].x = 2.0f * (float)(x + patch->width*FIXED_TO_FLOAT(vid.fdupx))/vid.width - 1;
+	v[2].x = v[1].x = 2.0f * (float)(x + patch->width*FIXED_TO_FLOAT(vid.fdup))/vid.width - 1;
 	v[0].y = v[1].y = 1.0f - 2.0f * (float)y/vid.height;
-	v[2].y = v[3].y = 1.0f - 2.0f * (float)(y + patch->height*FIXED_TO_FLOAT(vid.fdupy))/vid.height;
+	v[2].y = v[3].y = 1.0f - 2.0f * (float)(y + patch->height*FIXED_TO_FLOAT(vid.fdup))/vid.height;
 
 	v[0].z = v[1].z = v[2].z = v[3].z = 1.0f;
 
@@ -866,35 +854,33 @@ void HWR_DrawFadeFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 color, UINT16 ac
 
 	if (!(color & V_NOSCALESTART))
 	{
-		float dupx = (float)vid.dupx, dupy = (float)vid.dupy;
+		fx *= vid.dup;
+		fy *= vid.dup;
+		fw *= vid.dup;
+		fh *= vid.dup;
 
-		fx *= dupx;
-		fy *= dupy;
-		fw *= dupx;
-		fh *= dupy;
-
-		if (fabsf((float)vid.width - (float)BASEVIDWIDTH * dupx) > 1.0E-36f)
+		if (fabsf((float)vid.width - (float)BASEVIDWIDTH * vid.dup) > 1.0E-36f)
 		{
 			if (color & V_SNAPTORIGHT)
-				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx));
+				fx += ((float)vid.width - ((float)BASEVIDWIDTH * vid.dup));
 			else if (!(color & V_SNAPTOLEFT))
-				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx)) / 2;
+				fx += ((float)vid.width - ((float)BASEVIDWIDTH * vid.dup)) / 2;
 			if (perplayershuffle & 4)
-				fx -= ((float)vid.width - ((float)BASEVIDWIDTH * dupx)) / 4;
+				fx -= ((float)vid.width - ((float)BASEVIDWIDTH * vid.dup)) / 4;
 			else if (perplayershuffle & 8)
-				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx)) / 4;
+				fx += ((float)vid.width - ((float)BASEVIDWIDTH * vid.dup)) / 4;
 		}
-		if (fabsf((float)vid.height - (float)BASEVIDHEIGHT * dupy) > 1.0E-36f)
+		if (fabsf((float)vid.height - (float)BASEVIDHEIGHT * vid.dup) > 1.0E-36f)
 		{
 			// same thing here
 			if (color & V_SNAPTOBOTTOM)
-				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy));
+				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * vid.dup));
 			else if (!(color & V_SNAPTOTOP))
-				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy)) / 2;
+				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * vid.dup)) / 2;
 			if (perplayershuffle & 1)
-				fy -= ((float)vid.height - ((float)BASEVIDHEIGHT * dupy)) / 4;
+				fy -= ((float)vid.height - ((float)BASEVIDHEIGHT * vid.dup)) / 4;
 			else if (perplayershuffle & 2)
-				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy)) / 4;
+				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * vid.dup)) / 4;
 		}
 	}
 
@@ -1032,10 +1018,10 @@ void HWR_DrawViewBorder(INT32 clearlines)
 		clearlines = BASEVIDHEIGHT; // refresh all
 
 	// calc view size based on original game resolution
-	baseviewwidth =  FixedInt(FixedDiv(FLOAT_TO_FIXED(gl_viewwidth), vid.fdupx)); //(cv_viewsize.value * BASEVIDWIDTH/10)&~7;
-	baseviewheight = FixedInt(FixedDiv(FLOAT_TO_FIXED(gl_viewheight), vid.fdupy));
-	top = FixedInt(FixedDiv(FLOAT_TO_FIXED(gl_baseviewwindowy), vid.fdupy));
-	side = FixedInt(FixedDiv(FLOAT_TO_FIXED(gl_viewwindowx), vid.fdupx));
+	baseviewwidth =  FixedInt(FixedDiv(FLOAT_TO_FIXED(gl_viewwidth), vid.fdup)); //(cv_viewsize.value * BASEVIDWIDTH/10)&~7;
+	baseviewheight = FixedInt(FixedDiv(FLOAT_TO_FIXED(gl_viewheight), vid.fdup));
+	top = FixedInt(FixedDiv(FLOAT_TO_FIXED(gl_baseviewwindowy), vid.fdup));
+	side = FixedInt(FixedDiv(FLOAT_TO_FIXED(gl_viewwindowx), vid.fdup));
 
 	// top
 	HWR_DrawFlatFill(0, 0,
@@ -1250,35 +1236,35 @@ void HWR_DrawConsoleFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 color, UINT32
 
 	if (!(color & V_NOSCALESTART))
 	{
-		float dupx = (float)vid.dupx, dupy = (float)vid.dupy;
+		float dup = (float)vid.dup;
 
-		fx *= dupx;
-		fy *= dupy;
-		fw *= dupx;
-		fh *= dupy;
+		fx *= dup;
+		fy *= dup;
+		fw *= dup;
+		fh *= dup;
 
-		if (fabsf((float)vid.width - (float)BASEVIDWIDTH * dupx) > 1.0E-36f)
+		if (fabsf((float)vid.width - (float)BASEVIDWIDTH * dup) > 1.0E-36f)
 		{
 			if (color & V_SNAPTORIGHT)
-				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx));
+				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dup));
 			else if (!(color & V_SNAPTOLEFT))
-				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx)) / 2;
+				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dup)) / 2;
 			if (perplayershuffle & 4)
-				fx -= ((float)vid.width - ((float)BASEVIDWIDTH * dupx)) / 4;
+				fx -= ((float)vid.width - ((float)BASEVIDWIDTH * dup)) / 4;
 			else if (perplayershuffle & 8)
-				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx)) / 4;
+				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dup)) / 4;
 		}
-		if (fabsf((float)vid.height - (float)BASEVIDHEIGHT * dupy) > 1.0E-36f)
+		if (fabsf((float)vid.height - (float)BASEVIDHEIGHT * dup) > 1.0E-36f)
 		{
 			// same thing here
 			if (color & V_SNAPTOBOTTOM)
-				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy));
+				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dup));
 			else if (!(color & V_SNAPTOTOP))
-				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy)) / 2;
+				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dup)) / 2;
 			if (perplayershuffle & 1)
-				fy -= ((float)vid.height - ((float)BASEVIDHEIGHT * dupy)) / 4;
+				fy -= ((float)vid.height - ((float)BASEVIDHEIGHT * dup)) / 4;
 			else if (perplayershuffle & 2)
-				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy)) / 4;
+				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dup)) / 4;
 		}
 	}
 
@@ -1416,8 +1402,6 @@ void HWR_DrawFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 color)
 
 	if (!(color & V_NOSCALESTART))
 	{
-		float dupx = (float)vid.dupx, dupy = (float)vid.dupy;
-
 		if (x == 0 && y == 0 && w == BASEVIDWIDTH && h == BASEVIDHEIGHT)
 		{
 			RGBA_t rgbaColour = V_GetColor(color);
@@ -1430,33 +1414,33 @@ void HWR_DrawFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 color)
 			return;
 		}
 
-		fx *= dupx;
-		fy *= dupy;
-		fw *= dupx;
-		fh *= dupy;
+		fx *= vid.dup;
+		fy *= vid.dup;
+		fw *= vid.dup;
+		fh *= vid.dup;
 
-		if (fabsf((float)vid.width - (float)BASEVIDWIDTH * dupx) > 1.0E-36f)
+		if (fabsf((float)vid.width - (float)BASEVIDWIDTH * vid.dup) > 1.0E-36f)
 		{
 			if (color & V_SNAPTORIGHT)
-				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx));
+				fx += ((float)vid.width - ((float)BASEVIDWIDTH * vid.dup));
 			else if (!(color & V_SNAPTOLEFT))
-				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx)) / 2;
+				fx += ((float)vid.width - ((float)BASEVIDWIDTH * vid.dup)) / 2;
 			if (perplayershuffle & 4)
-				fx -= ((float)vid.width - ((float)BASEVIDWIDTH * dupx)) / 4;
+				fx -= ((float)vid.width - ((float)BASEVIDWIDTH * vid.dup)) / 4;
 			else if (perplayershuffle & 8)
-				fx += ((float)vid.width - ((float)BASEVIDWIDTH * dupx)) / 4;
+				fx += ((float)vid.width - ((float)BASEVIDWIDTH * vid.dup)) / 4;
 		}
-		if (fabsf((float)vid.height - (float)BASEVIDHEIGHT * dupy) > 1.0E-36f)
+		if (fabsf((float)vid.height - (float)BASEVIDHEIGHT * vid.dup) > 1.0E-36f)
 		{
 			// same thing here
 			if (color & V_SNAPTOBOTTOM)
-				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy));
+				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * vid.dup));
 			else if (!(color & V_SNAPTOTOP))
-				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy)) / 2;
+				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * vid.dup)) / 2;
 			if (perplayershuffle & 1)
-				fy -= ((float)vid.height - ((float)BASEVIDHEIGHT * dupy)) / 4;
+				fy -= ((float)vid.height - ((float)BASEVIDHEIGHT * vid.dup)) / 4;
 			else if (perplayershuffle & 2)
-				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * dupy)) / 4;
+				fy += ((float)vid.height - ((float)BASEVIDHEIGHT * vid.dup)) / 4;
 		}
 	}
 
diff --git a/src/hardware/hw_light.c b/src/hardware/hw_light.c
index c1f0b34076f62cf9acacec8b1efda103839ed2b0..94f01e2991ab8629676198272d76696c42d3afe1 100644
--- a/src/hardware/hw_light.c
+++ b/src/hardware/hw_light.c
@@ -138,6 +138,7 @@ light_t *t_lspr[NUMSPRITES] =
 	&lspr[NOLIGHT],     // SPR_UNKN
 
 	&lspr[NOLIGHT],     // SPR_THOK
+	&lspr[NOLIGHT],     // SPR_THKE
 	&lspr[SUPERSONIC_L],// SPR_PLAY
 
 	// Enemies
@@ -341,6 +342,7 @@ light_t *t_lspr[NUMSPRITES] =
 	&lspr[NOLIGHT],     // SPR_BMCH
 	&lspr[NOLIGHT],     // SPR_SMCE
 	&lspr[NOLIGHT],     // SPR_BMCE
+	&lspr[NOLIGHT],     // SPR_BSPB
 	&lspr[NOLIGHT],     // SPR_YSPB
 	&lspr[NOLIGHT],     // SPR_RSPB
 	&lspr[REDBALL_L],   // SPR_SFBR
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index 8260271bdd9f8bb52948f8d38e46e385f841fe47..1f0793f70ad8033e6aed9ca8839419d1affb8bbd 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -29,7 +29,7 @@
 #include "../r_patch.h"
 #include "../r_picformats.h"
 #include "../r_bsp.h"
-#include "../d_clisrv.h"
+#include "../netcode/d_clisrv.h"
 #include "../w_wad.h"
 #include "../z_zone.h"
 #include "../r_splats.h"
@@ -169,7 +169,6 @@ ps_metric_t ps_hw_batchdrawtime = {0};
 
 boolean gl_init = false;
 boolean gl_maploaded = false;
-boolean gl_sessioncommandsadded = false;
 boolean gl_shadersavailable = true;
 
 // ==========================================================================
@@ -181,13 +180,18 @@ static boolean HWR_UseShader(void)
 	return (cv_glshaders.value && gl_shadersavailable);
 }
 
+static boolean HWR_IsWireframeMode(void)
+{
+	return (cv_glwireframe.value && cv_debug);
+}
+
 void HWR_Lighting(FSurfaceInfo *Surface, INT32 light_level, extracolormap_t *colormap)
 {
 	RGBA_t poly_color, tint_color, fade_color;
 
 	poly_color.rgba = 0xFFFFFFFF;
-	tint_color.rgba = (colormap != NULL) ? (UINT32)colormap->rgba : GL_DEFAULTMIX;
-	fade_color.rgba = (colormap != NULL) ? (UINT32)colormap->fadergba : GL_DEFAULTFOG;
+	tint_color.rgba = (colormap != NULL) ? (UINT32)colormap->rgba : 0x00000000;
+	fade_color.rgba = (colormap != NULL) ? (UINT32)colormap->fadergba : 0xFF000000;
 
 	// Crappy backup coloring if you can't do shaders
 	if (!HWR_UseShader())
@@ -201,7 +205,7 @@ void HWR_Lighting(FSurfaceInfo *Surface, INT32 light_level, extracolormap_t *col
 		blue = (float)poly_color.s.blue;
 
 		// 48 is just an arbritrary value that looked relatively okay.
-		tint_alpha = (float)(sqrt(tint_color.s.alpha) * 48) / 255.0f;
+		tint_alpha = (float)(sqrt((float)tint_color.s.alpha / 10.2) * 48) / 255.0f;
 
 		// 8 is roughly the brightness of the "close" color in Software, and 16 the brightness of the "far" color.
 		// 8 is too bright for dark levels, and 16 is too dark for bright levels.
@@ -229,6 +233,8 @@ void HWR_Lighting(FSurfaceInfo *Surface, INT32 light_level, extracolormap_t *col
 	// Clamp the light level, since it can sometimes go out of the 0-255 range from animations
 	light_level = min(max(light_level, 0), 255);
 
+	V_CubeApply(&tint_color.s.red, &tint_color.s.green, &tint_color.s.blue);
+	V_CubeApply(&fade_color.s.red, &fade_color.s.green, &fade_color.s.blue);
 	Surface->PolyColor.rgba = poly_color.rgba;
 	Surface->TintColor.rgba = tint_color.rgba;
 	Surface->FadeColor.rgba = fade_color.rgba;
@@ -242,7 +248,7 @@ UINT8 HWR_FogBlockAlpha(INT32 light, extracolormap_t *colormap) // Let's see if
 	RGBA_t realcolor, surfcolor;
 	INT32 alpha;
 
-	realcolor.rgba = (colormap != NULL) ? colormap->rgba : GL_DEFAULTMIX;
+	realcolor.rgba = (colormap != NULL) ? colormap->rgba : 0x00000000;
 
 	if (cv_glshaders.value && gl_shadersavailable)
 	{
@@ -371,11 +377,9 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 	INT32 i;
 
 	float height; // constant y for all points on the convex flat polygon
-	float flatxref, flatyref, anglef = 0.0f;
+	float anglef = 0.0f;
 	float fflatwidth = 64.0f, fflatheight = 64.0f;
-	UINT16 flatflag = 63;
-
-	boolean texflat = false;
+	float xscale = 1.0f, yscale = 1.0f;
 
 	float tempxsow, tempytow;
 	float scrollx = 0.0f, scrolly = 0.0f;
@@ -410,11 +414,7 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 			slope = gl_frontsector->c_slope;
 	}
 
-	// Set fixedheight to the slope's height from our viewpoint, if we have a slope
-	if (slope)
-		fixedheight = P_GetSlopeZAt(slope, viewx, viewy);
-
-	height = FIXED_TO_FLOAT(fixedheight);
+	height = FixedToFloat(fixedheight);
 
 	// Allocate plane-vertex buffer if we need to
 	if (!planeVerts || nrPlaneVerts > numAllocedPlaneVerts)
@@ -430,8 +430,8 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 		if (levelflat->type == LEVELFLAT_FLAT)
 		{
 			size_t len = W_LumpLength(levelflat->u.flat.lumpnum);
-			flatflag = R_GetFlatSize(len) - 1;
-			fflatwidth = fflatheight = (float)(flatflag + 1);
+			unsigned flatflag = R_GetFlatSize(len);
+			fflatwidth = fflatheight = (float)flatflag;
 		}
 		else
 		{
@@ -445,29 +445,28 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 				fflatwidth = levelflat->width;
 				fflatheight = levelflat->height;
 			}
-			texflat = true;
 		}
 	}
 	else // set no texture
 		HWR_SetCurrentTexture(NULL);
 
-	// reference point for flat texture coord for each vertex around the polygon
-	flatxref = (float)(((fixed_t)pv->x & (~flatflag)) / fflatwidth);
-	flatyref = (float)(((fixed_t)pv->y & (~flatflag)) / fflatheight);
-
 	// transform
 	if (FOFsector != NULL)
 	{
 		if (!isceiling) // it's a floor
 		{
-			scrollx = FIXED_TO_FLOAT(FOFsector->floorxoffset)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(FOFsector->flooryoffset)/fflatheight;
+			xscale = FixedToFloat(FOFsector->floorxscale);
+			yscale = FixedToFloat(FOFsector->flooryscale);
+			scrollx = FixedToFloat(FOFsector->floorxoffset) / fflatwidth;
+			scrolly = FixedToFloat(FOFsector->flooryoffset) / fflatheight;
 			angle = FOFsector->floorangle;
 		}
 		else // it's a ceiling
 		{
-			scrollx = FIXED_TO_FLOAT(FOFsector->ceilingxoffset)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(FOFsector->ceilingyoffset)/fflatheight;
+			xscale = FixedToFloat(FOFsector->ceilingxscale);
+			yscale = FixedToFloat(FOFsector->ceilingyscale);
+			scrollx = FixedToFloat(FOFsector->ceilingxoffset) / fflatwidth;
+			scrolly = FixedToFloat(FOFsector->ceilingyoffset) / fflatheight;
 			angle = FOFsector->ceilingangle;
 		}
 	}
@@ -475,41 +474,28 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 	{
 		if (!isceiling) // it's a floor
 		{
-			scrollx = FIXED_TO_FLOAT(gl_frontsector->floorxoffset)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(gl_frontsector->flooryoffset)/fflatheight;
+			xscale = FixedToFloat(gl_frontsector->floorxscale);
+			yscale = FixedToFloat(gl_frontsector->flooryscale);
+			scrollx = FixedToFloat(gl_frontsector->floorxoffset) / fflatwidth;
+			scrolly = FixedToFloat(gl_frontsector->flooryoffset) / fflatheight;
 			angle = gl_frontsector->floorangle;
 		}
 		else // it's a ceiling
 		{
-			scrollx = FIXED_TO_FLOAT(gl_frontsector->ceilingxoffset)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(gl_frontsector->ceilingyoffset)/fflatheight;
+			xscale = FixedToFloat(gl_frontsector->ceilingxscale);
+			yscale = FixedToFloat(gl_frontsector->ceilingyscale);
+			scrollx = FixedToFloat(gl_frontsector->ceilingxoffset) / fflatwidth;
+			scrolly = FixedToFloat(gl_frontsector->ceilingyoffset) / fflatheight;
 			angle = gl_frontsector->ceilingangle;
 		}
 	}
 
-	if (angle) // Only needs to be done if there's an altered angle
-	{
-		tempxsow = flatxref;
-		tempytow = flatyref;
-
-		anglef = ANG2RAD(InvAngle(angle));
-
-		flatxref = (tempxsow * cos(anglef)) - (tempytow * sin(anglef));
-		flatyref = (tempxsow * sin(anglef)) + (tempytow * cos(anglef));
-	}
+	anglef = ANG2RAD(InvAngle(angle));
 
 #define SETUP3DVERT(vert, vx, vy) {\
 		/* Hurdler: add scrolling texture on floor/ceiling */\
-		if (texflat)\
-		{\
-			vert->s = (float)((vx) / fflatwidth) + scrollx;\
-			vert->t = -(float)((vy) / fflatheight) + scrolly;\
-		}\
-		else\
-		{\
-			vert->s = (float)(((vx) / fflatwidth) - flatxref + scrollx);\
-			vert->t = (float)(flatyref - ((vy) / fflatheight) + scrolly);\
-		}\
+		vert->s = ((vx) / fflatwidth) + (scrollx / xscale);\
+		vert->t = -((vy) / fflatheight) + (scrolly / yscale);\
 \
 		/* Need to rotate before translate */\
 		if (angle) /* Only needs to be done if there's an altered angle */\
@@ -520,15 +506,18 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 			vert->t = (tempxsow * sin(anglef)) + (tempytow * cos(anglef));\
 		}\
 \
-		vert->x = (vx);\
-		vert->y = height;\
-		vert->z = (vy);\
+		vert->s *= xscale;\
+		vert->t *= yscale;\
 \
 		if (slope)\
 		{\
-			fixedheight = P_GetSlopeZAt(slope, FLOAT_TO_FIXED((vx)), FLOAT_TO_FIXED((vy)));\
-			vert->y = FIXED_TO_FLOAT(fixedheight);\
+			fixedheight = P_GetSlopeZAt(slope, FloatToFixed((vx)), FloatToFixed((vy)));\
+			height = FixedToFloat(fixedheight);\
 		}\
+\
+		vert->x = (vx);\
+		vert->y = height;\
+		vert->z = (vy);\
 }
 
 	for (i = 0, v3d = planeVerts; i < (INT32)nrPlaneVerts; i++,v3d++,pv++)
@@ -582,10 +571,26 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 				P_ClosestPointOnLine(viewx, viewy, line->linedef, &v);
 				dist = FIXED_TO_FLOAT(R_PointToDist(v.x, v.y));
 
-				x1 = ((polyvertex_t *)line->pv1)->x;
-				y1 = ((polyvertex_t *)line->pv1)->y;
-				xd = ((polyvertex_t *)line->pv2)->x - x1;
-				yd = ((polyvertex_t *)line->pv2)->y - y1;
+				if (line->pv1)
+				{
+					x1 = ((polyvertex_t *)line->pv1)->x;
+					y1 = ((polyvertex_t *)line->pv1)->y;
+				}
+				else
+				{
+					x1 = FIXED_TO_FLOAT(line->v1->x);
+					y1 = FIXED_TO_FLOAT(line->v1->x);
+				}
+				if (line->pv2)
+				{
+					xd = ((polyvertex_t *)line->pv2)->x - x1;
+					yd = ((polyvertex_t *)line->pv2)->y - y1;
+				}
+				else
+				{
+					xd = FIXED_TO_FLOAT(line->v2->x) - x1;
+					yd = FIXED_TO_FLOAT(line->v2->y) - y1;
+				}
 
 				// Based on the seg length and the distance from the line, split horizon into multiple poly sets to reduce distortion
 				dist = sqrtf((xd*xd) + (yd*yd)) / dist / 16.0f;
@@ -1064,14 +1069,31 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 
 	fixed_t h, l; // 3D sides and 2s middle textures
 	fixed_t hS, lS;
+	float xscale, yscale;
 
 	gl_sidedef = gl_curline->sidedef;
 	gl_linedef = gl_curline->linedef;
 
-	vs.x = ((polyvertex_t *)gl_curline->pv1)->x;
-	vs.y = ((polyvertex_t *)gl_curline->pv1)->y;
-	ve.x = ((polyvertex_t *)gl_curline->pv2)->x;
-	ve.y = ((polyvertex_t *)gl_curline->pv2)->y;
+	if (gl_curline->pv1)
+	{
+		vs.x = ((polyvertex_t *)gl_curline->pv1)->x;
+		vs.y = ((polyvertex_t *)gl_curline->pv1)->y;
+	}
+	else
+	{
+		vs.x = FIXED_TO_FLOAT(gl_curline->v1->x);
+		vs.y = FIXED_TO_FLOAT(gl_curline->v1->y);
+	}
+	if (gl_curline->pv2)
+	{
+		ve.x = ((polyvertex_t *)gl_curline->pv2)->x;
+		ve.y = ((polyvertex_t *)gl_curline->pv2)->y;
+	}
+	else
+	{
+		ve.x = FIXED_TO_FLOAT(gl_curline->v2->x);
+		ve.y = FIXED_TO_FLOAT(gl_curline->v2->y);
+	}
 
 	v1x = FLOAT_TO_FIXED(vs.x);
 	v1y = FLOAT_TO_FIXED(vs.y);
@@ -1146,47 +1168,53 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 		// check TOP TEXTURE
 		if ((worldhighslope < worldtopslope || worldhigh < worldtop) && gl_toptexture)
 		{
+			grTex = HWR_GetTexture(gl_toptexture);
+			xscale = FixedToFloat(gl_sidedef->scalex_top);
+			yscale = FixedToFloat(gl_sidedef->scaley_top);
+
+			fixed_t texheight = FixedDiv(textureheight[gl_toptexture], gl_sidedef->scaley_top);
+
 			// PEGGING
 			if (gl_linedef->flags & ML_DONTPEGTOP)
 				texturevpeg = 0;
 			else if (gl_linedef->flags & ML_SKEWTD)
-				texturevpeg = worldhigh + textureheight[gl_toptexture] - worldtop;
+				texturevpeg = worldhigh + texheight - worldtop;
 			else
-				texturevpeg = gl_backsector->ceilingheight + textureheight[gl_toptexture] - gl_frontsector->ceilingheight;
+				texturevpeg = gl_backsector->ceilingheight + texheight - gl_frontsector->ceilingheight;
+
+			texturevpeg *= yscale;
 
 			texturevpeg += gl_sidedef->rowoffset + gl_sidedef->offsety_top;
 
 			// This is so that it doesn't overflow and screw up the wall, it doesn't need to go higher than the texture's height anyway
-			texturevpeg %= textureheight[gl_toptexture];
-
-			grTex = HWR_GetTexture(gl_toptexture);
+			texturevpeg %= texheight;
 
 			wallVerts[3].t = wallVerts[2].t = texturevpeg * grTex->scaleY;
-			wallVerts[0].t = wallVerts[1].t = (texturevpeg + gl_frontsector->ceilingheight - gl_backsector->ceilingheight) * grTex->scaleY;
-			wallVerts[0].s = wallVerts[3].s = (cliplow + gl_sidedef->offsetx_top) * grTex->scaleX;
-			wallVerts[2].s = wallVerts[1].s = (cliphigh + gl_sidedef->offsetx_top) * grTex->scaleX;
+			wallVerts[0].t = wallVerts[1].t = (texturevpeg + (gl_frontsector->ceilingheight - gl_backsector->ceilingheight) * yscale) * grTex->scaleY;
+			wallVerts[0].s = wallVerts[3].s = ((cliplow * xscale) + gl_sidedef->offsetx_top) * grTex->scaleX;
+			wallVerts[2].s = wallVerts[1].s = ((cliphigh * xscale) + gl_sidedef->offsetx_top) * grTex->scaleX;
 
 			// Adjust t value for sloped walls
 			if (!(gl_linedef->flags & ML_SKEWTD))
 			{
 				// Unskewed
-				wallVerts[3].t -= (worldtop - gl_frontsector->ceilingheight) * grTex->scaleY;
-				wallVerts[2].t -= (worldtopslope - gl_frontsector->ceilingheight) * grTex->scaleY;
-				wallVerts[0].t -= (worldhigh - gl_backsector->ceilingheight) * grTex->scaleY;
-				wallVerts[1].t -= (worldhighslope - gl_backsector->ceilingheight) * grTex->scaleY;
+				wallVerts[3].t -= (worldtop - gl_frontsector->ceilingheight) * yscale * grTex->scaleY;
+				wallVerts[2].t -= (worldtopslope - gl_frontsector->ceilingheight) * yscale * grTex->scaleY;
+				wallVerts[0].t -= (worldhigh - gl_backsector->ceilingheight) * yscale * grTex->scaleY;
+				wallVerts[1].t -= (worldhighslope - gl_backsector->ceilingheight) * yscale * grTex->scaleY;
 			}
 			else if (gl_linedef->flags & ML_DONTPEGTOP)
 			{
 				// Skewed by top
-				wallVerts[0].t = (texturevpeg + worldtop - worldhigh) * grTex->scaleY;
-				wallVerts[1].t = (texturevpeg + worldtopslope - worldhighslope) * grTex->scaleY;
+				wallVerts[0].t = (texturevpeg + (worldtop - worldhigh) * yscale) * grTex->scaleY;
+				wallVerts[1].t = (texturevpeg + (worldtopslope - worldhighslope) * yscale) * grTex->scaleY;
 			}
 			else
 			{
 				// Skewed by bottom
-				wallVerts[0].t = wallVerts[1].t = (texturevpeg + worldtop - worldhigh) * grTex->scaleY;
-				wallVerts[3].t = wallVerts[0].t - (worldtop - worldhigh) * grTex->scaleY;
-				wallVerts[2].t = wallVerts[1].t - (worldtopslope - worldhighslope) * grTex->scaleY;
+				wallVerts[0].t = wallVerts[1].t = (texturevpeg + (worldtop - worldhigh) * yscale) * grTex->scaleY;
+				wallVerts[3].t = wallVerts[0].t - (worldtop - worldhigh) * yscale * grTex->scaleY;
+				wallVerts[2].t = wallVerts[1].t - (worldtopslope - worldhighslope) * yscale * grTex->scaleY;
 			}
 
 			// set top/bottom coords
@@ -1206,6 +1234,10 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 		// check BOTTOM TEXTURE
 		if ((worldlowslope > worldbottomslope || worldlow > worldbottom) && gl_bottomtexture)
 		{
+			grTex = HWR_GetTexture(gl_bottomtexture);
+			xscale = FixedToFloat(gl_sidedef->scalex_bottom);
+			yscale = FixedToFloat(gl_sidedef->scaley_bottom);
+
 			// PEGGING
 			if (!(gl_linedef->flags & ML_DONTPEGBOTTOM))
 				texturevpeg = 0;
@@ -1214,38 +1246,38 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 			else
 				texturevpeg = gl_frontsector->floorheight - gl_backsector->floorheight;
 
-			texturevpeg += gl_sidedef->rowoffset + gl_sidedef->offsety_bot;
+			texturevpeg *= yscale;
 
-			// This is so that it doesn't overflow and screw up the wall, it doesn't need to go higher than the texture's height anyway
-			texturevpeg %= textureheight[gl_bottomtexture];
+			texturevpeg += gl_sidedef->rowoffset + gl_sidedef->offsety_bottom;
 
-			grTex = HWR_GetTexture(gl_bottomtexture);
+			// This is so that it doesn't overflow and screw up the wall, it doesn't need to go higher than the texture's height anyway
+			texturevpeg %= FixedDiv(textureheight[gl_bottomtexture], gl_sidedef->scaley_bottom);
 
 			wallVerts[3].t = wallVerts[2].t = texturevpeg * grTex->scaleY;
-			wallVerts[0].t = wallVerts[1].t = (texturevpeg + gl_backsector->floorheight - gl_frontsector->floorheight) * grTex->scaleY;
-			wallVerts[0].s = wallVerts[3].s = (cliplow + gl_sidedef->offsetx_bot) * grTex->scaleX;
-			wallVerts[2].s = wallVerts[1].s = (cliphigh + gl_sidedef->offsetx_bot) * grTex->scaleX;
+			wallVerts[0].t = wallVerts[1].t = (texturevpeg + (gl_backsector->floorheight - gl_frontsector->floorheight) * yscale) * grTex->scaleY;
+			wallVerts[0].s = wallVerts[3].s = ((cliplow * xscale) + gl_sidedef->offsetx_bottom) * grTex->scaleX;
+			wallVerts[2].s = wallVerts[1].s = ((cliphigh * xscale) + gl_sidedef->offsetx_bottom) * grTex->scaleX;
 
 			// Adjust t value for sloped walls
 			if (!(gl_linedef->flags & ML_SKEWTD))
 			{
 				// Unskewed
-				wallVerts[0].t -= (worldbottom - gl_frontsector->floorheight) * grTex->scaleY;
-				wallVerts[1].t -= (worldbottomslope - gl_frontsector->floorheight) * grTex->scaleY;
-				wallVerts[3].t -= (worldlow - gl_backsector->floorheight) * grTex->scaleY;
-				wallVerts[2].t -= (worldlowslope - gl_backsector->floorheight) * grTex->scaleY;
+				wallVerts[0].t -= (worldbottom - gl_frontsector->floorheight) * yscale * grTex->scaleY;
+				wallVerts[1].t -= (worldbottomslope - gl_frontsector->floorheight) * yscale * grTex->scaleY;
+				wallVerts[3].t -= (worldlow - gl_backsector->floorheight) * yscale * grTex->scaleY;
+				wallVerts[2].t -= (worldlowslope - gl_backsector->floorheight) * yscale * grTex->scaleY;
 			}
 			else if (gl_linedef->flags & ML_DONTPEGBOTTOM)
 			{
 				// Skewed by bottom
-				wallVerts[0].t = wallVerts[1].t = (texturevpeg + worldlow - worldbottom) * grTex->scaleY;
-				wallVerts[2].t = wallVerts[1].t - (worldlowslope - worldbottomslope) * grTex->scaleY;
+				wallVerts[0].t = wallVerts[1].t = (texturevpeg + (worldlow - worldbottom) * yscale) * grTex->scaleY;
+				wallVerts[2].t = wallVerts[1].t - (worldlowslope - worldbottomslope) * yscale * grTex->scaleY;
 			}
 			else
 			{
 				// Skewed by top
-				wallVerts[0].t = (texturevpeg + worldlow - worldbottom) * grTex->scaleY;
-				wallVerts[1].t = (texturevpeg + worldlowslope - worldbottomslope) * grTex->scaleY;
+				wallVerts[0].t = (texturevpeg + (worldlow - worldbottom) * yscale) * grTex->scaleY;
+				wallVerts[1].t = (texturevpeg + (worldlowslope - worldbottomslope) * yscale) * grTex->scaleY;
 			}
 
 			// set top/bottom coords
@@ -1266,6 +1298,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 		if (gl_midtexture && HWR_BlendMidtextureSurface(&Surf))
 		{
 			sector_t *front, *back;
+			fixed_t texheight = FixedDiv(textureheight[gl_midtexture], gl_sidedef->scaley_mid);
 			INT32 repeats;
 
 			if (gl_linedef->frontsector->heightsec != -1)
@@ -1294,13 +1327,17 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 				else
 					low = back->floorheight;
 
-				repeats = (high - low) / textureheight[gl_midtexture];
-				if ((high - low) % textureheight[gl_midtexture])
+				repeats = (high - low) / texheight;
+				if ((high - low) % texheight)
 					repeats++; // tile an extra time to fill the gap -- Monster Iestyn
 			}
 			else
 				repeats = 1;
 
+			grTex = HWR_GetTexture(gl_midtexture);
+			xscale = FixedToFloat(gl_sidedef->scalex_mid);
+			yscale = FixedToFloat(gl_sidedef->scaley_mid);
+
 			// SoM: a little note: popentop and popenbottom
 			// record the limits the texture can be displayed in.
 			// polytop and polybottom, are the ideal (i.e. unclipped)
@@ -1318,7 +1355,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 				popenbottom = popenbottomslope = back->floorheight;
 			}
 			else
-            {
+			{
 				popentop = min(worldtop, worldhigh);
 				popenbottom = max(worldbottom, worldlow);
 				popentopslope = min(worldtopslope, worldhighslope);
@@ -1326,7 +1363,9 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 			}
 
 			// Find the wall's coordinates
-			fixed_t midtexheight = textureheight[gl_midtexture] * repeats;
+			fixed_t midtexheight = texheight * repeats;
+
+			fixed_t rowoffset = FixedDiv(gl_sidedef->rowoffset + gl_sidedef->offsety_mid, gl_sidedef->scaley_mid);
 
 			// Texture is not skewed
 			if (gl_linedef->flags & ML_NOSKEW)
@@ -1334,13 +1373,13 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 				// Peg it to the floor
 				if (gl_linedef->flags & ML_MIDPEG)
 				{
-					polybottom = max(front->floorheight, back->floorheight) + gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
+					polybottom = max(front->floorheight, back->floorheight) + rowoffset;
 					polytop = polybottom + midtexheight;
 				}
 				// Peg it to the ceiling
 				else
 				{
-					polytop = min(front->ceilingheight, back->ceilingheight) + gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
+					polytop = min(front->ceilingheight, back->ceilingheight) + rowoffset;
 					polybottom = polytop - midtexheight;
 				}
 
@@ -1351,17 +1390,17 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 			// Skew the texture, but peg it to the floor
 			else if (gl_linedef->flags & ML_MIDPEG)
 			{
-				polybottom = popenbottom + gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
+				polybottom = popenbottom + rowoffset;
 				polytop = polybottom + midtexheight;
-				polybottomslope = popenbottomslope + gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
+				polybottomslope = popenbottomslope + rowoffset;
 				polytopslope = polybottomslope + midtexheight;
 			}
 			// Skew it according to the ceiling's slope
 			else
 			{
-				polytop = popentop + gl_sidedef->rowoffset;
+				polytop = popentop + rowoffset;
 				polybottom = polytop - midtexheight;
-				polytopslope = popentopslope + gl_sidedef->rowoffset;
+				polytopslope = popentopslope + rowoffset;
 				polybottomslope = polytopslope - midtexheight;
 			}
 
@@ -1403,17 +1442,15 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 				texturevpegslope = polytopslope - hS;
 			}
 
-			grTex = HWR_GetTexture(gl_midtexture);
-
 			// Left side
-			wallVerts[3].t = texturevpeg * grTex->scaleY;
-			wallVerts[0].t = (h - l + texturevpeg) * grTex->scaleY;
-			wallVerts[0].s = wallVerts[3].s = (cliplow + gl_sidedef->offsetx_mid) * grTex->scaleX;
+			wallVerts[3].t = texturevpeg * yscale * grTex->scaleY;
+			wallVerts[0].t = (h - l + texturevpeg) * yscale * grTex->scaleY;
+			wallVerts[0].s = wallVerts[3].s = ((cliplow * xscale) + gl_sidedef->offsetx_mid) * grTex->scaleX;
 
 			// Right side
-			wallVerts[2].t = texturevpegslope * grTex->scaleY;
-			wallVerts[1].t = (hS - lS + texturevpegslope) * grTex->scaleY;
-			wallVerts[2].s = wallVerts[1].s = (cliphigh + gl_sidedef->offsetx_mid) * grTex->scaleX;
+			wallVerts[2].t = texturevpegslope * yscale * grTex->scaleY;
+			wallVerts[1].t = (hS - lS + texturevpegslope) * yscale * grTex->scaleY;
+			wallVerts[2].s = wallVerts[1].s = ((cliphigh * xscale) + gl_sidedef->offsetx_mid) * grTex->scaleX;
 
 			// set top/bottom coords
 			// Take the texture peg into account, rather than changing the offsets past
@@ -1471,36 +1508,40 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 		// Single sided line... Deal only with the middletexture (if one exists)
 		if (gl_midtexture && gl_linedef->special != HORIZONSPECIAL) // (Ignore horizon line for OGL)
 		{
+			grTex = HWR_GetTexture(gl_midtexture);
+			xscale = FixedToFloat(gl_sidedef->scalex_mid);
+			yscale = FixedToFloat(gl_sidedef->scaley_mid);
+
 			fixed_t texturevpeg;
 
 			// PEGGING
 			if ((gl_linedef->flags & (ML_DONTPEGBOTTOM|ML_NOSKEW)) == (ML_DONTPEGBOTTOM|ML_NOSKEW))
-				texturevpeg = gl_frontsector->floorheight + textureheight[gl_sidedef->midtexture] - gl_frontsector->ceilingheight + gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
+				texturevpeg = (gl_frontsector->floorheight + textureheight[gl_sidedef->midtexture] - gl_frontsector->ceilingheight) * yscale;
 			else if (gl_linedef->flags & ML_DONTPEGBOTTOM)
-				texturevpeg = worldbottom + textureheight[gl_sidedef->midtexture] - worldtop + gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
+				texturevpeg = (worldbottom + textureheight[gl_sidedef->midtexture] - worldtop) * yscale;
 			else
 				// top of texture at top
-				texturevpeg = gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
+				texturevpeg = 0;
 
-			grTex = HWR_GetTexture(gl_midtexture);
+			texturevpeg += gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
 
 			wallVerts[3].t = wallVerts[2].t = texturevpeg * grTex->scaleY;
 			wallVerts[0].t = wallVerts[1].t = (texturevpeg + gl_frontsector->ceilingheight - gl_frontsector->floorheight) * grTex->scaleY;
-			wallVerts[0].s = wallVerts[3].s = (cliplow + gl_sidedef->offsetx_mid) * grTex->scaleX;
-			wallVerts[2].s = wallVerts[1].s = (cliphigh + gl_sidedef->offsetx_mid) * grTex->scaleX;
+			wallVerts[0].s = wallVerts[3].s = ((cliplow * xscale) + gl_sidedef->offsetx_mid) * grTex->scaleX;
+			wallVerts[2].s = wallVerts[1].s = ((cliphigh * xscale) + gl_sidedef->offsetx_mid) * grTex->scaleX;
 
 			// Texture correction for slopes
 			if (gl_linedef->flags & ML_NOSKEW) {
-				wallVerts[3].t += (gl_frontsector->ceilingheight - worldtop) * grTex->scaleY;
-				wallVerts[2].t += (gl_frontsector->ceilingheight - worldtopslope) * grTex->scaleY;
-				wallVerts[0].t += (gl_frontsector->floorheight - worldbottom) * grTex->scaleY;
-				wallVerts[1].t += (gl_frontsector->floorheight - worldbottomslope) * grTex->scaleY;
+				wallVerts[3].t += (gl_frontsector->ceilingheight - worldtop) * yscale * grTex->scaleY;
+				wallVerts[2].t += (gl_frontsector->ceilingheight - worldtopslope) * yscale * grTex->scaleY;
+				wallVerts[0].t += (gl_frontsector->floorheight - worldbottom) * yscale * grTex->scaleY;
+				wallVerts[1].t += (gl_frontsector->floorheight - worldbottomslope) * yscale * yscale;
 			} else if (gl_linedef->flags & ML_DONTPEGBOTTOM) {
-				wallVerts[3].t = wallVerts[0].t + (worldbottom-worldtop) * grTex->scaleY;
-				wallVerts[2].t = wallVerts[1].t + (worldbottomslope-worldtopslope) * grTex->scaleY;
+				wallVerts[3].t = wallVerts[0].t + ((worldbottom - worldtop) * yscale) * grTex->scaleY;
+				wallVerts[2].t = wallVerts[1].t + ((worldbottomslope - worldtopslope) * yscale) * grTex->scaleY;
 			} else {
-				wallVerts[0].t = wallVerts[3].t - (worldbottom-worldtop) * grTex->scaleY;
-				wallVerts[1].t = wallVerts[2].t - (worldbottomslope-worldtopslope) * grTex->scaleY;
+				wallVerts[0].t = wallVerts[3].t - ((worldbottom - worldtop) * yscale) * grTex->scaleY;
+				wallVerts[1].t = wallVerts[2].t - ((worldbottomslope - worldtopslope) * yscale) * grTex->scaleY;
 			}
 
 			//Set textures properly on single sided walls that are sloped
@@ -1585,15 +1626,17 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 				if ((high1 < lowcut && highslope1 < lowcutslope) || (low1 > highcut && lowslope1 > highcutslope))
 					continue;
 
-				texnum = R_GetTextureNum(sides[rover->master->sidenum[0]].midtexture);
+				side_t *side = &sides[rover->master->sidenum[0]];
 
 				if (rover->master->flags & ML_TFERLINE)
 				{
 					size_t linenum = gl_curline->linedef-gl_backsector->lines[0];
 					newline = rover->master->frontsector->lines[0] + linenum;
-					texnum = R_GetTextureNum(sides[newline->sidenum[0]].midtexture);
+					side = &sides[newline->sidenum[0]];
 				}
 
+				texnum = R_GetTextureNum(side->midtexture);
+
 				h  = P_GetFFloorTopZAt   (rover, v1x, v1y);
 				hS = P_GetFFloorTopZAt   (rover, v2x, v2y);
 				l  = P_GetFFloorBottomZAt(rover, v1x, v1y);
@@ -1609,14 +1652,13 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 					l = lowcut;
 					lS = lowcutslope;
 				}
-				//Hurdler: HW code starts here
-				//FIXME: check if peging is correct
-				// set top/bottom coords
 
+				// set top/bottom coords
 				wallVerts[3].y = FIXED_TO_FLOAT(h);
 				wallVerts[2].y = FIXED_TO_FLOAT(hS);
 				wallVerts[0].y = FIXED_TO_FLOAT(l);
 				wallVerts[1].y = FIXED_TO_FLOAT(lS);
+
 				if (rover->fofflags & FOF_FOG)
 				{
 					wallVerts[3].t = wallVerts[2].t = 0;
@@ -1626,56 +1668,46 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 				}
 				else
 				{
-					fixed_t texturevpeg;
-					boolean attachtobottom = false;
-					boolean slopeskew = false; // skew FOF walls with slopes?
-
 					// Wow, how was this missing from OpenGL for so long?
 					// ...Oh well, anyway, Lower Unpegged now changes pegging of FOFs like in software
 					// -- Monster Iestyn 26/06/18
-					if (newline)
-					{
-						texturevpeg = sides[newline->sidenum[0]].rowoffset + sides[newline->sidenum[0]].offsety_mid;
-						attachtobottom = !!(newline->flags & ML_DONTPEGBOTTOM);
-						slopeskew = !!(newline->flags & ML_SKEWTD);
-					}
-					else
-					{
-						texturevpeg = sides[rover->master->sidenum[0]].rowoffset + sides[rover->master->sidenum[0]].offsety_mid;
-						attachtobottom = !!(gl_linedef->flags & ML_DONTPEGBOTTOM);
-						slopeskew = !!(rover->master->flags & ML_SKEWTD);
-					}
+					fixed_t texturevpeg = side->rowoffset + side->offsety_mid;
+					boolean attachtobottom = !!(rover->master->flags & ML_DONTPEGBOTTOM);
 
 					grTex = HWR_GetTexture(texnum);
+					xscale = FixedToFloat(side->scalex_mid);
+					yscale = FixedToFloat(side->scaley_mid);
 
-					if (!slopeskew) // no skewing
+					if (!(rover->master->flags & ML_SKEWTD)) // no skewing
 					{
 						if (attachtobottom)
-							texturevpeg -= *rover->topheight - *rover->bottomheight;
-						wallVerts[3].t = (*rover->topheight - h + texturevpeg) * grTex->scaleY;
-						wallVerts[2].t = (*rover->topheight - hS + texturevpeg) * grTex->scaleY;
-						wallVerts[0].t = (*rover->topheight - l + texturevpeg) * grTex->scaleY;
-						wallVerts[1].t = (*rover->topheight - lS + texturevpeg) * grTex->scaleY;
+							texturevpeg -= (*rover->topheight - *rover->bottomheight) * yscale;
+
+						wallVerts[3].t = (((*rover->topheight - h) * yscale) + texturevpeg) * grTex->scaleY;
+						wallVerts[2].t = (((*rover->topheight - hS) * yscale) + texturevpeg) * grTex->scaleY;
+						wallVerts[0].t = (((*rover->topheight - l) * yscale) + texturevpeg) * grTex->scaleY;
+						wallVerts[1].t = (((*rover->topheight - lS) * yscale) + texturevpeg) * grTex->scaleY;
 					}
 					else
 					{
 						if (!attachtobottom) // skew by top
 						{
 							wallVerts[3].t = wallVerts[2].t = texturevpeg * grTex->scaleY;
-							wallVerts[0].t = (h - l + texturevpeg) * grTex->scaleY;
-							wallVerts[1].t = (hS - lS + texturevpeg) * grTex->scaleY;
+							wallVerts[0].t = (((h - l) * yscale) + texturevpeg) * grTex->scaleY;
+							wallVerts[1].t = (((hS - lS) * yscale) + texturevpeg) * grTex->scaleY;
 						}
 						else // skew by bottom
 						{
 							wallVerts[0].t = wallVerts[1].t = texturevpeg * grTex->scaleY;
-							wallVerts[3].t = wallVerts[0].t - (h - l) * grTex->scaleY;
-							wallVerts[2].t = wallVerts[1].t - (hS - lS) * grTex->scaleY;
+							wallVerts[3].t = wallVerts[0].t - ((h - l) * yscale) * grTex->scaleY;
+							wallVerts[2].t = wallVerts[1].t - ((hS - lS) * yscale) * grTex->scaleY;
 						}
 					}
 
-					wallVerts[0].s = wallVerts[3].s = (cliplow + gl_sidedef->offsetx_mid) * grTex->scaleX;
-					wallVerts[2].s = wallVerts[1].s = (cliphigh + gl_sidedef->offsetx_mid) * grTex->scaleX;
+					wallVerts[0].s = wallVerts[3].s = ((cliplow * xscale) + side->offsetx_mid) * grTex->scaleX;
+					wallVerts[2].s = wallVerts[1].s = ((cliphigh * xscale) + side->offsetx_mid) * grTex->scaleX;
 				}
+
 				if (rover->fofflags & FOF_FOG)
 				{
 					FBITFIELD blendmode;
@@ -1742,14 +1774,17 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 				if ((high1 < lowcut && highslope1 < lowcutslope) || (low1 > highcut && lowslope1 > highcutslope))
 					continue;
 
-				texnum = R_GetTextureNum(sides[rover->master->sidenum[0]].midtexture);
+				side_t *side = &sides[rover->master->sidenum[0]];
 
 				if (rover->master->flags & ML_TFERLINE)
 				{
 					size_t linenum = gl_curline->linedef-gl_backsector->lines[0];
 					newline = rover->master->frontsector->lines[0] + linenum;
-					texnum = R_GetTextureNum(sides[newline->sidenum[0]].midtexture);
+					side = &sides[newline->sidenum[0]];
 				}
+
+				texnum = R_GetTextureNum(side->midtexture);
+
 				h  = P_GetFFloorTopZAt   (rover, v1x, v1y);
 				hS = P_GetFFloorTopZAt   (rover, v2x, v2y);
 				l  = P_GetFFloorBottomZAt(rover, v1x, v1y);
@@ -1783,20 +1818,16 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 				else
 				{
 					grTex = HWR_GetTexture(texnum);
+					xscale = FixedToFloat(side->scalex_mid);
+					yscale = FixedToFloat(side->scaley_mid);
 
-					if (newline)
-					{
-						wallVerts[3].t = wallVerts[2].t = (*rover->topheight - h + sides[newline->sidenum[0]].rowoffset + sides[newline->sidenum[0]].offsety_mid) * grTex->scaleY;
-						wallVerts[0].t = wallVerts[1].t = (h - l + (*rover->topheight - h + sides[newline->sidenum[0]].rowoffset) + sides[newline->sidenum[0]].offsety_mid) * grTex->scaleY;
-					}
-					else
-					{
-						wallVerts[3].t = wallVerts[2].t = (*rover->topheight - h + sides[rover->master->sidenum[0]].rowoffset + sides[rover->master->sidenum[0]].offsety_mid) * grTex->scaleY;
-						wallVerts[0].t = wallVerts[1].t = (h - l + (*rover->topheight - h + sides[rover->master->sidenum[0]].rowoffset + sides[rover->master->sidenum[0]].offsety_mid)) * grTex->scaleY;
-					}
+					fixed_t diff = (*rover->topheight - h) * yscale;
 
-					wallVerts[0].s = wallVerts[3].s = (cliplow + gl_sidedef->offsetx_mid) * grTex->scaleX;
-					wallVerts[2].s = wallVerts[1].s = (cliphigh + gl_sidedef->offsetx_mid) * grTex->scaleX;
+					wallVerts[3].t = wallVerts[2].t = (diff + side->rowoffset + side->offsety_mid) * grTex->scaleY;
+					wallVerts[0].t = wallVerts[1].t = (((h - l) * yscale) + (diff + side->rowoffset + side->offsety_mid)) * grTex->scaleY;
+
+					wallVerts[0].s = wallVerts[3].s = ((cliplow * xscale) + side->offsetx_mid) * grTex->scaleX;
+					wallVerts[2].s = wallVerts[1].s = ((cliphigh * xscale) + side->offsetx_mid) * grTex->scaleX;
 				}
 
 				if (rover->fofflags & FOF_FOG)
@@ -1866,10 +1897,26 @@ static boolean CheckClip(seg_t * seg, sector_t * afrontsector, sector_t * abacks
 	if (afrontsector->f_slope || afrontsector->c_slope || abacksector->f_slope || abacksector->c_slope)
 	{
 		fixed_t v1x, v1y, v2x, v2y; // the seg's vertexes as fixed_t
-		v1x = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv1)->x);
-		v1y = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv1)->y);
-		v2x = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv2)->x);
-		v2y = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv2)->y);
+		if (gl_curline->pv1)
+		{
+			v1x = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv1)->x);
+			v1y = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv1)->y);
+		}
+		else
+		{
+			v1x = gl_curline->v1->x;
+			v1y = gl_curline->v1->y;
+		}
+		if (gl_curline->pv2)
+		{
+			v2x = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv2)->x);
+			v2y = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv2)->y);
+		}
+		else
+		{
+			v2x = gl_curline->v2->x;
+			v2y = gl_curline->v2->y;
+		}
 #define SLOPEPARAMS(slope, end1, end2, normalheight) \
 		end1 = P_GetZAt(slope, v1x, v1y, normalheight); \
 		end2 = P_GetZAt(slope, v2x, v2y, normalheight);
@@ -2242,10 +2289,26 @@ static void HWR_AddLine(seg_t * line)
 
 	gl_curline = line;
 
-	v1x = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv1)->x);
-	v1y = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv1)->y);
-	v2x = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv2)->x);
-	v2y = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv2)->y);
+	if (gl_curline->pv1)
+	{
+		v1x = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv1)->x);
+		v1y = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv1)->y);
+	}
+	else
+	{
+		v1x = gl_curline->v1->x;
+		v1y = gl_curline->v1->y;
+	}
+	if (gl_curline->pv2)
+	{
+		v2x = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv2)->x);
+		v2y = FLOAT_TO_FIXED(((polyvertex_t *)gl_curline->pv2)->y);
+	}
+	else
+	{
+		v2x = gl_curline->v2->x;
+		v2y = gl_curline->v2->y;
+	}
 
 	// OPTIMIZE: quickly reject orthogonal back sides.
 	angle1 = R_PointToAngle64(v1x, v1y);
@@ -2650,11 +2713,8 @@ static void HWR_RenderPolyObjectPlane(polyobj_t *polysector, boolean isceiling,
 	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;
-	UINT16 flatflag = 63;
-
-	boolean texflat = false;
+	float xscale = 1.0f, yscale = 1.0f;
 
 	float scrollx = 0.0f, scrolly = 0.0f;
 	float tempxsow, tempytow, anglef = 0.0f;
@@ -2685,8 +2745,8 @@ static void HWR_RenderPolyObjectPlane(polyobj_t *polysector, boolean isceiling,
 		if (levelflat->type == LEVELFLAT_FLAT)
 		{
 			size_t len = W_LumpLength(levelflat->u.flat.lumpnum);
-			flatflag = R_GetFlatSize(len) - 1;
-			fflatwidth = fflatheight = (float)(flatflag + 1);
+			unsigned flatflag = R_GetFlatSize(len);
+			fflatwidth = fflatheight = (float)flatflag;
 		}
 		else
 		{
@@ -2700,19 +2760,11 @@ static void HWR_RenderPolyObjectPlane(polyobj_t *polysector, boolean isceiling,
 				fflatwidth = levelflat->width;
 				fflatheight = levelflat->height;
 			}
-			texflat = true;
 		}
 	}
 	else // set no texture
 		HWR_SetCurrentTexture(NULL);
 
-	// reference point for flat texture coord for each vertex around the polygon
-	flatxref = FIXED_TO_FLOAT(polysector->origVerts[0].x);
-	flatyref = FIXED_TO_FLOAT(polysector->origVerts[0].y);
-
-	flatxref = (float)(((fixed_t)flatxref & (~flatflag)) / fflatwidth);
-	flatyref = (float)(((fixed_t)flatyref & (~flatflag)) / fflatheight);
-
 	// transform
 	v3d = planeVerts;
 
@@ -2720,14 +2772,18 @@ static void HWR_RenderPolyObjectPlane(polyobj_t *polysector, boolean isceiling,
 	{
 		if (!isceiling) // it's a floor
 		{
-			scrollx = FIXED_TO_FLOAT(FOFsector->floorxoffset)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(FOFsector->flooryoffset)/fflatheight;
+			xscale = FixedToFloat(FOFsector->floorxscale);
+			yscale = FixedToFloat(FOFsector->flooryscale);
+			scrollx = FixedToFloat(FOFsector->floorxoffset) / fflatwidth;
+			scrolly = FixedToFloat(FOFsector->flooryoffset) / fflatheight;
 			angle = FOFsector->floorangle;
 		}
 		else // it's a ceiling
 		{
-			scrollx = FIXED_TO_FLOAT(FOFsector->ceilingxoffset)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(FOFsector->ceilingyoffset)/fflatheight;
+			xscale = FixedToFloat(FOFsector->ceilingxscale);
+			yscale = FixedToFloat(FOFsector->ceilingyscale);
+			scrollx = FixedToFloat(FOFsector->ceilingxoffset) / fflatwidth;
+			scrolly = FixedToFloat(FOFsector->ceilingyoffset) / fflatheight;
 			angle = FOFsector->ceilingangle;
 		}
 	}
@@ -2735,43 +2791,30 @@ static void HWR_RenderPolyObjectPlane(polyobj_t *polysector, boolean isceiling,
 	{
 		if (!isceiling) // it's a floor
 		{
-			scrollx = FIXED_TO_FLOAT(gl_frontsector->floorxoffset)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(gl_frontsector->flooryoffset)/fflatheight;
+			xscale = FixedToFloat(gl_frontsector->floorxscale);
+			yscale = FixedToFloat(gl_frontsector->flooryscale);
+			scrollx = FixedToFloat(gl_frontsector->floorxoffset) / fflatwidth;
+			scrolly = FixedToFloat(gl_frontsector->flooryoffset) / fflatheight;
 			angle = gl_frontsector->floorangle;
 		}
 		else // it's a ceiling
 		{
-			scrollx = FIXED_TO_FLOAT(gl_frontsector->ceilingxoffset)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(gl_frontsector->ceilingyoffset)/fflatheight;
+			xscale = FixedToFloat(gl_frontsector->ceilingxscale);
+			yscale = FixedToFloat(gl_frontsector->ceilingyscale);
+			scrollx = FixedToFloat(gl_frontsector->ceilingxoffset) / fflatwidth;
+			scrolly = FixedToFloat(gl_frontsector->ceilingyoffset) / fflatheight;
 			angle = gl_frontsector->ceilingangle;
 		}
 	}
 
-	if (angle) // Only needs to be done if there's an altered angle
-	{
-		tempxsow = flatxref;
-		tempytow = flatyref;
-
-		anglef = ANG2RAD(InvAngle(angle));
-
-		flatxref = (tempxsow * cos(anglef)) - (tempytow * sin(anglef));
-		flatyref = (tempxsow * sin(anglef)) + (tempytow * cos(anglef));
-	}
+	anglef = ANG2RAD(InvAngle(angle));
 
 	for (i = 0; i < (INT32)nrPlaneVerts; i++,v3d++)
 	{
 		// Go from the polysector's original vertex locations
 		// Means the flat is offset based on the original vertex locations
-		if (texflat)
-		{
-			v3d->s = (float)(FIXED_TO_FLOAT(polysector->origVerts[i].x) / fflatwidth) + scrollx;
-			v3d->t = -(float)(FIXED_TO_FLOAT(polysector->origVerts[i].y) / fflatheight) + scrolly;
-		}
-		else
-		{
-			v3d->s = (float)((FIXED_TO_FLOAT(polysector->origVerts[i].x) / fflatwidth) - flatxref + scrollx);
-			v3d->t = (float)(flatyref - (FIXED_TO_FLOAT(polysector->origVerts[i].y) / fflatheight) + scrolly);
-		}
+		v3d->s = (FixedToFloat(polysector->origVerts[i].x) / fflatwidth) + (scrollx / xscale);
+		v3d->t = -(FixedToFloat(polysector->origVerts[i].y) / fflatheight) + (scrolly / yscale);
 
 		// Need to rotate before translate
 		if (angle) // Only needs to be done if there's an altered angle
@@ -2783,6 +2826,9 @@ static void HWR_RenderPolyObjectPlane(polyobj_t *polysector, boolean isceiling,
 			v3d->t = (tempxsow * sin(anglef)) + (tempytow * cos(anglef));
 		}
 
+		v3d->s *= xscale;
+		v3d->t *= yscale;
+
 		v3d->x = FIXED_TO_FLOAT(polysector->vertices[i]->x);
 		v3d->y = height;
 		v3d->z = FIXED_TO_FLOAT(polysector->vertices[i]->y);
@@ -5256,7 +5302,7 @@ static void HWR_ProjectSprite(mobj_t *thing)
 			rollangle = R_GetRollAngle(spriterotangle);
 		}
 
-		rotsprite = Patch_GetRotatedSprite(sprframe, (thing->frame & FF_FRAMEMASK), rot, flip, false, sprinfo, rollangle);
+		rotsprite = Patch_GetRotatedSprite(sprframe, (thing->frame & FF_FRAMEMASK), rot, flip, sprinfo, rollangle);
 
 		if (rotsprite != NULL)
 		{
@@ -5874,6 +5920,9 @@ void HWR_BuildSkyDome(void)
 
 static void HWR_DrawSkyBackground(player_t *player)
 {
+	if (HWR_IsWireframeMode())
+		return;
+
 	HWD.pfnSetBlend(PF_Translucent|PF_NoDepthTest|PF_Modulated);
 
 	if (cv_glskydome.value)
@@ -6230,6 +6279,9 @@ void HWR_RenderSkyboxView(INT32 viewnumber, player_t *player)
 	// Reset the shader state.
 	HWR_SetShaderState();
 
+	if (HWR_IsWireframeMode())
+		HWD.pfnSetSpecialState(HWD_SET_WIREFRAME, 1);
+
 	validcount++;
 
 	if (cv_glbatching.value)
@@ -6292,6 +6344,9 @@ void HWR_RenderSkyboxView(INT32 viewnumber, player_t *player)
 		HWR_CreateDrawNodes();
 	}
 
+	if (HWR_IsWireframeMode())
+		HWD.pfnSetSpecialState(HWD_SET_WIREFRAME, 0);
+
 	HWD.pfnSetTransform(NULL);
 	HWD.pfnUnSetShader();
 
@@ -6446,6 +6501,9 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 	// Reset the shader state.
 	HWR_SetShaderState();
 
+	if (HWR_IsWireframeMode())
+		HWD.pfnSetSpecialState(HWD_SET_WIREFRAME, 1);
+
 	ps_numbspcalls.value.i = 0;
 	ps_numpolyobjects.value.i = 0;
 	PS_START_TIMING(ps_bsptime);
@@ -6522,6 +6580,9 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 		HWR_CreateDrawNodes();
 	}
 
+	if (HWR_IsWireframeMode())
+		HWD.pfnSetSpecialState(HWD_SET_WIREFRAME, 0);
+
 	HWD.pfnSetTransform(NULL);
 	HWD.pfnUnSetShader();
 
@@ -6593,12 +6654,14 @@ consvar_t cv_glfakecontrast = CVAR_INIT ("gr_fakecontrast", "Smooth", CV_SAVE, g
 consvar_t cv_glslopecontrast = CVAR_INIT ("gr_slopecontrast", "Off", CV_SAVE, CV_OnOff, NULL);
 
 consvar_t cv_glfiltermode = CVAR_INIT ("gr_filtermode", "Nearest", CV_SAVE|CV_CALL, glfiltermode_cons_t, CV_glfiltermode_OnChange);
-consvar_t cv_glanisotropicmode = CVAR_INIT ("gr_anisotropicmode", "1", CV_CALL, glanisotropicmode_cons_t, CV_glanisotropic_OnChange);
+consvar_t cv_glanisotropicmode = CVAR_INIT ("gr_anisotropicmode", "1", CV_SAVE|CV_CALL, glanisotropicmode_cons_t, CV_glanisotropic_OnChange);
 
 consvar_t cv_glsolvetjoin = CVAR_INIT ("gr_solvetjoin", "On", 0, CV_OnOff, NULL);
 
 consvar_t cv_glbatching = CVAR_INIT ("gr_batching", "On", 0, CV_OnOff, NULL);
 
+consvar_t cv_glwireframe = CVAR_INIT ("gr_wireframe", "Off", 0, CV_OnOff, NULL);
+
 static void CV_glfiltermode_OnChange(void)
 {
 	if (rendermode == render_opengl)
@@ -6635,23 +6698,18 @@ void HWR_AddCommands(void)
 	CV_RegisterVar(&cv_glallowshaders);
 
 	CV_RegisterVar(&cv_glfiltermode);
+	CV_RegisterVar(&cv_glanisotropicmode);
 	CV_RegisterVar(&cv_glsolvetjoin);
 
 	CV_RegisterVar(&cv_glbatching);
 
+	CV_RegisterVar(&cv_glwireframe);
+
 #ifndef NEWCLIP
 	CV_RegisterVar(&cv_glclipwalls);
 #endif
 }
 
-void HWR_AddSessionCommands(void)
-{
-	if (gl_sessioncommandsadded)
-		return;
-	CV_RegisterVar(&cv_glanisotropicmode);
-	gl_sessioncommandsadded = true;
-}
-
 // --------------------------------------------------------------------------
 // Setup the hardware renderer
 // --------------------------------------------------------------------------
@@ -6662,7 +6720,6 @@ void HWR_Startup(void)
 		CONS_Printf("HWR_Startup()...\n");
 
 		HWR_InitPolyPool();
-		HWR_AddSessionCommands();
 		HWR_InitMapTextures();
 		HWR_InitModels();
 #ifdef ALAM_LIGHTING
@@ -6685,10 +6742,6 @@ void HWR_Startup(void)
 // --------------------------------------------------------------------------
 void HWR_Switch(void)
 {
-	// Add session commands
-	if (!gl_sessioncommandsadded)
-		HWR_AddSessionCommands();
-
 	// Set special states from CVARs
 	HWD.pfnSetSpecialState(HWD_SET_TEXTUREFILTERMODE, cv_glfiltermode.value);
 	HWD.pfnSetSpecialState(HWD_SET_TEXTUREANISOTROPICMODE, cv_glanisotropicmode.value);
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index 9450ca2c56ed56d52916f587136e2e9352f3f4b4..6348592af688a1e40ad0ec9dc2ff9a846508b561 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -54,7 +54,6 @@ UINT8 *HWR_GetScreenshot(void);
 boolean HWR_Screenshot(const char *pathname);
 
 void HWR_AddCommands(void);
-void HWR_AddSessionCommands(void);
 void transform(float *cx, float *cy, float *cz);
 INT32 HWR_GetTextureUsed(void);
 void HWR_DoPostProcessor(player_t *player);
@@ -108,6 +107,8 @@ extern consvar_t cv_glslopecontrast;
 
 extern consvar_t cv_glbatching;
 
+extern consvar_t cv_glwireframe;
+
 extern float gl_viewwidth, gl_viewheight, gl_baseviewwindowy;
 
 extern float gl_viewwindowx, gl_basewindowcentery;
diff --git a/src/hardware/hw_md2.c b/src/hardware/hw_md2.c
index 0f834213592e041c866ca05a90d2599476c81c76..3e080105cbde44fd66775c1d4065c10c57fb7041 100644
--- a/src/hardware/hw_md2.c
+++ b/src/hardware/hw_md2.c
@@ -1140,9 +1140,6 @@ static void HWR_GetBlendedTexture(patch_t *patch, patch_t *blendpatch, INT32 ski
 	Z_ChangeTag(newMipmap->data, PU_HWRMODELTEXTURE_UNLOCKED);
 }
 
-#define NORMALFOG 0x00000000
-#define FADEFOG 0x19000000
-
 static boolean HWR_AllowModel(mobj_t *mobj)
 {
 	// Signpost overlay. Not needed.
diff --git a/src/hardware/r_opengl/r_opengl.c b/src/hardware/r_opengl/r_opengl.c
index 71cb5ca70375629ee06c7943e0c5be881555a0c1..ea831e41dee3b7afff7eed47d4ead3f858870648 100644
--- a/src/hardware/r_opengl/r_opengl.c
+++ b/src/hardware/r_opengl/r_opengl.c
@@ -302,6 +302,8 @@ typedef void (APIENTRY * PFNglDisable) (GLenum cap);
 static PFNglDisable pglDisable;
 typedef void (APIENTRY * PFNglGetFloatv) (GLenum pname, GLfloat *params);
 static PFNglGetFloatv pglGetFloatv;
+typedef void (APIENTRY * PFNglPolygonMode) (GLenum, GLenum);
+static PFNglPolygonMode pglPolygonMode;
 
 /* Depth Buffer */
 typedef void (APIENTRY * PFNglClearDepth) (GLclampd depth);
@@ -476,6 +478,7 @@ boolean SetupGLfunc(void)
 	GETOPENGLFUNC(pglGetFloatv, glGetFloatv)
 	GETOPENGLFUNC(pglGetIntegerv, glGetIntegerv)
 	GETOPENGLFUNC(pglGetString, glGetString)
+	GETOPENGLFUNC(pglPolygonMode, glPolygonMode)
 
 	GETOPENGLFUNC(pglClearDepth, glClearDepth)
 	GETOPENGLFUNC(pglDepthFunc, glDepthFunc)
@@ -697,7 +700,7 @@ static GLRGBAFloat shader_defaultcolor = {1.0f, 1.0f, 1.0f, 1.0f};
 #define GLSL_SOFTWARE_TINT_EQUATION \
 	"if (tint_color.a > 0.0) {\n" \
 		"float color_bright = sqrt((base_color.r * base_color.r) + (base_color.g * base_color.g) + (base_color.b * base_color.b));\n" \
-		"float strength = sqrt(9.0 * tint_color.a);\n" \
+		"float strength = sqrt(tint_color.a);\n" \
 		"final_color.r = clamp((color_bright * (tint_color.r * strength)) + (base_color.r * (1.0 - strength)), 0.0, 1.0);\n" \
 		"final_color.g = clamp((color_bright * (tint_color.g * strength)) + (base_color.g * (1.0 - strength)), 0.0, 1.0);\n" \
 		"final_color.b = clamp((color_bright * (tint_color.b * strength)) + (base_color.b * (1.0 - strength)), 0.0, 1.0);\n" \
@@ -2474,6 +2477,10 @@ EXPORT void HWRAPI(SetSpecialState) (hwdspecialstate_t IdState, INT32 Value)
 				Flush(); //??? if we want to change filter mode by texture, remove this
 			break;
 
+		case HWD_SET_WIREFRAME:
+			pglPolygonMode(GL_FRONT_AND_BACK, Value ? GL_LINE : GL_FILL);
+			break;
+
 		default:
 			break;
 	}
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index e223d320814c6dff885886d6d57a434c0d120493..0a2b71aeceed9e20e4f18da5b422dfd943ff1cc5 100644
--- a/src/hu_stuff.c
+++ b/src/hu_stuff.c
@@ -19,7 +19,9 @@
 #include "m_cond.h" // emblems
 #include "m_misc.h" // word jumping
 
-#include "d_clisrv.h"
+#include "netcode/d_clisrv.h"
+#include "netcode/net_command.h"
+#include "netcode/gamestate.h"
 
 #include "g_game.h"
 #include "g_input.h"
@@ -77,6 +79,7 @@ patch_t *nto_font[NT_FONTSIZE];
 
 static player_t *plr;
 boolean chat_on; // entering a chat message?
+boolean chat_on_first_event; // blocker for first chat input event
 static char w_chat[HU_MAXMSGLEN + 1];
 static size_t c_input = 0; // let's try to make the chat input less shitty.
 static boolean headsupactive = false;
@@ -175,14 +178,12 @@ static huddrawlist_h luahuddrawlist_scores;
 
 static tic_t resynch_ticker = 0;
 
-#ifndef NONET
 // just after
 static void Command_Say_f(void);
 static void Command_Sayto_f(void);
 static void Command_Sayteam_f(void);
 static void Command_CSay_f(void);
 static void Got_Saycmd(UINT8 **p, INT32 playernum);
-#endif
 
 void HU_LoadGraphics(void)
 {
@@ -327,13 +328,11 @@ void HU_LoadGraphics(void)
 //
 void HU_Init(void)
 {
-#ifndef NONET
 	COM_AddCommand("say", Command_Say_f, COM_LUA);
 	COM_AddCommand("sayto", Command_Sayto_f, COM_LUA);
 	COM_AddCommand("sayteam", Command_Sayteam_f, COM_LUA);
 	COM_AddCommand("csay", Command_CSay_f, COM_LUA);
 	RegisterNetXCmd(XD_SAY, Got_Saycmd);
-#endif
 
 	// set shift translation table
 	shiftxform = english_shiftxform;
@@ -363,8 +362,6 @@ void HU_Start(void)
 //                            EXECUTION
 //======================================================================
 
-#ifndef NONET
-
 // EVERY CHANGE IN THIS SCRIPT IS LOL XD! BY VINCYTM
 
 static UINT32 chat_nummsg_log = 0;
@@ -412,11 +409,9 @@ static void HU_removeChatText_Log(void)
 	}
 	chat_nummsg_log--; // lost 1 msg.
 }
-#endif
 
 void HU_AddChatText(const char *text, boolean playsound)
 {
-#ifndef NONET
 	if (playsound && cv_consolechat.value != 2) // Don't play the sound if we're using hidden chat.
 		S_StartSound(NULL, sfx_radio);
 	// reguardless of our preferences, put all of this in the chat buffer in case we decide to change from oldchat mid-game.
@@ -438,14 +433,8 @@ void HU_AddChatText(const char *text, boolean playsound)
 		CONS_Printf("%s\n", text);
 	else			// if we aren't, still save the message to log.txt
 		CON_LogMessage(va("%s\n", text));
-#else
-	(void)playsound;
-	CONS_Printf("%s\n", text);
-#endif
 }
 
-#ifndef NONET
-
 /** Runs a say command, sending an ::XD_SAY message.
   * A say command consists of a signed 8-bit integer for the target, an
   * unsigned 8-bit flag variable, and then the message itself.
@@ -630,7 +619,9 @@ static void Command_CSay_f(void)
 
 	DoSayCommand(0, 1, HU_CSAY);
 }
-static tic_t stop_spamming[MAXPLAYERS];
+
+static tic_t spam_tokens[MAXPLAYERS];
+static tic_t spam_tics[MAXPLAYERS];
 
 /** Receives a message, processing an ::XD_SAY command.
   * \sa DoSayCommand
@@ -682,14 +673,14 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 	// before we do anything, let's verify the guy isn't spamming, get this easier on us.
 
 	//if (stop_spamming[playernum] != 0 && cv_chatspamprotection.value && !(flags & HU_CSAY))
-	if (stop_spamming[playernum] != 0 && consoleplayer != playernum && cv_chatspamprotection.value && !(flags & HU_CSAY))
+	if (spam_tokens[playernum] <= 0 && cv_chatspamprotection.value && !(flags & HU_CSAY))
 	{
 		CONS_Debug(DBG_NETPLAY,"Received SAY cmd too quickly from Player %d (%s), assuming as spam and blocking message.\n", playernum+1, player_names[playernum]);
-		stop_spamming[playernum] = 4;
+		spam_tics[playernum] = 0;
 		spam_eatmsg = 1;
 	}
 	else
-		stop_spamming[playernum] = 4; // you can hold off for 4 tics, can you?
+		spam_tokens[playernum] -= 1;
 
 	// run the lua hook even if we were supposed to eat the msg, netgame consistency goes first.
 
@@ -865,12 +856,29 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 #endif
 }
 
-#endif
-
 //
 //
 void HU_Ticker(void)
 {
+	// do this server-side, too
+	if (netgame)
+	{
+		size_t i = 0;
+
+		// handle spam while we're at it:
+		for(; (i<MAXPLAYERS); i++)
+		{
+			if (spam_tokens[i] < (tic_t)cv_chatspamburst.value)
+			{
+				if (++spam_tics[i] >= (tic_t)cv_chatspamspeed.value)
+				{
+					spam_tokens[i]++;
+					spam_tics[i] = 0;
+				}
+			}
+		}
+	}
+
 	if (dedicated)
 		return;
 
@@ -882,7 +890,6 @@ void HU_Ticker(void)
 	else
 		hu_showscores = false;
 
-#ifndef NONET
 	if (chat_on)
 	{
 		// count down the scroll timer.
@@ -894,13 +901,6 @@ void HU_Ticker(void)
 	{
 		size_t i = 0;
 
-		// handle spam while we're at it:
-		for(; (i<MAXPLAYERS); i++)
-		{
-			if (stop_spamming[i] > 0)
-				stop_spamming[i]--;
-		}
-
 		// handle chat timers
 		for (i=0; (i<chat_nummsg_min); i++)
 		{
@@ -910,7 +910,6 @@ void HU_Ticker(void)
 				HU_removeChatText_Mini();
 		}
 	}
-#endif
 
 	if (cechotimer > 0) --cechotimer;
 
@@ -918,8 +917,6 @@ void HU_Ticker(void)
 		resynch_ticker++;
 }
 
-#ifndef NONET
-
 static boolean teamtalk = false;
 static boolean justscrolleddown;
 static boolean justscrolledup;
@@ -1027,8 +1024,6 @@ static void HU_sendChatMessage(void)
 	}
 }
 
-#endif
-
 void HU_clearChatChars(void)
 {
 	memset(w_chat, '\0', sizeof(w_chat));
@@ -1043,11 +1038,9 @@ void HU_clearChatChars(void)
 //
 boolean HU_Responder(event_t *ev)
 {
-#ifndef NONET
 	INT32 c=0;
-#endif
 
-	if (ev->type != ev_keydown)
+	if (ev->type != ev_keydown && ev->type != ev_text)
 		return false;
 
 	// only KeyDown events now...
@@ -1072,16 +1065,19 @@ boolean HU_Responder(event_t *ev)
 			return false;
 	}*/	//We don't actually care about that unless we get splitscreen netgames. :V
 
-#ifndef NONET
 	c = (INT32)ev->key;
 
 	if (!chat_on)
 	{
+		if (ev->type == ev_text)
+			return false;
+
 		// enter chat mode
 		if ((ev->key == gamecontrol[GC_TALKKEY][0] || ev->key == gamecontrol[GC_TALKKEY][1])
 			&& netgame && !OLD_MUTE) // check for old chat mute, still let the players open the chat incase they want to scroll otherwise.
 		{
 			chat_on = true;
+			chat_on_first_event = false;
 			w_chat[0] = 0;
 			teamtalk = false;
 			chat_scrollmedown = true;
@@ -1092,6 +1088,7 @@ boolean HU_Responder(event_t *ev)
 			&& netgame && !OLD_MUTE)
 		{
 			chat_on = true;
+			chat_on_first_event = false;
 			w_chat[0] = 0;
 			teamtalk = G_GametypeHasTeams(); // Don't teamtalk if we don't have teams.
 			chat_scrollmedown = true;
@@ -1101,6 +1098,31 @@ boolean HU_Responder(event_t *ev)
 	}
 	else // if chat_on
 	{
+		if (!chat_on_first_event)
+		{
+			// since the text event is sent immediately after the keydown event,
+			// we need to make sure that nothing is displayed once the chat
+			// opens, otherwise a 't' would be outputted.
+			chat_on_first_event = true;
+			return true;
+		}
+
+		if (ev->type == ev_text)
+		{
+			if ((c < HU_FONTSTART || c > HU_FONTEND || !hu_font[c-HU_FONTSTART])
+				&& c != ' ') // Allow spaces, of course
+			{
+				return false;
+			}
+
+			if (CHAT_MUTE || strlen(w_chat) >= HU_MAXMSGLEN)
+				return true;
+
+			memmove(&w_chat[c_input + 1], &w_chat[c_input], strlen(w_chat) - c_input + 1);
+			w_chat[c_input] = c;
+			c_input++;
+			return true;
+		}
 
 		// Ignore modifier keys
 		// Note that we do this here so users can still set
@@ -1110,23 +1132,8 @@ boolean HU_Responder(event_t *ev)
 		 || ev->key == KEY_LALT || ev->key == KEY_RALT)
 			return true;
 
-		c = (INT32)ev->key;
-
-		// I know this looks very messy but this works. If it ain't broke, don't fix it!
-		// shift LETTERS to uppercase if we have capslock or are holding shift
-		if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
-		{
-			if (shiftdown ^ capslock)
-				c = shiftxform[c];
-		}
-		else // if we're holding shift we should still shift non letter symbols
-		{
-			if (shiftdown)
-				c = shiftxform[c];
-		}
-
 		// pasting. pasting is cool. chat is a bit limited, though :(
-		if ((c == 'v' || c == 'V') && ctrldown)
+		if (c == 'v' && ctrldown)
 		{
 			const char *paste;
 			size_t chatlen;
@@ -1194,16 +1201,6 @@ boolean HU_Responder(event_t *ev)
 			else
 				c_input++;
 		}
-		else if ((c >= HU_FONTSTART && c <= HU_FONTEND && hu_font[c-HU_FONTSTART])
-			|| c == ' ') // Allow spaces, of course
-		{
-			if (CHAT_MUTE || strlen(w_chat) >= HU_MAXMSGLEN)
-				return true;
-
-			memmove(&w_chat[c_input + 1], &w_chat[c_input], strlen(w_chat) - c_input + 1);
-			w_chat[c_input] = c;
-			c_input++;
-		}
 		else if (c == KEY_BACKSPACE)
 		{
 			if (CHAT_MUTE || c_input <= 0)
@@ -1222,7 +1219,6 @@ boolean HU_Responder(event_t *ev)
 
 		return true;
 	}
-#endif
 
 	return false;
 }
@@ -1232,8 +1228,6 @@ boolean HU_Responder(event_t *ev)
 //                         HEADS UP DRAWING
 //======================================================================
 
-#ifndef NONET
-
 // Precompile a wordwrapped string to any given width.
 // This is a muuuch better method than V_WORDWRAP.
 // again stolen and modified a bit from video.c, don't mind me, will need to rearrange this one day.
@@ -1813,7 +1807,6 @@ static void HU_DrawChat_Old(void)
 	if (hu_tick < 4)
 		V_DrawCharacter(HU_INPUTX + c, y, '_' | cv_constextsize.value |V_NOSCALESTART|t, true);
 }
-#endif
 
 // Draw crosshairs at the exact center of the view.
 // In splitscreen, crosshairs are stretched vertically to compensate for V_PERPLAYER squishing them.
@@ -1953,7 +1946,6 @@ static void HU_DrawDemoInfo(void)
 //
 void HU_Drawer(void)
 {
-#ifndef NONET
 	// draw chat string plus cursor
 	if (chat_on)
 	{
@@ -1970,7 +1962,6 @@ void HU_Drawer(void)
 		if (!OLDCHAT && cv_consolechat.value < 2 && netgame) // Don't display minimized chat if you set the mode to Window (Hidden)
 			HU_drawMiniChat(); // draw messages in a cool fashion.
 	}
-#endif
 
 	if (cechotimer)
 		HU_DrawCEcho();
diff --git a/src/i_time.c b/src/i_time.c
index 2a22503f1b8b5c70a8145d2c45696aa2c50f0922..39854b242db15dae5b89c634a938783e35691df2 100644
--- a/src/i_time.c
+++ b/src/i_time.c
@@ -17,7 +17,7 @@
 
 #include "command.h"
 #include "doomtype.h"
-#include "d_netcmd.h"
+#include "netcode/d_netcmd.h"
 #include "m_fixed.h"
 #include "i_system.h"
 
@@ -30,12 +30,6 @@ static precise_t enterprecise, oldenterprecise;
 static fixed_t entertic, oldentertics;
 static double tictimer;
 
-// A little more than the minimum sleep duration on Windows.
-// May be incorrect for other platforms, but we don't currently have a way to
-// query the scheduler granularity. SDL will do what's needed to make this as
-// low as possible though.
-#define MIN_SLEEP_DURATION_MS 2.1
-
 tic_t I_GetTime(void)
 {
 	return g_time.time;
@@ -88,38 +82,3 @@ void I_UpdateTime(fixed_t timescale)
 		g_time.timefrac = FLOAT_TO_FIXED(fractional);
 	}
 }
-
-void I_SleepDuration(precise_t duration)
-{
-	UINT64 precision = I_GetPrecisePrecision();
-	INT32 sleepvalue = cv_sleep.value;
-	UINT64 delaygranularity;
-	precise_t cur;
-	precise_t dest;
-
-	{
-		double gran = round(((double)(precision / 1000) * sleepvalue * MIN_SLEEP_DURATION_MS));
-		delaygranularity = (UINT64)gran;
-	}
-
-	cur = I_GetPreciseTime();
-	dest = cur + duration;
-
-	// the reason this is not dest > cur is because the precise counter may wrap
-	// two's complement arithmetic is our friend here, though!
-	// e.g. cur 0xFFFFFFFFFFFFFFFE = -2, dest 0x0000000000000001 = 1
-	// 0x0000000000000001 - 0xFFFFFFFFFFFFFFFE = 3
-	while ((INT64)(dest - cur) > 0)
-	{
-		// If our cv_sleep value exceeds the remaining sleep duration, use the
-		// hard sleep function.
-		if (sleepvalue > 0 && (dest - cur) > delaygranularity)
-		{
-			I_Sleep(sleepvalue);
-		}
-
-		// Otherwise, this is a spinloop.
-
-		cur = I_GetPreciseTime();
-	}
-}
diff --git a/src/info.c b/src/info.c
index 36389d849a38e21064e0f4e94b803cccfc9f7ef0..d1707c86a597902fdbbba249387cfa591a11c661 100644
--- a/src/info.c
+++ b/src/info.c
@@ -33,7 +33,8 @@ char sprnames[NUMSPRITES + 1][5] =
 	"NULL", // invisible object
 	"UNKN",
 
-	"THOK", // Thok! mobj
+	"THOK", // Spin trail mobj
+	"THKE", // Thok boom effect
 	"PLAY",
 
 	// Enemies
@@ -250,6 +251,7 @@ char sprnames[NUMSPRITES + 1][5] =
 	"BMCH", // Big Mace Chain
 	"SMCE", // Small Mace
 	"BMCE", // Big Mace
+	"BSPB", // Blue spring on a ball
 	"YSPB", // Yellow spring on a ball
 	"RSPB", // Red spring on a ball
 	"SFBR", // Small Firebar
@@ -702,8 +704,9 @@ state_t states[NUMSTATES] =
 	{SPR_UNKN, FF_FULLBRIGHT, -1, {A_InfoState}, 5, 0, S_NULL}, // S_XDEATHSTATE
 	{SPR_UNKN, FF_FULLBRIGHT, -1, {A_InfoState}, 6, 0, S_NULL}, // S_RAISESTATE
 
-	// Thok
+	// Spin trail and thok boom effect
 	{SPR_THOK, FF_TRANS50, 8, {NULL}, 0, 0, S_NULL}, // S_THOK
+	{SPR_THKE, FF_TRANS50|FF_PAPERSPRITE, 8, {NULL}, 0, 0, S_NULL}, // S_THOKEFFECT
 
 	// Player
 	{SPR_PLAY, SPR2_STND|FF_ANIMATE,    105, {NULL}, 0,  7, S_PLAY_WAIT}, // S_PLAY_STND
@@ -2297,6 +2300,13 @@ state_t states[NUMSTATES] =
 	{SPR_SMCH, 1, -1, {NULL}, 0, 0, S_NULL}, // S_SMALLGRABCHAIN
 	{SPR_BMCH, 1, -1, {NULL}, 0, 0, S_NULL}, // S_BIGGRABCHAIN
 
+	// Blue spring on a ball
+	{SPR_BSPB, 0, -1, {NULL},   0, 0, S_NULL},            // S_BLUESPRINGBALL
+	{SPR_BSPB, 4,  4, {A_Pain}, 0, 0, S_BLUESPRINGBALL3}, // S_BLUESPRINGBALL2
+	{SPR_BSPB, 3,  1, {NULL},   0, 0, S_BLUESPRINGBALL4}, // S_BLUESPRINGBALL3
+	{SPR_BSPB, 2,  1, {NULL},   0, 0, S_BLUESPRINGBALL5}, // S_BLUESPRINGBALL4
+	{SPR_BSPB, 1,  1, {NULL},   0, 0, S_BLUESPRINGBALL},  // S_BLUESPRINGBALL5
+
 	// Yellow spring on a ball
 	{SPR_YSPB, 0, -1, {NULL},   0, 0, S_NULL},              // S_YELLOWSPRINGBALL
 	{SPR_YSPB, 4,  4, {A_Pain}, 0, 0, S_YELLOWSPRINGBALL3}, // S_YELLOWSPRINGBALL2
@@ -4070,6 +4080,33 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY|MF_SCENERY, // flags
 		S_NULL          // raisestate
 	},
+	
+	{           // MT_THOKEFFECT
+		-1,             // doomednum
+		S_THOKEFFECT,   // spawnstate
+		1000,           // spawnhealth
+		S_NULL,         // seestate
+		sfx_None,       // seesound
+		8,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_None,       // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		8,              // speed
+		32*FRACUNIT,    // radius
+		64*FRACUNIT,    // height
+		0,              // display offset
+		16,             // mass
+		0,              // damage
+		sfx_None,       // activesound
+		MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY|MF_SCENERY, // flags
+		S_NULL          // raisestate
+	},
 
 	{           // MT_PLAYER
 		-1,             // doomednum
@@ -11679,6 +11716,33 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL          // raisestate
 	},
 
+	{            // MT_BLUESPRINGBALL
+		1133,           // doomednum
+		S_BLUESPRINGBALL, // spawnstate
+		1000,           // spawnhealth
+		S_BLUESPRINGBALL2, // seestate
+		sfx_None,       // seesound
+		0,              // reactiontime
+		sfx_None,       // attacksound
+		S_NULL,         // painstate
+		0,              // painchance
+		sfx_spring,     // painsound
+		S_NULL,         // meleestate
+		S_NULL,         // missilestate
+		S_NULL,         // deathstate
+		S_NULL,         // xdeathstate
+		sfx_None,       // deathsound
+		24*FRACUNIT,    // speed
+		17*FRACUNIT,    // radius
+		34*FRACUNIT,    // height
+		1,              // display offset
+		11*FRACUNIT,    // mass
+		0,              // damage
+		sfx_mswing,     // activesound
+		MF_SCENERY|MF_SPRING|MF_NOGRAVITY, // flags
+		S_BLUESPRINGBALL2 // raisestate
+	},
+
 	{            // MT_YELLOWSPRINGBALL
 		1134,           // doomednum
 		S_YELLOWSPRINGBALL, // spawnstate
diff --git a/src/info.h b/src/info.h
index 5c7a9f3fd428579c674c7f969db44f7d71b88f07..32aff18fbd27b94eb60505cc9dd0028304f8241b 100644
--- a/src/info.h
+++ b/src/info.h
@@ -580,7 +580,8 @@ typedef enum sprite
 	SPR_NULL, // invisible object
 	SPR_UNKN,
 
-	SPR_THOK, // Thok! mobj
+	SPR_THOK, // Spin trail mobj
+	SPR_THKE, // Thok boom effect
 	SPR_PLAY,
 
 	// Enemies
@@ -797,6 +798,7 @@ typedef enum sprite
 	SPR_BMCH, // Big Mace Chain
 	SPR_SMCE, // Small Mace
 	SPR_BMCE, // Big Mace
+	SPR_BSPB, // Blue spring on a ball
 	SPR_YSPB, // Yellow spring on a ball
 	SPR_RSPB, // Red spring on a ball
 	SPR_SFBR, // Small Firebar
@@ -1181,8 +1183,9 @@ typedef enum state
 	S_XDEATHSTATE,
 	S_RAISESTATE,
 
-	// Thok
+	// Thok boom effect and spin trail
 	S_THOK,
+	S_THOKEFFECT,
 
 	// Player
 	S_PLAY_STND,
@@ -2744,6 +2747,13 @@ typedef enum state
 	S_SMALLGRABCHAIN,
 	S_BIGGRABCHAIN,
 
+	// Blue spring on a ball
+	S_BLUESPRINGBALL,
+	S_BLUESPRINGBALL2,
+	S_BLUESPRINGBALL3,
+	S_BLUESPRINGBALL4,
+	S_BLUESPRINGBALL5,
+
 	// Yellow spring on a ball
 	S_YELLOWSPRINGBALL,
 	S_YELLOWSPRINGBALL2,
@@ -4384,7 +4394,8 @@ typedef enum mobj_type
 	MT_NULL,
 	MT_UNKNOWN,
 
-	MT_THOK, // Thok! mobj
+	MT_THOK, // Spin trail mobj
+	MT_THOKEFFECT, // Thok boom effect
 	MT_PLAYER,
 	MT_TAILSOVERLAY, // c:
 	MT_METALJETFUME,
@@ -4722,6 +4733,7 @@ typedef enum mobj_type
 	MT_BIGMACE, // Big Mace
 	MT_SMALLGRABCHAIN, // Small Grab Chain
 	MT_BIGGRABCHAIN, // Big Grab Chain
+	MT_BLUESPRINGBALL, // Blue spring on a ball
 	MT_YELLOWSPRINGBALL, // Yellow spring on a ball
 	MT_REDSPRINGBALL, // Red spring on a ball
 	MT_SMALLFIREBAR, // Small Firebar
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 1d183cdec6b6a518f38ed32725ed5f19ec4a45cd..9c723353b33ab6057fea4fc5b24de2499e607c50 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -26,11 +26,11 @@
 #include "y_inter.h"
 #include "hu_stuff.h"	// HU_AddChatText
 #include "console.h"
-#include "d_netcmd.h" // IsPlayerAdmin
+#include "netcode/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 "netcode/d_clisrv.h" // CL_RemovePlayer
 #include "i_system.h" // I_GetPreciseTime, I_GetPrecisePrecision
 
 #include "lua_script.h"
@@ -213,6 +213,8 @@ static const struct {
 	{META_HUDINFO,      "hudinfo_t"},
 	{META_PATCH,        "patch_t"},
 	{META_COLORMAP,     "colormap"},
+	{META_EXTRACOLORMAP,"extracolormap_t"},
+	{META_LIGHTTABLE,   "lighttable_t"},
 	{META_CAMERA,       "camera_t"},
 
 	{META_ACTION,       "action"},
@@ -1031,6 +1033,20 @@ static int lib_pRailThinker(lua_State *L)
 	return 1;
 }
 
+static int lib_pCheckSkyHit(lua_State *L)
+{
+	mobj_t *mobj = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+	line_t *line = *((line_t **)luaL_checkudata(L, 2, META_LINE));
+	//HUDSAFE
+	INLEVEL
+	if (!mobj)
+		return LUA_ErrInvalid(L, "mobj_t");
+	if (!line)
+		return LUA_ErrInvalid(L, "line_t");
+	lua_pushboolean(L, P_CheckSkyHit(mobj, line));
+	return 1;
+}
+
 static int lib_pXYMovement(lua_State *L)
 {
 	mobj_t *actor = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
@@ -1425,6 +1441,18 @@ static int lib_pGivePlayerRings(lua_State *L)
 	return 0;
 }
 
+static int lib_pGivePlayerSpheres(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	INT32 num_spheres = (INT32)luaL_checkinteger(L, 2);
+	NOHUD
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	P_GivePlayerSpheres(player, num_spheres);
+	return 0;
+}
+
 static int lib_pGivePlayerLives(lua_State *L)
 {
 	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
@@ -1567,6 +1595,19 @@ static int lib_pInstaThrust(lua_State *L)
 	return 0;
 }
 
+static int lib_pInstaThrustEvenIn2D(lua_State *L)
+{
+	mobj_t *mo = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+	angle_t angle = luaL_checkangle(L, 2);
+	fixed_t move = luaL_checkfixed(L, 3);
+	NOHUD
+	INLEVEL
+	if (!mo)
+		return LUA_ErrInvalid(L, "mobj_t");
+	P_InstaThrustEvenIn2D(mo, angle, move);
+	return 0;
+}
+
 static int lib_pReturnThrustX(lua_State *L)
 {
 	angle_t angle;
@@ -1647,11 +1688,12 @@ static int lib_pHomingAttack(lua_State *L)
 static int lib_pSuperReady(lua_State *L)
 {
 	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	boolean transform = (boolean)lua_opttrueboolean(L, 2);
 	//HUDSAFE
 	INLEVEL
 	if (!player)
 		return LUA_ErrInvalid(L, "player_t");
-	lua_pushboolean(L, P_SuperReady(player));
+	lua_pushboolean(L, P_SuperReady(player, transform));
 	return 1;
 }
 
@@ -1667,6 +1709,17 @@ static int lib_pDoJump(lua_State *L)
 	return 0;
 }
 
+static int lib_pDoSpinDashDust(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	NOHUD
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	P_DoSpinDashDust(player);
+	return 0;
+}
+
 static int lib_pSpawnThokMobj(lua_State *L)
 {
 	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
@@ -1717,6 +1770,48 @@ static int lib_pSwitchShield(lua_State *L)
 	return 0;
 }
 
+static int lib_pDoTailsOverlay(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	mobj_t *tails = *((mobj_t **)luaL_checkudata(L, 2, META_MOBJ));
+	NOHUD
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	if (!tails)
+		return LUA_ErrInvalid(L, "mobj_t");
+	P_DoTailsOverlay(player, tails);
+	return 0;
+}
+
+static int lib_pDoMetalJetFume(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	mobj_t *fume = *((mobj_t **)luaL_checkudata(L, 2, META_MOBJ));
+	NOHUD
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	if (!fume)
+		return LUA_ErrInvalid(L, "mobj_t");
+	P_DoMetalJetFume(player, fume);
+	return 0;
+}
+
+static int lib_pDoFollowMobj(lua_State *L)
+{
+	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
+	mobj_t *followmobj = *((mobj_t **)luaL_checkudata(L, 2, META_MOBJ));
+	NOHUD
+	INLEVEL
+	if (!player)
+		return LUA_ErrInvalid(L, "player_t");
+	if (!followmobj)
+		return LUA_ErrInvalid(L, "mobj_t");
+	P_DoFollowMobj(player, followmobj);
+	return 0;
+}
+
 static int lib_pPlayerCanEnterSpinGaps(lua_State *L)
 {
 	player_t *player = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
@@ -1788,6 +1883,7 @@ static int lib_pMove(lua_State *L)
 	return 2;
 }
 
+// TODO: 2.3: Delete
 static int lib_pTeleportMove(lua_State *L)
 {
 	mobj_t *ptmthing = tmthing;
@@ -1928,6 +2024,30 @@ static int lib_pCeilingzAtPos(lua_State *L)
 	return 1;
 }
 
+static int lib_pGetSectorColormapAt(lua_State *L)
+{
+	boolean has_sector = false;
+	sector_t *sector = NULL;
+	if (!lua_isnoneornil(L, 1))
+	{
+		has_sector = true;
+		sector = *((sector_t **)luaL_checkudata(L, 1, META_SECTOR));
+	}
+	fixed_t x = luaL_checkfixed(L, 2);
+	fixed_t y = luaL_checkfixed(L, 3);
+	fixed_t z = luaL_checkfixed(L, 4);
+	INLEVEL
+	if (has_sector && !sector)
+		return LUA_ErrInvalid(L, "sector_t");
+	extracolormap_t *exc;
+	if (sector)
+		exc = P_GetColormapFromSectorAt(sector, x, y, z);
+	else
+		exc = P_GetSectorColormapAt(x, y, z);
+	LUA_PushUserdata(L, exc, META_EXTRACOLORMAP);
+	return 1;
+}
+
 static int lib_pDoSpring(lua_State *L)
 {
 	mobj_t *spring = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
@@ -2198,6 +2318,21 @@ static int lib_pDoMatchSuper(lua_State *L)
 	return 0;
 }
 
+static int lib_pTouchSpecialThing(lua_State *L)
+{
+	mobj_t *special = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+	mobj_t *toucher = *((mobj_t **)luaL_checkudata(L, 2, META_MOBJ));
+	boolean heightcheck = lua_optboolean(L, 3);
+	NOHUD
+	INLEVEL
+	if (!special || !toucher)
+		return LUA_ErrInvalid(L, "mobj_t");
+	if (!toucher->player)
+		return luaL_error(L, "P_TouchSpecialThing requires a valid toucher.player.");
+	P_TouchSpecialThing(special, toucher, heightcheck);
+	return 0;
+}
+
 // P_SPEC
 ////////////
 
@@ -2214,6 +2349,40 @@ static int lib_pThrust(lua_State *L)
 	return 0;
 }
 
+static int lib_pThrustEvenIn2D(lua_State *L)
+{
+	mobj_t *mo = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
+	angle_t angle = luaL_checkangle(L, 2);
+	fixed_t move = luaL_checkfixed(L, 3);
+	NOHUD
+	INLEVEL
+	if (!mo)
+		return LUA_ErrInvalid(L, "mobj_t");
+	P_ThrustEvenIn2D(mo, angle, move);
+	return 0;
+}
+
+static int lib_pVectorInstaThrust(lua_State *L)
+{
+    fixed_t xa = luaL_checkfixed(L, 1);
+    fixed_t xb = luaL_checkfixed(L, 2);
+    fixed_t xc = luaL_checkfixed(L, 3);
+    fixed_t ya = luaL_checkfixed(L, 4);
+    fixed_t yb = luaL_checkfixed(L, 5);
+    fixed_t yc = luaL_checkfixed(L, 6);
+    fixed_t za = luaL_checkfixed(L, 7);
+    fixed_t zb = luaL_checkfixed(L, 8);
+    fixed_t zc = luaL_checkfixed(L, 9);
+    fixed_t momentum = luaL_checkfixed(L, 10);
+	mobj_t *mo = *((mobj_t **)luaL_checkudata(L, 11, META_MOBJ));
+	NOHUD
+	INLEVEL
+	if (!mo)
+		return LUA_ErrInvalid(L, "mobj_t");
+	P_VectorInstaThrust(xa, xb, xc, ya, yb, yc, za, zb, zc, momentum, mo);
+	return 0;
+}
+
 static int lib_pSetMobjStateNF(lua_State *L)
 {
 	mobj_t *mobj = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
@@ -2266,6 +2435,7 @@ static int lib_pMobjTouchingSectorSpecial(lua_State *L)
 	return 1;
 }
 
+// TODO: 2.3: Delete
 static int lib_pThingOnSpecial3DFloor(lua_State *L)
 {
 	mobj_t *mo = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
@@ -3570,6 +3740,7 @@ static int lib_gAddPlayer(lua_State *L)
 
 	newplayer->jointime = 0;
 	newplayer->quittime = 0;
+	newplayer->lastinputtime = 0;
 
 	// Read the skin argument (defaults to Sonic)
 	if (!lua_isnoneornil(L, 1))
@@ -4079,6 +4250,7 @@ static luaL_Reg lib[] = {
 	{"P_CreateFloorSpriteSlope",lib_pCreateFloorSpriteSlope},
 	{"P_RemoveFloorSpriteSlope",lib_pRemoveFloorSpriteSlope},
 	{"P_RailThinker",lib_pRailThinker},
+	{"P_CheckSkyHit",lib_pCheckSkyHit},
 	{"P_XYMovement",lib_pXYMovement},
 	{"P_RingXYMovement",lib_pRingXYMovement},
 	{"P_SceneryXYMovement",lib_pSceneryXYMovement},
@@ -4111,6 +4283,7 @@ static luaL_Reg lib[] = {
 	{"P_SpawnShieldOrb",lib_pSpawnShieldOrb},
 	{"P_SpawnGhostMobj",lib_pSpawnGhostMobj},
 	{"P_GivePlayerRings",lib_pGivePlayerRings},
+	{"P_GivePlayerSpheres",lib_pGivePlayerSpheres},
 	{"P_GivePlayerLives",lib_pGivePlayerLives},
 	{"P_GiveCoopLives",lib_pGiveCoopLives},
 	{"P_ResetScore",lib_pResetScore},
@@ -4123,6 +4296,7 @@ static luaL_Reg lib[] = {
 	{"P_DoPlayerFinish",lib_pDoPlayerFinish},
 	{"P_DoPlayerExit",lib_pDoPlayerExit},
 	{"P_InstaThrust",lib_pInstaThrust},
+	{"P_InstaThrustEvenIn2D",lib_pInstaThrustEvenIn2D},
 	{"P_ReturnThrustX",lib_pReturnThrustX},
 	{"P_ReturnThrustY",lib_pReturnThrustY},
 	{"P_LookForEnemies",lib_pLookForEnemies},
@@ -4131,10 +4305,14 @@ static luaL_Reg lib[] = {
 	{"P_HomingAttack",lib_pHomingAttack},
 	{"P_SuperReady",lib_pSuperReady},
 	{"P_DoJump",lib_pDoJump},
+	{"P_DoSpinDashDust",lib_pDoSpinDashDust},
 	{"P_SpawnThokMobj",lib_pSpawnThokMobj},
 	{"P_SpawnSpinMobj",lib_pSpawnSpinMobj},
 	{"P_Telekinesis",lib_pTelekinesis},
 	{"P_SwitchShield",lib_pSwitchShield},
+	{"P_DoTailsOverlay",lib_pDoTailsOverlay},
+	{"P_DoMetalJetFume",lib_pDoMetalJetFume},
+	{"P_DoFollowMobj",lib_pDoFollowMobj},
 	{"P_PlayerCanEnterSpinGaps",lib_pPlayerCanEnterSpinGaps},
 	{"P_PlayerShouldUseSpinHeight",lib_pPlayerShouldUseSpinHeight},
 
@@ -4152,7 +4330,9 @@ static luaL_Reg lib[] = {
 	{"P_RadiusAttack",lib_pRadiusAttack},
 	{"P_FloorzAtPos",lib_pFloorzAtPos},
 	{"P_CeilingzAtPos",lib_pCeilingzAtPos},
+	{"P_GetSectorColormapAt",lib_pGetSectorColormapAt},
 	{"P_DoSpring",lib_pDoSpring},
+	{"P_TouchSpecialThing",lib_pTouchSpecialThing},
 	{"P_TryCameraMove", lib_pTryCameraMove},
 	{"P_TeleportCameraMove", lib_pTeleportCameraMove},
 
@@ -4176,6 +4356,8 @@ static luaL_Reg lib[] = {
 
 	// p_spec
 	{"P_Thrust",lib_pThrust},
+	{"P_ThrustEvenIn2D",lib_pThrustEvenIn2D},
+	{"P_VectorInstaThrust",lib_pVectorInstaThrust},
 	{"P_SetMobjStateNF",lib_pSetMobjStateNF},
 	{"P_DoSuperTransformation",lib_pDoSuperTransformation},
 	{"P_ExplodeMissile",lib_pExplodeMissile},
@@ -4300,8 +4482,7 @@ int LUA_BaseLib(lua_State *L)
 	// Set metatable for string
 	lua_pushliteral(L, "");  // dummy string
 	lua_getmetatable(L, -1);  // get string metatable
-	lua_pushcfunction(L,lib_concat); // push concatination function
-	lua_setfield(L,-2,"__add"); // ... store it as mathematical addition
+	LUA_SetCFunctionField(L, "__add", lib_concat);
 	lua_pop(L, 2); // pop metatable and dummy string
 
 	lua_newtable(L);
diff --git a/src/lua_colorlib.c b/src/lua_colorlib.c
new file mode 100644
index 0000000000000000000000000000000000000000..1e6e82333f18f28421a651728afd42ffc6d387f5
--- /dev/null
+++ b/src/lua_colorlib.c
@@ -0,0 +1,332 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2021-2022 by "Lactozilla".
+// Copyright (C) 2014-2023 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_colorlib.c
+/// \brief color and colormap libraries for Lua scripting
+
+#include "doomdef.h"
+#include "fastcmp.h"
+#include "r_data.h"
+
+#include "lua_script.h"
+#include "lua_libs.h"
+
+#define IS_HEX_CHAR(x) ((x >= '0' && x <= '9') || (x >= 'a' && x <= 'f') || (x >= 'A' && x <= 'F'))
+#define ARE_HEX_CHARS(str, i) IS_HEX_CHAR(str[i]) && IS_HEX_CHAR(str[i + 1])
+
+static UINT32 hex2int(char x)
+{
+	if (x >= '0' && x <= '9')
+		return x - '0';
+	else if (x >= 'a' && x <= 'f')
+		return x - 'a' + 10;
+	else if (x >= 'A' && x <= 'F')
+		return x - 'A' + 10;
+
+	return 0;
+}
+
+static UINT8 ParseHTMLColor(const char *str, UINT8 *rgba, size_t numc)
+{
+	const char *hex = str;
+
+	if (hex[0] == '#')
+		hex++;
+	else if (!IS_HEX_CHAR(hex[0]))
+		return 0;
+
+	size_t len = strlen(hex);
+
+	if (len == 3)
+	{
+		// Shorthand like #09C
+		for (unsigned i = 0; i < 3; i++)
+		{
+			if (!IS_HEX_CHAR(hex[i]))
+				return 0;
+
+			UINT32 hx = hex2int(hex[i]);
+			*rgba++ = (hx * 16) + hx;
+		}
+
+		return 3;
+	}
+	else if (len == 6 || len == 8)
+	{
+		if (numc != 4)
+			len = 6;
+
+		// A triplet like #0099CC
+		for (unsigned i = 0; i < len; i += 2)
+		{
+			if (!ARE_HEX_CHARS(hex, i))
+				return false;
+
+			*rgba++ = (hex2int(hex[i]) * 16) + hex2int(hex[i + 1]);
+		}
+
+		return len;
+	}
+
+	return 0;
+}
+
+/////////////////////////
+// extracolormap userdata
+/////////////////////////
+
+enum extracolormap_e {
+	extracolormap_red = 0,
+	extracolormap_green,
+	extracolormap_blue,
+	extracolormap_alpha,
+	extracolormap_color,
+	extracolormap_fade_red,
+	extracolormap_fade_green,
+	extracolormap_fade_blue,
+	extracolormap_fade_alpha,
+	extracolormap_fade_color,
+	extracolormap_fade_start,
+	extracolormap_fade_end,
+	extracolormap_colormap
+};
+
+static const char *const extracolormap_opt[] = {
+	"red",
+	"green",
+	"blue",
+	"alpha",
+	"color",
+	"fade_red",
+	"fade_green",
+	"fade_blue",
+	"fade_alpha",
+	"fade_color",
+	"fade_start",
+	"fade_end",
+	"colormap",
+	NULL};
+
+static int extracolormap_get(lua_State *L)
+{
+	extracolormap_t *exc = *((extracolormap_t **)luaL_checkudata(L, 1, META_EXTRACOLORMAP));
+	enum extracolormap_e field = luaL_checkoption(L, 2, NULL, extracolormap_opt);
+
+	switch (field)
+	{
+	case extracolormap_red:
+		lua_pushinteger(L, R_GetRgbaR(exc->rgba));
+		break;
+	case extracolormap_green:
+		lua_pushinteger(L, R_GetRgbaG(exc->rgba));
+		break;
+	case extracolormap_blue:
+		lua_pushinteger(L, R_GetRgbaB(exc->rgba));
+		break;
+	case extracolormap_alpha:
+		lua_pushinteger(L, R_GetRgbaA(exc->rgba));
+		break;
+	case extracolormap_color:
+		lua_pushinteger(L, R_GetRgbaR(exc->rgba));
+		lua_pushinteger(L, R_GetRgbaG(exc->rgba));
+		lua_pushinteger(L, R_GetRgbaB(exc->rgba));
+		lua_pushinteger(L, R_GetRgbaA(exc->rgba));
+		return 4;
+	case extracolormap_fade_red:
+		lua_pushinteger(L, R_GetRgbaR(exc->fadergba));
+		break;
+	case extracolormap_fade_green:
+		lua_pushinteger(L, R_GetRgbaG(exc->fadergba));
+		break;
+	case extracolormap_fade_blue:
+		lua_pushinteger(L, R_GetRgbaB(exc->fadergba));
+		break;
+	case extracolormap_fade_alpha:
+		lua_pushinteger(L, R_GetRgbaA(exc->fadergba));
+		break;
+	case extracolormap_fade_color:
+		lua_pushinteger(L, R_GetRgbaR(exc->fadergba));
+		lua_pushinteger(L, R_GetRgbaG(exc->fadergba));
+		lua_pushinteger(L, R_GetRgbaB(exc->fadergba));
+		lua_pushinteger(L, R_GetRgbaA(exc->fadergba));
+		return 4;
+	case extracolormap_fade_start:
+		lua_pushinteger(L, exc->fadestart);
+		break;
+	case extracolormap_fade_end:
+		lua_pushinteger(L, exc->fadeend);
+		break;
+	case extracolormap_colormap:
+		LUA_PushUserdata(L, exc->colormap, META_LIGHTTABLE);
+		break;
+	}
+	return 1;
+}
+
+static void GetExtraColormapRGBA(lua_State *L, UINT8 *rgba, int arg)
+{
+	if (lua_type(L, arg) == LUA_TSTRING)
+	{
+		const char *str = lua_tostring(L, arg);
+		UINT8 parsed = ParseHTMLColor(str, rgba, 4);
+		if (!parsed)
+			luaL_error(L, "Malformed HTML color '%s'", str);
+	}
+	else
+	{
+		UINT32 colors = lua_tointeger(L, arg);
+		if (colors > 0xFFFFFF)
+		{
+			rgba[0] = (colors >> 24) & 0xFF;
+			rgba[1] = (colors >> 16) & 0xFF;
+			rgba[2] = (colors >> 8) & 0xFF;
+			rgba[3] = colors & 0xFF;
+		}
+		else
+		{
+			rgba[0] = (colors >> 16) & 0xFF;
+			rgba[1] = (colors >> 8) & 0xFF;
+			rgba[2] = colors & 0xFF;
+			rgba[3] = 0xFF;
+		}
+	}
+}
+
+static int extracolormap_set(lua_State *L)
+{
+	extracolormap_t *exc = *((extracolormap_t **)luaL_checkudata(L, 1, META_EXTRACOLORMAP));
+	enum extracolormap_e field = luaL_checkoption(L, 2, NULL, extracolormap_opt);
+
+	UINT8 r = R_GetRgbaR(exc->rgba);
+	UINT8 g = R_GetRgbaG(exc->rgba);
+	UINT8 b = R_GetRgbaB(exc->rgba);
+	UINT8 a = R_GetRgbaA(exc->rgba);
+
+	UINT8 fr = R_GetRgbaR(exc->fadergba);
+	UINT8 fg = R_GetRgbaG(exc->fadergba);
+	UINT8 fb = R_GetRgbaB(exc->fadergba);
+	UINT8 fa = R_GetRgbaA(exc->fadergba);
+
+	UINT8 rgba[4];
+
+	INT32 old_rgba = exc->rgba, old_fade_rgba = exc->fadergba; // It's not unsigned?
+	UINT8 old_fade_start = exc->fadestart, old_fade_end = exc->fadeend;
+
+#define val luaL_checkinteger(L, 3)
+
+	switch(field)
+	{
+	case extracolormap_red:
+		exc->rgba = R_PutRgbaRGBA(val, g, b, a);
+		break;
+	case extracolormap_green:
+		exc->rgba = R_PutRgbaRGBA(r, val, b, a);
+		break;
+	case extracolormap_blue:
+		exc->rgba = R_PutRgbaRGBA(r, g, val, a);
+		break;
+	case extracolormap_alpha:
+		exc->rgba = R_PutRgbaRGBA(r, g, b, val);
+		break;
+	case extracolormap_color:
+		rgba[0] = r;
+		rgba[1] = g;
+		rgba[2] = b;
+		rgba[3] = a;
+		GetExtraColormapRGBA(L, rgba, 3);
+		exc->rgba = R_PutRgbaRGBA(rgba[0], rgba[1], rgba[2], rgba[3]);
+		break;
+	case extracolormap_fade_red:
+		exc->fadergba = R_PutRgbaRGBA(val, fg, fb, fa);
+		break;
+	case extracolormap_fade_green:
+		exc->fadergba = R_PutRgbaRGBA(fr, val, fb, fa);
+		break;
+	case extracolormap_fade_blue:
+		exc->fadergba = R_PutRgbaRGBA(fr, fg, val, fa);
+		break;
+	case extracolormap_fade_alpha:
+		exc->fadergba = R_PutRgbaRGBA(fr, fg, fb, val);
+		break;
+	case extracolormap_fade_color:
+		rgba[0] = fr;
+		rgba[1] = fg;
+		rgba[2] = fb;
+		rgba[3] = fa;
+		GetExtraColormapRGBA(L, rgba, 3);
+		exc->fadergba = R_PutRgbaRGBA(rgba[0], rgba[1], rgba[2], rgba[3]);
+		break;
+	case extracolormap_fade_start:
+		if (val > 31)
+			return luaL_error(L, "fade start %d out of range (0 - 31)", val);
+		exc->fadestart = val;
+		break;
+	case extracolormap_fade_end:
+		if (val > 31)
+			return luaL_error(L, "fade end %d out of range (0 - 31)", val);
+		exc->fadeend = val;
+		break;
+	case extracolormap_colormap:
+		return luaL_error(L, LUA_QL("extracolormap_t") " field " LUA_QS " should not be set directly.", extracolormap_opt[field]);
+	}
+
+#undef val
+
+	if (exc->rgba != old_rgba
+		|| exc->fadergba != old_fade_rgba
+		|| exc->fadestart != old_fade_start
+		|| exc->fadeend != old_fade_end)
+	R_GenerateLightTable(exc, true);
+
+	return 0;
+}
+
+static int lighttable_get(lua_State *L)
+{
+	void **userdata;
+
+	lighttable_t *table = *((lighttable_t **)luaL_checkudata(L, 1, META_LIGHTTABLE));
+	UINT32 row = luaL_checkinteger(L, 2);
+	if (row < 1 || row > 34)
+		return luaL_error(L, "lighttable row %d out of range (1 - %d)", row, 34);
+
+	userdata = lua_newuserdata(L, sizeof(void *));
+	*userdata = &table[256 * (row - 1)];
+	luaL_getmetatable(L, META_COLORMAP);
+	lua_setmetatable(L, -2);
+
+	return 1;
+}
+
+static int lighttable_len(lua_State *L)
+{
+	lua_pushinteger(L, NUM_PALETTE_ENTRIES);
+	return 1;
+}
+
+int LUA_ColorLib(lua_State *L)
+{
+	luaL_newmetatable(L, META_EXTRACOLORMAP);
+		lua_pushcfunction(L, extracolormap_get);
+		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, extracolormap_set);
+		lua_setfield(L, -2, "__newindex");
+	lua_pop(L, 1);
+
+	luaL_newmetatable(L, META_LIGHTTABLE);
+		lua_pushcfunction(L, lighttable_get);
+		lua_setfield(L, -2, "__index");
+
+		lua_pushcfunction(L, lighttable_len);
+		lua_setfield(L, -2, "__len");
+	lua_pop(L, 1);
+
+	return 0;
+}
diff --git a/src/lua_consolelib.c b/src/lua_consolelib.c
index 3783b8f7b6dac036a2e169c807e8e1079f130c76..1fabe2c00ae17bbd505b576635d7ef7c47a7fd94 100644
--- a/src/lua_consolelib.c
+++ b/src/lua_consolelib.c
@@ -16,6 +16,7 @@
 #include "g_game.h"
 #include "byteptr.h"
 #include "z_zone.h"
+#include "netcode/net_command.h"
 
 #include "lua_script.h"
 #include "lua_libs.h"
@@ -193,6 +194,7 @@ static int lib_comAddCommand(lua_State *L)
 	if (lua_gettop(L) >= 3)
 	{ // For the third argument, only take a boolean or a number.
 		lua_settop(L, 3);
+		// TODO: 2.3: Remove boolean option
 		if (lua_type(L, 3) == LUA_TBOOLEAN)
 		{
 			CONS_Alert(CONS_WARNING,
@@ -373,6 +375,9 @@ static int lib_cvRegisterVar(lua_State *L)
 				size_t count = 0;
 				CV_PossibleValue_t *cvpv;
 
+				const char * const MINMAX[2] = {"MIN", "MAX"};
+				int minmax_unset = 3;
+
 				lua_pushnil(L);
 				while (lua_next(L, 4))
 				{
@@ -391,16 +396,45 @@ static int lib_cvRegisterVar(lua_State *L)
 				lua_pushnil(L);
 				while (lua_next(L, 4))
 				{
+					INT32 n;
+					const char * strval;
+
 					// stack: [...] PossibleValue table, index, value
 					//                       4             5      6
 					if (lua_type(L, 5) != LUA_TSTRING
 					|| lua_type(L, 6) != LUA_TNUMBER)
 						FIELDERROR("PossibleValue", "custom PossibleValue table requires a format of string=integer, i.e. {MIN=0, MAX=9999}");
-					cvpv[i].strvalue = Z_StrDup(lua_tostring(L, 5));
-					cvpv[i].value = (INT32)lua_tonumber(L, 6);
-					i++;
+
+					strval = lua_tostring(L, 5);
+
+					if (
+							stricmp(strval, MINMAX[n=0]) == 0 ||
+							stricmp(strval, MINMAX[n=1]) == 0
+					){
+						/* need to shift forward */
+						if (minmax_unset == 3)
+						{
+							memmove(&cvpv[2], &cvpv[0],
+									i * sizeof *cvpv);
+							i += 2;
+						}
+						cvpv[n].strvalue = MINMAX[n];
+						minmax_unset &= ~(1 << n);
+					}
+					else
+					{
+						n = i++;
+						cvpv[n].strvalue = Z_StrDup(strval);
+					}
+
+					cvpv[n].value = (INT32)lua_tonumber(L, 6);
+
 					lua_pop(L, 1);
 				}
+
+				if (minmax_unset && minmax_unset != 3)
+					FIELDERROR("PossibleValue", "custom PossibleValue table requires requires both MIN and MAX keys if one is present");
+
 				cvpv[i].value = 0;
 				cvpv[i].strvalue = NULL;
 				cvar->PossibleValue = cvpv;
@@ -625,10 +659,7 @@ static int cvar_get(lua_State *L)
 int LUA_ConsoleLib(lua_State *L)
 {
 	// Metatable for consvar_t
-	luaL_newmetatable(L, META_CVAR);
-		lua_pushcfunction(L, cvar_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L,1);
+	LUA_RegisterUserdataMetatable(L, META_CVAR, cvar_get, NULL, NULL);
 
 	cvar_fields_ref = Lua_CreateFieldTable(L, cvar_opt);
 
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 039a9677f421dad1ffc4fac29a2335c88c866afa..38815a06cfa4558b8ad5659bcbe1cdba5705ca03 100644
--- a/src/lua_hooklib.c
+++ b/src/lua_hooklib.c
@@ -24,7 +24,7 @@
 #include "lua_hud.h" // hud_running errors
 
 #include "m_perfstats.h"
-#include "d_netcmd.h" // for cv_perfstats
+#include "netcode/d_netcmd.h" // for cv_perfstats
 #include "i_system.h" // I_GetPreciseTime
 
 /* =========================================================================
@@ -76,12 +76,12 @@ static boolean mobj_hook_available(int hook_type, mobjtype_t mobj_type)
 		);
 }
 
-static int hook_in_list
+static unsigned hook_in_list
 (
 		const char * const         name,
 		const char * const * const list
 ){
-	int type;
+	unsigned type;
 
 	for (type = 0; list[type] != NULL; ++type)
 	{
@@ -200,7 +200,7 @@ static void add_hook_ref(lua_State *L, int idx)
 static int lib_addHook(lua_State *L)
 {
 	const char * name;
-	int type;
+	unsigned type;
 
 	if (!lua_lumploading)
 		return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index 6eec91273352db4adc598a80b7e0138795589640..2e6721a3a25d505126548a149dc63c3c9d889b31 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -517,7 +517,7 @@ static int libd_getSpritePatch(lua_State *L)
 		INT32 rot = R_GetRollAngle(rollangle);
 
 		if (rot) {
-			patch_t *rotsprite = Patch_GetRotatedSprite(sprframe, frame, angle, sprframe->flip & (1<<angle), true, &spriteinfo[i], rot);
+			patch_t *rotsprite = Patch_GetRotatedSprite(sprframe, frame, angle, sprframe->flip & (1<<angle), &spriteinfo[i], rot);
 			LUA_PushUserdata(L, rotsprite, META_PATCH);
 			lua_pushboolean(L, false);
 			lua_pushboolean(L, true);
@@ -629,7 +629,7 @@ static int libd_getSprite2Patch(lua_State *L)
 		INT32 rot = R_GetRollAngle(rollangle);
 
 		if (rot) {
-			patch_t *rotsprite = Patch_GetRotatedSprite(sprframe, frame, angle, sprframe->flip & (1<<angle), true, &skins[i].sprinfo[j], rot);
+			patch_t *rotsprite = Patch_GetRotatedSprite(sprframe, frame, angle, sprframe->flip & (1<<angle), &skins[i].sprinfo[j], rot);
 			LUA_PushUserdata(L, rotsprite, META_PATCH);
 			lua_pushboolean(L, false);
 			lua_pushboolean(L, true);
@@ -1206,19 +1206,11 @@ static int libd_height(lua_State *L)
 	return 1;
 }
 
-static int libd_dupx(lua_State *L)
+static int libd_dup(lua_State *L)
 {
 	HUDONLY
-	lua_pushinteger(L, vid.dupx); // push integral scale (patch scale)
-	lua_pushfixed(L, vid.fdupx); // push fixed point scale (position scale)
-	return 2;
-}
-
-static int libd_dupy(lua_State *L)
-{
-	HUDONLY
-	lua_pushinteger(L, vid.dupy); // push integral scale (patch scale)
-	lua_pushfixed(L, vid.fdupy); // push fixed point scale (position scale)
+	lua_pushinteger(L, vid.dup); // push integral scale (patch scale)
+	lua_pushfixed(L, vid.fdup); // push fixed point scale (position scale)
 	return 2;
 }
 
@@ -1338,8 +1330,8 @@ static luaL_Reg lib_draw[] = {
 	// properties
 	{"width", libd_width},
 	{"height", libd_height},
-	{"dupx", libd_dupx},
-	{"dupy", libd_dupy},
+	{"dupx", libd_dup},
+	{"dupy", libd_dup},
 	{"renderer", libd_renderer},
 	{"localTransFlag", libd_getlocaltransflag},
 	{"userTransFlag", libd_getusertransflag},
@@ -1404,52 +1396,16 @@ int LUA_HudLib(lua_State *L)
 	luaL_register(L, NULL, lib_draw);
 	lib_draw_ref = luaL_ref(L, LUA_REGISTRYINDEX);
 
-	luaL_newmetatable(L, META_HUDINFO);
-		lua_pushcfunction(L, hudinfo_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, hudinfo_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, hudinfo_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getHudInfo);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_hudinfolen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "hudinfo");
-
-	luaL_newmetatable(L, META_COLORMAP);
-		lua_pushcfunction(L, colormap_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L,1);
-
-	luaL_newmetatable(L, META_PATCH);
-		lua_pushcfunction(L, patch_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, patch_set);
-		lua_setfield(L, -2, "__newindex");
-	lua_pop(L,1);
+	LUA_RegisterUserdataMetatable(L, META_HUDINFO, hudinfo_get, hudinfo_set, hudinfo_num);
+	LUA_RegisterUserdataMetatable(L, META_COLORMAP, colormap_get, NULL, NULL);
+	LUA_RegisterUserdataMetatable(L, META_PATCH, patch_get, patch_set, NULL);
+	LUA_RegisterUserdataMetatable(L, META_CAMERA, camera_get, camera_set, NULL);
 
 	patch_fields_ref = Lua_CreateFieldTable(L, patch_opt);
-
-	luaL_newmetatable(L, META_CAMERA);
-		lua_pushcfunction(L, camera_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, camera_set);
-		lua_setfield(L, -2, "__newindex");
-	lua_pop(L,1);
-
 	camera_fields_ref = Lua_CreateFieldTable(L, camera_opt);
 
+	LUA_RegisterGlobalUserdata(L, "hudinfo", lib_getHudInfo, NULL, lib_hudinfolen);
+
 	luaL_register(L, "hud", lib_hud);
 	return 0;
 }
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index 3764acf6a40945489506ce8d94b1b976fa771752..ed78811ce4867642e660dd433c3153958e68797a 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -318,6 +318,7 @@ static int PopPivotSubTable(spriteframepivot_t *pivot, lua_State *L, int stk, in
 					pivot[idx].x = (INT32)value;
 				else if (ikey == 2 || (key && fastcmp(key, "y")))
 					pivot[idx].y = (INT32)value;
+				// TODO: 2.3: Delete
 				else if (ikey == 3 || (key && fastcmp(key, "rotaxis")))
 					LUA_UsageWarning(L, "\"rotaxis\" is deprecated and will be removed.")
 				else if (ikey == -1 && (key != NULL))
@@ -571,6 +572,7 @@ static int framepivot_get(lua_State *L)
 		lua_pushinteger(L, framepivot->x);
 	else if (fastcmp("y", field))
 		lua_pushinteger(L, framepivot->y);
+	// TODO: 2.3: Delete
 	else if (fastcmp("rotaxis", field))
 	{
 		LUA_UsageWarning(L, "\"rotaxis\" is deprecated and will be removed.");
@@ -600,6 +602,7 @@ static int framepivot_set(lua_State *L)
 		framepivot->x = luaL_checkinteger(L, 3);
 	else if (fastcmp("y", field))
 		framepivot->y = luaL_checkinteger(L, 3);
+	// TODO: 2.3: delete
 	else if (fastcmp("rotaxis", field))
 		LUA_UsageWarning(L, "\"rotaxis\" is deprecated and will be removed.")
 	else
@@ -1914,206 +1917,28 @@ int LUA_InfoLib(lua_State *L)
 	lua_newtable(L);
 	lua_setfield(L, LUA_REGISTRYINDEX, LREG_ACTIONS);
 
-	luaL_newmetatable(L, META_STATE);
-		lua_pushcfunction(L, state_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, state_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, state_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_MOBJINFO);
-		lua_pushcfunction(L, mobjinfo_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, mobjinfo_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, mobjinfo_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
+	LUA_RegisterUserdataMetatable(L, META_STATE, state_get, state_set, state_num);
+	LUA_RegisterUserdataMetatable(L, META_MOBJINFO, mobjinfo_get, mobjinfo_set, mobjinfo_num);
+	LUA_RegisterUserdataMetatable(L, META_SKINCOLOR, skincolor_get, skincolor_set, skincolor_num);
+	LUA_RegisterUserdataMetatable(L, META_COLORRAMP, colorramp_get, colorramp_set, colorramp_len);
+	LUA_RegisterUserdataMetatable(L, META_SFXINFO, sfxinfo_get, sfxinfo_set, sfxinfo_num);
+	LUA_RegisterUserdataMetatable(L, META_SPRITEINFO, spriteinfo_get, spriteinfo_set, spriteinfo_num);
+	LUA_RegisterUserdataMetatable(L, META_PIVOTLIST, pivotlist_get, pivotlist_set, pivotlist_num);
+	LUA_RegisterUserdataMetatable(L, META_FRAMEPIVOT, framepivot_get, framepivot_set, framepivot_num);
+	LUA_RegisterUserdataMetatable(L, META_LUABANKS, lib_getluabanks, lib_setluabanks, lib_luabankslen);
 
 	mobjinfo_fields_ref = Lua_CreateFieldTable(L, mobjinfo_opt);
 
-	luaL_newmetatable(L, META_SKINCOLOR);
-		lua_pushcfunction(L, skincolor_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, skincolor_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, skincolor_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_COLORRAMP);
-		lua_pushcfunction(L, colorramp_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, colorramp_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, colorramp_len);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
-
-	luaL_newmetatable(L, META_SFXINFO);
-		lua_pushcfunction(L, sfxinfo_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, sfxinfo_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, sfxinfo_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_SPRITEINFO);
-		lua_pushcfunction(L, spriteinfo_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, spriteinfo_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, spriteinfo_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_PIVOTLIST);
-		lua_pushcfunction(L, pivotlist_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, pivotlist_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, pivotlist_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_FRAMEPIVOT);
-		lua_pushcfunction(L, framepivot_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, framepivot_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, framepivot_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSprname);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_sprnamelen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "sprnames");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSpr2name);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_spr2namelen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "spr2names");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSpr2default);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setSpr2default);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_spr2namelen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "spr2defaults");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getState);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setState);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_statelen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "states");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getMobjInfo);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setMobjInfo);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_mobjinfolen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "mobjinfo");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSkinColor);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setSkinColor);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_skincolorslen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "skincolors");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSfxInfo);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setSfxInfo);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_sfxlen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_pushvalue(L, -1);
-	lua_setglobal(L, "S_sfx");
-	lua_setglobal(L, "sfxinfo");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSpriteInfo);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setSpriteInfo);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_spriteinfolen);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "spriteinfo");
-
-	luaL_newmetatable(L, META_LUABANKS);
-		lua_pushcfunction(L, lib_getluabanks);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, lib_setluabanks);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, lib_luabankslen);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
+	LUA_RegisterGlobalUserdata(L, "sprnames", lib_getSprname, NULL, lib_sprnamelen);
+	LUA_RegisterGlobalUserdata(L, "spr2names", lib_getSpr2name, NULL, lib_spr2namelen);
+	LUA_RegisterGlobalUserdata(L, "spr2defaults", lib_getSpr2default, lib_setSpr2default, lib_spr2namelen);
+	LUA_RegisterGlobalUserdata(L, "states", lib_getState, lib_setState, lib_statelen);
+	LUA_RegisterGlobalUserdata(L, "mobjinfo", lib_getMobjInfo, lib_setMobjInfo, lib_mobjinfolen);
+	LUA_RegisterGlobalUserdata(L, "skincolors", lib_getSkinColor, lib_setSkinColor, lib_skincolorslen);
+	LUA_RegisterGlobalUserdata(L, "spriteinfo", lib_getSpriteInfo, lib_setSpriteInfo, lib_spriteinfolen);
+	LUA_RegisterGlobalUserdata(L, "sfxinfo", lib_getSfxInfo, lib_setSfxInfo, lib_sfxlen);
+	// TODO: 2.3: Delete this alias
+	LUA_RegisterGlobalUserdata(L, "S_sfx", lib_getSfxInfo, lib_setSfxInfo, lib_sfxlen);
 
 	return 0;
 }
diff --git a/src/lua_inputlib.c b/src/lua_inputlib.c
index 1f75ee6fe1bb99cc79a9bac9b93556c3e96f43db..ef3a9011f1c47a27dc8c75b3e43dc2cd47b9a2d8 100644
--- a/src/lua_inputlib.c
+++ b/src/lua_inputlib.c
@@ -20,6 +20,7 @@
 #include "lua_libs.h"
 
 boolean mousegrabbedbylua = true;
+boolean ignoregameinputs = false;
 
 ///////////////
 // FUNCTIONS //
@@ -145,6 +146,51 @@ static luaL_Reg lib[] = {
 	{NULL, NULL}
 };
 
+///////////////
+// VARIABLES //
+///////////////
+
+static int lib_get(lua_State *L)
+{
+	const char *field = luaL_checkstring(L, 2);
+
+	if (fastcmp(field, "mouse"))
+	{
+		LUA_PushUserdata(L, &mouse, META_MOUSE);
+		return 1;
+	}
+	else if (fastcmp(field, "mouse2"))
+	{
+		LUA_PushUserdata(L, &mouse2, META_MOUSE);
+		return 1;
+	}
+	else if (fastcmp(field, "ignoregameinputs"))
+	{
+		lua_pushboolean(L, ignoregameinputs);
+		return 1;
+	}
+	else
+	{
+		return 0;
+	}
+}
+
+static int lib_set(lua_State *L)
+{
+	const char *field = luaL_checkstring(L, 2);
+
+	if (fastcmp(field, "ignoregameinputs"))
+	{
+		ignoregameinputs = luaL_checkboolean(L, 3);
+	}
+	else
+	{
+		lua_rawset(L, 1);
+	}
+
+	return 0;
+}
+
 ///////////////////
 // gamekeydown[] //
 ///////////////////
@@ -239,32 +285,18 @@ static int mouse_num(lua_State *L)
 
 int LUA_InputLib(lua_State *L)
 {
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getGameKeyDown);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_setGameKeyDown);
-			lua_setfield(L, -2, "__newindex");
-
-			lua_pushcfunction(L, lib_lenGameKeyDown);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "gamekeydown");
-
-	luaL_newmetatable(L, META_KEYEVENT);
-		lua_pushcfunction(L, keyevent_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L, 1);
+	LUA_RegisterUserdataMetatable(L, META_KEYEVENT, keyevent_get, NULL, NULL);
+	LUA_RegisterUserdataMetatable(L, META_MOUSE, mouse_get, NULL, mouse_num);
 
-	luaL_newmetatable(L, META_MOUSE);
-		lua_pushcfunction(L, mouse_get);
-		lua_setfield(L, -2, "__index");
+	// Register the library, then add __index and __newindex
+	// metamethods to it to allow global variables
+	luaL_register(L, "input", lib);
+		LUA_CreateAndSetMetatable(L, lib_get, lib_set, NULL, false);
 
-		lua_pushcfunction(L, mouse_num);
-		lua_setfield(L, -2, "__len");
+		LUA_CreateAndSetUserdataField(L, -1, "gamekeydown", lib_getGameKeyDown, lib_setGameKeyDown, lib_lenGameKeyDown, false);
+		// TODO: 2.3: Delete this alias (moved to input library)
+		LUA_RegisterGlobalUserdata(L, "gamekeydown", lib_getGameKeyDown, lib_setGameKeyDown, lib_lenGameKeyDown);
 	lua_pop(L, 1);
 
-	luaL_register(L, "input", lib);
 	return 0;
 }
diff --git a/src/lua_libs.h b/src/lua_libs.h
index 7f8d21f3807bbe19ba45a67eda0c1a860195b7b2..26f919ad89fb0e7ecde8aa39903c1838cf0776e5 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -13,6 +13,7 @@
 extern lua_State *gL;
 
 extern boolean mousegrabbedbylua;
+extern boolean ignoregameinputs;
 
 #define MUTABLE_TAGS
 
@@ -84,6 +85,8 @@ extern boolean mousegrabbedbylua;
 #define META_HUDINFO "HUDINFO_T*"
 #define META_PATCH "PATCH_T*"
 #define META_COLORMAP "COLORMAP"
+#define META_EXTRACOLORMAP "EXTRACOLORMAP_T*"
+#define META_LIGHTTABLE "LIGHTTABLE_T*"
 #define META_CAMERA "CAMERA_T*"
 
 #define META_ACTION "ACTIONF_T*"
@@ -111,4 +114,5 @@ 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_ColorLib(lua_State *L);
 int LUA_InputLib(lua_State *L);
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index e343979939056ec3c18b482a3bfd3423c446d70e..0c4ba6fd3e24451d4b3144264bb5c357eb3eea35 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -35,15 +35,19 @@ enum sector_e {
 	sector_floorpic,
 	sector_floorxoffset,
 	sector_flooryoffset,
-	sector_floorangle,	
+	sector_floorxscale,
+	sector_flooryscale,
+	sector_floorangle,
 	sector_ceilingpic,
 	sector_ceilingxoffset,
 	sector_ceilingyoffset,
+	sector_ceilingxscale,
+	sector_ceilingyscale,
 	sector_ceilingangle,
 	sector_lightlevel,
 	sector_floorlightlevel,
 	sector_floorlightabsolute,
-	sector_floorlightsec,	
+	sector_floorlightsec,
 	sector_ceilinglightlevel,
 	sector_ceilinglightabsolute,
 	sector_ceilinglightsec,
@@ -57,6 +61,7 @@ enum sector_e {
 	sector_ffloors,
 	sector_fslope,
 	sector_cslope,
+	sector_colormap,
 	sector_flags,
 	sector_specialflags,
 	sector_damagetype,
@@ -73,18 +78,22 @@ static const char *const sector_opt[] = {
 	"floorpic",
 	"floorxoffset",
 	"flooryoffset",
+	"floorxscale",
+	"flooryscale",
 	"floorangle",
 	"ceilingpic",
 	"ceilingxoffset",
 	"ceilingyoffset",
-	"ceilingangle",	
+	"ceilingxscale",
+	"ceilingyscale",
+	"ceilingangle",
 	"lightlevel",
 	"floorlightlevel",
 	"floorlightabsolute",
 	"floorlightsec",
 	"ceilinglightlevel",
 	"ceilinglightabsolute",
-	"ceilinglightsec",	
+	"ceilinglightsec",
 	"special",
 	"tag",
 	"taglist",
@@ -95,6 +104,7 @@ static const char *const sector_opt[] = {
 	"ffloors",
 	"f_slope",
 	"c_slope",
+	"colormap",
 	"flags",
 	"specialflags",
 	"damagetype",
@@ -186,8 +196,16 @@ enum side_e {
 	side_offsety_top,
 	side_offsetx_mid,
 	side_offsety_mid,
+	side_offsetx_bottom,
 	side_offsetx_bot,
+	side_offsety_bottom,
 	side_offsety_bot,
+	side_scalex_top,
+	side_scaley_top,
+	side_scalex_mid,
+	side_scaley_mid,
+	side_scalex_bottom,
+	side_scaley_bottom,
 	side_toptexture,
 	side_bottomtexture,
 	side_midtexture,
@@ -206,8 +224,16 @@ static const char *const side_opt[] = {
 	"offsety_top",
 	"offsetx_mid",
 	"offsety_mid",
+	"offsetx_bottom",
 	"offsetx_bot",
+	"offsety_bottom",
 	"offsety_bot",
+	"scalex_top",
+	"scaley_top",
+	"scalex_mid",
+	"scaley_mid",
+	"scalex_bottom",
+	"scaley_bottom",
 	"toptexture",
 	"bottomtexture",
 	"midtexture",
@@ -247,8 +273,16 @@ enum ffloor_e {
 	ffloor_topheight,
 	ffloor_toppic,
 	ffloor_toplightlevel,
+	ffloor_topxoffs,
+	ffloor_topyoffs,
+	ffloor_topxscale,
+	ffloor_topyscale,
 	ffloor_bottomheight,
 	ffloor_bottompic,
+	ffloor_bottomxoffs,
+	ffloor_bottomyoffs,
+	ffloor_bottomxscale,
+	ffloor_bottomyscale,
 	ffloor_tslope,
 	ffloor_bslope,
 	ffloor_sector,
@@ -273,8 +307,16 @@ static const char *const ffloor_opt[] = {
 	"topheight",
 	"toppic",
 	"toplightlevel",
+	"topxoffs",
+	"topyoffs",
+	"topxscale",
+	"topyscale",
 	"bottomheight",
 	"bottompic",
+	"bottomxoffs",
+	"bottomyoffs",
+	"bottomxscale",
+	"bottomyscale",
 	"t_slope",
 	"b_slope",
 	"sector", // secnum pushed as control sector userdata
@@ -654,20 +696,20 @@ static int sector_get(lua_State *L)
 		return 1;
 	}
 	case sector_floorxoffset:
-	{
 		lua_pushfixed(L, sector->floorxoffset);
 		return 1;
-	}
 	case sector_flooryoffset:
-	{
 		lua_pushfixed(L, sector->flooryoffset);
 		return 1;
-	}
-	case sector_floorangle: 
-	{
+	case sector_floorxscale:
+		lua_pushfixed(L, sector->floorxscale);
+		return 1;
+	case sector_flooryscale:
+		lua_pushfixed(L, sector->flooryscale);
+		return 1;
+	case sector_floorangle:
 		lua_pushangle(L, sector->floorangle);
 		return 1;
-	}	
 	case sector_ceilingpic: // ceilingpic
 	{
 		levelflat_t *levelflat = &levelflats[sector->ceilingpic];
@@ -678,20 +720,20 @@ static int sector_get(lua_State *L)
 		return 1;
 	}
 	case sector_ceilingxoffset:
-	{
 		lua_pushfixed(L, sector->ceilingxoffset);
 		return 1;
-	}
 	case sector_ceilingyoffset:
-	{
 		lua_pushfixed(L, sector->ceilingyoffset);
 		return 1;
-	}
+	case sector_ceilingxscale:
+		lua_pushfixed(L, sector->ceilingxscale);
+		return 1;
+	case sector_ceilingyscale:
+		lua_pushfixed(L, sector->ceilingyscale);
+		return 1;
 	case sector_ceilingangle:
-	{
 		lua_pushangle(L, sector->ceilingangle);
 		return 1;
-	}	
 	case sector_lightlevel:
 		lua_pushinteger(L, sector->lightlevel);
 		return 1;
@@ -703,7 +745,7 @@ static int sector_get(lua_State *L)
 		return 1;
 	case sector_floorlightsec:
 		lua_pushinteger(L, sector->floorlightsec);
-		return 1;		
+		return 1;
 	case sector_ceilinglightlevel:
 		lua_pushinteger(L, sector->ceilinglightlevel);
 		return 1;
@@ -712,7 +754,7 @@ static int sector_get(lua_State *L)
 		return 1;
 	case sector_ceilinglightsec:
 		lua_pushinteger(L, sector->ceilinglightsec);
-		return 1;		
+		return 1;
 	case sector_special:
 		lua_pushinteger(L, sector->special);
 		return 1;
@@ -751,6 +793,9 @@ static int sector_get(lua_State *L)
 	case sector_cslope: // c_slope
 		LUA_PushUserdata(L, sector->c_slope, META_SLOPE);
 		return 1;
+	case sector_colormap: // extra_colormap
+		LUA_PushUserdata(L, sector->extra_colormap, META_EXTRACOLORMAP);
+		return 1;
 	case sector_flags: // flags
 		lua_pushinteger(L, sector->flags);
 		return 1;
@@ -840,9 +885,15 @@ static int sector_set(lua_State *L)
 	case sector_flooryoffset:
 		sector->flooryoffset = luaL_checkfixed(L, 3);
 		break;
+	case sector_floorxscale:
+		sector->floorxscale = luaL_checkfixed(L, 3);
+		break;
+	case sector_flooryscale:
+		sector->flooryscale = luaL_checkfixed(L, 3);
+		break;
 	case sector_floorangle:
 		sector->floorangle = luaL_checkangle(L, 3);
-		break;				
+		break;
 	case sector_ceilingpic:
 		sector->ceilingpic = P_AddLevelFlatRuntime(luaL_checkstring(L, 3));
 		break;
@@ -852,6 +903,12 @@ static int sector_set(lua_State *L)
 	case sector_ceilingyoffset:
 		sector->ceilingyoffset = luaL_checkfixed(L, 3);
 		break;
+	case sector_ceilingxscale:
+		sector->ceilingxscale = luaL_checkfixed(L, 3);
+		break;
+	case sector_ceilingyscale:
+		sector->ceilingyscale = luaL_checkfixed(L, 3);
+		break;
 	case sector_ceilingangle:
 		sector->ceilingangle = luaL_checkangle(L, 3);
 		break;
@@ -866,7 +923,7 @@ static int sector_set(lua_State *L)
 		break;
 	case sector_floorlightsec:
 		sector->floorlightsec = (INT32)luaL_checkinteger(L, 3);
-		break;		
+		break;
 	case sector_ceilinglightlevel:
 		sector->ceilinglightlevel = (INT16)luaL_checkinteger(L, 3);
 		break;
@@ -875,7 +932,7 @@ static int sector_set(lua_State *L)
 		break;
 	case sector_ceilinglightsec:
 		sector->ceilinglightsec = (INT32)luaL_checkinteger(L, 3);
-		break;		
+		break;
 	case sector_special:
 		sector->special = (INT16)luaL_checkinteger(L, 3);
 		break;
@@ -1043,17 +1100,7 @@ static int line_get(lua_State *L)
 		lua_pushinteger(L, line->special);
 		return 1;
 	case line_tag:
-		// HELLO
-		// THIS IS LJ SONIC
-		// HOW IS YOUR DAY?
-		// BY THE WAY WHEN 2.3 OR 3.0 OR 4.0 OR SRB3 OR SRB4 OR WHATEVER IS OUT
-		// YOU SHOULD REMEMBER TO CHANGE THIS SO IT ALWAYS RETURNS A UNSIGNED VALUE
-		// HAVE A NICE DAY
-		//
-		//
-		//
-		//
-		// you are ugly
+		// TODO: 2.3: Always return a unsigned value
 		lua_pushinteger(L, Tag_FGet(&line->tags));
 		return 1;
 	case line_taglist:
@@ -1072,7 +1119,7 @@ static int line_get(lua_State *L)
 		LUA_PushUserdata(L, &sides[line->sidenum[0]], META_SIDE);
 		return 1;
 	case line_backside: // backside
-		if (line->sidenum[1] == 0xffff)
+		if (line->sidenum[1] == NO_SIDEDEF)
 			return 0;
 		LUA_PushUserdata(L, &sides[line->sidenum[1]], META_SIDE);
 		return 1;
@@ -1108,6 +1155,7 @@ static int line_get(lua_State *L)
 	case line_polyobj:
 		LUA_PushUserdata(L, line->polyobj, META_POLYOBJ);
 		return 1;
+	// TODO: 2.3: Delete
 	case line_text:
 		{
 			if (udmf)
@@ -1214,11 +1262,31 @@ static int side_get(lua_State *L)
 	case side_offsety_mid:
 		lua_pushfixed(L, side->offsety_mid);
 		return 1;
+	case side_offsetx_bottom:
 	case side_offsetx_bot:
-		lua_pushfixed(L, side->offsetx_bot);
+		lua_pushfixed(L, side->offsetx_bottom);
 		return 1;
+	case side_offsety_bottom:
 	case side_offsety_bot:
-		lua_pushfixed(L, side->offsety_bot);
+		lua_pushfixed(L, side->offsety_bottom);
+		return 1;
+	case side_scalex_top:
+		lua_pushfixed(L, side->scalex_top);
+		return 1;
+	case side_scaley_top:
+		lua_pushfixed(L, side->scaley_top);
+		return 1;
+	case side_scalex_mid:
+		lua_pushfixed(L, side->scalex_mid);
+		return 1;
+	case side_scaley_mid:
+		lua_pushfixed(L, side->scaley_mid);
+		return 1;
+	case side_scalex_bottom:
+		lua_pushfixed(L, side->scalex_bottom);
+		return 1;
+	case side_scaley_bottom:
+		lua_pushfixed(L, side->scaley_bottom);
 		return 1;
 	case side_toptexture:
 		lua_pushinteger(L, side->toptexture);
@@ -1241,8 +1309,12 @@ static int side_get(lua_State *L)
 	case side_repeatcnt:
 		lua_pushinteger(L, side->repeatcnt);
 		return 1;
+	// TODO: 2.3: Delete
 	case side_text:
 		{
+			boolean isfrontside;
+			size_t sidei = side-sides;
+
 			if (udmf)
 			{
 				LUA_Deprecated(L, "(sidedef_t).text", "(sidedef_t).line.stringargs");
@@ -1250,7 +1322,7 @@ static int side_get(lua_State *L)
 				return 1;
 			}
 
-			boolean isfrontside = side->line->sidenum[0] == side-sides;
+			isfrontside = side->line->sidenum[0] == sidei;
 
 			lua_pushstring(L, side->line->stringargs[isfrontside ? 0 : 1]);
 			return 1;
@@ -1302,10 +1374,30 @@ static int side_set(lua_State *L)
 		side->offsety_mid = luaL_checkfixed(L, 3);
 		break;
 	case side_offsetx_bot:
-		side->offsetx_bot = luaL_checkfixed(L, 3);
+	case side_offsetx_bottom:
+		side->offsetx_bottom = luaL_checkfixed(L, 3);
 		break;
 	case side_offsety_bot:
-		side->offsety_bot = luaL_checkfixed(L, 3);
+	case side_offsety_bottom:
+		side->offsety_bottom = luaL_checkfixed(L, 3);
+		break;
+	case side_scalex_top:
+		side->scalex_top = luaL_checkfixed(L, 3);
+		break;
+	case side_scaley_top:
+		side->scaley_top = luaL_checkfixed(L, 3);
+		break;
+	case side_scalex_mid:
+		side->scalex_mid = luaL_checkfixed(L, 3);
+		break;
+	case side_scaley_mid:
+		side->scaley_mid = luaL_checkfixed(L, 3);
+		break;
+	case side_scalex_bottom:
+		side->scalex_bottom = luaL_checkfixed(L, 3);
+		break;
+	case side_scaley_bottom:
+		side->scaley_bottom = luaL_checkfixed(L, 3);
 		break;
 	case side_toptexture:
 		side->toptexture = luaL_checkinteger(L, 3);
@@ -2122,6 +2214,18 @@ static int ffloor_get(lua_State *L)
 	case ffloor_toplightlevel:
 		lua_pushinteger(L, *ffloor->toplightlevel);
 		return 1;
+	case ffloor_topxoffs:
+		lua_pushfixed(L, *ffloor->topxoffs);
+		return 1;
+	case ffloor_topyoffs:
+		lua_pushfixed(L, *ffloor->topyoffs);
+		return 1;
+	case ffloor_topxscale:
+		lua_pushfixed(L, *ffloor->topxscale);
+		return 1;
+	case ffloor_topyscale:
+		lua_pushfixed(L, *ffloor->topyscale);
+		return 1;
 	case ffloor_bottomheight:
 		lua_pushfixed(L, *ffloor->bottomheight);
 		return 1;
@@ -2133,6 +2237,18 @@ static int ffloor_get(lua_State *L)
 		lua_pushlstring(L, levelflat->name, i);
 		return 1;
 	}
+	case ffloor_bottomxoffs:
+		lua_pushfixed(L, *ffloor->bottomxoffs);
+		return 1;
+	case ffloor_bottomyoffs:
+		lua_pushfixed(L, *ffloor->bottomyoffs);
+		return 1;
+	case ffloor_bottomxscale:
+		lua_pushfixed(L, *ffloor->bottomxscale);
+		return 1;
+	case ffloor_bottomyscale:
+		lua_pushfixed(L, *ffloor->bottomyscale);
+		return 1;
 	case ffloor_tslope:
 		LUA_PushUserdata(L, *ffloor->t_slope, META_SLOPE);
 		return 1;
@@ -2317,6 +2433,18 @@ static int ffloor_set(lua_State *L)
 	case ffloor_toplightlevel:
 		*ffloor->toplightlevel = (INT16)luaL_checkinteger(L, 3);
 		break;
+	case ffloor_topxoffs:
+		*ffloor->topxoffs = luaL_checkfixed(L, 3);
+		break;
+	case ffloor_topyoffs:
+		*ffloor->topyoffs = luaL_checkfixed(L, 3);
+		break;
+	case ffloor_topxscale:
+		*ffloor->topxscale = luaL_checkfixed(L, 3);
+		break;
+	case ffloor_topyscale:
+		*ffloor->topyscale = luaL_checkfixed(L, 3);
+		break;
 	case ffloor_bottomheight: { // bottomheight
 		boolean flag;
 		fixed_t lastpos = *ffloor->bottomheight;
@@ -2335,6 +2463,18 @@ static int ffloor_set(lua_State *L)
 	case ffloor_bottompic:
 		*ffloor->bottompic = P_AddLevelFlatRuntime(luaL_checkstring(L, 3));
 		break;
+	case ffloor_bottomxoffs:
+		*ffloor->bottomxoffs = luaL_checkfixed(L, 3);
+		break;
+	case ffloor_bottomyoffs:
+		*ffloor->bottomyoffs = luaL_checkfixed(L, 3);
+		break;
+	case ffloor_bottomxscale:
+		*ffloor->bottomxscale = luaL_checkfixed(L, 3);
+		break;
+	case ffloor_bottomyscale:
+		*ffloor->bottomyscale = luaL_checkfixed(L, 3);
+		break;
 	case ffloor_fofflags: {
 		ffloortype_e oldflags = ffloor->fofflags; // store FOF's old flags
 		ffloor->fofflags = luaL_checkinteger(L, 3);
@@ -2843,170 +2983,36 @@ static int mapheaderinfo_get(lua_State *L)
 
 int LUA_MapLib(lua_State *L)
 {
-	luaL_newmetatable(L, META_SECTORLINES);
-		lua_pushcfunction(L, sectorlines_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, sectorlines_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_SECTOR);
-		lua_pushcfunction(L, sector_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, sector_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, sector_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
+	LUA_RegisterUserdataMetatable(L, META_SECTORLINES, sectorlines_get, NULL, sectorlines_num);
+	LUA_RegisterUserdataMetatable(L, META_SECTOR, sector_get, sector_set, sector_num);
+	LUA_RegisterUserdataMetatable(L, META_SUBSECTOR, subsector_get, NULL, subsector_num);
+	LUA_RegisterUserdataMetatable(L, META_LINE, line_get, NULL, line_num);
+	LUA_RegisterUserdataMetatable(L, META_LINEARGS, lineargs_get, NULL, lineargs_len);
+	LUA_RegisterUserdataMetatable(L, META_LINESTRINGARGS, linestringargs_get, NULL, linestringargs_len);
+	LUA_RegisterUserdataMetatable(L, META_SIDENUM, sidenum_get, NULL, NULL);
+	LUA_RegisterUserdataMetatable(L, META_SIDE, side_get, side_set, side_num);
+	LUA_RegisterUserdataMetatable(L, META_VERTEX, vertex_get, NULL, vertex_num);
+	LUA_RegisterUserdataMetatable(L, META_FFLOOR, ffloor_get, ffloor_set, NULL);
+	LUA_RegisterUserdataMetatable(L, META_BBOX, bbox_get, NULL, NULL);
+	LUA_RegisterUserdataMetatable(L, META_SLOPE, slope_get, slope_set, NULL);
+	LUA_RegisterUserdataMetatable(L, META_VECTOR2, vector2_get, NULL, NULL);
+	LUA_RegisterUserdataMetatable(L, META_VECTOR3, vector3_get, NULL, NULL);
+	LUA_RegisterUserdataMetatable(L, META_MAPHEADER, mapheaderinfo_get, NULL, NULL);
 
 	sector_fields_ref = Lua_CreateFieldTable(L, sector_opt);
-
-	luaL_newmetatable(L, META_SUBSECTOR);
-		lua_pushcfunction(L, subsector_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, subsector_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
 	subsector_fields_ref = Lua_CreateFieldTable(L, subsector_opt);
-
-	luaL_newmetatable(L, META_LINE);
-		lua_pushcfunction(L, line_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, line_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
 	line_fields_ref = Lua_CreateFieldTable(L, line_opt);
-
-	luaL_newmetatable(L, META_LINEARGS);
-		lua_pushcfunction(L, lineargs_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, lineargs_len);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_LINESTRINGARGS);
-		lua_pushcfunction(L, linestringargs_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, linestringargs_len);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_SIDENUM);
-		lua_pushcfunction(L, sidenum_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_SIDE);
-		lua_pushcfunction(L, side_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, side_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, side_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
 	side_fields_ref = Lua_CreateFieldTable(L, side_opt);
-
-	luaL_newmetatable(L, META_VERTEX);
-		lua_pushcfunction(L, vertex_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, vertex_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
 	vertex_fields_ref = Lua_CreateFieldTable(L, vertex_opt);
-
-	luaL_newmetatable(L, META_FFLOOR);
-		lua_pushcfunction(L, ffloor_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, ffloor_set);
-		lua_setfield(L, -2, "__newindex");
-	lua_pop(L, 1);
-
 	ffloor_fields_ref = Lua_CreateFieldTable(L, ffloor_opt);
-
-#ifdef HAVE_LUA_SEGS
-	luaL_newmetatable(L, META_SEG);
-		lua_pushcfunction(L, seg_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, seg_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	seg_fields_ref = Lua_CreateFieldTable(L, seg_opt);
-
-	luaL_newmetatable(L, META_NODE);
-		lua_pushcfunction(L, node_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, node_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	node_fields_ref = Lua_CreateFieldTable(L, node_opt);
-
-	luaL_newmetatable(L, META_NODEBBOX);
-		//lua_pushcfunction(L, nodebbox_get);
-		//lua_setfield(L, -2, "__index");
-		lua_pushcfunction(L, nodebbox_call);
-		lua_setfield(L, -2, "__call");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_NODECHILDREN);
-		lua_pushcfunction(L, nodechildren_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L, 1);
-#endif
-
-	luaL_newmetatable(L, META_BBOX);
-		lua_pushcfunction(L, bbox_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_SLOPE);
-		lua_pushcfunction(L, slope_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, slope_set);
-		lua_setfield(L, -2, "__newindex");
-	lua_pop(L, 1);
-
 	slope_fields_ref = Lua_CreateFieldTable(L, slope_opt);
-
-	luaL_newmetatable(L, META_VECTOR2);
-		lua_pushcfunction(L, vector2_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_VECTOR3);
-		lua_pushcfunction(L, vector3_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_MAPHEADER);
-		lua_pushcfunction(L, mapheaderinfo_get);
-		lua_setfield(L, -2, "__index");
-
-		//lua_pushcfunction(L, mapheaderinfo_num);
-		//lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
 	mapheaderinfo_fields_ref = Lua_CreateFieldTable(L, mapheaderinfo_opt);
 
+	LUA_RegisterGlobalUserdata(L, "subsectors", lib_getSubsector, NULL, lib_numsubsectors);
+	LUA_RegisterGlobalUserdata(L, "sides", lib_getSide, NULL, lib_numsides);
+	LUA_RegisterGlobalUserdata(L, "vertexes", lib_getVertex, NULL, lib_numvertexes);
+	LUA_RegisterGlobalUserdata(L, "mapheaderinfo", lib_getMapheaderinfo, NULL, lib_nummapheaders);
+
 	LUA_PushTaggableObjectArray(L, "sectors",
 			lib_iterateSectors,
 			lib_getSector,
@@ -3015,16 +3021,6 @@ int LUA_MapLib(lua_State *L)
 			&numsectors, &sectors,
 			sizeof (sector_t), META_SECTOR);
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSubsector);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numsubsectors);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "subsectors");
-
 	LUA_PushTaggableObjectArray(L, "lines",
 			lib_iterateLines,
 			lib_getLine,
@@ -3033,56 +3029,22 @@ int LUA_MapLib(lua_State *L)
 			&numlines, &lines,
 			sizeof (line_t), META_LINE);
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSide);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numsides);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "sides");
+#ifdef HAVE_LUA_SEGS
+	LUA_RegisterUserdataMetatable(L, META_SEG, seg_get, NULL, seg_num);
+	LUA_RegisterUserdataMetatable(L, META_NODE, node_get, NULL, node_num);
+	LUA_RegisterUserdataMetatable(L, META_NODECHILDREN, nodechildren_get, NULL, NULL);
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getVertex);
-			lua_setfield(L, -2, "__index");
+	seg_fields_ref = Lua_CreateFieldTable(L, seg_opt);
+	node_fields_ref = Lua_CreateFieldTable(L, node_opt);
 
-			lua_pushcfunction(L, lib_numvertexes);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "vertexes");
+	luaL_newmetatable(L, META_NODEBBOX);
+		//LUA_SetCFunctionField(L, "__index", nodebbox_get);
+		LUA_SetCFunctionField(L, "__call", nodebbox_call);
+	lua_pop(L, 1);
 
-#ifdef HAVE_LUA_SEGS
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSeg);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numsegs);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "segs");
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getNode);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numnodes);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "nodes");
+	LUA_RegisterGlobalUserdata(L, "segs", lib_getSeg, NULL, lib_numsegs);
+	LUA_RegisterGlobalUserdata(L, "nodes", lib_getNode, NULL, lib_numnodes);
 #endif
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getMapheaderinfo);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_nummapheaders);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "mapheaderinfo");
 	return 0;
 }
diff --git a/src/lua_mathlib.c b/src/lua_mathlib.c
index d0fe6863f19fc9e807024683b0418da4ef2385f1..1bc6019dee9a507ebc6350a541807e75e53726d7 100644
--- a/src/lua_mathlib.c
+++ b/src/lua_mathlib.c
@@ -125,6 +125,7 @@ static int lib_fixeddiv(lua_State *L)
 	return 1;
 }
 
+// TODO: 2.3: Delete
 static int lib_fixedrem(lua_State *L)
 {
 	LUA_Deprecated(L, "FixedRem(a, b)", "a % b");
diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c
index fddf958beb783e27671cb966c3afbc3e20b0c620..d38e85a21a6ce2dea9590fd3df98096b7c0f7b10 100644
--- a/src/lua_mobjlib.c
+++ b/src/lua_mobjlib.c
@@ -641,10 +641,7 @@ static int mobj_set(lua_State *L)
 		mo->tics = luaL_checkinteger(L, 3);
 		break;
 	case mobj_state: // set state by enum
-		if (mo->player)
-			P_SetPlayerMobjState(mo, luaL_checkinteger(L, 3));
-		else
-			P_SetMobjState(mo, luaL_checkinteger(L, 3));
+		P_SetMobjState(mo, luaL_checkinteger(L, 3));
 		break;
 	case mobj_flags: // special handling for MF_NOBLOCKMAP and MF_NOSECTOR
 	{
@@ -1163,43 +1160,12 @@ static int lib_nummapthings(lua_State *L)
 
 int LUA_MobjLib(lua_State *L)
 {
-	luaL_newmetatable(L, META_MOBJ);
-		lua_pushcfunction(L, mobj_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, mobj_set);
-		lua_setfield(L, -2, "__newindex");
-	lua_pop(L,1);
+	LUA_RegisterUserdataMetatable(L, META_MOBJ, mobj_get, mobj_set, NULL);
+	LUA_RegisterUserdataMetatable(L, META_THINGARGS, thingargs_get, NULL, thingargs_len);
+	LUA_RegisterUserdataMetatable(L, META_THINGSTRINGARGS, thingstringargs_get, NULL, thingstringargs_len);
+	LUA_RegisterUserdataMetatable(L, META_MAPTHING, mapthing_get, mapthing_set, mapthing_num);
 
 	mobj_fields_ref = Lua_CreateFieldTable(L, mobj_opt);
-
-	luaL_newmetatable(L, META_THINGARGS);
-		lua_pushcfunction(L, thingargs_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, thingargs_len);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_THINGSTRINGARGS);
-		lua_pushcfunction(L, thingstringargs_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, thingstringargs_len);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_MAPTHING);
-		lua_pushcfunction(L, mapthing_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, mapthing_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, mapthing_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
-
 	mapthing_fields_ref = Lua_CreateFieldTable(L, mapthing_opt);
 
 	LUA_PushTaggableObjectArray(L, "mapthings",
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index 827e5a405fffb7fa1dda25f58aea049dc244bf38..d12aa1c3fae8f5e1e0e9171118c21b03d1310350 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -223,6 +223,7 @@ enum player_e
 	player_blocked,
 	player_jointime,
 	player_quittime,
+	player_lastinputtime,
 	player_ping,
 #ifdef HWRENDER
 	player_fovadd,
@@ -371,6 +372,7 @@ static const char *const player_opt[] = {
 	"blocked",
 	"jointime",
 	"quittime",
+	"lastinputtime",
 	"ping",
 #ifdef HWRENDER
 	"fovadd",
@@ -407,7 +409,7 @@ static int player_get(lua_State *L)
 	case player_realmo:
 		LUA_PushUserdata(L, plr->mo, META_MOBJ);
 		break;
-	// Kept for backward-compatibility
+	// TODO: 2.3: Kept for backward-compatibility
 	// Should be fixed to work like "realmo" later
 	case player_mo:
 		if (plr->spectator)
@@ -826,6 +828,9 @@ static int player_get(lua_State *L)
 	case player_quittime:
 		lua_pushinteger(L, plr->quittime);
 		break;
+	case player_lastinputtime:
+		lua_pushinteger(L, plr->lastinputtime);
+		break;
 	case player_ping:
 		lua_pushinteger(L, playerpingtable[plr - players]);
 		break;
@@ -1349,6 +1354,9 @@ static int player_set(lua_State *L)
 	case player_quittime:
 		plr->quittime = (tic_t)luaL_checkinteger(L, 3);
 		break;
+	case player_lastinputtime:
+		plr->lastinputtime = (tic_t)luaL_checkinteger(L, 3);
+		break;
 #ifdef HWRENDER
 	case player_fovadd:
 		plr->fovadd = luaL_checkfixed(L, 3);
@@ -1523,48 +1531,13 @@ static int ticcmd_set(lua_State *L)
 
 int LUA_PlayerLib(lua_State *L)
 {
-	luaL_newmetatable(L, META_PLAYER);
-		lua_pushcfunction(L, player_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, player_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, player_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
+	LUA_RegisterUserdataMetatable(L, META_PLAYER, player_get, player_set, player_num);
+	LUA_RegisterUserdataMetatable(L, META_POWERS, power_get, power_set, power_len);
+	LUA_RegisterUserdataMetatable(L, META_TICCMD, ticcmd_get, ticcmd_set, NULL);
 
 	player_fields_ref = Lua_CreateFieldTable(L, player_opt);
-
-	luaL_newmetatable(L, META_POWERS);
-		lua_pushcfunction(L, power_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, power_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, power_len);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
-
-	luaL_newmetatable(L, META_TICCMD);
-		lua_pushcfunction(L, ticcmd_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, ticcmd_set);
-		lua_setfield(L, -2, "__newindex");
-	lua_pop(L,1);
-
 	ticcmd_fields_ref = Lua_CreateFieldTable(L, ticcmd_opt);
 
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getPlayer);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_lenPlayer);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "players");
+	LUA_RegisterGlobalUserdata(L, "players", lib_getPlayer, NULL, lib_lenPlayer);
 	return 0;
 }
diff --git a/src/lua_polyobjlib.c b/src/lua_polyobjlib.c
index c3d9d9d1ad96072061be6b7909b1233d25e488fd..6c016f8b2cb24c8bcf993f4c3250e02e020c2f54 100644
--- a/src/lua_polyobjlib.c
+++ b/src/lua_polyobjlib.c
@@ -447,41 +447,10 @@ static int lib_numPolyObjects(lua_State *L)
 
 int LUA_PolyObjLib(lua_State *L)
 {
-	luaL_newmetatable(L, META_POLYOBJVERTICES);
-		lua_pushcfunction(L, polyobjvertices_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, polyobjvertices_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_POLYOBJLINES);
-		lua_pushcfunction(L, polyobjlines_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, polyobjlines_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L, 1);
-
-	luaL_newmetatable(L, META_POLYOBJ);
-		lua_pushcfunction(L, polyobj_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, polyobj_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, polyobj_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getPolyObject);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numPolyObjects);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "polyobjects");
+	LUA_RegisterUserdataMetatable(L, META_POLYOBJVERTICES, polyobjvertices_get, NULL, polyobjvertices_num);
+	LUA_RegisterUserdataMetatable(L, META_POLYOBJLINES, polyobjlines_get, NULL, polyobjlines_num);
+	LUA_RegisterUserdataMetatable(L, META_POLYOBJ, polyobj_get, polyobj_set, polyobj_num);
+
+	LUA_RegisterGlobalUserdata(L, "polyobjects", lib_getPolyObject, NULL, lib_numPolyObjects);
 	return 0;
 }
diff --git a/src/lua_script.c b/src/lua_script.c
index 6a5982006379d3e32e9694009e73fe67111a7f40..f90db7bc343fc16bd92f87cd6e1da7ce1acd16ea 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -28,7 +28,7 @@
 #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
+#include "netcode/d_netfil.h" // for LUA_DumpFile
 #endif
 
 #include "lua_script.h"
@@ -58,6 +58,7 @@ static lua_CFunction liblist[] = {
 	LUA_PolyObjLib, // polyobj_t
 	LUA_BlockmapLib, // blockmap stuff
 	LUA_HudLib, // HUD stuff
+	LUA_ColorLib, // general color functions
 	LUA_InputLib, // inputs
 	NULL
 };
@@ -415,9 +416,11 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 	} else if (fastcmp(word, "stagefailed")) {
 		lua_pushboolean(L, stagefailed);
 		return 1;
+	// TODO: 2.3: Deprecated (moved to the input library)
 	} else if (fastcmp(word, "mouse")) {
 		LUA_PushUserdata(L, &mouse, META_MOUSE);
 		return 1;
+	// TODO: 2.3: Deprecated (moved to the input library)
 	} else if (fastcmp(word, "mouse2")) {
 		LUA_PushUserdata(L, &mouse2, META_MOUSE);
 		return 1;
@@ -576,8 +579,7 @@ static void LUA_ClearState(void)
 
 	// lock the global namespace
 	lua_getmetatable(L, LUA_GLOBALSINDEX);
-		lua_pushcfunction(L, setglobals);
-		lua_setfield(L, -2, "__newindex");
+		LUA_SetCFunctionField(L, "__newindex", setglobals);
 		lua_newtable(L);
 		lua_setfield(L, -2, "__metatable");
 	lua_pop(L, 1);
@@ -602,64 +604,114 @@ void LUA_ClearExtVars(void)
 INT32 lua_lumploading = 0;
 
 // Load a script from a MYFILE
-static inline void LUA_LoadFile(MYFILE *f, char *name, boolean noresults)
+static inline boolean LUA_LoadFile(MYFILE *f, char *name)
 {
 	int errorhandlerindex;
+	boolean success;
 
 	if (!name)
 		name = wadfiles[f->wad]->filename;
+
 	CONS_Printf("Loading Lua script from %s\n", name);
+
 	if (!gL) // Lua needs to be initialized
 		LUA_ClearState();
+
 	lua_pushinteger(gL, f->wad);
 	lua_setfield(gL, LUA_REGISTRYINDEX, "WAD");
 
+	lua_pushcfunction(gL, LUA_GetErrorMessage);
+	errorhandlerindex = lua_gettop(gL);
+
+	success = !luaL_loadbuffer(gL, f->data, f->size, va("@%s",name));
+
+	if (!success) {
+		CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL,-1));
+		lua_pop(gL,1);
+	}
+
+	lua_gc(gL, LUA_GCCOLLECT, 0);
+	lua_remove(gL, errorhandlerindex);
+
+	return success;
+}
+
+// Runs a script loaded by LUA_LoadFile.
+static inline void LUA_DoFile(boolean noresults)
+{
+	int errorhandlerindex;
+
+	if (!gL) // LUA_LoadFile should've allocated gL for us!
+		return;
+
 	lua_lumploading++; // turn on loading flag
 
 	lua_pushcfunction(gL, LUA_GetErrorMessage);
-	errorhandlerindex = lua_gettop(gL);
-	if (luaL_loadbuffer(gL, f->data, f->size, va("@%s",name)) || lua_pcall(gL, 0, noresults ? 0 : LUA_MULTRET, lua_gettop(gL) - 1)) {
+	lua_insert(gL, -2); // move the function we're calling to the top.
+	errorhandlerindex = lua_gettop(gL) - 1;
+
+	if (lua_pcall(gL, 0, noresults ? 0 : LUA_MULTRET, lua_gettop(gL) - 1)) {
 		CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL,-1));
 		lua_pop(gL,1);
 	}
+
 	lua_gc(gL, LUA_GCCOLLECT, 0);
 	lua_remove(gL, errorhandlerindex);
 
 	lua_lumploading--; // turn off again
 }
 
-// Load a script from a lump
-void LUA_LoadLump(UINT16 wad, UINT16 lump, boolean noresults)
+static inline MYFILE *LUA_GetFile(UINT16 wad, UINT16 lump, char **name)
 {
-	MYFILE f;
-	char *name;
+	MYFILE *f = Z_Malloc(sizeof(MYFILE), PU_LUA, NULL);
 	size_t len;
-	f.wad = wad;
-	f.size = W_LumpLengthPwad(wad, lump);
-	f.data = Z_Malloc(f.size, PU_LUA, NULL);
-	W_ReadLumpPwad(wad, lump, f.data);
-	f.curpos = f.data;
+
+	f->wad = wad;
+	f->size = W_LumpLengthPwad(wad, lump);
+	f->data = Z_Malloc(f->size, PU_LUA, NULL);
+	W_ReadLumpPwad(wad, lump, f->data);
+	f->curpos = f->data;
 
 	len = strlen(wadfiles[wad]->filename); // length of file name
 
 	if (wadfiles[wad]->type == RET_LUA)
 	{
-		name = malloc(len+1);
-		strcpy(name, wadfiles[wad]->filename);
+		*name = malloc(len+1);
+		strcpy(*name, wadfiles[wad]->filename);
 	}
 	else // If it's not a .lua file, copy the lump name in too.
 	{
 		lumpinfo_t *lump_p = &wadfiles[wad]->lumpinfo[lump];
 		len += 1 + strlen(lump_p->fullname); // length of file name, '|', and lump name
-		name = malloc(len+1);
-		sprintf(name, "%s|%s", wadfiles[wad]->filename, lump_p->fullname);
-		name[len] = '\0';
+		*name = malloc(len+1);
+		sprintf(*name, "%s|%s", wadfiles[wad]->filename, lump_p->fullname);
+		(*name)[len] = '\0'; // annoying that index takes priority over dereference, but w/e
 	}
 
-	LUA_LoadFile(&f, name, noresults); // actually load file!
+	return f;
+}
+
+// Load a script from a lump
+boolean LUA_LoadLump(UINT16 wad, UINT16 lump)
+{
+	char *name = NULL;
+	MYFILE *f = LUA_GetFile(wad, lump, &name);
+	boolean success = LUA_LoadFile(f, name); // actually load file!
 
 	free(name);
-	Z_Free(f.data);
+
+	Z_Free(f->data);
+	Z_Free(f);
+
+	return success;
+}
+
+void LUA_DoLump(UINT16 wad, UINT16 lump, boolean noresults)
+{
+	boolean success = LUA_LoadLump(wad, lump);
+
+	if (success)
+		LUA_DoFile(noresults); // run it
 }
 
 #ifdef LUA_ALLOW_BYTECODE
@@ -1813,20 +1865,107 @@ void LUA_PushTaggableObjectArray
 	lua_newuserdata(L, 0);
 		lua_createtable(L, 0, 2);
 			lua_createtable(L, 0, 2);
-				lua_pushcfunction(L, iterator);
-				lua_setfield(L, -2, "iterate");
+				LUA_SetCFunctionField(L, "iterate", iterator);
 
 				LUA_InsertTaggroupIterator(L, garray,
 						max_elements, element_array, sizeof_element, meta);
 
 				lua_createtable(L, 0, 1);
-					lua_pushcfunction(L, indexer);
-					lua_setfield(L, -2, "__index");
+					LUA_SetCFunctionField(L, "__index", indexer);
 				lua_setmetatable(L, -2);
 			lua_setfield(L, -2, "__index");
 
-			lua_pushcfunction(L, counter);
-			lua_setfield(L, -2, "__len");
+			LUA_SetCFunctionField(L, "__len", counter);
 		lua_setmetatable(L, -2);
 	lua_setglobal(L, field);
 }
+
+static void SetBasicMetamethods(
+	lua_State *L,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len
+)
+{
+	if (get)
+		LUA_SetCFunctionField(L, "__index", get);
+	if (set)
+		LUA_SetCFunctionField(L, "__newindex", set);
+	if (len)
+		LUA_SetCFunctionField(L, "__len", len);
+}
+
+void LUA_SetCFunctionField(lua_State *L, const char *name, lua_CFunction value)
+{
+	lua_pushcfunction(L, value);
+	lua_setfield(L, -2, name);
+}
+
+void LUA_RegisterUserdataMetatable(
+	lua_State *L,
+	const char *name,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len
+)
+{
+	luaL_newmetatable(L, name);
+	SetBasicMetamethods(L, get, set, len);
+	lua_pop(L, 1);
+}
+
+// If keep is true, leaves the metatable on the stack.
+// Otherwise, the stack size remains unchanged.
+void LUA_CreateAndSetMetatable(
+	lua_State *L,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len,
+	boolean keep
+)
+{
+	lua_newtable(L);
+	SetBasicMetamethods(L, get, set, len);
+
+	lua_pushvalue(L, -1);
+	lua_setmetatable(L, -3);
+
+	if (!keep)
+		lua_pop(L, 1);
+}
+
+// If keep is true, leaves the userdata and metatable on the stack.
+// Otherwise, the stack size remains unchanged.
+void LUA_CreateAndSetUserdataField(
+	lua_State *L,
+	int index,
+	const char *name,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len,
+	boolean keep
+)
+{
+	if (index < 0 && index > LUA_REGISTRYINDEX)
+		index -= 3;
+
+	lua_newuserdata(L, 0);
+	LUA_CreateAndSetMetatable(L, get, set, len, true);
+
+	lua_pushvalue(L, -2);
+	lua_setfield(L, index, name);
+
+	if (!keep)
+		lua_pop(L, 2);
+}
+
+void LUA_RegisterGlobalUserdata(
+	lua_State *L,
+	const char *name,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len
+)
+{
+	LUA_CreateAndSetUserdataField(L, LUA_GLOBALSINDEX, name, get, set, len, false);
+}
diff --git a/src/lua_script.h b/src/lua_script.h
index d0b06a719e32a254ba56c5f860252aba15484da0..45d2a37ffd1a636f1e6aaf6848fa868a25fba74b 100644
--- a/src/lua_script.h
+++ b/src/lua_script.h
@@ -45,7 +45,8 @@ extern INT32 lua_lumploading; // is LUA_LoadLump being called?
 
 int LUA_GetErrorMessage(lua_State *L);
 int LUA_Call(lua_State *L, int nargs, int nresults, int errorhandlerindex);
-void LUA_LoadLump(UINT16 wad, UINT16 lump, boolean noresults);
+boolean LUA_LoadLump(UINT16 wad, UINT16 lump);
+void LUA_DoLump(UINT16 wad, UINT16 lump, boolean noresults);
 #ifdef LUA_ALLOW_BYTECODE
 void LUA_DumpFile(const char *filename);
 #endif
@@ -73,6 +74,42 @@ void LUA_PushTaggableObjectArray
 		size_t sizeof_element,
 		const char *meta);
 
+void LUA_SetCFunctionField(lua_State *L, const char *name, lua_CFunction value);
+
+void LUA_RegisterUserdataMetatable(
+	lua_State *L,
+	const char *name,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len
+);
+
+void LUA_CreateAndSetMetatable(
+	lua_State *L,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len,
+	boolean keep
+);
+
+void LUA_CreateAndSetUserdataField(
+	lua_State *L,
+	int index,
+	const char *name,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len,
+	boolean keep
+);
+
+void LUA_RegisterGlobalUserdata(
+	lua_State *L,
+	const char *name,
+	lua_CFunction get,
+	lua_CFunction set,
+	lua_CFunction len
+);
+
 void LUA_InsertTaggroupIterator
 (		lua_State *L,
 		taggroup_t *garray[],
diff --git a/src/lua_skinlib.c b/src/lua_skinlib.c
index 041c5d59851f3669b03d45c899c20b3440e193f0..3debd3746947a252be8e27f0f2608a21739049d2 100644
--- a/src/lua_skinlib.c
+++ b/src/lua_skinlib.c
@@ -373,49 +373,14 @@ static int sprite_get(lua_State *L)
 
 int LUA_SkinLib(lua_State *L)
 {
-	luaL_newmetatable(L, META_SKIN);
-		lua_pushcfunction(L, skin_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, skin_set);
-		lua_setfield(L, -2, "__newindex");
-
-		lua_pushcfunction(L, skin_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
+	LUA_RegisterUserdataMetatable(L, META_SKIN, skin_get, skin_set, skin_num);
+	LUA_RegisterUserdataMetatable(L, META_SOUNDSID, soundsid_get, NULL, soundsid_num);
+	LUA_RegisterUserdataMetatable(L, META_SKINSPRITES, lib_getSkinSprite, NULL, lib_numSkinsSprites);
+	LUA_RegisterUserdataMetatable(L, META_SKINSPRITESLIST, sprite_get, NULL, NULL);
 
 	skin_fields_ref = Lua_CreateFieldTable(L, skin_opt);
 
-	luaL_newmetatable(L, META_SOUNDSID);
-		lua_pushcfunction(L, soundsid_get);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, soundsid_num);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
-
-	luaL_newmetatable(L, META_SKINSPRITES);
-		lua_pushcfunction(L, lib_getSkinSprite);
-		lua_setfield(L, -2, "__index");
-
-		lua_pushcfunction(L, lib_numSkinsSprites);
-		lua_setfield(L, -2, "__len");
-	lua_pop(L,1);
-
-	luaL_newmetatable(L, META_SKINSPRITESLIST);
-		lua_pushcfunction(L, sprite_get);
-		lua_setfield(L, -2, "__index");
-	lua_pop(L,1);
-
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_pushcfunction(L, lib_getSkin);
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numSkins);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "skins");
+	LUA_RegisterGlobalUserdata(L, "skins", lib_getSkin, NULL, lib_numSkins);
 
 	return 0;
 }
diff --git a/src/lua_taglib.c b/src/lua_taglib.c
index 2ba60df998cf88831ec85618f88f57af2746f594..a040a7efcf5a7a5ef6f0532669c255bce0856373 100644
--- a/src/lua_taglib.c
+++ b/src/lua_taglib.c
@@ -228,7 +228,7 @@ static int taglist_get(lua_State *L)
 	}
 	else
 	{
-		lua_getmetatable(L, 1);
+		lua_getglobal(L, "taglist");
 		lua_replace(L, 1);
 		lua_rawget(L, 1);
 		return 1;
@@ -372,8 +372,7 @@ void LUA_InsertTaggroupIterator
 		lua_pushcclosure(L, lib_numTaggroupElements, 2);
 		lua_setfield(L, -2, "__len");
 
-		lua_pushcfunction(L, element_iterator);
-		lua_setfield(L, -2, "__call");
+		LUA_SetCFunctionField(L, "__call", element_iterator);
 	lua_pushcclosure(L, lib_getTaggroup, 1);
 	lua_setfield(L, -2, "tagged");
 }
@@ -414,11 +413,9 @@ set_taglist_metatable(lua_State *L, const char *meta)
 		lua_setfenv(L, -2);
 		lua_setfield(L, -2, "__index");
 
-		lua_pushcfunction(L, taglist_len);
-		lua_setfield(L, -2, "__len");
+		LUA_SetCFunctionField(L, "__len", taglist_len);
 
-		lua_pushcfunction(L, taglist_equal);
-		lua_setfield(L, -2, "__eq");
+		LUA_SetCFunctionField(L, "__eq", taglist_equal);
 #ifdef MUTABLE_TAGS
 	return luaL_ref(L, LUA_REGISTRYINDEX);
 #endif
@@ -426,17 +423,11 @@ set_taglist_metatable(lua_State *L, const char *meta)
 
 int LUA_TagLib(lua_State *L)
 {
-	lua_newuserdata(L, 0);
-		lua_createtable(L, 0, 2);
-			lua_createtable(L, 0, 1);
-				lua_pushcfunction(L, lib_iterateTags);
-				lua_setfield(L, -2, "iterate");
-			lua_setfield(L, -2, "__index");
-
-			lua_pushcfunction(L, lib_numTags);
-			lua_setfield(L, -2, "__len");
-		lua_setmetatable(L, -2);
-	lua_setglobal(L, "tags");
+	LUA_CreateAndSetUserdataField(L, LUA_GLOBALSINDEX, "tags", NULL, NULL, lib_numTags, true);
+		lua_createtable(L, 0, 1);
+			LUA_SetCFunctionField(L, "iterate", lib_iterateTags);
+		lua_setfield(L, -2, "__index");
+	lua_pop(L, 2);
 
 	open_taglist(L);
 
diff --git a/src/lua_thinkerlib.c b/src/lua_thinkerlib.c
index cff92f34d6e0a98dff5b1c76cc97ff358c11ff7d..f1be8c78933bab543d00388ae76b40512742d9aa 100644
--- a/src/lua_thinkerlib.c
+++ b/src/lua_thinkerlib.c
@@ -127,8 +127,7 @@ static int lib_startIterate(lua_State *L)
 int LUA_ThinkerLib(lua_State *L)
 {
 	luaL_newmetatable(L, META_ITERATIONSTATE);
-	lua_pushcfunction(L, iterationState_gc);
-	lua_setfield(L, -2, "__gc");
+	LUA_SetCFunctionField(L, "__gc", iterationState_gc);
 	lua_pop(L, 1);
 
 	lua_createtable(L, 0, 1);
diff --git a/src/m_anigif.c b/src/m_anigif.c
index 90e4c69ce494a8450de083ab0ca7cb25c081f8f9..5bc7717e0f44a4293e52a8907c57b0cfda6eade5 100644
--- a/src/m_anigif.c
+++ b/src/m_anigif.c
@@ -462,7 +462,7 @@ static void GIF_headwrite(void)
 	// Image width/height
 	if (gif_downscale)
 	{
-		scrbuf_downscaleamt = vid.dupx;
+		scrbuf_downscaleamt = vid.dup;
 		rwidth = (vid.width / scrbuf_downscaleamt);
 		rheight = (vid.height / scrbuf_downscaleamt);
 	}
diff --git a/src/m_cheat.c b/src/m_cheat.c
index 7ad86353ac86916b167d3e0768f9a8ad6f23444e..e61db2c2ee3d49e58f570567ee576ba94d3959eb 100644
--- a/src/m_cheat.c
+++ b/src/m_cheat.c
@@ -19,7 +19,7 @@
 #include "r_local.h"
 #include "p_local.h"
 #include "p_setup.h"
-#include "d_net.h"
+#include "netcode/d_net.h"
 
 #include "m_cheat.h"
 #include "m_menu.h"
@@ -1019,7 +1019,7 @@ static void OP_CycleThings(INT32 amt)
 	if (players[0].mo->eflags & MFE_VERTICALFLIP) // correct z when flipped
 		players[0].mo->z += players[0].mo->height - FixedMul(mobjinfo[op_currentthing].height, players[0].mo->scale);
 	players[0].mo->height = FixedMul(mobjinfo[op_currentthing].height, players[0].mo->scale);
-	P_SetPlayerMobjState(players[0].mo, S_OBJPLACE_DUMMY);
+	P_SetMobjState(players[0].mo, S_OBJPLACE_DUMMY);
 
 	op_currentdoomednum = mobjinfo[op_currentthing].doomednum;
 }
@@ -1528,7 +1528,7 @@ void Command_ObjectPlace_f(void)
 		else
 			OP_CycleThings(0); // sets all necessary height values without cycling op_currentthing
 
-		P_SetPlayerMobjState(players[0].mo, S_OBJPLACE_DUMMY);
+		P_SetMobjState(players[0].mo, S_OBJPLACE_DUMMY);
 	}
 	// Or are we leaving it instead?
 	else
@@ -1542,7 +1542,7 @@ void Command_ObjectPlace_f(void)
 
 		// If still in dummy state, get out of it.
 		if (players[0].mo->state == &states[S_OBJPLACE_DUMMY])
-			P_SetPlayerMobjState(players[0].mo, op_oldstate);
+			P_SetMobjState(players[0].mo, op_oldstate);
 
 		// Reset everything back to how it was before we entered objectplace.
 		P_UnsetThingPosition(players[0].mo);
diff --git a/src/m_cond.c b/src/m_cond.c
index 6c87ebf6e5d8fb30789be6b5675f10daa574a22e..706a1b5106f487e4ed285880304a87335f7f716e 100644
--- a/src/m_cond.c
+++ b/src/m_cond.c
@@ -509,7 +509,7 @@ UINT8 M_CampaignWarpIsCheat(INT32 gt, INT32 mapnum, gamedata_t *data)
 		return true;
 	}
 
-	if (mapheaderinfo[mapnum-1]->menuflags & LF2_HIDEINMENU)
+	if (!mapheaderinfo[mapnum-1] || mapheaderinfo[mapnum-1]->menuflags & LF2_HIDEINMENU)
 	{
 		// You're never allowed to warp to this level.
 		return true;
diff --git a/src/m_fixed.h b/src/m_fixed.h
index 94bd6a16bbf86e70c4f38c86a4758424da325db1..4e644c9b6d5db3687a211526e3a50a5f03e9702f 100644
--- a/src/m_fixed.h
+++ b/src/m_fixed.h
@@ -34,6 +34,7 @@
 */
 
 typedef INT32 fixed_t;
+typedef UINT32 ufixed_t;
 
 /*!
   \brief convert fixed_t into floating number
@@ -106,7 +107,7 @@ FUNCMATH FUNCINLINE static ATTRINLINE fixed_t FixedInt(fixed_t a)
 */
 FUNCMATH FUNCINLINE static ATTRINLINE fixed_t FixedDiv(fixed_t a, fixed_t b)
 {
-	if ((abs(a) >> (FRACBITS-2)) >= abs(b))
+	if (((ufixed_t)abs(a) >> (FRACBITS-2)) >= (ufixed_t)abs(b))
 		return (a^b) < 0 ? INT32_MIN : INT32_MAX;
 
 	return FixedDiv2(a, b);
diff --git a/src/m_menu.c b/src/m_menu.c
index 3946803b290e0240cac36accd363113256580933..9aae59445382270c6d45445dfb113425fe52eee6 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -20,7 +20,7 @@
 
 #include "doomdef.h"
 #include "d_main.h"
-#include "d_netcmd.h"
+#include "netcode/d_netcmd.h"
 #include "console.h"
 #include "r_fps.h"
 #include "r_local.h"
@@ -53,8 +53,10 @@
 #include "hardware/hw_main.h"
 #endif
 
-#include "d_net.h"
-#include "mserv.h"
+#include "netcode/d_net.h"
+#include "netcode/mserv.h"
+#include "netcode/server_connection.h"
+#include "netcode/client_connection.h"
 #include "m_misc.h"
 #include "m_anigif.h"
 #include "byteptr.h"
@@ -149,9 +151,7 @@ levellist_mode_t levellistmode = LLM_CREATESERVER;
 UINT8 maplistoption = 0;
 
 static char joystickInfo[MAX_JOYSTICKS+1][29];
-#ifndef NONET
 static UINT32 serverlistpage;
-#endif
 
 static UINT8 numsaves = 0;
 static saveinfo_t* savegameinfo = NULL; // Extra info about the save games.
@@ -190,10 +190,8 @@ static void M_GoBack(INT32 choice);
 static void M_StopMessage(INT32 choice);
 static boolean stopstopmessage = false;
 
-#ifndef NONET
 static void M_HandleServerPage(INT32 choice);
 static void M_RoomMenu(INT32 choice);
-#endif
 
 // Prototyping is fun, innit?
 // ==========================================================================
@@ -216,7 +214,7 @@ static fixed_t lsoffs[2];
 #define lshli levelselectselect[2]
 
 #define lshseperation 101
-#define lsbasevseperation ((62*vid.height)/(BASEVIDHEIGHT*vid.dupy)) //62
+#define lsbasevseperation ((62*vid.height)/(BASEVIDHEIGHT*vid.dup)) //62
 #define lsheadingheight 16
 #define getheadingoffset(row) (levelselect.rows[row].header[0] ? lsheadingheight : 0)
 #define lsvseperation(row) (lsbasevseperation + getheadingoffset(row))
@@ -296,7 +294,6 @@ static void M_SetupMultiPlayer2(INT32 choice);
 static void M_StartSplitServerMenu(INT32 choice);
 static void M_StartServer(INT32 choice);
 static void M_ServerOptions(INT32 choice);
-#ifndef NONET
 static void M_StartServerMenu(INT32 choice);
 static void M_ConnectMenu(INT32 choice);
 static void M_ConnectMenuModChecks(INT32 choice);
@@ -304,7 +301,6 @@ static void M_Refresh(INT32 choice);
 static void M_Connect(INT32 choice);
 static void M_ChooseRoom(INT32 choice);
 menu_t MP_MainDef;
-#endif
 
 // Options
 // Split into multiple parts due to size
@@ -382,11 +378,9 @@ static void M_DrawVideoMode(void);
 static void M_DrawColorMenu(void);
 static void M_DrawScreenshotMenu(void);
 static void M_DrawMonitorToggles(void);
-#ifndef NONET
 static void M_DrawConnectMenu(void);
 static void M_DrawMPMainMenu(void);
 static void M_DrawRoomMenu(void);
-#endif
 static void M_DrawJoystick(void);
 static void M_DrawSetupMultiPlayerMenu(void);
 static void M_DrawColorRamp(INT32 x, INT32 y, INT32 w, INT32 h, skincolor_t color);
@@ -401,10 +395,8 @@ static void M_HandleImageDef(INT32 choice);
 static void M_HandleLoadSave(INT32 choice);
 static void M_HandleLevelStats(INT32 choice);
 static void M_HandlePlaystyleMenu(INT32 choice);
-#ifndef NONET
 static boolean M_CancelConnect(void);
 static void M_HandleConnectIP(INT32 choice);
-#endif
 static void M_HandleSetupMultiPlayer(INT32 choice);
 static void M_HandleVideoMode(INT32 choice);
 
@@ -503,11 +495,7 @@ consvar_t cv_dummyloadless = CVAR_INIT ("dummyloadless", "In-game", CV_HIDEN, lo
 static menuitem_t MainMenu[] =
 {
 	{IT_STRING|IT_CALL,    NULL, "1  Player",   M_SinglePlayerMenu,      76},
-#ifndef NONET
 	{IT_STRING|IT_SUBMENU, NULL, "Multiplayer", &MP_MainDef,             84},
-#else
-	{IT_STRING|IT_CALL,    NULL, "Multiplayer", M_StartSplitServerMenu,  84},
-#endif
 	{IT_STRING|IT_CALL,    NULL, "Extras",      M_SecretsMenu,           92},
 	{IT_CALL   |IT_STRING, NULL, "Addons",      M_Addons,               100},
 	{IT_STRING|IT_CALL,    NULL, "Options",     M_Options,              108},
@@ -930,16 +918,10 @@ static menuitem_t SP_PlayerMenu[] =
 static menuitem_t MP_SplitServerMenu[] =
 {
 	{IT_STRING|IT_CALL,              NULL, "Select Gametype/Level...", M_MapChange,         100},
-#ifdef NONET // In order to keep player setup accessible.
-	{IT_STRING|IT_CALL,              NULL, "Player 1 setup...",        M_SetupMultiPlayer,  110},
-	{IT_STRING|IT_CALL,              NULL, "Player 2 setup...",        M_SetupMultiPlayer2, 120},
-#endif
 	{IT_STRING|IT_CALL,              NULL, "More Options...",          M_ServerOptions,     130},
 	{IT_WHITESTRING|IT_CALL,         NULL, "Start",                    M_StartServer,       140},
 };
 
-#ifndef NONET
-
 static menuitem_t MP_MainMenu[] =
 {
 	{IT_HEADER, NULL, "Join a game", NULL, 0},
@@ -1026,8 +1008,6 @@ menuitem_t MP_RoomMenu[] =
 	{IT_DISABLED,         NULL, "",               M_ChooseRoom, 162},
 };
 
-#endif
-
 static menuitem_t MP_PlayerSetupMenu[] =
 {
 	{IT_KEYHANDLER, NULL, "", M_HandleSetupMultiPlayer, 0}, // name
@@ -1088,6 +1068,7 @@ static menuitem_t OP_ChangeControlsMenu[] =
 	{IT_CALL | IT_STRING2, NULL, "Move Right",       M_ChangeControl, GC_STRAFERIGHT },
 	{IT_CALL | IT_STRING2, NULL, "Jump",             M_ChangeControl, GC_JUMP      },
 	{IT_CALL | IT_STRING2, NULL, "Spin",             M_ChangeControl, GC_SPIN     },
+	{IT_CALL | IT_STRING2, NULL, "Shield",           M_ChangeControl, GC_SHIELD    },
 	{IT_HEADER, NULL, "Camera", NULL, 0},
 	{IT_SPACE, NULL, NULL, NULL, 0}, // padding
 	{IT_CALL | IT_STRING2, NULL, "Look Up",        M_ChangeControl, GC_LOOKUP      },
@@ -1586,14 +1567,12 @@ enum
 static menuitem_t OP_ServerOptionsMenu[] =
 {
 	{IT_HEADER, NULL, "General", NULL, 0},
-#ifndef NONET
 	{IT_STRING | IT_CVAR | IT_CV_STRING,
 	                         NULL, "Server name",                      &cv_servername,           7},
 	{IT_STRING | IT_CVAR,    NULL, "Max Players",                      &cv_maxplayers,          21},
 	{IT_STRING | IT_CVAR,    NULL, "Allow Add-on Downloading",         &cv_downloading,         26},
 	{IT_STRING | IT_CVAR,    NULL, "Allow players to join",            &cv_allownewplayer,      31},
 	{IT_STRING | IT_CVAR,    NULL, "Minutes for reconnecting",         &cv_rejointimeout,       36},
-#endif
 	{IT_STRING | IT_CVAR,    NULL, "Map progression",                  &cv_advancemap,          41},
 	{IT_STRING | IT_CVAR,    NULL, "Intermission Timer",               &cv_inttime,             46},
 
@@ -1632,7 +1611,6 @@ static menuitem_t OP_ServerOptionsMenu[] =
 	{IT_STRING | IT_CVAR,    NULL, "Autobalance sizes",                &cv_autobalance,        216},
 	{IT_STRING | IT_CVAR,    NULL, "Scramble on Map Change",           &cv_scrambleonchange,   221},
 
-#ifndef NONET
 	{IT_HEADER, NULL, "Advanced", NULL, 230},
 	{IT_STRING | IT_CVAR | IT_CV_STRING, NULL, "Master server",        &cv_masterserver,       236},
 
@@ -1640,7 +1618,6 @@ static menuitem_t OP_ServerOptionsMenu[] =
 	{IT_STRING | IT_CVAR,    NULL, "Attempts to resynchronise",        &cv_resynchattempts,    256},
 
 	{IT_STRING | IT_CVAR,    NULL, "Show IP Address of Joiners",       &cv_showjoinaddress,    261},
-#endif
 };
 
 static menuitem_t OP_MonitorToggleMenu[] =
@@ -1954,11 +1931,7 @@ menu_t MP_SplitServerDef =
 	MTREE2(MN_MP_MAIN, MN_MP_SPLITSCREEN),
 	"M_MULTI",
 	sizeof (MP_SplitServerMenu)/sizeof (menuitem_t),
-#ifndef NONET
 	&MP_MainDef,
-#else
-	&MainDef,
-#endif
 	MP_SplitServerMenu,
 	M_DrawServerMenu,
 	27, 30 - 50,
@@ -1966,8 +1939,6 @@ menu_t MP_SplitServerDef =
 	NULL
 };
 
-#ifndef NONET
-
 menu_t MP_MainDef =
 {
 	MN_MP_MAIN,
@@ -2019,15 +1990,10 @@ menu_t MP_RoomDef =
 	0,
 	NULL
 };
-#endif
 
 menu_t MP_PlayerSetupDef =
 {
-#ifdef NONET
-	MTREE2(MN_MP_MAIN, MN_MP_PLAYERSETUP),
-#else
 	MTREE3(MN_MP_MAIN, MN_MP_SPLITSCREEN, MN_MP_PLAYERSETUP),
-#endif
 	"M_SPLAYR",
 	sizeof (MP_PlayerSetupMenu)/sizeof (menuitem_t),
 	&MainDef, // doesn't matter
@@ -2787,6 +2753,7 @@ void M_SetMenuCurBackground(const char *defaultname)
 {
 	char name[9];
 	strncpy(name, defaultname, 8);
+	name[8] = '\0';
 	M_IterateMenuTree(MIT_SetCurBackground, &name);
 }
 
@@ -3210,40 +3177,42 @@ boolean M_Responder(event_t *ev)
 	}
 	else if (menuactive)
 	{
-		if (ev->type == ev_keydown)
+		if (ev->type == ev_keydown || ev->type == ev_text)
 		{
-			keydown++;
 			ch = ev->key;
-
-			// added 5-2-98 remap virtual keys (mouse & joystick buttons)
-			switch (ch)
+			if (ev->type == ev_keydown)
 			{
-				case KEY_MOUSE1:
-				case KEY_JOY1:
-					ch = KEY_ENTER;
-					break;
-				case KEY_JOY1 + 3:
-					ch = 'n';
-					break;
-				case KEY_MOUSE1 + 1:
-				case KEY_JOY1 + 1:
-					ch = KEY_ESCAPE;
-					break;
-				case KEY_JOY1 + 2:
-					ch = KEY_BACKSPACE;
-					break;
-				case KEY_HAT1:
-					ch = KEY_UPARROW;
-					break;
-				case KEY_HAT1 + 1:
-					ch = KEY_DOWNARROW;
-					break;
-				case KEY_HAT1 + 2:
-					ch = KEY_LEFTARROW;
-					break;
-				case KEY_HAT1 + 3:
-					ch = KEY_RIGHTARROW;
-					break;
+				keydown++;
+				// added 5-2-98 remap virtual keys (mouse & joystick buttons)
+				switch (ch)
+				{
+					case KEY_MOUSE1:
+					case KEY_JOY1:
+						ch = KEY_ENTER;
+						break;
+					case KEY_JOY1 + 3:
+						ch = 'n';
+						break;
+					case KEY_MOUSE1 + 1:
+					case KEY_JOY1 + 1:
+						ch = KEY_ESCAPE;
+						break;
+					case KEY_JOY1 + 2:
+						ch = KEY_BACKSPACE;
+						break;
+					case KEY_HAT1:
+						ch = KEY_UPARROW;
+						break;
+					case KEY_HAT1 + 1:
+						ch = KEY_DOWNARROW;
+						break;
+					case KEY_HAT1 + 2:
+						ch = KEY_LEFTARROW;
+						break;
+					case KEY_HAT1 + 3:
+						ch = KEY_RIGHTARROW;
+						break;
+				}
 			}
 		}
 		else if (ev->type == ev_joystick  && ev->key == 0 && joywait < I_GetTime())
@@ -3405,8 +3374,11 @@ boolean M_Responder(event_t *ev)
 	// Handle menuitems which need a specific key handling
 	if (routine && (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_KEYHANDLER)
 	{
-		if (shiftdown && ch >= 32 && ch <= 127)
-			ch = shiftxform[ch];
+		// ignore ev_keydown events if the key maps to a character, since
+		// the ev_text event will follow immediately after in that case.
+		if (ev->type == ev_keydown && ch >= 32 && ch <= 127)
+			return true;
+
 		routine(ch);
 		return true;
 	}
@@ -3448,6 +3420,11 @@ boolean M_Responder(event_t *ev)
 	{
 		if ((currentMenu->menuitems[itemOn].status & IT_CVARTYPE) == IT_CV_STRING)
 		{
+			// ignore ev_keydown events if the key maps to a character, since
+			// the ev_text event will follow immediately after in that case.
+			if (ev->type == ev_keydown && ch >= 32 && ch <= 127)
+				return false;
+
 			if (M_ChangeStringCvar(ch))
 				return true;
 			else
@@ -3608,16 +3585,16 @@ void M_Drawer(void)
 		{
 			if (customversionstring[0] != '\0')
 			{
-				V_DrawThinString(vid.dupx, vid.height - 17*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT, "Mod version:");
-				V_DrawThinString(vid.dupx, vid.height - 9*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, customversionstring);
+				V_DrawThinString(vid.dup, vid.height - 17*vid.dup, V_NOSCALESTART|V_TRANSLUCENT, "Mod version:");
+				V_DrawThinString(vid.dup, vid.height -  9*vid.dup, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, customversionstring);
 			}
 			else
 			{
 #ifdef DEVELOP // Development -- show revision / branch info
-				V_DrawThinString(vid.dupx, vid.height - 17*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, compbranch);
-				V_DrawThinString(vid.dupx, vid.height - 9*vid.dupy,  V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, comprevision);
+				V_DrawThinString(vid.dup, vid.height - 17*vid.dup, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, compbranch);
+				V_DrawThinString(vid.dup, vid.height -  9*vid.dup, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, comprevision);
 #else // Regular build
-				V_DrawThinString(vid.dupx, vid.height - 9*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, va("%s", VERSIONSTRING));
+				V_DrawThinString(vid.dup, vid.height -  9*vid.dup, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, va("%s", VERSIONSTRING));
 #endif
 			}
 		}
@@ -3956,9 +3933,7 @@ void M_Init(void)
 		OP_JoystickSetMenu[i].itemaction = M_AssignJoystick;
 	}
 
-#ifndef NONET
 	CV_RegisterVar(&cv_serversort);
-#endif
 }
 
 void M_InitCharacterTables(void)
@@ -5793,16 +5768,15 @@ static void M_DrawRecordAttackForeground(void)
 
 	INT32 i;
 	INT32 height = (fg->height / 2);
-	INT32 dupz = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy);
 
 	for (i = -12; i < (BASEVIDHEIGHT/height) + 12; i++)
 	{
 		INT32 y = ((i*height) - (height - ((FixedInt(recatkdrawtimer*2))%height)));
 		// don't draw above the screen
 		{
-			INT32 sy = FixedMul(y, dupz<<FRACBITS) >> FRACBITS;
-			if (vid.height != BASEVIDHEIGHT * dupz)
-				sy += (vid.height - (BASEVIDHEIGHT * dupz)) / 2;
+			INT32 sy = FixedMul(y, vid.dup<<FRACBITS) >> FRACBITS;
+			if (vid.height != BASEVIDHEIGHT * vid.dup)
+				sy += (vid.height - (BASEVIDHEIGHT * vid.dup)) / 2;
 			if ((sy+height) < 0)
 				continue;
 		}
@@ -5826,13 +5800,12 @@ static void M_DrawRecordAttackForeground(void)
 static void M_DrawNightsAttackMountains(void)
 {
 	static fixed_t bgscrollx;
-	INT32 dupz = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy);
 	patch_t *background = W_CachePatchName(curbgname, PU_PATCH);
 	INT16 w = background->width;
 	INT32 x = FixedInt(-bgscrollx) % w;
 	INT32 y = BASEVIDHEIGHT - (background->height * 2);
 
-	if (vid.height != BASEVIDHEIGHT * dupz)
+	if (vid.height != BASEVIDHEIGHT * vid.dup)
 		V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 158);
 	V_DrawFill(0, y+50, vid.width, BASEVIDHEIGHT, V_SNAPTOLEFT|31);
 
@@ -5988,7 +5961,7 @@ static void M_DrawLevelPlatterMenu(void)
 	}
 
 	// draw from top to bottom
-	while (y < (vid.height/vid.dupy))
+	while (y < (vid.height/vid.dup))
 	{
 		M_DrawLevelPlatterRow(iter, y);
 		y += lsvseperation(iter);
@@ -7835,9 +7808,9 @@ static void M_DrawSoundTest(void)
 		}
 	}
 
-	y = (BASEVIDWIDTH-(vid.width/vid.dupx))/2;
+	y = (BASEVIDWIDTH-(vid.width/vid.dup))/2;
 
-	V_DrawFill(y, 20, vid.width/vid.dupx, 24, 159);
+	V_DrawFill(y, 20, vid.width/vid.dup, 24, 159);
 	{
 		static fixed_t st_scroll = -FRACUNIT;
 		const char* titl;
@@ -8407,8 +8380,8 @@ static void M_DrawLoadGameData(void)
 	INT32 i, prev_i = 1, savetodraw, x, y, hsep = 90;
 	skin_t *charskin = NULL;
 
-	if (vid.width != BASEVIDWIDTH*vid.dupx)
-		hsep = (hsep*vid.width)/(BASEVIDWIDTH*vid.dupx);
+	if (vid.width != BASEVIDWIDTH*vid.dup)
+		hsep = (hsep*vid.width)/(BASEVIDWIDTH*vid.dup);
 
 	for (i = 2; prev_i; i = -(i + ((UINT32)i >> 31))) // draws from outwards in; 2, -2, 1, -1, 0
 	{
@@ -9393,7 +9366,7 @@ static void M_DrawSetupChoosePlayerMenu(void)
 	INT16 bgwidth = charbg->width;
 	INT16 fgwidth = charfg->width;
 	INT32 x, y;
-	INT32 w = (vid.width/vid.dupx);
+	INT32 w = (vid.width/vid.dup);
 
 	if (abs(char_scroll) > FRACUNIT/4)
 		char_scroll -= FixedMul((char_scroll>>2), renderdeltatics);
@@ -9429,7 +9402,7 @@ static void M_DrawSetupChoosePlayerMenu(void)
 	// Background and borders
 	V_DrawFill(0, 0, bgwidth, vid.height, V_SNAPTOTOP|colormap[101]);
 	{
-		INT32 sw = (BASEVIDWIDTH * vid.dupx);
+		INT32 sw = (BASEVIDWIDTH * vid.dup);
 		INT32 bw = (vid.width - sw) / 2;
 		col = colormap[106];
 		if (bw)
@@ -10851,7 +10824,7 @@ void M_DrawMarathon(void)
 	const char *cvstring;
 	char *work;
 	angle_t fa;
-	INT32 dupz = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy), xspan = (vid.width/dupz), yspan = (vid.height/dupz), diffx = (xspan - BASEVIDWIDTH)/2, diffy = (yspan - BASEVIDHEIGHT)/2, maxy = BASEVIDHEIGHT + diffy;
+	INT32 xspan = (vid.width/vid.dup), yspan = (vid.height/vid.dup), diffx = (xspan - BASEVIDWIDTH)/2, diffy = (yspan - BASEVIDHEIGHT)/2, maxy = BASEVIDHEIGHT + diffy;
 
 	curbgxspeed = 0;
 	curbgyspeed = 18;
@@ -10924,16 +10897,17 @@ void M_DrawMarathon(void)
 		INT32 trans = V_60TRANS+((cnt&~3)<<(V_ALPHASHIFT-2));
 		INT32 height = fg->height / 2;
 		char patchname[7] = "CEMGx0";
+		INT32 dup;
 
-		dupz = (w*7)/6; //(w*42*120)/(360*6); -- I don't know why this works but I'm not going to complain.
-		dupz = ((dupz>>FRACBITS) % height);
+		dup = (w*7)/6; //(w*42*120)/(360*6); -- I don't know why this works but I'm not going to complain.
+		dup = ((dup>>FRACBITS) % height);
 		y = height/2;
-		while (y+dupz >= -diffy)
+		while (y+dup >= -diffy)
 			y -= height;
-		while (y-2-dupz < maxy)
+		while (y-2-dup < maxy)
 		{
-			V_DrawFixedPatch(((BASEVIDWIDTH-190)<<(FRACBITS-1)), (y-2-dupz)<<FRACBITS, FRACUNIT/2, trans, fg, NULL);
-			V_DrawFixedPatch(((BASEVIDWIDTH+190)<<(FRACBITS-1)), (y+dupz)<<FRACBITS, FRACUNIT/2, trans|V_FLIP, fg, NULL);
+			V_DrawFixedPatch(((BASEVIDWIDTH-190)<<(FRACBITS-1)), (y-2-dup)<<FRACBITS, FRACUNIT/2, trans, fg, NULL);
+			V_DrawFixedPatch(((BASEVIDWIDTH+190)<<(FRACBITS-1)), (y+dup)<<FRACBITS, FRACUNIT/2, trans|V_FLIP, fg, NULL);
 			y += height;
 		}
 
@@ -10951,16 +10925,16 @@ void M_DrawMarathon(void)
 		}
 
 		height = 18; // prevents the need for the next line
-		//dupz = (w*height)/18;
-		dupz = ((w>>FRACBITS) % height);
-		y = dupz+(height/4);
-		x = 105+dupz;
+		//dup = (w*height)/18;
+		dup = ((w>>FRACBITS) % height);
+		y = dup+(height/4);
+		x = 105+dup;
 		while (y >= -diffy)
 		{
 			x -= height;
 			y -= height;
 		}
-		while (y-dupz < maxy && x < (xspan/2))
+		while (y-dup < maxy && x < (xspan/2))
 		{
 			V_DrawFill((BASEVIDWIDTH/2)-x-height, -diffy, height, diffy+y+height, 153);
 			V_DrawFill((BASEVIDWIDTH/2)+x, (maxy-y)-height, height, height+y, 153);
@@ -11108,7 +11082,6 @@ static void M_EndGame(INT32 choice)
 
 #define S_LINEY(n) currentMenu->y + SERVERHEADERHEIGHT + (n * SERVERLINEHEIGHT)
 
-#ifndef NONET
 static UINT32 localservercount;
 
 static void M_HandleServerPage(INT32 choice)
@@ -11292,6 +11265,8 @@ static void M_DrawConnectMenu(void)
 			V_DrawSmallString(currentMenu->x+202, S_LINEY(i)+8, globalflags, "\x85" "Mod");
 		if (serverlist[slindex].info.cheatsenabled)
 			V_DrawSmallString(currentMenu->x+222, S_LINEY(i)+8, globalflags, "\x83" "Cheats");
+		if (Net_IsNodeIPv6(serverlist[slindex].node))
+			V_DrawSmallString(currentMenu->x+252, S_LINEY(i)+8, globalflags, "\x84" "IPv6");
 
 		V_DrawSmallString(currentMenu->x, S_LINEY(i)+8, globalflags,
 		                     va("Ping: %u", (UINT32)LONG(serverlist[slindex].info.time)));
@@ -11380,11 +11355,9 @@ static int ServerListEntryComparator_modified(const void *entry1, const void *en
 	// Default to strcmp.
 	return strcmp(sa->info.servername, sb->info.servername);
 }
-#endif
 
 void M_SortServerList(void)
 {
-#ifndef NONET
 	switch(cv_serversort.value)
 	{
 	case 0:		// Ping.
@@ -11406,10 +11379,8 @@ void M_SortServerList(void)
 		qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_gametypename);
 		break;
 	}
-#endif
 }
 
-#ifndef NONET
 #ifdef UPDATE_ALERT
 static boolean M_CheckMODVersion(int id)
 {
@@ -11608,7 +11579,6 @@ static void M_ChooseRoom(INT32 choice)
 	if (currentMenu == &MP_ConnectDef)
 		M_Refresh(0);
 }
-#endif //NONET
 
 //===========================================================================
 // Start Server Menu
@@ -11656,7 +11626,6 @@ static void M_DrawServerMenu(void)
 {
 	M_DrawGenericMenu();
 
-#ifndef NONET
 	// Room name
 	if (currentMenu == &MP_ServerDef)
 	{
@@ -11668,15 +11637,10 @@ static void M_DrawServerMenu(void)
 			V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + MP_ServerMenu[mp_server_room].alphaKey,
 			                         V_YELLOWMAP, room_list[menuRoomIndex].name);
 	}
-#endif
 
 	if (cv_nextmap.value)
 	{
-#ifndef NONET
 #define imgheight MP_ServerMenu[mp_server_levelgt].alphaKey
-#else
-#define imgheight 100
-#endif
 		patch_t *PictureOfLevel;
 		lumpnum_t lumpnum;
 		char headerstr[40];
@@ -11728,7 +11692,6 @@ static void M_ServerOptions(INT32 choice)
 {
 	(void)choice;
 
-#ifndef NONET
 	if ((splitscreen && !netgame) || currentMenu == &MP_SplitServerDef)
 	{
 		OP_ServerOptionsMenu[ 1].status = IT_GRAYEDOUT; // Server name
@@ -11749,7 +11712,6 @@ static void M_ServerOptions(INT32 choice)
 		OP_ServerOptionsMenu[37].status = IT_STRING | IT_CVAR;
 		OP_ServerOptionsMenu[38].status = IT_STRING | IT_CVAR;
 	}
-#endif
 
 	/* Disable fading because of different menu head. */
 	if (currentMenu == &OP_MainDef)/* from Options menu */
@@ -11761,7 +11723,6 @@ static void M_ServerOptions(INT32 choice)
 	M_SetupNextMenu(&OP_ServerOptionsDef);
 }
 
-#ifndef NONET
 static void M_StartServerMenu(INT32 choice)
 {
 	(void)choice;
@@ -11999,7 +11960,7 @@ static void M_HandleConnectIP(INT32 choice)
 			// Rudimentary number and period enforcing - also allows letters so hostnames can be used instead
 			// and square brackets for RFC 2732 IPv6 addresses
 			if ((choice >= '-' && choice <= ':') ||
-					(choice == '[' || choice == ']') ||
+					(choice == '[' || choice == ']' || choice == '%') ||
 					(choice >= 'A' && choice <= 'Z') ||
 					(choice >= 'a' && choice <= 'z'))
 			{
@@ -12028,7 +11989,6 @@ static void M_HandleConnectIP(INT32 choice)
 			M_ClearMenus(true);
 	}
 }
-#endif //!NONET
 
 // ========================
 // MULTIPLAYER PLAYER SETUP
@@ -12690,11 +12650,7 @@ static void M_SetupMultiPlayer(INT32 choice)
 	else
 		MP_PlayerSetupMenu[1].status = (IT_KEYHANDLER|IT_STRING);
 
-	// ditto with colour
-	if (Playing() && G_GametypeHasTeams())
-		MP_PlayerSetupMenu[2].status = (IT_GRAYEDOUT);
-	else
-		MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER|IT_STRING);
+	MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER|IT_STRING);
 
 	multi_spr2 = P_GetSkinSprite2(&skins[setupm_fakeskin], SPR2_WALK, NULL);
 
@@ -12709,7 +12665,7 @@ static void M_SetupMultiPlayer2(INT32 choice)
 
 	multi_frame = 0;
 	multi_tics = 4*FRACUNIT;
-	
+
 	strcpy (setupm_name, cv_playername2.string);
 
 	// set for splitscreen secondary player
@@ -12735,11 +12691,7 @@ static void M_SetupMultiPlayer2(INT32 choice)
 	else
 		MP_PlayerSetupMenu[1].status = (IT_KEYHANDLER | IT_STRING);
 
-	// ditto with colour
-	if (Playing() && G_GametypeHasTeams())
-		MP_PlayerSetupMenu[2].status = (IT_GRAYEDOUT);
-	else
-		MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER|IT_STRING);
+	MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER|IT_STRING);
 
 	multi_spr2 = P_GetSkinSprite2(&skins[setupm_fakeskin], SPR2_WALK, NULL);
 
@@ -13228,23 +13180,23 @@ static void M_Setup1PControlsMenu(INT32 choice)
 	currentMenu->lastOn = itemOn;
 
 	// Unhide the nine non-P2 controls and their headers
-	//OP_ChangeControlsMenu[18+0].status = IT_HEADER;
-	//OP_ChangeControlsMenu[18+1].status = IT_SPACE;
+	//OP_ChangeControlsMenu[19+0].status = IT_HEADER;
+	//OP_ChangeControlsMenu[19+1].status = IT_SPACE;
 	// ...
-	OP_ChangeControlsMenu[18+2].status = IT_CALL|IT_STRING2;
-	OP_ChangeControlsMenu[18+3].status = IT_CALL|IT_STRING2;
-	OP_ChangeControlsMenu[18+4].status = IT_CALL|IT_STRING2;
-	OP_ChangeControlsMenu[18+5].status = IT_CALL|IT_STRING2;
-	OP_ChangeControlsMenu[18+6].status = IT_CALL|IT_STRING2;
-	//OP_ChangeControlsMenu[18+7].status = IT_CALL|IT_STRING2;
-	//OP_ChangeControlsMenu[18+8].status = IT_CALL|IT_STRING2;
-	OP_ChangeControlsMenu[18+9].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[19+2].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[19+3].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[19+4].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[19+5].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[19+6].status = IT_CALL|IT_STRING2;
+	//OP_ChangeControlsMenu[19+7].status = IT_CALL|IT_STRING2;
+	//OP_ChangeControlsMenu[19+8].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[19+9].status = IT_CALL|IT_STRING2;
 	// ...
-	OP_ChangeControlsMenu[28+0].status = IT_HEADER;
-	OP_ChangeControlsMenu[28+1].status = IT_SPACE;
+	OP_ChangeControlsMenu[29+0].status = IT_HEADER;
+	OP_ChangeControlsMenu[29+1].status = IT_SPACE;
 	// ...
-	OP_ChangeControlsMenu[28+2].status = IT_CALL|IT_STRING2;
-	OP_ChangeControlsMenu[28+3].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[29+2].status = IT_CALL|IT_STRING2;
+	OP_ChangeControlsMenu[29+3].status = IT_CALL|IT_STRING2;
 
 	OP_ChangeControlsDef.prevMenu = &OP_P1ControlsDef;
 	OP_ChangeControlsDef.menuid &= ~(((1 << MENUBITS) - 1) << MENUBITS); // remove second level
@@ -13260,23 +13212,23 @@ static void M_Setup2PControlsMenu(INT32 choice)
 	currentMenu->lastOn = itemOn;
 
 	// Hide the nine non-P2 controls and their headers
-	//OP_ChangeControlsMenu[18+0].status = IT_GRAYEDOUT2;
-	//OP_ChangeControlsMenu[18+1].status = IT_GRAYEDOUT2;
+	//OP_ChangeControlsMenu[19+0].status = IT_GRAYEDOUT2;
+	//OP_ChangeControlsMenu[19+1].status = IT_GRAYEDOUT2;
 	// ...
-	OP_ChangeControlsMenu[18+2].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[18+3].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[18+4].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[18+5].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[18+6].status = IT_GRAYEDOUT2;
-	//OP_ChangeControlsMenu[18+7].status = IT_GRAYEDOUT2;
-	//OP_ChangeControlsMenu[18+8].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[18+9].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[19+2].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[19+3].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[19+4].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[19+5].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[19+6].status = IT_GRAYEDOUT2;
+	//OP_ChangeControlsMenu[19+7].status = IT_GRAYEDOUT2;
+	//OP_ChangeControlsMenu[19+8].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[19+9].status = IT_GRAYEDOUT2;
 	// ...
-	OP_ChangeControlsMenu[28+0].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[28+1].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[29+0].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[29+1].status = IT_GRAYEDOUT2;
 	// ...
-	OP_ChangeControlsMenu[28+2].status = IT_GRAYEDOUT2;
-	OP_ChangeControlsMenu[28+3].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[29+2].status = IT_GRAYEDOUT2;
+	OP_ChangeControlsMenu[29+3].status = IT_GRAYEDOUT2;
 
 	OP_ChangeControlsDef.prevMenu = &OP_P2ControlsDef;
 	OP_ChangeControlsDef.menuid &= ~(((1 << MENUBITS) - 1) << MENUBITS); // remove second level
diff --git a/src/m_menu.h b/src/m_menu.h
index c925c7f49c775d4a6e0becd7e2c5eb03d60784e4..b8fe3b808928b81c86bf3c09731e557f4b113616 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -20,7 +20,7 @@
 #include "command.h"
 #include "f_finale.h" // for ttmode_enum
 #include "i_threads.h"
-#include "mserv.h"
+#include "netcode/mserv.h"
 #include "r_things.h" // for SKINNAMESIZE
 
 // Compatibility with old-style named NiGHTS replay files.
@@ -74,7 +74,7 @@ typedef enum
 	MN_MP_SERVER,
 	MN_MP_CONNECT,
 	MN_MP_ROOM,
-	MN_MP_PLAYERSETUP, // MP_PlayerSetupDef shared with SPLITSCREEN if #defined NONET
+	MN_MP_PLAYERSETUP,
 	MN_MP_SERVER_OPTIONS,
 
 	// Options
diff --git a/src/m_misc.c b/src/m_misc.c
index f547f5c41ac8fb0fea405f8ed760ffe4070fdeb2..ce332910dc8ccf78a8c751adfc96c28c3721aea4 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -989,7 +989,7 @@ static inline boolean M_PNGLib(void)
 
 static void M_PNGFrame(png_structp png_ptr, png_infop png_info_ptr, png_bytep png_buf)
 {
-	png_uint_16 downscale = apng_downscale ? vid.dupx : 1;
+	png_uint_16 downscale = apng_downscale ? vid.dup : 1;
 
 	png_uint_32 pitch = png_get_rowbytes(png_ptr, png_info_ptr);
 	PNG_CONST png_uint_32 width = vid.width / downscale;
@@ -1055,7 +1055,7 @@ static boolean M_SetupaPNG(png_const_charp filename, png_bytep pal)
 
 	apng_downscale = (!!cv_apng_downscale.value);
 
-	downscale = apng_downscale ? vid.dupx : 1;
+	downscale = apng_downscale ? vid.dup : 1;
 
 	apng_FILE = fopen(filename,"wb+"); // + mode for reading
 	if (!apng_FILE)
@@ -2207,430 +2207,11 @@ char *sizeu5(size_t num)
 	return sizeu5_buf;
 }
 
-#if defined (__GNUC__) && defined (__i386__) // from libkwave, under GPL
-// Alam: note libkwave memcpy code comes from mplayer's libvo/aclib_template.c, r699
-
-/* for small memory blocks (<256 bytes) this version is faster */
-#define small_memcpy(dest,src,n)\
-{\
-register unsigned long int dummy;\
-__asm__ __volatile__(\
-	"cld\n\t"\
-	"rep; movsb"\
-	:"=&D"(dest), "=&S"(src), "=&c"(dummy)\
-	:"0" (dest), "1" (src),"2" (n)\
-	: "memory", "cc");\
-}
-/* linux kernel __memcpy (from: /include/asm/string.h) */
-ATTRINLINE static FUNCINLINE void *__memcpy (void *dest, const void * src, size_t n)
-{
-	int d0, d1, d2;
-
-	if ( n < 4 )
-	{
-		small_memcpy(dest, src, n);
-	}
-	else
-	{
-		__asm__ __volatile__ (
-			"rep ; movsl;"
-			"testb $2,%b4;"
-			"je 1f;"
-			"movsw;"
-			"1:\ttestb $1,%b4;"
-			"je 2f;"
-			"movsb;"
-			"2:"
-		: "=&c" (d0), "=&D" (d1), "=&S" (d2)
-		:"0" (n/4), "q" (n),"1" ((long) dest),"2" ((long) src)
-		: "memory");
-	}
-
-	return dest;
-}
-
-#define SSE_MMREG_SIZE 16
-#define MMX_MMREG_SIZE 8
-
-#define MMX1_MIN_LEN 0x800  /* 2K blocks */
-#define MIN_LEN 0x40  /* 64-byte blocks */
-
-/* SSE note: i tried to move 128 bytes a time instead of 64 but it
-didn't make any measureable difference. i'm using 64 for the sake of
-simplicity. [MF] */
-static /*FUNCTARGET("sse2")*/ void *sse_cpy(void * dest, const void * src, size_t n)
-{
-	void *retval = dest;
-	size_t i;
-
-	/* PREFETCH has effect even for MOVSB instruction ;) */
-	__asm__ __volatile__ (
-		"prefetchnta (%0);"
-		"prefetchnta 32(%0);"
-		"prefetchnta 64(%0);"
-		"prefetchnta 96(%0);"
-		"prefetchnta 128(%0);"
-		"prefetchnta 160(%0);"
-		"prefetchnta 192(%0);"
-		"prefetchnta 224(%0);"
-		"prefetchnta 256(%0);"
-		"prefetchnta 288(%0);"
-		: : "r" (src) );
-
-	if (n >= MIN_LEN)
-	{
-		register unsigned long int delta;
-		/* Align destinition to MMREG_SIZE -boundary */
-		delta = ((unsigned long int)dest)&(SSE_MMREG_SIZE-1);
-		if (delta)
-		{
-			delta=SSE_MMREG_SIZE-delta;
-			n -= delta;
-			small_memcpy(dest, src, delta);
-		}
-		i = n >> 6; /* n/64 */
-		n&=63;
-		if (((unsigned long)src) & 15)
-		/* if SRC is misaligned */
-		 for (; i>0; i--)
-		 {
-			__asm__ __volatile__ (
-				"prefetchnta 320(%0);"
-				"prefetchnta 352(%0);"
-				"movups (%0), %%xmm0;"
-				"movups 16(%0), %%xmm1;"
-				"movups 32(%0), %%xmm2;"
-				"movups 48(%0), %%xmm3;"
-				"movntps %%xmm0, (%1);"
-				"movntps %%xmm1, 16(%1);"
-				"movntps %%xmm2, 32(%1);"
-				"movntps %%xmm3, 48(%1);"
-			:: "r" (src), "r" (dest) : "memory");
-			src = (const unsigned char *)src + 64;
-			dest = (unsigned char *)dest + 64;
-		}
-		else
-			/*
-			   Only if SRC is aligned on 16-byte boundary.
-			   It allows to use movaps instead of movups, which required data
-			   to be aligned or a general-protection exception (#GP) is generated.
-			*/
-		 for (; i>0; i--)
-		 {
-			__asm__ __volatile__ (
-				"prefetchnta 320(%0);"
-				"prefetchnta 352(%0);"
-				"movaps (%0), %%xmm0;"
-				"movaps 16(%0), %%xmm1;"
-				"movaps 32(%0), %%xmm2;"
-				"movaps 48(%0), %%xmm3;"
-				"movntps %%xmm0, (%1);"
-				"movntps %%xmm1, 16(%1);"
-				"movntps %%xmm2, 32(%1);"
-				"movntps %%xmm3, 48(%1);"
-			:: "r" (src), "r" (dest) : "memory");
-			src = ((const unsigned char *)src) + 64;
-			dest = ((unsigned char *)dest) + 64;
-		}
-		/* since movntq is weakly-ordered, a "sfence"
-		 * is needed to become ordered again. */
-		__asm__ __volatile__ ("sfence":::"memory");
-		/* enables to use FPU */
-		__asm__ __volatile__ ("emms":::"memory");
-	}
-	/*
-	 *	Now do the tail of the block
-	 */
-	if (n) __memcpy(dest, src, n);
-	return retval;
-}
-
-static FUNCTARGET("mmx") void *mmx2_cpy(void *dest, const void *src, size_t n)
-{
-	void *retval = dest;
-	size_t i;
-
-	/* PREFETCH has effect even for MOVSB instruction ;) */
-	__asm__ __volatile__ (
-		"prefetchnta (%0);"
-		"prefetchnta 32(%0);"
-		"prefetchnta 64(%0);"
-		"prefetchnta 96(%0);"
-		"prefetchnta 128(%0);"
-		"prefetchnta 160(%0);"
-		"prefetchnta 192(%0);"
-		"prefetchnta 224(%0);"
-		"prefetchnta 256(%0);"
-		"prefetchnta 288(%0);"
-	: : "r" (src));
-
-	if (n >= MIN_LEN)
-	{
-		register unsigned long int delta;
-		/* Align destinition to MMREG_SIZE -boundary */
-		delta = ((unsigned long int)dest)&(MMX_MMREG_SIZE-1);
-		if (delta)
-		{
-			delta=MMX_MMREG_SIZE-delta;
-			n -= delta;
-			small_memcpy(dest, src, delta);
-		}
-		i = n >> 6; /* n/64 */
-		n&=63;
-		for (; i>0; i--)
-		{
-			__asm__ __volatile__ (
-				"prefetchnta 320(%0);"
-				"prefetchnta 352(%0);"
-				"movq (%0), %%mm0;"
-				"movq 8(%0), %%mm1;"
-				"movq 16(%0), %%mm2;"
-				"movq 24(%0), %%mm3;"
-				"movq 32(%0), %%mm4;"
-				"movq 40(%0), %%mm5;"
-				"movq 48(%0), %%mm6;"
-				"movq 56(%0), %%mm7;"
-				"movntq %%mm0, (%1);"
-				"movntq %%mm1, 8(%1);"
-				"movntq %%mm2, 16(%1);"
-				"movntq %%mm3, 24(%1);"
-				"movntq %%mm4, 32(%1);"
-				"movntq %%mm5, 40(%1);"
-				"movntq %%mm6, 48(%1);"
-				"movntq %%mm7, 56(%1);"
-			:: "r" (src), "r" (dest) : "memory");
-			src = ((const unsigned char *)src) + 64;
-			dest = ((unsigned char *)dest) + 64;
-		}
-		/* since movntq is weakly-ordered, a "sfence"
-		* is needed to become ordered again. */
-		__asm__ __volatile__ ("sfence":::"memory");
-		__asm__ __volatile__ ("emms":::"memory");
-	}
-	/*
-	 *	Now do the tail of the block
-	 */
-	if (n) __memcpy(dest, src, n);
-	return retval;
-}
-
-static FUNCTARGET("mmx") void *mmx1_cpy(void *dest, const void *src, size_t n) //3DNOW
-{
-	void *retval = dest;
-	size_t i;
-
-	/* PREFETCH has effect even for MOVSB instruction ;) */
-	__asm__ __volatile__ (
-		"prefetch (%0);"
-		"prefetch 32(%0);"
-		"prefetch 64(%0);"
-		"prefetch 96(%0);"
-		"prefetch 128(%0);"
-		"prefetch 160(%0);"
-		"prefetch 192(%0);"
-		"prefetch 224(%0);"
-		"prefetch 256(%0);"
-		"prefetch 288(%0);"
-	: : "r" (src));
-
-	if (n >= MMX1_MIN_LEN)
-	{
-		register unsigned long int delta;
-		/* Align destinition to MMREG_SIZE -boundary */
-		delta = ((unsigned long int)dest)&(MMX_MMREG_SIZE-1);
-		if (delta)
-		{
-			delta=MMX_MMREG_SIZE-delta;
-			n -= delta;
-			small_memcpy(dest, src, delta);
-		}
-		i = n >> 6; /* n/64 */
-		n&=63;
-		for (; i>0; i--)
-		{
-			__asm__ __volatile__ (
-				"prefetch 320(%0);"
-				"prefetch 352(%0);"
-				"movq (%0), %%mm0;"
-				"movq 8(%0), %%mm1;"
-				"movq 16(%0), %%mm2;"
-				"movq 24(%0), %%mm3;"
-				"movq 32(%0), %%mm4;"
-				"movq 40(%0), %%mm5;"
-				"movq 48(%0), %%mm6;"
-				"movq 56(%0), %%mm7;"
-				"movq %%mm0, (%1);"
-				"movq %%mm1, 8(%1);"
-				"movq %%mm2, 16(%1);"
-				"movq %%mm3, 24(%1);"
-				"movq %%mm4, 32(%1);"
-				"movq %%mm5, 40(%1);"
-				"movq %%mm6, 48(%1);"
-				"movq %%mm7, 56(%1);"
-			:: "r" (src), "r" (dest) : "memory");
-			src = ((const unsigned char *)src) + 64;
-			dest = ((unsigned char *)dest) + 64;
-		}
-		__asm__ __volatile__ ("femms":::"memory"); // same as mmx_cpy() but with a femms
-	}
-	/*
-	 *	Now do the tail of the block
-	 */
-	if (n) __memcpy(dest, src, n);
-	return retval;
-}
-#endif
-
-// Alam: why? memcpy may be __cdecl/_System and our code may be not the same type
-static void *cpu_cpy(void *dest, const void *src, size_t n)
+void *M_Memcpy(void *dest, const void *src, size_t n)
 {
-	if (src == NULL)
-	{
-		CONS_Debug(DBG_MEMORY, "Memcpy from 0x0?!: %p %p %s\n", dest, src, sizeu1(n));
-		return dest;
-	}
-
-	if(dest == NULL)
-	{
-		CONS_Debug(DBG_MEMORY, "Memcpy to 0x0?!: %p %p %s\n", dest, src, sizeu1(n));
-		return dest;
-	}
-
 	return memcpy(dest, src, n);
 }
 
-static /*FUNCTARGET("mmx")*/ void *mmx_cpy(void *dest, const void *src, size_t n)
-{
-#if defined (_MSC_VER) && defined (_X86_)
-	_asm
-	{
-		mov ecx, [n]
-		mov esi, [src]
-		mov edi, [dest]
-		shr ecx, 6 // mit mmx: 64bytes per iteration
-		jz lower_64 // if lower than 64 bytes
-		loop_64: // MMX transfers multiples of 64bytes
-		movq mm0,  0[ESI] // read sources
-		movq mm1,  8[ESI]
-		movq mm2, 16[ESI]
-		movq mm3, 24[ESI]
-		movq mm4, 32[ESI]
-		movq mm5, 40[ESI]
-		movq mm6, 48[ESI]
-		movq mm7, 56[ESI]
-
-		movq  0[EDI], mm0 // write destination
-		movq  8[EDI], mm1
-		movq 16[EDI], mm2
-		movq 24[EDI], mm3
-		movq 32[EDI], mm4
-		movq 40[EDI], mm5
-		movq 48[EDI], mm6
-		movq 56[EDI], mm7
-
-		add esi, 64
-		add edi, 64
-		dec ecx
-		jnz loop_64
-		emms // close mmx operation
-		lower_64:// transfer rest of buffer
-		mov ebx,esi
-		sub ebx,src
-		mov ecx,[n]
-		sub ecx,ebx
-		shr ecx, 3 // multiples of 8 bytes
-		jz lower_8
-		loop_8:
-		movq  mm0, [esi] // read source
-		movq [edi], mm0 // write destination
-		add esi, 8
-		add edi, 8
-		dec ecx
-		jnz loop_8
-		emms // close mmx operation
-		lower_8:
-		mov ebx,esi
-		sub ebx,src
-		mov ecx,[n]
-		sub ecx,ebx
-		rep movsb
-		mov eax, [dest] // return dest
-	}
-#elif defined (__GNUC__) && defined (__i386__)
-	void *retval = dest;
-	size_t i;
-
-	if (n >= MMX1_MIN_LEN)
-	{
-		register unsigned long int delta;
-		/* Align destinition to MMREG_SIZE -boundary */
-		delta = ((unsigned long int)dest)&(MMX_MMREG_SIZE-1);
-		if (delta)
-		{
-			delta=MMX_MMREG_SIZE-delta;
-			n -= delta;
-			small_memcpy(dest, src, delta);
-		}
-		i = n >> 6; /* n/64 */
-		n&=63;
-		for (; i>0; i--)
-		{
-			__asm__ __volatile__ (
-				"movq (%0), %%mm0;"
-				"movq 8(%0), %%mm1;"
-				"movq 16(%0), %%mm2;"
-				"movq 24(%0), %%mm3;"
-				"movq 32(%0), %%mm4;"
-				"movq 40(%0), %%mm5;"
-				"movq 48(%0), %%mm6;"
-				"movq 56(%0), %%mm7;"
-				"movq %%mm0, (%1);"
-				"movq %%mm1, 8(%1);"
-				"movq %%mm2, 16(%1);"
-				"movq %%mm3, 24(%1);"
-				"movq %%mm4, 32(%1);"
-				"movq %%mm5, 40(%1);"
-				"movq %%mm6, 48(%1);"
-				"movq %%mm7, 56(%1);"
-			:: "r" (src), "r" (dest) : "memory");
-			src = ((const unsigned char *)src) + 64;
-			dest = ((unsigned char *)dest) + 64;
-		}
-		__asm__ __volatile__ ("emms":::"memory");
-	}
-	/*
-	 *	Now do the tail of the block
-	 */
-	if (n) __memcpy(dest, src, n);
-	return retval;
-#else
-	return cpu_cpy(dest, src, n);
-#endif
-}
-
-void *(*M_Memcpy)(void* dest, const void* src, size_t n) = cpu_cpy;
-
-/** Memcpy that uses MMX, 3DNow, MMXExt or even SSE
-  * Do not use on overlapped memory, use memmove for that
-  */
-void M_SetupMemcpy(void)
-{
-#if defined (__GNUC__) && defined (__i386__)
-	if (R_SSE2)
-		M_Memcpy = sse_cpy;
-	else if (R_MMXExt)
-		M_Memcpy = mmx2_cpy;
-	else if (R_3DNow)
-		M_Memcpy = mmx1_cpy;
-	else
-#endif
-	if (R_MMX)
-		M_Memcpy = mmx_cpy;
-#if 0
-	M_Memcpy = cpu_cpy;
-#endif
-}
-
 /** Return the appropriate message for a file error or end of file.
 */
 const char *M_FileError(FILE *fp)
@@ -2805,3 +2386,17 @@ boolean M_IsStringEmpty(const char *s)
 
 	return true;
 }
+
+// Rounds off floating numbers and checks for 0 - 255 bounds
+int M_RoundUp(double number)
+{
+	if (number > 255.0l)
+		return 255;
+	if (number < 0.0l)
+		return 0;
+
+	if ((int)number <= (int)(number - 0.5f))
+		return (int)number + 1;
+
+	return (int)number;
+}
diff --git a/src/m_misc.h b/src/m_misc.h
index 8cad7ba9a43353c0aaca9acf3688939ebf270775..753991e70465c075fa874ce7662d6aa6603e7b6a 100644
--- a/src/m_misc.h
+++ b/src/m_misc.h
@@ -112,6 +112,9 @@ boolean M_IsStringEmpty(const char *s);
 // counting bits, for weapon ammo code, usually
 FUNCMATH UINT8 M_CountBits(UINT32 num, UINT8 size);
 
+// Rounds off floating numbers and checks for 0 - 255 bounds
+int M_RoundUp(double number);
+
 #include "w_wad.h"
 extern char configfile[MAX_WADPATH];
 
diff --git a/src/m_perfstats.c b/src/m_perfstats.c
index 17e026b3e181714acee6ff4ca7887a1b199f1b77..1511859324059a5ce8e5836ff99dbdbe841c02a1 100644
--- a/src/m_perfstats.c
+++ b/src/m_perfstats.c
@@ -12,7 +12,7 @@
 #include "m_perfstats.h"
 #include "v_video.h"
 #include "i_video.h"
-#include "d_netcmd.h"
+#include "netcode/d_netcmd.h"
 #include "r_main.h"
 #include "i_system.h"
 #include "z_zone.h"
diff --git a/src/netcode/CMakeLists.txt b/src/netcode/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..69b976d17e404e6e389bdf36079c72dac2480cdc
--- /dev/null
+++ b/src/netcode/CMakeLists.txt
@@ -0,0 +1,15 @@
+target_sources(SRB2SDL2 PRIVATE
+	d_clisrv.c
+	server_connection.c
+	client_connection.c
+	tic_command.c
+	net_command.c
+	gamestate.c
+	commands.c
+	d_net.c
+	d_netcmd.c
+	d_netfil.c
+	http-mserv.c
+	i_tcp.c
+	mserv.c
+)
diff --git a/src/netcode/Sourcefile b/src/netcode/Sourcefile
new file mode 100644
index 0000000000000000000000000000000000000000..7c0354714f9c3739c9b0920b0c7db6466dcef2c3
--- /dev/null
+++ b/src/netcode/Sourcefile
@@ -0,0 +1,13 @@
+d_clisrv.c
+server_connection.c
+client_connection.c
+tic_command.c
+net_command.c
+gamestate.c
+commands.c
+d_net.c
+d_netcmd.c
+d_netfil.c
+http-mserv.c
+i_tcp.c
+mserv.c
diff --git a/src/netcode/client_connection.c b/src/netcode/client_connection.c
new file mode 100644
index 0000000000000000000000000000000000000000..907021e7dafddfb3f31b2a2dd850f15d4890a16e
--- /dev/null
+++ b/src/netcode/client_connection.c
@@ -0,0 +1,1178 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 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  client_connection.h
+/// \brief Client connection handling
+
+#include "client_connection.h"
+#include "gamestate.h"
+#include "d_clisrv.h"
+#include "d_netfil.h"
+#include "../d_main.h"
+#include "../f_finale.h"
+#include "../g_game.h"
+#include "../g_input.h"
+#include "i_net.h"
+#include "../i_system.h"
+#include "../i_time.h"
+#include "../i_video.h"
+#include "../keys.h"
+#include "../m_menu.h"
+#include "../m_misc.h"
+#include "../snake.h"
+#include "../s_sound.h"
+#include "../v_video.h"
+#include "../y_inter.h"
+#include "../z_zone.h"
+#include "../doomtype.h"
+#include "../doomstat.h"
+#if defined (__GNUC__) || defined (__unix__)
+#include <unistd.h>
+#endif
+
+cl_mode_t cl_mode = CL_SEARCHING;
+static UINT16 cl_lastcheckedfilecount = 0;	// used for full file list
+boolean serverisfull = false; // lets us be aware if the server was full after we check files, but before downloading, so we can ask if the user still wants to download or not
+tic_t firstconnectattempttime = 0;
+UINT8 mynode;
+static void *snake = NULL;
+
+static void CL_DrawConnectionStatusBox(void)
+{
+	M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
+	if (cl_mode != CL_CONFIRMCONNECT)
+		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
+}
+
+//
+// CL_DrawConnectionStatus
+//
+// Keep the local client informed of our status.
+//
+static inline void CL_DrawConnectionStatus(void)
+{
+	INT32 ccstime = I_GetTime();
+
+	// Draw background fade
+	V_DrawFadeScreen(0xFF00, 16); // force default
+
+	if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_LOADFILES)
+	{
+		INT32 animtime = ((ccstime / 4) & 15) + 16;
+		UINT8 palstart;
+		const char *cltext;
+
+		// Draw the bottom box.
+		CL_DrawConnectionStatusBox();
+
+		if (cl_mode == CL_SEARCHING)
+			palstart = 32; // Red
+		else if (cl_mode == CL_CONFIRMCONNECT)
+			palstart = 48; // Orange
+		else
+			palstart = 96; // Green
+
+		if (!(cl_mode == CL_DOWNLOADSAVEGAME && lastfilenum != -1))
+			for (INT32 i = 0; i < 16; ++i) // 15 pal entries total.
+				V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-16, 16, 8, palstart + ((animtime - i) & 15));
+
+		switch (cl_mode)
+		{
+			case CL_DOWNLOADSAVEGAME:
+				if (fileneeded && lastfilenum != -1)
+				{
+					UINT32 currentsize = fileneeded[lastfilenum].currentsize;
+					UINT32 totalsize = fileneeded[lastfilenum].totalsize;
+					INT32 dldlength;
+
+					cltext = M_GetText("Downloading game state...");
+					Net_GetNetStat();
+
+					dldlength = (INT32)((currentsize/(double)totalsize) * 256);
+					if (dldlength > 256)
+						dldlength = 256;
+					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
+					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
+
+					V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
+						va(" %4uK/%4uK",currentsize>>10,totalsize>>10));
+
+					V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
+						va("%3.1fK/s ", ((double)getbps)/1024));
+				}
+				else
+					cltext = M_GetText("Waiting to download game state...");
+				break;
+			case CL_ASKFULLFILELIST:
+			case CL_CHECKFILES:
+				cltext = M_GetText("Checking server addon list...");
+				break;
+			case CL_CONFIRMCONNECT:
+				cltext = "";
+				break;
+			case CL_LOADFILES:
+				cltext = M_GetText("Loading server addons...");
+				break;
+			case CL_ASKJOIN:
+			case CL_WAITJOINRESPONSE:
+				if (serverisfull)
+					cltext = M_GetText("Server full, waiting for a slot...");
+				else
+					cltext = M_GetText("Requesting to join...");
+				break;
+			default:
+				cltext = M_GetText("Connecting to server...");
+				break;
+		}
+		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, cltext);
+	}
+	else
+	{
+		if (cl_mode == CL_LOADFILES)
+		{
+			INT32 totalfileslength;
+			INT32 loadcompletednum = 0;
+
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
+
+			// ima just count files here
+			if (fileneeded)
+			{
+				for (INT32 i = 0; i < fileneedednum; i++)
+					if (fileneeded[i].status == FS_OPEN)
+						loadcompletednum++;
+			}
+
+			// Loading progress
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, "Loading server addons...");
+			totalfileslength = (INT32)((loadcompletednum/(double)(fileneedednum)) * 256);
+			M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
+			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
+			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, totalfileslength, 8, 96);
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
+				va(" %2u/%2u Files",loadcompletednum,fileneedednum));
+		}
+		else if (lastfilenum != -1)
+		{
+			INT32 dldlength;
+			static char tempname[28];
+			fileneeded_t *file;
+			char *filename;
+
+			if (snake)
+				Snake_Draw(snake);
+
+			// Draw the bottom box.
+			CL_DrawConnectionStatusBox();
+
+			if (fileneeded)
+			{
+				file = &fileneeded[lastfilenum];
+				filename = file->filename;
+			}
+			else
+				return;
+
+			Net_GetNetStat();
+			dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256);
+			if (dldlength > 256)
+				dldlength = 256;
+			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
+			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
+
+			memset(tempname, 0, sizeof(tempname));
+			// offset filename to just the name only part
+			filename += strlen(filename) - nameonlylength(filename);
+
+			if (strlen(filename) > sizeof(tempname)-1) // too long to display fully
+			{
+				size_t endhalfpos = strlen(filename)-10;
+				// display as first 14 chars + ... + last 10 chars
+				// which should add up to 27 if our math(s) is correct
+				snprintf(tempname, sizeof(tempname), "%.14s...%.10s", filename, filename+endhalfpos);
+			}
+			else // we can copy the whole thing in safely
+			{
+				strncpy(tempname, filename, sizeof(tempname)-1);
+			}
+
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
+				va(M_GetText("Downloading \"%s\""), tempname));
+			V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
+				va(" %4uK/%4uK",fileneeded[lastfilenum].currentsize>>10,file->totalsize>>10));
+			V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
+				va("%3.1fK/s ", ((double)getbps)/1024));
+		}
+		else
+		{
+			if (snake)
+				Snake_Draw(snake);
+
+			CL_DrawConnectionStatusBox();
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
+				M_GetText("Waiting to download files..."));
+		}
+	}
+}
+
+static boolean CL_AskFileList(INT32 firstfile)
+{
+	netbuffer->packettype = PT_TELLFILESNEEDED;
+	netbuffer->u.filesneedednum = firstfile;
+
+	return HSendPacket(servernode, false, 0, sizeof (INT32));
+}
+
+/** Sends a PT_CLIENTJOIN packet to the server
+  *
+  * \return True if the packet was successfully sent
+  *
+  */
+boolean CL_SendJoin(void)
+{
+	UINT8 localplayers = 1;
+	char const *player2name;
+	if (netgame)
+		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;
+
+	CleanupPlayerName(consoleplayer, cv_playername.zstring);
+	if (splitscreen)
+		CleanupPlayerName(1, cv_playername2.zstring); // 1 is a HACK? oh no
+
+	// Avoid empty string on bots to avoid softlocking in singleplayer
+	if (botingame)
+		player2name = strcmp(cv_playername.zstring, "Tails") == 0 ? "Tail" : "Tails";
+	else
+		player2name = cv_playername2.zstring;
+
+	strncpy(netbuffer->u.clientcfg.names[0], cv_playername.zstring, MAXPLAYERNAME);
+	strncpy(netbuffer->u.clientcfg.names[1], player2name, MAXPLAYERNAME);
+
+	return HSendPacket(servernode, true, 0, sizeof (clientconfig_pak));
+}
+
+static void SendAskInfo(INT32 node)
+{
+	const tic_t asktime = I_GetTime();
+	netbuffer->packettype = PT_ASKINFO;
+	netbuffer->u.askinfo.version = VERSION;
+	netbuffer->u.askinfo.time = (tic_t)LONG(asktime);
+
+	// Even if this never arrives due to the host being firewalled, we've
+	// now allowed traffic from the host to us in, so once the MS relays
+	// our address to the host, it'll be able to speak to us.
+	HSendPacket(node, false, 0, sizeof (askinfo_pak));
+}
+
+serverelem_t serverlist[MAXSERVERLIST];
+UINT32 serverlistcount = 0;
+
+#define FORCECLOSE 0x8000
+
+static void SL_ClearServerList(INT32 connectedserver)
+{
+	for (UINT32 i = 0; i < serverlistcount; i++)
+		if (connectedserver != serverlist[i].node)
+		{
+			Net_CloseConnection(serverlist[i].node|FORCECLOSE);
+			serverlist[i].node = 0;
+		}
+	serverlistcount = 0;
+}
+
+static UINT32 SL_SearchServer(INT32 node)
+{
+	for (UINT32 i = 0; i < serverlistcount; i++)
+		if (serverlist[i].node == node)
+			return i;
+
+	return UINT32_MAX;
+}
+
+static void SL_InsertServer(serverinfo_pak* info, SINT8 node)
+{
+	UINT32 i;
+
+	// search if not already on it
+	i = SL_SearchServer(node);
+	if (i == UINT32_MAX)
+	{
+		// not found add it
+		if (serverlistcount >= MAXSERVERLIST)
+			return; // list full
+
+		// 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->version != VERSION)
+				return; // Not same version.
+
+			if (info->subversion != SUBVERSION)
+				return; // Close, but no cigar.
+
+			if (strcmp(info->application, SRB2APPLICATION))
+				return; // That's a different mod
+		}
+
+		i = serverlistcount++;
+	}
+
+	serverlist[i].info = *info;
+	serverlist[i].node = node;
+
+	// resort server list
+	M_SortServerList();
+}
+
+#if defined (MASTERSERVER) && defined (HAVE_THREADS)
+struct Fetch_servers_ctx
+{
+	int room;
+	int id;
+};
+
+static void
+Fetch_servers_thread (struct Fetch_servers_ctx *ctx)
+{
+	msg_server_t *server_list;
+
+	server_list = GetShortServersList(ctx->room, ctx->id);
+
+	if (server_list)
+	{
+		I_lock_mutex(&ms_QueryId_mutex);
+		{
+			if (ctx->id != ms_QueryId)
+			{
+				free(server_list);
+				server_list = NULL;
+			}
+		}
+		I_unlock_mutex(ms_QueryId_mutex);
+
+		if (server_list)
+		{
+			I_lock_mutex(&m_menu_mutex);
+			{
+				if (m_waiting_mode == M_WAITING_SERVERS)
+					m_waiting_mode = M_NOT_WAITING;
+			}
+			I_unlock_mutex(m_menu_mutex);
+
+			I_lock_mutex(&ms_ServerList_mutex);
+			{
+				ms_ServerList = server_list;
+			}
+			I_unlock_mutex(ms_ServerList_mutex);
+		}
+	}
+
+	free(ctx);
+}
+#endif // defined (MASTERSERVER) && defined (HAVE_THREADS)
+
+void CL_QueryServerList (msg_server_t *server_list)
+{
+	for (INT32 i = 0; server_list[i].header.buffer[0]; i++)
+	{
+		// Make sure MS version matches our own, to
+		// thwart nefarious servers who lie to the MS.
+
+		// lol bruh, that version COMES from the servers
+		//if (strcmp(version, server_list[i].version) == 0)
+		{
+			INT32 node = I_NetMakeNodewPort(server_list[i].ip, server_list[i].port);
+			if (node == -1)
+				break; // no more node free
+			SendAskInfo(node);
+			// Force close the connection so that servers can't eat
+			// up nodes forever if we never get a reply back from them
+			// (usually when they've not forwarded their ports).
+			//
+			// Don't worry, we'll get in contact with the working
+			// servers again when they send SERVERINFO to us later!
+			//
+			// (Note: as a side effect this probably means every
+			// server in the list will probably be using the same node (e.g. node 1),
+			// not that it matters which nodes they use when
+			// the connections are closed afterwards anyway)
+			// -- Monster Iestyn 12/11/18
+			Net_CloseConnection(node|FORCECLOSE);
+		}
+	}
+}
+
+void CL_UpdateServerList(boolean internetsearch, INT32 room)
+{
+	(void)internetsearch;
+	(void)room;
+
+	SL_ClearServerList(0);
+
+	if (!netgame && I_NetOpenSocket)
+	{
+		if (I_NetOpenSocket())
+		{
+			netgame = true;
+			multiplayer = true;
+		}
+	}
+
+	// search for local servers
+	if (netgame)
+		SendAskInfo(BROADCASTADDR);
+
+#ifdef MASTERSERVER
+	if (internetsearch)
+	{
+#ifdef HAVE_THREADS
+		struct Fetch_servers_ctx *ctx;
+
+		ctx = malloc(sizeof *ctx);
+
+		// This called from M_Refresh so I don't use a mutex
+		m_waiting_mode = M_WAITING_SERVERS;
+
+		I_lock_mutex(&ms_QueryId_mutex);
+		{
+			ctx->id = ms_QueryId;
+		}
+		I_unlock_mutex(ms_QueryId_mutex);
+
+		ctx->room = room;
+
+		I_spawn_thread("fetch-servers", (I_thread_fn)Fetch_servers_thread, ctx);
+#else
+		msg_server_t *server_list;
+
+		server_list = GetShortServersList(room, 0);
+
+		if (server_list)
+		{
+			CL_QueryServerList(server_list);
+			free(server_list);
+		}
+#endif
+	}
+#endif // MASTERSERVER
+}
+
+static void M_ConfirmConnect(event_t *ev)
+{
+	if (ev->type == ev_keydown)
+	{
+		if (ev->key == ' ' || ev->key == 'y' || ev->key == KEY_ENTER || ev->key == KEY_JOY1)
+		{
+			if (totalfilesrequestednum > 0)
+			{
+				if (CL_SendFileRequest())
+				{
+					cl_mode = CL_DOWNLOADFILES;
+					Snake_Allocate(&snake);
+				}
+			}
+			else
+				cl_mode = CL_LOADFILES;
+
+			M_ClearMenus(true);
+		}
+		else if (ev->key == 'n' || ev->key == KEY_ESCAPE || ev->key == KEY_JOY1 + 3)
+		{
+			cl_mode = CL_ABORTED;
+			M_ClearMenus(true);
+		}
+	}
+}
+
+static boolean CL_FinishedFileList(void)
+{
+	INT32 i;
+	char *downloadsize = NULL;
+
+	//CONS_Printf(M_GetText("Checking files...\n"));
+	i = CL_CheckFiles();
+	if (i == 4) // still checking ...
+	{
+		return true;
+	}
+	else if (i == 3) // too many files
+	{
+		D_QuitNetGame();
+		CL_Reset();
+		D_StartTitle();
+		M_StartMessage(M_GetText(
+			"You have too many WAD files loaded\n"
+			"to add ones the server is using.\n"
+			"Please restart SRB2 before connecting.\n\n"
+			"Press ESC\n"
+		), NULL, MM_NOTHING);
+		return false;
+	}
+	else if (i == 2) // cannot join for some reason
+	{
+		D_QuitNetGame();
+		CL_Reset();
+		D_StartTitle();
+		M_StartMessage(M_GetText(
+			"You have the wrong addons loaded.\n\n"
+			"To play on this server, restart\n"
+			"the game and don't load any addons.\n"
+			"SRB2 will automatically add\n"
+			"everything you need when you join.\n\n"
+			"Press ESC\n"
+		), NULL, MM_NOTHING);
+		return false;
+	}
+	else if (i == 1)
+	{
+		if (serverisfull)
+		{
+			M_StartMessage(M_GetText(
+				"This server is full!\n"
+				"\n"
+				"You may load server addons (if any), and wait for a slot.\n"
+				"\n"
+				"Press ENTER to continue\nor ESC to cancel.\n\n"
+			), M_ConfirmConnect, MM_EVENTHANDLER);
+			cl_mode = CL_CONFIRMCONNECT;
+			curfadevalue = 0;
+		}
+		else
+			cl_mode = CL_LOADFILES;
+	}
+	else
+	{
+		// must download something
+		// can we, though?
+		if (!CL_CheckDownloadable()) // nope!
+		{
+			D_QuitNetGame();
+			CL_Reset();
+			D_StartTitle();
+			M_StartMessage(M_GetText(
+				"An error occurred when trying to\n"
+				"download missing addons.\n"
+				"(This is almost always a problem\n"
+				"with the server, not your game.)\n\n"
+				"See the console or log file\n"
+				"for additional details.\n\n"
+				"Press ESC\n"
+			), NULL, MM_NOTHING);
+			return false;
+		}
+
+		downloadcompletednum = 0;
+		downloadcompletedsize = 0;
+		totalfilesrequestednum = 0;
+		totalfilesrequestedsize = 0;
+
+		if (fileneeded == NULL)
+			I_Error("CL_FinishedFileList: fileneeded == NULL");
+
+		for (i = 0; i < fileneedednum; i++)
+			if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
+			{
+				totalfilesrequestednum++;
+				totalfilesrequestedsize += fileneeded[i].totalsize;
+			}
+
+		if (totalfilesrequestedsize>>20 >= 100)
+			downloadsize = Z_StrDup(va("%uM",totalfilesrequestedsize>>20));
+		else
+			downloadsize = Z_StrDup(va("%uK",totalfilesrequestedsize>>10));
+
+		if (serverisfull)
+			M_StartMessage(va(M_GetText(
+				"This server is full!\n"
+				"Download of %s additional content\nis required to join.\n"
+				"\n"
+				"You may download, load server addons,\nand wait for a slot.\n"
+				"\n"
+				"Press ENTER to continue\nor ESC to cancel.\n"
+			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
+		else
+			M_StartMessage(va(M_GetText(
+				"Download of %s additional content\nis required to join.\n"
+				"\n"
+				"Press ENTER to continue\nor ESC to cancel.\n"
+			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
+
+		Z_Free(downloadsize);
+		cl_mode = CL_CONFIRMCONNECT;
+		curfadevalue = 0;
+	}
+	return true;
+}
+
+static const char * InvalidServerReason (serverinfo_pak *info)
+{
+#define EOT "\nPress ESC\n"
+
+	// Magic number for new packet format
+	if (info->_255 != 255)
+	{
+		return
+			"Outdated server (version unknown).\n" EOT;
+	}
+
+	if (strncmp(info->application, SRB2APPLICATION, sizeof
+				info->application))
+	{
+		return va(
+				"%s cannot connect\n"
+				"to %s servers.\n" EOT,
+				SRB2APPLICATION,
+				info->application);
+	}
+
+	if (
+			info->packetversion != PACKETVERSION ||
+			info->version != VERSION ||
+			info->subversion != SUBVERSION
+	){
+		return va(
+				"Incompatible %s versions.\n"
+				"(server version %d.%d.%d)\n" EOT,
+				SRB2APPLICATION,
+				info->version / 100,
+				info->version % 100,
+				info->subversion);
+	}
+
+	switch (info->refusereason)
+	{
+		case REFUSE_BANNED:
+			return
+				"You have been banned\n"
+				"from the server.\n" EOT;
+		case REFUSE_JOINS_DISABLED:
+			return
+				"The server is not accepting\n"
+				"joins for the moment.\n" EOT;
+		case REFUSE_SLOTS_FULL:
+			return va(
+					"Maximum players reached: %d\n" EOT,
+					info->maxplayer - D_NumBots());
+		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.
+  * \return False if the connection was aborted
+  * \sa CL_ServerConnectionTicker
+  * \sa CL_ConnectToServer
+  *
+  */
+static boolean CL_ServerConnectionSearchTicker(tic_t *asksent)
+{
+	INT32 i;
+
+	// serverlist is updated by GetPackets
+	if (serverlistcount > 0)
+	{
+		// This can be a response to our broadcast request
+		if (servernode == -1 || servernode >= MAXNETNODES)
+		{
+			i = 0;
+			servernode = serverlist[i].node;
+			CONS_Printf(M_GetText("Found, "));
+		}
+		else
+		{
+			i = SL_SearchServer(servernode);
+			if (i < 0)
+				return true;
+		}
+
+		if (client)
+		{
+			serverinfo_pak *info = &serverlist[i].info;
+
+			if (info->refusereason == REFUSE_SLOTS_FULL)
+				serverisfull = true;
+			else
+			{
+				const char *reason = InvalidServerReason(info);
+
+				// Quit here rather than downloading files
+				// and being refused later.
+				if (reason)
+				{
+					char *message = Z_StrDup(reason);
+					D_QuitNetGame();
+					CL_Reset();
+					D_StartTitle();
+					M_StartMessage(message, NULL, MM_NOTHING);
+					Z_Free(message);
+					return false;
+				}
+			}
+
+			D_ParseFileneeded(info->fileneedednum, info->fileneeded, 0);
+
+			if (info->flags & SV_LOTSOFADDONS)
+			{
+				cl_mode = CL_ASKFULLFILELIST;
+				cl_lastcheckedfilecount = 0;
+				return true;
+			}
+
+			cl_mode = CL_CHECKFILES;
+		}
+		else
+		{
+			cl_mode = CL_ASKJOIN; // files need not be checked for the server.
+			*asksent = 0;
+		}
+
+		return true;
+	}
+
+	// Ask the info to the server (askinfo packet)
+	if (*asksent + NEWTICRATE < I_GetTime())
+	{
+		SendAskInfo(servernode);
+		*asksent = I_GetTime();
+	}
+
+	return true;
+}
+
+/** Called by CL_ConnectToServer
+  *
+  * \param tmpsave The name of the gamestate file???
+  * \param oldtic Used for knowing when to poll events and redraw
+  * \param asksent ???
+  * \return False if the connection was aborted
+  * \sa CL_ServerConnectionSearchTicker
+  * \sa CL_ConnectToServer
+  *
+  */
+static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic_t *asksent)
+{
+	boolean waitmore;
+
+	switch (cl_mode)
+	{
+		case CL_SEARCHING:
+			if (!CL_ServerConnectionSearchTicker(asksent))
+				return false;
+			break;
+
+		case CL_ASKFULLFILELIST:
+			if (cl_lastcheckedfilecount == UINT16_MAX) // All files retrieved
+				cl_mode = CL_CHECKFILES;
+			else if (fileneedednum != cl_lastcheckedfilecount || I_GetTime() >= *asksent)
+			{
+				if (CL_AskFileList(fileneedednum))
+				{
+					cl_lastcheckedfilecount = fileneedednum;
+					*asksent = I_GetTime() + NEWTICRATE;
+				}
+			}
+			break;
+		case CL_CHECKFILES:
+			if (!CL_FinishedFileList())
+				return false;
+			break;
+		case CL_DOWNLOADFILES:
+			waitmore = false;
+			for (INT32 i = 0; i < fileneedednum; i++)
+				if (fileneeded[i].status == FS_DOWNLOADING
+					|| fileneeded[i].status == FS_REQUESTED)
+				{
+					waitmore = true;
+					break;
+				}
+			if (waitmore)
+				break; // exit the case
+
+			Snake_Free(&snake);
+
+			cl_mode = CL_LOADFILES;
+			break;
+		case CL_LOADFILES:
+			if (CL_LoadServerFiles())
+			{
+				FreeFileNeeded();
+				*asksent = 0; // This ensures the first join request is right away
+				firstconnectattempttime = I_GetTime();
+				cl_mode = CL_ASKJOIN;
+			}
+			break;
+		case CL_ASKJOIN:
+			if (firstconnectattempttime + NEWTICRATE*300 < I_GetTime() && !server)
+			{
+				CONS_Printf(M_GetText("5 minute wait time exceeded.\n"));
+				CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
+				D_QuitNetGame();
+				CL_Reset();
+				D_StartTitle();
+				M_StartMessage(M_GetText(
+					"5 minute wait time exceeded.\n"
+					"You may retry connection.\n"
+					"\n"
+					"Press ESC\n"
+				), NULL, MM_NOTHING);
+				return false;
+			}
+
+			// Prepare structures to save the file
+			CL_PrepareDownloadSaveGame(tmpsave);
+
+			if (I_GetTime() >= *asksent && CL_SendJoin())
+			{
+				*asksent = I_GetTime() + NEWTICRATE*3;
+				cl_mode = CL_WAITJOINRESPONSE;
+			}
+			break;
+		case CL_WAITJOINRESPONSE:
+			if (I_GetTime() >= *asksent)
+			{
+				cl_mode = CL_ASKJOIN;
+			}
+			break;
+		case CL_DOWNLOADSAVEGAME:
+			// At this state, the first (and only) needed file is the gamestate
+			if (fileneeded[0].status == FS_FOUND)
+			{
+				// Gamestate is now handled within CL_LoadReceivedSavegame()
+				CL_LoadReceivedSavegame(false);
+				cl_mode = CL_CONNECTED;
+			} // don't break case continue to CL_CONNECTED
+			else
+				break;
+
+		case CL_CONNECTED:
+		case CL_CONFIRMCONNECT: //logic is handled by M_ConfirmConnect
+		default:
+			break;
+
+		// Connection closed by cancel, timeout or refusal.
+		case CL_ABORTED:
+			cl_mode = CL_SEARCHING;
+			return false;
+	}
+
+	GetPackets();
+	Net_AckTicker();
+
+	// Call it only once by tic
+	if (*oldtic != I_GetTime())
+	{
+		I_OsPolling();
+
+		if (cl_mode == CL_CONFIRMCONNECT)
+			D_ProcessEvents(); //needed for menu system to receive inputs
+		else
+		{
+			// my hand has been forced and I am dearly sorry for this awful hack :vomit:
+			for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
+			{
+				if (!Snake_JoyGrabber(snake, &events[eventtail]))
+					G_MapEventsToControls(&events[eventtail]);
+			}
+		}
+
+		if (gamekeydown[KEY_ESCAPE] || gamekeydown[KEY_JOY1+1] || cl_mode == CL_ABORTED)
+		{
+			CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
+			M_StartMessage(M_GetText("Network game synchronization aborted.\n\nPress ESC\n"), NULL, MM_NOTHING);
+
+			Snake_Free(&snake);
+
+			D_QuitNetGame();
+			CL_Reset();
+			D_StartTitle();
+			memset(gamekeydown, 0, NUMKEYS);
+			return false;
+		}
+		else if (cl_mode == CL_DOWNLOADFILES && snake)
+			Snake_Update(snake);
+
+		if (client && (cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADSAVEGAME))
+			FileReceiveTicker();
+
+		*oldtic = I_GetTime();
+
+		if (client && cl_mode != CL_CONNECTED && cl_mode != CL_ABORTED)
+		{
+			if (!snake)
+			{
+				F_MenuPresTicker(); // title sky
+				F_TitleScreenTicker(true);
+				F_TitleScreenDrawer();
+			}
+			CL_DrawConnectionStatus();
+#ifdef HAVE_THREADS
+			I_lock_mutex(&m_menu_mutex);
+#endif
+			M_Drawer(); //Needed for drawing messageboxes on the connection screen
+#ifdef HAVE_THREADS
+			I_unlock_mutex(m_menu_mutex);
+#endif
+			I_UpdateNoVsync(); // page flip or blit buffer
+			if (moviemode)
+				M_SaveFrame();
+			S_UpdateSounds();
+			S_UpdateClosedCaptions();
+		}
+	}
+	else
+	{
+		I_Sleep(cv_sleep.value);
+		I_UpdateTime(cv_timescale.value);
+	}
+
+	return true;
+}
+
+#define TMPSAVENAME "$$$.sav"
+
+void CL_ConnectToServer(void)
+{
+	INT32 pnumnodes, nodewaited = doomcom->numnodes, i;
+	tic_t oldtic;
+	tic_t asksent;
+	char tmpsave[256];
+
+	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
+
+	lastfilenum = -1;
+
+	cl_mode = CL_SEARCHING;
+
+	// Don't get a corrupt savegame error because tmpsave already exists
+	if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1)
+		I_Error("Can't delete %s\n", tmpsave);
+
+	if (netgame)
+	{
+		if (servernode < 0 || servernode >= MAXNETNODES)
+			CONS_Printf(M_GetText("Searching for a server...\n"));
+		else
+			CONS_Printf(M_GetText("Contacting the server...\n"));
+	}
+
+	if (gamestate == GS_INTERMISSION)
+		Y_EndIntermission(); // clean up intermission graphics etc
+
+	DEBFILE(va("waiting %d nodes\n", doomcom->numnodes));
+	G_SetGamestate(GS_WAITINGPLAYERS);
+	wipegamestate = GS_WAITINGPLAYERS;
+
+	ClearAdminPlayers();
+	pnumnodes = 1;
+	oldtic = I_GetTime() - 1;
+
+	asksent = (tic_t) - TICRATE;
+	firstconnectattempttime = I_GetTime();
+
+	i = SL_SearchServer(servernode);
+
+	if (i != -1)
+	{
+		char *gametypestr = serverlist[i].info.gametypename;
+		CONS_Printf(M_GetText("Connecting to: %s\n"), serverlist[i].info.servername);
+		gametypestr[sizeof serverlist[i].info.gametypename - 1] = '\0';
+		CONS_Printf(M_GetText("Gametype: %s\n"), gametypestr);
+		CONS_Printf(M_GetText("Version: %d.%d.%u\n"), serverlist[i].info.version/100,
+		 serverlist[i].info.version%100, serverlist[i].info.subversion);
+	}
+	SL_ClearServerList(servernode);
+
+	do
+	{
+		// If the connection was aborted for some reason, leave
+		if (!CL_ServerConnectionTicker(tmpsave, &oldtic, &asksent))
+			return;
+
+		if (server)
+		{
+			pnumnodes = 0;
+			for (i = 0; i < MAXNETNODES; i++)
+				if (netnodes[i].ingame)
+					pnumnodes++;
+		}
+	}
+	while (!(cl_mode == CL_CONNECTED && (client || (server && nodewaited <= pnumnodes))));
+
+	if (netgame)
+		F_StartWaitingPlayers();
+
+	DEBFILE(va("Synchronisation Finished\n"));
+
+	displayplayer = consoleplayer;
+}
+
+/** Called when a PT_SERVERINFO packet is received
+  *
+  * \param node The packet sender
+  * \note What happens if the packet comes from a client or something like that?
+  *
+  */
+void PT_ServerInfo(SINT8 node)
+{
+	// compute ping in ms
+	const tic_t ticnow = I_GetTime();
+	const tic_t ticthen = (tic_t)LONG(netbuffer->u.serverinfo.time);
+	const tic_t ticdiff = (ticnow - ticthen)*1000/NEWTICRATE;
+	netbuffer->u.serverinfo.time = (tic_t)LONG(ticdiff);
+	netbuffer->u.serverinfo.servername[MAXSERVERNAME-1] = 0;
+	netbuffer->u.serverinfo.application
+		[sizeof netbuffer->u.serverinfo.application - 1] = '\0';
+	netbuffer->u.serverinfo.gametypename
+		[sizeof netbuffer->u.serverinfo.gametypename - 1] = '\0';
+
+	SL_InsertServer(&netbuffer->u.serverinfo, node);
+}
+
+// Helper function for packets that should only be sent by the server
+// If it is NOT from the server, bail out and close the connection!
+static boolean ServerOnly(SINT8 node)
+{
+	if (node == servernode)
+		return false;
+
+	Net_CloseConnection(node);
+	return true;
+}
+
+void PT_MoreFilesNeeded(SINT8 node)
+{
+	if (server && serverrunning)
+	{ // But wait I thought I'm the server?
+		Net_CloseConnection(node);
+		return;
+	}
+	if (ServerOnly(node))
+		return;
+	if (cl_mode == CL_ASKFULLFILELIST && netbuffer->u.filesneededcfg.first == fileneedednum)
+	{
+		D_ParseFileneeded(netbuffer->u.filesneededcfg.num, netbuffer->u.filesneededcfg.files, netbuffer->u.filesneededcfg.first);
+		if (!netbuffer->u.filesneededcfg.more)
+			cl_lastcheckedfilecount = UINT16_MAX; // Got the whole file list
+	}
+}
+
+// Negative response of client join request
+void PT_ServerRefuse(SINT8 node)
+{
+	if (server && serverrunning)
+	{ // But wait I thought I'm the server?
+		Net_CloseConnection(node);
+		return;
+	}
+	if (ServerOnly(node))
+		return;
+	if (cl_mode == CL_WAITJOINRESPONSE)
+	{
+		// Save the reason so it can be displayed after quitting the netgame
+		char *reason = strdup(netbuffer->u.serverrefuse.reason);
+		if (!reason)
+			I_Error("Out of memory!\n");
+
+		if (strstr(reason, "Maximum players reached"))
+		{
+			serverisfull = true;
+			//Special timeout for when refusing due to player cap. The client will wait 3 seconds between join requests when waiting for a slot, so we need this to be much longer
+			//We set it back to the value of cv_nettimeout.value in CL_Reset
+			connectiontimeout = NEWTICRATE*7;
+			cl_mode = CL_ASKJOIN;
+			free(reason);
+			return;
+		}
+
+		M_StartMessage(va(M_GetText("Server refuses connection\n\nReason:\n%s"),
+			reason), NULL, MM_NOTHING);
+
+		D_QuitNetGame();
+		CL_Reset();
+		D_StartTitle();
+
+		free(reason);
+
+		// Will be reset by caller. Signals refusal.
+		cl_mode = CL_ABORTED;
+	}
+}
+
+// Positive response of client join request
+void PT_ServerCFG(SINT8 node)
+{
+	if (server && serverrunning && node != servernode)
+	{ // but wait I thought I'm the server?
+		Net_CloseConnection(node);
+		return;
+	}
+	if (ServerOnly(node))
+		return;
+	/// \note how would this happen? and is it doing the right thing if it does?
+	if (cl_mode != CL_WAITJOINRESPONSE)
+		return;
+
+	if (client)
+	{
+		maketic = gametic = neededtic = (tic_t)LONG(netbuffer->u.servercfg.gametic);
+		G_SetGametype(netbuffer->u.servercfg.gametype);
+		modifiedgame = netbuffer->u.servercfg.modifiedgame;
+		if (netbuffer->u.servercfg.usedCheats)
+			G_SetUsedCheats(true);
+		memcpy(server_context, netbuffer->u.servercfg.server_context, 8);
+	}
+
+	netnodes[(UINT8)servernode].ingame = true;
+	serverplayer = netbuffer->u.servercfg.serverplayer;
+	doomcom->numslots = SHORT(netbuffer->u.servercfg.totalslotnum);
+	mynode = netbuffer->u.servercfg.clientnode;
+	if (serverplayer >= 0)
+		playernode[(UINT8)serverplayer] = servernode;
+
+	if (netgame)
+		CONS_Printf(M_GetText("Join accepted, waiting for complete game state...\n"));
+	DEBFILE(va("Server accept join gametic=%u mynode=%d\n", gametic, mynode));
+
+	/// \note Wait. What if a Lua script uses some global custom variables synched with the NetVars hook?
+	///       Shouldn't they be downloaded even at intermission time?
+	///       Also, according to PT_ClientJoin, the server will send the savegame even during intermission...
+	if (netbuffer->u.servercfg.gamestate == GS_LEVEL/* ||
+		netbuffer->u.servercfg.gamestate == GS_INTERMISSION*/)
+		cl_mode = CL_DOWNLOADSAVEGAME;
+	else
+		cl_mode = CL_CONNECTED;
+}
diff --git a/src/netcode/client_connection.h b/src/netcode/client_connection.h
new file mode 100644
index 0000000000000000000000000000000000000000..4d75160d4a280061f08410b1f834df049a1b2a3e
--- /dev/null
+++ b/src/netcode/client_connection.h
@@ -0,0 +1,61 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 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  client_connection.h
+/// \brief Client connection handling
+
+#ifndef __D_CLIENT_CONNECTION__
+#define __D_CLIENT_CONNECTION__
+
+#include "../doomtype.h"
+#include "d_clisrv.h"
+
+#define MAXSERVERLIST (MAXNETNODES-1)
+
+typedef struct
+{
+	SINT8 node;
+	serverinfo_pak info;
+} serverelem_t;
+
+typedef enum
+{
+	CL_SEARCHING,
+	CL_CHECKFILES,
+	CL_DOWNLOADFILES,
+	CL_ASKJOIN,
+	CL_LOADFILES,
+	CL_WAITJOINRESPONSE,
+	CL_DOWNLOADSAVEGAME,
+	CL_CONNECTED,
+	CL_ABORTED,
+	CL_ASKFULLFILELIST,
+	CL_CONFIRMCONNECT
+} cl_mode_t;
+
+extern serverelem_t serverlist[MAXSERVERLIST];
+extern UINT32 serverlistcount;
+
+extern cl_mode_t cl_mode;
+extern boolean serverisfull; //lets us be aware if the server was full after we check files, but before downloading, so we can ask if the user still wants to download or not
+extern tic_t firstconnectattempttime;
+extern UINT8 mynode; // my address pointofview server
+
+void CL_QueryServerList(msg_server_t *list);
+void CL_UpdateServerList(boolean internetsearch, INT32 room);
+
+void CL_ConnectToServer(void);
+boolean CL_SendJoin(void);
+
+void PT_ServerInfo(SINT8 node);
+void PT_MoreFilesNeeded(SINT8 node);
+void PT_ServerRefuse(SINT8 node);
+void PT_ServerCFG(SINT8 node);
+
+#endif
diff --git a/src/netcode/commands.c b/src/netcode/commands.c
new file mode 100644
index 0000000000000000000000000000000000000000..e7d51437e42d381827a3e702f9c49a8f9d1780b2
--- /dev/null
+++ b/src/netcode/commands.c
@@ -0,0 +1,492 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 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  commands.c
+/// \brief Various netgame commands, such as kick and ban
+
+#include "commands.h"
+#include "d_clisrv.h"
+#include "client_connection.h"
+#include "net_command.h"
+#include "d_netcmd.h"
+#include "d_net.h"
+#include "i_net.h"
+#include "protocol.h"
+#include "../byteptr.h"
+#include "../d_main.h"
+#include "../g_game.h"
+#include "../w_wad.h"
+#include "../z_zone.h"
+#include "../doomstat.h"
+#include "../doomdef.h"
+#include "../r_local.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <stddef.h>
+#include <string.h>
+
+typedef struct banreason_s
+{
+	char *reason;
+	struct banreason_s *prev; //-1
+	struct banreason_s *next; //+1
+} banreason_t;
+
+static banreason_t *reasontail = NULL; //last entry, use prev
+static banreason_t *reasonhead = NULL; //1st entry, use next
+
+void Ban_Add(const char *reason)
+{
+	banreason_t *reasonlist = malloc(sizeof(*reasonlist));
+
+	if (!reasonlist)
+		return;
+	if (!reason)
+		reason = "NA";
+
+	reasonlist->next = NULL;
+	reasonlist->reason = Z_StrDup(reason);
+	if ((reasonlist->prev = reasontail) == NULL)
+		reasonhead = reasonlist;
+	else
+		reasontail->next = reasonlist;
+	reasontail = reasonlist;
+}
+
+static void Ban_Clear(void)
+{
+	banreason_t *temp;
+
+	I_ClearBans();
+
+	reasontail = NULL;
+
+	while (reasonhead)
+	{
+		temp = reasonhead->next;
+		Z_Free(reasonhead->reason);
+		free(reasonhead);
+		reasonhead = temp;
+	}
+}
+
+void Ban_Load_File(boolean warning)
+{
+	FILE *f;
+	char *address, *mask;
+	char buffer[MAX_WADPATH];
+
+	if (!I_ClearBans)
+		return;
+
+	f = fopen(va("%s"PATHSEP"%s", srb2home, "ban.txt"), "r");
+
+	if (!f)
+	{
+		if (warning)
+			CONS_Alert(CONS_WARNING, M_GetText("Could not open ban.txt for ban list\n"));
+		return;
+	}
+
+	Ban_Clear();
+
+	for (; fgets(buffer, (int)sizeof(buffer), f);)
+	{
+		address = strtok(buffer, " \t\r\n");
+		mask = strtok(NULL, " \t\r\n");
+		if (address[0] == '[')
+		{
+			size_t len;
+			address++;
+			len = strlen(address);
+			if (address[len-1] == ']')
+				address[len-1] = '\0';
+		}
+
+		I_SetBanAddress(address, mask);
+
+		Ban_Add(strtok(NULL, "\r\n"));
+	}
+
+	fclose(f);
+}
+
+void D_SaveBan(void)
+{
+	FILE *f;
+	banreason_t *reasonlist = reasonhead;
+	const char *address, *mask;
+	const char *path = va("%s"PATHSEP"%s", srb2home, "ban.txt");
+
+	if (!reasonhead)
+	{
+		remove(path);
+		return;
+	}
+
+	f = fopen(path, "w");
+
+	if (!f)
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("Could not save ban list into ban.txt\n"));
+		return;
+	}
+
+	for (size_t i = 0;(address = I_GetBanAddress(i)) != NULL;i++)
+	{
+		if (!I_GetBanMask || (mask = I_GetBanMask(i)) == NULL)
+			fprintf(f, "%s 0", address);
+		else
+			fprintf(f, "%s %s", address, mask);
+
+		if (reasonlist && reasonlist->reason)
+			fprintf(f, " %s\n", reasonlist->reason);
+		else
+			fprintf(f, " %s\n", "NA");
+
+		if (reasonlist) reasonlist = reasonlist->next;
+	}
+
+	fclose(f);
+}
+
+void Command_ShowBan(void) //Print out ban list
+{
+	size_t i;
+	const char *address, *mask;
+	banreason_t *reasonlist = reasonhead;
+
+	if (I_GetBanAddress)
+		CONS_Printf(M_GetText("Ban List:\n"));
+	else
+		return;
+
+	for (i = 0;(address = I_GetBanAddress(i)) != NULL;i++)
+	{
+		if (!I_GetBanMask || (mask = I_GetBanMask(i)) == NULL)
+			CONS_Printf("%s: %s ", sizeu1(i+1), address);
+		else
+			CONS_Printf("%s: %s/%s ", sizeu1(i+1), address, mask);
+
+		if (reasonlist && reasonlist->reason)
+			CONS_Printf("(%s)\n", reasonlist->reason);
+		else
+			CONS_Printf("\n");
+
+		if (reasonlist) reasonlist = reasonlist->next;
+	}
+
+	if (i == 0 && !address)
+		CONS_Printf(M_GetText("(empty)\n"));
+}
+
+void Command_ClearBans(void)
+{
+	if (!I_ClearBans)
+		return;
+
+	Ban_Clear();
+	D_SaveBan();
+}
+
+void Command_Ban(void)
+{
+	if (COM_Argc() < 2)
+	{
+		CONS_Printf(M_GetText("Ban <playername/playernum> <reason>: ban and kick a player\n"));
+		return;
+	}
+
+	if (!netgame) // Don't kick Tails in splitscreen!
+	{
+		CONS_Printf(M_GetText("This only works in a netgame.\n"));
+		return;
+	}
+
+	if (server || IsPlayerAdmin(consoleplayer))
+	{
+		UINT8 buf[3 + MAX_REASONLENGTH];
+		UINT8 *p = buf;
+		const SINT8 pn = nametonum(COM_Argv(1));
+		const INT32 node = playernode[(INT32)pn];
+
+		if (pn == -1 || pn == 0)
+			return;
+
+		WRITEUINT8(p, pn);
+
+		if (server && I_Ban && !I_Ban(node)) // only the server is allowed to do this right now
+		{
+			CONS_Alert(CONS_WARNING, M_GetText("Too many bans! Geez, that's a lot of people you're excluding...\n"));
+			WRITEUINT8(p, KICK_MSG_GO_AWAY);
+			SendNetXCmd(XD_KICK, &buf, 2);
+		}
+		else
+		{
+			if (server) // only the server is allowed to do this right now
+			{
+				Ban_Add(COM_Argv(2));
+				D_SaveBan(); // save the ban list
+			}
+
+			if (COM_Argc() == 2)
+			{
+				WRITEUINT8(p, KICK_MSG_BANNED);
+				SendNetXCmd(XD_KICK, &buf, 2);
+			}
+			else
+			{
+				size_t j = COM_Argc();
+				char message[MAX_REASONLENGTH];
+
+				//Steal from the motd code so you don't have to put the reason in quotes.
+				strlcpy(message, COM_Argv(2), sizeof message);
+				for (size_t i = 3; i < j; i++)
+				{
+					strlcat(message, " ", sizeof message);
+					strlcat(message, COM_Argv(i), sizeof message);
+				}
+
+				WRITEUINT8(p, KICK_MSG_CUSTOM_BAN);
+				WRITESTRINGN(p, message, MAX_REASONLENGTH);
+				SendNetXCmd(XD_KICK, &buf, p - buf);
+			}
+		}
+	}
+	else
+		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
+
+}
+
+void Command_BanIP(void)
+{
+	if (COM_Argc() < 2)
+	{
+		CONS_Printf(M_GetText("banip <ip> <reason>: ban an ip address\n"));
+		return;
+	}
+
+	if (server) // Only the server can use this, otherwise does nothing.
+	{
+		const char *address = (COM_Argv(1));
+		const char *reason;
+
+		if (COM_Argc() == 2)
+			reason = NULL;
+		else
+			reason = COM_Argv(2);
+
+
+		if (I_SetBanAddress && I_SetBanAddress(address, NULL))
+		{
+			if (reason)
+				CONS_Printf("Banned IP address %s for: %s\n", address, reason);
+			else
+				CONS_Printf("Banned IP address %s\n", address);
+
+			Ban_Add(reason);
+			D_SaveBan();
+		}
+		else
+		{
+			return;
+		}
+	}
+}
+
+void Command_ReloadBan(void)  //recheck ban.txt
+{
+	Ban_Load_File(true);
+}
+
+void Command_Kick(void)
+{
+	if (COM_Argc() < 2)
+	{
+		CONS_Printf(M_GetText("kick <playername/playernum> <reason>: kick a player\n"));
+		return;
+	}
+
+	if (!netgame) // Don't kick Tails in splitscreen!
+	{
+		CONS_Printf(M_GetText("This only works in a netgame.\n"));
+		return;
+	}
+
+	if (server || IsPlayerAdmin(consoleplayer))
+	{
+		UINT8 buf[3 + MAX_REASONLENGTH];
+		UINT8 *p = buf;
+		const SINT8 pn = nametonum(COM_Argv(1));
+
+		if (pn == -1 || pn == 0)
+			return;
+
+		// Special case if we are trying to kick a player who is downloading the game state:
+		// trigger a timeout instead of kicking them, because a kick would only
+		// take effect after they have finished downloading
+		if (server && playernode[pn] != UINT8_MAX && netnodes[playernode[pn]].sendingsavegame)
+		{
+			Net_ConnectionTimeout(playernode[pn]);
+			return;
+		}
+
+		WRITESINT8(p, pn);
+
+		if (COM_Argc() == 2)
+		{
+			WRITEUINT8(p, KICK_MSG_GO_AWAY);
+			SendNetXCmd(XD_KICK, &buf, 2);
+		}
+		else
+		{
+			size_t j = COM_Argc();
+			char message[MAX_REASONLENGTH];
+
+			//Steal from the motd code so you don't have to put the reason in quotes.
+			strlcpy(message, COM_Argv(2), sizeof message);
+			for (size_t i = 3; i < j; i++)
+			{
+				strlcat(message, " ", sizeof message);
+				strlcat(message, COM_Argv(i), sizeof message);
+			}
+
+			WRITEUINT8(p, KICK_MSG_CUSTOM_KICK);
+			WRITESTRINGN(p, message, MAX_REASONLENGTH);
+			SendNetXCmd(XD_KICK, &buf, p - buf);
+		}
+	}
+	else
+		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
+}
+
+void Command_connect(void)
+{
+	if (COM_Argc() < 2 || *COM_Argv(1) == 0)
+	{
+		CONS_Printf(M_GetText(
+			"Connect <serveraddress> (port): connect to a server\n"
+			"Connect ANY: connect to the first lan server found\n"
+			//"Connect SELF: connect to your own server.\n"
+			));
+		return;
+	}
+
+	if (Playing() || titledemo)
+	{
+		CONS_Printf(M_GetText("You cannot connect while in a game. End this game first.\n"));
+		return;
+	}
+
+	server = false;
+/*
+	if (!stricmp(COM_Argv(1), "self"))
+	{
+		servernode = 0;
+		server = true;
+		/// \bug should be but...
+		//SV_SpawnServer();
+	}
+	else
+*/
+	{
+		// used in menu to connect to a server in the list
+		if (netgame && !stricmp(COM_Argv(1), "node"))
+		{
+			servernode = (SINT8)atoi(COM_Argv(2));
+		}
+		else if (netgame)
+		{
+			CONS_Printf(M_GetText("You cannot connect while in a game. End this game first.\n"));
+			return;
+		}
+		else if (I_NetOpenSocket)
+		{
+			I_NetOpenSocket();
+			netgame = true;
+			multiplayer = true;
+
+			if (!stricmp(COM_Argv(1), "any"))
+				servernode = BROADCASTADDR;
+			else if (I_NetMakeNodewPort)
+			{
+				if (COM_Argc() >= 3) // address AND port
+					servernode = I_NetMakeNodewPort(COM_Argv(1), COM_Argv(2));
+				else // address only, or address:port
+					servernode = I_NetMakeNode(COM_Argv(1));
+			}
+			else
+			{
+				CONS_Alert(CONS_ERROR, M_GetText("There is no server identification with this network driver\n"));
+				D_CloseConnection();
+				return;
+			}
+		}
+		else
+			CONS_Alert(CONS_ERROR, M_GetText("There is no network driver\n"));
+	}
+
+	splitscreen = false;
+	SplitScreen_OnChange();
+	botingame = false;
+	botskin = 0;
+	CL_ConnectToServer();
+}
+
+void Command_GetPlayerNum(void)
+{
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+		if (playeringame[i])
+		{
+			if (serverplayer == i)
+				CONS_Printf(M_GetText("num:%2d  node:%2d  %s\n"), i, playernode[i], player_names[i]);
+			else
+				CONS_Printf(M_GetText("\x82num:%2d  node:%2d  %s\n"), i, playernode[i], player_names[i]);
+		}
+}
+
+/** Lists all players and their player numbers.
+  *
+  * \sa Command_GetPlayerNum
+  */
+void Command_Nodes(void)
+{
+	size_t maxlen = 0;
+	const char *address;
+
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+	{
+		const size_t plen = strlen(player_names[i]);
+		if (playeringame[i] && plen > maxlen)
+			maxlen = plen;
+	}
+
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+	{
+		if (playeringame[i])
+		{
+			CONS_Printf("%.2u: %*s", i, (int)maxlen, player_names[i]);
+
+			if (playernode[i] != UINT8_MAX)
+			{
+				CONS_Printf(" - node %.2d", playernode[i]);
+				if (I_GetNodeAddress && (address = I_GetNodeAddress(playernode[i])) != NULL)
+					CONS_Printf(" - %s", address);
+			}
+
+			if (IsPlayerAdmin(i))
+				CONS_Printf(M_GetText(" (verified admin)"));
+
+			if (players[i].spectator)
+				CONS_Printf(M_GetText(" (spectator)"));
+
+			CONS_Printf("\n");
+		}
+	}
+}
diff --git a/src/netcode/commands.h b/src/netcode/commands.h
new file mode 100644
index 0000000000000000000000000000000000000000..d328114eecd1ece5888cd333934ba87886f3a28e
--- /dev/null
+++ b/src/netcode/commands.h
@@ -0,0 +1,33 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 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  commands.h
+/// \brief Various netgame commands, such as kick and ban
+
+#ifndef __COMMANDS__
+#define __COMMANDS__
+
+#include "../doomdef.h"
+
+#define MAX_REASONLENGTH 30
+
+void Ban_Add(const char *reason);
+void D_SaveBan(void);
+void Ban_Load_File(boolean warning);
+void Command_ShowBan(void);
+void Command_ClearBans(void);
+void Command_Ban(void);
+void Command_BanIP(void);
+void Command_ReloadBan(void);
+void Command_Kick(void);
+void Command_connect(void);
+void Command_GetPlayerNum(void);
+void Command_Nodes(void);
+
+#endif
diff --git a/src/netcode/d_clisrv.c b/src/netcode/d_clisrv.c
new file mode 100644
index 0000000000000000000000000000000000000000..d222920c3baf085ba800d8c80c66ad88fba58944
--- /dev/null
+++ b/src/netcode/d_clisrv.c
@@ -0,0 +1,1790 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 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  d_clisrv.c
+/// \brief SRB2 Network game communication and protocol, all OS independent parts.
+
+#include <time.h>
+#ifdef __GNUC__
+#include <unistd.h> //for unlink
+#endif
+
+#include "../i_time.h"
+#include "i_net.h"
+#include "../i_system.h"
+#include "../i_video.h"
+#include "d_net.h"
+#include "../d_main.h"
+#include "../g_game.h"
+#include "../st_stuff.h"
+#include "../hu_stuff.h"
+#include "../keys.h"
+#include "../m_menu.h"
+#include "../console.h"
+#include "d_netfil.h"
+#include "../byteptr.h"
+#include "../p_saveg.h"
+#include "../z_zone.h"
+#include "../p_local.h"
+#include "../m_misc.h"
+#include "../am_map.h"
+#include "../m_random.h"
+#include "mserv.h"
+#include "../y_inter.h"
+#include "../r_local.h"
+#include "../m_argv.h"
+#include "../p_setup.h"
+#include "../lzf.h"
+#include "../lua_script.h"
+#include "../lua_hook.h"
+#include "../lua_libs.h"
+#include "../md5.h"
+#include "../m_perfstats.h"
+#include "server_connection.h"
+#include "client_connection.h"
+#include "tic_command.h"
+#include "net_command.h"
+#include "gamestate.h"
+#include "commands.h"
+#include "protocol.h"
+
+//
+// NETWORKING
+//
+// gametic is the tic about to (or currently being) run
+// Server:
+//   maketic is the tic that hasn't had control made for it yet
+//   nettics is the tic for each node
+//   firstticstosend is the lowest value of nettics
+// Client:
+//   neededtic is the tic needed by the client to run the game
+//   firstticstosend is used to optimize a condition
+// Normally maketic >= gametic > 0
+
+boolean server = true; // true or false but !server == client
+boolean serverrunning = false;
+INT32 serverplayer = 0;
+char motd[254], server_context[8]; // Message of the Day, Unique Context (even without Mumble support)
+
+netnode_t netnodes[MAXNETNODES];
+
+// Server specific vars
+UINT8 playernode[MAXPLAYERS];
+
+UINT16 pingmeasurecount = 1;
+UINT32 realpingtable[MAXPLAYERS]; //the base table of ping where an average will be sent to everyone.
+UINT32 playerpingtable[MAXPLAYERS]; //table of player latency values.
+static INT32 pingtimeout[MAXPLAYERS];
+tic_t servermaxping = 800; // server's max ping. Defaults to 800
+
+tic_t maketic;
+
+INT16 consistancy[BACKUPTICS];
+
+// true when a player is connecting or disconnecting so that the gameplay has stopped in its tracks
+boolean hu_stopped = false;
+
+UINT8 (*adminpassmd5)[16];
+UINT32 adminpasscount = 0;
+
+tic_t neededtic;
+SINT8 servernode = 0; // the number of the server node
+
+boolean acceptnewnode = true;
+
+UINT16 software_MAXPACKETLENGTH;
+
+static tic_t gametime = 0;
+
+static CV_PossibleValue_t netticbuffer_cons_t[] = {{0, "MIN"}, {3, "MAX"}, {0, NULL}};
+consvar_t cv_netticbuffer = CVAR_INIT ("netticbuffer", "1", CV_SAVE, netticbuffer_cons_t, NULL);
+
+static CV_PossibleValue_t resynchattempts_cons_t[] = {{1, "MIN"}, {20, "MAX"}, {0, "No"}, {0, NULL}};
+consvar_t cv_resynchattempts = CVAR_INIT ("resynchattempts", "10", CV_SAVE|CV_NETVAR, resynchattempts_cons_t, NULL);
+
+consvar_t cv_blamecfail = CVAR_INIT ("blamecfail", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
+
+static CV_PossibleValue_t playbackspeed_cons_t[] = {{1, "MIN"}, {10, "MAX"}, {0, NULL}};
+consvar_t cv_playbackspeed = CVAR_INIT ("playbackspeed", "1", 0, playbackspeed_cons_t, NULL);
+
+consvar_t cv_idletime = CVAR_INIT ("idletime", "0", CV_SAVE, CV_Unsigned, NULL);
+consvar_t cv_dedicatedidletime = CVAR_INIT ("dedicatedidletime", "10", CV_SAVE, CV_Unsigned, NULL);
+
+void ResetNode(INT32 node)
+{
+	memset(&netnodes[node], 0, sizeof(*netnodes));
+	netnodes[node].player = -1;
+	netnodes[node].player2 = -1;
+}
+
+void CL_Reset(void)
+{
+	if (metalrecording)
+		G_StopMetalRecording(false);
+	if (metalplayback)
+		G_StopMetalDemo();
+	if (demorecording)
+		G_CheckDemoStatus();
+
+	// reset client/server code
+	DEBFILE(va("\n-=-=-=-=-=-=-= Client reset =-=-=-=-=-=-=-\n\n"));
+
+	if (servernode > 0 && servernode < MAXNETNODES)
+	{
+		netnodes[(UINT8)servernode].ingame = false;
+		Net_CloseConnection(servernode);
+	}
+	D_CloseConnection(); // netgame = false
+	multiplayer = false;
+	servernode = 0;
+	server = true;
+	doomcom->numnodes = 1;
+	doomcom->numslots = 1;
+	SV_StopServer();
+	SV_ResetServer();
+
+	// make sure we don't leave any fileneeded gunk over from a failed join
+	FreeFileNeeded();
+	fileneedednum = 0;
+
+	totalfilesrequestednum = 0;
+	totalfilesrequestedsize = 0;
+	firstconnectattempttime = 0;
+	serverisfull = false;
+	connectiontimeout = (tic_t)cv_nettimeout.value; //reset this temporary hack
+
+	// D_StartTitle should get done now, but the calling function will handle it
+}
+
+//
+// CL_ClearPlayer
+//
+// Clears the player data so that a future client can use this slot
+//
+void CL_ClearPlayer(INT32 playernum)
+{
+	if (players[playernum].mo)
+		P_RemoveMobj(players[playernum].mo);
+	memset(&players[playernum], 0, sizeof (player_t));
+	memset(playeraddress[playernum], 0, sizeof(*playeraddress));
+}
+
+// Xcmd XD_ADDPLAYER
+static void Got_AddPlayer(UINT8 **p, INT32 playernum)
+{
+	INT16 node, newplayernum;
+	boolean splitscreenplayer;
+	boolean rejoined;
+	player_t *newplayer;
+
+	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
+	{
+		// protect against hacked/buggy client
+		CONS_Alert(CONS_WARNING, M_GetText("Illegal add player command received from %s\n"), player_names[playernum]);
+		if (server)
+			SendKick(playernum, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
+		return;
+	}
+
+	node = READUINT8(*p);
+	newplayernum = READUINT8(*p);
+	splitscreenplayer = newplayernum & 0x80;
+	newplayernum &= ~0x80;
+
+	rejoined = playeringame[newplayernum];
+
+	if (!rejoined)
+	{
+		// Clear player before joining, lest some things get set incorrectly
+		// HACK: don't do this for splitscreen, it relies on preset values
+		if (!splitscreen && !botingame)
+			CL_ClearPlayer(newplayernum);
+		playeringame[newplayernum] = true;
+		G_AddPlayer(newplayernum);
+		if (newplayernum+1 > doomcom->numslots)
+			doomcom->numslots = (INT16)(newplayernum+1);
+
+		if (server && I_GetNodeAddress)
+		{
+			char addressbuffer[64];
+			const char *address = I_GetNodeAddress(node);
+			if (address) // MI: fix msvcrt.dll!_mbscat crash?
+			{
+				strcpy(addressbuffer, address);
+				strcpy(playeraddress[newplayernum],
+						I_NetSplitAddress(addressbuffer, NULL));
+			}
+		}
+	}
+
+	newplayer = &players[newplayernum];
+
+	newplayer->jointime = 0;
+	newplayer->quittime = 0;
+	newplayer->lastinputtime = 0;
+
+	READSTRINGN(*p, player_names[newplayernum], MAXPLAYERNAME);
+
+	// the server is creating my player
+	if (node == mynode)
+	{
+		playernode[newplayernum] = 0; // for information only
+		if (!splitscreenplayer)
+		{
+			consoleplayer = newplayernum;
+			displayplayer = newplayernum;
+			secondarydisplayplayer = newplayernum;
+			DEBFILE("spawning me\n");
+			ticcmd_oldangleturn[0] = newplayer->oldrelangleturn;
+		}
+		else
+		{
+			secondarydisplayplayer = newplayernum;
+			DEBFILE("spawning my brother\n");
+			if (botingame)
+				newplayer->bot = 1;
+			ticcmd_oldangleturn[1] = newplayer->oldrelangleturn;
+		}
+		P_ForceLocalAngle(newplayer, (angle_t)(newplayer->angleturn << 16));
+		D_SendPlayerConfig();
+		addedtogame = true;
+
+		if (rejoined)
+		{
+			if (newplayer->mo)
+			{
+				newplayer->viewheight = 41*newplayer->height/48;
+
+				if (newplayer->mo->eflags & MFE_VERTICALFLIP)
+					newplayer->viewz = newplayer->mo->z + newplayer->mo->height - newplayer->viewheight;
+				else
+					newplayer->viewz = newplayer->mo->z + newplayer->viewheight;
+			}
+
+			// wake up the status bar
+			ST_Start();
+			// wake up the heads up text
+			HU_Start();
+
+			if (camera.chase && !splitscreenplayer)
+				P_ResetCamera(newplayer, &camera);
+			if (camera2.chase && splitscreenplayer)
+				P_ResetCamera(newplayer, &camera2);
+		}
+	}
+
+	if (netgame)
+	{
+		char joinmsg[256];
+
+		if (rejoined)
+			strcpy(joinmsg, M_GetText("\x82*%s has rejoined the game (player %d)"));
+		else
+			strcpy(joinmsg, M_GetText("\x82*%s has joined the game (player %d)"));
+		strcpy(joinmsg, va(joinmsg, player_names[newplayernum], newplayernum));
+
+		// Merge join notification + IP to avoid clogging console/chat
+		if (server && cv_showjoinaddress.value && I_GetNodeAddress)
+		{
+			const char *address = I_GetNodeAddress(node);
+			if (address)
+				strcat(joinmsg, va(" (%s)", address));
+		}
+
+		HU_AddChatText(joinmsg, false);
+	}
+
+	if (server && multiplayer && motd[0] != '\0')
+		COM_BufAddText(va("sayto %d %s\n", newplayernum, motd));
+
+	if (!rejoined)
+		LUA_HookInt(newplayernum, HOOK(PlayerJoin));
+}
+
+static void UnlinkPlayerFromNode(INT32 playernum)
+{
+	INT32 node = playernode[playernum];
+
+	if (node == UINT8_MAX)
+		return;
+
+	playernode[playernum] = UINT8_MAX;
+
+	netnodes[node].numplayers--;
+	if (netnodes[node].numplayers <= 0)
+	{
+		netnodes[node].ingame = false;
+		Net_CloseConnection(node);
+		ResetNode(node);
+	}
+}
+
+static void PT_ClientQuit(SINT8 node, INT32 netconsole)
+{
+	if (client)
+		return;
+
+	if (netnodes[node].ingame && netconsole != -1 && playeringame[netconsole])
+		SendKicksForNode(node, KICK_MSG_PLAYER_QUIT | KICK_MSG_KEEP_BODY);
+
+	Net_CloseConnection(node);
+	netnodes[node].ingame = false;
+	netnodes[node].player = -1;
+	netnodes[node].player2 = -1;
+}
+
+static void Got_KickCmd(UINT8 **p, INT32 playernum)
+{
+	INT32 pnum, msg;
+	char buf[3 + MAX_REASONLENGTH];
+	char *reason = buf;
+	kickreason_t kickreason = KR_KICK;
+	boolean keepbody;
+
+	pnum = READUINT8(*p);
+	msg = READUINT8(*p);
+	keepbody = (msg & KICK_MSG_KEEP_BODY) != 0;
+	msg &= ~KICK_MSG_KEEP_BODY;
+
+	if (pnum == serverplayer && IsPlayerAdmin(playernum))
+	{
+		CONS_Printf(M_GetText("Server is being shut down remotely. Goodbye!\n"));
+
+		if (server)
+			COM_BufAddText("quit\n");
+
+		return;
+	}
+
+	// Is playernum authorized to make this kick?
+	if (playernum != serverplayer && !IsPlayerAdmin(playernum)
+		&& !(playernode[playernum] != UINT8_MAX && netnodes[playernode[playernum]].numplayers == 2
+		&& netnodes[playernode[playernum]].player2 == pnum))
+	{
+		// We received a kick command from someone who isn't the
+		// server or admin, and who isn't in splitscreen removing
+		// player 2. Thus, it must be someone with a modified
+		// binary, trying to kick someone but without having
+		// authorization.
+
+		// We deal with this by changing the kick reason to
+		// "consistency failure" and kicking the offending user
+		// instead.
+
+		// Note: Splitscreen in netgames is broken because of
+		// this. Only the server has any idea of which players
+		// are using splitscreen on the same computer, so
+		// clients cannot always determine if a kick is
+		// legitimate.
+
+		CONS_Alert(CONS_WARNING, M_GetText("Illegal kick command received from %s for player %d\n"), player_names[playernum], pnum);
+
+		// In debug, print a longer message with more details.
+		// TODO Callum: Should we translate this?
+/*
+		CONS_Debug(DBG_NETPLAY,
+			"So, you must be asking, why is this an illegal kick?\n"
+			"Well, let's take a look at the facts, shall we?\n"
+			"\n"
+			"playernum (this is the guy who did it), he's %d.\n"
+			"pnum (the guy he's trying to kick) is %d.\n"
+			"playernum's node is %d.\n"
+			"That node has %d players.\n"
+			"Player 2 on that node is %d.\n"
+			"pnum's node is %d.\n"
+			"That node has %d players.\n"
+			"Player 2 on that node is %d.\n"
+			"\n"
+			"If you think this is a bug, please report it, including all of the details above.\n",
+				playernum, pnum,
+				playernode[playernum], netnodes[playernode[playernum]].numplayers,
+				netnodes[playernode[playernum]].player2,
+				playernode[pnum], netnodes[playernode[pnum]].numplayers,
+				netnodes[playernode[pnum]].player2);
+*/
+		pnum = playernum;
+		msg = KICK_MSG_CON_FAIL;
+		keepbody = true;
+	}
+
+	//CONS_Printf("\x82%s ", player_names[pnum]);
+
+	// If a verified admin banned someone, the server needs to know about it.
+	// If the playernum isn't zero (the server) then the server needs to record the ban.
+	if (server && playernum && (msg == KICK_MSG_BANNED || msg == KICK_MSG_CUSTOM_BAN))
+	{
+		if (I_Ban && !I_Ban(playernode[(INT32)pnum]))
+			CONS_Alert(CONS_WARNING, M_GetText("Too many bans! Geez, that's a lot of people you're excluding...\n"));
+		else
+			Ban_Add(reason);
+	}
+
+	switch (msg)
+	{
+		case KICK_MSG_GO_AWAY:
+			if (!players[pnum].quittime)
+				HU_AddChatText(va("\x82*%s has been kicked (No reason given)", player_names[pnum]), false);
+			kickreason = KR_KICK;
+			break;
+		case KICK_MSG_PING_HIGH:
+			HU_AddChatText(va("\x82*%s left the game (Broke ping limit)", player_names[pnum]), false);
+			kickreason = KR_PINGLIMIT;
+			break;
+		case KICK_MSG_CON_FAIL:
+			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
+			{
+				CONS_Printf(M_GetText("Player kicked is #%d, dumping consistency...\n"), pnum);
+
+				for (INT32 i = 0; i < MAXPLAYERS; i++)
+				{
+					if (!playeringame[i])
+						continue;
+					CONS_Printf("-------------------------------------\n");
+					CONS_Printf("Player %d: %s\n", i, player_names[i]);
+					CONS_Printf("Skin: %d\n", players[i].skin);
+					CONS_Printf("Color: %d\n", players[i].skincolor);
+					CONS_Printf("Speed: %d\n",players[i].speed>>FRACBITS);
+					if (players[i].mo)
+					{
+						if (!players[i].mo->skin)
+							CONS_Printf("Mobj skin: NULL!\n");
+						else
+							CONS_Printf("Mobj skin: %s\n", ((skin_t *)players[i].mo->skin)->name);
+						CONS_Printf("Position: %d, %d, %d\n", players[i].mo->x, players[i].mo->y, players[i].mo->z);
+						if (!players[i].mo->state)
+							CONS_Printf("State: S_NULL\n");
+						else
+							CONS_Printf("State: %d\n", (statenum_t)(players[i].mo->state-states));
+					}
+					else
+						CONS_Printf("Mobj: NULL\n");
+					CONS_Printf("-------------------------------------\n");
+				}
+			}
+			break;
+		case KICK_MSG_TIMEOUT:
+			HU_AddChatText(va("\x82*%s left the game (Connection timeout)", player_names[pnum]), false);
+			kickreason = KR_TIMEOUT;
+			break;
+		case KICK_MSG_PLAYER_QUIT:
+			if (netgame && !players[pnum].quittime) // not splitscreen/bots or soulless body
+				HU_AddChatText(va("\x82*%s left the game", player_names[pnum]), false);
+			kickreason = KR_LEAVE;
+			break;
+		case KICK_MSG_BANNED:
+			HU_AddChatText(va("\x82*%s has been banned (No reason given)", player_names[pnum]), false);
+			kickreason = KR_BAN;
+			break;
+		case KICK_MSG_CUSTOM_KICK:
+			READSTRINGN(*p, reason, MAX_REASONLENGTH+1);
+			HU_AddChatText(va("\x82*%s has been kicked (%s)", player_names[pnum], reason), false);
+			kickreason = KR_KICK;
+			break;
+		case KICK_MSG_CUSTOM_BAN:
+			READSTRINGN(*p, reason, MAX_REASONLENGTH+1);
+			HU_AddChatText(va("\x82*%s has been banned (%s)", player_names[pnum], reason), false);
+			kickreason = KR_BAN;
+			break;
+		case KICK_MSG_IDLE:
+			HU_AddChatText(va("\x82*%s has left the game (Inactive for too long)", player_names[pnum]), false);
+			kickreason = KR_TIMEOUT;
+			break;
+	}
+
+	if (pnum == consoleplayer)
+	{
+		LUA_HookBool(false, HOOK(GameQuit));
+#ifdef DUMPCONSISTENCY
+		if (msg == KICK_MSG_CON_FAIL) SV_SavedGame();
+#endif
+		D_QuitNetGame();
+		CL_Reset();
+		D_StartTitle();
+		if (msg == KICK_MSG_CON_FAIL)
+			M_StartMessage(M_GetText("Server closed connection\n(synch failure)\nPress ESC\n"), NULL, MM_NOTHING);
+		else if (msg == KICK_MSG_PING_HIGH)
+			M_StartMessage(M_GetText("Server closed connection\n(Broke ping limit)\nPress ESC\n"), NULL, MM_NOTHING);
+		else if (msg == KICK_MSG_IDLE)
+			M_StartMessage(M_GetText("Server closed connection\n(Inactive for too long)\nPress ESC\n"), NULL, MM_NOTHING);
+		else if (msg == KICK_MSG_BANNED)
+			M_StartMessage(M_GetText("You have been banned by the server\n\nPress ESC\n"), NULL, MM_NOTHING);
+		else if (msg == KICK_MSG_CUSTOM_KICK)
+			M_StartMessage(va(M_GetText("You have been kicked\n(%s)\nPress ESC\n"), reason), NULL, MM_NOTHING);
+		else if (msg == KICK_MSG_CUSTOM_BAN)
+			M_StartMessage(va(M_GetText("You have been banned\n(%s)\nPress ESC\n"), reason), NULL, MM_NOTHING);
+		else
+			M_StartMessage(M_GetText("You have been kicked by the server\n\nPress ESC\n"), NULL, MM_NOTHING);
+	}
+	else if (keepbody)
+	{
+		if (server)
+			UnlinkPlayerFromNode(pnum);
+		players[pnum].quittime = 1;
+	}
+	else
+		CL_RemovePlayer(pnum, kickreason);
+}
+
+// If in a special stage, redistribute the player's
+// spheres across the remaining players.
+// I feel like this shouldn't even be in this file at all, but well.
+static void RedistributeSpecialStageSpheres(INT32 playernum)
+{
+	if (!G_IsSpecialStage(gamemap) || D_NumPlayers() <= 1)
+		return;
+
+	INT32 count = D_NumPlayers() - 1;
+	INT32 spheres = players[playernum].spheres;
+	INT32 rings = players[playernum].rings;
+
+	while (spheres || rings)
+	{
+		INT32 sincrement = max(spheres / count, 1);
+		INT32 rincrement = max(rings / count, 1);
+
+		INT32 n;
+		for (INT32 i = 0; i < MAXPLAYERS; i++)
+		{
+			if (!playeringame[i] || i == playernum)
+				continue;
+
+			n = min(spheres, sincrement);
+			P_GivePlayerSpheres(&players[i], n);
+			spheres -= n;
+
+			n = min(rings, rincrement);
+			P_GivePlayerRings(&players[i], n);
+			rings -= n;
+		}
+	}
+}
+
+//
+// CL_RemovePlayer
+//
+// Removes a player from the current game
+//
+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.
+	if (!playeringame[playernum])
+		return;
+
+	if (server)
+		UnlinkPlayerFromNode(playernum);
+
+	if (gametyperules & GTR_TEAMFLAGS)
+		P_PlayerFlagBurst(&players[playernum], false); // Don't take the flag with you!
+
+	RedistributeSpecialStageSpheres(playernum);
+
+	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.
+		LUA_HookViewpointSwitch(&players[consoleplayer], &players[consoleplayer], true);
+		displayplayer = consoleplayer;
+	}
+
+	// Reset player data
+	CL_ClearPlayer(playernum);
+
+	// remove avatar of player
+	playeringame[playernum] = false;
+	while (!playeringame[doomcom->numslots-1] && doomcom->numslots > 1)
+		doomcom->numslots--;
+
+	// Reset the name
+	sprintf(player_names[playernum], "Player %d", playernum+1);
+
+	player_name_changes[playernum] = 0;
+
+	if (IsPlayerAdmin(playernum))
+	{
+		RemoveAdminPlayer(playernum); // don't stay admin after you're gone
+	}
+
+	LUA_InvalidatePlayer(&players[playernum]);
+
+	if (G_TagGametype()) //Check if you still have a game. Location flexible. =P
+		P_CheckSurvivors();
+	else if (gametyperules & GTR_RACE)
+		P_CheckRacers();
+}
+
+//
+// D_QuitNetGame
+// Called before quitting to leave a net game
+// without hanging the other players
+//
+void D_QuitNetGame(void)
+{
+	mousegrabbedbylua = true;
+	I_UpdateMouseGrab();
+
+	if (!netgame || !netbuffer)
+		return;
+
+	DEBFILE("===========================================================================\n"
+	        "                  Quitting Game, closing connection\n"
+	        "===========================================================================\n");
+
+	// abort send/receive of files
+	CloseNetFile();
+	RemoveAllLuaFileTransfers();
+	waitingforluafiletransfer = false;
+	waitingforluafilecommand = false;
+
+	if (server)
+	{
+		netbuffer->packettype = PT_SERVERSHUTDOWN;
+		for (INT32 i = 0; i < MAXNETNODES; i++)
+			if (netnodes[i].ingame)
+				HSendPacket(i, true, 0, 0);
+#ifdef MASTERSERVER
+		if (serverrunning && ms_RoomId > 0)
+			UnregisterServer();
+#endif
+	}
+	else if (servernode > 0 && servernode < MAXNETNODES && netnodes[(UINT8)servernode].ingame)
+	{
+		netbuffer->packettype = PT_CLIENTQUIT;
+		HSendPacket(servernode, true, 0, 0);
+	}
+
+	D_CloseConnection();
+	ClearAdminPlayers();
+
+	DEBFILE("===========================================================================\n"
+	        "                         Log finish\n"
+	        "===========================================================================\n");
+#ifdef DEBUGFILE
+	if (debugfile)
+	{
+		fclose(debugfile);
+		debugfile = NULL;
+	}
+#endif
+}
+
+void CL_HandleTimeout(void)
+{
+	LUA_HookBool(false, HOOK(GameQuit));
+	D_QuitNetGame();
+	CL_Reset();
+	D_StartTitle();
+	M_StartMessage(M_GetText("Server Timeout\n\nPress Esc\n"), NULL, MM_NOTHING);
+}
+
+void CL_AddSplitscreenPlayer(void)
+{
+	if (cl_mode == CL_CONNECTED)
+		CL_SendJoin();
+}
+
+void CL_RemoveSplitscreenPlayer(void)
+{
+	if (cl_mode != CL_CONNECTED)
+		return;
+
+	SendKick(secondarydisplayplayer, KICK_MSG_PLAYER_QUIT);
+}
+
+void SV_ResetServer(void)
+{
+	// +1 because this command will be executed in com_executebuffer in
+	// tryruntic so gametic will be incremented, anyway maketic > gametic
+	// is not an issue
+
+	maketic = gametic + 1;
+	neededtic = maketic;
+	tictoclear = maketic;
+
+	joindelay = 0;
+
+	for (INT32 i = 0; i < MAXNETNODES; i++)
+		ResetNode(i);
+
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+	{
+		LUA_InvalidatePlayer(&players[i]);
+		playeringame[i] = false;
+		playernode[i] = UINT8_MAX;
+		memset(playeraddress[i], 0, sizeof(*playeraddress));
+		sprintf(player_names[i], "Player %d", i + 1);
+		adminplayers[i] = -1; // Populate the entire adminplayers array with -1.
+	}
+
+	memset(player_name_changes, 0, sizeof player_name_changes);
+
+	mynode = 0;
+	cl_packetmissed = false;
+	cl_redownloadinggamestate = false;
+
+	if (dedicated)
+	{
+		netnodes[0].ingame = true;
+		serverplayer = 0;
+	}
+	else
+		serverplayer = consoleplayer;
+
+	if (server)
+		servernode = 0;
+
+	doomcom->numslots = 0;
+
+	// clear server_context
+	memset(server_context, '-', 8);
+
+	CV_RevertNetVars();
+
+	// Ensure synched when creating a new server
+	M_CopyGameData(serverGamedata, clientGamedata);
+
+	DEBFILE("\n-=-=-=-=-=-=-= Server Reset =-=-=-=-=-=-=-\n\n");
+}
+
+static inline void SV_GenContext(void)
+{
+	// generate server_context, as exactly 8 bytes of randomly mixed A-Z and a-z
+	// (hopefully M_Random is initialized!! if not this will be awfully silly!)
+	for (UINT8 i = 0; i < 8; i++)
+	{
+		const char a = M_RandomKey(26*2);
+		if (a < 26) // uppercase
+			server_context[i] = 'A'+a;
+		else // lowercase
+			server_context[i] = 'a'+(a-26);
+	}
+}
+
+void SV_SpawnServer(void)
+{
+	if (demoplayback)
+		G_StopDemo(); // reset engine parameter
+	if (metalplayback)
+		G_StopMetalDemo();
+
+	if (!serverrunning)
+	{
+		CONS_Printf(M_GetText("Starting Server....\n"));
+		serverrunning = true;
+		SV_ResetServer();
+		SV_GenContext();
+		if (netgame && I_NetOpenSocket)
+		{
+			I_NetOpenSocket();
+#ifdef MASTERSERVER
+			if (ms_RoomId > 0)
+				RegisterServer();
+#endif
+		}
+
+		// non dedicated server just connect to itself
+		if (!dedicated)
+			CL_ConnectToServer();
+		else doomcom->numslots = 1;
+	}
+}
+
+// called at singleplayer start and stopdemo
+void SV_StartSinglePlayerServer(void)
+{
+	server = true;
+	netgame = false;
+	multiplayer = false;
+	G_SetGametype(GT_COOP);
+
+	// no more tic the game with this settings!
+	SV_StopServer();
+
+	if (splitscreen)
+		multiplayer = true;
+}
+
+void SV_StopServer(void)
+{
+	if (gamestate == GS_INTERMISSION)
+		Y_EndIntermission();
+	gamestate = wipegamestate = GS_NULL;
+
+	localtextcmd[0] = 0;
+	localtextcmd2[0] = 0;
+
+	for (tic_t i = firstticstosend; i < firstticstosend + BACKUPTICS; i++)
+		D_Clearticcmd(i);
+
+	consoleplayer = 0;
+	cl_mode = CL_SEARCHING;
+	maketic = gametic+1;
+	neededtic = maketic;
+	serverrunning = false;
+}
+
+/** Called when a PT_SERVERSHUTDOWN packet is received
+  *
+  * \param node The packet sender (should be the server)
+  *
+  */
+static void PT_ServerShutdown(SINT8 node)
+{
+	if (node != servernode || server || cl_mode == CL_SEARCHING)
+		return;
+
+	(void)node;
+	LUA_HookBool(false, HOOK(GameQuit));
+	D_QuitNetGame();
+	CL_Reset();
+	D_StartTitle();
+	M_StartMessage(M_GetText("Server has shutdown\n\nPress Esc\n"), NULL, MM_NOTHING);
+}
+
+static void PT_Login(SINT8 node, INT32 netconsole)
+{
+	(void)node;
+
+	if (client)
+		return;
+
+#ifndef NOMD5
+	UINT8 finalmd5[16];/* Well, it's the cool thing to do? */
+	UINT32 i;
+
+	if (doomcom->datalength < 16)/* ignore partial sends */
+		return;
+
+	if (adminpasscount == 0)
+	{
+		CONS_Printf(M_GetText("Password from %s failed (no password set).\n"), player_names[netconsole]);
+		return;
+	}
+
+	for (i = 0; i < adminpasscount; i++)
+	{
+		// Do the final pass to compare with the sent md5
+		D_MD5PasswordPass(adminpassmd5[i], 16, va("PNUM%02d", netconsole), &finalmd5);
+
+		if (!memcmp(netbuffer->u.md5sum, finalmd5, 16))
+		{
+			CONS_Printf(M_GetText("%s passed authentication.\n"), player_names[netconsole]);
+			COM_BufInsertText(va("promote %d\n", netconsole)); // do this immediately
+			return;
+		}
+	}
+
+	CONS_Printf(M_GetText("Password from %s failed.\n"), player_names[netconsole]);
+#else
+	(void)netconsole;
+#endif
+}
+
+static void PT_AskLuaFile(SINT8 node)
+{
+	if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_ASKED)
+		AddLuaFileToSendQueue(node, luafiletransfers->realfilename);
+}
+
+static void PT_HasLuaFile(SINT8 node)
+{
+	if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_SENDING)
+		SV_HandleLuaFileSent(node);
+}
+
+static void PT_SendingLuaFile(SINT8 node)
+{
+	(void)node;
+
+	if (client)
+		CL_PrepareDownloadLuaFile();
+}
+
+/*
+Ping Update except better:
+We call this once per second and check for people's pings. If their ping happens to be too high, we increment some timer and kick them out.
+If they're not lagging, decrement the timer by 1. Of course, reset all of this if they leave.
+*/
+
+static inline void PingUpdate(void)
+{
+	boolean laggers[MAXPLAYERS];
+	UINT8 numlaggers = 0;
+	memset(laggers, 0, sizeof(boolean) * MAXPLAYERS);
+
+	netbuffer->packettype = PT_PING;
+
+	//check for ping limit breakage.
+	if (cv_maxping.value)
+	{
+		for (INT32 i = 1; i < MAXPLAYERS; i++)
+		{
+			if (playeringame[i] && !players[i].quittime
+			&& (realpingtable[i] / pingmeasurecount > (unsigned)cv_maxping.value))
+			{
+				if (players[i].jointime > 30 * TICRATE)
+					laggers[i] = true;
+				numlaggers++;
+			}
+			else
+				pingtimeout[i] = 0;
+		}
+
+		//kick lagging players... unless everyone but the server's ping sucks.
+		//in that case, it is probably the server's fault.
+		if (numlaggers < D_NumPlayers() - 1)
+		{
+			for (INT32 i = 1; i < MAXPLAYERS; i++)
+			{
+				if (playeringame[i] && laggers[i])
+				{
+					pingtimeout[i]++;
+					// ok your net has been bad for too long, you deserve to die.
+					if (pingtimeout[i] > cv_pingtimeout.value)
+					{
+						pingtimeout[i] = 0;
+						SendKick(i, KICK_MSG_PING_HIGH | KICK_MSG_KEEP_BODY);
+					}
+				}
+				/*
+					you aren't lagging,
+					but you aren't free yet.
+					In case you'll keep spiking,
+					we just make the timer go back down. (Very unstable net must still get kicked).
+				*/
+				else
+					pingtimeout[i] = (pingtimeout[i] == 0 ? 0 : pingtimeout[i]-1);
+			}
+		}
+	}
+
+	//make the ping packet and clear server data for next one
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+	{
+		netbuffer->u.pingtable[i] = realpingtable[i] / pingmeasurecount;
+		//server takes a snapshot of the real ping for display.
+		//otherwise, pings fluctuate a lot and would be odd to look at.
+		playerpingtable[i] = realpingtable[i] / pingmeasurecount;
+		realpingtable[i] = 0; //Reset each as we go.
+	}
+
+	// send the server's maxping as last element of our ping table. This is useful to let us know when we're about to get kicked.
+	netbuffer->u.pingtable[MAXPLAYERS] = cv_maxping.value;
+
+	//send out our ping packets
+	for (INT32 i = 0; i < MAXNETNODES; i++)
+		if (netnodes[i].ingame)
+			HSendPacket(i, true, 0, sizeof(INT32) * (MAXPLAYERS+1));
+
+	pingmeasurecount = 1; //Reset count
+}
+
+static void PT_Ping(SINT8 node, INT32 netconsole)
+{
+	// Only accept PT_PING from the server.
+	if (node != servernode)
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_PING", node);
+		if (server)
+			SendKick(netconsole, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
+		return;
+	}
+
+	//Update client ping table from the server.
+	if (client)
+	{
+		for (INT32 i = 0; i < MAXPLAYERS; i++)
+			if (playeringame[i])
+				playerpingtable[i] = (tic_t)netbuffer->u.pingtable[i];
+
+		servermaxping = (tic_t)netbuffer->u.pingtable[MAXPLAYERS];
+	}
+}
+
+static void PT_BasicKeepAlive(SINT8 node, INT32 netconsole)
+{
+	if (client)
+		return;
+
+	// This should probably still timeout though, as the node should always have a player 1 number
+	if (netconsole == -1)
+		return;
+
+	// If a client sends this it should mean they are done receiving the savegame
+	netnodes[node].sendingsavegame = false;
+
+	// As long as clients send keep alives, the server can keep running, so reset the timeout
+	/// \todo Use a separate cvar for that kind of timeout?
+	netnodes[node].freezetimeout = I_GetTime() + connectiontimeout;
+	return;
+}
+
+// Confusing, but this DOESN'T send PT_NODEKEEPALIVE, it sends PT_BASICKEEPALIVE
+// Used during wipes to tell the server that a node is still connected
+static void CL_SendClientKeepAlive(void)
+{
+	netbuffer->packettype = PT_BASICKEEPALIVE;
+
+	HSendPacket(servernode, false, 0, 0);
+}
+
+static void SV_SendServerKeepAlive(void)
+{
+	for (INT32 n = 1; n < MAXNETNODES; n++)
+	{
+		if (netnodes[n].ingame)
+		{
+			netbuffer->packettype = PT_BASICKEEPALIVE;
+			HSendPacket(n, false, 0, 0);
+		}
+	}
+}
+
+/** Handles a packet received from a node that isn't in game
+  *
+  * \param node The packet sender
+  * \todo Choose a better name, as the packet can also come from the server apparently?
+  * \sa HandlePacketFromPlayer
+  * \sa GetPackets
+  *
+  */
+static void HandlePacketFromAwayNode(SINT8 node)
+{
+	if (node != servernode)
+		DEBFILE(va("Received packet from unknown host %d\n", node));
+
+	switch (netbuffer->packettype)
+	{
+		case PT_ASKINFOVIAMS   : PT_AskInfoViaMS   (node    ); break;
+		case PT_SERVERINFO     : PT_ServerInfo     (node    ); break;
+		case PT_TELLFILESNEEDED: PT_TellFilesNeeded(node    ); break;
+		case PT_MOREFILESNEEDED: PT_MoreFilesNeeded(node    ); break;
+		case PT_ASKINFO        : PT_AskInfo        (node    ); break;
+		case PT_SERVERREFUSE   : PT_ServerRefuse   (node    ); break;
+		case PT_SERVERCFG      : PT_ServerCFG      (node    ); break;
+		case PT_FILEFRAGMENT   : PT_FileFragment   (node, -1); break;
+		case PT_FILEACK        : PT_FileAck        (node    ); break;
+		case PT_FILERECEIVED   : PT_FileReceived   (node    ); break;
+		case PT_REQUESTFILE    : PT_RequestFile    (node    ); break;
+		case PT_CLIENTQUIT     : PT_ClientQuit     (node, -1); break;
+		case PT_SERVERTICS     : PT_ServerTics     (node, -1); break;
+		case PT_CLIENTJOIN     : PT_ClientJoin     (node    ); break;
+		case PT_SERVERSHUTDOWN : PT_ServerShutdown (node    ); break;
+		case PT_CLIENTCMD      :                               break; // This is not an "unknown packet"
+		case PT_PLAYERINFO     :                               break; // This is not an "unknown packet"
+
+		default:
+			DEBFILE(va("unknown packet received (%d) from unknown host\n",netbuffer->packettype));
+			Net_CloseConnection(node);
+	}
+}
+
+/** Handles a packet received from a node that is in game
+  *
+  * \param node The packet sender
+  * \todo Choose a better name
+  * \sa HandlePacketFromAwayNode
+  * \sa GetPackets
+  *
+  */
+static void HandlePacketFromPlayer(SINT8 node)
+{
+	INT32 netconsole;
+
+	if (dedicated && node == 0)
+		netconsole = 0;
+	else
+		netconsole = netnodes[node].player;
+
+#ifdef PARANOIA
+	if (netconsole >= MAXPLAYERS)
+		I_Error("bad table nodetoplayer: node %d player %d", doomcom->remotenode, netconsole);
+#endif
+
+	switch (netbuffer->packettype)
+	{
+		// SERVER RECEIVE
+		case PT_CLIENTCMD:
+		case PT_CLIENT2CMD:
+		case PT_CLIENTMIS:
+		case PT_CLIENT2MIS:
+		case PT_NODEKEEPALIVE:
+		case PT_NODEKEEPALIVEMIS:
+			PT_ClientCmd(node, netconsole);
+			break;
+		case PT_BASICKEEPALIVE     : PT_BasicKeepAlive     (node, netconsole); break;
+		case PT_TEXTCMD            : PT_TextCmd            (node, netconsole); break;
+		case PT_TEXTCMD2           : PT_TextCmd            (node, netconsole); break;
+		case PT_LOGIN              : PT_Login              (node, netconsole); break;
+		case PT_CLIENTQUIT         : PT_ClientQuit         (node, netconsole); break;
+		case PT_CANRECEIVEGAMESTATE: PT_CanReceiveGamestate(node            ); break;
+		case PT_ASKLUAFILE         : PT_AskLuaFile         (node            ); break;
+		case PT_HASLUAFILE         : PT_HasLuaFile         (node            ); break;
+		case PT_RECEIVEDGAMESTATE  : PT_ReceivedGamestate  (node            ); break;
+		case PT_SERVERINFO         : PT_ServerInfo         (node            ); break;
+
+		// CLIENT RECEIVE
+		case PT_SERVERTICS         : PT_ServerTics         (node, netconsole); break;
+		case PT_PING               : PT_Ping               (node, netconsole); break;
+		case PT_FILEFRAGMENT       : PT_FileFragment       (node, netconsole); break;
+		case PT_FILEACK            : PT_FileAck            (node            ); break;
+		case PT_FILERECEIVED       : PT_FileReceived       (node            ); break;
+		case PT_WILLRESENDGAMESTATE: PT_WillResendGamestate(node            ); break;
+		case PT_SENDINGLUAFILE     : PT_SendingLuaFile     (node            ); break;
+		case PT_SERVERSHUTDOWN     : PT_ServerShutdown     (node            ); break;
+		case PT_SERVERCFG          :                                           break;
+		case PT_CLIENTJOIN         :                                           break;
+
+		default:
+			DEBFILE(va("UNKNOWN PACKET TYPE RECEIVED %d from host %d\n",
+				netbuffer->packettype, node));
+	}
+}
+
+/**	Handles all received packets, if any
+  *
+  * \todo Add details to this description (lol)
+  *
+  */
+void GetPackets(void)
+{
+	while (HGetPacket())
+	{
+		SINT8 node = doomcom->remotenode;
+
+		// Packet received from someone already playing
+		if (netnodes[node].ingame)
+			HandlePacketFromPlayer(node);
+		// Packet received from someone not playing
+		else
+			HandlePacketFromAwayNode(node);
+	}
+}
+
+boolean TryRunTics(tic_t realtics)
+{
+	// the machine has lagged but it is not so bad
+	if (realtics > TICRATE/7)
+	{
+		if (server)
+			realtics = 1;
+		else
+			realtics = TICRATE/7;
+	}
+
+	if (singletics)
+		realtics = 1;
+
+	if (realtics >= 1)
+	{
+		COM_BufTicker();
+		if (mapchangepending)
+			D_MapChange(-1, 0, ultimatemode, false, 2, false, fromlevelselect); // finish the map change
+	}
+
+	NetUpdate();
+
+	if (demoplayback)
+	{
+		neededtic = gametic + realtics;
+		// start a game after a demo
+		maketic += realtics;
+		firstticstosend = maketic;
+		tictoclear = firstticstosend;
+	}
+
+	GetPackets();
+
+#ifdef DEBUGFILE
+	if (debugfile && (realtics || neededtic > gametic))
+	{
+		fprintf(debugfile, "------------ Tryruntic: REAL:%d NEED:%d GAME:%d LOAD: %d\n",
+			realtics, neededtic, gametic, debugload);
+		debugload = 100000;
+	}
+#endif
+
+	if (neededtic > gametic)
+	{
+		if (realtics)
+			hu_stopped = false;
+
+		if (advancedemo)
+		{
+			if (timedemo_quit)
+				COM_ImmedExecute("quit");
+			else
+				D_StartTitle();
+		}
+		else
+			// run the count * tics
+			while (neededtic > gametic)
+			{
+				boolean update_stats = !(paused || P_AutoPause());
+
+				DEBFILE(va("============ Running tic %d (local %d)\n", gametic, localgametic));
+
+				if (update_stats)
+					PS_START_TIMING(ps_tictime);
+
+				G_Ticker((gametic % NEWTICRATERATIO) == 0);
+				ExtraDataTicker();
+				gametic++;
+				consistancy[gametic%BACKUPTICS] = Consistancy();
+
+				if (update_stats)
+				{
+					PS_STOP_TIMING(ps_tictime);
+					PS_UpdateTickStats();
+				}
+
+				// Leave a certain amount of tics present in the net buffer as long as we've ran at least one tic this frame.
+				if (client && gamestate == GS_LEVEL && leveltime > 3 && neededtic <= gametic + cv_netticbuffer.value)
+					break;
+			}
+
+		return true;
+	}
+	else
+	{
+		if (realtics)
+			hu_stopped = true;
+
+		return false;
+	}
+}
+
+static void UpdatePingTable(void)
+{
+	if (server)
+	{
+		if (netgame && !(gametime % 35)) // update once per second.
+			PingUpdate();
+		// update node latency values so we can take an average later.
+		for (INT32 i = 0; i < MAXPLAYERS; i++)
+			if (playeringame[i] && playernode[i] != UINT8_MAX)
+				realpingtable[i] += G_TicsToMilliseconds(GetLag(playernode[i]));
+		pingmeasurecount++;
+	}
+}
+
+static void IdleUpdate(void)
+{
+	INT32 i;
+	if (!server || !netgame)
+		return;
+
+	for (i = 1; i < MAXPLAYERS; i++)
+	{
+		if (cv_idletime.value && playeringame[i] && playernode[i] != UINT8_MAX && !players[i].quittime && !players[i].spectator && !players[i].bot && !IsPlayerAdmin(i) && i != serverplayer)
+		{
+			if (players[i].cmd.forwardmove || players[i].cmd.sidemove || players[i].cmd.buttons)
+				players[i].lastinputtime = 0;
+			else
+				players[i].lastinputtime++;
+
+			if (players[i].lastinputtime > (tic_t)cv_idletime.value * TICRATE * 60)
+			{
+				players[i].lastinputtime = 0;
+				SendKick(i, KICK_MSG_IDLE | KICK_MSG_KEEP_BODY);
+			}
+		}
+		else
+			players[i].lastinputtime = 0;
+	}
+}
+
+// Handle timeouts to prevent definitive freezes from happenning
+static void HandleNodeTimeouts(void)
+{
+	if (server)
+	{
+		for (INT32 i = 1; i < MAXNETNODES; i++)
+			if (netnodes[i].ingame && netnodes[i].freezetimeout < I_GetTime())
+				Net_ConnectionTimeout(i);
+
+		// In case the cvar value was lowered
+		if (joindelay)
+			joindelay = min(joindelay - 1, 3 * (tic_t)cv_joindelay.value * TICRATE);
+	}
+}
+
+// Keep the network alive while not advancing tics!
+void NetKeepAlive(void)
+{
+	tic_t nowtime;
+	INT32 realtics;
+
+	nowtime = I_GetTime();
+	realtics = nowtime - gametime;
+
+	// return if there's no time passed since the last call
+	if (realtics <= 0) // nothing new to update
+		return;
+
+	UpdatePingTable();
+
+	GetPackets();
+
+	IdleUpdate();
+
+#ifdef MASTERSERVER
+	MasterClient_Ticker();
+#endif
+
+	if (client)
+	{
+		// send keep alive
+		CL_SendClientKeepAlive();
+		// No need to check for resynch because we aren't running any tics
+	}
+	else
+	{
+		SV_SendServerKeepAlive();
+	}
+
+	// No else because no tics are being run and we can't resynch during this
+
+	Net_AckTicker();
+	HandleNodeTimeouts();
+	FileSendTicker();
+}
+
+void NetUpdate(void)
+{
+	static tic_t resptime = 0;
+	tic_t nowtime;
+	INT32 realtics;
+
+	nowtime = I_GetTime();
+	realtics = nowtime - gametime;
+
+	if (realtics <= 0) // nothing new to update
+		return;
+
+	if (realtics > 5)
+	{
+		if (server)
+			realtics = 1;
+		else
+			realtics = 5;
+	}
+
+	if (server && dedicated && gamestate == GS_LEVEL)
+ 	{
+		const tic_t dedicatedidletime = cv_dedicatedidletime.value * TICRATE;
+		static tic_t dedicatedidletimeprev = 0;
+		static tic_t dedicatedidle = 0;
+
+		if (dedicatedidletime > 0)
+		{
+			INT32 i;
+
+			for (i = 1; i < MAXNETNODES; ++i)
+				if (netnodes[i].ingame)
+				{
+					if (dedicatedidle >= dedicatedidletime)
+					{
+						CONS_Printf("DEDICATED: Awakening from idle (Node %d detected...)\n", i);
+						dedicatedidle = 0;
+					}
+					break;
+				}
+
+			if (i == MAXNETNODES)
+			{
+				if (leveltime == 2)
+				{
+					// On next tick...
+					dedicatedidle = dedicatedidletime-1;
+				}
+				else if (dedicatedidle >= dedicatedidletime)
+				{
+					if (D_GetExistingTextcmd(gametic, 0) || D_GetExistingTextcmd(gametic+1, 0))
+					{
+						CONS_Printf("DEDICATED: Awakening from idle (Netxcmd detected...)\n");
+						dedicatedidle = 0;
+					}
+					else
+					{
+						realtics = 0;
+					}
+				}
+				else if ((dedicatedidle += realtics) >= dedicatedidletime)
+				{
+					const char *idlereason = "at round start";
+					if (leveltime > 3)
+						idlereason = va("for %d seconds", dedicatedidle/TICRATE);
+
+					CONS_Printf("DEDICATED: No nodes %s, idling...\n", idlereason);
+					realtics = 0;
+					dedicatedidle = dedicatedidletime;
+				}
+			}
+		}
+		else
+		{
+			if (dedicatedidletimeprev > 0 && dedicatedidle >= dedicatedidletimeprev)
+			{
+				CONS_Printf("DEDICATED: Awakening from idle (Idle disabled...)\n");
+			}
+			dedicatedidle = 0;
+		}
+
+		dedicatedidletimeprev = dedicatedidletime;
+ 	}
+
+	gametime = nowtime;
+
+	UpdatePingTable();
+
+	if (client)
+		maketic = neededtic;
+
+	Local_Maketic(realtics);
+
+	if (server)
+		CL_SendClientCmd(); // send it
+
+	GetPackets(); // get packet from client or from server
+
+	IdleUpdate();
+
+	// The client sends the command after receiving from the server
+	// The server sends it before because this is better in single player
+
+#ifdef MASTERSERVER
+	MasterClient_Ticker(); // Acking the Master Server
+#endif
+
+	if (client)
+	{
+		// If the client just finished redownloading the game state, load it
+		if (cl_redownloadinggamestate && fileneeded[0].status == FS_FOUND)
+			CL_ReloadReceivedSavegame();
+
+		CL_SendClientCmd(); // Send tic cmd
+		hu_redownloadinggamestate = cl_redownloadinggamestate;
+	}
+	else
+	{
+		if (!demoplayback && realtics > 0)
+		{
+			hu_redownloadinggamestate = false;
+
+			firstticstosend = gametic;
+			for (INT32 i = 0; i < MAXNETNODES; i++)
+				if (netnodes[i].ingame)
+				{
+					if (netnodes[i].tic < firstticstosend)
+						firstticstosend = netnodes[i].tic;
+
+					if (maketic + realtics >= netnodes[i].tic + BACKUPTICS - TICRATE)
+						Net_ConnectionTimeout(i);
+				}
+
+			// Don't erase tics not acknowledged
+			INT32 counts = realtics;
+			if (maketic + counts >= firstticstosend + BACKUPTICS)
+				counts = firstticstosend+BACKUPTICS-maketic-1;
+
+			for (INT32 i = 0; i < counts; i++)
+				SV_Maketic(); // Create missed tics and increment maketic
+
+			for (; tictoclear < firstticstosend; tictoclear++) // Clear only when acknowledged
+				D_Clearticcmd(tictoclear);                    // Clear the maketic the new tic
+
+			SV_SendTics();
+
+			neededtic = maketic; // The server is a client too
+		}
+	}
+
+	Net_AckTicker();
+	HandleNodeTimeouts();
+
+	nowtime /= NEWTICRATERATIO;
+
+	if (nowtime > resptime)
+	{
+		resptime = nowtime;
+#ifdef HAVE_THREADS
+		I_lock_mutex(&m_menu_mutex);
+#endif
+		M_Ticker();
+#ifdef HAVE_THREADS
+		I_unlock_mutex(m_menu_mutex);
+#endif
+		CON_Ticker();
+	}
+
+	FileSendTicker();
+}
+
+// called one time at init
+void D_ClientServerInit(void)
+{
+	DEBFILE(va("- - -== SRB2 v%d.%.2d.%d "VERSIONSTRING" debugfile ==- - -\n",
+		VERSION/100, VERSION%100, SUBVERSION));
+
+	COM_AddCommand("getplayernum", Command_GetPlayerNum, COM_LUA);
+	COM_AddCommand("kick", Command_Kick, COM_LUA);
+	COM_AddCommand("ban", Command_Ban, COM_LUA);
+	COM_AddCommand("banip", Command_BanIP, COM_LUA);
+	COM_AddCommand("clearbans", Command_ClearBans, COM_LUA);
+	COM_AddCommand("showbanlist", Command_ShowBan, COM_LUA);
+	COM_AddCommand("reloadbans", Command_ReloadBan, COM_LUA);
+	COM_AddCommand("connect", Command_connect, COM_LUA);
+	COM_AddCommand("nodes", Command_Nodes, COM_LUA);
+	COM_AddCommand("resendgamestate", Command_ResendGamestate, COM_LUA);
+#ifdef PACKETDROP
+	COM_AddCommand("drop", Command_Drop, COM_LUA);
+	COM_AddCommand("droprate", Command_Droprate, COM_LUA);
+#endif
+#ifdef _DEBUG
+	COM_AddCommand("numnodes", Command_Numnodes, COM_LUA);
+#endif
+
+	RegisterNetXCmd(XD_KICK, Got_KickCmd);
+	RegisterNetXCmd(XD_ADDPLAYER, Got_AddPlayer);
+#ifdef DUMPCONSISTENCY
+	CV_RegisterVar(&cv_dumpconsistency);
+#endif
+	Ban_Load_File(false);
+
+	gametic = 0;
+	localgametic = 0;
+
+	// do not send anything before the real begin
+	SV_StopServer();
+	SV_ResetServer();
+	if (dedicated)
+		SV_SpawnServer();
+}
+
+SINT8 nametonum(const char *name)
+{
+	INT32 playernum;
+
+	if (!strcmp(name, "0"))
+		return 0;
+
+	playernum = (SINT8)atoi(name);
+
+	if (playernum < 0 || playernum >= MAXPLAYERS)
+		return -1;
+
+	if (playernum)
+	{
+		if (playeringame[playernum])
+			return (SINT8)playernum;
+		else
+			return -1;
+	}
+
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+		if (playeringame[i] && !stricmp(player_names[i], name))
+			return (SINT8)i;
+
+	CONS_Printf(M_GetText("There is no player named \"%s\"\n"), name);
+
+	return -1;
+}
+
+// Is there a game running?
+boolean Playing(void)
+{
+	return (server && serverrunning) || (client && cl_mode == CL_CONNECTED);
+}
+
+/** Returns the number of players playing.
+  * \return Number of players. Can be zero if we're running a ::dedicated
+  *         server.
+  * \author Graue <graue@oceanbase.org>
+  */
+INT32 D_NumPlayers(void)
+{
+	INT32 num = 0;
+	for (INT32 ix = 0; ix < MAXPLAYERS; ix++)
+		if (playeringame[ix])
+			num++;
+	return num;
+}
+
+/** Returns the number of nodes on the server.
+  */
+INT32 D_NumNodes(void)
+{
+	INT32 num = 0;
+	for (INT32 ix = 0; ix < MAXNETNODES; ix++)
+		if (netnodes[ix].ingame)
+			num++;
+	return num;
+}
+
+/** Similar to the above, but counts only bots.
+  * Purpose is to remove bots from both the player count and the
+  * max player count on the server view
+*/
+INT32 D_NumBots(void)
+{
+	INT32 num = 0, ix;
+	for (ix = 0; ix < MAXPLAYERS; ix++)
+		if (playeringame[ix] && players[ix].bot)
+			num++;
+	return num;
+}
+
+
+//
+// Consistancy
+//
+// Note: It is called consistAncy on purpose.
+//
+INT16 Consistancy(void)
+{
+	UINT32 ret = 0;
+#ifdef MOBJCONSISTANCY
+	thinker_t *th;
+	mobj_t *mo;
+#endif
+
+	DEBFILE(va("TIC %u ", gametic));
+
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!playeringame[i])
+			ret ^= 0xCCCC;
+		else if (!players[i].mo);
+		else
+		{
+			ret += players[i].mo->x;
+			ret -= players[i].mo->y;
+			ret += players[i].powers[pw_shield];
+			ret *= i+1;
+		}
+	}
+	// I give up
+	// Coop desynching enemies is painful
+	if (!G_PlatformGametype())
+		ret += P_GetRandSeed();
+
+#ifdef MOBJCONSISTANCY
+	if (gamestate == GS_LEVEL)
+	{
+		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
+		{
+			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
+				continue;
+
+			mo = (mobj_t *)th;
+
+			if (mo->flags & (MF_SPECIAL | MF_SOLID | MF_PUSHABLE | MF_BOSS | MF_MISSILE | MF_SPRING | MF_MONITOR | MF_FIRE | MF_ENEMY | MF_PAIN | MF_STICKY))
+			{
+				ret -= mo->type;
+				ret += mo->x;
+				ret -= mo->y;
+				ret += mo->z;
+				ret -= mo->momx;
+				ret += mo->momy;
+				ret -= mo->momz;
+				ret += mo->angle;
+				ret -= mo->flags;
+				ret += mo->flags2;
+				ret -= mo->eflags;
+				if (mo->target)
+				{
+					ret += mo->target->type;
+					ret -= mo->target->x;
+					ret += mo->target->y;
+					ret -= mo->target->z;
+					ret += mo->target->momx;
+					ret -= mo->target->momy;
+					ret += mo->target->momz;
+					ret -= mo->target->angle;
+					ret += mo->target->flags;
+					ret -= mo->target->flags2;
+					ret += mo->target->eflags;
+					ret -= mo->target->state - states;
+					ret += mo->target->tics;
+					ret -= mo->target->sprite;
+					ret += mo->target->frame;
+				}
+				else
+					ret ^= 0x3333;
+				if (mo->tracer && mo->tracer->type != MT_OVERLAY)
+				{
+					ret += mo->tracer->type;
+					ret -= mo->tracer->x;
+					ret += mo->tracer->y;
+					ret -= mo->tracer->z;
+					ret += mo->tracer->momx;
+					ret -= mo->tracer->momy;
+					ret += mo->tracer->momz;
+					ret -= mo->tracer->angle;
+					ret += mo->tracer->flags;
+					ret -= mo->tracer->flags2;
+					ret += mo->tracer->eflags;
+					ret -= mo->tracer->state - states;
+					ret += mo->tracer->tics;
+					ret -= mo->tracer->sprite;
+					ret += mo->tracer->frame;
+				}
+				else
+					ret ^= 0xAAAA;
+				ret -= mo->state - states;
+				ret += mo->tics;
+				ret -= mo->sprite;
+				ret += mo->frame;
+			}
+		}
+	}
+#endif
+
+	DEBFILE(va("Consistancy = %u\n", (ret & 0xFFFF)));
+
+	return (INT16)(ret & 0xFFFF);
+}
+
+tic_t GetLag(INT32 node)
+{
+	return gametic - netnodes[node].tic;
+}
+
+void D_MD5PasswordPass(const UINT8 *buffer, size_t len, const char *salt, void *dest)
+{
+#ifdef NOMD5
+	(void)buffer;
+	(void)len;
+	(void)salt;
+	memset(dest, 0, 16);
+#else
+	char tmpbuf[256];
+	const size_t sl = strlen(salt);
+
+	if (len > 256-sl)
+		len = 256-sl;
+
+	memcpy(tmpbuf, buffer, len);
+	memmove(&tmpbuf[len], salt, sl);
+	//strcpy(&tmpbuf[len], salt);
+	len += strlen(salt);
+	if (len < 256)
+		memset(&tmpbuf[len],0,256-len);
+
+	// Yes, we intentionally md5 the ENTIRE buffer regardless of size...
+	md5_buffer(tmpbuf, 256, dest);
+#endif
+}
diff --git a/src/netcode/d_clisrv.h b/src/netcode/d_clisrv.h
new file mode 100644
index 0000000000000000000000000000000000000000..f9cbf8876833486c232e7f51bbf8ae699ba6bd96
--- /dev/null
+++ b/src/netcode/d_clisrv.h
@@ -0,0 +1,136 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 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  d_clisrv.h
+/// \brief high level networking stuff
+
+#ifndef __D_CLISRV__
+#define __D_CLISRV__
+
+#include "protocol.h"
+#include "../d_ticcmd.h"
+#include "d_net.h"
+#include "d_netcmd.h"
+#include "d_net.h"
+#include "../tables.h"
+#include "../d_player.h"
+#include "mserv.h"
+
+#define CLIENTBACKUPTICS 32
+
+#ifdef PACKETDROP
+void Command_Drop(void);
+void Command_Droprate(void);
+#endif
+#ifdef _DEBUG
+void Command_Numnodes(void);
+#endif
+
+extern INT32 mapchangepending;
+
+// Points inside doomcom
+extern doomdata_t *netbuffer;
+
+#define BASEPACKETSIZE      offsetof(doomdata_t, u)
+#define BASESERVERTICSSIZE  offsetof(doomdata_t, u.serverpak.cmds[0])
+
+typedef enum
+{
+	KR_KICK          = 1, //Kicked by server
+	KR_PINGLIMIT     = 2, //Broke Ping Limit
+	KR_SYNCH         = 3, //Synch Failure
+	KR_TIMEOUT       = 4, //Connection Timeout
+	KR_BAN           = 5, //Banned by server
+	KR_LEAVE         = 6, //Quit the game
+	KR_IDLE          = 7, //Remained still for too long
+} kickreason_t;
+
+/* the max number of name changes in some time period */
+#define MAXNAMECHANGES (5)
+#define NAMECHANGERATE (60*TICRATE)
+
+extern boolean server;
+extern boolean serverrunning;
+#define client (!server)
+extern boolean dedicated; // For dedicated server
+extern UINT16 software_MAXPACKETLENGTH;
+extern boolean acceptnewnode;
+extern SINT8 servernode;
+extern tic_t maketic;
+extern tic_t neededtic;
+extern INT16 consistancy[BACKUPTICS];
+
+void Command_Ping_f(void);
+extern tic_t connectiontimeout;
+extern UINT16 pingmeasurecount;
+extern UINT32 realpingtable[MAXPLAYERS];
+extern UINT32 playerpingtable[MAXPLAYERS];
+extern tic_t servermaxping;
+
+extern consvar_t cv_netticbuffer, cv_resynchattempts, cv_blamecfail, cv_playbackspeed, cv_idletime, cv_dedicatedidletime;
+
+// Used in d_net, the only dependence
+void D_ClientServerInit(void);
+
+// Create any new ticcmds and broadcast to other players.
+void NetUpdate(void);
+
+// Maintain connections to nodes without timing them all out.
+void NetKeepAlive(void);
+
+void GetPackets(void);
+void ResetNode(INT32 node);
+INT16 Consistancy(void);
+
+void SV_StartSinglePlayerServer(void);
+void SV_SpawnServer(void);
+void SV_StopServer(void);
+void SV_ResetServer(void);
+void CL_AddSplitscreenPlayer(void);
+void CL_RemoveSplitscreenPlayer(void);
+void CL_Reset(void);
+void CL_ClearPlayer(INT32 playernum);
+void CL_RemovePlayer(INT32 playernum, kickreason_t reason);
+void CL_HandleTimeout(void);
+// Is there a game running
+boolean Playing(void);
+
+// Broadcasts special packets to other players
+//  to notify of game exit
+void D_QuitNetGame(void);
+
+//? How many ticks to run?
+boolean TryRunTics(tic_t realtic);
+
+// extra data for lmps
+// these functions scare me. they contain magic.
+/*boolean AddLmpExtradata(UINT8 **demo_p, INT32 playernum);
+void ReadLmpExtraData(UINT8 **demo_pointer, INT32 playernum);*/
+
+// translate a playername in a player number return -1 if not found and
+// print a error message in the console
+SINT8 nametonum(const char *name);
+
+extern char motd[254], server_context[8];
+extern UINT8 playernode[MAXPLAYERS];
+
+INT32 D_NumPlayers(void);
+INT32 D_NumNodes(void);
+INT32 D_NumBots(void);
+
+tic_t GetLag(INT32 node);
+
+void D_MD5PasswordPass(const UINT8 *buffer, size_t len, const char *salt, void *dest);
+
+extern UINT8 (*adminpassmd5)[16];
+extern UINT32 adminpasscount;
+
+extern boolean hu_stopped;
+
+#endif
diff --git a/src/d_net.c b/src/netcode/d_net.c
similarity index 93%
rename from src/d_net.c
rename to src/netcode/d_net.c
index 6d8c72942bbe8cd6714db4c6761a17e13f70bdef..5a2e229d3f47c9eadf66729a1250a1d2b0719343 100644
--- a/src/d_net.c
+++ b/src/netcode/d_net.c
@@ -16,19 +16,21 @@
 ///        This protocol uses a mix of "goback n" and "selective repeat" implementation
 ///        The NOTHING packet is sent when connection is idle to acknowledge packets
 
-#include "doomdef.h"
-#include "g_game.h"
-#include "i_time.h"
+#include "../doomdef.h"
+#include "../g_game.h"
+#include "../i_time.h"
 #include "i_net.h"
-#include "i_system.h"
-#include "m_argv.h"
+#include "../i_system.h"
+#include "../m_argv.h"
 #include "d_net.h"
-#include "w_wad.h"
+#include "../w_wad.h"
 #include "d_netfil.h"
 #include "d_clisrv.h"
-#include "z_zone.h"
+#include "tic_command.h"
+#include "net_command.h"
+#include "../z_zone.h"
 #include "i_tcp.h"
-#include "d_main.h" // srb2home
+#include "../d_main.h" // srb2home
 
 //
 // NETWORKING
@@ -138,7 +140,6 @@ boolean Net_GetNetStat(void)
 #define URGENTFREESLOTNUM 10
 #define ACKTOSENDTIMEOUT (TICRATE/11)
 
-#ifndef NONET
 typedef struct
 {
 	UINT8 acknum;
@@ -152,7 +153,6 @@ typedef struct
 		doomdata_t data;
 	} pak;
 } ackpak_t;
-#endif
 
 typedef enum
 {
@@ -160,10 +160,8 @@ typedef enum
 	NF_TIMEOUT = 2, // Flag is set when the node got a timeout
 } node_flags_t;
 
-#ifndef NONET
 // Table of packets that were not acknowleged can be resent (the sender window)
 static ackpak_t ackpak[MAXACKPACKETS];
-#endif
 
 typedef struct
 {
@@ -191,7 +189,6 @@ typedef struct
 static node_t nodes[MAXNETNODES];
 #define NODETIMEOUT 14
 
-#ifndef NONET
 // return <0 if a < b (mod 256)
 //         0 if a = n (mod 256)
 //        >0 if a > b (mod 256)
@@ -214,7 +211,7 @@ FUNCMATH static INT32 cmpack(UINT8 a, UINT8 b)
 static boolean GetFreeAcknum(UINT8 *freeack, boolean lowtimer)
 {
 	node_t *node = &nodes[doomcom->remotenode];
-	INT32 i, numfreeslot = 0;
+	INT32 numfreeslot = 0;
 
 	if (cmpack((UINT8)((node->remotefirstack + MAXACKTOSEND) % 256), node->nextacknum) < 0)
 	{
@@ -222,7 +219,7 @@ static boolean GetFreeAcknum(UINT8 *freeack, boolean lowtimer)
 		return false;
 	}
 
-	for (i = 0; i < MAXACKPACKETS; i++)
+	for (INT32 i = 0; i < MAXACKPACKETS; i++)
 		if (!ackpak[i].acknum)
 		{
 			// For low priority packets, make sure to let freeslots so urgent packets can be sent
@@ -279,10 +276,10 @@ static boolean GetFreeAcknum(UINT8 *freeack, boolean lowtimer)
   */
 INT32 Net_GetFreeAcks(boolean urgent)
 {
-	INT32 i, numfreeslot = 0;
+	INT32 numfreeslot = 0;
 	INT32 n = 0; // Number of free acks found
 
-	for (i = 0; i < MAXACKPACKETS; i++)
+	for (INT32 i = 0; i < MAXACKPACKETS; i++)
 		if (!ackpak[i].acknum)
 		{
 			// For low priority packets, make sure to let freeslots so urgent packets can be sent
@@ -316,10 +313,9 @@ static void RemoveAck(INT32 i)
 }
 
 // We have got a packet, proceed the ack request and ack return
-static boolean Processackpak(void)
+static int Processackpak(void)
 {
-	INT32 i;
-	boolean goodpacket = true;
+	int goodpacket = 0;
 	node_t *node = &nodes[doomcom->remotenode];
 
 	// Received an ack return, so remove the ack in the list
@@ -327,7 +323,7 @@ static boolean Processackpak(void)
 	{
 		node->remotefirstack = netbuffer->ackreturn;
 		// Search the ackbuffer and free it
-		for (i = 0; i < MAXACKPACKETS; i++)
+		for (INT32 i = 0; i < MAXACKPACKETS; i++)
 			if (ackpak[i].acknum && ackpak[i].destinationnode == node - nodes
 				&& cmpack(ackpak[i].acknum, netbuffer->ackreturn) <= 0)
 			{
@@ -344,20 +340,20 @@ static boolean Processackpak(void)
 		{
 			DEBFILE(va("Discard(1) ack %d (duplicated)\n", ack));
 			duppacket++;
-			goodpacket = false; // Discard packet (duplicate)
+			goodpacket = 1; // Discard packet (duplicate)
 		}
 		else
 		{
 			// Check if it is not already in the queue
-			for (i = node->acktosend_tail; i != node->acktosend_head; i = (i+1) % MAXACKTOSEND)
+			for (INT32 i = node->acktosend_tail; i != node->acktosend_head; i = (i+1) % MAXACKTOSEND)
 				if (node->acktosend[i] == ack)
 				{
 					DEBFILE(va("Discard(2) ack %d (duplicated)\n", ack));
 					duppacket++;
-					goodpacket = false; // Discard packet (duplicate)
+					goodpacket = 1; // Discard packet (duplicate)
 					break;
 				}
-			if (goodpacket)
+			if (goodpacket == 0)
 			{
 				// Is a good packet so increment the acknowledge number,
 				// Then search for a "hole" in the queue
@@ -377,7 +373,7 @@ static boolean Processackpak(void)
 					while (change)
 					{
 						change = false;
-						for (i = node->acktosend_tail; i != node->acktosend_head;
+						for (INT32 i = node->acktosend_tail; i != node->acktosend_head;
 							i = (i+1) % MAXACKTOSEND)
 						{
 							if (cmpack(node->acktosend[i], nextfirstack) <= 0)
@@ -418,36 +414,29 @@ static boolean Processackpak(void)
 					else // Buffer full discard packet, sender will resend it
 					{ // We can admit the packet but we will not detect the duplication after :(
 						DEBFILE("no more freeackret\n");
-						goodpacket = false;
+						goodpacket = 2;
 					}
 				}
 			}
 		}
 	}
+	// return values: 0 = ok, 1 = duplicate, 2 = out of order
 	return goodpacket;
 }
-#endif
 
 // send special packet with only ack on it
 void Net_SendAcks(INT32 node)
 {
-#ifdef NONET
-	(void)node;
-#else
 	netbuffer->packettype = PT_NOTHING;
 	M_Memcpy(netbuffer->u.textcmd, nodes[node].acktosend, MAXACKTOSEND);
 	HSendPacket(node, false, 0, MAXACKTOSEND);
-#endif
 }
 
-#ifndef NONET
 static void GotAcks(void)
 {
-	INT32 i, j;
-
-	for (j = 0; j < MAXACKTOSEND; j++)
+	for (INT32 j = 0; j < MAXACKTOSEND; j++)
 		if (netbuffer->u.textcmd[j])
-			for (i = 0; i < MAXACKPACKETS; i++)
+			for (INT32 i = 0; i < MAXACKPACKETS; i++)
 				if (ackpak[i].acknum && ackpak[i].destinationnode == doomcom->remotenode)
 				{
 					if (ackpak[i].acknum == netbuffer->u.textcmd[j])
@@ -463,7 +452,6 @@ static void GotAcks(void)
 						}
 				}
 }
-#endif
 
 void Net_ConnectionTimeout(INT32 node)
 {
@@ -472,14 +460,10 @@ void Net_ConnectionTimeout(INT32 node)
 		return;
 	nodes[node].flags |= NF_TIMEOUT;
 
-	// Send a very special packet to self (hack the reboundstore queue)
-	// Main code will handle it
-	reboundstore[rebound_head].packettype = PT_NODETIMEOUT;
-	reboundstore[rebound_head].ack = 0;
-	reboundstore[rebound_head].ackreturn = 0;
-	reboundstore[rebound_head].u.textcmd[0] = (UINT8)node;
-	reboundsize[rebound_head] = (INT16)(BASEPACKETSIZE + 1);
-	rebound_head = (rebound_head+1) % MAXREBOUND;
+	if (server)
+		SendKicksForNode(node, KICK_MSG_TIMEOUT | KICK_MSG_KEEP_BODY);
+	else
+		CL_HandleTimeout();
 
 	// Do not redo it quickly (if we do not close connection it is
 	// for a good reason!)
@@ -489,10 +473,8 @@ void Net_ConnectionTimeout(INT32 node)
 // Resend the data if needed
 void Net_AckTicker(void)
 {
-#ifndef NONET
-	INT32 i;
 
-	for (i = 0; i < MAXACKPACKETS; i++)
+	for (INT32 i = 0; i < MAXACKPACKETS; i++)
 	{
 		const INT32 nodei = ackpak[i].destinationnode;
 		node_t *node = &nodes[nodei];
@@ -519,7 +501,7 @@ void Net_AckTicker(void)
 		}
 	}
 
-	for (i = 1; i < MAXNETNODES; i++)
+	for (INT32 i = 1; i < MAXNETNODES; i++)
 	{
 		// This is something like node open flag
 		if (nodes[i].firstacktosend)
@@ -536,16 +518,12 @@ void Net_AckTicker(void)
 			}
 		}
 	}
-#endif
 }
 
 // Remove last packet received ack before resending the ackreturn
 // (the higher layer doesn't have room, or something else ....)
 void Net_UnAcknowledgePacket(INT32 node)
 {
-#ifdef NONET
-	(void)node;
-#else
 	INT32 hm1 = (nodes[node].acktosend_head-1+MAXACKTOSEND) % MAXACKTOSEND;
 	DEBFILE(va("UnAcknowledge node %d\n", node));
 	if (!node)
@@ -577,10 +555,8 @@ void Net_UnAcknowledgePacket(INT32 node)
 		if (!nodes[node].firstacktosend)
 			nodes[node].firstacktosend = 1;
 	}
-#endif
 }
 
-#ifndef NONET
 /** Checks if all acks have been received
   *
   * \return True if all acks have been received
@@ -588,15 +564,12 @@ void Net_UnAcknowledgePacket(INT32 node)
   */
 static boolean Net_AllAcksReceived(void)
 {
-	INT32 i;
-
-	for (i = 0; i < MAXACKPACKETS; i++)
+	for (INT32 i = 0; i < MAXACKPACKETS; i++)
 		if (ackpak[i].acknum)
 			return false;
 
 	return true;
 }
-#endif
 
 /** Waits for all ackreturns
   *
@@ -605,9 +578,6 @@ static boolean Net_AllAcksReceived(void)
   */
 void Net_WaitAllAckReceived(UINT32 timeout)
 {
-#ifdef NONET
-	(void)timeout;
-#else
 	tic_t tictac = I_GetTime();
 	timeout = tictac + timeout*NEWTICRATE;
 
@@ -623,7 +593,6 @@ void Net_WaitAllAckReceived(UINT32 timeout)
 		HGetPacket();
 		Net_AckTicker();
 	}
-#endif
 }
 
 static void InitNode(node_t *node)
@@ -637,14 +606,10 @@ static void InitNode(node_t *node)
 
 static void InitAck(void)
 {
-	INT32 i;
-
-#ifndef NONET
-	for (i = 0; i < MAXACKPACKETS; i++)
+	for (INT32 i = 0; i < MAXACKPACKETS; i++)
 		ackpak[i].acknum = 0;
-#endif
 
-	for (i = 0; i < MAXNETNODES; i++)
+	for (INT32 i = 0; i < MAXNETNODES; i++)
 		InitNode(&nodes[i]);
 }
 
@@ -655,17 +620,12 @@ static void InitAck(void)
   */
 void Net_AbortPacketType(UINT8 packettype)
 {
-#ifdef NONET
-	(void)packettype;
-#else
-	INT32 i;
-	for (i = 0; i < MAXACKPACKETS; i++)
+	for (INT32 i = 0; i < MAXACKPACKETS; i++)
 		if (ackpak[i].acknum && (ackpak[i].pak.data.packettype == packettype
 			|| packettype == UINT8_MAX))
 		{
 			ackpak[i].acknum = 0;
 		}
-#endif
 }
 
 // -----------------------------------------------------------------
@@ -675,10 +635,6 @@ void Net_AbortPacketType(UINT8 packettype)
 // remove a node, clear all ack from this node and reset askret
 void Net_CloseConnection(INT32 node)
 {
-#ifdef NONET
-	(void)node;
-#else
-	INT32 i;
 	boolean forceclose = (node & FORCECLOSE) != 0;
 
 	if (node == -1)
@@ -708,7 +664,7 @@ void Net_CloseConnection(INT32 node)
 	}
 
 	// check if we are waiting for an ack from this node
-	for (i = 0; i < MAXACKPACKETS; i++)
+	for (INT32 i = 0; i < MAXACKPACKETS; i++)
 		if (ackpak[i].acknum && ackpak[i].destinationnode == node)
 		{
 			if (!forceclose)
@@ -722,10 +678,8 @@ void Net_CloseConnection(INT32 node)
 	if (server)
 		SV_AbortLuaFileTransfer(node);
 	I_NetFreeNodenum(node);
-#endif
 }
 
-#ifndef NONET
 //
 // Checksum
 //
@@ -734,23 +688,20 @@ static UINT32 NetbufferChecksum(void)
 	UINT32 c = 0x1234567;
 	const INT32 l = doomcom->datalength - 4;
 	const UINT8 *buf = (UINT8 *)netbuffer + 4;
-	INT32 i;
 
-	for (i = 0; i < l; i++, buf++)
+	for (INT32 i = 0; i < l; i++, buf++)
 		c += (*buf) * (i+1);
 
 	return LONG(c);
 }
-#endif
 
 #ifdef DEBUGFILE
 
 static void fprintfstring(char *s, size_t len)
 {
 	INT32 mode = 0;
-	size_t i;
 
-	for (i = 0; i < len; i++)
+	for (size_t i = 0; i < len; i++)
 		if (s[i] < 32)
 		{
 			if (!mode)
@@ -810,6 +761,8 @@ static const char *packettypename[NUMPACKETTYPE] =
 	"ASKLUAFILE",
 	"HASLUAFILE",
 
+	"PT_BASICKEEPALIVE",
+
 	"FILEFRAGMENT",
 	"FILEACK",
 	"FILERECEIVED",
@@ -817,7 +770,6 @@ static const char *packettypename[NUMPACKETTYPE] =
 	"TEXTCMD",
 	"TEXTCMD2",
 	"CLIENTJOIN",
-	"NODETIMEOUT",
 	"LOGIN",
 	"TELLFILESNEEDED",
 	"MOREFILESNEEDED",
@@ -921,7 +873,6 @@ void Command_Drop(void)
 {
 	INT32 packetquantity;
 	const char *packetname;
-	size_t i;
 
 	if (COM_Argc() < 2)
 	{
@@ -951,11 +902,11 @@ void Command_Drop(void)
 	packetname = COM_Argv(1);
 
 	if (!(stricmp(packetname, "all") && stricmp(packetname, "any")))
-		for (i = 0; i < NUMPACKETTYPE; i++)
+		for (size_t i = 0; i < NUMPACKETTYPE; i++)
 			packetdropquantity[i] = packetquantity;
 	else
 	{
-		for (i = 0; i < NUMPACKETTYPE; i++)
+		for (size_t i = 0; i < NUMPACKETTYPE; i++)
 			if (!stricmp(packetname, packettypename[i]))
 			{
 				packetdropquantity[i] = packetquantity;
@@ -986,14 +937,12 @@ void Command_Droprate(void)
 	packetdroprate = droprate;
 }
 
-#ifndef NONET
 static boolean ShouldDropPacket(void)
 {
 	return (packetdropquantity[netbuffer->packettype])
 		|| (packetdroprate != 0 && rand() < (RAND_MAX * (packetdroprate / 100.f))) || packetdroprate == 100;
 }
 #endif
-#endif
 
 //
 // HSendPacket
@@ -1028,11 +977,6 @@ boolean HSendPacket(INT32 node, boolean reliable, UINT8 acknum, size_t packetlen
 	if (!netgame)
 		I_Error("Tried to transmit to another node");
 
-#ifdef NONET
-	(void)node;
-	(void)reliable;
-	(void)acknum;
-#else
 	// do this before GetFreeAcknum because this function backups
 	// the current packet
 	doomcom->remotenode = (INT16)node;
@@ -1093,8 +1037,6 @@ boolean HSendPacket(INT32 node, boolean reliable, UINT8 acknum, size_t packetlen
 	}
 #endif
 
-#endif // ndef NONET
-
 	return true;
 }
 
@@ -1112,10 +1054,7 @@ boolean HGetPacket(void)
 	{
 		M_Memcpy(netbuffer, &reboundstore[rebound_tail], reboundsize[rebound_tail]);
 		doomcom->datalength = reboundsize[rebound_tail];
-		if (netbuffer->packettype == PT_NODETIMEOUT)
-			doomcom->remotenode = netbuffer->u.textcmd[0];
-		else
-			doomcom->remotenode = 0;
+		doomcom->remotenode = 0;
 
 		rebound_tail = (rebound_tail+1) % MAXREBOUND;
 #ifdef DEBUGFILE
@@ -1128,11 +1067,10 @@ boolean HGetPacket(void)
 	if (!netgame)
 		return false;
 
-#ifndef NONET
-
 	while(true)
 	{
 		//nodejustjoined = I_NetGet();
+		int goodpacket;
 		I_NetGet();
 
 		if (doomcom->remotenode == -1) // No packet received
@@ -1178,8 +1116,15 @@ boolean HGetPacket(void)
 		}*/
 
 		// Proceed the ack and ackreturn field
-		if (!Processackpak())
+		goodpacket = Processackpak();
+		if (goodpacket != 0)
+		{
+			// resend the ACK in case the previous ACK didn't reach the client.
+			// prevents the client's netbuffer from locking up.
+			if (goodpacket == 1)
+				Net_SendAcks(doomcom->remotenode);
 			continue; // discarded (duplicated)
+		}
 
 		// A packet with just ackreturn
 		if (netbuffer->packettype == PT_NOTHING)
@@ -1189,7 +1134,6 @@ boolean HGetPacket(void)
 		}
 		break;
 	}
-#endif // ndef NONET
 
 	return true;
 }
@@ -1399,13 +1343,12 @@ void Command_Ping_f(void)
 	int name_width = 0;
 	int   ms_width = 0;
 
-	int n;
-	INT32 i;
-
 	pingc = 0;
-	for (i = 1; i < MAXPLAYERS; ++i)
+	for (INT32 i = 1; i < MAXPLAYERS; ++i)
 		if (playeringame[i])
 	{
+		int n;
+
 		n = strlen(player_names[i]);
 		if (n > name_width)
 			name_width = n;
@@ -1425,7 +1368,7 @@ void Command_Ping_f(void)
 
 	qsort(pingv, pingc, sizeof (struct pingcell), &pingcellcmp);
 
-	for (i = 0; i < pingc; ++i)
+	for (INT32 i = 0; i < pingc; ++i)
 	{
 		CONS_Printf("%02d : %-*s %*d ms\n",
 				pingv[i].num,
@@ -1441,15 +1384,13 @@ void Command_Ping_f(void)
 
 void D_CloseConnection(void)
 {
-	INT32 i;
-
 	if (netgame)
 	{
 		// wait the ackreturn with timout of 5 Sec
 		Net_WaitAllAckReceived(5);
 
 		// close all connection
-		for (i = 0; i < MAXNETNODES; i++)
+		for (INT32 i = 0; i < MAXNETNODES; i++)
 			Net_CloseConnection(i|FORCECLOSE);
 
 		InitAck();
diff --git a/src/d_net.h b/src/netcode/d_net.h
similarity index 73%
rename from src/d_net.h
rename to src/netcode/d_net.h
index ddedbef4a806df597a0e56b29a51b7c6e7f3c606..2f83939f8c5324883eb11e90dbb4e2897a54254c 100644
--- a/src/d_net.h
+++ b/src/netcode/d_net.h
@@ -18,6 +18,8 @@
 #ifndef __D_NET__
 #define __D_NET__
 
+#include "../doomtype.h"
+
 // Max computers in a game
 // 127 is probably as high as this can go, because
 // SINT8 is used for nodes sometimes >:(
@@ -37,10 +39,24 @@ boolean Net_GetNetStat(void);
 extern INT32 getbytes;
 extern INT64 sendbytes; // Realtime updated
 
-extern SINT8 nodetoplayer[MAXNETNODES];
-extern SINT8 nodetoplayer2[MAXNETNODES]; // Say the numplayer for this node if any (splitscreen)
-extern UINT8 playerpernode[MAXNETNODES]; // Used specially for splitscreen
-extern boolean nodeingame[MAXNETNODES]; // Set false as nodes leave game
+typedef struct netnode_s
+{
+	boolean ingame; // set false as nodes leave game
+	tic_t freezetimeout; // Until when can this node freeze the server before getting a timeout?
+
+	SINT8 player;
+	SINT8 player2; // say the numplayer for this node if any (splitscreen)
+	UINT8 numplayers; // used specialy for scplitscreen
+
+	tic_t tic; // what tic the client have received
+	tic_t supposedtic; // nettics prevision for smaller packet
+
+	boolean sendingsavegame; // Are we sending the savegame?
+	boolean resendingsavegame; // Are we resending the savegame?
+	tic_t savegameresendcooldown; // How long before we can resend again?
+} netnode_t;
+
+extern netnode_t netnodes[MAXNETNODES];
 
 extern boolean serverrunning;
 
@@ -52,11 +68,9 @@ boolean HSendPacket(INT32 node, boolean reliable, UINT8 acknum,
 	size_t packetlength);
 boolean HGetPacket(void);
 void D_SetDoomcom(void);
-#ifndef NONET
-void D_SaveBan(void);
-#endif
 boolean D_CheckNetGame(void);
 void D_CloseConnection(void);
+boolean Net_IsNodeIPv6(INT32 node);
 void Net_UnAcknowledgePacket(INT32 node);
 void Net_CloseConnection(INT32 node);
 void Net_ConnectionTimeout(INT32 node);
diff --git a/src/d_netcmd.c b/src/netcode/d_netcmd.c
similarity index 95%
rename from src/d_netcmd.c
rename to src/netcode/d_netcmd.c
index 3426c1f835d8025fcb18b245a90a2fd6c4c42c70..ef1ef9aeb1971d1a63b41944e59c161d8e05f676 100644
--- a/src/d_netcmd.c
+++ b/src/netcode/d_netcmd.c
@@ -12,44 +12,46 @@
 ///        commands are executed through the command buffer
 ///	       like console commands, other miscellaneous commands (at the end)
 
-#include "doomdef.h"
-
-#include "console.h"
-#include "command.h"
-#include "i_time.h"
-#include "i_system.h"
-#include "g_game.h"
-#include "hu_stuff.h"
-#include "g_input.h"
-#include "m_menu.h"
-#include "r_local.h"
-#include "r_skins.h"
-#include "p_local.h"
-#include "p_setup.h"
-#include "s_sound.h"
-#include "i_sound.h"
-#include "m_misc.h"
-#include "am_map.h"
-#include "byteptr.h"
+#include "../doomdef.h"
+
+#include "../console.h"
+#include "../command.h"
+#include "../i_time.h"
+#include "../i_system.h"
+#include "../g_game.h"
+#include "../hu_stuff.h"
+#include "../g_input.h"
+#include "../m_menu.h"
+#include "../r_local.h"
+#include "../r_skins.h"
+#include "../p_local.h"
+#include "../p_setup.h"
+#include "../s_sound.h"
+#include "../i_sound.h"
+#include "../m_misc.h"
+#include "../am_map.h"
+#include "../byteptr.h"
 #include "d_netfil.h"
-#include "p_spec.h"
-#include "m_cheat.h"
+#include "../p_spec.h"
+#include "../m_cheat.h"
 #include "d_clisrv.h"
+#include "server_connection.h"
+#include "net_command.h"
 #include "d_net.h"
-#include "v_video.h"
-#include "d_main.h"
-#include "m_random.h"
-#include "f_finale.h"
-#include "filesrch.h"
+#include "../v_video.h"
+#include "../d_main.h"
+#include "../m_random.h"
+#include "../f_finale.h"
+#include "../filesrch.h"
 #include "mserv.h"
-#include "z_zone.h"
-#include "lua_script.h"
-#include "lua_hook.h"
-#include "m_cond.h"
-#include "m_anigif.h"
-#include "md5.h"
-#include "m_perfstats.h"
-#include "u_list.h"
+#include "../z_zone.h"
+#include "../lua_script.h"
+#include "../lua_hook.h"
+#include "../m_cond.h"
+#include "../m_anigif.h"
+#include "../md5.h"
+#include "../m_perfstats.h"
+#include "../u_list.h"
 
 #ifdef NETGAME_DEVMODE
 #define CV_RESTRICT CV_NETVAR
@@ -148,6 +150,7 @@ static void Command_Clearscores_f(void);
 
 // Remote Administration
 static void Command_Changepassword_f(void);
+static void Command_Clearpassword_f(void);
 static void Command_Login_f(void);
 static void Got_Verification(UINT8 **cp, INT32 playernum);
 static void Got_Removal(UINT8 **cp, INT32 playernum);
@@ -225,6 +228,7 @@ consvar_t cv_seenames = CVAR_INIT ("seenames", "Ally/Foe", CV_SAVE|CV_ALLOWLUA,
 consvar_t cv_allowseenames = CVAR_INIT ("allowseenames", "Yes", CV_SAVE|CV_NETVAR|CV_ALLOWLUA, CV_YesNo, NULL);
 
 // names
+static char *lastskinnames[2];
 consvar_t cv_playername = CVAR_INIT ("name", "Sonic", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Name_OnChange);
 consvar_t cv_playername2 = CVAR_INIT ("name2", "Tails", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Name2_OnChange);
 // player colors
@@ -465,6 +469,7 @@ void D_RegisterServerCommands(void)
 
 	// Remote Administration
 	COM_AddCommand("password", Command_Changepassword_f, COM_LUA);
+	COM_AddCommand("clearpassword", Command_Clearpassword_f, COM_LUA);
 	COM_AddCommand("login", Command_Login_f, COM_LUA); // useful in dedicated to kick off remote admin
 	COM_AddCommand("promote", Command_Verify_f, COM_LUA);
 	RegisterNetXCmd(XD_VERIFIED, Got_Verification);
@@ -594,13 +599,11 @@ void D_RegisterServerCommands(void)
 	CV_RegisterVar(&cv_maxsend);
 	CV_RegisterVar(&cv_noticedownload);
 	CV_RegisterVar(&cv_downloadspeed);
-#ifndef NONET
 	CV_RegisterVar(&cv_allownewplayer);
-	CV_RegisterVar(&cv_joinnextround);
 	CV_RegisterVar(&cv_showjoinaddress);
 	CV_RegisterVar(&cv_blamecfail);
 	CV_RegisterVar(&cv_dedicatedidletime);
-#endif
+	CV_RegisterVar(&cv_idletime);
 
 	COM_AddCommand("ping", Command_Ping_f, COM_LUA);
 	CV_RegisterVar(&cv_nettimeout);
@@ -619,6 +622,10 @@ void D_RegisterServerCommands(void)
 	CV_RegisterVar(&cv_addons_folder);
 
 	CV_RegisterVar(&cv_dummyconsvar);
+
+	CV_RegisterVar(&cv_chatspamprotection);
+	CV_RegisterVar(&cv_chatspamspeed);
+	CV_RegisterVar(&cv_chatspamburst);
 }
 
 // =========================================================================
@@ -762,7 +769,6 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_chatheight);
 	CV_RegisterVar(&cv_chatwidth);
 	CV_RegisterVar(&cv_chattime);
-	CV_RegisterVar(&cv_chatspamprotection);
 	CV_RegisterVar(&cv_chatbacktint);
 	CV_RegisterVar(&cv_consolechat);
 	CV_RegisterVar(&cv_chatnotifications);
@@ -1205,45 +1211,47 @@ UINT8 CanChangeSkin(INT32 playernum)
 
 static void ForceAllSkins(INT32 forcedskin)
 {
-	INT32 i;
-	for (i = 0; i < MAXPLAYERS; ++i)
+	for (INT32 i = 0; i < MAXPLAYERS; ++i)
 	{
-		if (!playeringame[i])
-			continue;
+		if (playeringame[i])
+			SetPlayerSkinByNum(i, forcedskin);
+	}
+}
 
-		SetPlayerSkinByNum(i, forcedskin);
+static INT32 snacpending = 0, snac2pending = 0, chmappending = 0;
 
-		// If it's me (or my brother), set appropriate skin value in cv_skin/cv_skin2
-		if (!dedicated) // But don't do this for dedicated servers, of course.
-		{
-			if (i == consoleplayer)
-				CV_StealthSet(&cv_skin, skins[forcedskin].name);
-			else if (i == secondarydisplayplayer)
-				CV_StealthSet(&cv_skin2, skins[forcedskin].name);
-		}
+static void SetSkinLocal(INT32 playernum, INT32 skinnum)
+{
+	if (metalrecording && playernum == consoleplayer)
+	{
+		// Starring Metal Sonic as themselves, obviously.
+		SetPlayerSkinByNum(playernum, 5);
+		return;
 	}
+
+	if (skinnum != -1 && R_SkinUsable(playernum, skinnum))
+		SetPlayerSkinByNum(playernum, skinnum);
+	else
+		SetPlayerSkinByNum(playernum, GetPlayerDefaultSkin(playernum));
 }
 
-static INT32 snacpending = 0, snac2pending = 0, chmappending = 0;
+static void SetColorLocal(INT32 playernum, UINT16 color)
+{
+	players[playernum].skincolor = color;
+
+	if (players[playernum].mo && !players[playernum].powers[pw_dye])
+		players[playernum].mo->color = P_GetPlayerColor(&players[playernum]);
+}
 
 // name, color, or skin has changed
 //
 static void SendNameAndColor(void)
 {
-	char buf[MAXPLAYERNAME+6];
+	char buf[MAXPLAYERNAME+7];
 	char *p;
 
 	p = buf;
 
-	// normal player colors
-	if (G_GametypeHasTeams())
-	{
-		if (players[consoleplayer].ctfteam == 1 && cv_playercolor.value != skincolor_redteam)
-			CV_StealthSetValue(&cv_playercolor, skincolor_redteam);
-		else if (players[consoleplayer].ctfteam == 2 && cv_playercolor.value != skincolor_blueteam)
-			CV_StealthSetValue(&cv_playercolor, skincolor_blueteam);
-	}
-
 	// don't allow inaccessible colors
 	if (!skincolors[cv_playercolor.value].accessible)
 	{
@@ -1274,50 +1282,15 @@ static void SendNameAndColor(void)
 	// If you're not in a netgame, merely update the skin, color, and name.
 	if (!netgame)
 	{
-		INT32 foundskin;
-
 		CleanupPlayerName(consoleplayer, cv_playername.zstring);
 		strcpy(player_names[consoleplayer], cv_playername.zstring);
 
-		players[consoleplayer].skincolor = cv_playercolor.value;
-
-		if (players[consoleplayer].mo && !players[consoleplayer].powers[pw_dye])
-			players[consoleplayer].mo->color = players[consoleplayer].skincolor;
-
-		if (metalrecording)
-		{ // Starring Metal Sonic as themselves, obviously.
-			SetPlayerSkinByNum(consoleplayer, 5);
-			CV_StealthSet(&cv_skin, skins[5].name);
-		}
-		else if ((foundskin = R_SkinAvailable(cv_skin.string)) != -1 && R_SkinUsable(consoleplayer, foundskin))
-		{
-			//boolean notsame;
-
-			cv_skin.value = foundskin;
-
-			//notsame = (cv_skin.value != players[consoleplayer].skin);
-
-			SetPlayerSkin(consoleplayer, cv_skin.string);
-			CV_StealthSet(&cv_skin, skins[cv_skin.value].name);
-
-			/*if (notsame)
-			{
-				CV_StealthSetValue(&cv_playercolor, skins[cv_skin.value].prefcolor);
-
-				players[consoleplayer].skincolor = cv_playercolor.value % numskincolors;
+		SetColorLocal(consoleplayer, cv_playercolor.value);
 
-				if (players[consoleplayer].mo)
-					players[consoleplayer].mo->color = (UINT16)players[consoleplayer].skincolor;
-			}*/
-		}
+		if (splitscreen)
+			SetSkinLocal(consoleplayer, R_SkinAvailable(cv_skin.string));
 		else
-		{
-			cv_skin.value = players[consoleplayer].skin;
-			CV_StealthSet(&cv_skin, skins[players[consoleplayer].skin].name);
-			// will always be same as current
-			SetPlayerSkin(consoleplayer, cv_skin.string);
-		}
-
+			SetSkinLocal(consoleplayer, pickedchar);
 		return;
 	}
 
@@ -1334,10 +1307,6 @@ static void SendNameAndColor(void)
 	else // Cleanup name if changing it
 		CleanupPlayerName(consoleplayer, cv_playername.zstring);
 
-	// Don't change skin if the server doesn't want you to.
-	if (!CanChangeSkin(consoleplayer))
-		CV_StealthSet(&cv_skin, skins[players[consoleplayer].skin].name);
-
 	// check if player has the skin loaded (cv_skin may have
 	// the name of a skin that was available in the previous game)
 	cv_skin.value = R_SkinAvailable(cv_skin.string);
@@ -1369,16 +1338,6 @@ static void SendNameAndColor2(void)
 	else // HACK
 		secondplaya = 1;
 
-	// normal player colors
-	if (G_GametypeHasTeams())
-	{
-		if (players[secondplaya].ctfteam == 1 && cv_playercolor2.value != skincolor_redteam)
-			CV_StealthSetValue(&cv_playercolor2, skincolor_redteam);
-		else if (players[secondplaya].ctfteam == 2 && cv_playercolor2.value != skincolor_blueteam)
-			CV_StealthSetValue(&cv_playercolor2, skincolor_blueteam);
-	}
-
-	// don't allow inaccessible colors
 	if (!skincolors[cv_playercolor2.value].accessible)
 	{
 		if (players[secondplaya].skincolor && skincolors[players[secondplaya].skincolor].accessible)
@@ -1400,63 +1359,24 @@ static void SendNameAndColor2(void)
 	if (!Playing())
 		return;
 
-	// If you're not in a netgame, merely update the skin, color, and name.
 	if (botingame)
 	{
-		players[secondplaya].skincolor = botcolor;
-		if (players[secondplaya].mo && !players[secondplaya].powers[pw_dye])
-			players[secondplaya].mo->color = players[secondplaya].skincolor;
-
+		SetColorLocal(secondplaya, botcolor);
 		SetPlayerSkinByNum(secondplaya, botskin-1);
 		return;
 	}
 	else if (!netgame)
 	{
-		INT32 foundskin;
-
+		// If you're not in a netgame, merely update the skin, color, and name.
 		CleanupPlayerName(secondplaya, cv_playername2.zstring);
 		strcpy(player_names[secondplaya], cv_playername2.zstring);
 
-		// don't use secondarydisplayplayer: the second player must be 1
-		players[secondplaya].skincolor = cv_playercolor2.value;
-		if (players[secondplaya].mo && !players[secondplaya].powers[pw_dye])
-			players[secondplaya].mo->color = players[secondplaya].skincolor;
-
-		if (cv_forceskin.value >= 0 && (netgame || multiplayer)) // Server wants everyone to use the same player
-		{
-			const INT32 forcedskin = cv_forceskin.value;
-
-			SetPlayerSkinByNum(secondplaya, forcedskin);
-			CV_StealthSet(&cv_skin2, skins[forcedskin].name);
-		}
-		else if ((foundskin = R_SkinAvailable(cv_skin2.string)) != -1 && R_SkinUsable(secondplaya, foundskin))
-		{
-			//boolean notsame;
-
-			cv_skin2.value = foundskin;
-
-			//notsame = (cv_skin2.value != players[secondplaya].skin);
+		SetColorLocal(secondplaya, cv_playercolor2.value);
 
-			SetPlayerSkin(secondplaya, cv_skin2.string);
-			CV_StealthSet(&cv_skin2, skins[cv_skin2.value].name);
-
-			/*if (notsame)
-			{
-				CV_StealthSetValue(&cv_playercolor2, skins[players[secondplaya].skin].prefcolor);
-
-				players[secondplaya].skincolor = cv_playercolor2.value % numskincolors;
-
-				if (players[secondplaya].mo)
-					players[secondplaya].mo->color = players[secondplaya].skincolor;
-			}*/
-		}
+		if (cv_forceskin.value >= 0)
+			SetSkinLocal(secondplaya, cv_forceskin.value);
 		else
-		{
-			cv_skin2.value = players[secondplaya].skin;
-			CV_StealthSet(&cv_skin2, skins[players[secondplaya].skin].name);
-			// will always be same as current
-			SetPlayerSkin(secondplaya, cv_skin2.string);
-		}
+			SetSkinLocal(secondplaya, R_SkinAvailable(cv_skin2.string));
 		return;
 	}
 
@@ -1500,7 +1420,7 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum)
 	// set color
 	p->skincolor = color % numskincolors;
 	if (p->mo)
-		p->mo->color = (UINT16)p->skincolor;
+		p->mo->color = P_GetPlayerColor(p);
 
 	// normal player colors
 	if (server && (p != &players[consoleplayer] && p != &players[secondarydisplayplayer]))
@@ -1509,15 +1429,6 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum)
 		UINT32 unlockShift = 0;
 		UINT32 i;
 
-		// team colors
-		if (G_GametypeHasTeams())
-		{
-			if (p->ctfteam == 1 && p->skincolor != skincolor_redteam)
-				kick = true;
-			else if (p->ctfteam == 2 && p->skincolor != skincolor_blueteam)
-				kick = true;
-		}
-
 		// don't allow inaccessible colors
 		if (skincolors[p->skincolor].accessible == false)
 			kick = true;
@@ -1558,16 +1469,9 @@ static void Got_NameAndColor(UINT8 **cp, INT32 playernum)
 	}
 
 	// set skin
-	if (cv_forceskin.value >= 0 && (netgame || multiplayer)) // Server wants everyone to use the same player
-	{
-		const INT32 forcedskin = cv_forceskin.value;
+	INT32 forcedskin = R_GetForcedSkin(playernum);
+	if (forcedskin != -1 && (netgame || multiplayer)) // Server wants everyone to use the same player (or the level is forcing one.)
 		SetPlayerSkinByNum(playernum, forcedskin);
-
-		if (playernum == consoleplayer)
-			CV_StealthSet(&cv_skin, skins[forcedskin].name);
-		else if (playernum == secondarydisplayplayer)
-			CV_StealthSet(&cv_skin2, skins[forcedskin].name);
-	}
 	else
 		SetPlayerSkinByNum(playernum, skin);
 }
@@ -1843,8 +1747,7 @@ void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pultmode, boolean rese
 		// reset players if there is a new one
 		if (!IsPlayerAdmin(consoleplayer))
 		{
-			if (SV_SpawnServer())
-				buf[0] &= ~(1<<1);
+			SV_SpawnServer();
 			if (!Playing()) // you failed to start a server somehow, so cancel the map change
 				return;
 		}
@@ -2177,7 +2080,6 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 	{
 		SetPlayerSkinByNum(0, cv_chooseskin.value-1);
 		players[0].skincolor = skins[players[0].skin].prefcolor;
-		CV_StealthSetValue(&cv_playercolor, players[0].skincolor);
 	}
 
 	mapnumber = M_MapNumber(mapname[3], mapname[4]);
@@ -2931,17 +2833,6 @@ static void Got_Teamchange(UINT8 **cp, INT32 playernum)
 		displayplayer = consoleplayer;
 	}
 
-	if (G_GametypeHasTeams())
-	{
-		if (NetPacket.packet.newteam)
-		{
-			if (playernum == consoleplayer) //CTF and Team Match colors.
-				CV_SetValue(&cv_playercolor, NetPacket.packet.newteam + 5);
-			else if (playernum == secondarydisplayplayer)
-				CV_SetValue(&cv_playercolor2, NetPacket.packet.newteam + 5);
-		}
-	}
-
 	// In tag, check to see if you still have a game.
 	if (G_TagGametype())
 		P_CheckSurvivors();
@@ -2955,8 +2846,15 @@ static void Got_Teamchange(UINT8 **cp, INT32 playernum)
 
 void D_SetPassword(const char *pw)
 {
-	D_MD5PasswordPass((const UINT8 *)pw, strlen(pw), BASESALT, &adminpassmd5);
-	adminpasswordset = true;
+	adminpassmd5 = Z_Realloc(adminpassmd5, sizeof(*adminpassmd5) * ++adminpasscount, PU_STATIC, NULL);
+	D_MD5PasswordPass((const UINT8 *)pw, strlen(pw), BASESALT, &adminpassmd5[adminpasscount-1]);
+}
+
+void D_ClearPassword(void)
+{
+	Z_Free(adminpassmd5);
+	adminpassmd5 = NULL;
+	adminpasscount = 0;
 }
 
 // Remote Administration
@@ -2974,12 +2872,30 @@ static void Command_Changepassword_f(void)
 
 	if (COM_Argc() != 2)
 	{
-		CONS_Printf(M_GetText("password <password>: change remote admin password\n"));
+		CONS_Printf(M_GetText("password <password>: add remote admin password\n"));
 		return;
 	}
 
 	D_SetPassword(COM_Argv(1));
-	CONS_Printf(M_GetText("Password set.\n"));
+	CONS_Printf(M_GetText("Password added.\n"));
+#endif
+}
+
+// Remote Administration
+static void Command_Clearpassword_f(void)
+{
+#ifdef NOMD5
+	// If we have no MD5 support then completely disable XD_LOGIN responses for security.
+	CONS_Alert(CONS_NOTICE, "Remote administration commands are not supported in this build.\n");
+#else
+	if (client) // cannot change remotely
+	{
+		CONS_Printf(M_GetText("Only the server can use this.\n"));
+		return;
+	}
+
+	D_ClearPassword();
+	CONS_Printf(M_GetText("Passwords cleared.\n"));
 #endif
 }
 
@@ -4820,11 +4736,16 @@ static void ForceSkin_OnChange(void)
 		return;
 
 	if (cv_forceskin.value < 0)
+	{
 		CONS_Printf("The server has lifted the forced skin restrictions.\n");
+		if (Playing())
+			D_SendPlayerConfig();
+	}
 	else
 	{
 		CONS_Printf("The server is restricting all players to skin \"%s\".\n",skins[cv_forceskin.value].name);
-		ForceAllSkins(cv_forceskin.value);
+		if (Playing())
+			ForceAllSkins(cv_forceskin.value);
 	}
 }
 
@@ -4838,7 +4759,6 @@ static void Name_OnChange(void)
 	}
 	else
 		SendNameAndColor();
-
 }
 
 static void Name2_OnChange(void)
@@ -4861,19 +4781,33 @@ static void Skin_OnChange(void)
 	if (!Playing())
 		return; // do whatever you want
 
-	if (!(cv_debug || devparm) && !(multiplayer || netgame) // In single player.
-		&& (gamestate != GS_WAITINGPLAYERS)) // allows command line -warp x +skin y
+	if (lastskinnames[0] == NULL)
+		lastskinnames[0] = Z_StrDup(cv_skin.string);
+
+	if (!(multiplayer || netgame)) // In single player.
 	{
-		CV_StealthSet(&cv_skin, skins[players[consoleplayer].skin].name);
+		if (!(cv_debug || devparm)
+		&& (gamestate != GS_WAITINGPLAYERS)) // allows command line -warp x +skin y
+		{
+			CV_StealthSet(&cv_skin, skins[players[consoleplayer].skin].name);
+			return;
+		}
+
+		// Just do it here if devmode is enabled
+		SetSkinLocal(consoleplayer, R_SkinAvailable(cv_skin.string));
 		return;
 	}
 
 	if (CanChangeSkin(consoleplayer) && !P_PlayerMoving(consoleplayer))
+	{
 		SendNameAndColor();
+		Z_Free(lastskinnames[0]);
+		lastskinnames[0] = Z_StrDup(cv_skin.string);
+	}
 	else
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("You can't change your skin at the moment.\n"));
-		CV_StealthSet(&cv_skin, skins[players[consoleplayer].skin].name);
+		CV_StealthSet(&cv_skin, lastskinnames[0]);
 	}
 }
 
@@ -4887,12 +4821,19 @@ static void Skin2_OnChange(void)
 	if (!Playing() || !splitscreen)
 		return; // do whatever you want
 
+	if (lastskinnames[1] == NULL)
+		lastskinnames[1] = Z_StrDup(cv_skin2.string);
+
 	if (CanChangeSkin(secondarydisplayplayer) && !P_PlayerMoving(secondarydisplayplayer))
+	{
 		SendNameAndColor2();
+		Z_Free(lastskinnames[1]);
+		lastskinnames[1] = Z_StrDup(cv_skin.string);
+	}
 	else
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("You can't change your skin at the moment.\n"));
-		CV_StealthSet(&cv_skin2, skins[players[secondarydisplayplayer].skin].name);
+		CV_StealthSet(&cv_skin2, lastskinnames[1]);
 	}
 }
 
@@ -4902,15 +4843,18 @@ static void Skin2_OnChange(void)
   */
 static void Color_OnChange(void)
 {
-	if (!Playing()) {
+	if (!Playing())
+	{
 		if (!cv_playercolor.value || !skincolors[cv_playercolor.value].accessible)
 			CV_StealthSetValue(&cv_playercolor, lastgoodcolor);
 	}
 	else
 	{
-		if (!(cv_debug || devparm) && !(multiplayer || netgame)) // In single player.
+		if (!(multiplayer || netgame)) // In single player.
 		{
-			CV_StealthSet(&cv_skin, skins[players[consoleplayer].skin].name);
+			// Just do it here if devmode is enabled
+			if (cv_debug || devparm)
+				SetColorLocal(consoleplayer, cv_playercolor.value);
 			return;
 		}
 
diff --git a/src/d_netcmd.h b/src/netcode/d_netcmd.h
similarity index 99%
rename from src/d_netcmd.h
rename to src/netcode/d_netcmd.h
index 8bbc801d0ef700868482fd982346dff5296c5e14..0f2a1f92ba93c95e16d95afd0cf179cf625c0b5c 100644
--- a/src/d_netcmd.h
+++ b/src/netcode/d_netcmd.h
@@ -15,7 +15,7 @@
 #ifndef __D_NETCMD__
 #define __D_NETCMD__
 
-#include "command.h"
+#include "../command.h"
 
 // console vars
 extern consvar_t cv_playername;
@@ -209,6 +209,7 @@ void ClearAdminPlayers(void);
 void RemoveAdminPlayer(INT32 playernum);
 void ItemFinder_OnChange(void);
 void D_SetPassword(const char *pw);
+void D_ClearPassword(void);
 
 // used for the player setup menu
 UINT8 CanChangeSkin(INT32 playernum);
diff --git a/src/d_netfil.c b/src/netcode/d_netfil.c
similarity index 93%
rename from src/d_netfil.c
rename to src/netcode/d_netfil.c
index 3fef7568128f34bda6d8716cf5d227a19495b9de..c5ddef7b6563f22082c09e3b161acb3f1ff59f57 100644
--- a/src/d_netfil.c
+++ b/src/netcode/d_netfil.c
@@ -31,24 +31,25 @@
 #include <sys/utime.h>
 #endif
 
-#include "doomdef.h"
-#include "doomstat.h"
-#include "d_main.h"
-#include "g_game.h"
-#include "i_time.h"
+#include "../doomdef.h"
+#include "../doomstat.h"
+#include "../d_main.h"
+#include "../g_game.h"
+#include "../i_time.h"
 #include "i_net.h"
-#include "i_system.h"
-#include "m_argv.h"
+#include "../i_system.h"
+#include "../m_argv.h"
 #include "d_net.h"
-#include "w_wad.h"
+#include "../w_wad.h"
 #include "d_netfil.h"
-#include "z_zone.h"
-#include "byteptr.h"
-#include "p_setup.h"
-#include "m_misc.h"
-#include "m_menu.h"
-#include "md5.h"
-#include "filesrch.h"
+#include "net_command.h"
+#include "../z_zone.h"
+#include "../byteptr.h"
+#include "../p_setup.h"
+#include "../m_misc.h"
+#include "../m_menu.h"
+#include "../md5.h"
+#include "../filesrch.h"
 
 #include <errno.h>
 
@@ -103,26 +104,31 @@ typedef struct
 } pauseddownload_t;
 static pauseddownload_t *pauseddownload = NULL;
 
-#ifndef NONET
 // for cl loading screen
 INT32 lastfilenum = -1;
 INT32 downloadcompletednum = 0;
 UINT32 downloadcompletedsize = 0;
 INT32 totalfilesrequestednum = 0;
 UINT32 totalfilesrequestedsize = 0;
-#endif
 
 luafiletransfer_t *luafiletransfers = NULL;
 boolean waitingforluafiletransfer = false;
 boolean waitingforluafilecommand = false;
 char luafiledir[256 + 16] = "luafiles";
 
+// max file size to send to a player (in kilobytes)
+static CV_PossibleValue_t maxsend_cons_t[] = {{0, "MIN"}, {204800, "MAX"}, {0, NULL}};
+consvar_t cv_maxsend = CVAR_INIT ("maxsend", "4096", CV_SAVE|CV_NETVAR, maxsend_cons_t, NULL);
+
+consvar_t cv_noticedownload = CVAR_INIT ("noticedownload", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
+
+// Speed of file downloading (in packets per tic)
+static CV_PossibleValue_t downloadspeed_cons_t[] = {{1, "MIN"}, {300, "MAX"}, {0, NULL}};
+consvar_t cv_downloadspeed = CVAR_INIT ("downloadspeed", "16", CV_SAVE|CV_NETVAR, downloadspeed_cons_t, NULL);
 
 static UINT16 GetWadNumFromFileNeededId(UINT8 id)
 {
-	UINT16 wadnum;
-
-	for (wadnum = mainwads; wadnum < numwadfiles; wadnum++)
+	for (UINT16 wadnum = mainwads; wadnum < numwadfiles; wadnum++)
 	{
 		if (!wadfiles[wadnum]->important)
 			continue;
@@ -142,14 +148,13 @@ static UINT16 GetWadNumFromFileNeededId(UINT8 id)
   */
 UINT8 *PutFileNeeded(UINT16 firstfile)
 {
-	size_t i;
 	UINT8 count = 0;
 	UINT8 *p_start = netbuffer->packettype == PT_MOREFILESNEEDED ? netbuffer->u.filesneededcfg.files : netbuffer->u.serverinfo.fileneeded;
 	UINT8 *p = p_start;
 	char wadfilename[MAX_WADPATH] = "";
 	UINT8 filestatus, folder;
 
-	for (i = mainwads; i < numwadfiles; i++) //mainwads, otherwise we start on the first mainwad
+	for (size_t i = mainwads; i < numwadfiles; i++) //mainwads, otherwise we start on the first mainwad
 	{
 		// If it has only music/sound lumps, don't put it in the list
 		if (!wadfiles[i]->important)
@@ -224,7 +229,6 @@ void FreeFileNeeded(void)
   */
 void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 firstfile)
 {
-	INT32 i;
 	UINT8 *p;
 	UINT8 filestatus;
 
@@ -233,7 +237,7 @@ void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 fi
 
 	AllocFileNeeded(fileneedednum);
 
-	for (i = firstfile; i < fileneedednum; i++)
+	for (INT32 i = firstfile; i < fileneedednum; i++)
 	{
 		fileneeded[i].type = FILENEEDED_WAD;
 		fileneeded[i].status = FS_NOTCHECKED; // We haven't even started looking for the file yet
@@ -250,9 +254,7 @@ void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 fi
 
 void CL_PrepareDownloadSaveGame(const char *tmpsave)
 {
-#ifndef NONET
 	lastfilenum = -1;
-#endif
 
 	FreeFileNeeded();
 	AllocFileNeeded(1);
@@ -275,9 +277,9 @@ void CL_PrepareDownloadSaveGame(const char *tmpsave)
   */
 boolean CL_CheckDownloadable(void)
 {
-	UINT8 i,dlstatus = 0;
+	UINT8 dlstatus = 0;
 
-	for (i = 0; i < fileneedednum; i++)
+	for (UINT8 i = 0; i < fileneedednum; i++)
 		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN)
 		{
 			if (fileneeded[i].willsend == 1)
@@ -298,7 +300,7 @@ boolean CL_CheckDownloadable(void)
 
 	// not downloadable, put reason in console
 	CONS_Alert(CONS_NOTICE, M_GetText("You need additional files to connect to this server:\n"));
-	for (i = 0; i < fileneedednum; i++)
+	for (UINT8 i = 0; i < fileneedednum; i++)
 		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN)
 		{
 			CONS_Printf(" * \"%s\" (%dK)", fileneeded[i].filename, fileneeded[i].totalsize >> 10);
@@ -368,14 +370,13 @@ void CL_AbortDownloadResume(void)
 boolean CL_SendFileRequest(void)
 {
 	char *p;
-	INT32 i;
 	INT64 totalfreespaceneeded = 0, availablefreespace;
 
 #ifdef PARANOIA
 	if (M_CheckParm("-nodownload"))
 		I_Error("Attempted to download files in -nodownload mode");
 
-	for (i = 0; i < fileneedednum; i++)
+	for (INT32 i = 0; i < fileneedednum; i++)
 		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN
 			&& (fileneeded[i].willsend == 0 || fileneeded[i].willsend == 2))
 		{
@@ -385,7 +386,7 @@ boolean CL_SendFileRequest(void)
 
 	netbuffer->packettype = PT_REQUESTFILE;
 	p = (char *)netbuffer->u.textcmd;
-	for (i = 0; i < fileneedednum; i++)
+	for (INT32 i = 0; i < fileneedednum; i++)
 		if ((fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD))
 		{
 			totalfreespaceneeded += fileneeded[i].totalsize;
@@ -413,26 +414,31 @@ boolean CL_SendFileRequest(void)
 }
 
 // get request filepak and put it on the send queue
-// returns false if a requested file was not found or cannot be sent
-boolean PT_RequestFile(INT32 node)
+void PT_RequestFile(SINT8 node)
 {
 	UINT8 *p = netbuffer->u.textcmd;
-	UINT8 id;
+
+	if (client || !cv_downloading.value)
+	{
+		Net_CloseConnection(node); // close connection if you are not the server or disabled downloading
+		return;
+	}
 
 	while (p < netbuffer->u.textcmd + MAXTEXTCMD-1) // Don't allow hacked client to overflow
 	{
-		id = READUINT8(p);
+		UINT8 id = READUINT8(p);
 		if (id == 0xFF)
 			break;
 
 		if (!AddFileToSendQueue(node, id))
 		{
 			SV_AbortSendFiles(node);
-			return false; // don't read the rest of the files
+			Net_CloseConnection(node); // close connection if one of the requested files could not be sent
+			return; // don't read the rest of the files
 		}
 	}
 
-	return true; // no problems with any files
+	return; // no problems with any files
 }
 
 /** Checks if the files needed aren't already loaded or on the disk
@@ -531,9 +537,7 @@ INT32 CL_CheckFiles(void)
 // Load it now
 boolean CL_LoadServerFiles(void)
 {
-	INT32 i;
-
-	for (i = 0; i < fileneedednum; i++)
+	for (INT32 i = 0; i < fileneedednum; i++)
 	{
 		if (fileneeded[i].status == FS_OPEN)
 			continue; // Already loaded
@@ -629,11 +633,10 @@ void AddLuaFileTransfer(const char *filename, const char *mode)
 
 static void SV_PrepareSendLuaFileToNextNode(void)
 {
-	INT32 i;
 	UINT8 success = 1;
 
     // Find a client to send the file to
-	for (i = 1; i < MAXNETNODES; i++)
+	for (INT32 i = 1; i < MAXNETNODES; i++)
 		if (luafiletransfers->nodestatus[i] == LFTNS_WAITING) // Node waiting
 		{
 			// Tell the client we're about to send them the file
@@ -655,13 +658,12 @@ static void SV_PrepareSendLuaFileToNextNode(void)
 void SV_PrepareSendLuaFile(void)
 {
 	char *binfilename;
-	INT32 i;
 
 	luafiletransfers->ongoing = true;
 
 	// Set status to "waiting" for everyone
-	for (i = 0; i < MAXNETNODES; i++)
-		luafiletransfers->nodestatus[i] = (nodeingame[i] ? LFTNS_WAITING : LFTNS_NONE);
+	for (INT32 i = 0; i < MAXNETNODES; i++)
+		luafiletransfers->nodestatus[i] = (netnodes[i].ingame ? LFTNS_WAITING : LFTNS_NONE);
 
 	if (FIL_ReadFileOK(luafiletransfers->realfilename))
 	{
@@ -1137,12 +1139,13 @@ void FileSendTicker(void)
 	}
 }
 
-void PT_FileAck(void)
+void PT_FileAck(SINT8 node)
 {
 	fileack_pak *packet = &netbuffer->u.fileack;
-	INT32 node = doomcom->remotenode;
 	filetran_t *trans = &transfer[node];
-	INT32 i, j;
+
+	if (client)
+		return;
 
 	// Wrong file id? Ignore it, it's probably a late packet
 	if (!(trans->txlist && packet->fileid == trans->txlist->fileid))
@@ -1161,11 +1164,11 @@ void PT_FileAck(void)
 			trans->dontsenduntil = 0;
 	}
 
-	for (i = 0; i < packet->numsegments; i++)
+	for (INT32 i = 0; i < packet->numsegments; i++)
 	{
 		fileacksegment_t *segment = &packet->segments[i];
 
-		for (j = 0; j < 32; j++)
+		for (INT32 j = 0; j < 32; j++)
 			if (LONG(segment->acks) & (1 << j))
 			{
 				if (LONG(segment->start) * FILEFRAGMENTSIZE >= trans->txlist->size)
@@ -1190,24 +1193,23 @@ void PT_FileAck(void)
 	}
 }
 
-void PT_FileReceived(void)
+void PT_FileReceived(SINT8 node)
 {
-	filetx_t *trans = transfer[doomcom->remotenode].txlist;
+	filetx_t *trans = transfer[node].txlist;
 
-	if (trans && netbuffer->u.filereceived == trans->fileid)
-		SV_EndFileSend(doomcom->remotenode);
+	if (server && trans && netbuffer->u.filereceived == trans->fileid)
+		SV_EndFileSend(node);
 }
 
 static void SendAckPacket(fileack_pak *packet, UINT8 fileid)
 {
 	size_t packetsize;
-	INT32 i;
 
 	packetsize = sizeof(*packet) + packet->numsegments * sizeof(*packet->segments);
 
 	// Finalise the packet
 	packet->fileid = fileid;
-	for (i = 0; i < packet->numsegments; i++)
+	for (INT32 i = 0; i < packet->numsegments; i++)
 	{
 		packet->segments[i].start = LONG(packet->segments[i].start);
 		packet->segments[i].acks = LONG(packet->segments[i].acks);
@@ -1247,9 +1249,7 @@ static void AddFragmentToAckPacket(fileack_pak *packet, UINT8 iteration, UINT32
 
 void FileReceiveTicker(void)
 {
-	INT32 i;
-
-	for (i = 0; i < fileneedednum; i++)
+	for (INT32 i = 0; i < fileneedednum; i++)
 	{
 		fileneeded_t *file = &fileneeded[i];
 
@@ -1263,8 +1263,7 @@ void FileReceiveTicker(void)
 			if (file->ackresendposition != UINT32_MAX && file->status == FS_DOWNLOADING)
 			{
 				// Acknowledge ~70 MB/s, whichs means the client sends ~18 KB/s
-				INT32 j;
-				for (j = 0; j < 2048; j++)
+				for (INT32 j = 0; j < 2048; j++)
 				{
 					if (file->receivedfragments[file->ackresendposition])
 						AddFragmentToAckPacket(file->ackpacket, file->iteration, file->ackresendposition, i);
@@ -1281,8 +1280,27 @@ void FileReceiveTicker(void)
 	}
 }
 
-void PT_FileFragment(void)
+void PT_FileFragment(SINT8 node, INT32 netconsole)
 {
+	if (netnodes[node].ingame)
+	{
+		// Only accept PT_FILEFRAGMENT from the server.
+		if (node != servernode)
+		{
+			CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_FILEFRAGMENT", node);
+			if (server)
+				SendKick(netconsole, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
+			return;
+		}
+		if (server)
+			return;
+	}
+	else if (server || node != servernode)
+	{
+		Net_CloseConnection(node);
+		return;
+	}
+
 	INT32 filenum = netbuffer->u.filetxpak.fileid;
 	fileneeded_t *file = &fileneeded[filenum];
 	UINT32 fragmentpos = LONG(netbuffer->u.filetxpak.position);
@@ -1439,9 +1457,7 @@ void PT_FileFragment(void)
 		I_Error("Received a file not requested (file id: %d, file status: %s)\n", filenum, s);
 	}
 
-#ifndef NONET
 	lastfilenum = filenum;
-#endif
 }
 
 /** \brief Checks if a node is downloading a file
@@ -1469,15 +1485,14 @@ void SV_AbortSendFiles(INT32 node)
 
 void CloseNetFile(void)
 {
-	INT32 i;
 	// Is sending?
-	for (i = 0; i < MAXNETNODES; i++)
+	for (INT32 i = 0; i < MAXNETNODES; i++)
 		SV_AbortSendFiles(i);
 
 	// Receiving a file?
 	if (fileneeded)
 	{
-		for (i = 0; i < fileneedednum; i++)
+		for (INT32 i = 0; i < fileneedednum; i++)
 			if (fileneeded[i].status == FS_DOWNLOADING && fileneeded[i].file)
 			{
 				fclose(fileneeded[i].file);
@@ -1510,9 +1525,7 @@ void CloseNetFile(void)
 
 void Command_Downloads_f(void)
 {
-	INT32 node;
-
-	for (node = 0; node < MAXNETNODES; node++)
+	for (INT32 node = 0; node < MAXNETNODES; node++)
 		if (transfer[node].txlist
 		&& transfer[node].txlist->ram == SF_FILE) // Node is downloading a file?
 		{
@@ -1546,14 +1559,11 @@ void Command_Downloads_f(void)
 
 void nameonly(char *s)
 {
-	size_t j, len;
-	void *ns;
-
-	for (j = strlen(s); j != (size_t)-1; j--)
+	for (size_t j = strlen(s); j != (size_t)-1; j--)
 		if ((s[j] == '\\') || (s[j] == ':') || (s[j] == '/'))
 		{
-			ns = &(s[j+1]);
-			len = strlen(ns);
+			void *ns = &(s[j+1]);
+			size_t len = strlen(ns);
 #if 0
 			M_Memcpy(s, ns, len+1);
 #else
@@ -1566,9 +1576,9 @@ void nameonly(char *s)
 // Returns the length in characters of the last element of a path.
 size_t nameonlylength(const char *s)
 {
-	size_t j, len = strlen(s);
+	size_t len = strlen(s);
 
-	for (j = len; j != (size_t)-1; j--)
+	for (size_t j = len; j != (size_t)-1; j--)
 		if ((s[j] == '\\') || (s[j] == ':') || (s[j] == '/'))
 			return len - j - 1;
 
diff --git a/src/d_netfil.h b/src/netcode/d_netfil.h
similarity index 94%
rename from src/d_netfil.h
rename to src/netcode/d_netfil.h
index ecec976be8dca97d8874aaa88d2279d7e4c7aaae..fdbec8c5396fa1619efccfa6f200fbd7a17903c6 100644
--- a/src/d_netfil.h
+++ b/src/netcode/d_netfil.h
@@ -15,7 +15,7 @@
 
 #include "d_net.h"
 #include "d_clisrv.h"
-#include "w_wad.h"
+#include "../w_wad.h"
 
 typedef enum
 {
@@ -70,13 +70,13 @@ extern INT32 fileneedednum;
 extern fileneeded_t *fileneeded;
 extern char downloaddir[512];
 
-#ifndef NONET
 extern INT32 lastfilenum;
 extern INT32 downloadcompletednum;
 extern UINT32 downloadcompletedsize;
 extern INT32 totalfilesrequestednum;
 extern UINT32 totalfilesrequestedsize;
-#endif
+
+extern consvar_t cv_maxsend, cv_noticedownload, cv_downloadspeed;
 
 void AllocFileNeeded(INT32 size);
 void FreeFileNeeded(void);
@@ -90,16 +90,16 @@ void AddRamToSendQueue(INT32 node, void *data, size_t size, freemethod_t freemet
 	UINT8 fileid);
 
 void FileSendTicker(void);
-void PT_FileAck(void);
-void PT_FileReceived(void);
+void PT_FileAck(SINT8 node);
+void PT_FileReceived(SINT8 node);
 boolean SendingFile(INT32 node);
 
 void FileReceiveTicker(void);
-void PT_FileFragment(void);
+void PT_FileFragment(SINT8 node, INT32 netconsole);
 
 boolean CL_CheckDownloadable(void);
 boolean CL_SendFileRequest(void);
-boolean PT_RequestFile(INT32 node);
+void PT_RequestFile(SINT8 node);
 
 typedef enum
 {
diff --git a/src/netcode/gamestate.c b/src/netcode/gamestate.c
new file mode 100644
index 0000000000000000000000000000000000000000..f36347c6d88e94a444fb186937d2672cfe3a0df9
--- /dev/null
+++ b/src/netcode/gamestate.c
@@ -0,0 +1,336 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 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  gamestate.c
+/// \brief Gamestate (re)sending
+
+#include "d_clisrv.h"
+#include "d_netfil.h"
+#include "gamestate.h"
+#include "i_net.h"
+#include "protocol.h"
+#include "server_connection.h"
+#include "../am_map.h"
+#include "../byteptr.h"
+#include "../console.h"
+#include "../d_main.h"
+#include "../doomstat.h"
+#include "../doomtype.h"
+#include "../f_finale.h"
+#include "../g_demo.h"
+#include "../g_game.h"
+#include "../i_time.h"
+#include "../lua_script.h"
+#include "../lzf.h"
+#include "../m_misc.h"
+#include "../p_local.h"
+#include "../p_saveg.h"
+#include "../r_main.h"
+#include "../tables.h"
+#include "../z_zone.h"
+#if defined (__GNUC__) || defined (__unix__)
+#include <unistd.h>
+#endif
+
+#define SAVEGAMESIZE (768*1024)
+
+UINT8 hu_redownloadinggamestate = 0;
+boolean cl_redownloadinggamestate = false;
+
+boolean SV_ResendingSavegameToAnyone(void)
+{
+	for (INT32 i = 0; i < MAXNETNODES; i++)
+		if (netnodes[i].resendingsavegame)
+			return true;
+	return false;
+}
+
+void SV_SendSaveGame(INT32 node, boolean resending)
+{
+	size_t length, compressedlen;
+	UINT8 *savebuffer;
+	UINT8 *compressedsave;
+	UINT8 *buffertosend;
+
+	// first save it in a malloced buffer
+	savebuffer = (UINT8 *)malloc(SAVEGAMESIZE);
+	if (!savebuffer)
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
+		return;
+	}
+
+	// Leave room for the uncompressed length.
+	save_p = savebuffer + sizeof(UINT32);
+
+	P_SaveNetGame(resending);
+
+	length = save_p - savebuffer;
+	if (length > SAVEGAMESIZE)
+	{
+		free(savebuffer);
+		save_p = NULL;
+		I_Error("Savegame buffer overrun");
+	}
+
+	// Allocate space for compressed save: one byte fewer than for the
+	// uncompressed data to ensure that the compression is worthwhile.
+	compressedsave = malloc(length - 1);
+	if (!compressedsave)
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
+		return;
+	}
+
+	// Attempt to compress it.
+	if((compressedlen = lzf_compress(savebuffer + sizeof(UINT32), length - sizeof(UINT32), compressedsave + sizeof(UINT32), length - sizeof(UINT32) - 1)))
+	{
+		// Compressing succeeded; send compressed data
+
+		free(savebuffer);
+
+		// State that we're compressed.
+		buffertosend = compressedsave;
+		WRITEUINT32(compressedsave, length - sizeof(UINT32));
+		length = compressedlen + sizeof(UINT32);
+	}
+	else
+	{
+		// Compression failed to make it smaller; send original
+
+		free(compressedsave);
+
+		// State that we're not compressed
+		buffertosend = savebuffer;
+		WRITEUINT32(savebuffer, 0);
+	}
+
+	AddRamToSendQueue(node, buffertosend, length, SF_RAM, 0);
+	save_p = NULL;
+
+	// Remember when we started sending the savegame so we can handle timeouts
+	netnodes[node].sendingsavegame = true;
+	netnodes[node].freezetimeout = I_GetTime() + jointimeout + length / 1024; // 1 extra tic for each kilobyte
+}
+
+#ifdef DUMPCONSISTENCY
+#define TMPSAVENAME "badmath.sav"
+static consvar_t cv_dumpconsistency = CVAR_INIT ("dumpconsistency", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
+
+void SV_SavedGame(void)
+{
+	size_t length;
+	UINT8 *savebuffer;
+	char tmpsave[256];
+
+	if (!cv_dumpconsistency.value)
+		return;
+
+	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
+
+	// first save it in a malloced buffer
+	save_p = savebuffer = (UINT8 *)malloc(SAVEGAMESIZE);
+	if (!save_p)
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
+		return;
+	}
+
+	P_SaveNetGame(false);
+
+	length = save_p - savebuffer;
+	if (length > SAVEGAMESIZE)
+	{
+		free(savebuffer);
+		save_p = NULL;
+		I_Error("Savegame buffer overrun");
+	}
+
+	// then save it!
+	if (!FIL_WriteFile(tmpsave, savebuffer, length))
+		CONS_Printf(M_GetText("Didn't save %s for netgame"), tmpsave);
+
+	free(savebuffer);
+	save_p = NULL;
+}
+
+#undef  TMPSAVENAME
+#endif
+#define TMPSAVENAME "$$$.sav"
+
+
+void CL_LoadReceivedSavegame(boolean reloading)
+{
+	UINT8 *savebuffer = NULL;
+	size_t length, decompressedlen;
+	char tmpsave[256];
+
+	FreeFileNeeded();
+
+	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
+
+	length = FIL_ReadFile(tmpsave, &savebuffer);
+
+	CONS_Printf(M_GetText("Loading savegame length %s\n"), sizeu1(length));
+	if (!length)
+	{
+		I_Error("Can't read savegame sent");
+		return;
+	}
+
+	save_p = savebuffer;
+
+	// Decompress saved game if necessary.
+	decompressedlen = READUINT32(save_p);
+	if(decompressedlen > 0)
+	{
+		UINT8 *decompressedbuffer = Z_Malloc(decompressedlen, PU_STATIC, NULL);
+		lzf_decompress(save_p, length - sizeof(UINT32), decompressedbuffer, decompressedlen);
+		Z_Free(savebuffer);
+		save_p = savebuffer = decompressedbuffer;
+	}
+
+	paused = false;
+	demoplayback = false;
+	titlemapinaction = TITLEMAP_OFF;
+	titledemo = false;
+	automapactive = false;
+
+	// load a base level
+	if (P_LoadNetGame(reloading))
+	{
+		const UINT8 actnum = mapheaderinfo[gamemap-1]->actnum;
+		CONS_Printf(M_GetText("Map is now \"%s"), G_BuildMapName(gamemap));
+		if (strcmp(mapheaderinfo[gamemap-1]->lvlttl, ""))
+		{
+			CONS_Printf(": %s", mapheaderinfo[gamemap-1]->lvlttl);
+			if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
+				CONS_Printf(M_GetText(" Zone"));
+			if (actnum > 0)
+				CONS_Printf(" %2d", actnum);
+		}
+		CONS_Printf("\"\n");
+	}
+
+	// done
+	Z_Free(savebuffer);
+	save_p = NULL;
+	if (unlink(tmpsave) == -1)
+		CONS_Alert(CONS_ERROR, M_GetText("Can't delete %s\n"), tmpsave);
+	consistancy[gametic%BACKUPTICS] = Consistancy();
+	CON_ToggleOff();
+
+	// Tell the server we have received and reloaded the gamestate
+	// so they know they can resume the game
+	netbuffer->packettype = PT_RECEIVEDGAMESTATE;
+	HSendPacket(servernode, true, 0, 0);
+}
+
+void CL_ReloadReceivedSavegame(void)
+{
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+	{
+		LUA_InvalidatePlayer(&players[i]);
+		sprintf(player_names[i], "Player %d", i + 1);
+	}
+
+	CL_LoadReceivedSavegame(true);
+
+	neededtic = max(neededtic, gametic);
+	maketic = neededtic;
+
+	ticcmd_oldangleturn[0] = players[consoleplayer].oldrelangleturn;
+	P_ForceLocalAngle(&players[consoleplayer], (angle_t)(players[consoleplayer].angleturn << 16));
+	if (splitscreen)
+	{
+		ticcmd_oldangleturn[1] = players[secondarydisplayplayer].oldrelangleturn;
+		P_ForceLocalAngle(&players[secondarydisplayplayer], (angle_t)(players[secondarydisplayplayer].angleturn << 16));
+	}
+
+	camera.subsector = R_PointInSubsector(camera.x, camera.y);
+	camera2.subsector = R_PointInSubsector(camera2.x, camera2.y);
+
+	cl_redownloadinggamestate = false;
+
+	CONS_Printf(M_GetText("Game state reloaded\n"));
+}
+
+void Command_ResendGamestate(void)
+{
+	SINT8 playernum;
+
+	if (COM_Argc() == 1)
+	{
+		CONS_Printf(M_GetText("resendgamestate <playername/playernum>: resend the game state to a player\n"));
+		return;
+	}
+	else if (client)
+	{
+		CONS_Printf(M_GetText("Only the server can use this.\n"));
+		return;
+	}
+
+	playernum = nametonum(COM_Argv(1));
+	if (playernum == -1 || playernum == 0)
+		return;
+
+	// Send a PT_WILLRESENDGAMESTATE packet to the client so they know what's going on
+	netbuffer->packettype = PT_WILLRESENDGAMESTATE;
+	if (!HSendPacket(playernode[playernum], true, 0, 0))
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("A problem occurred, please try again.\n"));
+		return;
+	}
+}
+
+void PT_CanReceiveGamestate(SINT8 node)
+{
+	if (client || netnodes[node].sendingsavegame)
+		return;
+
+	CONS_Printf(M_GetText("Resending game state to %s...\n"), player_names[netnodes[node].player]);
+
+	SV_SendSaveGame(node, true); // Resend a complete game state
+	netnodes[node].resendingsavegame = true;
+}
+
+void PT_ReceivedGamestate(SINT8 node)
+{
+	netnodes[node].sendingsavegame = false;
+	netnodes[node].resendingsavegame = false;
+	netnodes[node].savegameresendcooldown = I_GetTime() + 5 * TICRATE;
+}
+
+void PT_WillResendGamestate(SINT8 node)
+{
+	(void)node;
+
+	char tmpsave[256];
+
+	if (server || cl_redownloadinggamestate)
+		return;
+
+	// Send back a PT_CANRECEIVEGAMESTATE packet to the server
+	// so they know they can start sending the game state
+	netbuffer->packettype = PT_CANRECEIVEGAMESTATE;
+	if (!HSendPacket(servernode, true, 0, 0))
+		return;
+
+	CONS_Printf(M_GetText("Reloading game state...\n"));
+
+	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
+
+	// Don't get a corrupt savegame error because tmpsave already exists
+	if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1)
+		I_Error("Can't delete %s\n", tmpsave);
+
+	CL_PrepareDownloadSaveGame(tmpsave);
+
+	cl_redownloadinggamestate = true;
+}
diff --git a/src/netcode/gamestate.h b/src/netcode/gamestate.h
new file mode 100644
index 0000000000000000000000000000000000000000..a2fae1f14a75d13cd0479acf7950a09f08754493
--- /dev/null
+++ b/src/netcode/gamestate.h
@@ -0,0 +1,31 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 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  gamestate.h
+/// \brief Gamestate (re)sending
+
+#ifndef __GAMESTATE__
+#define __GAMESTATE__
+
+#include "../doomtype.h"
+
+extern UINT8 hu_redownloadinggamestate;
+extern boolean cl_redownloadinggamestate;
+
+boolean SV_ResendingSavegameToAnyone(void);
+void SV_SendSaveGame(INT32 node, boolean resending);
+void SV_SavedGame(void);
+void CL_LoadReceivedSavegame(boolean reloading);
+void CL_ReloadReceivedSavegame(void);
+void Command_ResendGamestate(void);
+void PT_CanReceiveGamestate(SINT8 node);
+void PT_ReceivedGamestate(SINT8 node);
+void PT_WillResendGamestate(SINT8 node);
+
+#endif
diff --git a/src/http-mserv.c b/src/netcode/http-mserv.c
similarity index 85%
rename from src/http-mserv.c
rename to src/netcode/http-mserv.c
index df9a71a5c5d6cfcb8f73ecd3eeff371170e76c8e..10137e67b042cef0cd192d00d7d4fcbb62d5b842 100644
--- a/src/http-mserv.c
+++ b/src/netcode/http-mserv.c
@@ -18,14 +18,15 @@ Documentation available here.
 #include <curl/curl.h>
 #endif
 
-#include "doomdef.h"
+#include "../doomdef.h"
 #include "d_clisrv.h"
-#include "command.h"
-#include "m_argv.h"
-#include "m_menu.h"
+#include "client_connection.h"
+#include "../command.h"
+#include "../m_argv.h"
+#include "../m_menu.h"
 #include "mserv.h"
 #include "i_tcp.h"/* for current_port */
-#include "i_threads.h"
+#include "../i_threads.h"
 
 /* reasonable default I guess?? */
 #define DEFAULT_BUFFER_SIZE (4096)
@@ -34,6 +35,10 @@ Documentation available here.
 #define Blame( ... ) \
 	CONS_Printf("\x85" __VA_ARGS__)
 
+#define PROTO_ANY 0
+#define PROTO_V4 1
+#define PROTO_V6 2
+
 static void MasterServer_Debug_OnChange (void);
 
 consvar_t cv_masterserver_timeout = CVAR_INIT
@@ -58,12 +63,17 @@ consvar_t cv_masterserver_token = CVAR_INIT
 
 static int hms_started;
 
+static boolean hms_allow_ipv6;
+
 static char *hms_api;
 #ifdef HAVE_THREADS
 static I_mutex hms_api_mutex;
 #endif
 
 static char *hms_server_token;
+#ifndef NO_IPV6
+static char *hms_server_token_ipv6;
+#endif
 
 static char hms_useragent[512];
 
@@ -95,7 +105,7 @@ init_user_agent_once(void)
 {
 	if (hms_useragent[0] != '\0')
 		return;
-	
+
 	get_user_agent(hms_useragent, 512);
 }
 
@@ -125,7 +135,7 @@ HMS_on_read (char *s, size_t _1, size_t n, void *userdata)
 }
 
 static struct HMS_buffer *
-HMS_connect (const char *format, ...)
+HMS_connect (int proto, const char *format, ...)
 {
 	va_list ap;
 	CURL *curl;
@@ -135,8 +145,14 @@ HMS_connect (const char *format, ...)
 	size_t token_length;
 	struct HMS_buffer *buffer;
 
+#ifdef NO_IPV6
+	if (proto == PROTO_V6)
+		return NULL;
+#endif
+
 	if (! hms_started)
 	{
+		hms_allow_ipv6 = !M_CheckParm("-noipv6");
 		if (curl_global_init(CURL_GLOBAL_ALL) != 0)
 		{
 			Contact_error();
@@ -218,7 +234,9 @@ HMS_connect (const char *format, ...)
 	curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
 
 #ifndef NO_IPV6
-	if (M_CheckParm("-noipv6"))
+	if (proto == PROTO_V6)
+		curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);
+	if (proto == PROTO_V4)
 #endif
 		curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
 
@@ -308,7 +326,7 @@ HMS_fetch_rooms (int joining, int query_id)
 
 	(void)query_id;
 
-	hms = HMS_connect("rooms");
+	hms = HMS_connect(PROTO_ANY, "rooms");
 
 	if (! hms)
 		return 0;
@@ -408,7 +426,7 @@ HMS_register (void)
 
 	char *title;
 
-	hms = HMS_connect("rooms/%d/register", ms_RoomId);
+	hms = HMS_connect(PROTO_V4, "rooms/%d/register", ms_RoomId);
 
 	if (! hms)
 		return 0;
@@ -440,6 +458,27 @@ HMS_register (void)
 
 	HMS_end(hms);
 
+#ifndef NO_IPV6
+	if (!hms_allow_ipv6)
+		return ok;
+
+	hms = HMS_connect(PROTO_V6, "rooms/%d/register", ms_RoomId);
+
+	if (! hms)
+		return 0;
+
+	curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDS, post);
+
+	ok = HMS_do(hms);
+
+	if (ok)
+	{
+		hms_server_token_ipv6 = strdup(strtok(hms->buffer, "\n"));
+	}
+
+	HMS_end(hms);
+#endif
+
 	return ok;
 }
 
@@ -449,7 +488,7 @@ HMS_unlist (void)
 	struct HMS_buffer *hms;
 	int ok;
 
-	hms = HMS_connect("servers/%s/unlist", hms_server_token);
+	hms = HMS_connect(PROTO_V4, "servers/%s/unlist", hms_server_token);
 
 	if (! hms)
 		return 0;
@@ -461,6 +500,23 @@ HMS_unlist (void)
 
 	free(hms_server_token);
 
+#ifndef NO_IPV6
+	if (hms_server_token_ipv6 && hms_allow_ipv6)
+	{
+		hms = HMS_connect(PROTO_V6, "servers/%s/unlist", hms_server_token_ipv6);
+
+		if (! hms)
+			return 0;
+
+		curl_easy_setopt(hms->curl, CURLOPT_CUSTOMREQUEST, "POST");
+
+		ok = HMS_do(hms);
+		HMS_end(hms);
+
+		free(hms_server_token_ipv6);
+	}
+#endif
+
 	return ok;
 }
 
@@ -474,7 +530,7 @@ HMS_update (void)
 
 	char *title;
 
-	hms = HMS_connect("servers/%s/update", hms_server_token);
+	hms = HMS_connect(PROTO_V4, "servers/%s/update", hms_server_token);
 
 	if (! hms)
 		return 0;
@@ -493,6 +549,21 @@ HMS_update (void)
 	ok = HMS_do(hms);
 	HMS_end(hms);
 
+#ifndef NO_IPV6
+	if (hms_server_token_ipv6 && hms_allow_ipv6)
+	{
+		hms = HMS_connect(PROTO_V6, "servers/%s/update", hms_server_token_ipv6);
+
+		if (! hms)
+			return ok;
+
+		curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDS, post);
+
+		ok = HMS_do(hms);
+		HMS_end(hms);
+	}
+#endif
+
 	return ok;
 }
 
@@ -504,7 +575,7 @@ HMS_list_servers (void)
 	char *list;
 	char *p;
 
-	hms = HMS_connect("servers");
+	hms = HMS_connect(PROTO_ANY, "servers");
 
 	if (! hms)
 		return;
@@ -553,10 +624,10 @@ HMS_fetch_servers (msg_server_t *list, int room_number, int query_id)
 
 	if (room_number > 0)
 	{
-		hms = HMS_connect("rooms/%d/servers", room_number);
+		hms = HMS_connect(PROTO_ANY, "rooms/%d/servers", room_number);
 	}
 	else
-		hms = HMS_connect("servers");
+		hms = HMS_connect(PROTO_ANY, "servers");
 
 	if (! hms)
 		return NULL;
@@ -660,7 +731,7 @@ HMS_compare_mod_version (char *buffer, size_t buffer_size)
 	char *version;
 	char *version_name;
 
-	hms = HMS_connect("versions/%d", MODID);
+	hms = HMS_connect(PROTO_ANY, "versions/%d", MODID);
 
 	if (! hms)
 		return 0;
diff --git a/src/i_addrinfo.c b/src/netcode/i_addrinfo.c
similarity index 100%
rename from src/i_addrinfo.c
rename to src/netcode/i_addrinfo.c
diff --git a/src/i_addrinfo.h b/src/netcode/i_addrinfo.h
similarity index 100%
rename from src/i_addrinfo.h
rename to src/netcode/i_addrinfo.h
diff --git a/src/i_net.h b/src/netcode/i_net.h
similarity index 98%
rename from src/i_net.h
rename to src/netcode/i_net.h
index 12a07f183e515f7ca985afb67ac50cb07cf2c313..09b842296c313cdd43623cfecd1d8da64534397b 100644
--- a/src/i_net.h
+++ b/src/netcode/i_net.h
@@ -18,8 +18,8 @@
 #pragma interface
 #endif
 
-#include "doomdef.h"
-#include "command.h"
+#include "../doomdef.h"
+#include "../command.h"
 
 /// \brief program net id
 #define DOOMCOM_ID (INT32)0x12345678l
diff --git a/src/i_tcp.c b/src/netcode/i_tcp.c
similarity index 84%
rename from src/i_tcp.c
rename to src/netcode/i_tcp.c
index d95b381f4a65fa86030f9efd44c7164b487df61e..2f996cd2923c77501302b5261526ed74aa7d8100 100644
--- a/src/i_tcp.c
+++ b/src/netcode/i_tcp.c
@@ -36,109 +36,104 @@
 	#include <ws2tcpip.h>
 #endif
 
-#include "doomdef.h"
+#include "../doomdef.h"
 
-#if defined (NOMD5) && !defined (NONET)
-	//#define NONET
+#ifdef USE_WINSOCK1
+	#include <winsock.h>
+#else
+	#ifndef USE_WINSOCK
+		#include <arpa/inet.h>
+		#ifdef __APPLE_CC__
+			#ifndef _BSD_SOCKLEN_T_
+				#define _BSD_SOCKLEN_T_
+			#endif //_BSD_SOCKLEN_T_
+		#endif //__APPLE_CC__
+		#include <sys/socket.h>
+		#include <netinet/in.h>
+		#include <netdb.h>
+		#include <sys/ioctl.h>
+	#endif //normal BSD API
+
+	#include <errno.h>
+	#include <time.h>
+
+	#if defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)
+		#include <sys/time.h>
+	#endif // UNIXCOMMON
 #endif
 
-#ifdef NONET
-	#undef HAVE_MINIUPNPC
-#else
-	#ifdef USE_WINSOCK1
-		#include <winsock.h>
-	#else
-		#ifndef USE_WINSOCK
-			#include <arpa/inet.h>
-			#ifdef __APPLE_CC__
-				#ifndef _BSD_SOCKLEN_T_
-					#define _BSD_SOCKLEN_T_
-				#endif //_BSD_SOCKLEN_T_
-			#endif //__APPLE_CC__
-			#include <sys/socket.h>
-			#include <netinet/in.h>
-			#include <netdb.h>
-			#include <sys/ioctl.h>
-		#endif //normal BSD API
-
-		#include <errno.h>
-		#include <time.h>
-
-		#if defined (__unix__) || defined (__APPLE__) || defined (UNIXCOMMON)
-			#include <sys/time.h>
-		#endif // UNIXCOMMON
+#ifdef USE_WINSOCK
+	// some undefined under win32
+	#undef errno
+	//#define errno WSAGetLastError() //Alam_GBC: this is the correct way, right?
+	#define errno h_errno // some very strange things happen when not using h_error?!?
+	#ifdef EWOULDBLOCK
+	#undef EWOULDBLOCK
 	#endif
-
-	#ifdef USE_WINSOCK
-		// some undefined under win32
-		#undef errno
-		//#define errno WSAGetLastError() //Alam_GBC: this is the correct way, right?
-		#define errno h_errno // some very strange things happen when not using h_error?!?
-		#ifdef EWOULDBLOCK
-		#undef EWOULDBLOCK
-		#endif
-		#define EWOULDBLOCK WSAEWOULDBLOCK
-		#ifdef EMSGSIZE
-		#undef EMSGSIZE
-		#endif
-		#define EMSGSIZE WSAEMSGSIZE
-		#ifdef ECONNREFUSED
-		#undef ECONNREFUSED
-		#endif
-		#define ECONNREFUSED WSAECONNREFUSED
-		#ifdef ETIMEDOUT
-		#undef ETIMEDOUT
-		#endif
-		#define ETIMEDOUT WSAETIMEDOUT
-		#ifndef IOC_VENDOR
-		#define IOC_VENDOR 0x18000000
-		#endif
-		#ifndef _WSAIOW
-		#define _WSAIOW(x,y) (IOC_IN|(x)|(y))
-		#endif
-		#ifndef SIO_UDP_CONNRESET
-		#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR,12)
-		#endif
-		#ifndef AI_ADDRCONFIG
-		#define AI_ADDRCONFIG 0x00000400
-		#endif
-		#ifndef STATUS_INVALID_PARAMETER
-		#define STATUS_INVALID_PARAMETER 0xC000000D
-		#endif
-	#endif // USE_WINSOCK
-
-	typedef union
-	{
-		struct sockaddr     any;
-		struct sockaddr_in  ip4;
-	#ifdef HAVE_IPV6
-		struct sockaddr_in6 ip6;
+	#define EWOULDBLOCK WSAEWOULDBLOCK
+	#ifdef EMSGSIZE
+	#undef EMSGSIZE
+	#endif
+	#define EMSGSIZE WSAEMSGSIZE
+	#ifdef ECONNREFUSED
+	#undef ECONNREFUSED
+	#endif
+	#define ECONNREFUSED WSAECONNREFUSED
+	#ifdef ETIMEDOUT
+	#undef ETIMEDOUT
 	#endif
-	} mysockaddr_t;
+	#define ETIMEDOUT WSAETIMEDOUT
+	#ifdef EHOSTUNREACH
+	#undef EHOSTUNREACH
+	#endif
+	#define EHOSTUNREACH WSAEHOSTUNREACH
+	#ifndef IOC_VENDOR
+	#define IOC_VENDOR 0x18000000
+	#endif
+	#ifndef _WSAIOW
+	#define _WSAIOW(x,y) (IOC_IN|(x)|(y))
+	#endif
+	#ifndef SIO_UDP_CONNRESET
+	#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR,12)
+	#endif
+	#ifndef AI_ADDRCONFIG
+	#define AI_ADDRCONFIG 0x00000400
+	#endif
+	#ifndef STATUS_INVALID_PARAMETER
+	#define STATUS_INVALID_PARAMETER 0xC000000D
+	#endif
+#endif // USE_WINSOCK
 
-	#ifdef HAVE_MINIUPNPC
-		#ifdef STATIC_MINIUPNPC
-			#define STATICLIB
-		#endif
-		#include "miniupnpc/miniwget.h"
-		#include "miniupnpc/miniupnpc.h"
-		#include "miniupnpc/upnpcommands.h"
-		#undef STATICLIB
-		static UINT8 UPNP_support = TRUE;
-	#endif // HAVE_MINIUPNC
+typedef union
+{
+	struct sockaddr     any;
+	struct sockaddr_in  ip4;
+#ifdef HAVE_IPV6
+	struct sockaddr_in6 ip6;
+#endif
+} mysockaddr_t;
 
-#endif // !NONET
+#ifdef HAVE_MINIUPNPC
+	#ifdef STATIC_MINIUPNPC
+		#define STATICLIB
+	#endif
+	#include "miniupnpc/miniwget.h"
+	#include "miniupnpc/miniupnpc.h"
+	#include "miniupnpc/upnpcommands.h"
+	#undef STATICLIB
+	static UINT8 UPNP_support = TRUE;
+#endif // HAVE_MINIUPNC
 
 #define MAXBANS 100
 
-#include "i_system.h"
+#include "../i_system.h"
 #include "i_net.h"
 #include "d_net.h"
 #include "d_netfil.h"
 #include "i_tcp.h"
-#include "m_argv.h"
+#include "../m_argv.h"
 
-#include "doomstat.h"
+#include "../doomstat.h"
 
 // win32
 #ifdef USE_WINSOCK
@@ -151,7 +146,7 @@
 #define SELECTTEST
 #define DEFAULTPORT "5029"
 
-#if defined (USE_WINSOCK) && !defined (NONET)
+#ifdef USE_WINSOCK
 	typedef SOCKET SOCKET_TYPE;
 	#define ERRSOCKET (SOCKET_ERROR)
 #else
@@ -163,22 +158,22 @@
 	#define ERRSOCKET (-1)
 #endif
 
-#ifndef NONET
-	// define socklen_t in DOS/Windows if it is not already defined
-	#ifdef USE_WINSOCK1
-		typedef int socklen_t;
-	#endif
-	static SOCKET_TYPE mysockets[MAXNETNODES+1] = {ERRSOCKET};
-	static size_t mysocketses = 0;
-	static int myfamily[MAXNETNODES+1] = {0};
-	static SOCKET_TYPE nodesocket[MAXNETNODES+1] = {ERRSOCKET};
-	static mysockaddr_t clientaddress[MAXNETNODES+1];
-	static mysockaddr_t broadcastaddress[MAXNETNODES+1];
-	static size_t broadcastaddresses = 0;
-	static boolean nodeconnected[MAXNETNODES+1];
-	static mysockaddr_t banned[MAXBANS];
-	static UINT8 bannedmask[MAXBANS];
+#define IPV6_MULTICAST_ADDRESS "ff15::57e1:1a12"
+
+// define socklen_t in DOS/Windows if it is not already defined
+#ifdef USE_WINSOCK1
+	typedef int socklen_t;
 #endif
+static SOCKET_TYPE mysockets[MAXNETNODES+1] = {ERRSOCKET};
+static size_t mysocketses = 0;
+static int myfamily[MAXNETNODES+1] = {0};
+static SOCKET_TYPE nodesocket[MAXNETNODES+1] = {ERRSOCKET};
+static mysockaddr_t clientaddress[MAXNETNODES+1];
+static mysockaddr_t broadcastaddress[MAXNETNODES+1];
+static size_t broadcastaddresses = 0;
+static boolean nodeconnected[MAXNETNODES+1];
+static mysockaddr_t banned[MAXBANS];
+static UINT8 bannedmask[MAXBANS];
 
 static size_t numbans = 0;
 static boolean SOCK_bannednode[MAXNETNODES+1]; /// \note do we really need the +1?
@@ -187,7 +182,6 @@ static boolean init_tcp_driver = false;
 static const char *serverport_name = DEFAULTPORT;
 static const char *clientport_name;/* any port */
 
-#ifndef NONET
 #ifdef USE_WINSOCK
 // stupid microsoft makes things complicated
 static char *get_WSAErrorStr(int e)
@@ -387,47 +381,47 @@ static const char *SOCK_AddrToStr(mysockaddr_t *sk)
 #endif
 	return s;
 }
-#endif
 
 static const char *SOCK_GetNodeAddress(INT32 node)
 {
 	if (node == 0)
 		return "self";
-#ifdef NONET
-	return NULL;
-#else
 	if (!nodeconnected[node])
 		return NULL;
 	return SOCK_AddrToStr(&clientaddress[node]);
-#endif
 }
 
 static const char *SOCK_GetBanAddress(size_t ban)
 {
 	if (ban >= numbans)
 		return NULL;
-#ifdef NONET
-	return NULL;
-#else
 	return SOCK_AddrToStr(&banned[ban]);
-#endif
 }
 
 static const char *SOCK_GetBanMask(size_t ban)
 {
-#ifdef NONET
-	(void)ban;
-#else
 	static char s[16]; //255.255.255.255 netmask? no, just CDIR for only
 	if (ban >= numbans)
 		return NULL;
 	if (sprintf(s,"%d",bannedmask[ban]) > 0)
 		return s;
-#endif
 	return NULL;
 }
 
-#ifndef NONET
+#ifdef HAVE_IPV6
+static boolean SOCK_cmpipv6(mysockaddr_t *a, mysockaddr_t *b, UINT8 mask)
+{
+	UINT8 bitmask;
+	I_Assert(mask <= 128);
+	if (memcmp(&a->ip6.sin6_addr, &b->ip6.sin6_addr, mask / 8) != 0)
+		return false;
+	if (mask % 8 == 0)
+		return true;
+	bitmask = 255 << (mask % 8);
+	return (a->ip6.sin6_addr.s6_addr[mask / 8] & bitmask) == (b->ip6.sin6_addr.s6_addr[mask / 8] & bitmask);
+}
+#endif
+
 static boolean SOCK_cmpaddr(mysockaddr_t *a, mysockaddr_t *b, UINT8 mask)
 {
 	UINT32 bitmask = INADDR_NONE;
@@ -440,7 +434,7 @@ static boolean SOCK_cmpaddr(mysockaddr_t *a, mysockaddr_t *b, UINT8 mask)
 			&& (b->ip4.sin_port == 0 || (a->ip4.sin_port == b->ip4.sin_port));
 #ifdef HAVE_IPV6
 	else if (b->any.sa_family == AF_INET6)
-		return !memcmp(&a->ip6.sin6_addr, &b->ip6.sin6_addr, sizeof(b->ip6.sin6_addr))
+		return SOCK_cmpipv6(a, b, mask)
 			&& (b->ip6.sin6_port == 0 || (a->ip6.sin6_port == b->ip6.sin6_port));
 #endif
 	else
@@ -455,24 +449,20 @@ static boolean SOCK_cmpaddr(mysockaddr_t *a, mysockaddr_t *b, UINT8 mask)
   */
 static void cleanupnodes(void)
 {
-	SINT8 j;
-
 	if (!Playing())
 		return;
 
 	// Why can't I start at zero?
-	for (j = 1; j < MAXNETNODES; j++)
-		if (!(nodeingame[j] || SendingFile(j)))
+	for (SINT8 j = 1; j < MAXNETNODES; j++)
+		if (!(netnodes[j].ingame || SendingFile(j)))
 			nodeconnected[j] = false;
 }
 
 static SINT8 getfreenode(void)
 {
-	SINT8 j;
-
 	cleanupnodes();
 
-	for (j = 0; j < MAXNETNODES; j++)
+	for (SINT8 j = 0; j < MAXNETNODES; j++)
 		if (!nodeconnected[j])
 		{
 			nodeconnected[j] = true;
@@ -485,8 +475,8 @@ static SINT8 getfreenode(void)
 	  *          downloading a needed wad, but it's better than not letting anyone join...
 	  */
 	/*I_Error("No more free nodes!!1!11!11!!1111\n");
-	for (j = 1; j < MAXNETNODES; j++)
-		if (!nodeingame[j])
+	for (SINT8 j = 1; j < MAXNETNODES; j++)
+		if (!netnodes[j].ingame)
 			return j;*/
 
 	return -1;
@@ -497,28 +487,27 @@ void Command_Numnodes(void)
 {
 	INT32 connected = 0;
 	INT32 ingame = 0;
-	INT32 i;
 
-	for (i = 1; i < MAXNETNODES; i++)
+	for (INT32 i = 1; i < MAXNETNODES; i++)
 	{
-		if (!(nodeconnected[i] || nodeingame[i]))
+		if (!(nodeconnected[i] || netnodes[i].ingame))
 			continue;
 
 		if (nodeconnected[i])
 			connected++;
-		if (nodeingame[i])
+		if (netnodes[i].ingame)
 			ingame++;
 
 		CONS_Printf("%2d - ", i);
-		if (nodetoplayer[i] != -1)
-			CONS_Printf("player %.2d", nodetoplayer[i]);
+		if (netnodes[i].player != -1)
+			CONS_Printf("player %.2d", netnodes[i].player);
 		else
 			CONS_Printf("         ");
 		if (nodeconnected[i])
 			CONS_Printf(" - connected");
 		else
 			CONS_Printf(" -          ");
-		if (nodeingame[i])
+		if (netnodes[i].ingame)
 			CONS_Printf(" - ingame");
 		else
 			CONS_Printf(" -       ");
@@ -531,19 +520,17 @@ void Command_Numnodes(void)
 				connected, ingame);
 }
 #endif
-#endif
 
-#ifndef NONET
 // Returns true if a packet was received from a new node, false in all other cases
 static boolean SOCK_Get(void)
 {
-	size_t i, n;
+	size_t i;
 	int j;
 	ssize_t c;
 	mysockaddr_t fromaddress;
 	socklen_t fromlen;
 
-	for (n = 0; n < mysocketses; n++)
+	for (size_t n = 0; n < mysocketses; n++)
 	{
 		fromlen = (socklen_t)sizeof(fromaddress);
 		c = recvfrom(mysockets[n], (char *)&doomcom->data, MAXPACKETLENGTH, 0,
@@ -596,20 +583,17 @@ static boolean SOCK_Get(void)
 	doomcom->remotenode = -1; // no packet
 	return false;
 }
-#endif
 
 // check if we can send (do not go over the buffer)
-#ifndef NONET
 
 static fd_set masterset;
 
 #ifdef SELECTTEST
 static boolean FD_CPY(fd_set *src, fd_set *dst, SOCKET_TYPE *fd, size_t len)
 {
-	size_t i;
 	boolean testset = false;
 	FD_ZERO(dst);
-	for (i = 0; i < len;i++)
+	for (size_t i = 0; i < len;i++)
 	{
 		if(fd[i] != (SOCKET_TYPE)ERRSOCKET &&
 		   FD_ISSET(fd[i], src) && !FD_ISSET(fd[i], dst)) // no checking for dups
@@ -649,9 +633,7 @@ static boolean SOCK_CanGet(void)
 	return false;
 }
 #endif
-#endif
 
-#ifndef NONET
 static inline ssize_t SOCK_SendToAddr(SOCKET_TYPE socket, mysockaddr_t *sockaddr)
 {
 	socklen_t d4 = (socklen_t)sizeof(struct sockaddr_in);
@@ -659,6 +641,7 @@ static inline ssize_t SOCK_SendToAddr(SOCKET_TYPE socket, mysockaddr_t *sockaddr
 	socklen_t d6 = (socklen_t)sizeof(struct sockaddr_in6);
 #endif
 	socklen_t d, da = (socklen_t)sizeof(mysockaddr_t);
+	ssize_t status;
 
 	switch (sockaddr->any.sa_family)
 	{
@@ -669,22 +652,26 @@ static inline ssize_t SOCK_SendToAddr(SOCKET_TYPE socket, mysockaddr_t *sockaddr
 		default:       d = da; break;
 	}
 
-	return sendto(socket, (char *)&doomcom->data, doomcom->datalength, 0, &sockaddr->any, d);
+	status = sendto(socket, (char *)&doomcom->data, doomcom->datalength, 0, &sockaddr->any, d);
+	if (status == -1)
+	{
+		CONS_Alert(CONS_WARNING, "Unable to send packet to %s: %s\n", SOCK_AddrToStr(sockaddr), strerror(errno));
+	}
+	return status;
 }
 
 static void SOCK_Send(void)
 {
 	ssize_t c = ERRSOCKET;
-	size_t i, j;
 
 	if (!nodeconnected[doomcom->remotenode])
 		return;
 
 	if (doomcom->remotenode == BROADCASTADDR)
 	{
-		for (i = 0; i < mysocketses; i++)
+		for (size_t i = 0; i < mysocketses; i++)
 		{
-			for (j = 0; j < broadcastaddresses; j++)
+			for (size_t j = 0; j < broadcastaddresses; j++)
 			{
 				if (myfamily[i] == broadcastaddress[j].any.sa_family)
 					SOCK_SendToAddr(mysockets[i], &broadcastaddress[j]);
@@ -694,7 +681,7 @@ static void SOCK_Send(void)
 	}
 	else if (nodesocket[doomcom->remotenode] == (SOCKET_TYPE)ERRSOCKET)
 	{
-		for (i = 0; i < mysocketses; i++)
+		for (size_t i = 0; i < mysocketses; i++)
 		{
 			if (myfamily[i] == clientaddress[doomcom->remotenode].any.sa_family)
 				SOCK_SendToAddr(mysockets[i], &clientaddress[doomcom->remotenode]);
@@ -709,14 +696,12 @@ static void SOCK_Send(void)
 	if (c == ERRSOCKET)
 	{
 		int e = errno; // save error code so it can't be modified later
-		if (e != ECONNREFUSED && e != EWOULDBLOCK)
+		if (e != ECONNREFUSED && e != EWOULDBLOCK && e != EHOSTUNREACH)
 			I_Error("SOCK_Send, error sending to node %d (%s) #%u: %s", doomcom->remotenode,
 				SOCK_GetNodeAddress(doomcom->remotenode), e, strerror(e));
 	}
 }
-#endif
 
-#ifndef NONET
 static void SOCK_FreeNodenum(INT32 numnode)
 {
 	// can't disconnect from self :)
@@ -731,12 +716,10 @@ static void SOCK_FreeNodenum(INT32 numnode)
 	// put invalid address
 	memset(&clientaddress[numnode], 0, sizeof (clientaddress[numnode]));
 }
-#endif
 
 //
 // UDPsocket
 //
-#ifndef NONET
 
 // allocate a socket
 static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen)
@@ -813,6 +796,24 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 		return (SOCKET_TYPE)ERRSOCKET;
 	}
 
+#ifdef HAVE_IPV6
+	if (family == AF_INET6)
+	{
+		// we need to set all of this *after* binding to an address!
+		if (memcmp(&straddr.ip6.sin6_addr, &in6addr_any, sizeof(in6addr_any)) == 0) //IN6_ARE_ADDR_EQUAL
+		{
+			struct ipv6_mreq maddr;
+
+			inet_pton(AF_INET6, IPV6_MULTICAST_ADDRESS, &maddr.ipv6mr_multiaddr);
+			maddr.ipv6mr_interface = 0;
+			if (setsockopt(s, IPPROTO_IPV6, IPV6_JOIN_GROUP, (const char *)&maddr, sizeof(maddr)) != 0)
+			{
+				CONS_Alert(CONS_WARNING, M_GetText("Could not register multicast address\n"));
+			}
+		}
+	}
+#endif
+
 #ifdef FIONBIO
 	// make it non blocking
 	opt = true;
@@ -993,65 +994,28 @@ static boolean UDP_Socket(void)
 	// ip + udp
 	packetheaderlength = 20 + 8; // for stats
 
-	hints.ai_family = AF_INET;
-	gaie = I_getaddrinfo("127.0.0.1", "0", &hints, &ai);
-	if (gaie == 0)
-	{
-		runp = ai;
-		while (runp != NULL && s < MAXNETNODES+1)
-		{
-			memcpy(&clientaddress[s], runp->ai_addr, runp->ai_addrlen);
-			s++;
-			runp = runp->ai_next;
-		}
-		I_freeaddrinfo(ai);
-	}
-	else
-	{
-		clientaddress[s].any.sa_family = AF_INET;
-		clientaddress[s].ip4.sin_port = htons(0);
-		clientaddress[s].ip4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); //GetLocalAddress(); // my own ip
-		s++;
-	}
+	clientaddress[s].any.sa_family = AF_INET;
+	clientaddress[s].ip4.sin_port = htons(0);
+	clientaddress[s].ip4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); //GetLocalAddress(); // my own ip
+	s++;
 
 	s = 0;
 
 	// setup broadcast adress to BROADCASTADDR entry
-	gaie = I_getaddrinfo("255.255.255.255", "0", &hints, &ai);
-	if (gaie == 0)
-	{
-		runp = ai;
-		while (runp != NULL && s < MAXNETNODES+1)
-		{
-			memcpy(&broadcastaddress[s], runp->ai_addr, runp->ai_addrlen);
-			s++;
-			runp = runp->ai_next;
-		}
-		I_freeaddrinfo(ai);
-	}
-	else
-	{
-		broadcastaddress[s].any.sa_family = AF_INET;
-		broadcastaddress[s].ip4.sin_port = htons(0);
-		broadcastaddress[s].ip4.sin_addr.s_addr = htonl(INADDR_BROADCAST);
-		s++;
-	}
+	broadcastaddress[s].any.sa_family = AF_INET;
+	broadcastaddress[s].ip4.sin_port = htons(atoi(DEFAULTPORT));
+	broadcastaddress[s].ip4.sin_addr.s_addr = htonl(INADDR_BROADCAST);
+	s++;
+
 #ifdef HAVE_IPV6
 	if (b_ipv6)
 	{
-		hints.ai_family = AF_INET6;
-		gaie = I_getaddrinfo("ff02::1", "0", &hints, &ai);
-		if (gaie == 0)
-		{
-			runp = ai;
-			while (runp != NULL && s < MAXNETNODES+1)
-			{
-				memcpy(&broadcastaddress[s], runp->ai_addr, runp->ai_addrlen);
-				s++;
-				runp = runp->ai_next;
-			}
-			I_freeaddrinfo(ai);
-		}
+		broadcastaddress[s].any.sa_family = AF_INET6;
+		broadcastaddress[s].ip6.sin6_port = htons(atoi(DEFAULTPORT));
+		broadcastaddress[s].ip6.sin6_flowinfo = 0;
+		inet_pton(AF_INET6, IPV6_MULTICAST_ADDRESS, &broadcastaddress[s].ip6.sin6_addr);
+		broadcastaddress[s].ip6.sin6_scope_id = 0;
+		s++;
 	}
 #endif
 
@@ -1061,12 +1025,10 @@ static boolean UDP_Socket(void)
 
 	return true;
 }
-#endif
 
 boolean I_InitTcpDriver(void)
 {
 	boolean tcp_was_up = init_tcp_driver;
-#ifndef NONET
 	if (!init_tcp_driver)
 	{
 #ifdef USE_WINSOCK
@@ -1121,7 +1083,7 @@ boolean I_InitTcpDriver(void)
 #endif
 		init_tcp_driver = true;
 	}
-#endif
+
 	if (!tcp_was_up && init_tcp_driver)
 	{
 		I_AddExitFunc(I_ShutdownTcpDriver);
@@ -1135,11 +1097,9 @@ boolean I_InitTcpDriver(void)
 	return init_tcp_driver;
 }
 
-#ifndef NONET
 static void SOCK_CloseSocket(void)
 {
-	size_t i;
-	for (i=0; i < MAXNETNODES+1; i++)
+	for (size_t i=0; i < MAXNETNODES+1; i++)
 	{
 		if (mysockets[i] != (SOCKET_TYPE)ERRSOCKET
 		 && FD_ISSET(mysockets[i], &masterset))
@@ -1150,11 +1110,9 @@ static void SOCK_CloseSocket(void)
 		mysockets[i] = ERRSOCKET;
 	}
 }
-#endif
 
 void I_ShutdownTcpDriver(void)
 {
-#ifndef NONET
 	SOCK_CloseSocket();
 
 	CONS_Printf("I_ShutdownTcpDriver: ");
@@ -1164,10 +1122,8 @@ void I_ShutdownTcpDriver(void)
 #endif
 	CONS_Printf("shut down\n");
 	init_tcp_driver = false;
-#endif
 }
 
-#ifndef NONET
 static SINT8 SOCK_NetMakeNodewPort(const char *address, const char *port)
 {
 	SINT8 newnode = -1;
@@ -1223,17 +1179,13 @@ static SINT8 SOCK_NetMakeNodewPort(const char *address, const char *port)
 	I_freeaddrinfo(ai);
 	return newnode;
 }
-#endif
 
 static boolean SOCK_OpenSocket(void)
 {
-#ifndef NONET
-	size_t i;
-
 	memset(clientaddress, 0, sizeof (clientaddress));
 
 	nodeconnected[0] = true; // always connected to self
-	for (i = 1; i < MAXNETNODES; i++)
+	for (size_t i = 1; i < MAXNETNODES; i++)
 		nodeconnected[i] = false;
 	nodeconnected[BROADCASTADDR] = true;
 	I_NetSend = SOCK_Send;
@@ -1251,18 +1203,12 @@ static boolean SOCK_OpenSocket(void)
 	// build the socket but close it first
 	SOCK_CloseSocket();
 	return UDP_Socket();
-#else
-	return false;
-#endif
 }
 
 static boolean SOCK_Ban(INT32 node)
 {
 	if (node > MAXNETNODES)
 		return false;
-#ifdef NONET
-	return false;
-#else
 	if (numbans == MAXBANS)
 		return false;
 
@@ -1281,16 +1227,10 @@ static boolean SOCK_Ban(INT32 node)
 #endif
 	numbans++;
 	return true;
-#endif
 }
 
 static boolean SOCK_SetBanAddress(const char *address, const char *mask)
 {
-#ifdef NONET
-	(void)address;
-	(void)mask;
-	return false;
-#else
 	struct my_addrinfo *ai, *runp, hints;
 	int gaie;
 
@@ -1335,7 +1275,6 @@ static boolean SOCK_SetBanAddress(const char *address, const char *mask)
 	I_freeaddrinfo(ai);
 
 	return true;
-#endif
 }
 
 static void SOCK_ClearBans(void)
@@ -1441,4 +1380,13 @@ boolean I_InitTcpNetwork(void)
 	return ret;
 }
 
+boolean Net_IsNodeIPv6(INT32 node)
+{
+#ifdef NO_IPV6
+	return false;
+#else
+	return clientaddress[node].any.sa_family == AF_INET6;
+#endif
+}
+
 #include "i_addrinfo.c"
diff --git a/src/i_tcp.h b/src/netcode/i_tcp.h
similarity index 100%
rename from src/i_tcp.h
rename to src/netcode/i_tcp.h
diff --git a/src/mserv.c b/src/netcode/mserv.c
similarity index 95%
rename from src/mserv.c
rename to src/netcode/mserv.c
index 62cda96e45baf7b8e2e785ef5ee6f51437272610..6d6fac26faed55e4b7bfa7512bb16ddab8ee1cfb 100644
--- a/src/mserv.c
+++ b/src/netcode/mserv.c
@@ -15,13 +15,14 @@
 #include <time.h>
 #endif
 
-#include "doomstat.h"
-#include "doomdef.h"
-#include "command.h"
-#include "i_threads.h"
+#include "../doomstat.h"
+#include "../doomdef.h"
+#include "../command.h"
+#include "../i_threads.h"
 #include "mserv.h"
-#include "m_menu.h"
-#include "z_zone.h"
+#include "client_connection.h"
+#include "../m_menu.h"
+#include "../z_zone.h"
 
 #ifdef MASTERSERVER
 
@@ -45,9 +46,7 @@ static I_cond  MSCond;
 #  define Unlock_state()
 #endif/*HAVE_THREADS*/
 
-#ifndef NONET
 static void Command_Listserv_f(void);
-#endif
 
 #endif/*MASTERSERVER*/
 
@@ -61,7 +60,7 @@ static CV_PossibleValue_t masterserver_update_rate_cons_t[] = {
 	{0,NULL}
 };
 
-consvar_t cv_masterserver = CVAR_INIT ("masterserver", "https://mb.srb2.org/MS/0", CV_SAVE|CV_CALL, NULL, MasterServer_OnChange);
+consvar_t cv_masterserver = CVAR_INIT ("masterserver", "https://ds.ms.srb2.org/MS/0", CV_SAVE|CV_CALL, NULL, MasterServer_OnChange);
 consvar_t cv_servername = CVAR_INIT ("servername", "SRB2 server", CV_SAVE|CV_NETVAR|CV_CALL|CV_NOINIT|CV_ALLOWLUA, NULL, Update_parameters);
 
 consvar_t cv_masterserver_update_rate = CVAR_INIT ("masterserver_update_rate", "15", CV_SAVE|CV_CALL|CV_NOINIT, masterserver_update_rate_cons_t, Update_parameters);
@@ -89,7 +88,6 @@ msg_rooms_t room_list[NUM_LIST_ROOMS+1]; // +1 for easy test
   */
 void AddMServCommands(void)
 {
-#ifndef NONET
 	CV_RegisterVar(&cv_masterserver);
 	CV_RegisterVar(&cv_masterserver_update_rate);
 	CV_RegisterVar(&cv_masterserver_timeout);
@@ -100,7 +98,6 @@ void AddMServCommands(void)
 	COM_AddCommand("listserv", Command_Listserv_f, 0);
 	COM_AddCommand("masterserver_update", Update_parameters, COM_LUA); // allows people to updates manually in case you were delisted by accident
 #endif
-#endif
 }
 
 #ifdef MASTERSERVER
@@ -189,7 +186,6 @@ void GetMODVersion_Console(void)
 }
 #endif
 
-#ifndef NONET
 /** Gets a list of game servers. Called from console.
   */
 static void Command_Listserv_f(void)
@@ -200,7 +196,6 @@ static void Command_Listserv_f(void)
 		HMS_list_servers();
 	}
 }
-#endif
 
 static void
 Finish_registration (void)
@@ -544,6 +539,13 @@ static void MasterServer_OnChange(void)
 		CV_StealthSet(&cv_masterserver, cv_masterserver.defaultvalue);
 	}
 
+	if (
+			! cv_masterserver.changed &&
+			strcmp(cv_masterserver.string, "https://mb.srb2.org/MS/0") == 0
+	){
+		CV_StealthSet(&cv_masterserver, cv_masterserver.defaultvalue);
+	}
+
 	Set_api(cv_masterserver.string);
 
 	if (Online())
diff --git a/src/mserv.h b/src/netcode/mserv.h
similarity index 99%
rename from src/mserv.h
rename to src/netcode/mserv.h
index 07253da8562906cb51c59e6a27f2ec289d62c9a8..0bc8c8e6b878a415887e2a9613188d3ceac30956 100644
--- a/src/mserv.h
+++ b/src/netcode/mserv.h
@@ -14,7 +14,7 @@
 #ifndef _MSERV_H_
 #define _MSERV_H_
 
-#include "i_threads.h"
+#include "../i_threads.h"
 
 // lowered from 32 due to menu changes
 #define NUM_LIST_ROOMS 16
diff --git a/src/netcode/net_command.c b/src/netcode/net_command.c
new file mode 100644
index 0000000000000000000000000000000000000000..2b3abfd0238a3246fedc086afc007a85c993b425
--- /dev/null
+++ b/src/netcode/net_command.c
@@ -0,0 +1,382 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2022 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  net_command.c
+/// \brief Net command handling
+
+#include "net_command.h"
+#include "tic_command.h"
+#include "gamestate.h"
+#include "server_connection.h"
+#include "d_clisrv.h"
+#include "i_net.h"
+#include "../byteptr.h"
+#include "../g_game.h"
+#include "../z_zone.h"
+#include "../doomtype.h"
+
+textcmdtic_t *textcmds[TEXTCMD_HASH_SIZE] = {NULL};
+UINT8 localtextcmd[MAXTEXTCMD];
+UINT8 localtextcmd2[MAXTEXTCMD]; // splitscreen
+static void (*listnetxcmd[MAXNETXCMD])(UINT8 **p, INT32 playernum);
+
+void RegisterNetXCmd(netxcmd_t id, void (*cmd_f)(UINT8 **p, INT32 playernum))
+{
+#ifdef PARANOIA
+	if (id >= MAXNETXCMD)
+		I_Error("Command id %d too big", id);
+	if (listnetxcmd[id] != 0)
+		I_Error("Command id %d already used", id);
+#endif
+	listnetxcmd[id] = cmd_f;
+}
+
+void SendNetXCmd(netxcmd_t id, const void *param, size_t nparam)
+{
+	if (localtextcmd[0]+2+nparam > MAXTEXTCMD)
+	{
+		// for future reference: if (cv_debug) != debug disabled.
+		CONS_Alert(CONS_ERROR, M_GetText("NetXCmd buffer full, cannot add netcmd %d! (size: %d, needed: %s)\n"), id, localtextcmd[0], sizeu1(nparam));
+		return;
+	}
+	localtextcmd[0]++;
+	localtextcmd[localtextcmd[0]] = (UINT8)id;
+	if (param && nparam)
+	{
+		M_Memcpy(&localtextcmd[localtextcmd[0]+1], param, nparam);
+		localtextcmd[0] = (UINT8)(localtextcmd[0] + (UINT8)nparam);
+	}
+}
+
+// splitscreen player
+void SendNetXCmd2(netxcmd_t id, const void *param, size_t nparam)
+{
+	if (localtextcmd2[0]+2+nparam > MAXTEXTCMD)
+	{
+		I_Error("No more place in the buffer for netcmd %d\n",id);
+		return;
+	}
+	localtextcmd2[0]++;
+	localtextcmd2[localtextcmd2[0]] = (UINT8)id;
+	if (param && nparam)
+	{
+		M_Memcpy(&localtextcmd2[localtextcmd2[0]+1], param, nparam);
+		localtextcmd2[0] = (UINT8)(localtextcmd2[0] + (UINT8)nparam);
+	}
+}
+
+UINT8 GetFreeXCmdSize(void)
+{
+	// -1 for the size and another -1 for the ID.
+	return (UINT8)(localtextcmd[0] - 2);
+}
+
+// Frees all textcmd memory for the specified tic
+void D_FreeTextcmd(tic_t tic)
+{
+	textcmdtic_t **tctprev = &textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
+	textcmdtic_t *textcmdtic = *tctprev;
+
+	while (textcmdtic && textcmdtic->tic != tic)
+	{
+		tctprev = &textcmdtic->next;
+		textcmdtic = textcmdtic->next;
+	}
+
+	if (textcmdtic)
+	{
+		// Remove this tic from the list.
+		*tctprev = textcmdtic->next;
+
+		// Free all players.
+		for (INT32 i = 0; i < TEXTCMD_HASH_SIZE; i++)
+		{
+			textcmdplayer_t *textcmdplayer = textcmdtic->playercmds[i];
+
+			while (textcmdplayer)
+			{
+				textcmdplayer_t *tcpnext = textcmdplayer->next;
+				Z_Free(textcmdplayer);
+				textcmdplayer = tcpnext;
+			}
+		}
+
+		// Free this tic's own memory.
+		Z_Free(textcmdtic);
+	}
+}
+
+// Gets the buffer for the specified ticcmd, or NULL if there isn't one
+UINT8* D_GetExistingTextcmd(tic_t tic, INT32 playernum)
+{
+	textcmdtic_t *textcmdtic = textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
+	while (textcmdtic && textcmdtic->tic != tic) textcmdtic = textcmdtic->next;
+
+	// Do we have an entry for the tic? If so, look for player.
+	if (textcmdtic)
+	{
+		textcmdplayer_t *textcmdplayer = textcmdtic->playercmds[playernum & (TEXTCMD_HASH_SIZE - 1)];
+		while (textcmdplayer && textcmdplayer->playernum != playernum) textcmdplayer = textcmdplayer->next;
+
+		if (textcmdplayer) return textcmdplayer->cmd;
+	}
+
+	return NULL;
+}
+
+// Gets the buffer for the specified ticcmd, creating one if necessary
+UINT8* D_GetTextcmd(tic_t tic, INT32 playernum)
+{
+	textcmdtic_t *textcmdtic = textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
+	textcmdtic_t **tctprev = &textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
+	textcmdplayer_t *textcmdplayer, **tcpprev;
+
+	// Look for the tic.
+	while (textcmdtic && textcmdtic->tic != tic)
+	{
+		tctprev = &textcmdtic->next;
+		textcmdtic = textcmdtic->next;
+	}
+
+	// If we don't have an entry for the tic, make it.
+	if (!textcmdtic)
+	{
+		textcmdtic = *tctprev = Z_Calloc(sizeof (textcmdtic_t), PU_STATIC, NULL);
+		textcmdtic->tic = tic;
+	}
+
+	tcpprev = &textcmdtic->playercmds[playernum & (TEXTCMD_HASH_SIZE - 1)];
+	textcmdplayer = *tcpprev;
+
+	// Look for the player.
+	while (textcmdplayer && textcmdplayer->playernum != playernum)
+	{
+		tcpprev = &textcmdplayer->next;
+		textcmdplayer = textcmdplayer->next;
+	}
+
+	// If we don't have an entry for the player, make it.
+	if (!textcmdplayer)
+	{
+		textcmdplayer = *tcpprev = Z_Calloc(sizeof (textcmdplayer_t), PU_STATIC, NULL);
+		textcmdplayer->playernum = playernum;
+	}
+
+	return textcmdplayer->cmd;
+}
+
+void ExtraDataTicker(void)
+{
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+		if (playeringame[i] || i == 0)
+		{
+			UINT8 *bufferstart = D_GetExistingTextcmd(gametic, i);
+
+			if (bufferstart)
+			{
+				UINT8 *curpos = bufferstart;
+				UINT8 *bufferend = &curpos[curpos[0]+1];
+
+				curpos++;
+				while (curpos < bufferend)
+				{
+					if (*curpos < MAXNETXCMD && listnetxcmd[*curpos])
+					{
+						const UINT8 id = *curpos;
+						curpos++;
+						DEBFILE(va("executing x_cmd %s ply %u ", netxcmdnames[id - 1], i));
+						(listnetxcmd[id])(&curpos, i);
+						DEBFILE("done\n");
+					}
+					else
+					{
+						if (server)
+						{
+							SendKick(i, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
+							DEBFILE(va("player %d kicked [gametic=%u] reason as follows:\n", i, gametic));
+						}
+						CONS_Alert(CONS_WARNING, M_GetText("Got unknown net command [%s]=%d (max %d)\n"), sizeu1(curpos - bufferstart), *curpos, bufferstart[0]);
+						break;
+					}
+				}
+			}
+		}
+
+	// If you are a client, you can safely forget the net commands for this tic
+	// If you are the server, you need to remember them until every client has been acknowledged,
+	// because if you need to resend a PT_SERVERTICS packet, you will need to put the commands in it
+	if (client)
+		D_FreeTextcmd(gametic);
+}
+
+// used at txtcmds received to check packetsize bound
+size_t TotalTextCmdPerTic(tic_t tic)
+{
+	size_t total = 1; // num of textcmds in the tic (ntextcmd byte)
+
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+	{
+		UINT8 *textcmd = D_GetExistingTextcmd(tic, i);
+		if ((!i || playeringame[i]) && textcmd)
+			total += 2 + textcmd[0]; // "+2" for size and playernum
+	}
+
+	return total;
+}
+
+void PT_TextCmd(SINT8 node, INT32 netconsole)
+{
+	if (client)
+		return;
+
+	// splitscreen special
+	if (netbuffer->packettype == PT_TEXTCMD2)
+		netconsole = netnodes[node].player2;
+
+	if (netconsole < 0 || netconsole >= MAXPLAYERS)
+		Net_UnAcknowledgePacket(node);
+	else
+	{
+		size_t j;
+		tic_t tic = maketic;
+		UINT8 *textcmd;
+
+		// ignore if the textcmd has a reported size of zero
+		// this shouldn't be sent at all
+		if (!netbuffer->u.textcmd[0])
+		{
+			DEBFILE(va("GetPacket: Textcmd with size 0 detected! (node %u, player %d)\n",
+				node, netconsole));
+			Net_UnAcknowledgePacket(node);
+			return;
+		}
+
+		// ignore if the textcmd size var is actually larger than it should be
+		// BASEPACKETSIZE + 1 (for size) + textcmd[0] should == datalength
+		if (netbuffer->u.textcmd[0] > (size_t)doomcom->datalength-BASEPACKETSIZE-1)
+		{
+			DEBFILE(va("GetPacket: Bad Textcmd packet size! (expected %d, actual %s, node %u, player %d)\n",
+			netbuffer->u.textcmd[0], sizeu1((size_t)doomcom->datalength-BASEPACKETSIZE-1),
+				node, netconsole));
+			Net_UnAcknowledgePacket(node);
+			return;
+		}
+
+		// check if tic that we are making isn't too large else we cannot send it :(
+		// doomcom->numslots+1 "+1" since doomcom->numslots can change within this time and sent time
+		j = software_MAXPACKETLENGTH
+			- (netbuffer->u.textcmd[0]+2+BASESERVERTICSSIZE
+			+ (doomcom->numslots+1)*sizeof(ticcmd_t));
+
+		// search a tic that have enougth space in the ticcmd
+		while ((textcmd = D_GetExistingTextcmd(tic, netconsole)),
+			(TotalTextCmdPerTic(tic) > j || netbuffer->u.textcmd[0] + (textcmd ? textcmd[0] : 0) > MAXTEXTCMD)
+			&& tic < firstticstosend + BACKUPTICS)
+			tic++;
+
+		if (tic >= firstticstosend + BACKUPTICS)
+		{
+			DEBFILE(va("GetPacket: Textcmd too long (max %s, used %s, mak %d, "
+				"tosend %u, node %u, player %d)\n", sizeu1(j), sizeu2(TotalTextCmdPerTic(maketic)),
+				maketic, firstticstosend, node, netconsole));
+			Net_UnAcknowledgePacket(node);
+			return;
+		}
+
+		// Make sure we have a buffer
+		if (!textcmd) textcmd = D_GetTextcmd(tic, netconsole);
+
+		DEBFILE(va("textcmd put in tic %u at position %d (player %d) ftts %u mk %u\n",
+			tic, textcmd[0]+1, netconsole, firstticstosend, maketic));
+
+		M_Memcpy(&textcmd[textcmd[0]+1], netbuffer->u.textcmd+1, netbuffer->u.textcmd[0]);
+		textcmd[0] += (UINT8)netbuffer->u.textcmd[0];
+	}
+}
+
+void SV_WriteNetCommandsForTic(tic_t tic, UINT8 **buf)
+{
+	UINT8 *numcmds;
+
+	numcmds = (*buf)++;
+	*numcmds = 0;
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+	{
+		UINT8 *cmd = D_GetExistingTextcmd(tic, i);
+		INT32 size = cmd ? cmd[0] : 0;
+
+		if ((!i || playeringame[i]) && size)
+		{
+			(*numcmds)++;
+			WRITEUINT8(*buf, i);
+			M_Memcpy(*buf, cmd, size + 1);
+			*buf += size + 1;
+		}
+	}
+}
+
+void CL_CopyNetCommandsFromServerPacket(tic_t tic, UINT8 **buf)
+{
+	UINT8 numcmds = *(*buf)++;
+
+	for (UINT32 i = 0; i < numcmds; i++)
+	{
+		INT32 playernum = *(*buf)++; // playernum
+		size_t size = (*buf)[0]+1;
+
+		if (tic >= gametic) // Don't copy old net commands
+			M_Memcpy(D_GetTextcmd(tic, playernum), *buf, size);
+		*buf += size;
+	}
+}
+
+void CL_SendNetCommands(void)
+{
+	// Send extra data if needed
+	if (localtextcmd[0])
+	{
+		netbuffer->packettype = PT_TEXTCMD;
+		M_Memcpy(netbuffer->u.textcmd,localtextcmd, localtextcmd[0]+1);
+		// All extra data have been sent
+		if (HSendPacket(servernode, true, 0, localtextcmd[0]+1)) // Send can fail...
+			localtextcmd[0] = 0;
+	}
+
+	// Send extra data if needed for player 2 (splitscreen)
+	if (localtextcmd2[0])
+	{
+		netbuffer->packettype = PT_TEXTCMD2;
+		M_Memcpy(netbuffer->u.textcmd, localtextcmd2, localtextcmd2[0]+1);
+		// All extra data have been sent
+		if (HSendPacket(servernode, true, 0, localtextcmd2[0]+1)) // Send can fail...
+			localtextcmd2[0] = 0;
+	}
+}
+
+void SendKick(UINT8 playernum, UINT8 msg)
+{
+	UINT8 buf[2];
+
+	if (!(server && cv_rejointimeout.value))
+		msg &= ~KICK_MSG_KEEP_BODY;
+
+	buf[0] = playernum;
+	buf[1] = msg;
+	SendNetXCmd(XD_KICK, &buf, 2);
+}
+
+void SendKicksForNode(SINT8 node, UINT8 msg)
+{
+	if (!netnodes[node].ingame)
+		return;
+
+	for (INT32 playernum = netnodes[node].player; playernum != -1; playernum = netnodes[node].player2)
+		if (playernum != -1 && playeringame[playernum])
+			SendKick(playernum, msg);
+}
diff --git a/src/netcode/net_command.h b/src/netcode/net_command.h
new file mode 100644
index 0000000000000000000000000000000000000000..a0c46f3a2ffb26156708bfe3fbb3244b5fbc48ec
--- /dev/null
+++ b/src/netcode/net_command.h
@@ -0,0 +1,66 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2022 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  net_command.h
+/// \brief Net command handling
+
+#ifndef __D_NET_COMMAND__
+#define __D_NET_COMMAND__
+
+#include "d_clisrv.h"
+#include "../doomtype.h"
+
+// Must be a power of two
+#define TEXTCMD_HASH_SIZE 4
+
+typedef struct textcmdplayer_s
+{
+	INT32 playernum;
+	UINT8 cmd[MAXTEXTCMD];
+	struct textcmdplayer_s *next;
+} textcmdplayer_t;
+
+typedef struct textcmdtic_s
+{
+	tic_t tic;
+	textcmdplayer_t *playercmds[TEXTCMD_HASH_SIZE];
+	struct textcmdtic_s *next;
+} textcmdtic_t;
+
+extern textcmdtic_t *textcmds[TEXTCMD_HASH_SIZE];
+
+extern UINT8 localtextcmd[MAXTEXTCMD];
+extern UINT8 localtextcmd2[MAXTEXTCMD]; // splitscreen
+
+void RegisterNetXCmd(netxcmd_t id, void (*cmd_f)(UINT8 **p, INT32 playernum));
+void SendNetXCmd(netxcmd_t id, const void *param, size_t nparam);
+void SendNetXCmd2(netxcmd_t id, const void *param, size_t nparam); // splitsreen player
+
+UINT8 GetFreeXCmdSize(void);
+void D_FreeTextcmd(tic_t tic);
+
+// Gets the buffer for the specified ticcmd, or NULL if there isn't one
+UINT8* D_GetExistingTextcmd(tic_t tic, INT32 playernum);
+
+// Gets the buffer for the specified ticcmd, creating one if necessary
+UINT8* D_GetTextcmd(tic_t tic, INT32 playernum);
+
+void ExtraDataTicker(void);
+
+// used at txtcmds received to check packetsize bound
+size_t TotalTextCmdPerTic(tic_t tic);
+
+void PT_TextCmd(SINT8 node, INT32 netconsole);
+void SV_WriteNetCommandsForTic(tic_t tic, UINT8 **buf);
+void CL_CopyNetCommandsFromServerPacket(tic_t tic, UINT8 **buf);
+void CL_SendNetCommands(void);
+void SendKick(UINT8 playernum, UINT8 msg);
+void SendKicksForNode(SINT8 node, UINT8 msg);
+
+#endif
diff --git a/src/d_clisrv.h b/src/netcode/protocol.h
similarity index 59%
rename from src/d_clisrv.h
rename to src/netcode/protocol.h
index 49fb5fc1db1507a75c1b379451c27141146d1036..c084d920cfa4dc14d3e0cf475b3c62ea7226020d 100644
--- a/src/d_clisrv.h
+++ b/src/netcode/protocol.h
@@ -7,19 +7,15 @@
 // terms of the GNU General Public License, version 2.
 // See the 'LICENSE' file for more details.
 //-----------------------------------------------------------------------------
-/// \file  d_clisrv.h
-/// \brief high level networking stuff
+/// \file  protocol.h
+/// \brief Data exchanged through the network
 
-#ifndef __D_CLISRV__
-#define __D_CLISRV__
+#ifndef __PROTOCOL__
+#define __PROTOCOL__
 
-#include "d_ticcmd.h"
 #include "d_net.h"
-#include "d_netcmd.h"
-#include "d_net.h"
-#include "tables.h"
-#include "d_player.h"
-#include "mserv.h"
+#include "../d_ticcmd.h"
+#include "../doomdef.h"
 
 /*
 The 'packet version' is used to distinguish packet
@@ -38,10 +34,9 @@ therein, increment this number.
 //  one that defines the actual packets to
 //  be transmitted.
 
-// Networking and tick handling related.
 #define BACKUPTICS 1024
-#define CLIENTBACKUPTICS 32
 #define MAXTEXTCMD 256
+
 //
 // Packet structure
 //
@@ -77,7 +72,7 @@ typedef enum
 	PT_ASKLUAFILE,     // Client telling the server they don't have the file
 	PT_HASLUAFILE,     // Client telling the server they have the file
 
-	PT_BASICKEEPALIVE,// Keep the network alive during wipes, as tics aren't advanced and NetUpdate isn't called
+	PT_BASICKEEPALIVE, // Keep the network alive during wipes, as tics aren't advanced and NetUpdate isn't called
 
 	// Add non-PT_CANFAIL packet types here to avoid breaking MS compatibility.
 
@@ -92,7 +87,6 @@ typedef enum
 	PT_TEXTCMD,       // Extra text commands from the client.
 	PT_TEXTCMD2,      // Splitscreen text commands.
 	PT_CLIENTJOIN,    // Client wants to join; used in start game.
-	PT_NODETIMEOUT,   // Packet sent to self if the connection times out.
 
 	PT_LOGIN,         // Login attempt from the client.
 
@@ -103,14 +97,6 @@ typedef enum
 	NUMPACKETTYPE
 } packettype_t;
 
-#ifdef PACKETDROP
-void Command_Drop(void);
-void Command_Droprate(void);
-#endif
-#ifdef _DEBUG
-void Command_Numnodes(void);
-#endif
-
 #if defined(_MSC_VER)
 #pragma pack(1)
 #endif
@@ -139,13 +125,12 @@ typedef struct
 #endif
 
 // Server to client packet
-// this packet is too large
 typedef struct
 {
 	tic_t starttic;
 	UINT8 numtics;
 	UINT8 numslots; // "Slots filled": Highest player number in use plus one.
-	ticcmd_t cmds[45]; // Normally [BACKUPTIC][MAXPLAYERS] but too large
+	ticcmd_t cmds[45];
 } ATTRPACK servertics_pak;
 
 typedef struct
@@ -215,6 +200,7 @@ enum {
 
 #define MAXSERVERNAME 32
 #define MAXFILENEEDED 915
+
 // This packet is too large
 typedef struct
 {
@@ -275,7 +261,7 @@ typedef struct
 	UINT8 data; // Color is first four bits, hasflag, isit and issuper have one bit each, the last is unused.
 	UINT32 score;
 	UINT16 timeinserver; // In seconds.
-} ATTRPACK plrinfo;
+} ATTRPACK plrinfo_pak;
 
 // Shortest player information for join during intermission.
 typedef struct
@@ -286,7 +272,7 @@ typedef struct
 	UINT32 pflags;
 	UINT32 score;
 	UINT8 ctfteam;
-} ATTRPACK plrconfig;
+} ATTRPACK plrconfig_pak;
 
 typedef struct
 {
@@ -309,25 +295,25 @@ typedef struct
 	UINT8 reserved; // Padding
 	union
 	{
-		clientcmd_pak clientpak;            //         144 bytes
-		client2cmd_pak client2pak;          //         200 bytes
-		servertics_pak serverpak;           //      132495 bytes (more around 360, no?)
-		serverconfig_pak servercfg;         //         773 bytes
-		UINT8 textcmd[MAXTEXTCMD+1];        //       66049 bytes (wut??? 64k??? More like 257 bytes...)
-		filetx_pak filetxpak;               //         139 bytes
+		clientcmd_pak clientpak;
+		client2cmd_pak client2pak;
+		servertics_pak serverpak;
+		serverconfig_pak servercfg;
+		UINT8 textcmd[MAXTEXTCMD+1];
+		filetx_pak filetxpak;
 		fileack_pak fileack;
 		UINT8 filereceived;
-		clientconfig_pak clientcfg;         //         136 bytes
+		clientconfig_pak clientcfg;
 		UINT8 md5sum[16];
-		serverinfo_pak serverinfo;          //        1024 bytes
-		serverrefuse_pak serverrefuse;      //       65025 bytes (somehow I feel like those values are garbage...)
-		askinfo_pak askinfo;                //          61 bytes
-		msaskinfo_pak msaskinfo;            //          22 bytes
-		plrinfo playerinfo[MAXPLAYERS];     //         576 bytes(?)
-		plrconfig playerconfig[MAXPLAYERS]; // (up to) 528 bytes(?)
-		INT32 filesneedednum;               //           4 bytes
-		filesneededconfig_pak filesneededcfg; //       ??? bytes
-		UINT32 pingtable[MAXPLAYERS+1];     //          68 bytes
+		serverinfo_pak serverinfo;
+		serverrefuse_pak serverrefuse;
+		askinfo_pak askinfo;
+		msaskinfo_pak msaskinfo;
+		plrinfo_pak playerinfo[MAXPLAYERS];
+		plrconfig_pak playerconfig[MAXPLAYERS];
+		INT32 filesneedednum;
+		filesneededconfig_pak filesneededcfg;
+		UINT32 pingtable[MAXPLAYERS+1];
 	} u; // This is needed to pack diff packet types data together
 } ATTRPACK doomdata_t;
 
@@ -335,26 +321,7 @@ typedef struct
 #pragma pack()
 #endif
 
-#define MAXSERVERLIST (MAXNETNODES-1)
-typedef struct
-{
-	SINT8 node;
-	serverinfo_pak info;
-} serverelem_t;
-
-extern serverelem_t serverlist[MAXSERVERLIST];
-extern UINT32 serverlistcount;
-extern INT32 mapchangepending;
-
-// Points inside doomcom
-extern doomdata_t *netbuffer;
-
-extern consvar_t cv_showjoinaddress;
-extern consvar_t cv_playbackspeed;
-
-#define BASEPACKETSIZE      offsetof(doomdata_t, u)
 #define FILETXHEADER        offsetof(filetx_pak, data)
-#define BASESERVERTICSSIZE  offsetof(doomdata_t, u.serverpak.cmds[0])
 
 #define KICK_MSG_GO_AWAY     1
 #define KICK_MSG_CON_FAIL    2
@@ -364,109 +331,7 @@ extern consvar_t cv_playbackspeed;
 #define KICK_MSG_PING_HIGH   6
 #define KICK_MSG_CUSTOM_KICK 7
 #define KICK_MSG_CUSTOM_BAN  8
+#define KICK_MSG_IDLE        9
 #define KICK_MSG_KEEP_BODY   0x80
 
-typedef enum
-{
-	KR_KICK          = 1, //Kicked by server
-	KR_PINGLIMIT     = 2, //Broke Ping Limit
-	KR_SYNCH         = 3, //Synch Failure
-	KR_TIMEOUT       = 4, //Connection Timeout
-	KR_BAN           = 5, //Banned by server
-	KR_LEAVE         = 6, //Quit the game
-
-} kickreason_t;
-
-/* the max number of name changes in some time period */
-#define MAXNAMECHANGES (5)
-#define NAMECHANGERATE (60*TICRATE)
-
-extern boolean server;
-extern boolean serverrunning;
-#define client (!server)
-extern boolean dedicated; // For dedicated server
-extern UINT16 software_MAXPACKETLENGTH;
-extern boolean acceptnewnode;
-extern SINT8 servernode;
-
-void Command_Ping_f(void);
-extern tic_t connectiontimeout;
-extern tic_t jointimeout;
-extern UINT16 pingmeasurecount;
-extern UINT32 realpingtable[MAXPLAYERS];
-extern UINT32 playerpingtable[MAXPLAYERS];
-extern tic_t servermaxping;
-
-extern consvar_t cv_netticbuffer, cv_allownewplayer, cv_joinnextround, cv_maxplayers, cv_joindelay, cv_rejointimeout;
-extern consvar_t cv_resynchattempts, cv_blamecfail;
-extern consvar_t cv_maxsend, cv_noticedownload, cv_downloadspeed;
-extern consvar_t cv_dedicatedidletime;
-
-// Used in d_net, the only dependence
-tic_t ExpandTics(INT32 low, INT32 node);
-void D_ClientServerInit(void);
-
-// Initialise the other field
-void RegisterNetXCmd(netxcmd_t id, void (*cmd_f)(UINT8 **p, INT32 playernum));
-void SendNetXCmd(netxcmd_t id, const void *param, size_t nparam);
-void SendNetXCmd2(netxcmd_t id, const void *param, size_t nparam); // splitsreen player
-void SendKick(UINT8 playernum, UINT8 msg);
-
-// Create any new ticcmds and broadcast to other players.
-void NetUpdate(void);
-
-// Maintain connections to nodes without timing them all out.
-void NetKeepAlive(void);
-
-void SV_StartSinglePlayerServer(void);
-boolean SV_SpawnServer(void);
-void SV_StopServer(void);
-void SV_ResetServer(void);
-void CL_AddSplitscreenPlayer(void);
-void CL_RemoveSplitscreenPlayer(void);
-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);
-
-// Broadcasts special packets to other players
-//  to notify of game exit
-void D_QuitNetGame(void);
-
-//? How many ticks to run?
-boolean TryRunTics(tic_t realtic);
-
-// extra data for lmps
-// these functions scare me. they contain magic.
-/*boolean AddLmpExtradata(UINT8 **demo_p, INT32 playernum);
-void ReadLmpExtraData(UINT8 **demo_pointer, INT32 playernum);*/
-
-#ifndef NONET
-// translate a playername in a player number return -1 if not found and
-// print a error message in the console
-SINT8 nametonum(const char *name);
-#endif
-
-extern char motd[254], server_context[8];
-extern UINT8 playernode[MAXPLAYERS];
-
-INT32 D_NumPlayers(void);
-INT32 D_NumBots(void);
-void D_ResetTiccmds(void);
-
-tic_t GetLag(INT32 node);
-UINT8 GetFreeXCmdSize(void);
-
-void D_MD5PasswordPass(const UINT8 *buffer, size_t len, const char *salt, void *dest);
-
-extern UINT8 hu_redownloadinggamestate;
-
-extern UINT8 adminpassmd5[16];
-extern boolean adminpasswordset;
-
-extern boolean hu_stopped;
-
 #endif
diff --git a/src/netcode/server_connection.c b/src/netcode/server_connection.c
new file mode 100644
index 0000000000000000000000000000000000000000..bfbe30a08ed89aba1af4bb5ef6f2a51448d00c1c
--- /dev/null
+++ b/src/netcode/server_connection.c
@@ -0,0 +1,507 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 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  server_connection.c
+/// \brief Server-side part of connection handling
+
+#include "server_connection.h"
+#include "i_net.h"
+#include "d_clisrv.h"
+#include "d_netfil.h"
+#include "mserv.h"
+#include "net_command.h"
+#include "gamestate.h"
+#include "../byteptr.h"
+#include "../g_game.h"
+#include "../g_state.h"
+#include "../p_setup.h"
+#include "../p_tick.h"
+#include "../command.h"
+#include "../doomstat.h"
+
+// Minimum timeout for sending the savegame
+// The actual timeout will be longer depending on the savegame length
+tic_t jointimeout = (10*TICRATE);
+
+// Incremented by cv_joindelay when a client joins, decremented each tic.
+// If higher than cv_joindelay * 2 (3 joins in a short timespan), joins are temporarily disabled.
+tic_t joindelay = 0;
+
+// Minimum timeout for sending the savegame
+// The actual timeout will be longer depending on the savegame length
+char playeraddress[MAXPLAYERS][64];
+
+consvar_t cv_showjoinaddress = CVAR_INIT ("showjoinaddress", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
+
+consvar_t cv_allownewplayer = CVAR_INIT ("allowjoin", "On", CV_SAVE|CV_NETVAR|CV_ALLOWLUA, CV_OnOff, NULL);
+
+static CV_PossibleValue_t maxplayers_cons_t[] = {{2, "MIN"}, {32, "MAX"}, {0, NULL}};
+consvar_t cv_maxplayers = CVAR_INIT ("maxplayers", "8", CV_SAVE|CV_NETVAR|CV_ALLOWLUA, maxplayers_cons_t, NULL);
+
+static CV_PossibleValue_t joindelay_cons_t[] = {{1, "MIN"}, {3600, "MAX"}, {0, "Off"}, {0, NULL}};
+consvar_t cv_joindelay = CVAR_INIT ("joindelay", "10", CV_SAVE|CV_NETVAR, joindelay_cons_t, NULL);
+
+static CV_PossibleValue_t rejointimeout_cons_t[] = {{1, "MIN"}, {60 * FRACUNIT, "MAX"}, {0, "Off"}, {0, NULL}};
+consvar_t cv_rejointimeout = CVAR_INIT ("rejointimeout", "2", CV_SAVE|CV_NETVAR|CV_FLOAT, rejointimeout_cons_t, NULL);
+
+static INT32 FindRejoinerNum(SINT8 node)
+{
+	char addressbuffer[64];
+	const char *nodeaddress;
+	const char *strippednodeaddress;
+
+	// Make sure there is no dead dress before proceeding to the stripping
+	if (!I_GetNodeAddress)
+		return -1;
+	nodeaddress = I_GetNodeAddress(node);
+	if (!nodeaddress)
+		return -1;
+
+	// Strip the address of its port
+	strcpy(addressbuffer, nodeaddress);
+	strippednodeaddress = I_NetSplitAddress(addressbuffer, NULL);
+
+	// Check if any player matches the stripped address
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+	{
+		if (playeringame[i] && playeraddress[i][0] && playernode[i] == UINT8_MAX
+		&& !strcmp(playeraddress[i], strippednodeaddress))
+			return i;
+	}
+
+	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;
+
+	netbuffer->packettype = PT_SERVERINFO;
+	netbuffer->u.serverinfo._255 = 255;
+	netbuffer->u.serverinfo.packetversion = PACKETVERSION;
+	netbuffer->u.serverinfo.version = VERSION;
+	netbuffer->u.serverinfo.subversion = SUBVERSION;
+	strncpy(netbuffer->u.serverinfo.application, SRB2APPLICATION,
+			sizeof netbuffer->u.serverinfo.application);
+	// return back the time value so client can compute their ping
+	netbuffer->u.serverinfo.time = (tic_t)LONG(servertime);
+	netbuffer->u.serverinfo.leveltime = (tic_t)LONG(leveltime);
+
+	// Exclude bots from both counts
+	netbuffer->u.serverinfo.numberofplayer = (UINT8)(D_NumNodes() - (dedicated ? 1 : 0));
+	netbuffer->u.serverinfo.maxplayer = (UINT8)(cv_maxplayers.value - D_NumBots());
+
+	netbuffer->u.serverinfo.refusereason = GetRefuseReason(node);
+
+	strncpy(netbuffer->u.serverinfo.gametypename, Gametype_Names[gametype],
+			sizeof netbuffer->u.serverinfo.gametypename);
+	netbuffer->u.serverinfo.modifiedgame = (UINT8)modifiedgame;
+	netbuffer->u.serverinfo.cheatsenabled = CV_CheatsEnabled();
+	netbuffer->u.serverinfo.flags = (dedicated ? SV_DEDICATED : 0);
+	strncpy(netbuffer->u.serverinfo.servername, cv_servername.string,
+		MAXSERVERNAME);
+	strncpy(netbuffer->u.serverinfo.mapname, G_BuildMapName(gamemap), 7);
+
+	M_Memcpy(netbuffer->u.serverinfo.mapmd5, mapmd5, 16);
+
+	memset(netbuffer->u.serverinfo.maptitle, 0, sizeof netbuffer->u.serverinfo.maptitle);
+
+	if (mapheaderinfo[gamemap-1] && *mapheaderinfo[gamemap-1]->lvlttl)
+	{
+		char *read = mapheaderinfo[gamemap-1]->lvlttl, *writ = netbuffer->u.serverinfo.maptitle;
+		while (writ < (netbuffer->u.serverinfo.maptitle+32) && *read != '\0')
+		{
+			if (!(*read & 0x80))
+			{
+				*writ = toupper(*read);
+				writ++;
+			}
+			read++;
+		}
+		*writ = '\0';
+		//strncpy(netbuffer->u.serverinfo.maptitle, (char *)mapheaderinfo[gamemap-1]->lvlttl, 33);
+	}
+	else
+		strncpy(netbuffer->u.serverinfo.maptitle, "UNKNOWN", 32);
+
+	if (mapheaderinfo[gamemap-1] && !(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
+		netbuffer->u.serverinfo.iszone = 1;
+	else
+		netbuffer->u.serverinfo.iszone = 0;
+
+	if (mapheaderinfo[gamemap-1])
+		netbuffer->u.serverinfo.actnum = mapheaderinfo[gamemap-1]->actnum;
+
+	p = PutFileNeeded(0);
+
+	HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
+}
+
+static void SV_SendPlayerInfo(INT32 node)
+{
+	netbuffer->packettype = PT_PLAYERINFO;
+
+	for (UINT8 i = 0; i < MAXPLAYERS; i++)
+	{
+		if (playernode[i] == UINT8_MAX || !netnodes[playernode[i]].ingame)
+		{
+			netbuffer->u.playerinfo[i].num = 255; // This slot is empty.
+			continue;
+		}
+
+		netbuffer->u.playerinfo[i].num = i;
+		strncpy(netbuffer->u.playerinfo[i].name, (const char *)&player_names[i], MAXPLAYERNAME+1);
+		netbuffer->u.playerinfo[i].name[MAXPLAYERNAME] = '\0';
+
+		//fetch IP address
+		//No, don't do that, you fuckface.
+		memset(netbuffer->u.playerinfo[i].address, 0, 4);
+
+		if (G_GametypeHasTeams())
+		{
+			if (!players[i].ctfteam)
+				netbuffer->u.playerinfo[i].team = 255;
+			else
+				netbuffer->u.playerinfo[i].team = (UINT8)players[i].ctfteam;
+		}
+		else
+		{
+			if (players[i].spectator)
+				netbuffer->u.playerinfo[i].team = 255;
+			else
+				netbuffer->u.playerinfo[i].team = 0;
+		}
+
+		netbuffer->u.playerinfo[i].score = LONG(players[i].score);
+		netbuffer->u.playerinfo[i].timeinserver = SHORT((UINT16)(players[i].jointime / TICRATE));
+		netbuffer->u.playerinfo[i].skin = (UINT8)(players[i].skin
+#ifdef DEVELOP // it's safe to do this only because PLAYERINFO isn't read by the game itself
+		% 3
+#endif
+		);
+
+		// Extra data
+		netbuffer->u.playerinfo[i].data = 0; //players[i].skincolor;
+
+		if (players[i].pflags & PF_TAGIT)
+			netbuffer->u.playerinfo[i].data |= 0x20;
+
+		if (players[i].gotflag)
+			netbuffer->u.playerinfo[i].data |= 0x40;
+
+		if (players[i].powers[pw_super])
+			netbuffer->u.playerinfo[i].data |= 0x80;
+	}
+
+	HSendPacket(node, false, 0, sizeof(plrinfo_pak) * MAXPLAYERS);
+}
+
+/** Sends a PT_SERVERCFG packet
+  *
+  * \param node The destination
+  * \return True if the packet was successfully sent
+  *
+  */
+static boolean SV_SendServerConfig(INT32 node)
+{
+	boolean waspacketsent;
+
+	netbuffer->packettype = PT_SERVERCFG;
+
+	netbuffer->u.servercfg.serverplayer = (UINT8)serverplayer;
+	netbuffer->u.servercfg.totalslotnum = (UINT8)(doomcom->numslots);
+	netbuffer->u.servercfg.gametic = (tic_t)LONG(gametic);
+	netbuffer->u.servercfg.clientnode = (UINT8)node;
+	netbuffer->u.servercfg.gamestate = (UINT8)gamestate;
+	netbuffer->u.servercfg.gametype = (UINT8)gametype;
+	netbuffer->u.servercfg.modifiedgame = (UINT8)modifiedgame;
+	netbuffer->u.servercfg.usedCheats = (UINT8)usedCheats;
+
+	memcpy(netbuffer->u.servercfg.server_context, server_context, 8);
+
+	{
+		const size_t len = sizeof (serverconfig_pak);
+
+#ifdef DEBUGFILE
+		if (debugfile)
+		{
+			fprintf(debugfile, "ServerConfig Packet about to be sent, size of packet:%s to node:%d\n",
+				sizeu1(len), node);
+		}
+#endif
+
+		waspacketsent = HSendPacket(node, true, 0, len);
+	}
+
+#ifdef DEBUGFILE
+	if (debugfile)
+	{
+		if (waspacketsent)
+		{
+			fprintf(debugfile, "ServerConfig Packet was sent\n");
+		}
+		else
+		{
+			fprintf(debugfile, "ServerConfig Packet could not be sent right now\n");
+		}
+	}
+#endif
+
+	return waspacketsent;
+}
+
+// Adds a node to the game (player will follow at map change or at savegame....)
+static inline void SV_AddNode(INT32 node)
+{
+	netnodes[node].tic = gametic;
+	netnodes[node].supposedtic = gametic;
+	// little hack because the server connects to itself and puts
+	// nodeingame when connected not here
+	if (node)
+		netnodes[node].ingame = true;
+}
+
+static void SV_AddPlayer(SINT8 node, const char *name)
+{
+	INT32 n;
+	UINT8 buf[2 + MAXPLAYERNAME];
+	UINT8 *p;
+	INT32 newplayernum;
+
+	newplayernum = FindRejoinerNum(node);
+	if (newplayernum == -1)
+	{
+		// search for a free playernum
+		// we can't use playeringame since it is not updated here
+		for (newplayernum = dedicated ? 1 : 0; newplayernum < MAXPLAYERS; newplayernum++)
+		{
+			if (playeringame[newplayernum])
+				continue;
+			for (n = 0; n < MAXNETNODES; n++)
+				if (netnodes[n].player == newplayernum || netnodes[n].player2 == newplayernum)
+					break;
+			if (n == MAXNETNODES)
+				break;
+		}
+	}
+
+	// should never happen since we check the playernum
+	// before accepting the join
+	I_Assert(newplayernum < MAXPLAYERS);
+
+	playernode[newplayernum] = (UINT8)node;
+
+	p = buf + 2;
+	buf[0] = (UINT8)node;
+	buf[1] = newplayernum;
+	if (netnodes[node].numplayers < 1)
+	{
+		netnodes[node].player = newplayernum;
+	}
+	else
+	{
+		netnodes[node].player2 = newplayernum;
+		buf[1] |= 0x80;
+	}
+	WRITESTRINGN(p, name, MAXPLAYERNAME);
+	netnodes[node].numplayers++;
+
+	SendNetXCmd(XD_ADDPLAYER, &buf, p - buf);
+
+	DEBFILE(va("Server added player %d node %d\n", newplayernum, node));
+}
+
+static void SV_SendRefuse(INT32 node, const char *reason)
+{
+	strcpy(netbuffer->u.serverrefuse.reason, reason);
+
+	netbuffer->packettype = PT_SERVERREFUSE;
+	HSendPacket(node, true, 0, strlen(netbuffer->u.serverrefuse.reason) + 1);
+	Net_CloseConnection(node);
+}
+
+static const char *
+GetRefuseMessage (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
+  *
+  */
+void PT_ClientJoin(SINT8 node)
+{
+	char names[MAXSPLITSCREENPLAYERS][MAXPLAYERNAME + 1];
+	INT32 numplayers = netbuffer->u.clientcfg.localplayers;
+	INT32 rejoinernum;
+
+	// Ignore duplicate packets
+	if (client || netnodes[node].ingame)
+		return;
+
+	rejoinernum = FindRejoinerNum(node);
+
+	const char *refuse = GetRefuseMessage(node, rejoinernum);
+	if (refuse)
+	{
+		SV_SendRefuse(node, refuse);
+		return;
+	}
+
+	for (INT32 i = 0; i < numplayers; i++)
+	{
+		strlcpy(names[i], netbuffer->u.clientcfg.names[i], MAXPLAYERNAME + 1);
+		if (!EnsurePlayerNameIsGood(names[i], rejoinernum))
+		{
+			SV_SendRefuse(node, "Bad player name");
+			return;
+		}
+	}
+
+	SV_AddNode(node);
+
+	if (!SV_SendServerConfig(node))
+	{
+		/// \note Shouldn't SV_SendRefuse be called before ResetNode?
+		ResetNode(node);
+		SV_SendRefuse(node, M_GetText("Server couldn't send info, please try again"));
+		/// \todo fix this !!!
+		return;
+	}
+	DEBFILE("new node joined\n");
+
+	if (gamestate == GS_LEVEL || gamestate == GS_INTERMISSION)
+	{
+		SV_SendSaveGame(node, false); // send a complete game state
+		DEBFILE("send savegame\n");
+	}
+
+	// Splitscreen can allow 2 players in one node
+	for (INT32 i = 0; i < numplayers; i++)
+		SV_AddPlayer(node, names[i]);
+
+	joindelay += cv_joindelay.value * TICRATE;
+}
+
+void PT_AskInfoViaMS(SINT8 node)
+{
+	Net_CloseConnection(node);
+}
+
+void PT_TellFilesNeeded(SINT8 node)
+{
+	if (server && serverrunning)
+	{
+		UINT8 *p;
+		INT32 firstfile = netbuffer->u.filesneedednum;
+
+		netbuffer->packettype = PT_MOREFILESNEEDED;
+		netbuffer->u.filesneededcfg.first = firstfile;
+		netbuffer->u.filesneededcfg.more = 0;
+
+		p = PutFileNeeded(firstfile);
+
+		HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
+	}
+	else // Shouldn't get this if you aren't the server...?
+		Net_CloseConnection(node);
+}
+
+void PT_AskInfo(SINT8 node)
+{
+	if (server && serverrunning)
+	{
+		SV_SendServerInfo(node, (tic_t)LONG(netbuffer->u.askinfo.time));
+		SV_SendPlayerInfo(node); // Send extra info
+	}
+	Net_CloseConnection(node);
+}
diff --git a/src/netcode/server_connection.h b/src/netcode/server_connection.h
new file mode 100644
index 0000000000000000000000000000000000000000..14ac5913c99eac0da9fb0ab19018c3eadd7db189
--- /dev/null
+++ b/src/netcode/server_connection.h
@@ -0,0 +1,30 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 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  server_connection.h
+/// \brief Server-side part of connection handling
+
+#ifndef __D_SERVER_CONNECTION__
+#define __D_SERVER_CONNECTION__
+
+#include "../command.h"
+#include "../doomdef.h"
+#include "../doomtype.h"
+
+void PT_ClientJoin(SINT8 node);
+void PT_AskInfoViaMS(SINT8 node);
+void PT_TellFilesNeeded(SINT8 node);
+void PT_AskInfo(SINT8 node);
+
+extern tic_t jointimeout;
+extern tic_t joindelay;
+extern char playeraddress[MAXPLAYERS][64];
+extern consvar_t cv_showjoinaddress, cv_allownewplayer, cv_maxplayers, cv_joindelay, cv_rejointimeout;
+
+#endif
diff --git a/src/netcode/tic_command.c b/src/netcode/tic_command.c
new file mode 100644
index 0000000000000000000000000000000000000000..7721bc3f1b1149790ce4de35ba1f1a7aa69f00c5
--- /dev/null
+++ b/src/netcode/tic_command.c
@@ -0,0 +1,471 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 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  tic_command.c
+/// \brief Tic command handling
+
+#include "tic_command.h"
+#include "d_clisrv.h"
+#include "net_command.h"
+#include "client_connection.h"
+#include "gamestate.h"
+#include "i_net.h"
+#include "../d_main.h"
+#include "../g_game.h"
+#include "../i_system.h"
+#include "../i_time.h"
+#include "../byteptr.h"
+#include "../doomstat.h"
+#include "../doomtype.h"
+
+tic_t firstticstosend; // Smallest netnode.tic
+tic_t tictoclear = 0; // Optimize D_ClearTiccmd
+ticcmd_t localcmds;
+ticcmd_t localcmds2;
+boolean cl_packetmissed;
+ticcmd_t netcmds[BACKUPTICS][MAXPLAYERS];
+
+static inline void *G_DcpyTiccmd(void* dest, const ticcmd_t* src, const size_t n)
+{
+	const size_t d = n / sizeof(ticcmd_t);
+	const size_t r = n % sizeof(ticcmd_t);
+	UINT8 *ret = dest;
+
+	if (r)
+		M_Memcpy(dest, src, n);
+	else if (d)
+		G_MoveTiccmd(dest, src, d);
+	return ret+n;
+}
+
+static inline void *G_ScpyTiccmd(ticcmd_t* dest, void* src, const size_t n)
+{
+	const size_t d = n / sizeof(ticcmd_t);
+	const size_t r = n % sizeof(ticcmd_t);
+	UINT8 *ret = src;
+
+	if (r)
+		M_Memcpy(dest, src, n);
+	else if (d)
+		G_MoveTiccmd(dest, src, d);
+	return ret+n;
+}
+
+/** Guesses the full value of a tic from its lowest byte, for a specific node
+  *
+  * \param low The lowest byte of the tic value
+  * \param node The node to deduce the tic for
+  * \return The full tic value
+  *
+  */
+tic_t ExpandTics(INT32 low, INT32 node)
+{
+	INT32 delta;
+
+	delta = low - (netnodes[node].tic & UINT8_MAX);
+
+	if (delta >= -64 && delta <= 64)
+		return (netnodes[node].tic & ~UINT8_MAX) + low;
+	else if (delta > 64)
+		return (netnodes[node].tic & ~UINT8_MAX) - 256 + low;
+	else //if (delta < -64)
+		return (netnodes[node].tic & ~UINT8_MAX) + 256 + low;
+}
+
+void D_Clearticcmd(tic_t tic)
+{
+	D_FreeTextcmd(tic);
+
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+		netcmds[tic%BACKUPTICS][i].angleturn = 0;
+
+	DEBFILE(va("clear tic %5u (%2u)\n", tic, tic%BACKUPTICS));
+}
+
+void D_ResetTiccmds(void)
+{
+	memset(&localcmds, 0, sizeof(ticcmd_t));
+	memset(&localcmds2, 0, sizeof(ticcmd_t));
+
+	// Reset the net command list
+	for (INT32 i = 0; i < TEXTCMD_HASH_SIZE; i++)
+		while (textcmds[i])
+			D_Clearticcmd(textcmds[i]->tic);
+}
+
+// Check ticcmd for "speed hacks"
+static void CheckTiccmdHacks(INT32 playernum, tic_t tic)
+{
+	ticcmd_t *cmd = &netcmds[tic%BACKUPTICS][playernum];
+	if (cmd->forwardmove > MAXPLMOVE || cmd->forwardmove < -MAXPLMOVE
+		|| cmd->sidemove > MAXPLMOVE || cmd->sidemove < -MAXPLMOVE)
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("Illegal movement value received from node %d\n"), playernum);
+		SendKick(playernum, KICK_MSG_CON_FAIL);
+	}
+}
+
+// Check player consistancy during the level
+static void CheckConsistancy(SINT8 nodenum, tic_t tic)
+{
+	netnode_t *node = &netnodes[nodenum];
+	INT16 neededconsistancy = consistancy[tic%BACKUPTICS];
+	INT16 clientconsistancy = SHORT(netbuffer->u.clientpak.consistancy);
+
+	if (tic > gametic || tic + BACKUPTICS - 1 <= gametic || gamestate != GS_LEVEL
+		|| neededconsistancy == clientconsistancy || SV_ResendingSavegameToAnyone()
+		|| node->resendingsavegame || node->savegameresendcooldown > I_GetTime())
+		return;
+
+	if (cv_resynchattempts.value)
+	{
+		// Tell the client we are about to resend them the gamestate
+		netbuffer->packettype = PT_WILLRESENDGAMESTATE;
+		HSendPacket(nodenum, true, 0, 0);
+
+		node->resendingsavegame = true;
+
+		if (cv_blamecfail.value)
+			CONS_Printf(M_GetText("Synch failure for player %d (%s); expected %hd, got %hd\n"),
+				node->player+1, player_names[node->player],
+				neededconsistancy, clientconsistancy);
+
+		DEBFILE(va("Restoring player %d (synch failure) [%update] %d!=%d\n",
+			node->player, tic, neededconsistancy, clientconsistancy));
+	}
+	else
+	{
+		SendKick(node->player, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
+
+		DEBFILE(va("player %d kicked (synch failure) [%u] %d!=%d\n",
+			node->player, tic, neededconsistancy, clientconsistancy));
+	}
+}
+
+void PT_ClientCmd(SINT8 nodenum, INT32 netconsole)
+{
+	netnode_t *node = &netnodes[nodenum];
+	tic_t realend, realstart;
+
+	if (client)
+		return;
+
+	// To save bytes, only the low byte of tic numbers are sent
+	// Use ExpandTics to figure out what the rest of the bytes are
+	realstart = ExpandTics(netbuffer->u.clientpak.client_tic, nodenum);
+	realend = ExpandTics(netbuffer->u.clientpak.resendfrom, nodenum);
+
+	if (netbuffer->packettype == PT_CLIENTMIS || netbuffer->packettype == PT_CLIENT2MIS
+		|| netbuffer->packettype == PT_NODEKEEPALIVEMIS
+		|| node->supposedtic < realend)
+	{
+		node->supposedtic = realend;
+	}
+	// Discard out of order packet
+	if (node->tic > realend)
+	{
+		DEBFILE(va("out of order ticcmd discarded nettics = %u\n", node->tic));
+		return;
+	}
+
+	// Update the nettics
+	node->tic = realend;
+
+	// This should probably still timeout though, as the node should always have a player 1 number
+	if (netconsole == -1)
+		return;
+
+	// As long as clients send valid ticcmds, the server can keep running, so reset the timeout
+	/// \todo Use a separate cvar for that kind of timeout?
+	node->freezetimeout = I_GetTime() + connectiontimeout;
+
+	// Don't do anything for packets of type NODEKEEPALIVE?
+	// Sryder 2018/07/01: Update the freezetimeout still!
+	if (netbuffer->packettype == PT_NODEKEEPALIVE
+		|| netbuffer->packettype == PT_NODEKEEPALIVEMIS)
+		return;
+
+	// If we've alredy received a ticcmd for this tic, just submit it for the next one.
+	tic_t faketic = maketic;
+	if ((!!(netcmds[maketic % BACKUPTICS][netconsole].angleturn & TICCMD_RECEIVED))
+		&& (maketic - firstticstosend < BACKUPTICS - 1))
+		faketic++;
+
+	// Copy ticcmd
+	G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][netconsole], &netbuffer->u.clientpak.cmd, 1);
+
+	// Splitscreen cmd
+	if ((netbuffer->packettype == PT_CLIENT2CMD || netbuffer->packettype == PT_CLIENT2MIS)
+		&& node->player2 >= 0)
+		G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][(UINT8)node->player2],
+			&netbuffer->u.client2pak.cmd2, 1);
+
+	CheckTiccmdHacks(netconsole, faketic);
+	CheckConsistancy(nodenum, realstart);
+}
+
+void PT_ServerTics(SINT8 node, INT32 netconsole)
+{
+	tic_t realend, realstart;
+	servertics_pak *packet = &netbuffer->u.serverpak;
+
+	if (!netnodes[node].ingame)
+	{
+		// Do not remove my own server (we have just get a out of order packet)
+		if (node != servernode)
+		{
+			DEBFILE(va("unknown packet received (%d) from unknown host\n",netbuffer->packettype));
+			Net_CloseConnection(node);
+		}
+		return;
+	}
+
+	// Only accept PT_SERVERTICS from the server.
+	if (node != servernode)
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_SERVERTICS", node);
+		if (server)
+			SendKick(netconsole, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
+		return;
+	}
+
+	realstart = packet->starttic;
+	realend = realstart + packet->numtics;
+
+	realend = min(realend, gametic + CLIENTBACKUPTICS);
+	cl_packetmissed = realstart > neededtic;
+
+	if (realstart <= neededtic && realend > neededtic)
+	{
+		UINT8 *pak = (UINT8 *)&packet->cmds;
+		UINT8 *txtpak = (UINT8 *)&packet->cmds[packet->numslots * packet->numtics];
+
+		for (tic_t i = realstart; i < realend; i++)
+		{
+			// clear first
+			D_Clearticcmd(i);
+
+			// copy the tics
+			pak = G_ScpyTiccmd(netcmds[i%BACKUPTICS], pak,
+				packet->numslots*sizeof (ticcmd_t));
+
+			CL_CopyNetCommandsFromServerPacket(i, &txtpak);
+		}
+
+		neededtic = realend;
+	}
+	else
+	{
+		DEBFILE(va("frame not in bound: %u\n", neededtic));
+	}
+}
+
+// send the client packet to the server
+void CL_SendClientCmd(void)
+{
+	size_t packetsize = 0;
+	boolean mis = false;
+
+	netbuffer->packettype = PT_CLIENTCMD;
+
+	if (cl_packetmissed)
+	{
+		netbuffer->packettype = PT_CLIENTMIS;
+		mis = true;
+	}
+
+	netbuffer->u.clientpak.resendfrom = (UINT8)(neededtic & UINT8_MAX);
+	netbuffer->u.clientpak.client_tic = (UINT8)(gametic & UINT8_MAX);
+
+	if (gamestate == GS_WAITINGPLAYERS)
+	{
+		// Send PT_NODEKEEPALIVE packet
+		netbuffer->packettype = (mis ? PT_NODEKEEPALIVEMIS : PT_NODEKEEPALIVE);
+		packetsize = sizeof (clientcmd_pak) - sizeof (ticcmd_t) - sizeof (INT16);
+		HSendPacket(servernode, false, 0, packetsize);
+	}
+	else if (gamestate != GS_NULL && (addedtogame || dedicated))
+	{
+		packetsize = sizeof (clientcmd_pak);
+		G_MoveTiccmd(&netbuffer->u.clientpak.cmd, &localcmds, 1);
+		netbuffer->u.clientpak.consistancy = SHORT(consistancy[gametic%BACKUPTICS]);
+
+		// Send a special packet with 2 cmd for splitscreen
+		if (splitscreen || botingame)
+		{
+			netbuffer->packettype = (mis ? PT_CLIENT2MIS : PT_CLIENT2CMD);
+			packetsize = sizeof (client2cmd_pak);
+			G_MoveTiccmd(&netbuffer->u.client2pak.cmd2, &localcmds2, 1);
+		}
+
+		HSendPacket(servernode, false, 0, packetsize);
+	}
+
+	if (cl_mode == CL_CONNECTED || dedicated)
+		CL_SendNetCommands();
+}
+
+// PT_SERVERTICS packets can grow too large for a single UDP packet,
+// So this checks how many tics worth of data can be sent in one packet.
+// The rest can be sent later, usually the next tic.
+static tic_t SV_CalculateNumTicsForPacket(SINT8 nodenum, tic_t firsttic, tic_t lasttic)
+{
+	size_t size = BASESERVERTICSSIZE;
+
+	for (tic_t tic = firsttic; tic < lasttic; tic++)
+	{
+		size += sizeof (ticcmd_t) * doomcom->numslots;
+		size += TotalTextCmdPerTic(tic);
+
+		if (size > software_MAXPACKETLENGTH)
+		{
+			DEBFILE(va("packet too large (%s) at tic %d (should be from %d to %d)\n",
+				sizeu1(size), tic, firsttic, lasttic));
+			lasttic = tic;
+
+			// Too bad: too many players have sent extra data
+			// and there is too much data for a single tic.
+			// To avoid that, keep the data for the next tic (see PT_TEXTCMD).
+			if (lasttic == firsttic)
+			{
+				if (size > MAXPACKETLENGTH)
+					I_Error("Too many players: can't send %s data for %d players to node %d\n"
+							"Well sorry nobody is perfect....\n",
+							sizeu1(size), doomcom->numslots, nodenum);
+				else
+				{
+					lasttic++; // send it anyway!
+					DEBFILE("sending it anyway\n");
+				}
+			}
+			break;
+		}
+	}
+
+	return lasttic - firsttic;
+}
+
+// Sends the server packet
+// Sends tic/net commands from firstticstosend to maketic-1
+void SV_SendTics(void)
+{
+	tic_t realfirsttic, lasttictosend;
+
+	// Send to all clients except yourself
+	// For each node, create a packet with x tics and send it
+	// x is computed using node.supposedtic, max packet size and maketic
+	for (INT32 n = 1; n < MAXNETNODES; n++)
+		if (netnodes[n].ingame)
+		{
+			netnode_t *node = &netnodes[n];
+
+			// assert node->supposedtic>=node->tic
+			realfirsttic = node->supposedtic;
+			lasttictosend = min(maketic, node->tic + CLIENTBACKUPTICS);
+
+			if (realfirsttic >= lasttictosend)
+			{
+				// Well, we have sent all the tics, so we will use extra bandwidth
+				// to resend packets that are supposed lost.
+				// This is necessary since lost packet detection
+				// works when we receive a packet with firsttic > neededtic (PT_SERVERTICS)
+				DEBFILE(va("Nothing to send node %u mak=%u sup=%u net=%u \n",
+					n, maketic, node->supposedtic, node->tic));
+
+				realfirsttic = node->tic;
+
+				if (realfirsttic >= lasttictosend || (I_GetTime() + n)&3)
+					// All tics are Ok
+					continue;
+
+				DEBFILE(va("Sent %d anyway\n", realfirsttic));
+			}
+			realfirsttic = max(realfirsttic, firstticstosend);
+
+			lasttictosend = realfirsttic + SV_CalculateNumTicsForPacket(n, realfirsttic, lasttictosend);
+
+			// Prepare the packet header
+			netbuffer->packettype = PT_SERVERTICS;
+			netbuffer->u.serverpak.starttic = realfirsttic;
+			netbuffer->u.serverpak.numtics = (UINT8)(lasttictosend - realfirsttic);
+			netbuffer->u.serverpak.numslots = (UINT8)SHORT(doomcom->numslots);
+
+			// Fill and send the packet
+			UINT8 *bufpos = (UINT8 *)&netbuffer->u.serverpak.cmds;
+			for (tic_t i = realfirsttic; i < lasttictosend; i++)
+				bufpos = G_DcpyTiccmd(bufpos, netcmds[i%BACKUPTICS], doomcom->numslots * sizeof (ticcmd_t));
+			for (tic_t i = realfirsttic; i < lasttictosend; i++)
+				SV_WriteNetCommandsForTic(i, &bufpos);
+			size_t packsize = bufpos - (UINT8 *)&(netbuffer->u);
+			HSendPacket(n, false, 0, packsize);
+
+			// When tics are too large, only one tic is sent so don't go backwards!
+			if (lasttictosend-doomcom->extratics > realfirsttic)
+				node->supposedtic = lasttictosend-doomcom->extratics;
+			else
+				node->supposedtic = lasttictosend;
+			node->supposedtic = max(node->supposedtic, node->tic);
+		}
+
+	// node 0 is me!
+	netnodes[0].supposedtic = maketic;
+}
+
+void Local_Maketic(INT32 realtics)
+{
+	I_OsPolling(); // I_Getevent
+	D_ProcessEvents(); // menu responder, cons responder,
+	                   // game responder calls HU_Responder, AM_Responder,
+	                   // and G_MapEventsToControls
+	if (!dedicated)
+		rendergametic = gametic;
+	// translate inputs (keyboard/mouse/joystick) into game controls
+	G_BuildTiccmd(&localcmds, realtics, 1);
+	if (splitscreen || botingame)
+		G_BuildTiccmd(&localcmds2, realtics, 2);
+
+	localcmds.angleturn |= TICCMD_RECEIVED;
+	localcmds2.angleturn |= TICCMD_RECEIVED;
+}
+
+// create missed tic
+void SV_Maketic(void)
+{
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!playeringame[i])
+			continue;
+
+		// We didn't receive this tic
+		if ((netcmds[maketic % BACKUPTICS][i].angleturn & TICCMD_RECEIVED) == 0)
+		{
+			ticcmd_t *    ticcmd = &netcmds[(maketic    ) % BACKUPTICS][i];
+			ticcmd_t *prevticcmd = &netcmds[(maketic - 1) % BACKUPTICS][i];
+
+			if (players[i].quittime)
+			{
+				// Copy the angle/aiming from the previous tic
+				// and empty the other inputs
+				memset(ticcmd, 0, sizeof(netcmds[0][0]));
+				ticcmd->angleturn = prevticcmd->angleturn | TICCMD_RECEIVED;
+				ticcmd->aiming = prevticcmd->aiming;
+			}
+			else
+			{
+				DEBFILE(va("MISS tic%4d for player %d\n", maketic, i));
+				// Copy the input from the previous tic
+				*ticcmd = *prevticcmd;
+				ticcmd->angleturn &= ~TICCMD_RECEIVED;
+			}
+		}
+	}
+
+	// All tics have been processed, make the next
+	maketic++;
+}
diff --git a/src/netcode/tic_command.h b/src/netcode/tic_command.h
new file mode 100644
index 0000000000000000000000000000000000000000..725037216dbe475183a37961873fdc18f41e4de4
--- /dev/null
+++ b/src/netcode/tic_command.h
@@ -0,0 +1,44 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 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  tic_command.h
+/// \brief Tic command handling
+
+#ifndef __D_TIC_COMMAND__
+#define __D_TIC_COMMAND__
+
+#include "d_clisrv.h"
+#include "../d_ticcmd.h"
+#include "../doomdef.h"
+#include "../doomtype.h"
+
+extern tic_t firstticstosend; // min of the nettics
+extern tic_t tictoclear; // optimize d_clearticcmd
+
+extern ticcmd_t localcmds;
+extern ticcmd_t localcmds2;
+extern boolean cl_packetmissed;
+
+extern ticcmd_t netcmds[BACKUPTICS][MAXPLAYERS];
+
+tic_t ExpandTics(INT32 low, INT32 node);
+void D_Clearticcmd(tic_t tic);
+void D_ResetTiccmds(void);
+
+void PT_ClientCmd(SINT8 nodenum, INT32 netconsole);
+void PT_ServerTics(SINT8 node, INT32 netconsole);
+
+// send the client packet to the server
+void CL_SendClientCmd(void);
+
+void SV_SendTics(void);
+void Local_Maketic(INT32 realtics);
+void SV_Maketic(void);
+
+#endif
diff --git a/src/p_ceilng.c b/src/p_ceilng.c
index 98e9313625268fedd13a865038c4fc895692cdd6..61430d73f1599ff411639938f8d90f2b07fbae3e 100644
--- a/src/p_ceilng.c
+++ b/src/p_ceilng.c
@@ -17,7 +17,7 @@
 #include "r_main.h"
 #include "s_sound.h"
 #include "z_zone.h"
-#include "d_netcmd.h"
+#include "netcode/d_netcmd.h"
 
 // ==========================================================================
 //                              CEILINGS
diff --git a/src/p_enemy.c b/src/p_enemy.c
index eebb65f3cb146f229183b014acdc89e0042f8da2..819f0dc1d992f1251def75c6a82eb687d7c5e69c 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -1201,7 +1201,8 @@ static void P_SharpDust(mobj_t *actor, mobjtype_t type, angle_t ang)
 			-P_ReturnThrustX(actor, ang, 16<<FRACBITS),
 			-P_ReturnThrustY(actor, ang, 16<<FRACBITS),
 			0, type);
-	P_SetObjectMomZ(dust, P_RandomRange(1, 4)<<FRACBITS, false);
+	if (!P_MobjWasRemoved(dust))
+		P_SetObjectMomZ(dust, P_RandomRange(1, 4)<<FRACBITS, false);
 }
 
 static void P_FaceStabFlume(mobj_t *actor)
@@ -1215,6 +1216,9 @@ static void P_FaceStabFlume(mobj_t *actor)
 		-P_ReturnThrustY(actor, actor->angle, actor->radius),
 		actor->height/3,
 		MT_PARTICLE);
+	if (P_MobjWasRemoved(flume))
+		return;
+
 	flume->destscale = actor->scale*3;
 	P_SetScale(flume, flume->destscale);
 	P_SetTarget(&flume->target, actor);
@@ -1329,12 +1333,15 @@ void A_FaceStabHurl(mobj_t *actor)
 				{
 					if (!hwork->hnext)
 						P_SetTarget(&hwork->hnext, P_SpawnMobjFromMobj(actor, 0, 0, 0, MT_FACESTABBERSPEAR));
-					hwork = hwork->hnext;
-					hwork->angle = actor->angle + ANGLE_90;
-					hwork->destscale = FixedSqrt(step*basesize);
-					P_SetScale(hwork, hwork->destscale);
-					hwork->fuse = 2;
-					P_MoveOrigin(hwork, actor->x + xo*(15-step), actor->y + yo*(15-step), actor->z + (actor->height - hwork->height)/2 + (P_MobjFlip(actor)*(8<<FRACBITS)));
+					if (!P_MobjWasRemoved(hwork->hnext))
+					{
+						hwork = hwork->hnext;
+						hwork->angle = actor->angle + ANGLE_90;
+						hwork->destscale = FixedSqrt(step*basesize);
+						P_SetScale(hwork, hwork->destscale);
+						hwork->fuse = 2;
+						P_MoveOrigin(hwork, actor->x + xo*(15-step), actor->y + yo*(15-step), actor->z + (actor->height - hwork->height)/2 + (P_MobjFlip(actor)*(8<<FRACBITS)));
+					}
 					step -= NUMGRADS;
 				}
 
@@ -1438,6 +1445,8 @@ void A_StatueBurst(mobj_t *actor)
 			}
 
 			spawned = P_SpawnMobjFromMobj(actor, a, b, c, chunktype);
+			if (P_MobjWasRemoved(spawned))
+				continue;
 
 			P_InstaThrust(spawned, R_PointToAngle2(0, 0, a, b), 8<<FRACBITS);
 			P_SetObjectMomZ(spawned, v, false);
@@ -2165,6 +2174,8 @@ void A_CrushclawLaunch(mobj_t *actor)
 		for (i = 0; (i < CSEGS); i++)
 		{
 			mobj_t *newchain = P_SpawnMobjFromMobj(actor, 0, 0, 0, (mobjtype_t)actor->info->raisestate);
+			if (P_MobjWasRemoved(newchain))
+				continue;
 			P_SetTarget(&prevchain->target, newchain);
 			prevchain = newchain;
 		}
@@ -2335,10 +2346,13 @@ static void P_VultureHoverParticle(mobj_t *actor)
 			fixed_t pz = P_FloorzAtPos(px, py, actor->z, actor->height);
 
 			dust = P_SpawnMobj(px, py, pz, MT_ARIDDUST);
-			P_SetMobjState(dust, (statenum_t)(dust->state - states + P_RandomRange(0, 2)));
-			P_Thrust(dust, angle, FixedDiv(12*FRACUNIT, max(FRACUNIT, fdist/2)));
-			dust->momx += actor->momx;
-			dust->momy += actor->momy;
+			if (!P_MobjWasRemoved(dust))
+			{
+				P_SetMobjState(dust, (statenum_t)(dust->state - states + P_RandomRange(0, 2)));
+				P_Thrust(dust, angle, FixedDiv(12*FRACUNIT, max(FRACUNIT, fdist/2)));
+				dust->momx += actor->momx;
+				dust->momy += actor->momy;
+			}
 			angle += ANGLE_45;
 		}
 	}
@@ -2436,6 +2450,8 @@ void A_VultureBlast(mobj_t *actor)
 	{
 		angle_t fa = ((i*(angle_t)ANGLE_45) >> ANGLETOFINESHIFT) & FINEMASK;
 		dust = P_SpawnMobj(actor->x + 48*FixedMul(FINECOSINE(fa), -faasin), actor->y + 48*FixedMul(FINECOSINE(fa), faacos), actor->z + actor->height/2 + 48*FINESINE(fa), MT_PARTICLE);
+		if (P_MobjWasRemoved(dust))
+			continue;
 
 		P_SetScale(dust, 4*FRACUNIT);
 		dust->destscale = FRACUNIT;
@@ -2505,10 +2521,13 @@ void A_VultureFly(mobj_t *actor)
 	P_VultureHoverParticle(actor);
 
 	dust = P_SpawnMobj(actor->x + P_RandomFixed() - FRACUNIT/2, actor->y + P_RandomFixed() - FRACUNIT/2, actor->z + actor->height/2 + P_RandomFixed() - FRACUNIT/2, MT_PARTICLE);
-	P_SetScale(dust, 2*FRACUNIT);
-	dust->destscale = FRACUNIT/3;
-	dust->scalespeed = FRACUNIT/40;
-	dust->fuse = TICRATE*2;
+	if (!P_MobjWasRemoved(dust))
+	{
+		P_SetScale(dust, 2*FRACUNIT);
+		dust->destscale = FRACUNIT/3;
+		dust->scalespeed = FRACUNIT/40;
+		dust->fuse = TICRATE*2;
+	}
 
 	actor->momx += FixedDiv(dx, dm)*2;
 	actor->momy += FixedDiv(dy, dm)*2;
@@ -2709,6 +2728,8 @@ void A_LobShot(mobj_t *actor)
 		z = actor->z + FixedMul(locvar2*FRACUNIT, actor->scale);
 
 	shot = P_SpawnMobj(actor->x, actor->y, z, locvar1);
+	if (P_MobjWasRemoved(shot))
+		return;
 
 	if (actor->type == MT_BLACKEGGMAN)
 	{
@@ -3098,15 +3119,21 @@ void A_Boss1Laser(mobj_t *actor)
 			S_StartSound(actor, mobjinfo[locvar1].seesound);
 
 		point = P_SpawnMobj(x + P_ReturnThrustX(actor, actor->angle, actor->radius), y + P_ReturnThrustY(actor, actor->angle, actor->radius), actor->z - actor->height / 2, MT_EGGMOBILE_TARGET);
-		point->angle = actor->angle;
-		point->fuse = dur+1;
-		P_SetTarget(&point->target, actor->target);
-		P_SetTarget(&actor->target, point);
+		if (!P_MobjWasRemoved(point))
+		{
+			point->angle = actor->angle;
+			point->fuse = dur+1;
+			P_SetTarget(&point->target, actor->target);
+			P_SetTarget(&actor->target, point);
+		}
 	}
 
 	angle = R_PointToAngle2(z + (mobjinfo[locvar1].height>>1), 0, actor->target->z, R_PointToDist2(x, y, actor->target->x, actor->target->y));
 
 	point = P_SpawnMobj(x, y, z, locvar1);
+	if (P_MobjWasRemoved(point))
+		return;
+
 	P_SetTarget(&point->target, actor);
 	point->angle = actor->angle;
 	speed = point->radius;
@@ -3117,6 +3144,9 @@ void A_Boss1Laser(mobj_t *actor)
 	for (i = 0; i < 256; i++)
 	{
 		mobj_t *mo = P_SpawnMobj(point->x, point->y, point->z, point->type);
+		if (P_MobjWasRemoved(mo))
+			continue;
+
 		mo->angle = point->angle;
 		mo->color = LASERCOLORS[((UINT8)(i + 3*dur) >> 2) % sizeof(LASERCOLORS)]; // codeing
 		P_UnsetThingPosition(mo);
@@ -3129,9 +3159,12 @@ void A_Boss1Laser(mobj_t *actor)
 			if (mo->info->meleestate)
 			{
 				mobj_t *mo2 = P_SpawnMobjFromMobj(mo, 0, 0, 0, MT_PARTICLE);
-				mo2->flags2 |= MF2_LINKDRAW;
-				P_SetTarget(&mo2->tracer, actor);
-				P_SetMobjState(mo2, mo->info->meleestate);
+				if (!P_MobjWasRemoved(mo2))
+				{
+					mo2->flags2 |= MF2_LINKDRAW;
+					P_SetTarget(&mo2->tracer, actor);
+					P_SetMobjState(mo2, mo->info->meleestate);
+				}
 			}
 		}
 
@@ -3149,37 +3182,42 @@ void A_Boss1Laser(mobj_t *actor)
 	if (z - floorz < mobjinfo[MT_EGGMOBILE_FIRE].height>>1 && dur & 1)
 	{
 		point = P_SpawnMobj(x, y, floorz, MT_EGGMOBILE_FIRE);
-		point->angle = actor->angle;
-		point->destscale = actor->scale;
-		P_SetScale(point, point->destscale);
-		P_SetTarget(&point->target, actor);
-		P_MobjCheckWater(point);
-		if (point->eflags & (MFE_UNDERWATER|MFE_TOUCHWATER))
-		{
-			for (i = 0; i < 2; i++)
+		if (!P_MobjWasRemoved(point))
+		{
+			point->angle = actor->angle;
+			point->destscale = actor->scale;
+			P_SetScale(point, point->destscale);
+			P_SetTarget(&point->target, actor);
+			P_MobjCheckWater(point);
+			if (point->eflags & (MFE_UNDERWATER|MFE_TOUCHWATER))
 			{
-				UINT8 size = 3;
-				mobj_t *steam = P_SpawnMobj(x, y, point->watertop - size*mobjinfo[MT_DUST].height, MT_DUST);
-				P_SetScale(steam, size*actor->scale);
-				P_SetObjectMomZ(steam, FRACUNIT + 2*P_RandomFixed(), true);
-				P_InstaThrust(steam, FixedAngle(P_RandomKey(360)*FRACUNIT), 2*P_RandomFixed());
-				if (point->info->painsound)
-					S_StartSound(steam, point->info->painsound);
+				for (i = 0; i < 2; i++)
+				{
+					UINT8 size = 3;
+					mobj_t *steam = P_SpawnMobj(x, y, point->watertop - size*mobjinfo[MT_DUST].height, MT_DUST);
+					if (P_MobjWasRemoved(steam))
+						continue;
+					P_SetScale(steam, size*actor->scale);
+					P_SetObjectMomZ(steam, FRACUNIT + 2*P_RandomFixed(), true);
+					P_InstaThrust(steam, FixedAngle(P_RandomKey(360)*FRACUNIT), 2*P_RandomFixed());
+					if (point->info->painsound)
+						S_StartSound(steam, point->info->painsound);
+				}
 			}
-		}
-		else
-		{
-			fixed_t distx = P_ReturnThrustX(point, point->angle, point->radius);
-			fixed_t disty = P_ReturnThrustY(point, point->angle, point->radius);
-			if (P_TryMove(point, point->x + distx, point->y + disty, false) // prevents the sprite from clipping into the wall or dangling off ledges
-				&& P_TryMove(point, point->x - 2*distx, point->y - 2*disty, false)
-				&& P_TryMove(point, point->x + distx, point->y + disty, false))
+			else
 			{
-				if (point->info->seesound)
-					S_StartSound(point, point->info->seesound);
+				fixed_t distx = P_ReturnThrustX(point, point->angle, point->radius);
+				fixed_t disty = P_ReturnThrustY(point, point->angle, point->radius);
+				if (P_TryMove(point, point->x + distx, point->y + disty, false) // prevents the sprite from clipping into the wall or dangling off ledges
+					&& P_TryMove(point, point->x - 2*distx, point->y - 2*disty, false)
+					&& P_TryMove(point, point->x + distx, point->y + disty, false))
+				{
+					if (point->info->seesound)
+						S_StartSound(point, point->info->seesound);
+				}
+				else
+					P_RemoveMobj(point);
 			}
-			else
-				P_RemoveMobj(point);
 		}
 	}
 
@@ -3528,6 +3566,8 @@ void A_BossScream(mobj_t *actor)
 		z = actor->z + FixedMul((P_RandomByte()<<(FRACBITS-2)) - 8*FRACUNIT, actor->scale);
 
 	mo = P_SpawnMobj(x, y, z, explodetype);
+	if (P_MobjWasRemoved(mo))
+		return;
 	if (actor->eflags & MFE_VERTICALFLIP)
 		mo->flags2 |= MF2_OBJECTFLIP;
 	mo->destscale = actor->scale;
@@ -3654,6 +3694,9 @@ void A_1upThinker(mobj_t *actor)
 	if (!actor->tracer)
 	{
 		P_SetTarget(&actor->tracer, P_SpawnMobj(actor->x, actor->y, actor->z, MT_OVERLAY));
+		if (P_MobjWasRemoved(actor->tracer))
+			return;
+
 		P_SetTarget(&actor->tracer->target, actor);
 		actor->tracer->skin = &skins[players[closestplayer].skin]; // required here to prevent spr2 default showing stand for a single frame
 		P_SetMobjState(actor->tracer, actor->info->seestate);
@@ -3715,31 +3758,40 @@ void A_MonitorPop(mobj_t *actor)
 		return;
 	}
 
-	newmobj = P_SpawnMobjFromMobj(actor, 0, 0, 13*FRACUNIT, item);
-	P_SetTarget(&newmobj->target, actor->target); // Transfer target
-
 	if (item == MT_1UP_ICON)
 	{
 		if (actor->tracer) // Remove the old lives icon.
 			P_RemoveMobj(actor->tracer);
+	}
 
-		if (!newmobj->target
-		 || !newmobj->target->player
-		 || !newmobj->target->skin
-		 || ((skin_t *)newmobj->target->skin)->sprites[SPR2_LIFE].numframes == 0)
-			{} // No lives icon for this player, use the default.
-		else
-		{ // Spawn the lives icon.
-			mobj_t *livesico = P_SpawnMobjFromMobj(newmobj, 0, 0, 0, MT_OVERLAY);
-			P_SetTarget(&livesico->target, newmobj);
-			P_SetTarget(&newmobj->tracer, livesico);
+	newmobj = P_SpawnMobjFromMobj(actor, 0, 0, 13*FRACUNIT, item);
+	if (!P_MobjWasRemoved(newmobj))
+	{
+		P_SetTarget(&newmobj->target, actor->target); // Transfer target
+
+		if (item == MT_1UP_ICON)
+		{
+			if (!newmobj->target
+			 || !newmobj->target->player
+			 || !newmobj->target->skin
+			 || ((skin_t *)newmobj->target->skin)->sprites[SPR2_LIFE].numframes == 0)
+				{} // No lives icon for this player, use the default.
+			else
+			{ // Spawn the lives icon.
+				mobj_t *livesico = P_SpawnMobjFromMobj(newmobj, 0, 0, 0, MT_OVERLAY);
+				if (!P_MobjWasRemoved(livesico))
+				{
+					P_SetTarget(&livesico->target, newmobj);
+					P_SetTarget(&newmobj->tracer, livesico);
 
-			livesico->color = newmobj->target->player->mo->color;
-			livesico->skin = &skins[newmobj->target->player->skin];
-			P_SetMobjState(livesico, newmobj->info->seestate);
+					livesico->color = newmobj->target->player->mo->color;
+					livesico->skin = &skins[newmobj->target->player->skin];
+					P_SetMobjState(livesico, newmobj->info->seestate);
+				}
 
-			// We're using the overlay, so use the overlay 1up sprite (no text)
-			newmobj->sprite = SPR_TV1P;
+				// We're using the overlay, so use the overlay 1up sprite (no text)
+				newmobj->sprite = SPR_TV1P;
+			}
 		}
 	}
 
@@ -3802,30 +3854,36 @@ void A_GoldMonitorPop(mobj_t *actor)
 
 	// Note: the icon spawns 1 fracunit higher
 	newmobj = P_SpawnMobjFromMobj(actor, 0, 0, 14*FRACUNIT, item);
-	P_SetTarget(&newmobj->target, actor->target); // Transfer target
-
-	if (item == MT_1UP_ICON)
+	if (!P_MobjWasRemoved(newmobj))
 	{
-		if (actor->tracer) // Remove the old lives icon.
-			P_RemoveMobj(actor->tracer);
+		P_SetTarget(&newmobj->target, actor->target); // Transfer target
 
-		if (!newmobj->target
-		 || !newmobj->target->player
-		 || !newmobj->target->skin
-		 || ((skin_t *)newmobj->target->skin)->sprites[SPR2_LIFE].numframes == 0)
-			{} // No lives icon for this player, use the default.
-		else
-		{ // Spawn the lives icon.
-			mobj_t *livesico = P_SpawnMobjFromMobj(newmobj, 0, 0, 0, MT_OVERLAY);
-			P_SetTarget(&livesico->target, newmobj);
-			P_SetTarget(&newmobj->tracer, livesico);
+		if (item == MT_1UP_ICON)
+		{
+			if (actor->tracer) // Remove the old lives icon.
+				P_RemoveMobj(actor->tracer);
 
-			livesico->color = newmobj->target->player->mo->color;
-			livesico->skin = &skins[newmobj->target->player->skin];
-			P_SetMobjState(livesico, newmobj->info->seestate);
+			if (!newmobj->target
+			 || !newmobj->target->player
+			 || !newmobj->target->skin
+			 || ((skin_t *)newmobj->target->skin)->sprites[SPR2_LIFE].numframes == 0)
+				{} // No lives icon for this player, use the default.
+			else
+			{ // Spawn the lives icon.
+				mobj_t *livesico = P_SpawnMobjFromMobj(newmobj, 0, 0, 0, MT_OVERLAY);
+				if (!P_MobjWasRemoved(livesico))
+				{
+					P_SetTarget(&livesico->target, newmobj);
+					P_SetTarget(&newmobj->tracer, livesico);
 
-			// We're using the overlay, so use the overlay 1up sprite (no text)
-			newmobj->sprite = SPR_TV1P;
+					livesico->color = newmobj->target->player->mo->color;
+					livesico->skin = &skins[newmobj->target->player->skin];
+					P_SetMobjState(livesico, newmobj->info->seestate);
+				}
+
+				// We're using the overlay, so use the overlay 1up sprite (no text)
+				newmobj->sprite = SPR_TV1P;
+			}
 		}
 	}
 
@@ -3870,7 +3928,13 @@ void A_GoldMonitorSparkle(mobj_t *actor)
 	yofs = FINECOSINE((ngangle>>ANGLETOFINESHIFT) & FINEMASK) * (actor->radius>>FRACBITS);
 
 	for (i = FRACUNIT*2; i <= FRACUNIT*3; i += FRACUNIT/2)
-		P_SetObjectMomZ(P_SpawnMobjFromMobj(actor, xofs, yofs, 0, MT_BOXSPARKLE), i, false);
+	{
+		mobj_t *sparkle = P_SpawnMobjFromMobj(actor, xofs, yofs, 0, MT_BOXSPARKLE);
+		if (P_MobjWasRemoved(sparkle))
+			continue;
+
+		P_SetObjectMomZ(sparkle, i, false);
+	}
 }
 
 // Function: A_Explode
@@ -3992,56 +4056,73 @@ static void P_DoBossVictory(mobj_t *mo)
 static void P_SpawnBoss1Junk(mobj_t *mo)
 {
 	mobj_t *mo2 = P_SpawnMobjFromMobj(mo,
-	P_ReturnThrustX(mo, mo->angle - ANGLE_90, 32<<FRACBITS),
-	P_ReturnThrustY(mo, mo->angle - ANGLE_90, 32<<FRACBITS),
-	32<<FRACBITS, MT_BOSSJUNK);
-	mo2->angle = mo->angle;
-	P_InstaThrust(mo2, mo2->angle - ANGLE_90, 4*mo2->scale);
-	P_SetObjectMomZ(mo2, 4*FRACUNIT, false);
-	P_SetMobjState(mo2, S_BOSSEGLZ1);
+		P_ReturnThrustX(mo, mo->angle - ANGLE_90, 32<<FRACBITS),
+		P_ReturnThrustY(mo, mo->angle - ANGLE_90, 32<<FRACBITS),
+		32<<FRACBITS, MT_BOSSJUNK);
+	if (!P_MobjWasRemoved(mo2))
+	{
+		mo2->angle = mo->angle;
+		P_InstaThrust(mo2, mo2->angle - ANGLE_90, 4*mo2->scale);
+		P_SetObjectMomZ(mo2, 4*FRACUNIT, false);
+		P_SetMobjState(mo2, S_BOSSEGLZ1);
+	}
 
 	mo2 = P_SpawnMobjFromMobj(mo,
 		P_ReturnThrustX(mo, mo->angle + ANGLE_90, 32<<FRACBITS),
 		P_ReturnThrustY(mo, mo->angle + ANGLE_90, 32<<FRACBITS),
 		32<<FRACBITS, MT_BOSSJUNK);
-	mo2->angle = mo->angle;
-	P_InstaThrust(mo2, mo2->angle + ANGLE_90, 4*mo2->scale);
-	P_SetObjectMomZ(mo2, 4*FRACUNIT, false);
-	P_SetMobjState(mo2, S_BOSSEGLZ2);
+	if (!P_MobjWasRemoved(mo2))
+	{
+		mo2->angle = mo->angle;
+		P_InstaThrust(mo2, mo2->angle + ANGLE_90, 4*mo2->scale);
+		P_SetObjectMomZ(mo2, 4*FRACUNIT, false);
+		P_SetMobjState(mo2, S_BOSSEGLZ2);
+	}
 }
 
 static void P_SpawnBoss2Junk(mobj_t *mo)
 {
 	mobj_t *mo2 = P_SpawnMobjFromMobj(mo,
-	P_ReturnThrustX(mo, mo->angle - ANGLE_90, 32<<FRACBITS),
-	P_ReturnThrustY(mo, mo->angle - ANGLE_90, 32<<FRACBITS),
-	32<<FRACBITS, MT_BOSSJUNK);
-	mo2->angle = mo->angle;
-	P_InstaThrust(mo2, mo2->angle - ANGLE_90, 4*mo2->scale);
-	P_SetObjectMomZ(mo2, 4*FRACUNIT, false);
-	P_SetMobjState(mo2, S_BOSSTANK1);
+		P_ReturnThrustX(mo, mo->angle - ANGLE_90, 32<<FRACBITS),
+		P_ReturnThrustY(mo, mo->angle - ANGLE_90, 32<<FRACBITS),
+		32<<FRACBITS, MT_BOSSJUNK);
+	if (!P_MobjWasRemoved(mo2))
+	{
+		mo2->angle = mo->angle;
+		P_InstaThrust(mo2, mo2->angle - ANGLE_90, 4*mo2->scale);
+		P_SetObjectMomZ(mo2, 4*FRACUNIT, false);
+		P_SetMobjState(mo2, S_BOSSTANK1);
+	}
 
 	mo2 = P_SpawnMobjFromMobj(mo,
 		P_ReturnThrustX(mo, mo->angle + ANGLE_90, 32<<FRACBITS),
 		P_ReturnThrustY(mo, mo->angle + ANGLE_90, 32<<FRACBITS),
 		32<<FRACBITS, MT_BOSSJUNK);
-	mo2->angle = mo->angle;
-	P_InstaThrust(mo2, mo2->angle + ANGLE_90, 4*mo2->scale);
-	P_SetObjectMomZ(mo2, 4*FRACUNIT, false);
-	P_SetMobjState(mo2, S_BOSSTANK2);
+	if (!P_MobjWasRemoved(mo2))
+	{
+		mo2->angle = mo->angle;
+		P_InstaThrust(mo2, mo2->angle + ANGLE_90, 4*mo2->scale);
+		P_SetObjectMomZ(mo2, 4*FRACUNIT, false);
+		P_SetMobjState(mo2, S_BOSSTANK2);
+	}
 
 	mo2 = P_SpawnMobjFromMobj(mo, 0, 0,
 		mobjinfo[MT_EGGMOBILE2].height + (32<<FRACBITS),
 		MT_BOSSJUNK);
-	mo2->angle = mo->angle;
-	P_SetObjectMomZ(mo2, 4*FRACUNIT, false);
-	mo2->momz += mo->momz;
-	P_SetMobjState(mo2, S_BOSSSPIGOT);
+	if (!P_MobjWasRemoved(mo2))
+	{
+		mo2->angle = mo->angle;
+		P_SetObjectMomZ(mo2, 4*FRACUNIT, false);
+		mo2->momz += mo->momz;
+		P_SetMobjState(mo2, S_BOSSSPIGOT);
+	}
 }
 
 static void P_SpawnBoss3Junk(mobj_t *mo)
 {
 	mobj_t *mo2 = P_SpawnMobjFromMobj(mo, 0, 0, 0, MT_BOSSJUNK);
+	if (P_MobjWasRemoved(mo2))
+		return;
 	mo2->angle = mo->angle;
 	P_SetMobjState(mo2, S_BOSSSEBH1);
 }
@@ -4081,26 +4162,36 @@ static void P_DoBoss5Death(mobj_t *mo)
 				mo->tracer->y - P_ReturnThrustY(mo->tracer, mo->tracer->angle, speed*time),
 				mo->tracer->floorz + (256+1)*FRACUNIT,
 				MT_FSGNB);
-			P_SetTarget(&pole->tracer, P_SpawnMobj(
-				pole->x, pole->y,
-				pole->z - 256*FRACUNIT,
-				MT_FSGNB));
-			P_SetTarget(&pole->tracer->tracer, P_SpawnMobj(
-				pole->x + P_ReturnThrustX(pole, mo->tracer->angle, FRACUNIT),
-				pole->y + P_ReturnThrustY(pole, mo->tracer->angle, FRACUNIT),
-				pole->z + 256*FRACUNIT,
-				MT_FSGNA));
-			pole->tracer->flags |= MF_NOCLIPTHING;
-			P_SetScale(pole, (pole->destscale = 2*FRACUNIT));
-			P_SetScale(pole->tracer, (pole->tracer->destscale = 2*FRACUNIT));
-			pole->angle = pole->tracer->angle = mo->tracer->angle;
-			pole->tracer->tracer->angle = pole->angle - ANGLE_90;
-			pole->momx = P_ReturnThrustX(pole, pole->angle, speed);
-			pole->momy = P_ReturnThrustY(pole, pole->angle, speed);
-			pole->tracer->momx = pole->momx;
-			pole->tracer->momy = pole->momy;
-			pole->tracer->tracer->momx = pole->momx;
-			pole->tracer->tracer->momy = pole->momy;
+			if (!P_MobjWasRemoved(pole))
+			{
+				P_SetScale(pole, (pole->destscale = 2*FRACUNIT));
+				pole->momx = P_ReturnThrustX(pole, pole->angle, speed);
+				pole->momy = P_ReturnThrustY(pole, pole->angle, speed);
+				P_SetTarget(&pole->tracer, P_SpawnMobj(
+					pole->x, pole->y,
+					pole->z - 256*FRACUNIT,
+					MT_FSGNB));
+				if (!P_MobjWasRemoved(pole->tracer))
+				{
+					pole->tracer->flags |= MF_NOCLIPTHING;
+					P_SetScale(pole->tracer, (pole->tracer->destscale = 2*FRACUNIT));
+					pole->angle = pole->tracer->angle = mo->tracer->angle;
+					pole->tracer->momx = pole->momx;
+					pole->tracer->momy = pole->momy;
+
+					P_SetTarget(&pole->tracer->tracer, P_SpawnMobj(
+						pole->x + P_ReturnThrustX(pole, mo->tracer->angle, FRACUNIT),
+						pole->y + P_ReturnThrustY(pole, mo->tracer->angle, FRACUNIT),
+						pole->z + 256*FRACUNIT,
+						MT_FSGNA));
+					if (!P_MobjWasRemoved(pole->tracer->tracer))
+					{
+						pole->tracer->tracer->angle = pole->angle - ANGLE_90;
+						pole->tracer->tracer->momx = pole->momx;
+						pole->tracer->tracer->momy = pole->momy;
+					}
+				}
+			}
 		}
 	}
 	else
@@ -4760,9 +4851,12 @@ void A_AttractChase(mobj_t *actor)
 	{
 		mobj_t *newring;
 		newring = P_SpawnMobj(actor->x, actor->y, actor->z, actor->info->reactiontime);
-		newring->momx = actor->momx;
-		newring->momy = actor->momy;
-		newring->momz = actor->momz;
+		if (!P_MobjWasRemoved(newring))
+		{
+			newring->momx = actor->momx;
+			newring->momy = actor->momy;
+			newring->momz = actor->momz;
+		}
 		P_RemoveMobj(actor);
 		return;
 	}
@@ -4842,9 +4936,12 @@ void A_DropMine(mobj_t *actor)
 
 	// Use raisestate instead of MT_MINE
 	mine = P_SpawnMobj(actor->x, actor->y, z, (mobjtype_t)actor->info->raisestate);
-	if (actor->eflags & MFE_VERTICALFLIP)
-		mine->eflags |= MFE_VERTICALFLIP;
-	mine->momz = actor->momz + actor->pmomz;
+	if (!P_MobjWasRemoved(mine))
+	{
+		if (actor->eflags & MFE_VERTICALFLIP)
+			mine->eflags |= MFE_VERTICALFLIP;
+		mine->momz = actor->momz + actor->pmomz;
+	}
 
 	S_StartSound(actor, actor->info->attacksound);
 }
@@ -5169,11 +5266,13 @@ void A_SignSpin(mobj_t *actor)
 	if (actor->tracer == NULL || P_MobjWasRemoved(actor->tracer)) return;
 	for (i = -1; i < 2; i += 2)
 	{
-		P_SpawnMobjFromMobj(actor,
+		mobj_t *spinmobj = P_SpawnMobjFromMobj(actor,
 			P_ReturnThrustX(actor, actor->tracer->angle, i * actor->radius),
 			P_ReturnThrustY(actor, actor->tracer->angle, i * actor->radius),
 			(actor->eflags & MFE_VERTICALFLIP) ? 0 : actor->height,
-			actor->info->painchance)->destscale >>= 1;
+			actor->info->painchance);
+		if (!P_MobjWasRemoved(spinmobj))
+			spinmobj->destscale >>= 1;
 	}
 }
 
@@ -5233,8 +5332,11 @@ void A_SignPlayer(mobj_t *actor)
 	if (actor->tracer->tracer == NULL || P_MobjWasRemoved(actor->tracer->tracer))
 	{
 		ov = P_SpawnMobj(actor->x, actor->y, actor->z, MT_OVERLAY);
-		P_SetTarget(&ov->target, actor->tracer);
-		P_SetTarget(&actor->tracer->tracer, ov);
+		if (!P_MobjWasRemoved(ov))
+		{
+			P_SetTarget(&ov->target, actor->tracer);
+			P_SetTarget(&actor->tracer->tracer, ov);
+		}
 	}
 	else
 		ov = actor->tracer->tracer;
@@ -5248,7 +5350,7 @@ void A_SignPlayer(mobj_t *actor)
 			return;
 
 		skin = &skins[actor->target->player->skin];
-		facecolor = actor->target->player->skincolor;
+		facecolor = P_GetPlayerColor(actor->target->player);
 
 		if (signcolor)
 			;
@@ -5294,33 +5396,36 @@ void A_SignPlayer(mobj_t *actor)
 			signcolor = skincolors[facecolor].invcolor;
 	}
 
-	if (skin)
-	{
-		if (skin->sprites[SPR2_SIGN].numframes) // player face
+    if (!P_MobjWasRemoved(ov))
+    {
+		if (skin)
 		{
-			ov->color = facecolor;
-			ov->skin = skin;
-			if ((statenum_t)(ov->state-states) != actor->info->seestate)
-				P_SetMobjState(ov, actor->info->seestate); // S_PLAY_SIGN
+			if (skin->sprites[SPR2_SIGN].numframes) // player face
+			{
+				ov->color = facecolor;
+				ov->skin = skin;
+				if ((statenum_t)(ov->state-states) != actor->info->seestate)
+					P_SetMobjState(ov, actor->info->seestate); // S_PLAY_SIGN
+			}
+			else // CLEAR! sign
+			{
+				ov->color = SKINCOLOR_NONE;
+				ov->skin = NULL; // needs to be NULL in the case of SF_HIRES characters
+				if ((statenum_t)(ov->state-states) != actor->info->missilestate)
+					P_SetMobjState(ov, actor->info->missilestate); // S_CLEARSIGN
+			}
 		}
-		else // CLEAR! sign
+		else // Eggman face
 		{
 			ov->color = SKINCOLOR_NONE;
-			ov->skin = NULL; // needs to be NULL in the case of SF_HIRES characters
-			if ((statenum_t)(ov->state-states) != actor->info->missilestate)
-				P_SetMobjState(ov, actor->info->missilestate); // S_CLEARSIGN
+			ov->skin = NULL;
+			if ((statenum_t)(ov->state-states) != actor->info->meleestate)
+				P_SetMobjState(ov, actor->info->meleestate); // S_EGGMANSIGN
+			if (!signcolor)
+				signcolor = SKINCOLOR_CARBON;
+			facecolor = signcolor;
 		}
-	}
-	else // Eggman face
-	{
-		ov->color = SKINCOLOR_NONE;
-		ov->skin = NULL;
-		if ((statenum_t)(ov->state-states) != actor->info->meleestate)
-			P_SetMobjState(ov, actor->info->meleestate); // S_EGGMANSIGN
-		if (!signcolor)
-			signcolor = SKINCOLOR_CARBON;
-		facecolor = signcolor;
-	}
+    }
 
 	actor->tracer->color = signcolor;
 	if (signcolor && signcolor < numskincolors)
@@ -5511,12 +5616,14 @@ void A_JetbThink(mobj_t *actor)
 
 			// use raisestate instead of MT_MINE
 			bomb = P_SpawnMobj(actor->x, actor->y, actor->z - FixedMul((32<<FRACBITS), actor->scale), (mobjtype_t)actor->info->raisestate);
-
-			P_SetTarget(&bomb->target, actor);
-			bomb->destscale = actor->scale;
-			P_SetScale(bomb, actor->scale);
-			actor->reactiontime = TICRATE; // one second
-			S_StartSound(actor, actor->info->attacksound);
+			if (!P_MobjWasRemoved(bomb))
+			{
+				P_SetTarget(&bomb->target, actor);
+				bomb->destscale = actor->scale;
+				P_SetScale(bomb, actor->scale);
+				actor->reactiontime = TICRATE; // one second
+				S_StartSound(actor, actor->info->attacksound);
+			}
 		}
 	}
 	else if (((actor->z - FixedMul((32<<FRACBITS), actor->scale)) < thefloor) && !((thefloor + FixedMul((32<<FRACBITS), actor->scale) + actor->height) > actor->ceilingz))
@@ -5668,9 +5775,12 @@ void A_MinusDigging(mobj_t *actor)
 	}
 
 	par = P_SpawnMobj(actor->x, actor->y, mz, MT_MINUSDIRT);
-	if (actor->eflags & MFE_VERTICALFLIP)
-		par->eflags |= MFE_VERTICALFLIP;
-	P_TryMove(par, x, y, false);
+	if (!P_MobjWasRemoved(par))
+	{
+		if (actor->eflags & MFE_VERTICALFLIP)
+			par->eflags |= MFE_VERTICALFLIP;
+		P_TryMove(par, x, y, false);
+	}
 
 	// If close enough, prepare to attack
 	if (P_AproxDistance(actor->x - actor->target->x, actor->y - actor->target->y) < actor->radius*2)
@@ -5681,6 +5791,8 @@ void A_MinusDigging(mobj_t *actor)
 
 		// Spawn growing dirt pile.
 		par = P_SpawnMobj(actor->x, actor->y, mz, MT_MINUSDIRT);
+		if (P_MobjWasRemoved(par))
+			return;
 		P_SetMobjState(par, actor->info->raisestate);
 		P_SetScale(par, actor->scale*2);
 		if (actor->eflags & MFE_VERTICALFLIP)
@@ -5751,6 +5863,8 @@ void A_MinusPopup(mobj_t *actor)
 	for (i = 1; i <= num; i++)
 	{
 		mobj_t *rock = P_SpawnMobjFromMobj(actor, 0, 0, actor->height/4, MT_ROCKCRUMBLE1);
+		if (P_MobjWasRemoved(rock))
+			continue;
 		P_Thrust(rock, ani*i, FRACUNIT);
 		P_SetObjectMomZ(rock, 3*FRACUNIT, false);
 		P_SetScale(rock, rock->scale/3);
@@ -5788,6 +5902,8 @@ void A_MinusCheck(mobj_t *actor)
 			for (i = 1; i <= num; i++)
 			{
 				mobj_t *rock = P_SpawnMobjFromMobj(actor, 0, 0, actor->height/4, MT_ROCKCRUMBLE1);
+				if (P_MobjWasRemoved(rock))
+					continue;
 				P_Thrust(rock, ani*i, FRACUNIT);
 				P_SetObjectMomZ(rock, 3*FRACUNIT, false);
 				P_SetScale(rock, rock->scale/3);
@@ -6299,6 +6415,8 @@ void A_RockSpawn(mobj_t *actor)
 		dist += actor->spawnpoint->args[2] ? P_RandomByte() * (FRACUNIT/32) : 0; // random oomph
 
 	mo = P_SpawnMobj(actor->x, actor->y, actor->z, MT_FALLINGROCK);
+	if (P_MobjWasRemoved(mo))
+		return;
 	P_SetMobjState(mo, mobjinfo[type].spawnstate);
 	mo->angle = FixedAngle(actor->spawnpoint->angle << FRACBITS);
 
@@ -6335,6 +6453,8 @@ void A_SlingAppear(mobj_t *actor)
 	actor->friction = 128;
 
 	hprev = P_SpawnMobj(actor->x, actor->y, actor->z, MT_SMALLGRABCHAIN);
+	if (P_MobjWasRemoved(hprev))
+		return;
 	P_SetTarget(&hprev->tracer, actor);
 	P_SetTarget(&hprev->hprev, actor);
 	P_SetTarget(&actor->hnext, hprev);
@@ -6346,13 +6466,16 @@ void A_SlingAppear(mobj_t *actor)
 	while (mlength > 0)
 	{
 		spawnee = P_SpawnMobj(actor->x, actor->y, actor->z, MT_SMALLMACECHAIN);
-		P_SetTarget(&spawnee->tracer, actor);
-		P_SetTarget(&spawnee->hprev, hprev);
-		P_SetTarget(&hprev->hnext, spawnee);
-		hprev = spawnee;
+		if (!P_MobjWasRemoved(spawnee))
+		{
+			P_SetTarget(&spawnee->tracer, actor);
+			P_SetTarget(&spawnee->hprev, hprev);
+			P_SetTarget(&hprev->hnext, spawnee);
+			hprev = spawnee;
 
-		spawnee->flags |= MF_NOCLIP|MF_NOCLIPHEIGHT;
-		spawnee->movecount = mlength;
+			spawnee->flags |= MF_NOCLIP|MF_NOCLIPHEIGHT;
+			spawnee->movecount = mlength;
+		}
 
 		mlength--;
 	}
@@ -6612,6 +6735,8 @@ void A_OldRingExplode(mobj_t *actor) {
 		const angle_t fa = (i*FINEANGLES/16) & FINEMASK;
 
 		mo = P_SpawnMobjFromMobj(actor, 0, 0, 0, locvar1);
+		if (P_MobjWasRemoved(mo))
+			continue;
 		P_SetTarget(&mo->target, actor->target); // Transfer target so player gets the points
 
 		mo->momx = FixedMul(FINECOSINE(fa),ns);
@@ -6638,33 +6763,37 @@ void A_OldRingExplode(mobj_t *actor) {
 	}
 
 	mo = P_SpawnMobjFromMobj(actor, 0, 0, 0, locvar1);
-
-	P_SetTarget(&mo->target, actor->target);
-	mo->momz = ns;
-	mo->flags2 |= MF2_DEBRIS;
-	mo->fuse = TICRATE/5;
-
-	if (changecolor)
+	if (!P_MobjWasRemoved(mo))
 	{
-		if (!(gametyperules & GTR_TEAMS))
-			mo->color = actor->target->color; //copy color
-		else if (actor->target->player->ctfteam == 2)
-			mo->color = skincolor_bluering;
+		P_SetTarget(&mo->target, actor->target);
+		mo->momz = ns;
+		mo->flags2 |= MF2_DEBRIS;
+		mo->fuse = TICRATE/5;
+
+		if (changecolor)
+		{
+			if (!(gametyperules & GTR_TEAMS))
+				mo->color = actor->target->color; //copy color
+			else if (actor->target->player->ctfteam == 2)
+				mo->color = skincolor_bluering;
+		}
 	}
 
 	mo = P_SpawnMobjFromMobj(actor, 0, 0, 0, locvar1);
-
-	P_SetTarget(&mo->target, actor->target);
-	mo->momz = -ns;
-	mo->flags2 |= MF2_DEBRIS;
-	mo->fuse = TICRATE/5;
-
-	if (changecolor)
+	if (!P_MobjWasRemoved(mo))
 	{
-		if (!(gametyperules & GTR_TEAMS))
-			mo->color = actor->target->color; //copy color
-		else if (actor->target->player->ctfteam == 2)
-			mo->color = skincolor_bluering;
+		P_SetTarget(&mo->target, actor->target);
+		mo->momz = -ns;
+		mo->flags2 |= MF2_DEBRIS;
+		mo->fuse = TICRATE/5;
+
+		if (changecolor)
+		{
+			if (!(gametyperules & GTR_TEAMS))
+				mo->color = actor->target->color; //copy color
+			else if (actor->target->player->ctfteam == 2)
+				mo->color = skincolor_bluering;
+		}
 	}
 }
 
@@ -7300,23 +7429,26 @@ void A_Boss2Chase(mobj_t *actor)
 			fa = (actor->movedir*FINEANGLES/8) & FINEMASK;
 
 			goop = P_SpawnMobj(actor->x, actor->y, fz, actor->info->painchance);
-			goop->momx = FixedMul(FINECOSINE(fa),ns);
-			goop->momy = FixedMul(FINESINE(fa),ns);
-			goop->momz = FixedMul(4*FRACUNIT, actor->scale);
-			goop->fuse = 10*TICRATE;
+			if (!P_MobjWasRemoved(goop))
+			{
+				goop->momx = FixedMul(FINECOSINE(fa),ns);
+				goop->momy = FixedMul(FINESINE(fa),ns);
+				goop->momz = FixedMul(4*FRACUNIT, actor->scale);
+				goop->fuse = 10*TICRATE;
 
-			if (actor->info->attacksound)
-				S_StartAttackSound(actor, actor->info->attacksound);
+				if (actor->info->attacksound)
+					S_StartAttackSound(actor, actor->info->attacksound);
 
-			if (P_RandomChance(FRACUNIT/2))
-			{
-				goop->momx *= 2;
-				goop->momy *= 2;
-			}
-			else if (P_RandomChance(129*FRACUNIT/256))
-			{
-				goop->momx *= 3;
-				goop->momy *= 3;
+				if (P_RandomChance(FRACUNIT/2))
+				{
+					goop->momx *= 2;
+					goop->momy *= 2;
+				}
+				else if (P_RandomChance(129*FRACUNIT/256))
+				{
+					goop->momx *= 3;
+					goop->momy *= 3;
+				}
 			}
 
 			actor->flags2 |= MF2_JUSTATTACKED;
@@ -7357,6 +7489,8 @@ void A_Boss2Pogo(mobj_t *actor)
 			fa = (actor->movedir*FINEANGLES/8) & FINEMASK;
 
 			goop = P_SpawnMobj(actor->x, actor->y, fz, actor->info->painchance);
+			if (P_MobjWasRemoved(goop))
+				continue;
 			goop->momx = FixedMul(FINECOSINE(fa),ns);
 			goop->momy = FixedMul(FINESINE(fa),ns);
 			goop->momz = FixedMul(4*FRACUNIT, actor->scale);
@@ -7685,7 +7819,8 @@ void A_Boss2PogoTarget(mobj_t *actor)
 	if (actor->info->missilestate) // spawn the pogo stick collision box
 	{
 		mobj_t *pogo = P_SpawnMobj(actor->x, actor->y, actor->z - mobjinfo[actor->info->missilestate].height, (mobjtype_t)actor->info->missilestate);
-		P_SetTarget(&pogo->target, actor);
+		if (!P_MobjWasRemoved(pogo))
+			P_SetTarget(&pogo->target, actor);
 	}
 
 	actor->reactiontime = 1;
@@ -8176,6 +8311,8 @@ void A_Boss1Spikeballs(mobj_t *actor)
 		return;
 
 	ball = P_SpawnMobj(actor->x, actor->y, actor->z, MT_EGGMOBILE_BALL);
+	if (P_MobjWasRemoved(ball))
+		return;
 	P_SetTarget(&ball->target, actor);
 	ball->movedir = FixedAngle(FixedMul(FixedDiv(locvar1<<FRACBITS, locvar2<<FRACBITS), 360<<FRACBITS));
 	ball->threshold = ball->radius + actor->radius + ball->info->painchance;
@@ -8363,19 +8500,22 @@ void A_Boss3ShockThink(mobj_t *actor)
 			snew = P_SpawnMobj((x0 >> 1) + (x1 >> 1),
 				(y0 >> 1) + (y1 >> 1),
 				(actor->z >> 1) + (snext->z >> 1), actor->type);
-			snew->momx = (actor->momx + snext->momx) >> 1;
-			snew->momy = (actor->momy + snext->momy) >> 1;
-			snew->momz = (actor->momz + snext->momz) >> 1; // is this really needed?
-			snew->angle = (actor->angle + snext->angle) >> 1;
-			P_SetTarget(&snew->target, actor->target);
-			snew->fuse = actor->fuse;
-
-			P_SetScale(snew, actor->scale);
-			snew->destscale = actor->destscale;
-			snew->scalespeed = actor->scalespeed;
-
-			P_SetTarget(&actor->hnext, snew);
-			P_SetTarget(&snew->hnext, snext);
+			if (!P_MobjWasRemoved(snew))
+			{
+				snew->momx = (actor->momx + snext->momx) >> 1;
+				snew->momy = (actor->momy + snext->momy) >> 1;
+				snew->momz = (actor->momz + snext->momz) >> 1; // is this really needed?
+				snew->angle = (actor->angle + snext->angle) >> 1;
+				P_SetTarget(&snew->target, actor->target);
+				snew->fuse = actor->fuse;
+
+				P_SetScale(snew, actor->scale);
+				snew->destscale = actor->destscale;
+				snew->scalespeed = actor->scalespeed;
+
+				P_SetTarget(&actor->hnext, snew);
+				P_SetTarget(&snew->hnext, snext);
+			}
 		}
 	}
 }
@@ -8560,10 +8700,13 @@ void A_SmokeTrailer(mobj_t *actor)
 	if (actor->eflags & MFE_VERTICALFLIP)
 	{
 		th = P_SpawnMobj(actor->x-actor->momx, actor->y-actor->momy, actor->z + actor->height - FixedMul(mobjinfo[locvar1].height, actor->scale), locvar1);
-		th->flags2 |= MF2_OBJECTFLIP;
+		if (!P_MobjWasRemoved(th))
+			th->flags2 |= MF2_OBJECTFLIP;
 	}
 	else
 		th = P_SpawnMobj(actor->x-actor->momx, actor->y-actor->momy, actor->z, locvar1);
+	if (P_MobjWasRemoved(th))
+		return;
 	P_SetObjectMomZ(th, FRACUNIT, false);
 	th->destscale = actor->scale;
 	P_SetScale(th, actor->scale);
@@ -8600,6 +8743,8 @@ void A_SpawnObjectAbsolute(mobj_t *actor)
 	type = (mobjtype_t)(locvar2&65535);
 
 	mo = P_SpawnMobj(x<<FRACBITS, y<<FRACBITS, z<<FRACBITS, type);
+	if (P_MobjWasRemoved(mo))
+		return;
 
 	// Spawn objects with an angle matching the spawner's, rather than spawning Eastwards - Monster Iestyn
 	mo->angle = actor->angle;
@@ -8642,6 +8787,8 @@ void A_SpawnObjectRelative(mobj_t *actor)
 	mo = P_SpawnMobj(actor->x + FixedMul(x<<FRACBITS, actor->scale),
 		actor->y + FixedMul(y<<FRACBITS, actor->scale),
 		(actor->eflags & MFE_VERTICALFLIP) ? ((actor->z + actor->height - mobjinfo[type].height) - FixedMul(z<<FRACBITS, actor->scale)) : (actor->z + FixedMul(z<<FRACBITS, actor->scale)), type);
+	if (P_MobjWasRemoved(mo))
+		return;
 
 	// Spawn objects with an angle matching the spawner's, rather than spawning Eastwards - Monster Iestyn
 	mo->angle = actor->angle;
@@ -9059,7 +9206,7 @@ void A_Dye(mobj_t *actor)
 	if (!color)
 	{
 		target->colorized = false;
-		target->color = target->player ? target->player->skincolor : SKINCOLOR_NONE;
+		target->color = target->player ? P_GetPlayerColor(target->player) : SKINCOLOR_NONE;
 	}
 	else if (!(target->player))
 	{
@@ -9303,12 +9450,15 @@ void A_BossJetFume(mobj_t *actor)
 			jetz = actor->z + FixedMul(38*FRACUNIT, actor->scale);
 
 		filler = P_SpawnMobj(jetx, jety, jetz, MT_JETFUME1);
-		P_SetTarget(&filler->target, actor);
-		filler->destscale = actor->scale;
-		P_SetScale(filler, filler->destscale);
-		if (actor->eflags & MFE_VERTICALFLIP)
-			filler->flags2 |= MF2_OBJECTFLIP;
-		filler->fuse = 56;
+		if (!P_MobjWasRemoved(filler))
+		{
+			P_SetTarget(&filler->target, actor);
+			filler->destscale = actor->scale;
+			P_SetScale(filler, filler->destscale);
+			if (actor->eflags & MFE_VERTICALFLIP)
+				filler->flags2 |= MF2_OBJECTFLIP;
+			filler->fuse = 56;
+		}
 
 		if (actor->eflags & MFE_VERTICALFLIP)
 			jetz = actor->z + actor->height - FixedMul(12*FRACUNIT + mobjinfo[MT_JETFUME1].height, actor->scale);
@@ -9318,22 +9468,28 @@ void A_BossJetFume(mobj_t *actor)
 		filler = P_SpawnMobj(jetx + P_ReturnThrustX(actor, actor->angle-ANGLE_90, FixedMul(24*FRACUNIT, actor->scale)),
 				jety + P_ReturnThrustY(actor, actor->angle-ANGLE_90, FixedMul(24*FRACUNIT, actor->scale)),
 				jetz, MT_JETFUME1);
-		P_SetTarget(&filler->target, actor);
-		filler->destscale = actor->scale;
-		P_SetScale(filler, filler->destscale);
-		if (actor->eflags & MFE_VERTICALFLIP)
-			filler->flags2 |= MF2_OBJECTFLIP;
-		filler->fuse = 57;
+		if (!P_MobjWasRemoved(filler))
+		{
+			P_SetTarget(&filler->target, actor);
+			filler->destscale = actor->scale;
+			P_SetScale(filler, filler->destscale);
+			if (actor->eflags & MFE_VERTICALFLIP)
+				filler->flags2 |= MF2_OBJECTFLIP;
+			filler->fuse = 57;
+		}
 
 		filler = P_SpawnMobj(jetx + P_ReturnThrustX(actor, actor->angle+ANGLE_90, FixedMul(24*FRACUNIT, actor->scale)),
 				jety + P_ReturnThrustY(actor, actor->angle+ANGLE_90, FixedMul(24*FRACUNIT, actor->scale)),
 				jetz, MT_JETFUME1);
-		P_SetTarget(&filler->target, actor);
-		filler->destscale = actor->scale;
-		P_SetScale(filler, filler->destscale);
-		if (actor->eflags & MFE_VERTICALFLIP)
-			filler->flags2 |= MF2_OBJECTFLIP;
-		filler->fuse = 58;
+		if (!P_MobjWasRemoved(filler))
+		{
+			P_SetTarget(&filler->target, actor);
+			filler->destscale = actor->scale;
+			P_SetScale(filler, filler->destscale);
+			if (actor->eflags & MFE_VERTICALFLIP)
+				filler->flags2 |= MF2_OBJECTFLIP;
+			filler->fuse = 58;
+		}
 
 		P_SetTarget(&actor->tracer, filler);
 	}
@@ -9361,14 +9517,17 @@ void A_BossJetFume(mobj_t *actor)
 	else if (locvar1 == 2) // Metal Sonic jet fumes
 	{
 		filler = P_SpawnMobj(actor->x, actor->y, actor->z, MT_JETFUME1);
-		P_SetTarget(&filler->target, actor);
-		filler->fuse = 59;
-		P_SetTarget(&actor->tracer, filler);
-		P_SetScale(filler, (filler->destscale = actor->scale/3));
-		if (actor->eflags & MFE_VERTICALFLIP)
-			filler->flags2 |= MF2_OBJECTFLIP;
-		filler->color = SKINCOLOR_ICY;
-		filler->colorized = true;
+		if (!P_MobjWasRemoved(filler))
+		{
+			P_SetTarget(&filler->target, actor);
+			filler->fuse = 59;
+			P_SetTarget(&actor->tracer, filler);
+			P_SetScale(filler, (filler->destscale = actor->scale/3));
+			if (actor->eflags & MFE_VERTICALFLIP)
+				filler->flags2 |= MF2_OBJECTFLIP;
+			filler->color = SKINCOLOR_ICY;
+			filler->colorized = true;
+		}
 	}
 	else if (locvar1 == 3) // Boss 4 jet flame
 	{
@@ -9378,12 +9537,15 @@ void A_BossJetFume(mobj_t *actor)
 		else
 			jetz = actor->z - 50*actor->scale;
 		filler = P_SpawnMobj(actor->x, actor->y, jetz, MT_JETFLAME);
-		P_SetTarget(&filler->target, actor);
-		// Boss 4 already uses its tracer for other things
-		filler->destscale = actor->scale;
-		P_SetScale(filler, filler->destscale);
-		if (actor->eflags & MFE_VERTICALFLIP)
-			filler->flags2 |= MF2_OBJECTFLIP;
+		if (!P_MobjWasRemoved(filler))
+		{
+			P_SetTarget(&filler->target, actor);
+			// Boss 4 already uses its tracer for other things
+			filler->destscale = actor->scale;
+			P_SetScale(filler, filler->destscale);
+			if (actor->eflags & MFE_VERTICALFLIP)
+				filler->flags2 |= MF2_OBJECTFLIP;
+		}
 	}
 	else if (locvar1 == 4) // Boss 4 Spectator Eggrobo jet flame
 	{
@@ -9398,12 +9560,15 @@ void A_BossJetFume(mobj_t *actor)
 			jetx = actor->x + P_ReturnThrustX(actor, actor->angle+ANGLE_90, movefactor*actor->scale) - P_ReturnThrustX(actor, actor->angle, 19*actor->scale);
 			jety = actor->y + P_ReturnThrustY(actor, actor->angle+ANGLE_90, movefactor*actor->scale) - P_ReturnThrustY(actor, actor->angle, 19*actor->scale);
 			filler = P_SpawnMobj(jetx, jety, jetz, MT_EGGROBO1JET);
-			filler->movefactor = movefactor;
-			P_SetTarget(&filler->target, actor);
-			filler->destscale = actor->scale;
-			P_SetScale(filler, filler->destscale);
-			if (actor->eflags & MFE_VERTICALFLIP)
-				filler->flags2 |= MF2_OBJECTFLIP;
+			if (!P_MobjWasRemoved(filler))
+			{
+				filler->movefactor = movefactor;
+				P_SetTarget(&filler->target, actor);
+				filler->destscale = actor->scale;
+				P_SetScale(filler, filler->destscale);
+				if (actor->eflags & MFE_VERTICALFLIP)
+					filler->flags2 |= MF2_OBJECTFLIP;
+			}
 			if (movefactor <= 0)
 				break;
 			movefactor = -movefactor;
@@ -9780,7 +9945,7 @@ void A_SetObjectState(mobj_t *actor)
 		if (!target->player)
 			P_SetMobjState(target, locvar1);
 		else
-			P_SetPlayerMobjState(target, locvar1);
+			P_SetMobjState(target, locvar1);
 	}
 }
 
@@ -11110,6 +11275,8 @@ void A_TrapShot(mobj_t *actor)
 		type, frontoff, (INT16)(locvar2 & 65535), vertoff);
 
 	missile = P_SpawnMobj(x, y, z, type);
+	if (P_MobjWasRemoved(missile))
+		return;
 
 	if (actor->eflags & MFE_VERTICALFLIP)
 		missile->flags2 |= MF2_OBJECTFLIP;
@@ -11182,18 +11349,21 @@ void A_VileTarget(mobj_t *actor)
 							actor->target->y,
 							actor->target->z + ((actor->target->eflags & MFE_VERTICALFLIP) ? actor->target->height - mobjinfo[fogtype].height : 0),
 							fogtype);
-		if (actor->target->eflags & MFE_VERTICALFLIP)
+		if (!P_MobjWasRemoved(fog))
 		{
-			fog->eflags |= MFE_VERTICALFLIP;
-			fog->flags2 |= MF2_OBJECTFLIP;
-		}
-		fog->destscale = actor->target->scale;
-		P_SetScale(fog, fog->destscale);
+			if (actor->target->eflags & MFE_VERTICALFLIP)
+			{
+				fog->eflags |= MFE_VERTICALFLIP;
+				fog->flags2 |= MF2_OBJECTFLIP;
+			}
+			fog->destscale = actor->target->scale;
+			P_SetScale(fog, fog->destscale);
 
-		P_SetTarget(&actor->tracer, fog);
-		P_SetTarget(&fog->target, actor);
-		P_SetTarget(&fog->tracer, actor->target);
-		A_VileFire(fog);
+			P_SetTarget(&actor->tracer, fog);
+			P_SetTarget(&fog->target, actor);
+			P_SetTarget(&fog->tracer, actor->target);
+			A_VileFire(fog);
+		}
 	}
 	else
 	{
@@ -11213,19 +11383,22 @@ void A_VileTarget(mobj_t *actor)
 							players[i].mo->y,
 							players[i].mo->z + ((players[i].mo->eflags & MFE_VERTICALFLIP) ? players[i].mo->height - mobjinfo[fogtype].height : 0),
 							fogtype);
-			if (players[i].mo->eflags & MFE_VERTICALFLIP)
+			if (!P_MobjWasRemoved(fog))
 			{
-				fog->eflags |= MFE_VERTICALFLIP;
-				fog->flags2 |= MF2_OBJECTFLIP;
+				if (players[i].mo->eflags & MFE_VERTICALFLIP)
+				{
+					fog->eflags |= MFE_VERTICALFLIP;
+					fog->flags2 |= MF2_OBJECTFLIP;
+				}
+				fog->destscale = players[i].mo->scale;
+				P_SetScale(fog, fog->destscale);
+
+				if (players[i].mo == actor->target) // We only care to track the fog targeting who we REALLY hate right now
+					P_SetTarget(&actor->tracer, fog);
+				P_SetTarget(&fog->target, actor);
+				P_SetTarget(&fog->tracer, players[i].mo);
+				A_VileFire(fog);
 			}
-			fog->destscale = players[i].mo->scale;
-			P_SetScale(fog, fog->destscale);
-
-			if (players[i].mo == actor->target) // We only care to track the fog targeting who we REALLY hate right now
-				P_SetTarget(&actor->tracer, fog);
-			P_SetTarget(&fog->target, actor);
-			P_SetTarget(&fog->tracer, players[i].mo);
-			A_VileFire(fog);
 		}
 	}
 }
@@ -11702,6 +11875,8 @@ void A_BrakLobShot(mobj_t *actor)
 		typeOfShot = MT_CANNONBALL;
 	else typeOfShot = (mobjtype_t)locvar1;
 	shot = P_SpawnMobj(actor->x, actor->y, actor->z + FixedMul(locvar2*FRACUNIT, actor->scale), typeOfShot);
+	if (P_MobjWasRemoved(shot))
+		return;
 	if (shot->info->seesound)
 		S_StartSound(shot, shot->info->seesound);
 	P_SetTarget(&shot->target, actor); // where it came from
@@ -11768,6 +11943,8 @@ void A_NapalmScatter(mobj_t *actor)
 		const angle_t fa = (i*FINEANGLES/numToShoot) & FINEMASK;
 
 		mo = P_SpawnMobj(actor->x, actor->y, actor->z, typeOfShot);
+		if (P_MobjWasRemoved(mo))
+			continue;
 		P_SetTarget(&mo->target, actor->target); // Transfer target so Brak doesn't hit himself like an idiot
 
 		mo->angle = fa << ANGLETOFINESHIFT;
@@ -11793,6 +11970,8 @@ void A_SpawnFreshCopy(mobj_t *actor)
 		return;
 
 	newObject = P_SpawnMobjFromMobj(actor, 0, 0, 0, actor->type);
+	if (P_MobjWasRemoved(newObject))
+		return;
 	newObject->flags2 = actor->flags2 & MF2_AMBUSH;
 	newObject->angle = actor->angle;
 	newObject->color = actor->color;
@@ -11828,6 +12007,8 @@ mobj_t *P_InternalFlickySpawn(mobj_t *actor, mobjtype_t flickytype, fixed_t momz
 	}
 
 	flicky = P_SpawnMobjFromMobj(actor, offsx, offsy, 0, flickytype);
+	if (P_MobjWasRemoved(flicky))
+		return NULL;
 	flicky->angle = actor->angle;
 
 	if (flickytype == MT_SEED)
@@ -11939,8 +12120,11 @@ void A_FlickyCenter(mobj_t *actor)
 	if (!actor->tracer)
 	{
 		mobj_t *flicky = P_InternalFlickySpawn(actor, locvar1, 1, false, 0);
-		P_SetTarget(&flicky->target, actor);
-		P_SetTarget(&actor->tracer, flicky);
+		if (!P_MobjWasRemoved(flicky))
+		{
+			P_SetTarget(&flicky->target, actor);
+			P_SetTarget(&actor->tracer, flicky);
+		}
 
 		if (actor->spawnpoint)
 		{
@@ -11975,18 +12159,21 @@ void A_FlickyCenter(mobj_t *actor)
 			locvar1 = flickytype;
 		}
 
-		if (actor->flags & MF_GRENADEBOUNCE) // in-place
-			actor->tracer->fuse = 0;
-		else if (actor->flags & MF_SLIDEME) // aimless
+		if (!P_MobjWasRemoved(flicky))
 		{
-			actor->tracer->fuse = 0; // less than 2*TICRATE means move aimlessly.
-			actor->tracer->angle = P_RandomKey(180)*ANG2;
-		}
-		else //orbit
-			actor->tracer->fuse = FRACUNIT;
+			if (actor->flags & MF_GRENADEBOUNCE) // in-place
+				actor->tracer->fuse = 0;
+			else if (actor->flags & MF_SLIDEME) // aimless
+			{
+				actor->tracer->fuse = 0; // less than 2*TICRATE means move aimlessly.
+				actor->tracer->angle = P_RandomKey(180)*ANG2;
+			}
+			else //orbit
+				actor->tracer->fuse = FRACUNIT;
 
-		if (locvar1 == MT_FLICKY_08)
-			P_InternalFlickySetColor(actor->tracer, actor->extravalue2);
+			if (locvar1 == MT_FLICKY_08)
+				P_InternalFlickySetColor(actor->tracer, actor->extravalue2);
+		}
 
 		actor->extravalue2 = 0;
 	}
@@ -11997,7 +12184,8 @@ void A_FlickyCenter(mobj_t *actor)
 		fixed_t originy = actor->movefactor;
 		fixed_t originz = actor->watertop;
 
-		actor->tracer->fuse = FRACUNIT;
+		if (!P_MobjWasRemoved(actor->tracer))
+			actor->tracer->fuse = FRACUNIT;
 
 		// Impose default home radius if flicky orbits around player
 		if (!actor->extravalue1)
@@ -12029,6 +12217,8 @@ void P_InternalFlickyBubble(mobj_t *actor)
 			return;
 
 		overlay = P_SpawnMobj(actor->x, actor->y, actor->z, MT_OVERLAY);
+		if (P_MobjWasRemoved(overlay))
+			return;
 		P_SetMobjStateNF(overlay, mobjinfo[actor->type].raisestate);
 		P_SetTarget(&actor->tracer, overlay);
 		P_SetTarget(&overlay->target, actor);
@@ -12403,7 +12593,8 @@ void A_FlameParticle(mobj_t *actor)
 		P_RandomRange(rad, -rad)<<FRACBITS,
 		P_RandomRange(hei/2, hei)<<FRACBITS,
 		type);
-	P_SetObjectMomZ(particle, 2<<FRACBITS, false);
+	if (!P_MobjWasRemoved(particle))
+		P_SetObjectMomZ(particle, 2<<FRACBITS, false);
 }
 
 // Function: A_FadeOverlay
@@ -12422,6 +12613,8 @@ void A_FadeOverlay(mobj_t *actor)
 		return;
 
 	fade = P_SpawnGhostMobj(actor);
+	if (P_MobjWasRemoved(fade))
+		return;
 	fade->frame = actor->frame;
 
 	if (!(locvar1 & 1))
@@ -12589,6 +12782,8 @@ void A_MineExplode(mobj_t *actor)
 				actor->y+P_RandomRange(-dist, dist)*FRACUNIT,
 				actor->z+P_RandomRange(((actor->eflags & MFE_UNDERWATER) ? -dist : 0), dist)*FRACUNIT,
 				type);
+			if (P_MobjWasRemoved(b))
+				continue;
 			fixed_t dx = b->x - actor->x, dy = b->y - actor->y, dz = b->z - actor->z;
 			fixed_t dm = P_AproxDistance(dz, P_AproxDistance(dy, dx));
 			b->momx = FixedDiv(dx, dm)*3;
@@ -12720,6 +12915,8 @@ void A_SpawnParticleRelative(mobj_t *actor)
 	mo = P_SpawnMobj(actor->x + FixedMul(x<<FRACBITS, actor->scale),
 		actor->y + FixedMul(y<<FRACBITS, actor->scale),
 		(actor->eflags & MFE_VERTICALFLIP) ? ((actor->z + actor->height - mobjinfo[MT_PARTICLE].height) - FixedMul(z<<FRACBITS, actor->scale)) : (actor->z + FixedMul(z<<FRACBITS, actor->scale)), MT_PARTICLE);
+	if (P_MobjWasRemoved(mo))
+		return;
 
 	// Spawn objects with an angle matching the spawner's, rather than spawning Eastwards - Monster Iestyn
 	mo->angle = actor->angle;
@@ -13114,11 +13311,14 @@ void A_DoNPCSkid(mobj_t *actor)
 	if (!(leveltime % 3))
 	{
 		mobj_t *particle = P_SpawnMobjFromMobj(actor, 0, 0, 0, MT_SPINDUST);
-		particle->tics = 10;
+		if (!P_MobjWasRemoved(particle))
+		{
+			particle->tics = 10;
 
-		P_SetScale(particle, 2*actor->scale/3);
-		particle->destscale = actor->scale;
-		P_SetObjectMomZ(particle, FRACUNIT, false);
+			P_SetScale(particle, 2*actor->scale/3);
+			particle->destscale = actor->scale;
+			P_SetObjectMomZ(particle, FRACUNIT, false);
+		}
 	}
 }
 
@@ -13421,11 +13621,18 @@ void A_Boss5MakeJunk(mobj_t *actor)
 			if (actor->extravalue2 > 10)
 			{
 				mobj_t *front = P_SpawnMobjFromMobj(actor, 0, 0, 0, MT_VWREF);
-				broked = P_SpawnMobjFromMobj(front, 0, 0, 0, MT_VWREB);
-				front->z = broked->z = front->z - broked->height;
-				P_SetObjectMomZ(front, (4<<FRACBITS), false);
-				broked->momz = front->momz;
-				broked->fuse = front->fuse = (actor->height+(2*front->height))/front->momz;
+				if (!P_MobjWasRemoved(front))
+				{
+					P_SetObjectMomZ(front, (4<<FRACBITS), false);
+					broked = P_SpawnMobjFromMobj(front, 0, 0, 0, MT_VWREB);
+					if (!P_MobjWasRemoved(broked))
+					{
+						front->z -= broked->height;
+						broked->z = front->z;
+						broked->momz = front->momz;
+						broked->fuse = front->fuse = (actor->height+(2*front->height))/front->momz;
+					}
+				}
 			}
 			if (!(actor->colorized = !actor->colorized))
 				actor->frame |= FF_FULLBRIGHT;
@@ -13439,6 +13646,8 @@ void A_Boss5MakeJunk(mobj_t *actor)
 	while (i--)
 	{
 		broked = P_SpawnMobjFromMobj(actor, 0, 0, FRACUNIT, MT_BROKENROBOT);
+		if (P_MobjWasRemoved(broked))
+			continue;
 		if (locvar2 & 2)
 			broked->fuse = TICRATE;
 		else
@@ -13456,13 +13665,17 @@ void A_Boss5MakeJunk(mobj_t *actor)
 	if (locvar2 & 2)
 	{
 		broked = P_SpawnMobjFromMobj(actor, 0, 0, 64<<FRACBITS, MT_GHOST);
-		S_StartSound(broked, sfx_alart);
-		broked->fuse = states[S_FANG_INTRO12].tics+10;
-		P_SetMobjState(broked, S_ALART1);
+		if (!P_MobjWasRemoved(broked))
+		{
+			S_StartSound(broked, sfx_alart);
+			broked->fuse = states[S_FANG_INTRO12].tics+10;
+			P_SetMobjState(broked, S_ALART1);
+		}
 	}
 	else if (locvar2 & 1)
 	{
-		broked->z += broked->momz;
+		if (!P_MobjWasRemoved(broked))
+			broked->z += broked->momz;
 		S_StartSound(actor, sfx_s3kccs);
 		actor->flags &= ~MF_NOCLIPTHING;
 	}
@@ -13521,6 +13734,8 @@ static void P_DustRing(mobjtype_t mobjtype, UINT32 div, fixed_t x, fixed_t y, fi
 			z,
 			mobjtype
 			);
+		if (P_MobjWasRemoved(dust))
+			continue;
 
 		dust->angle = ang*i + ANGLE_90;
 		P_SetScale(dust, FixedMul(initscale, scale));
@@ -13609,7 +13824,7 @@ static boolean PIT_DustDevilLaunch(mobj_t *thing)
 			player->powers[pw_carry] = CR_DUSTDEVIL;
 			player->powers[pw_nocontrol] = 2;
 			P_SetTarget(&thing->tracer, dustdevil);
-			P_SetPlayerMobjState(thing, S_PLAY_PAIN);
+			P_SetMobjState(thing, S_PLAY_PAIN);
 
 			if (dist > dragamount)
 			{
@@ -13632,7 +13847,7 @@ static boolean PIT_DustDevilLaunch(mobj_t *thing)
 			player->powers[pw_nocontrol] = 0;
 			P_SetTarget(&thing->tracer, NULL);
 			S_StartSound(thing, sfx_wdjump);
-			P_SetPlayerMobjState(thing, S_PLAY_FALL);
+			P_SetMobjState(thing, S_PLAY_FALL);
 		}
 
 		thing->momz = thrust;
@@ -13673,9 +13888,12 @@ void A_DustDevilThink(mobj_t *actor)
 	if (P_IsObjectOnGround(actor)) {
 		angle_t dustang = ((P_RandomRange(0, 7)*ANGLE_45)>>ANGLETOFINESHIFT) & FINEMASK;
 		mobj_t *dust = P_SpawnMobj(actor->x + 96 * FixedMul(scale, FINECOSINE(dustang)), actor->y + 96 * FixedMul(scale, FINESINE(dustang)), actor->z, MT_ARIDDUST);
-		P_SetMobjState(dust, dust->info->spawnstate + P_RandomRange(0, 2));
-		dust->destscale = scale * 3;
-		P_SetScale(dust, dust->destscale);
+		if (!P_MobjWasRemoved(dust))
+		{
+			P_SetMobjState(dust, dust->info->spawnstate + P_RandomRange(0, 2));
+			dust->destscale = scale * 3;
+			P_SetScale(dust, dust->destscale);
+		}
 	}
 
 	actor->extravalue1++;
@@ -13691,6 +13909,8 @@ void A_DustDevilThink(mobj_t *actor)
 			fixed_t pz = actor->z;
 
 			layer = P_SpawnMobj(px, py, pz, MT_DUSTLAYER);
+			if (P_MobjWasRemoved(layer))
+				continue;
 			layer->momz = 5 * scale;
 			layer->angle = ANGLE_90 + ANGLE_90*i;
 			layer->extravalue1 = TICRATE * 3;
@@ -13876,6 +14096,8 @@ void A_DebrisRandom(mobj_t *actor)
 static mobj_t *P_TrainSeg(mobj_t *src, fixed_t x, fixed_t y, fixed_t z, angle_t ang, spritenum_t spr, UINT32 frame)
 {
 	mobj_t *s = P_SpawnMobj(x, y, z, MT_TRAINSEG);
+	if (P_MobjWasRemoved(s))
+		return NULL;
 	s->fuse = 16*TICRATE;
 	s->sprite = spr;
 	s->frame = frame|FF_PAPERSPRITE;
@@ -14028,7 +14250,7 @@ static void P_SnapperLegPlace(mobj_t *mo)
 
 	// Move as many legs as available.
 	seg = seg->tracer;
-	do
+	while (seg)
 	{
 		o1 = seg->extravalue1;
 		o2 = seg->extravalue2;
@@ -14053,7 +14275,7 @@ static void P_SnapperLegPlace(mobj_t *mo)
 		seg->angle = R_PointToAngle2(mo->x, mo->y, seg->x, seg->y);
 
 		seg = seg->tracer;
-	} while (seg);
+	}
 }
 
 //
@@ -14077,6 +14299,12 @@ void A_SnapperSpawn(mobj_t *actor)
 
 	// It spawns 1 head.
 	seg = P_SpawnMobjFromMobj(actor, 0, 0, 0, headtype);
+	if (P_MobjWasRemoved(seg))
+	{
+		// if we can't spawn the head, don't spawn the snapper at all
+		P_RemoveMobj(actor);
+		return;
+	}
 	P_SetTarget(&ptr->tracer, seg);
 	ptr = seg;
 
@@ -14084,6 +14312,8 @@ void A_SnapperSpawn(mobj_t *actor)
 	for (i = 1; i <= 4; i++)
 	{
 		seg = P_SpawnMobjFromMobj(actor, 0, 0, 0, legtype);
+		if (P_MobjWasRemoved(seg))
+			continue;
 		P_SetTarget(&ptr->tracer, seg);
 		ptr = seg;
 
@@ -14185,7 +14415,8 @@ void A_SnapperThinker(mobj_t *actor)
 		if (actor->reactiontime < 4)
 		{
 			mobj_t *dust = P_SpawnMobj(x0, y0, actor->z, MT_SPINDUST);
-			P_Thrust(dust, ang + ANGLE_180 + FixedAngle(P_RandomRange(-20, 20)*FRACUNIT), speed*FRACUNIT);
+			if (!P_MobjWasRemoved(dust))
+				P_Thrust(dust, ang + ANGLE_180 + FixedAngle(P_RandomRange(-20, 20)*FRACUNIT), speed*FRACUNIT);
 		}
 
 		if (actor->extravalue2 == 0)
@@ -14295,6 +14526,8 @@ void A_MinecartSparkThink(mobj_t *actor)
 	for (i = 1; i <= 8; i++)
 	{
 		mobj_t *trail = P_SpawnMobj(actor->x - dx*i, actor->y - dy*i, actor->z - dz*i, MT_PARTICLE);
+		if (P_MobjWasRemoved(trail))
+			continue;
 		trail->tics = 2;
 		trail->sprite = actor->sprite;
 		P_SetScale(trail, trail->scale/4);
@@ -14380,7 +14613,8 @@ void A_LavafallLava(mobj_t *actor)
 		return;
 
 	lavafall = P_SpawnMobjFromMobj(actor, 0, 0, -8*FRACUNIT, MT_LAVAFALL_LAVA);
-	lavafall->momz = -P_MobjFlip(actor)*25*FRACUNIT;
+	if (!P_MobjWasRemoved(lavafall))
+		lavafall->momz = -P_MobjFlip(actor)*25*FRACUNIT;
 }
 
 // Function: A_FallingLavaCheck
@@ -14455,9 +14689,18 @@ void A_SpawnPterabytes(mobj_t *actor)
 		c = FINECOSINE(fa);
 		s = FINESINE(fa);
 		waypoint = P_SpawnMobjFromMobj(actor, FixedMul(c, rad), FixedMul(s, rad), 0, MT_PTERABYTEWAYPOINT);
+		if (P_MobjWasRemoved(waypoint))
+			continue;
+
 		waypoint->angle = ang + ANGLE_90;
 		P_SetTarget(&waypoint->tracer, actor);
 		ptera = P_SpawnMobjFromMobj(waypoint, 0, 0, 0, MT_PTERABYTE);
+		if (P_MobjWasRemoved(ptera))
+		{
+			P_RemoveMobj(waypoint);
+			continue;
+		}
+
 		ptera->angle = waypoint->angle;
 		P_SetTarget(&ptera->tracer, waypoint);
 		ptera->extravalue1 = 0;
@@ -14505,14 +14748,17 @@ void A_RolloutSpawn(mobj_t *actor)
 		|| P_MobjWasRemoved(actor->target)
 		|| P_AproxDistance(actor->x - actor->target->x, actor->y - actor->target->y) > locvar1)
 	{
-		actor->target = P_SpawnMobj(actor->x, actor->y, actor->z, locvar2);
-		actor->target->flags2 |= (actor->flags2 & (MF2_AMBUSH | MF2_OBJECTFLIP)) | MF2_SLIDEPUSH;
-		actor->target->eflags |= (actor->eflags & MFE_VERTICALFLIP);
-
-		if (actor->target->flags2 & MF2_AMBUSH)
+		P_SetTarget(&actor->target, P_SpawnMobj(actor->x, actor->y, actor->z, locvar2));
+		if (!P_MobjWasRemoved(actor->target))
 		{
-			actor->target->color = SKINCOLOR_SUPERRUST3;
-			actor->target->colorized = true;
+			actor->target->flags2 |= (actor->flags2 & (MF2_AMBUSH | MF2_OBJECTFLIP)) | MF2_SLIDEPUSH;
+			actor->target->eflags |= (actor->eflags & MFE_VERTICALFLIP);
+
+			if (actor->target->flags2 & MF2_AMBUSH)
+			{
+				actor->target->color = SKINCOLOR_SUPERRUST3;
+				actor->target->colorized = true;
+			}
 		}
 	}
 }
@@ -14633,6 +14879,8 @@ void A_DragonbomberSpawn(mobj_t *actor)
 		x = P_ReturnThrustX(mo, mo->angle, -mo->radius << 1);
 		y = P_ReturnThrustY(mo, mo->angle, -mo->radius << 1);
 		segment = P_SpawnMobjFromMobj(mo, x, y, 0, MT_DRAGONTAIL);
+		if (P_MobjWasRemoved(segment))
+			continue;
 		P_SetTarget(&segment->target, mo);
 		P_SetTarget(&mo->tracer, segment);
 		segment->angle = mo->angle;
@@ -14641,6 +14889,8 @@ void A_DragonbomberSpawn(mobj_t *actor)
 	for (i = 0; i < 2; i++) // spawn wings
 	{
 		mo = P_SpawnMobjFromMobj(actor, 0, 0, 0, MT_DRAGONWING);
+		if (P_MobjWasRemoved(mo))
+			continue;
 		P_SetTarget(&mo->target, actor);
 		mo->movedir = ANGLE_90 + i * ANGLE_180;
 	}
diff --git a/src/p_floor.c b/src/p_floor.c
index 38f0c5a0fbc496564e7b69c96f7cacd8a93287f6..ede2a86abb416e72d1831928845e30c35e135ff7 100644
--- a/src/p_floor.c
+++ b/src/p_floor.c
@@ -1921,6 +1921,9 @@ void EV_CrumbleChain(sector_t *sec, ffloor_t *rover)
 				for (c = topz; c > bottomz; c -= spacing)
 				{
 					spawned = P_SpawnMobj(a, b, c, type);
+					if (P_MobjWasRemoved(spawned))
+						continue;
+
 					spawned->angle += P_RandomKey(36)*ANG10; // irrelevant for default objects but might make sense for some custom ones
 
 					if (fromcenter)
diff --git a/src/p_inter.c b/src/p_inter.c
index 271b6ebc45a5bf42d048b39ace8db61d212741c8..9a6f0ad06f6ea6ea65a5eda91bee070fb5827585 100644
--- a/src/p_inter.c
+++ b/src/p_inter.c
@@ -28,6 +28,7 @@
 #include "m_misc.h"
 #include "v_video.h" // video flags for CEchos
 #include "f_finale.h"
+#include "netcode/net_command.h"
 
 // CTF player names
 #define CTFTEAMCODE(pl) pl->ctfteam ? (pl->ctfteam == 1 ? "\x85" : "\x84") : ""
@@ -255,6 +256,9 @@ void P_DoNightsScore(player_t *player)
 		player->linktimer = nightslinktics;
 	}
 
+	if (P_MobjWasRemoved(dummymo))
+		return;
+
 	// Award 10-100 score, doubled if bonus time is active
 	P_AddPlayerScore(player, min(player->linkcount,10)*(player->bonustime ? 20 : 10));
 	P_SetMobjState(dummymo, (player->bonustime ? dummymo->info->xdeathstate : dummymo->info->spawnstate) + min(player->linkcount,10)-1);
@@ -525,7 +529,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				else if (player->pflags & PF_GLIDING && !P_IsObjectOnGround(toucher))
 				{
 					player->pflags &= ~(PF_GLIDING|PF_JUMPED|PF_NOJUMPDAMAGE);
-					P_SetPlayerMobjState(toucher, S_PLAY_FALL);
+					P_SetMobjState(toucher, S_PLAY_FALL);
 					toucher->momz += P_MobjFlip(toucher) * (player->speed >> 3);
 					toucher->momx = 7*toucher->momx>>3;
 					toucher->momy = 7*toucher->momy>>3;
@@ -1245,7 +1249,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 
 					P_ResetPlayer(player);
 
-					P_SetPlayerMobjState(toucher, S_PLAY_FALL);
+					P_SetMobjState(toucher, S_PLAY_FALL);
 				}
 			}
 			return;
@@ -1300,7 +1304,8 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			{
 				// A flicky orbits us now
 				mobj_t *flickyobj = P_SpawnMobj(toucher->x, toucher->y, toucher->z + toucher->info->height, MT_NIGHTOPIANHELPER);
-				P_SetTarget(&flickyobj->target, toucher);
+				if (!P_MobjWasRemoved(flickyobj))
+					P_SetTarget(&flickyobj->target, toucher);
 
 				player->powers[pw_nights_helper] = (UINT16)special->info->speed;
 			}
@@ -1311,7 +1316,8 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 					if (playeringame[i] && players[i].mo && players[i].powers[pw_carry] == CR_NIGHTSMODE) {
 						players[i].powers[pw_nights_helper] = (UINT16)special->info->speed;
 						flickyobj = P_SpawnMobj(players[i].mo->x, players[i].mo->y, players[i].mo->z + players[i].mo->info->height, MT_NIGHTOPIANHELPER);
-						P_SetTarget(&flickyobj->target, players[i].mo);
+						if (!P_MobjWasRemoved(flickyobj))
+							P_SetTarget(&flickyobj->target, players[i].mo);
 					}
 				if (special->info->deathsound != sfx_None)
 					S_StartSound(NULL, special->info->deathsound);
@@ -1552,7 +1558,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				if (player->pflags & PF_GLIDING && !P_IsObjectOnGround(toucher))
 				{
 					player->pflags &= ~(PF_GLIDING|PF_JUMPED|PF_NOJUMPDAMAGE);
-					P_SetPlayerMobjState(toucher, S_PLAY_FALL);
+					P_SetMobjState(toucher, S_PLAY_FALL);
 					toucher->momz += P_MobjFlip(toucher) * (player->speed >> 3);
 					toucher->momx = 7*toucher->momx>>3;
 					toucher->momy = 7*toucher->momy>>3;
@@ -1603,7 +1609,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 					if (player->pflags & PF_GLIDING && !P_IsObjectOnGround(toucher))
 					{
 						player->pflags &= ~(PF_GLIDING|PF_JUMPED|PF_NOJUMPDAMAGE);
-						P_SetPlayerMobjState(toucher, S_PLAY_FALL);
+						P_SetMobjState(toucher, S_PLAY_FALL);
 						toucher->momz += P_MobjFlip(toucher) * (player->speed >> 3);
 						toucher->momx = 7*toucher->momx>>3;
 						toucher->momy = 7*toucher->momy>>3;
@@ -1639,7 +1645,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			special->momx = special->momy = 0;
 
 			// Buenos Dias Mandy
-			P_SetPlayerMobjState(toucher, S_PLAY_STUN);
+			P_SetMobjState(toucher, S_PLAY_STUN);
 			player->pflags &= ~PF_APPLYAUTOBRAKE;
 			P_ResetPlayer(player);
 			player->drawangle = special->angle + ANGLE_180;
@@ -1718,7 +1724,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				{
 					player->powers[pw_carry] = CR_MACESPIN;
 					S_StartSound(toucher, sfx_spin);
-					P_SetPlayerMobjState(toucher, S_PLAY_ROLL);
+					P_SetMobjState(toucher, S_PLAY_ROLL);
 				}
 				else
 					player->powers[pw_carry] = CR_GENERIC;
@@ -1779,7 +1785,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			{
 				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_SetMobjState(toucher, S_PLAY_GASP);
 				P_ResetPlayer(player);
 			}
 
@@ -1820,9 +1826,12 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			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);
-				mcart->angle = toucher->angle = player->drawangle = special->angle;
-				mcart->friction = FRACUNIT;
+				if (!P_MobjWasRemoved(mcart))
+				{
+					P_SetTarget(&mcart->target, toucher);
+					mcart->angle = toucher->angle = player->drawangle = special->angle;
+					mcart->friction = FRACUNIT;
+				}
 
 				P_ResetPlayer(player);
 				player->pflags |= PF_JUMPDOWN;
@@ -1846,7 +1855,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 				toucher->momz = toucher->tracer->momz + P_AproxDistance(toucher->tracer->momx, toucher->tracer->momy)/2;
 				P_ResetPlayer(player);
 				player->pflags &= ~PF_APPLYAUTOBRAKE;
-				P_SetPlayerMobjState(toucher, S_PLAY_FALL);
+				P_SetMobjState(toucher, S_PLAY_FALL);
 				P_SetTarget(&toucher->tracer->target, NULL);
 				P_KillMobj(toucher->tracer, toucher, special, 0);
 				P_SetTarget(&toucher->tracer, NULL);
@@ -2592,7 +2601,8 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 							break;
 					}
 
-					P_SetMobjState(scoremobj, scorestate);
+					if (!P_MobjWasRemoved(scoremobj))
+						P_SetMobjState(scoremobj, scorestate);
 
 					source->player->scoreadd = locscoreadd;
 				}
@@ -2630,7 +2640,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 			}
 		}
 
-		target->color = target->player->skincolor;
+		target->color = P_GetPlayerColor(target->player);
 		target->colorized = false;
 		G_GhostAddColor(GHC_NORMAL);
 
@@ -2753,6 +2763,8 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 				mo = P_SpawnMobj(inflictor->x + inflictor->momx, inflictor->y + inflictor->momy, inflictor->z + (inflictor->height / 2) + inflictor->momz, MT_EXTRALARGEBUBBLE);
 			else
 				mo = P_SpawnMobj(target->x, target->y, target->z, MT_EXTRALARGEBUBBLE);
+			if (P_MobjWasRemoved(mo))
+				break;
 			mo->destscale = target->scale;
 			P_SetScale(mo, mo->destscale);
 			P_SetMobjState(mo, mo->info->raisestate);
@@ -2831,8 +2843,11 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 					mo->angle = FixedAngle((P_RandomKey(36)*10)<<FRACBITS);
 
 					mo2 = P_SpawnMobjFromMobj(mo, 0, 0, 0, MT_BOSSJUNK);
-					mo2->angle = mo->angle;
-					P_SetMobjState(mo2, S_BOSSSEBH2);
+					if (!P_MobjWasRemoved(mo2))
+					{
+						mo2->angle = mo->angle;
+						P_SetMobjState(mo2, S_BOSSSEBH2);
+					}
 
 					if (++i == 2) // we've already removed 2 of these, let's stop now
 						break;
@@ -2931,7 +2946,10 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 			if (flip)
 				momz *= -1;
 #define makechunk(angtweak, xmov, ymov) \
+			do {\
 			chunk = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_SPIKE);\
+			if (P_MobjWasRemoved(chunk))\
+				break;\
 			P_SetMobjState(chunk, target->info->xdeathstate);\
 			chunk->health = 0;\
 			chunk->angle = angtweak;\
@@ -2941,7 +2959,8 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 			chunk->y += ymov;\
 			P_SetThingPosition(chunk);\
 			P_InstaThrust(chunk,chunk->angle, 4*scale);\
-			chunk->momz = momz
+			chunk->momz = momz;\
+			} while (0)
 
 			makechunk(ang + ANGLE_180, -xoffs, -yoffs);
 			makechunk(ang, xoffs, yoffs);
@@ -2954,20 +2973,23 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 			momz *= -1;
 
 		chunk = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_SPIKE);
-		P_SetMobjState(chunk, target->info->deathstate);
-		chunk->health = 0;
-		chunk->angle = ang + ANGLE_180;
-		P_UnsetThingPosition(chunk);
-		chunk->flags = MF_NOCLIP;
-		chunk->x -= xoffs;
-		chunk->y -= yoffs;
-		if (flip)
-			chunk->z -= 12*scale;
-		else
-			chunk->z += 12*scale;
-		P_SetThingPosition(chunk);
-		P_InstaThrust(chunk, chunk->angle, 2*scale);
-		chunk->momz = momz;
+		if (!P_MobjWasRemoved(chunk))
+		{
+			P_SetMobjState(chunk, target->info->deathstate);
+			chunk->health = 0;
+			chunk->angle = ang + ANGLE_180;
+			P_UnsetThingPosition(chunk);
+			chunk->flags = MF_NOCLIP;
+			chunk->x -= xoffs;
+			chunk->y -= yoffs;
+			if (flip)
+				chunk->z -= 12*scale;
+			else
+				chunk->z += 12*scale;
+			P_SetThingPosition(chunk);
+			P_InstaThrust(chunk, chunk->angle, 2*scale);
+			chunk->momz = momz;
+		}
 
 		P_SetMobjState(target, target->info->deathstate);
 		target->health = 0;
@@ -2976,7 +2998,10 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 		target->flags = MF_NOCLIP;
 		target->x += xoffs;
 		target->y += yoffs;
-		target->z = chunk->z;
+		if (flip)
+			target->z -= 12*scale;
+		else
+			target->z += 12*scale;
 		P_SetThingPosition(target);
 		P_InstaThrust(target, target->angle, 2*scale);
 		target->momz = momz;
@@ -2999,21 +3024,25 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 			sprflip = P_RandomChance(FRACUNIT/2);
 
 #define makechunk(angtweak, xmov, ymov) \
-			chunk = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_WALLSPIKE);\
-			P_SetMobjState(chunk, target->info->xdeathstate);\
-			chunk->health = 0;\
-			chunk->angle = target->angle;\
-			P_UnsetThingPosition(chunk);\
-			chunk->flags = MF_NOCLIP;\
-			chunk->x += xmov - forwardxoffs;\
-			chunk->y += ymov - forwardyoffs;\
-			P_SetThingPosition(chunk);\
-			P_InstaThrust(chunk, angtweak, 4*scale);\
-			chunk->momz = P_RandomRange(5, 7)*scale;\
-			if (flip)\
-				chunk->momz *= -1;\
-			if (sprflip)\
-				chunk->frame |= FF_VERTICALFLIP
+			do {\
+				chunk = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_WALLSPIKE);\
+				if (P_MobjWasRemoved(chunk))\
+					break;\
+				P_SetMobjState(chunk, target->info->xdeathstate);\
+				chunk->health = 0;\
+				chunk->angle = target->angle;\
+				P_UnsetThingPosition(chunk);\
+				chunk->flags = MF_NOCLIP;\
+				chunk->x += xmov - forwardxoffs;\
+				chunk->y += ymov - forwardyoffs;\
+				P_SetThingPosition(chunk);\
+				P_InstaThrust(chunk, angtweak, 4*scale);\
+				chunk->momz = P_RandomRange(5, 7)*scale;\
+				if (flip)\
+					chunk->momz *= -1;\
+				if (sprflip)\
+					chunk->frame |= FF_VERTICALFLIP;\
+			} while (0)
 
 			makechunk(ang + ANGLE_180, -xoffs, -yoffs);
 			sprflip = !sprflip;
@@ -3025,21 +3054,23 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 		sprflip = P_RandomChance(FRACUNIT/2);
 
 		chunk = P_SpawnMobjFromMobj(target, 0, 0, 0, MT_WALLSPIKE);
-
-		P_SetMobjState(chunk, target->info->deathstate);
-		chunk->health = 0;
-		chunk->angle = target->angle;
-		P_UnsetThingPosition(chunk);
-		chunk->flags = MF_NOCLIP;
-		chunk->x += forwardxoffs - xoffs;
-		chunk->y += forwardyoffs - yoffs;
-		P_SetThingPosition(chunk);
-		P_InstaThrust(chunk, ang + ANGLE_180, 2*scale);
-		chunk->momz = P_RandomRange(5, 7)*scale;
-		if (flip)
-			chunk->momz *= -1;
-		if (sprflip)
-			chunk->frame |= FF_VERTICALFLIP;
+		if (!P_MobjWasRemoved(chunk))
+		{
+			P_SetMobjState(chunk, target->info->deathstate);
+			chunk->health = 0;
+			chunk->angle = target->angle;
+			P_UnsetThingPosition(chunk);
+			chunk->flags = MF_NOCLIP;
+			chunk->x += forwardxoffs - xoffs;
+			chunk->y += forwardyoffs - yoffs;
+			P_SetThingPosition(chunk);
+			P_InstaThrust(chunk, ang + ANGLE_180, 2*scale);
+			chunk->momz = P_RandomRange(5, 7)*scale;
+			if (flip)
+				chunk->momz *= -1;
+			if (sprflip)
+				chunk->frame |= FF_VERTICALFLIP;
+		}
 
 		P_SetMobjState(target, target->info->deathstate);
 		target->health = 0;
@@ -3058,9 +3089,9 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
 	else if (target->player)
 	{
 		if (damagetype == DMG_DROWNED || damagetype == DMG_SPACEDROWN)
-			P_SetPlayerMobjState(target, target->info->xdeathstate);
+			P_SetMobjState(target, target->info->xdeathstate);
 		else
-			P_SetPlayerMobjState(target, target->info->deathstate);
+			P_SetMobjState(target, target->info->deathstate);
 	}
 	else
 #ifdef DEBUG_NULL_DEATHSTATE
@@ -3114,7 +3145,7 @@ static void P_NiGHTSDamage(mobj_t *target, mobj_t *source)
 		}
 
 		player->powers[pw_flashing] = flashingtics;
-		P_SetPlayerMobjState(target, S_PLAY_NIGHTS_STUN);
+		P_SetMobjState(target, S_PLAY_NIGHTS_STUN);
 		S_StartSound(target, sfx_nghurt);
 
 		player->mo->spriteroll = 0;
@@ -3323,7 +3354,7 @@ static void P_KillPlayer(player_t *player, mobj_t *source, INT32 damage)
 
 	// Get rid of shield
 	player->powers[pw_shield] = SH_NONE;
-	player->mo->color = player->skincolor;
+	player->mo->color = P_GetPlayerColor(player);
 
 	// Get rid of emeralds
 	player->powers[pw_emeralds] = 0;
@@ -3335,7 +3366,7 @@ static void P_KillPlayer(player_t *player, mobj_t *source, INT32 damage)
 	if (!player->spectator)
 		player->mo->flags2 &= ~MF2_DONTDRAW;
 
-	P_SetPlayerMobjState(player->mo, player->mo->info->deathstate);
+	P_SetMobjState(player->mo, player->mo->info->deathstate);
 	if ((gametyperules & GTR_TEAMFLAGS) && (player->gotflag & (GF_REDFLAG|GF_BLUEFLAG)))
 	{
 		P_PlayerFlagBurst(player, false);
@@ -3409,7 +3440,7 @@ static void P_SuperDamage(player_t *player, mobj_t *inflictor, mobj_t *source, I
 
 	P_InstaThrust(player->mo, ang, fallbackspeed);
 
-	P_SetPlayerMobjState(player->mo, S_PLAY_STUN);
+	P_SetMobjState(player->mo, S_PLAY_STUN);
 
 	P_ResetPlayer(player);
 
@@ -3440,7 +3471,7 @@ void P_RemoveShield(player_t *player)
 	{ // Second layer shields
 		if (((player->powers[pw_shield] & SH_STACK) == SH_FIREFLOWER) && !(player->powers[pw_super] || (mariomode && player->powers[pw_invulnerability])))
 		{
-			player->mo->color = player->skincolor;
+			player->mo->color = P_GetPlayerColor(player);
 			G_GhostAddColor(GHC_NORMAL);
 		}
 		player->powers[pw_shield] = SH_NONE;
@@ -3920,6 +3951,8 @@ void P_PlayerRingBurst(player_t *player, INT32 num_rings)
 			z += player->mo->height - mobjinfo[objType].height;
 
 		mo = P_SpawnMobj(player->mo->x, player->mo->y, z, objType);
+		if (P_MobjWasRemoved(mo))
+			continue;
 
 		mo->fuse = 8*TICRATE;
 		P_SetTarget(&mo->target, player->mo);
@@ -4053,6 +4086,9 @@ void P_PlayerWeaponPanelBurst(player_t *player)
 			z += player->mo->height - mobjinfo[weptype].height;
 
 		mo = P_SpawnMobj(player->mo->x, player->mo->y, z, weptype);
+		if (P_MobjWasRemoved(mo))
+			continue;
+
 		mo->reactiontime = ammoamt;
 		mo->flags2 |= MF2_DONTRESPAWN;
 		mo->flags &= ~(MF_NOGRAVITY|MF_NOCLIPHEIGHT);
@@ -4086,13 +4122,13 @@ void P_PlayerWeaponAmmoBurst(player_t *player)
 	mobj_t *mo;
 	angle_t fa;
 	fixed_t ns;
-	INT32 i = 0;
+	INT32 i;
 	fixed_t z;
 
 	mobjtype_t weptype = 0;
 	powertype_t power = 0;
 
-	while (true)
+	for (i = 0;; i++)
 	{
 		if (player->powers[pw_bouncering])
 		{
@@ -4137,6 +4173,8 @@ void P_PlayerWeaponAmmoBurst(player_t *player)
 			z += player->mo->height - mobjinfo[weptype].height;
 
 		mo = P_SpawnMobj(player->mo->x, player->mo->y, z, weptype);
+		if (P_MobjWasRemoved(mo))
+			continue;
 		mo->health = player->powers[power];
 		mo->flags2 |= MF2_DONTRESPAWN;
 		mo->flags &= ~(MF_NOGRAVITY|MF_NOCLIPHEIGHT);
@@ -4162,8 +4200,6 @@ void P_PlayerWeaponAmmoBurst(player_t *player)
 
 		if (i & 1)
 			P_SetObjectMomZ(mo, 3*FRACUNIT, true);
-
-		i++;
 	}
 }
 
@@ -4188,45 +4224,50 @@ void P_PlayerWeaponPanelOrAmmoBurst(player_t *player)
 		player->ringweapons &= ~rwflag; \
 		SETUP_DROP(pickup) \
 		mo = P_SpawnMobj(player->mo->x, player->mo->y, z, pickup); \
-		mo->reactiontime = 0; \
-		mo->flags2 |= MF2_DONTRESPAWN; \
-		mo->flags &= ~(MF_NOGRAVITY|MF_NOCLIPHEIGHT); \
-		P_SetTarget(&mo->target, player->mo); \
-		mo->fuse = 12*TICRATE; \
-		mo->destscale = player->mo->scale; \
-		P_SetScale(mo, player->mo->scale); \
-		mo->momx = FixedMul(FINECOSINE(fa),ns); \
-		if (!(twodlevel || (player->mo->flags2 & MF2_TWOD))) \
-			mo->momy = FixedMul(FINESINE(fa),ns); \
-		P_SetObjectMomZ(mo, 4*FRACUNIT, false); \
-		if (i & 1) \
-			P_SetObjectMomZ(mo, 4*FRACUNIT, true); \
-		if (player->mo->eflags & MFE_VERTICALFLIP) \
-			mo->flags2 |= MF2_OBJECTFLIP; \
-		++i; \
+		if (!P_MobjWasRemoved(mo)) \
+		{ \
+			mo->reactiontime = 0; \
+			mo->flags2 |= MF2_DONTRESPAWN; \
+			mo->flags &= ~(MF_NOGRAVITY|MF_NOCLIPHEIGHT); \
+			P_SetTarget(&mo->target, player->mo); \
+			mo->fuse = 12*TICRATE; \
+			mo->destscale = player->mo->scale; \
+			P_SetScale(mo, player->mo->scale); \
+			mo->momx = FixedMul(FINECOSINE(fa),ns); \
+			if (!(twodlevel || (player->mo->flags2 & MF2_TWOD))) \
+				mo->momy = FixedMul(FINESINE(fa),ns); \
+			P_SetObjectMomZ(mo, 4*FRACUNIT, false); \
+			if (i & 1) \
+				P_SetObjectMomZ(mo, 4*FRACUNIT, true); \
+			if (player->mo->eflags & MFE_VERTICALFLIP) \
+				mo->flags2 |= MF2_OBJECTFLIP; \
+		} \
 	} \
 	else if (player->powers[power] > 0) \
 	{ \
 		SETUP_DROP(ammo) \
 		mo = P_SpawnMobj(player->mo->x, player->mo->y, z, ammo); \
-		mo->health = player->powers[power]; \
-		mo->flags2 |= MF2_DONTRESPAWN; \
-		mo->flags &= ~(MF_NOGRAVITY|MF_NOCLIPHEIGHT); \
-		P_SetTarget(&mo->target, player->mo); \
-		mo->fuse = 12*TICRATE; \
-		mo->destscale = player->mo->scale; \
-		P_SetScale(mo, player->mo->scale); \
-		mo->momx = FixedMul(FINECOSINE(fa),ns); \
-		if (!(twodlevel || (player->mo->flags2 & MF2_TWOD))) \
-			mo->momy = FixedMul(FINESINE(fa),ns); \
-		P_SetObjectMomZ(mo, 3*FRACUNIT, false); \
-		if (i & 1) \
-			P_SetObjectMomZ(mo, 3*FRACUNIT, true); \
-		if (player->mo->eflags & MFE_VERTICALFLIP) \
-			mo->flags2 |= MF2_OBJECTFLIP; \
-		player->powers[power] = 0; \
-		++i; \
-	}
+		if (!P_MobjWasRemoved(mo)) \
+		{ \
+			mo->health = player->powers[power]; \
+			mo->flags2 |= MF2_DONTRESPAWN; \
+			mo->flags &= ~(MF_NOGRAVITY|MF_NOCLIPHEIGHT); \
+			P_SetTarget(&mo->target, player->mo); \
+			mo->fuse = 12*TICRATE; \
+			mo->destscale = player->mo->scale; \
+			P_SetScale(mo, player->mo->scale); \
+			mo->momx = FixedMul(FINECOSINE(fa),ns); \
+			if (!(twodlevel || (player->mo->flags2 & MF2_TWOD))) \
+				mo->momy = FixedMul(FINESINE(fa),ns); \
+			P_SetObjectMomZ(mo, 3*FRACUNIT, false); \
+			if (i & 1) \
+				P_SetObjectMomZ(mo, 3*FRACUNIT, true); \
+			if (player->mo->eflags & MFE_VERTICALFLIP) \
+				mo->flags2 |= MF2_OBJECTFLIP; \
+			player->powers[power] = 0; \
+		} \
+	} \
+	++i
 
 	DROP_WEAPON(RW_BOUNCE, MT_BOUNCEPICKUP, MT_BOUNCERING, pw_bouncering);
 	DROP_WEAPON(RW_RAIL, MT_RAILPICKUP, MT_RAILRING, pw_railring);
@@ -4350,23 +4391,26 @@ void P_PlayerEmeraldBurst(player_t *player, boolean toss)
 				momy = 0;
 
 			mo = P_SpawnMobj(player->mo->x, player->mo->y, z, MT_FLINGEMERALD);
-			mo->health = 1;
-			mo->threshold = stoneflag;
-			mo->flags2 |= (MF2_DONTRESPAWN|MF2_SLIDEPUSH);
-			mo->flags &= ~(MF_NOGRAVITY|MF_NOCLIPHEIGHT);
-			P_SetTarget(&mo->target, player->mo);
-			mo->fuse = 12*TICRATE;
-			P_SetMobjState(mo, statenum);
+			if (!P_MobjWasRemoved(mo))
+			{
+				mo->health = 1;
+				mo->threshold = stoneflag;
+				mo->flags2 |= (MF2_DONTRESPAWN|MF2_SLIDEPUSH);
+				mo->flags &= ~(MF_NOGRAVITY|MF_NOCLIPHEIGHT);
+				P_SetTarget(&mo->target, player->mo);
+				mo->fuse = 12*TICRATE;
+				P_SetMobjState(mo, statenum);
 
-			mo->momx = momx;
-			mo->momy = momy;
+				mo->momx = momx;
+				mo->momy = momy;
 
-			P_SetObjectMomZ(mo, 3*FRACUNIT, false);
+				P_SetObjectMomZ(mo, 3*FRACUNIT, false);
 
-			if (player->mo->eflags & MFE_VERTICALFLIP)
-			{
-				mo->momz = -mo->momz;
-				mo->flags2 |= MF2_OBJECTFLIP;
+				if (player->mo->eflags & MFE_VERTICALFLIP)
+				{
+					mo->momz = -mo->momz;
+					mo->flags2 |= MF2_OBJECTFLIP;
+				}
 			}
 
 			if (toss)
@@ -4394,6 +4438,8 @@ void P_PlayerFlagBurst(player_t *player, boolean toss)
 		type = MT_BLUEFLAG;
 
 	flag = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, type);
+	if (P_MobjWasRemoved(flag))
+		return;
 
 	if (player->mo->eflags & MFE_VERTICALFLIP)
 	{
diff --git a/src/p_lights.c b/src/p_lights.c
index 4b6a3673b51b6a1720d237e4b85ae8f7c7839c4b..971165e88e9593634813da9fd89cb86359811cf1 100644
--- a/src/p_lights.c
+++ b/src/p_lights.c
@@ -17,7 +17,7 @@
 #include "r_state.h"
 #include "z_zone.h"
 #include "m_random.h"
-#include "d_netcmd.h"
+#include "netcode/d_netcmd.h"
 
 /** Removes any active lighting effects in a sector.
   *
diff --git a/src/p_local.h b/src/p_local.h
index 563e257d8f1e67e7e5d45d0a8f7122659c66b435..84a0aace09b83b4a6f5bc71552ccb910a486ae43 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -71,6 +71,7 @@ typedef enum
 	NUM_THINKERLISTS
 } thinklistnum_t; /**< Thinker lists. */
 extern thinker_t thlist[];
+extern mobj_t *mobjcache;
 
 void P_InitThinkers(void);
 void P_AddThinker(const thinklistnum_t n, thinker_t *thinker);
@@ -146,6 +147,7 @@ void P_ForceLocalAngle(player_t *player, angle_t angle);
 boolean P_PlayerFullbright(player_t *player);
 boolean P_PlayerCanEnterSpinGaps(player_t *player);
 boolean P_PlayerShouldUseSpinHeight(player_t *player);
+UINT16 P_GetPlayerColor(player_t *player);
 
 boolean P_IsObjectInGoop(mobj_t *mo);
 boolean P_IsObjectOnGround(mobj_t *mo);
@@ -201,8 +203,9 @@ mobj_t *P_LookForEnemies(player_t *player, boolean nonenemies, boolean bullet);
 void P_NukeEnemies(mobj_t *inflictor, mobj_t *source, fixed_t radius);
 void P_Earthquake(mobj_t *inflictor, mobj_t *source, fixed_t radius);
 boolean P_HomingAttack(mobj_t *source, mobj_t *enemy); /// \todo doesn't belong in p_user
-boolean P_SuperReady(player_t *player);
+boolean P_SuperReady(player_t *player, boolean transform);
 void P_DoJump(player_t *player, boolean soundandstate);
+void P_DoSpinDashDust(player_t *player);
 #define P_AnalogMove(player) (P_ControlStyle(player) == CS_LMAOGALOG)
 boolean P_TransferToNextMare(player_t *player);
 UINT8 P_FindLowestMare(void);
@@ -213,6 +216,10 @@ void P_SpawnThokMobj(player_t *player);
 void P_SpawnSpinMobj(player_t *player, mobjtype_t type);
 void P_Telekinesis(player_t *player, fixed_t thrust, fixed_t range);
 
+void P_DoTailsOverlay(player_t *player, mobj_t *tails);
+void P_DoMetalJetFume(player_t *player, mobj_t *fume);
+void P_DoFollowMobj(player_t *player, mobj_t *followmobj);
+
 void P_PlayLivesJingle(player_t *player);
 #define P_PlayRinglossSound(s)	S_StartSound(s, (mariomode) ? sfx_mario8 : sfx_altow1 + P_RandomKey(4));
 #define P_PlayDeathSound(s)		S_StartSound(s, sfx_altdi1 + P_RandomKey(4));
@@ -282,7 +289,7 @@ mobjtype_t P_GetMobjtype(UINT16 mthingtype);
 
 void P_RespawnSpecials(void);
 
-mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type);
+mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type, ...);
 
 void P_RecalcPrecipInSector(sector_t *sector);
 void P_PrecipitationEffects(void);
@@ -290,13 +297,13 @@ void P_PrecipitationEffects(void);
 void P_RemoveMobj(mobj_t *th);
 boolean P_MobjWasRemoved(mobj_t *th);
 void P_RemoveSavegameMobj(mobj_t *th);
-boolean P_SetPlayerMobjState(mobj_t *mobj, statenum_t state);
 boolean P_SetMobjState(mobj_t *mobj, statenum_t state);
 void P_RunShields(void);
 void P_RunOverlays(void);
 void P_HandleMinecartSegments(mobj_t *mobj);
 void P_MobjThinker(mobj_t *mobj);
 boolean P_RailThinker(mobj_t *mobj);
+boolean P_CheckSkyHit(mobj_t *mo, line_t *line);
 void P_PushableThinker(mobj_t *mobj);
 void P_SceneryThinker(mobj_t *mobj);
 
@@ -438,6 +445,10 @@ boolean PIT_PushableMoved(mobj_t *thing);
 
 boolean P_DoSpring(mobj_t *spring, mobj_t *object);
 
+INT32 P_GetSectorLightAt(sector_t *sector, fixed_t x, fixed_t y, fixed_t z);
+extracolormap_t *P_GetColormapFromSectorAt(sector_t *sector, fixed_t x, fixed_t y, fixed_t z);
+extracolormap_t *P_GetSectorColormapAt(fixed_t x, fixed_t y, fixed_t z);
+
 //
 // P_SETUP
 //
@@ -534,6 +545,9 @@ boolean P_Teleport(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z, angle_t angle
 boolean P_SetMobjStateNF(mobj_t *mobj, statenum_t state);
 boolean P_CheckMissileSpawn(mobj_t *th);
 void P_Thrust(mobj_t *mo, angle_t angle, fixed_t move);
+void P_ThrustEvenIn2D(mobj_t *mo, angle_t angle, fixed_t move);
+void P_VectorInstaThrust(fixed_t xa, fixed_t xb, fixed_t xc, fixed_t ya, fixed_t yb, fixed_t yc,
+            fixed_t za, fixed_t zb, fixed_t zc, fixed_t momentum, mobj_t *mo);
 void P_DoSuperTransformation(player_t *player, boolean giverings);
 void P_ExplodeMissile(mobj_t *mo);
 void P_CheckGravity(mobj_t *mo, boolean affect);
diff --git a/src/p_map.c b/src/p_map.c
index 251837876caab9f979b92ef1ece83a1750f24768..e01798054970ee3364fa65883fa25f577c700ed7 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -264,7 +264,7 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object)
 				UINT8 secondjump = object->player->secondjump;
 				UINT16 tailsfly = object->player->powers[pw_tailsfly];
 				if (object->player->pflags & PF_GLIDING)
-					P_SetPlayerMobjState(object, S_PLAY_FALL);
+					P_SetMobjState(object, S_PLAY_FALL);
 				P_ResetPlayer(object->player);
 				object->player->pflags |= pflags;
 				object->player->secondjump = secondjump;
@@ -403,7 +403,7 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object)
 		}
 
 		if (object->player->pflags & PF_GLIDING)
-			P_SetPlayerMobjState(object, S_PLAY_FALL);
+			P_SetMobjState(object, S_PLAY_FALL);
 		if ((spring->info->painchance == 3))
 		{
 			if (!(pflags = (object->player->pflags & PF_SPINNING)) &&
@@ -411,11 +411,11 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object)
 				|| (spring->flags2 & MF2_AMBUSH)))
 			{
 				pflags = PF_SPINNING;
-				P_SetPlayerMobjState(object, S_PLAY_ROLL);
+				P_SetMobjState(object, S_PLAY_ROLL);
 				S_StartSound(object, sfx_spin);
 			}
 			else
-				P_SetPlayerMobjState(object, S_PLAY_ROLL);
+				P_SetMobjState(object, S_PLAY_ROLL);
 		}
 		else
 		{
@@ -424,7 +424,7 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object)
 			pflags = object->player->pflags & (PF_STARTJUMP | PF_JUMPED | PF_NOJUMPDAMAGE | PF_SPINNING | PF_THOKKED | PF_BOUNCING); // I still need these.
 
 			if (wasSpindashing) // Ensure we're in the rolling state, and not spindash.
-				P_SetPlayerMobjState(object, S_PLAY_ROLL);
+				P_SetMobjState(object, S_PLAY_ROLL);
 
 			if (object->player->charability == CA_GLIDEANDCLIMB && object->player->skidtime && (pflags & PF_JUMPED))
 			{
@@ -439,7 +439,7 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object)
 		if (spring->info->painchance == 1) // For all those ancient, SOC'd abilities.
 		{
 			object->player->pflags |= P_GetJumpFlags(object->player);
-			P_SetPlayerMobjState(object, S_PLAY_JUMP);
+			P_SetMobjState(object, S_PLAY_JUMP);
 		}
 		else if ((spring->info->painchance == 2) || ((spring->info->painchance != 3) && (pflags & PF_BOUNCING))) // Adding momentum only.
 		{
@@ -456,16 +456,16 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object)
 				object->player->secondjump = secondjump;
 			}
 			else if (object->player->dashmode >= DASHMODE_THRESHOLD)
-				P_SetPlayerMobjState(object, S_PLAY_DASH);
+				P_SetMobjState(object, S_PLAY_DASH);
 			else if (P_IsObjectOnGround(object))
-				P_SetPlayerMobjState(object, (horizspeed >= FixedMul(object->player->runspeed, object->scale)) ? S_PLAY_RUN : S_PLAY_WALK);
+				P_SetMobjState(object, (horizspeed >= FixedMul(object->player->runspeed, object->scale)) ? S_PLAY_RUN : S_PLAY_WALK);
 			else
-				P_SetPlayerMobjState(object, (object->momz > 0) ? S_PLAY_SPRING : S_PLAY_FALL);
+				P_SetMobjState(object, (object->momz > 0) ? S_PLAY_SPRING : S_PLAY_FALL);
 		}
 		else if (P_MobjFlip(object)*vertispeed > 0)
-			P_SetPlayerMobjState(object, S_PLAY_SPRING);
+			P_SetMobjState(object, S_PLAY_SPRING);
 		else
-			P_SetPlayerMobjState(object, S_PLAY_FALL);
+			P_SetMobjState(object, S_PLAY_FALL);
 	}
 	else if (horizspeed
 		&& object->tracer
@@ -547,7 +547,7 @@ static void P_DoFanAndGasJet(mobj_t *spring, mobj_t *object)
 			if (p && !p->powers[pw_tailsfly] && !p->powers[pw_carry]) // doesn't reset anim for Tails' flight
 			{
 				P_ResetPlayer(p);
-				P_SetPlayerMobjState(object, S_PLAY_FALL);
+				P_SetMobjState(object, S_PLAY_FALL);
 				P_SetTarget(&object->tracer, spring);
 				p->powers[pw_carry] = CR_FAN;
 			}
@@ -565,7 +565,7 @@ static void P_DoFanAndGasJet(mobj_t *spring, mobj_t *object)
 			{
 				P_ResetPlayer(p);
 				if (p->panim != PA_FALL)
-					P_SetPlayerMobjState(object, S_PLAY_FALL);
+					P_SetMobjState(object, S_PLAY_FALL);
 			}
 			break;
 		default:
@@ -1047,7 +1047,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 			thing->flags2 &= ~MF2_DONTDRAW; // don't leave the rock invisible if it was flashing prior to boarding
 			P_SetTarget(&thing->tracer, tmthing);
 			P_ResetPlayer(tmthing->player);
-			P_SetPlayerMobjState(tmthing, S_PLAY_WALK);
+			P_SetMobjState(tmthing, S_PLAY_WALK);
 			tmthing->player->powers[pw_carry] = CR_ROLLOUT;
 			P_SetTarget(&tmthing->tracer, thing);
 			if (!P_IsObjectOnGround(thing))
@@ -2865,6 +2865,8 @@ boolean P_CheckMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff)
 {
 	boolean moveok;
 	mobj_t *hack = P_SpawnMobjFromMobj(thing, 0, 0, 0, MT_RAY);
+	if (P_MobjWasRemoved(hack))
+		return false;
 
 	hack->radius = thing->radius;
 	hack->height = thing->height;
@@ -3073,11 +3075,14 @@ static boolean P_ThingHeightClip(mobj_t *thing)
 		if (!rover || ((rover->fofflags & FOF_EXISTS) && (rover->fofflags & FOF_SOLID)))
 		{
 			hitfloor = bouncing;
-			if (thing->eflags & MFE_VERTICALFLIP)
-				thing->pmomz = thing->ceilingz - (thing->z + thing->height);
-			else
-				thing->pmomz = thing->floorz - thing->z;
-			thing->eflags |= MFE_APPLYPMOMZ;
+			if (!(thing->player) || !(thing->player->pflags & PF_JUMPED || bouncing))
+			{
+				if (thing->eflags & MFE_VERTICALFLIP)
+					thing->pmomz = thing->ceilingz - (thing->z + thing->height);
+				else
+					thing->pmomz = thing->floorz - thing->z;
+				thing->eflags |= MFE_APPLYPMOMZ;
+			}
 
 			if (thing->eflags & MFE_VERTICALFLIP)
 				thing->z = thing->ceilingz - thing->height;
@@ -3902,7 +3907,7 @@ retry:
 	P_PathTraverse(leadx, traily, leadx + mo->momx, traily + mo->momy,
 		PT_ADDLINES, PTR_SlideTraverse);
 
-	if (bestslideline && mo->player && bestslideline->sidenum[1] != 0xffff)
+	if (bestslideline && mo->player && bestslideline->sidenum[1] != NO_SIDEDEF)
 	{
 		sector_t *sec = P_PointOnLineSide(mo->x, mo->y, bestslideline) ? bestslideline->frontsector : bestslideline->backsector;
 		P_CheckLavaWall(mo, sec);
@@ -5068,3 +5073,35 @@ fixed_t P_CeilingzAtPos(fixed_t x, fixed_t y, fixed_t z, fixed_t height)
 
 	return ceilingz;
 }
+
+INT32 P_GetSectorLightAt(sector_t *sector, fixed_t x, fixed_t y, fixed_t z)
+{
+	if (!sector->numlights)
+		return -1;
+
+	INT32 light = sector->numlights - 1;
+
+	// R_GetPlaneLight won't work on sloped lights!
+	for (INT32 lightnum = 1; lightnum < sector->numlights; lightnum++) {
+		fixed_t h = P_GetLightZAt(&sector->lightlist[lightnum], x, y);
+		if (h <= z) {
+			light = lightnum - 1;
+			break;
+		}
+	}
+
+	return light;
+}
+
+extracolormap_t *P_GetColormapFromSectorAt(sector_t *sector, fixed_t x, fixed_t y, fixed_t z)
+{
+	if (sector->numlights)
+		return *sector->lightlist[P_GetSectorLightAt(sector, x, y, z)].extra_colormap;
+	else
+		return sector->extra_colormap;
+}
+
+extracolormap_t *P_GetSectorColormapAt(fixed_t x, fixed_t y, fixed_t z)
+{
+	return P_GetColormapFromSectorAt(R_PointInSubsector(x, y)->sector, x, y, z);
+}
diff --git a/src/p_maputl.c b/src/p_maputl.c
index e36d5fd724fd152346b34b8ec047277e25735ecb..9c30d3ead094bcbbcf7e3c98e90fc159285a36b0 100644
--- a/src/p_maputl.c
+++ b/src/p_maputl.c
@@ -290,7 +290,7 @@ void P_CameraLineOpening(line_t *linedef)
 	sector_t *back;
 	fixed_t frontfloor, frontceiling, backfloor, backceiling;
 
-	if (linedef->sidenum[1] == 0xffff)
+	if (linedef->sidenum[1] == NO_SIDEDEF)
 	{
 		// single sided line
 		openrange = 0;
@@ -426,7 +426,7 @@ void P_LineOpening(line_t *linedef, mobj_t *mobj)
 {
 	sector_t *front, *back;
 
-	if (linedef->sidenum[1] == 0xffff)
+	if (linedef->sidenum[1] == NO_SIDEDEF)
 	{
 		// single sided line
 		openrange = 0;
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 686f08478e8d7006e3d01c7a69ea3f38b4e1534a..ec7455a3c8a135d8637fd07fbf29e2b9a85821f3 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -36,6 +36,7 @@
 #include "p_slopes.h"
 #include "f_finale.h"
 #include "m_cond.h"
+#include "netcode/net_command.h"
 
 static CV_PossibleValue_t CV_BobSpeed[] = {{0, "MIN"}, {4*FRACUNIT, "MAX"}, {0, NULL}};
 consvar_t cv_movebob = CVAR_INIT ("movebob", "1.0", CV_FLOAT|CV_SAVE, CV_BobSpeed, NULL);
@@ -44,6 +45,8 @@ actioncache_t actioncachehead;
 
 static mobj_t *overlaycap = NULL;
 
+mobj_t *mobjcache = NULL;
+
 void P_InitCachedActions(void)
 {
 	actioncachehead.prev = actioncachehead.next = &actioncachehead;
@@ -176,7 +179,7 @@ static void P_CyclePlayerMobjState(mobj_t *mobj)
 
 		// you can cycle through multiple states in a tic
 		if (!mobj->tics && mobj->state)
-			if (!P_SetPlayerMobjState(mobj, mobj->state->nextstate))
+			if (!P_SetMobjState(mobj, mobj->state->nextstate))
 				return; // freed itself
 	}
 }
@@ -187,7 +190,7 @@ static void P_CyclePlayerMobjState(mobj_t *mobj)
 //
 // Separate from P_SetMobjState because of the pw_flashing check and Super states
 //
-boolean P_SetPlayerMobjState(mobj_t *mobj, statenum_t state)
+static boolean P_SetPlayerMobjState(mobj_t *mobj, statenum_t state)
 {
 	state_t *st;
 	player_t *player = mobj->player;
@@ -513,10 +516,8 @@ boolean P_SetMobjState(mobj_t *mobj, statenum_t state)
 	statenum_t i = state; // initial state
 	statenum_t tempstate[NUMSTATES]; // for use with recursion
 
-#ifdef PARANOIA
 	if (mobj->player != NULL)
-		I_Error("P_SetMobjState used for player mobj. Use P_SetPlayerMobjState instead!\n(State called: %d)", state);
-#endif
+		return P_SetPlayerMobjState(mobj, state);
 
 	if (recursion++) // if recursion detected,
 		memset(seenstate = tempstate, 0, sizeof tempstate); // clear state table
@@ -914,29 +915,41 @@ void P_ExplodeMissile(mobj_t *mo)
 		P_RadiusAttack(mo, mo, 96*FRACUNIT, 0, true);
 
 		explodemo = P_SpawnMobj(mo->x, mo->y, mo->z, MT_EXPLODE);
-		P_SetScale(explodemo, mo->scale);
-		explodemo->destscale = mo->destscale;
-		explodemo->momx += (P_RandomByte() % 32) * FixedMul(FRACUNIT/8, explodemo->scale);
-		explodemo->momy += (P_RandomByte() % 32) * FixedMul(FRACUNIT/8, explodemo->scale);
-		S_StartSound(explodemo, sfx_pop);
+		if (!P_MobjWasRemoved(explodemo))
+		{
+			P_SetScale(explodemo, mo->scale);
+			explodemo->destscale = mo->destscale;
+			explodemo->momx += (P_RandomByte() % 32) * FixedMul(FRACUNIT/8, explodemo->scale);
+			explodemo->momy += (P_RandomByte() % 32) * FixedMul(FRACUNIT/8, explodemo->scale);
+			S_StartSound(explodemo, sfx_pop);
+		}
 		explodemo = P_SpawnMobj(mo->x, mo->y, mo->z, MT_EXPLODE);
-		P_SetScale(explodemo, mo->scale);
-		explodemo->destscale = mo->destscale;
-		explodemo->momx += (P_RandomByte() % 64) * FixedMul(FRACUNIT/8, explodemo->scale);
-		explodemo->momy -= (P_RandomByte() % 64) * FixedMul(FRACUNIT/8, explodemo->scale);
-		S_StartSound(explodemo, sfx_dmpain);
+		if (!P_MobjWasRemoved(explodemo))
+		{
+			P_SetScale(explodemo, mo->scale);
+			explodemo->destscale = mo->destscale;
+			explodemo->momx += (P_RandomByte() % 64) * FixedMul(FRACUNIT/8, explodemo->scale);
+			explodemo->momy -= (P_RandomByte() % 64) * FixedMul(FRACUNIT/8, explodemo->scale);
+			S_StartSound(explodemo, sfx_dmpain);
+		}
 		explodemo = P_SpawnMobj(mo->x, mo->y, mo->z, MT_EXPLODE);
-		P_SetScale(explodemo, mo->scale);
-		explodemo->destscale = mo->destscale;
-		explodemo->momx -= (P_RandomByte() % 128) * FixedMul(FRACUNIT/8, explodemo->scale);
-		explodemo->momy += (P_RandomByte() % 128) * FixedMul(FRACUNIT/8, explodemo->scale);
-		S_StartSound(explodemo, sfx_pop);
+		if (!P_MobjWasRemoved(explodemo))
+		{
+			P_SetScale(explodemo, mo->scale);
+			explodemo->destscale = mo->destscale;
+			explodemo->momx -= (P_RandomByte() % 128) * FixedMul(FRACUNIT/8, explodemo->scale);
+			explodemo->momy += (P_RandomByte() % 128) * FixedMul(FRACUNIT/8, explodemo->scale);
+			S_StartSound(explodemo, sfx_pop);
+		}
 		explodemo = P_SpawnMobj(mo->x, mo->y, mo->z, MT_EXPLODE);
-		P_SetScale(explodemo, mo->scale);
-		explodemo->destscale = mo->destscale;
-		explodemo->momx -= (P_RandomByte() % 96) * FixedMul(FRACUNIT/8, explodemo->scale);
-		explodemo->momy -= (P_RandomByte() % 96) * FixedMul(FRACUNIT/8, explodemo->scale);
-		S_StartSound(explodemo, sfx_cybdth);
+		if (!P_MobjWasRemoved(explodemo))
+		{
+			P_SetScale(explodemo, mo->scale);
+			explodemo->destscale = mo->destscale;
+			explodemo->momx -= (P_RandomByte() % 96) * FixedMul(FRACUNIT/8, explodemo->scale);
+			explodemo->momy -= (P_RandomByte() % 96) * FixedMul(FRACUNIT/8, explodemo->scale);
+			S_StartSound(explodemo, sfx_cybdth);
+		}
 	}
 
 	mo->flags &= ~MF_MISSILE;
@@ -1108,7 +1121,7 @@ fixed_t P_MobjFloorZ(mobj_t *mobj, sector_t *sector, sector_t *boundsec, fixed_t
 		testy += y;
 
 		// If the highest point is in the sector, then we have it easy! Just get the Z at that point
-		if (R_PointInSubsector(testx, testy)->sector == (boundsec ? boundsec : sector))
+		if (R_IsPointInSector(boundsec ? boundsec : sector, testx, testy))
 			return P_GetSlopeZAt(slope, testx, testy);
 
 		// If boundsec is set, we're looking for specials. In that case, iterate over every line in this sector to find the TRUE highest/lowest point
@@ -1185,7 +1198,7 @@ fixed_t P_MobjCeilingZ(mobj_t *mobj, sector_t *sector, sector_t *boundsec, fixed
 		testy += y;
 
 		// If the highest point is in the sector, then we have it easy! Just get the Z at that point
-		if (R_PointInSubsector(testx, testy)->sector == (boundsec ? boundsec : sector))
+		if (R_IsPointInSector(boundsec ? boundsec : sector, testx, testy))
 			return P_GetSlopeZAt(slope, testx, testy);
 
 		// If boundsec is set, we're looking for specials. In that case, iterate over every line in this sector to find the TRUE highest/lowest point
@@ -1263,7 +1276,7 @@ fixed_t P_CameraFloorZ(camera_t *mobj, sector_t *sector, sector_t *boundsec, fix
 		testy += y;
 
 		// If the highest point is in the sector, then we have it easy! Just get the Z at that point
-		if (R_PointInSubsector(testx, testy)->sector == (boundsec ? boundsec : sector))
+		if (R_IsPointInSector(boundsec ? boundsec : sector, testx, testy))
 			return P_GetSlopeZAt(slope, testx, testy);
 
 		// If boundsec is set, we're looking for specials. In that case, iterate over every line in this sector to find the TRUE highest/lowest point
@@ -1340,7 +1353,7 @@ fixed_t P_CameraCeilingZ(camera_t *mobj, sector_t *sector, sector_t *boundsec, f
 		testy += y;
 
 		// If the highest point is in the sector, then we have it easy! Just get the Z at that point
-		if (R_PointInSubsector(testx, testy)->sector == (boundsec ? boundsec : sector))
+		if (R_IsPointInSector(boundsec ? boundsec : sector, testx, testy))
 			return P_GetSlopeZAt(slope, testx, testy);
 
 		// If boundsec is set, we're looking for specials. In that case, iterate over every line in this sector to find the TRUE highest/lowest point
@@ -1647,7 +1660,7 @@ static void P_XYFriction(mobj_t *mo, fixed_t oldx, fixed_t oldy)
 		{
 			// if in a walking frame, stop moving
 			if (player->panim == PA_WALK)
-				P_SetPlayerMobjState(mo, S_PLAY_STND);
+				P_SetMobjState(mo, S_PLAY_STND);
 			mo->momx = player->cmomx;
 			mo->momy = player->cmomy;
 		}
@@ -1778,14 +1791,15 @@ bustupdone:
 //
 // P_CheckSkyHit
 //
-static boolean P_CheckSkyHit(mobj_t *mo)
-{
-	if (ceilingline && ceilingline->backsector
-		&& ceilingline->backsector->ceilingpic == skyflatnum
-		&& ceilingline->frontsector
-		&& ceilingline->frontsector->ceilingpic == skyflatnum
-		&& (mo->z >= ceilingline->frontsector->ceilingheight
-		|| mo->z >= ceilingline->backsector->ceilingheight))
+boolean P_CheckSkyHit(mobj_t *mo, line_t *line)
+{
+	if (line && (line->special == 41 ||
+		(line->backsector
+		&& line->backsector->ceilingpic == skyflatnum
+		&& line->frontsector
+		&& line->frontsector->ceilingpic == skyflatnum
+		&& (mo->z >= line->frontsector->ceilingheight
+		|| mo->z >= line->backsector->ceilingheight))))
 			return true;
 	return false;
 }
@@ -1892,7 +1906,7 @@ void P_XYMovement(mobj_t *mo)
 					mo->fuse += ((5 - mo->threshold) * TICRATE);
 
 				// Check for hit against sky here
-				if (P_CheckSkyHit(mo))
+				if (P_CheckSkyHit(mo, ceilingline))
 				{
 					// Hack to prevent missiles exploding
 					// against the sky.
@@ -1912,7 +1926,7 @@ void P_XYMovement(mobj_t *mo)
 			mo->flags &= ~MF_STICKY; //Don't check again!
 
 			// Check for hit against sky here
-			if (P_CheckSkyHit(mo))
+			if (P_CheckSkyHit(mo, ceilingline))
 			{
 				// Hack to prevent missiles exploding
 				// against the sky.
@@ -1971,7 +1985,7 @@ void P_XYMovement(mobj_t *mo)
 		else if (mo->flags & MF_MISSILE)
 		{
 			// explode a missile
-			if (P_CheckSkyHit(mo))
+			if (P_CheckSkyHit(mo, ceilingline))
 			{
 				// Hack to prevent missiles exploding
 				// against the sky.
@@ -2972,7 +2986,7 @@ void P_PlayerZMovement(mobj_t *mo)
 		}
 		// Get up if you fell.
 		if (mo->player->panim == PA_PAIN)
-			P_SetPlayerMobjState(mo, S_PLAY_WALK);
+			P_SetMobjState(mo, S_PLAY_WALK);
 
 		if (!mo->standingslope && (mo->eflags & MFE_VERTICALFLIP ? tmceilingslope : tmfloorslope)) {
 			// Handle landing on slope during Z movement
@@ -3118,6 +3132,8 @@ boolean P_SceneryZMovement(mobj_t *mo)
 				{
 					prandom = P_RandomByte();
 					explodemo = P_SpawnMobj(mo->x, mo->y, mo->z, MT_SMALLBUBBLE);
+					if (P_MobjWasRemoved(explodemo))
+						continue;
 					explodemo->momx += ((prandom & 0x0F) << (FRACBITS-2)) * (i & 2 ? -1 : 1);
 					explodemo->momy += ((prandom & 0xF0) << (FRACBITS-6)) * (i & 1 ? -1 : 1);
 					explodemo->destscale = mo->scale;
@@ -3390,13 +3406,19 @@ void P_MobjCheckWater(mobj_t *mobj)
 				if (mobj->eflags & MFE_VERTICALFLIP)
 				{
 					splish = P_SpawnMobj(mobj->x, mobj->y, mobj->waterbottom-FixedMul(mobjinfo[splishtype].height, mobj->scale), splishtype);
-					splish->flags2 |= MF2_OBJECTFLIP;
-					splish->eflags |= MFE_VERTICALFLIP;
+					if (!P_MobjWasRemoved(splish))
+					{
+						splish->flags2 |= MF2_OBJECTFLIP;
+						splish->eflags |= MFE_VERTICALFLIP;
+					}
 				}
 				else
 					splish = P_SpawnMobj(mobj->x, mobj->y, mobj->watertop, splishtype);
-				splish->destscale = mobj->scale;
-				P_SetScale(splish, mobj->scale);
+				if (!P_MobjWasRemoved(splish))
+				{
+					splish->destscale = mobj->scale;
+					P_SetScale(splish, mobj->scale);
+				}
 			}
 
 			// skipping stone!
@@ -3426,13 +3448,19 @@ void P_MobjCheckWater(mobj_t *mobj)
 				if (mobj->eflags & MFE_VERTICALFLIP)
 				{
 					splish = P_SpawnMobj(mobj->x, mobj->y, mobj->waterbottom-FixedMul(mobjinfo[splishtype].height, mobj->scale), splishtype);
-					splish->flags2 |= MF2_OBJECTFLIP;
-					splish->eflags |= MFE_VERTICALFLIP;
+					if (!P_MobjWasRemoved(splish))
+					{
+						splish->flags2 |= MF2_OBJECTFLIP;
+						splish->eflags |= MFE_VERTICALFLIP;
+					}
 				}
 				else
 					splish = P_SpawnMobj(mobj->x, mobj->y, mobj->watertop, splishtype);
-				splish->destscale = mobj->scale;
-				P_SetScale(splish, mobj->scale);
+				if (!P_MobjWasRemoved(splish))
+				{
+					splish->destscale = mobj->scale;
+					P_SetScale(splish, mobj->scale);
+				}
 			}
 		}
 
@@ -3955,7 +3983,7 @@ static void P_PlayerMobjThinker(mobj_t *mobj)
 		{
 			mobj->player->secondjump = 0;
 			mobj->player->powers[pw_tailsfly] = 0;
-			P_SetPlayerMobjState(mobj, S_PLAY_WALK);
+			P_SetMobjState(mobj, S_PLAY_WALK);
 		}
 #endif
 		mobj->eflags &= ~MFE_JUSTHITFLOOR;
@@ -4442,11 +4470,14 @@ static void P_Boss3Thinker(mobj_t *mobj)
 			}
 
 			dummy = P_SpawnMobj(mobj->x, mobj->y, mobj->z, mobj->info->mass);
-			dummy->angle = mobj->angle;
-			dummy->threshold = way1;
-			P_SetTarget(&dummy->tracer, mobj);
-			dummy->movefactor = mobj->movefactor;
-			dummy->cusval = mobj->cusval;
+			if (!P_MobjWasRemoved(dummy))
+			{
+				dummy->angle = mobj->angle;
+				dummy->threshold = way1;
+				P_SetTarget(&dummy->tracer, mobj);
+				dummy->movefactor = mobj->movefactor;
+				dummy->cusval = mobj->cusval;
+			}
 
 			way2 = P_RandomKey(8-3);
 			if (way2 >= curpath)
@@ -4465,11 +4496,14 @@ static void P_Boss3Thinker(mobj_t *mobj)
 			}
 
 			dummy = P_SpawnMobj(mobj->x, mobj->y, mobj->z, mobj->info->mass);
-			dummy->angle = mobj->angle;
-			dummy->threshold = way2;
-			P_SetTarget(&dummy->tracer, mobj);
-			dummy->movefactor = mobj->movefactor;
-			dummy->cusval = mobj->cusval;
+			if (!P_MobjWasRemoved(dummy))
+			{
+				dummy->angle = mobj->angle;
+				dummy->threshold = way2;
+				P_SetTarget(&dummy->tracer, mobj);
+				dummy->movefactor = mobj->movefactor;
+				dummy->cusval = mobj->cusval;
+			}
 
 			CONS_Debug(DBG_GAMELOGIC, "Eggman path %d - Dummy selected paths %d and %d\n", way0, way1, way2);
 			if (mobj->spawnpoint)
@@ -4587,6 +4621,8 @@ static void P_Boss3Thinker(mobj_t *mobj)
 				for (i = 0; i < numtospawn; i++)
 				{
 					shock = P_SpawnMobj(mobj->x, mobj->y, mobj->z, MT_SHOCKWAVE);
+					if (P_MobjWasRemoved(shock))
+						continue;
 					P_SetTarget(&shock->target, mobj);
 					shock->fuse = shock->info->painchance;
 
@@ -4606,7 +4642,8 @@ static void P_Boss3Thinker(mobj_t *mobj)
 					ang += interval;
 					sprev = shock;
 				}
-				S_StartSound(mobj, shock->info->seesound);
+				if (!P_MobjWasRemoved(shock))
+					S_StartSound(mobj, shock->info->seesound);
 
 				// look for a new target
 				P_BossTargetPlayer(mobj, true);
@@ -4856,12 +4893,17 @@ static void P_Boss4Thinker(mobj_t *mobj)
 			for (arm = 0; arm <3 ; arm++)
 			{
 				seg = P_SpawnMobj(mobj->x, mobj->y, z, MT_EGGMOBILE4_MACE);
+				if (P_MobjWasRemoved(seg))
+					continue;
+
 				P_SetTarget(&base->tracer, seg);
 				base = seg;
 				P_SetTarget(&seg->target, mobj);
 				for (i = 0; i < 9; i++)
 				{
 					P_SetTarget(&seg->hnext, P_SpawnMobj(mobj->x, mobj->y, z, MT_EGGMOBILE4_MACE));
+					if (P_MobjWasRemoved(seg->hnext))
+						continue;
 					P_SetTarget(&seg->hnext->hprev, seg);
 					seg = seg->hnext;
 				}
@@ -5132,9 +5174,12 @@ static void P_Boss7Thinker(mobj_t *mobj)
 	if (mobj->health >= mobj->info->spawnhealth && (leveltime & 14) == 0)
 	{
 		mobj_t *smoke = P_SpawnMobj(mobj->x, mobj->y, mobj->z + mobj->height, MT_SMOKE);
-		smoke->destscale = mobj->destscale;
-		P_SetScale(smoke, smoke->destscale);
-		smoke->momz = FixedMul(FRACUNIT, smoke->scale);
+		if (!P_MobjWasRemoved(smoke))
+		{
+			smoke->destscale = mobj->destscale;
+			P_SetScale(smoke, smoke->destscale);
+			smoke->momz = FixedMul(FRACUNIT, smoke->scale);
+		}
 	}
 
 	if (mobj->state == &states[S_BLACKEGG_STND] && mobj->tics == mobj->state->tics)
@@ -5408,6 +5453,8 @@ static void P_Boss7Thinker(mobj_t *mobj)
 				y = mobj->y + FixedMul(FINECOSINE(fa),ns);
 
 				mo2 = P_SpawnMobj(x, y, z, MT_EXPLODE);
+				if (P_MobjWasRemoved(mo2))
+					continue;
 				ns = 16 * FRACUNIT;
 				mo2->momx = FixedMul(FINESINE(fa),ns);
 				mo2->momy = FixedMul(FINECOSINE(fa),ns);
@@ -5601,76 +5648,84 @@ static void P_Boss9Thinker(mobj_t *mobj)
 						mobj_t *missile;
 
 						missile = P_SpawnMissile(mobj, mobj->target, MT_MSGATHER);
-						S_StopSound(missile);
-						if (mobj->extravalue1 >= 2)
-							P_SetScale(missile, FRACUNIT>>1);
-						missile->destscale = missile->scale>>1;
-						missile->fuse = TICRATE/2;
-						missile->scalespeed = abs(missile->destscale - missile->scale)/missile->fuse;
-						missile->z -= missile->height/2;
-						missile->momx *= -1;
-						missile->momy *= -1;
-						missile->momz *= -1;
-
-						if (mobj->extravalue1 == 2)
-						{
-							UINT8 i;
-							mobj_t *spread;
-							for (i = 0; i < 5; i++)
-							{
-								if (i == 2)
-									continue;
-								spread = P_SpawnMobj(missile->x, missile->y, missile->z, missile->type);
-								spread->angle = missile->angle+(ANGLE_11hh/2)*(i-2);
-								P_InstaThrust(spread,spread->angle,-spread->info->speed);
-								spread->momz = missile->momz;
-								P_SetScale(spread, missile->scale);
-								spread->destscale = missile->destscale;
-								spread->scalespeed = missile->scalespeed;
-								spread->fuse = missile->fuse;
-								P_UnsetThingPosition(spread);
-								spread->x -= spread->fuse*spread->momx;
-								spread->y -= spread->fuse*spread->momy;
-								spread->z -= spread->fuse*spread->momz;
-								P_SetThingPosition(spread);
-							}
-							P_InstaThrust(missile,missile->angle,-missile->info->speed);
-						}
-						else if (mobj->extravalue1 >= 3)
+						if (!P_MobjWasRemoved(missile))
 						{
-							UINT8 i;
-							mobj_t *spread;
-							mobj->target->z -= (4*missile->height);
-							for (i = 0; i < 5; i++)
+							S_StopSound(missile);
+							if (mobj->extravalue1 >= 2)
+								P_SetScale(missile, FRACUNIT>>1);
+							missile->destscale = missile->scale>>1;
+							missile->fuse = TICRATE/2;
+							missile->scalespeed = abs(missile->destscale - missile->scale)/missile->fuse;
+							missile->z -= missile->height/2;
+							missile->momx *= -1;
+							missile->momy *= -1;
+							missile->momz *= -1;
+
+							if (mobj->extravalue1 == 2)
 							{
-								if (i != 2)
+								UINT8 i;
+								mobj_t *spread;
+								for (i = 0; i < 5; i++)
 								{
-									spread = P_SpawnMissile(mobj, mobj->target, missile->type);
+									if (i == 2)
+										continue;
+									spread = P_SpawnMobj(missile->x, missile->y, missile->z, missile->type);
+									if (P_MobjWasRemoved(spread))
+										continue;
+
+									spread->angle = missile->angle+(ANGLE_11hh/2)*(i-2);
+									P_InstaThrust(spread,spread->angle,-spread->info->speed);
+									spread->momz = missile->momz;
 									P_SetScale(spread, missile->scale);
 									spread->destscale = missile->destscale;
+									spread->scalespeed = missile->scalespeed;
 									spread->fuse = missile->fuse;
-									spread->z -= spread->height/2;
-									spread->momx *= -1;
-									spread->momy *= -1;
-									spread->momz *= -1;
 									P_UnsetThingPosition(spread);
 									spread->x -= spread->fuse*spread->momx;
 									spread->y -= spread->fuse*spread->momy;
 									spread->z -= spread->fuse*spread->momz;
 									P_SetThingPosition(spread);
 								}
-								mobj->target->z += missile->height*2;
+								P_InstaThrust(missile,missile->angle,-missile->info->speed);
+							}
+							else if (mobj->extravalue1 >= 3)
+							{
+								UINT8 i;
+								mobj_t *spread;
+								mobj->target->z -= (4*missile->height);
+								for (i = 0; i < 5; i++)
+								{
+									if (i != 2)
+									{
+										spread = P_SpawnMissile(mobj, mobj->target, missile->type);
+										if (P_MobjWasRemoved(spread))
+											continue;
+										P_SetScale(spread, missile->scale);
+										spread->destscale = missile->destscale;
+										spread->fuse = missile->fuse;
+										spread->z -= spread->height/2;
+										spread->momx *= -1;
+										spread->momy *= -1;
+										spread->momz *= -1;
+										P_UnsetThingPosition(spread);
+										spread->x -= spread->fuse*spread->momx;
+										spread->y -= spread->fuse*spread->momy;
+										spread->z -= spread->fuse*spread->momz;
+										P_SetThingPosition(spread);
+									}
+									mobj->target->z += missile->height*2;
+								}
+								mobj->target->z -= (6*missile->height);
 							}
-							mobj->target->z -= (6*missile->height);
-						}
 
-						P_UnsetThingPosition(missile);
-						missile->x -= missile->fuse*missile->momx;
-						missile->y -= missile->fuse*missile->momy;
-						missile->z -= missile->fuse*missile->momz;
-						P_SetThingPosition(missile);
+							P_UnsetThingPosition(missile);
+							missile->x -= missile->fuse*missile->momx;
+							missile->y -= missile->fuse*missile->momy;
+							missile->z -= missile->fuse*missile->momz;
+							P_SetThingPosition(missile);
 
-						S_StartSound(mobj, sfx_s3kb3);
+							S_StartSound(mobj, sfx_s3kb3);
+						}
 					}
 				}
 			}
@@ -5688,29 +5743,32 @@ static void P_Boss9Thinker(mobj_t *mobj)
 			if (spawner && dist)
 			{
 				mobj_t *missile = P_SpawnMissile(spawner, mobj, MT_MSGATHER);
-				missile->fuse = (dist/P_AproxDistance(missile->momx, missile->momy));
-				if (missile->fuse <= 0) // Prevents a division by zero when calculating missile->scalespeed
-					missile->fuse = 1;
-
-				if (missile->fuse > mobj->fuse)
-				{
-					P_RemoveMobj(missile);
-				}
-				else
+				if (!P_MobjWasRemoved(missile))
 				{
-					if (mobj->health > mobj->info->damage)
+					missile->fuse = (dist/P_AproxDistance(missile->momx, missile->momy));
+					if (missile->fuse <= 0) // Prevents a division by zero when calculating missile->scalespeed
+						missile->fuse = 1;
+
+					if (missile->fuse > mobj->fuse)
 					{
-						P_SetScale(missile, FRACUNIT/3);
-						missile->color = SKINCOLOR_MAGENTA; // sonic OVA/4 purple power
+						P_RemoveMobj(missile);
 					}
 					else
 					{
-						P_SetScale(missile, FRACUNIT/5);
-						missile->color = SKINCOLOR_SUNSET; // sonic cd electric power
+						if (mobj->health > mobj->info->damage)
+						{
+							P_SetScale(missile, FRACUNIT/3);
+							missile->color = SKINCOLOR_MAGENTA; // sonic OVA/4 purple power
+						}
+						else
+						{
+							P_SetScale(missile, FRACUNIT/5);
+							missile->color = SKINCOLOR_SUNSET; // sonic cd electric power
+						}
+						missile->destscale = missile->scale*2;
+						missile->scalespeed = abs(missile->scale - missile->destscale)/missile->fuse;
+						missile->colorized = true;
 					}
-					missile->destscale = missile->scale*2;
-					missile->scalespeed = abs(missile->scale - missile->destscale)/missile->fuse;
-					missile->colorized = true;
 				}
 			}
 
@@ -5744,6 +5802,7 @@ static void P_Boss9Thinker(mobj_t *mobj)
 
 		// threshold is used for attacks/maneuvers.
 		if (mobj->threshold && mobj->movecount != 2) {
+			mobj_t *ghost;
 			fixed_t speed = 20*FRACUNIT + FixedMul(40*FRACUNIT, FixedDiv((mobj->info->spawnhealth - mobj->health)<<FRACBITS, mobj->info->spawnhealth<<FRACBITS));
 			UINT8 tries = 0;
 
@@ -5763,50 +5822,59 @@ static void P_Boss9Thinker(mobj_t *mobj)
 
 					A_FaceTarget(mobj);
 					missile = P_SpawnMissile(mobj, mobj->target, mobj->info->speed);
-					if (mobj->extravalue1 >= 2)
-					{
-						missile->destscale = FRACUNIT>>1;
-						P_SetScale(missile, missile->destscale);
-					}
-					missile->fuse = 3*TICRATE;
-					missile->z -= missile->height/2;
-
-					if (mobj->extravalue1 == 2)
+					if (!P_MobjWasRemoved(missile))
 					{
-						UINT8 i;
-						mobj_t *spread;
-						for (i = 0; i < 5; i++)
+						if (mobj->extravalue1 >= 2)
 						{
-							if (i == 2)
-								continue;
-							spread = P_SpawnMobj(missile->x, missile->y, missile->z, missile->type);
-							spread->angle = missile->angle+(ANGLE_11hh/2)*(i-2);
-							P_InstaThrust(spread,spread->angle,spread->info->speed);
-							spread->momz = missile->momz;
-							spread->destscale = FRACUNIT>>1;
-							P_SetScale(spread, spread->destscale);
-							spread->fuse = missile->fuse;
+							missile->destscale = FRACUNIT>>1;
+							P_SetScale(missile, missile->destscale);
 						}
-						P_InstaThrust(missile,missile->angle,missile->info->speed);
-					}
-					else if (mobj->extravalue1 >= 3)
-					{
-						UINT8 i;
-						mobj_t *spread;
-						mobj->target->z -= (2*missile->height);
-						for (i = 0; i < 5; i++)
+						missile->fuse = 3*TICRATE;
+						missile->z -= missile->height/2;
+
+						if (mobj->extravalue1 == 2)
 						{
-							if (i != 2)
+							UINT8 i;
+							mobj_t *spread;
+							for (i = 0; i < 5; i++)
 							{
-								spread = P_SpawnMissile(mobj, mobj->target, missile->type);
+								if (i == 2)
+									continue;
+								spread = P_SpawnMobj(missile->x, missile->y, missile->z, missile->type);
+								if (P_MobjWasRemoved(spread))
+									continue;
+
+								spread->angle = missile->angle+(ANGLE_11hh/2)*(i-2);
+								P_InstaThrust(spread,spread->angle,spread->info->speed);
+								spread->momz = missile->momz;
 								spread->destscale = FRACUNIT>>1;
 								P_SetScale(spread, spread->destscale);
 								spread->fuse = missile->fuse;
-								spread->z -= spread->height/2;
 							}
-							mobj->target->z += missile->height;
+							P_InstaThrust(missile,missile->angle,missile->info->speed);
+						}
+						else if (mobj->extravalue1 >= 3)
+						{
+							UINT8 i;
+							mobj_t *spread;
+							mobj->target->z -= (2*missile->height);
+							for (i = 0; i < 5; i++)
+							{
+								if (i != 2)
+								{
+									spread = P_SpawnMissile(mobj, mobj->target, missile->type);
+									if (!P_MobjWasRemoved(spread))
+									{
+										spread->destscale = FRACUNIT>>1;
+										P_SetScale(spread, spread->destscale);
+										spread->fuse = missile->fuse;
+										spread->z -= spread->height/2;
+									}
+								}
+								mobj->target->z += missile->height;
+							}
+							mobj->target->z -= (3*missile->height);
 						}
-						mobj->target->z -= (3*missile->height);
 					}
 				}
 				else
@@ -5865,7 +5933,9 @@ static void P_Boss9Thinker(mobj_t *mobj)
 				return;
 			}
 
-			P_SpawnGhostMobj(mobj)->colorized = false;
+			ghost = P_SpawnGhostMobj(mobj);
+			if (!P_MobjWasRemoved(ghost))
+				ghost->colorized = false;
 
 			// Vector form dodge!
 			mobj->angle += mobj->movedir;
@@ -5963,8 +6033,11 @@ static void P_Boss9Thinker(mobj_t *mobj)
 				if (mobj->health > mobj->info->damage)
 				{ // No more bubble if we're broken (pinch phase)
 					mobj_t *shield = P_SpawnMobj(mobj->x, mobj->y, mobj->z, MT_MSSHIELD_FRONT);
-					P_SetTarget(&mobj->hprev, shield);
-					P_SetTarget(&shield->target, mobj);
+					if (!P_MobjWasRemoved(shield))
+					{
+						P_SetTarget(&mobj->hprev, shield);
+						P_SetTarget(&shield->target, mobj);
+					}
 
 					// Attack 2: Energy shot!
 					switch (mobj->health)
@@ -6299,7 +6372,8 @@ void P_SpawnHoopOfSomething(fixed_t x, fixed_t y, fixed_t z, fixed_t radius, INT
 		finalz = z + v.z;
 
 		mobj = P_SpawnMobj(finalx, finaly, finalz, type);
-		mobj->z -= mobj->height/2;
+		if (!P_MobjWasRemoved(mobj))
+			mobj->z -= mobj->height/2;
 	}
 }
 
@@ -6340,6 +6414,8 @@ void P_SpawnParaloop(fixed_t x, fixed_t y, fixed_t z, fixed_t radius, INT32 numb
 		finalz = z + v.z;
 
 		mobj = P_SpawnMobj(finalx, finaly, finalz, type);
+		if (P_MobjWasRemoved(mobj))
+			continue;
 
 		mobj->z -= mobj->height>>1;
 
@@ -6996,6 +7072,8 @@ static void P_KoopaThinker(mobj_t *koopa)
 	{
 		mobj_t *flame;
 		flame = P_SpawnMobj(koopa->x - koopa->radius + FixedMul(5*FRACUNIT, koopa->scale), koopa->y, koopa->z + (P_RandomByte()<<(FRACBITS-2)), MT_KOOPAFLAME);
+		if (P_MobjWasRemoved(flame))
+			return;
 		flame->momx = -FixedMul(flame->info->speed, flame->scale);
 		S_StartSound(flame, sfx_koopfr);
 	}
@@ -7003,6 +7081,8 @@ static void P_KoopaThinker(mobj_t *koopa)
 	{
 		mobj_t *hammer;
 		hammer = P_SpawnMobj(koopa->x - koopa->radius, koopa->y, koopa->z + koopa->height, MT_HAMMER);
+		if (P_MobjWasRemoved(hammer))
+			return;
 		hammer->momx = FixedMul(-5*FRACUNIT, hammer->scale);
 		hammer->momz = FixedMul(7*FRACUNIT, hammer->scale);
 	}
@@ -7021,6 +7101,9 @@ static void P_SpawnMinecartSegments(mobj_t *mobj, boolean mode)
 	for (i = 0; i < 4; i++)
 	{
 		seg = P_SpawnMobj(x, y, z, MT_MINECARTSEG);
+		if (P_MobjWasRemoved(seg))
+			continue;
+
 		P_SetMobjState(seg, (statenum_t)(S_MINECARTSEG_FRONT + i));
 		if (i >= 2)
 			seg->extravalue1 = (i == 2) ? -18 : 18; // make -20/20 when papersprite projection fixed
@@ -7076,6 +7159,8 @@ static void P_PyreFlyBurn(mobj_t *mobj, fixed_t hoffs, INT16 vrange, mobjtype_t
 	fixed_t yoffs = FixedMul(FINESINE(fa), mobj->radius + hoffs);
 	fixed_t zoffs = P_RandomRange(-vrange, vrange)*FRACUNIT;
 	mobj_t *particle = P_SpawnMobjFromMobj(mobj, xoffs, yoffs, zoffs, mobjtype);
+	if (P_MobjWasRemoved(particle))
+		return;
 	particle->momz = momz;
 	particle->flags2 |= MF2_LINKDRAW;
 	P_SetTarget(&particle->tracer, mobj);
@@ -7234,6 +7319,8 @@ static void P_FlameJetSceneryThink(mobj_t *mobj)
 		mobj->fuse -= 2;
 
 	flame = P_SpawnMobj(mobj->x, mobj->y, mobj->z, MT_FLAMEJETFLAME);
+	if (P_MobjWasRemoved(flame))
+		return;
 	P_SetMobjState(flame, S_FLAMEJETFLAME4);
 
 	flame->angle = mobj->angle;
@@ -7272,6 +7359,8 @@ static void P_VerticalFlameJetSceneryThink(mobj_t *mobj)
 		mobj->fuse--;
 
 	flame = P_SpawnMobj(mobj->x, mobj->y, mobj->z, MT_FLAMEJETFLAME);
+	if (P_MobjWasRemoved(flame))
+		return;
 
 	strength = (mobj->movedir ? mobj->movedir : 80)<<(FRACBITS-2);
 
@@ -7345,13 +7434,16 @@ static boolean P_ParticleGenSceneryThink(mobj_t *mobj)
 				mobj->y + FixedMul(FixedMul(mobj->friction, mobj->scale), FINESINE(mobj->angle >> ANGLETOFINESHIFT)),
 				mobj->z,
 				(mobjtype_t)mobj->threshold);
-			P_SetScale(spawn, mobj->scale);
-			spawn->momz = FixedMul(mobj->movefactor, spawn->scale);
-			spawn->destscale = spawn->scale/100;
-			spawn->scalespeed = spawn->scale/mobj->health;
-			spawn->tics = (tic_t)mobj->health;
-			spawn->flags2 |= (mobj->flags2 & MF2_OBJECTFLIP);
-			spawn->angle += P_RandomKey(36)*ANG10; // irrelevant for default objects but might make sense for some custom ones
+			if (!P_MobjWasRemoved(spawn))
+			{
+				P_SetScale(spawn, mobj->scale);
+				spawn->momz = FixedMul(mobj->movefactor, spawn->scale);
+				spawn->destscale = spawn->scale/100;
+				spawn->scalespeed = spawn->scale/mobj->health;
+				spawn->tics = (tic_t)mobj->health;
+				spawn->flags2 |= (mobj->flags2 & MF2_OBJECTFLIP);
+				spawn->angle += P_RandomKey(36)*ANG10; // irrelevant for default objects but might make sense for some custom ones
+			}
 
 			mobj->angle += mobj->movedir;
 		}
@@ -7547,7 +7639,7 @@ static void P_RosySceneryThink(mobj_t *mobj)
 		if (stat == S_ROSY_HUG)
 		{
 			if (player->panim != PA_IDLE)
-				P_SetPlayerMobjState(mobj->target, S_PLAY_STND);
+				P_SetMobjState(mobj->target, S_PLAY_STND);
 			player->pflags |= PF_STASIS;
 		}
 
@@ -7563,13 +7655,16 @@ static void P_RosySceneryThink(mobj_t *mobj)
 		if (makeheart)
 		{
 			mobj_t *cdlhrt = P_SpawnMobjFromMobj(mobj, 0, 0, mobj->height, MT_CDLHRT);
-			cdlhrt->destscale = (5*mobj->scale) >> 4;
-			P_SetScale(cdlhrt, cdlhrt->destscale);
-			cdlhrt->fuse = (5*TICRATE) >> 1;
-			cdlhrt->momz = mobj->scale;
-			P_SetTarget(&cdlhrt->target, mobj);
-			cdlhrt->extravalue1 = mobj->x;
-			cdlhrt->extravalue2 = mobj->y;
+			if (!P_MobjWasRemoved(cdlhrt))
+			{
+				cdlhrt->destscale = (5*mobj->scale) >> 4;
+				P_SetScale(cdlhrt, cdlhrt->destscale);
+				cdlhrt->fuse = (5*TICRATE) >> 1;
+				cdlhrt->momz = mobj->scale;
+				P_SetTarget(&cdlhrt->target, mobj);
+				cdlhrt->extravalue1 = mobj->x;
+				cdlhrt->extravalue2 = mobj->y;
+			}
 		}
 	}
 }
@@ -7696,13 +7791,16 @@ static void P_MobjSceneryThink(mobj_t *mobj)
 		&& */ (mobj->target->player->pflags & PF_SHIELDABILITY))
 		{
 			mobj_t *whoosh = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_GHOST); // done here so the offset is correct
-			P_SetMobjState(whoosh, mobj->info->raisestate);
-			whoosh->destscale = whoosh->scale << 1;
-			whoosh->scalespeed = FixedMul(whoosh->scalespeed, whoosh->scale);
-			whoosh->height = 38*whoosh->scale;
-			whoosh->fuse = 10;
-			whoosh->flags |= MF_NOCLIPHEIGHT;
-			whoosh->momz = mobj->target->momz; // Stay reasonably centered for a few frames
+			if (!P_MobjWasRemoved(whoosh))
+			{
+				P_SetMobjState(whoosh, mobj->info->raisestate);
+				whoosh->destscale = whoosh->scale << 1;
+				whoosh->scalespeed = FixedMul(whoosh->scalespeed, whoosh->scale);
+				whoosh->height = 38*whoosh->scale;
+				whoosh->fuse = 10;
+				whoosh->flags |= MF_NOCLIPHEIGHT;
+				whoosh->momz = mobj->target->momz; // Stay reasonably centered for a few frames
+			}
 			mobj->target->player->pflags &= ~PF_SHIELDABILITY; // prevent eternal whoosh
 		}
 		/* FALLTHRU */
@@ -8038,8 +8136,11 @@ static boolean P_MobjBossThink(mobj_t *mobj)
 					P_RandomRange(rad, -rad) << FRACBITS,
 					P_RandomRange(hei / 2, hei) << FRACBITS,
 					MT_SMOKE);
-				P_SetObjectMomZ(particle, 2 << FRACBITS, false);
-				particle->momz += mobj->momz;
+				if (!P_MobjWasRemoved(particle))
+				{
+					P_SetObjectMomZ(particle, 2 << FRACBITS, false);
+					particle->momz += mobj->momz;
+				}
 			}
 			if (mobj->flags2 & MF2_SKULLFLY)
 #if 1
@@ -8048,8 +8149,11 @@ static boolean P_MobjBossThink(mobj_t *mobj)
 			{
 				mobj_t *spawnmobj;
 				spawnmobj = P_SpawnMobj(mobj->x, mobj->y, mobj->z, mobj->info->painchance);
-				P_SetTarget(&spawnmobj->target, mobj);
-				spawnmobj->color = SKINCOLOR_GREY;
+				if (!P_MobjWasRemoved(spawnmobj))
+				{
+					P_SetTarget(&spawnmobj->target, mobj);
+					spawnmobj->color = SKINCOLOR_GREY;
+				}
 			}
 #endif
 			P_Boss1Thinker(mobj);
@@ -8064,8 +8168,11 @@ static boolean P_MobjBossThink(mobj_t *mobj)
 					P_RandomRange(rad, -rad) << FRACBITS,
 					P_RandomRange(hei/2, hei) << FRACBITS,
 					MT_SMOKE);
-				P_SetObjectMomZ(particle, 2 << FRACBITS, false);
-				particle->momz += mobj->momz;
+				if (!P_MobjWasRemoved(particle))
+				{
+					P_SetObjectMomZ(particle, 2 << FRACBITS, false);
+					particle->momz += mobj->momz;
+				}
 			}
 			P_Boss2Thinker(mobj);
 			break;
@@ -8079,8 +8186,11 @@ static boolean P_MobjBossThink(mobj_t *mobj)
 					P_RandomRange(rad, -rad) << FRACBITS,
 					P_RandomRange(hei/2, hei) << FRACBITS,
 					MT_SMOKE);
-				P_SetObjectMomZ(particle, 2 << FRACBITS, false);
-				particle->momz += mobj->momz;
+				if (!P_MobjWasRemoved(particle))
+				{
+					P_SetObjectMomZ(particle, 2 << FRACBITS, false);
+					particle->momz += mobj->momz;
+				}
 			}
 			P_Boss3Thinker(mobj);
 			break;
@@ -8094,8 +8204,11 @@ static boolean P_MobjBossThink(mobj_t *mobj)
 					P_RandomRange(rad, -rad) << FRACBITS,
 					P_RandomRange(hei/2, hei) << FRACBITS,
 					MT_SMOKE);
-				P_SetObjectMomZ(particle, 2 << FRACBITS, false);
-				particle->momz += mobj->momz;
+				if (!P_MobjWasRemoved(particle))
+				{
+					P_SetObjectMomZ(particle, 2 << FRACBITS, false);
+					particle->momz += mobj->momz;
+				}
 			}
 			P_Boss4Thinker(mobj);
 			break;
@@ -8223,6 +8336,9 @@ static boolean P_MobjDeadThink(mobj_t *mobj)
 				y = mobj->y + FixedMul(FINECOSINE(fa), ns);
 
 				mo2 = P_SpawnMobj(x, y, z, MT_EXPLODE);
+				if (P_MobjWasRemoved(mo2))
+					continue;
+
 				P_SetMobjStateNF(mo2, S_XPLD_EGGTRAP); // so the flickies don't lose their target if they spawn
 				ns = 4*FRACUNIT;
 				mo2->momx = FixedMul(FINESINE(fa), ns);
@@ -8268,7 +8384,8 @@ static boolean P_MobjDeadThink(mobj_t *mobj)
 					mobj->y + (P_RandomRange(r, -r) << FRACBITS),
 					mobj->z + (P_RandomKey(mobj->height >> FRACBITS) << FRACBITS),
 					MT_SONIC3KBOSSEXPLODE);
-				S_StartSound(explosion, sfx_s3kb4);
+				if (!P_MobjWasRemoved(explosion))
+					S_StartSound(explosion, sfx_s3kb4);
 			}
 			if (mobj->movedir == DMG_DROWNED)
 				P_SetObjectMomZ(mobj, -FRACUNIT/2, true); // slower fall from drowning
@@ -8286,7 +8403,8 @@ static boolean P_MobjDeadThink(mobj_t *mobj)
 				mobj->y + (P_RandomRange(r, -r) << FRACBITS),
 				mobj->z + (P_RandomKey(mobj->height >> FRACBITS) << FRACBITS),
 				MT_SONIC3KBOSSEXPLODE);
-			S_StartSound(explosion, sfx_s3kb4);
+			if (!P_MobjWasRemoved(explosion))
+				S_StartSound(explosion, sfx_s3kb4);
 		}
 		P_SetObjectMomZ(mobj, -2*FRACUNIT/3, true);
 	}
@@ -8384,9 +8502,12 @@ static void P_ArrowThink(mobj_t *mobj)
 		if (leveltime & 1)
 		{
 			mobj_t *dust = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_PARTICLE);
-			dust->tics = 18;
-			dust->scalespeed = 4096;
-			dust->destscale = FRACUNIT/32;
+			if (!P_MobjWasRemoved(dust))
+			{
+				dust->tics = 18;
+				dust->scalespeed = 4096;
+				dust->destscale = FRACUNIT/32;
+			}
 		}
 	}
 	else
@@ -8989,6 +9110,9 @@ static boolean P_TurretThink(mobj_t *mobj)
 				y = mobj->y + FixedMul(FINECOSINE(fa), ns);
 
 				mo2 = P_SpawnMobj(x, y, z, MT_EXPLODE);
+				if (P_MobjWasRemoved(mo2))
+					continue;
+
 				ns = FixedMul(16*FRACUNIT, mobj->scale);
 				mo2->momx = FixedMul(FINESINE(fa), ns);
 				mo2->momy = FixedMul(FINECOSINE(fa), ns);
@@ -9210,10 +9334,13 @@ static void P_DragonbomberThink(mobj_t *mobj)
 			if (segment != mobj) // found an unactivated segment?
 			{
 				mobj_t *mine = P_SpawnMobjFromMobj(segment, 0, 0, 0, segment->info->painchance);
-				mine->angle = segment->angle;
-				P_InstaThrust(mine, mobj->angle, P_AproxDistance(mobj->momx, mobj->momy) >> 1);
-				P_SetObjectMomZ(mine, -2*FRACUNIT, true);
-				S_StartSound(mine, mine->info->seesound);
+				if (!P_MobjWasRemoved(mine))
+				{
+					mine->angle = segment->angle;
+					P_InstaThrust(mine, mobj->angle, P_AproxDistance(mobj->momx, mobj->momy) >> 1);
+					P_SetObjectMomZ(mine, -2*FRACUNIT, true);
+					S_StartSound(mine, mine->info->seesound);
+				}
 				P_SetMobjState(segment, segment->info->raisestate);
 				mobj->threshold = mobj->info->painchance;
 			}
@@ -9432,9 +9559,12 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
 			if (!mobj->threshold && !mobj->target && mobj->reactiontime)
 			{
 				mobj_t *emerald = P_SpawnMobj(mobj->x, mobj->y, mobj->z, mobj->reactiontime);
-				emerald->threshold = 42;
-				P_SetTarget(&mobj->target, emerald);
-				P_SetTarget(&emerald->target, mobj);
+				if (!P_MobjWasRemoved(emerald))
+				{
+					emerald->threshold = 42;
+					P_SetTarget(&mobj->target, emerald);
+					P_SetTarget(&emerald->target, mobj);
+				}
 			}
 		}
 		break;
@@ -9781,6 +9911,8 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
 	case MT_TRAINDUSTSPAWNER:
 		if (leveltime % 5 == 0) {
 			mobj_t* traindust = P_SpawnMobj(mobj->x, mobj->y, mobj->z, MT_PARTICLE);
+			if (P_MobjWasRemoved(traindust))
+				break;
 			traindust->flags = MF_SCENERY;
 			P_SetMobjState(traindust, S_TRAINDUST);
 			traindust->frame = P_RandomRange(0, 8)|FF_TRANS90;
@@ -9794,6 +9926,8 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
 	case MT_TRAINSTEAMSPAWNER:
 		if (leveltime % 5 == 0) {
 			mobj_t *steam = P_SpawnMobj(mobj->x + FRACUNIT*P_SignedRandom()/2, mobj->y + FRACUNIT*P_SignedRandom()/2, mobj->z, MT_PARTICLE);
+			if (P_MobjWasRemoved(steam))
+				break;
 			P_SetMobjState(steam, S_TRAINSTEAM);
 			steam->frame = P_RandomRange(0, 1)|FF_TRANS90;
 			steam->tics = TICRATE*8;
@@ -9995,7 +10129,8 @@ for (i = ((mobj->flags2 & MF2_STRONGBOX) ? strongboxamt : weakboxamt); i; --i) s
 		newmobj = P_SpawnMobj(mobj->x, mobj->y, mobj->z, mobj->type);
 
 	// Transfer flags2 (ambush, strongbox, objectflip)
-	newmobj->flags2 = mobj->flags2;
+	if (!P_MobjWasRemoved(newmobj))
+		newmobj->flags2 = mobj->flags2;
 	P_RemoveMobj(mobj); // make sure they disappear
 }
 
@@ -10017,6 +10152,8 @@ static void P_FlagFuseThink(mobj_t *mobj)
 	else
 		z = ss->sector->floorheight + z;
 	flagmo = P_SpawnMobj(x, y, z, mobj->type);
+	if (P_MobjWasRemoved(flagmo))
+		return;
 	flagmo->spawnpoint = mobj->spawnpoint;
 	if (mobj->spawnpoint->options & MTF_OBJECTFLIP)
 	{
@@ -10454,12 +10591,15 @@ void P_PushableThinker(mobj_t *mobj)
 					z = ss->sector->floorheight;
 
 				spawnmo = P_SpawnMobj(x, y, z, mobj->type);
-				spawnmo->spawnpoint = mobj->spawnpoint;
-				P_UnsetThingPosition(spawnmo);
-				spawnmo->flags = mobj->flags;
-				P_SetThingPosition(spawnmo);
-				spawnmo->flags2 = mobj->flags2;
-				spawnmo->flags |= MF_PUSHABLE;
+				if (!P_MobjWasRemoved(spawnmo))
+				{
+					spawnmo->spawnpoint = mobj->spawnpoint;
+					P_UnsetThingPosition(spawnmo);
+					spawnmo->flags = mobj->flags;
+					P_SetThingPosition(spawnmo);
+					spawnmo->flags2 = mobj->flags2;
+					spawnmo->flags |= MF_PUSHABLE;
+				}
 				P_RemoveMobj(mobj);
 				break;
 			default:
@@ -10543,6 +10683,7 @@ static fixed_t P_DefaultMobjShadowScale (mobj_t *thing)
 		case MT_SMALLGRABCHAIN:
 		case MT_BIGGRABCHAIN:
 
+		case MT_BLUESPRINGBALL:
 		case MT_YELLOWSPRINGBALL:
 		case MT_REDSPRINGBALL:
 
@@ -10570,14 +10711,14 @@ static fixed_t P_DefaultMobjShadowScale (mobj_t *thing)
 		case MT_EXPLOSIONRING:
 		case MT_SCATTERRING:
 		case MT_GRENADERING:
-		
+
 		case MT_BOUNCEPICKUP:
 		case MT_RAILPICKUP:
 		case MT_AUTOPICKUP:
 		case MT_EXPLODEPICKUP:
 		case MT_SCATTERPICKUP:
 		case MT_GRENADEPICKUP:
-		
+
 		case MT_REDRING:
 		case MT_THROWNBOUNCE:
 		case MT_THROWNINFINITY:
@@ -10634,29 +10775,28 @@ static fixed_t P_DefaultMobjShadowScale (mobj_t *thing)
 //
 // P_SpawnMobj
 //
-mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
+mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type, ...)
 {
 	const mobjinfo_t *info = &mobjinfo[type];
 	SINT8 sc = -1;
 	state_t *st;
 	mobj_t *mobj;
+	int status;
+	va_list args;
 
 	if (type == MT_NULL)
-	{
-#if 0
-#ifdef PARANOIA
-		I_Error("Tried to spawn MT_NULL\n");
-#endif
 		return NULL;
-#endif
-		// Hack: Some code assumes that P_SpawnMobj can never return NULL
-		// So replace MT_NULL with MT_RAY in the meantime
-		// Remove when dealt properly
-		CONS_Debug(DBG_GAMELOGIC, "Tried to spawn MT_NULL, using MT_RAY\n");
-		type = MT_RAY;
-	}
 
-	mobj = Z_Calloc(sizeof (*mobj), PU_LEVEL, NULL);
+	if (mobjcache != NULL)
+	{
+		mobj = mobjcache;
+		mobjcache = mobjcache->hnext;
+		memset(mobj, 0, sizeof(*mobj));
+	}
+	else
+	{
+		mobj = Z_Calloc(sizeof (*mobj), PU_LEVEL, NULL);
+	}
 
 	// this is officially a mobj, declared as soon as possible.
 	mobj->thinker.function.acp1 = (actionf_p1)P_MobjThinker;
@@ -10748,9 +10888,27 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 	// Set shadowscale here, before spawn hook so that Lua can change it
 	mobj->shadowscale = P_DefaultMobjShadowScale(mobj);
 
+	if (!(mobj->flags & MF_NOTHINK))
+		P_AddThinker(THINK_MOBJ, &mobj->thinker);
+
+
+	if (type == MT_PLAYER)
+	{
+		// when spawning MT_PLAYER, set mobj->player before calling MobjSpawn hook to prevent P_RemoveMobj from succeeding on player mobj.
+		va_start(args, type);
+		mobj->player = va_arg(args, player_t *);
+		va_end(args);
+	}
+
+	// increment mobj reference, so we don't get a dangling reference in case MobjSpawn calls P_RemoveMobj
+	mobj->thinker.references++;
+
 	// DANGER! This can cause P_SpawnMobj to return NULL!
 	// Avoid using P_RemoveMobj on the newly created mobj in "MobjSpawn" Lua hooks!
-	if (LUA_HookMobj(mobj, MOBJ_HOOK(MobjSpawn)))
+	status = LUA_HookMobj(mobj, MOBJ_HOOK(MobjSpawn));
+	mobj->thinker.references--;
+
+	if (status)
 	{
 		if (P_MobjWasRemoved(mobj))
 			return NULL;
@@ -10772,6 +10930,9 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 		case MT_BLACKEGGMAN:
 			{
 				mobj_t *spawn = P_SpawnMobj(mobj->x, mobj->z, mobj->z+mobj->height-16*FRACUNIT, MT_BLACKEGGMAN_HELPER);
+				if (P_MobjWasRemoved(spawn))
+					break;
+
 				spawn->destscale = mobj->scale;
 				P_SetScale(spawn, mobj->scale);
 				P_SetTarget(&spawn->target, mobj);
@@ -10787,6 +10948,9 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 		case MT_EGGGUARD:
 			{
 				mobj_t *spawn = P_SpawnMobj(x, y, z, MT_EGGSHIELD);
+				if (P_MobjWasRemoved(spawn))
+					break;
+
 				spawn->destscale = mobj->scale;
 				P_SetScale(spawn, mobj->scale);
 				P_SetTarget(&mobj->tracer, spawn);
@@ -10802,6 +10966,9 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 				for (i = 0; i < mobj->info->damage; i++)
 				{
 					ball = P_SpawnMobj(x, y, z, mobj->info->painchance);
+					if (P_MobjWasRemoved(ball))
+						continue;
+
 					ball->destscale = mobj->scale;
 					P_SetScale(ball, mobj->scale);
 					P_SetTarget(&ball->target, mobj);
@@ -10821,6 +10988,9 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 				for (q = 0; q < mobj->info->painchance; q++)
 				{
 					ball = P_SpawnMobj(x, y, z, mobj->info->mass);
+					if (P_MobjWasRemoved(ball))
+						continue;
+
 					ball->destscale = mobj->scale;
 					P_SetScale(ball, mobj->scale);
 					P_SetTarget(&lastball->tracer, ball);
@@ -10832,18 +11002,24 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 		case MT_CRUSHSTACEAN:
 			{
 				mobj_t *bigmeatyclaw = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_CRUSHCLAW);
-				bigmeatyclaw->angle = mobj->angle + ((mobj->flags2 & MF2_AMBUSH) ? ANGLE_90 : ANGLE_270);;
-				P_SetTarget(&mobj->tracer, bigmeatyclaw);
-				P_SetTarget(&bigmeatyclaw->tracer, mobj);
+				if (!P_MobjWasRemoved(bigmeatyclaw))
+				{
+					bigmeatyclaw->angle = mobj->angle + ((mobj->flags2 & MF2_AMBUSH) ? ANGLE_90 : ANGLE_270);
+					P_SetTarget(&mobj->tracer, bigmeatyclaw);
+					P_SetTarget(&bigmeatyclaw->tracer, mobj);
+				}
 				mobj->reactiontime >>= 1;
 			}
 			break;
 		case MT_BANPYURA:
 			{
 				mobj_t *bigmeatyclaw = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_BANPSPRING);
-				bigmeatyclaw->angle = mobj->angle + ((mobj->flags2 & MF2_AMBUSH) ? ANGLE_90 : ANGLE_270);;
-				P_SetTarget(&mobj->tracer, bigmeatyclaw);
-				P_SetTarget(&bigmeatyclaw->tracer, mobj);
+				if (!P_MobjWasRemoved(bigmeatyclaw))
+				{
+					bigmeatyclaw->angle = mobj->angle + ((mobj->flags2 & MF2_AMBUSH) ? ANGLE_90 : ANGLE_270);
+					P_SetTarget(&mobj->tracer, bigmeatyclaw);
+					P_SetTarget(&bigmeatyclaw->tracer, mobj);
+				}
 				mobj->reactiontime >>= 1;
 			}
 			break;
@@ -10858,6 +11034,9 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 				for (i = 0; i <= 16; i++) // probably should be < but staying authentic to the Lua version
 				{
 					cur = P_SpawnMobjFromMobj(mobj, 0, 0, 0, ((mobj->type == MT_WAVINGFLAG1) ? MT_WAVINGFLAGSEG1 : MT_WAVINGFLAGSEG2));;
+					if (P_MobjWasRemoved(cur))
+						continue;
+
 					P_SetTarget(&prev->tracer, cur);
 					cur->extravalue1 = i;
 					prev = cur;
@@ -10895,11 +11074,17 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 			{
 				mobj_t *fire;
 				fire = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_SPINBOBERT_FIRE1);
-				P_SetTarget(&fire->target, mobj);
-				P_SetTarget(&mobj->hnext, fire);
+				if (!P_MobjWasRemoved(fire))
+				{
+					P_SetTarget(&fire->target, mobj);
+					P_SetTarget(&mobj->hnext, fire);
+				}
 				fire = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_SPINBOBERT_FIRE2);
-				P_SetTarget(&fire->target, mobj);
-				P_SetTarget(&mobj->hprev, fire);
+				if (!P_MobjWasRemoved(fire))
+				{
+					P_SetTarget(&fire->target, mobj);
+					P_SetTarget(&mobj->hprev, fire);
+				}
 			}
 			break;
 		case MT_REDRING: // Make MT_REDRING red by default
@@ -10916,8 +11101,8 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 		case MT_EGGCAPSULE:
 			mobj->reactiontime = 0;
 			mobj->extravalue1 = mobj->cvmem =\
-			 mobj->cusval = mobj->movecount =\
-			 mobj->lastlook = mobj->extravalue2 = -1;
+			mobj->cusval = mobj->movecount =\
+			mobj->lastlook = mobj->extravalue2 = -1;
 			break;
 		case MT_REDTEAMRING:
 			mobj->color = skincolor_redteam;
@@ -10953,6 +11138,8 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 		case MT_OILLAMP:
 			{
 				mobj_t* overlay = P_SpawnMobj(mobj->x, mobj->y, mobj->z, MT_OVERLAY);
+				if (P_MobjWasRemoved(overlay))
+					break;
 				P_SetTarget(&overlay->target, mobj);
 				P_SetMobjState(overlay, S_OILLAMPFLARE);
 				break;
@@ -10962,13 +11149,19 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 			mobj->flags2 |= MF2_INVERTAIMABLE;
 			break;
 		case MT_MINECARTEND:
-			P_SetTarget(&mobj->tracer, P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_MINECARTENDSOLID));
-			mobj->tracer->angle = mobj->angle + ANGLE_90;
+			{
+				mobj_t *mcsolid = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_MINECARTENDSOLID);
+				if (P_MobjWasRemoved(mcsolid))
+					break;
+				P_SetTarget(&mobj->tracer, mcsolid);
+				mcsolid->angle = mobj->angle + ANGLE_90;
+			}
 			break;
 		case MT_TORCHFLOWER:
 			{
 				mobj_t *fire = P_SpawnMobjFromMobj(mobj, 0, 0, 46*FRACUNIT, MT_FLAME);
-				P_SetTarget(&mobj->target, fire);
+				if (!P_MobjWasRemoved(fire))
+					P_SetTarget(&mobj->target, fire);
 				break;
 			}
 		case MT_PYREFLY:
@@ -10977,10 +11170,16 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 			mobj->fuse = 100;
 			break;
 		case MT_SIGN:
-			P_SetTarget(&mobj->tracer, P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_OVERLAY));
-			P_SetTarget(&mobj->tracer->target, mobj);
-			P_SetMobjState(mobj->tracer, S_SIGNBOARD);
-			mobj->tracer->movedir = ANGLE_90;
+			{
+				mobj_t *sign = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_OVERLAY);
+				if (P_MobjWasRemoved(sign))
+					break;
+
+				P_SetTarget(&mobj->tracer, sign);
+				P_SetTarget(&sign->target, mobj);
+				P_SetMobjState(sign, S_SIGNBOARD);
+				sign->movedir = ANGLE_90;
+			}
 		default:
 			break;
 	}
@@ -11003,9 +11202,6 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
 		}
 	}
 
-	if (!(mobj->flags & MF_NOTHINK))
-		P_AddThinker(THINK_MOBJ, &mobj->thinker);
-
 	if (mobj->skin) // correct inadequecies above.
 	{
 		mobj->sprite2 = P_GetSkinSprite2(mobj->skin, (mobj->frame & FF_FRAMEMASK), NULL);
@@ -11203,6 +11399,10 @@ void P_RemoveMobj(mobj_t *mobj)
 
 	P_SetTarget(&mobj->hnext, P_SetTarget(&mobj->hprev, NULL));
 
+	// clear the reference from the mapthing
+	if (mobj->spawnpoint)
+		mobj->spawnpoint->mobj = NULL;
+
 	R_RemoveMobjInterpolator(mobj);
 
 	// free block
@@ -11211,7 +11411,9 @@ void P_RemoveMobj(mobj_t *mobj)
 		INT32 prevreferences;
 		if (!mobj->thinker.references)
 		{
-			Z_Free(mobj); // No refrrences? Can be removed immediately! :D
+			// no references, dump it directly in the mobj cache
+			mobj->hnext = mobjcache;
+			mobjcache = mobj;
 			return;
 		}
 
@@ -11586,8 +11788,6 @@ void P_SpawnPlayer(INT32 playernum)
 				// Spawn as a spectator,
 				// yes even in splitscreen mode
 				p->spectator = true;
-				if (playernum&1) p->skincolor = skincolor_redteam;
-				else             p->skincolor = skincolor_blueteam;
 
 				// but immediately send a team change packet.
 				NetPacket.packet.playernum = playernum;
@@ -11607,25 +11807,20 @@ void P_SpawnPlayer(INT32 playernum)
 		// Fix stupid non spectator spectators.
 		if (!p->spectator && !p->ctfteam)
 			p->spectator = true;
-
-		// Fix team colors.
-		// This code isn't being done right somewhere else. Oh well.
-		if (p->ctfteam == 1)
-			p->skincolor = skincolor_redteam;
-		else if (p->ctfteam == 2)
-			p->skincolor = skincolor_blueteam;
 	}
 
 	if ((netgame || multiplayer) && ((gametyperules & GTR_SPAWNINVUL) || leveltime) && !p->spectator && !(maptol & TOL_NIGHTS))
 		p->powers[pw_flashing] = flashingtics-1; // Babysitting deterrent
 
-	mobj = P_SpawnMobj(0, 0, 0, MT_PLAYER);
-	(mobj->player = p)->mo = mobj;
+    // MT_PLAYER cannot be removed, so this shouldn't be able to return NULL.
+	mobj = P_SpawnMobj(0, 0, 0, MT_PLAYER, p);
+	I_Assert(mobj != NULL);
+	p->mo = mobj;
 
 	mobj->angle = 0;
 
 	// set color translations for player sprites
-	mobj->color = p->skincolor;
+	mobj->color = P_GetPlayerColor(p);
 
 	// set 'spritedef' override in mobj for player skins.. (see ProjectSprite)
 	// (usefulness: when body mobj is detached from player (who respawns),
@@ -11671,10 +11866,13 @@ void P_SpawnPlayer(INT32 playernum)
 			if (p == players) // this is totally the wrong place to do this aaargh.
 			{
 				mobj_t *idya = P_SpawnMobjFromMobj(mobj, 0, 0, mobj->height, MT_GOTEMERALD);
-				idya->health = 0; // for identification
-				P_SetTarget(&idya->target, mobj);
-				P_SetMobjState(idya, mobjinfo[MT_GOTEMERALD].missilestate);
-				P_SetTarget(&mobj->tracer, idya);
+				if (!P_MobjWasRemoved(idya))
+				{
+					idya->health = 0; // for identification
+					P_SetTarget(&idya->target, mobj);
+					P_SetMobjState(idya, mobjinfo[MT_GOTEMERALD].missilestate);
+					P_SetTarget(&mobj->tracer, idya);
+				}
 			}
 		}
 		else if (sstimer)
@@ -11773,9 +11971,9 @@ void P_MovePlayerToSpawn(INT32 playernum, mapthing_t *mthing)
 			mobj->flags2 |= MF2_OBJECTFLIP;
 		}
 		if (mthing->args[0])
-			P_SetPlayerMobjState(mobj, S_PLAY_FALL);
+			P_SetMobjState(mobj, S_PLAY_FALL);
 		else if (metalrecording)
-			P_SetPlayerMobjState(mobj, S_PLAY_WAIT);
+			P_SetMobjState(mobj, S_PLAY_WAIT);
 	}
 	else
 		z = floor;
@@ -12396,8 +12594,10 @@ static boolean P_SetupMace(mapthing_t *mthing, mobj_t *mobj, boolean *doangle)
 	mphase = (FixedAngle(mphase << FRACBITS) >> ANGLETOFINESHIFT);
 	mroll = (FixedAngle(mroll << FRACBITS) >> ANGLETOFINESHIFT);
 
-#define makemace(mobjtype, dist, moreflags2) {\
+#define makemace(mobjtype, dist, moreflags2) do {\
 	spawnee = P_SpawnMobj(mobj->x, mobj->y, mobj->z, mobjtype);\
+	if (P_MobjWasRemoved(spawnee))\
+		break;\
 	P_SetTarget(&spawnee->tracer, mobj);\
 	spawnee->threshold = mphase;\
 	spawnee->friction = mroll;\
@@ -12410,7 +12610,7 @@ static boolean P_SetupMace(mapthing_t *mthing, mobj_t *mobj, boolean *doangle)
 	P_SetTarget(&hprev->hnext, spawnee);\
 	P_SetTarget(&spawnee->hprev, hprev);\
 	hprev = spawnee;\
-}
+} while (0)
 
 	mdosound = (mspeed && !(mthing->args[8] & TMM_SILENT));
 	mdocenter = (macetype && (mthing->args[8] & TMM_CENTERLINK));
@@ -12475,7 +12675,7 @@ static boolean P_SetupMace(mapthing_t *mthing, mobj_t *mobj, boolean *doangle)
 
 		if (!mwidth)
 		{
-			if (mdosound && mnumspokes <= mmin) // Can it make a sound?
+			if (!P_MobjWasRemoved(spawnee) && mdosound && mnumspokes <= mmin) // Can it make a sound?
 				spawnee->flags2 |= MF2_BOSSNOTRAP;
 		}
 		else
@@ -12488,7 +12688,7 @@ static boolean P_SetupMace(mapthing_t *mthing, mobj_t *mobj, boolean *doangle)
 				while ((mwidthset -= widthfactor) > -mwidth)
 				{
 					makemace(firsttype, radiusfactor*mlengthset, MF2_AMBUSH);
-					if (mdosound && (mwidthset == msound) && mnumspokes <= mmin) // Can it make a sound?
+					if (!P_MobjWasRemoved(spawnee) && mdosound && (mwidthset == msound) && mnumspokes <= mmin) // Can it make a sound?
 						spawnee->flags2 |= MF2_BOSSNOTRAP;
 				}
 			}
@@ -12497,7 +12697,7 @@ static boolean P_SetupMace(mapthing_t *mthing, mobj_t *mobj, boolean *doangle)
 				while ((mwidthset += widthfactor) < -mwidth)
 				{
 					makemace(firsttype, radiusfactor*mlengthset, MF2_AMBUSH);
-					if (mdosound && (mwidthset == msound) && mnumspokes <= mmin) // Can it make a sound?
+					if (!P_MobjWasRemoved(spawnee) && mdosound && (mwidthset == msound) && mnumspokes <= mmin) // Can it make a sound?
 						spawnee->flags2 |= MF2_BOSSNOTRAP;
 				}
 			}
@@ -12679,15 +12879,21 @@ static boolean P_SetupNiGHTSDrone(mapthing_t *mthing, mobj_t *mobj)
 		mobj_t *droneman = P_SpawnMobjFromMobj(mobj, 0, 0, dronemanoffset, MT_NIGHTSDRONE_MAN);
 
 		P_SetTarget(&mobj->target, goalpost);
-		P_SetTarget(&goalpost->target, sparkle);
-		P_SetTarget(&goalpost->tracer, droneman);
+		if (!P_MobjWasRemoved(goalpost))
+		{
+			P_SetTarget(&goalpost->target, sparkle);
+			P_SetTarget(&goalpost->tracer, droneman);
+		}
 
 		// correct Z position
 		if (flip)
 		{
-			P_MoveOrigin(goalpost, goalpost->x, goalpost->y, mobj->z + goaloffset);
-			P_MoveOrigin(sparkle, sparkle->x, sparkle->y, mobj->z + sparkleoffset);
-			P_MoveOrigin(droneman, droneman->x, droneman->y, mobj->z + dronemanoffset);
+			if (!P_MobjWasRemoved(goalpost))
+				P_MoveOrigin(goalpost, goalpost->x, goalpost->y, mobj->z + goaloffset);
+			if (!P_MobjWasRemoved(sparkle))
+				P_MoveOrigin(sparkle, sparkle->x, sparkle->y, mobj->z + sparkleoffset);
+			if (!P_MobjWasRemoved(droneman))
+				P_MoveOrigin(droneman, droneman->x, droneman->y, mobj->z + dronemanoffset);
 		}
 
 		// Remember position preference for later
@@ -12709,9 +12915,12 @@ static boolean P_SetupNiGHTSDrone(mapthing_t *mthing, mobj_t *mobj)
 		}
 
 		// Remember old Z position and flags for correction detection
-		goalpost->movefactor = mobj->z;
-		goalpost->friction = mobj->height;
-		goalpost->threshold = mobj->flags & (MF_SLIDEME|MF_GRENADEBOUNCE);
+		if (!P_MobjWasRemoved(goalpost))
+		{
+			goalpost->movefactor = mobj->z;
+			goalpost->friction = mobj->height;
+			goalpost->threshold = mobj->flags & (MF_SLIDEME|MF_GRENADEBOUNCE);
+		}
 	}
 	return true;
 }
@@ -12729,30 +12938,54 @@ static boolean P_SetupBooster(mapthing_t* mthing, mobj_t* mobj, boolean strong)
 	statenum_t rollerstate = strong ? S_REDBOOSTERROLLER : S_YELLOWBOOSTERROLLER;
 
 	mobj_t *seg = P_SpawnMobjFromMobj(mobj, 26*x1, 26*y1, 0, MT_BOOSTERSEG);
-	seg->angle = angle - ANGLE_90;
-	P_SetMobjState(seg, facestate);
+	if (!P_MobjWasRemoved(seg))
+	{
+		seg->angle = angle - ANGLE_90;
+		P_SetMobjState(seg, facestate);
+	}
 	seg = P_SpawnMobjFromMobj(mobj, -26*x1, -26*y1, 0, MT_BOOSTERSEG);
-	seg->angle = angle + ANGLE_90;
-	P_SetMobjState(seg, facestate);
+	if (!P_MobjWasRemoved(seg))
+	{
+		seg->angle = angle + ANGLE_90;
+		P_SetMobjState(seg, facestate);
+	}
 	seg = P_SpawnMobjFromMobj(mobj, 21*x2, 21*y2, 0, MT_BOOSTERSEG);
-	seg->angle = angle;
-	P_SetMobjState(seg, leftstate);
+	if (!P_MobjWasRemoved(seg))
+	{
+		seg->angle = angle;
+		P_SetMobjState(seg, leftstate);
+	}
 	seg = P_SpawnMobjFromMobj(mobj, -21*x2, -21*y2, 0, MT_BOOSTERSEG);
-	seg->angle = angle;
-	P_SetMobjState(seg, rightstate);
+	if (!P_MobjWasRemoved(seg))
+	{
+		seg->angle = angle;
+		P_SetMobjState(seg, rightstate);
+	}
 
 	seg = P_SpawnMobjFromMobj(mobj, 13*(x1 + x2), 13*(y1 + y2), 0, MT_BOOSTERROLLER);
-	seg->angle = angle;
-	P_SetMobjState(seg, rollerstate);
+	if (!P_MobjWasRemoved(seg))
+	{
+		seg->angle = angle;
+		P_SetMobjState(seg, rollerstate);
+	}
 	seg = P_SpawnMobjFromMobj(mobj, 13*(x1 - x2), 13*(y1 - y2), 0, MT_BOOSTERROLLER);
-	seg->angle = angle;
-	P_SetMobjState(seg, rollerstate);
+	if (!P_MobjWasRemoved(seg))
+	{
+		seg->angle = angle;
+		P_SetMobjState(seg, rollerstate);
+	}
 	seg = P_SpawnMobjFromMobj(mobj, -13*(x1 + x2), -13*(y1 + y2), 0, MT_BOOSTERROLLER);
-	seg->angle = angle;
-	P_SetMobjState(seg, rollerstate);
+	if (!P_MobjWasRemoved(seg))
+	{
+		seg->angle = angle;
+		P_SetMobjState(seg, rollerstate);
+	}
 	seg = P_SpawnMobjFromMobj(mobj, -13*(x1 - x2), -13*(y1 - y2), 0, MT_BOOSTERROLLER);
-	seg->angle = angle;
-	P_SetMobjState(seg, rollerstate);
+	if (!P_MobjWasRemoved(seg))
+	{
+		seg->angle = angle;
+		P_SetMobjState(seg, rollerstate);
+	}
 
 	return true;
 }
@@ -12760,6 +12993,9 @@ static boolean P_SetupBooster(mapthing_t* mthing, mobj_t* mobj, boolean strong)
 static mobj_t *P_MakeSoftwareCorona(mobj_t *mo, INT32 height)
 {
 	mobj_t *corona = P_SpawnMobjFromMobj(mo, 0, 0, height<<FRACBITS, MT_PARTICLE);
+	if (P_MobjWasRemoved(corona))
+		return NULL;
+
 	corona->sprite = SPR_FLAM;
 	corona->frame = (FF_FULLBRIGHT|FF_TRANS90|12);
 	corona->tics = -1;
@@ -12895,6 +13131,9 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 		if (mthing->args[0])
 		{
 			mobj_t *corona = P_MakeSoftwareCorona(mobj, 20);
+			if (P_MobjWasRemoved(corona))
+				break;
+
 			P_SetScale(corona, (corona->destscale = mobj->scale*3));
 			P_SetTarget(&mobj->tracer, corona);
 		}
@@ -12903,11 +13142,17 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 		if (!(mthing->args[0] & TMFH_NOFLAME)) // Spawn the fire
 		{
 			mobj_t *flame = P_SpawnMobjFromMobj(mobj, 0, 0, mobj->height, MT_FLAME);
+			if (P_MobjWasRemoved(flame))
+				break;
+
 			P_SetTarget(&flame->target, mobj);
 			flame->flags2 |= MF2_BOSSNOTRAP;
 			if (mthing->args[0] & TMFH_CORONA)
 			{
 				mobj_t *corona = P_MakeSoftwareCorona(flame, 20);
+				if (P_MobjWasRemoved(corona))
+					break;
+
 				P_SetScale(corona, (corona->destscale = flame->scale*3));
 				P_SetTarget(&flame->tracer, corona);
 			}
@@ -12924,6 +13169,8 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 		if (!(mthing->args[0])) // take the torch out of the crafting recipe
 		{
 			mobj_t *overlay = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_OVERLAY);
+			if (P_MobjWasRemoved(overlay))
+				break;
 			P_SetTarget(&overlay->target, mobj);
 			P_SetMobjState(overlay, mobj->info->raisestate);
 		}
@@ -13003,9 +13250,15 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 	case MT_THZTREE:
 	{ // Spawn the branches
 		angle_t mobjangle = FixedAngle((mthing->angle % 113) << FRACBITS);
-		P_SpawnMobjFromMobj(mobj, FRACUNIT, 0, 0, MT_THZTREEBRANCH)->angle = mobjangle + ANGLE_22h;
-		P_SpawnMobjFromMobj(mobj, 0, FRACUNIT, 0, MT_THZTREEBRANCH)->angle = mobjangle + ANGLE_157h;
-		P_SpawnMobjFromMobj(mobj, -FRACUNIT, 0, 0, MT_THZTREEBRANCH)->angle = mobjangle + ANGLE_270;
+		mobj_t *branch = P_SpawnMobjFromMobj(mobj, FRACUNIT, 0, 0, MT_THZTREEBRANCH);
+		if (!P_MobjWasRemoved(branch))
+			branch->angle = mobjangle + ANGLE_22h;
+		branch = P_SpawnMobjFromMobj(mobj, 0, FRACUNIT, 0, MT_THZTREEBRANCH);
+		if (!P_MobjWasRemoved(branch))
+			branch->angle = mobjangle + ANGLE_157h;
+		branch = P_SpawnMobjFromMobj(mobj, -FRACUNIT, 0, 0, MT_THZTREEBRANCH);
+		if (!P_MobjWasRemoved(branch))
+			branch->angle = mobjangle + ANGLE_270;
 	}
 	break;
 	case MT_TUTORIALPLANT:
@@ -13015,26 +13268,34 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 		for (i = 0; i < 6; i++)
 		{
 			segment = P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_TUTORIALLEAF);
+			if (P_MobjWasRemoved(segment))
+				continue;
 			segment->angle = mobj->angle + FixedAngle(i*60*FRACUNIT);
 			P_SetMobjState(segment, S_TUTORIALLEAF1 + mthing->args[0]);
 		}
 		for (i = 0; i < 3; i++)
 		{
 			segment = P_SpawnMobjFromMobj(mobj, 0, 0, 112*FRACUNIT, MT_TUTORIALFLOWER);
+			if (P_MobjWasRemoved(segment))
+				continue;
 			segment->angle = mobj->angle + FixedAngle(i*120*FRACUNIT);
 			P_SetMobjState(segment, S_TUTORIALFLOWER1 + mthing->args[0]);
 		}
-		P_SetMobjState(P_SpawnMobjFromMobj(mobj, 0, 0, 112*FRACUNIT, MT_TUTORIALFLOWERF), S_TUTORIALFLOWERF1 + mthing->args[0]);
+		segment = P_SpawnMobjFromMobj(mobj, 0, 0, 112*FRACUNIT, MT_TUTORIALFLOWERF);
+		if (!P_MobjWasRemoved(segment))
+			P_SetMobjState(segment, S_TUTORIALFLOWERF1 + mthing->args[0]);
 	}
 	break;
 	case MT_CEZPOLE1:
 	case MT_CEZPOLE2:
 	{ // Spawn the banner
 		angle_t mobjangle = FixedAngle(mthing->angle << FRACBITS);
-		P_SpawnMobjFromMobj(mobj,
+		mobj_t *banner = P_SpawnMobjFromMobj(mobj,
 			P_ReturnThrustX(mobj, mobjangle, 4 << FRACBITS),
 			P_ReturnThrustY(mobj, mobjangle, 4 << FRACBITS),
-			0, ((mobj->type == MT_CEZPOLE1) ? MT_CEZBANNER1 : MT_CEZBANNER2))->angle = mobjangle + ANGLE_90;
+			0, ((mobj->type == MT_CEZPOLE1) ? MT_CEZBANNER1 : MT_CEZBANNER2));
+		if (!P_MobjWasRemoved(banner))
+			banner->angle = mobjangle + ANGLE_90;
 	}
 	break;
 	case MT_HHZTREE_TOP:
@@ -13043,8 +13304,11 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 		mobj_t* leaf;
 #define doleaf(x, y) \
 			leaf = P_SpawnMobjFromMobj(mobj, x, y, 0, MT_HHZTREE_PART);\
-			leaf->angle = mobjangle;\
-			P_SetMobjState(leaf, leaf->info->seestate);\
+			if (!P_MobjWasRemoved(leaf))\
+			{\
+				leaf->angle = mobjangle;\
+				P_SetMobjState(leaf, leaf->info->seestate);\
+			}\
 			mobjangle += ANGLE_90
 		doleaf(FRACUNIT, 0);
 		doleaf(0, FRACUNIT);
@@ -13083,8 +13347,9 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 			angle_t fa = (angle >> ANGLETOFINESHIFT) & FINEMASK;
 			fixed_t xoffs = FINECOSINE(fa);
 			fixed_t yoffs = FINESINE(fa);
-			mobj_t* leaf = P_SpawnMobjFromMobj(mobj, xoffs, yoffs, 0, MT_BIGFERNLEAF);
-			leaf->angle = angle;
+			mobj_t *leaf = P_SpawnMobjFromMobj(mobj, xoffs, yoffs, 0, MT_BIGFERNLEAF);
+			if (!P_MobjWasRemoved(leaf))
+				leaf->angle = angle;
 			angle += ANGLE_45;
 		}
 		break;
@@ -13121,6 +13386,8 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 		{
 			mobj_t* elecmobj;
 			elecmobj = P_SpawnMobj(mobj->x, mobj->y, mobj->z, MT_CYBRAKDEMON_ELECTRIC_BARRIER);
+			if (P_MobjWasRemoved(elecmobj))
+				break;
 			P_SetTarget(&elecmobj->target, mobj);
 			elecmobj->angle = FixedAngle(mthing->angle << FRACBITS);
 			elecmobj->destscale = mobj->scale*2;
@@ -13175,6 +13442,12 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 				mobj->x - P_ReturnThrustX(mobj, mobjangle, baseradius),
 				mobj->y - P_ReturnThrustY(mobj, mobjangle, baseradius),
 				mobj->z, MT_WALLSPIKEBASE);
+			if (P_MobjWasRemoved(base))
+			{
+				// if we can't spawn the base, don't spawn the spike at all.
+				P_RemoveMobj(mobj);
+				return false;
+			}
 			base->angle = mobjangle + ANGLE_90;
 			base->destscale = mobj->destscale;
 			P_SetScale(base, mobj->scale);
@@ -13327,7 +13600,7 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
 	return true;
 }
 
-// Pre-UDMF backwards compatibility stuff. Remove for 2.3
+// TODO: 2.3: Delete (Pre-UDMF backwards compatibility stuff)
 static void P_SetAmbush(mapthing_t *mthing, mobj_t *mobj)
 {
 	if (mobj->type == MT_NIGHTSBUMPER
@@ -13350,6 +13623,8 @@ static mobj_t *P_SpawnMobjFromMapThing(mapthing_t *mthing, fixed_t x, fixed_t y,
 	boolean doangle = true;
 
 	mobj = P_SpawnMobj(x, y, z, i);
+	if (mobj == NULL)
+		return NULL;
 	mobj->spawnpoint = mthing;
 
 	P_SetScale(mobj, FixedMul(mobj->scale, mthing->scale));
@@ -13447,6 +13722,9 @@ void P_SpawnHoop(mapthing_t *mthing)
 	fixed_t z = P_GetMobjSpawnHeight(MT_HOOP, x, y, mthing->z << FRACBITS, 0, false, mthing->scale, mthing->options & MTF_ABSOLUTEZ);
 
 	hoopcenter = P_SpawnMobj(x, y, z, MT_HOOPCENTER);
+	if (P_MobjWasRemoved(hoopcenter))
+		return;
+
 	hoopcenter->spawnpoint = mthing;
 	hoopcenter->z -= hoopcenter->height/2;
 
@@ -13477,6 +13755,8 @@ void P_SpawnHoop(mapthing_t *mthing)
 		FV4_Copy(&v, FM_MultMatrixVec4(&yawmatrix, &v, &res));
 
 		mobj = P_SpawnMobj(x + v.x, y + v.y, z + v.z, MT_HOOP);
+		if (P_MobjWasRemoved(mobj))
+			continue;
 		mobj->z -= mobj->height/2;
 
 		if (maptol & TOL_XMAS)
@@ -13521,6 +13801,8 @@ void P_SpawnHoop(mapthing_t *mthing)
 			FV4_Copy(&v, FM_MultMatrixVec4(&yawmatrix, &v, &res));
 
 			mobj = P_SpawnMobj(x + v.x, y + v.y, z + v.z, MT_HOOPCOLLIDE);
+			if (P_MobjWasRemoved(mobj))
+				continue;
 			mobj->z -= mobj->height/2;
 
 			// Link all the collision sprites together.
@@ -13793,6 +14075,8 @@ mobj_t *P_SpawnXYZMissile(mobj_t *source, mobj_t *dest, mobjtype_t type,
 		z -= FixedMul(mobjinfo[type].height, source->scale);
 
 	th = P_SpawnMobj(x, y, z, type);
+	if (P_MobjWasRemoved(th))
+		return NULL;
 
 	if (source->eflags & MFE_VERTICALFLIP)
 		th->flags2 |= MF2_OBJECTFLIP;
@@ -13855,6 +14139,8 @@ mobj_t *P_SpawnAlteredDirectionMissile(mobj_t *source, mobjtype_t type, fixed_t
 		z -= FixedMul(mobjinfo[type].height, source->scale);
 
 	th = P_SpawnMobj(x, y, z, type);
+	if (P_MobjWasRemoved(th))
+		return NULL;
 
 	if (source->eflags & MFE_VERTICALFLIP)
 		th->flags2 |= MF2_OBJECTFLIP;
@@ -13920,6 +14206,8 @@ mobj_t *P_SpawnPointMissile(mobj_t *source, fixed_t xa, fixed_t ya, fixed_t za,
 		z -= FixedMul(mobjinfo[type].height, source->scale);
 
 	th = P_SpawnMobj(x, y, z, type);
+	if (P_MobjWasRemoved(th))
+		return NULL;
 
 	if (source->eflags & MFE_VERTICALFLIP)
 		th->flags2 |= MF2_OBJECTFLIP;
@@ -13990,6 +14278,8 @@ mobj_t *P_SpawnMissile(mobj_t *source, mobj_t *dest, mobjtype_t type)
 		z -= FixedMul(mobjinfo[type].height, source->scale);
 
 	th = P_SpawnMobj(source->x, source->y, z, type);
+	if (P_MobjWasRemoved(th))
+		return NULL;
 
 	if (source->eflags & MFE_VERTICALFLIP)
 		th->flags2 |= MF2_OBJECTFLIP;
@@ -14091,6 +14381,8 @@ mobj_t *P_SPMAngle(mobj_t *source, mobjtype_t type, angle_t angle, UINT8 allowai
 		z = source->z + source->height/3;
 
 	th = P_SpawnMobj(x, y, z, type);
+	if (P_MobjWasRemoved(th))
+		return NULL;
 
 	if (source->eflags & MFE_VERTICALFLIP)
 		th->flags2 |= MF2_OBJECTFLIP;
diff --git a/src/p_polyobj.c b/src/p_polyobj.c
index b207bb740f6b6c6cd87f88da38e1b1f74958f0d2..331bc5c7f0397876d10c92ad6c316d2ffeb56d41 100644
--- a/src/p_polyobj.c
+++ b/src/p_polyobj.c
@@ -1243,6 +1243,8 @@ boolean Polyobj_rotate(polyobj_t *po, angle_t delta, boolean turnplayers, boolea
 // Returns NULL if no such polyobject exists.
 polyobj_t *Polyobj_GetForNum(INT32 id)
 {
+	if (numPolyObjects == 0)
+		return NULL;
 	INT32 curidx  = PolyObjects[id % numPolyObjects].first;
 
 	while (curidx != numPolyObjects && PolyObjects[curidx].id != id)
diff --git a/src/p_saveg.c b/src/p_saveg.c
index faecd13770b3d81b992017af51b4b663487aa50d..8c74ebef4815cd535c16436b682efcdd769b5f42 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -175,7 +175,7 @@ static void P_NetArchivePlayers(void)
 		WRITEUINT16(save_p, players[i].flashpal);
 		WRITEUINT16(save_p, players[i].flashcount);
 
-		WRITEUINT8(save_p, players[i].skincolor);
+		WRITEUINT16(save_p, players[i].skincolor);
 		WRITEINT32(save_p, players[i].skin);
 		WRITEUINT32(save_p, players[i].availabilities);
 		WRITEUINT32(save_p, players[i].score);
@@ -404,7 +404,7 @@ static void P_NetUnArchivePlayers(void)
 		players[i].flashpal = READUINT16(save_p);
 		players[i].flashcount = READUINT16(save_p);
 
-		players[i].skincolor = READUINT8(save_p);
+		players[i].skincolor = READUINT16(save_p);
 		players[i].skin = READINT32(save_p);
 		players[i].availabilities = READUINT32(save_p);
 		players[i].score = READUINT32(save_p);
@@ -867,24 +867,47 @@ static void P_NetUnArchiveWaypoints(void)
 #define SD_TRIGGERTAG 0x02
 #define SD_TRIGGERER 0x04
 #define SD_GRAVITY   0x08
-
-#define LD_FLAG     0x01
-#define LD_SPECIAL  0x02
-#define LD_CLLCOUNT 0x04
-#define LD_S1TEXOFF 0x08
-#define LD_S1TOPTEX 0x10
-#define LD_S1BOTTEX 0x20
-#define LD_S1MIDTEX 0x40
-#define LD_DIFF2    0x80
-
-// diff2 flags
-#define LD_S2TEXOFF      0x01
-#define LD_S2TOPTEX      0x02
-#define LD_S2BOTTEX      0x04
-#define LD_S2MIDTEX      0x08
-#define LD_ARGS          0x10
-#define LD_STRINGARGS    0x20
-#define LD_EXECUTORDELAY 0x40
+#define SD_FXSCALE   0x10
+#define SD_FYSCALE   0x20
+#define SD_CXSCALE   0x40
+#define SD_CYSCALE   0x80
+
+#define LD_FLAG          0x01
+#define LD_SPECIAL       0x02
+#define LD_CLLCOUNT      0x04
+#define LD_ARGS          0x08
+#define LD_STRINGARGS    0x10
+#define LD_EXECUTORDELAY 0x20
+#define LD_SIDE1         0x40
+#define LD_SIDE2         0x80
+
+// sidedef flags
+enum
+{
+	LD_SDTEXOFFX     = 1,
+	LD_SDTEXOFFY     = 1<<1,
+	LD_SDTOPTEX      = 1<<2,
+	LD_SDBOTTEX      = 1<<3,
+	LD_SDMIDTEX      = 1<<4,
+	LD_SDTOPOFFX     = 1<<5,
+	LD_SDTOPOFFY     = 1<<6,
+	LD_SDMIDOFFX     = 1<<7,
+	LD_SDMIDOFFY     = 1<<8,
+	LD_SDBOTOFFX     = 1<<9,
+	LD_SDBOTOFFY     = 1<<10,
+	LD_SDTOPSCALEX   = 1<<11,
+	LD_SDTOPSCALEY   = 1<<12,
+	LD_SDMIDSCALEX   = 1<<13,
+	LD_SDMIDSCALEY   = 1<<14,
+	LD_SDBOTSCALEX   = 1<<15,
+	LD_SDBOTSCALEY   = 1<<16,
+	LD_SDLIGHT       = 1<<17,
+	LD_SDTOPLIGHT    = 1<<18,
+	LD_SDMIDLIGHT    = 1<<19,
+	LD_SDBOTLIGHT    = 1<<20,
+	LD_SDREPEATCNT   = 1<<21,
+	LD_SDFLAGS       = 1<<22
+};
 
 static boolean P_AreArgsEqual(const line_t *li, const line_t *spawnli)
 {
@@ -1035,6 +1058,14 @@ static void ArchiveSectors(void)
 			diff2 |= SD_CXOFFS;
 		if (ss->ceilingyoffset != spawnss->ceilingyoffset)
 			diff2 |= SD_CYOFFS;
+		if (ss->floorxscale != spawnss->floorxscale)
+			diff2 |= SD_FXSCALE;
+		if (ss->flooryscale != spawnss->flooryscale)
+			diff2 |= SD_FYSCALE;
+		if (ss->ceilingxscale != spawnss->ceilingxscale)
+			diff2 |= SD_CXSCALE;
+		if (ss->ceilingyscale != spawnss->ceilingyscale)
+			diff2 |= SD_CYSCALE;
 		if (ss->floorangle != spawnss->floorangle)
 			diff2 |= SD_FLOORANG;
 		if (ss->ceilingangle != spawnss->ceilingangle)
@@ -1079,7 +1110,7 @@ static void ArchiveSectors(void)
 
 		if (diff)
 		{
-			WRITEUINT16(save_p, i);
+			WRITEUINT32(save_p, i);
 			WRITEUINT8(save_p, diff);
 			if (diff & SD_DIFF2)
 				WRITEUINT8(save_p, diff2);
@@ -1145,23 +1176,32 @@ static void ArchiveSectors(void)
 				WRITEUINT8(save_p, ss->triggerer);
 			if (diff4 & SD_GRAVITY)
 				WRITEFIXED(save_p, ss->gravity);
+			if (diff4 & SD_FXSCALE)
+				WRITEFIXED(save_p, ss->floorxscale);
+			if (diff4 & SD_FYSCALE)
+				WRITEFIXED(save_p, ss->flooryscale);
+			if (diff4 & SD_CXSCALE)
+				WRITEFIXED(save_p, ss->ceilingxscale);
+			if (diff4 & SD_CYSCALE)
+				WRITEFIXED(save_p, ss->ceilingyscale);
 			if (diff & SD_FFLOORS)
 				ArchiveFFloors(ss);
 		}
 	}
 
-	WRITEUINT16(save_p, 0xffff);
+	WRITEUINT32(save_p, 0xffffffff);
 }
 
 static void UnArchiveSectors(void)
 {
-	UINT16 i, j;
+	UINT32 i;
+	UINT16 j;
 	UINT8 diff, diff2, diff3, diff4;
 	for (;;)
 	{
-		i = READUINT16(save_p);
+		i = READUINT32(save_p);
 
-		if (i == 0xffff)
+		if (i == 0xffffffff)
 			break;
 
 		if (i > numsectors)
@@ -1265,24 +1305,117 @@ static void UnArchiveSectors(void)
 			sectors[i].triggerer = READUINT8(save_p);
 		if (diff4 & SD_GRAVITY)
 			sectors[i].gravity = READFIXED(save_p);
+		if (diff4 & SD_FXSCALE)
+			sectors[i].floorxscale = READFIXED(save_p);
+		if (diff4 & SD_FYSCALE)
+			sectors[i].flooryscale = READFIXED(save_p);
+		if (diff4 & SD_CXSCALE)
+			sectors[i].ceilingxscale = READFIXED(save_p);
+		if (diff4 & SD_CYSCALE)
+			sectors[i].ceilingyscale = READFIXED(save_p);
 
 		if (diff & SD_FFLOORS)
 			UnArchiveFFloors(&sectors[i]);
 	}
 }
 
+static UINT32 GetSideDiff(const side_t *si, const side_t *spawnsi)
+{
+	UINT32 diff = 0;
+	if (si->textureoffset != spawnsi->textureoffset)
+		diff |= LD_SDTEXOFFX;
+	if (si->rowoffset != spawnsi->rowoffset)
+		diff |= LD_SDTEXOFFY;
+	//SoM: 4/1/2000: Some textures are colormaps. Don't worry about invalid textures.
+	if (si->toptexture != spawnsi->toptexture)
+		diff |= LD_SDTOPTEX;
+	if (si->bottomtexture != spawnsi->bottomtexture)
+		diff |= LD_SDBOTTEX;
+	if (si->midtexture != spawnsi->midtexture)
+		diff |= LD_SDMIDTEX;
+	if (si->offsetx_top != spawnsi->offsetx_top)
+		diff |= LD_SDTOPOFFX;
+	if (si->offsetx_mid != spawnsi->offsetx_mid)
+		diff |= LD_SDMIDOFFX;
+	if (si->offsetx_bottom != spawnsi->offsetx_bottom)
+		diff |= LD_SDBOTOFFX;
+	if (si->offsety_top != spawnsi->offsety_top)
+		diff |= LD_SDTOPOFFY;
+	if (si->offsety_mid != spawnsi->offsety_mid)
+		diff |= LD_SDMIDOFFY;
+	if (si->offsety_bottom != spawnsi->offsety_bottom)
+		diff |= LD_SDBOTOFFY;
+	if (si->scalex_top != spawnsi->scalex_top)
+		diff |= LD_SDTOPSCALEX;
+	if (si->scalex_mid != spawnsi->scalex_mid)
+		diff |= LD_SDMIDSCALEX;
+	if (si->scalex_bottom != spawnsi->scalex_bottom)
+		diff |= LD_SDBOTSCALEX;
+	if (si->scaley_top != spawnsi->scaley_top)
+		diff |= LD_SDTOPSCALEY;
+	if (si->scaley_mid != spawnsi->scaley_mid)
+		diff |= LD_SDMIDSCALEY;
+	if (si->scaley_bottom != spawnsi->scaley_bottom)
+		diff |= LD_SDBOTSCALEY;
+	if (si->repeatcnt != spawnsi->repeatcnt)
+		diff |= LD_SDREPEATCNT;
+	return diff;
+}
+
+static void ArchiveSide(const side_t *si, UINT32 diff)
+{
+	WRITEUINT32(save_p, diff);
+
+	if (diff & LD_SDTEXOFFX)
+		WRITEFIXED(save_p, si->textureoffset);
+	if (diff & LD_SDTEXOFFY)
+		WRITEFIXED(save_p, si->rowoffset);
+	if (diff & LD_SDTOPTEX)
+		WRITEINT32(save_p, si->toptexture);
+	if (diff & LD_SDBOTTEX)
+		WRITEINT32(save_p, si->bottomtexture);
+	if (diff & LD_SDMIDTEX)
+		WRITEINT32(save_p, si->midtexture);
+	if (diff & LD_SDTOPOFFX)
+		WRITEFIXED(save_p, si->offsetx_top);
+	if (diff & LD_SDMIDOFFX)
+		WRITEFIXED(save_p, si->offsetx_mid);
+	if (diff & LD_SDBOTOFFX)
+		WRITEFIXED(save_p, si->offsetx_bottom);
+	if (diff & LD_SDTOPOFFY)
+		WRITEFIXED(save_p, si->offsety_top);
+	if (diff & LD_SDMIDOFFY)
+		WRITEFIXED(save_p, si->offsety_mid);
+	if (diff & LD_SDBOTOFFY)
+		WRITEFIXED(save_p, si->offsety_bottom);
+	if (diff & LD_SDTOPSCALEX)
+		WRITEFIXED(save_p, si->scalex_top);
+	if (diff & LD_SDMIDSCALEX)
+		WRITEFIXED(save_p, si->scalex_mid);
+	if (diff & LD_SDBOTSCALEX)
+		WRITEFIXED(save_p, si->scalex_bottom);
+	if (diff & LD_SDTOPSCALEY)
+		WRITEFIXED(save_p, si->scaley_top);
+	if (diff & LD_SDMIDSCALEY)
+		WRITEFIXED(save_p, si->scaley_mid);
+	if (diff & LD_SDBOTSCALEY)
+		WRITEFIXED(save_p, si->scaley_bottom);
+	if (diff & LD_SDREPEATCNT)
+		WRITEINT16(save_p, si->repeatcnt);
+}
+
 static void ArchiveLines(void)
 {
 	size_t i;
 	const line_t *li = lines;
 	const line_t *spawnli = spawnlines;
-	const side_t *si;
-	const side_t *spawnsi;
-	UINT8 diff, diff2; // no diff3
+	UINT8 diff;
+	UINT32 diff2;
+	UINT32 diff3;
 
 	for (i = 0; i < numlines; i++, spawnli++, li++)
 	{
-		diff = diff2 = 0;
+		diff = diff2 = diff3 = 0;
 
 		if (li->special != spawnli->special)
 			diff |= LD_SPECIAL;
@@ -1291,84 +1424,44 @@ static void ArchiveLines(void)
 			diff |= LD_CLLCOUNT;
 
 		if (!P_AreArgsEqual(li, spawnli))
-			diff2 |= LD_ARGS;
+			diff |= LD_ARGS;
 
 		if (!P_AreStringArgsEqual(li, spawnli))
-			diff2 |= LD_STRINGARGS;
+			diff |= LD_STRINGARGS;
 
 		if (li->executordelay != spawnli->executordelay)
-			diff2 |= LD_EXECUTORDELAY;
+			diff |= LD_EXECUTORDELAY;
 
-		if (li->sidenum[0] != 0xffff)
+		if (li->sidenum[0] != NO_SIDEDEF)
 		{
-			si = &sides[li->sidenum[0]];
-			spawnsi = &spawnsides[li->sidenum[0]];
-			if (si->textureoffset != spawnsi->textureoffset)
-				diff |= LD_S1TEXOFF;
-			//SoM: 4/1/2000: Some textures are colormaps. Don't worry about invalid textures.
-			if (si->toptexture != spawnsi->toptexture)
-				diff |= LD_S1TOPTEX;
-			if (si->bottomtexture != spawnsi->bottomtexture)
-				diff |= LD_S1BOTTEX;
-			if (si->midtexture != spawnsi->midtexture)
-				diff |= LD_S1MIDTEX;
+			diff2 = GetSideDiff(&sides[li->sidenum[0]], &spawnsides[li->sidenum[0]]);
+			if (diff2)
+				diff |= LD_SIDE1;
 		}
-		if (li->sidenum[1] != 0xffff)
+		if (li->sidenum[1] != NO_SIDEDEF)
 		{
-			si = &sides[li->sidenum[1]];
-			spawnsi = &spawnsides[li->sidenum[1]];
-			if (si->textureoffset != spawnsi->textureoffset)
-				diff2 |= LD_S2TEXOFF;
-			if (si->toptexture != spawnsi->toptexture)
-				diff2 |= LD_S2TOPTEX;
-			if (si->bottomtexture != spawnsi->bottomtexture)
-				diff2 |= LD_S2BOTTEX;
-			if (si->midtexture != spawnsi->midtexture)
-				diff2 |= LD_S2MIDTEX;
+			diff3 = GetSideDiff(&sides[li->sidenum[1]], &spawnsides[li->sidenum[1]]);
+			if (diff3)
+				diff |= LD_SIDE2;
 		}
 
-		if (diff2)
-			diff |= LD_DIFF2;
-
 		if (diff)
 		{
-			WRITEINT16(save_p, i);
+			WRITEUINT32(save_p, i);
 			WRITEUINT8(save_p, diff);
-			if (diff & LD_DIFF2)
-				WRITEUINT8(save_p, diff2);
 			if (diff & LD_FLAG)
 				WRITEINT16(save_p, li->flags);
 			if (diff & LD_SPECIAL)
 				WRITEINT16(save_p, li->special);
 			if (diff & LD_CLLCOUNT)
 				WRITEINT16(save_p, li->callcount);
-
-			si = &sides[li->sidenum[0]];
-			if (diff & LD_S1TEXOFF)
-				WRITEFIXED(save_p, si->textureoffset);
-			if (diff & LD_S1TOPTEX)
-				WRITEINT32(save_p, si->toptexture);
-			if (diff & LD_S1BOTTEX)
-				WRITEINT32(save_p, si->bottomtexture);
-			if (diff & LD_S1MIDTEX)
-				WRITEINT32(save_p, si->midtexture);
-
-			si = &sides[li->sidenum[1]];
-			if (diff2 & LD_S2TEXOFF)
-				WRITEFIXED(save_p, si->textureoffset);
-			if (diff2 & LD_S2TOPTEX)
-				WRITEINT32(save_p, si->toptexture);
-			if (diff2 & LD_S2BOTTEX)
-				WRITEINT32(save_p, si->bottomtexture);
-			if (diff2 & LD_S2MIDTEX)
-				WRITEINT32(save_p, si->midtexture);
-			if (diff2 & LD_ARGS)
+			if (diff & LD_ARGS)
 			{
 				UINT8 j;
 				for (j = 0; j < NUMLINEARGS; j++)
 					WRITEINT32(save_p, li->args[j]);
 			}
-			if (diff2 & LD_STRINGARGS)
+			if (diff & LD_STRINGARGS)
 			{
 				UINT8 j;
 				for (j = 0; j < NUMLINESTRINGARGS; j++)
@@ -1387,25 +1480,70 @@ static void ArchiveLines(void)
 						WRITECHAR(save_p, li->stringargs[j][k]);
 				}
 			}
-			if (diff2 & LD_EXECUTORDELAY)
+			if (diff & LD_EXECUTORDELAY)
 				WRITEINT32(save_p, li->executordelay);
+			if (diff & LD_SIDE1)
+				ArchiveSide(&sides[li->sidenum[0]], diff2);
+			if (diff & LD_SIDE2)
+				ArchiveSide(&sides[li->sidenum[1]], diff3);
 		}
 	}
-	WRITEUINT16(save_p, 0xffff);
+	WRITEUINT32(save_p, 0xffffffff);
+}
+
+static void UnArchiveSide(side_t *si)
+{
+	UINT32 diff = READUINT32(save_p);
+
+	if (diff & LD_SDTEXOFFX)
+		si->textureoffset = READFIXED(save_p);
+	if (diff & LD_SDTEXOFFY)
+		si->rowoffset = READFIXED(save_p);
+	if (diff & LD_SDTOPTEX)
+		si->toptexture = READINT32(save_p);
+	if (diff & LD_SDBOTTEX)
+		si->bottomtexture = READINT32(save_p);
+	if (diff & LD_SDMIDTEX)
+		si->midtexture = READINT32(save_p);
+	if (diff & LD_SDTOPOFFX)
+		si->offsetx_top = READFIXED(save_p);
+	if (diff & LD_SDMIDOFFX)
+		si->offsetx_mid = READFIXED(save_p);
+	if (diff & LD_SDBOTOFFX)
+		si->offsetx_bottom = READFIXED(save_p);
+	if (diff & LD_SDTOPOFFY)
+		si->offsety_top = READFIXED(save_p);
+	if (diff & LD_SDMIDOFFY)
+		si->offsety_mid = READFIXED(save_p);
+	if (diff & LD_SDBOTOFFY)
+		si->offsety_bottom = READFIXED(save_p);
+	if (diff & LD_SDTOPSCALEX)
+		si->scalex_top = READFIXED(save_p);
+	if (diff & LD_SDMIDSCALEX)
+		si->scalex_mid = READFIXED(save_p);
+	if (diff & LD_SDBOTSCALEX)
+		si->scalex_bottom = READFIXED(save_p);
+	if (diff & LD_SDTOPSCALEY)
+		si->scaley_top = READFIXED(save_p);
+	if (diff & LD_SDMIDSCALEY)
+		si->scaley_mid = READFIXED(save_p);
+	if (diff & LD_SDBOTSCALEY)
+		si->scaley_bottom = READFIXED(save_p);
+	if (diff & LD_SDREPEATCNT)
+		si->repeatcnt = READINT16(save_p);
 }
 
 static void UnArchiveLines(void)
 {
-	UINT16 i;
+	UINT32 i;
 	line_t *li;
-	side_t *si;
-	UINT8 diff, diff2; // no diff3
+	UINT8 diff;
 
 	for (;;)
 	{
-		i = READUINT16(save_p);
+		i = READUINT32(save_p);
 
-		if (i == 0xffff)
+		if (i == 0xffffffff)
 			break;
 		if (i > numlines)
 			I_Error("Invalid line number %u from server", i);
@@ -1413,44 +1551,19 @@ static void UnArchiveLines(void)
 		diff = READUINT8(save_p);
 		li = &lines[i];
 
-		if (diff & LD_DIFF2)
-			diff2 = READUINT8(save_p);
-		else
-			diff2 = 0;
-
 		if (diff & LD_FLAG)
 			li->flags = READINT16(save_p);
 		if (diff & LD_SPECIAL)
 			li->special = READINT16(save_p);
 		if (diff & LD_CLLCOUNT)
 			li->callcount = READINT16(save_p);
-
-		si = &sides[li->sidenum[0]];
-		if (diff & LD_S1TEXOFF)
-			si->textureoffset = READFIXED(save_p);
-		if (diff & LD_S1TOPTEX)
-			si->toptexture = READINT32(save_p);
-		if (diff & LD_S1BOTTEX)
-			si->bottomtexture = READINT32(save_p);
-		if (diff & LD_S1MIDTEX)
-			si->midtexture = READINT32(save_p);
-
-		si = &sides[li->sidenum[1]];
-		if (diff2 & LD_S2TEXOFF)
-			si->textureoffset = READFIXED(save_p);
-		if (diff2 & LD_S2TOPTEX)
-			si->toptexture = READINT32(save_p);
-		if (diff2 & LD_S2BOTTEX)
-			si->bottomtexture = READINT32(save_p);
-		if (diff2 & LD_S2MIDTEX)
-			si->midtexture = READINT32(save_p);
-		if (diff2 & LD_ARGS)
+		if (diff & LD_ARGS)
 		{
 			UINT8 j;
 			for (j = 0; j < NUMLINEARGS; j++)
 				li->args[j] = READINT32(save_p);
 		}
-		if (diff2 & LD_STRINGARGS)
+		if (diff & LD_STRINGARGS)
 		{
 			UINT8 j;
 			for (j = 0; j < NUMLINESTRINGARGS; j++)
@@ -1471,9 +1584,12 @@ static void UnArchiveLines(void)
 				li->stringargs[j][len] = '\0';
 			}
 		}
-		if (diff2 & LD_EXECUTORDELAY)
+		if (diff & LD_EXECUTORDELAY)
 			li->executordelay = READINT32(save_p);
-
+		if (diff & LD_SIDE1)
+			UnArchiveSide(&sides[li->sidenum[0]]);
+		if (diff & LD_SIDE2)
+			UnArchiveSide(&sides[li->sidenum[1]]);
 	}
 }
 
diff --git a/src/p_saveg.h b/src/p_saveg.h
index 73fcfd5836e4c5283d3fb56c7f397e9b774c5405..545008e7efc6af656fe94864a4ebcf8ea28e5f27 100644
--- a/src/p_saveg.h
+++ b/src/p_saveg.h
@@ -18,7 +18,7 @@
 #pragma interface
 #endif
 
-#define NEWSKINSAVES (INT16_MAX) // Purely for backwards compatibility, remove this for 2.3
+#define NEWSKINSAVES (INT16_MAX) // TODO: 2.3: Delete (Purely for backwards compatibility)
 
 // Persistent storage/archiving.
 // These are the load / save game routines.
diff --git a/src/p_setup.c b/src/p_setup.c
index e289b834699dd957aabc3ec554c25c720b6d5b2a..6f5ec17dcd59814ddae0032911704c3c9e4f6b16 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -88,6 +88,8 @@
 
 #include "taglist.h"
 
+#include "netcode/net_command.h"
+
 //
 // Map MD5, calculated on level load.
 // Sent to clients in PT_SERVERINFO.
@@ -1053,6 +1055,9 @@ static void P_LoadSectors(UINT8 *data)
 		ss->floorxoffset = ss->flooryoffset = 0;
 		ss->ceilingxoffset = ss->ceilingyoffset = 0;
 
+		ss->floorxscale = ss->flooryscale = FRACUNIT;
+		ss->ceilingxscale = ss->ceilingyscale = FRACUNIT;
+
 		ss->floorangle = ss->ceilingangle = 0;
 
 		ss->floorlightlevel = ss->ceilinglightlevel = 0;
@@ -1109,40 +1114,40 @@ static void P_InitializeLinedef(line_t *ld)
 	// cph 2006/09/30 - fix sidedef errors right away.
 	// cph 2002/07/20 - these errors are fatal if not fixed, so apply them
 	for (j = 0; j < 2; j++)
-		if (ld->sidenum[j] != 0xffff && ld->sidenum[j] >= (UINT16)numsides)
+		if (ld->sidenum[j] != NO_SIDEDEF && ld->sidenum[j] >= (UINT32)numsides)
 		{
-			ld->sidenum[j] = 0xffff;
+			ld->sidenum[j] = NO_SIDEDEF;
 			CONS_Debug(DBG_SETUP, "P_InitializeLinedef: Linedef %s has out-of-range sidedef number\n", sizeu1((size_t)(ld - lines)));
 		}
 
 	// killough 11/98: fix common wad errors (missing sidedefs):
-	if (ld->sidenum[0] == 0xffff)
+	if (ld->sidenum[0] == NO_SIDEDEF)
 	{
 		ld->sidenum[0] = 0;  // Substitute dummy sidedef for missing right side
 		// cph - print a warning about the bug
 		CONS_Debug(DBG_SETUP, "P_InitializeLinedef: Linedef %s missing first sidedef\n", sizeu1((size_t)(ld - lines)));
 	}
 
-	if ((ld->sidenum[1] == 0xffff) && (ld->flags & ML_TWOSIDED))
+	if ((ld->sidenum[1] == NO_SIDEDEF) && (ld->flags & ML_TWOSIDED))
 	{
 		ld->flags &= ~ML_TWOSIDED;  // Clear 2s flag for missing left side
 		// cph - print a warning about the bug
 		CONS_Debug(DBG_SETUP, "P_InitializeLinedef: Linedef %s has two-sided flag set, but no second sidedef\n", sizeu1((size_t)(ld - lines)));
 	}
 
-	if (ld->sidenum[0] != 0xffff)
+	if (ld->sidenum[0] != NO_SIDEDEF)
 	{
 		sides[ld->sidenum[0]].special = ld->special;
 		sides[ld->sidenum[0]].line = ld;
 	}
-	if (ld->sidenum[1] != 0xffff)
+	if (ld->sidenum[1] != NO_SIDEDEF)
 	{
 		sides[ld->sidenum[1]].special = ld->special;
 		sides[ld->sidenum[1]].line = ld;
 	}
 }
 
-static void P_SetLinedefV1(size_t i, UINT16 vertex_num)
+static void P_SetLinedefV1(size_t i, UINT32 vertex_num)
 {
 	if (vertex_num >= numvertexes)
 	{
@@ -1152,7 +1157,7 @@ static void P_SetLinedefV1(size_t i, UINT16 vertex_num)
 	lines[i].v1 = &vertexes[vertex_num];
 }
 
-static void P_SetLinedefV2(size_t i, UINT16 vertex_num)
+static void P_SetLinedefV2(size_t i, UINT32 vertex_num)
 {
 	if (vertex_num >= numvertexes)
 	{
@@ -1177,17 +1182,22 @@ static void P_LoadLinedefs(UINT8 *data)
 		memset(ld->stringargs, 0x00, NUMLINESTRINGARGS*sizeof(*ld->stringargs));
 		ld->alpha = FRACUNIT;
 		ld->executordelay = 0;
-		P_SetLinedefV1(i, SHORT(mld->v1));
-		P_SetLinedefV2(i, SHORT(mld->v2));
+		P_SetLinedefV1(i, (UINT16)SHORT(mld->v1));
+		P_SetLinedefV2(i, (UINT16)SHORT(mld->v2));
 
-		ld->sidenum[0] = SHORT(mld->sidenum[0]);
-		ld->sidenum[1] = SHORT(mld->sidenum[1]);
+		ld->sidenum[0] = (UINT16)SHORT(mld->sidenum[0]);
+		ld->sidenum[1] = (UINT16)SHORT(mld->sidenum[1]);
+
+		if (ld->sidenum[0] == 0xffff)
+			ld->sidenum[0] = NO_SIDEDEF;
+		if (ld->sidenum[1] == 0xffff)
+			ld->sidenum[1] = NO_SIDEDEF;
 
 		P_InitializeLinedef(ld);
 	}
 }
 
-static void P_SetSidedefSector(size_t i, UINT16 sector_num)
+static void P_SetSidedefSector(size_t i, UINT32 sector_num)
 {
 	// cph 2006/09/30 - catch out-of-range sector numbers; use sector 0 instead
 	if (sector_num >= numsectors)
@@ -1345,10 +1355,13 @@ static void P_LoadSidedefs(UINT8 *data)
 		}
 		sd->rowoffset = SHORT(msd->rowoffset)<<FRACBITS;
 
-		sd->offsetx_top = sd->offsetx_mid = sd->offsetx_bot = 0;
-		sd->offsety_top = sd->offsety_mid = sd->offsety_bot = 0;
+		sd->offsetx_top = sd->offsetx_mid = sd->offsetx_bottom = 0;
+		sd->offsety_top = sd->offsety_mid = sd->offsety_bottom = 0;
+
+		sd->scalex_top = sd->scalex_mid = sd->scalex_bottom = FRACUNIT;
+		sd->scaley_top = sd->scaley_mid = sd->scaley_bottom = FRACUNIT;
 
-		P_SetSidedefSector(i, SHORT(msd->sector));
+		P_SetSidedefSector(i, (UINT16)SHORT(msd->sector));
 
 		// Special info stored in texture fields!
 		switch (sd->special)
@@ -1677,7 +1690,7 @@ static void ParseTextmapSectorParameter(UINT32 i, const char *param, const char
 			if ((id = strchr(id, ' ')))
 				id++;
 		}
-	}	
+	}
 	else if (fastcmp(param, "xpanningfloor"))
 		sectors[i].floorxoffset = FLOAT_TO_FIXED(atof(val));
 	else if (fastcmp(param, "ypanningfloor"))
@@ -1686,6 +1699,14 @@ static void ParseTextmapSectorParameter(UINT32 i, const char *param, const char
 		sectors[i].ceilingxoffset = FLOAT_TO_FIXED(atof(val));
 	else if (fastcmp(param, "ypanningceiling"))
 		sectors[i].ceilingyoffset = FLOAT_TO_FIXED(atof(val));
+	else if (fastcmp(param, "xscalefloor"))
+		sectors[i].floorxscale = FLOAT_TO_FIXED(atof(val));
+	else if (fastcmp(param, "yscalefloor"))
+		sectors[i].flooryscale = FLOAT_TO_FIXED(atof(val));
+	else if (fastcmp(param, "xscaleceiling"))
+		sectors[i].ceilingxscale = FLOAT_TO_FIXED(atof(val));
+	else if (fastcmp(param, "yscaleceiling"))
+		sectors[i].ceilingyscale = FLOAT_TO_FIXED(atof(val));
 	else if (fastcmp(param, "rotationfloor"))
 		sectors[i].floorangle = FixedAngle(FLOAT_TO_FIXED(atof(val)));
 	else if (fastcmp(param, "rotationceiling"))
@@ -1881,13 +1902,25 @@ static void ParseTextmapSidedefParameter(UINT32 i, const char *param, const char
 	else if (fastcmp(param, "offsetx_mid"))
 		sides[i].offsetx_mid = atol(val) << FRACBITS;
 	else if (fastcmp(param, "offsetx_bottom"))
-		sides[i].offsetx_bot = atol(val) << FRACBITS;
+		sides[i].offsetx_bottom = atol(val) << FRACBITS;
 	else if (fastcmp(param, "offsety_top"))
 		sides[i].offsety_top = atol(val) << FRACBITS;
 	else if (fastcmp(param, "offsety_mid"))
 		sides[i].offsety_mid = atol(val) << FRACBITS;
 	else if (fastcmp(param, "offsety_bottom"))
-		sides[i].offsety_bot = atol(val) << FRACBITS;
+		sides[i].offsety_bottom = atol(val) << FRACBITS;
+	else if (fastcmp(param, "scalex_top"))
+		sides[i].scalex_top = FLOAT_TO_FIXED(atof(val));
+	else if (fastcmp(param, "scalex_mid"))
+		sides[i].scalex_mid = FLOAT_TO_FIXED(atof(val));
+	else if (fastcmp(param, "scalex_bottom"))
+		sides[i].scalex_bottom = FLOAT_TO_FIXED(atof(val));
+	else if (fastcmp(param, "scaley_top"))
+		sides[i].scaley_top = FLOAT_TO_FIXED(atof(val));
+	else if (fastcmp(param, "scaley_mid"))
+		sides[i].scaley_mid = FLOAT_TO_FIXED(atof(val));
+	else if (fastcmp(param, "scaley_bottom"))
+		sides[i].scaley_bottom = FLOAT_TO_FIXED(atof(val));
 	else if (fastcmp(param, "texturetop"))
 		sides[i].toptexture = R_TextureNumForName(val);
 	else if (fastcmp(param, "texturebottom"))
@@ -2487,7 +2520,7 @@ static void P_WriteTextmap(void)
 		fprintf(f, "v1 = %s;\n", sizeu1(wlines[i].v1 - vertexes));
 		fprintf(f, "v2 = %s;\n", sizeu1(wlines[i].v2 - vertexes));
 		fprintf(f, "sidefront = %d;\n", wlines[i].sidenum[0]);
-		if (wlines[i].sidenum[1] != 0xffff)
+		if (wlines[i].sidenum[1] != NO_SIDEDEF)
 			fprintf(f, "sideback = %d;\n", wlines[i].sidenum[1]);
 		firsttag = Tag_FGet(&wlines[i].tags);
 		if (firsttag != 0)
@@ -2592,10 +2625,22 @@ static void P_WriteTextmap(void)
 			fprintf(f, "offsetx_mid = %d;\n", wsides[i].offsetx_mid >> FRACBITS);
 		if (wsides[i].offsety_mid != 0)
 			fprintf(f, "offsety_mid = %d;\n", wsides[i].offsety_mid >> FRACBITS);
-		if (wsides[i].offsetx_bot != 0)
-			fprintf(f, "offsetx_bottom = %d;\n", wsides[i].offsetx_bot >> FRACBITS);
-		if (wsides[i].offsety_bot != 0)
-			fprintf(f, "offsety_bottom = %d;\n", wsides[i].offsety_bot >> FRACBITS);
+		if (wsides[i].offsetx_bottom != 0)
+			fprintf(f, "offsetx_bottom = %d;\n", wsides[i].offsetx_bottom >> FRACBITS);
+		if (wsides[i].offsety_bottom != 0)
+			fprintf(f, "offsety_bottom = %d;\n", wsides[i].offsety_bottom >> FRACBITS);
+		if (wsides[i].scalex_top != FRACUNIT)
+			fprintf(f, "scalex_top = %f;\n", FIXED_TO_FLOAT(wsides[i].scalex_top));
+		if (wsides[i].scaley_top != FRACUNIT)
+			fprintf(f, "scaley_top = %f;\n", FIXED_TO_FLOAT(wsides[i].scaley_top));
+		if (wsides[i].scalex_mid != FRACUNIT)
+			fprintf(f, "scalex_mid = %f;\n", FIXED_TO_FLOAT(wsides[i].scalex_mid));
+		if (wsides[i].scaley_mid != FRACUNIT)
+			fprintf(f, "scaley_mid = %f;\n", FIXED_TO_FLOAT(wsides[i].scaley_mid));
+		if (wsides[i].scalex_bottom != FRACUNIT)
+			fprintf(f, "scalex_bottom = %f;\n", FIXED_TO_FLOAT(wsides[i].scalex_bottom));
+		if (wsides[i].scaley_bottom != FRACUNIT)
+			fprintf(f, "scaley_bottom = %f;\n", FIXED_TO_FLOAT(wsides[i].scaley_bottom));
 		if (wsides[i].toptexture > 0 && wsides[i].toptexture < numtextures)
 			fprintf(f, "texturetop = \"%.*s\";\n", 8, textures[wsides[i].toptexture]->name);
 		if (wsides[i].bottomtexture > 0 && wsides[i].bottomtexture < numtextures)
@@ -2651,6 +2696,14 @@ static void P_WriteTextmap(void)
 			fprintf(f, "xpanningceiling = %f;\n", FIXED_TO_FLOAT(tempsec.ceilingxoffset));
 		if (tempsec.ceilingyoffset != 0)
 			fprintf(f, "ypanningceiling = %f;\n", FIXED_TO_FLOAT(tempsec.ceilingyoffset));
+		if (tempsec.floorxscale != 0)
+			fprintf(f, "xscalefloor = %f;\n", FIXED_TO_FLOAT(tempsec.floorxscale));
+		if (tempsec.flooryscale != 0)
+			fprintf(f, "yscalefloor = %f;\n", FIXED_TO_FLOAT(tempsec.flooryscale));
+		if (tempsec.ceilingxscale != 0)
+			fprintf(f, "xscaleceiling = %f;\n", FIXED_TO_FLOAT(tempsec.ceilingxscale));
+		if (tempsec.ceilingyscale != 0)
+			fprintf(f, "yscaleceiling = %f;\n", FIXED_TO_FLOAT(tempsec.ceilingyscale));
 		if (wsectors[i].floorangle != 0)
 			fprintf(f, "rotationfloor = %f;\n", FIXED_TO_FLOAT(AngleFixed(wsectors[i].floorangle)));
 		if (wsectors[i].ceilingangle != 0)
@@ -2886,6 +2939,9 @@ static void P_LoadTextmap(void)
 		sc->floorxoffset = sc->flooryoffset = 0;
 		sc->ceilingxoffset = sc->ceilingyoffset = 0;
 
+		sc->floorxscale = sc->flooryscale = FRACUNIT;
+		sc->ceilingxscale = sc->ceilingyscale = FRACUNIT;
+
 		sc->floorangle = sc->ceilingangle = 0;
 
 		sc->floorlightlevel = sc->ceilinglightlevel = 0;
@@ -2920,22 +2976,26 @@ static void P_LoadTextmap(void)
 		P_InitializeSector(sc);
 		if (textmap_colormap.used)
 		{
-			INT32 rgba = P_ColorToRGBA(textmap_colormap.lightcolor, textmap_colormap.lightalpha);
-			INT32 fadergba = P_ColorToRGBA(textmap_colormap.fadecolor, textmap_colormap.fadealpha);
+			// Convert alpha values from old 0-25 (A-Z) range to 0-255 range
+			UINT8 lightalpha = (textmap_colormap.lightalpha * 102) / 10;
+			UINT8 fadealpha = (textmap_colormap.fadealpha * 102) / 10;
+			
+			INT32 rgba = P_ColorToRGBA(textmap_colormap.lightcolor, lightalpha);
+			INT32 fadergba = P_ColorToRGBA(textmap_colormap.fadecolor, 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);
 	}
@@ -2952,8 +3012,8 @@ static void P_LoadTextmap(void)
 		memset(ld->stringargs, 0x00, NUMLINESTRINGARGS*sizeof(*ld->stringargs));
 		ld->alpha = FRACUNIT;
 		ld->executordelay = 0;
-		ld->sidenum[0] = 0xffff;
-		ld->sidenum[1] = 0xffff;
+		ld->sidenum[0] = NO_SIDEDEF;
+		ld->sidenum[1] = NO_SIDEDEF;
 
 		TextmapParse(linesPos[i], i, ParseTextmapLinedefParameter);
 
@@ -2961,7 +3021,7 @@ static void P_LoadTextmap(void)
 			I_Error("P_LoadTextmap: linedef %s has no v1 value set!\n", sizeu1(i));
 		if (!ld->v2)
 			I_Error("P_LoadTextmap: linedef %s has no v2 value set!\n", sizeu1(i));
-		if (ld->sidenum[0] == 0xffff)
+		if (ld->sidenum[0] == NO_SIDEDEF)
 			I_Error("P_LoadTextmap: linedef %s has no sidefront value set!\n", sizeu1(i));
 
 		P_InitializeLinedef(ld);
@@ -2972,8 +3032,10 @@ static void P_LoadTextmap(void)
 		// Defaults.
 		sd->textureoffset = 0;
 		sd->rowoffset = 0;
-		sd->offsetx_top = sd->offsetx_mid = sd->offsetx_bot = 0;
-		sd->offsety_top = sd->offsety_mid = sd->offsety_bot = 0;
+		sd->offsetx_top = sd->offsetx_mid = sd->offsetx_bottom = 0;
+		sd->offsety_top = sd->offsety_mid = sd->offsety_bottom = 0;
+		sd->scalex_top = sd->scalex_mid = sd->scalex_bottom = FRACUNIT;
+		sd->scaley_top = sd->scaley_mid = sd->scaley_bottom = FRACUNIT;
 		sd->toptexture = R_TextureNumForName("-");
 		sd->midtexture = R_TextureNumForName("-");
 		sd->bottomtexture = R_TextureNumForName("-");
@@ -3015,7 +3077,7 @@ static void P_ProcessLinedefsAfterSidedefs(void)
 	for (; i--; ld++)
 	{
 		ld->frontsector = sides[ld->sidenum[0]].sector; //e6y: Can't be -1 here
-		ld->backsector = ld->sidenum[1] != 0xffff ? sides[ld->sidenum[1]].sector : 0;
+		ld->backsector = ld->sidenum[1] != NO_SIDEDEF ? sides[ld->sidenum[1]].sector : 0;
 
 		if (udmf)
 			continue;
@@ -3254,10 +3316,10 @@ static void P_InitializeSeg(seg_t *seg)
 {
 	if (seg->linedef)
 	{
-		UINT16 side = seg->linedef->sidenum[seg->side];
+		UINT32 side = seg->linedef->sidenum[seg->side];
 
-		if (side == 0xffff)
-			I_Error("P_InitializeSeg: Seg %s refers to side %d of linedef %s, which doesn't exist!\n", sizeu1((size_t)(seg - segs)), seg->side, sizeu1((size_t)(seg->linedef - lines)));
+		if (side == NO_SIDEDEF)
+			I_Error("P_InitializeSeg: Seg %s refers to side %d of linedef %s, which doesn't exist!\n", sizeu1((size_t)(seg - segs)), seg->side, sizeu2((size_t)(seg->linedef - lines)));
 
 		seg->sidedef = &sides[side];
 
@@ -3299,7 +3361,7 @@ static void P_LoadSegs(UINT8 *data)
 
 		seg->length = P_SegLength(seg);
 #ifdef HWRENDER
-		seg->flength = (rendermode == render_opengl) ? P_SegLengthFloat(seg) : 0;
+		seg->flength = P_SegLengthFloat(seg);
 #endif
 
 		seg->glseg = false;
@@ -3441,7 +3503,7 @@ static boolean P_LoadExtraVertices(UINT8 **data)
 static boolean P_LoadExtendedSubsectorsAndSegs(UINT8 **data, nodetype_t nodetype)
 {
 	size_t i, k;
-	INT16 m;
+	size_t m;
 	seg_t *seg;
 
 	// Subsectors
@@ -3464,23 +3526,33 @@ static boolean P_LoadExtendedSubsectorsAndSegs(UINT8 **data, nodetype_t nodetype
 		{
 		case NT_XGLN:
 		case NT_XGL3:
-			for (m = 0; m < subsectors[i].numlines; m++, k++)
+			for (m = 0; m < (size_t)subsectors[i].numlines; m++, k++)
 			{
 				UINT32 vertexnum = READUINT32((*data));
-				UINT16 linenum;
-
 				if (vertexnum >= numvertexes)
-					I_Error("P_LoadExtendedSubsectorsAndSegs: Seg %s in subsector %d has invalid vertex %d!\n", sizeu1(k), m, vertexnum);
+					I_Error("P_LoadExtendedSubsectorsAndSegs: Seg %s in subsector %s has invalid vertex %d!\n", sizeu1(k), sizeu2(m), vertexnum);
 
 				segs[k - 1 + ((m == 0) ? subsectors[i].numlines : 0)].v2 = segs[k].v1 = &vertexes[vertexnum];
 
 				READUINT32((*data)); // partner, can be ignored by software renderer
 
-				linenum = (nodetype == NT_XGL3) ? READUINT32((*data)) : READUINT16((*data));
-				if (linenum != 0xFFFF && linenum >= numlines)
-					I_Error("P_LoadExtendedSubsectorsAndSegs: Seg %s in subsector %s has invalid linedef %d!\n", sizeu1(k), sizeu2(i), linenum);
-				segs[k].glseg = (linenum == 0xFFFF);
-				segs[k].linedef = (linenum == 0xFFFF) ? NULL : &lines[linenum];
+				if (nodetype == NT_XGL3)
+				{
+					UINT32 linenum = READUINT32((*data));
+					if (linenum != 0xFFFFFFFF && linenum >= numlines)
+						I_Error("P_LoadExtendedSubsectorsAndSegs: Seg %s in subsector %s has invalid linedef %d!\n", sizeu1(k), sizeu2(i), linenum);
+					segs[k].glseg = linenum == 0xFFFFFFFF;
+					segs[k].linedef = linenum == 0xFFFFFFFF ? NULL : &lines[linenum];
+				}
+				else
+				{
+					UINT16 linenum = READUINT16((*data));
+					if (linenum != 0xFFFF && linenum >= numlines)
+						I_Error("P_LoadExtendedSubsectorsAndSegs: Seg %s in subsector %s has invalid linedef %d!\n", sizeu1(k), sizeu2(i), linenum);
+					segs[k].glseg = linenum == 0xFFFF;
+					segs[k].linedef = linenum == 0xFFFF ? NULL : &lines[linenum];
+				}
+
 				segs[k].side = READUINT8((*data));
 			}
 			while (segs[subsectors[i].firstline].glseg)
@@ -3492,18 +3564,18 @@ static boolean P_LoadExtendedSubsectorsAndSegs(UINT8 **data, nodetype_t nodetype
 			break;
 
 		case NT_XNOD:
-			for (m = 0; m < subsectors[i].numlines; m++, k++)
+			for (m = 0; m < (size_t)subsectors[i].numlines; m++, k++)
 			{
 				UINT32 v1num = READUINT32((*data));
 				UINT32 v2num = READUINT32((*data));
 				UINT16 linenum = READUINT16((*data));
 
 				if (v1num >= numvertexes)
-					I_Error("P_LoadExtendedSubsectorsAndSegs: Seg %s in subsector %d has invalid v1 %d!\n", sizeu1(k), m, v1num);
+					I_Error("P_LoadExtendedSubsectorsAndSegs: Seg %s in subsector %s has invalid v1 %d!\n", sizeu1(k), sizeu2(m), v1num);
 				if (v2num >= numvertexes)
-					I_Error("P_LoadExtendedSubsectorsAndSegs: Seg %s in subsector %d has invalid v2 %d!\n", sizeu1(k), m, v2num);
+					I_Error("P_LoadExtendedSubsectorsAndSegs: Seg %s in subsector %s has invalid v2 %d!\n", sizeu1(k), sizeu2(m), v2num);
 				if (linenum >= numlines)
-					I_Error("P_LoadExtendedSubsectorsAndSegs: Seg %s in subsector %d has invalid linedef %d!\n", sizeu1(k), m, linenum);
+					I_Error("P_LoadExtendedSubsectorsAndSegs: Seg %s in subsector %s has invalid linedef %d!\n", sizeu1(k), sizeu2(m), linenum);
 
 				segs[k].v1 = &vertexes[v1num];
 				segs[k].v2 = &vertexes[v2num];
@@ -3531,7 +3603,7 @@ static boolean P_LoadExtendedSubsectorsAndSegs(UINT8 **data, nodetype_t nodetype
 		}
 		seg->length = P_SegLength(seg);
 #ifdef HWRENDER
-		seg->flength = (rendermode == render_opengl) ? P_SegLengthFloat(seg) : 0;
+		seg->flength = P_SegLengthFloat(seg);
 #endif
 	}
 
@@ -3988,14 +4060,14 @@ static void P_LinkMapData(void)
 		if (!seg->sidedef)
 			CorruptMapError(va("P_LinkMapData: seg->sidedef is NULL "
 				"(subsector %s, firstline is %d)", sizeu1(i), ss->firstline));
-		if (seg->sidedef - sides < 0 || seg->sidedef - sides > (UINT16)numsides)
+		if (seg->sidedef - sides < 0 || sidei > numsides)
 			CorruptMapError(va("P_LinkMapData: seg->sidedef refers to sidedef %s of %s "
 				"(subsector %s, firstline is %d)", sizeu1(sidei), sizeu2(numsides),
 				sizeu3(i), ss->firstline));
 		if (!seg->sidedef->sector)
 			CorruptMapError(va("P_LinkMapData: seg->sidedef->sector is NULL "
 				"(subsector %s, firstline is %d, sidedef is %s)", sizeu1(i), ss->firstline,
-				sizeu1(sidei)));
+				sizeu2(sidei)));
 		ss->sector = seg->sidedef->sector;
 	}
 
@@ -4913,7 +4985,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 
 			break;
 		case 259: //Custom FOF
-			if (lines[i].sidenum[1] == 0xffff)
+			if (lines[i].sidenum[1] == NO_SIDEDEF)
 				I_Error("Custom FOF (tag %d) found without a linedef back side!", tag);
 
 			lines[i].args[0] = tag;
@@ -5267,8 +5339,8 @@ static void P_ConvertBinaryLinedefTypes(void)
 			lines[i].args[1] = sides[lines[i].sidenum[0]].midtexture;
 			lines[i].args[2] = sides[lines[i].sidenum[0]].textureoffset >> FRACBITS;
 			lines[i].args[3] = sides[lines[i].sidenum[0]].rowoffset >> FRACBITS;
-			lines[i].args[4] = (lines[i].sidenum[1] != 0xffff) ? sides[lines[i].sidenum[1]].textureoffset >> FRACBITS : 0;
-			lines[i].args[5] = (lines[i].sidenum[1] != 0xffff) ? sides[lines[i].sidenum[1]].rowoffset >> FRACBITS : -1;
+			lines[i].args[4] = (lines[i].sidenum[1] != NO_SIDEDEF) ? sides[lines[i].sidenum[1]].textureoffset >> FRACBITS : 0;
+			lines[i].args[5] = (lines[i].sidenum[1] != NO_SIDEDEF) ? sides[lines[i].sidenum[1]].rowoffset >> FRACBITS : -1;
 			lines[i].args[6] = sides[lines[i].sidenum[0]].bottomtexture;
 			break;
 		case 414: //Play sound effect
@@ -5372,7 +5444,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 				lines[i].args[1] = max(sides[lines[i].sidenum[0]].textureoffset >> FRACBITS, 0);
 				// failsafe: if user specifies Back Y Offset and NOT Front Y Offset, use the Back Offset
 				// to be consistent with other light and fade specials
-				lines[i].args[2] = ((lines[i].sidenum[1] != 0xFFFF && !(sides[lines[i].sidenum[0]].rowoffset >> FRACBITS)) ?
+				lines[i].args[2] = ((lines[i].sidenum[1] != NO_SIDEDEF && !(sides[lines[i].sidenum[0]].rowoffset >> FRACBITS)) ?
 					max(min(sides[lines[i].sidenum[1]].rowoffset >> FRACBITS, 255), 0)
 					: max(min(sides[lines[i].sidenum[0]].rowoffset >> FRACBITS, 255), 0));
 			}
@@ -5477,7 +5549,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 			break;
 		case 442: //Change object type state
 			lines[i].args[0] = tag;
-			lines[i].args[1] = (lines[i].sidenum[1] == 0xffff) ? 1 : 0;
+			lines[i].args[1] = (lines[i].sidenum[1] == NO_SIDEDEF) ? 1 : 0;
 			break;
 		case 443: //Call Lua function
 			if (lines[i].stringargs[0] == NULL)
@@ -5545,7 +5617,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 		case 452: //Set FOF translucency
 			lines[i].args[0] = sides[lines[i].sidenum[0]].textureoffset >> FRACBITS;
 			lines[i].args[1] = sides[lines[i].sidenum[0]].rowoffset >> FRACBITS;
-			lines[i].args[2] = lines[i].sidenum[1] != 0xffff ? (sides[lines[i].sidenum[1]].textureoffset >> FRACBITS) : (P_AproxDistance(lines[i].dx, lines[i].dy) >> FRACBITS);
+			lines[i].args[2] = lines[i].sidenum[1] != NO_SIDEDEF ? (sides[lines[i].sidenum[1]].textureoffset >> FRACBITS) : (P_AproxDistance(lines[i].dx, lines[i].dy) >> FRACBITS);
 			if (lines[i].flags & ML_MIDPEG)
 				lines[i].args[3] |= TMST_RELATIVE;
 			if (lines[i].flags & ML_NOCLIMB)
@@ -5554,8 +5626,8 @@ static void P_ConvertBinaryLinedefTypes(void)
 		case 453: //Fade FOF
 			lines[i].args[0] = sides[lines[i].sidenum[0]].textureoffset >> FRACBITS;
 			lines[i].args[1] = sides[lines[i].sidenum[0]].rowoffset >> FRACBITS;
-			lines[i].args[2] = lines[i].sidenum[1] != 0xffff ? (sides[lines[i].sidenum[1]].textureoffset >> FRACBITS) : (lines[i].dx >> FRACBITS);
-			lines[i].args[3] = lines[i].sidenum[1] != 0xffff ? (sides[lines[i].sidenum[1]].rowoffset >> FRACBITS) : (abs(lines[i].dy) >> FRACBITS);
+			lines[i].args[2] = lines[i].sidenum[1] != NO_SIDEDEF ? (sides[lines[i].sidenum[1]].textureoffset >> FRACBITS) : (lines[i].dx >> FRACBITS);
+			lines[i].args[3] = lines[i].sidenum[1] != NO_SIDEDEF ? (sides[lines[i].sidenum[1]].rowoffset >> FRACBITS) : (abs(lines[i].dy) >> FRACBITS);
 			if (lines[i].flags & ML_MIDPEG)
 				lines[i].args[4] |= TMFT_RELATIVE;
 			if (lines[i].flags & ML_WRAPMIDTEX)
@@ -5582,7 +5654,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 			break;
 		case 455: //Fade colormap
 		{
-			INT32 speed = (INT32)((((lines[i].flags & ML_DONTPEGBOTTOM) || !sides[lines[i].sidenum[0]].rowoffset) && lines[i].sidenum[1] != 0xFFFF) ?
+			INT32 speed = (INT32)((((lines[i].flags & ML_DONTPEGBOTTOM) || !sides[lines[i].sidenum[0]].rowoffset) && lines[i].sidenum[1] != NO_SIDEDEF) ?
 				abs(sides[lines[i].sidenum[1]].rowoffset >> FRACBITS)
 				: abs(sides[lines[i].sidenum[0]].rowoffset >> FRACBITS));
 
@@ -5612,7 +5684,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 			lines[i].args[0] = tag;
 			lines[i].args[1] = sides[lines[i].sidenum[0]].textureoffset >> FRACBITS;
 			lines[i].args[2] = sides[lines[i].sidenum[0]].rowoffset >> FRACBITS;
-			lines[i].args[3] = (lines[i].sidenum[1] != 0xffff) ? sides[lines[i].sidenum[1]].textureoffset >> FRACBITS : 0;
+			lines[i].args[3] = (lines[i].sidenum[1] != NO_SIDEDEF) ? sides[lines[i].sidenum[1]].textureoffset >> FRACBITS : 0;
 			lines[i].args[4] = !!(lines[i].flags & ML_NOSKEW);
 			break;
 		case 459: //Control text prompt
@@ -5632,7 +5704,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 				lines[i].args[2] |= TMP_ALLPLAYERS;
 			if (lines[i].flags & ML_MIDSOLID)
 				lines[i].args[2] |= TMP_FREEZETHINKERS;*/
-			lines[i].args[3] = (lines[i].sidenum[1] != 0xFFFF) ? sides[lines[i].sidenum[1]].textureoffset >> FRACBITS : tag;
+			lines[i].args[3] = (lines[i].sidenum[1] != NO_SIDEDEF) ? sides[lines[i].sidenum[1]].textureoffset >> FRACBITS : tag;
 			break;
 		case 460: //Award rings
 			lines[i].args[0] = sides[lines[i].sidenum[0]].textureoffset >> FRACBITS;
@@ -5645,7 +5717,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 			lines[i].args[3] = (lines[i].flags & ML_SKEWTD) ? AngleFixed(R_PointToAngle2(lines[i].v1->x, lines[i].v1->y, lines[i].v2->x, lines[i].v2->y)) >> FRACBITS : 0;
 			if (lines[i].flags & ML_NOCLIMB)
 			{
-				if (lines[i].sidenum[1] != 0xffff) // Make sure the linedef has a back side
+				if (lines[i].sidenum[1] != NO_SIDEDEF) // Make sure the linedef has a back side
 				{
 					lines[i].args[4] = 1;
 					lines[i].args[5] = sides[lines[i].sidenum[1]].textureoffset >> FRACBITS;
@@ -5679,7 +5751,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 			lines[i].args[0] = tag;
 			lines[i].args[1] = sides[lines[i].sidenum[0]].textureoffset >> FRACBITS;
 			lines[i].args[2] = sides[lines[i].sidenum[0]].rowoffset >> FRACBITS;
-			if (lines[i].sidenum[1] != 0xffff)
+			if (lines[i].sidenum[1] != NO_SIDEDEF)
 				lines[i].args[3] = sides[lines[i].sidenum[1]].textureoffset >> FRACBITS;
 			break;
 		case 482: //Polyobject - move
@@ -5751,7 +5823,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 			if (!(lines[i].flags & ML_DONTPEGBOTTOM))
 				lines[i].args[1] /= 100;
 			// allow Back Y Offset to be consistent with other fade specials
-			lines[i].args[2] = (lines[i].sidenum[1] != 0xffff && !sides[lines[i].sidenum[0]].rowoffset) ?
+			lines[i].args[2] = (lines[i].sidenum[1] != NO_SIDEDEF && !sides[lines[i].sidenum[0]].rowoffset) ?
 				abs(sides[lines[i].sidenum[1]].rowoffset >> FRACBITS)
 				: abs(sides[lines[i].sidenum[0]].rowoffset >> FRACBITS);
 			if (lines[i].flags & ML_MIDPEG)
@@ -5778,7 +5850,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 			lines[i].args[0] = tag;
 			if (lines[i].flags & ML_MIDPEG)
 			{
-				if (lines[i].sidenum[1] == 0xffff)
+				if (lines[i].sidenum[1] == NO_SIDEDEF)
 				{
 					CONS_Debug(DBG_GAMELOGIC, "Line special %d (line #%s) missing back side!\n", lines[i].special, sizeu1(i));
 					lines[i].special = 0;
@@ -5808,7 +5880,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 			lines[i].args[0] = lines[i].special >= 507;
 			if (lines[i].special % 2 == 0)
 			{
-				if (lines[i].sidenum[1] == 0xffff)
+				if (lines[i].sidenum[1] == NO_SIDEDEF)
 				{
 					CONS_Debug(DBG_GAMELOGIC, "Line special %d (line #%s) missing back side!\n", lines[i].special, sizeu1(i));
 					lines[i].special = 0;
@@ -5964,7 +6036,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 			{
 				UINT8 side = lines[i].special >= 714;
 
-				if (side == 1 && lines[i].sidenum[1] == 0xffff)
+				if (side == 1 && lines[i].sidenum[1] == NO_SIDEDEF)
 					CONS_Debug(DBG_GAMELOGIC, "P_ConvertBinaryMap: Line special %d (line #%s) missing 2nd side!\n", lines[i].special, sizeu1(i));
 				else
 				{
@@ -6130,6 +6202,7 @@ static void P_ConvertBinarySectorTypes(void)
 			case 14: //Non-ramp sector
 				sectors[i].specialflags |= SSF_NOSTEPDOWN;
 				break;
+			// TODO: 2.3: Delete
 			case 15: //Bouncy FOF
 				CONS_Alert(CONS_WARNING, M_GetText("Deprecated bouncy FOF sector type detected. Please use linedef type 76 instead.\n"));
 				break;
@@ -6164,12 +6237,14 @@ static void P_ConvertBinarySectorTypes(void)
 				sectors[i].flags |= MSF_TRIGGERLINE_PLANE;
 				sectors[i].triggerer = TO_PLAYER;
 				break;
+			// TODO: 2.3: Delete
 			case 6: //Trigger linedef executor (Emerald check)
 				CONS_Alert(CONS_WARNING, M_GetText("Deprecated emerald check sector type detected. Please use linedef types 337-339 instead.\n"));
 				sectors[i].triggertag = tag;
 				sectors[i].flags &= ~MSF_TRIGGERLINE_PLANE;
 				sectors[i].triggerer = TO_PLAYEREMERALDS;
 				break;
+			// TODO: 2.3: Delete
 			case 7: //Trigger linedef executor (NiGHTS mare)
 				CONS_Alert(CONS_WARNING, M_GetText("Deprecated NiGHTS mare sector type detected. Please use linedef types 340-342 instead.\n"));
 				sectors[i].triggertag = tag;
@@ -6179,9 +6254,11 @@ static void P_ConvertBinarySectorTypes(void)
 			case 8: //Check for linedef executor on FOFs
 				sectors[i].flags |= MSF_TRIGGERLINE_MOBJ;
 				break;
+			// TODO: 2.3: Delete
 			case 10: //Special stage time/spheres requirements
 				CONS_Alert(CONS_WARNING, M_GetText("Deprecated sector type for special stage requirements detected. Please use the SpecialStageTime and SpecialStageSpheres level header options instead.\n"));
 				break;
+			// TODO: 2.3: Delete
 			case 11: //Custom global gravity
 				CONS_Alert(CONS_WARNING, M_GetText("Deprecated sector type for global gravity detected. Please use the Gravity level header option instead.\n"));
 				break;
@@ -6820,7 +6897,7 @@ static void P_ConvertBinaryThingTypes(void)
 		default:
 			break;
 		}
-		
+
 		// Clear binary thing height hacks, to prevent interfering with UDMF-only flags
 		mapthings[i].options &= 0xF;
 	}
@@ -7174,37 +7251,23 @@ static void P_RunLevelScript(const char *scriptname)
 
 static void P_ForceCharacter(const char *forcecharskin)
 {
-	if (netgame)
-	{
-		char skincmd[33];
-		if (splitscreen)
-		{
-			sprintf(skincmd, "skin2 %s\n", forcecharskin);
-			CV_Set(&cv_skin2, forcecharskin);
-		}
+	// Don't do anything if the server is forcing a skin already.
+	if (netgame && cv_forceskin.value >= 0)
+		return;
 
-		sprintf(skincmd, "skin %s\n", forcecharskin);
-		COM_BufAddText(skincmd);
-	}
-	else
+	for (unsigned i = 0; i < MAXPLAYERS; i++)
 	{
-		if (splitscreen)
-		{
-			SetPlayerSkin(secondarydisplayplayer, forcecharskin);
-			if ((unsigned)cv_playercolor2.value != skins[players[secondarydisplayplayer].skin].prefcolor)
-			{
-				CV_StealthSetValue(&cv_playercolor2, skins[players[secondarydisplayplayer].skin].prefcolor);
-				players[secondarydisplayplayer].skincolor = skins[players[secondarydisplayplayer].skin].prefcolor;
-			}
-		}
+		if (!playeringame[i])
+			continue;
 
-		SetPlayerSkin(consoleplayer, forcecharskin);
-		// normal player colors in single player
-		if ((unsigned)cv_playercolor.value != skins[players[consoleplayer].skin].prefcolor)
-		{
-			CV_StealthSetValue(&cv_playercolor, skins[players[consoleplayer].skin].prefcolor);
-			players[consoleplayer].skincolor = skins[players[consoleplayer].skin].prefcolor;
-		}
+		INT32 skinnum = R_SkinAvailable(forcecharskin);
+		if (skinnum == -1 || !R_SkinUsable(i, skinnum))
+			continue;
+
+		SetPlayerSkinByNum(i, skinnum);
+
+		if (!netgame)
+			players[i].skincolor = skins[skinnum].prefcolor;
 	}
 }
 
@@ -7813,6 +7876,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	Patch_FreeTag(PU_PATCH_LOWPRIORITY);
 	Patch_FreeTag(PU_PATCH_ROTATED);
 	Z_FreeTags(PU_LEVEL, PU_PURGELEVEL - 1);
+	mobjcache = NULL;
 
 	R_InitializeLevelInterpolators();
 
diff --git a/src/p_spec.c b/src/p_spec.c
index 28ecc60f4dedb5f67f9aa33e092232eb0b3782f6..131a58d20c4a5b3d7f5800b2b3a2e4db93a229c7 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -557,7 +557,7 @@ static inline sector_t *getSector(INT32 currentSector, INT32 line, INT32 side)
   */
 static inline boolean twoSided(INT32 sector, INT32 line)
 {
-	return (sectors[sector].lines[line])->sidenum[1] != 0xffff;
+	return (sectors[sector].lines[line])->sidenum[1] != NO_SIDEDEF;
 }
 #endif
 
@@ -1796,7 +1796,7 @@ void P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller)
 
 				if (trigid < 0 || trigid > 31) // limited by 32 bit variable
 				{
-					CONS_Debug(DBG_GAMELOGIC, "Unlockable trigger (sidedef %hu): bad trigger ID %d\n", triggerline->sidenum[0], trigid);
+					CONS_Debug(DBG_GAMELOGIC, "Unlockable trigger (sidedef %u): bad trigger ID %d\n", triggerline->sidenum[0], trigid);
 					return;
 				}
 				else if (!(unlocktriggers & (1 << trigid)))
@@ -1809,7 +1809,7 @@ void P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller)
 
 				if (unlockid <= 0 || unlockid > MAXUNLOCKABLES) // limited by unlockable count
 				{
-					CONS_Debug(DBG_GAMELOGIC, "Unlockable check (sidedef %hu): bad unlockable ID %d\n", triggerline->sidenum[0], unlockid);
+					CONS_Debug(DBG_GAMELOGIC, "Unlockable check (sidedef %u): bad unlockable ID %d\n", triggerline->sidenum[0], unlockid);
 					return;
 				}
 				else if (!(serverGamedata->unlocked[unlockid-1]))
@@ -2607,7 +2607,15 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				if (lumpnum == LUMPERROR || W_LumpLength(lumpnum) == 0)
 					CONS_Debug(DBG_SETUP, "Line type 415 Executor: script lump %s not found/not valid.\n", line->stringargs[0]);
 				else
-					COM_BufInsertText(W_CacheLumpNum(lumpnum, PU_CACHE));
+				{
+					void *lump = W_CacheLumpNum(lumpnum, PU_CACHE);
+					size_t len = W_LumpLength(lumpnum);
+					char *text = Z_Malloc(len + 1, PU_CACHE, NULL);
+					memcpy(text, lump, len);
+					text[len] = '\0';
+					COM_BufInsertText(text);
+					Z_Free(text);
+				}
 			}
 			break;
 
@@ -2661,7 +2669,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					titlemapcameraref = altview;
 				else if (!mo->player->awayviewtics || mo->player->awayviewmobj != altview) {
 					P_SetTarget(&mo->player->awayviewmobj, altview);
-					
+
 					if (mo->player == &players[displayplayer])
 						P_ResetCamera(mo->player, &camera); // reset p1 camera on p1 getting an awayviewmobj
 					else if (splitscreen && mo->player == &players[secondarydisplayplayer])
@@ -2727,7 +2735,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				mo->player->rmomx = mo->player->rmomy = 1;
 				mo->player->cmomx = mo->player->cmomy = 0;
 				P_ResetPlayer(mo->player);
-				P_SetPlayerMobjState(mo, S_PLAY_STND);
+				P_SetMobjState(mo, S_PLAY_STND);
 
 				// Reset bot too.
 				if (bot) {
@@ -2738,7 +2746,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					bot->player->rmomx = bot->player->rmomy = 1;
 					bot->player->cmomx = bot->player->cmomy = 0;
 					P_ResetPlayer(bot->player);
-					P_SetPlayerMobjState(bot, S_PLAY_STND);
+					P_SetMobjState(bot, S_PLAY_STND);
 				}
 			}
 			break;
@@ -2897,7 +2905,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			{
 				size_t linenum;
 				side_t *setfront = &sides[line->sidenum[0]];
-				side_t *setback = (line->args[3] && line->sidenum[1] != 0xffff) ? &sides[line->sidenum[1]] : setfront;
+				side_t *setback = (line->args[3] && line->sidenum[1] != NO_SIDEDEF) ? &sides[line->sidenum[1]] : setfront;
 				side_t *this;
 				boolean always = !(line->args[2]); // If args[2] is set: Only change mid texture if mid texture already exists on tagged lines, etc.
 
@@ -2919,7 +2927,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					}
 
 					// Back side
-					if (line->args[1] != TMSD_FRONT && lines[linenum].sidenum[1] != 0xffff)
+					if (line->args[1] != TMSD_FRONT && lines[linenum].sidenum[1] != NO_SIDEDEF)
 					{
 						this = &sides[lines[linenum].sidenum[1]];
 						if (always || this->toptexture) this->toptexture = setback->toptexture;
@@ -2940,7 +2948,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				INT32 trigid = line->args[0];
 
 				if (trigid < 0 || trigid > 31) // limited by 32 bit variable
-					CONS_Debug(DBG_GAMELOGIC, "Unlockable trigger (sidedef %hu): bad trigger ID %d\n", line->sidenum[0], trigid);
+					CONS_Debug(DBG_GAMELOGIC, "Unlockable trigger (sidedef %u): bad trigger ID %d\n", line->sidenum[0], trigid);
 				else
 				{
 					unlocktriggers |= 1 << trigid;
@@ -3153,7 +3161,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 
 				if (line->args[2] & TMCF_RELATIVE)
 				{
-					extracolormap_t *target = (!udmf && (line->flags & ML_TFERLINE) && line->sidenum[1] != 0xFFFF) ?
+					extracolormap_t *target = (!udmf && (line->flags & ML_TFERLINE) && line->sidenum[1] != NO_SIDEDEF) ?
 						sides[line->sidenum[1]].colormap_data : sectors[secnum].extra_colormap; // use back colormap instead of target sector
 
 						extracolormap_t *exc = R_AddColormaps(
@@ -3482,7 +3490,7 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				}
 
 				if (!udmf && (line->flags & ML_TFERLINE)) // use back colormap instead of target sector
-					sectors[secnum].extra_colormap = (line->sidenum[1] != 0xFFFF) ?
+					sectors[secnum].extra_colormap = (line->sidenum[1] != NO_SIDEDEF) ?
 					sides[line->sidenum[1]].colormap_data : NULL;
 
 				exc = sectors[secnum].extra_colormap;
@@ -4178,6 +4186,7 @@ sector_t *P_MobjTouchingSectorSpecial(mobj_t *mo, INT32 section, INT32 number)
 	return NULL;
 }
 
+// TODO: 2.3: Delete
 // Deprecated in favor of P_MobjTouchingSectorSpecial
 // Kept for Lua backwards compatibility only
 sector_t *P_ThingOnSpecial3DFloor(mobj_t *mo)
@@ -4557,7 +4566,7 @@ static void P_ProcessSpeedPad(player_t *player, sector_t *sector, sector_t *rove
 		if (!(player->pflags & PF_SPINNING))
 			player->pflags |= PF_SPINNING;
 
-		P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+		P_SetMobjState(player->mo, S_PLAY_ROLL);
 	}
 
 	player->powers[pw_flashing] = TICRATE/3;
@@ -4735,7 +4744,7 @@ static void P_ProcessZoomTube(player_t *player, mtag_t sectag, boolean end)
 
 	if (player->mo->state-states != S_PLAY_ROLL)
 	{
-		P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+		P_SetMobjState(player->mo, S_PLAY_ROLL);
 		S_StartSound(player->mo, sfx_spin);
 	}
 }
@@ -4949,7 +4958,7 @@ static void P_ProcessRopeHang(player_t *player, mtag_t sectag)
 	player->pflags &= ~(PF_JUMPED|PF_NOJUMPDAMAGE|PF_GLIDING|PF_BOUNCING|PF_SLIDING|PF_CANCARRY);
 	player->climbing = 0;
 	P_SetThingPosition(player->mo);
-	P_SetPlayerMobjState(player->mo, S_PLAY_RIDE);
+	P_SetMobjState(player->mo, S_PLAY_RIDE);
 }
 
 static boolean P_SectorHasSpecial(sector_t *sec)
@@ -5008,7 +5017,7 @@ static void P_EvaluateSpecialFlags(player_t *player, sector_t *sector, sector_t
 		if (!player->powers[pw_carry])
 		{
 			P_ResetPlayer(player);
-			P_SetPlayerMobjState(player->mo, S_PLAY_FALL);
+			P_SetMobjState(player->mo, S_PLAY_FALL);
 			P_SetTarget(&player->mo->tracer, player->mo);
 			player->powers[pw_carry] = CR_FAN;
 		}
@@ -5023,7 +5032,7 @@ static void P_EvaluateSpecialFlags(player_t *player, sector_t *sector, sector_t
 		if (!(player->pflags & PF_SPINNING))
 		{
 			player->pflags |= PF_SPINNING;
-			P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+			P_SetMobjState(player->mo, S_PLAY_ROLL);
 			S_StartAttackSound(player->mo, sfx_spin);
 
 			if (abs(player->rmomx) < FixedMul(5*FRACUNIT, player->mo->scale)
@@ -5596,6 +5605,8 @@ static ffloor_t *P_AddFakeFloor(sector_t *sec, sector_t *sec2, line_t *master, I
 	fflr->bottompic = &sec2->floorpic;
 	fflr->bottomxoffs = &sec2->floorxoffset;
 	fflr->bottomyoffs = &sec2->flooryoffset;
+	fflr->bottomxscale = &sec2->floorxscale;
+	fflr->bottomyscale = &sec2->flooryscale;
 	fflr->bottomangle = &sec2->floorangle;
 
 	// Add the ceiling
@@ -5604,6 +5615,8 @@ static ffloor_t *P_AddFakeFloor(sector_t *sec, sector_t *sec2, line_t *master, I
 	fflr->toplightlevel = &sec2->lightlevel;
 	fflr->topxoffs = &sec2->ceilingxoffset;
 	fflr->topyoffs = &sec2->ceilingyoffset;
+	fflr->topxscale = &sec2->ceilingxscale;
+	fflr->topyscale = &sec2->ceilingyscale;
 	fflr->topangle = &sec2->ceilingangle;
 
 	// Add slopes
@@ -6229,6 +6242,7 @@ void P_SpawnSpecials(boolean fromnetsave)
 			sector->flags |= MSF_TRIGGERSPECIAL_TOUCH;
 		}
 
+		// TODO: 2.3: Delete everything below
 		// Process deprecated binary sector specials
 		if (udmf || !sector->special)
 			continue;
@@ -7598,7 +7612,7 @@ static void P_SpawnScrollers(void)
 					{
 						if (l->args[1] != TMSD_BACK)
 							Add_Scroller(sc_side, l->args[2] << (FRACBITS - SCROLL_SHIFT), l->args[3] << (FRACBITS - SCROLL_SHIFT), control, lines[s].sidenum[0], accel, 0);
-						if (l->args[1] != TMSD_FRONT && lines[s].sidenum[1] != 0xffff)
+						if (l->args[1] != TMSD_FRONT && lines[s].sidenum[1] != NO_SIDEDEF)
 							Add_Scroller(sc_side, l->args[2] << (FRACBITS - SCROLL_SHIFT), l->args[3] << (FRACBITS - SCROLL_SHIFT), control, lines[s].sidenum[1], accel, 0);
 					}
 				break;
@@ -7609,7 +7623,7 @@ static void P_SpawnScrollers(void)
 					Add_Scroller(sc_side, -l->args[1] << FRACBITS, l->args[2] << FRACBITS, -1, l->sidenum[0], accel, 0);
 				if (l->args[0] != TMSD_FRONT)
 				{
-					if (l->sidenum[1] != 0xffff)
+					if (l->sidenum[1] != NO_SIDEDEF)
 						Add_Scroller(sc_side, -l->args[1] << FRACBITS, l->args[2] << FRACBITS, -1, l->sidenum[1], accel, 0);
 					else
 						CONS_Debug(DBG_GAMELOGIC, "Line special 500 (line #%s) missing back side!\n", sizeu1(i));
diff --git a/src/p_telept.c b/src/p_telept.c
index 66b05ff01c6b6ba2c4efbee60b15acecdcfd412b..32669edaf7a0014991c7a6bff84f2a8df78d2933 100644
--- a/src/p_telept.c
+++ b/src/p_telept.c
@@ -96,7 +96,7 @@ void P_MixUp(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z, angle_t angle,
 		P_ClearStarPost(starpostnum);
 
 		P_ResetPlayer(thing->player);
-		P_SetPlayerMobjState(thing, S_PLAY_STND);
+		P_SetMobjState(thing, S_PLAY_STND);
 
 		P_FlashPal(thing->player, PAL_MIXUP, 10);
 	}
@@ -153,7 +153,7 @@ boolean P_Teleport(mobj_t *thing, fixed_t x, fixed_t y, fixed_t z, angle_t angle
 			thing->player->rmomx = thing->player->rmomy = 0;
 			thing->player->speed = 0;
 			P_ResetPlayer(thing->player);
-			P_SetPlayerMobjState(thing, S_PLAY_STND);
+			P_SetMobjState(thing, S_PLAY_STND);
 
 			thing->reactiontime = TICRATE/2; // don't move for about half a second
 			thing->player->drawangle = angle;
diff --git a/src/p_tick.c b/src/p_tick.c
index ec5d8a2da0a487a6e4743e39dedc04c2fb27069d..dca806ebee2810b5556f8cf6d288c7ffd1607069 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -26,6 +26,8 @@
 #include "r_main.h"
 #include "r_fps.h"
 #include "i_video.h" // rendermode
+#include "netcode/net_command.h"
+#include "netcode/server_connection.h"
 
 // Object place
 #include "m_cheat.h"
@@ -215,6 +217,7 @@ void P_AddThinker(const thinklistnum_t n, thinker_t *thinker)
 	thlist[n].prev = thinker;
 
 	thinker->references = 0;    // killough 11/98: init reference counter to 0
+	thinker->cachable = n == THINK_MOBJ;
 
 #ifdef PARANOIA
 	thinker->debug_mobjtype = MT_NULL;
@@ -224,21 +227,22 @@ void P_AddThinker(const thinklistnum_t n, thinker_t *thinker)
 #ifdef PARANOIA
 static const char *MobjTypeName(const mobj_t *mobj)
 {
+	mobjtype_t type;
 	actionf_p1 p1 = mobj->thinker.function.acp1;
 
 	if (p1 == (actionf_p1)P_MobjThinker)
-	{
-		return MOBJTYPE_LIST[mobj->type];
-	}
-	else if (p1 == (actionf_p1)P_RemoveThinkerDelayed)
-	{
-		if (mobj->thinker.debug_mobjtype != MT_NULL)
-		{
-			return MOBJTYPE_LIST[mobj->thinker.debug_mobjtype];
-		}
-	}
+		type = mobj->type;
+	else if (p1 == (actionf_p1)P_RemoveThinkerDelayed && mobj->thinker.debug_mobjtype != MT_NULL)
+		type = mobj->thinker.debug_mobjtype;
+	else
+		return "<Not a mobj>";
 
-	return "<Not a mobj>";
+	if (type < 0 || type >= NUMMOBJTYPES || (type >= MT_FIRSTFREESLOT && !FREE_MOBJS[type - MT_FIRSTFREESLOT]))
+		return "<Invalid mobj type>";
+	else if (type >= MT_FIRSTFREESLOT)
+		return FREE_MOBJS[type - MT_FIRSTFREESLOT]; // This doesn't include "MT_"...
+	else
+		return MOBJTYPE_LIST[type];
 }
 
 static const char *MobjThinkerName(const mobj_t *mobj)
@@ -317,7 +321,16 @@ void P_RemoveThinkerDelayed(thinker_t *thinker)
 	(next->prev = currentthinker = thinker->prev)->next = next;
 
 	R_DestroyLevelInterpolators(thinker);
-	Z_Free(thinker);
+	if (thinker->cachable)
+	{
+		// put cachable thinkers in the mobj cache, so we can avoid allocations
+		((mobj_t *)thinker)->hnext = mobjcache;
+		mobjcache = (mobj_t *)thinker;
+	}
+	else
+	{
+		Z_Free(thinker);
+	}
 }
 
 //
@@ -841,16 +854,7 @@ void P_Ticker(boolean run)
 			countdown2--;
 
 		if (quake.time)
-		{
-			fixed_t ir = quake.intensity>>1;
-			/// \todo Calculate distance from epicenter if set and modulate the intensity accordingly based on radius.
-			quake.x = M_RandomRange(-ir,ir);
-			quake.y = M_RandomRange(-ir,ir);
-			quake.z = M_RandomRange(-ir,ir);
 			--quake.time;
-		}
-		else
-			quake.x = quake.y = quake.z = 0;
 
 		if (metalplayback)
 			G_ReadMetalTic(metalplayback);
diff --git a/src/p_user.c b/src/p_user.c
index 3b2c60e3a6d744d5da157e0a5952efd5ad0cf3d4..5cb1d9d5aea95422c58b41493d308eb0382bd673 100644
--- a/src/p_user.c
+++ b/src/p_user.c
@@ -17,7 +17,8 @@
 #include "doomdef.h"
 #include "i_system.h"
 #include "d_event.h"
-#include "d_net.h"
+#include "netcode/d_net.h"
+#include "netcode/net_command.h"
 #include "g_game.h"
 #include "p_local.h"
 #include "r_fps.h"
@@ -105,8 +106,7 @@ void P_Thrust(mobj_t *mo, angle_t angle, fixed_t move)
 		mo->momy += FixedMul(move, FINESINE(angle));
 }
 
-#if 0
-static inline void P_ThrustEvenIn2D(mobj_t *mo, angle_t angle, fixed_t move)
+void P_ThrustEvenIn2D(mobj_t *mo, angle_t angle, fixed_t move)
 {
 	angle >>= ANGLETOFINESHIFT;
 
@@ -114,7 +114,7 @@ static inline void P_ThrustEvenIn2D(mobj_t *mo, angle_t angle, fixed_t move)
 	mo->momy += FixedMul(move, FINESINE(angle));
 }
 
-static inline void P_VectorInstaThrust(fixed_t xa, fixed_t xb, fixed_t xc, fixed_t ya, fixed_t yb, fixed_t yc,
+void P_VectorInstaThrust(fixed_t xa, fixed_t xb, fixed_t xc, fixed_t ya, fixed_t yb, fixed_t yc,
 	fixed_t za, fixed_t zb, fixed_t zc, fixed_t momentum, mobj_t *mo)
 {
 	fixed_t a1, b1, c1, a2, b2, c2, i, j, k;
@@ -144,7 +144,6 @@ static inline void P_VectorInstaThrust(fixed_t xa, fixed_t xb, fixed_t xc, fixed
 	mo->momy = j;
 	mo->momz = k;
 }
-#endif
 
 //
 // P_InstaThrust
@@ -394,6 +393,8 @@ void P_GiveFinishFlags(player_t *player)
 		fixed_t xoffs = FINECOSINE(fa);
 		fixed_t yoffs = FINESINE(fa);
 		mobj_t* flag = P_SpawnMobjFromMobj(player->mo, xoffs, yoffs, 0, MT_FINISHFLAG);
+		if (P_MobjWasRemoved(flag))
+			continue;
 		flag->angle = angle;
 		angle += FixedAngle(120*FRACUNIT);
 
@@ -667,7 +668,7 @@ static void P_DeNightserizePlayer(player_t *player)
 	player->powers[pw_carry] = CR_NIGHTSFALL;
 
 	player->powers[pw_underwater] = 0;
-	player->pflags &= ~(PF_SPINDOWN|PF_JUMPDOWN|PF_ATTACKDOWN|PF_STARTDASH|PF_GLIDING|PF_STARTJUMP|PF_JUMPED|PF_NOJUMPDAMAGE|PF_THOKKED|PF_SPINNING|PF_DRILLING|PF_TRANSFERTOCLOSEST);
+	player->pflags &= ~(PF_SPINDOWN|PF_JUMPDOWN|PF_ATTACKDOWN|PF_SHIELDDOWN|PF_STARTDASH|PF_GLIDING|PF_STARTJUMP|PF_JUMPED|PF_NOJUMPDAMAGE|PF_THOKKED|PF_SPINNING|PF_DRILLING|PF_TRANSFERTOCLOSEST);
 	player->secondjump = 0;
 	player->homing = 0;
 	player->climbing = 0;
@@ -686,7 +687,7 @@ static void P_DeNightserizePlayer(player_t *player)
 
 	player->mo->skin = &skins[player->skin];
 	player->followitem = skins[player->skin].followitem;
-	player->mo->color = player->skincolor;
+	player->mo->color = P_GetPlayerColor(player);
 	G_GhostAddColor(GHC_RETURNSKIN);
 
 	// Restore aiming angle
@@ -695,7 +696,7 @@ static void P_DeNightserizePlayer(player_t *player)
 	else if (player == &players[secondarydisplayplayer])
 		localaiming2 = 0;
 
-	P_SetPlayerMobjState(player->mo, S_PLAY_FALL);
+	P_SetMobjState(player->mo, S_PLAY_FALL);
 
 	// If in a special stage, add some preliminary exit time.
 	if (G_IsSpecialStage(gamemap))
@@ -795,7 +796,7 @@ void P_NightserizePlayer(player_t *player, INT32 nighttime)
 		}
 	}
 
-	player->pflags &= ~(PF_SPINDOWN|PF_JUMPDOWN|PF_ATTACKDOWN|PF_STARTDASH|PF_GLIDING|PF_JUMPED|PF_NOJUMPDAMAGE|PF_THOKKED|PF_SHIELDABILITY|PF_SPINNING|PF_DRILLING);
+	player->pflags &= ~(PF_SPINDOWN|PF_JUMPDOWN|PF_ATTACKDOWN|PF_SHIELDDOWN|PF_STARTDASH|PF_GLIDING|PF_JUMPED|PF_NOJUMPDAMAGE|PF_THOKKED|PF_SHIELDABILITY|PF_SPINNING|PF_DRILLING);
 	player->homing = 0;
 	player->mo->fuse = 0;
 	player->speed = 0;
@@ -949,7 +950,7 @@ void P_NightserizePlayer(player_t *player, INT32 nighttime)
 	P_RunNightserizeExecutors(player->mo);
 
 	player->powers[pw_carry] = CR_NIGHTSMODE;
-	P_SetPlayerMobjState(player->mo, S_PLAY_NIGHTS_TRANS1);
+	P_SetMobjState(player->mo, S_PLAY_NIGHTS_TRANS1);
 }
 
 pflags_t P_GetJumpFlags(player_t *player)
@@ -995,7 +996,7 @@ void P_DoPlayerPain(player_t *player, mobj_t *source, mobj_t *inflictor)
 		fixed_t fallbackspeed;
 
 		P_ResetPlayer(player);
-		P_SetPlayerMobjState(player->mo, player->mo->info->painstate);
+		P_SetMobjState(player->mo, player->mo->info->painstate);
 
 		if (player->mo->eflags & MFE_VERTICALFLIP)
 			player->mo->z--;
@@ -1343,12 +1344,10 @@ void P_DoSuperTransformation(player_t *player, boolean giverings)
 	if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOSSMUSIC) && P_IsLocalPlayer(player))
 		P_PlayJingle(player, JT_SUPER);
 
-	S_StartSound(NULL, sfx_supert); //let all players hear it -mattw_cfi
-
 	player->mo->momx = player->mo->momy = player->mo->momz = player->cmomx = player->cmomy = player->rmomx = player->rmomy = 0;
 
 	// Transformation animation
-	P_SetPlayerMobjState(player->mo, S_PLAY_SUPER_TRANS1);
+	P_SetMobjState(player->mo, S_PLAY_SUPER_TRANS1);
 
 	if (giverings && player->rings < 50)
 		player->rings = 50;
@@ -1361,8 +1360,11 @@ void P_DoSuperTransformation(player_t *player, boolean giverings)
 		player->powers[pw_sneakers] = 0;
 	}
 
-	if (!G_CoopGametype())
+	if (G_CoopGametype())
+		S_StartSound(player->mo, sfx_supert); //only hear it near yourself in co-op
+	else
 	{
+		S_StartSound(NULL, sfx_supert); //let all players hear it -mattw_cfi
 		HU_SetCEchoFlags(0);
 		HU_SetCEchoDuration(5);
 		HU_DoCEcho(va("%s\\is now super.\\\\\\\\", player_names[player-players]));
@@ -1371,6 +1373,56 @@ void P_DoSuperTransformation(player_t *player, boolean giverings)
 	P_PlayerFlagBurst(player, false);
 }
 
+//
+// P_DoSuperDetransformation
+//
+// Detransform into regular Sonic!
+static void P_DoSuperDetransformation(player_t *player)
+{
+	player->powers[pw_emeralds] = 0; // lost the power stones
+	P_SpawnGhostMobj(player->mo);
+
+	player->powers[pw_super] = 0;
+
+	// Restore color
+	if ((player->powers[pw_shield] & SH_STACK) == SH_FIREFLOWER)
+	{
+		player->mo->color = SKINCOLOR_WHITE;
+		G_GhostAddColor(GHC_FIREFLOWER);
+	}
+	else
+	{
+		player->mo->color = P_GetPlayerColor(player);
+		G_GhostAddColor(GHC_NORMAL);
+	}
+
+	if (!G_CoopGametype())
+		player->powers[pw_flashing] = flashingtics-1;
+
+	if (player->mo->sprite2 & FF_SPR2SUPER)
+		P_SetMobjState(player->mo, player->mo->state-states);
+
+	// Inform the netgame that the champion has fallen in the heat of battle.
+	if (!G_CoopGametype())
+	{
+		S_StartSound(NULL, sfx_s3k66); //let all players hear it.
+		HU_SetCEchoFlags(0);
+		HU_SetCEchoDuration(5);
+		HU_DoCEcho(va("%s\\is no longer super.\\\\\\\\", player_names[player-players]));
+	}
+
+	// Resume normal music if you're the console player
+	if (P_IsLocalPlayer(player))
+	{
+		music_stack_noposition = true; // HACK: Do not reposition next music
+		music_stack_fadeout = MUSICRATE/2; // HACK: Fade out current music
+	}
+	P_RestoreMusic(player);
+
+	// If you had a shield, restore its visual significance.
+	P_SpawnShieldOrb(player);
+}
+
 // Adds to the player's score
 void P_AddPlayerScore(player_t *player, UINT32 amount)
 {
@@ -1839,6 +1891,9 @@ void P_SpawnShieldOrb(player_t *player)
 	}
 
 	shieldobj = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, orbtype);
+	if (P_MobjWasRemoved(shieldobj))
+		return;
+
 	shieldobj->flags2 |= MF2_SHIELD;
 	P_SetTarget(&shieldobj->target, player->mo);
 	P_SetTarget(&shieldobj->dontdrawforviewmobj, player->mo); // Hide the shield in first-person
@@ -1854,24 +1909,33 @@ void P_SpawnShieldOrb(player_t *player)
 	if (shieldobj->info->seestate)
 	{
 		ov = P_SpawnMobj(shieldobj->x, shieldobj->y, shieldobj->z, MT_OVERLAY);
-		P_SetTarget(&ov->target, shieldobj);
+		if (!P_MobjWasRemoved(ov))
+		{
+			P_SetTarget(&ov->target, shieldobj);
 		P_SetTarget(&ov->dontdrawforviewmobj, player->mo); // Hide the shield in first-person
-		P_SetMobjState(ov, shieldobj->info->seestate);
-		P_SetTarget(&shieldobj->tracer, ov);
+			P_SetMobjState(ov, shieldobj->info->seestate);
+			P_SetTarget(&shieldobj->tracer, ov);
+		}
 	}
 	if (shieldobj->info->meleestate)
 	{
 		ov = P_SpawnMobj(shieldobj->x, shieldobj->y, shieldobj->z, MT_OVERLAY);
-		P_SetTarget(&ov->target, shieldobj);
+		if (!P_MobjWasRemoved(ov))
+		{
+			P_SetTarget(&ov->target, shieldobj);
 		P_SetTarget(&ov->dontdrawforviewmobj, player->mo); // Hide the shield in first-person
-		P_SetMobjState(ov, shieldobj->info->meleestate);
+			P_SetMobjState(ov, shieldobj->info->meleestate);
+		}
 	}
 	if (shieldobj->info->missilestate)
 	{
 		ov = P_SpawnMobj(shieldobj->x, shieldobj->y, shieldobj->z, MT_OVERLAY);
-		P_SetTarget(&ov->target, shieldobj);
+		if (!P_MobjWasRemoved(ov))
+		{
+			P_SetTarget(&ov->target, shieldobj);
 		P_SetTarget(&ov->dontdrawforviewmobj, player->mo); // Hide the shield in first-person
-		P_SetMobjState(ov, shieldobj->info->missilestate);
+			P_SetMobjState(ov, shieldobj->info->missilestate);
+		}
 	}
 	if (player->powers[pw_shield] & SH_FORCE)
 	{
@@ -1967,6 +2031,8 @@ void P_SetPower(player_t *player, powertype_t power, UINT16 value)
 mobj_t *P_SpawnGhostMobj(mobj_t *mobj)
 {
 	mobj_t *ghost = P_SpawnMobj(mobj->x, mobj->y, mobj->z, MT_GHOST);
+	if (P_MobjWasRemoved(ghost))
+		return NULL;
 
 	P_SetTarget(&ghost->target, mobj);
 	P_SetTarget(&ghost->dontdrawforviewmobj, mobj); // Hide the ghost in first-person
@@ -2013,10 +2079,13 @@ mobj_t *P_SpawnGhostMobj(mobj_t *mobj)
 	if (mobj->player && mobj->player->followmobj)
 	{
 		mobj_t *ghost2 = P_SpawnGhostMobj(mobj->player->followmobj);
-		P_SetTarget(&ghost2->tracer, ghost);
-		P_SetTarget(&ghost->tracer, ghost2);
+		if (!P_MobjWasRemoved(ghost2))
+		{
+			P_SetTarget(&ghost2->tracer, ghost);
+			P_SetTarget(&ghost->tracer, ghost2);
 		P_SetTarget(&ghost2->dontdrawforviewmobj, mobj); // Hide the follow-ghost for the non-follow target
-		ghost2->flags2 |= (mobj->player->followmobj->flags2 & MF2_LINKDRAW);
+			ghost2->flags2 |= (mobj->player->followmobj->flags2 & MF2_LINKDRAW);
+		}
 	}
 
 	// Copy interpolation data :)
@@ -2053,7 +2122,19 @@ void P_SpawnThokMobj(player_t *player)
 
 	if (type == MT_GHOST)
 		mobj = P_SpawnGhostMobj(player->mo); // virtually does everything here for us
-	else
+	else if (type == MT_THOKEFFECT) // Thok boom effect for Sonic
+	{
+		mobj = P_SpawnMobjFromMobj(player->mo, 0, 0, FixedDiv(player->mo->height, player->mo->scale)*3/4, type);
+		mobj->angle = player->mo->angle + ANGLE_90;
+		mobj->fuse = 7;
+		mobj->scale = player->mo->scale / 3;
+		mobj->destscale = 10 * player->mo->scale;
+		mobj->colorized = true;
+		mobj->color = player->mo->color;
+		mobj->momx = -player->mo->momx / 2;
+		mobj->momy = -player->mo->momy / 2;
+	}
+	else // Normal thok object handling
 	{
 		if (player->mo->eflags & MFE_VERTICALFLIP)
 			zheight = player->mo->z + player->mo->height + FixedDiv(P_GetPlayerHeight(player) - player->mo->height, 3*FRACUNIT) - FixedMul(mobjinfo[type].height, player->mo->scale);
@@ -2066,6 +2147,8 @@ void P_SpawnThokMobj(player_t *player)
 			zheight = player->mo->ceilingz - FixedMul(mobjinfo[type].height, player->mo->scale);
 
 		mobj = P_SpawnMobj(player->mo->x, player->mo->y, zheight, type);
+		if (P_MobjWasRemoved(mobj))
+			return;
 
 		// set to player's angle, just in case
 		mobj->angle = player->drawangle;
@@ -2089,7 +2172,8 @@ void P_SpawnThokMobj(player_t *player)
 		}
 	}
 
-	P_SetTarget(&mobj->target, player->mo); // the one thing P_SpawnGhostMobj doesn't do
+	if (!P_MobjWasRemoved(mobj))
+		P_SetTarget(&mobj->target, player->mo); // the one thing P_SpawnGhostMobj doesn't do
 	G_GhostAddThok();
 }
 
@@ -2127,6 +2211,8 @@ void P_SpawnSpinMobj(player_t *player, mobjtype_t type)
 			zheight = player->mo->ceilingz - FixedMul(mobjinfo[type].height, player->mo->scale);
 
 		mobj = P_SpawnMobj(player->mo->x, player->mo->y, zheight, type);
+		if (P_MobjWasRemoved(mobj))
+			return;
 
 		// set to player's angle, just in case
 		mobj->angle = player->drawangle;
@@ -2151,7 +2237,8 @@ void P_SpawnSpinMobj(player_t *player, mobjtype_t type)
 		}
 	}
 
-	P_SetTarget(&mobj->target, player->mo); // the one thing P_SpawnGhostMobj doesn't do
+	if (!P_MobjWasRemoved(mobj))
+		P_SetTarget(&mobj->target, player->mo); // the one thing P_SpawnGhostMobj doesn't do
 }
 
 /** Called when \p player finishes the level.
@@ -2215,12 +2302,12 @@ void P_DoPlayerExit(player_t *player)
 	{
 		player->climbing = 0;
 		player->pflags |= P_GetJumpFlags(player);
-		P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
+		P_SetMobjState(player->mo, S_PLAY_JUMP);
 	}
 	else if (player->pflags & PF_STARTDASH)
 	{
 		player->pflags &= ~PF_STARTDASH;
-		P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+		P_SetMobjState(player->mo, S_PLAY_STND);
 	}
 	player->powers[pw_underwater] = 0;
 	player->powers[pw_spacetime] = 0;
@@ -2309,7 +2396,7 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 				if (!(player->pflags & PF_STARTDASH) && player->panim != PA_ROLL && player->panim != PA_ETC
 				&& player->panim != PA_ABILITY && player->panim != PA_ABILITY2)
 				{
-					P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+					P_SetMobjState(player->mo, S_PLAY_ROLL);
 					S_StartSound(player->mo, sfx_spin);
 				}
 			}
@@ -2318,7 +2405,7 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 				if (dorollstuff)
 				{
 					player->skidtime = TICRATE;
-					P_SetPlayerMobjState(player->mo, S_PLAY_GLIDE);
+					P_SetMobjState(player->mo, S_PLAY_GLIDE);
 					P_SpawnSkidDust(player, player->mo->radius, true); // make sure the player knows they landed
 					player->mo->tics = -1;
 				}
@@ -2331,7 +2418,7 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 				if (player->mo->state-states != S_PLAY_GLIDE_LANDING)
 				{
 					P_ResetPlayer(player);
-					P_SetPlayerMobjState(player->mo, S_PLAY_GLIDE_LANDING);
+					P_SetMobjState(player->mo, S_PLAY_GLIDE_LANDING);
 					player->pflags |= PF_STASIS;
 					if (player->speed > FixedMul(player->runspeed, player->mo->scale))
 						player->skidtime += player->mo->tics;
@@ -2352,7 +2439,7 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 				if (player->mo->state-states != S_PLAY_MELEE_LANDING)
 				{
 					mobjtype_t type = player->revitem;
-					P_SetPlayerMobjState(player->mo, S_PLAY_MELEE_LANDING);
+					P_SetMobjState(player->mo, S_PLAY_MELEE_LANDING);
 					player->mo->tics = (player->mo->movefactor == FRACUNIT) ? TICRATE/2 : (FixedDiv(35<<(FRACBITS-1), FixedSqrt(player->mo->movefactor)))>>FRACBITS;
 					S_StartSound(player->mo, sfx_s3k8b);
 					player->pflags |= PF_FULLSTASIS;
@@ -2376,15 +2463,18 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 						while (i < 5)
 						{
 							missile = P_SpawnMobjFromMobj(player->mo, xo, yo, zo, type);
-							P_SetTarget(&missile->target, player->mo);
-							missile->angle = throwang + player->drawangle;
-							P_Thrust(missile, player->drawangle + ANGLE_90,
-								P_ReturnThrustY(missile, throwang, mu)); // side to side component
-							P_Thrust(missile, player->drawangle, mu2); // forward component
-							P_SetObjectMomZ(missile, (4 + ((i&1)<<1))*FRACUNIT, true);
-							missile->momz += player->mo->pmomz;
-							missile->fuse = TICRATE/2;
-							missile->extravalue2 = ev;
+							if (!P_MobjWasRemoved(missile))
+							{
+								P_SetTarget(&missile->target, player->mo);
+								missile->angle = throwang + player->drawangle;
+								P_Thrust(missile, player->drawangle + ANGLE_90,
+									P_ReturnThrustY(missile, throwang, mu)); // side to side component
+								P_Thrust(missile, player->drawangle, mu2); // forward component
+								P_SetObjectMomZ(missile, (4 + ((i&1)<<1))*FRACUNIT, true);
+								missile->momz += player->mo->pmomz;
+								missile->fuse = TICRATE/2;
+								missile->extravalue2 = ev;
+							}
 
 							i++;
 							throwang += ANG30;
@@ -2415,28 +2505,28 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 				if (player->cmomx || player->cmomy)
 				{
 					if (player->charflags & SF_DASHMODE && player->dashmode >= DASHMODE_THRESHOLD && player->panim != PA_DASH)
-						P_SetPlayerMobjState(player->mo, S_PLAY_DASH);
+						P_SetMobjState(player->mo, S_PLAY_DASH);
 					else if (player->speed >= runspd
 					&& (player->panim != PA_RUN || player->mo->state-states == S_PLAY_FLOAT_RUN))
-						P_SetPlayerMobjState(player->mo, S_PLAY_RUN);
+						P_SetMobjState(player->mo, S_PLAY_RUN);
 					else if ((player->rmomx || player->rmomy)
 					&& (player->panim != PA_WALK || player->mo->state-states == S_PLAY_FLOAT))
-						P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
+						P_SetMobjState(player->mo, S_PLAY_WALK);
 					else if (!player->rmomx && !player->rmomy && player->panim != PA_IDLE)
-						P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+						P_SetMobjState(player->mo, S_PLAY_STND);
 				}
 				else
 				{
 					if (player->charflags & SF_DASHMODE && player->dashmode >= DASHMODE_THRESHOLD && player->panim != PA_DASH)
-						P_SetPlayerMobjState(player->mo, S_PLAY_DASH);
+						P_SetMobjState(player->mo, S_PLAY_DASH);
 					else if (player->speed >= runspd
 					&& (player->panim != PA_RUN || player->mo->state-states == S_PLAY_FLOAT_RUN))
-						P_SetPlayerMobjState(player->mo, S_PLAY_RUN);
+						P_SetMobjState(player->mo, S_PLAY_RUN);
 					else if ((player->mo->momx || player->mo->momy)
 					&& (player->panim != PA_WALK || player->mo->state-states == S_PLAY_FLOAT))
-						P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
+						P_SetMobjState(player->mo, S_PLAY_WALK);
 					else if (!player->mo->momx && !player->mo->momy && player->panim != PA_IDLE)
-						P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+						P_SetMobjState(player->mo, S_PLAY_STND);
 				}
 			}
 
@@ -2466,7 +2556,7 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 					? 6*FRACUNIT/5
 					: 5*FRACUNIT/2,
 					false);
-					P_SetPlayerMobjState(player->mo, S_PLAY_FALL);
+					P_SetMobjState(player->mo, S_PLAY_FALL);
 					player->mo->momx = player->mo->momy = 0;
 					clipmomz = false;
 				}
@@ -2774,6 +2864,7 @@ static void P_CheckBouncySectors(player_t *player)
 			if (!(rover->fofflags & FOF_EXISTS))
 				continue; // FOFs should not be bouncy if they don't even "exist"
 
+			// TODO: 2.3: Delete
 			// Handle deprecated bouncy FOF sector type
 			if (!udmf && GETSECSPECIAL(rover->master->frontsector->special, 1) == 15)
 			{
@@ -2938,24 +3029,29 @@ static void P_CheckUnderwaterAndSpaceTimer(player_t *player)
 		: player->mo->z + player->mo->height + FixedMul(8*FRACUNIT, FixedMul(player->mo->scale, player->shieldscale));
 
 		mobj_t *numbermobj = P_SpawnMobj(player->mo->x, player->mo->y, height, MT_DROWNNUMBERS);
-
-		timeleft /= (2*TICRATE); // To be strictly accurate it'd need to be ((timeleft/TICRATE) - 1)/2, but integer division rounds down for us
-
-		if (player->charflags & SF_MACHINE)
+		if (!P_MobjWasRemoved(numbermobj))
 		{
-			S_StartSound(player->mo, sfx_buzz1);
-			timeleft += 6;
-		}
-		else
-			S_StartSound(player->mo, sfx_dwnind);
+			timeleft /= (2*TICRATE); // To be strictly accurate it'd need to be ((timeleft/TICRATE) - 1)/2, but integer division rounds down for us
 
-		if (timeleft) // Don't waste time setting the state if the time is 0.
-			P_SetMobjState(numbermobj, numbermobj->info->spawnstate+timeleft);
+			if (player->charflags & SF_MACHINE)
+			{
+				S_StartSound(player->mo, sfx_buzz1);
+				timeleft += 6;
+			}
+			else
+				S_StartSound(player->mo, sfx_dwnind);
+
+			if (!P_MobjWasRemoved(numbermobj))
+			{
+				if (timeleft) // Don't waste time setting the state if the time is 0.
+					P_SetMobjState(numbermobj, numbermobj->info->spawnstate+timeleft);
 
-		P_SetTarget(&numbermobj->target, player->mo);
-		numbermobj->threshold = 40;
-		numbermobj->destscale = player->mo->scale;
-		P_SetScale(numbermobj, player->mo->scale);
+				P_SetTarget(&numbermobj->target, player->mo);
+				numbermobj->threshold = 40;
+				numbermobj->destscale = player->mo->scale;
+				P_SetScale(numbermobj, player->mo->scale);
+			}
+		}
 	}
 	// Underwater timer runs out
 	else if (timeleft == 1)
@@ -3014,8 +3110,11 @@ static void P_CheckInvincibilityTimer(player_t *player)
 	else if (leveltime % (TICRATE/7) == 0)
 	{
 		mobj_t *sparkle = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_IVSP);
-		sparkle->destscale = player->mo->scale;
-		P_SetScale(sparkle, player->mo->scale);
+		if (!P_MobjWasRemoved(sparkle))
+		{
+			sparkle->destscale = player->mo->scale;
+			P_SetScale(sparkle, player->mo->scale);
+		}
 	}
 
 	// Resume normal music stuff.
@@ -3032,7 +3131,7 @@ static void P_CheckInvincibilityTimer(player_t *player)
 				}
 				else
 				{
-					player->mo->color = player->skincolor;
+					player->mo->color = P_GetPlayerColor(player);
 					G_GhostAddColor(GHC_NORMAL);
 				}
 			}
@@ -3070,7 +3169,8 @@ static void P_DoBubbleBreath(player_t *player)
 			y += (P_RandomRange(r, -r)<<FRACBITS);
 			z += (P_RandomKey(player->mo->height>>FRACBITS)<<FRACBITS);
 			bubble = P_SpawnMobj(x, y, z, MT_WATERZAP);
-			S_StartSound(bubble, sfx_beelec);
+			if (!P_MobjWasRemoved(bubble))
+				S_StartSound(bubble, sfx_beelec);
 		}
 	}
 	else
@@ -3111,15 +3211,21 @@ static void P_DoBubbleBreath(player_t *player)
 			player->mo->x + stirwaterx,
 			player->mo->y + stirwatery,
 			stirwaterz, MT_SMALLBUBBLE);
-		bubble->destscale = player->mo->scale;
-		P_SetScale(bubble,bubble->destscale);
+		if (!P_MobjWasRemoved(bubble))
+		{
+			bubble->destscale = player->mo->scale;
+			P_SetScale(bubble,bubble->destscale);
+		}
 
 		bubble = P_SpawnMobj(
 			player->mo->x - stirwaterx,
 			player->mo->y - stirwatery,
 			stirwaterz, MT_SMALLBUBBLE);
-		bubble->destscale = player->mo->scale;
-		P_SetScale(bubble,bubble->destscale);
+		if (!P_MobjWasRemoved(bubble))
+		{
+			bubble->destscale = player->mo->scale;
+			P_SetScale(bubble,bubble->destscale);
+		}
 	}
 }
 
@@ -3139,22 +3245,25 @@ static void P_DoPlayerHeadSigns(player_t *player)
 		if (player->pflags & PF_TAGIT && (!P_IsLocalPlayer(player) || consoleplayer != displayplayer || splitscreen))
 		{
 			sign = P_SpawnMobjFromMobj(player->mo, 0, 0, 0, MT_TAG);
-			sign->x = player->mo->x;
-			sign->y = player->mo->y;
-			sign->z = player->mo->z;
-			sign->old_x = player->mo->old_x;
-			sign->old_y = player->mo->old_y;
-			sign->old_z = player->mo->old_z;
-
-			if (!(player->mo->eflags & MFE_VERTICALFLIP))
-			{
-				sign->z += player->mo->height;
-				sign->old_z += player->mo->height;
-			}
-			else
+			if (!P_MobjWasRemoved(sign))
 			{
-				sign->z -= mobjinfo[MT_TAG].height;
-				sign->old_z -= mobjinfo[MT_TAG].height;
+				sign->x = player->mo->x;
+				sign->y = player->mo->y;
+				sign->z = player->mo->z;
+				sign->old_x = player->mo->old_x;
+				sign->old_y = player->mo->old_y;
+				sign->old_z = player->mo->old_z;
+
+				if (!(player->mo->eflags & MFE_VERTICALFLIP))
+				{
+					sign->z += player->mo->height;
+					sign->old_z += player->mo->height;
+				}
+				else
+				{
+					sign->z -= mobjinfo[MT_TAG].height;
+					sign->old_z -= mobjinfo[MT_TAG].height;
+				}
 			}
 		}
 	}
@@ -3178,24 +3287,26 @@ static void P_DoPlayerHeadSigns(player_t *player)
 			}
 
 			sign = P_SpawnMobjFromMobj(player->mo, 0, 0, 0, MT_GOTFLAG);
-			sign->x = player->mo->x;
-			sign->y = player->mo->y;
-			sign->z = player->mo->z + zofs;
-			sign->old_x = player->mo->old_x;
-			sign->old_y = player->mo->old_y;
-			sign->old_z = player->mo->old_z + zofs;
-
-			if (player_is_flipped)
+			if (!P_MobjWasRemoved(sign))
 			{
-				sign->eflags |= MFE_VERTICALFLIP;
-			}
+				sign->x = player->mo->x;
+				sign->y = player->mo->y;
+				sign->z = player->mo->z + zofs;
+				sign->old_x = player->mo->old_x;
+				sign->old_y = player->mo->old_y;
+				sign->old_z = player->mo->old_z + zofs;
+
+				if (player_is_flipped)
+				{
+					sign->eflags |= MFE_VERTICALFLIP;
+				}
 
-			if (player->gotflag & GF_REDFLAG)
-				sign->frame = 1|FF_FULLBRIGHT;
-			else //if (player->gotflag & GF_BLUEFLAG)
-				sign->frame = 2|FF_FULLBRIGHT;
+				if (player->gotflag & GF_REDFLAG)
+					sign->frame = 1|FF_FULLBRIGHT;
+				else //if (player->gotflag & GF_BLUEFLAG)
+					sign->frame = 2|FF_FULLBRIGHT;
+			}
 		}
-	}
 
 	if (!P_MobjWasRemoved(sign) && splitscreen) // Hide the sign from yourself in splitscreen - In single-screen, it wouldn't get spawned if it shouldn't be visible
 	{
@@ -3233,6 +3344,7 @@ static void P_DoPlayerHeadSigns(player_t *player)
 		}
 #endif
 	}
+	}
 }
 
 //
@@ -3597,9 +3709,9 @@ static void P_DoClimbing(player_t *player)
 
 		if (player->climbing && climb && (player->mo->momx || player->mo->momy || player->mo->momz)
 			&& player->mo->state-states != S_PLAY_CLIMB)
-			P_SetPlayerMobjState(player->mo, S_PLAY_CLIMB);
+			P_SetMobjState(player->mo, S_PLAY_CLIMB);
 		else if ((!(player->mo->momx || player->mo->momy || player->mo->momz) || !climb) && player->mo->state-states != S_PLAY_CLING)
-			P_SetPlayerMobjState(player->mo, S_PLAY_CLING);
+			P_SetMobjState(player->mo, S_PLAY_CLING);
 
 		if (!floorclimb)
 		{
@@ -3614,14 +3726,14 @@ static void P_DoClimbing(player_t *player)
 
 			player->climbing = 0;
 			player->pflags |= P_GetJumpFlags(player);
-			P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
+			P_SetMobjState(player->mo, S_PLAY_JUMP);
 		}
 
 		if (skyclimber)
 		{
 			player->climbing = 0;
 			player->pflags |= P_GetJumpFlags(player);
-			P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
+			P_SetMobjState(player->mo, S_PLAY_JUMP);
 		}
 	}
 
@@ -3632,15 +3744,15 @@ static void P_DoClimbing(player_t *player)
 
 	if (player->climbing && climb && (player->mo->momx || player->mo->momy || player->mo->momz)
 		&& player->mo->state-states != S_PLAY_CLIMB)
-		P_SetPlayerMobjState(player->mo, S_PLAY_CLIMB);
+		P_SetMobjState(player->mo, S_PLAY_CLIMB);
 	else if ((!(player->mo->momx || player->mo->momy || player->mo->momz) || !climb) && player->mo->state-states != S_PLAY_CLING)
-		P_SetPlayerMobjState(player->mo, S_PLAY_CLING);
+		P_SetMobjState(player->mo, S_PLAY_CLING);
 
 	if (cmd->buttons & BT_SPIN && !(player->pflags & PF_JUMPSTASIS))
 	{
 		player->climbing = 0;
 		player->pflags |= P_GetJumpFlags(player);
-		P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
+		P_SetMobjState(player->mo, S_PLAY_JUMP);
 		P_SetObjectMomZ(player->mo, 4*FRACUNIT, false);
 		P_Thrust(player->mo, player->mo->angle, FixedMul(-4*FRACUNIT, player->mo->scale));
 	}
@@ -3656,12 +3768,12 @@ static void P_DoClimbing(player_t *player)
 	}
 
 	if (player->climbing == 0)
-		P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
+		P_SetMobjState(player->mo, S_PLAY_JUMP);
 
 	if (player->climbing && P_IsObjectOnGround(player->mo))
 	{
 		P_ResetPlayer(player);
-		P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+		P_SetMobjState(player->mo, S_PLAY_STND);
 	}
 }
 
@@ -4023,10 +4135,10 @@ teeterdone:
 	if (teeter)
 	{
 		if (player->panim == PA_IDLE)
-			P_SetPlayerMobjState(player->mo, S_PLAY_EDGE);
+			P_SetMobjState(player->mo, S_PLAY_EDGE);
 	}
 	else if (player->panim == PA_EDGE)
-		P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+		P_SetMobjState(player->mo, S_PLAY_STND);
 }
 
 //
@@ -4086,6 +4198,16 @@ static void P_DoFiring(player_t *player, ticcmd_t *cmd)
 
 	I_Assert(player != NULL);
 	I_Assert(!P_MobjWasRemoved(player->mo));
+	
+	// Toss a flag
+	if (cmd->buttons & BT_TOSSFLAG && G_GametypeHasTeams()
+		&& !(player->powers[pw_super]) && !(player->tossdelay))
+	{
+		if (!(player->gotflag & (GF_REDFLAG|GF_BLUEFLAG)))
+			P_PlayerEmeraldBurst(player, true); // Toss emeralds
+		else
+			P_PlayerFlagBurst(player, true);
+	}
 
 	if (!(cmd->buttons & (BT_ATTACK|BT_FIRENORMAL)))
 	{
@@ -4095,9 +4217,10 @@ static void P_DoFiring(player_t *player, ticcmd_t *cmd)
 		return;
 	}
 
-	if (player->pflags & PF_ATTACKDOWN || player->climbing || (G_TagGametype() && !(player->pflags & PF_TAGIT)))
+	if (player->pflags & PF_ATTACKDOWN || player->climbing)
 		return;
 
+	// Fire a fireball if we have the Fire Flower powerup!
 	if (((player->powers[pw_shield] & SH_STACK) == SH_FIREFLOWER) && !(player->weapondelay))
 	{
 		player->pflags |= PF_ATTACKDOWN;
@@ -4109,7 +4232,8 @@ static void P_DoFiring(player_t *player, ticcmd_t *cmd)
 		return;
 	}
 
-	if (!G_RingSlingerGametype() || player->weapondelay)
+	// No ringslinging outside of ringslinger!
+	if (!G_RingSlingerGametype() || player->weapondelay || (G_TagGametype() && !(player->pflags & PF_TAGIT)))
 		return;
 
 	player->pflags |= PF_ATTACKDOWN;
@@ -4287,34 +4411,7 @@ static void P_DoSuperStuff(player_t *player)
 		// If you're super and not Sonic, de-superize!
 		if (!(ALL7EMERALDS(emeralds) && player->charflags & SF_SUPER))
 		{
-			player->powers[pw_super] = 0;
-			P_SetPlayerMobjState(player->mo, S_PLAY_STND);
-			if (P_IsLocalPlayer(player))
-			{
-				music_stack_noposition = true; // HACK: Do not reposition next music
-				music_stack_fadeout = MUSICRATE/2; // HACK: Fade out current music
-			}
-			P_RestoreMusic(player);
-			P_SpawnShieldOrb(player);
-
-			// Restore color
-			if ((player->powers[pw_shield] & SH_STACK) == SH_FIREFLOWER)
-			{
-				player->mo->color = SKINCOLOR_WHITE;
-				G_GhostAddColor(GHC_FIREFLOWER);
-			}
-			else
-			{
-				player->mo->color = player->skincolor;
-				G_GhostAddColor(GHC_NORMAL);
-			}
-
-			if (!G_CoopGametype())
-			{
-				HU_SetCEchoFlags(0);
-				HU_SetCEchoDuration(5);
-				HU_DoCEcho(va("%s\\is no longer super.\\\\\\\\", player_names[player-players]));
-			}
+			P_DoSuperDetransformation(player);
 			return;
 		}
 
@@ -4335,75 +4432,46 @@ static void P_DoSuperStuff(player_t *player)
 		&& !(leveltime % TICRATE) && (player->mo->momx || player->mo->momy))
 		{
 			spark = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_SUPERSPARK);
-			spark->destscale = player->mo->scale;
-			P_SetScale(spark, player->mo->scale);
+			if (!P_MobjWasRemoved(spark))
+			{
+				spark->destscale = player->mo->scale;
+				P_SetScale(spark, player->mo->scale);
+			}
 		}
 
 		// Ran out of rings while super!
 		if (player->rings <= 0 || player->exiting)
-		{
-			player->powers[pw_emeralds] = 0; // lost the power stones
-			P_SpawnGhostMobj(player->mo);
-
-			player->powers[pw_super] = 0;
-
-			// Restore color
-			if ((player->powers[pw_shield] & SH_STACK) == SH_FIREFLOWER)
-			{
-				player->mo->color = SKINCOLOR_WHITE;
-				G_GhostAddColor(GHC_FIREFLOWER);
-			}
-			else
-			{
-				player->mo->color = player->skincolor;
-				G_GhostAddColor(GHC_NORMAL);
-			}
-
-			if (!G_CoopGametype())
-				player->powers[pw_flashing] = flashingtics-1;
-
-			if (player->mo->sprite2 & FF_SPR2SUPER)
-				P_SetPlayerMobjState(player->mo, player->mo->state-states);
-
-			// Inform the netgame that the champion has fallen in the heat of battle.
-			if (!G_CoopGametype())
-			{
-				S_StartSound(NULL, sfx_s3k66); //let all players hear it.
-				HU_SetCEchoFlags(0);
-				HU_SetCEchoDuration(5);
-				HU_DoCEcho(va("%s\\is no longer super.\\\\\\\\", player_names[player-players]));
-			}
-
-			// Resume normal music if you're the console player
-			if (P_IsLocalPlayer(player))
-			{
-				music_stack_noposition = true; // HACK: Do not reposition next music
-				music_stack_fadeout = MUSICRATE/2; // HACK: Fade out current music
-			}
-			P_RestoreMusic(player);
-
-			// If you had a shield, restore its visual significance.
-			P_SpawnShieldOrb(player);
-		}
+			P_DoSuperDetransformation(player);
 	}
 }
 
 //
 // P_SuperReady
 //
-// Returns true if player is ready to turn super, duh
+// Returns true if player is ready to transform or detransform
 //
-boolean P_SuperReady(player_t *player)
+boolean P_SuperReady(player_t *player, boolean transform)
 {
-	if (!player->powers[pw_super]
-	&& !player->powers[pw_invulnerability]
+	if (!transform &&
+	(player->powers[pw_super] < TICRATE*3/2
+	|| !G_CoopGametype())) // No turning back in competitive!
+		return false;
+	else if (transform
+	&& (player->powers[pw_super]
+	|| !ALL7EMERALDS(emeralds)
+	|| !(player->rings >= 50)))
+		return false;
+	
+	if (player->mo
 	&& !player->powers[pw_tailsfly]
+	&& !player->powers[pw_carry]
 	&& (player->charflags & SF_SUPER)
-	&& (player->pflags & PF_JUMPED)
-	&& !(player->powers[pw_shield] & SH_NOSTACK)
-	&& !(maptol & TOL_NIGHTS)
-	&& ALL7EMERALDS(emeralds)
-	&& (player->rings >= 50))
+	&& !P_PlayerInPain(player)
+	&& !player->climbing
+	&& !(player->pflags & (PF_FULLSTASIS|PF_THOKKED|PF_STARTDASH|PF_GLIDING|PF_SLIDING|PF_SHIELDABILITY))
+	&& ((player->pflags & PF_JUMPED) || (P_IsObjectOnGround(player->mo) && (player->panim == PA_IDLE || player->panim == PA_EDGE
+	|| player->panim == PA_WALK || player->panim == PA_RUN || (player->charflags & SF_DASHMODE && player->panim == PA_DASH))))
+	&& !(maptol & TOL_NIGHTS))
 		return true;
 
 	return false;
@@ -4598,17 +4666,19 @@ void P_DoJump(player_t *player, boolean soundandstate)
 		if (!player->spectator)
 			S_StartSound(player->mo, sfx_jump); // Play jump sound!
 
-		P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
+		P_SetMobjState(player->mo, S_PLAY_JUMP);
 	}
 }
 
-static void P_DoSpinDashDust(player_t *player)
+void P_DoSpinDashDust(player_t *player)
 {
 	UINT32 i;
 	mobj_t *particle;
 	INT32 prandom[3];
 	for (i = 0; i <= (leveltime%7)/2; i++) { // 1, 2, 3 or 4 particles
 		particle = P_SpawnMobjFromMobj(player->mo, 0, 0, 0, MT_SPINDUST);
+		if (P_MobjWasRemoved(particle))
+			return;
 
 		if (player->mo->eflags & (MFE_TOUCHWATER|MFE_UNDERWATER)) // overrides fire version
 			P_SetMobjState(particle, S_SPINDUST_BUBBLE1);
@@ -4668,7 +4738,7 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 					player->mo->momy = player->cmomy;
 					player->pflags |= (PF_SPINDOWN|PF_STARTDASH|PF_SPINNING);
 					player->dashspeed = player->mindash;
-					P_SetPlayerMobjState(player->mo, S_PLAY_SPINDASH);
+					P_SetMobjState(player->mo, S_PLAY_SPINDASH);
 					if (!player->spectator)
 						S_StartSound(player->mo, sfx_spndsh); // Make the rev sound!
 				}
@@ -4678,7 +4748,7 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 					if (player->speed > 5*player->mo->scale)
 					{
 						player->pflags &= ~PF_STARTDASH;
-						P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+						P_SetMobjState(player->mo, S_PLAY_ROLL);
 						S_StartSound(player->mo, sfx_spin);
 						break;
 					}
@@ -4711,7 +4781,7 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 						|| !canstand) && !(player->pflags & (PF_SPINDOWN|PF_SPINNING)))
 				{
 					player->pflags |= (PF_SPINDOWN|PF_SPINNING);
-					P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+					P_SetMobjState(player->mo, S_PLAY_ROLL);
 					if (!player->spectator)
 						S_StartSound(player->mo, sfx_spin);
 				}
@@ -4727,12 +4797,12 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 					{
 						if (player->dashspeed)
 						{
-							P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+							P_SetMobjState(player->mo, S_PLAY_ROLL);
 							P_InstaThrust(player->mo, player->mo->angle, (player->speed = FixedMul(player->dashspeed, player->mo->scale))); // catapult forward ho!!
 						}
 						else
 						{
-							P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+							P_SetMobjState(player->mo, S_PLAY_STND);
 							player->pflags &= ~PF_SPINNING;
 						}
 
@@ -4756,7 +4826,8 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 							if (P_IsLocalPlayer(player)) // Only display it on your own view. Don't display it for spectators
 							{
 								mobj_t *visual = P_SpawnMobj(lockon->x, lockon->y, lockon->z, MT_LOCKON); // positioning, flip handled in P_SceneryThinker
-								P_SetTarget(&visual->target, lockon);
+								if (!P_MobjWasRemoved(visual))
+									P_SetTarget(&visual->target, lockon);
 								visual->drawonlyforplayer = player; // Hide it from the other player in splitscreen, and yourself when spectating
 							}
 						}
@@ -4764,7 +4835,7 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 						{
 							mobj_t *bullet;
 
-							P_SetPlayerMobjState(player->mo, S_PLAY_FIRE);
+							P_SetMobjState(player->mo, S_PLAY_FIRE);
 
 #define zpos(posmo) (posmo->z + (posmo->height - mobjinfo[player->revitem].height)/2)
 							if (lockon)
@@ -4808,7 +4879,7 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 						P_DoJump(player, false);
 						player->pflags &= ~PF_STARTJUMP;
 						player->mo->momz = FixedMul(player->mo->momz, 3*FRACUNIT/2); // NOT 1.5 times the jump height, but 2.25 times.
-						P_SetPlayerMobjState(player->mo, S_PLAY_TWINSPIN);
+						P_SetMobjState(player->mo, S_PLAY_TWINSPIN);
 						S_StartSound(player->mo, sfx_s3k8b);
 					}
 					else
@@ -4834,7 +4905,7 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 						}
 						player->mo->momx += player->cmomx;
 						player->mo->momy += player->cmomy;
-						P_SetPlayerMobjState(player->mo, S_PLAY_MELEE);
+						P_SetMobjState(player->mo, S_PLAY_MELEE);
 						player->powers[pw_strong] = STR_MELEE;
 						S_StartSound(player->mo, sfx_s3k42);
 					}
@@ -4858,7 +4929,7 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 		{
 			player->skidtime = 0;
 			player->pflags &= ~PF_SPINNING;
-			P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+			P_SetMobjState(player->mo, S_PLAY_STND);
 			player->mo->momx = player->cmomx;
 			player->mo->momy = player->cmomy;
 		}
@@ -4899,21 +4970,24 @@ void P_DoJumpShield(player_t *player)
 		for (i = 0; i < numangles; i++)
 		{
 			spark = P_SpawnMobjFromMobj(player->mo, 0, 0, 0, MT_THUNDERCOIN_SPARK);
-			P_InstaThrust(spark, travelangle + i*(ANGLE_MAX/numangles), FixedMul(4*FRACUNIT, spark->scale));
-			if (i % 2)
-				P_SetObjectMomZ(spark, -4*FRACUNIT, false);
-			spark->fuse = 18;
+			if (!P_MobjWasRemoved(spark))
+			{
+				P_InstaThrust(spark, travelangle + i*(ANGLE_MAX/numangles), FixedMul(4*FRACUNIT, spark->scale));
+				if (i % 2)
+					P_SetObjectMomZ(spark, -4*FRACUNIT, false);
+				spark->fuse = 18;
+			}
 		}
 #undef limitangle
 #undef numangles
 		player->pflags &= ~PF_NOJUMPDAMAGE;
-		P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+		P_SetMobjState(player->mo, S_PLAY_ROLL);
 		S_StartSound(player->mo, sfx_s3k45);
 	}
 	else
 	{
 		player->pflags &= ~(PF_JUMPED|PF_NOJUMPDAMAGE);
-		P_SetPlayerMobjState(player->mo, S_PLAY_FALL);
+		P_SetMobjState(player->mo, S_PLAY_FALL);
 		S_StartSound(player->mo, sfx_wdjump);
 	}
 }
@@ -4930,9 +5004,9 @@ void P_DoBubbleBounce(player_t *player)
 	P_MobjCheckWater(player->mo);
 	P_DoJump(player, false);
 	if (player->charflags & SF_NOJUMPSPIN)
-		P_SetPlayerMobjState(player->mo, S_PLAY_FALL);
+		P_SetMobjState(player->mo, S_PLAY_FALL);
 	else
-		P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+		P_SetMobjState(player->mo, S_PLAY_ROLL);
 	player->pflags |= PF_THOKKED;
 	player->pflags &= ~PF_STARTJUMP;
 	player->secondjump = UINT8_MAX;
@@ -4969,7 +5043,7 @@ void P_DoAbilityBounce(player_t *player, boolean changemomz)
 	}
 
 	S_StartSound(player->mo, sfx_boingf);
-	P_SetPlayerMobjState(player->mo, S_PLAY_BOUNCE_LANDING);
+	P_SetMobjState(player->mo, S_PLAY_BOUNCE_LANDING);
 	player->pflags |= PF_BOUNCING|PF_THOKKED;
 }
 
@@ -5009,14 +5083,17 @@ void P_TwinSpinRejuvenate(player_t *player, mobjtype_t type)
 			yo,
 			player->mo->height/2 + zo,
 			type);
-		P_SetTarget(&missile->target, player->mo);
-		P_SetScale(missile, (missile->destscale >>= 1));
-		missile->angle = ang + movang;
-		missile->fuse = TICRATE/2;
-		missile->extravalue2 = (99*FRACUNIT)/100;
-		missile->momx = xo;
-		missile->momy = yo;
-		missile->momz = zo;
+		if (!P_MobjWasRemoved(missile))
+		{
+			P_SetTarget(&missile->target, player->mo);
+			P_SetScale(missile, (missile->destscale >>= 1));
+			missile->angle = ang + movang;
+			missile->fuse = TICRATE/2;
+			missile->extravalue2 = (99*FRACUNIT)/100;
+			missile->momx = xo;
+			missile->momy = yo;
+			missile->momz = zo;
+		}
 
 		ang += ANGLE_45;
 	}
@@ -5082,7 +5159,7 @@ static void P_DoTwinSpin(player_t *player)
 	player->pflags |= P_GetJumpFlags(player) | PF_THOKKED;
 	S_StartSound(player->mo, sfx_s3k42);
 	player->mo->frame = 0;
-	P_SetPlayerMobjState(player->mo, S_PLAY_TWINSPIN);
+	P_SetMobjState(player->mo, S_PLAY_TWINSPIN);
 	player->powers[pw_strong] = STR_TWINSPIN;
 }
 
@@ -5094,7 +5171,7 @@ static boolean P_PlayerShieldThink(player_t *player, ticcmd_t *cmd, mobj_t *lock
 {
 	mobj_t *lockonshield = NULL;
 
-	if ((player->powers[pw_shield] & SH_NOSTACK) && !player->powers[pw_super] && !(player->pflags & PF_SPINDOWN)
+	if ((player->powers[pw_shield] & SH_NOSTACK) && !player->powers[pw_super] && !(player->pflags & PF_SHIELDDOWN)
 		&& ((!(player->pflags & PF_THOKKED) || (((player->powers[pw_shield] & SH_NOSTACK) == SH_BUBBLEWRAP || (player->powers[pw_shield] & SH_NOSTACK) == SH_ATTRACT) && player->secondjump == UINT8_MAX) ))) // thokked is optional if you're bubblewrapped / 3dblasted
 	{
 		if ((player->powers[pw_shield] & SH_NOSTACK) == SH_ATTRACT && !(player->charflags & SF_NOSHIELDABILITY))
@@ -5114,14 +5191,17 @@ static boolean P_PlayerShieldThink(player_t *player, ticcmd_t *cmd, mobj_t *lock
 					if (dovis)
 					{
 						visual = P_SpawnMobj(lockonshield->x, lockonshield->y, lockonshield->z, MT_LOCKON); // positioning, flip handled in P_SceneryThinker
-						P_SetTarget(&visual->target, lockonshield);
+						if (!P_MobjWasRemoved(visual))
+						{
+							P_SetTarget(&visual->target, lockonshield);
 						visual->drawonlyforplayer = player; // Hide it from the other player in splitscreen, and yourself when spectating
-						P_SetMobjStateNF(visual, visual->info->spawnstate+1);
+							P_SetMobjStateNF(visual, visual->info->spawnstate+1);
+						}
 					}
 				}
 			}
 		}
-		if ((!(player->charflags & SF_NOSHIELDABILITY)) && (cmd->buttons & BT_SPIN && !LUA_HookPlayer(player, HOOK(ShieldSpecial)))) // Spin button effects
+		if ((!(player->charflags & SF_NOSHIELDABILITY)) && (cmd->buttons & BT_SHIELD && !LUA_HookPlayer(player, HOOK(ShieldSpecial)))) // Shield button effects
 		{
 			// Force stop
 			if ((player->powers[pw_shield] & ~(SH_FORCEHP|SH_STACK)) == SH_FORCE)
@@ -5156,7 +5236,7 @@ static boolean P_PlayerShieldThink(player_t *player, ticcmd_t *cmd, mobj_t *lock
 							{
 								player->mo->angle = R_PointToAngle2(player->mo->x, player->mo->y, lockonshield->x, lockonshield->y);
 									player->pflags &= ~PF_NOJUMPDAMAGE;
-									P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+									P_SetMobjState(player->mo, S_PLAY_ROLL);
 									S_StartSound(player->mo, sfx_s3k40);
 									player->homing = 3*TICRATE;
 							}
@@ -5180,7 +5260,7 @@ static boolean P_PlayerShieldThink(player_t *player, ticcmd_t *cmd, mobj_t *lock
 									player->mo->momx -= (player->mo->momx/3);
 									player->mo->momy -= (player->mo->momy/3);
 									player->pflags &= ~PF_NOJUMPDAMAGE;
-									P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+									P_SetMobjState(player->mo, S_PLAY_ROLL);
 									S_StartSound(player->mo, sfx_s3k44);
 								}
 								player->secondjump = 0;
@@ -5193,7 +5273,7 @@ static boolean P_PlayerShieldThink(player_t *player, ticcmd_t *cmd, mobj_t *lock
 							P_Thrust(player->mo, player->mo->angle, FixedMul(30*FRACUNIT - FixedSqrt(FixedDiv(player->speed, player->mo->scale)), player->mo->scale));
 							player->drawangle = player->mo->angle;
 							player->pflags &= ~(PF_NOJUMPDAMAGE|PF_SPINNING);
-							P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+							P_SetMobjState(player->mo, S_PLAY_ROLL);
 							S_StartSound(player->mo, sfx_s3k43);
 						default:
 							break;
@@ -5222,7 +5302,8 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 		if (P_IsLocalPlayer(player)) // Only display it on your own view. Don't display it for spectators
 		{
 			visual = P_SpawnMobj(lockonthok->x, lockonthok->y, lockonthok->z, MT_LOCKON); // positioning, flip handled in P_SceneryThinker
-			P_SetTarget(&visual->target, lockonthok);
+			if (!P_MobjWasRemoved(visual))
+				P_SetTarget(&visual->target, lockonthok);
 			visual->drawonlyforplayer = player; // Hide it from the other player in splitscreen, and yourself when spectating
 		}
 	}
@@ -5242,52 +5323,45 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 			;
 		else if (P_PlayerShieldThink(player, cmd, lockonthok, visual))
 			;
-		else if ((cmd->buttons & BT_SPIN))
+		else if ((cmd->buttons & BT_SPIN) && !LUA_HookPlayer(player, HOOK(JumpSpinSpecial)))
 		{
-			if (!(player->pflags & PF_SPINDOWN) && P_SuperReady(player))
+			switch (player->charability)
 			{
-				// If you can turn super and aren't already,
-				// and you don't have a shield, do it!
-				P_DoSuperTransformation(player, false);
-			}
-			else if (!LUA_HookPlayer(player, HOOK(JumpSpinSpecial)))
-				switch (player->charability)
-				{
-					case CA_THOK:
-						if (player->powers[pw_super]) // Super Sonic float
+				case CA_THOK:
+					if (player->powers[pw_super]) // Super Sonic float
+					{
+						if ((player->speed > 5*player->mo->scale) // FixedMul(5<<FRACBITS, player->mo->scale), but scale is FRACUNIT-based
+						&& (P_MobjFlip(player->mo)*player->mo->momz <= 0))
 						{
-							if ((player->speed > 5*player->mo->scale) // FixedMul(5<<FRACBITS, player->mo->scale), but scale is FRACUNIT-based
-							&& (P_MobjFlip(player->mo)*player->mo->momz <= 0))
+							if (player->panim != PA_RUN && player->panim != PA_WALK)
 							{
-								if (player->panim != PA_RUN && player->panim != PA_WALK)
-								{
-									if (player->speed >= FixedMul(player->runspeed, player->mo->scale))
-										P_SetPlayerMobjState(player->mo, S_PLAY_FLOAT_RUN);
-									else
-										P_SetPlayerMobjState(player->mo, S_PLAY_FLOAT);
-								}
-
-								player->mo->momz = 0;
-								player->pflags &= ~(PF_STARTJUMP|PF_SPINNING);
-								player->secondjump = 1;
+								if (player->speed >= FixedMul(player->runspeed, player->mo->scale))
+									P_SetMobjState(player->mo, S_PLAY_FLOAT_RUN);
+								else
+									P_SetMobjState(player->mo, S_PLAY_FLOAT);
 							}
+
+							player->mo->momz = 0;
+							player->pflags &= ~(PF_STARTJUMP|PF_SPINNING);
+							player->secondjump = 1;
 						}
-						break;
-					case CA_TELEKINESIS:
-						if (!(player->pflags & (PF_THOKKED|PF_SPINDOWN)) || (player->charflags & SF_MULTIABILITY))
-						{
-							P_Telekinesis(player,
-								-FixedMul(player->actionspd, player->mo->scale), // -ve thrust (pulling towards player)
-								FixedMul(384*FRACUNIT, player->mo->scale));
-						}
-						break;
-					case CA_TWINSPIN:
-						if ((player->charability2 == CA2_MELEE) && (!(player->pflags & (PF_THOKKED|PF_SPINDOWN)) || player->charflags & SF_MULTIABILITY))
-							P_DoTwinSpin(player);
-						break;
-					default:
-						break;
-				}
+					}
+					break;
+				case CA_TELEKINESIS:
+					if (!(player->pflags & (PF_THOKKED|PF_SPINDOWN)) || (player->charflags & SF_MULTIABILITY))
+					{
+						P_Telekinesis(player,
+							-FixedMul(player->actionspd, player->mo->scale), // -ve thrust (pulling towards player)
+							FixedMul(384*FRACUNIT, player->mo->scale));
+					}
+					break;
+				case CA_TWINSPIN:
+					if ((player->charability2 == CA2_MELEE) && (!(player->pflags & (PF_THOKKED|PF_SPINDOWN)) || player->charflags & SF_MULTIABILITY))
+						P_DoTwinSpin(player);
+					break;
+				default:
+					break;
+			}
 		}
 	}
 
@@ -5350,12 +5424,6 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 		}
 		else if (player->pflags & PF_SLIDING || ((gametyperules & GTR_TEAMFLAGS) && player->gotflag) || player->pflags & PF_SHIELDABILITY)
 			;
-		/*else if (P_SuperReady(player))
-		{
-			// If you can turn super and aren't already,
-			// and you don't have a shield, do it!
-			P_DoSuperTransformation(player, false);
-		}*/
 		else if (player->pflags & PF_JUMPED)
 		{
 			if (!LUA_HookPlayer(player, HOOK(AbilitySpecial)))
@@ -5401,13 +5469,13 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 							P_SetTarget(&player->mo->target, P_SetTarget(&player->mo->tracer, lockonthok));
 							if (lockonthok)
 							{
-								P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+								P_SetMobjState(player->mo, S_PLAY_ROLL);
 								player->mo->angle = R_PointToAngle2(player->mo->x, player->mo->y, lockonthok->x, lockonthok->y);
 								player->homing = 3*TICRATE;
 							}
 							else
 							{
-								P_SetPlayerMobjState(player->mo, S_PLAY_FALL);
+								P_SetMobjState(player->mo, S_PLAY_FALL);
 								player->pflags &= ~PF_JUMPED;
 								player->mo->height = P_GetPlayerHeight(player);
 							}
@@ -5444,7 +5512,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 						; // Can't do anything if you're a fish out of water!
 					else if (!(player->pflags & PF_THOKKED) && !(player->powers[pw_tailsfly]))
 					{
-						P_SetPlayerMobjState(player->mo, S_PLAY_FLY); // Change to the flying animation
+						P_SetMobjState(player->mo, S_PLAY_FLY); // Change to the flying animation
 
 						player->powers[pw_tailsfly] = tailsflytics + 1; // Set the fly timer
 
@@ -5477,7 +5545,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 						player->pflags |= PF_GLIDING|PF_THOKKED;
 						player->glidetime = 0;
 
-						P_SetPlayerMobjState(player->mo, S_PLAY_GLIDE);
+						P_SetMobjState(player->mo, S_PLAY_GLIDE);
 						if (playerspeed < glidespeed)
 							P_Thrust(player->mo, player->mo->angle, glidespeed - playerspeed);
 						player->pflags &= ~(PF_JUMPED|PF_SPINNING|PF_STARTDASH);
@@ -5498,11 +5566,11 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 					if (!(player->pflags & PF_THOKKED) || player->charflags & SF_MULTIABILITY)
 					{
 						if (player->charflags & SF_DASHMODE && player->dashmode >= DASHMODE_THRESHOLD)
-							P_SetPlayerMobjState(player->mo, S_PLAY_DASH);
+							P_SetMobjState(player->mo, S_PLAY_DASH);
 						else if (player->speed >= FixedMul(player->runspeed, player->mo->scale))
-							P_SetPlayerMobjState(player->mo, S_PLAY_FLOAT_RUN);
+							P_SetMobjState(player->mo, S_PLAY_FLOAT_RUN);
 						else
-							P_SetPlayerMobjState(player->mo, S_PLAY_FLOAT);
+							P_SetMobjState(player->mo, S_PLAY_FLOAT);
 						player->pflags |= PF_THOKKED;
 						player->pflags &= ~(PF_JUMPED|PF_NOJUMPDAMAGE|PF_SPINNING);
 						player->secondjump = 1;
@@ -5538,7 +5606,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 				case CA_BOUNCE:
 					if (!(player->pflags & PF_THOKKED) || player->charflags & SF_MULTIABILITY)
 					{
-						P_SetPlayerMobjState(player->mo, S_PLAY_BOUNCE);
+						P_SetMobjState(player->mo, S_PLAY_BOUNCE);
 						player->pflags &= ~(PF_JUMPED|PF_NOJUMPDAMAGE|PF_SPINNING);
 						player->pflags |= PF_THOKKED|PF_BOUNCING;
 						player->powers[pw_strong] = STR_BOUNCE;
@@ -5628,7 +5696,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 				if (!(player->mo->tracer->flags & MF_BOSS))
 					player->pflags &= ~PF_THOKKED;
 
-				P_SetPlayerMobjState(player->mo, S_PLAY_SPRING);
+				P_SetMobjState(player->mo, S_PLAY_SPRING);
 				player->pflags |= PF_NOJUMPDAMAGE;
 			}
 		}
@@ -5676,7 +5744,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 			else
 				player->secondjump = 2;
 
-			P_SetPlayerMobjState(player->mo, S_PLAY_FALL);
+			P_SetMobjState(player->mo, S_PLAY_FALL);
 		}
 
 		// If letting go of the jump button while still on ascent, cut the jump height.
@@ -5796,7 +5864,7 @@ static void P_2dMovement(player_t *player)
 			else if (player->exiting)
 			{
 				player->pflags &= ~PF_GLIDING;
-				P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
+				P_SetMobjState(player->mo, S_PLAY_WALK);
 				player->skidtime = 0;
 			}
 		}
@@ -5805,7 +5873,7 @@ static void P_2dMovement(player_t *player)
 		if (player->pflags & PF_SPINNING && !player->exiting)
 		{
 			player->pflags &= ~PF_SPINNING;
-			P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+			P_SetMobjState(player->mo, S_PLAY_STND);
 		}
 	}
 
@@ -5969,7 +6037,7 @@ static void P_3dMovement(player_t *player)
 			else if (player->exiting)
 			{
 				player->pflags &= ~PF_GLIDING;
-				P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
+				P_SetMobjState(player->mo, S_PLAY_WALK);
 				player->skidtime = 0;
 			}
 		}
@@ -5978,7 +6046,7 @@ static void P_3dMovement(player_t *player)
 		if (player->pflags & PF_SPINNING && !player->exiting)
 		{
 			player->pflags &= ~PF_SPINNING;
-			P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+			P_SetMobjState(player->mo, S_PLAY_STND);
 		}
 	}
 
@@ -6020,7 +6088,7 @@ static void P_3dMovement(player_t *player)
 	if (player->pflags & PF_SLIDING)
 		cmd->forwardmove = 0;
 	else if (onground && player->mo->state == states+S_PLAY_PAIN)
-		P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
+		P_SetMobjState(player->mo, S_PLAY_WALK);
 
 	player->aiming = cmd->aiming<<FRACBITS;
 
@@ -6825,12 +6893,12 @@ static void P_DoNiGHTSCapsule(player_t *player)
 		if (player->mo->momx || player->mo->momy || player->mo->momz)
 		{
 			if (player->mo->state != &states[S_PLAY_NIGHTS_PULL])
-				P_SetPlayerMobjState(player->mo, S_PLAY_NIGHTS_PULL);
+				P_SetMobjState(player->mo, S_PLAY_NIGHTS_PULL);
 		}
 		else if (player->mo->state != &states[S_PLAY_NIGHTS_ATTACK])
 		{
 			S_StartSound(player->mo, sfx_spin);
-			P_SetPlayerMobjState(player->mo, S_PLAY_NIGHTS_ATTACK);
+			P_SetMobjState(player->mo, S_PLAY_NIGHTS_ATTACK);
 		}
 	}
 	else
@@ -6838,7 +6906,7 @@ static void P_DoNiGHTSCapsule(player_t *player)
 		if (!(player->pflags & PF_JUMPED) && !(player->pflags & PF_SPINNING))
 			player->pflags |= PF_JUMPED;
 		if (player->panim != PA_ROLL)
-			P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+			P_SetMobjState(player->mo, S_PLAY_ROLL);
 	}
 
 	if (!(player->charflags & SF_NONIGHTSROTATION))
@@ -6921,10 +6989,14 @@ static void P_DoNiGHTSCapsule(player_t *player)
 
 			// Spawn a 'pop' for every 2 tics
 			if (!((tictimer - firstpoptic) % 2))
-				S_StartSound(P_SpawnMobj(player->capsule->x + ((P_SignedRandom()/2)<<FRACBITS),
-				player->capsule->y + ((P_SignedRandom()/2)<<FRACBITS),
-				player->capsule->z + (player->capsule->height/2) + ((P_SignedRandom()/2)<<FRACBITS),
-				MT_SONIC3KBOSSEXPLODE),sfx_s3kb4);
+			{
+				mobj_t *explodemo = P_SpawnMobj(player->capsule->x + ((P_SignedRandom()/2)<<FRACBITS),
+					player->capsule->y + ((P_SignedRandom()/2)<<FRACBITS),
+					player->capsule->z + (player->capsule->height/2) + ((P_SignedRandom()/2)<<FRACBITS),
+					MT_SONIC3KBOSSEXPLODE);
+				if (!P_MobjWasRemoved(explodemo))
+					S_StartSound(explodemo,sfx_s3kb4);
+			}
 		}
 		else
 		{
@@ -6981,10 +7053,13 @@ static void P_DoNiGHTSCapsule(player_t *player)
 						UINT8 em = P_GetNextEmerald();
 						// Only give it to ONE person, and THAT player has to get to the goal!
 						mobj_t *emmo = P_SpawnMobjFromMobj(player->mo, 0, 0, player->mo->height, MT_GOTEMERALD);
-						emmo->health = em; // for identification
-						P_SetTarget(&emmo->target, player->mo);
-						P_SetMobjState(emmo, mobjinfo[MT_GOTEMERALD].meleestate + em);
-						P_SetTarget(&player->mo->tracer, emmo);
+						if (!P_MobjWasRemoved(emmo))
+						{
+							emmo->health = em; // for identification
+							P_SetTarget(&emmo->target, player->mo);
+							P_SetMobjState(emmo, mobjinfo[MT_GOTEMERALD].meleestate + em);
+							P_SetTarget(&player->mo->tracer, emmo);
+						}
 					}
 
 					// Okay, we're doing this down here because we're handling time weirdly for co-op special stages
@@ -7007,19 +7082,22 @@ static void P_DoNiGHTSCapsule(player_t *player)
 						P_InstaThrust(flicky, flicky->angle, 8*FRACUNIT);
 					}*/
 					mobj_t *idya = P_SpawnMobjFromMobj(player->mo, 0, 0, player->mo->height, MT_GOTEMERALD);
-					idya->extravalue2 = player->mare/5;
-					idya->health = player->mare + 1; // for identification
-					P_SetTarget(&idya->target, player->mo);
-					P_SetMobjState(idya, mobjinfo[MT_GOTEMERALD].missilestate + ((player->mare + 1) % 5));
-
-					if (player->mo->tracer)
+					if (!P_MobjWasRemoved(idya))
 					{
-						P_SetTarget(&idya->hnext, player->mo->tracer);
-						idya->extravalue1 = (angle_t)(player->mo->tracer->extravalue1 - 72*ANG1);
-						if (idya->extravalue1 > player->mo->tracer->extravalue1)
-							idya->extravalue1 -= (72*ANG1)/idya->extravalue1;
+						idya->extravalue2 = player->mare/5;
+						idya->health = player->mare + 1; // for identification
+						P_SetTarget(&idya->target, player->mo);
+						P_SetMobjState(idya, mobjinfo[MT_GOTEMERALD].missilestate + ((player->mare + 1) % 5));
+
+						if (player->mo->tracer)
+						{
+							P_SetTarget(&idya->hnext, player->mo->tracer);
+							idya->extravalue1 = (angle_t)(player->mo->tracer->extravalue1 - 72*ANG1);
+							if (idya->extravalue1 > player->mo->tracer->extravalue1)
+								idya->extravalue1 -= (72*ANG1)/idya->extravalue1;
+						}
+						P_SetTarget(&player->mo->tracer, idya);
 					}
-					P_SetTarget(&player->mo->tracer, idya);
 				}
 				for (i = 0; i < MAXPLAYERS; i++)
 					if (playeringame[i] && players[i].mare == player->mare)
@@ -7317,14 +7395,14 @@ static void P_NiGHTSMovement(player_t *player)
 		if (!(player->charflags & SF_NONIGHTSROTATION) && player->mo->momz)
 		{
 			if (player->mo->state != &states[S_PLAY_NIGHTS_DRILL])
-				P_SetPlayerMobjState(player->mo, S_PLAY_NIGHTS_DRILL);
+				P_SetMobjState(player->mo, S_PLAY_NIGHTS_DRILL);
 			player->mo->spriteroll = ANGLE_90;
 		}
 		else
 #endif
 		{
 			if (player->mo->state != &states[S_PLAY_NIGHTS_FLOAT])
-				P_SetPlayerMobjState(player->mo, S_PLAY_NIGHTS_FLOAT);
+				P_SetMobjState(player->mo, S_PLAY_NIGHTS_FLOAT);
 			player->drawangle += ANGLE_22h;
 		}
 
@@ -7344,19 +7422,25 @@ static void P_NiGHTSMovement(player_t *player)
 			z -= FixedMul(mobjinfo[MT_NIGHTSPARKLE].height, player->mo->scale);
 
 		firstmobj = P_SpawnMobj(player->mo->x + P_ReturnThrustX(player->mo, player->mo->angle+ANGLE_90, spawndist), player->mo->y + P_ReturnThrustY(player->mo, player->mo->angle+ANGLE_90, spawndist), z, MT_NIGHTSPARKLE);
+		if (!P_MobjWasRemoved(firstmobj))
+		{
+			firstmobj->destscale = player->mo->scale;
+			P_SetTarget(&firstmobj->target, player->mo);
+			P_SetScale(firstmobj, player->mo->scale);
+			// Superloop turns sparkles red
+			if (player->powers[pw_nights_superloop])
+				P_SetMobjState(firstmobj, mobjinfo[MT_NIGHTSPARKLE].seestate);
+		}
 		secondmobj = P_SpawnMobj(player->mo->x + P_ReturnThrustX(player->mo, player->mo->angle-ANGLE_90, spawndist), player->mo->y + P_ReturnThrustY(player->mo, player->mo->angle-ANGLE_90, spawndist), z, MT_NIGHTSPARKLE);
-
-		firstmobj->destscale = secondmobj->destscale = player->mo->scale;
-		P_SetTarget(&firstmobj->target, player->mo);
-		P_SetScale(firstmobj, player->mo->scale);
-		P_SetTarget(&secondmobj->target, player->mo);
-		P_SetScale(secondmobj, player->mo->scale);
-
-		// Superloop turns sparkles red
-		if (player->powers[pw_nights_superloop])
+		if (!P_MobjWasRemoved(secondmobj))
 		{
-			P_SetMobjState(firstmobj, mobjinfo[MT_NIGHTSPARKLE].seestate);
-			P_SetMobjState(secondmobj, mobjinfo[MT_NIGHTSPARKLE].seestate);
+			secondmobj->destscale = player->mo->scale;
+			P_SetTarget(&secondmobj->target, player->mo);
+			P_SetScale(secondmobj, player->mo->scale);
+
+			// Superloop turns sparkles red
+			if (player->powers[pw_nights_superloop])
+				P_SetMobjState(secondmobj, mobjinfo[MT_NIGHTSPARKLE].seestate);
 		}
 	}
 
@@ -7364,9 +7448,12 @@ static void P_NiGHTSMovement(player_t *player)
 	// It also spawns every tic to avoid failed paraloops
 	{
 		mobj_t *helpermobj = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z + player->mo->height/2, MT_NIGHTSLOOPHELPER);
-		helpermobj->fuse = player->mo->fuse = leveltime;
-		P_SetTarget(&helpermobj->target, player->mo);
-		P_SetScale(helpermobj, player->mo->scale);
+		if (!P_MobjWasRemoved(helpermobj))
+		{
+			helpermobj->fuse = player->mo->fuse = leveltime;
+			P_SetTarget(&helpermobj->target, player->mo);
+			P_SetScale(helpermobj, player->mo->scale);
+		}
 	}
 
 	if (player->bumpertime)
@@ -7555,19 +7642,22 @@ static void P_NiGHTSMovement(player_t *player)
 		mobjtype_t splishtype = (player->mo->eflags & MFE_TOUCHLAVA) ? MT_LAVASPLISH : MT_SPLISH;
 		mobj_t *water = P_SpawnMobj(player->mo->x, player->mo->y,
 			((player->mo->eflags & MFE_VERTICALFLIP) ? player->mo->waterbottom - FixedMul(mobjinfo[splishtype].height, player->mo->scale) : player->mo->watertop), splishtype);
-		if (player->mo->eflags & MFE_GOOWATER)
-			S_StartSound(water, sfx_ghit);
-		else if (player->mo->eflags & MFE_TOUCHLAVA)
-			S_StartSound(water, sfx_splash);
-		else
-			S_StartSound(water, sfx_wslap);
-		if (player->mo->eflags & MFE_VERTICALFLIP)
+		if (!P_MobjWasRemoved(water))
 		{
-			water->flags2 |= MF2_OBJECTFLIP;
-			water->eflags |= MFE_VERTICALFLIP;
+			if (player->mo->eflags & MFE_GOOWATER)
+				S_StartSound(water, sfx_ghit);
+			else if (player->mo->eflags & MFE_TOUCHLAVA)
+				S_StartSound(water, sfx_splash);
+			else
+				S_StartSound(water, sfx_wslap);
+			if (player->mo->eflags & MFE_VERTICALFLIP)
+			{
+				water->flags2 |= MF2_OBJECTFLIP;
+				water->eflags |= MFE_VERTICALFLIP;
+			}
+			water->destscale = player->mo->scale;
+			P_SetScale(water, player->mo->scale);
 		}
-		water->destscale = player->mo->scale;
-		P_SetScale(water, player->mo->scale);
 	}
 
 	if (player->mo->momx || player->mo->momy)
@@ -7648,7 +7738,7 @@ static void P_NiGHTSMovement(player_t *player)
 	}
 
 	if (player->mo->state != &states[flystate])
-		P_SetPlayerMobjState(player->mo, flystate);
+		P_SetMobjState(player->mo, flystate);
 
 	if (player->charflags & SF_NONIGHTSROTATION)
 		player->mo->spriteroll = 0;
@@ -7800,6 +7890,8 @@ void P_ElementalFire(player_t *player, boolean cropcircle)
 		for (i = 0; i < numangles; i++)
 		{
 			flame = P_SpawnMobj(player->mo->x, player->mo->y, ground, MT_SPINFIRE);
+			if (P_MobjWasRemoved(flame))
+				continue;
 			flame->flags &= ~MF_NOGRAVITY;
 			P_SetTarget(&flame->target, player->mo);
 			flame->angle = travelangle + i*(ANGLE_MAX/numangles);
@@ -7837,6 +7929,8 @@ void P_ElementalFire(player_t *player, boolean cropcircle)
 			}
 
 			flame = P_SpawnMobj(newx, newy, ground, MT_SPINFIRE);
+			if (P_MobjWasRemoved(flame))
+				continue;
 			P_SetTarget(&flame->target, player->mo);
 			flame->angle = travelangle;
 			flame->fuse = TICRATE*6;
@@ -7878,6 +7972,8 @@ void P_SpawnSkidDust(player_t *player, fixed_t radius, boolean sound)
 	mobj_t *particle;
 
 	particle = P_SpawnMobjFromMobj(mo, 0, 0, 0, MT_SPINDUST);
+	if (P_MobjWasRemoved(particle))
+		return;
 	if (radius >>= FRACBITS)
 	{
 		P_UnsetThingPosition(particle);
@@ -7914,13 +8010,13 @@ static void P_SkidStuff(player_t *player)
 			player->skidtime = 0;
 			player->pflags &= ~(PF_GLIDING|PF_JUMPED|PF_NOJUMPDAMAGE);
 			player->pflags |= PF_THOKKED;
-			P_SetPlayerMobjState(player->mo, S_PLAY_FALL);
+			P_SetMobjState(player->mo, S_PLAY_FALL);
 		}
 		// Get up and brush yourself off, idiot.
 		else if (player->glidetime > 15 || !(player->cmd.buttons & BT_JUMP))
 		{
 			P_ResetPlayer(player);
-			P_SetPlayerMobjState(player->mo, S_PLAY_GLIDE_LANDING);
+			P_SetMobjState(player->mo, S_PLAY_GLIDE_LANDING);
 			player->pflags |= PF_STASIS;
 			if (player->speed > FixedMul(player->runspeed, player->mo->scale))
 				player->skidtime += player->mo->tics;
@@ -7965,7 +8061,7 @@ static void P_SkidStuff(player_t *player)
 			if (dang > ANGLE_157h)
 			{
 				if (player->mo->state-states != S_PLAY_SKID)
-					P_SetPlayerMobjState(player->mo, S_PLAY_SKID);
+					P_SetMobjState(player->mo, S_PLAY_SKID);
 				player->mo->tics = player->skidtime = (player->mo->movefactor == FRACUNIT) ? TICRATE/2 : (FixedDiv(35<<(FRACBITS-1), FixedSqrt(player->mo->movefactor)))>>FRACBITS;
 				S_StartSound(player->mo, sfx_skid);
 			}
@@ -8197,63 +8293,63 @@ void P_MovePlayer(player_t *player)
 	{
 		// If the player is in dashmode, here's their peelout.
 		if (player->charflags & SF_DASHMODE && player->dashmode >= DASHMODE_THRESHOLD && player->panim == PA_RUN && !player->skidtime && (onground || ((player->charability == CA_FLOAT || player->charability == CA_SLOWFALL) && player->secondjump == 1) || player->powers[pw_super]))
-			P_SetPlayerMobjState (player->mo, S_PLAY_DASH);
+			P_SetMobjState (player->mo, S_PLAY_DASH);
 		// If the player is moving fast enough,
 		// break into a run!
 		else if (player->speed >= runspd && player->panim == PA_WALK && !player->skidtime
 		&& (onground || ((player->charability == CA_FLOAT || player->charability == CA_SLOWFALL) && player->secondjump == 1) || player->powers[pw_super]))
 		{
 			if (!onground)
-				P_SetPlayerMobjState(player->mo, S_PLAY_FLOAT_RUN);
+				P_SetMobjState(player->mo, S_PLAY_FLOAT_RUN);
 			else
-				P_SetPlayerMobjState(player->mo, S_PLAY_RUN);
+				P_SetMobjState(player->mo, S_PLAY_RUN);
 		}
 
 		// Floating at slow speeds has its own special animation.
 		else if ((((player->charability == CA_FLOAT || player->charability == CA_SLOWFALL) && player->secondjump == 1) || player->powers[pw_super]) && player->panim == PA_IDLE && !onground)
-			P_SetPlayerMobjState (player->mo, S_PLAY_FLOAT);
+			P_SetMobjState (player->mo, S_PLAY_FLOAT);
 
 		// Otherwise, just walk.
 		else if ((player->rmomx || player->rmomy) && player->panim == PA_IDLE)
-			P_SetPlayerMobjState (player->mo, S_PLAY_WALK);
+			P_SetMobjState (player->mo, S_PLAY_WALK);
 	}
 
 	// If your peelout animation is playing, and you're
 	// going too slow, switch back to the run.
 	if (player->charflags & SF_DASHMODE && player->panim == PA_DASH && player->dashmode < DASHMODE_THRESHOLD)
-		P_SetPlayerMobjState(player->mo, S_PLAY_RUN);
+		P_SetMobjState(player->mo, S_PLAY_RUN);
 
 	// If your running animation is playing, and you're
 	// going too slow, switch back to the walking frames.
 	if (player->panim == PA_RUN && player->speed < runspd)
 	{
 		if (!onground && (((player->charability == CA_FLOAT || player->charability == CA_SLOWFALL) && player->secondjump == 1) || player->powers[pw_super]))
-			P_SetPlayerMobjState(player->mo, S_PLAY_FLOAT);
+			P_SetMobjState(player->mo, S_PLAY_FLOAT);
 		else
-			P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
+			P_SetMobjState(player->mo, S_PLAY_WALK);
 	}
 
 	// Correct floating when ending up on the ground.
 	if (onground)
 	{
 		if (player->mo->state-states == S_PLAY_FLOAT)
-			P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
+			P_SetMobjState(player->mo, S_PLAY_WALK);
 		else if (player->mo->state-states == S_PLAY_FLOAT_RUN)
-			P_SetPlayerMobjState(player->mo, S_PLAY_RUN);
+			P_SetMobjState(player->mo, S_PLAY_RUN);
 	}
 
 	// If Springing (or nojumpspinning), but travelling DOWNWARD, change back!
 	if ((player->panim == PA_SPRING && P_MobjFlip(player->mo)*player->mo->momz < 0)
 		|| ((((player->charflags & SF_NOJUMPSPIN) && (player->pflags & PF_JUMPED) && player->panim == PA_JUMP))
 			&& (P_MobjFlip(player->mo)*player->mo->momz < 0)))
-		P_SetPlayerMobjState(player->mo, S_PLAY_FALL);
+		P_SetMobjState(player->mo, S_PLAY_FALL);
 	// If doing an air animation but on the ground, change back!
 	else if (onground && (player->panim == PA_SPRING || player->panim == PA_FALL || player->panim == PA_RIDE || player->panim == PA_JUMP) && !player->mo->momz)
-		P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+		P_SetMobjState(player->mo, S_PLAY_STND);
 
 	// If you are stopped and are still walking, stand still!
 	if (!player->mo->momx && !player->mo->momy && !player->mo->momz && player->panim == PA_WALK)
-		P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+		P_SetMobjState(player->mo, S_PLAY_STND);
 
 //////////////////
 //GAMEPLAY STUFF//
@@ -8265,18 +8361,18 @@ void P_MovePlayer(player_t *player)
 	{
 		player->pflags &= ~(PF_STARTJUMP|PF_JUMPED|PF_NOJUMPDAMAGE|PF_THOKKED|PF_SHIELDABILITY);
 		player->secondjump = 0;
-		P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+		P_SetMobjState(player->mo, S_PLAY_STND);
 	}
 
 	if ((!(player->charability == CA_GLIDEANDCLIMB) || player->gotflag) // If you can't glide, then why the heck would you be gliding?
 		&& (player->pflags & PF_GLIDING || player->climbing))
 	{
 		if (onground)
-			P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
+			P_SetMobjState(player->mo, S_PLAY_WALK);
 		else
 		{
 			player->pflags |= P_GetJumpFlags(player);
-			P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
+			P_SetMobjState(player->mo, S_PLAY_JUMP);
 		}
 		player->pflags &= ~PF_GLIDING;
 		player->glidetime = 0;
@@ -8287,11 +8383,11 @@ void P_MovePlayer(player_t *player)
 		&& (player->pflags & PF_BOUNCING))
 	{
 		if (onground)
-			P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
+			P_SetMobjState(player->mo, S_PLAY_WALK);
 		else
 		{
 			player->pflags |= P_GetJumpFlags(player);
-			P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
+			P_SetMobjState(player->mo, S_PLAY_JUMP);
 		}
 		player->pflags &= ~PF_BOUNCING;
 	}
@@ -8408,18 +8504,18 @@ void P_MovePlayer(player_t *player)
 		{
 			P_ResetPlayer(player); // down, stop gliding.
 			if (onground)
-				P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
+				P_SetMobjState(player->mo, S_PLAY_WALK);
 			else if (player->charflags & SF_MULTIABILITY)
 			{
 				player->pflags |= P_GetJumpFlags(player);
-				P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
+				P_SetMobjState(player->mo, S_PLAY_JUMP);
 			}
 			else
 			{
 				player->pflags |= PF_THOKKED;
 				player->mo->momx >>= 1;
 				player->mo->momy >>= 1;
-				P_SetPlayerMobjState(player->mo, S_PLAY_FALL);
+				P_SetMobjState(player->mo, S_PLAY_FALL);
 			}
 		}
 	}
@@ -8436,23 +8532,23 @@ void P_MovePlayer(player_t *player)
 			P_ResetPlayer(player); // down, stop bouncing.
 			player->pflags |= PF_THOKKED;
 			if (onground)
-				P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
+				P_SetMobjState(player->mo, S_PLAY_WALK);
 			else if (player->charflags & SF_MULTIABILITY)
 			{
 				player->pflags |= P_GetJumpFlags(player);
-				P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
+				P_SetMobjState(player->mo, S_PLAY_JUMP);
 			}
 			else
 			{
 				player->mo->momx >>= 1;
 				player->mo->momy >>= 1;
 				player->mo->momz >>= 1;
-				P_SetPlayerMobjState(player->mo, S_PLAY_FALL);
+				P_SetMobjState(player->mo, S_PLAY_FALL);
 			}
 		}
 	}
 	else if (player->mo->state-states == S_PLAY_BOUNCE)
-		P_SetPlayerMobjState(player->mo, S_PLAY_FALL);
+		P_SetMobjState(player->mo, S_PLAY_FALL);
 
 	// If you're running fast enough, you can create splashes as you run in shallow water.
 	if (!player->climbing
@@ -8464,19 +8560,22 @@ void P_MovePlayer(player_t *player)
 		mobjtype_t splishtype = (player->mo->eflags & MFE_TOUCHLAVA) ? MT_LAVASPLISH : MT_SPLISH;
 		mobj_t *water = P_SpawnMobj(player->mo->x - P_ReturnThrustX(NULL, player->mo->angle, player->mo->radius), player->mo->y - P_ReturnThrustY(NULL, player->mo->angle, player->mo->radius),
 			((player->mo->eflags & MFE_VERTICALFLIP) ? player->mo->waterbottom - FixedMul(mobjinfo[splishtype].height, player->mo->scale) : player->mo->watertop), splishtype);
-		if (player->mo->eflags & MFE_GOOWATER)
-			S_StartSound(water, sfx_ghit);
-		else if (player->mo->eflags & MFE_TOUCHLAVA)
-			S_StartSound(water, sfx_splash);
-		else
-			S_StartSound(water, sfx_wslap);
-		if (player->mo->eflags & MFE_VERTICALFLIP)
+		if (!P_MobjWasRemoved(water))
 		{
-			water->flags2 |= MF2_OBJECTFLIP;
-			water->eflags |= MFE_VERTICALFLIP;
+			if (player->mo->eflags & MFE_GOOWATER)
+				S_StartSound(water, sfx_ghit);
+			else if (player->mo->eflags & MFE_TOUCHLAVA)
+				S_StartSound(water, sfx_splash);
+			else
+				S_StartSound(water, sfx_wslap);
+			if (player->mo->eflags & MFE_VERTICALFLIP)
+			{
+				water->flags2 |= MF2_OBJECTFLIP;
+				water->eflags |= MFE_VERTICALFLIP;
+			}
+			water->destscale = player->mo->scale;
+			P_SetScale(water, player->mo->scale);
 		}
-		water->destscale = player->mo->scale;
-		P_SetScale(water, player->mo->scale);
 	}
 
 	// Little water sound while touching water - just a nicety.
@@ -8496,11 +8595,11 @@ void P_MovePlayer(player_t *player)
 		|| player->mo->state-states == S_PLAY_FLY_TIRED)
 		{
 			if (onground)
-				P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
+				P_SetMobjState(player->mo, S_PLAY_WALK);
 			else
 			{
 				player->pflags |= P_GetJumpFlags(player);
-				P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
+				P_SetMobjState(player->mo, S_PLAY_JUMP);
 			}
 		}
 		player->powers[pw_tailsfly] = 0;
@@ -8534,7 +8633,7 @@ void P_MovePlayer(player_t *player)
 					if (P_MobjFlip(player->mo)*player->mo->momz < FixedMul(5*actionspd, player->mo->scale))
 						P_SetObjectMomZ(player->mo, actionspd/2, true);
 
-					P_SetPlayerMobjState(player->mo, player->mo->state->nextstate);
+					P_SetMobjState(player->mo, player->mo->state->nextstate);
 
 					player->fly1--;
 				}
@@ -8562,7 +8661,7 @@ void P_MovePlayer(player_t *player)
 		{
 			// Tails-gets-tired Stuff
 			if (player->panim == PA_ABILITY && player->mo->state-states != S_PLAY_FLY_TIRED)
-				P_SetPlayerMobjState(player->mo, S_PLAY_FLY_TIRED);
+				P_SetMobjState(player->mo, S_PLAY_FLY_TIRED);
 
 			if (player->charability == CA_FLY && (leveltime % 10 == 0)
 				&& player->mo->state-states == S_PLAY_FLY_TIRED
@@ -8679,25 +8778,38 @@ void P_MovePlayer(player_t *player)
 	// Make sure you're not teetering when you shouldn't be.
 	if (player->panim == PA_EDGE
 	&& (player->mo->momx || player->mo->momy || player->mo->momz))
-		P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+		P_SetMobjState(player->mo, S_PLAY_STND);
 
 	// Check for teeter!
 	if (!(player->mo->momz || player->mo->momx || player->mo->momy) && !(player->mo->eflags & MFE_GOOWATER)
 	&& player->panim == PA_IDLE && !(player->powers[pw_carry]))
 		P_DoTeeter(player);
 
-	// Toss a flag
-	if (G_GametypeHasTeams() && (cmd->buttons & BT_TOSSFLAG) && !(player->powers[pw_super]) && !(player->tossdelay))
+	// Check for fire and shield buttons
+	if (!player->exiting && !(player->pflags & PF_STASIS))
 	{
-		if (!(player->gotflag & (GF_REDFLAG|GF_BLUEFLAG)))
-			P_PlayerEmeraldBurst(player, true); // Toss emeralds
-		else
-			P_PlayerFlagBurst(player, true);
-	}
-
-	// check for fire
-	if (!player->exiting)
+		// Check for fire buttons
 		P_DoFiring(player, cmd);
+		
+		// Release the shield button
+		if (!(cmd->buttons & BT_SHIELD))
+			player->pflags &= ~PF_SHIELDDOWN;
+		
+		// Shield button behavior
+		// Check P_PlayerShieldThink for actual shields!
+		else if (!(player->pflags & PF_SHIELDDOWN))
+		{
+			// Transform into super if we can!
+			if (P_SuperReady(player, true))
+				P_DoSuperTransformation(player, false);
+			
+			// Detransform from super if we can!
+			else if (P_SuperReady(player, false))
+				P_DoSuperDetransformation(player);
+			
+			player->pflags |= PF_SHIELDDOWN;
+		}
+	}
 
 	{
 		boolean atspinheight = false;
@@ -8732,7 +8844,7 @@ void P_MovePlayer(player_t *player)
 			if (!atspinheight)
 			{
 				player->pflags |= PF_SPINNING;
-				P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+				P_SetMobjState(player->mo, S_PLAY_ROLL);
 			}
 			else if (player->mo->ceilingz - player->mo->floorz < player->mo->height)
 			{
@@ -8913,7 +9025,7 @@ static void P_DoRopeHang(player_t *player)
 	if (player->cmd.buttons & BT_SPIN && !(player->pflags & PF_STASIS)) // Drop off of the rope
 	{
 		player->pflags |= (P_GetJumpFlags(player)|PF_SPINDOWN);
-		P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
+		P_SetMobjState(player->mo, S_PLAY_JUMP);
 
 		P_SetTarget(&player->mo->tracer, NULL);
 		player->powers[pw_carry] = CR_NONE;
@@ -8922,7 +9034,7 @@ static void P_DoRopeHang(player_t *player)
 	}
 
 	if (player->mo->state-states != S_PLAY_RIDE)
-		P_SetPlayerMobjState(player->mo, S_PLAY_RIDE);
+		P_SetMobjState(player->mo, S_PLAY_RIDE);
 
 	// If not allowed to move, we're done here.
 	if (!speed)
@@ -8978,7 +9090,7 @@ static void P_DoRopeHang(player_t *player)
 			if (player->mo->tracer->flags & MF_SLIDEME)
 			{
 				player->pflags |= P_GetJumpFlags(player);
-				P_SetPlayerMobjState(player->mo, S_PLAY_JUMP);
+				P_SetMobjState(player->mo, S_PLAY_JUMP);
 			}
 
 			P_SetTarget(&player->mo->tracer, NULL);
@@ -10889,6 +11001,8 @@ static void P_SpawnSparks(mobj_t *mo, angle_t maindir)
 	fixed_t fm = (maindir >> ANGLETOFINESHIFT) & FINEMASK;
 
 	spark = P_SpawnMobj(mo->x - b2*s + b1*c, mo->y + b2*c + b1*s, mo->z, MT_MINECARTSPARK);
+	if (P_MobjWasRemoved(spark))
+		return;
 	spark->momx = mo->momx + r1 + 8*FINECOSINE(fm);
 	spark->momy = mo->momy + r2 + 8*FINESINE(fm);
 	spark->momz = mo->momz + r3;
@@ -11159,7 +11273,7 @@ static void P_MinecartThink(player_t *player)
 					detright->drawonlyforplayer = player;
 				}
 				else
-					P_RemoveMobj(detleft);
+					P_RemoveMobj(detright);
 			}
 		}
 		else
@@ -11176,7 +11290,7 @@ static void P_MinecartThink(player_t *player)
 
 	if (player->mo->state-states != S_PLAY_STND)
 	{
-		P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+		P_SetMobjState(player->mo, S_PLAY_STND);
 		player->mo->tics = -1;
 	}
 
@@ -11192,7 +11306,7 @@ static void P_MinecartThink(player_t *player)
 }
 
 // Handle Tails' fluff
-static void P_DoTailsOverlay(player_t *player, mobj_t *tails)
+void P_DoTailsOverlay(player_t *player, mobj_t *tails)
 {
 	// init...
 	boolean smilesonground = P_IsObjectOnGround(player->mo);
@@ -11389,7 +11503,7 @@ static void P_DoTailsOverlay(player_t *player, mobj_t *tails)
 	tails->y = player->mo->y + P_ReturnThrustY(tails, tails->angle, FixedMul(backwards, tails->scale));
 	tails->z = player->mo->z + zoffs;
 	P_SetThingPosition(tails);
-	
+
 	if (player->mo->flags2 & MF2_SHADOW)
 		tails->flags2 |= MF2_SHADOW;
 	else
@@ -11397,7 +11511,7 @@ static void P_DoTailsOverlay(player_t *player, mobj_t *tails)
 }
 
 // Metal Sonic's jet fume
-static void P_DoMetalJetFume(player_t *player, mobj_t *fume)
+void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 {
 	static const UINT8 FUME_SKINCOLORS[] =
 	{
@@ -11535,6 +11649,30 @@ static void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 	}
 }
 
+// Handle Followmobj behavior
+void P_DoFollowMobj(player_t *player, mobj_t *followmobj)
+{
+	if (LUA_HookFollowMobj(player, followmobj) || P_MobjWasRemoved(followmobj))
+		{;}
+	else
+	{
+		switch (followmobj->type)
+		{
+			case MT_TAILSOVERLAY: // c:
+				P_DoTailsOverlay(player, followmobj);
+				break;
+			case MT_METALJETFUME:
+				P_DoMetalJetFume(player, followmobj);
+				break;
+			default:
+				var1 = 1;
+				var2 = 0;
+				A_CapeChase(followmobj);
+				break;
+		}
+	}
+}
+
 //
 // P_PlayerThink
 //
@@ -11605,7 +11743,7 @@ void P_PlayerThink(player_t *player)
 		P_SetTarget(&player->awayviewmobj, NULL); // remove awayviewmobj asap if invalid
 		player->awayviewtics = 1; // reset to one, the below code will immediately set it to zero
 	}
-	
+
 	if (player->awayviewtics && player->awayviewtics != -1)
 	{
 		player->awayviewtics--;
@@ -11930,7 +12068,7 @@ void P_PlayerThink(player_t *player)
 		{
 			P_DoZoomTube(player);
 			if (!(player->panim == PA_ROLL))
-				P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+				P_SetMobjState(player->mo, S_PLAY_ROLL);
 		}
 		player->rmomx = player->rmomy = 0; // no actual momentum from your controls
 		P_ResetScore(player);
@@ -12089,7 +12227,7 @@ void P_PlayerThink(player_t *player)
 			{
 				statenum_t stat = player->mo->state-states;
 				if (stat == S_PLAY_WAIT)
-					P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+					P_SetMobjState(player->mo, S_PLAY_STND);
 				else if (stat == S_PLAY_STND && player->mo->tics != -1)
 					player->mo->tics++;
 			}
@@ -12116,7 +12254,7 @@ void P_PlayerThink(player_t *player)
 				else if (!player->skidtime && !(player->mo->eflags & MFE_GOOWATER) && !(player->pflags & (PF_JUMPED|PF_SPINNING|PF_SLIDING)) && !(player->charflags & SF_NOSKID) && P_AproxDistance(player->mo->momx, player->mo->momy) >= FixedMul(player->runspeed, player->mo->scale)) // modified from player->runspeed/2 'cuz the skid was just TOO frequent ngl
 				{
 					if (player->mo->state-states != S_PLAY_SKID)
-						P_SetPlayerMobjState(player->mo, S_PLAY_SKID);
+						P_SetMobjState(player->mo, S_PLAY_SKID);
 					player->mo->tics = player->skidtime = (player->mo->movefactor == FRACUNIT) ? TICRATE/2 : (FixedDiv(35<<(FRACBITS-1), FixedSqrt(player->mo->movefactor)))>>FRACBITS;
 
 					if (P_IsLocalPlayer(player)) // the sound happens way more frequently now, so give co-op players' ears a brake...
@@ -12166,17 +12304,20 @@ void P_PlayerThink(player_t *player)
 	if ((player->powers[pw_super] || player->powers[pw_sneakers]) && (player->speed + abs(player->mo->momz)) > FixedMul(20*FRACUNIT,player->mo->scale))
 	{
 		mobj_t *gmobj = P_SpawnGhostMobj(player->mo);
-		gmobj->fuse = 2;
-		if (gmobj->tracer)
-			gmobj->tracer->fuse = 2;
-		if (leveltime & 1)
+		if (!P_MobjWasRemoved(gmobj))
 		{
-			gmobj->frame &= ~FF_TRANSMASK;
-			gmobj->frame |= tr_trans70<<FF_TRANSSHIFT;
+			gmobj->fuse = 2;
 			if (gmobj->tracer)
+				gmobj->tracer->fuse = 2;
+			if (leveltime & 1)
 			{
-				gmobj->tracer->frame &= ~FF_TRANSMASK;
-				gmobj->tracer->frame |= tr_trans70<<FF_TRANSSHIFT;
+				gmobj->frame &= ~FF_TRANSMASK;
+				gmobj->frame |= tr_trans70<<FF_TRANSSHIFT;
+				if (gmobj->tracer)
+				{
+					gmobj->tracer->frame &= ~FF_TRANSMASK;
+					gmobj->tracer->frame |= tr_trans70<<FF_TRANSSHIFT;
+				}
 			}
 		}
 	}
@@ -12277,11 +12418,11 @@ void P_PlayerThink(player_t *player)
 		if (!(player->stronganim))
 			player->stronganim = player->panim;
 		else if (player->panim != player->stronganim)
-			player->powers[pw_strong] = STR_NONE; 
-	}	
+			player->powers[pw_strong] = STR_NONE;
+	}
 	else if (player->stronganim)
 		player->stronganim = 0;
-			
+
 	//pw_super acts as a timer now
 	if (player->powers[pw_super]
 	&& (player->mo->state < &states[S_PLAY_SUPER_TRANS1]
@@ -12293,7 +12434,7 @@ void P_PlayerThink(player_t *player)
 		if (!player->powers[pw_flashing])
 		{
 			if (player->mo->state != &states[S_PLAY_STND])
-				P_SetPlayerMobjState(player->mo, S_PLAY_STND);
+				P_SetMobjState(player->mo, S_PLAY_STND);
 			else
 				player->mo->tics = 2;
 		}
@@ -12388,16 +12529,19 @@ void P_PlayerThink(player_t *player)
 			if (player->jumpfactor < FixedMul(skins[player->skin].jumpfactor, 5*FRACUNIT/4)) // Boost jump height.
 				player->jumpfactor += FRACUNIT/300;
 
-			if ((player->charflags & SF_MACHINE) && (!(player->powers[pw_strong] == STR_METAL))) 
+			if ((player->charflags & SF_MACHINE) && (!(player->powers[pw_strong] == STR_METAL)))
 					player->powers[pw_strong] = STR_METAL;
 		}
 
 		if (player->normalspeed >= skins[player->skin].normalspeed*2)
 		{
 			mobj_t *ghost = P_SpawnGhostMobj(player->mo); // Spawns afterimages
-			ghost->fuse = 2; // Makes the images fade quickly
-			if (ghost->tracer && !P_MobjWasRemoved(ghost->tracer))
-				ghost->tracer->fuse = ghost->fuse;
+			if (!P_MobjWasRemoved(ghost))
+			{
+				ghost->fuse = 2; // Makes the images fade quickly
+				if (ghost->tracer && !P_MobjWasRemoved(ghost->tracer))
+					ghost->tracer->fuse = ghost->fuse;
+			}
 		}
 	}
 	else if (dashmode)
@@ -12654,7 +12798,7 @@ void P_PlayerAfterThink(player_t *player)
 		S_StartSound(NULL, sfx_wepchg);
 
 	if ((player->pflags & PF_SLIDING) && ((player->pflags & (PF_JUMPED|PF_NOJUMPDAMAGE)) != PF_JUMPED))
-		P_SetPlayerMobjState(player->mo, player->mo->info->painstate);
+		P_SetMobjState(player->mo, player->mo->info->painstate);
 
 	/* if (player->powers[pw_carry] == CR_NONE && player->mo->tracer && !player->homing)
 		P_SetTarget(&player->mo->tracer, NULL);
@@ -12715,7 +12859,7 @@ void P_PlayerAfterThink(player_t *player)
 				if (player->powers[pw_carry] == CR_PLAYER)
 				{
 					if (player->mo->state-states != S_PLAY_RIDE)
-						P_SetPlayerMobjState(player->mo, S_PLAY_RIDE);
+						P_SetMobjState(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;
 				}
@@ -12740,7 +12884,7 @@ void P_PlayerAfterThink(player_t *player)
 				player->mo->momx = player->mo->momy = player->mo->momz = 0;
 				P_SetThingPosition(player->mo);
 				if (player->mo->state-states != S_PLAY_RIDE)
-					P_SetPlayerMobjState(player->mo, S_PLAY_RIDE);
+					P_SetMobjState(player->mo, S_PLAY_RIDE);
 
 				// Controllable missile
 				if (item->type == MT_BLACKEGGMAN_MISSILE)
@@ -12861,7 +13005,7 @@ void P_PlayerAfterThink(player_t *player)
 
 				if (player->panim == PA_IDLE && (mo->momx || mo->momy))
 				{
-					P_SetPlayerMobjState(player->mo, S_PLAY_WALK);
+					P_SetMobjState(player->mo, S_PLAY_WALK);
 				}
 
 				if (player->panim == PA_WALK && mo->tics > walktics)
@@ -12917,7 +13061,7 @@ void P_PlayerAfterThink(player_t *player)
 					P_KillMobj(ptera, player->mo, player->mo, 0);
 					P_SetObjectMomZ(player->mo, 12*FRACUNIT, false);
 					player->pflags |= PF_APPLYAUTOBRAKE|PF_JUMPED|PF_THOKKED;
-					P_SetPlayerMobjState(player->mo, S_PLAY_ROLL);
+					P_SetMobjState(player->mo, S_PLAY_ROLL);
 					break;
 				}
 
@@ -12938,7 +13082,7 @@ void P_PlayerAfterThink(player_t *player)
 				ptera->cvmem >>= 1;
 
 				if (player->mo->state-states != S_PLAY_FALL)
-					P_SetPlayerMobjState(player->mo, S_PLAY_FALL);
+					P_SetMobjState(player->mo, S_PLAY_FALL);
 				break;
 
 			dropoff:
@@ -12998,40 +13142,23 @@ void P_PlayerAfterThink(player_t *player)
 		if (!player->followmobj || P_MobjWasRemoved(player->followmobj))
 		{
 			P_SetTarget(&player->followmobj, P_SpawnMobjFromMobj(player->mo, 0, 0, 0, player->followitem));
-			P_SetTarget(&player->followmobj->tracer, player->mo);
-			switch (player->followmobj->type)
-			{
-				case MT_METALJETFUME:
-					player->followmobj->colorized = true;
-					break;
-				default:
-					player->followmobj->flags2 |= MF2_LINKDRAW;
-					break;
-			}
-		}
-
-		if (player->followmobj)
-		{
-			if (LUA_HookFollowMobj(player, player->followmobj) || P_MobjWasRemoved(player->followmobj))
-				{;}
-			else
+			if (!P_MobjWasRemoved(player->followmobj))
 			{
+				P_SetTarget(&player->followmobj->tracer, player->mo);
 				switch (player->followmobj->type)
 				{
-					case MT_TAILSOVERLAY: // c:
-						P_DoTailsOverlay(player, player->followmobj);
-						break;
 					case MT_METALJETFUME:
-						P_DoMetalJetFume(player, player->followmobj);
+						player->followmobj->colorized = true;
 						break;
 					default:
-						var1 = 1;
-						var2 = 0;
-						A_CapeChase(player->followmobj);
+						player->followmobj->flags2 |= MF2_LINKDRAW;
 						break;
 				}
 			}
 		}
+
+		if (player->followmobj)
+			P_DoFollowMobj(player, player->followmobj);
 	}
 
 	P_DoPlayerHeadSigns(player); // Spawn Tag/CTF signs over player's head
@@ -13119,3 +13246,16 @@ boolean P_PlayerShouldUseSpinHeight(player_t *player)
 			&& player->dashmode >= DASHMODE_THRESHOLD && player->mo->state-states == S_PLAY_DASH)
 		|| JUMPCURLED(player));
 }
+
+UINT16 P_GetPlayerColor(player_t *player)
+{
+	if (G_GametypeHasTeams() && player->ctfteam)
+	{
+		if (player->ctfteam == 1)
+			return skincolor_redteam;
+		else if (player->ctfteam == 2)
+			return skincolor_blueteam;
+	}
+
+	return player->skincolor;
+}
diff --git a/src/r_bbox.c b/src/r_bbox.c
index cf417ec37639477b43a5a5e5035b059dc81490b4..8ccad2bb58186098810f299405fc0f4d23fa9b5a 100644
--- a/src/r_bbox.c
+++ b/src/r_bbox.c
@@ -267,18 +267,17 @@ static boolean is_tangible (mobj_t *thing)
 boolean R_ThingBoundingBoxVisible(mobj_t *thing)
 {
 	INT32 cvmode = cv_renderhitbox.value;
+	boolean ring = false;
 
 	if (multiplayer) // No hitboxes in multiplayer to avoid cheating
 		return false;
 
-	// Do not render bbox for these
 	switch (thing->type)
 	{
 		default:
-			// First person / awayviewmobj -- rendering
-			// a bbox too close to the viewpoint causes
-			// anomalies and these are exactly on the
-			// viewpoint!
+			// First person / awayviewmobj -- rendering a bbox 
+			// too close to the viewpoint causes anomalies
+			// and these are exactly on the viewpoint!
 			if (thing != r_viewmobj)
 			{
 				break;
@@ -290,6 +289,17 @@ boolean R_ThingBoundingBoxVisible(mobj_t *thing)
 			// are rendered using portals in Software,
 			// r_viewmobj does not point here.
 			return false;
+
+		case MT_RING:
+		case MT_BLUESPHERE:
+		case MT_NIGHTSSTAR:
+		case MT_NIGHTSCHIP:
+		case MT_COIN:
+			// Rings and similar objects are often placed
+			// in large amounts, so they are handled
+			// separately from other tangible objects.
+			ring = true;
+			break;
 	}
 
 	switch (cvmode)
@@ -304,16 +314,10 @@ boolean R_ThingBoundingBoxVisible(mobj_t *thing)
 			return !is_tangible(thing);
 
 		case RENDERHITBOX_TANGIBLE:
-			// Exclude rings from here, lots of them!
-			if (thing->type == MT_RING)
-			{
-				return false;
-			}
-
-			return is_tangible(thing);
+			return !ring && is_tangible(thing);
 
 		case RENDERHITBOX_RINGS:
-			return (thing->type == MT_RING || thing->type == MT_BLUESPHERE);
+			return ring;
 
 		default:
 			return false;
diff --git a/src/r_bsp.c b/src/r_bsp.c
index 42e050adf831f15a7b3e653c57e265cab781b6cc..d99de5981ba789395347a01ae28ad14f4c68bb0d 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -277,6 +277,8 @@ sector_t *R_FakeFlat(sector_t *sec, sector_t *tempsec, INT32 *floorlightlevel,
 			tempsec->floorpic = s->floorpic;
 			tempsec->floorxoffset = s->floorxoffset;
 			tempsec->flooryoffset = s->flooryoffset;
+			tempsec->floorxscale = s->floorxscale;
+			tempsec->flooryscale = s->flooryscale;
 			tempsec->floorangle = s->floorangle;
 
 			if (underwater)
@@ -287,6 +289,8 @@ sector_t *R_FakeFlat(sector_t *sec, sector_t *tempsec, INT32 *floorlightlevel,
 					tempsec->ceilingpic = tempsec->floorpic;
 					tempsec->ceilingxoffset = tempsec->floorxoffset;
 					tempsec->ceilingyoffset = tempsec->flooryoffset;
+					tempsec->ceilingxscale = tempsec->floorxscale;
+					tempsec->ceilingyscale = tempsec->flooryscale;
 					tempsec->ceilingangle = tempsec->floorangle;
 				}
 				else
@@ -294,6 +298,8 @@ sector_t *R_FakeFlat(sector_t *sec, sector_t *tempsec, INT32 *floorlightlevel,
 					tempsec->ceilingpic = s->ceilingpic;
 					tempsec->ceilingxoffset = s->ceilingxoffset;
 					tempsec->ceilingyoffset = s->ceilingyoffset;
+					tempsec->ceilingxscale = s->ceilingxscale;
+					tempsec->ceilingyscale = s->ceilingyscale;
 					tempsec->ceilingangle = s->ceilingangle;
 				}
 			}
@@ -317,6 +323,8 @@ sector_t *R_FakeFlat(sector_t *sec, sector_t *tempsec, INT32 *floorlightlevel,
 			tempsec->floorpic = tempsec->ceilingpic = s->ceilingpic;
 			tempsec->floorxoffset = tempsec->ceilingxoffset = s->ceilingxoffset;
 			tempsec->flooryoffset = tempsec->ceilingyoffset = s->ceilingyoffset;
+			tempsec->floorxscale = tempsec->ceilingxscale = s->ceilingxscale;
+			tempsec->flooryscale = tempsec->ceilingyscale = s->ceilingyscale;
 			tempsec->floorangle = tempsec->ceilingangle = s->ceilingangle;
 
 			if (s->floorpic == skyflatnum) // SKYFIX?
@@ -325,6 +333,8 @@ sector_t *R_FakeFlat(sector_t *sec, sector_t *tempsec, INT32 *floorlightlevel,
 				tempsec->floorpic = tempsec->ceilingpic;
 				tempsec->floorxoffset = tempsec->ceilingxoffset;
 				tempsec->flooryoffset = tempsec->ceilingyoffset;
+				tempsec->floorxscale = tempsec->ceilingxscale;
+				tempsec->flooryscale = tempsec->ceilingyscale;
 				tempsec->floorangle = tempsec->ceilingangle;
 			}
 			else
@@ -333,6 +343,8 @@ sector_t *R_FakeFlat(sector_t *sec, sector_t *tempsec, INT32 *floorlightlevel,
 				tempsec->floorpic = s->floorpic;
 				tempsec->floorxoffset = s->floorxoffset;
 				tempsec->flooryoffset = s->flooryoffset;
+				tempsec->floorxscale = s->floorxscale;
+				tempsec->flooryscale = s->flooryscale;
 				tempsec->floorangle = s->floorangle;
 			}
 
@@ -362,12 +374,16 @@ boolean R_IsEmptyLine(seg_t *line, sector_t *front, sector_t *back)
 		&& back->c_slope == front->c_slope
 		&& back->lightlevel == front->lightlevel
 		&& !line->sidedef->midtexture
-		// Check offsets too!
+		// Check offsets and scale too!
 		&& back->floorxoffset == front->floorxoffset
 		&& back->flooryoffset == front->flooryoffset
+		&& back->floorxscale == front->floorxscale
+		&& back->flooryscale == front->flooryscale
 		&& back->floorangle == front->floorangle
 		&& back->ceilingxoffset == front->ceilingxoffset
 		&& back->ceilingyoffset == front->ceilingyoffset
+		&& back->ceilingxscale == front->ceilingxscale
+		&& back->ceilingyscale == front->ceilingyscale
 		&& back->ceilingangle == front->ceilingangle
 		// Consider altered lighting.
 		&& back->floorlightlevel == front->floorlightlevel
@@ -909,7 +925,9 @@ static void R_Subsector(size_t num)
 		|| (frontsector->heightsec != -1 && sectors[frontsector->heightsec].ceilingpic == skyflatnum))
 	{
 		floorplane = R_FindPlane(frontsector->floorheight, frontsector->floorpic, floorlightlevel,
-			frontsector->floorxoffset, frontsector->flooryoffset, frontsector->floorangle, floorcolormap, NULL, NULL, frontsector->f_slope);
+			frontsector->floorxoffset, frontsector->flooryoffset,
+			frontsector->floorxscale, frontsector->flooryscale, frontsector->floorangle,
+			floorcolormap, NULL, NULL, frontsector->f_slope);
 	}
 	else
 		floorplane = NULL;
@@ -918,8 +936,9 @@ static void R_Subsector(size_t num)
 		|| frontsector->ceilingpic == skyflatnum
 		|| (frontsector->heightsec != -1 && sectors[frontsector->heightsec].floorpic == skyflatnum))
 	{
-		ceilingplane = R_FindPlane(frontsector->ceilingheight, frontsector->ceilingpic,
-			ceilinglightlevel, frontsector->ceilingxoffset, frontsector->ceilingyoffset, frontsector->ceilingangle,
+		ceilingplane = R_FindPlane(frontsector->ceilingheight, frontsector->ceilingpic, ceilinglightlevel,
+			frontsector->ceilingxoffset, frontsector->ceilingyoffset,
+			frontsector->ceilingxscale, frontsector->ceilingyscale, frontsector->ceilingangle,
 			ceilingcolormap, NULL, NULL, frontsector->c_slope);
 	}
 	else
@@ -962,8 +981,9 @@ static void R_Subsector(size_t num)
 					viewz < heightcheck);
 
 				ffloor[numffloors].plane = R_FindPlane(*rover->bottomheight, *rover->bottompic,
-					*frontsector->lightlist[light].lightlevel, *rover->bottomxoffs,
-					*rover->bottomyoffs, *rover->bottomangle, *frontsector->lightlist[light].extra_colormap, rover, NULL, *rover->b_slope);
+					*frontsector->lightlist[light].lightlevel, *rover->bottomxoffs, *rover->bottomyoffs,
+					*rover->bottomxscale, *rover->bottomyscale, *rover->bottomangle,
+					*frontsector->lightlist[light].extra_colormap, rover, NULL, *rover->b_slope);
 
 				ffloor[numffloors].slope = *rover->b_slope;
 
@@ -991,7 +1011,8 @@ static void R_Subsector(size_t num)
 				light = R_GetPlaneLight(frontsector, planecenterz, viewz < heightcheck);
 
 				ffloor[numffloors].plane = R_FindPlane(*rover->topheight, *rover->toppic,
-					*frontsector->lightlist[light].lightlevel, *rover->topxoffs, *rover->topyoffs, *rover->topangle,
+					*frontsector->lightlist[light].lightlevel, *rover->topxoffs, *rover->topyoffs,
+					*rover->topxscale, *rover->topyscale, *rover->topangle,
 					*frontsector->lightlist[light].extra_colormap, rover, NULL, *rover->t_slope);
 
 				ffloor[numffloors].slope = *rover->t_slope;
@@ -1033,7 +1054,9 @@ static void R_Subsector(size_t num)
 			{
 				light = R_GetPlaneLight(frontsector, polysec->floorheight, viewz < polysec->floorheight);
 				ffloor[numffloors].plane = R_FindPlane(polysec->floorheight, polysec->floorpic,
-					(light == -1 ? frontsector->lightlevel : *frontsector->lightlist[light].lightlevel), polysec->floorxoffset, polysec->flooryoffset,
+					(light == -1 ? frontsector->lightlevel : *frontsector->lightlist[light].lightlevel),
+					polysec->floorxoffset, polysec->flooryoffset,
+					polysec->floorxscale, polysec->flooryscale,
 					polysec->floorangle-po->angle,
 					(light == -1 ? frontsector->extra_colormap : *frontsector->lightlist[light].extra_colormap), NULL, po,
 					NULL); // will ffloors be slopable eventually?
@@ -1057,7 +1080,10 @@ static void R_Subsector(size_t num)
 			{
 				light = R_GetPlaneLight(frontsector, polysec->floorheight, viewz < polysec->floorheight);
 				ffloor[numffloors].plane = R_FindPlane(polysec->ceilingheight, polysec->ceilingpic,
-					(light == -1 ? frontsector->lightlevel : *frontsector->lightlist[light].lightlevel), polysec->ceilingxoffset, polysec->ceilingyoffset, polysec->ceilingangle-po->angle,
+					(light == -1 ? frontsector->lightlevel : *frontsector->lightlist[light].lightlevel),
+					polysec->ceilingxoffset, polysec->ceilingyoffset,
+					polysec->ceilingxscale, polysec->ceilingyscale,
+					polysec->ceilingangle-po->angle,
 					(light == -1 ? frontsector->extra_colormap : *frontsector->lightlist[light].extra_colormap), NULL, po,
 					NULL); // will ffloors be slopable eventually?
 
diff --git a/src/r_data.c b/src/r_data.c
index 4b7492f908e51412f211f2816ac85904efd047b4..e2b74da40712014986b3adc495b77a3ce5c9a221 100644
--- a/src/r_data.c
+++ b/src/r_data.c
@@ -438,7 +438,7 @@ extracolormap_t *R_CreateDefaultColormap(boolean lighttable)
 	exc->fadeend = 31;
 	exc->flags = 0;
 	exc->rgba = 0;
-	exc->fadergba = 0x19000000;
+	exc->fadergba = 0xFF000000;
 	exc->colormap = lighttable ? R_CreateLightTable(exc) : NULL;
 #ifdef EXTRACOLORMAPLUMPS
 	exc->lump = LUMPERROR;
@@ -553,7 +553,7 @@ boolean R_CheckDefaultColormapByValues(boolean checkrgba, boolean checkfadergba,
 				&& !flags)
 			)
 		&& (!checkrgba ? true : rgba == 0)
-		&& (!checkfadergba ? true : fadergba == 0x19000000)
+		&& (!checkfadergba ? true : (unsigned)fadergba == 0xFF000000)
 #ifdef EXTRACOLORMAPLUMPS
 		&& lump == LUMPERROR
 		&& extra_colormap->lumpname[0] == 0
@@ -654,7 +654,7 @@ extracolormap_t *R_ColormapForName(char *name)
 	if (lump == LUMPERROR)
 		I_Error("R_ColormapForName: Cannot find colormap lump %.8s\n", name);
 
-	exc = R_GetColormapFromListByValues(0, 0x19000000, 0, 31, 0, lump);
+	exc = R_GetColormapFromListByValues(0, 0xFF000000, 0, 31, 0, lump);
 	if (exc)
 		return exc;
 
@@ -674,7 +674,7 @@ extracolormap_t *R_ColormapForName(char *name)
 	exc->fadeend = 31;
 	exc->flags = 0;
 	exc->rgba = 0;
-	exc->fadergba = 0x19000000;
+	exc->fadergba = 0xFF000000;
 
 	R_AddColormapToList(exc);
 
@@ -692,9 +692,26 @@ extracolormap_t *R_ColormapForName(char *name)
 //
 static double deltas[256][3], map[256][3];
 
-static int RoundUp(double number);
+static colorlookup_t lighttable_lut;
+
+static UINT8 LightTableNearest(UINT8 r, UINT8 g, UINT8 b)
+{
+	return NearestColor(r, g, b);
+}
+
+static UINT8 LightTableNearest_LUT(UINT8 r, UINT8 g, UINT8 b)
+{
+	return GetColorLUT(&lighttable_lut, r, g, b);
+}
 
 lighttable_t *R_CreateLightTable(extracolormap_t *extra_colormap)
+{
+	extra_colormap->colormap = Z_MallocAlign((256 * 34) + 10, PU_LEVEL, NULL, 8);
+	R_GenerateLightTable(extra_colormap, false);
+	return extra_colormap->colormap;
+}
+
+void R_GenerateLightTable(extracolormap_t *extra_colormap, boolean uselookup)
 {
 	double cmaskr, cmaskg, cmaskb, cdestr, cdestg, cdestb;
 	double maskamt = 0, othermask = 0;
@@ -711,7 +728,6 @@ lighttable_t *R_CreateLightTable(extracolormap_t *extra_colormap)
 	UINT8 fadestart = extra_colormap->fadestart,
 		fadedist = extra_colormap->fadeend - extra_colormap->fadestart;
 
-	lighttable_t *lighttable = NULL;
 	size_t i;
 
 	/////////////////////
@@ -721,7 +737,7 @@ lighttable_t *R_CreateLightTable(extracolormap_t *extra_colormap)
 	cmaskg = cg;
 	cmaskb = cb;
 
-	maskamt = (double)(ca/24.0l);
+	maskamt = (double)(ca/255.0l);
 	othermask = 1 - maskamt;
 	maskamt /= 0xff;
 
@@ -737,7 +753,7 @@ lighttable_t *R_CreateLightTable(extracolormap_t *extra_colormap)
 	cdestb = cfb;
 
 	// fade alpha unused in software
-	// maskamt = (double)(cfa/24.0l);
+	// maskamt = (double)(cfa/255.0l);
 	// othermask = 1 - maskamt;
 	// maskamt /= 0xff;
 
@@ -753,6 +769,16 @@ lighttable_t *R_CreateLightTable(extracolormap_t *extra_colormap)
 		int p;
 		char *colormap_p;
 
+		UINT8 (*NearestColorFunc)(UINT8, UINT8, UINT8);
+
+		if (uselookup)
+		{
+			InitColorLUT(&lighttable_lut, pMasterPalette, false);
+			NearestColorFunc = LightTableNearest_LUT;
+		}
+		else
+			NearestColorFunc = LightTableNearest;
+
 		// Initialise the map and delta arrays
 		// map[i] stores an RGB color (as double) for index i,
 		//  which is then converted to SRB2's palette later
@@ -783,8 +809,7 @@ lighttable_t *R_CreateLightTable(extracolormap_t *extra_colormap)
 
 		// Now allocate memory for the actual colormap array itself!
 		// aligned on 8 bit for asm code
-		colormap_p = Z_MallocAlign((256 * 34) + 10, PU_LEVEL, NULL, 8);
-		lighttable = (UINT8 *)colormap_p;
+		colormap_p = (char *)extra_colormap->colormap;
 
 		// Calculate the palette index for each palette index, for each light level
 		// (as well as the two unused colormap lines we inherited from Doom)
@@ -792,9 +817,9 @@ lighttable_t *R_CreateLightTable(extracolormap_t *extra_colormap)
 		{
 			for (i = 0; i < 256; i++)
 			{
-				*colormap_p = NearestColor((UINT8)RoundUp(map[i][0]),
-					(UINT8)RoundUp(map[i][1]),
-					(UINT8)RoundUp(map[i][2]));
+				*colormap_p = NearestColorFunc((UINT8)M_RoundUp(map[i][0]),
+					(UINT8)M_RoundUp(map[i][1]),
+					(UINT8)M_RoundUp(map[i][2]));
 				colormap_p++;
 
 				if ((UINT32)p < fadestart)
@@ -818,8 +843,6 @@ lighttable_t *R_CreateLightTable(extracolormap_t *extra_colormap)
 			}
 		}
 	}
-
-	return lighttable;
 }
 
 extracolormap_t *R_CreateColormapFromLinedef(char *p1, char *p2, char *p3)
@@ -828,7 +851,7 @@ extracolormap_t *R_CreateColormapFromLinedef(char *p1, char *p2, char *p3)
 	UINT8 cr = 0, cg = 0, cb = 0, ca = 0, cfr = 0, cfg = 0, cfb = 0, cfa = 25;
 	UINT32 fadestart = 0, fadeend = 31;
 	UINT8 flags = 0;
-	INT32 rgba = 0, fadergba = 0x19000000;
+	INT32 rgba = 0, fadergba = 0xFF000000;
 
 #define HEX2INT(x) (UINT32)(x >= '0' && x <= '9' ? x - '0' : x >= 'a' && x <= 'f' ? x - 'a' + 10 : x >= 'A' && x <= 'F' ? x - 'A' + 10 : 0)
 #define ALPHA2INT(x) (x >= 'a' && x <= 'z' ? x - 'a' : x >= 'A' && x <= 'Z' ? x - 'A' : x >= '0' && x <= '9' ? 25 : 0)
@@ -836,13 +859,13 @@ extracolormap_t *R_CreateColormapFromLinedef(char *p1, char *p2, char *p3)
 	// Get base colormap value
 	// First alpha-only, then full value
 	if (p1[0] >= 'a' && p1[0] <= 'z' && !p1[1])
-		ca = (p1[0] - 'a');
+		ca = ((p1[0] - 'a') * 102) / 10;
 	else if (p1[0] == '#' && p1[1] >= 'a' && p1[1] <= 'z' && !p1[2])
-		ca = (p1[1] - 'a');
+		ca = ((p1[1] - 'a') * 102) / 10;
 	else if (p1[0] >= 'A' && p1[0] <= 'Z' && !p1[1])
-		ca = (p1[0] - 'A');
+		ca = ((p1[0] - 'A') * 102) / 10;
 	else if (p1[0] == '#' && p1[1] >= 'A' && p1[1] <= 'Z' && !p1[2])
-		ca = (p1[1] - 'A');
+		ca = ((p1[1] - 'A') * 102) / 10;
 	else if (p1[0] == '#')
 	{
 		// For each subsequent value, the value before it must exist
@@ -858,20 +881,20 @@ extracolormap_t *R_CreateColormapFromLinedef(char *p1, char *p2, char *p3)
 					cb = ((HEX2INT(p1[5]) * 16) + HEX2INT(p1[6]));
 
 					if (p1[7] >= 'a' && p1[7] <= 'z')
-						ca = (p1[7] - 'a');
+						ca = ((p1[7] - 'a') * 102) / 10;
 					else if (p1[7] >= 'A' && p1[7] <= 'Z')
-						ca = (p1[7] - 'A');
+						ca = ((p1[7] - 'A') * 102) / 10;
 					else
-						ca = 25;
+						ca = 255;
 				}
 				else
-					ca = 25;
+					ca = 255;
 			}
 			else
-				ca = 25;
+				ca = 255;
 		}
 		else
-			ca = 25;
+			ca = 255;
 	}
 
 #define NUMFROMCHAR(c) (c >= '0' && c <= '9' ? c - '0' : 0)
@@ -901,13 +924,13 @@ extracolormap_t *R_CreateColormapFromLinedef(char *p1, char *p2, char *p3)
 	// Get fade (dark) colormap value
 	// First alpha-only, then full value
 	if (p3[0] >= 'a' && p3[0] <= 'z' && !p3[1])
-		cfa = (p3[0] - 'a');
+		cfa = ((p3[0] - 'a') * 102) / 10;
 	else if (p3[0] == '#' && p3[1] >= 'a' && p3[1] <= 'z' && !p3[2])
-		cfa = (p3[1] - 'a');
+		cfa = ((p3[1] - 'a') * 102) / 10;
 	else if (p3[0] >= 'A' && p3[0] <= 'Z' && !p3[1])
-		cfa = (p3[0] - 'A');
+		cfa = ((p3[0] - 'A') * 102) / 10;
 	else if (p3[0] == '#' && p3[1] >= 'A' && p3[1] <= 'Z' && !p3[2])
-		cfa = (p3[1] - 'A');
+		cfa = ((p3[1] - 'A') * 102) / 10;
 	else if (p3[0] == '#')
 	{
 		// For each subsequent value, the value before it must exist
@@ -923,20 +946,20 @@ extracolormap_t *R_CreateColormapFromLinedef(char *p1, char *p2, char *p3)
 					cfb = ((HEX2INT(p3[5]) * 16) + HEX2INT(p3[6]));
 
 					if (p3[7] >= 'a' && p3[7] <= 'z')
-						cfa = (p3[7] - 'a');
+						cfa = ((p3[7] - 'a') * 102) / 10;
 					else if (p3[7] >= 'A' && p3[7] <= 'Z')
-						cfa = (p3[7] - 'A');
+						cfa = ((p3[7] - 'A') * 102) / 10;
 					else
-						cfa = 25;
+						cfa = 255;
 				}
 				else
-					cfa = 25;
+					cfa = 255;
 			}
 			else
-				cfa = 25;
+				cfa = 255;
 		}
 		else
-			cfa = 25;
+			cfa = 255;
 	}
 #undef ALPHA2INT
 #undef HEX2INT
@@ -1133,20 +1156,6 @@ UINT8 NearestPaletteColor(UINT8 r, UINT8 g, UINT8 b, RGBA_t *palette)
 	return (UINT8)bestcolor;
 }
 
-// Rounds off floating numbers and checks for 0 - 255 bounds
-static int RoundUp(double number)
-{
-	if (number > 255.0l)
-		return 255;
-	if (number < 0.0l)
-		return 0;
-
-	if ((int)number <= (int)(number - 0.5f))
-		return (int)number + 1;
-
-	return (int)number;
-}
-
 #ifdef EXTRACOLORMAPLUMPS
 const char *R_NameForColormap(extracolormap_t *extra_colormap)
 {
diff --git a/src/r_data.h b/src/r_data.h
index ef5c967e5ae41e4a726ae167716c05a931015dd6..364f85b6d4b0cbdd09448144f0546db9bc661bfc 100644
--- a/src/r_data.h
+++ b/src/r_data.h
@@ -92,6 +92,7 @@ typedef enum
 	TMCF_OVERRIDE     = 1<<13,
 } textmapcolormapflags_t;
 
+void R_GenerateLightTable(extracolormap_t *extra_colormap, boolean uselookup);
 lighttable_t *R_CreateLightTable(extracolormap_t *extra_colormap);
 extracolormap_t * R_CreateColormapFromLinedef(char *p1, char *p2, char *p3);
 extracolormap_t* R_CreateColormap(INT32 rgba, INT32 fadergba, UINT8 fadestart, UINT8 fadeend, UINT8 flags);
diff --git a/src/r_defs.h b/src/r_defs.h
index dfd2d6d708f8a5a9dae0da401719ad1d3eb24235..bce045ab47d910513ef98313e6b4ed24b36026ef 100644
--- a/src/r_defs.h
+++ b/src/r_defs.h
@@ -53,6 +53,9 @@ typedef struct
 // Could even use more than 32 levels.
 typedef UINT8 lighttable_t;
 
+#define NUM_PALETTE_ENTRIES 256
+#define DEFAULT_STARTTRANSCOLOR 96
+
 #define CMF_FADEFULLBRIGHTSPRITES  1
 #define CMF_FOG 4
 
@@ -215,12 +218,16 @@ typedef struct ffloor_s
 	INT16 *toplightlevel;
 	fixed_t *topxoffs;
 	fixed_t *topyoffs;
+	fixed_t *topxscale;
+	fixed_t *topyscale;
 	angle_t *topangle;
 
 	fixed_t *bottomheight;
 	INT32 *bottompic;
 	fixed_t *bottomxoffs;
 	fixed_t *bottomyoffs;
+	fixed_t *bottomxscale;
+	fixed_t *bottomyscale;
 	angle_t *bottomangle;
 
 	// Pointers to pointers. Yup.
@@ -426,6 +433,10 @@ typedef struct sector_s
 	fixed_t floorxoffset, flooryoffset;
 	fixed_t ceilingxoffset, ceilingyoffset;
 
+	// floor and ceiling texture scale
+	fixed_t floorxscale, flooryscale;
+	fixed_t ceilingxscale, ceilingyscale;
+
 	// flat angle
 	angle_t floorangle;
 	angle_t ceilingangle;
@@ -512,6 +523,8 @@ typedef enum
 #define NUMLINEARGS 10
 #define NUMLINESTRINGARGS 2
 
+#define NO_SIDEDEF 0xFFFFFFFF
+
 typedef struct line_s
 {
 	// Vertices, from v1 to v2.
@@ -529,7 +542,7 @@ typedef struct line_s
 	char *stringargs[NUMLINESTRINGARGS];
 
 	// Visual appearance: sidedefs.
-	UINT16 sidenum[2]; // sidenum[1] will be 0xffff if one-sided
+	UINT32 sidenum[2]; // sidenum[1] will be NO_SIDEDEF if one-sided
 	fixed_t alpha; // translucency
 	UINT8 blendmode; // blendmode
 	INT32 executordelay;
@@ -559,8 +572,11 @@ typedef struct
 	fixed_t rowoffset;
 
 	// per-texture offsets for UDMF
-	fixed_t offsetx_top, offsetx_mid, offsetx_bot;
-	fixed_t offsety_top, offsety_mid, offsety_bot;
+	fixed_t offsetx_top, offsetx_mid, offsetx_bottom;
+	fixed_t offsety_top, offsety_mid, offsety_bottom;
+
+	fixed_t scalex_top, scalex_mid, scalex_bottom;
+	fixed_t scaley_top, scaley_mid, scaley_bottom;
 
 	// Texture indices.
 	// We do not maintain names here.
@@ -754,10 +770,13 @@ typedef struct drawseg_s
 	fixed_t bsilheight; // do not clip sprites above this
 	fixed_t tsilheight; // do not clip sprites below this
 
+	fixed_t offsetx;
+
 	// Pointers to lists for sprite clipping, all three adjusted so [x1] is first value.
 	INT16 *sprtopclip;
 	INT16 *sprbottomclip;
 	fixed_t *maskedtexturecol;
+	fixed_t *invscale;
 
 	struct visplane_s *ffloorplanes[MAXFFLOORS];
 	INT32 numffloorplanes;
@@ -922,7 +941,7 @@ typedef struct
 	UINT16 flip;
 
 #ifdef ROTSPRITE
-	rotsprite_t *rotated[2][16]; // Rotated patches
+	rotsprite_t *rotated[16]; // Rotated patches
 #endif
 } spriteframe_t;
 
diff --git a/src/r_draw.c b/src/r_draw.c
index df9e1a4608b568706452df29bbc347adef075b01..643f843d3efb9fcbc4a8c20110e19739c992adfd 100644
--- a/src/r_draw.c
+++ b/src/r_draw.c
@@ -106,14 +106,13 @@ fixed_t ds_xfrac, ds_yfrac, ds_xstep, ds_ystep;
 INT32 ds_waterofs, ds_bgofs;
 
 UINT16 ds_flatwidth, ds_flatheight;
-boolean ds_powersoftwo, ds_solidcolor;
+boolean ds_powersoftwo, ds_solidcolor, ds_fog;
 
 UINT8 *ds_source; // points to the start of a flat
 UINT8 *ds_transmap; // one of the translucency tables
 
 // Vectors for Software's tilted slope drawers
-floatv3_t *ds_su, *ds_sv, *ds_sz;
-floatv3_t *ds_sup, *ds_svp, *ds_szp;
+floatv3_t ds_su, ds_sv, ds_sz, ds_slopelight;
 float focallengthf, zeroheight;
 
 /**	\brief Variable flat sizes
@@ -132,8 +131,6 @@ UINT32 nflatxshift, nflatyshift, nflatshiftup, nflatmask;
 #define RAINBOW_TT_CACHE_INDEX (MAXSKINS + 4)
 #define BLINK_TT_CACHE_INDEX (MAXSKINS + 5)
 #define DASHMODE_TT_CACHE_INDEX (MAXSKINS + 6)
-#define DEFAULT_STARTTRANSCOLOR 96
-#define NUM_PALETTE_ENTRIES 256
 
 static UINT8 **translationtablecache[MAXSKINS + 7] = {NULL};
 UINT8 skincolor_modified[MAXSKINCOLORS];
@@ -908,13 +905,15 @@ static void R_CalcTiltedLighting(fixed_t start, fixed_t end)
 	}
 }
 
+#define PLANELIGHTFLOAT (BASEVIDWIDTH * BASEVIDWIDTH / vid.width / zeroheight / 21.0f * FIXED_TO_FLOAT(fovtan))
+
 // Lighting is simple. It's just linear interpolation from start to end
-#define CALC_SLOPE_LIGHT { \
-	float planelightfloat = PLANELIGHTFLOAT; \
-	float lightstart, lightend; \
-	lightend = (iz + ds_szp->x*width) * planelightfloat; \
-	lightstart = iz * planelightfloat; \
-	R_CalcTiltedLighting(FloatToFixed(lightstart), FloatToFixed(lightend)); \
+static void R_CalcSlopeLight(void)
+{
+	float iz = ds_slopelight.z + ds_slopelight.y * (centery - ds_y) + ds_slopelight.x * (ds_x1 - centerx);
+	float lightstart = iz * PLANELIGHTFLOAT;
+	float lightend = (iz + ds_slopelight.x * (ds_x2 - ds_x1)) * PLANELIGHTFLOAT;
+	R_CalcTiltedLighting(FloatToFixed(lightstart), FloatToFixed(lightend));
 }
 
 // ==========================================================================
diff --git a/src/r_draw.h b/src/r_draw.h
index 0103ed82782b22c7a51beb10c20473a3e8ba3787..0f08a48bf2d729962b0fe48fe99f762d27d98ee0 100644
--- a/src/r_draw.h
+++ b/src/r_draw.h
@@ -61,7 +61,7 @@ extern fixed_t ds_xfrac, ds_yfrac, ds_xstep, ds_ystep;
 extern INT32 ds_waterofs, ds_bgofs;
 
 extern UINT16 ds_flatwidth, ds_flatheight;
-extern boolean ds_powersoftwo, ds_solidcolor;
+extern boolean ds_powersoftwo, ds_solidcolor, ds_fog;
 
 extern UINT8 *ds_source;
 extern UINT8 *ds_transmap;
@@ -71,8 +71,7 @@ typedef struct {
 } floatv3_t;
 
 // Vectors for Software's tilted slope drawers
-extern floatv3_t *ds_su, *ds_sv, *ds_sz;
-extern floatv3_t *ds_sup, *ds_svp, *ds_szp;
+extern floatv3_t ds_su, ds_sv, ds_sz, ds_slopelight;
 extern float focallengthf, zeroheight;
 
 // Variable flat sizes
@@ -178,8 +177,6 @@ void R_Draw2sMultiPatchTranslucentColumn_8(void);
 void R_DrawFogColumn_8(void);
 void R_DrawColumnShadowed_8(void);
 
-#define PLANELIGHTFLOAT (BASEVIDWIDTH * BASEVIDWIDTH / vid.width / zeroheight / 21.0f * FIXED_TO_FLOAT(fovtan))
-
 void R_DrawSpan_8(void);
 void R_DrawTranslucentSpan_8(void);
 void R_DrawTiltedSpan_8(void);
diff --git a/src/r_draw8.c b/src/r_draw8.c
index b80a47984fba4c8914fc1e75c91631aaf37ea3ea..fe7d321dfcf1a1a1c0aa6aabc8ba5169c445acdf 100644
--- a/src/r_draw8.c
+++ b/src/r_draw8.c
@@ -676,12 +676,11 @@ void R_DrawTiltedSpan_8(void)
 	double endz, endu, endv;
 	UINT32 stepu, stepv;
 
-	iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
 
-	CALC_SLOPE_LIGHT
-
-	uz = ds_sup->z + ds_sup->y*(centery-ds_y) + ds_sup->x*(ds_x1-centerx);
-	vz = ds_svp->z + ds_svp->y*(centery-ds_y) + ds_svp->x*(ds_x1-centerx);
+	R_CalcSlopeLight();
 
 	dest = ylookup[ds_y] + columnofs[ds_x1];
 	source = ds_source;
@@ -700,18 +699,18 @@ void R_DrawTiltedSpan_8(void)
 
 		*dest = colormap[source[((v >> nflatyshift) & nflatmask) | (u >> nflatxshift)]];
 		dest++;
-		iz += ds_szp->x;
-		uz += ds_sup->x;
-		vz += ds_svp->x;
+		iz += ds_sz.x;
+		uz += ds_su.x;
+		vz += ds_sv.x;
 	} while (--width >= 0);
 #else
 	startz = 1.f/iz;
 	startu = uz*startz;
 	startv = vz*startz;
 
-	izstep = ds_szp->x * SPANSIZE;
-	uzstep = ds_sup->x * SPANSIZE;
-	vzstep = ds_svp->x * SPANSIZE;
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
 	//x1 = 0;
 	width++;
 
@@ -753,9 +752,9 @@ void R_DrawTiltedSpan_8(void)
 		else
 		{
 			double left = width;
-			iz += ds_szp->x * left;
-			uz += ds_sup->x * left;
-			vz += ds_svp->x * left;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
 
 			endz = 1.f/iz;
 			endu = uz*endz;
@@ -799,12 +798,11 @@ void R_DrawTiltedTranslucentSpan_8(void)
 	double endz, endu, endv;
 	UINT32 stepu, stepv;
 
-	iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
-
-	CALC_SLOPE_LIGHT
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
 
-	uz = ds_sup->z + ds_sup->y*(centery-ds_y) + ds_sup->x*(ds_x1-centerx);
-	vz = ds_svp->z + ds_svp->y*(centery-ds_y) + ds_svp->x*(ds_x1-centerx);
+	R_CalcSlopeLight();
 
 	dest = ylookup[ds_y] + columnofs[ds_x1];
 	source = ds_source;
@@ -822,18 +820,18 @@ void R_DrawTiltedTranslucentSpan_8(void)
 		colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 		*dest = *(ds_transmap + (colormap[source[((v >> nflatyshift) & nflatmask) | (u >> nflatxshift)]] << 8) + *dest);
 		dest++;
-		iz += ds_szp->x;
-		uz += ds_sup->x;
-		vz += ds_svp->x;
+		iz += ds_sz.x;
+		uz += ds_su.x;
+		vz += ds_sv.x;
 	} while (--width >= 0);
 #else
 	startz = 1.f/iz;
 	startu = uz*startz;
 	startv = vz*startz;
 
-	izstep = ds_szp->x * SPANSIZE;
-	uzstep = ds_sup->x * SPANSIZE;
-	vzstep = ds_svp->x * SPANSIZE;
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
 	//x1 = 0;
 	width++;
 
@@ -875,9 +873,9 @@ void R_DrawTiltedTranslucentSpan_8(void)
 		else
 		{
 			double left = width;
-			iz += ds_szp->x * left;
-			uz += ds_sup->x * left;
-			vz += ds_svp->x * left;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
 
 			endz = 1.f/iz;
 			endu = uz*endz;
@@ -922,12 +920,11 @@ void R_DrawTiltedWaterSpan_8(void)
 	double endz, endu, endv;
 	UINT32 stepu, stepv;
 
-	iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
 
-	CALC_SLOPE_LIGHT
-
-	uz = ds_sup->z + ds_sup->y*(centery-ds_y) + ds_sup->x*(ds_x1-centerx);
-	vz = ds_svp->z + ds_svp->y*(centery-ds_y) + ds_svp->x*(ds_x1-centerx);
+	R_CalcSlopeLight();
 
 	dest = ylookup[ds_y] + columnofs[ds_x1];
 	dsrc = screens[1] + (ds_y+ds_bgofs)*vid.width + ds_x1;
@@ -946,18 +943,18 @@ void R_DrawTiltedWaterSpan_8(void)
 		colormap = planezlight[tiltlighting[ds_x1++]] + (ds_colormap - colormaps);
 		*dest = *(ds_transmap + (colormap[source[((v >> nflatyshift) & nflatmask) | (u >> nflatxshift)]] << 8) + *dsrc++);
 		dest++;
-		iz += ds_szp->x;
-		uz += ds_sup->x;
-		vz += ds_svp->x;
+		iz += ds_sz.x;
+		uz += ds_su.x;
+		vz += ds_sv.x;
 	} while (--width >= 0);
 #else
 	startz = 1.f/iz;
 	startu = uz*startz;
 	startv = vz*startz;
 
-	izstep = ds_szp->x * SPANSIZE;
-	uzstep = ds_sup->x * SPANSIZE;
-	vzstep = ds_svp->x * SPANSIZE;
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
 	//x1 = 0;
 	width++;
 
@@ -999,9 +996,9 @@ void R_DrawTiltedWaterSpan_8(void)
 		else
 		{
 			double left = width;
-			iz += ds_szp->x * left;
-			uz += ds_sup->x * left;
-			vz += ds_svp->x * left;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
 
 			endz = 1.f/iz;
 			endu = uz*endz;
@@ -1044,12 +1041,11 @@ void R_DrawTiltedSplat_8(void)
 	double endz, endu, endv;
 	UINT32 stepu, stepv;
 
-	iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
-
-	CALC_SLOPE_LIGHT
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
 
-	uz = ds_sup->z + ds_sup->y*(centery-ds_y) + ds_sup->x*(ds_x1-centerx);
-	vz = ds_svp->z + ds_svp->y*(centery-ds_y) + ds_svp->x*(ds_x1-centerx);
+	R_CalcSlopeLight();
 
 	dest = ylookup[ds_y] + columnofs[ds_x1];
 	source = ds_source;
@@ -1071,18 +1067,18 @@ void R_DrawTiltedSplat_8(void)
 			*dest = colormap[val];
 
 		dest++;
-		iz += ds_szp->x;
-		uz += ds_sup->x;
-		vz += ds_svp->x;
+		iz += ds_sz.x;
+		uz += ds_su.x;
+		vz += ds_sv.x;
 	} while (--width >= 0);
 #else
 	startz = 1.f/iz;
 	startu = uz*startz;
 	startv = vz*startz;
 
-	izstep = ds_szp->x * SPANSIZE;
-	uzstep = ds_sup->x * SPANSIZE;
-	vzstep = ds_svp->x * SPANSIZE;
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
 	//x1 = 0;
 	width++;
 
@@ -1128,9 +1124,9 @@ void R_DrawTiltedSplat_8(void)
 		else
 		{
 			double left = width;
-			iz += ds_szp->x * left;
-			uz += ds_sup->x * left;
-			vz += ds_svp->x * left;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
 
 			endz = 1.f/iz;
 			endu = uz*endz;
@@ -1613,9 +1609,9 @@ void R_DrawTiltedFloorSprite_8(void)
 	double endz, endu, endv;
 	UINT32 stepu, stepv;
 
-	iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
-	uz = ds_sup->z + ds_sup->y*(centery-ds_y) + ds_sup->x*(ds_x1-centerx);
-	vz = ds_svp->z + ds_svp->y*(centery-ds_y) + ds_svp->x*(ds_x1-centerx);
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
 
 	dest = ylookup[ds_y] + columnofs[ds_x1];
 	source = (UINT16 *)ds_source;
@@ -1626,9 +1622,9 @@ void R_DrawTiltedFloorSprite_8(void)
 	startu = uz*startz;
 	startv = vz*startz;
 
-	izstep = ds_szp->x * SPANSIZE;
-	uzstep = ds_sup->x * SPANSIZE;
-	vzstep = ds_svp->x * SPANSIZE;
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
 	//x1 = 0;
 	width++;
 
@@ -1673,9 +1669,9 @@ void R_DrawTiltedFloorSprite_8(void)
 		else
 		{
 			double left = width;
-			iz += ds_szp->x * left;
-			uz += ds_sup->x * left;
-			vz += ds_svp->x * left;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
 
 			endz = 1.f/iz;
 			endu = uz*endz;
@@ -1722,9 +1718,9 @@ void R_DrawTiltedTranslucentFloorSprite_8(void)
 	double endz, endu, endv;
 	UINT32 stepu, stepv;
 
-	iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
-	uz = ds_sup->z + ds_sup->y*(centery-ds_y) + ds_sup->x*(ds_x1-centerx);
-	vz = ds_svp->z + ds_svp->y*(centery-ds_y) + ds_svp->x*(ds_x1-centerx);
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
 
 	dest = ylookup[ds_y] + columnofs[ds_x1];
 	source = (UINT16 *)ds_source;
@@ -1735,9 +1731,9 @@ void R_DrawTiltedTranslucentFloorSprite_8(void)
 	startu = uz*startz;
 	startv = vz*startz;
 
-	izstep = ds_szp->x * SPANSIZE;
-	uzstep = ds_sup->x * SPANSIZE;
-	vzstep = ds_svp->x * SPANSIZE;
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
 	//x1 = 0;
 	width++;
 
@@ -1782,9 +1778,9 @@ void R_DrawTiltedTranslucentFloorSprite_8(void)
 		else
 		{
 			double left = width;
-			iz += ds_szp->x * left;
-			uz += ds_sup->x * left;
-			vz += ds_svp->x * left;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
 
 			endz = 1.f/iz;
 			endu = uz*endz;
@@ -2013,9 +2009,7 @@ void R_DrawTiltedFogSpan_8(void)
 
 	UINT8 *dest = ylookup[ds_y] + columnofs[ds_x1];
 
-	double iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
-
-	CALC_SLOPE_LIGHT
+	R_CalcSlopeLight();
 
 	do
 	{
@@ -2067,9 +2061,7 @@ void R_DrawTiltedSolidColorSpan_8(void)
 	UINT8 source = ds_source[0];
 	UINT8 *dest = ylookup[ds_y] + columnofs[ds_x1];
 
-	double iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
-
-	CALC_SLOPE_LIGHT
+	R_CalcSlopeLight();
 
 	do
 	{
@@ -2088,9 +2080,7 @@ void R_DrawTiltedTransSolidColorSpan_8(void)
 	UINT8 source = ds_source[0];
 	UINT8 *dest = ylookup[ds_y] + columnofs[ds_x1];
 
-	double iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
-
-	CALC_SLOPE_LIGHT
+	R_CalcSlopeLight();
 
 	do
 	{
@@ -2131,9 +2121,7 @@ void R_DrawTiltedWaterSolidColorSpan_8(void)
 	UINT8 *dest = ylookup[ds_y] + columnofs[ds_x1];
 	UINT8 *dsrc = screens[1] + (ds_y+ds_bgofs)*vid.width + ds_x1;
 
-	double iz = ds_szp->z + ds_szp->y*(centery-ds_y) + ds_szp->x*(ds_x1-centerx);
-
-	CALC_SLOPE_LIGHT
+	R_CalcSlopeLight();
 
 	do
 	{
diff --git a/src/r_draw8_npo2.c b/src/r_draw8_npo2.c
index 91f3b06c4270ba28901d35b561f68a002ddbc6f3..78cde8a2ce195678333916f38a1b51bf5ba8e898 100644
--- a/src/r_draw8_npo2.c
+++ b/src/r_draw8_npo2.c
@@ -114,12 +114,11 @@ void R_DrawTiltedSpan_NPO2_8(void)
 	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);
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
 
-	CALC_SLOPE_LIGHT
-
-	uz = ds_sup->z + ds_sup->y*(centery-ds_y) + ds_sup->x*(ds_x1-centerx);
-	vz = ds_svp->z + ds_svp->y*(centery-ds_y) + ds_svp->x*(ds_x1-centerx);
+	R_CalcSlopeLight();
 
 	dest = ylookup[ds_y] + columnofs[ds_x1];
 	source = ds_source;
@@ -154,18 +153,18 @@ void R_DrawTiltedSpan_NPO2_8(void)
 			*dest = colormap[source[((y * ds_flatwidth) + x)]];
 		}
 		dest++;
-		iz += ds_szp->x;
-		uz += ds_sup->x;
-		vz += ds_svp->x;
+		iz += ds_sz.x;
+		uz += ds_su.x;
+		vz += ds_sv.x;
 	} while (--width >= 0);
 #else
 	startz = 1.f/iz;
 	startu = uz*startz;
 	startv = vz*startz;
 
-	izstep = ds_szp->x * SPANSIZE;
-	uzstep = ds_sup->x * SPANSIZE;
-	vzstep = ds_svp->x * SPANSIZE;
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
 	//x1 = 0;
 	width++;
 
@@ -239,9 +238,9 @@ void R_DrawTiltedSpan_NPO2_8(void)
 		else
 		{
 			double left = width;
-			iz += ds_szp->x * left;
-			uz += ds_sup->x * left;
-			vz += ds_svp->x * left;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
 
 			endz = 1.f/iz;
 			endu = uz*endz;
@@ -304,12 +303,11 @@ void R_DrawTiltedTranslucentSpan_NPO2_8(void)
 	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);
-
-	CALC_SLOPE_LIGHT
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
 
-	uz = ds_sup->z + ds_sup->y*(centery-ds_y) + ds_sup->x*(ds_x1-centerx);
-	vz = ds_svp->z + ds_svp->y*(centery-ds_y) + ds_svp->x*(ds_x1-centerx);
+	R_CalcSlopeLight();
 
 	dest = ylookup[ds_y] + columnofs[ds_x1];
 	source = ds_source;
@@ -343,18 +341,18 @@ void R_DrawTiltedTranslucentSpan_NPO2_8(void)
 			*dest = *(ds_transmap + (colormap[source[((y * ds_flatwidth) + x)]] << 8) + *dest);
 		}
 		dest++;
-		iz += ds_szp->x;
-		uz += ds_sup->x;
-		vz += ds_svp->x;
+		iz += ds_sz.x;
+		uz += ds_su.x;
+		vz += ds_sv.x;
 	} while (--width >= 0);
 #else
 	startz = 1.f/iz;
 	startu = uz*startz;
 	startv = vz*startz;
 
-	izstep = ds_szp->x * SPANSIZE;
-	uzstep = ds_sup->x * SPANSIZE;
-	vzstep = ds_svp->x * SPANSIZE;
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
 	//x1 = 0;
 	width++;
 
@@ -428,9 +426,9 @@ void R_DrawTiltedTranslucentSpan_NPO2_8(void)
 		else
 		{
 			double left = width;
-			iz += ds_szp->x * left;
-			uz += ds_sup->x * left;
-			vz += ds_svp->x * left;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
 
 			endz = 1.f/iz;
 			endu = uz*endz;
@@ -492,12 +490,11 @@ void R_DrawTiltedSplat_NPO2_8(void)
 	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);
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
 
-	CALC_SLOPE_LIGHT
-
-	uz = ds_sup->z + ds_sup->y*(centery-ds_y) + ds_sup->x*(ds_x1-centerx);
-	vz = ds_svp->z + ds_svp->y*(centery-ds_y) + ds_svp->x*(ds_x1-centerx);
+	R_CalcSlopeLight();
 
 	dest = ylookup[ds_y] + columnofs[ds_x1];
 	source = ds_source;
@@ -536,18 +533,18 @@ void R_DrawTiltedSplat_NPO2_8(void)
 			*dest = colormap[val];
 
 		dest++;
-		iz += ds_szp->x;
-		uz += ds_sup->x;
-		vz += ds_svp->x;
+		iz += ds_sz.x;
+		uz += ds_su.x;
+		vz += ds_sv.x;
 	} while (--width >= 0);
 #else
 	startz = 1.f/iz;
 	startu = uz*startz;
 	startv = vz*startz;
 
-	izstep = ds_szp->x * SPANSIZE;
-	uzstep = ds_sup->x * SPANSIZE;
-	vzstep = ds_svp->x * SPANSIZE;
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
 	//x1 = 0;
 	width++;
 
@@ -625,9 +622,9 @@ void R_DrawTiltedSplat_NPO2_8(void)
 		else
 		{
 			double left = width;
-			iz += ds_szp->x * left;
-			uz += ds_sup->x * left;
-			vz += ds_svp->x * left;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
 
 			endz = 1.f/iz;
 			endu = uz*endz;
@@ -970,9 +967,9 @@ void R_DrawTiltedFloorSprite_NPO2_8(void)
 	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);
-	uz = ds_sup->z + ds_sup->y*(centery-ds_y) + ds_sup->x*(ds_x1-centerx);
-	vz = ds_svp->z + ds_svp->y*(centery-ds_y) + ds_svp->x*(ds_x1-centerx);
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
 
 	dest = ylookup[ds_y] + columnofs[ds_x1];
 	source = (UINT16 *)ds_source;
@@ -983,9 +980,9 @@ void R_DrawTiltedFloorSprite_NPO2_8(void)
 	startu = uz*startz;
 	startv = vz*startz;
 
-	izstep = ds_szp->x * SPANSIZE;
-	uzstep = ds_sup->x * SPANSIZE;
-	vzstep = ds_svp->x * SPANSIZE;
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
 	//x1 = 0;
 	width++;
 
@@ -1060,9 +1057,9 @@ void R_DrawTiltedFloorSprite_NPO2_8(void)
 		else
 		{
 			double left = width;
-			iz += ds_szp->x * left;
-			uz += ds_sup->x * left;
-			vz += ds_svp->x * left;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
 
 			endz = 1.f/iz;
 			endu = uz*endz;
@@ -1126,9 +1123,9 @@ void R_DrawTiltedTranslucentFloorSprite_NPO2_8(void)
 	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);
-	uz = ds_sup->z + ds_sup->y*(centery-ds_y) + ds_sup->x*(ds_x1-centerx);
-	vz = ds_svp->z + ds_svp->y*(centery-ds_y) + ds_svp->x*(ds_x1-centerx);
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
 
 	dest = ylookup[ds_y] + columnofs[ds_x1];
 	source = (UINT16 *)ds_source;
@@ -1139,9 +1136,9 @@ void R_DrawTiltedTranslucentFloorSprite_NPO2_8(void)
 	startu = uz*startz;
 	startv = vz*startz;
 
-	izstep = ds_szp->x * SPANSIZE;
-	uzstep = ds_sup->x * SPANSIZE;
-	vzstep = ds_svp->x * SPANSIZE;
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
 	//x1 = 0;
 	width++;
 
@@ -1216,9 +1213,9 @@ void R_DrawTiltedTranslucentFloorSprite_NPO2_8(void)
 		else
 		{
 			double left = width;
-			iz += ds_szp->x * left;
-			uz += ds_sup->x * left;
-			vz += ds_svp->x * left;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
 
 			endz = 1.f/iz;
 			endu = uz*endz;
@@ -1411,12 +1408,11 @@ void R_DrawTiltedWaterSpan_NPO2_8(void)
 	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);
-
-	CALC_SLOPE_LIGHT
+	iz = ds_sz.z + ds_sz.y*(centery-ds_y) + ds_sz.x*(ds_x1-centerx);
+	uz = ds_su.z + ds_su.y*(centery-ds_y) + ds_su.x*(ds_x1-centerx);
+	vz = ds_sv.z + ds_sv.y*(centery-ds_y) + ds_sv.x*(ds_x1-centerx);
 
-	uz = ds_sup->z + ds_sup->y*(centery-ds_y) + ds_sup->x*(ds_x1-centerx);
-	vz = ds_svp->z + ds_svp->y*(centery-ds_y) + ds_svp->x*(ds_x1-centerx);
+	R_CalcSlopeLight();
 
 	dest = ylookup[ds_y] + columnofs[ds_x1];
 	dsrc = screens[1] + (ds_y+ds_bgofs)*vid.width + ds_x1;
@@ -1451,18 +1447,18 @@ void R_DrawTiltedWaterSpan_NPO2_8(void)
 			*dest = *(ds_transmap + (colormap[source[((y * ds_flatwidth) + x)]] << 8) + *dsrc++);
 		}
 		dest++;
-		iz += ds_szp->x;
-		uz += ds_sup->x;
-		vz += ds_svp->x;
+		iz += ds_sz.x;
+		uz += ds_su.x;
+		vz += ds_sv.x;
 	} while (--width >= 0);
 #else
 	startz = 1.f/iz;
 	startu = uz*startz;
 	startv = vz*startz;
 
-	izstep = ds_szp->x * SPANSIZE;
-	uzstep = ds_sup->x * SPANSIZE;
-	vzstep = ds_svp->x * SPANSIZE;
+	izstep = ds_sz.x * SPANSIZE;
+	uzstep = ds_su.x * SPANSIZE;
+	vzstep = ds_sv.x * SPANSIZE;
 	//x1 = 0;
 	width++;
 
@@ -1536,9 +1532,9 @@ void R_DrawTiltedWaterSpan_NPO2_8(void)
 		else
 		{
 			double left = width;
-			iz += ds_szp->x * left;
-			uz += ds_sup->x * left;
-			vz += ds_svp->x * left;
+			iz += ds_sz.x * left;
+			uz += ds_su.x * left;
+			vz += ds_sv.x * left;
 
 			endz = 1.f/iz;
 			endu = uz*endz;
diff --git a/src/r_main.c b/src/r_main.c
index 54f7d7639e775f73b1d8de57d9923745eb9ff493..125f80b7849cad91786cc394f1da8a958ad4f8cc 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -158,7 +158,7 @@ consvar_t cv_drawdist = CVAR_INIT ("drawdist", "Infinite", CV_SAVE, drawdist_con
 consvar_t cv_drawdist_nights = CVAR_INIT ("drawdist_nights", "2048", CV_SAVE, drawdist_cons_t, NULL);
 consvar_t cv_drawdist_precip = CVAR_INIT ("drawdist_precip", "1024", CV_SAVE, drawdist_precip_cons_t, NULL);
 //consvar_t cv_precipdensity = CVAR_INIT ("precipdensity", "Moderate", CV_SAVE, precipdensity_cons_t, NULL);
-consvar_t cv_fov = CVAR_INIT ("fov", "90", CV_FLOAT|CV_CALL, fov_cons_t, Fov_OnChange);
+consvar_t cv_fov = CVAR_INIT ("fov", "90", CV_SAVE|CV_FLOAT|CV_CALL, fov_cons_t, Fov_OnChange);
 
 // Okay, whoever said homremoval causes a performance hit should be shot.
 consvar_t cv_homremoval = CVAR_INIT ("homremoval", "No", CV_SAVE, homremoval_cons_t, NULL);
@@ -249,7 +249,7 @@ static void FlipCam2_OnChange(void)
 //
 // killough 5/2/98: reformatted
 //
-INT32 R_PointOnSide(fixed_t x, fixed_t y, node_t *node)
+INT32 R_PointOnSide(fixed_t x, fixed_t y, node_t *restrict node)
 {
 	if (!node->dx)
 		return x <= node->x ? node->dy > 0 : node->dy < 0;
@@ -261,9 +261,10 @@ INT32 R_PointOnSide(fixed_t x, fixed_t y, node_t *node)
 	fixed_t dy = (y >> 1) - (node->y >> 1);
 
 	// Try to quickly decide by looking at sign bits.
-	if ((node->dy ^ node->dx ^ dx ^ dy) < 0)
-		return (node->dy ^ dx) < 0;  // (left is negative)
-	return FixedMul(dy, node->dx>>FRACBITS) >= FixedMul(node->dy>>FRACBITS, dx);
+	// also use a mask to avoid branch prediction
+	INT32 mask = (node->dy ^ node->dx ^ dx ^ dy) >> 31;
+	return (mask & ((node->dy ^ dx) < 0)) |  // (left is negative)
+		(~mask & (FixedMul(dy, node->dx>>FRACBITS) >= FixedMul(node->dy>>FRACBITS, dx)));
 }
 
 // killough 5/2/98: reformatted
@@ -355,7 +356,7 @@ angle_t R_PointToAngle2(fixed_t pviewx, fixed_t pviewy, fixed_t x, fixed_t y)
 fixed_t R_PointToDist2(fixed_t px2, fixed_t py2, fixed_t px1, fixed_t py1)
 {
 	angle_t angle;
-	fixed_t dx, dy, dist;
+	ufixed_t dx, dy, dist;
 
 	dx = abs(px1 - px2);
 	dy = abs(py1 - py2);
@@ -956,16 +957,6 @@ void R_ExecuteSetViewSize(void)
 			dy = FixedMul(abs(dy), fovtan);
 			yslopetab[i] = FixedDiv(centerx*FRACUNIT, dy);
 		}
-
-		if (ds_su)
-			Z_Free(ds_su);
-		if (ds_sv)
-			Z_Free(ds_sv);
-		if (ds_sz)
-			Z_Free(ds_sz);
-
-		ds_su = ds_sv = ds_sz = NULL;
-		ds_sup = ds_svp = ds_szp = NULL;
 	}
 
 	memset(scalelight, 0xFF, sizeof(scalelight));
@@ -1011,9 +1002,6 @@ void R_Init(void)
 	R_InitViewBorder();
 	R_SetViewSize(); // setsizeneeded is set true
 
-	//I_OutputMsg("\nR_InitPlanes");
-	R_InitPlanes();
-
 	// this is now done by SCR_Recalc() at the first mode set
 	//I_OutputMsg("\nR_InitLightTables");
 	R_InitLightTables();
@@ -1026,6 +1014,34 @@ void R_Init(void)
 	framecount = 0;
 }
 
+//
+// R_IsPointInSector
+//
+boolean R_IsPointInSector(sector_t *sector, fixed_t x, fixed_t y)
+{
+	size_t i;
+	line_t *closest = NULL;
+	fixed_t closestdist = INT32_MAX;
+
+	for (i = 0; i < sector->linecount; i++)
+	{
+		vertex_t v;
+		fixed_t dist;
+
+		// find the line closest to the point we're looking for.
+		P_ClosestPointOnLine(x, y, sector->lines[i], &v);
+		dist = R_PointToDist2(0, 0, v.x - x, v.y - y);
+		if (dist < closestdist)
+		{
+			closest = sector->lines[i];
+			closestdist = dist;
+		}
+	}
+
+	// if the side of the closest line is in this sector, we're inside of it.
+	return P_PointOnLineSide(x, y, closest) == 0 ? closest->frontsector == sector : closest->backsector == sector;
+}
+
 //
 // R_PointInSubsector
 //
@@ -1084,6 +1100,7 @@ void R_SetupFrame(player_t *player)
 {
 	camera_t *thiscam;
 	boolean chasecam = R_ViewpointHasChasecam(player);
+	boolean ispaused = paused || P_AutoPause();
 	
 	if (splitscreen && player == &players[secondarydisplayplayer] && player != &players[consoleplayer])
 		thiscam = &camera2;
@@ -1134,6 +1151,30 @@ void R_SetupFrame(player_t *player)
 			}
 		}
 	}
+
+	if (quake.time && !ispaused)
+	{
+		fixed_t ir = quake.intensity>>1;
+
+		if (quake.epicenter) {
+			// Calculate 3D distance from epicenter, using the camera.
+			fixed_t xydist = R_PointToDist2(thiscam->x, thiscam->y, quake.epicenter->x, quake.epicenter->y);
+			fixed_t dist = R_PointToDist2(0, thiscam->z, xydist, quake.epicenter->z);
+
+			// More effect closer to epicenter, outside of radius = no effect
+			if (!quake.radius || dist > quake.radius)
+				ir = 0;
+			else
+				ir = FixedMul(ir, FRACUNIT - FixedDiv(dist, quake.radius));
+		}
+
+		quake.x = M_RandomRange(-ir,ir);
+		quake.y = M_RandomRange(-ir,ir);
+		quake.z = M_RandomRange(-ir,ir);
+	}
+	else if (!ispaused)
+		quake.x = quake.y = quake.z = 0;
+
 	newview->z += quake.z;
 
 	newview->player = player;
diff --git a/src/r_main.h b/src/r_main.h
index a6fb42ba2410ba42c8adc7ba84ef8fde21e0802f..02c640b51f55e36f219156d2957faec14c2bb333 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -79,6 +79,7 @@ fixed_t R_PointToDist(fixed_t x, fixed_t y);
 fixed_t R_PointToDist2(fixed_t px2, fixed_t py2, fixed_t px1, fixed_t py1);
 
 fixed_t R_ScaleFromGlobalAngle(angle_t visangle);
+boolean R_IsPointInSector(sector_t *sector, fixed_t x, fixed_t y);
 subsector_t *R_PointInSubsector(fixed_t x, fixed_t y);
 subsector_t *R_PointInSubsectorOrNull(fixed_t x, fixed_t y);
 
diff --git a/src/r_patch.h b/src/r_patch.h
index a0ab3e75ac0c81a7fa16b6129cd2deabb8d02d46..cc41639b3a1913706c21b3041044aab84d70ff06 100644
--- a/src/r_patch.h
+++ b/src/r_patch.h
@@ -37,7 +37,7 @@ patch_t *Patch_GetRotated(patch_t *patch, INT32 angle, boolean flip);
 patch_t *Patch_GetRotatedSprite(
 	spriteframe_t *sprite,
 	size_t frame, size_t spriteangle,
-	boolean flip, boolean adjustfeet,
+	boolean flip,
 	void *info, INT32 rotationangle);
 angle_t R_ModelRotationAngle(interpmobjstate_t *interp);
 angle_t R_SpriteRotationAngle(interpmobjstate_t *interp);
diff --git a/src/r_patchrotation.c b/src/r_patchrotation.c
index b0cbeaa42911d1506d9928b906198afdf50ff0be..b9106984970f728e83e567627daa4c1b9c478011 100644
--- a/src/r_patchrotation.c
+++ b/src/r_patchrotation.c
@@ -10,7 +10,7 @@
 /// \brief Patch rotation.
 
 #include "r_patchrotation.h"
-#include "r_things.h" // FEETADJUST
+#include "r_things.h" // FEETADJUST (todo: is this needed anymore? -- Monster Iestyn 21 Sep 2023 )
 #include "z_zone.h"
 #include "w_wad.h"
 #include "r_main.h" // R_PointToAngle
@@ -66,23 +66,20 @@ patch_t *Patch_GetRotated(patch_t *patch, INT32 angle, boolean flip)
 patch_t *Patch_GetRotatedSprite(
 	spriteframe_t *sprite,
 	size_t frame, size_t spriteangle,
-	boolean flip, boolean adjustfeet,
+	boolean flip,
 	void *info, INT32 rotationangle)
 {
-	rotsprite_t *rotsprite;
+	rotsprite_t *rotsprite = sprite->rotated[spriteangle];
 	spriteinfo_t *sprinfo = (spriteinfo_t *)info;
 	INT32 idx = rotationangle;
-	UINT8 type = (adjustfeet ? 1 : 0);
 
 	if (rotationangle < 1 || rotationangle >= ROTANGLES)
 		return NULL;
 
-	rotsprite = sprite->rotated[type][spriteangle];
-
 	if (rotsprite == NULL)
 	{
 		rotsprite = RotatedPatch_Create(ROTANGLES);
-		sprite->rotated[type][spriteangle] = rotsprite;
+		sprite->rotated[spriteangle] = rotsprite;
 	}
 
 	if (flip)
@@ -111,10 +108,6 @@ patch_t *Patch_GetRotatedSprite(
 		}
 
 		RotatedPatch_DoRotation(rotsprite, patch, rotationangle, xpivot, ypivot, flip);
-
-		//BP: we cannot use special tric in hardware mode because feet in ground caused by z-buffer
-		if (adjustfeet)
-			((patch_t *)rotsprite->patches[idx])->topoffset += FEETADJUST>>FRACBITS;
 	}
 
 	return rotsprite->patches[idx];
diff --git a/src/r_plane.c b/src/r_plane.c
index 29ce26b292e5ea529d937aded0028e41a8d294ba..08e147c89b8f8a7e290af44b4272630aa6109a98 100644
--- a/src/r_plane.c
+++ b/src/r_plane.c
@@ -83,22 +83,15 @@ static fixed_t planeheight;
 fixed_t yslopetab[MAXVIDHEIGHT*16];
 fixed_t *yslope;
 
-fixed_t cachedheight[MAXVIDHEIGHT];
-fixed_t cacheddistance[MAXVIDHEIGHT];
-fixed_t cachedxstep[MAXVIDHEIGHT];
-fixed_t cachedystep[MAXVIDHEIGHT];
-
 static fixed_t xoffs, yoffs;
-static floatv3_t ds_slope_origin, ds_slope_u, ds_slope_v;
+static floatv3_t slope_origin, slope_u, slope_v;
+static floatv3_t slope_lightu, slope_lightv;
 
-//
-// R_InitPlanes
-// Only at game startup.
-//
-void R_InitPlanes(void)
-{
-	// FIXME: unused
-}
+static void CalcSlopePlaneVectors(visplane_t *pl, fixed_t xoff, fixed_t yoff);
+static void CalcSlopeLightVectors(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t height, float ang, angle_t plangle);
+
+static void DoSlopeCrossProducts(void);
+static void DoSlopeLightCrossProduct(void);
 
 //
 // Water ripple effect
@@ -159,37 +152,28 @@ static void R_MapPlane(INT32 y, INT32 x1, INT32 x2)
 	planecos = FINECOSINE(angle);
 	planesin = FINESINE(angle);
 
-	if (planeheight != cachedheight[y])
-	{
-		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 = ds_ystep = FRACUNIT;
+	// [RH] Notice that I dumped the caching scheme used by Doom.
+	// It did not offer any appreciable speedup.
+	distance = FixedMul(planeheight, yslope[y]);
+	span = abs(centery - y);
 
-		cachedxstep[y] = ds_xstep;
-		cachedystep[y] = ds_ystep;
-	}
-	else
+	if (span) // Don't divide by zero
 	{
-		distance = cacheddistance[y];
-		ds_xstep = cachedxstep[y];
-		ds_ystep = cachedystep[y];
+		ds_xstep = FixedMul(planesin, planeheight) / span;
+		ds_ystep = FixedMul(planecos, planeheight) / span;
+		ds_xstep = FixedMul(currentplane->xscale, ds_xstep);
+		ds_ystep = FixedMul(currentplane->yscale, ds_ystep);
 	}
+	else
+		ds_xstep = ds_ystep = FRACUNIT;
 
 	// [RH] Instead of using the xtoviewangle array, I calculated the fractional values
 	// at the middle of the screen, then used the calculated ds_xstep and ds_ystep
 	// to step from those to the proper texture coordinate to start drawing at.
 	// That way, the texture coordinate is always calculated by its position
 	// on the screen and not by its position relative to the edge of the visplane.
-	ds_xfrac = xoffs + FixedMul(planecos, distance) + (x1 - centerx) * ds_xstep;
-	ds_yfrac = yoffs - FixedMul(planesin, distance) + (x1 - centerx) * ds_ystep;
+	ds_xfrac = xoffs + FixedMul(currentplane->xscale, FixedMul(planecos, distance)) + (x1 - centerx) * ds_xstep;
+	ds_yfrac = yoffs - FixedMul(currentplane->yscale, FixedMul(planesin, distance)) + (x1 - centerx) * ds_ystep;
 
 	// Water ripple effect
 	if (planeripple.active)
@@ -238,9 +222,9 @@ static void R_MapTiltedPlane(INT32 y, INT32 x1, INT32 x2)
 	{
 		ds_bgofs = R_CalculateRippleOffset(y);
 
-		ds_sup = &ds_su[y];
-		ds_svp = &ds_sv[y];
-		ds_szp = &ds_sz[y];
+		R_CalculatePlaneRipple(currentplane->viewangle + currentplane->plangle);
+
+		CalcSlopePlaneVectors(currentplane, (xoffs + planeripple.xfrac), (yoffs + planeripple.yfrac));
 
 		ds_bgofs >>= FRACBITS;
 
@@ -275,10 +259,7 @@ static void R_MapFogPlane(INT32 y, INT32 x1, INT32 x2)
 	if (x1 >= vid.width)
 		x1 = vid.width - 1;
 
-	if (planeheight != cachedheight[y])
-		distance = FixedMul(planeheight, yslope[y]);
-	else
-		distance = cacheddistance[y];
+	distance = FixedMul(planeheight, yslope[y]);
 
 	pindex = distance >> LIGHTZSHIFT;
 	if (pindex >= MAXLIGHTZ)
@@ -361,9 +342,6 @@ void R_ClearPlanes(void)
 	{
 		freehead = &(*freehead)->next;
 	}
-
-	// texture calculation
-	memset(cachedheight, 0, sizeof (cachedheight));
 }
 
 static visplane_t *new_visplane(unsigned hash)
@@ -391,7 +369,8 @@ static visplane_t *new_visplane(unsigned hash)
 //              If not, allocates another of them.
 //
 visplane_t *R_FindPlane(fixed_t height, INT32 picnum, INT32 lightlevel,
-	fixed_t xoff, fixed_t yoff, angle_t plangle, extracolormap_t *planecolormap,
+	fixed_t xoff, fixed_t yoff, fixed_t xscale, fixed_t yscale,
+	angle_t plangle, extracolormap_t *planecolormap,
 	ffloor_t *pfloor, polyobj_t *polyobj, pslope_t *slope)
 {
 	visplane_t *check;
@@ -399,8 +378,9 @@ visplane_t *R_FindPlane(fixed_t height, INT32 picnum, INT32 lightlevel,
 
 	if (!slope) // Don't mess with this right now if a slope is involved
 	{
-		xoff += viewx;
-		yoff -= viewy;
+		xoff += FixedMul(viewx, xscale);
+		yoff -= FixedMul(viewy, yscale);
+
 		if (plangle != 0)
 		{
 			// Add the view offset, rotated by the plane angle.
@@ -441,16 +421,16 @@ visplane_t *R_FindPlane(fixed_t height, INT32 picnum, INT32 lightlevel,
 		hash = visplane_hash(picnum, lightlevel, height);
 		for (check = visplanes[hash]; check; check = check->next)
 		{
-			if (polyobj != check->polyobj)
-				continue;
 			if (height == check->height && picnum == check->picnum
 				&& lightlevel == check->lightlevel
 				&& xoff == check->xoffs && yoff == check->yoffs
+				&& xscale == check->xscale && yscale == check->yscale
 				&& planecolormap == check->extra_colormap
 				&& check->viewx == viewx && check->viewy == viewy && check->viewz == viewz
 				&& check->viewangle == viewangle
 				&& check->plangle == plangle
-				&& check->slope == slope)
+				&& check->slope == slope
+				&& check->polyobj == polyobj)
 			{
 				return check;
 			}
@@ -470,6 +450,8 @@ visplane_t *R_FindPlane(fixed_t height, INT32 picnum, INT32 lightlevel,
 	check->maxx = -1;
 	check->xoffs = xoff;
 	check->yoffs = yoff;
+	check->xscale = xscale;
+	check->yscale = yscale;
 	check->extra_colormap = planecolormap;
 	check->ffloor = pfloor;
 	check->viewx = viewx;
@@ -546,6 +528,8 @@ visplane_t *R_CheckPlane(visplane_t *pl, INT32 start, INT32 stop)
 		new_pl->lightlevel = pl->lightlevel;
 		new_pl->xoffs = pl->xoffs;
 		new_pl->yoffs = pl->yoffs;
+		new_pl->xscale = pl->xscale;
+		new_pl->yscale = pl->yscale;
 		new_pl->extra_colormap = pl->extra_colormap;
 		new_pl->ffloor = pl->ffloor;
 		new_pl->viewx = pl->viewx;
@@ -686,8 +670,6 @@ static INT64 R_GetSlopeZAt(const pslope_t *slope, fixed_t x, fixed_t y)
 // 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;
 
@@ -695,125 +677,164 @@ static void R_SetSlopePlaneOrigin(pslope_t *slope, fixed_t xpos, fixed_t ypos, f
 	float vyf = vy / (float)FRACUNIT;
 	float ang = ANG2RAD(ANGLE_270 - angle);
 
-	// p is the texture origin in view space
+	// slope_origin 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.
-	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;
+	slope_origin.x = vxf * cos(ang) - vyf * sin(ang);
+	slope_origin.z = vxf * sin(ang) + vyf * cos(ang);
+	slope_origin.y = (R_GetSlopeZAt(slope, -xoff, yoff) - zpos) / (float)FRACUNIT;
 }
 
 // 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;
+	fixed_t height, z_at_xy;
 	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 - (angle + plangle));
-	m->x = cos(ang);
-	m->z = sin(ang);
 
-	// n is the u direction vector in view space
-	n->x = sin(ang);
-	n->z = -cos(ang);
+	CalcSlopeLightVectors(slope, xpos, ypos, height, ang, plangle);
+
+	if (ds_solidcolor || ds_fog)
+	{
+		DoSlopeLightCrossProduct();
+		return;
+	}
+
+	// slope_v is the v direction vector in view space
+	slope_v.x = cos(ang);
+	slope_v.z = sin(ang);
+
+	// slope_u is the u direction vector in view space
+	slope_u.x = sin(ang);
+	slope_u.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);
+	z_at_xy = P_GetSlopeZAt(slope, xpos + FINESINE(plangle), ypos + FINECOSINE(plangle));
+	slope_v.y = FixedToFloat(z_at_xy - height);
+	z_at_xy = P_GetSlopeZAt(slope, xpos + FINECOSINE(plangle), ypos - FINESINE(plangle));
+	slope_u.y = FixedToFloat(z_at_xy - height);
+
+	DoSlopeCrossProducts();
+	DoSlopeLightCrossProduct();
 }
 
 // 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;
+	fixed_t height, z_at_xy;
 
-	float xscale = FixedToFloat(xs);
-	float yscale = FixedToFloat(ys);
 	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 - (angle + plangle));
-	m->x = yscale * cos(ang);
-	m->z = yscale * sin(ang);
+
+	CalcSlopeLightVectors(slope, xpos, ypos, height, ang, plangle);
+
+	if (ds_solidcolor || ds_fog)
+	{
+		DoSlopeLightCrossProduct();
+		return;
+	}
+
+	float xscale = FixedToFloat(xs);
+	float yscale = FixedToFloat(ys);
+
+	// m is the v direction vector in view space
+	slope_v.x = yscale * cos(ang);
+	slope_v.z = yscale * sin(ang);
 
 	// n is the u direction vector in view space
-	n->x = xscale * sin(ang);
-	n->z = -xscale * cos(ang);
+	slope_u.x = xscale * sin(ang);
+	slope_u.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);
+	z_at_xy = P_GetSlopeZAt(slope, xpos + FloatToFixed(yscale * sin(ang)), ypos + FloatToFixed(yscale * cos(ang)));
+	slope_v.y = FixedToFloat(z_at_xy - height);
+	z_at_xy = P_GetSlopeZAt(slope, xpos + FloatToFixed(xscale * cos(ang)), ypos - FloatToFixed(xscale * sin(ang)));
+	slope_u.y = FixedToFloat(z_at_xy - height);
+
+	DoSlopeCrossProducts();
+	DoSlopeLightCrossProduct();
 }
 
-void R_CalculateSlopeVectors(void)
+static void CalcSlopeLightVectors(pslope_t *slope, fixed_t xpos, fixed_t ypos, fixed_t height, float ang, angle_t plangle)
 {
-	float sfmult = 65536.f;
+	fixed_t z_at_xy;
 
-	// 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.
+	slope_lightv.x = cos(ang);
+	slope_lightv.z = sin(ang);
+
+	slope_lightu.x = sin(ang);
+	slope_lightu.z = -cos(ang);
+
+	plangle >>= ANGLETOFINESHIFT;
+	z_at_xy = P_GetSlopeZAt(slope, xpos + FINESINE(plangle), ypos + FINECOSINE(plangle));
+	slope_lightv.y = FixedToFloat(z_at_xy - height);
+	z_at_xy = P_GetSlopeZAt(slope, xpos + FINECOSINE(plangle), ypos - FINESINE(plangle));
+	slope_lightu.y = FixedToFloat(z_at_xy - height);
+}
+
+// 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, 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
+d.x = (v1.y * v2.z) - (v1.z * v2.y);\
+d.y = (v1.z * v2.x) - (v1.x * v2.z);\
+d.z = (v1.x * v2.y) - (v1.y * v2.x)
+
+static void DoSlopeCrossProducts(void)
+{
+	float sfmult = 65536.f;
 
-	ds_sup->z *= focallengthf;
-	ds_svp->z *= focallengthf;
-	ds_szp->z *= focallengthf;
+	CROSS(ds_su, slope_origin, slope_v);
+	CROSS(ds_sv, slope_origin, slope_u);
+	CROSS(ds_sz, slope_v, slope_u);
+
+	ds_su.z *= focallengthf;
+	ds_sv.z *= focallengthf;
+	ds_sz.z *= focallengthf;
 
 	if (ds_solidcolor)
 		return;
 
 	// Premultiply the texture vectors with the scale factors
 	if (ds_powersoftwo)
-		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;
+		sfmult *= 1 << nflatshiftup;
+
+	ds_su.x *= sfmult;
+	ds_su.y *= sfmult;
+	ds_su.z *= sfmult;
+	ds_sv.x *= sfmult;
+	ds_sv.y *= sfmult;
+	ds_sv.z *= sfmult;
 }
 
-void R_SetTiltedSpan(INT32 span)
+static void DoSlopeLightCrossProduct(void)
 {
-	if (ds_su == NULL)
-		ds_su = Z_Malloc(sizeof(*ds_su) * vid.height, PU_STATIC, NULL);
-	if (ds_sv == NULL)
-		ds_sv = Z_Malloc(sizeof(*ds_sv) * vid.height, PU_STATIC, NULL);
-	if (ds_sz == NULL)
-		ds_sz = Z_Malloc(sizeof(*ds_sz) * vid.height, PU_STATIC, NULL);
-
-	ds_sup = &ds_su[span];
-	ds_svp = &ds_sv[span];
-	ds_szp = &ds_sz[span];
+	CROSS(ds_slopelight, slope_lightv, slope_lightu);
+
+	ds_slopelight.z *= focallengthf;
 }
 
-static void R_SetSlopePlaneVectors(visplane_t *pl, INT32 y, fixed_t xoff, fixed_t yoff)
+#undef CROSS
+
+static void CalcSlopePlaneVectors(visplane_t *pl, fixed_t xoff, fixed_t yoff)
 {
-	R_SetTiltedSpan(y);
-	R_SetSlopePlane(pl->slope, pl->viewx, pl->viewy, pl->viewz, xoff, yoff, pl->viewangle, pl->plangle);
-	R_CalculateSlopeVectors();
+	if (!ds_fog && (pl->xscale != FRACUNIT || pl->yscale != FRACUNIT))
+	{
+		R_SetScaledSlopePlane(pl->slope, pl->viewx, pl->viewy, pl->viewz,
+			FixedDiv(FRACUNIT, pl->xscale), FixedDiv(FRACUNIT, pl->yscale),
+			FixedDiv(xoff, pl->xscale), FixedDiv(yoff, pl->yscale), pl->viewangle, pl->plangle);
+	}
+	else
+		R_SetSlopePlane(pl->slope, pl->viewx, pl->viewy, pl->viewz, xoff, yoff, pl->viewangle, pl->plangle);
 }
 
 static inline void R_AdjustSlopeCoordinates(vector3_t *origin)
@@ -850,7 +871,6 @@ void R_DrawSinglePlane(visplane_t *pl)
 	INT32 light = 0;
 	INT32 x, stop;
 	ffloor_t *rover;
-	boolean fog = false;
 	INT32 spanfunctype = BASEDRAWFUNC;
 	void (*mapfunc)(INT32, INT32, INT32);
 
@@ -864,6 +884,8 @@ void R_DrawSinglePlane(visplane_t *pl)
 		return;
 	}
 
+	ds_powersoftwo = ds_solidcolor = ds_fog = false;
+
 	planeripple.active = false;
 
 	if (pl->polyobj)
@@ -928,13 +950,13 @@ void R_DrawSinglePlane(visplane_t *pl)
 			}
 			else if (pl->ffloor->fofflags & FOF_FOG)
 			{
-				fog = true;
+				ds_fog = true;
 				spanfunctype = SPANDRAWFUNC_FOG;
 				light = (pl->lightlevel >> LIGHTSEGSHIFT);
 			}
 			else light = (pl->lightlevel >> LIGHTSEGSHIFT);
 
-			if (pl->ffloor->fofflags & FOF_RIPPLE && !fog)
+			if (pl->ffloor->fofflags & FOF_RIPPLE && !ds_fog)
 			{
 				planeripple.active = true;
 
@@ -962,9 +984,7 @@ void R_DrawSinglePlane(visplane_t *pl)
 			light = (pl->lightlevel >> LIGHTSEGSHIFT);
 	}
 
-	ds_powersoftwo = ds_solidcolor = false;
-
-	if (fog)
+	if (ds_fog)
 	{
 		// Since all fog planes do is apply a colormap, it's not required
 		// to know any information about their textures.
@@ -1000,13 +1020,6 @@ void R_DrawSinglePlane(visplane_t *pl)
 				}
 		}
 
-		// Don't mess with angle on slopes! We'll handle this ourselves later
-		if (!pl->slope && viewangle != pl->viewangle+pl->plangle)
-		{
-			memset(cachedheight, 0, sizeof (cachedheight));
-			viewangle = pl->viewangle+pl->plangle;
-		}
-
 		mapfunc = R_MapPlane;
 
 		if (ds_solidcolor)
@@ -1037,13 +1050,13 @@ void R_DrawSinglePlane(visplane_t *pl)
 
 	if (pl->slope)
 	{
-		if (fog)
+		if (ds_fog)
 			mapfunc = R_MapTiltedFogPlane;
 		else
 		{
 			mapfunc = R_MapTiltedPlane;
 
-			if (!pl->plangle && !ds_solidcolor)
+			if (!pl->plangle && !ds_solidcolor && pl->xscale == FRACUNIT && pl->yscale == FRACUNIT)
 			{
 				if (ds_powersoftwo)
 					R_AdjustSlopeCoordinates(&pl->slope->o);
@@ -1052,21 +1065,10 @@ void R_DrawSinglePlane(visplane_t *pl)
 			}
 		}
 
-		if (planeripple.active)
-		{
+		if (!ds_fog && planeripple.active)
 			planeheight = abs(P_GetSlopeZAt(pl->slope, pl->viewx, pl->viewy) - pl->viewz);
-
-			R_PlaneBounds(pl);
-
-			for (x = pl->high; x < pl->low; x++)
-			{
-				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);
+			CalcSlopePlaneVectors(pl, xoffs, yoffs);
 
 		switch (spanfunctype)
 		{
diff --git a/src/r_plane.h b/src/r_plane.h
index 917e8b041b75775016dbc2a1e10b8cc6ad7a3778..5ce49e3cc9f2789b113076b64677125f68aca2c1 100644
--- a/src/r_plane.h
+++ b/src/r_plane.h
@@ -49,6 +49,7 @@ typedef struct visplane_s
 	INT32 high, low; // R_PlaneBounds should set these.
 
 	fixed_t xoffs, yoffs; // Scrolling flats.
+	fixed_t xscale, yscale;
 
 	struct ffloor_s *ffloor;
 	polyobj_t *polyobj;
@@ -62,21 +63,16 @@ extern visplane_t *ceilingplane;
 // Visplane related.
 extern INT16 floorclip[MAXVIDWIDTH], ceilingclip[MAXVIDWIDTH];
 extern fixed_t frontscale[MAXVIDWIDTH], yslopetab[MAXVIDHEIGHT*16];
-extern fixed_t cachedheight[MAXVIDHEIGHT];
-extern fixed_t cacheddistance[MAXVIDHEIGHT];
-extern fixed_t cachedxstep[MAXVIDHEIGHT];
-extern fixed_t cachedystep[MAXVIDHEIGHT];
 
 extern fixed_t *yslope;
 extern lighttable_t **planezlight;
 
-void R_InitPlanes(void);
 void R_ClearPlanes(void);
 void R_ClearFFloorClips (void);
 
 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);
+visplane_t *R_FindPlane(fixed_t height, INT32 picnum, INT32 lightlevel, fixed_t xoff, fixed_t yoff, fixed_t xscale, fixed_t yscale,
+	angle_t plangle, extracolormap_t *planecolormap, ffloor_t *ffloor, polyobj_t *polyobj, pslope_t *slope);
 visplane_t *R_CheckPlane(visplane_t *pl, INT32 start, INT32 stop);
 void R_ExpandPlane(visplane_t *pl, INT32 start, INT32 stop);
 void R_PlaneBounds(visplane_t *plane);
@@ -87,10 +83,6 @@ void R_DrawSinglePlane(visplane_t *pl);
 // Calculates the slope vectors needed for tilted span drawing.
 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);
 
 typedef struct planemgr_s
 {
diff --git a/src/r_segs.c b/src/r_segs.c
index 9af83f0c7ff8af10057f253a5419dca41c86638c..ecc49fbc23ec80e7100c82dbd957fb453735f643 100644
--- a/src/r_segs.c
+++ b/src/r_segs.c
@@ -20,7 +20,7 @@
 
 #include "w_wad.h"
 #include "z_zone.h"
-#include "d_netcmd.h"
+#include "netcode/d_netcmd.h"
 #include "m_misc.h"
 #include "p_local.h" // Camera...
 #include "p_slopes.h"
@@ -48,15 +48,18 @@ fixed_t rw_distance;
 //
 static INT32 rw_x, rw_stopx;
 static angle_t rw_centerangle;
-static fixed_t rw_offset;
-static fixed_t rw_offset_top, rw_offset_mid, rw_offset_bot;
-static fixed_t rw_offset2; // for splats
+static fixed_t rw_offset, rw_offsetx;
+static fixed_t rw_offset_top, rw_offset_mid, rw_offset_bottom;
 static fixed_t rw_scale, rw_scalestep;
 static fixed_t rw_midtexturemid, rw_toptexturemid, rw_bottomtexturemid;
 static INT32 worldtop, worldbottom, worldhigh, worldlow;
 static INT32 worldtopslope, worldbottomslope, worldhighslope, worldlowslope; // worldtop/bottom at end of slope
 static fixed_t rw_toptextureslide, rw_midtextureslide, rw_bottomtextureslide; // Defines how to adjust Y offsets along the wall for slopes
 static fixed_t rw_midtextureback, rw_midtexturebackslide; // Values for masked midtexture height calculation
+static fixed_t rw_midtexturescalex, rw_midtexturescaley;
+static fixed_t rw_toptexturescalex, rw_toptexturescaley;
+static fixed_t rw_bottomtexturescalex, rw_bottomtexturescaley;
+static fixed_t rw_invmidtexturescalex, rw_invtoptexturescalex, rw_invbottomtexturescalex;
 
 // Lactozilla: 3D floor clipping
 static boolean rw_floormarked = false;
@@ -71,8 +74,10 @@ static fixed_t topfrac, topstep;
 static fixed_t bottomfrac, bottomstep;
 
 static lighttable_t **walllights;
-static fixed_t *maskedtexturecol;
+static fixed_t *maskedtexturecol = NULL;
 static fixed_t *maskedtextureheight = NULL;
+static fixed_t *thicksidecol = NULL;
+static fixed_t *invscale = NULL;
 
 //SoM: 3/23/2000: Use boom opening limit removal
 static size_t numopenings;
@@ -162,7 +167,8 @@ void R_RenderMaskedSegRange(drawseg_t *ds, INT32 x1, INT32 x2)
 
 	frontsector = curline->frontsector;
 	backsector = curline->backsector;
-	texnum = R_GetTextureNum(curline->sidedef->midtexture);
+	sidedef = curline->sidedef;
+	texnum = R_GetTextureNum(sidedef->midtexture);
 	windowbottom = windowtop = sprbotscreen = INT32_MAX;
 
 	ldef = curline->linedef;
@@ -200,9 +206,13 @@ void R_RenderMaskedSegRange(drawseg_t *ds, INT32 x1, INT32 x2)
 		colfunc = colfuncs[COLDRAWFUNC_FUZZY];
 	}
 
+	fixed_t wall_scaley = sidedef->scaley_mid;
+	fixed_t scalestep = FixedDiv(ds->scalestep, wall_scaley);
+	fixed_t scale1 = FixedDiv(ds->scale1, wall_scaley);
+
 	range = max(ds->x2-ds->x1, 1);
-	rw_scalestep = ds->scalestep;
-	spryscale = ds->scale1 + (x1 - ds->x1)*rw_scalestep;
+	rw_scalestep = scalestep;
+	spryscale = scale1 + (x1 - ds->x1)*rw_scalestep;
 
 	// Texture must be cached before setting colfunc_2s,
 	// otherwise texture[texnum]->holes may be false when it shouldn't be
@@ -313,8 +323,8 @@ void R_RenderMaskedSegRange(drawseg_t *ds, INT32 x1, INT32 x2)
 	else
 		back = backsector;
 
-	if (ds->curline->sidedef->repeatcnt)
-		repeats = 1 + ds->curline->sidedef->repeatcnt;
+	if (sidedef->repeatcnt)
+		repeats = 1 + sidedef->repeatcnt;
 	else if (ldef->flags & ML_WRAPMIDTEX)
 	{
 		fixed_t high, low;
@@ -340,15 +350,14 @@ void R_RenderMaskedSegRange(drawseg_t *ds, INT32 x1, INT32 x2)
 	{
 		if (times > 0)
 		{
-			rw_scalestep = ds->scalestep;
-			spryscale = ds->scale1 + (x1 - ds->x1)*rw_scalestep;
-			if (dc_numlights)
-			{ // reset all lights to their starting heights
-				for (i = 0; i < dc_numlights; i++)
-				{
-					rlight = &dc_lightlist[i];
-					rlight->height = rlight->startheight;
-				}
+			rw_scalestep = scalestep;
+			spryscale = scale1 + (x1 - ds->x1)*rw_scalestep;
+
+			// reset all lights to their starting heights
+			for (i = 0; i < dc_numlights; i++)
+			{
+				rlight = &dc_lightlist[i];
+				rlight->height = rlight->startheight;
 			}
 		}
 
@@ -390,8 +399,10 @@ void R_RenderMaskedSegRange(drawseg_t *ds, INT32 x1, INT32 x2)
 				sprbotscreen = INT32_MAX;
 				sprtopscreen = windowtop = (centeryfrac - FixedMul(dc_texturemid, spryscale));
 
-				realbot = windowbottom = FixedMul(textureheight[texnum], spryscale) + sprtopscreen;
-				dc_iscale = 0xffffffffu / (unsigned)spryscale;
+				realbot = FixedMul(textureheight[texnum], spryscale) + sprtopscreen;
+				dc_iscale = FixedMul(ds->invscale[dc_x], wall_scaley);
+
+				windowbottom = realbot;
 
 				// draw the texture
 				col = (column_t *)((UINT8 *)R_GetColumn(texnum, (maskedtexturecol[dc_x] >> FRACBITS)) - 3);
@@ -466,7 +477,7 @@ void R_RenderMaskedSegRange(drawseg_t *ds, INT32 x1, INT32 x2)
 				dc_colormap = frontsector->extra_colormap->colormap + (dc_colormap - colormaps);
 
 			sprtopscreen = centeryfrac - FixedMul(dc_texturemid, spryscale);
-			dc_iscale = 0xffffffffu / (unsigned)spryscale;
+			dc_iscale = FixedMul(ds->invscale[dc_x], wall_scaley);
 
 			// draw the texture
 			col = (column_t *)((UINT8 *)R_GetColumn(texnum, (maskedtexturecol[dc_x] >> FRACBITS)) - 3);
@@ -525,7 +536,7 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 	INT32             i, p;
 	fixed_t         bottombounds = viewheight << FRACBITS;
 	fixed_t         topbounds = (con_clipviewtop - 1) << FRACBITS;
-	fixed_t         offsetvalue = 0;
+	fixed_t         offsetvalue;
 	lightlist_t     *light;
 	r_lightlist_t   *rlight;
 	INT32           range;
@@ -534,11 +545,13 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 	// NOTE: INT64 instead of fixed_t because overflow concerns
 	INT64         top_frac, top_step, bottom_frac, bottom_step;
 	// skew FOF walls with slopes?
-	boolean	      slopeskew = false;
 	fixed_t       ffloortextureslide = 0;
 	INT32         oldx = -1;
 	fixed_t       left_top, left_bottom; // needed here for slope skewing
 	pslope_t      *skewslope = NULL;
+	boolean do_texture_skew;
+	UINT32 lineflags;
+	fixed_t wall_scalex, wall_scaley;
 
 	void (*colfunc_2s) (column_t *);
 
@@ -550,7 +563,7 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 	curline = ds->curline;
 	backsector = pfloor->target;
 	frontsector = curline->frontsector == pfloor->target ? curline->backsector : curline->frontsector;
-	texnum = R_GetTextureNum(sides[pfloor->master->sidenum[0]].midtexture);
+	sidedef = &sides[pfloor->master->sidenum[0]];
 
 	colfunc = colfuncs[BASEDRAWFUNC];
 
@@ -558,8 +571,13 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 	{
 		size_t linenum = curline->linedef-backsector->lines[0];
 		newline = pfloor->master->frontsector->lines[0] + linenum;
-		texnum = R_GetTextureNum(sides[newline->sidenum[0]].midtexture);
+		sidedef = &sides[newline->sidenum[0]];
+		lineflags = newline->flags;
 	}
+	else
+		lineflags = pfloor->master->flags;
+
+	texnum = R_GetTextureNum(sidedef->midtexture);
 
 	if (pfloor->fofflags & FOF_TRANSLUCENT)
 	{
@@ -711,7 +729,13 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 			walllights = scalelight[lightnum];
 	}
 
-	maskedtexturecol = ds->thicksidecol;
+	wall_scalex = FixedDiv(FRACUNIT, sidedef->scalex_mid);
+	wall_scaley = sidedef->scaley_mid;
+
+	thicksidecol = ds->thicksidecol;
+
+	for (INT32 x = x1; x <= x2; x++)
+		thicksidecol[x] = FixedDiv(thicksidecol[x], wall_scalex) + ds->offsetx;
 
 	mfloorclip = ds->sprbottomclip;
 	mceilingclip = ds->sprtopclip;
@@ -721,51 +745,29 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 	left_top    = P_GetFFloorTopZAt   (pfloor, ds->leftpos.x, ds->leftpos.y) - viewz;
 	left_bottom = P_GetFFloorBottomZAt(pfloor, ds->leftpos.x, ds->leftpos.y) - viewz;
 
+	do_texture_skew = lineflags & ML_SKEWTD;
 	skewslope = *pfloor->t_slope; // skew using top slope by default
-	if (newline)
-	{
-		if (newline->flags & ML_SKEWTD)
-			slopeskew = true;
-	}
-	else if (pfloor->master->flags & ML_SKEWTD)
-		slopeskew = true;
 
-	if (slopeskew)
-		dc_texturemid = left_top;
+	if (do_texture_skew)
+		dc_texturemid = FixedMul(left_top, wall_scaley);
 	else
-		dc_texturemid = *pfloor->topheight - viewz;
+		dc_texturemid = FixedMul(*pfloor->topheight - viewz, wall_scaley);
 
-	if (newline)
-	{
-		offsetvalue = sides[newline->sidenum[0]].rowoffset + sides[newline->sidenum[0]].offsety_mid;
-		if (newline->flags & ML_DONTPEGBOTTOM)
-		{
-			skewslope = *pfloor->b_slope; // skew using bottom slope
-			if (slopeskew)
-				dc_texturemid = left_bottom;
-			else
-				offsetvalue -= *pfloor->topheight - *pfloor->bottomheight;
-		}
-	}
-	else
+	offsetvalue = sidedef->rowoffset + sidedef->offsety_mid;
+
+	if (lineflags & ML_DONTPEGBOTTOM)
 	{
-		offsetvalue = sides[pfloor->master->sidenum[0]].rowoffset + sides[pfloor->master->sidenum[0]].offsety_mid;
-		if (curline->linedef->flags & ML_DONTPEGBOTTOM)
-		{
-			skewslope = *pfloor->b_slope; // skew using bottom slope
-			if (slopeskew)
-				dc_texturemid = left_bottom;
-			else
-				offsetvalue -= *pfloor->topheight - *pfloor->bottomheight;
-		}
+		skewslope = *pfloor->b_slope; // skew using bottom slope
+		if (do_texture_skew)
+			dc_texturemid = FixedMul(left_bottom, wall_scaley);
+		else
+			offsetvalue -= FixedMul(*pfloor->topheight - *pfloor->bottomheight, wall_scaley);
 	}
 
-	if (slopeskew)
+	if (do_texture_skew && skewslope)
 	{
 		angle_t lineangle = R_PointToAngle2(curline->v1->x, curline->v1->y, curline->v2->x, curline->v2->y);
-
-		if (skewslope)
-			ffloortextureslide = FixedMul(skewslope->zdelta, FINECOSINE((lineangle-skewslope->xydirection)>>ANGLETOFINESHIFT));
+		ffloortextureslide = FixedMul(skewslope->zdelta, FINECOSINE((lineangle-skewslope->xydirection)>>ANGLETOFINESHIFT));
 	}
 
 	dc_texturemid += offsetvalue;
@@ -819,7 +821,7 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 		if (ffloortextureslide)
 		{
 			if (oldx != -1)
-				dc_texturemid += FixedMul(ffloortextureslide, maskedtexturecol[oldx]-maskedtexturecol[dc_x]);
+				dc_texturemid += FixedMul(ffloortextureslide, thicksidecol[oldx]-thicksidecol[dc_x]);
 			oldx = dc_x;
 		}
 
@@ -852,10 +854,10 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 			continue;
 		}
 
-		dc_iscale = 0xffffffffu / (unsigned)spryscale;
+		dc_iscale = FixedMul(0xffffffffu / (unsigned)spryscale, wall_scaley);
 
 		// Get data for the column
-		col = (column_t *)((UINT8 *)R_GetColumn(texnum, (maskedtexturecol[dc_x] >> FRACBITS)) - 3);
+		col = (column_t *)((UINT8 *)R_GetColumn(texnum, (thicksidecol[dc_x] >> FRACBITS)) - 3);
 
 		// SoM: New code does not rely on R_DrawColumnShadowed_8 which
 		// will (hopefully) put less strain on the stack.
@@ -1044,13 +1046,18 @@ UINT32 nombre = 100000;
 static void R_RenderSegLoop (void)
 {
 	angle_t angle;
+	fixed_t textureoffset;
 	size_t  pindex;
 	INT32     yl;
 	INT32     yh;
 
 	INT32     mid;
 	fixed_t texturecolumn = 0;
+	fixed_t toptexturecolumn = 0;
+	fixed_t bottomtexturecolumn = 0;
 	fixed_t oldtexturecolumn = -1;
+	fixed_t oldtexturecolumn_top = -1;
+	fixed_t oldtexturecolumn_bottom = -1;
 	INT32     top;
 	INT32     bottom;
 	INT32     i;
@@ -1217,17 +1224,8 @@ static void R_RenderSegLoop (void)
 		//SoM: Calculate offsets for Thick fake floors.
 		// calculate texture offset
 		angle = (rw_centerangle + xtoviewangle[rw_x])>>ANGLETOFINESHIFT;
-		texturecolumn = rw_offset-FixedMul(FINETANGENT(angle),rw_distance);
-
-		if (oldtexturecolumn != -1) {
-			rw_bottomtexturemid += FixedMul(rw_bottomtextureslide,  oldtexturecolumn-texturecolumn);
-			rw_midtexturemid    += FixedMul(rw_midtextureslide,     oldtexturecolumn-texturecolumn);
-			rw_toptexturemid    += FixedMul(rw_toptextureslide,     oldtexturecolumn-texturecolumn);
-			rw_midtextureback   += FixedMul(rw_midtexturebackslide, oldtexturecolumn-texturecolumn);
-		}
-		oldtexturecolumn = texturecolumn;
-
-		INT32 itexturecolumn = texturecolumn >> FRACBITS;
+		textureoffset = rw_offset - FixedMul(FINETANGENT(angle), rw_distance);
+		texturecolumn = FixedDiv(textureoffset, rw_invmidtexturescalex);
 
 		// texturecolumn and lighting are independent of wall tiers
 		if (segtextured)
@@ -1240,7 +1238,6 @@ static void R_RenderSegLoop (void)
 
 			dc_colormap = walllights[pindex];
 			dc_x = rw_x;
-			dc_iscale = 0xffffffffu / (unsigned)rw_scale;
 
 			if (frontsector->extra_colormap)
 				dc_colormap = frontsector->extra_colormap->colormap + (dc_colormap - colormaps);
@@ -1290,10 +1287,13 @@ static void R_RenderSegLoop (void)
 			// single sided line
 			if (yl <= yh && yh >= 0 && yl < viewheight)
 			{
+				fixed_t offset = texturecolumn + rw_offsetx;
+
 				dc_yl = yl;
 				dc_yh = yh;
 				dc_texturemid = rw_midtexturemid;
-				dc_source = R_GetColumn(midtexture, itexturecolumn + (rw_offset_mid>>FRACBITS));
+				dc_iscale = FixedMul(0xffffffffu / (unsigned)rw_scale, rw_midtexturescaley);
+				dc_source = R_GetColumn(midtexture, offset >> FRACBITS);
 				dc_texheight = textureheight[midtexture]>>FRACBITS;
 
 				//profile stuff ---------------------------------------------------------
@@ -1342,6 +1342,8 @@ static void R_RenderSegLoop (void)
 				if (mid >= floorclip[rw_x])
 					mid = floorclip[rw_x]-1;
 
+				toptexturecolumn = FixedDiv(textureoffset, rw_invtoptexturescalex);
+
 				if (mid >= yl) // back ceiling lower than front ceiling ?
 				{
 					if (yl >= viewheight) // entirely off bottom of screen
@@ -1351,10 +1353,16 @@ static void R_RenderSegLoop (void)
 					}
 					else if (mid >= 0) // safe to draw top texture
 					{
+						fixed_t offset = rw_offset_top;
+						if (rw_toptexturescalex < 0)
+							offset = -offset;
+						offset = toptexturecolumn + offset;
+
 						dc_yl = yl;
 						dc_yh = mid;
 						dc_texturemid = rw_toptexturemid;
-						dc_source = R_GetColumn(toptexture, itexturecolumn + (rw_offset_top>>FRACBITS));
+						dc_iscale = FixedMul(0xffffffffu / (unsigned)rw_scale, rw_toptexturescaley);
+						dc_source = R_GetColumn(toptexture, offset >> FRACBITS);
 						dc_texheight = textureheight[toptexture]>>FRACBITS;
 						colfunc();
 						ceilingclip[rw_x] = (INT16)mid;
@@ -1364,6 +1372,10 @@ static void R_RenderSegLoop (void)
 				}
 				else if (!rw_ceilingmarked)
 					ceilingclip[rw_x] = topclip;
+
+				if (oldtexturecolumn_top != -1)
+					rw_toptexturemid += FixedMul(rw_toptextureslide, oldtexturecolumn_top-toptexturecolumn);
+				oldtexturecolumn_top = toptexturecolumn;
 			}
 			else if (markceiling && (!rw_ceilingmarked)) // no top wall
 				ceilingclip[rw_x] = topclip;
@@ -1378,6 +1390,8 @@ static void R_RenderSegLoop (void)
 				if (mid <= ceilingclip[rw_x])
 					mid = ceilingclip[rw_x]+1;
 
+				bottomtexturecolumn = FixedDiv(textureoffset, rw_invbottomtexturescalex);
+
 				if (mid <= yh) // back floor higher than front floor ?
 				{
 					if (yh < 0) // entirely off top of screen
@@ -1387,10 +1401,16 @@ static void R_RenderSegLoop (void)
 					}
 					else if (mid < viewheight) // safe to draw bottom texture
 					{
+						fixed_t offset = rw_offset_bottom;
+						if (rw_bottomtexturescalex < 0)
+							offset = -offset;
+						offset = bottomtexturecolumn + offset;
+
 						dc_yl = mid;
 						dc_yh = yh;
 						dc_texturemid = rw_bottomtexturemid;
-						dc_source = R_GetColumn(bottomtexture, itexturecolumn + (rw_offset_bot>>FRACBITS));
+						dc_iscale = FixedMul(0xffffffffu / (unsigned)rw_scale, rw_bottomtexturescaley);
+						dc_source = R_GetColumn(bottomtexture, offset >> FRACBITS);
 						dc_texheight = textureheight[bottomtexture]>>FRACBITS;
 						colfunc();
 						floorclip[rw_x] = (INT16)mid;
@@ -1400,24 +1420,43 @@ static void R_RenderSegLoop (void)
 				}
 				else if (!rw_floormarked)
 					floorclip[rw_x] = bottomclip;
+
+				if (oldtexturecolumn_bottom != -1)
+					rw_bottomtexturemid += FixedMul(rw_bottomtextureslide, oldtexturecolumn_bottom-bottomtexturecolumn);
+				oldtexturecolumn_bottom = bottomtexturecolumn;
 			}
 			else if (markfloor && (!rw_floormarked)) // no bottom wall
 				floorclip[rw_x] = bottomclip;
 		}
 
-		if (maskedtexture || numthicksides)
+		if (maskedtexturecol)
+			maskedtexturecol[rw_x] = texturecolumn + rw_offsetx;
+
+		if (thicksidecol)
+			thicksidecol[rw_x] = textureoffset;
+
+		if (maskedtextureheight)
+		{
+			if (curline->linedef->flags & ML_MIDPEG)
+				maskedtextureheight[rw_x] = max(rw_midtexturemid, rw_midtextureback);
+			else
+				maskedtextureheight[rw_x] = min(rw_midtexturemid, rw_midtextureback);
+		}
+
+		if (midtexture || maskedtextureheight)
 		{
-			// save texturecol
-			//  for backdrawing of masked mid texture
-			maskedtexturecol[rw_x] = texturecolumn + rw_offset_mid;
-
-			if (maskedtextureheight != NULL) {
-				maskedtextureheight[rw_x] = (curline->linedef->flags & ML_MIDPEG) ?
-											max(rw_midtexturemid, rw_midtextureback) :
-											min(rw_midtexturemid, rw_midtextureback);
+			if (oldtexturecolumn != -1)
+			{
+				rw_midtexturemid += FixedMul(rw_midtextureslide, oldtexturecolumn-texturecolumn);
+				rw_midtextureback += FixedMul(rw_midtexturebackslide, oldtexturecolumn-texturecolumn);
 			}
+
+			oldtexturecolumn = texturecolumn;
 		}
 
+		if (invscale)
+			invscale[rw_x] = 0xffffffffu / (unsigned)rw_scale;
+
 		if (dc_numlights)
 		{
 			for (i = 0; i < dc_numlights; i++)
@@ -1506,10 +1545,9 @@ static void R_AllocClippingTables(size_t range)
 static void R_AllocTextureColumnTables(size_t range)
 {
 	size_t pos = curtexturecolumntable - texturecolumntable;
+	size_t need = range * 3;
 
-	// For both tables, we reserve exactly an amount of memory that's equivalent to
-	// how many columns the seg will take on the entire screen (think about it)
-	if (pos + range < texturecolumntablesize)
+	if (pos + need < texturecolumntablesize)
 		return;
 
 	fixed_t *oldtable = texturecolumntable;
@@ -1518,7 +1556,7 @@ static void R_AllocTextureColumnTables(size_t range)
 	if (texturecolumntablesize == 0)
 		texturecolumntablesize = 16384;
 
-	texturecolumntablesize += range;
+	texturecolumntablesize += need;
 	texturecolumntable = Z_Realloc(texturecolumntable, texturecolumntablesize * sizeof (*texturecolumntable), PU_STATIC, NULL);
 	curtexturecolumntable = texturecolumntable + pos;
 
@@ -1532,6 +1570,8 @@ static void R_AllocTextureColumnTables(size_t range)
 			ds->maskedtexturecol = (ds->maskedtexturecol - oldtable) + texturecolumntable;
 		if (ds->thicksidecol + ds->x1 >= oldtable && ds->thicksidecol + ds->x1 <= oldlast)
 			ds->thicksidecol = (ds->thicksidecol - oldtable) + texturecolumntable;
+		if (ds->invscale + ds->x1 >= oldtable && ds->invscale + ds->x1 <= oldlast)
+			ds->invscale = (ds->invscale - oldtable) + texturecolumntable;
 	}
 }
 
@@ -1555,7 +1595,11 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 	fixed_t ceilingfrontslide, floorfrontslide, ceilingbackslide, floorbackslide;
 	static size_t maxdrawsegs = 0;
 
+	maskedtexturecol = NULL;
 	maskedtextureheight = NULL;
+	thicksidecol = NULL;
+	invscale = NULL;
+
 	//initialize segleft and segright
 	memset(&segleft, 0x00, sizeof(segleft));
 	memset(&segright, 0x00, sizeof(segright));
@@ -1712,6 +1756,7 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 	ds_p->maskedtexturecol = NULL;
 	ds_p->numthicksides = numthicksides = 0;
 	ds_p->thicksidecol = NULL;
+	ds_p->invscale = NULL;
 	ds_p->tsilheight = 0;
 
 	numbackffloors = 0;
@@ -1751,32 +1796,67 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			ceilingbackslide = FixedMul(backsector->c_slope->zdelta, FINECOSINE((lineangle-backsector->c_slope->xydirection)>>ANGLETOFINESHIFT));
 	}
 
+	rw_midtexturescalex = sidedef->scalex_mid;
+	rw_midtexturescaley = sidedef->scaley_mid;
+	rw_invmidtexturescalex = FixedDiv(FRACUNIT, rw_midtexturescalex);
+
 	if (!backsector)
 	{
-		fixed_t texheight;
-		// single sided line
 		midtexture = R_GetTextureNum(sidedef->midtexture);
-		texheight = textureheight[midtexture];
+
 		// a single sided line is terminal, so it must mark ends
 		markfloor = markceiling = true;
-		if (linedef->flags & ML_NOSKEW) {
-			if (linedef->flags & ML_DONTPEGBOTTOM)
-				rw_midtexturemid = frontsector->floorheight + texheight - viewz;
-			else
-				rw_midtexturemid = frontsector->ceilingheight - viewz;
-		}
-		else if (linedef->flags & ML_DONTPEGBOTTOM)
+
+		fixed_t rowoffset = sidedef->rowoffset + sidedef->offsety_mid;
+		fixed_t texheight = textureheight[midtexture];
+
+		if (rw_midtexturescaley > 0)
 		{
-			rw_midtexturemid = worldbottom + texheight;
-			rw_midtextureslide = floorfrontslide;
+			if (linedef->flags & ML_NOSKEW)
+			{
+				if (linedef->flags & ML_DONTPEGBOTTOM)
+					rw_midtexturemid = FixedMul(frontsector->floorheight - viewz, rw_midtexturescaley) + texheight;
+				else
+					rw_midtexturemid = FixedMul(frontsector->ceilingheight - viewz, rw_midtexturescaley);
+			}
+			else if (linedef->flags & ML_DONTPEGBOTTOM)
+			{
+				rw_midtexturemid = FixedMul(worldbottom, rw_midtexturescaley) + texheight;
+				rw_midtextureslide = floorfrontslide;
+			}
+			else
+			{
+				// top of texture at top
+				rw_midtexturemid = FixedMul(worldtop, rw_midtexturescaley);
+				rw_midtextureslide = ceilingfrontslide;
+			}
 		}
 		else
 		{
-			// top of texture at top
-			rw_midtexturemid = worldtop;
-			rw_midtextureslide = ceilingfrontslide;
+			// Upside down
+			rowoffset = -rowoffset;
+
+			if (linedef->flags & ML_NOSKEW)
+			{
+				if (linedef->flags & ML_DONTPEGBOTTOM)
+					rw_midtexturemid = FixedMul(frontsector->floorheight - viewz, rw_midtexturescaley);
+				else
+					rw_midtexturemid = FixedMul(frontsector->ceilingheight - viewz, rw_midtexturescaley) + texheight;
+			}
+			else if (linedef->flags & ML_DONTPEGBOTTOM)
+			{
+				rw_midtexturemid = FixedMul(worldbottom, rw_midtexturescaley);
+				rw_midtextureslide = floorfrontslide;
+			}
+			else
+			{
+				// top of texture at top
+				rw_midtexturemid = FixedMul(worldtop, rw_midtexturescaley) + texheight;
+				rw_midtextureslide = ceilingfrontslide;
+			}
 		}
-		rw_midtexturemid += sidedef->rowoffset + sidedef->offsety_mid;
+
+		rw_midtexturemid += rowoffset;
 
 		ds_p->silhouette = SIL_BOTH;
 		ds_p->sprtopclip = screenheightarray;
@@ -1901,6 +1981,8 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			//SoM: 3/22/2000: Check floor x and y offsets.
 			|| backsector->floorxoffset != frontsector->floorxoffset
 			|| backsector->flooryoffset != frontsector->flooryoffset
+			|| backsector->floorxscale != frontsector->floorxscale
+			|| backsector->flooryscale != frontsector->flooryscale
 			|| backsector->floorangle != frontsector->floorangle
 			//SoM: 3/22/2000: Prevents bleeding.
 			|| (frontsector->heightsec != -1 && frontsector->floorpic != skyflatnum)
@@ -1934,6 +2016,8 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			//SoM: 3/22/2000: Check floor x and y offsets.
 			|| backsector->ceilingxoffset != frontsector->ceilingxoffset
 			|| backsector->ceilingyoffset != frontsector->ceilingyoffset
+			|| backsector->ceilingxscale != frontsector->ceilingxscale
+			|| backsector->ceilingyscale != frontsector->ceilingyscale
 			|| backsector->ceilingangle != frontsector->ceilingangle
 			//SoM: 3/22/2000: Prevents bleeding.
 			|| (frontsector->heightsec != -1 && frontsector->ceilingpic != skyflatnum)
@@ -1962,16 +2046,28 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			}
 		}
 
+		fixed_t toprowoffset = sidedef->rowoffset + sidedef->offsety_top;
+		fixed_t botrowoffset = sidedef->rowoffset + sidedef->offsety_bottom;
+
 		// check TOP TEXTURE
 		if (!bothceilingssky // never draw the top texture if on
 			&& (worldhigh < worldtop || worldhighslope < worldtopslope))
 		{
-			fixed_t texheight;
-			// top texture
 			toptexture = R_GetTextureNum(sidedef->toptexture);
-			texheight = textureheight[toptexture];
 
-			if (!(linedef->flags & ML_SKEWTD)) { // Ignore slopes for lower/upper textures unless flag is checked
+			rw_toptexturescalex = sidedef->scalex_top;
+			rw_toptexturescaley = sidedef->scaley_top;
+
+			rw_invtoptexturescalex = FixedDiv(FRACUNIT, rw_toptexturescalex);
+
+			if (rw_toptexturescaley < 0)
+				toprowoffset = -toprowoffset;
+
+			fixed_t texheight = textureheight[toptexture];
+
+			// Ignore slopes for lower/upper textures unless flag is checked
+			if (!(linedef->flags & ML_SKEWTD))
+			{
 				if (linedef->flags & ML_DONTPEGTOP)
 					rw_toptexturemid = frontsector->ceilingheight - viewz;
 				else
@@ -1988,7 +2084,10 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 				rw_toptexturemid = worldhigh + texheight;
 				rw_toptextureslide = ceilingbackslide;
 			}
+
+			rw_toptexturemid = FixedMul(rw_toptexturemid, rw_toptexturescaley);
 		}
+
 		// check BOTTOM TEXTURE
 		if (!bothfloorssky // never draw the bottom texture if on
 			&& (worldlow > worldbottom || worldlowslope > worldbottomslope)) // Only if VISIBLE!!!
@@ -1996,7 +2095,17 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			// bottom texture
 			bottomtexture = R_GetTextureNum(sidedef->bottomtexture);
 
-			if (!(linedef->flags & ML_SKEWTD)) { // Ignore slopes for lower/upper textures unless flag is checked
+			rw_bottomtexturescalex = sidedef->scalex_bottom;
+			rw_bottomtexturescaley = sidedef->scaley_bottom;
+
+			rw_invbottomtexturescalex = FixedDiv(FRACUNIT, rw_bottomtexturescalex);
+
+			if (rw_bottomtexturescaley < 0)
+				botrowoffset = -botrowoffset;
+
+			// Ignore slopes for lower/upper textures unless flag is checked
+			if (!(linedef->flags & ML_SKEWTD))
+			{
 				if (linedef->flags & ML_DONTPEGBOTTOM)
 					rw_bottomtexturemid = frontsector->floorheight - viewz;
 				else
@@ -2009,18 +2118,22 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 				rw_bottomtexturemid = worldbottom;
 				rw_bottomtextureslide = floorfrontslide;
 			}
-			else {   // top of texture at top
+			else
+			{
+				// top of texture at top
 				rw_bottomtexturemid = worldlow;
 				rw_bottomtextureslide = floorbackslide;
 			}
+
+			rw_bottomtexturemid = FixedMul(rw_bottomtexturemid, rw_bottomtexturescaley);
 		}
 
-		rw_toptexturemid += sidedef->rowoffset + sidedef->offsety_top;
-		rw_bottomtexturemid += sidedef->rowoffset + sidedef->offsety_bot;
+		rw_toptexturemid += toprowoffset;
+		rw_bottomtexturemid += botrowoffset;
 
+		// allocate space for masked texture tables
 		R_AllocTextureColumnTables(rw_stopx - start);
 
-		// allocate space for masked texture tables
 		if (frontsector && backsector && !Tag_Compare(&frontsector->tags, &backsector->tags) && (backsector->ffloors || frontsector->ffloors))
 		{
 			ffloor_t *rover;
@@ -2031,10 +2144,9 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			// Used for height comparisons and etc across FOFs and slopes
 			fixed_t high1, highslope1, low1, lowslope1, high2, highslope2, low2, lowslope2;
 
-			//markceiling = markfloor = true;
 			maskedtexture = true;
 
-			ds_p->thicksidecol = maskedtexturecol = curtexturecolumntable - rw_x;
+			ds_p->thicksidecol = thicksidecol = curtexturecolumntable - rw_x;
 			curtexturecolumntable += rw_stopx - rw_x;
 
 			lowcut = max(worldbottom, worldlow) + viewz;
@@ -2213,21 +2325,20 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 
 			ds_p->numthicksides = numthicksides = i;
 		}
+
+		// masked midtexture
 		if (sidedef->midtexture > 0 && sidedef->midtexture < numtextures)
 		{
-			// masked midtexture
-			if (!ds_p->thicksidecol)
-			{
-				ds_p->maskedtexturecol = maskedtexturecol = curtexturecolumntable - rw_x;
-				curtexturecolumntable += rw_stopx - rw_x;
-			}
-			else
-				ds_p->maskedtexturecol = ds_p->thicksidecol;
+			ds_p->maskedtexturecol = maskedtexturecol = curtexturecolumntable - rw_x;
+			curtexturecolumntable += rw_stopx - rw_x;
 
 			maskedtextureheight = ds_p->maskedtextureheight; // note to red, this == &(ds_p->maskedtextureheight[0])
 
+			maskedtexture = true;
+
 			if (curline->polyseg)
-			{ // use REAL front and back floors please, so midtexture rendering isn't mucked up
+			{
+				// use REAL front and back floors please, so midtexture rendering isn't mucked up
 				rw_midtextureslide = rw_midtexturebackslide = 0;
 				if (linedef->flags & ML_MIDPEG)
 					rw_midtexturemid = rw_midtextureback = max(curline->frontsector->floorheight, curline->backsector->floorheight) - viewz;
@@ -2238,7 +2349,8 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			{
 				// Set midtexture starting height
 				if (linedef->flags & ML_NOSKEW)
-				{ // Ignore slopes when texturing
+				{
+					// Ignore slopes when texturing
 					rw_midtextureslide = rw_midtexturebackslide = 0;
 					if (linedef->flags & ML_MIDPEG)
 						rw_midtexturemid = rw_midtextureback = max(frontsector->floorheight, backsector->floorheight) - viewz;
@@ -2261,6 +2373,10 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 					rw_midtexturebackslide = ceilingbackslide;
 				}
 			}
+
+			rw_midtexturemid = FixedMul(rw_midtexturemid, rw_midtexturescaley);
+			rw_midtextureback = FixedMul(rw_midtextureback, rw_midtexturescaley);
+
 			rw_midtexturemid += sidedef->rowoffset + sidedef->offsety_mid;
 			rw_midtextureback += sidedef->rowoffset + sidedef->offsety_mid;
 
@@ -2273,6 +2389,8 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 
 	if (segtextured)
 	{
+		fixed_t sideoffset = sidedef->textureoffset;
+
 		offsetangle = rw_normalangle-rw_angle1;
 
 		if (offsetangle > ANGLE_180)
@@ -2297,14 +2415,20 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 		if (rw_normalangle-rw_angle1 < ANGLE_180)
 			rw_offset = -rw_offset;
 
-		/// don't use texture offset for splats
-		rw_offset2 = rw_offset + curline->offset;
-		rw_offset += sidedef->textureoffset + curline->offset;
-		rw_offset_top = sidedef->offsetx_top;
-		rw_offset_mid = sidedef->offsetx_mid;
-		rw_offset_bot = sidedef->offsetx_bot;
+		rw_offset += curline->offset;
 		rw_centerangle = ANGLE_90 + viewangle - rw_normalangle;
 
+		rw_offset_top = sideoffset + sidedef->offsetx_top;
+		rw_offset_mid = sideoffset + sidedef->offsetx_mid;
+		rw_offset_bottom = sideoffset + sidedef->offsetx_bottom;
+
+		rw_offsetx = rw_offset_mid;
+		if (rw_midtexturescalex < 0)
+			rw_offsetx = -rw_offsetx;
+
+		if (numthicksides)
+			ds_p->offsetx = rw_offsetx;
+
 		// calculate light table
 		//  use different light tables
 		//  for horizontal / vertical / diagonal
@@ -2324,6 +2448,12 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			walllights = scalelight[lightnum];
 	}
 
+	if (maskedtexture)
+	{
+		ds_p->invscale = invscale = curtexturecolumntable - rw_x;
+		curtexturecolumntable += rw_stopx - rw_x;
+	}
+
 	// if a floor / ceiling plane is on the wrong side
 	//  of the view plane, it is definitely invisible
 	//  and doesn't need to be marked.
diff --git a/src/r_skins.c b/src/r_skins.c
index 72598f38185acf3d5801365e315e54a920e38511..fbc2a30e128895c440598c747292a1b8b5312091 100644
--- a/src/r_skins.c
+++ b/src/r_skins.c
@@ -306,6 +306,21 @@ INT32 R_SkinAvailable(const char *name)
 	return -1;
 }
 
+INT32 R_GetForcedSkin(INT32 playernum)
+{
+	if (netgame && cv_forceskin.value >= 0 && R_SkinUsable(playernum, cv_forceskin.value))
+		return cv_forceskin.value;
+
+	if (mapheaderinfo[gamemap-1] && mapheaderinfo[gamemap-1]->forcecharacter[0] != '\0')
+	{
+		INT32 skinnum = R_SkinAvailable(mapheaderinfo[gamemap-1]->forcecharacter);
+		if (skinnum != -1 && R_SkinUsable(playernum, skinnum))
+			return skinnum;
+	}
+
+	return -1;
+}
+
 // Auxillary function that actually sets the skin
 static void SetSkin(player_t *player, INT32 skinnum)
 {
@@ -347,10 +362,6 @@ static void SetSkin(player_t *player, INT32 skinnum)
 
 	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)
 		{
@@ -381,7 +392,7 @@ static void SetSkin(player_t *player, INT32 skinnum)
 		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
+		P_SetMobjState(player->mo, player->mo->state-states); // Prevent visual errors when switching between skins with differing number of frames
 	}
 }
 
diff --git a/src/r_skins.h b/src/r_skins.h
index fab6fc12c0f659e8b35f5d111a497850edf127b0..bf2275a49a795a1a984343dc82d881b769a89592 100644
--- a/src/r_skins.h
+++ b/src/r_skins.h
@@ -96,6 +96,7 @@ 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);
+INT32 R_GetForcedSkin(INT32 playernum);
 void R_AddSkins(UINT16 wadnum, boolean mainfile);
 void R_PatchSkins(UINT16 wadnum, boolean mainfile);
 
diff --git a/src/r_sky.c b/src/r_sky.c
index c47029f0b34416fda4dabd2f43a62550e3e31fdf..edde4c218b1c62717055852de7d35f6cd3c747d4 100644
--- a/src/r_sky.c
+++ b/src/r_sky.c
@@ -75,6 +75,6 @@ void R_SetupSkyDraw(void)
 */
 void R_SetSkyScale(void)
 {
-	fixed_t difference = vid.fdupx-(vid.dupx<<FRACBITS);
-	skyscale = FixedDiv(fovtan, vid.fdupx+difference);
+	fixed_t difference = vid.fdup-(vid.dup<<FRACBITS);
+	skyscale = FixedDiv(fovtan, vid.fdup+difference);
 }
diff --git a/src/r_splats.c b/src/r_splats.c
index 0b482d7988cc0e3a32f719a0f84a272fdf63d2f3..e9665e84a35a4089e72cfa40120157cf418cf9e9 100644
--- a/src/r_splats.c
+++ b/src/r_splats.c
@@ -369,7 +369,7 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 	ds_flatwidth = pSplat->width;
 	ds_flatheight = pSplat->height;
 
-	ds_powersoftwo = ds_solidcolor = false;
+	ds_powersoftwo = ds_solidcolor = ds_fog = false;
 
 	if (R_CheckSolidColorFlat())
 		ds_solidcolor = true;
@@ -381,9 +381,7 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 
 	if (pSplat->slope)
 	{
-		R_SetTiltedSpan(0);
 		R_SetScaledSlopePlane(pSplat->slope, vis->viewpoint.x, vis->viewpoint.y, vis->viewpoint.z, pSplat->xscale, pSplat->yscale, -pSplat->verts[0].x, pSplat->verts[0].y, vis->viewpoint.angle, pSplat->angle);
-		R_CalculateSlopeVectors();
 	}
 	else if (!ds_solidcolor)
 	{
@@ -391,8 +389,6 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 
 		if (pSplat->angle)
 		{
-			memset(cachedheight, 0, sizeof(cachedheight));
-
 			// Add the view offset, rotated by the plane angle.
 			fixed_t a = -pSplat->verts[0].x + vis->viewpoint.x;
 			fixed_t b = -pSplat->verts[0].y + vis->viewpoint.y;
@@ -547,29 +543,18 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 			angle_t planecos = FINECOSINE(angle);
 			angle_t planesin = FINESINE(angle);
 
-			if (planeheight != cachedheight[y])
-			{
-				cachedheight[y] = planeheight;
-				distance = cacheddistance[y] = FixedMul(planeheight, yslope[y]);
-				span = abs(centery - y);
-
-				if (span) // Don't divide by zero
-				{
-					xstep = FixedMul(planesin, planeheight) / span;
-					ystep = FixedMul(planecos, planeheight) / span;
-				}
-				else
-					xstep = ystep = FRACUNIT;
+			// [RH] Notice that I dumped the caching scheme used by Doom.
+			// It did not offer any appreciable speedup.
+			distance = FixedMul(planeheight, yslope[y]);
+			span = abs(centery - y);
 
-				cachedxstep[y] = xstep;
-				cachedystep[y] = ystep;
-			}
-			else
+			if (span) // Don't divide by zero
 			{
-				distance = cacheddistance[y];
-				xstep = cachedxstep[y];
-				ystep = cachedystep[y];
+				xstep = FixedMul(planesin, planeheight) / span;
+				ystep = FixedMul(planecos, planeheight) / span;
 			}
+			else
+				xstep = ystep = FRACUNIT;
 
 			ds_xstep = FixedDiv(xstep, pSplat->xscale);
 			ds_ystep = FixedDiv(ystep, pSplat->yscale);
@@ -586,9 +571,6 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 		rastertab[y].minx = INT32_MAX;
 		rastertab[y].maxx = INT32_MIN;
 	}
-
-	if (!ds_solidcolor && pSplat->angle && !pSplat->slope)
-		memset(cachedheight, 0, sizeof(cachedheight));
 }
 
 static void prepare_rastertab(void)
diff --git a/src/r_things.c b/src/r_things.c
index be9c5cdffe26be0a5482489d9540a50a2a599998..557e1681439cd13d6bd871f7c33a789a0d2ba2be 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -34,7 +34,7 @@
 #include "p_tick.h"
 #include "p_local.h"
 #include "p_slopes.h"
-#include "d_netfil.h" // blargh. for nameonly().
+#include "netcode/d_netfil.h" // blargh. for nameonly().
 #include "m_cheat.h" // objectplace
 #ifdef HWRENDER
 #include "hardware/hw_md2.h"
@@ -138,8 +138,7 @@ static void R_InstallSpriteLump(UINT16 wad,            // graphics patch
 #ifdef ROTSPRITE
 	for (r = 0; r < 16; r++)
 	{
-		sprtemp[frame].rotated[0][r] = NULL;
-		sprtemp[frame].rotated[1][r] = NULL;
+		sprtemp[frame].rotated[r] = NULL;
 	}
 #endif
 
@@ -337,6 +336,11 @@ boolean R_AddSingleSpriteDef(const char *sprname, spritedef_t *spritedef, UINT16
 			spritecachedinfo[numspritelumps].height = height<<FRACBITS;
 
 			// BP: we cannot use special tric in hardware mode because feet in ground caused by z-buffer
+			// Monster Iestyn (21 Sep 2023): the above comment no longer makes sense in context!!! So I give an explanation here!
+			// FEETADJUST was originally an OpenGL-exclusive hack from Doom Legacy to avoid the player's feet being clipped as
+			// a result of rendering partially under the ground, but sometime before SRB2 2.1's release this was changed to apply
+			// to the software renderer as well.
+			// TODO: kill FEETADJUST altogether somehow and somehow fix OpenGL not to clip sprites that are partially underground (if possible)?
 			spritecachedinfo[numspritelumps].topoffset += FEETADJUST;
 
 			//----------------------------------------------------
@@ -1316,8 +1320,8 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 	patch_t *patch;
 	fixed_t xscale, yscale, shadowxscale, shadowyscale, shadowskew, x1, x2;
 	INT32 heightsec, phs;
-	INT32 light = 0;
-	fixed_t scalemul; UINT8 trans;
+	fixed_t scalemul;
+	UINT8 trans;
 	fixed_t floordiff;
 	fixed_t groundz;
 	pslope_t *groundslope;
@@ -1433,27 +1437,7 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 	if (thing->renderflags & RF_NOCOLORMAPS)
 		shadow->extra_colormap = NULL;
 	else
-	{
-		if (thing->subsector->sector->numlights)
-		{
-			INT32 lightnum;
-			light = thing->subsector->sector->numlights - 1;
-
-			// R_GetPlaneLight won't work on sloped lights!
-			for (lightnum = 1; lightnum < thing->subsector->sector->numlights; lightnum++) {
-				fixed_t h = P_GetLightZAt(&thing->subsector->sector->lightlist[lightnum], interp.x, interp.y);
-				if (h <= shadow->gzt) {
-					light = lightnum - 1;
-					break;
-				}
-			}
-		}
-
-		if (thing->subsector->sector->numlights)
-			shadow->extra_colormap = *thing->subsector->sector->lightlist[light].extra_colormap;
-		else
-			shadow->extra_colormap = thing->subsector->sector->extra_colormap;
-	}
+		shadow->extra_colormap = P_GetColormapFromSectorAt(thing->subsector->sector, interp.x, interp.y, shadow->gzt);
 
 	shadow->transmap = R_GetTranslucencyTable(trans + 1);
 	shadow->colormap = scalelight[0][0]; // full dark!
@@ -1794,7 +1778,7 @@ static void R_ProjectSprite(mobj_t *thing)
 			rollangle = R_GetRollAngle(spriterotangle);
 		}
 
-		rotsprite = Patch_GetRotatedSprite(sprframe, (thing->frame & FF_FRAMEMASK), rot, flip, false, sprinfo, rollangle);
+		rotsprite = Patch_GetRotatedSprite(sprframe, (thing->frame & FF_FRAMEMASK), rot, flip, sprinfo, rollangle);
 
 		if (rotsprite != NULL)
 		{
@@ -2150,21 +2134,9 @@ static void R_ProjectSprite(mobj_t *thing)
 
 	if (thing->subsector->sector->numlights)
 	{
-		INT32 lightnum;
-		fixed_t top = (splat) ? gz : gzt;
-		light = thing->subsector->sector->numlights - 1;
-
-		// R_GetPlaneLight won't work on sloped lights!
-		for (lightnum = 1; lightnum < thing->subsector->sector->numlights; lightnum++) {
-			fixed_t h = P_GetLightZAt(&thing->subsector->sector->lightlist[lightnum], interp.x, interp.y);
-			if (h <= top) {
-				light = lightnum - 1;
-				break;
-			}
-		}
-		//light = R_GetPlaneLight(thing->subsector->sector, gzt, false);
-		lightnum = (*thing->subsector->sector->lightlist[light].lightlevel >> LIGHTSEGSHIFT);
+		light = P_GetSectorLightAt(thing->subsector->sector, interp.x, interp.y, splat ? gz : gzt);
 
+		INT32 lightnum = (*thing->subsector->sector->lightlist[light].lightlevel >> LIGHTSEGSHIFT);
 		if (lightnum < 0)
 			spritelights = scalelight[0];
 		else if (lightnum >= LIGHTLEVELS)
@@ -3201,8 +3173,8 @@ static boolean R_CheckSpriteVisible(vissprite_t *spr, INT32 x1, INT32 x2)
 	INT16 sz = spr->sz;
 	INT16 szt = spr->szt;
 
-	fixed_t texturemid, yscale, scalestep = spr->scalestep;
-	INT32 height;
+	fixed_t texturemid = 0, yscale = 0, scalestep = spr->scalestep; // "= 0" pleases the compiler
+	INT32 height = 0;
 
 	if (scalestep)
 	{
diff --git a/src/s_sound.c b/src/s_sound.c
index ada1a0fd2f399f09849a7330cf14ca283c7b1784..a579292ff3aa791176b389d686901799c47c89a1 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -513,7 +513,7 @@ void S_StartCaption(sfxenum_t sfx_id, INT32 cnum, UINT16 lifespan)
 	closedcaptions[set].c = ((cnum == -1) ? NULL : &channels[cnum]);
 	closedcaptions[set].s = sfx;
 	closedcaptions[set].t = lifespan;
-	closedcaptions[set].b = 2; // bob
+	closedcaptions[set].b = 3; // bob
 }
 
 void S_StartSoundAtVolume(const void *origin_p, sfxenum_t sfx_id, INT32 volume)
diff --git a/src/screen.c b/src/screen.c
index 417e793bde540c62a9edf2b6a0b8073250ee7f73..ca59b251dce1f6f1371af433b010a18b4304e621 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -27,7 +27,7 @@
 #include "hu_stuff.h"
 #include "z_zone.h"
 #include "d_main.h"
-#include "d_clisrv.h"
+#include "netcode/d_clisrv.h"
 #include "f_finale.h"
 #include "y_inter.h" // usebuffer
 #include "i_sound.h" // closed captions
@@ -98,14 +98,6 @@ UINT8 *scr_borderpatch; // flat used to fill the reduced view borders set at ST_
 //  Short and Tall sky drawer, for the current color mode
 void (*walldrawerfunc)(void);
 
-boolean R_486 = false;
-boolean R_586 = false;
-boolean R_MMX = false;
-boolean R_SSE = false;
-boolean R_3DNow = false;
-boolean R_MMXExt = false;
-boolean R_SSE2 = false;
-
 void SCR_SetDrawFuncs(void)
 {
 	//
@@ -225,48 +217,6 @@ void SCR_SetMode(void)
 //
 void SCR_Startup(void)
 {
-	const CPUInfoFlags *RCpuInfo = I_CPUInfo();
-	if (!M_CheckParm("-NOCPUID") && RCpuInfo)
-	{
-#if defined (__i386__) || defined (_M_IX86) || defined (__WATCOMC__)
-		R_486 = true;
-#endif
-		if (RCpuInfo->RDTSC)
-			R_586 = true;
-		if (RCpuInfo->MMX)
-			R_MMX = true;
-		if (RCpuInfo->AMD3DNow)
-			R_3DNow = true;
-		if (RCpuInfo->MMXExt)
-			R_MMXExt = true;
-		if (RCpuInfo->SSE)
-			R_SSE = true;
-		if (RCpuInfo->SSE2)
-			R_SSE2 = true;
-		CONS_Printf("CPU Info: 486: %i, 586: %i, MMX: %i, 3DNow: %i, MMXExt: %i, SSE2: %i\n", R_486, R_586, R_MMX, R_3DNow, R_MMXExt, R_SSE2);
-	}
-
-	if (M_CheckParm("-486"))
-		R_486 = true;
-	if (M_CheckParm("-586"))
-		R_586 = true;
-	if (M_CheckParm("-MMX"))
-		R_MMX = true;
-	if (M_CheckParm("-3DNow"))
-		R_3DNow = true;
-	if (M_CheckParm("-MMXExt"))
-		R_MMXExt = true;
-
-	if (M_CheckParm("-SSE"))
-		R_SSE = true;
-	if (M_CheckParm("-noSSE"))
-		R_SSE = false;
-
-	if (M_CheckParm("-SSE2"))
-		R_SSE2 = true;
-
-	M_SetupMemcpy();
-
 	if (dedicated)
 	{
 		V_Init();
@@ -494,7 +444,7 @@ void SCR_CalculateFPS(void)
 void SCR_DisplayTicRate(void)
 {
 	INT32 ticcntcolor = 0;
-	const INT32 h = vid.height-(8*vid.dupy);
+	const INT32 h = vid.height-(8*vid.dup);
 	UINT32 cap = R_GetFramerateCap();
 	double fps = round(averageFPS);
 
@@ -530,7 +480,7 @@ void SCR_DisplayTicRate(void)
 
 		width = V_StringWidth(drawnstr, V_NOSCALESTART);
 
-		V_DrawString(vid.width - ((7 * 8 * vid.dupx) + V_StringWidth("FPS: ", V_NOSCALESTART)), h,
+		V_DrawString(vid.width - ((7 * 8 * vid.dup) + V_StringWidth("FPS: ", V_NOSCALESTART)), h,
 			V_YELLOWMAP|V_NOSCALESTART|V_USERHUDTRANS, "FPS:");
 		V_DrawString(vid.width - width, h,
 			ticcntcolor|V_NOSCALESTART|V_USERHUDTRANS, drawnstr);
@@ -552,7 +502,7 @@ void SCR_ClosedCaptions(void)
 {
 	UINT8 i;
 	boolean gamestopped = (paused || P_AutoPause());
-	INT32 basey = BASEVIDHEIGHT;
+	INT32 basey = BASEVIDHEIGHT - 20;
 
 	if (gamestate != wipegamestate)
 		return;
@@ -572,7 +522,8 @@ void SCR_ClosedCaptions(void)
 
 	for (i = 0; i < NUMCAPTIONS; i++)
 	{
-		INT32 flags, y;
+		INT32 flags;
+		fixed_t y;
 		char dot;
 		boolean music;
 
@@ -585,14 +536,19 @@ void SCR_ClosedCaptions(void)
 			continue;
 
 		flags = V_SNAPTORIGHT|V_SNAPTOBOTTOM|V_ALLOWLOWERCASE;
-		y = basey-((i + 2)*10);
+		y = (basey-(i*10)) * FRACUNIT;
 
 		if (closedcaptions[i].b)
 		{
-			y -= closedcaptions[i].b * vid.dupy;
 			if (renderisnewtic)
-			{
 				closedcaptions[i].b--;
+
+			if (closedcaptions[i].b) // If the caption hasn't reached its final destination...
+			{
+				y -= closedcaptions[i].b * 4 * FRACUNIT; // ...move it per tic...
+				y += (rendertimefrac % FRACUNIT) * 4; // ...and interpolate it per frame
+				// We have to modulo it by FRACUNIT, so that it won't be a tic ahead with interpolation disabled
+				// Unlike everything else, captions are (intentionally) interpolated from T to T+1 instead of T-1 to T
 			}
 		}
 
@@ -606,7 +562,7 @@ void SCR_ClosedCaptions(void)
 		else
 			dot = ' ';
 
-		V_DrawRightAlignedString(BASEVIDWIDTH - 20, y, flags,
+		V_DrawRightAlignedStringAtFixed((BASEVIDWIDTH-20) * FRACUNIT, y, flags,
 			va("%c [%s]", dot, (closedcaptions[i].s->caption[0] ? closedcaptions[i].s->caption : closedcaptions[i].s->name)));
 	}
 }
@@ -639,9 +595,9 @@ void SCR_DisplayMarathonInfo(void)
 #define PRIMEV1 13
 #define PRIMEV2 17 // I can't believe it! I'm on TV!
 		antisplice[0] += (entertic - oldentertics)*PRIMEV2;
-		antisplice[0] %= PRIMEV1*((vid.width/vid.dupx)+1);
+		antisplice[0] %= PRIMEV1*((vid.width/vid.dup)+1);
 		antisplice[1] += (entertic - oldentertics)*PRIMEV1;
-		antisplice[1] %= PRIMEV1*((vid.width/vid.dupx)+1);
+		antisplice[1] %= PRIMEV1*((vid.width/vid.dup)+1);
 		str = va("%i:%02i:%02i.%02i",
 			G_TicsToHours(marathontime),
 			G_TicsToMinutes(marathontime, false),
diff --git a/src/screen.h b/src/screen.h
index 65e82ff4df2bff3701a0f57d4667ee32a34784f5..e4c1006c35b79a7f9948f818c0a3b03edf84c558 100644
--- a/src/screen.h
+++ b/src/screen.h
@@ -59,19 +59,19 @@ typedef struct viddef_s
 	} u;
 	INT32 recalc; // if true, recalc vid-based stuff
 	UINT8 *direct; // linear frame buffer, or vga base mem.
-	INT32 dupx, dupy; // scale 1, 2, 3 value for menus & overlays
-	INT32/*fixed_t*/ fdupx, fdupy; // same as dupx, dupy, but exact value when aspect ratio isn't 320/200
+	INT32 dup; // scale 1, 2, 3 value for menus & overlays
+	INT32/*fixed_t*/ fdup; // same as dup, but exact value when aspect ratio isn't 320/200
 	INT32 bpp; // BYTES per pixel: 1 = 256color, 2 = highcolor
 
 	INT32 baseratio; // Used to get the correct value for lighting walls
 
 	// for Win32 version
 	DNWH WndParent; // handle of the application's window
-	UINT8 smalldupx, smalldupy; // factor for a little bit of scaling
-	UINT8 meddupx, meddupy; // factor for moderate, but not full, scaling
+	UINT8 smalldup; // factor for a little bit of scaling
+	UINT8 meddup; // factor for moderate, but not full, scaling
 #ifdef HWRENDER
-	INT32/*fixed_t*/ fsmalldupx, fsmalldupy;
-	INT32/*fixed_t*/ fmeddupx, fmeddupy;
+	INT32/*fixed_t*/ fsmalldup;
+	INT32/*fixed_t*/ fmeddup;
 	INT32 glstate;
 #endif
 } viddef_t;
@@ -172,17 +172,6 @@ extern void (*spanfunc)(void);
 extern void (*spanfuncs[SPANDRAWFUNC_MAX])(void);
 extern void (*spanfuncs_npo2[SPANDRAWFUNC_MAX])(void);
 
-// -----
-// CPUID
-// -----
-extern boolean R_ASM;
-extern boolean R_486;
-extern boolean R_586;
-extern boolean R_MMX;
-extern boolean R_3DNow;
-extern boolean R_MMXExt;
-extern boolean R_SSE2;
-
 // ----------------
 // screen variables
 // ----------------
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index 0b95cd0b2e0732b3f562d85d5e7454c7ec351b64..9b51cfb8094a1d6897e0685e9531ccd21df0fbdb 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -262,7 +262,6 @@
     <ClInclude Include="..\u_list.h" />
     <ClInclude Include="..\hu_stuff.h" />
     <ClInclude Include="..\info.h" />
-    <ClInclude Include="..\i_addrinfo.h" />
     <ClInclude Include="..\i_joy.h" />
     <ClInclude Include="..\i_net.h" />
     <ClInclude Include="..\i_sound.h" />
@@ -297,6 +296,21 @@
     <ClInclude Include="..\m_queue.h" />
     <ClInclude Include="..\m_random.h" />
     <ClInclude Include="..\m_swap.h" />
+    <ClInclude Include="..\netcode\client_connection.h" />
+    <ClInclude Include="..\netcode\commands.h" />
+    <ClInclude Include="..\netcode\d_clisrv.h" />
+    <ClInclude Include="..\netcode\d_net.h" />
+    <ClInclude Include="..\netcode\d_netcmd.h" />
+    <ClInclude Include="..\netcode\d_netfil.h" />
+    <ClInclude Include="..\netcode\gamestate.h" />
+    <ClInclude Include="..\netcode\i_addrinfo.h" />
+    <ClInclude Include="..\netcode\i_net.h" />
+    <ClInclude Include="..\netcode\i_tcp.h" />
+    <ClInclude Include="..\netcode\mserv.h" />
+    <ClInclude Include="..\netcode\net_command.h" />
+    <ClInclude Include="..\netcode\protocol.h" />
+    <ClInclude Include="..\netcode\server_connection.h" />
+    <ClInclude Include="..\netcode\tic_command.h" />
     <ClInclude Include="..\p5prof.h" />
     <ClInclude Include="..\p_haptic.h" />
     <ClInclude Include="..\p_local.h" />
@@ -329,6 +343,7 @@
     <ClInclude Include="..\r_textures.h" />
     <ClInclude Include="..\r_things.h" />
     <ClInclude Include="..\screen.h" />
+    <ClInclude Include="..\snake.h" />
     <ClInclude Include="..\sounds.h" />
     <ClInclude Include="..\st_stuff.h" />
     <ClInclude Include="..\s_sound.h" />
@@ -400,11 +415,7 @@
     <ClCompile Include="..\deh_soc.c" />
     <ClCompile Include="..\deh_lua.c" />
     <ClCompile Include="..\deh_tables.c" />
-    <ClCompile Include="..\d_clisrv.c" />
     <ClCompile Include="..\d_main.c" />
-    <ClCompile Include="..\d_net.c" />
-    <ClCompile Include="..\d_netcmd.c" />
-    <ClCompile Include="..\d_netfil.c" />
     <ClCompile Include="..\filesrch.c" />
     <ClCompile Include="..\f_finale.c" />
     <ClCompile Include="..\f_wipe.c" />
@@ -427,10 +438,6 @@
     <ClCompile Include="..\u_list.c" />
     <ClCompile Include="..\hu_stuff.c" />
     <ClCompile Include="..\info.c" />
-    <ClCompile Include="..\i_addrinfo.c">
-      <ExcludedFromBuild>true</ExcludedFromBuild>
-    </ClCompile>
-    <ClCompile Include="..\i_tcp.c" />
     <ClCompile Include="..\i_time.c" />
     <ClCompile Include="..\lua_baselib.c" />
     <ClCompile Include="..\lua_blockmaplib.c" />
@@ -451,8 +458,6 @@
     <ClCompile Include="..\lua_thinkerlib.c" />
     <ClCompile Include="..\lzf.c" />
     <ClCompile Include="..\md5.c" />
-    <ClCompile Include="..\mserv.c" />
-    <ClCompile Include="..\http-mserv.c" />
     <ClCompile Include="..\m_aatree.c" />
     <ClCompile Include="..\m_anigif.c" />
     <ClCompile Include="..\m_argv.c" />
@@ -466,6 +471,22 @@
     <ClCompile Include="..\m_perfstats.c" />
     <ClCompile Include="..\m_queue.c" />
     <ClCompile Include="..\m_random.c" />
+    <ClCompile Include="..\netcode\client_connection.c" />
+    <ClCompile Include="..\netcode\commands.c" />
+    <ClCompile Include="..\netcode\d_clisrv.c" />
+    <ClCompile Include="..\netcode\d_net.c" />
+    <ClCompile Include="..\netcode\d_netcmd.c" />
+    <ClCompile Include="..\netcode\d_netfil.c" />
+    <ClCompile Include="..\netcode\gamestate.c" />
+    <ClCompile Include="..\netcode\http-mserv.c" />
+    <ClCompile Include="..\netcode\i_addrinfo.c">
+      <ExcludedFromBuild>true</ExcludedFromBuild>
+    </ClCompile>
+    <ClCompile Include="..\netcode\i_tcp.c" />
+    <ClCompile Include="..\netcode\mserv.c" />
+    <ClCompile Include="..\netcode\net_command.c" />
+    <ClCompile Include="..\netcode\server_connection.c" />
+    <ClCompile Include="..\netcode\tic_command.c" />
     <ClCompile Include="..\p_ceilng.c" />
     <ClCompile Include="..\p_enemy.c" />
     <ClCompile Include="..\p_floor.c" />
@@ -510,6 +531,7 @@
     <ClCompile Include="..\r_textures.c" />
     <ClCompile Include="..\r_things.c" />
     <ClCompile Include="..\screen.c" />
+    <ClCompile Include="..\snake.c" />
     <ClCompile Include="..\sounds.c" />
     <ClCompile Include="..\string.c" />
     <ClCompile Include="..\st_stuff.c" />
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj.filters b/src/sdl/Srb2SDL-vc10.vcxproj.filters
index eb37ce9da6815ad539789e972a18b963e020cb71..96501b2160e587937f5513fa864f3a2f93a6d6ac 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj.filters
+++ b/src/sdl/Srb2SDL-vc10.vcxproj.filters
@@ -156,24 +156,12 @@
     <ClInclude Include="..\doomtype.h">
       <Filter>D_Doom</Filter>
     </ClInclude>
-    <ClInclude Include="..\d_clisrv.h">
-      <Filter>D_Doom</Filter>
-    </ClInclude>
     <ClInclude Include="..\d_event.h">
       <Filter>D_Doom</Filter>
     </ClInclude>
     <ClInclude Include="..\d_main.h">
       <Filter>D_Doom</Filter>
     </ClInclude>
-    <ClInclude Include="..\d_net.h">
-      <Filter>D_Doom</Filter>
-    </ClInclude>
-    <ClInclude Include="..\d_netcmd.h">
-      <Filter>D_Doom</Filter>
-    </ClInclude>
-    <ClInclude Include="..\d_netfil.h">
-      <Filter>D_Doom</Filter>
-    </ClInclude>
     <ClInclude Include="..\d_player.h">
       <Filter>D_Doom</Filter>
     </ClInclude>
@@ -279,9 +267,6 @@
     <ClInclude Include="..\filesrch.h">
       <Filter>I_Interface</Filter>
     </ClInclude>
-    <ClInclude Include="..\i_addrinfo.h">
-      <Filter>I_Interface</Filter>
-    </ClInclude>
     <ClInclude Include="..\i_joy.h">
       <Filter>I_Interface</Filter>
     </ClInclude>
@@ -306,12 +291,6 @@
     <ClInclude Include="..\keys.h">
       <Filter>I_Interface</Filter>
     </ClInclude>
-    <ClInclude Include="..\mserv.h">
-      <Filter>I_Interface</Filter>
-    </ClInclude>
-    <ClInclude Include="..\http-mserv.h">
-      <Filter>I_Interface</Filter>
-    </ClInclude>
     <ClInclude Include="..\lua_hook.h">
       <Filter>LUA</Filter>
     </ClInclude>
@@ -372,6 +351,54 @@
     <ClInclude Include="..\m_swap.h">
       <Filter>M_Misc</Filter>
     </ClInclude>
+    <ClInclude Include="..\netcode\client_connection.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\commands.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\d_clisrv.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\d_net.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\d_netcmd.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\d_netfil.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\gamestate.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\http-mserv.h">
+      <Filter>I_Interface</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\i_addrinfo.h">
+      <Filter>I_Interface</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\i_net.h">
+      <Filter>I_Interface</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\i_tcp.h">
+      <Filter>I_Interface</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\mserv.h">
+      <Filter>I_Interface</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\net_command.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\protocol.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\server_connection.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\tic_command.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
     <ClInclude Include="..\comptime.h">
       <Filter>O_Other</Filter>
     </ClInclude>
@@ -465,6 +492,9 @@
     <ClInclude Include="..\v_video.h">
       <Filter>R_Rend</Filter>
     </ClInclude>
+    <ClInclude Include="..\snake.h">
+      <Filter>M_Misc</Filter>
+    </ClInclude>
     <ClInclude Include="..\sounds.h">
       <Filter>S_Sounds</Filter>
     </ClInclude>
@@ -639,21 +669,9 @@
     <ClCompile Include="..\deh_tables.c">
       <Filter>D_Doom</Filter>
     </ClCompile>
-    <ClCompile Include="..\d_clisrv.c">
-      <Filter>D_Doom</Filter>
-    </ClCompile>
     <ClCompile Include="..\d_main.c">
       <Filter>D_Doom</Filter>
     </ClCompile>
-    <ClCompile Include="..\d_net.c">
-      <Filter>D_Doom</Filter>
-    </ClCompile>
-    <ClCompile Include="..\d_netcmd.c">
-      <Filter>D_Doom</Filter>
-    </ClCompile>
-    <ClCompile Include="..\d_netfil.c">
-      <Filter>D_Doom</Filter>
-    </ClCompile>
     <ClCompile Include="..\z_zone.c">
       <Filter>D_Doom</Filter>
     </ClCompile>
@@ -732,18 +750,9 @@
     <ClCompile Include="..\filesrch.c">
       <Filter>I_Interface</Filter>
     </ClCompile>
-    <ClCompile Include="..\i_addrinfo.c">
-      <Filter>I_Interface</Filter>
-    </ClCompile>
     <ClCompile Include="..\i_tcp.c">
       <Filter>I_Interface</Filter>
     </ClCompile>
-    <ClCompile Include="..\mserv.c">
-      <Filter>I_Interface</Filter>
-    </ClCompile>
-    <ClCompile Include="..\http-mserv.c">
-      <Filter>I_Interface</Filter>
-    </ClCompile>
     <ClCompile Include="..\lua_baselib.c">
       <Filter>LUA</Filter>
     </ClCompile>
@@ -831,6 +840,48 @@
     <ClCompile Include="..\m_random.c">
       <Filter>M_Misc</Filter>
     </ClCompile>
+    <ClCompile Include="..\netcode\client_connection.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\commands.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\d_clisrv.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\d_net.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\d_netcmd.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\d_netfil.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\gamestate.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\http-mserv.c">
+      <Filter>I_Interface</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\i_addrinfo.c">
+      <Filter>I_Interface</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\i_tcp.c">
+      <Filter>I_Interface</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\mserv.c">
+      <Filter>I_Interface</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\net_command.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\server_connection.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\tic_command.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
     <ClCompile Include="..\string.c">
       <Filter>M_Misc</Filter>
     </ClCompile>
@@ -954,6 +1005,9 @@
     <ClCompile Include="..\v_video.c">
       <Filter>R_Rend</Filter>
     </ClCompile>
+    <ClCompile Include="..\snake.c">
+      <Filter>M_Misc</Filter>
+    </ClCompile>
     <ClCompile Include="..\sounds.c">
       <Filter>S_Sounds</Filter>
     </ClCompile>
diff --git a/src/sdl/i_net.c b/src/sdl/i_net.c
index ee4a34c13dd8d4d10950d53d972d0cf873068943..515a855684e1e3d13dce6d35a785a2c7d07c9e93 100644
--- a/src/sdl/i_net.c
+++ b/src/sdl/i_net.c
@@ -21,16 +21,16 @@
 
 #include "../i_system.h"
 #include "../d_event.h"
-#include "../d_net.h"
+#include "../netcode/d_net.h"
 #include "../m_argv.h"
 
 #include "../doomstat.h"
 
-#include "../i_net.h"
+#include "../netcode/i_net.h"
 
 #include "../z_zone.h"
 
-#include "../i_tcp.h"
+#include "../netcode/i_tcp.h"
 
 #ifdef HAVE_SDL
 
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index 2a26f3f501f9c5708ec56d1f5624d087056f40ae..847806270f78175624fd2fabcdcf32a0b9e0e29e 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -41,6 +41,12 @@ typedef LPVOID (WINAPI *p_MapViewOfFile) (HANDLE, DWORD, DWORD, DWORD, SIZE_T);
 #include <ntsecapi.h>
 #undef SystemFunction036
 
+// A little more than the minimum sleep duration on Windows.
+// May be incorrect for other platforms, but we don't currently have a way to
+// query the scheduler granularity. SDL will do what's needed to make this as
+// low as possible though.
+#define MIN_SLEEP_DURATION_MS 2.1
+
 #endif
 #include <stdio.h>
 #include <stdlib.h>
@@ -138,7 +144,9 @@ typedef LPVOID (WINAPI *p_MapViewOfFile) (HANDLE, DWORD, DWORD, DWORD, SIZE_T);
 #endif
 
 #if defined (__unix__) || defined(__APPLE__) || defined (UNIXCOMMON)
+#ifndef NOEXECINFO
 #include <execinfo.h>
+#endif
 #include <time.h>
 #define UNIXBACKTRACE
 #endif
@@ -187,7 +195,8 @@ static char returnWadPath[256];
 #include "../i_system.h"
 #include "../i_threads.h"
 #include "../screen.h" //vid.WndParent
-#include "../d_net.h"
+#include "../netcode/d_net.h"
+#include "../netcode/commands.h"
 #include "../g_game.h"
 #include "../filesrch.h"
 #include "endtxt.h"
@@ -208,7 +217,7 @@ static char returnWadPath[256];
 
 #if !defined(NOMUMBLE) && defined(HAVE_MUMBLE)
 // Mumble context string
-#include "../d_clisrv.h"
+#include "../netcode/d_clisrv.h"
 #include "../byteptr.h"
 #endif
 
@@ -268,13 +277,17 @@ UINT8 keyboard_started = false;
 static void write_backtrace(INT32 signal)
 {
 	int fd = -1;
+#ifndef NOEXECINFO
 	size_t size;
+#endif
 	time_t rawtime;
 	struct tm timeinfo;
 	ssize_t junk;
 
 	enum { BT_SIZE = 1024, STR_SIZE = 32 };
+#ifndef NOEXECINFO
 	void *array[BT_SIZE];
+#endif
 	char timestr[STR_SIZE];
 
 	const char *error = "An error occurred within SRB2! Send this stack trace to someone who can help!\n";
@@ -307,12 +320,14 @@ static void write_backtrace(INT32 signal)
 	CRASHLOG_WRITE(strsignal(signal));
 	CRASHLOG_WRITE("\n"); // Newline for the signal name
 
+#ifndef NOEXECINFO
 	CRASHLOG_STDERR_WRITE("\nBacktrace:\n");
 
 	// Flood the output and log with the backtrace
 	size = backtrace(array, BT_SIZE);
 	backtrace_symbols_fd(array, size, fd);
 	backtrace_symbols_fd(array, size, STDERR_FILENO);
+#endif
 
 	CRASHLOG_WRITE("\n"); // Write another newline to the log so it looks nice :)
 	(void)junk;
@@ -607,6 +622,7 @@ void I_GetConsoleEvents(void)
 		return;
 
 	ev.type = ev_console;
+	ev.key = 0;
 	if (read(STDIN_FILENO, &key, 1) == -1 || !key)
 		return;
 
@@ -633,9 +649,10 @@ void I_GetConsoleEvents(void)
 		}
 		else return;
 	}
-	else
+	else if (tty_con.cursor < sizeof (tty_con.buffer))
 	{
 		// push regular character
+		ev.type = ev_text;
 		ev.key = tty_con.buffer[tty_con.cursor] = key;
 		tty_con.cursor++;
 		// print the current line (this is differential)
@@ -2264,6 +2281,52 @@ void I_Sleep(UINT32 ms)
 	SDL_Delay(ms);
 }
 
+void I_SleepDuration(precise_t duration)
+{
+#if defined(__linux__) || defined(__FreeBSD__)
+	UINT64 precision = I_GetPrecisePrecision();
+	struct timespec ts = {
+		.tv_sec = duration / precision,
+		.tv_nsec = duration * 1000000000 / precision % 1000000000,
+	};
+	int status;
+	do status = clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, &ts);
+	while (status == EINTR);
+#else
+	UINT64 precision = I_GetPrecisePrecision();
+	INT32 sleepvalue = cv_sleep.value;
+	UINT64 delaygranularity;
+	precise_t cur;
+	precise_t dest;
+
+	{
+		double gran = round(((double)(precision / 1000) * sleepvalue * MIN_SLEEP_DURATION_MS));
+		delaygranularity = (UINT64)gran;
+	}
+
+	cur = I_GetPreciseTime();
+	dest = cur + duration;
+
+	// the reason this is not dest > cur is because the precise counter may wrap
+	// two's complement arithmetic is our friend here, though!
+	// e.g. cur 0xFFFFFFFFFFFFFFFE = -2, dest 0x0000000000000001 = 1
+	// 0x0000000000000001 - 0xFFFFFFFFFFFFFFFE = 3
+	while ((INT64)(dest - cur) > 0)
+	{
+		// If our cv_sleep value exceeds the remaining sleep duration, use the
+		// hard sleep function.
+		if (sleepvalue > 0 && (dest - cur) > delaygranularity)
+		{
+			I_Sleep(sleepvalue);
+		}
+
+		// Otherwise, this is a spinloop.
+
+		cur = I_GetPreciseTime();
+	}
+#endif
+}
+
 #ifdef NEWSIGNALHANDLER
 ATTRNORETURN static FUNCNORETURN void newsignalhandler_Warn(const char *pr)
 {
@@ -2387,9 +2450,7 @@ void I_Quit(void)
 	SDLforceUngrabMouse();
 	quiting = SDL_FALSE;
 	M_SaveConfig(NULL); //save game config, cvars..
-#ifndef NONET
 	D_SaveBan(); // save the ban list
-#endif
 	G_SaveGameData(clientGamedata); // Tails 12-08-2002
 	//added:16-02-98: when recording a demo, should exit using 'q' key,
 	//        but sometimes we forget and use 'F10'.. so save here too.
@@ -2504,9 +2565,7 @@ void I_Error(const char *error, ...)
 	// ---
 
 	M_SaveConfig(NULL); // save game config, cvars..
-#ifndef NONET
 	D_SaveBan(); // save the ban list
-#endif
 	G_SaveGameData(clientGamedata); // Tails 12-08-2002
 
 	// Shutdown. Here might be other errors.
diff --git a/src/sdl/i_ttf.c b/src/sdl/i_ttf.c
index f2cd497eec03f24a6e3088e7e4f64e0925e16cc1..1f838e9b46267dfc5adec7f1f75541bf7765754b 100644
--- a/src/sdl/i_ttf.c
+++ b/src/sdl/i_ttf.c
@@ -21,7 +21,7 @@
 #include "SDL_ttf.h"
 #include "../doomdef.h"
 #include "../doomstat.h"
-#include "../d_netfil.h"
+#include "../netcode/d_netfil.h"
 #include "../filesrch.h"
 #include "i_ttf.h"
 
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index 590d7d142a7a536678bfb96d3891c78264a19fba..d3a602c05803fcd5d6aced670321aad5c723ea74 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -382,10 +382,8 @@ static INT32 Impl_SDL_Scancode_To_Keycode(SDL_Scancode code)
 	return 0;
 }
 
-static boolean IgnoreMouse(void)
+static boolean ShouldIgnoreMouse(void)
 {
-	if (cv_alwaysgrabmouse.value)
-		return false;
 	if (menuactive)
 		return !M_MouseNeeded();
 	if (paused || con_destlines || chat_on)
@@ -393,11 +391,20 @@ static boolean IgnoreMouse(void)
 	if (gamestate != GS_LEVEL && gamestate != GS_INTERMISSION &&
 			gamestate != GS_CONTINUING && gamestate != GS_CUTSCENE)
 		return true;
-	if (!mousegrabbedbylua)
-		return true;
 	return false;
 }
 
+static boolean ShouldGrabMouse(void)
+{
+	if (cv_alwaysgrabmouse.value)
+		return true;
+	if (ShouldIgnoreMouse())
+		return false;
+	if (!mousegrabbedbylua)
+		return false;
+	return true;
+}
+
 static void SDLdoGrabMouse(void)
 {
 	SDL_ShowCursor(SDL_DISABLE);
@@ -424,7 +431,7 @@ void I_UpdateMouseGrab(void)
 {
 	if (SDL_WasInit(SDL_INIT_VIDEO) == SDL_INIT_VIDEO && window != NULL
 	&& SDL_GetMouseFocus() == window && SDL_GetKeyboardFocus() == window
-	&& USE_MOUSEINPUT && !IgnoreMouse())
+	&& USE_MOUSEINPUT && ShouldGrabMouse())
 		SDLdoGrabMouse();
 }
 
@@ -640,7 +647,7 @@ static void Impl_HandleWindowEvent(SDL_WindowEvent evt)
 		}
 		//else firsttimeonmouse = SDL_FALSE;
 
-		if (USE_MOUSEINPUT && !IgnoreMouse())
+		if (USE_MOUSEINPUT && ShouldGrabMouse())
 			SDLdoGrabMouse();
 	}
 	else if (!mousefocus && !kbfocus)
@@ -686,13 +693,26 @@ static void Impl_HandleKeyboardEvent(SDL_KeyboardEvent evt, Uint32 type)
 	if (event.key) D_PostEvent(&event);
 }
 
+static void Impl_HandleTextEvent(SDL_TextInputEvent evt)
+{
+	event_t event;
+	event.type = ev_text;
+	if (evt.text[1] != '\0')
+	{
+		// limit ourselves to ASCII for now, we can add UTF-8 support later
+		return;
+	}
+	event.key = evt.text[0];
+	D_PostEvent(&event);
+}
+
 static void Impl_HandleMouseMotionEvent(SDL_MouseMotionEvent evt)
 {
 	static boolean firstmove = true;
 
 	if (USE_MOUSEINPUT)
 	{
-		if ((SDL_GetMouseFocus() != window && SDL_GetKeyboardFocus() != window) || (IgnoreMouse() && !firstmove))
+		if ((SDL_GetMouseFocus() != window && SDL_GetKeyboardFocus() != window) || (!ShouldGrabMouse() && !firstmove))
 		{
 			SDLdoUngrabMouse();
 			firstmove = false;
@@ -745,7 +765,7 @@ static void Impl_HandleMouseButtonEvent(SDL_MouseButtonEvent evt, Uint32 type)
 	// this apparently makes a mouse button down event but not a mouse button up event,
 	// resulting in whatever key was pressed down getting "stuck" if we don't ignore it.
 	// -- Monster Iestyn (28/05/18)
-	if (SDL_GetMouseFocus() != window || IgnoreMouse())
+	if (SDL_GetMouseFocus() != window || ShouldIgnoreMouse())
 		return;
 
 	/// \todo inputEvent.button.which
@@ -934,6 +954,9 @@ void I_GetEvent(void)
 			case SDL_KEYDOWN:
 				Impl_HandleKeyboardEvent(evt.key, evt.type);
 				break;
+			case SDL_TEXTINPUT:
+				Impl_HandleTextEvent(evt.text);
+				break;
 			case SDL_MOUSEMOTION:
 				//if (!mouseMotionOnce)
 				Impl_HandleMouseMotionEvent(evt.motion);
@@ -1127,7 +1150,7 @@ void I_StartupMouse(void)
 	}
 	else
 		firsttimeonmouse = SDL_FALSE;
-	if (cv_usemouse.value && !IgnoreMouse())
+	if (cv_usemouse.value && ShouldGrabMouse())
 		SDLdoGrabMouse();
 	else
 		SDLdoUngrabMouse();
diff --git a/src/sdl/macosx/Srb2mac.pbproj/project.pbxproj b/src/sdl/macosx/Srb2mac.pbproj/project.pbxproj
index 909bb2ced72d7f3cc7a157d87fea0eac37dfd178..40f580be12967abc4e049372810215e89005ce8f 100644
--- a/src/sdl/macosx/Srb2mac.pbproj/project.pbxproj
+++ b/src/sdl/macosx/Srb2mac.pbproj/project.pbxproj
@@ -2133,7 +2133,7 @@
 				INSTALL_PATH = "$(HOME)/Applications";
 				JAVA_COMPILER_DEBUGGING_SYMBOLS = NO;
 				OPTIMIZATION_CFLAGS = "-O2";
-				OTHER_CFLAGS = "-DMAC_ALERT -DUNIXCOMMON -DSDLMAIN -DHAVE_MIXER -DHAVE_PNG -D_BIG_ENDIAN -DSTDC_HEADERS -DSDL -Wall -Winline -fno-strict-aliasing";
+				OTHER_CFLAGS = "-DMAC_ALERT -DUNIXCOMMON -DSDLMAIN -DHAVE_MIXER -DHAVE_PNG -D_BIG_ENDIAN -DSTDC_HEADERS -DSDL -Wall -Winline -fno-strict-aliasing -fwrapv";
 				OTHER_REZFLAGS = "";
 				PREBINDING = NO;
 				PRODUCT_NAME = Srb2;
diff --git a/src/sdl/mixer_sound.c b/src/sdl/mixer_sound.c
index 0a39c7f286f8dc3e4b563709de240b0370e1b2b9..a5be3a754ce5a44b5c985f5881300d06a5ad07c7 100644
--- a/src/sdl/mixer_sound.c
+++ b/src/sdl/mixer_sound.c
@@ -759,8 +759,8 @@ static void mix_gme(void *udata, Uint8 *stream, int len)
 		music_volume = 18;
 
 	// apply volume to stream
-	for (i = 0, p = (short *)stream; i < len/2; i++, p++)
-		*p = ((INT32)*p) * (music_volume*internal_volume/100)*2 / 40;
+	for (i = 0, p = (short *)stream; i < len / 2; i++, p++)
+		*p = ((INT32)*p) * music_volume * internal_volume / 100 / 20;
 }
 #endif
 
@@ -783,8 +783,8 @@ static void mix_openmpt(void *udata, Uint8 *stream, int len)
 		music_volume = 18;
 
 	// apply volume to stream
-	for (i = 0, p = (short *)stream; i < len/2; i++, p++)
-		*p = ((INT32)*p) * (music_volume*internal_volume/100)*2 / 40;
+	for (i = 0, p = (short *)stream; i < len / 2; i++, p++)
+		*p = ((INT32)*p) * music_volume * internal_volume / 100 / 20;
 }
 #endif
 
@@ -1441,7 +1441,7 @@ void I_SetMusicVolume(UINT8 volume)
 	Mix_VolumeMusic(get_real_volume(music_volume));
 }
 
-boolean I_SetSongTrack(int track)
+boolean I_SetSongTrack(INT32 track)
 {
 #ifdef HAVE_GME
 	// If the specified track is within the number of tracks playing, then change it
diff --git a/src/sdl/sdl_sound.c b/src/sdl/sdl_sound.c
index 2ca35b95456e004ee53105165428529d79d16f0d..2705261d623e4d1109ecdbdd2aa7d6fb5072579b 100644
--- a/src/sdl/sdl_sound.c
+++ b/src/sdl/sdl_sound.c
@@ -1471,7 +1471,7 @@ void I_SetMusicVolume(UINT8 volume)
 	(void)volume;
 }
 
-boolean I_SetSongTrack(int track)
+boolean I_SetSongTrack(INT32 track)
 {
 	(void)track;
 	return false;
diff --git a/src/snake.c b/src/snake.c
new file mode 100644
index 0000000000000000000000000000000000000000..2349d5fdbfb6d66af7d4bc55df03b553052153c8
--- /dev/null
+++ b/src/snake.c
@@ -0,0 +1,593 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2023-2023 by Louis-Antoine de Moulins de Rochefort.
+//
+// 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  snake.c
+/// \brief Snake minigame for the download screen.
+
+#include "snake.h"
+#include "g_input.h"
+#include "g_game.h"
+#include "i_joy.h"
+#include "m_random.h"
+#include "s_sound.h"
+#include "screen.h"
+#include "v_video.h"
+#include "w_wad.h"
+#include "z_zone.h"
+
+#define SPEED 5
+
+#define NUM_BLOCKS_X 20
+#define NUM_BLOCKS_Y 10
+#define BLOCK_SIZE 12
+#define BORDER_SIZE 12
+
+#define MAP_WIDTH  (NUM_BLOCKS_X * BLOCK_SIZE)
+#define MAP_HEIGHT (NUM_BLOCKS_Y * BLOCK_SIZE)
+
+#define LEFT_X ((BASEVIDWIDTH - MAP_WIDTH) / 2 - BORDER_SIZE)
+#define RIGHT_X (LEFT_X + MAP_WIDTH + BORDER_SIZE * 2 - 1)
+#define BOTTOM_Y (BASEVIDHEIGHT - 48)
+#define TOP_Y (BOTTOM_Y - MAP_HEIGHT - BORDER_SIZE * 2 + 1)
+
+enum bonustype_s {
+	BONUS_NONE = 0,
+	BONUS_SLOW,
+	BONUS_FAST,
+	BONUS_GHOST,
+	BONUS_NUKE,
+	BONUS_SCISSORS,
+	BONUS_REVERSE,
+	BONUS_EGGMAN,
+	NUM_BONUSES,
+};
+
+typedef struct snake_s
+{
+	boolean paused;
+	boolean pausepressed;
+	tic_t time;
+	tic_t nextupdate;
+	boolean gameover;
+	UINT8 background;
+
+	UINT16 snakelength;
+	enum bonustype_s snakebonus;
+	tic_t snakebonustime;
+	UINT8 snakex[NUM_BLOCKS_X * NUM_BLOCKS_Y];
+	UINT8 snakey[NUM_BLOCKS_X * NUM_BLOCKS_Y];
+	UINT8 snakedir[NUM_BLOCKS_X * NUM_BLOCKS_Y];
+
+	UINT8 applex;
+	UINT8 appley;
+
+	enum bonustype_s bonustype;
+	UINT8 bonusx;
+	UINT8 bonusy;
+
+	event_t *joyevents[MAXEVENTS];
+	UINT16 joyeventcount;
+} snake_t;
+
+static const char *bonuspatches[] = {
+	NULL,
+	"DL_SLOW",
+	"TVSSC0",
+	"TVIVC0",
+	"TVARC0",
+	"DL_SCISSORS",
+	"TVRCC0",
+	"TVEGC0",
+};
+
+static const char *backgrounds[] = {
+	"RVPUMICF",
+	"FRSTRCKF",
+	"TAR",
+	"MMFLRB4",
+	"RVDARKF1",
+	"RVZWALF1",
+	"RVZWALF4",
+	"RVZWALF5",
+	"RVZGRS02",
+	"RVZGRS04",
+};
+
+static void Initialise(snake_t *snake)
+{
+	snake->paused = false;
+	snake->pausepressed = false;
+	snake->time = 0;
+	snake->nextupdate = SPEED;
+	snake->gameover = false;
+	snake->background = M_RandomKey(sizeof(backgrounds) / sizeof(*backgrounds));
+
+	snake->snakelength = 1;
+	snake->snakebonus = BONUS_NONE;
+	snake->snakex[0] = M_RandomKey(NUM_BLOCKS_X);
+	snake->snakey[0] = M_RandomKey(NUM_BLOCKS_Y);
+	snake->snakedir[0] = 0;
+	snake->snakedir[1] = 0;
+
+	snake->applex = M_RandomKey(NUM_BLOCKS_X);
+	snake->appley = M_RandomKey(NUM_BLOCKS_Y);
+
+	snake->bonustype = BONUS_NONE;
+
+	snake->joyeventcount = 0;
+}
+
+static UINT8 GetOppositeDir(UINT8 dir)
+{
+	if (dir == 1 || dir == 3)
+		return dir + 1;
+	else if (dir == 2 || dir == 4)
+		return dir - 1;
+	else
+		return 12 + 5 - dir;
+}
+
+static void FindFreeSlot(snake_t *snake, UINT8 *freex, UINT8 *freey, UINT8 headx, UINT8 heady)
+{
+	UINT8 x, y;
+	UINT16 i;
+
+	do
+	{
+		x = M_RandomKey(NUM_BLOCKS_X);
+		y = M_RandomKey(NUM_BLOCKS_Y);
+
+		for (i = 0; i < snake->snakelength; i++)
+			if (x == snake->snakex[i] && y == snake->snakey[i])
+				break;
+	} while (i < snake->snakelength || (x == headx && y == heady)
+		|| (x == snake->applex && y == snake->appley)
+		|| (snake->bonustype != BONUS_NONE && x == snake->bonusx && y == snake->bonusy));
+
+	*freex = x;
+	*freey = y;
+}
+
+void Snake_Allocate(void **opaque)
+{
+	if (*opaque)
+		Snake_Free(opaque);
+	*opaque = malloc(sizeof(snake_t));
+	Initialise(*opaque);
+}
+
+void Snake_Update(void *opaque)
+{
+	UINT8 x, y;
+	UINT8 oldx, oldy;
+	UINT16 i;
+	UINT16 joystate = 0;
+	static INT32 pjoyx = 0, pjoyy = 0;
+
+	snake_t *snake = opaque;
+
+	// Handle retry
+	if (snake->gameover && (PLAYER1INPUTDOWN(GC_JUMP) || gamekeydown[KEY_ENTER]))
+	{
+		Initialise(snake);
+		snake->pausepressed = true; // Avoid accidental pause on respawn
+	}
+
+	// Handle pause
+	if (PLAYER1INPUTDOWN(GC_PAUSE) || gamekeydown[KEY_ENTER])
+	{
+		if (!snake->pausepressed)
+			snake->paused = !snake->paused;
+		snake->pausepressed = true;
+	}
+	else
+		snake->pausepressed = false;
+
+	if (snake->paused)
+		return;
+
+	snake->time++;
+
+	x = snake->snakex[0];
+	y = snake->snakey[0];
+	oldx = snake->snakex[1];
+	oldy = snake->snakey[1];
+
+	// Process the input events in here dear lord
+	for (UINT16 j = 0; j < snake->joyeventcount; j++)
+	{
+		event_t *ev = snake->joyevents[j];
+		const INT32 jdeadzone = (JOYAXISRANGE * cv_digitaldeadzone.value) / FRACUNIT;
+		if (ev->y != INT32_MAX)
+		{
+			if (Joystick.bGamepadStyle || abs(ev->y) > jdeadzone)
+			{
+				if (ev->y < 0 && pjoyy >= 0)
+					joystate = 1;
+				else if (ev->y > 0 && pjoyy <= 0)
+					joystate = 2;
+				pjoyy = ev->y;
+			}
+			else
+				pjoyy = 0;
+		}
+
+		if (ev->x != INT32_MAX)
+		{
+			if (Joystick.bGamepadStyle || abs(ev->x) > jdeadzone)
+			{
+				if (ev->x < 0 && pjoyx >= 0)
+					joystate = 3;
+				else if (ev->x > 0 && pjoyx <= 0)
+					joystate = 4;
+				pjoyx = ev->x;
+			}
+			else
+				pjoyx = 0;
+		}
+	}
+	snake->joyeventcount = 0;
+
+	// Update direction
+	if (PLAYER1INPUTDOWN(GC_STRAFELEFT) || gamekeydown[KEY_LEFTARROW] || joystate == 3)
+	{
+		if (snake->snakelength < 2 || x <= oldx)
+			snake->snakedir[0] = 1;
+	}
+	else if (PLAYER1INPUTDOWN(GC_STRAFERIGHT) || gamekeydown[KEY_RIGHTARROW] || joystate == 4)
+	{
+		if (snake->snakelength < 2 || x >= oldx)
+			snake->snakedir[0] = 2;
+	}
+	else if (PLAYER1INPUTDOWN(GC_FORWARD) || gamekeydown[KEY_UPARROW] || joystate == 1)
+	{
+		if (snake->snakelength < 2 || y <= oldy)
+			snake->snakedir[0] = 3;
+	}
+	else if (PLAYER1INPUTDOWN(GC_BACKWARD) || gamekeydown[KEY_DOWNARROW] || joystate == 2)
+	{
+		if (snake->snakelength < 2 || y >= oldy)
+			snake->snakedir[0] = 4;
+	}
+
+	if (snake->snakebonustime)
+	{
+		snake->snakebonustime--;
+		if (!snake->snakebonustime)
+			snake->snakebonus = BONUS_NONE;
+	}
+
+	snake->nextupdate--;
+	if (snake->nextupdate)
+		return;
+	if (snake->snakebonus == BONUS_SLOW)
+		snake->nextupdate = SPEED * 2;
+	else if (snake->snakebonus == BONUS_FAST)
+		snake->nextupdate = SPEED * 2 / 3;
+	else
+		snake->nextupdate = SPEED;
+
+	if (snake->gameover)
+		return;
+
+	// Find new position
+	switch (snake->snakedir[0])
+	{
+		case 1:
+			if (x > 0)
+				x--;
+			else
+				snake->gameover = true;
+			break;
+		case 2:
+			if (x < NUM_BLOCKS_X - 1)
+				x++;
+			else
+				snake->gameover = true;
+			break;
+		case 3:
+			if (y > 0)
+				y--;
+			else
+				snake->gameover = true;
+			break;
+		case 4:
+			if (y < NUM_BLOCKS_Y - 1)
+				y++;
+			else
+				snake->gameover = true;
+			break;
+	}
+
+	// Check collision with snake
+	if (snake->snakebonus != BONUS_GHOST)
+		for (i = 1; i < snake->snakelength - 1; i++)
+			if (x == snake->snakex[i] && y == snake->snakey[i])
+			{
+				if (snake->snakebonus == BONUS_SCISSORS)
+				{
+					snake->snakebonus = BONUS_NONE;
+					snake->snakelength = i;
+					S_StartSound(NULL, sfx_adderr);
+				}
+				else
+					snake->gameover = true;
+			}
+
+	if (snake->gameover)
+	{
+		S_StartSound(NULL, sfx_lose);
+		return;
+	}
+
+	// Check collision with apple
+	if (x == snake->applex && y == snake->appley)
+	{
+		if (snake->snakelength + 3 < NUM_BLOCKS_X * NUM_BLOCKS_Y)
+		{
+			snake->snakelength++;
+			snake->snakex  [snake->snakelength - 1] = snake->snakex  [snake->snakelength - 2];
+			snake->snakey  [snake->snakelength - 1] = snake->snakey  [snake->snakelength - 2];
+			snake->snakedir[snake->snakelength - 1] = snake->snakedir[snake->snakelength - 2];
+		}
+
+		// Spawn new apple
+		FindFreeSlot(snake, &snake->applex, &snake->appley, x, y);
+
+		// Spawn new bonus
+		if (!(snake->snakelength % 5))
+		{
+			do
+			{
+				snake->bonustype = M_RandomKey(NUM_BONUSES - 1) + 1;
+			} while (snake->snakelength > NUM_BLOCKS_X * NUM_BLOCKS_Y * 3 / 4
+				&& (snake->bonustype == BONUS_EGGMAN || snake->bonustype == BONUS_FAST || snake->bonustype == BONUS_REVERSE));
+
+			FindFreeSlot(snake, &snake->bonusx, &snake->bonusy, x, y);
+		}
+
+		S_StartSound(NULL, sfx_s3k6b);
+	}
+
+	if (snake->snakelength > 1 && snake->snakedir[0])
+	{
+		UINT8 dir = snake->snakedir[0];
+
+		oldx = snake->snakex[1];
+		oldy = snake->snakey[1];
+
+		// Move
+		for (i = snake->snakelength - 1; i > 0; i--)
+		{
+			snake->snakex[i] = snake->snakex[i - 1];
+			snake->snakey[i] = snake->snakey[i - 1];
+			snake->snakedir[i] = snake->snakedir[i - 1];
+		}
+
+		// Handle corners
+		if      (x < oldx && dir == 3)
+			dir = 5;
+		else if (x > oldx && dir == 3)
+			dir = 6;
+		else if (x < oldx && dir == 4)
+			dir = 7;
+		else if (x > oldx && dir == 4)
+			dir = 8;
+		else if (y < oldy && dir == 1)
+			dir = 9;
+		else if (y < oldy && dir == 2)
+			dir = 10;
+		else if (y > oldy && dir == 1)
+			dir = 11;
+		else if (y > oldy && dir == 2)
+			dir = 12;
+		snake->snakedir[1] = dir;
+	}
+
+	snake->snakex[0] = x;
+	snake->snakey[0] = y;
+
+	// Check collision with bonus
+	if (snake->bonustype != BONUS_NONE && x == snake->bonusx && y == snake->bonusy)
+	{
+		S_StartSound(NULL, sfx_ncchip);
+
+		switch (snake->bonustype)
+		{
+		case BONUS_SLOW:
+			snake->snakebonus = BONUS_SLOW;
+			snake->snakebonustime = 20 * TICRATE;
+			break;
+		case BONUS_FAST:
+			snake->snakebonus = BONUS_FAST;
+			snake->snakebonustime = 20 * TICRATE;
+			break;
+		case BONUS_GHOST:
+			snake->snakebonus = BONUS_GHOST;
+			snake->snakebonustime = 10 * TICRATE;
+			break;
+		case BONUS_NUKE:
+			for (i = 0; i < snake->snakelength; i++)
+			{
+				snake->snakex  [i] = snake->snakex  [0];
+				snake->snakey  [i] = snake->snakey  [0];
+				snake->snakedir[i] = snake->snakedir[0];
+			}
+
+			S_StartSound(NULL, sfx_bkpoof);
+			break;
+		case BONUS_SCISSORS:
+			snake->snakebonus = BONUS_SCISSORS;
+			snake->snakebonustime = 60 * TICRATE;
+			break;
+		case BONUS_REVERSE:
+			for (i = 0; i < (snake->snakelength + 1) / 2; i++)
+			{
+				UINT16 i2 = snake->snakelength - 1 - i;
+				UINT8 tmpx   = snake->snakex  [i];
+				UINT8 tmpy   = snake->snakey  [i];
+				UINT8 tmpdir = snake->snakedir[i];
+
+				// Swap first segment with last segment
+				snake->snakex  [i] = snake->snakex  [i2];
+				snake->snakey  [i] = snake->snakey  [i2];
+				snake->snakedir[i] = GetOppositeDir(snake->snakedir[i2]);
+				snake->snakex  [i2] = tmpx;
+				snake->snakey  [i2] = tmpy;
+				snake->snakedir[i2] = GetOppositeDir(tmpdir);
+			}
+
+			snake->snakedir[0] = 0;
+
+			S_StartSound(NULL, sfx_gravch);
+			break;
+		default:
+			if (snake->snakebonus != BONUS_GHOST)
+			{
+				snake->gameover = true;
+				S_StartSound(NULL, sfx_lose);
+			}
+		}
+
+		snake->bonustype = BONUS_NONE;
+	}
+}
+
+void Snake_Draw(void *opaque)
+{
+	INT16 i;
+
+	snake_t *snake = opaque;
+
+	// Background
+	V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+
+	V_DrawFlatFill(
+		LEFT_X + BORDER_SIZE,
+		TOP_Y  + BORDER_SIZE,
+		MAP_WIDTH,
+		MAP_HEIGHT,
+		W_GetNumForName(backgrounds[snake->background])
+	);
+
+	// Borders
+	V_DrawFill(LEFT_X, TOP_Y, BORDER_SIZE + MAP_WIDTH, BORDER_SIZE, 242); // Top
+	V_DrawFill(LEFT_X + BORDER_SIZE + MAP_WIDTH, TOP_Y, BORDER_SIZE, BORDER_SIZE + MAP_HEIGHT, 242); // Right
+	V_DrawFill(LEFT_X + BORDER_SIZE, TOP_Y + BORDER_SIZE + MAP_HEIGHT, BORDER_SIZE + MAP_WIDTH, BORDER_SIZE, 242); // Bottom
+	V_DrawFill(LEFT_X, TOP_Y + BORDER_SIZE, BORDER_SIZE, BORDER_SIZE + MAP_HEIGHT, 242); // Left
+
+	// Apple
+	V_DrawFixedPatch(
+		(LEFT_X + BORDER_SIZE + snake->applex * BLOCK_SIZE + BLOCK_SIZE / 2) * FRACUNIT,
+		(TOP_Y  + BORDER_SIZE + snake->appley * BLOCK_SIZE + BLOCK_SIZE / 2) * FRACUNIT,
+		FRACUNIT / 4,
+		0,
+		W_CachePatchLongName("DL_APPLE", PU_HUDGFX),
+		NULL
+	);
+
+	// Bonus
+	if (snake->bonustype != BONUS_NONE)
+		V_DrawFixedPatch(
+			(LEFT_X + BORDER_SIZE + snake->bonusx * BLOCK_SIZE + BLOCK_SIZE / 2    ) * FRACUNIT,
+			(TOP_Y  + BORDER_SIZE + snake->bonusy * BLOCK_SIZE + BLOCK_SIZE / 2 + 4) * FRACUNIT,
+			FRACUNIT / 2,
+			0,
+			W_CachePatchLongName(bonuspatches[snake->bonustype], PU_HUDGFX),
+			NULL
+		);
+
+	// Snake
+	if (!snake->gameover || snake->time % 8 < 8 / 2) // Blink if game over
+	{
+		for (i = snake->snakelength - 1; i >= 0; i--)
+		{
+			const char *patchname;
+			UINT8 dir = snake->snakedir[i];
+
+			if (i == 0) // Head
+			{
+				switch (dir)
+				{
+					case  1: patchname = "DL_SNAKEHEAD_L"; break;
+					case  2: patchname = "DL_SNAKEHEAD_R"; break;
+					case  3: patchname = "DL_SNAKEHEAD_T"; break;
+					case  4: patchname = "DL_SNAKEHEAD_B"; break;
+					default: patchname = "DL_SNAKEHEAD_M";
+				}
+			}
+			else // Body
+			{
+				switch (dir)
+				{
+					case  1: patchname = "DL_SNAKEBODY_L"; break;
+					case  2: patchname = "DL_SNAKEBODY_R"; break;
+					case  3: patchname = "DL_SNAKEBODY_T"; break;
+					case  4: patchname = "DL_SNAKEBODY_B"; break;
+					case  5: patchname = "DL_SNAKEBODY_LT"; break;
+					case  6: patchname = "DL_SNAKEBODY_RT"; break;
+					case  7: patchname = "DL_SNAKEBODY_LB"; break;
+					case  8: patchname = "DL_SNAKEBODY_RB"; break;
+					case  9: patchname = "DL_SNAKEBODY_TL"; break;
+					case 10: patchname = "DL_SNAKEBODY_TR"; break;
+					case 11: patchname = "DL_SNAKEBODY_BL"; break;
+					case 12: patchname = "DL_SNAKEBODY_BR"; break;
+					default: patchname = "DL_SNAKEBODY_B";
+				}
+			}
+
+			V_DrawFixedPatch(
+				(LEFT_X + BORDER_SIZE + snake->snakex[i] * BLOCK_SIZE + BLOCK_SIZE / 2) * FRACUNIT,
+				(TOP_Y  + BORDER_SIZE + snake->snakey[i] * BLOCK_SIZE + BLOCK_SIZE / 2) * FRACUNIT,
+				i == 0 && dir == 0 ? FRACUNIT / 5 : FRACUNIT / 2,
+				snake->snakebonus == BONUS_GHOST ? V_TRANSLUCENT : 0,
+				W_CachePatchLongName(patchname, PU_HUDGFX),
+				NULL
+			);
+		}
+	}
+
+	// Length
+	V_DrawString(RIGHT_X + 4, TOP_Y, V_MONOSPACE, va("%u", snake->snakelength));
+
+	// Bonus
+	if (snake->snakebonus != BONUS_NONE
+	&& (snake->snakebonustime >= 3 * TICRATE || snake->time % 4 < 4 / 2))
+		V_DrawFixedPatch(
+			(RIGHT_X + 10) * FRACUNIT,
+			(TOP_Y + 24) * FRACUNIT,
+			FRACUNIT / 2,
+			0,
+			W_CachePatchLongName(bonuspatches[snake->snakebonus], PU_HUDGFX),
+			NULL
+		);
+}
+
+void Snake_Free(void **opaque)
+{
+	if (*opaque)
+	{
+		free(*opaque);
+		*opaque = NULL;
+	}
+}
+
+// I'm screaming the hack is clean - ashi
+boolean Snake_JoyGrabber(void *opaque, event_t *ev)
+{
+	snake_t *snake = opaque;
+
+	if (ev->type == ev_joystick  && ev->key == 0)
+	{
+		snake->joyevents[snake->joyeventcount] = ev;
+		snake->joyeventcount++;
+		return true;
+	}
+	else
+		return false;
+}
diff --git a/src/snake.h b/src/snake.h
new file mode 100644
index 0000000000000000000000000000000000000000..6bca338e9643d91f567a1b2f701013bb7cfb29c6
--- /dev/null
+++ b/src/snake.h
@@ -0,0 +1,23 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2023-2023 by Louis-Antoine de Moulins de Rochefort.
+//
+// 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  snake.h
+/// \brief Snake minigame for the download screen.
+
+#ifndef __SNAKE__
+#define __SNAKE__
+
+#include "d_event.h"
+
+void Snake_Allocate(void **opaque);
+void Snake_Update(void *opaque);
+void Snake_Draw(void *opaque);
+void Snake_Free(void **opaque);
+boolean Snake_JoyGrabber(void *opaque, event_t *ev);
+
+#endif
diff --git a/src/st_stuff.c b/src/st_stuff.c
index b9f0c6bb93e1ab4d1b9e35a958670571883086e4..a975379785570a6dca859031932c0f749c75dc70 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -513,10 +513,10 @@ static void ST_drawDebugInfo(void)
 
 #define VFLAGS V_MONOSPACE|V_SNAPTOTOP|V_SNAPTORIGHT
 
-	if ((moviemode == MM_GIF && cv_gif_downscale.value) || vid.dupx == 1)
+	if ((moviemode == MM_GIF && cv_gif_downscale.value) || vid.dup == 1)
 	{
 		textfunc = V_DrawRightAlignedString;
-		lowh = ((vid.height/vid.dupy) - 16);
+		lowh = ((vid.height/vid.dup) - 16);
 	}
 	else
 	{
@@ -827,6 +827,8 @@ static void ST_drawLivesArea(void)
 	V_DrawSmallScaledPatch(hudinfo[HUD_LIVES].x, hudinfo[HUD_LIVES].y,
 		hudinfo[HUD_LIVES].f|V_PERPLAYER|V_HUDTRANS, livesback);
 
+	UINT16 facecolor = P_GetPlayerColor(stplyr);
+
 	// face
 	if (stplyr->spectator)
 	{
@@ -856,10 +858,10 @@ static void ST_drawLivesArea(void)
 			}
 		}
 	}
-	else if (stplyr->skincolor)
+	else if (facecolor)
 	{
 		// skincolor face
-		UINT8 *colormap = R_GetTranslationColormap(stplyr->skin, stplyr->skincolor, GTC_CACHE);
+		UINT8 *colormap = R_GetTranslationColormap(stplyr->skin, facecolor, GTC_CACHE);
 		V_DrawSmallMappedPatch(hudinfo[HUD_LIVES].x, hudinfo[HUD_LIVES].y,
 			hudinfo[HUD_LIVES].f|V_PERPLAYER|V_HUDTRANS, faceprefix[stplyr->skin], colormap);
 	}
@@ -1030,7 +1032,8 @@ static void ST_drawLivesArea(void)
 
 static void ST_drawInput(void)
 {
-	const INT32 accent = V_SNAPTOLEFT|V_SNAPTOBOTTOM|(stplyr->skincolor ? skincolors[stplyr->skincolor].ramp[4] : 0);
+	UINT16 color = P_GetPlayerColor(stplyr);
+	const INT32 accent = V_SNAPTOLEFT|V_SNAPTOBOTTOM|(color ? skincolors[color].ramp[4] : 0);
 	INT32 col;
 	UINT8 offs;
 
diff --git a/src/taglist.c b/src/taglist.c
index e4e385b9eb7f57e9f0758418e6db2ecbe7748a34..7bd0046275e6d5c3bf463fa9a65c230391ae92ca 100644
--- a/src/taglist.c
+++ b/src/taglist.c
@@ -180,10 +180,10 @@ void Taggroup_Add (taggroup_t *garray[], const mtag_t tag, size_t id)
 	if (Taggroup_Find(group, id) != (size_t)-1)
 		return;
 
-	if (! in_bit_array(tags_available, tag))
+	if (! in_bit_array(tags_available, (UINT16)tag))
 	{
 		num_tags++;
-		set_bit_array(tags_available, tag);
+		set_bit_array(tags_available, (UINT16)tag);
 	}
 
 	// Create group if empty.
@@ -220,10 +220,10 @@ static void Taggroup_Add_Init(taggroup_t *garray[], const mtag_t tag, size_t id)
 
 	group = garray[(UINT16)tag];
 
-	if (! in_bit_array(tags_available, tag))
+	if (! in_bit_array(tags_available, (UINT16)tag))
 	{
 		num_tags++;
-		set_bit_array(tags_available, tag);
+		set_bit_array(tags_available, (UINT16)tag);
 	}
 
 	// Create group if empty.
@@ -271,7 +271,7 @@ void Taggroup_Remove (taggroup_t *garray[], const mtag_t tag, size_t id)
 	if (group->count == 1 && total_elements_with_tag(tag) == 1)
 	{
 		num_tags--;
-		unset_bit_array(tags_available, tag);
+		unset_bit_array(tags_available, (UINT16)tag);
 	}
 
 	// Strip away taggroup if no elements left.
diff --git a/src/v_video.c b/src/v_video.c
index 3f958b286cdfdcc275a23c5822605812d218c242..30aef92cc3c3f2df107f6de3104c78a86fca9e5c 100644
--- a/src/v_video.c
+++ b/src/v_video.c
@@ -505,7 +505,7 @@ void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vsca
 	UINT32 blendmode = ((scrn & V_BLENDMASK) >> V_BLENDSHIFT);
 
 	fixed_t col, ofs, colfrac, rowfrac, fdup, vdup;
-	INT32 dupx, dupy;
+	INT32 dup;
 	const column_t *column;
 	UINT8 *desttop, *dest, *deststart, *destend;
 	const UINT8 *source, *deststop;
@@ -555,30 +555,23 @@ void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vsca
 		patchdrawfunc = (v_translevel) ? transmappedpdraw : mappedpdraw;
 	}
 
-	dupx = vid.dupx;
-	dupy = vid.dupy;
-	if (scrn & V_SCALEPATCHMASK) switch ((scrn & V_SCALEPATCHMASK) >> V_SCALEPATCHSHIFT)
+	dup = vid.dup;
+	if (scrn & V_SCALEPATCHMASK) switch (scrn & V_SCALEPATCHMASK)
 	{
-		case 1: // V_NOSCALEPATCH
-			dupx = dupy = 1;
-			break;
-		case 2: // V_SMALLSCALEPATCH
-			dupx = vid.smalldupx;
-			dupy = vid.smalldupy;
+		case V_NOSCALEPATCH:
+			dup = 1;
 			break;
-		case 3: // V_MEDSCALEPATCH
-			dupx = vid.meddupx;
-			dupy = vid.meddupy;
+		case V_SMALLSCALEPATCH:
+			dup = vid.smalldup;
 			break;
-		default:
+		case V_MEDSCALEPATCH:
+			dup = vid.meddup;
 			break;
 	}
 
-	// only use one dup, to avoid stretching (har har)
-	dupx = dupy = (dupx < dupy ? dupx : dupy);
-	fdup = vdup = FixedMul(dupx<<FRACBITS, pscale);
+	fdup = vdup = pscale * dup;
 	if (vscale != pscale)
-		vdup = FixedMul(dupx<<FRACBITS, vscale);
+		vdup = vscale * dup;
 	colfrac = FixedDiv(FRACUNIT, fdup);
 	rowfrac = FixedDiv(FRACUNIT, vdup);
 
@@ -684,8 +677,8 @@ void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vsca
 	}
 	else
 	{
-		x = FixedMul(x,dupx<<FRACBITS);
-		y = FixedMul(y,dupy<<FRACBITS);
+		x *= dup;
+		y *= dup;
 		x >>= FRACBITS;
 		y >>= FRACBITS;
 
@@ -703,30 +696,30 @@ void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vsca
 				}
 			}
 
-			if (vid.width != BASEVIDWIDTH * dupx)
+			if (vid.width != BASEVIDWIDTH * dup)
 			{
-				// dupx adjustments pretend that screen width is BASEVIDWIDTH * dupx,
+				// dup adjustments pretend that screen width is BASEVIDWIDTH * dup,
 				// so center this imaginary screen
 				if (scrn & V_SNAPTORIGHT)
-					x += (vid.width - (BASEVIDWIDTH * dupx));
+					x += (vid.width - (BASEVIDWIDTH * dup));
 				else if (!(scrn & V_SNAPTOLEFT))
-					x += (vid.width - (BASEVIDWIDTH * dupx)) / 2;
+					x += (vid.width - (BASEVIDWIDTH * dup)) / 2;
 				if (perplayershuffle & 4)
-					x -= (vid.width - (BASEVIDWIDTH * dupx)) / 4;
+					x -= (vid.width - (BASEVIDWIDTH * dup)) / 4;
 				else if (perplayershuffle & 8)
-					x += (vid.width - (BASEVIDWIDTH * dupx)) / 4;
+					x += (vid.width - (BASEVIDWIDTH * dup)) / 4;
 			}
-			if (vid.height != BASEVIDHEIGHT * dupy)
+			if (vid.height != BASEVIDHEIGHT * dup)
 			{
 				// same thing here
 				if (scrn & V_SNAPTOBOTTOM)
-					y += (vid.height - (BASEVIDHEIGHT * dupy));
+					y += (vid.height - (BASEVIDHEIGHT * dup));
 				else if (!(scrn & V_SNAPTOTOP))
-					y += (vid.height - (BASEVIDHEIGHT * dupy)) / 2;
+					y += (vid.height - (BASEVIDHEIGHT * dup)) / 2;
 				if (perplayershuffle & 1)
-					y -= (vid.height - (BASEVIDHEIGHT * dupy)) / 4;
+					y -= (vid.height - (BASEVIDHEIGHT * dup)) / 4;
 				else if (perplayershuffle & 2)
-					y += (vid.height - (BASEVIDHEIGHT * dupy)) / 4;
+					y += (vid.height - (BASEVIDHEIGHT * dup)) / 4;
 			}
 		}
 
@@ -737,11 +730,11 @@ void V_DrawStretchyFixedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vsca
 	{
 		pwidth = patch->width<<FRACBITS;
 		pwidth = FixedMul(pwidth, pscale);
-		pwidth = FixedMul(pwidth, dupx<<FRACBITS);
+		pwidth *= dup;
 		pwidth >>= FRACBITS;
 	}
 	else
-		pwidth = patch->width * dupx;
+		pwidth = patch->width * dup;
 
 	deststart = desttop;
 	destend = desttop + pwidth;
@@ -797,7 +790,7 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vscale, IN
 	// boolean flip = false;
 
 	fixed_t col, ofs, colfrac, rowfrac, fdup, vdup;
-	INT32 dupx, dupy;
+	INT32 dup;
 	const column_t *column;
 	UINT8 *desttop, *dest;
 	const UINT8 *source, *deststop;
@@ -845,30 +838,23 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vscale, IN
 		patchdrawfunc = (v_translevel) ? transmappedpdraw : mappedpdraw;
 	}
 
-	dupx = vid.dupx;
-	dupy = vid.dupy;
-	if (scrn & V_SCALEPATCHMASK) switch ((scrn & V_SCALEPATCHMASK) >> V_SCALEPATCHSHIFT)
+	dup = vid.dup;
+	if (scrn & V_SCALEPATCHMASK) switch (scrn & V_SCALEPATCHMASK)
 	{
-		case 1: // V_NOSCALEPATCH
-			dupx = dupy = 1;
-			break;
-		case 2: // V_SMALLSCALEPATCH
-			dupx = vid.smalldupx;
-			dupy = vid.smalldupy;
+		case V_NOSCALEPATCH:
+			dup = 1;
 			break;
-		case 3: // V_MEDSCALEPATCH
-			dupx = vid.meddupx;
-			dupy = vid.meddupy;
+		case V_SMALLSCALEPATCH:
+			dup = vid.smalldup;
 			break;
-		default:
+		case V_MEDSCALEPATCH:
+			dup = vid.meddup;
 			break;
 	}
 
-	// only use one dup, to avoid stretching (har har)
-	dupx = dupy = (dupx < dupy ? dupx : dupy);
-	fdup = vdup = FixedMul(dupx<<FRACBITS, pscale);
+	fdup = vdup = pscale * dup;
 	if (vscale != pscale)
-		vdup = FixedMul(dupx<<FRACBITS, vscale);
+		vdup = vscale * dup;
 	colfrac = FixedDiv(FRACUNIT, fdup);
 	rowfrac = FixedDiv(FRACUNIT, vdup);
 
@@ -960,8 +946,8 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vscale, IN
 	}
 	else
 	{
-		x = FixedMul(x,dupx<<FRACBITS);
-		y = FixedMul(y,dupy<<FRACBITS);
+		x *= dup;
+		y *= dup;
 		x >>= FRACBITS;
 		y >>= FRACBITS;
 
@@ -971,30 +957,30 @@ void V_DrawCroppedPatch(fixed_t x, fixed_t y, fixed_t pscale, fixed_t vscale, IN
 			// if it's meant to cover the whole screen, black out the rest
 			// no the patch is cropped do not do this ever
 
-			if (vid.width != BASEVIDWIDTH * dupx)
+			if (vid.width != BASEVIDWIDTH * dup)
 			{
-				// dupx adjustments pretend that screen width is BASEVIDWIDTH * dupx,
+				// dup adjustments pretend that screen width is BASEVIDWIDTH * dup,
 				// so center this imaginary screen
 				if (scrn & V_SNAPTORIGHT)
-					x += (vid.width - (BASEVIDWIDTH * dupx));
+					x += (vid.width - (BASEVIDWIDTH * dup));
 				else if (!(scrn & V_SNAPTOLEFT))
-					x += (vid.width - (BASEVIDWIDTH * dupx)) / 2;
+					x += (vid.width - (BASEVIDWIDTH * dup)) / 2;
 				if (perplayershuffle & 4)
-					x -= (vid.width - (BASEVIDWIDTH * dupx)) / 4;
+					x -= (vid.width - (BASEVIDWIDTH * dup)) / 4;
 				else if (perplayershuffle & 8)
-					x += (vid.width - (BASEVIDWIDTH * dupx)) / 4;
+					x += (vid.width - (BASEVIDWIDTH * dup)) / 4;
 			}
-			if (vid.height != BASEVIDHEIGHT * dupy)
+			if (vid.height != BASEVIDHEIGHT * dup)
 			{
 				// same thing here
 				if (scrn & V_SNAPTOBOTTOM)
-					y += (vid.height - (BASEVIDHEIGHT * dupy));
+					y += (vid.height - (BASEVIDHEIGHT * dup));
 				else if (!(scrn & V_SNAPTOTOP))
-					y += (vid.height - (BASEVIDHEIGHT * dupy)) / 2;
+					y += (vid.height - (BASEVIDHEIGHT * dup)) / 2;
 				if (perplayershuffle & 1)
-					y -= (vid.height - (BASEVIDHEIGHT * dupy)) / 4;
+					y -= (vid.height - (BASEVIDHEIGHT * dup)) / 4;
 				else if (perplayershuffle & 2)
-					y += (vid.height - (BASEVIDHEIGHT * dupy)) / 4;
+					y += (vid.height - (BASEVIDHEIGHT * dup)) / 4;
 			}
 		}
 
@@ -1150,22 +1136,22 @@ static void V_BlitScaledPic(INT32 rx1, INT32 ry1, INT32 scrn, pic_t * pic)
 
 	dest = screens[scrn] + max(0, ry1 * vid.width) + max(0, rx1);
 	// y cliping to the screen
-	if (ry1 + height * vid.dupy >= vid.width)
-		height = (vid.width - ry1) / vid.dupy - 1;
+	if (ry1 + height * vid.dup >= vid.width)
+		height = (vid.width - ry1) / vid.dup - 1;
 	// WARNING no x clipping (not needed for the moment)
 
-	for (y = max(0, -ry1 / vid.dupy); y < height; y++)
+	for (y = max(0, -ry1 / vid.dup); y < height; y++)
 	{
-		for (dupy = vid.dupy; dupy; dupy--)
+		for (dupy = vid.dup; dupy; dupy--)
 		{
 			src = pic->data + y * width;
 			for (x = 0; x < width; x++)
 			{
-				for (dupx = vid.dupx; dupx; dupx--)
+				for (dupx = vid.dup; dupx; dupx--)
 					*dest++ = *src;
 				src++;
 			}
-			dest += vid.width - vid.dupx * width;
+			dest += vid.width - vid.dup * width;
 		}
 	}
 }
@@ -1262,44 +1248,42 @@ void V_DrawFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c)
 
 	if (!(c & V_NOSCALESTART))
 	{
-		INT32 dupx = vid.dupx, dupy = vid.dupy;
-
 		if (x == 0 && y == 0 && w == BASEVIDWIDTH && h == BASEVIDHEIGHT)
 		{ // Clear the entire screen, from dest to deststop. Yes, this really works.
 			memset(screens[0], (c&255), vid.width * vid.height * vid.bpp);
 			return;
 		}
 
-		x *= dupx;
-		y *= dupy;
-		w *= dupx;
-		h *= dupy;
+		x *= vid.dup;
+		y *= vid.dup;
+		w *= vid.dup;
+		h *= vid.dup;
 
 		// Center it if necessary
-		if (vid.width != BASEVIDWIDTH * dupx)
+		if (vid.width != BASEVIDWIDTH * vid.dup)
 		{
-			// dupx adjustments pretend that screen width is BASEVIDWIDTH * dupx,
+			// dup adjustments pretend that screen width is BASEVIDWIDTH * dup,
 			// so center this imaginary screen
 			if (c & V_SNAPTORIGHT)
-				x += (vid.width - (BASEVIDWIDTH * dupx));
+				x += (vid.width - (BASEVIDWIDTH * vid.dup));
 			else if (!(c & V_SNAPTOLEFT))
-				x += (vid.width - (BASEVIDWIDTH * dupx)) / 2;
+				x += (vid.width - (BASEVIDWIDTH * vid.dup)) / 2;
 			if (perplayershuffle & 4)
-				x -= (vid.width - (BASEVIDWIDTH * dupx)) / 4;
+				x -= (vid.width - (BASEVIDWIDTH * vid.dup)) / 4;
 			else if (perplayershuffle & 8)
-				x += (vid.width - (BASEVIDWIDTH * dupx)) / 4;
+				x += (vid.width - (BASEVIDWIDTH * vid.dup)) / 4;
 		}
-		if (vid.height != BASEVIDHEIGHT * dupy)
+		if (vid.height != BASEVIDHEIGHT * vid.dup)
 		{
 			// same thing here
 			if (c & V_SNAPTOBOTTOM)
-				y += (vid.height - (BASEVIDHEIGHT * dupy));
+				y += (vid.height - (BASEVIDHEIGHT * vid.dup));
 			else if (!(c & V_SNAPTOTOP))
-				y += (vid.height - (BASEVIDHEIGHT * dupy)) / 2;
+				y += (vid.height - (BASEVIDHEIGHT * vid.dup)) / 2;
 			if (perplayershuffle & 1)
-				y -= (vid.height - (BASEVIDHEIGHT * dupy)) / 4;
+				y -= (vid.height - (BASEVIDHEIGHT * vid.dup)) / 4;
 			else if (perplayershuffle & 2)
-				y += (vid.height - (BASEVIDHEIGHT * dupy)) / 4;
+				y += (vid.height - (BASEVIDHEIGHT * vid.dup)) / 4;
 		}
 	}
 
@@ -1336,32 +1320,33 @@ void V_DrawFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c)
 // This is now a function since it's otherwise repeated 2 times and honestly looks retarded:
 static UINT32 V_GetHWConsBackColor(void)
 {
-	UINT32 hwcolor;
+	UINT8 r, g, b;
 	switch (cons_backcolor.value)
 	{
-		case 0:		hwcolor = 0xffffff00;	break; 	// White
-		case 1:		hwcolor = 0x80808000;	break; 	// Black
-		case 2:		hwcolor = 0xdeb88700;	break;	// Sepia
-		case 3:		hwcolor = 0x40201000;	break; 	// Brown
-		case 4:		hwcolor = 0xfa807200;	break; 	// Pink
-		case 5:		hwcolor = 0xff69b400;	break; 	// Raspberry
-		case 6:		hwcolor = 0xff000000;	break; 	// Red
-		case 7:		hwcolor = 0xffd68300;	break;	// Creamsicle
-		case 8:		hwcolor = 0xff800000;	break; 	// Orange
-		case 9:		hwcolor = 0xdaa52000;	break; 	// Gold
-		case 10:	hwcolor = 0x80800000;	break; 	// Yellow
-		case 11:	hwcolor = 0x00ff0000;	break; 	// Emerald
-		case 12:	hwcolor = 0x00800000;	break; 	// Green
-		case 13:	hwcolor = 0x4080ff00;	break; 	// Cyan
-		case 14:	hwcolor = 0x4682b400;	break; 	// Steel
-		case 15:	hwcolor = 0x1e90ff00;	break;	// Periwinkle
-		case 16:	hwcolor = 0x0000ff00;	break; 	// Blue
-		case 17:	hwcolor = 0xff00ff00;	break; 	// Purple
-		case 18:	hwcolor = 0xee82ee00;	break; 	// Lavender
+		case 0:		r = 0xff; g = 0xff; b = 0xff;	break; 	// White
+		case 1:		r = 0x80; g = 0x80; b = 0x80;	break; 	// Black
+		case 2:		r = 0xde; g = 0xb8; b = 0x87;	break;	// Sepia
+		case 3:		r = 0x40; g = 0x20; b = 0x10;	break; 	// Brown
+		case 4:		r = 0xfa; g = 0x80; b = 0x72;	break; 	// Pink
+		case 5:		r = 0xff; g = 0x69; b = 0xb4;	break; 	// Raspberry
+		case 6:		r = 0xff; g = 0x00; b = 0x00;	break; 	// Red
+		case 7:		r = 0xff; g = 0xd6; b = 0x83;	break;	// Creamsicle
+		case 8:		r = 0xff; g = 0x80; b = 0x00;	break; 	// Orange
+		case 9:		r = 0xda; g = 0xa5; b = 0x20;	break; 	// Gold
+		case 10:	r = 0x80; g = 0x80; b = 0x00;	break; 	// Yellow
+		case 11:	r = 0x00; g = 0xff; b = 0x00;	break; 	// Emerald
+		case 12:	r = 0x00; g = 0x80; b = 0x00;	break; 	// Green
+		case 13:	r = 0x40; g = 0x80; b = 0xff;	break; 	// Cyan
+		case 14:	r = 0x46; g = 0x82; b = 0xb4;	break; 	// Steel
+		case 15:	r = 0x1e; g = 0x90; b = 0xff;	break;	// Periwinkle
+		case 16:	r = 0x00; g = 0x00; b = 0xff;	break; 	// Blue
+		case 17:	r = 0xff; g = 0x00; b = 0xff;	break; 	// Purple
+		case 18:	r = 0xee; g = 0x82; b = 0xee;	break; 	// Lavender
 		// Default green
-		default:	hwcolor = 0x00800000;	break;
+		default:	r = 0x00; g = 0x80; b = 0x00;	break;
 	}
-	return hwcolor;
+	V_CubeApply(&r, &g, &b);
+	return (r << 24) | (g << 16) | (b << 8);
 }
 #endif
 
@@ -1473,38 +1458,36 @@ void V_DrawFillConsoleMap(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c)
 
 	if (!(c & V_NOSCALESTART))
 	{
-		INT32 dupx = vid.dupx, dupy = vid.dupy;
-
-		x *= dupx;
-		y *= dupy;
-		w *= dupx;
-		h *= dupy;
+		x *= vid.dup;
+		y *= vid.dup;
+		w *= vid.dup;
+		h *= vid.dup;
 
 		// Center it if necessary
-		if (vid.width != BASEVIDWIDTH * dupx)
+		if (vid.width != BASEVIDWIDTH * vid.dup)
 		{
-			// dupx adjustments pretend that screen width is BASEVIDWIDTH * dupx,
+			// dup adjustments pretend that screen width is BASEVIDWIDTH * dup,
 			// so center this imaginary screen
 			if (c & V_SNAPTORIGHT)
-				x += (vid.width - (BASEVIDWIDTH * dupx));
+				x += (vid.width - (BASEVIDWIDTH * vid.dup));
 			else if (!(c & V_SNAPTOLEFT))
-				x += (vid.width - (BASEVIDWIDTH * dupx)) / 2;
+				x += (vid.width - (BASEVIDWIDTH * vid.dup)) / 2;
 			if (perplayershuffle & 4)
-				x -= (vid.width - (BASEVIDWIDTH * dupx)) / 4;
+				x -= (vid.width - (BASEVIDWIDTH * vid.dup)) / 4;
 			else if (perplayershuffle & 8)
-				x += (vid.width - (BASEVIDWIDTH * dupx)) / 4;
+				x += (vid.width - (BASEVIDWIDTH * vid.dup)) / 4;
 		}
-		if (vid.height != BASEVIDHEIGHT * dupy)
+		if (vid.height != BASEVIDHEIGHT * vid.dup)
 		{
 			// same thing here
 			if (c & V_SNAPTOBOTTOM)
-				y += (vid.height - (BASEVIDHEIGHT * dupy));
+				y += (vid.height - (BASEVIDHEIGHT * vid.dup));
 			else if (!(c & V_SNAPTOTOP))
-				y += (vid.height - (BASEVIDHEIGHT * dupy)) / 2;
+				y += (vid.height - (BASEVIDHEIGHT * vid.dup)) / 2;
 			if (perplayershuffle & 1)
-				y -= (vid.height - (BASEVIDHEIGHT * dupy)) / 4;
+				y -= (vid.height - (BASEVIDHEIGHT * vid.dup)) / 4;
 			else if (perplayershuffle & 2)
-				y += (vid.height - (BASEVIDHEIGHT * dupy)) / 4;
+				y += (vid.height - (BASEVIDHEIGHT * vid.dup)) / 4;
 		}
 	}
 
@@ -1657,38 +1640,36 @@ void V_DrawFadeFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c, UINT16 color, U
 
 	if (!(c & V_NOSCALESTART))
 	{
-		INT32 dupx = vid.dupx, dupy = vid.dupy;
-
-		x *= dupx;
-		y *= dupy;
-		w *= dupx;
-		h *= dupy;
+		x *= vid.dup;
+		y *= vid.dup;
+		w *= vid.dup;
+		h *= vid.dup;
 
 		// Center it if necessary
-		if (vid.width != BASEVIDWIDTH * dupx)
+		if (vid.width != BASEVIDWIDTH * vid.dup)
 		{
-			// dupx adjustments pretend that screen width is BASEVIDWIDTH * dupx,
+			// dup adjustments pretend that screen width is BASEVIDWIDTH * dup,
 			// so center this imaginary screen
 			if (c & V_SNAPTORIGHT)
-				x += (vid.width - (BASEVIDWIDTH * dupx));
+				x += (vid.width - (BASEVIDWIDTH * vid.dup));
 			else if (!(c & V_SNAPTOLEFT))
-				x += (vid.width - (BASEVIDWIDTH * dupx)) / 2;
+				x += (vid.width - (BASEVIDWIDTH * vid.dup)) / 2;
 			if (perplayershuffle & 4)
-				x -= (vid.width - (BASEVIDWIDTH * dupx)) / 4;
+				x -= (vid.width - (BASEVIDWIDTH * vid.dup)) / 4;
 			else if (perplayershuffle & 8)
-				x += (vid.width - (BASEVIDWIDTH * dupx)) / 4;
+				x += (vid.width - (BASEVIDWIDTH * vid.dup)) / 4;
 		}
-		if (vid.height != BASEVIDHEIGHT * dupy)
+		if (vid.height != BASEVIDHEIGHT * vid.dup)
 		{
 			// same thing here
 			if (c & V_SNAPTOBOTTOM)
-				y += (vid.height - (BASEVIDHEIGHT * dupy));
+				y += (vid.height - (BASEVIDHEIGHT * vid.dup));
 			else if (!(c & V_SNAPTOTOP))
-				y += (vid.height - (BASEVIDHEIGHT * dupy)) / 2;
+				y += (vid.height - (BASEVIDHEIGHT * vid.dup)) / 2;
 			if (perplayershuffle & 1)
-				y -= (vid.height - (BASEVIDHEIGHT * dupy)) / 4;
+				y -= (vid.height - (BASEVIDHEIGHT * vid.dup)) / 4;
 			else if (perplayershuffle & 2)
-				y += (vid.height - (BASEVIDHEIGHT * dupy)) / 4;
+				y += (vid.height - (BASEVIDHEIGHT * vid.dup)) / 4;
 		}
 	}
 
@@ -1734,7 +1715,7 @@ void V_DrawFadeFill(INT32 x, INT32 y, INT32 w, INT32 h, INT32 c, UINT16 color, U
 //
 void V_DrawFlatFill(INT32 x, INT32 y, INT32 w, INT32 h, lumpnum_t flatnum)
 {
-	INT32 u, v, dupx, dupy;
+	INT32 u, v;
 	fixed_t dx, dy, xfrac, yfrac;
 	const UINT8 *src, *deststop;
 	UINT8 *flat, *dest;
@@ -1753,29 +1734,27 @@ void V_DrawFlatFill(INT32 x, INT32 y, INT32 w, INT32 h, lumpnum_t flatnum)
 
 	flat = W_CacheLumpNum(flatnum, PU_CACHE);
 
-	dupx = dupy = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy);
-
-	dest = screens[0] + y*dupy*vid.width + x*dupx;
+	dest = screens[0] + y*vid.dup*vid.width + x*vid.dup;
 	deststop = screens[0] + vid.rowbytes * vid.height;
 
 	// from V_DrawScaledPatch
-	if (vid.width != BASEVIDWIDTH * dupx)
+	if (vid.width != BASEVIDWIDTH * vid.dup)
 	{
-		// dupx adjustments pretend that screen width is BASEVIDWIDTH * dupx,
+		// dup adjustments pretend that screen width is BASEVIDWIDTH * dup,
 		// so center this imaginary screen
-		dest += (vid.width - (BASEVIDWIDTH * dupx)) / 2;
+		dest += (vid.width - (BASEVIDWIDTH * vid.dup)) / 2;
 	}
-	if (vid.height != BASEVIDHEIGHT * dupy)
+	if (vid.height != BASEVIDHEIGHT * vid.dup)
 	{
 		// same thing here
-		dest += (vid.height - (BASEVIDHEIGHT * dupy)) * vid.width / 2;
+		dest += (vid.height - (BASEVIDHEIGHT * vid.dup)) * vid.width / 2;
 	}
 
-	w *= dupx;
-	h *= dupy;
+	w *= vid.dup;
+	h *= vid.dup;
 
-	dx = FixedDiv(FRACUNIT, dupx<<(FRACBITS-2));
-	dy = FixedDiv(FRACUNIT, dupy<<(FRACBITS-2));
+	dx = FixedDiv(FRACUNIT, vid.dup<<(FRACBITS-2));
+	dy = FixedDiv(FRACUNIT, vid.dup<<(FRACBITS-2));
 
 	yfrac = 0;
 	for (v = 0; v < h; v++, dest += vid.width)
@@ -1798,8 +1777,7 @@ void V_DrawFlatFill(INT32 x, INT32 y, INT32 w, INT32 h, lumpnum_t flatnum)
 //
 void V_DrawPatchFill(patch_t *pat)
 {
-	INT32 dupz = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy);
-	INT32 x, y, pw = pat->width * dupz, ph = pat->height * dupz;
+	INT32 x, y, pw = pat->width * vid.dup, ph = pat->height * vid.dup;
 
 	for (x = 0; x < vid.width; x += pw)
 	{
@@ -1875,11 +1853,11 @@ void V_DrawPromptBack(INT32 boxheight, INT32 color)
 			boxheight = -boxheight;
 		else // 4 lines of space plus gaps between and some leeway
 			boxheight = ((boxheight * 4) + (boxheight/2)*5);
-		V_DrawFill((BASEVIDWIDTH-(vid.width/vid.dupx))/2, BASEVIDHEIGHT-boxheight, (vid.width/vid.dupx),boxheight, (color-256)|V_SNAPTOBOTTOM);
+		V_DrawFill((BASEVIDWIDTH-(vid.width/vid.dup))/2, BASEVIDHEIGHT-boxheight, (vid.width/vid.dup),boxheight, (color-256)|V_SNAPTOBOTTOM);
 		return;
 	}
 
-	boxheight *= vid.dupy;
+	boxheight *= vid.dup;
 
 	if (color == INT32_MAX)
 		color = cons_backcolor.value;
@@ -2098,7 +2076,7 @@ char *V_WordWrap(INT32 x, INT32 w, INT32 option, const char *string)
 //
 void V_DrawString(INT32 x, INT32 y, INT32 option, const char *string)
 {
-	INT32 w, c, cx = x, cy = y, dupx, dupy, scrwidth, center = 0, left = 0;
+	INT32 w, c, cx = x, cy = y, dup, scrwidth, center = 0, left = 0;
 	const char *ch = string;
 	INT32 charflags = (option & V_CHARCOLORMASK);
 	const UINT8 *colormap = NULL;
@@ -2109,20 +2087,19 @@ void V_DrawString(INT32 x, INT32 y, INT32 option, const char *string)
 
 	if (option & V_NOSCALESTART)
 	{
-		dupx = vid.dupx;
-		dupy = vid.dupy;
+		dup = vid.dup;
 		scrwidth = vid.width;
 	}
 	else
 	{
-		dupx = dupy = 1;
-		scrwidth = vid.width/vid.dupx;
+		dup = 1;
+		scrwidth = vid.width/vid.dup;
 		left = (scrwidth - BASEVIDWIDTH)/2;
 		scrwidth -= left;
 	}
 
 	if (option & V_NOSCALEPATCH)
-		scrwidth *= vid.dupx;
+		scrwidth *= vid.dup;
 
 	switch (option & V_SPACINGMASK)
 	{
@@ -2154,9 +2131,9 @@ void V_DrawString(INT32 x, INT32 y, INT32 option, const char *string)
 			cx = x;
 
 			if (option & V_RETURN8)
-				cy += 8*dupy;
+				cy += 8*dup;
 			else
-				cy += 12*dupy;
+				cy += 12*dup;
 
 			continue;
 		}
@@ -2169,17 +2146,17 @@ void V_DrawString(INT32 x, INT32 y, INT32 option, const char *string)
 		// character does not exist or is a space
 		if (c < 0 || c >= HU_FONTSIZE || !hu_font[c])
 		{
-			cx += spacewidth * dupx;
+			cx += spacewidth * dup;
 			continue;
 		}
 
 		if (charwidth)
 		{
-			w = charwidth * dupx;
-			center = w/2 - hu_font[c]->width*dupx/2;
+			w = charwidth * dup;
+			center = w/2 - hu_font[c]->width*dup/2;
 		}
 		else
-			w = hu_font[c]->width * dupx;
+			w = hu_font[c]->width * dup;
 
 		if (cx > scrwidth)
 			continue;
@@ -2214,7 +2191,7 @@ void V_DrawRightAlignedString(INT32 x, INT32 y, INT32 option, const char *string
 //
 void V_DrawSmallString(INT32 x, INT32 y, INT32 option, const char *string)
 {
-	INT32 w, c, cx = x, cy = y, dupx, dupy, scrwidth, center = 0, left = 0;
+	INT32 w, c, cx = x, cy = y, dup, scrwidth, center = 0, left = 0;
 	const char *ch = string;
 	INT32 charflags = 0;
 	const UINT8 *colormap = NULL;
@@ -2225,20 +2202,19 @@ void V_DrawSmallString(INT32 x, INT32 y, INT32 option, const char *string)
 
 	if (option & V_NOSCALESTART)
 	{
-		dupx = vid.dupx;
-		dupy = vid.dupy;
+		dup = vid.dup;
 		scrwidth = vid.width;
 	}
 	else
 	{
-		dupx = dupy = 1;
-		scrwidth = vid.width/vid.dupx;
+		dup = 1;
+		scrwidth = vid.width/vid.dup;
 		left = (scrwidth - BASEVIDWIDTH)/2;
 		scrwidth -= left;
 	}
 
 	if (option & V_NOSCALEPATCH)
-		scrwidth *= vid.dupx;
+		scrwidth *= vid.dup;
 
 	charflags = (option & V_CHARCOLORMASK);
 
@@ -2272,9 +2248,9 @@ void V_DrawSmallString(INT32 x, INT32 y, INT32 option, const char *string)
 			cx = x;
 
 			if (option & V_RETURN8)
-				cy += 4*dupy;
+				cy += 4*dup;
 			else
-				cy += 6*dupy;
+				cy += 6*dup;
 
 			continue;
 		}
@@ -2286,17 +2262,17 @@ void V_DrawSmallString(INT32 x, INT32 y, INT32 option, const char *string)
 
 		if (c < 0 || c >= HU_FONTSIZE || !hu_font[c])
 		{
-			cx += spacewidth * dupx;
+			cx += spacewidth * dup;
 			continue;
 		}
 
 		if (charwidth)
 		{
-			w = charwidth * dupx;
-			center = w/2 - hu_font[c]->width*dupx/4;
+			w = charwidth * dup;
+			center = w/2 - hu_font[c]->width*dup/4;
 		}
 		else
-			w = hu_font[c]->width * dupx / 2;
+			w = hu_font[c]->width * dup / 2;
 
 		if (cx > scrwidth)
 			continue;
@@ -2332,7 +2308,7 @@ void V_DrawRightAlignedSmallString(INT32 x, INT32 y, INT32 option, const char *s
 //
 void V_DrawThinString(INT32 x, INT32 y, INT32 option, const char *string)
 {
-	INT32 w, c, cx = x, cy = y, dupx, dupy, scrwidth, left = 0;
+	INT32 w, c, cx = x, cy = y, dup, scrwidth, left = 0;
 	const char *ch = string;
 	INT32 charflags = 0;
 	const UINT8 *colormap = NULL;
@@ -2343,20 +2319,19 @@ void V_DrawThinString(INT32 x, INT32 y, INT32 option, const char *string)
 
 	if (option & V_NOSCALESTART)
 	{
-		dupx = vid.dupx;
-		dupy = vid.dupy;
+		dup = vid.dup;
 		scrwidth = vid.width;
 	}
 	else
 	{
-		dupx = dupy = 1;
-		scrwidth = vid.width/vid.dupx;
+		dup = 1;
+		scrwidth = vid.width/vid.dup;
 		left = (scrwidth - BASEVIDWIDTH)/2;
 		scrwidth -= left;
 	}
 
 	if (option & V_NOSCALEPATCH)
-		scrwidth *= vid.dupx;
+		scrwidth *= vid.dup;
 
 	charflags = (option & V_CHARCOLORMASK);
 
@@ -2390,9 +2365,9 @@ void V_DrawThinString(INT32 x, INT32 y, INT32 option, const char *string)
 			cx = x;
 
 			if (option & V_RETURN8)
-				cy += 8*dupy;
+				cy += 8*dup;
 			else
-				cy += 12*dupy;
+				cy += 12*dup;
 
 			continue;
 		}
@@ -2404,14 +2379,14 @@ void V_DrawThinString(INT32 x, INT32 y, INT32 option, const char *string)
 
 		if (c < 0 || c >= HU_FONTSIZE || !tny_font[c])
 		{
-			cx += spacewidth * dupx;
+			cx += spacewidth * dup;
 			continue;
 		}
 
 		if (charwidth)
-			w = charwidth * dupx;
+			w = charwidth * dup;
 		else
-			w = tny_font[c]->width * dupx;
+			w = tny_font[c]->width * dup;
 
 		if (cx > scrwidth)
 			continue;
@@ -2470,7 +2445,7 @@ void V_DrawRightAlignedSmallThinString(INT32 x, INT32 y, INT32 option, const cha
 void V_DrawStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string)
 {
 	fixed_t cx = x, cy = y;
-	INT32 w, c, dupx, dupy, scrwidth, center = 0, left = 0;
+	INT32 w, c, dup, scrwidth, center = 0, left = 0;
 	const char *ch = string;
 	INT32 charflags = 0;
 	const UINT8 *colormap = NULL;
@@ -2481,20 +2456,19 @@ void V_DrawStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string)
 
 	if (option & V_NOSCALESTART)
 	{
-		dupx = vid.dupx;
-		dupy = vid.dupy;
+		dup = vid.dup;
 		scrwidth = vid.width;
 	}
 	else
 	{
-		dupx = dupy = 1;
-		scrwidth = vid.width/vid.dupx;
+		dup = 1;
+		scrwidth = vid.width/vid.dup;
 		left = (scrwidth - BASEVIDWIDTH)/2;
 		scrwidth -= left;
 	}
 
 	if (option & V_NOSCALEPATCH)
-		scrwidth *= vid.dupx;
+		scrwidth *= vid.dup;
 
 	charflags = (option & V_CHARCOLORMASK);
 
@@ -2528,9 +2502,9 @@ void V_DrawStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string)
 			cx = x;
 
 			if (option & V_RETURN8)
-				cy += (8*dupy)<<FRACBITS;
+				cy += (8*dup)<<FRACBITS;
 			else
-				cy += (12*dupy)<<FRACBITS;
+				cy += (12*dup)<<FRACBITS;
 
 			continue;
 		}
@@ -2543,17 +2517,17 @@ void V_DrawStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string)
 		// character does not exist or is a space
 		if (c < 0 || c >= HU_FONTSIZE || !hu_font[c])
 		{
-			cx += (spacewidth * dupx)<<FRACBITS;
+			cx += (spacewidth * dup)<<FRACBITS;
 			continue;
 		}
 
 		if (charwidth)
 		{
-			w = charwidth * dupx;
-			center = w/2 - hu_font[c]->width*(dupx/2);
+			w = charwidth * dup;
+			center = w/2 - hu_font[c]->width*(dup/2);
 		}
 		else
-			w = hu_font[c]->width * dupx;
+			w = hu_font[c]->width * dup;
 
 		if ((cx>>FRACBITS) > scrwidth)
 			continue;
@@ -2586,7 +2560,7 @@ void V_DrawRightAlignedStringAtFixed(fixed_t x, fixed_t y, INT32 option, const c
 void V_DrawSmallStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string)
 {
 	fixed_t cx = x, cy = y;
-	INT32 w, c, dupx, dupy, scrwidth, center = 0, left = 0;
+	INT32 w, c, dup, scrwidth, center = 0, left = 0;
 	const char *ch = string;
 	INT32 charflags = 0;
 	const UINT8 *colormap = NULL;
@@ -2597,20 +2571,19 @@ void V_DrawSmallStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *st
 
 	if (option & V_NOSCALESTART)
 	{
-		dupx = vid.dupx;
-		dupy = vid.dupy;
+		dup = vid.dup;
 		scrwidth = vid.width;
 	}
 	else
 	{
-		dupx = dupy = 1;
-		scrwidth = vid.width/vid.dupx;
+		dup = 1;
+		scrwidth = vid.width/vid.dup;
 		left = (scrwidth - BASEVIDWIDTH)/2;
 		scrwidth -= left;
 	}
 
 	if (option & V_NOSCALEPATCH)
-		scrwidth *= vid.dupx;
+		scrwidth *= vid.dup;
 
 	charflags = (option & V_CHARCOLORMASK);
 
@@ -2644,9 +2617,9 @@ void V_DrawSmallStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *st
 			cx = x;
 
 			if (option & V_RETURN8)
-				cy += (4*dupy)<<FRACBITS;
+				cy += (4*dup)<<FRACBITS;
 			else
-				cy += (6*dupy)<<FRACBITS;
+				cy += (6*dup)<<FRACBITS;
 
 			continue;
 		}
@@ -2659,17 +2632,17 @@ void V_DrawSmallStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *st
 		// character does not exist or is a space
 		if (c < 0 || c >= HU_FONTSIZE || !hu_font[c])
 		{
-			cx += (spacewidth * dupx)<<FRACBITS;
+			cx += (spacewidth * dup)<<FRACBITS;
 			continue;
 		}
 
 		if (charwidth)
 		{
-			w = charwidth * dupx;
-			center = w/2 - hu_font[c]->width*(dupx/4);
+			w = charwidth * dup;
+			center = w/2 - hu_font[c]->width*(dup/4);
 		}
 		else
-			w = hu_font[c]->width * dupx / 2;
+			w = hu_font[c]->width * dup / 2;
 
 		if ((cx>>FRACBITS) > scrwidth)
 			break;
@@ -2703,7 +2676,7 @@ void V_DrawRightAlignedSmallStringAtFixed(fixed_t x, fixed_t y, INT32 option, co
 void V_DrawThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string)
 {
 	fixed_t cx = x, cy = y;
-	INT32 w, c, dupx, dupy, scrwidth, center = 0, left = 0;
+	INT32 w, c, dup, scrwidth, center = 0, left = 0;
 	const char *ch = string;
 	INT32 charflags = 0;
 	const UINT8 *colormap = NULL;
@@ -2714,20 +2687,19 @@ void V_DrawThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *str
 
 	if (option & V_NOSCALESTART)
 	{
-		dupx = vid.dupx;
-		dupy = vid.dupy;
+		dup = vid.dup;
 		scrwidth = vid.width;
 	}
 	else
 	{
-		dupx = dupy = 1;
-		scrwidth = vid.width/vid.dupx;
+		dup = 1;
+		scrwidth = vid.width/vid.dup;
 		left = (scrwidth - BASEVIDWIDTH)/2;
 		scrwidth -= left;
 	}
 
 	if (option & V_NOSCALEPATCH)
-		scrwidth *= vid.dupx;
+		scrwidth *= vid.dup;
 
 	charflags = (option & V_CHARCOLORMASK);
 
@@ -2761,9 +2733,9 @@ void V_DrawThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *str
 			cx = x;
 
 			if (option & V_RETURN8)
-				cy += (8*dupy)<<FRACBITS;
+				cy += (8*dup)<<FRACBITS;
 			else
-				cy += (12*dupy)<<FRACBITS;
+				cy += (12*dup)<<FRACBITS;
 
 			continue;
 		}
@@ -2776,17 +2748,17 @@ void V_DrawThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *str
 		// character does not exist or is a space
 		if (c < 0 || c >= HU_FONTSIZE || !tny_font[c])
 		{
-			cx += (spacewidth * dupx)<<FRACBITS;
+			cx += (spacewidth * dup)<<FRACBITS;
 			continue;
 		}
 
 		if (charwidth)
 		{
-			w = charwidth * dupx;
-			center = w/2 - tny_font[c]->width*(dupx/2);
+			w = charwidth * dup;
+			center = w/2 - tny_font[c]->width*(dup/2);
 		}
 		else
-			w = tny_font[c]->width * dupx;
+			w = tny_font[c]->width * dup;
 
 		if ((cx>>FRACBITS) > scrwidth)
 			break;
@@ -2820,7 +2792,7 @@ void V_DrawRightAlignedThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, con
 void V_DrawSmallThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char *string)
 {
 	fixed_t cx = x, cy = y;
-	INT32 w, c, dupx, dupy, scrwidth, center = 0, left = 0;
+	INT32 w, c, dup, scrwidth, center = 0, left = 0;
 	const char *ch = string;
 	INT32 charflags = 0;
 	const UINT8 *colormap = NULL;
@@ -2831,20 +2803,19 @@ void V_DrawSmallThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char
 
 	if (option & V_NOSCALESTART)
 	{
-		dupx = vid.dupx<<FRACBITS;
-		dupy = vid.dupy<<FRACBITS;
+		dup = vid.dup<<FRACBITS;
 		scrwidth = vid.width;
 	}
 	else
 	{
-		dupx = dupy = FRACUNIT;
-		scrwidth = FixedDiv(vid.width<<FRACBITS, vid.dupx);
+		dup = FRACUNIT;
+		scrwidth = FixedDiv(vid.width<<FRACBITS, vid.dup);
 		left = ((scrwidth - (BASEVIDWIDTH<<FRACBITS))/2);
 		scrwidth -= left;
 	}
 
 	if (option & V_NOSCALEPATCH)
-		scrwidth *= vid.dupx;
+		scrwidth *= vid.dup;
 
 	charflags = (option & V_CHARCOLORMASK);
 
@@ -2878,9 +2849,9 @@ void V_DrawSmallThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char
 			cx = x;
 
 			if (option & V_RETURN8)
-				cy += 4*dupy;
+				cy += 4*dup;
 			else
-				cy += 6*dupy;
+				cy += 6*dup;
 
 			continue;
 		}
@@ -2893,17 +2864,17 @@ void V_DrawSmallThinStringAtFixed(fixed_t x, fixed_t y, INT32 option, const char
 		// character does not exist or is a space
 		if (c < 0 || c >= HU_FONTSIZE || !tny_font[c])
 		{
-			cx += FixedMul(spacewidth, dupx);
+			cx += FixedMul(spacewidth, dup);
 			continue;
 		}
 
 		if (charwidth)
 		{
-			w = FixedMul(charwidth, dupx);
-			center = w/2 - tny_font[c]->width*(dupx/4);
+			w = FixedMul(charwidth, dup);
+			center = w/2 - tny_font[c]->width*(dup/4);
 		}
 		else
-			w = tny_font[c]->width * dupx / 2;
+			w = tny_font[c]->width * dup / 2;
 
 		if (cx > scrwidth)
 			break;
@@ -2940,7 +2911,7 @@ void V_DrawTallNum(INT32 x, INT32 y, INT32 flags, INT32 num)
 	boolean neg;
 
 	if (flags & (V_NOSCALESTART|V_NOSCALEPATCH))
-		w *= vid.dupx;
+		w *= vid.dup;
 
 	if ((neg = num < 0))
 		num = -num;
@@ -2965,7 +2936,7 @@ void V_DrawPaddedTallNum(INT32 x, INT32 y, INT32 flags, INT32 num, INT32 digits)
 	INT32 w = tallnum[0]->width;
 
 	if (flags & (V_NOSCALESTART|V_NOSCALEPATCH))
-		w *= vid.dupx;
+		w *= vid.dup;
 
 	if (num < 0)
 		num = -num;
@@ -3000,7 +2971,7 @@ void V_DrawLevelActNum(INT32 x, INT32 y, INT32 flags, UINT8 num)
 //
 void V_DrawCreditString(fixed_t x, fixed_t y, INT32 option, const char *string)
 {
-	INT32 w, c, dupx, dupy, scrwidth = BASEVIDWIDTH;
+	INT32 w, c, dup, scrwidth = BASEVIDWIDTH;
 	fixed_t cx = x, cy = y;
 	const char *ch = string;
 
@@ -3010,15 +2981,14 @@ void V_DrawCreditString(fixed_t x, fixed_t y, INT32 option, const char *string)
 
 	if (option & V_NOSCALESTART)
 	{
-		dupx = vid.dupx;
-		dupy = vid.dupy;
+		dup = vid.dup;
 		scrwidth = vid.width;
 	}
 	else
-		dupx = dupy = 1;
+		dup = 1;
 
 	if (option & V_NOSCALEPATCH)
-		scrwidth *= vid.dupx;
+		scrwidth *= vid.dup;
 
 	for (;;)
 	{
@@ -3028,18 +2998,18 @@ void V_DrawCreditString(fixed_t x, fixed_t y, INT32 option, const char *string)
 		if (c == '\n')
 		{
 			cx = x;
-			cy += (12*dupy)<<FRACBITS;
+			cy += (12*dup)<<FRACBITS;
 			continue;
 		}
 
 		c = toupper(c) - CRED_FONTSTART;
 		if (c < 0 || c >= CRED_FONTSIZE)
 		{
-			cx += (16*dupx)<<FRACBITS;
+			cx += (16*dup)<<FRACBITS;
 			continue;
 		}
 
-		w = cred_font[c]->width * dupx;
+		w = cred_font[c]->width * dup;
 		if ((cx>>FRACBITS) > scrwidth)
 			continue;
 
@@ -3053,7 +3023,7 @@ void V_DrawCreditString(fixed_t x, fixed_t y, INT32 option, const char *string)
 static void V_DrawNameTagLine(INT32 x, INT32 y, INT32 option, fixed_t scale, UINT8 *basecolormap, UINT8 *outlinecolormap, const char *string)
 {
 	fixed_t cx, cy, w;
-	INT32 c, dupx, dupy, scrwidth, left = 0;
+	INT32 c, dup, scrwidth, left = 0;
 	const char *ch = string;
 
 	if (option & V_CENTERNAMETAG)
@@ -3065,20 +3035,19 @@ static void V_DrawNameTagLine(INT32 x, INT32 y, INT32 option, fixed_t scale, UIN
 
 	if (option & V_NOSCALESTART)
 	{
-		dupx = vid.dupx;
-		dupy = vid.dupy;
+		dup = vid.dup;
 		scrwidth = vid.width;
 	}
 	else
 	{
-		dupx = dupy = 1;
-		scrwidth = vid.width/vid.dupx;
+		dup = 1;
+		scrwidth = vid.width/vid.dup;
 		left = (scrwidth - BASEVIDWIDTH)/2;
 		scrwidth -= left;
 	}
 
 	if (option & V_NOSCALEPATCH)
-		scrwidth *= vid.dupx;
+		scrwidth *= vid.dup;
 
 	for (;;ch++)
 	{
@@ -3087,7 +3056,7 @@ static void V_DrawNameTagLine(INT32 x, INT32 y, INT32 option, fixed_t scale, UIN
 		if (*ch == '\n')
 		{
 			cx = x<<FRACBITS;
-			cy += FixedMul((21*dupy)*FRACUNIT, scale);
+			cy += FixedMul((21*dup)*FRACUNIT, scale);
 			continue;
 		}
 
@@ -3097,11 +3066,11 @@ static void V_DrawNameTagLine(INT32 x, INT32 y, INT32 option, fixed_t scale, UIN
 		// character does not exist or is a space
 		if (c < 0 || c >= NT_FONTSIZE || !ntb_font[c] || !nto_font[c])
 		{
-			cx += FixedMul((4 * dupx)*FRACUNIT, scale);
+			cx += FixedMul((4 * dup)*FRACUNIT, scale);
 			continue;
 		}
 
-		w = FixedMul(((ntb_font[c]->width)+2 * dupx) * FRACUNIT, scale);
+		w = FixedMul(((ntb_font[c]->width)+2 * dup) * FRACUNIT, scale);
 
 		if (FixedInt(cx) > scrwidth)
 			continue;
@@ -3276,27 +3245,26 @@ INT32 V_CreditStringWidth(const char *string)
 //
 void V_DrawLevelTitle(INT32 x, INT32 y, INT32 option, const char *string)
 {
-	INT32 w, c, cx = x, cy = y, dupx, dupy, scrwidth, left = 0;
+	INT32 w, c, cx = x, cy = y, dup, scrwidth, left = 0;
 	const char *ch = string;
 	INT32 charflags = (option & V_CHARCOLORMASK);
 	const UINT8 *colormap = NULL;
 
 	if (option & V_NOSCALESTART)
 	{
-		dupx = vid.dupx;
-		dupy = vid.dupy;
+		dup = vid.dup;
 		scrwidth = vid.width;
 	}
 	else
 	{
-		dupx = dupy = 1;
-		scrwidth = vid.width/vid.dupx;
+		dup = 1;
+		scrwidth = vid.width/vid.dup;
 		left = (scrwidth - BASEVIDWIDTH)/2;
 		scrwidth -= left;
 	}
 
 	if (option & V_NOSCALEPATCH)
-		scrwidth *= vid.dupx;
+		scrwidth *= vid.dup;
 
 	for (;;ch++)
 	{
@@ -3312,18 +3280,18 @@ void V_DrawLevelTitle(INT32 x, INT32 y, INT32 option, const char *string)
 		if (*ch == '\n')
 		{
 			cx = x;
-			cy += 12*dupy;
+			cy += 12*dup;
 			continue;
 		}
 
 		c = *ch - LT_FONTSTART;
 		if (c < 0 || c >= LT_FONTSIZE || !lt_font[c])
 		{
-			cx += 16*dupx;
+			cx += 16*dup;
 			continue;
 		}
 
-		w = lt_font[c]->width * dupx;
+		w = lt_font[c]->width * dup;
 
 		if (cx > scrwidth)
 			continue;
@@ -3434,7 +3402,7 @@ INT32 V_StringWidth(const char *string, INT32 option)
 	}
 
 	if (option & (V_NOSCALESTART|V_NOSCALEPATCH))
-		w *= vid.dupx;
+		w *= vid.dup;
 
 	return w;
 }
@@ -3681,7 +3649,7 @@ Unoptimized version
 			{
 				// Shift this row of pixels to the right by 2
 				tmpscr[y*vid.width] = srcscr[y*vid.width];
-				M_Memcpy(&tmpscr[y*vid.width+vid.dupx], &srcscr[y*vid.width], vid.width-vid.dupx);
+				M_Memcpy(&tmpscr[y*vid.width+vid.dup], &srcscr[y*vid.width], vid.width-vid.dup);
 			}
 			else
 				M_Memcpy(&tmpscr[y*vid.width], &srcscr[y*vid.width], vid.width);
@@ -3782,31 +3750,23 @@ void V_Recalc(void)
 {
 	// scale 1,2,3 times in x and y the patches for the menus and overlays...
 	// calculated once and for all, used by routines in v_video.c and v_draw.c
-	vid.dupx = vid.width / BASEVIDWIDTH;
-	vid.dupy = vid.height / BASEVIDHEIGHT;
-	vid.dupx = vid.dupy = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy);
-	vid.fdupx = FixedDiv(vid.width*FRACUNIT, BASEVIDWIDTH*FRACUNIT);
-	vid.fdupy = FixedDiv(vid.height*FRACUNIT, BASEVIDHEIGHT*FRACUNIT);
 
-#ifdef HWRENDER
-	//if (rendermode != render_opengl && rendermode != render_none) // This was just placing it incorrectly at non aspect correct resolutions in opengl
-	// 13/11/18:
-	// The above is no longer necessary, since we want OpenGL to be just like software now
-	// -- Monster Iestyn
-#endif
-		vid.fdupx = vid.fdupy = (vid.fdupx < vid.fdupy ? vid.fdupx : vid.fdupy);
-
-	vid.meddupx = (UINT8)(vid.dupx >> 1) + 1;
-	vid.meddupy = (UINT8)(vid.dupy >> 1) + 1;
-#ifdef HWRENDER
-	vid.fmeddupx = vid.meddupx*FRACUNIT;
-	vid.fmeddupy = vid.meddupy*FRACUNIT;
-#endif
+	// Set dup based on width or height, whichever is less
+	if (((vid.width*FRACUNIT) / BASEVIDWIDTH) < ((vid.height*FRACUNIT) / BASEVIDHEIGHT))
+	{
+		vid.dup = vid.width / BASEVIDWIDTH;
+		vid.fdup = (vid.width*FRACUNIT) / BASEVIDWIDTH;
+	}
+	else
+	{
+		vid.dup = vid.height / BASEVIDHEIGHT;
+		vid.fdup = (vid.height*FRACUNIT) / BASEVIDHEIGHT;
+	}
 
-	vid.smalldupx = (UINT8)(vid.dupx / 3) + 1;
-	vid.smalldupy = (UINT8)(vid.dupy / 3) + 1;
+	vid.meddup = (UINT8)(vid.dup >> 1) + 1;
+	vid.smalldup = (UINT8)(vid.dup / 3) + 1;
 #ifdef HWRENDER
-	vid.fsmalldupx = vid.smalldupx*FRACUNIT;
-	vid.fsmalldupy = vid.smalldupy*FRACUNIT;
+	vid.fmeddup = vid.meddup*FRACUNIT;
+	vid.fsmalldup = vid.smalldup*FRACUNIT;
 #endif
 }
diff --git a/src/v_video.h b/src/v_video.h
index ff03836b597bec40f8712f534afef32bd46a2db8..80936f3ee9f6f6585b0a3d4208a43dda266e8635 100644
--- a/src/v_video.h
+++ b/src/v_video.h
@@ -36,7 +36,7 @@ cv_rsaturation, cv_ysaturation, cv_gsaturation, cv_csaturation, cv_bsaturation,
 // Allocates buffer screens, call before R_Init.
 void V_Init(void);
 
-// Recalculates the viddef (dupx, dupy, etc.) according to the current screen resolution.
+// Recalculates the viddef (dup, fdup, etc.) according to the current screen resolution.
 void V_Recalc(void);
 
 // Color look-up table
diff --git a/src/version.h b/src/version.h
index 3242cad672df6757e74741ca482a403f7544e31b..8d8f8978e7ea3cf593247a73a9460ed139193c10 100644
--- a/src/version.h
+++ b/src/version.h
@@ -1,4 +1,4 @@
-#define SRB2VERSION "2.2.13"/* this must be the first line, for cmake !! */
+#define SRB2VERSION "2.2.14"/* this must be the first line, for cmake !! */
 
 // 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.
@@ -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 54
+#define MODVERSION 55
 
 // Define this as a prerelease version suffix (pre#, RC#)
-//#define BETAVERSION "pre1"
+#define BETAVERSION "nightly"
diff --git a/src/w_wad.c b/src/w_wad.c
index c8880f6934f0f04bbe892da599e90f487339b242..051469ef57285b28003b43569de8e625856ce5e8 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -51,8 +51,8 @@
 #include "filesrch.h"
 
 #include "d_main.h"
-#include "d_netfil.h"
-#include "d_clisrv.h"
+#include "netcode/d_netfil.h"
+#include "netcode/d_clisrv.h"
 #include "dehacked.h"
 #include "r_defs.h"
 #include "r_data.h"
@@ -208,7 +208,7 @@ static void W_LoadDehackedLumpsPK3(UINT16 wadnum, boolean mainfile)
 	posStart = W_CheckNumForFullNamePK3("Init.lua", wadnum, 0);
 	if (posStart != INT16_MAX)
 	{
-		LUA_LoadLump(wadnum, posStart, true);
+		LUA_DoLump(wadnum, posStart, true);
 	}
 	else
 	{
@@ -217,7 +217,7 @@ static void W_LoadDehackedLumpsPK3(UINT16 wadnum, boolean mainfile)
 		{
 			posEnd = W_CheckNumForFolderEndPK3("Lua/", wadnum, posStart);
 			for (; posStart < posEnd; posStart++)
-				LUA_LoadLump(wadnum, posStart, true);
+				LUA_DoLump(wadnum, posStart, true);
 		}
 	}
 
@@ -250,7 +250,7 @@ static void W_LoadDehackedLumps(UINT16 wadnum, boolean mainfile)
 		lumpinfo_t *lump_p = wadfiles[wadnum]->lumpinfo;
 		for (lump = 0; lump < wadfiles[wadnum]->numlumps; lump++, lump_p++)
 			if (memcmp(lump_p->name,"LUA_",4)==0)
-				LUA_LoadLump(wadnum, lump, true);
+				LUA_DoLump(wadnum, lump, true);
 	}
 
 	{
@@ -993,7 +993,7 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 		DEH_LoadDehackedLumpPwad(numwadfiles - 1, 0, mainfile);
 		break;
 	case RET_LUA:
-		LUA_LoadLump(numwadfiles - 1, 0, true);
+		LUA_DoLump(numwadfiles - 1, 0, true);
 		break;
 	default:
 		break;
diff --git a/src/win32/Srb2win.rc b/src/win32/Srb2win.rc
index b699007463ad3f37527367b40e4a1c29411012ab..9ee9b7d3f32dd3fcb9e5e2b2dac370e176d97df1 100644
--- a/src/win32/Srb2win.rc
+++ b/src/win32/Srb2win.rc
@@ -77,8 +77,8 @@ END
 #include "../doomdef.h" // Needed for version string
 
 VS_VERSION_INFO VERSIONINFO
- FILEVERSION 2,2,13,0
- PRODUCTVERSION 2,2,13,0
+ FILEVERSION 2,2,14,0
+ PRODUCTVERSION 2,2,14,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
diff --git a/src/y_inter.c b/src/y_inter.c
index 8bec2b30f4e9314eb529defe4578daac5d0b697b..cbe057582161e15d92504fdaf834095bb68fddd1 100644
--- a/src/y_inter.c
+++ b/src/y_inter.c
@@ -15,7 +15,7 @@
 #include "f_finale.h"
 #include "g_game.h"
 #include "hu_stuff.h"
-#include "i_net.h"
+#include "netcode/i_net.h"
 #include "i_video.h"
 #include "p_tick.h"
 #include "r_defs.h"
@@ -536,7 +536,7 @@ void Y_IntermissionDrawer(void)
 
 		if (animatetic && (tic_t)intertic >= animatetic)
 		{
-			const INT32 scradjust = (vid.width/vid.dupx)>>3; // 40 for BASEVIDWIDTH
+			const INT32 scradjust = (vid.width/vid.dup)>>3; // 40 for BASEVIDWIDTH
 			INT32 animatetimer = (intertic - animatetic);
 			if (animatetimer <= 16)
 			{
@@ -579,9 +579,9 @@ void Y_IntermissionDrawer(void)
 		{
 			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";
+				const char *ringtext = "\x82" "get 50 rings then";
+				const char *tut1text = "\x82" "press " "\x80" "shield";
+				const char *tut2text = "\x82" "to " "\x80" "transform";
 				ttheight = 8;
 				V_DrawLevelTitle(data.spec.passedx1 + xoffset1, ttheight, 0, data.spec.passed1);
 				ttheight += V_LevelNameHeight(data.spec.passed3) + 2;
@@ -687,7 +687,7 @@ void Y_IntermissionDrawer(void)
 
 				if (intertic > 1)
 				{
-					if (stagefailed && data.spec.emeraldy < (vid.height/vid.dupy)+16)
+					if (stagefailed && data.spec.emeraldy < (vid.height/vid.dup)+16)
 					{
 						emeraldx += intertic - 6;
 					}
@@ -1163,7 +1163,7 @@ void Y_Ticker(void)
 			}
 			else
 			{
-				if (data.spec.emeraldy < (vid.height/vid.dupy)+16)
+				if (data.spec.emeraldy < (vid.height/vid.dup)+16)
 				{
 					data.spec.emeraldy += (++data.spec.emeraldmomy);
 				}