diff --git a/.circleci/config.yml b/.circleci/config.yml
index 711be39d76fdfb80c4680bbae19c8dd135af0afb..b3b97363eeb3f31e41ddf01f01e6f01f52c86204 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,9 +1,9 @@
 version: 2
 jobs:
   build:
-    working_directory: /root/SRB2
+    working_directory: /home/circleci/SRB2
     docker:
-      - image: debian:stretch
+      - image: cimg/base:current
         environment:
           CC: ccache gcc -m32
           PKG_CONFIG_LIBDIR: /usr/lib/i386-linux-gnu/pkgconfig
@@ -11,7 +11,7 @@ jobs:
           LIBGME_LDFLAGS: -lgme
           CCACHE_COMPRESS: true
           WFLAGS: -Wno-unsuffixed-float-constants
-          GCC49: true
+          GCC81: true
       #- image: ubuntu:trusty
       #  environment:
       #    CC: ccache gcc -m32
@@ -25,39 +25,42 @@ jobs:
     steps:
       - run:
           name: Add i386 arch
-          command: dpkg --add-architecture i386
+          command: sudo dpkg --add-architecture i386
       - run:
           name: Add STJr PPA
           command: |
-            apt-get -qq update
-            apt-get -qq -y install dirmngr
-            apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 0B1702D71499D9C25F986507F240F4449D3B0EC6
-            echo "deb http://ppa.launchpad.net/stjr/srb2/ubuntu trusty main" >> /etc/apt/sources.list
+            sudo apt-get -qq update
+            sudo apt-get -qq -y install dirmngr
+            sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 0B1702D71499D9C25F986507F240F4449D3B0EC6
+            echo "deb http://ppa.launchpad.net/stjr/srb2/ubuntu trusty main" | sudo tee -a /etc/apt/sources.list
       - run:
           name: Make APT cache folder
-          command: mkdir -p /root/.cache/apt/archives/partial
+          command: mkdir -p /home/circleci/.cache/apt/archives/partial
       - run:
           name: Make APT cache usage by _apt
-          command: chown -Rv _apt:root /root/.cache/apt/archives/partial
+          command: sudo chown -Rv _apt:root /home/circleci/.cache/apt/archives/partial
       - run:
           name: Update APT listing
-          command: apt-get -qq update
+          command: sudo apt-get -qq update
       - run:
           name: Support S3 upload
-          command: apt-get -qq -y install ca-certificates
+          command: sudo apt-get -qq -y install ca-certificates
       - restore_cache:
           keys:
             - v1-SRB2-APT
       - run:
-          name: Install SDK
-          command: apt-get -o Dir::Cache="/root/.cache/apt" -qq -y --no-install-recommends install git build-essential nasm libpng-dev:i386 libsdl2-mixer-dev:i386 libgme-dev:i386 libcurl4-openssl-dev:i386 libopenmpt-dev:i386 gettext ccache wget gcc-multilib upx openssh-client
+         name: Uninstall amd64 SDK
+         command: sudo apt-get -o Dir::Cache="/home/circleci/.cache/apt" -qq -y --no-install-recommends remove libcurl4-openssl-dev:amd64
+      - run:
+          name: Install i386 SDK
+          command: sudo apt-get -o Dir::Cache="/home/circleci/.cache/apt" -qq -y --no-install-recommends install git build-essential libpng-dev:i386 libsdl2-mixer-dev:i386 libgme-dev:i386 libcurl4-openssl-dev:i386 libopenmpt-dev:i386 gettext ccache wget gcc-multilib upx openssh-client
       - run:
           name: make md5sum
-          command: find /root/.cache/apt/archives -type f -print0 | sort -z | xargs -r0 md5sum > /root/.cache/apt_archives.md5
+          command: sudo find /home/circleci/.cache/apt/archives -type f -print0 | sort -z | sudo xargs -r0 md5sum > /home/circleci/.cache/apt_archives.md5
       - save_cache:
-          key: v1-SRB2-APT-{{ checksum "/root/.cache/apt_archives.md5" }}
+          key: v1-SRB2-APT-{{ checksum "/home/circleci/.cache/apt_archives.md5" }}
           paths:
-            - /root/.cache/apt
+            - /home/circleci/.cache/apt
       - checkout
       - run:
           name: Compile without network support
@@ -78,9 +81,9 @@ jobs:
           name: Compile
           command: make -C src LINUX=1 ERRORMODE=1 -k -j4
       - store_artifacts:
-          path: /root/SRB2/bin/
+          path: /home/circleci/SRB2/bin/
           destination: bin
       - save_cache:
           key: v1-SRB2-{{ .Branch }}-{{ checksum "make/linux/SDL.deps" }}
           paths:
-            - /root/.ccache
+            - /home/circleci/.ccache
diff --git a/.gitattributes b/.gitattributes
index 7751149ac07713a953529c5e8ba579109d76356d..c2e507352e6419be54e8866cc5bb40f5570a0fc7 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,15 +1,17 @@
 #Source code
+/Makefile text=auto
 /src/*.c text=auto
 /src/*.h text=auto
 /src/*.s text=auto
 /src/*.m text=auto
 /src/*.xpm text=auto
 /src/Makefile text=auto
+/tools/Makefile text=auto
 /src/Make*.cfg text=auto
 /src/CMakeLists.txt text=auto
+*.mk -whitespace text=auto
 # Windows EOL
 *.cs -crlf -whitespace
-*.mk -crlf -whitespace
 *.bat -crlf -whitespace
 *.dev -crlf -whitespace
 *.dsp -crlf -whitespace
diff --git a/.gitignore b/.gitignore
index cd828dc116957ceda70d6c5b8a2b929b158f1f80..268e3632906a9b840747e6c015b32848ba45b50f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,4 +22,5 @@ Win32_LIB_ASM_Release
 /make
 /bin
 /build
-/build.*
+/build/*
+/CMakeUserPresets.json
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b292bfc06c21f5ef02d6bb155ae4ab5e4ee6533c
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,434 @@
+variables:
+  GIT_STRATEGY: clone
+  GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_PATH
+
+default:
+  image: debian:stable-slim
+
+  cache:
+    - key: ccache-$CI_PROJECT_PATH_SLUG-$CI_JOB_NAME_SLUG
+      fallback_keys:
+        - cache-$CI_PROJECT_PATH_SLUG-$CI_DEFAULT_BRANCH
+        - cache-$CI_PROJECT_PATH_SLUG-default
+      paths:
+        - ccache
+        - ccache_statslog
+
+    - key: apt-$CI_JOB_IMAGE
+      paths:
+        - apt-cache
+      unprotect: true
+
+  before_script:
+    - - |
+         # debconf
+         echo -e "\e[0Ksection_start:`date +%s`:debconf[collapsed=true]\r\e[0KSetup debconf's environment"
+      - export DEBIAN_FRONTEND="noninteractive"
+      - export DEBIAN_PRIORITY="low"
+      - export DEBCONF_NONINTERACTIVE_SEEN="true"
+      - |
+          # debconf
+          echo -e "\e[0Ksection_end:`date +%s`:debconf\r\e[0K"
+    - - |
+          # dpkg_aa
+          echo -e "\e[0Ksection_start:`date +%s`:dpkg_aa[collapsed=true]\r\e[0KAdding architectures to dpkg"
+      - dpkg --add-architecture i386
+      - dpkg --add-architecture amd64
+      - dpkg --add-architecture arm64
+      - |
+          # dpkg_aa
+          echo -e "\e[0Ksection_end:`date +%s`:dpkg_aa\r\e[0K"
+    - - |
+          # apt_conf
+          echo -e "\e[0Ksection_start:`date +%s`:apt_conf[collapsed=true]\r\e[0KSetting up APT conf"
+      - export APT_CACHE_DIR=`pwd`/apt-cache
+      - mkdir --parents --verbose $APT_CACHE_DIR/partial/
+      - touch /etc/apt/apt.conf.d/99build
+      - |
+          # apt.conf
+          echo Adding options to apt.conf':'
+      - |
+          # APT::Install-Recommends
+          echo APT::Install-Recommends "false"\;       | tee --append /etc/apt/apt.conf.d/99build
+      - |
+          # quit
+          echo quiet "1"\;                             | tee --append /etc/apt/apt.conf.d/99build
+      - |
+          # APT::Get::Assume-Yes
+          echo APT::Get::Assume-Yes "true"\;           | tee --append /etc/apt/apt.conf.d/99build
+      - |
+          # Dir::Cache::Archives
+          echo Dir::Cache::Archives "$APT_CACHE_DIR"\; | tee --append /etc/apt/apt.conf.d/99build
+      - |
+          # apt_conf
+          echo -e "\e[0Ksection_end:`date +%s`:apt_conf\r\e[0K"
+    - - |
+          # apt_update
+          echo -e "\e[0Ksection_start:`date +%s`:apt_update[collapsed=true]\r\e[0KUpdating APT listing"
+      - apt-get update
+      - |
+          # apt_update
+          echo -e "\e[0Ksection_end:`date +%s`:apt_update\r\e[0K"
+
+    - - |
+          # apt_pre
+          echo -e "\e[0Ksection_start:`date +%s`:apt_pre[collapsed=true]\r\e[0KInstalling pre packages"
+      - apt-get install apt-utils
+      - |
+          # apt_pre
+          echo -e "\e[0Ksection_end:`date +%s`:apt_pre\r\e[0K"
+
+    - - |
+          # apt_upgrade
+          echo -e "\e[0Ksection_start:`date +%s`:apt_upgrade[collapsed=true]\r\e[0KUpdating existing packages"
+      - apt-get upgrade
+      - |
+          # apt_update
+          echo -e "\e[0Ksection_end:`date +%s`:apt_upgrade\r\e[0K"
+
+    - - |
+          # apt_common
+          echo -e "\e[0Ksection_start:`date +%s`:apt_common[collapsed=true]\r\e[0KInstalling common packages"
+      - apt-get install make git ccache nasm
+      - |
+          # apt_common
+          echo -e "\e[0Ksection_end:`date +%s`:apt_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 --append ~/.ccache/ccache.conf
+      - |
+          # cache_dir
+          echo cache_dir = $PWD/ccache          | tee --append ~/.ccache/ccache.conf
+      - |
+          # compiler_check
+          echo compiler_check = content         | tee --append ~/.ccache/ccache.conf
+      - |
+          # stats_log
+          echo stats_log = $PWD/ccache_statslog | tee --append ~/.ccache/ccache.conf
+      - |
+          # max_size
+          echo max_size = 50M                   | tee --append ~/.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"
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-$CI_JOB_NAME_SLUG"
+
+  after_script:
+    - - |
+           # apt_clean
+           echo -e "\e[0Ksection_start:`date +%s`:apt_clean[collapsed=true]\r\e[0KCleaning of unneeded APT packages"
+      - apt-get autoclean
+      - |
+          # apt_clean
+          echo -e "\e[0Ksection_end:`date +%s`:apt_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"
+
+stages:
+  - build
+
+Debian testing GCC:
+  stage: build
+  image: debian:testing-slim
+
+  allow_failure: true
+
+  artifacts:
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-testing-gcc"
+
+  variables:
+    CC: gcc
+    LDFLAGS: -Wl,-fuse-ld=gold
+
+  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 libsdl2-mixer-dev libpng-dev libcurl4-openssl-dev libgme-dev libopenmpt-dev
+      - |
+          # 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 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+Windows x86:
+  stage: build
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Win32"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-Win32"
+
+  variables:
+    PREFIX: i686-w64-mingw32
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install gcc-mingw-w64-i686-win32
+      - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_end:`date +%s`:apt_toolchain\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 MINGW=1 SDL=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 MINGW=1 SDL=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+Debian stable:amd64:
+  stage: build
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Debian amd64"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-x86-64"
+
+  variables:
+    CC: x86_64-linux-gnu-gcc
+    LDFLAGS: -Wl,-fuse-ld=gold
+    OBJCOPY: x86_64-linux-gnu-objcopy
+    OBJDUMP: x86_64-linux-gnu-objdump
+    PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install gcc-x86-64-linux-gnu || 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 libsdl2-mixer-dev:amd64 libpng-dev:amd64 libcurl4-openssl-dev:amd64 libgme-dev:amd64 libopenmpt-dev:amd64
+      - |
+          # 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 LINUX64=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+Debian stable:i386:
+  stage: build
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Debian i386"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-i686"
+
+  variables:
+    CC: i686-linux-gnu-gcc
+    OBJCOPY: i686-linux-gnu-objcopy
+    OBJDUMP: i686-linux-gnu-objdump
+    PKG_CONFIG_PATH: /usr/lib/i386-linux-gnu/pkgconfig
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install gcc-i686-linux-gnu || 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 libsdl2-mixer-dev:i386 libpng-dev:i386 libcurl4-openssl-dev:i386 libgme-dev:i386 libopenmpt-dev:i386
+      - |
+          # 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 LINUX=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+Debian stable:arm64:
+  stage: build
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Debian arm64"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-aarch64"
+
+  variables:
+    CC: aarch64-linux-gnu-gcc
+    LDFLAGS: -Wl,-fuse-ld=gold
+    OBJCOPY: aarch64-linux-gnu-objcopy
+    OBJDUMP: aarch64-linux-gnu-objdump
+    PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install gcc-aarch64-linux-gnu || 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 libsdl2-mixer-dev:arm64 libpng-dev:arm64 libcurl4-openssl-dev:arm64 libgme-dev:arm64 libopenmpt-dev:arm64
+      - |
+          # 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 LINUX64=1 ERRORMODE=1 NONX86=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 LINUX64=1 NONX86=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+Windows x64:
+  stage: build
+
+  artifacts:
+    paths:
+      - "bin/"
+      - "src/comptime.h"
+    expose_as: "Win64"
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-Win64"
+
+  variables:
+    PREFIX: x86_64-w64-mingw32
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install gcc-mingw-w64-x86-64-win32
+      - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_end:`date +%s`:apt_toolchain\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 MINGW64=1 SDL=1 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 MINGW64=1 SDL=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+Debian stable Clang:
+  stage: build
+
+  allow_failure: true
+
+  artifacts:
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-clang"
+
+  variables:
+    CC: clang
+    WFLAGS: -Wno-cast-align
+    CFLAGS: -Wno-cast-align
+    LDFLAGS: -Wl,-fuse-ld=gold
+
+  script:
+    - - |
+          # apt_toolchain
+          echo -e "\e[0Ksection_start:`date +%s`:apt_toolchain[collapsed=true]\r\e[0KInstalling toolchain packages"
+      - apt-get install clang
+      - |
+          # 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 libsdl2-mixer-dev libpng-dev libcurl4-openssl-dev libgme-dev libopenmpt-dev
+      - |
+          # 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 || make --directory=src --keep-going CCACHE=1 ERRORMODE=1 NONX86=1
+      - |
+          # make
+          echo -e "\e[0Ksection_end:`date +%s`:make\r\e[0K"
+
+Debian testing Clang:
+  extends: Debian stable Clang
+
+  image: debian:testing-slim
+
+  artifacts:
+    name: "$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA-testing-clang"
+
+  variables:
+    CC: clang
+    WFLAGS: -Wno-cast-align -Wno-deprecated-non-prototype
+    CFLAGS: -Wno-cast-align -Wno-deprecated-non-prototype
+    LDFLAGS: -Wl,-fuse-ld=gold
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 915912af5e8689b782e62b30c8eafbee66804b97..80a3bdcd6798a48d10ea928d60ca3c4fb77bf353 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -53,11 +53,15 @@ else()
 	set(SRB2_CONFIG_SYSTEM_LIBRARIES_DEFAULT OFF)
 endif()
 
+# Clang tidy options will be ignored if CMAKE_<LANG>_CLANG_TIDY are set.
+option(SRB2_CONFIG_ENABLE_CLANG_TIDY_C "Enable default clang-tidy check configuration for C" OFF)
+option(SRB2_CONFIG_ENABLE_CLANG_TIDY_CXX "Enable default clang-tidy check configuration for C++" OFF)
 option(
 	SRB2_CONFIG_SYSTEM_LIBRARIES
 	"Link dependencies using CMake's find_package and do not use internal builds"
 	${SRB2_CONFIG_SYSTEM_LIBRARIES_DEFAULT}
 )
+option(SRB2_CONFIG_ENABLE_TESTS "Build the test suite" ON)
 # This option isn't recommended for distribution builds and probably won't work (yet).
 cmake_dependent_option(
 	SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES
@@ -76,6 +80,25 @@ option(SRB2_CONFIG_ZDEBUG "Compile with ZDEBUG defined." OFF)
 option(SRB2_CONFIG_PROFILEMODE "Compile for profiling (GCC only)." OFF)
 set(SRB2_CONFIG_ASSET_DIRECTORY "" CACHE PATH "Path to directory that contains all asset files for the installer. If set, assets will be part of installation and cpack.")
 
+if(SRB2_CONFIG_ENABLE_TESTS)
+	# https://github.com/catchorg/Catch2
+	CPMAddPackage(
+		NAME Catch2
+		VERSION 3.4.0
+		GITHUB_REPOSITORY catchorg/Catch2
+		OPTIONS
+			"CATCH_INSTALL_DOCS OFF"
+	)
+	list(APPEND CMAKE_MODULE_PATH "${Catch2_SOURCE_DIR}/extras")
+	include(CTest)
+	include(Catch)
+	add_executable(srb2tests)
+	# To add tests, use target_sources to add individual test files to the target in subdirs.
+	target_link_libraries(srb2tests PRIVATE Catch2::Catch2 Catch2::Catch2WithMain)
+	target_compile_features(srb2tests PRIVATE c_std_11 cxx_std_17)
+	catch_discover_tests(srb2tests)
+endif()
+
 # Enable CCache
 # (Set USE_CCACHE=ON to use, CCACHE_OPTIONS for options)
 if("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL Windows)
@@ -108,7 +131,11 @@ if("${SRB2_CONFIG_SYSTEM_LIBRARIES}")
 	find_package(SDL2_mixer REQUIRED)
 	find_package(CURL REQUIRED)
 	find_package(OPENMPT REQUIRED)
-	find_package(GME REQUIRED)
+
+	# libgme defaults to "Nuked" YM2612 emulator, which is
+	# very SLOW. The system library probably uses the
+	# default so just always build it.
+	#find_package(GME REQUIRED)
 endif()
 
 if(${PROJECT_SOURCE_DIR} MATCHES ${PROJECT_BINARY_DIR})
@@ -119,13 +146,6 @@ if ((${SRB2_USE_CCACHE}) AND (${CMAKE_C_COMPILER} MATCHES "clang"))
 	message(WARNING "Using clang and CCache: You may want to set environment variable CCACHE_CPP2=yes to prevent include errors during compile.")
 endif()
 
-# Add sources from Sourcefile
-function(target_sourcefile type)
-	file(STRINGS Sourcefile list
-		REGEX "[-0-9A-Za-z_]+\.${type}")
-	target_sources(SRB2SDL2 PRIVATE ${list})
-endfunction()
-
 # bitness check
 set(SRB2_SYSTEM_BITS 0)
 if(CMAKE_SIZEOF_VOID_P EQUAL 8)
@@ -144,7 +164,8 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
 set(CMAKE_PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
 
 # Set EXE names so the assets CMakeLists can refer to its target
-set(SRB2_SDL2_EXE_NAME srb2 CACHE STRING "Executable binary output name")
+set(SRB2_SDL2_EXE_NAME "" CACHE STRING "Override executable binary output name")
+set(SRB2_SDL2_EXE_SUFFIX "" CACHE STRING "Optional executable suffix, separated by an underscore")
 
 include_directories(${CMAKE_CURRENT_BINARY_DIR}/src)
 
@@ -152,11 +173,37 @@ add_subdirectory(src)
 add_subdirectory(assets)
 
 
-## config.h generation
 set(GIT_EXECUTABLE "git" CACHE FILEPATH "Path to git binary")
 include(GitUtilities)
-git_latest_commit(SRB2_COMP_COMMIT "${CMAKE_SOURCE_DIR}")
-git_current_branch(SRB2_GIT_BRANCH "${CMAKE_SOURCE_DIR}")
-set(SRB2_COMP_BRANCH "${SRB2_GIT_BRANCH}")
-set(SRB2_COMP_REVISION "${SRB2_COMP_COMMIT}")
-configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/src/config.h)
+
+if("${SRB2_SDL2_EXE_NAME}" STREQUAL "")
+	# cause a reconfigure if the branch changes
+	get_git_dir(SRB2_GIT_DIR)
+	configure_file("${SRB2_GIT_DIR}/HEAD" HEAD COPYONLY)
+
+	git_current_branch(SRB2_GIT_REVISION)
+
+	if("${SRB2_GIT_REVISION}" STREQUAL "")
+		# use abbreviated commit hash if on detached HEAD
+		git_latest_commit(SRB2_GIT_REVISION)
+	endif()
+
+	if("${CMAKE_SYSTEM_NAME}" MATCHES "Windows")
+		list(APPEND EXE_NAME_PARTS "srb2win")
+	elseif("${CMAKE_SYSTEM_NAME}" MATCHES "Linux")
+		list(APPEND EXE_NAME_PARTS "lsdlsrb2")
+	else()
+		list(APPEND EXE_NAME_PARTS "srb2")
+	endif()
+
+	if(NOT "${SRB2_GIT_REVISION}" STREQUAL "master")
+		list(APPEND EXE_NAME_PARTS ${SRB2_GIT_REVISION})
+	endif()
+else()
+	list(APPEND EXE_NAME_PARTS ${SRB2_SDL2_EXE_NAME})
+endif()
+
+list(APPEND EXE_NAME_PARTS ${SRB2_SDL2_EXE_SUFFIX})
+
+list(JOIN EXE_NAME_PARTS "_" EXE_NAME)
+set_target_properties(SRB2SDL2 PROPERTIES OUTPUT_NAME ${EXE_NAME})
diff --git a/CMakePresets.json b/CMakePresets.json
new file mode 100644
index 0000000000000000000000000000000000000000..7713bb38516877d5e8e50cc46914a7f58c9c6d73
--- /dev/null
+++ b/CMakePresets.json
@@ -0,0 +1,29 @@
+{
+	"version": 3,
+	"configurePresets": [
+		{
+			"name": "default",
+			"description": "Build using default generator",
+			"binaryDir": "build",
+			"cacheVariables": {
+				"CMAKE_C_FLAGS": "-fdiagnostics-color",
+				"CMAKE_CXX_FLAGS": "-fdiagnostics-color",
+				"CMAKE_BUILD_TYPE": "RelWithDebInfo"
+			}
+		},
+		{
+			"name": "debug",
+			"description": "Build for development (no optimizations)",
+			"inherits": "default",
+			"cacheVariables": {
+				"CMAKE_BUILD_TYPE": "Debug"
+			}
+		}
+	],
+	"buildPresets": [
+		{
+			"name": "default",
+			"configurePreset": "default"
+		}
+	]
+}
diff --git a/README.md b/README.md
index 49a3cc36d167169467a2d65bec7527610691694d..56ff2d02d24e39fe6f8038a0770e7b1b265c7ae7 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,6 @@
 [Sonic Robo Blast 2](https://srb2.org/) is a 3D Sonic the Hedgehog fangame based on a modified version of [Doom Legacy](http://doomlegacy.sourceforge.net/).
 
 ## Dependencies
-- NASM (x86 builds only)
 - SDL2 (Linux/OS X only)
 - SDL2-Mixer (Linux/OS X only)
 - libupnp (Linux/OS X only)
diff --git a/SRB2.cbp b/SRB2.cbp
index 2a1eb87b8565b3d2d9c13216c23d39dceecd6f92..9e887bf859c05ab821b3936f29cb3089daef2b2d 100644
--- a/SRB2.cbp
+++ b/SRB2.cbp
@@ -1992,24 +1992,6 @@ HW3SOUND for 3D hardware sound  support
 			<Option compilerVar="CC" />
 		</Unit>
 		<Unit filename="src/v_video.h" />
-		<Unit filename="src/vid_copy.s">
-			<Option compilerVar="CC" />
-			<Option compiler="avrgcc" use="1" buildCommand="$compiler $options -x assembler-with-cpp -c $file -o $object" />
-			<Option compiler="gnu_gcc_compiler_for_mingw32" use="1" buildCommand="$compiler $options -x assembler-with-cpp -c $file -o $object" />
-			<Option compiler="gnu_gcc_compiler_for_mingw64" use="1" buildCommand="$compiler $options -x assembler-with-cpp -c $file -o $object" />
-			<Option compiler="armelfgcc" use="1" buildCommand="$compiler $options -x assembler-with-cpp -c $file -o $object" />
-			<Option compiler="tricoregcc" use="1" buildCommand="$compiler $options -x assembler-with-cpp -c $file -o $object" />
-			<Option compiler="ppcgcc" use="1" buildCommand="$compiler $options -x assembler-with-cpp -c $file -o $object" />
-			<Option compiler="gcc" use="1" buildCommand="$compiler $options -x assembler-with-cpp -c $file -o $object" />
-			<Option target="Debug Native/SDL" />
-			<Option target="Release Native/SDL" />
-			<Option target="Debug Linux/SDL" />
-			<Option target="Release Linux/SDL" />
-			<Option target="Debug Mingw/SDL" />
-			<Option target="Release Mingw/SDL" />
-			<Option target="Debug Mingw/DirectX" />
-			<Option target="Release Mingw/DirectX" />
-		</Unit>
 		<Unit filename="src/w_wad.c">
 			<Option compilerVar="CC" />
 		</Unit>
diff --git a/SRB2_common.props b/SRB2_common.props
index 0f80ceb174874e682f0205de06733cb25e2b247a..6a0d53484f10106bc254851b1e1056dc7b24a86a 100644
--- a/SRB2_common.props
+++ b/SRB2_common.props
@@ -25,9 +25,6 @@
     </Link>
   </ItemDefinitionGroup>
   <ItemDefinitionGroup Condition="'$(PlatformTarget)'=='x86'">
-    <ClCompile>
-      <PreprocessorDefinitions>USEASM;%(PreprocessorDefinitions)</PreprocessorDefinitions>
-    </ClCompile>
     <Link>
       <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
     </Link>
diff --git a/Srb2.dev b/Srb2.dev
index 21683e7c3c5e055393d91778fba3405bfae240de..8bd36cf490cc84dae69d25399113d346392e4fc8 100644
--- a/Srb2.dev
+++ b/Srb2.dev
@@ -5,7 +5,7 @@ Ver=3
 IsCpp=0
 Type=0
 UnitCount=279
-Folders=A_Asm,B_Bot,BLUA,D_Doom,F_Frame,G_Game,H_Hud,Hw_Hardware,Hw_Hardware/r_opengl,I_Interface,I_Interface/Dummy,I_Interface/SDL,I_Interface/Win32,LUA,M_Misc,P_Play,R_Rend,S_Sounds,W_Wad
+Folders=B_Bot,BLUA,D_Doom,F_Frame,G_Game,H_Hud,Hw_Hardware,Hw_Hardware/r_opengl,I_Interface,I_Interface/Dummy,I_Interface/SDL,I_Interface/Win32,LUA,M_Misc,P_Play,R_Rend,S_Sounds,W_Wad
 CommandLine=
 CompilerSettings=00000000000100000111e1
 PchHead=-1
@@ -1473,36 +1473,6 @@ Priority=1000
 OverrideBuildCmd=0
 BuildCmd=
 
-[Unit149]
-FileName=src\tmap.nas
-Folder=A_Asm
-Compile=0
-CompileCpp=0
-Link=0
-Priority=1000
-OverrideBuildCmd=1
-BuildCmd=nasm.exe -g -o $@ -f win32 src/tmap.nas
-
-[Unit150]
-FileName=src\asm_defs.inc
-Folder=A_Asm
-Compile=0
-CompileCpp=0
-Link=0
-Priority=1000
-OverrideBuildCmd=0
-BuildCmd=
-
-[Unit151]
-FileName=src\vid_copy.s
-Folder=A_Asm
-Compile=1
-CompileCpp=0
-Link=1
-Priority=1000
-OverrideBuildCmd=1
-BuildCmd=$(CC) $(CFLAGS) -x assembler-with-cpp -c src/vid_copy.s -o $@
-
 [Unit152]
 FileName=src\y_inter.h
 Folder=H_Hud
@@ -1543,26 +1513,6 @@ Priority=1000
 OverrideBuildCmd=0
 BuildCmd=
 
-[Unit156]
-FileName=src\p5prof.h
-Folder=A_Asm
-Compile=1
-CompileCpp=0
-Link=1
-Priority=1000
-OverrideBuildCmd=0
-BuildCmd=
-
-[Unit157]
-FileName=src\tmap_mmx.nas
-Folder=A_Asm
-Compile=0
-CompileCpp=0
-Link=0
-Priority=1000
-OverrideBuildCmd=1
-BuildCmd=nasm.exe -g -o $@ -f win32 src/tmap_mmx.nas
-
 [Unit159]
 FileName=src\lzf.h
 Folder=W_Wad
diff --git a/alias-bootstrap.sh b/alias-bootstrap.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f1a6ac4ed918db4391a39c9a3076dfcc38d2861b
--- /dev/null
+++ b/alias-bootstrap.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env sh
+
+# All these commands can be run from anywhere in the git
+# tree, not just the top level.
+
+# Usage: git cmake
+#
+# Same usage as standard CMake command.
+#
+git config 'alias.cmake' '!cmake'
+
+# Usage: git build <build preset> [options]
+# Usage: git build [options]
+#
+# In the second usage, when no preset is given, the
+# "default" build preset is used.
+#
+# Available options can be found by running:
+#
+#     git cmake --build
+#
+git config 'alias.build' '!p="${1##-*}"; [ "$p" ] && shift; git cmake --build --preset "${p:-default}"'
+
+# Usage: git crossmake
+#
+# Shortcut to i686-w64-mingw32-cmake (CMake cross
+# compiler)
+#
+git config 'alias.crossmake' '!i686-w64-mingw32-cmake'
diff --git a/appveyor.yml b/appveyor.yml
index e3348d35cb2b001fba3162792867548b278d63f5..63d801b734719bf4ba47dfb42b1b7849c3a23189 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
-version: 2.2.11.{branch}-{build}
+version: 2.2.13.{branch}-{build}
 os: MinGW
 
 environment:
@@ -7,8 +7,6 @@ environment:
  # c:\mingw-w64 i686 has gcc 6.3.0, so use c:\msys64 7.3.0 instead
  MINGW_SDK: c:\msys64\mingw32
  CFLAGS: -Wno-implicit-fallthrough
- NASM_ZIP: nasm-2.12.01
- NASM_URL: http://www.nasm.us/pub/nasm/releasebuilds/2.12.01/win64/nasm-2.12.01-win64.zip
  UPX_ZIP: upx391w
  UPX_URL: http://upx.sourceforge.net/download/upx391w.zip
  CCACHE_EXE: ccache.exe
@@ -40,17 +38,12 @@ environment:
  ASSET_CLEAN: 0
 
 cache:
-- nasm-2.12.01.zip
 - upx391w.zip
 - ccache.exe
 - C:\Users\appveyor\.ccache
 - C:\Users\appveyor\srb2_cache
 
 install:
-- if not exist "%NASM_ZIP%.zip" appveyor DownloadFile "%NASM_URL%" -FileName "%NASM_ZIP%.zip"
-- 7z x -y "%NASM_ZIP%.zip" -o%TMP% >null
-- robocopy /S /xx /ns /nc /nfl /ndl /np /njh /njs "%TMP%\%NASM_ZIP%" "%MINGW_SDK%\bin" nasm.exe || exit 0
-
 - if not exist "%UPX_ZIP%.zip" appveyor DownloadFile "%UPX_URL%" -FileName "%UPX_ZIP%.zip"
 - 7z x -y "%UPX_ZIP%.zip" -o%TMP% >null
 - robocopy /S /xx /ns /nc /nfl /ndl /np /njh /njs "%TMP%\%UPX_ZIP%" "%MINGW_SDK%\bin" upx.exe || exit 0
@@ -65,7 +58,6 @@ configuration:
 before_build:
 - set "Path=%MINGW_SDK%\bin;%Path%"
 - mingw32-make --version
-- nasm -v
 - if not [%NOUPX%] == [1] ( upx -V )
 - ccache -V
 - ccache -s
diff --git a/cmake/Comptime.cmake b/cmake/Comptime.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..8388aed9ece077229a35d601ffd0679cb2ecd035
--- /dev/null
+++ b/cmake/Comptime.cmake
@@ -0,0 +1,32 @@
+cmake_minimum_required(VERSION 3.3 FATAL_ERROR)
+
+set(CMAKE_BINARY_DIR "${BINARY_DIR}")
+set(CMAKE_CURRENT_BINARY_DIR "${BINARY_DIR}")
+
+# Set up CMAKE path
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/")
+
+include(GitUtilities)
+
+git_current_branch(SRB2_COMP_BRANCH)
+git_working_tree_dirty(SRB2_COMP_UNCOMMITTED)
+
+git_latest_commit(SRB2_COMP_REVISION)
+git_subject(subject)
+string(REGEX REPLACE "([\"\\])" "\\\\\\1" SRB2_COMP_NOTE "${subject}")
+
+if("${CMAKE_BUILD_TYPE}" STREQUAL "")
+	set(CMAKE_BUILD_TYPE None)
+endif()
+
+# These build types enable optimizations of some kind by default.
+set(optimized_build_types "MINSIZEREL;RELEASE;RELWITHDEBINFO")
+
+string(TOUPPER "${CMAKE_BUILD_TYPE}" build_type)
+if("${build_type}" IN_LIST optimized_build_types)
+	set(SRB2_COMP_OPTIMIZED TRUE)
+else()
+	set(SRB2_COMP_OPTIMIZED FALSE)
+endif()
+
+configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/config.h.in" "${CMAKE_CURRENT_BINARY_DIR}/src/config.h")
diff --git a/cmake/Modules/CMakeASM_YASMInformation.cmake b/cmake/Modules/CMakeASM_YASMInformation.cmake
deleted file mode 100644
index 1765180853bb2d23217a1eb785f97737411e68b1..0000000000000000000000000000000000000000
--- a/cmake/Modules/CMakeASM_YASMInformation.cmake
+++ /dev/null
@@ -1,46 +0,0 @@
-
-#=============================================================================
-# Copyright 2010 Kitware, Inc.
-#
-# Distributed under the OSI-approved BSD License (the "License");
-# see accompanying file Copyright.txt for details.
-#
-# This software is distributed WITHOUT ANY WARRANTY; without even the
-# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-# See the License for more information.
-#=============================================================================
-# (To distribute this file outside of CMake, substitute the full
-#  License text for the above reference.)
-
-# support for the yasm assembler
-
-set(CMAKE_ASM_YASM_SOURCE_FILE_EXTENSIONS nasm yasm asm)
-
-if(NOT CMAKE_ASM_YASM_OBJECT_FORMAT)
-  if(WIN32)
-    if(CMAKE_C_SIZEOF_DATA_PTR EQUAL 8)
-      set(CMAKE_ASM_YASM_OBJECT_FORMAT win64)
-    else()
-      set(CMAKE_ASM_YASM_OBJECT_FORMAT win32)
-    endif()
-  elseif(APPLE)
-    if(CMAKE_C_SIZEOF_DATA_PTR EQUAL 8)
-      set(CMAKE_ASM_YASM_OBJECT_FORMAT macho64)
-    else()
-      set(CMAKE_ASM_YASM_OBJECT_FORMAT macho)
-    endif()
-  else()
-    if(CMAKE_C_SIZEOF_DATA_PTR EQUAL 8)
-      set(CMAKE_ASM_YASM_OBJECT_FORMAT elf64)
-    else()
-      set(CMAKE_ASM_YASM_OBJECT_FORMAT elf)
-    endif()
-  endif()
-endif()
-
-set(CMAKE_ASM_YASM_COMPILE_OBJECT "<CMAKE_ASM_YASM_COMPILER> <FLAGS> -f ${CMAKE_ASM_YASM_OBJECT_FORMAT} -o <OBJECT> <SOURCE>")
-
-# Load the generic ASMInformation file:
-set(ASM_DIALECT "_YASM")
-include(CMakeASMInformation)
-set(ASM_DIALECT)
diff --git a/cmake/Modules/CMakeDetermineASM_YASMCompiler.cmake b/cmake/Modules/CMakeDetermineASM_YASMCompiler.cmake
deleted file mode 100644
index a5e7c9e5801121f04411e5f1b1c6efa98736bcc1..0000000000000000000000000000000000000000
--- a/cmake/Modules/CMakeDetermineASM_YASMCompiler.cmake
+++ /dev/null
@@ -1,27 +0,0 @@
-
-#=============================================================================
-# Copyright 2010 Kitware, Inc.
-#
-# Distributed under the OSI-approved BSD License (the "License");
-# see accompanying file Copyright.txt for details.
-#
-# This software is distributed WITHOUT ANY WARRANTY; without even the
-# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-# See the License for more information.
-#=============================================================================
-# (To distribute this file outside of CMake, substitute the full
-#  License text for the above reference.)
-
-# Find the nasm assembler. yasm (http://www.tortall.net/projects/yasm/) is nasm compatible
-
-set(CMAKE_ASM_YASM_COMPILER_LIST nasm yasm)
-
-if(NOT CMAKE_ASM_YASM_COMPILER)
-  find_program(CMAKE_ASM_YASM_COMPILER yasm
-    "$ENV{ProgramFiles}/YASM")
-endif()
-
-# Load the generic DetermineASM compiler file with the DIALECT set properly:
-set(ASM_DIALECT "_YASM")
-include(CMakeDetermineASMCompiler)
-set(ASM_DIALECT)
diff --git a/cmake/Modules/CMakeTestASM_YASMCompiler.cmake b/cmake/Modules/CMakeTestASM_YASMCompiler.cmake
deleted file mode 100644
index 745f7125c4a2f7a003c488b89d977b75a8eb3ebc..0000000000000000000000000000000000000000
--- a/cmake/Modules/CMakeTestASM_YASMCompiler.cmake
+++ /dev/null
@@ -1,23 +0,0 @@
-
-#=============================================================================
-# Copyright 2010 Kitware, Inc.
-#
-# Distributed under the OSI-approved BSD License (the "License");
-# see accompanying file Copyright.txt for details.
-#
-# This software is distributed WITHOUT ANY WARRANTY; without even the
-# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-# See the License for more information.
-#=============================================================================
-# (To distribute this file outside of CMake, substitute the full
-#  License text for the above reference.)
-
-# This file is used by EnableLanguage in cmGlobalGenerator to
-# determine that the selected ASM_NASM "compiler" works.
-# For assembler this can only check whether the compiler has been found,
-# because otherwise there would have to be a separate assembler source file
-# for each assembler on every architecture.
-
-set(ASM_DIALECT "_YASM")
-include(CMakeTestASMCompiler)
-set(ASM_DIALECT)
diff --git a/cmake/Modules/GitUtilities.cmake b/cmake/Modules/GitUtilities.cmake
index d29e6b509dd27015f429c9e8f4de12e20fcc5b12..586c7b433ff31476fe2a4cf0d5634d36d2c997be 100644
--- a/cmake/Modules/GitUtilities.cmake
+++ b/cmake/Modules/GitUtilities.cmake
@@ -6,38 +6,54 @@ endif()
 
 set(__GitUtilities ON)
 
-function(git_describe variable path)
-	execute_process(COMMAND "${GIT_EXECUTABLE}" "describe"
-		WORKING_DIRECTORY "${path}"
-		RESULT_VARIABLE result
+macro(_git_command)
+	execute_process(
+		COMMAND "${GIT_EXECUTABLE}" ${ARGN}
+		WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
 		OUTPUT_VARIABLE output
 		ERROR_QUIET
 		OUTPUT_STRIP_TRAILING_WHITESPACE
 	)
+endmacro()
 
+macro(_git_easy_command)
+	_git_command(${ARGN})
 	set(${variable} "${output}" PARENT_SCOPE)
-endfunction()
+endmacro()
 
-function(git_current_branch variable path)
-	execute_process(COMMAND ${GIT_EXECUTABLE} "symbolic-ref" "--short" "HEAD"
-		WORKING_DIRECTORY "${path}"
-		RESULT_VARIABLE result
-		OUTPUT_VARIABLE output
-		ERROR_QUIET
-		OUTPUT_STRIP_TRAILING_WHITESPACE
-	)
+function(git_current_branch variable)
+	_git_command(symbolic-ref -q --short HEAD)
+
+	# If a detached head, a ref could still be resolved.
+	if("${output}" STREQUAL "")
+		_git_command(describe --all --exact-match)
+
+		# Get the ref, in the form heads/master or
+		# remotes/origin/master so isolate the final part.
+		string(REGEX REPLACE ".*/" "" output "${output}")
+	endif()
 
 	set(${variable} "${output}" PARENT_SCOPE)
 endfunction()
 
-function(git_latest_commit variable path)
-	execute_process(COMMAND ${GIT_EXECUTABLE} "rev-parse" "--short" "HEAD"
-		WORKING_DIRECTORY "${path}"
-		RESULT_VARIABLE result
-		OUTPUT_VARIABLE output
-		ERROR_QUIET
-		OUTPUT_STRIP_TRAILING_WHITESPACE
-	)
+function(git_latest_commit variable)
+	_git_easy_command(rev-parse --short HEAD)
+endfunction()
 
-	set(${variable} "${output}" PARENT_SCOPE)
-endfunction()
\ No newline at end of file
+function(git_working_tree_dirty variable)
+	_git_command(status --porcelain -uno)
+
+	if(output STREQUAL "")
+		set(${variable} FALSE PARENT_SCOPE)
+	else()
+		set(${variable} TRUE PARENT_SCOPE)
+	endif()
+endfunction()
+
+function(git_subject variable)
+	_git_easy_command(log -1 --format=%s)
+endfunction()
+
+function(get_git_dir variable)
+	_git_easy_command(rev-parse --git-dir)
+endfunction()
diff --git a/cmake/Modules/clang-tidy-default.cmake b/cmake/Modules/clang-tidy-default.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..2be3af10d961229bac835c7083a0d81f677f7144
--- /dev/null
+++ b/cmake/Modules/clang-tidy-default.cmake
@@ -0,0 +1,21 @@
+find_program(CLANG_TIDY clang-tidy)
+
+# Note: Apple Clang does not ship with clang tools. If you want clang-tidy on
+# macOS, it's best to install the Homebrew llvm bottle and set CLANG_TIDY
+# in your build directory. The llvm package is keg-only, so it will not
+# collide with Apple Clang.
+
+function(target_set_default_clang_tidy target lang checks)
+    if("${CLANG_TIDY}" STREQUAL "CLANG_TIDY-NOTFOUND")
+        return()
+    endif()
+
+    get_target_property(c_clang_tidy_prop SRB2SDL2 C_CLANG_TIDY)
+    if(NOT ("${c_clang_tidy_prop}" STREQUAL "c_clang_tidy_prop-NOTFOUND"))
+        return()
+    endif()
+
+    set_target_properties("${target}" PROPERTIES
+	    ${lang}_CLANG_TIDY "${CLANG_TIDY};-checks=${checks}"
+    )
+endfunction()
diff --git a/cpdebug.mk b/cpdebug.mk
index 6baedf2275db4bbb06606a646cce9a4cb2c1d35e..75f08c66f4af721939c6f6c904aefc62f0d75534 100644
--- a/cpdebug.mk
+++ b/cpdebug.mk
@@ -3,12 +3,12 @@ ifdef ComSpec
 COMSPEC=$(ComSpec)
 endif
 ifdef COMSPEC
-OBJCOPY=objcopy.exe
-OBJDUMP=objdump.exe
+OBJCOPY?=objcopy.exe
+OBJDUMP?=objdump.exe
 GZIP?=gzip.exe
 else
-OBJCOPY=objcopy
-OBJDUMP=objdump
+OBJCOPY?=objcopy
+OBJDUMP?=objdump
 GZIP?=gzip
 endif
 DBGNAME=$(BIN).debug
diff --git a/extras/conf/SRB2-22.cfg b/extras/conf/SRB2-22.cfg
index dd5cdb46b2c2338fd1b2728f877ab903389054ad..41ad998891154f56fe850cdfe457a48189dd648f 100644
--- a/extras/conf/SRB2-22.cfg
+++ b/extras/conf/SRB2-22.cfg
@@ -4367,7 +4367,6 @@ thingtypes
 	{
 		color = 14; // Yellow
 		title = "Rings and Weapon Panels";
-		width = 24;
 		height = 24;
 		flags8height = 24;
 		flags8text = "[8] Float";
@@ -4377,7 +4376,6 @@ thingtypes
 		{
 			title = "Ring";
 			sprite = "RINGA0";
-			width = 16;
 		}
 		301
 		{
@@ -4393,6 +4391,7 @@ thingtypes
 		{
 			title = "Infinity Ring";
 			sprite = "RNGIA0";
+			width = 24;
 		}
 		304
 		{
@@ -4418,43 +4417,53 @@ thingtypes
 		{
 			title = "CTF Team Ring (Red)";
 			sprite = "internal:TRNGA0R";
-			width = 16;
 		}
 		309
 		{
 			title = "CTF Team Ring (Blue)";
 			sprite = "internal:TRNGA0B";
-			width = 16;
 		}
 		330
 		{
 			title = "Bounce Ring Panel";
 			sprite = "PIKBA0";
+			width = 24;
+			height = 40;
 		}
 		331
 		{
 			title = "Rail Ring Panel";
 			sprite = "PIKRA0";
+			width = 24;
+			height = 40;
 		}
 		332
 		{
 			title = "Automatic Ring Panel";
 			sprite = "PIKAA0";
+			width = 24;
+			height = 40;
 		}
 		333
 		{
 			title = "Explosion Ring Panel";
 			sprite = "PIKEA0";
+			width = 24;
+			height = 40;
 		}
 		334
 		{
 			title = "Scatter Ring Panel";
 			sprite = "PIKSA0";
+			width = 24;
+			height = 40;
 		}
 		335
 		{
 			title = "Grenade Ring Panel";
 			sprite = "PIKGA0";
+			width = 24;
+			height = 40;
 		}
 	}
 
@@ -4463,7 +4472,7 @@ thingtypes
 		color = 10; // Light Green
 		title = "Other Collectibles";
 		width = 16;
-		height = 32;
+		height = 24;
 		sort = 1;
 		sprite = "CEMGA0";
 
@@ -4529,6 +4538,7 @@ thingtypes
 		{
 			title = "Emerald Hunt Location";
 			sprite = "SHRDA0";
+			height = 32;
 			flags8height = 24;
 			flags8text = "[8] Float";
 		}
diff --git a/extras/conf/udb/Includes/SRB222_common.cfg b/extras/conf/udb/Includes/SRB222_common.cfg
index 0ff044a6d382cc102c5b5dd98681e04ff3c81b31..8f37aabaa41de4072ede5b6910666fb654d7cfd0 100644
--- a/extras/conf/udb/Includes/SRB222_common.cfg
+++ b/extras/conf/udb/Includes/SRB222_common.cfg
@@ -15,7 +15,7 @@ common
 	ignoredextensions = "wad pk3 pk7 bak backup1 backup2 backup3 zip rar 7z";
 
 	// Default testing parameters
-	testparameters = "-file \"%AP\" \"%F\" -warp %L";
+	testparameters = "-folder \"%AF\" -file \"%AA\" \"%F\" -warp %L";
 	testshortpaths = true;
 
 	// Action special help
@@ -26,7 +26,7 @@ common
 	generalizedsectors = true;
 
 	// Maximum safe map size check (0 means skip check)
-	safeboundary = 1;
+	safeboundary = 0;
 
 	// Map boundaries. Map objects can only be placed within these boundaries
 	leftboundary = -32768;
@@ -40,6 +40,8 @@ common
 	defaultflatscale = 1.0f;
 	scaledtextureoffsets = true;
 
+	maxcolormapalpha = 25;
+
 	// Thing number for start position in 3D Mode
 	start3dmode = 3328;
 
@@ -68,137 +70,6 @@ common
 	}
 }
 
-mapformat_doom
-{
-	// The format interface handles the map data format
-	formatinterface = "DoomMapSetIO";
-
-	// Default nodebuilder configurations
-	defaultsavecompiler = "zennode_normal";
-	defaulttestcompiler = "zennode_fast";
-
-	/*
-	GAME DETECT PATTERN
-	Used to guess the game for which a WAD file is made.
-
-	1 = One of these lumps must exist
-	2 = None of these lumps must exist
-	3 = All of these lumps must exist
-	*/
-
-	gamedetect
-	{
-		EXTENDED = 2;
-
-
-		BEHAVIOR = 2;
-
-		E#M# = 2;
-
-		MAP?? = 1;
-	}
-
-	/*
-	MAP LUMP NAMES
-	Map lumps are loaded with the map as long as they are right after each other. When the editor
-	meets a lump which is not defined in this list it will ignore the map if not satisfied.
-	The order of items defines the order in which lumps will be written to WAD file on save.
-	To indicate the map header lump, use ~MAP
-
-	Legenda:
-	required = Lump is required to exist.
-	blindcopy = Lump will be copied along with the map blindly. (usefull for lumps Doom Builder doesn't use)
-	nodebuild = The nodebuilder generates this lump.
-	allowempty = The nodebuilder is allowed to leave this lump empty.
-	script = This lump is a text-based script. Specify the filename of the script configuration to use.
-	*/
-
-	maplumpnames
-	{
-		include("SRB222_misc.cfg", "doommaplumpnames");
-	}
-
-	// When this is set to true, sectors with the same tag will light up when a line is highlighted
-	linetagindicatesectors = true;
-
-	// Special linedefs
-	include("SRB222_misc.cfg", "speciallinedefs");
-
-	// Default flags for first new thing
-	defaultthingflags
-	{
-	}
-
-	// DEFAULT SECTOR BRIGHTNESS LEVELS
-	sectorbrightness
-	{
-		include("SRB222_misc.cfg", "sectorbrightness");
-	}
-
-	// SECTOR TYPES
-	sectortypes
-	{
-		include("SRB222_sectors.cfg", "sectortypes");
-	}
-
-	// GENERALISED SECTOR TYPES
-	gen_sectortypes
-	{
-		include("SRB222_sectors.cfg", "gen_sectortypes");
-	}
-
-	// LINEDEF FLAGS
-	linedefflags
-	{
-		include("SRB222_misc.cfg", "linedefflags");
-	}
-
-	// Linedef flags UDMF translation table
-	// This is needed for copy/paste and prefabs to work properly
-	// When the UDMF field name is prefixed with ! it is inverted
-	linedefflagstranslation
-	{
-		include("SRB222_misc.cfg", "linedefflagstranslation");
-	}
-
-	// LINEDEF ACTIVATIONS
-	linedefactivations
-	{
-	}
-
-	// LINEDEF TYPES
-	linedeftypes
-	{
-		include("SRB222_linedefs.cfg", "doom");
-	}
-
-	// THING FLAGS
-	thingflags
-	{
-		include("SRB222_misc.cfg", "thingflags");
-	}
-
-	// Thing flags UDMF translation table
-	// This is needed for copy/paste and prefabs to work properly
-	// When the UDMF field name is prefixed with ! it is inverted
-	thingflagstranslation
-	{
-		include("SRB222_misc.cfg", "thingflagstranslation");
-	}
-
-	// THING FLAGS ERROR MASK
-	// Mask for the thing flags which indicates the options
-	// that make the same thing appear in the same modes
-	thingflagsmask1 = 7;	// 1 + 2 + 4
-	thingflagsmask2 = 0;
-
-	// THING TYPES
-	thingtypes
-	{
-		include("SRB222_things.cfg", "doom");
-	}
-}
-
 mapformat_udmf
 {
 	// The format interface handles the map data format
@@ -222,9 +93,17 @@ mapformat_udmf
 	{
 		include("SRB222_misc.cfg", "universalfields");
 	}
+	
+	// Disable Doom-related modes that don't make sense for SRB2
+	soundsupport = false;
+	automapsupport = false;
 
 	// When this is set to true, sectors with the same tag will light up when a line is highlighted
 	linetagindicatesectors = false;
+	localsidedeftextureoffsets = true;
+	distinctfloorandceilingbrightness = true;
+	
+	planeequationsupport = true;
 
 	// Special linedefs
 	include("SRB222_misc.cfg", "speciallinedefs_udmf");
@@ -240,6 +119,11 @@ mapformat_udmf
 		include("SRB222_misc.cfg", "sectorflags");
 	}
 
+	sectorflagscategories
+	{
+		include("SRB222_misc.cfg", "sectorflagscategories");
+	}
+
 	// DEFAULT SECTOR BRIGHTNESS LEVELS
 	sectorbrightness
 	{
@@ -247,6 +131,7 @@ mapformat_udmf
 	}
 
 	damagetypes = "Generic Water Fire Lava Electric Spike DeathPitTilt DeathPitNoTilt Instakill SpecialStage";
+	triggerertypes = "Player AllPlayers Mobj";
 
 	// LINEDEF FLAGS
 	linedefflags
@@ -282,7 +167,6 @@ mapformat_udmf
 	// How to compare thing flags (for the stuck things error checker)
 	thingflagscompare
 	{
-		include("UDMF_misc.cfg", "thingflagscompare");
 	}
 
 	// THING TYPES
diff --git a/extras/conf/udb/Includes/SRB222_linedefs.cfg b/extras/conf/udb/Includes/SRB222_linedefs.cfg
index b526e84e7bfff6eb77fa3161bbca16df18b8d393..68077862392a0ca2668d296f051203cd250e93ed 100644
--- a/extras/conf/udb/Includes/SRB222_linedefs.cfg
+++ b/extras/conf/udb/Includes/SRB222_linedefs.cfg
@@ -1,1793 +1,3 @@
-doom
-{
-	misc
-	{
-		title = "Miscellaneous";
-
-		0
-		{
-			title = "None";
-			prefix = "(0)";
-		}
-		1
-		{
-			title = "Per-Sector Gravity";
-			prefix = "(1)";
-		}
-		5
-		{
-			title = "Camera Scanner <deprecated>";
-			prefix = "(5)";
-		}
-		7
-		{
-			title = "Sector Flat Alignment";
-			prefix = "(7)";
-		}
-		10
-		{
-			title = "Culling Plane";
-			prefix = "(10)";
-		}
-		13
-		{
-			title = "Heat Wave Effect";
-			prefix = "(13)";
-		}
-		40
-		{
-			title = "Visual Portal Between Tagged Linedefs";
-			prefix = "(40)";
-		}
-		41
-		{
-			title = "Horizon Effect";
-			prefix = "(41)";
-		}
-		50
-		{
-			title = "Instantly Lower Floor on Level Load";
-			prefix = "(50)";
-		}
-		51
-		{
-			title = "Instantly Raise Ceiling on Level Load";
-			prefix = "(51)";
-		}
-		63
-		{
-			title = "Fake Floor/Ceiling Planes";
-			prefix = "(63)";
-		}
-		540
-		{
-			title = "Floor Friction";
-			prefix = "(540)";
-		}
-	}
-
-	parameters
-	{
-		title = "Parameters";
-
-		2
-		{
-			title = "Custom Exit";
-			prefix = "(2)";
-		}
-		3
-		{
-			title = "Zoom Tube Parameters";
-			prefix = "(3)";
-		}
-		4
-		{
-			title = "Speed Pad Parameters";
-			prefix = "(4)";
-		}
-		8
-		{
-			title = "Special Sector Properties";
-			prefix = "(8)";
-		}
-		9
-		{
-			title = "Chain Parameters";
-			prefix = "(9)";
-		}
-		11
-		{
-			title = "Rope Hang Parameters";
-			prefix = "(11)";
-		}
-		12
-		{
-			title = "Rock Spawner Parameters";
-			prefix = "(12)";
-		}
-		14
-		{
-			title = "Bustable Block Parameters";
-			prefix = "(14)";
-		}
-		15
-		{
-			title = "Fan Particle Spawner Parameters";
-			prefix = "(15)";
-		}
-		16
-		{
-			title = "Minecart Parameters";
-			prefix = "(16)";
-		}
-		64
-		{
-			title = "Continuously Appearing/Disappearing FOF";
-			prefix = "(64)";
-		}
-		76
-		{
-			title = "Make FOF Bouncy";
-			prefix = "(76)";
-		}
-	}
-
-	polyobject
-	{
-		title = "PolyObject";
-
-		20
-		{
-			title = "First Line";
-			prefix = "(20)";
-		}
-		21
-		{
-			title = "Explicitly Include Line <disabled>";
-			prefix = "(21)";
-		}
-		22
-		{
-			title = "Parameters";
-			prefix = "(22)";
-		}
-		30
-		{
-			title = "Waving Flag";
-			prefix = "(30)";
-		}
-		31
-		{
-			title = "Displacement by Front Sector";
-			prefix = "(31)";
-		}
-		32
-		{
-			title = "Angular Displacement by Front Sector";
-			prefix = "(32)";
-		}
-	}
-
-	planemove
-	{
-		title = "Plane Movement";
-
-		52
-		{
-			title = "Continuously Falling Sector";
-			prefix = "(52)";
-		}
-		53
-		{
-			title = "Continuous Floor/Ceiling Mover";
-			prefix = "(53)";
-		}
-		54
-		{
-			title = "Continuous Floor Mover";
-			prefix = "(54)";
-		}
-		55
-		{
-			title = "Continuous Ceiling Mover";
-			prefix = "(55)";
-		}
-		56
-		{
-			title = "Continuous Two-Speed Floor/Ceiling Mover";
-			prefix = "(56)";
-		}
-		57
-		{
-			title = "Continuous Two-Speed Floor Mover";
-			prefix = "(57)";
-		}
-		58
-		{
-			title = "Continuous Two-Speed Ceiling Mover";
-			prefix = "(58)";
-		}
-		59
-		{
-			title = "Activate Moving Platform";
-			prefix = "(59)";
-		}
-		60
-		{
-			title = "Activate Moving Platform (Adjustable Speed)";
-			prefix = "(60)";
-		}
-		61
-		{
-			title = "Crusher (Ceiling to Floor)";
-			prefix = "(61)";
-		}
-		62
-		{
-			title = "Crusher (Floor to Ceiling)";
-			prefix = "(62)";
-		}
-		66
-		{
-			title = "Move Floor by Displacement";
-			prefix = "(66)";
-		}
-		67
-		{
-			title = "Move Ceiling by Displacement";
-			prefix = "(67)";
-		}
-		68
-		{
-			title = "Move Floor and Ceiling by Displacement";
-			prefix = "(68)";
-		}
-	}
-
-	fofsolid
-	{
-		title = "FOF (solid)";
-
-		100
-		{
-			title = "Solid, Opaque";
-			prefix = "(100)";
-		}
-		101
-		{
-			title = "Solid, Opaque, No Shadow";
-			prefix = "(101)";
-		}
-		102
-		{
-			title = "Solid, Translucent";
-			prefix = "(102)";
-		}
-		103
-		{
-			title = "Solid, Sides Only";
-			prefix = "(103)";
-		}
-		104
-		{
-			title = "Solid, No Sides";
-			prefix = "(104)";
-		}
-		105
-		{
-			title = "Solid, Invisible";
-			prefix = "(105)";
-		}
-		140
-		{
-			title = "Intangible from Bottom, Opaque";
-			prefix = "(140)";
-		}
-		141
-		{
-			title = "Intangible from Bottom, Translucent";
-			prefix = "(141)";
-		}
-		142
-		{
-			title = "Intangible from Bottom, Translucent, No Sides";
-			prefix = "(142)";
-		}
-		143
-		{
-			title = "Intangible from Top, Opaque";
-			prefix = "(143)";
-		}
-		144
-		{
-			title = "Intangible from Top, Translucent";
-			prefix = "(144)";
-		}
-		145
-		{
-			title = "Intangible from Top, Translucent, No Sides";
-			prefix = "(145)";
-		}
-		146
-		{
-			title = "Only Tangible from Sides";
-			prefix = "(146)";
-		}
-	}
-
-	fofintangible
-	{
-		title = "FOF (intangible)";
-
-		120
-		{
-			title = "Water, Opaque";
-			prefix = "(120)";
-		}
-		121
-		{
-			title = "Water, Translucent";
-			prefix = "(121)";
-		}
-		122
-		{
-			title = "Water, Opaque, No Sides";
-			prefix = "(122)";
-		}
-		123
-		{
-			title = "Water, Translucent, No Sides";
-			prefix = "(123)";
-		}
-		124
-		{
-			title = "Goo Water, Translucent";
-			prefix = "(124)";
-		}
-		125
-		{
-			title = "Goo Water, Translucent, No Sides";
-			prefix = "(125)";
-		}
-		220
-		{
-			title = "Intangible, Opaque";
-			prefix = "(220)";
-		}
-		221
-		{
-			title = "Intangible, Translucent";
-			prefix = "(221)";
-		}
-		222
-		{
-			title = "Intangible, Sides Only";
-			prefix = "(222)";
-		}
-		223
-		{
-			title = "Intangible, Invisible";
-			prefix = "(223)";
-		}
-	}
-
-	fofmoving
-	{
-		title = "FOF (moving)";
-
-		150
-		{
-			title = "Air Bobbing";
-			prefix = "(150)";
-		}
-		151
-		{
-			title = "Air Bobbing (Adjustable)";
-			prefix = "(151)";
-		}
-		152
-		{
-			title = "Reverse Air Bobbing (Adjustable)";
-			prefix = "(152)";
-		}
-		153
-		{
-			title = "Dynamically Sinking Platform";
-			prefix = "(153)";
-		}
-		160
-		{
-			title = "Water Bobbing";
-			prefix = "(160)";
-		}
-		190
-		{
-			title = "Rising Platform, Solid, Opaque";
-			prefix = "(190)";
-		}
-		191
-		{
-			title = "Rising Platform, Solid, Opaque, No Shadow";
-			prefix = "(191)";
-		}
-		192
-		{
-			title = "Rising Platform, Solid, Translucent";
-			prefix = "(192)";
-		}
-		193
-		{
-			title = "Rising Platform, Solid, Invisible";
-			prefix = "(193)";
-		}
-		194
-		{
-			title = "Rising Platform, Intangible from Bottom, Opaque";
-			prefix = "(194)";
-		}
-		195
-		{
-			title = "Rising Platform, Intangible from Bottom, Translucent";
-			prefix = "(195)";
-		}
-	}
-
-	fofcrumbling
-	{
-		title = "FOF (crumbling)";
-
-		170
-		{
-			title = "Crumbling, Respawn";
-			prefix = "(170)";
-		}
-		171
-		{
-			title = "Crumbling, No Respawn";
-			prefix = "(171)";
-		}
-		172
-		{
-			title = "Crumbling, Respawn, Intangible from Bottom";
-			prefix = "(172)";
-		}
-		173
-		{
-			title = "Crumbling, No Respawn, Intangible from Bottom";
-			prefix = "(173)";
-		}
-		174
-		{
-			title = "Crumbling, Respawn, Int. from Bottom, Translucent";
-			prefix = "(174)";
-		}
-		175
-		{
-			title = "Crumbling, No Respawn, Int. from Bottom, Translucent";
-			prefix = "(175)";
-		}
-		176
-		{
-			title = "Crumbling, Respawn, Floating, Bobbing";
-			prefix = "(176)";
-		}
-		177
-		{
-			title = "Crumbling, No Respawn, Floating, Bobbing";
-			prefix = "(177)";
-		}
-		178
-		{
-			title = "Crumbling, Respawn, Floating";
-			prefix = "(178)";
-		}
-		179
-		{
-			title = "Crumbling, No Respawn, Floating";
-			prefix = "(179)";
-		}
-		180
-		{
-			title = "Crumbling, Respawn, Air Bobbing";
-			prefix = "(180)";
-		}
-	}
-
-	fofspecial
-	{
-		title = "FOF (special)";
-
-		200
-		{
-			title = "Light Block";
-			prefix = "(200)";
-		}
-		201
-		{
-			title = "Half Light Block";
-			prefix = "(201)";
-		}
-		202
-		{
-			title = "Fog Block";
-			prefix = "(202)";
-		}
-		250
-		{
-			title = "Mario Block";
-			prefix = "(250)";
-		}
-		251
-		{
-			title = "Thwomp Block";
-			prefix = "(251)";
-		}
-		252
-		{
-			title = "Shatter Block";
-			prefix = "(252)";
-		}
-		253
-		{
-			title = "Shatter Block, Translucent";
-			prefix = "(253)";
-		}
-		254
-		{
-			title = "Bustable Block";
-			prefix = "(254)";
-		}
-		255
-		{
-			title = "Spin-Bustable Block";
-			prefix = "(255)";
-		}
-		256
-		{
-			title = "Spin-Bustable Block, Translucent";
-			prefix = "(256)";
-		}
-		257
-		{
-			title = "Quicksand";
-			prefix = "(257)";
-		}
-		258
-		{
-			title = "Laser";
-			prefix = "(258)";
-		}
-		259
-		{
-			title = "Custom FOF";
-			prefix = "(259)";
-		}
-	}
-
-	linedeftrigger
-	{
-		title = "Linedef Executor Trigger";
-
-		300
-		{
-			title = "Continuous";
-			prefix = "(300)";
-		}
-		301
-		{
-			title = "Each Time";
-			prefix = "(301)";
-		}
-		302
-		{
-			title = "Once";
-			prefix = "(302)";
-		}
-		303
-		{
-			title = "Ring Count - Continuous";
-			prefix = "(303)";
-		}
-		304
-		{
-			title = "Ring Count - Once";
-			prefix = "(304)";
-		}
-		305
-		{
-			title = "Character Ability - Continuous";
-			prefix = "(305)";
-		}
-		306
-		{
-			title = "Character Ability - Each Time";
-			prefix = "(306)";
-		}
-		307
-		{
-			title = "Character Ability - Once";
-			prefix = "(307)";
-		}
-		308
-		{
-			title = "Race Only - Once";
-			prefix = "(308)";
-		}
-		309
-		{
-			title = "CTF Red Team - Continuous";
-			prefix = "(309)";
-		}
-		310
-		{
-			title = "CTF Red Team - Each Time";
-			prefix = "(310)";
-		}
-		311
-		{
-			title = "CTF Blue Team - Continuous";
-			prefix = "(311)";
-		}
-		312
-		{
-			title = "CTF Blue Team - Each Time";
-			prefix = "(312)";
-		}
-		313
-		{
-			title = "No More Enemies - Once";
-			prefix = "(313)";
-		}
-		314
-		{
-			title = "Number of Pushables - Continuous";
-			prefix = "(314)";
-		}
-		315
-		{
-			title = "Number of Pushables - Once";
-			prefix = "(315)";
-		}
-		317
-		{
-			title = "Condition Set Trigger - Continuous";
-			prefix = "(317)";
-		}
-		318
-		{
-			title = "Condition Set Trigger - Once";
-			prefix = "(318)";
-		}
-		319
-		{
-			title = "Unlockable - Continuous";
-			prefix = "(319)";
-		}
-		320
-		{
-			title = "Unlockable - Once";
-			prefix = "(320)";
-		}
-		321
-		{
-			title = "Trigger After X Calls - Continuous";
-			prefix = "(321)";
-		}
-		322
-		{
-			title = "Trigger After X Calls - Each Time";
-			prefix = "(322)";
-		}
-		323
-		{
-			title = "NiGHTSerize - Each Time";
-			prefix = "(323)";
-		}
-		324
-		{
-			title = "NiGHTSerize - Once";
-			prefix = "(324)";
-		}
-		325
-		{
-			title = "De-NiGHTSerize - Each Time";
-			prefix = "(325)";
-		}
-		326
-		{
-			title = "De-NiGHTSerize - Once";
-			prefix = "(326)";
-		}
-		327
-		{
-			title = "NiGHTS Lap - Each Time";
-			prefix = "(327)";
-		}
-		328
-		{
-			title = "NiGHTS Lap - Once";
-			prefix = "(328)";
-		}
-		329
-		{
-			title = "Ideya Capture Touch - Each Time";
-			prefix = "(329)";
-		}
-		330
-		{
-			title = "Ideya Capture Touch - Once";
-			prefix = "(330)";
-		}
-		331
-		{
-			title = "Player Skin - Continuous";
-			flags64text = "[6] Disable for this skin";
-			prefix = "(331)";
-		}
-		332
-		{
-			title = "Player Skin - Each Time";
-			prefix = "(332)";
-		}
-		333
-		{
-			title = "Player Skin - Once";
-			prefix = "(333)";
-		}
-		334
-		{
-			title = "Object Dye - Continuous";
-			prefix = "(334)";
-		}
-		335
-		{
-			title = "Object Dye - Each Time";
-			prefix = "(335)";
-		}
-		336
-		{
-			title = "Object Dye - Once";
-			prefix = "(336)";
-		}
-		337
-		{
-			title = "Emerald Check - Continuous";
-			prefix = "(337)";
-		}
-		338
-		{
-			title = "Emerald Check - Each Time";
-			prefix = "(338)";
-		}
-		339
-		{
-			title = "Emerald Check - Once";
-			prefix = "(339)";
-		}
-		340
-		{
-			title = "NiGHTS Mare - Continuous";
-			prefix = "(340)";
-		}
-		341
-		{
-			title = "NiGHTS Mare - Each Time";
-			prefix = "(341)";
-		}
-		342
-		{
-			title = "NiGHTS Mare - Once";
-			prefix = "(342)";
-		}
-		343
-		{
-			title = "Gravity Check - Continuous";
-			prefix = "(343)";
-		}
-		344
-		{
-			title = "Gravity Check - Each Time";
-			prefix = "(344)";
-		}
-		345
-		{
-			title = "Gravity Check - Once";
-			prefix = "(345)";
-		}
-		399
-		{
-			title = "Level Load";
-			prefix = "(399)";
-		}
-	}
-
-	linedefexecsector
-	{
-		title = "Linedef Executor (sector)";
-
-		400
-		{
-			title = "Set Tagged Sector's Floor Height/Texture";
-			prefix = "(400)";
-		}
-		401
-		{
-			title = "Set Tagged Sector's Ceiling Height/Texture";
-			prefix = "(401)";
-		}
-		402
-		{
-			title = "Copy Light Level to Tagged Sectors";
-			prefix = "(402)";
-		}
-		408
-		{
-			title = "Set Tagged Sector's Flats";
-			prefix = "(408)";
-		}
-		409
-		{
-			title = "Change Tagged Sector's Tag";
-			prefix = "(409)";
-		}
-		410
-		{
-			title = "Change Front Sector's Tag";
-			prefix = "(410)";
-		}
-		416
-		{
-			title = "Start Adjustable Flickering Light";
-			prefix = "(416)";
-		}
-		417
-		{
-			title = "Start Adjustable Pulsating Light";
-			prefix = "(417)";
-		}
-		418
-		{
-			title = "Start Adjustable Blinking Light (unsynchronized)";
-			prefix = "(418)";
-		}
-		419
-		{
-			title = "Start Adjustable Blinking Light (synchronized)";
-			prefix = "(419)";
-		}
-		420
-		{
-			title = "Fade Light Level";
-			prefix = "(420)";
-		}
-		421
-		{
-			title = "Stop Lighting Effect";
-			prefix = "(421)";
-		}
-		435
-		{
-			title = "Change Plane Scroller Direction";
-			prefix = "(435)";
-		}
-		467
-		{
-			title = "Set Tagged Sector's Light Level";
-			prefix = "(467)";
-		}
-	}
-
-	linedefexecplane
-	{
-		title = "Linedef Executor (plane movement)";
-
-		403
-		{
-			title = "Move Tagged Sector's Floor";
-			prefix = "(403)";
-		}
-		404
-		{
-			title = "Move Tagged Sector's Ceiling";
-			prefix = "(404)";
-		}
-		405
-		{
-			title = "Move Floor According to Front Texture Offsets";
-			prefix = "(405)";
-		}
-		407
-		{
-			title = "Move Ceiling According to Front Texture Offsets";
-			prefix = "(407)";
-		}
-		411
-		{
-			title = "Stop Plane Movement";
-			prefix = "(411)";
-		}
-		428
-		{
-			title = "Start Platform Movement";
-			prefix = "(428)";
-		}
-		429
-		{
-			title = "Crush Ceiling Once";
-			prefix = "(429)";
-		}
-		430
-		{
-			title = "Crush Floor Once";
-			prefix = "(430)";
-		}
-		431
-		{
-			title = "Crush Floor and Ceiling Once";
-			prefix = "(431)";
-		}
-	}
-
-	linedefexecplayer
-	{
-		title = "Linedef Executor (player/object)";
-
-		412
-		{
-			title = "Teleporter";
-			prefix = "(412)";
-		}
-		425
-		{
-			title = "Change Object State";
-			prefix = "(425)";
-		}
-		426
-		{
-			title = "Stop Object";
-			prefix = "(426)";
-		}
-		427
-		{
-			title = "Award Score";
-			prefix = "(427)";
-		}
-		432
-		{
-			title = "Enable/Disable 2D Mode";
-			prefix = "(432)";
-		}
-		433
-		{
-			title = "Enable/Disable Gravity Flip";
-			prefix = "(433)";
-		}
-		434
-		{
-			title = "Award Power-Up";
-			prefix = "(434)";
-		}
-		437
-		{
-			title = "Disable Player Control";
-			prefix = "(437)";
-		}
-		438
-		{
-			title = "Change Object Size";
-			prefix = "(438)";
-		}
-		442
-		{
-			title = "Change Object Type State";
-			prefix = "(442)";
-		}
-		457
-		{
-			title = "Track Object's Angle";
-			prefix = "(457)";
-		}
-		458
-		{
-			title = "Stop Tracking Object's Angle";
-			prefix = "(458)";
-		}
-		460
-		{
-			title = "Award Rings";
-			prefix = "(460)";
-		}
-		461
-		{
-			title = "Spawn Object";
-			prefix = "(461)";
-		}
-		462
-		{
-			title = "Stop Timer/Exit Stage in Record Attack";
-			prefix = "(462)";
-		}
-		463
-		{
-			title = "Dye Object";
-			prefix = "(463)";
-		}
-		464
-		{
-			title = "Trigger Egg Capsule";
-			prefix = "(464)";
-		}
-		466
-		{
-			title = "Set Level Failure State";
-			prefix = "(466)";
-		}
-	}
-
-	linedefexecmisc
-	{
-		title = "Linedef Executor (misc.)";
-
-		413
-		{
-			title = "Change Music";
-			prefix = "(413)";
-		}
-		414
-		{
-			title = "Play Sound Effect";
-			prefix = "(414)";
-		}
-		415
-		{
-			title = "Run Script";
-			prefix = "(415)";
-		}
-		422
-		{
-			title = "Switch to Cut-Away View";
-			prefix = "(422)";
-		}
-		423
-		{
-			title = "Change Sky";
-			prefix = "(423)";
-		}
-		424
-		{
-			title = "Change Weather";
-			prefix = "(424)";
-		}
-		436
-		{
-			title = "Shatter FOF";
-			prefix = "(436)";
-		}
-		439
-		{
-			title = "Change Tagged Linedef's Textures";
-			prefix = "(439)";
-		}
-		440
-		{
-			title = "Start Metal Sonic Race";
-			prefix = "(440)";
-		}
-		441
-		{
-			title = "Condition Set Trigger";
-			prefix = "(441)";
-		}
-		443
-		{
-			title = "Call Lua Function";
-			prefix = "(443)";
-		}
-		444
-		{
-			title = "Earthquake";
-			prefix = "(444)";
-		}
-		445
-		{
-			title = "Make FOF Disappear/Reappear";
-			prefix = "(445)";
-		}
-		446
-		{
-			title = "Make FOF Crumble";
-			prefix = "(446)";
-		}
-		447
-		{
-			title = "Change Tagged Sector's Colormap";
-			prefix = "(447)";
-		}
-		448
-		{
-			title = "Change Skybox";
-			prefix = "(448)";
-		}
-		449
-		{
-			title = "Enable Bosses with Parameter";
-			prefix = "(449)";
-		}
-		450
-		{
-			title = "Execute Linedef Executor (specific tag)";
-			prefix = "(450)";
-		}
-		451
-		{
-			title = "Execute Linedef Executor (random tag in range)";
-			prefix = "(451)";
-		}
-		452
-		{
-			title = "Set FOF Translucency";
-			prefix = "(452)";
-		}
-		453
-		{
-			title = "Fade FOF";
-			prefix = "(453)";
-		}
-		454
-		{
-			title = "Stop Fading FOF";
-			prefix = "(454)";
-		}
-		455
-		{
-			title = "Fade Tagged Sector's Colormap";
-			prefix = "(455)";
-		}
-		456
-		{
-			title = "Stop Fading Tagged Sector's Colormap";
-			prefix = "(456)";
-		}
-		459
-		{
-			title = "Control Text Prompt";
-			prefix = "(459)";
-		}
-	}
-
-	linedefexecpoly
-	{
-		title = "Linedef Executor (polyobject)";
-
-		480
-		{
-			title = "Door Slide";
-			prefix = "(480)";
-		}
-		481
-		{
-			title = "Door Swing";
-			prefix = "(481)";
-		}
-		482
-		{
-			title = "Move";
-			prefix = "(482)";
-		}
-		483
-		{
-			title = "Move, Override";
-			prefix = "(483)";
-		}
-		484
-		{
-			title = "Rotate Right";
-			prefix = "(484)";
-		}
-		485
-		{
-			title = "Rotate Right, Override";
-			prefix = "(485)";
-		}
-		486
-		{
-			title = "Rotate Left";
-			prefix = "(486)";
-		}
-		487
-		{
-			title = "Rotate Left, Override";
-			prefix = "(487)";
-		}
-		488
-		{
-			title = "Move by Waypoints";
-			prefix = "(488)";
-		}
-		489
-		{
-			title = "Turn Invisible, Intangible";
-			prefix = "(489)";
-		}
-		490
-		{
-			title = "Turn Visible, Tangible";
-			prefix = "(490)";
-		}
-		491
-		{
-			title = "Set Translucency";
-			prefix = "(491)";
-		}
-		492
-		{
-			title = "Fade Translucency";
-			prefix = "(492)";
-		}
-	}
-
-	wallscroll
-	{
-		title = "Wall Scrolling";
-
-		500
-		{
-			title = "Scroll Wall Front Side Left";
-			prefix = "(500)";
-		}
-		501
-		{
-			title = "Scroll Wall Front Side Right";
-			prefix = "(501)";
-		}
-		502
-		{
-			title = "Scroll Wall According to Linedef";
-			prefix = "(502)";
-		}
-		503
-		{
-			title = "Scroll Wall According to Linedef (Accelerative)";
-			prefix = "(503)";
-		}
-		504
-		{
-			title = "Scroll Wall According to Linedef (Displacement)";
-			prefix = "(504)";
-		}
-		505
-		{
-			title = "Scroll Texture by Front Side Offsets";
-			prefix = "(505)";
-		}
-		506
-		{
-			title = "Scroll Texture by Back Side Offsets";
-			prefix = "(506)";
-		}
-	}
-
-	planescroll
-	{
-		title = "Plane Scrolling";
-
-		510
-		{
-			title = "Scroll Floor Texture";
-			prefix = "(510)";
-		}
-		511
-		{
-			title = "Scroll Floor Texture (Accelerative)";
-			prefix = "(511)";
-		}
-		512
-		{
-			title = "Scroll Floor Texture (Displacement)";
-			prefix = "(512)";
-		}
-		513
-		{
-			title = "Scroll Ceiling Texture";
-			prefix = "(513)";
-		}
-		514
-		{
-			title = "Scroll Ceiling Texture (Accelerative)";
-			prefix = "(514)";
-		}
-		515
-		{
-			title = "Scroll Ceiling Texture (Displacement)";
-			prefix = "(515)";
-		}
-		516
-		{
-			title = "Scroll Floor and Ceiling Texture";
-			prefix = "(516)";
-		}
-		517
-		{
-			title = "Scroll Floor and Ceiling Texture (Accelerative)";
-			prefix = "(517)";
-		}
-		518
-		{
-			title = "Scroll Floor and Ceiling Texture (Displacement)";
-			prefix = "(518)";
-		}
-		520
-		{
-			title = "Carry Objects on Floor";
-			prefix = "(520)";
-		}
-		521
-		{
-			title = "Carry Objects on Floor (Accelerative)";
-			prefix = "(521)";
-		}
-		522
-		{
-			title = "Carry Objects on Floor (Displacement)";
-			prefix = "(522)";
-		}
-		523
-		{
-			title = "Carry Objects on Ceiling";
-			prefix = "(523)";
-		}
-		524
-		{
-			title = "Carry Objects on Ceiling (Accelerative)";
-			prefix = "(524)";
-		}
-		525
-		{
-			title = "Carry Objects on Ceiling (Displacement)";
-			prefix = "(525)";
-		}
-		526
-		{
-			title = "Carry Objects on Floor and Ceiling";
-			prefix = "(526)";
-		}
-		527
-		{
-			title = "Carry Objects on Floor and Ceiling (Accelerative)";
-			prefix = "(527)";
-		}
-		528
-		{
-			title = "Carry Objects on Floor and Ceiling (Displacement)";
-			prefix = "(528)";
-		}
-		530
-		{
-			title = "Scroll Floor Texture and Carry Objects";
-			prefix = "(530)";
-		}
-		531
-		{
-			title = "Scroll Floor Texture and Carry Objects (Accelerative)";
-			prefix = "(531)";
-		}
-		532
-		{
-			title = "Scroll Floor Texture and Carry Objects (Displacement)";
-			prefix = "(532)";
-		}
-		533
-		{
-			title = "Scroll Ceiling Texture and Carry Objects";
-			prefix = "(533)";
-		}
-		534
-		{
-			title = "Scroll Ceiling Texture and Carry Objects (Accelerative)";
-			prefix = "(534)";
-		}
-		535
-		{
-			title = "Scroll Ceiling Texture and Carry Objects (Displacement)";
-			prefix = "(535)";
-		}
-		536
-		{
-			title = "Scroll Floor and Ceiling Texture and Carry Objects";
-			prefix = "(536)";
-		}
-		537
-		{
-			title = "Scroll Floor and Ceiling Texture and Carry Objects (Accelerative)";
-			prefix = "(537)";
-		}
-		538
-		{
-			title = "Scroll Floor and Ceiling Texture and Carry Objects (Displacement)";
-			prefix = "(538)";
-		}
-	}
-
-	pusher
-	{
-		title = "Pusher";
-
-		541
-		{
-			title = "Wind";
-			prefix = "(541)";
-		}
-		542
-		{
-			title = "Upwards Wind";
-			prefix = "(542)";
-		}
-		543
-		{
-			title = "Downwards Wind";
-			prefix = "(543)";
-		}
-		544
-		{
-			title = "Current";
-			prefix = "(544)";
-		}
-		545
-		{
-			title = "Upwards Current";
-			prefix = "(545)";
-		}
-		546
-		{
-			title = "Downwards Current";
-			prefix = "(546)";
-		}
-		547
-		{
-			title = "Push/Pull";
-			prefix = "(547)";
-		}
-	}
-
-	light
-	{
-		title = "Lighting";
-
-		600
-		{
-			title = "Floor Lighting";
-			prefix = "(600)";
-		}
-		601
-		{
-			title = "Ceiling Lighting";
-			prefix = "(601)";
-		}
-		602
-		{
-			title = "Adjustable Pulsating Light";
-			prefix = "(602)";
-		}
-		603
-		{
-			title = "Adjustable Flickering Light";
-			prefix = "(603)";
-		}
-		604
-		{
-			title = "Adjustable Blinking Light (unsynchronized)";
-			prefix = "(604)";
-		}
-		605
-		{
-			title = "Adjustable Blinking Light (synchronized)";
-			prefix = "(605)";
-		}
-		606
-		{
-			title = "Colormap";
-			prefix = "(606)";
-		}
-	}
-
-	slope
-	{
-		title = "Slope";
-
-		700
-		{
-			title = "Slope Frontside Floor";
-			prefix = "(700)";
-		}
-		701
-		{
-			title = "Slope Frontside Ceiling";
-			prefix = "(701)";
-		}
-		702
-		{
-			title = "Slope Frontside Floor and Ceiling";
-			prefix = "(702)";
-		}
-		703
-		{
-			title = "Slope Frontside Floor and Backside Ceiling";
-			prefix = "(703)";
-´		}
-		704
-		{
-			title = "Slope Frontside Floor by 3 Tagged Vertex Things";
-			prefix = "(704)";
-		}
-		705
-		{
-			title = "Slope Frontside Ceiling by 3 Tagged Vertex Things";
-			prefix = "(705)";
-		}
-		710
-		{
-			title = "Slope Backside Floor";
-			prefix = "(710)";
-		}
-		711
-		{
-			title = "Slope Backside Ceiling";
-			prefix = "(711)";
-		}
-		712
-		{
-			title = "Slope Backside Floor and Ceiling";
-			prefix = "(712)";
-		}
-		713
-		{
-			title = "Slope Backside Floor and Frontside Ceiling";
-			prefix = "(713)";
-		}
-		714
-		{
-			title = "Slope Backside Floor by 3 Tagged Vertex Things";
-			prefix = "(714)";
-		}
-		715
-		{
-			title = "Slope Backside Ceiling by 3 Tagged Vertex Things";
-			prefix = "(715)";
-		}
-		720
-		{
-			title = "Copy Frontside Floor Slope from Line Tag";
-			prefix = "(720)";
-		}
-		721
-		{
-			title = "Copy Frontside Ceiling Slope from Line Tag";
-			prefix = "(721)";
-		}
-		722
-		{
-			title = "Copy Frontside Floor and Ceiling Slope from Line Tag";
-			prefix = "(722)";
-		}
-		799
-		{
-			title = "Set Tagged Dynamic Slope Vertex to Front Sector Height";
-			prefix = "(799)";
-		}
-	}
-
-	transwall
-	{
-		title = "Translucent Wall";
-
-		900
-		{
-			title = "90% Opaque";
-			prefix = "(900)";
-		}
-		901
-		{
-			title = "80% Opaque";
-			prefix = "(901)";
-		}
-		902
-		{
-			title = "70% Opaque";
-			prefix = "(902)";
-		}
-		903
-		{
-			title = "60% Opaque";
-			prefix = "(903)";
-		}
-		904
-		{
-			title = "50% Opaque";
-			prefix = "(904)";
-		}
-		905
-		{
-			title = "40% Opaque";
-			prefix = "(905)";
-		}
-		906
-		{
-			title = "30% Opaque";
-			prefix = "(906)";
-		}
-		907
-		{
-			title = "20% Opaque";
-			prefix = "(907)";
-		}
-		908
-		{
-			title = "10% Opaque";
-			prefix = "(908)";
-		}
-		909
-		{
-			title = "Fog Wall";
-			prefix = "(909)";
-		}
-		910
-		{
-			title = "100% Additive";
-			prefix = "(910)";
-		}
-		911
-		{
-			title = "90% Additive";
-			prefix = "(911)";
-		}
-		912
-		{
-			title = "80% Additive";
-			prefix = "(912)";
-		}
-		913
-		{
-			title = "70% Additive";
-			prefix = "(913)";
-		}
-		914
-		{
-			title = "60% Additive";
-			prefix = "(914)";
-		}
-		915
-		{
-			title = "50% Additive";
-			prefix = "(915)";
-		}
-		916
-		{
-			title = "40% Additive";
-			prefix = "(916)";
-		}
-		917
-		{
-			title = "30% Additive";
-			prefix = "(917)";
-		}
-		918
-		{
-			title = "20% Additive";
-			prefix = "(918)";
-		}
-		919
-		{
-			title = "10% Additive";
-			prefix = "(919)";
-		}
-		920
-		{
-			title = "100% Subtractive";
-			prefix = "(920)";
-		}
-		921
-		{
-			title = "90% Subtractive";
-			prefix = "(921)";
-		}
-		922
-		{
-			title = "80% Subtractive";
-			prefix = "(922)";
-		}
-		923
-		{
-			title = "70% Subtractive";
-			prefix = "(923)";
-		}
-		924
-		{
-			title = "60% Subtractive";
-			prefix = "(924)";
-		}
-		925
-		{
-			title = "50% Subtractive";
-			prefix = "(925)";
-		}
-		926
-		{
-			title = "40% Subtractive";
-			prefix = "(926)";
-		}
-		927
-		{
-			title = "30% Subtractive";
-			prefix = "(927)";
-		}
-		928
-		{
-			title = "20% Subtractive";
-			prefix = "(928)";
-		}
-		929
-		{
-			title = "10% Subtractive";
-			prefix = "(929)";
-		}
-		930
-		{
-			title = "100% Reverse Subtractive";
-			prefix = "(930)";
-		}
-		931
-		{
-			title = "90% Reverse Subtractive";
-			prefix = "(931)";
-		}
-		932
-		{
-			title = "80% Reverse Subtractive";
-			prefix = "(932)";
-		}
-		933
-		{
-			title = "70% Reverse Subtractive";
-			prefix = "(933)";
-		}
-		934
-		{
-			title = "60% Reverse Subtractive";
-			prefix = "(934)";
-		}
-		935
-		{
-			title = "50% Reverse Subtractive";
-			prefix = "(935)";
-		}
-		936
-		{
-			title = "40% Reverse Subtractive";
-			prefix = "(936)";
-		}
-		937
-		{
-			title = "30% Reverse Subtractive";
-			prefix = "(937)";
-		}
-		938
-		{
-			title = "20% Reverse Subtractive";
-			prefix = "(938)";
-		}
-		939
-		{
-			title = "10% Reverse Subtractive";
-			prefix = "(939)";
-		}
-		940
-		{
-			title = "Modulate";
-			prefix = "(940)";
-		}
-	}
-}
-
 udmf
 {
 	misc
@@ -2502,6 +712,7 @@ udmf
 		{
 			title = "Solid";
 			prefix = "(100)";
+			id = "srb2_fofsolid";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2544,6 +755,7 @@ udmf
 		{
 			title = "Water";
 			prefix = "(120)";
+			id = "srb2_fofwater";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2580,6 +792,7 @@ udmf
 		{
 			title = "Air Bobbing";
 			prefix = "(150)";
+			id = "srb2_fofsolidopaque";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2606,6 +819,7 @@ udmf
 		{
 			title = "Water Bobbing";
 			prefix = "(160)";
+			id = "srb2_fofsolidopaque";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2617,6 +831,7 @@ udmf
 		{
 			title = "Crumbling";
 			prefix = "(170)";
+			id = "srb2_fofcrumbling";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2653,11 +868,12 @@ udmf
 				}
 			}
 		}
-
+		
 		190
 		{
 			title = "Rising";
 			prefix = "(190)";
+			id = "srb2_fofsolid";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2714,6 +930,7 @@ udmf
 		{
 			title = "Light Block";
 			prefix = "(200)";
+			id = "srb2_foflight";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2731,6 +948,7 @@ udmf
 		{
 			title = "Fog Block";
 			prefix = "(202)";
+			id = "srb2_foffog";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2742,6 +960,7 @@ udmf
 		{
 			title = "Intangible";
 			prefix = "(220)";
+			id = "srb2_fofintangible";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2778,6 +997,7 @@ udmf
 		{
 			title = "Intangible, Invisible";
 			prefix = "(223)";
+			id = "srb2_fofintangibleinvisible";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2789,6 +1009,7 @@ udmf
 		{
 			title = "Mario Block";
 			prefix = "(250)";
+			id = "srb2_fofsolidopaque";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2810,6 +1031,7 @@ udmf
 		{
 			title = "Thwomp Block";
 			prefix = "(251)";
+			id = "srb2_fofsolidopaque";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2834,6 +1056,7 @@ udmf
 		{
 			title = "Bustable Block";
 			prefix = "(254)";
+			id = "srb2_fofbustable";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2885,6 +1108,7 @@ udmf
 		{
 			title = "Quicksand";
 			prefix = "(257)";
+			id = "srb2_fofsolidopaque";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2910,6 +1134,7 @@ udmf
 		{
 			title = "Laser";
 			prefix = "(258)";
+			id = "srb2_foflaser";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2942,6 +1167,7 @@ udmf
 		{
 			title = "Custom";
 			prefix = "(259)";
+			id = "srb2_fofcustom";
 			arg0
 			{
 				title = "Target sector tag";
@@ -2995,52 +1221,6 @@ udmf
 				}
 			}
 		}
-		260
-		{
-			title = "Generalized 3D Floor";
-			prefix = "(260)";
-			id = "Sector_Set3dFloor";
-			requiresactivation = false;
-
-			arg0
-			{
-				title = "Target sector tag";
-				type = 13;
-			}
-			arg1
-			{
-				title = "Type";
-				type = 26;
-				default = 1;
-				enum
-				{
-					1 = "Solid";
-					2 = "Water";
-					3 = "Intangible";
-				}
-				flags
-				{
-					4 = "Render insides";
-					16 = "Only render insides";
-				}
-			}
-			arg2
-			{
-				title = "Flags";
-				type = 12;
-				enum
-				{
-					1 = "No shadow";
-					2 = "Double shadow";
-					4 = "Fog";
-				}
-			}
-			arg3
-			{
-				title = "Alpha";
-				default = 255;
-			}
-		}
 	}
 
 	linedeftrigger
@@ -5686,6 +3866,7 @@ udmf
 		{
 			title = "Create Vertex-Based Slope";
 			prefix = "(704)";
+			id = "srb2_vertexslope";
 			arg0
 			{
 				title = "Plane";
@@ -5729,6 +3910,7 @@ udmf
 		{
 			title = "Copy Slope";
 			prefix = "(720)";
+			id = "plane_copy";
 			arg0
 			{
 				title = "Front floor tag";
diff --git a/extras/conf/udb/Includes/SRB222_misc.cfg b/extras/conf/udb/Includes/SRB222_misc.cfg
index ed0488a3ff872aa28e5262ee876ccd2b53ce1397..e274fece69ea37d4be43cd8b7560fde69e181ee5 100644
--- a/extras/conf/udb/Includes/SRB222_misc.cfg
+++ b/extras/conf/udb/Includes/SRB222_misc.cfg
@@ -1,24 +1,3 @@
-linedefflags
-{
-	1 = "[0] Impassable";
-	2 = "[1] Block Enemies";
-	4 = "[2] Double-Sided";
-	8 = "[3] Upper Unpegged";
-	16 = "[4] Lower Unpegged";
-	32 = "[5] Slope Skew (E1)";
-	64 = "[6] Not Climbable";
-	128 = "[7] No Midtexture Skew (E2)";
-	256 = "[8] Peg Midtexture (E3)";
-	512 = "[9] Solid Midtexture (E4)";
-	1024 = "[10] Repeat Midtexture (E5)";
-	2048 = "[11] Netgame Only";
-	4096 = "[12] No Netgame";
-	8192 = "[13] Effect 6";
-	16384 = "[14] Bouncy Wall";
-	32768 = "[15] Transfer Line";
-}
-
-
 // Linedef flags UDMF translation table
 // This is needed for copy/paste and prefabs to work properly
 // When the UDMF field name is prefixed with ! it is inverted
@@ -42,7 +21,6 @@ linedefflagstranslation
 	32768 = "transfer";
 }
 
-
 linedefflags_udmf
 {
 	blocking = "Impassable";
@@ -74,19 +52,13 @@ linedefrenderstyles
 
 sectorflags
 {
-	colormapfog = "Fog Planes in Colormap";
-	colormapfadesprites = "Fade Fullbright in Colormap";
-	colormapprotected = "Protected Colormap";
-	flipspecial_nofloor = "No Trigger on Floor Touch";
-	flipspecial_ceiling = "Trigger on Ceiling Touch";
-	triggerspecial_touch = "Trigger on Edge Touch";
-	triggerspecial_headbump = "Trigger on Headbump";
-	triggerline_plane = "Linedef Trigger Requires Plane Touch";
-	triggerline_mobj = "Non-Pushables Can Trigger Linedef";
 	invertprecip = "Invert Precipitation";
 	gravityflip = "Flip Objects in Reverse Gravity";
 	heatwave = "Heat Wave";
 	noclipcamera = "Intangible to the Camera";
+	colormapfog = "Fog Planes";
+	colormapfadesprites = "Fade Fullbright";
+	colormapprotected = "Protected from Tagging";
 	outerspace = "Space Countdown";
 	doublestepup = "Ramp Sector (double step-up/down)";
 	nostepdown = "Non-Ramp Sector (No step-down)";
@@ -104,23 +76,59 @@ sectorflags
 	zoomtubeend = "Zoom Tube End";
 	finishline = "Circuit Finish Line";
 	ropehang = "Rope Hang";
+	jumpflip = "Flip Gravity on Jump";
+	gravityoverride = "Make Reverse Gravity Temporary";
+	flipspecial_nofloor = "No Trigger on Floor Touch";
+	flipspecial_ceiling = "Trigger on Ceiling Touch";
+	triggerspecial_touch = "Trigger on Edge Touch";
+	triggerspecial_headbump = "Trigger on Headbump";
+	triggerline_plane = "Linedef Trigger Requires Plane Touch";
+	triggerline_mobj = "Non-Pushables Can Trigger Linedef";
 }
 
-thingflags
+sectorflagscategories
 {
-	1 = "[1] Extra";
-	2 = "[2] Flip";
-	4 = "[4] Special";
-	8 = "[8] Ambush";
+	invertprecip = "regular";
+	gravityflip = "regular";
+	heatwave = "regular";
+	noclipcamera = "regular";
+	colormapfog = "colormap";
+	colormapfadesprites = "colormap";
+	colormapprotected = "colormap";
+	outerspace = "special";
+	doublestepup = "special";
+	nostepdown = "special";
+	speedpad = "special";
+	starpostactivator = "special";
+	exit = "special";
+	specialstagepit = "special";
+	returnflag = "special";
+	redteambase = "special";
+	blueteambase = "special";
+	fan = "special";
+	supertransform = "special";
+	forcespin = "special";
+	zoomtubestart = "special";
+	zoomtubeend = "special";
+	finishline = "special";
+	ropehang = "special";
+	jumpflip = "special";
+	gravityoverride = "special";
+	flipspecial_nofloor = "trigger";
+	flipspecial_ceiling = "trigger";
+	triggerspecial_touch = "trigger";
+	triggerspecial_headbump = "trigger";
+	triggerline_plane = "trigger";
+	triggerline_mobj = "trigger";
 }
 
 // THING FLAGS
 thingflags_udmf
 {
 	flip = "Flip";
+	absolutez = "Absolute Z height";
 }
 
-
 // Thing flags UDMF translation table
 // This is needed for copy/paste and prefabs to work properly
 // When the UDMF field name is prefixed with ! it is inverted
@@ -130,9 +138,9 @@ thingflagstranslation
 	2 = "flip";
 	4 = "special";
 	8 = "ambush";
+	16 = "absolutez";
 }
 
-
 // DEFAULT SECTOR BRIGHTNESS LEVELS
 sectorbrightness
 {
@@ -171,6 +179,8 @@ sectorbrightness
 	0;
 }
 
+numbrightnesslevels = 32;
+
 /*
 TEXTURES AND FLAT SOURCES
 This tells Doom Builder where to find the information for textures
@@ -221,145 +231,18 @@ universalfields
 {
 	sector
 	{
-		lightalpha
-		{
-			type = 0;
-			default = 25;
-		}
-
-		fadealpha
-		{
-			type = 0;
-			default = 25;
-		}
-
-		fadestart
-		{
-			type = 0;
-			default = 0;
-		}
-
-		fadeend
-		{
-			type = 0;
-			default = 33;
-		}
-
-		foglighting
-		{
-			type = 3;
-			default = false;
-		}
-
-		friction
-		{
-			type = 1;
-			default = 0.90625;
-		}
-
-		triggertag
-		{
-			type = 15;
-			default = 0;
-		}
-
-		triggerer
-		{
-			type = 2;
-			default = "Player";
-		}
 	}
 
 	linedef
 	{
-		arg5
-		{
-			type = 0;
-			default = 0;
-		}
-		arg6
-		{
-			type = 0;
-			default = 0;
-		}
-		arg7
-		{
-			type = 0;
-			default = 0;
-		}
-		arg8
-		{
-			type = 0;
-			default = 0;
-		}
-		arg9
-		{
-			type = 0;
-			default = 0;
-		}
-		stringarg0
-		{
-			type = 2;
-			default = "";
-		}
-		stringarg1
-		{
-			type = 2;
-			default = "";
-		}
-		executordelay
-		{
-			type = 0;
-			default = 0;
-		}
 	}
 
 	sidedef
 	{
-		repeatcnt
-		{
-			type = 0;
-			default = 0;
-		}
 	}
 
 	thing
 	{
-		arg5
-		{
-			type = 0;
-			default = 0;
-		}
-		arg6
-		{
-			type = 0;
-			default = 0;
-		}
-		arg7
-		{
-			type = 0;
-			default = 0;
-		}
-		arg8
-		{
-			type = 0;
-			default = 0;
-		}
-		arg9
-		{
-			type = 0;
-			default = 0;
-		}
-		stringarg0
-		{
-			type = 2;
-			default = "";
-		}
-		stringarg1
-		{
-			type = 2;
-			default = "";
-		}
 	}
 }
 
@@ -378,87 +261,6 @@ allowempty = The nodebuilder is allowed to leave this lump empty.
 scriptbuild = This lump is a text-based script, which should be compiled using current script compiler;
 script = This lump is a text-based script. Specify the filename of the script configuration to use.
 */
-
-doommaplumpnames
-{
-	~MAP
-	{
-		required = true;
-		blindcopy = true;
-		nodebuild = false;
-	}
-
-	THINGS
-	{
-		required = true;
-		nodebuild = true;
-		allowempty = true;
-	}
-
-	LINEDEFS
-	{
-		required = true;
-		nodebuild = true;
-		allowempty = false;
-	}
-
-	SIDEDEFS
-	{
-		required = true;
-		nodebuild = true;
-		allowempty = false;
-	}
-
-	VERTEXES
-	{
-		required = true;
-		nodebuild = true;
-		allowempty = false;
-	}
-
-	SEGS
-	{
-		required = false;
-		nodebuild = true;
-		allowempty = false;
-	}
-
-	SSECTORS
-	{
-		required = false;
-		nodebuild = true;
-		allowempty = false;
-	}
-
-	NODES
-	{
-		required = false;
-		nodebuild = true;
-		allowempty = false;
-	}
-
-	SECTORS
-	{
-		required = true;
-		nodebuild = true;
-		allowempty = false;
-	}
-
-	REJECT
-	{
-		required = false;
-		nodebuild = true;
-		allowempty = false;
-	}
-
-	BLOCKMAP
-	{
-		required = false;
-		nodebuild = true;
-		allowempty = true;
-	}
-}
-
 udmfmaplumpnames
 {
 	ZNODES
@@ -682,48 +484,32 @@ thingsfilters
 
 	}
 
-
-	filter3
-	{
-		name = "Normal Gravity";
-		category = "";
-		type = -1;
-
-		fields
-		{
-			2 = false;
-		}
-
-	}
-
-
-	filter4
-	{
-		name = "Reverse Gravity";
-		category = "";
-		type = -1;
-
-		fields
-		{
-			2 = true;
-		}
-
-	}
+	//filter3
+	//{
+	//	name = "Normal Gravity";
+	//	category = "";
+	//	type = -1;
+	//
+	//	fields
+	//	{
+	//		2 = false;
+	//	}
+	//}
+
+	//filter4
+	//{
+	//	name = "Reverse Gravity";
+	//	category = "";
+	//	type = -1;
+	//
+	//	fields
+	//	{
+	//		2 = true;
+	//	}
+	//}
 }
 
 // Special linedefs
-speciallinedefs
-{
-	soundlinedefflag = 64;	// See linedefflags
-	singlesidedflag = 1;	// See linedefflags
-	doublesidedflag = 4;	// See linedefflags
-	impassableflag = 1;
-	upperunpeggedflag = 8;
-	lowerunpeggedflag = 16;
-	repeatmidtextureflag = 1024;
-	pegmidtextureflag = 256;
-}
-
 speciallinedefs_udmf
 {
 	soundlinedefflag = "noclimb";
@@ -734,6 +520,8 @@ speciallinedefs_udmf
 	lowerunpeggedflag = "dontpegbottom";
 	repeatmidtextureflag = "wrapmidtex";
 	pegmidtextureflag = "midpeg";
+	slopeskewflag = "skewtd";
+	nomidtextureskewflag = "noskew";
 }
 
 scriptlumpnames
diff --git a/extras/conf/udb/Includes/SRB222_sectors.cfg b/extras/conf/udb/Includes/SRB222_sectors.cfg
deleted file mode 100644
index 5b3ad4155c176b229eee741b9a1744121b68d4ae..0000000000000000000000000000000000000000
--- a/extras/conf/udb/Includes/SRB222_sectors.cfg
+++ /dev/null
@@ -1,107 +0,0 @@
-sectortypes
-{
-	0 = "Normal";
-	1 = "Damage";
-	2 = "Damage (Water)";
-	3 = "Damage (Fire)";
-	4 = "Damage (Electrical)";
-	5 = "Spikes";
-	6 = "Death Pit (Camera Tilt)";
-	7 = "Death Pit (No Camera Tilt)";
-	8 = "Instant Kill";
-	9 = "Ring Drainer (Floor Touch)";
-	10 = "Ring Drainer (Anywhere in Sector)";
-	11 = "Special Stage Damage";
-	12 = "Space Countdown";
-	13 = "Ramp Sector (double step-up/down)";
-	14 = "Non-Ramp Sector (no step-down)";
-	15 = "Bouncy FOF <deprecated>";
-	16 = "Trigger Line Ex. (Pushable Objects)";
-	32 = "Trigger Line Ex. (Anywhere, All Players)";
-	48 = "Trigger Line Ex. (Floor Touch, All Players)";
-	64 = "Trigger Line Ex. (Anywhere in Sector)";
-	80 = "Trigger Line Ex. (Floor Touch)";
-	96 = "Trigger Line Ex. (Emerald Check) <deprecated>";
-	112 = "Trigger Line Ex. (NiGHTS Mare) <deprecated>";
-	128 = "Check for Linedef Executor on FOFs";
-	144 = "Egg Capsule";
-	160 = "Special Stage Time/Spheres Parameters <deprecated>";
-	176 = "Custom Global Gravity <deprecated>";
-	1280 = "Speed Pad";
-	1536 = "Flip Gravity on Jump";
-	4096 = "Star Post Activator";
-	8192 = "Exit/Special Stage Pit/Return Flag";
-	12288 = "CTF Red Team Base";
-	16384 = "CTF Blue Team Base";
-	20480 = "Fan Sector";
-	24576 = "Super Sonic Transform";
-	28672 = "Force Spin";
-	32768 = "Zoom Tube Start";
-	36864 = "Zoom Tube End";
-	40960 = "Circuit Finish Line";
-	45056 = "Rope Hang";
-	49152 = "Intangible to the Camera";
-}
-
-gen_sectortypes
-{
-	first
-	{
-		0 = "Normal";
-		1 = "Damage";
-		2 = "Damage (Water)";
-		3 = "Damage (Fire)";
-		4 = "Damage (Electrical)";
-		5 = "Spikes";
-		6 = "Death Pit (Camera Tilt)";
-		7 = "Death Pit (No Camera Tilt)";
-		8 = "Instant Kill";
-		9 = "Ring Drainer (Floor Touch)";
-		10 = "Ring Drainer (Anywhere in Sector)";
-		11 = "Special Stage Damage";
-		12 = "Space Countdown";
-		13 = "Ramp Sector (double step-up/down)";
-		14 = "Non-Ramp Sector (no step-down)";
-		15 = "Bouncy FOF <deprecated>";
-	}
-
-	second
-	{
-		0 = "Normal";
-		16 = "Trigger Line Ex. (Pushable Objects)";
-		32 = "Trigger Line Ex. (Anywhere, All Players)";
-		48 = "Trigger Line Ex. (Floor Touch, All Players)";
-		64 = "Trigger Line Ex. (Anywhere in Sector)";
-		80 = "Trigger Line Ex. (Floor Touch)";
-		96 = "Trigger Line Ex. (Emerald Check) <deprecated>";
-		112 = "Trigger Line Ex. (NiGHTS Mare) <deprecated>";
-		128 = "Check for Linedef Executor on FOFs";
-		144 = "Egg Capsule";
-		160 = "Special Stage Time/Spheres Parameters <deprecated>";
-		176 = "Custom Global Gravity <deprecated>";
-	}
-
-	third
-	{
-		0 = "Normal";
-		1280 = "Speed Pad";
-		1536 = "Flip Gravity on Jump";
-	}
-
-	fourth
-	{
-		0 = "Normal";
-		4096 = "Star Post Activator";
-		8192 = "Exit/Special Stage Pit/Return Flag";
-		12288 = "CTF Red Team Base";
-		16384 = "CTF Blue Team Base";
-		20480 = "Fan Sector";
-		24576 = "Super Sonic Transform";
-		28672 = "Force Spin";
-		32768 = "Zoom Tube Start";
-		36864 = "Zoom Tube End";
-		40960 = "Circuit Finish Line";
-		45056 = "Rope Hang";
-		49152 = "Intangible to the Camera";
-	}
-}
diff --git a/extras/conf/udb/Includes/SRB222_things.cfg b/extras/conf/udb/Includes/SRB222_things.cfg
index b4508c91ea568aecc702ad1bf9927f079e820bd1..9eb227974dfd0b33de593a7ced83b42fab7d6738 100644
--- a/extras/conf/udb/Includes/SRB222_things.cfg
+++ b/extras/conf/udb/Includes/SRB222_things.cfg
@@ -3,3175 +3,8 @@
 // 8-Dark_Gray 9-Blue 10-Green 11-Cyan 12-Red 13-Magenta
 // 14-Yellow 15-White 16-Pink 17-Orange 18-Gold 19-Cream
 
-doom
-{
-	editor
-	{
-		color = 15; // White
-		arrow = 1;
-		title = "<Editor Things>";
-		error = -1;
-		width = 8;
-		height = 16;
-		sort = 1;
-
-		3328 = "3D Mode Start";
-	}
-
-	starts
-	{
-		color = 1; // Blue
-		arrow = 1;
-		title = "Player Starts";
-		width = 16;
-		height = 48;
-		sprite = "PLAYA0";
-
-		1
-		{
-			title = "Player 01 Start";
-			sprite = "PLAYA0";
-		}
-		2
-		{
-			title = "Player 02 Start";
-			sprite = "PLAYA0";
-		}
-		3
-		{
-			title = "Player 03 Start";
-			sprite = "PLAYA0";
-		}
-		4
-		{
-			title = "Player 04 Start";
-			sprite = "PLAYA0";
-		}
-		5
-		{
-			title = "Player 05 Start";
-			sprite = "PLAYA0";
-		}
-		6
-		{
-			title = "Player 06 Start";
-			sprite = "PLAYA0";
-		}
-		7
-		{
-			title = "Player 07 Start";
-			sprite = "PLAYA0";
-		}
-		8
-		{
-			title = "Player 08 Start";
-			sprite = "PLAYA0";
-		}
-		9
-		{
-			title = "Player 09 Start";
-			sprite = "PLAYA0";
-		}
-		10
-		{
-			title = "Player 10 Start";
-			sprite = "PLAYA0";
-		}
-		11
-		{
-			title = "Player 11 Start";
-			sprite = "PLAYA0";
-		}
-		12
-		{
-			title = "Player 12 Start";
-			sprite = "PLAYA0";
-		}
-		13
-		{
-			title = "Player 13 Start";
-			sprite = "PLAYA0";
-		}
-		14
-		{
-			title = "Player 14 Start";
-			sprite = "PLAYA0";
-		}
-		15
-		{
-			title = "Player 15 Start";
-			sprite = "PLAYA0";
-		}
-		16
-		{
-			title = "Player 16 Start";
-			sprite = "PLAYA0";
-		}
-		17
-		{
-			title = "Player 17 Start";
-			sprite = "PLAYA0";
-		}
-		18
-		{
-			title = "Player 18 Start";
-			sprite = "PLAYA0";
-		}
-		19
-		{
-			title = "Player 19 Start";
-			sprite = "PLAYA0";
-		}
-		20
-		{
-			title = "Player 20 Start";
-			sprite = "PLAYA0";
-		}
-		21
-		{
-			title = "Player 21 Start";
-			sprite = "PLAYA0";
-		}
-		22
-		{
-			title = "Player 22 Start";
-			sprite = "PLAYA0";
-		}
-		23
-		{
-			title = "Player 23 Start";
-			sprite = "PLAYA0";
-		}
-		24
-		{
-			title = "Player 24 Start";
-			sprite = "PLAYA0";
-		}
-		25
-		{
-			title = "Player 25 Start";
-			sprite = "PLAYA0";
-		}
-		26
-		{
-			title = "Player 26 Start";
-			sprite = "PLAYA0";
-		}
-		27
-		{
-			title = "Player 27 Start";
-			sprite = "PLAYA0";
-		}
-		28
-		{
-			title = "Player 28 Start";
-			sprite = "PLAYA0";
-		}
-		29
-		{
-			title = "Player 29 Start";
-			sprite = "PLAYA0";
-		}
-		30
-		{
-			title = "Player 30 Start";
-			sprite = "PLAYA0";
-		}
-		31
-		{
-			title = "Player 31 Start";
-			sprite = "PLAYA0";
-		}
-		32
-		{
-			title = "Player 32 Start";
-			sprite = "PLAYA0";
-		}
-		33
-		{
-			title = "Match Start";
-			sprite = "NDRNA2A8";
-		}
-		34
-		{
-			title = "CTF Red Team Start";
-			sprite = "SIGNG0";
-		}
-		35
-		{
-			title = "CTF Blue Team Start";
-			sprite = "SIGNE0";
-		}
-	}
-
-	enemies
-	{
-		color = 9; // Light_Blue
-		arrow = 1;
-		title = "Enemies";
-
-		100
-		{
-			title = "Crawla (Blue)";
-			sprite = "POSSA1";
-			width = 24;
-			height = 32;
-		}
-		101
-		{
-			title = "Crawla (Red)";
-			sprite = "SPOSA1";
-			width = 24;
-			height = 32;
-		}
-		102
-		{
-			title = "Stupid Dumb Unnamed RoboFish";
-			sprite = "FISHA0";
-			width = 8;
-			height = 28;
-		}
-		103
-		{
-			title = "Buzz (Gold)";
-			sprite = "BUZZA1";
-			width = 28;
-			height = 40;
-		}
-		104
-		{
-			title = "Buzz (Red)";
-			sprite = "RBUZA1";
-			width = 28;
-			height = 40;
-		}
-		108
-		{
-			title = "Deton";
-			sprite = "DETNA1";
-			width = 20;
-			height = 32;
-		}
-		110
-		{
-			title = "Turret";
-			sprite = "TRETA1";
-			width = 16;
-			height = 32;
-		}
-		111
-		{
-			title = "Pop-up Turret";
-			sprite = "TURRI1";
-			width = 12;
-			height = 64;
-		}
-		122
-		{
-			title = "Spring Shell (Green)";
-			sprite = "SSHLA1";
-			width = 24;
-			height = 40;
-		}
-		125
-		{
-			title = "Spring Shell (Yellow)";
-			sprite = "SSHLI1";
-			width = 24;
-			height = 40;
-		}
-		109
-		{
-			title = "Skim";
-			sprite = "SKIMA1";
-			width = 16;
-			height = 24;
-		}
-		113
-		{
-			title = "Jet Jaw";
-			sprite = "JJAWA3A7";
-			width = 12;
-			height = 20;
-		}
-		126
-		{
-			title = "Crushstacean";
-			sprite = "CRABA0";
-			width = 24;
-			height = 32;
-		}
-		138
-		{
-			title = "Banpyura";
-			sprite = "CR2BA0";
-			width = 24;
-			height = 32;
-		}
-		117
-		{
-			title = "Robo-Hood";
-			sprite = "ARCHA1";
-			width = 24;
-			height = 32;
-		}
-		118
-		{
-			title = "Lance-a-Bot";
-			sprite = "CBFSA1";
-			width = 32;
-			height = 72;
-		}
-		1113
-		{
-			title = "Suspicious Lance-a-Bot Statue";
-			sprite = "CBBSA1";
-			width = 32;
-			height = 72;
-		}
-		119
-		{
-			title = "Egg Guard";
-			sprite = "ESHIA1";
-			width = 16;
-			height = 48;
-		}
-		115
-		{
-			title = "Bird Aircraft Strike Hazard";
-			sprite = "VLTRF1";
-			width = 12;
-			height = 24;
-		}
-		120
-		{
-			title = "Green Snapper";
-			sprite = "GSNPA1";
-			width = 24;
-			height = 24;
-		}
-		121
-		{
-			title = "Minus";
-			sprite = "MNUSA0";
-			width = 24;
-			height = 32;
-		}
-		134
-		{
-			title = "Canarivore";
-			sprite = "CANAA0";
-			width = 12;
-			height = 80;
-			hangs = 1;
-		}
-		123
-		{
-			title = "Unidus";
-			sprite = "UNIDA1";
-			width = 18;
-			height = 36;
-		}
-		135
-		{
-			title = "Pterabyte Spawner";
-			sprite = "PTERA2A8";
-			width = 16;
-			height = 16;
-		}
-		136
-		{
-			title = "Pyre Fly";
-			sprite = "PYREA0";
-			width = 24;
-			height = 34;
-		}
-		137
-		{
-			title = "Dragonbomber";
-			sprite = "DRABA1";
-			width = 28;
-			height = 48;
-		}
-		105
-		{
-			title = "Jetty-Syn Bomber";
-			sprite = "JETBB1";
-			width = 20;
-			height = 50;
-		}
-		106
-		{
-			title = "Jetty-Syn Gunner";
-			sprite = "JETGB1";
-			width = 20;
-			height = 48;
-		}
-		112
-		{
-			title = "Spincushion";
-			sprite = "SHRPA1";
-			width = 16;
-			height = 24;
-		}
-		114
-		{
-			title = "Snailer";
-			sprite = "SNLRA3A7";
-			width = 24;
-			height = 48;
-		}
-		129
-		{
-			title = "Penguinator";
-			sprite = "PENGA1";
-			width = 24;
-			height = 32;
-		}
-		130
-		{
-			title = "Pophat";
-			sprite = "POPHA1";
-			width = 24;
-			height = 32;
-		}
-		107
-		{
-			title = "Crawla Commander";
-			sprite = "CCOMA1";
-			width = 16;
-			height = 32;
-		}
-		131
-		{
-			title = "Spinbobert";
-			sprite = "SBOBB0";
-			width = 32;
-			height = 32;
-		}
-		132
-		{
-			title = "Cacolantern";
-			sprite = "CACOA0";
-			width = 32;
-			height = 32;
-		}
-		133
-		{
-			title = "Hangster";
-			sprite = "HBATC1";
-			width = 24;
-			height = 24;
-			hangs = 1;
-		}
-		127
-		{
-			title = "Hive Elemental";
-			sprite = "HIVEA0";
-			width = 32;
-			height = 80;
-		}
-		128
-		{
-			title = "Bumblebore";
-			sprite = "BUMBA1";
-			width = 16;
-			height = 32;
-		}
-		124
-		{
-			title = "Buggle";
-			sprite = "BBUZA1";
-			width = 20;
-			height = 24;
-		}
-		116
-		{
-			title = "Pointy";
-			sprite = "PNTYA1";
-			width = 8;
-			height = 16;
-		}
-	}
-
-	bosses
-	{
-		color = 8; // Dark_Gray
-		arrow = 1;
-		title = "Bosses";
-
-		200
-		{
-			title = "Egg Mobile";
-			sprite = "EGGMA1";
-			width = 24;
-			height = 76;
-		}
-		201
-		{
-			title = "Egg Slimer";
-			sprite = "EGGNA1";
-			width = 24;
-			height = 76;
-		}
-		202
-		{
-			title = "Sea Egg";
-			sprite = "EGGOA1";
-			width = 32;
-			height = 116;
-		}
-		203
-		{
-			title = "Egg Colosseum";
-			sprite = "EGGPA1";
-			width = 24;
-			height = 76;
-		}
-		204
-		{
-			title = "Fang";
-			sprite = "FANGA1";
-			width = 24;
-			height = 60;
-		}
-		206
-		{
-			title = "Brak Eggman (Old)";
-			sprite = "BRAKB1";
-			width = 48;
-			height = 160;
-		}
-		207
-		{
-			title = "Metal Sonic (Race)";
-			sprite = "METLI1";
-			width = 16;
-			height = 48;
-		}
-		208
-		{
-			title = "Metal Sonic (Battle)";
-			sprite = "METLC1";
-			width = 16;
-			height = 48;
-		}
-		209
-		{
-			title = "Brak Eggman";
-			sprite = "BRAK01";
-			width = 48;
-			height = 160;
-		}
-		290
-		{
-			arrow = 0;
-			title = "Boss Escape Point";
-			width = 8;
-			height = 16;
-			sprite = "internal:eggmanend";
-		}
-		291
-		{
-			arrow = 0;
-			title = "Egg Capsule Center";
-			width = 8;
-			height = 16;
-			sprite = "internal:capsule";
-		}
-		292
-		{
-			arrow = 0;
-			title = "Boss Waypoint";
-			width = 8;
-			height = 16;
-			sprite = "internal:eggmanway";
-		}
-		293
-		{
-			title = "Metal Sonic Gather Point";
-			sprite = "internal:metal";
-			width = 8;
-			height = 16;
-		}
-		294
-		{
-			title = "Fang Waypoint";
-			sprite = "internal:eggmanway";
-			width = 8;
-			height = 16;
-		}
-	}
-
-	rings
-	{
-		color = 14; // Yellow
-		title = "Rings and Weapon Panels";
-		width = 24;
-		height = 24;
-		sprite = "RINGA0";
-
-		300
-		{
-			title = "Ring";
-			sprite = "RINGA0";
-			width = 16;
-		}
-		301
-		{
-			title = "Bounce Ring";
-			sprite = "internal:RNGBA0";
-		}
-		302
-		{
-			title = "Rail Ring";
-			sprite = "internal:RNGRA0";
-		}
-		303
-		{
-			title = "Infinity Ring";
-			sprite = "internal:RNGIA0";
-		}
-		304
-		{
-			title = "Automatic Ring";
-			sprite = "internal:RNGAA0";
-		}
-		305
-		{
-			title = "Explosion Ring";
-			sprite = "internal:RNGEA0";
-		}
-		306
-		{
-			title = "Scatter Ring";
-			sprite = "internal:RNGSA0";
-		}
-		307
-		{
-			title = "Grenade Ring";
-			sprite = "internal:RNGGA0";
-		}
-		308
-		{
-			title = "CTF Team Ring (Red)";
-			sprite = "internal:RRNGA0";
-			width = 16;
-		}
-		309
-		{
-			title = "CTF Team Ring (Blue)";
-			sprite = "internal:BRNGA0";
-			width = 16;
-		}
-		330
-		{
-			title = "Bounce Ring Panel";
-			sprite = "internal:PIKBA0";
-		}
-		331
-		{
-			title = "Rail Ring Panel";
-			sprite = "internal:PIKRA0";
-		}
-		332
-		{
-			title = "Automatic Ring Panel";
-			sprite = "internal:PIKAA0";
-		}
-		333
-		{
-			title = "Explosion Ring Panel";
-			sprite = "internal:PIKEA0";
-		}
-		334
-		{
-			title = "Scatter Ring Panel";
-			sprite = "internal:PIKSA0";
-		}
-		335
-		{
-			title = "Grenade Ring Panel";
-			sprite = "internal:PIKGA0";
-		}
-	}
-
-	collectibles
-	{
-		color = 10; // Light_Green
-		title = "Other Collectibles";
-		width = 16;
-		height = 32;
-		sort = 1;
-		sprite = "CEMGA0";
-
-		310
-		{
-			title = "CTF Red Flag";
-			sprite = "RFLGA0";
-			width = 24;
-			height = 64;
-		}
-		311
-		{
-			title = "CTF Blue Flag";
-			sprite = "BFLGA0";
-			width = 24;
-			height = 64;
-		}
-		312
-		{
-			title = "Emerald Token";
-			sprite = "TOKEA0";
-			width = 16;
-			height = 32;
-		}
-		313
-		{
-			title = "Chaos Emerald 1 (Green)";
-			sprite = "CEMGA0";
-		}
-		314
-		{
-			title = "Chaos Emerald 2 (Purple)";
-			sprite = "CEMGB0";
-		}
-		315
-		{
-			title = "Chaos Emerald 3 (Blue)";
-			sprite = "CEMGC0";
-		}
-		316
-		{
-			title = "Chaos Emerald 4 (Cyan)";
-			sprite = "CEMGD0";
-		}
-		317
-		{
-			title = "Chaos Emerald 5 (Orange)";
-			sprite = "CEMGE0";
-		}
-		318
-		{
-			title = "Chaos Emerald 6 (Red)";
-			sprite = "CEMGF0";
-		}
-		319
-		{
-			title = "Chaos Emerald 7 (Gray)";
-			sprite = "CEMGG0";
-		}
-		320
-		{
-			title = "Emerald Hunt Location";
-			sprite = "SHRDA0";
-		}
-		321
-		{
-			title = "Match Chaos Emerald Spawn";
-			sprite = "CEMGA0";
-		}
-		322
-		{
-			title = "Emblem";
-			sprite = "EMBMA0";
-			width = 16;
-			height = 30;
-		}
-	}
-
-	boxes
-	{
-		color = 7; // Gray
-		blocking = 2;
-		title = "Monitors";
-		width = 18;
-		height = 40;
-
-		400
-		{
-			title = "Super Ring (10 Rings)";
-			sprite = "TVRIA0";
-		}
-		401
-		{
-			title = "Pity Shield";
-			sprite = "TVPIA0";
-		}
-		402
-		{
-			title = "Attraction Shield";
-			sprite = "TVATA0";
-		}
-		403
-		{
-			title = "Force Shield";
-			sprite = "TVFOA0";
-		}
-		404
-		{
-			title = "Armageddon Shield";
-			sprite = "TVARA0";
-		}
-		405
-		{
-			title = "Whirlwind Shield";
-			sprite = "TVWWA0";
-		}
-		406
-		{
-			title = "Elemental Shield";
-			sprite = "TVELA0";
-		}
-		407
-		{
-			title = "Super Sneakers";
-			sprite = "TVSSA0";
-		}
-		408
-		{
-			title = "Invincibility";
-			sprite = "TVIVA0";
-		}
-		409
-		{
-			title = "Extra Life";
-			sprite = "TV1UA0";
-		}
-		410
-		{
-			title = "Eggman";
-			sprite = "TVEGA0";
-		}
-		411
-		{
-			title = "Teleporter";
-			sprite = "TVMXA0";
-		}
-		413
-		{
-			title = "Gravity Boots";
-			sprite = "TVGVA0";
-		}
-		414
-		{
-			title = "CTF Team Ring Monitor (Red)";
-			sprite = "TRRIA0";
-		}
-		415
-		{
-			title = "CTF Team Ring Monitor (Blue)";
-			sprite = "TBRIA0";
-		}
-		416
-		{
-			title = "Recycler";
-			sprite = "TVRCA0";
-		}
-		418
-		{
-			title = "Score (1,000 Points)";
-			sprite = "TV1KA0";
-		}
-		419
-		{
-			title = "Score (10,000 Points)";
-			sprite = "TVTKA0";
-		}
-		420
-		{
-			title = "Flame Shield";
-			sprite = "TVFLA0";
-		}
-		421
-		{
-			title = "Water Shield";
-			sprite = "TVBBA0";
-		}
-		422
-		{
-			title = "Lightning Shield";
-			sprite = "TVZPA0";
-		}
-	}
-
-	boxes2
-	{
-		color = 18; // Gold
-		blocking = 2;
-		title = "Monitors (Respawning)";
-		width = 20;
-		height = 44;
-
-		431
-		{
-			title = "Pity Shield (Respawn)";
-			sprite = "TVPIB0";
-		}
-		432
-		{
-			title = "Attraction Shield (Respawn)";
-			sprite = "TVATB0";
-		}
-		433
-		{
-			title = "Force Shield (Respawn)";
-			sprite = "TVFOB0";
-		}
-		434
-		{
-			title = "Armageddon Shield (Respawn)";
-			sprite = "TVARB0";
-		}
-		435
-		{
-			title = "Whirlwind Shield (Respawn)";
-			sprite = "TVWWB0";
-		}
-		436
-		{
-			title = "Elemental Shield (Respawn)";
-			sprite = "TVELB0";
-		}
-		437
-		{
-			title = "Super Sneakers (Respawn)";
-			sprite = "TVSSB0";
-		}
-		438
-		{
-			title = "Invincibility (Respawn)";
-			sprite = "TVIVB0";
-		}
-		440
-		{
-			title = "Eggman (Respawn)";
-			sprite = "TVEGB0";
-		}
-		443
-		{
-			title = "Gravity Boots (Respawn)";
-			sprite = "TVGVB0";
-		}
-		450
-		{
-			title = "Flame Shield (Respawn)";
-			sprite = "TVFLB0";
-		}
-		451
-		{
-			title = "Water Shield (Respawn)";
-			sprite = "TVBBB0";
-		}
-		452
-		{
-			title = "Lightning Shield (Respawn)";
-			sprite = "TVZPB0";
-		}
-	}
-
-	generic
-	{
-		color = 11; // Light_Cyan
-		title = "Generic Items & Hazards";
-
-		500
-		{
-			title = "Air Bubble Patch";
-			sprite = "BUBLE0";
-			width = 8;
-			height = 16;
-		}
-		501
-		{
-			title = "Signpost";
-			sprite = "SIGND0";
-			width = 8;
-			height = 32;
-		}
-		502
-		{
-			arrow = 1;
-			title = "Star Post";
-			sprite = "STPTA0M0";
-			width = 64;
-			height = 128;
-		}
-		520
-		{
-			title = "Bomb Sphere";
-			sprite = "SPHRD0";
-			width = 16;
-			height = 24;
-		}
-		521
-		{
-			title = "Spikeball";
-			sprite = "SPIKA0";
-			width = 12;
-			height = 8;
-		}
-		522
-		{
-			title = "Wall Spike";
-			sprite = "WSPKALAR";
-			width = 16;
-			height = 14;
-			arrow = 1;
-		}
-		523
-		{
-			title = "Spike";
-			sprite = "USPKA0";
-			width = 8;
-			height = 32;
-		}
-		1130
-		{
-			title = "Small Mace";
-			sprite = "SMCEA0";
-			width = 17;
-			height = 34;
-		}
-		1131
-		{
-			title = "Big Mace";
-			sprite = "BMCEA0";
-			width = 34;
-			height = 68;
-		}
-		1136
-		{
-			title = "Small Fireball";
-			sprite = "SFBRA0";
-			width = 17;
-			height = 34;
-		}
-		1137
-		{
-			title = "Large Fireball";
-			sprite = "BFBRA0";
-			width = 34;
-			height = 68;
-		}
-	}
-
-	springs
-	{
-		color = 12; // Light_Red
-		title = "Springs and Fans";
-		width = 20;
-		height = 16;
-		sprite = "RSPRD2";
-
-		540
-		{
-			title = "Fan";
-			sprite = "FANSA0D0";
-			width = 16;
-			height = 8;
-		}
-		541
-		{
-			title = "Gas Jet";
-			sprite = "STEMD0";
-			width = 32;
-		}
-		542
-		{
-			title = "Bumper";
-			sprite = "BUMPA0";
-			width = 32;
-			height = 64;
-		}
-		543
-		{
-			title = "Balloon";
-			sprite = "BLONA0";
-			width = 32;
-			height = 64;
-		}
-		550
-		{
-			title = "Yellow Spring";
-			sprite = "SPRYA0";
-		}
-		551
-		{
-			title = "Red Spring";
-			sprite = "SPRRA0";
-		}
-		552
-		{
-			title = "Blue Spring";
-			sprite = "SPRBA0";
-		}
-		555
-		{
-			arrow = 1;
-			title = "Diagonal Yellow Spring";
-			sprite = "YSPRD2";
-			width = 16;
-		}
-		556
-		{
-			arrow = 1;
-			title = "Diagonal Red Spring";
-			sprite = "RSPRD2";
-			width = 16;
-		}
-		557
-		{
-			arrow = 1;
-			title = "Diagonal Blue Spring";
-			sprite = "BSPRD2";
-			width = 16;
-		}
-		558
-		{
-			arrow = 1;
-			title = "Horizontal Yellow Spring";
-			sprite = "SSWYD2D8";
-			width = 16;
-			height = 32;
-		}
-		559
-		{
-			arrow = 1;
-			title = "Horizontal Red Spring";
-			sprite = "SSWRD2D8";
-			width = 16;
-			height = 32;
-		}
-		560
-		{
-			arrow = 1;
-			title = "Horizontal Blue Spring";
-			sprite = "SSWBD2D8";
-			width = 16;
-			height = 32;
-		}
-		1134
-		{
-			title = "Yellow Spring Ball";
-			sprite = "YSPBA0";
-			width = 17;
-			height = 34;
-		}
-		1135
-		{
-			title = "Red Spring Ball";
-			sprite = "RSPBA0";
-			width = 17;
-			height = 34;
-		}
-		544
-		{
-			arrow = 1;
-			title = "Yellow Boost Panel";
-			sprite = "BSTYA0";
-			width = 28;
-			height = 2;
-		}
-		545
-		{
-			arrow = 1;
-			title = "Red Boost Panel";
-			sprite = "BSTRA0";
-			width = 28;
-			height = 2;
-		}
-	}
-
-	patterns
-	{
-		color = 5; // Magenta
-		arrow = 1;
-		title = "Special Placement Patterns";
-		width = 16;
-		height = 384;
-		sprite = "RINGA0";
-
-		600
-		{
-			arrow = 0;
-			title = "5 Vertical Rings (Yellow Spring)";
-			sprite = "RINGA0";
-		}
-		601
-		{
-			arrow = 0;
-			title = "5 Vertical Rings (Red Spring)";
-			sprite = "RINGA0";
-			height = 1024;
-		}
-		602
-		{
-			title = "5 Diagonal Rings (Yellow Spring)";
-			sprite = "RINGA0";
-			height = 32;
-		}
-		603
-		{
-			title = "10 Diagonal Rings (Red Spring)";
-			sprite = "RINGA0";
-			height = 32;
-		}
-		604
-		{
-			title = "Circle of Rings";
-			sprite = "RINGA0";
-			width = 96;
-			height = 192;
-		}
-		605
-		{
-			title = "Circle of Rings (Big)";
-			sprite = "RINGA0";
-			width = 192;
-		}
-		606
-		{
-			title = "Circle of Blue Spheres";
-			sprite = "SPHRA0";
-			width = 96;
-			height = 192;
-		}
-		607
-		{
-			title = "Circle of Blue Spheres (Big)";
-			sprite = "SPHRA0";
-			width = 192;
-		}
-		608
-		{
-			title = "Circle of Rings and Spheres";
-			sprite = "SPHRA0";
-			width = 96;
-			height = 192;
-		}
-		609
-		{
-			title = "Circle of Rings and Spheres (Big)";
-			sprite = "SPHRA0";
-			width = 192;
-		}
-	}
-
-	invisible
-	{
-		color = 15; // White
-		title = "Misc. Invisible";
-		width = 0;
-		height = 0;
-		sprite = "UNKNA0";
-		sort = 1;
-		fixedsize = true;
-		blocking = 0;
-
-		700
-		{
-			title = "Water Ambience A (Large)";
-			sprite = "internal:ambiance";
-		}
-
-		701
-		{
-			title = "Water Ambience B (Large)";
-			sprite = "internal:ambiance";
-		}
-
-		702
-		{
-			title = "Water Ambience C (Medium)";
-			sprite = "internal:ambiance";
-		}
-
-		703
-		{
-			title = "Water Ambience D (Medium)";
-			sprite = "internal:ambiance";
-		}
-
-		704
-		{
-			title = "Water Ambience E (Small)";
-			sprite = "internal:ambiance";
-		}
-
-		705
-		{
-			title = "Water Ambience F (Small)";
-			sprite = "internal:ambiance";
-		}
-
-		706
-		{
-			title = "Water Ambience G (Extra Large)";
-			sprite = "internal:ambiance";
-		}
-
-		707
-		{
-			title = "Water Ambience H (Extra Large)";
-			sprite = "internal:ambiance";
-		}
-
-		708
-		{
-			title = "Disco Ambience";
-			sprite = "internal:ambiance";
-		}
-
-		709
-		{
-			title = "Volcano Ambience";
-			sprite = "internal:ambiance";
-		}
-
-		710
-		{
-			title = "Machine Ambience";
-			sprite = "internal:ambiance";
-		}
-
-		750
-		{
-			title = "Slope Vertex";
-			sprite = "internal:vertexslope";
-		}
-
-		751
-		{
-			arrow = 1;
-			title = "Teleport Destination";
-			sprite = "internal:tele";
-		}
-
-		752
-		{
-			arrow = 1;
-			title = "Alternate View Point";
-			sprite = "internal:view";
-		}
-
-		753
-		{
-			title = "Zoom Tube Waypoint";
-			sprite = "internal:zoom";
-		}
-
-		754
-		{
-			title = "Push Point";
-			sprite = "GWLGA0";
-		}
-		755
-		{
-			title = "Pull Point";
-			sprite = "GWLRA0";
-		}
-		756
-		{
-			title = "Blast Linedef Executor";
-			sprite = "TOADA0";
-			width = 32;
-			height = 16;
-		}
-		757
-		{
-			title = "Fan Particle Generator";
-			sprite = "PRTLA0";
-			width = 8;
-			height = 16;
-		}
-		758
-		{
-			title = "Object Angle Anchor";
-			sprite = "internal:view";
-		}
-		760
-		{
-			title = "PolyObject Anchor";
-			sprite = "internal:polyanchor";
-		}
-		761
-		{
-			title = "PolyObject Spawn Point";
-			sprite = "internal:polycenter";
-		}
-		762
-		{
-			title = "PolyObject Spawn Point (Crush)";
-			sprite = "internal:polycentercrush";
-		}
-		780
-		{
-			title = "Skybox View Point";
-			sprite = "internal:skyb";
-		}
-	}
-
-	greenflower
-	{
-		color = 10; // Green
-		title = "Greenflower";
-
-		800
-		{
-			title = "GFZ Flower";
-			sprite = "FWR1A0";
-			width = 16;
-			height = 40;
-		}
-		801
-		{
-			title = "Sunflower";
-			sprite = "FWR2A0";
-			width = 16;
-			height = 96;
-		}
-		802
-		{
-			title = "Budding Flower";
-			sprite = "FWR3A0";
-			width = 8;
-			height = 32;
-		}
-		803
-		{
-			title = "Blueberry Bush";
-			sprite = "BUS3A0";
-			width = 16;
-			height = 32;
-		}
-		804
-		{
-			title = "Berry Bush";
-			sprite = "BUS1A0";
-			width = 16;
-			height = 32;
-		}
-		805
-		{
-			title = "Bush";
-			sprite = "BUS2A0";
-			width = 16;
-			height = 32;
-		}
-		806
-		{
-			title = "GFZ Tree";
-			sprite = "TRE1A0";
-			width = 20;
-			height = 128;
-		}
-		807
-		{
-			title = "GFZ Berry Tree";
-			sprite = "TRE1B0";
-			width = 20;
-			height = 128;
-		}
-		808
-		{
-			title = "GFZ Cherry Tree";
-			sprite = "TRE1C0";
-			width = 20;
-			height = 128;
-		}
-		809
-		{
-			title = "Checkered Tree";
-			sprite = "TRE2A0";
-			width = 20;
-			height = 200;
-		}
-		810
-		{
-			title = "Checkered Tree (Sunset)";
-			sprite = "TRE2B0";
-			width = 20;
-			height = 200;
-		}
-		811
-		{
-			title = "Polygon Tree";
-			sprite = "TRE4A0";
-			width = 20;
-			height = 200;
-		}
-		812
-		{
-			title = "Bush Tree";
-			sprite = "TRE5A0";
-			width = 20;
-			height = 200;
-		}
-		813
-		{
-			title = "Red Bush Tree";
-			sprite = "TRE5B0";
-			width = 20;
-			height = 200;
-		}
-	}
-
-	technohill
-	{
-		color = 10; // Green
-		title = "Techno Hill";
-
-		900
-		{
-			title = "THZ Steam Flower";
-			sprite = "THZPA0";
-			width = 8;
-			height = 32;
-		}
-		901
-		{
-			title = "Alarm";
-			sprite = "ALRMA0";
-			width = 8;
-			height = 16;
-			hangs = 1;
-		}
-		902
-		{
-			title = "THZ Spin Flower (Red)";
-			sprite = "FWR5A0";
-			width = 16;
-			height = 64;
-		}
-		903
-		{
-			title = "THZ Spin Flower (Yellow)";
-			sprite = "FWR6A0";
-			width = 16;
-			height = 64;
-		}
-		904
-		{
-			arrow = 1;
-			title = "Whistlebush";
-			sprite = "THZTA0";
-			width = 16;
-			height = 64;
-		}
-	}
-
-	deepsea
-	{
-		color = 10; // Green
-		title = "Deep Sea";
-
-		1000
-		{
-			arrow = 1;
-			blocking = 2;
-			title = "Gargoyle";
-			sprite = "GARGA1";
-			width = 16;
-			height = 40;
-		}
-		1009
-		{
-			arrow = 1;
-			blocking = 2;
-			title = "Gargoyle (Big)";
-			sprite = "GARGB1";
-			width = 32;
-			height = 80;
-		}
-		1001
-		{
-			title = "Seaweed";
-			sprite = "SEWEA0";
-			width = 24;
-			height = 56;
-		}
-		1002
-		{
-			title = "Dripping Water";
-			sprite = "DRIPD0";
-			width = 8;
-			height = 16;
-			hangs = 1;
-		}
-		1003
-		{
-			title = "Coral (Green)";
-			sprite = "CORLA0";
-			width = 29;
-			height = 40;
-		}
-		1004
-		{
-			title = "Coral (Red)";
-			sprite = "CORLB0";
-			width = 30;
-			height = 53;
-		}
-		1005
-		{
-			title = "Coral (Orange)";
-			sprite = "CORLC0";
-			width = 28;
-			height = 41;
-		}
-		1006
-		{
-			title = "Blue Crystal";
-			sprite = "BCRYA1";
-			width = 8;
-			height = 16;
-		}
-		1007
-		{
-			title = "Kelp";
-			sprite = "KELPA0";
-			width = 16;
-			height = 292;
-		}
-		1008
-		{
-			title = "Stalagmite (DSZ1)";
-			sprite = "DSTGA0";
-			width = 8;
-			height = 116;
-		}
-		1010
-		{
-			arrow = 1;
-			title = "Light Beam";
-			sprite = "LIBEARAL";
-			width = 16;
-			height = 16;
-		}
-		1011
-		{
-			title = "Stalagmite (DSZ2)";
-			sprite = "DSTGA0";
-			width = 8;
-			height = 116;
-		}
-		1012
-		{
-			arrow = 1;
-			title = "Big Floating Mine";
-			width = 28;
-			height = 56;
-			sprite = "BMNEA1";
-		}
-		1013
-		{
-			title = "Animated Kelp";
-			sprite = "ALGAA0";
-			width = 48;
-			height = 120;
-		}
-		1014
-		{
-			title = "Large Coral (Brown)";
-			sprite = "CORLD0";
-			width = 56;
-			height = 112;
-		}
-		1015
-		{
-			title = "Large Coral (Beige)";
-			sprite = "CORLE0";
-			width = 56;
-			height = 112;
-		}
-	}
-
-	castleeggman
-	{
-		color = 10; // Green
-		title = "Castle Eggman";
-
-		1100
-		{
-			title = "Chain (Decorative)";
-			sprite = "CHANA0";
-			width = 4;
-			height = 128;
-			hangs = 1;
-		}
-		1101
-		{
-			title = "Torch";
-			sprite = "FLAMA0E0";
-			width = 8;
-			height = 32;
-		}
-		1102
-		{
-			arrow = 1;
-			blocking = 2;
-			title = "Eggman Statue";
-			sprite = "ESTAA1";
-			width = 32;
-			height = 240;
-		}
-		1103
-		{
-			title = "CEZ Flower";
-			sprite = "FWR4A0";
-			width = 16;
-			height = 40;
-		}
-		1104
-		{
-			title = "Mace Spawnpoint";
-			sprite = "SMCEA0";
-			width = 17;
-			height = 34;
-		}
-		1105
-		{
-			title = "Chain with Maces Spawnpoint";
-			sprite = "SMCEA0";
-			width = 17;
-			height = 34;
-		}
-		1106
-		{
-			title = "Chained Spring Spawnpoint";
-			sprite = "YSPBA0";
-			width = 17;
-			height = 34;
-		}
-		1107
-		{
-			title = "Chain Spawnpoint";
-			sprite = "BMCHA0";
-			width = 17;
-			height = 34;
-		}
-		1108
-		{
-			arrow = 1;
-			title = "Hidden Chain Spawnpoint";
-			sprite = "internal:chain3";
-			width = 17;
-			height = 34;
-		}
-		1109
-		{
-			title = "Firebar Spawnpoint";
-			sprite = "BFBRA0";
-			width = 17;
-			height = 34;
-		}
-		1110
-		{
-			title = "Custom Mace Spawnpoint";
-			sprite = "SMCEA0";
-			width = 17;
-			height = 34;
-		}
-		1111
-		{
-			arrow = 1;
-			blocking = 2;
-			title = "Crawla Statue";
-			sprite = "CSTAA1";
-			width = 16;
-			height = 40;
-		}
-		1112
-		{
-			arrow = 1;
-			blocking = 2;
-			title = "Lance-a-Bot Statue";
-			sprite = "CBBSA1";
-			width = 32;
-			height = 72;
-		}
-		1114
-		{
-			title = "Pine Tree";
-			sprite = "PINEA0";
-			width = 16;
-			height = 628;
-		}
-		1115
-		{
-			title = "CEZ Shrub (Small)";
-			sprite = "CEZBA0";
-			width = 16;
-			height = 24;
-		}
-		1116
-		{
-			title = "CEZ Shrub (Large)";
-			sprite = "CEZBB0";
-			width = 32;
-			height = 48;
-		}
-		1117
-		{
-			arrow = 1;
-			title = "Pole Banner (Red)";
-			sprite = "BANRA0";
-			width = 40;
-			height = 224;
-		}
-		1118
-		{
-			arrow = 1;
-			title = "Pole Banner (Blue)";
-			sprite = "BANRA0";
-			width = 40;
-			height = 224;
-		}
-		1119
-		{
-			title = "Candle";
-			sprite = "CNDLA0";
-			width = 8;
-			height = 48;
-		}
-		1120
-		{
-			title = "Candle Pricket";
-			sprite = "CNDLB0";
-			width = 8;
-			height = 176;
-		}
-		1121
-		{
-			title = "Flame Holder";
-			sprite = "FLMHA0";
-			width = 24;
-			height = 80;
-		}
-		1122
-		{
-			title = "Fire Torch";
-			sprite = "CTRCA0";
-			width = 16;
-			height = 80;
-		}
-		1123
-		{
-			title = "Cannonball Launcher";
-			sprite = "internal:cannonball";
-			width = 8;
-			height = 16;
-		}
-		1124
-		{
-			blocking = 2;
-			title = "Cannonball";
-			sprite = "CBLLA0";
-			width = 20;
-			height = 40;
-		}
-		1125
-		{
-			title = "Brambles";
-			sprite = "CABRALAR";
-			width = 48;
-			height = 32;
-		}
-		1126
-		{
-			title = "Invisible Lockon Object";
-			sprite = "LCKNC0";
-			width = 16;
-			height = 32;
-		}
-		1127
-		{
-			title = "Spectator Eggrobo";
-			sprite = "EGR1A1";
-			width = 20;
-			height = 72;
-		}
-		1128
-		{
-			arrow = 1;
-			title = "Waving Flag (Red)";
-			sprite = "CFLGA0";
-			width = 8;
-			height = 208;
-		}
-		1129
-		{
-			arrow = 1;
-			title = "Waving Flag (Blue)";
-			sprite = "CFLGA0";
-			width = 8;
-			height = 208;
-		}
-	}
-
-	aridcanyon
-	{
-		color = 10; // Green
-		title = "Arid Canyon";
-
-		1200
-		{
-			title = "Tumbleweed (Big)";
-			sprite = "BTBLA0";
-			width = 24;
-			height = 48;
-		}
-		1201
-		{
-			title = "Tumbleweed (Small)";
-			sprite = "STBLA0";
-			width = 12;
-			height = 24;
-		}
-		1202
-		{
-			arrow = 1;
-			title = "Rock Spawner";
-			sprite = "ROIAA0";
-			width = 8;
-			height = 16;
-		}
-		1203
-		{
-			title = "Tiny Red Flower Cactus";
-			sprite = "CACTA0";
-			width = 13;
-			height = 24;
-		}
-		1204
-		{
-			title = "Small Red Flower Cactus";
-			sprite = "CACTB0";
-			width = 15;
-			height = 52;
-		}
-		1205
-		{
-			title = "Tiny Blue Flower Cactus";
-			sprite = "CACTC0";
-			width = 13;
-			height = 24;
-		}
-		1206
-		{
-			title = "Small Blue Flower Cactus";
-			sprite = "CACTD0";
-			width = 15;
-			height = 52;
-		}
-		1207
-		{
-			title = "Prickly Pear";
-			sprite = "CACTE0";
-			width = 32;
-			height = 96;
-		}
-		1208
-		{
-			title = "Barrel Cactus";
-			sprite = "CACTF0";
-			width = 20;
-			height = 128;
-		}
-		1209
-		{
-			title = "Tall Barrel Cactus";
-			sprite = "CACTG0";
-			width = 24;
-			height = 224;
-		}
-		1210
-		{
-			title = "Armed Cactus";
-			sprite = "CACTH0";
-			width = 24;
-			height = 256;
-		}
-		1211
-		{
-			title = "Ball Cactus";
-			sprite = "CACTI0";
-			width = 48;
-			height = 96;
-		}
-		1212
-		{
-			title = "Caution Sign";
-			sprite = "WWSGAR";
-			width = 22;
-			height = 64;
-		}
-		1213
-		{
-			title = "Cacti Sign";
-			sprite = "WWS2AR";
-			width = 22;
-			height = 64;
-		}
-		1214
-		{
-			title = "Sharp Turn Sign";
-			sprite = "WWS3ALAR";
-			width = 16;
-			height = 192;
-		}
-		1215
-		{
-			title = "Mine Oil Lamp";
-			sprite = "OILLA0";
-			width = 22;
-			height = 64;
-			hangs = 1;
-		}
-		1216
-		{
-			title = "TNT Barrel";
-			sprite = "BARRA1";
-			width = 24;
-			height = 63;
-		}
-		1217
-		{
-			title = "TNT Proximity Shell";
-			sprite = "REMTA0";
-			width = 64;
-			height = 40;
-		}
-		1218
-		{
-			title = "Dust Devil";
-			sprite = "TAZDCR";
-			width = 80;
-			height = 416;
-		}
-		1219
-		{
-			title = "Minecart Spawner";
-			sprite = "MCRTCLFR";
-			width = 22;
-			height = 32;
-		}
-		1220
-		{
-			title = "Minecart Stopper";
-			sprite = "MCRTIR";
-			width = 32;
-			height = 32;
-		}
-		1221
-		{
-			title = "Minecart Saloon Door";
-			sprite = "SALDARAL";
-			width = 96;
-			height = 160;
-		}
-		1222
-		{
-			title = "Train Cameo Spawner";
-			sprite = "TRAEBRBL";
-			width = 28;
-			height = 32;
-		}
-		1223
-		{
-			title = "Train Dust Spawner";
-			sprite = "ADSTA0";
-			width = 4;
-			height = 4;
-		}
-		1224
-		{
-			title = "Train Steam Spawner";
-			sprite = "STEAA0";
-			width = 4;
-			height = 4;
-		}
-		1229
-		{
-			title = "Minecart Switch Point";
-			sprite = "internal:zoom";
-			width = 8;
-			height = 16;
-		}
-		1230
-		{
-			title = "Tiny Cactus";
-			sprite = "CACTJ0";
-			width = 13;
-			height = 28;
-		}
-		1231
-		{
-			title = "Small Cactus";
-			sprite = "CACTK0";
-			width = 15;
-			height = 60;
-		}
-	}
-
-	redvolcano
-	{
-		color = 10; // Green
-		title = "Red Volcano";
-
-		1300
-		{
-			arrow = 1;
-			title = "Flame Jet (Horizontal)";
-			sprite = "internal:flameh";
-			width = 16;
-			height = 40;
-		}
-		1301
-		{
-			title = "Flame Jet (Vertical)";
-			sprite = "internal:flamev";
-			width = 16;
-			height = 40;
-		}
-		1302
-		{
-			title = "Spinning Flame Jet (Counter-Clockwise)";
-			sprite = "internal:flame2";
-			width = 16;
-			height = 24;
-		}
-		1303
-		{
-			title = "Spinning Flame Jet (Clockwise)";
-			sprite = "internal:flame1";
-			width = 16;
-			height = 24;
-		}
-		1304
-		{
-			title = "Lavafall";
-			sprite = "LFALF0";
-			width = 30;
-			height = 32;
-		}
-		1305
-		{
-			title = "Rollout Rock";
-			sprite = "PUMIA1A5";
-			width = 30;
-			height = 60;
-		}
-		1306
-		{
-			title = "Big Fern";
-			sprite = "JPLAB0";
-			width = 32;
-			height = 48;
-		}
-		1307
-		{
-			title = "Jungle Palm";
-			sprite = "JPLAC0";
-			width = 32;
-			height = 48;
-		}
-		1308
-		{
-			title = "Torch Flower";
-			sprite = "TFLOA0";
-			width = 14;
-			height = 110;
-		}
-		1309
-		{
-			title = "RVZ1 Wall Vine (Long)";
-			sprite = "WVINALAR";
-			width = 1;
-			height = 288;
-		}
-		1310
-		{
-			title = "RVZ1 Wall Vine (Short)";
-			sprite = "WVINBLBR";
-			width = 1;
-			height = 288;
-		}
-	}
-
-	botanicserenity
-	{
-		color = 10; // Green
-		title = "Botanic Serenity";
-		width = 16;
-		height = 32;
-		sprite = "BSZ1A0";
-		1400
-		{
-			title = "Tall Flower (Red)";
-			sprite = "BSZ1A0";
-		}
-		1401
-		{
-			title = "Tall Flower (Purple)";
-			sprite = "BSZ1B0";
-		}
-		1402
-		{
-			title = "Tall Flower (Blue)";
-			sprite = "BSZ1C0";
-		}
-		1403
-		{
-			title = "Tall Flower (Cyan)";
-			sprite = "BSZ1D0";
-		}
-		1404
-		{
-			title = "Tall Flower (Yellow)";
-			sprite = "BSZ1E0";
-		}
-		1405
-		{
-			title = "Tall Flower (Orange)";
-			sprite = "BSZ1F0";
-		}
-		1410
-		{
-			title = "Medium Flower (Red)";
-			sprite = "BSZ2A0";
-		}
-		1411
-		{
-			title = "Medium Flower (Purple)";
-			sprite = "BSZ2B0";
-		}
-		1412
-		{
-			title = "Medium Flower (Blue)";
-			sprite = "BSZ2C0";
-		}
-		1413
-		{
-			title = "Medium Flower (Cyan)";
-			sprite = "BSZ2D0";
-		}
-		1414
-		{
-			title = "Medium Flower (Yellow)";
-			sprite = "BSZ2E0";
-		}
-		1415
-		{
-			title = "Medium Flower (Orange)";
-			sprite = "BSZ2F0";
-		}
-		1420
-		{
-			title = "Short Flower (Red)";
-			sprite = "BSZ3A0";
-		}
-		1421
-		{
-			title = "Short Flower (Purple)";
-			sprite = "BSZ3B0";
-		}
-		1422
-		{
-			title = "Short Flower (Blue)";
-			sprite = "BSZ3C0";
-		}
-		1423
-		{
-			title = "Short Flower (Cyan)";
-			sprite = "BSZ3D0";
-		}
-		1424
-		{
-			title = "Short Flower (Yellow)";
-			sprite = "BSZ3E0";
-		}
-		1425
-		{
-			title = "Short Flower (Orange)";
-			sprite = "BSZ3F0";
-		}
-		1430
-		{
-			title = "Tulip (Red)";
-			sprite = "BST1A0";
-		}
-		1431
-		{
-			title = "Tulip (Purple)";
-			sprite = "BST2A0";
-		}
-		1432
-		{
-			title = "Tulip (Blue)";
-			sprite = "BST3A0";
-		}
-		1433
-		{
-			title = "Tulip (Cyan)";
-			sprite = "BST4A0";
-		}
-		1434
-		{
-			title = "Tulip (Yellow)";
-			sprite = "BST5A0";
-		}
-		1435
-		{
-			title = "Tulip (Orange)";
-			sprite = "BST6A0";
-		}
-		1440
-		{
-			title = "Cluster (Red)";
-			sprite = "BSZ5A0";
-		}
-		1441
-		{
-			title = "Cluster (Purple)";
-			sprite = "BSZ5B0";
-		}
-		1442
-		{
-			title = "Cluster (Blue)";
-			sprite = "BSZ5C0";
-		}
-		1443
-		{
-			title = "Cluster (Cyan)";
-			sprite = "BSZ5D0";
-		}
-		1444
-		{
-			title = "Cluster (Yellow)";
-			sprite = "BSZ5E0";
-		}
-		1445
-		{
-			title = "Cluster (Orange)";
-			sprite = "BSZ5F0";
-		}
-		1450
-		{
-			title = "Bush (Red)";
-			sprite = "BSZ6A0";
-		}
-		1451
-		{
-			title = "Bush (Purple)";
-			sprite = "BSZ6B0";
-		}
-		1452
-		{
-			title = "Bush (Blue)";
-			sprite = "BSZ6C0";
-		}
-		1453
-		{
-			title = "Bush (Cyan)";
-			sprite = "BSZ6D0";
-		}
-		1454
-		{
-			title = "Bush (Yellow)";
-			sprite = "BSZ6E0";
-		}
-		1455
-		{
-			title = "Bush (Orange)";
-			sprite = "BSZ6F0";
-		}
-		1460
-		{
-			title = "Vine (Red)";
-			sprite = "BSZ7A0";
-		}
-		1461
-		{
-			title = "Vine (Purple)";
-			sprite = "BSZ7B0";
-		}
-		1462
-		{
-			title = "Vine (Blue)";
-			sprite = "BSZ7C0";
-		}
-		1463
-		{
-			title = "Vine (Cyan)";
-			sprite = "BSZ7D0";
-		}
-		1464
-		{
-			title = "Vine (Yellow)";
-			sprite = "BSZ7E0";
-		}
-		1465
-		{
-			title = "Vine (Orange)";
-			sprite = "BSZ7F0";
-		}
-		1470
-		{
-			title = "BSZ Shrub";
-			sprite = "BSZ8A0";
-		}
-		1471
-		{
-			title = "BSZ Clover";
-			sprite = "BSZ8B0";
-		}
-		1473
-		{
-			title = "Palm Tree (Big)";
-			width = 16;
-			height = 160;
-			sprite = "BSZ8D0";
-		}
-		1475
-		{
-			title = "Palm Tree (Small)";
-			width = 16;
-			height = 80;
-			sprite = "BSZ8F0";
-		}
-	}
-
-	azuretemple
-	{
-		color = 10; // Green
-		title = "Azure Temple";
-
-		1500
-		{
-			arrow = 1;
-			blocking = 2;
-			title = "Glaregoyle";
-			sprite = "BGARA1";
-			width = 16;
-			height = 40;
-		}
-		1501
-		{
-			arrow = 1;
-			blocking = 2;
-			title = "Glaregoyle (Up)";
-			sprite = "BGARA1";
-			width = 16;
-			height = 40;
-		}
-		1502
-		{
-			arrow = 1;
-			blocking = 2;
-			title = "Glaregoyle (Down)";
-			sprite = "BGARA1";
-			width = 16;
-			height = 40;
-		}
-		1503
-		{
-			arrow = 1;
-			blocking = 2;
-			title = "Glaregoyle (Long)";
-			sprite = "BGARA1";
-			width = 16;
-			height = 40;
-		}
-		1504
-		{
-			title = "ATZ Target";
-			sprite = "RCRYB0";
-			width = 24;
-			height = 32;
-		}
-		1505
-		{
-			title = "Green Flame";
-			sprite = "CFLMA0E0";
-			width = 8;
-			height = 32;
-		}
-		1506
-		{
-			arrow = 1;
-			blocking = 2;
-			title = "Blue Gargoyle";
-			sprite = "BGARD1";
-			width = 16;
-			height = 40;
-		}
-	}
-
-	dreamhill
-	{
-		color = 10; // Green
-		title = "Dream Hill";
-
-		1600
-		{
-			title = "Spring Tree";
-			sprite = "TRE6A0";
-			width = 16;
-			height = 32;
-		}
-		1601
-		{
-			title = "Shleep";
-			sprite = "SHLPA0";
-			width = 24;
-			height = 32;
-		}
-		1602
-		{
-			title = "Nightopian";
-			sprite = "NTPNA1";
-			width = 16;
-			height = 40;
-		}
-	}
-
-	nightstrk
-	{
-		color = 13; // Pink
-		title = "NiGHTS Track";
-		width = 8;
-		height = 4096;
-		sprite = "UNKNA0";
-
-		1700
-		{
-			title = "Axis";
-			sprite = "internal:axis1";
-			circle = 1;
-		}
-		1701
-		{
-			title = "Axis Transfer";
-			sprite = "internal:axis2";
-		}
-		1702
-		{
-			title = "Axis Transfer Line";
-			sprite = "internal:axis3";
-		}
-		1710
-		{
-			title = "Ideya Capture";
-			sprite = "CAPSA0";
-			width = 72;
-			height = 144;
-		}
-	}
-
-	nights
-	{
-		color = 13; // Pink
-		title = "NiGHTS Items";
-		width = 16;
-		height = 32;
-
-		1703
-		{
-			title = "Ideya Drone";
-			sprite = "NDRNA1";
-			width = 16;
-			height = 56;
-		}
-		1704
-		{
-			arrow = 1;
-			title = "NiGHTS Bumper";
-			sprite = "NBMPG3G7";
-			width = 32;
-			height = 64;
-		}
-		1705
-		{
-			arrow = 1;
-			title = "Hoop (Generic)";
-			sprite = "HOOPA0";
-			width = 80;
-			height = 160;
-		}
-		1706
-		{
-			title = "Blue Sphere";
-			sprite = "SPHRA0";
-			width = 16;
-			height = 24;
-		}
-		1707
-		{
-			title = "Super Paraloop";
-			sprite = "NPRUA0";
-		}
-		1708
-		{
-			title = "Drill Refill";
-			sprite = "NPRUB0";
-		}
-		1709
-		{
-			title = "Nightopian Helper";
-			sprite = "NPRUC0";
-		}
-		1711
-		{
-			title = "Extra Time";
-			sprite = "NPRUD0";
-		}
-		1712
-		{
-			title = "Link Freeze";
-			sprite = "NPRUE0";
-		}
-		1713
-		{
-			arrow = 1;
-			title = "Hoop (Customizable)";
-			sprite = "HOOPA0";
-			width = 80;
-			height = 160;
-		}
-		1714
-		{
-			title = "Ideya Anchor Point";
-			sprite = "internal:axis1";
-			width = 8;
-			height = 16;
-		}
-	}
-
-	mario
-	{
-		color = 6; // Brown
-		title = "Mario";
-
-		1800
-		{
-			title = "Coin";
-			sprite = "COINA0";
-			width = 16;
-			height = 24;
-		}
-		1801
-		{
-			arrow = 1;
-			title = "Goomba";
-			sprite = "GOOMA0";
-			width = 24;
-			height = 32;
-		}
-		1802
-		{
-			arrow = 1;
-			title = "Goomba (Blue)";
-			sprite = "BGOMA0";
-			width = 24;
-			height = 32;
-		}
-		1803
-		{
-			title = "Fire Flower";
-			sprite = "FFWRB0";
-			width = 16;
-			height = 32;
-		}
-		1804
-		{
-			title = "Koopa Shell";
-			sprite = "SHLLA1";
-			width = 16;
-			height = 20;
-		}
-		1805
-		{
-			title = "Puma (Jumping Fireball)";
-			sprite = "PUMAA0";
-			width = 8;
-			height = 16;
-		}
-		1806
-		{
-			title = "King Bowser";
-			sprite = "KOOPA0";
-			width = 16;
-			height = 48;
-		}
-		1807
-		{
-			title = "Axe";
-			sprite = "MAXEA0";
-			width = 8;
-			height = 16;
-		}
-		1808
-		{
-			title = "Bush (Short)";
-			sprite = "MUS1A0";
-			width = 16;
-			height = 32;
-		}
-		1809
-		{
-			title = "Bush (Tall)";
-			sprite = "MUS2A0";
-			width = 16;
-			height = 32;
-		}
-		1810
-		{
-			title = "Toad";
-			sprite = "TOADA0";
-			width = 8;
-			height = 32;
-		}
-	}
-
-	christmasdisco
-	{
-		color = 10; // Green
-		title = "Christmas & Disco";
-
-		1850
-		{
-			title = "Christmas Pole";
-			sprite = "XMS1A0";
-			width = 16;
-			height = 40;
-		}
-		1851
-		{
-			title = "Candy Cane";
-			sprite = "XMS2A0";
-			width = 8;
-			height = 32;
-		}
-		1852
-		{
-			blocking = 2;
-			title = "Snowman";
-			sprite = "XMS3A0";
-			width = 16;
-			height = 64;
-		}
-		1853
-		{
-			blocking = 2;
-			title = "Snowman (With Hat)";
-			sprite = "XMS3B0";
-			width = 16;
-			height = 80;
-		}
-		1854
-		{
-			title = "Lamp Post";
-			sprite = "XMS4A0";
-			width = 8;
-			height = 120;
-		}
-		1855
-		{
-			title = "Lamp Post (Snow)";
-			sprite = "XMS4B0";
-			width = 8;
-			height = 120;
-		}
-		1856
-		{
-			title = "Hanging Star";
-			sprite = "XMS5A0";
-			width = 4;
-			height = 80;
-			hangs = 1;
-		}
-		1857
-		{
-			title = "Berry Bush (Snow)";
-			sprite = "BUS1B0";
-			width = 16;
-			height = 32;
-		}
-		1858
-		{
-			title = "Bush (Snow)";
-			sprite = "BUS2B0";
-			width = 16;
-			height = 32;
-		}
-		1859
-		{
-			title = "Blueberry Bush (Snow)";
-			sprite = "BUS3B0";
-			width = 16;
-			height = 32;
-		}
-		1875
-		{
-			title = "Disco Ball";
-			sprite = "DBALA0";
-			width = 16;
-			height = 54;
-			hangs = 1;
-		}
-		1876
-		{
-			arrow = 1;
-			blocking = 2;
-			title = "Eggman Disco Statue";
-			sprite = "ESTAB1";
-			width = 20;
-			height = 96;
-		}
-	}
-
-	stalagmites
-	{
-		color = 10; // Green
-		title = "Stalagmites";
-		width = 16;
-		height = 40;
-
-		1900
-		{
-			title = "Brown Stalagmite (Tall)";
-			sprite = "STLGA0";
-			width = 16;
-			height = 40;
-		}
-		1901
-		{
-			title = "Brown Stalagmite";
-			sprite = "STLGB0";
-			width = 16;
-			height = 40;
-		}
-		1902
-		{
-			title = "Orange Stalagmite (Tall)";
-			sprite = "STLGC0";
-			width = 16;
-			height = 40;
-		}
-		1903
-		{
-			title = "Orange Stalagmite";
-			sprite = "STLGD0";
-			width = 16;
-			height = 40;
-		}
-		1904
-		{
-			title = "Red Stalagmite (Tall)";
-			sprite = "STLGE0";
-			width = 16;
-			height = 40;
-		}
-		1905
-		{
-			title = "Red Stalagmite";
-			sprite = "STLGF0";
-			width = 16;
-			height = 40;
-		}
-		1906
-		{
-			title = "Gray Stalagmite (Tall)";
-			sprite = "STLGG0";
-			width = 24;
-			height = 96;
-		}
-		1907
-		{
-			title = "Gray Stalagmite";
-			sprite = "STLGH0";
-			width = 16;
-			height = 40;
-		}
-		1908
-		{
-			title = "Blue Stalagmite (Tall)";
-			sprite = "STLGI0";
-			width = 16;
-			height = 40;
-		}
-		1909
-		{
-			title = "Blue Stalagmite";
-			sprite = "STLGJ0";
-			width = 16;
-			height = 40;
-		}
-	}
-
-	hauntedheights
-	{
-		color = 10; // Green
-		title = "Haunted Heights";
-
-		2000
-		{
-			title = "Smashing Spikeball";
-			sprite = "FMCEA0";
-			width = 18;
-			height = 28;
-		}
-		2001
-		{
-			title = "HHZ Grass";
-			sprite = "HHZMA0";
-			width = 16;
-			height = 40;
-		}
-		2002
-		{
-			title = "HHZ Tentacle 1";
-			sprite = "HHZMB0";
-			width = 16;
-			height = 40;
-		}
-		2003
-		{
-			title = "HHZ Tentacle 2";
-			sprite = "HHZMC0";
-			width = 16;
-			height = 40;
-		}
-		2004
-		{
-			title = "HHZ Stalagmite (Tall)";
-			sprite = "HHZME0";
-			width = 16;
-			height = 40;
-		}
-		2005
-		{
-			title = "HHZ Stalagmite (Short)";
-			sprite = "HHZMF0";
-			width = 16;
-			height = 40;
-		}
-		2006
-		{
-			title = "Jack-o'-lantern 1";
-			sprite = "PUMKA0";
-			width = 16;
-			height = 40;
-		}
-		2007
-		{
-			title = "Jack-o'-lantern 2";
-			sprite = "PUMKB0";
-			width = 16;
-			height = 40;
-		}
-		2008
-		{
-			title = "Jack-o'-lantern 3";
-			sprite = "PUMKC0";
-			width = 16;
-			height = 40;
-		}
-		2009
-		{
-			title = "Purple Mushroom";
-			sprite = "SHRMD0";
-			width = 16;
-			height = 48;
-		}
-		2010
-		{
-			title = "HHZ Tree";
-			sprite = "HHPLC0";
-			width = 12;
-			height = 40;
-		}
-	}
-
-	frozenhillside
-	{
-		color = 10; // Green
-		title = "Frozen Hillside";
-
-		2100
-		{
-			title = "Ice Shard (Small)";
-			sprite = "FHZIA0";
-			width = 8;
-			height = 32;
-		}
-		2101
-		{
-			title = "Ice Shard (Large)";
-			sprite = "FHZIB0";
-			width = 8;
-			height = 32;
-		}
-		2102
-		{
-			title = "Crystal Tree (Aqua)";
-			sprite = "TRE3A0";
-			width = 20;
-			height = 200;
-		}
-		2103
-		{
-			title = "Crystal Tree (Pink)";
-			sprite = "TRE3B0";
-			width = 20;
-			height = 200;
-		}
-		2104
-		{
-			title = "Amy Cameo";
-			sprite = "ROSYA1";
-			width = 16;
-			height = 48;
-		}
-		2105
-		{
-			title = "Mistletoe";
-			sprite = "XMS6A0";
-			width = 52;
-			height = 106;
-		}
-	}
-
-	tutorial
-	{
-		color = 10; // Green
-		title = "Tutorial";
-
-		799
-		{
-			title = "Tutorial Plant";
-			sprite = "TUPFH0";
-			width = 40;
-			height = 144;
-		}
-	}
-
-	flickies
-	{
-		color = 10; // Green
-		title = "Flickies";
-		width = 8;
-		height = 20;
-
-		2200
-		{
-			title = "Bluebird";
-			sprite = "FL01A1";
-		}
-		2201
-		{
-			title = "Rabbit";
-			sprite = "FL02A1";
-		}
-		2202
-		{
-			title = "Chicken";
-			sprite = "FL03A1";
-		}
-		2203
-		{
-			title = "Seal";
-			sprite = "FL04A1";
-		}
-		2204
-		{
-			title = "Pig";
-			sprite = "FL05A1";
-		}
-		2205
-		{
-			title = "Chipmunk";
-			sprite = "FL06A1";
-		}
-		2206
-		{
-			title = "Penguin";
-			sprite = "FL07A1";
-		}
-		2207
-		{
-			title = "Fish";
-			sprite = "FL08A1";
-		}
-		2208
-		{
-			title = "Ram";
-			sprite = "FL09A1";
-		}
-		2209
-		{
-			title = "Puffin";
-			sprite = "FL10A1";
-		}
-		2210
-		{
-			title = "Cow";
-			sprite = "FL11A1";
-		}
-		2211
-		{
-			title = "Rat";
-			sprite = "FL12A1";
-		}
-		2212
-		{
-			title = "Bear";
-			sprite = "FL13A1";
-		}
-		2213
-		{
-			title = "Dove";
-			sprite = "FL14A1";
-		}
-		2214
-		{
-			title = "Cat";
-			sprite = "FL15A1";
-		}
-		2215
-		{
-			title = "Canary";
-			sprite = "FL16A1";
-		}
-		2216
-		{
-			title = "Spider";
-			sprite = "FS01A1";
-		}
-		2217
-		{
-			title = "Bat";
-			sprite = "FS02A0";
-		}
-	}
-}
-
 udmf
 {
-	editor
-	{
-		color = 15; // White
-		arrow = 1;
-		title = "<Editor Things>";
-		error = -1;
-		width = 8;
-		height = 16;
-		sort = 1;
-
-		3328 = "3D Mode Start";
-	}
 
 	starts
 	{
@@ -3974,7 +807,7 @@ udmf
 
 	bosses
 	{
-		color = 8; // Dark_Gray
+		color = 4; // Dark Red
 		arrow = 1;
 		title = "Bosses";
 
@@ -4352,98 +1185,198 @@ udmf
 	{
 		color = 14; // Yellow
 		title = "Rings and Weapon Panels";
-		width = 24;
+		width = 16;
 		height = 24;
 		sprite = "RINGA0";
-		arg0
-		{
-			title = "Float?";
-			type = 11;
-			enum = "yesno";
-		}
 
 		300
 		{
 			title = "Ring";
 			sprite = "RINGA0";
-			width = 16;
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		301
 		{
 			title = "Bounce Ring";
-			sprite = "internal:RNGBA0";
+			sprite = "RNGBA0";
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		302
 		{
 			title = "Rail Ring";
-			sprite = "internal:RNGRA0";
+			sprite = "RNGRA0";
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		303
 		{
 			title = "Infinity Ring";
-			sprite = "internal:RNGIA0";
+			sprite = "RNGIA0";
+			width = 24;
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		304
 		{
 			title = "Automatic Ring";
-			sprite = "internal:RNGAA0";
+			sprite = "RNGAA0";
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		305
 		{
 			title = "Explosion Ring";
-			sprite = "internal:RNGEA0";
+			sprite = "RNGEA0";
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		306
 		{
 			title = "Scatter Ring";
-			sprite = "internal:RNGSA0";
+			sprite = "RNGSA0";
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		307
 		{
 			title = "Grenade Ring";
-			sprite = "internal:RNGGA0";
+			sprite = "RNGGA0";
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		308
 		{
 			title = "CTF Team Ring (Red)";
-			sprite = "internal:RRNGA0";
-			width = 16;
+			sprite = "internal:TRNGA0R";
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		309
 		{
 			title = "CTF Team Ring (Blue)";
-			sprite = "internal:BRNGA0";
-			width = 16;
+			sprite = "internal:TRNGA0B";
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		330
 		{
 			title = "Bounce Ring Panel";
-			sprite = "internal:PIKBA0";
+			sprite = "PIKBA0";
+			width = 24;
+			height = 40;
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		331
 		{
 			title = "Rail Ring Panel";
-			sprite = "internal:PIKRA0";
+			sprite = "PIKRA0";
+			width = 24;
+			height = 40;
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		332
 		{
 			title = "Automatic Ring Panel";
-			sprite = "internal:PIKAA0";
+			sprite = "PIKAA0";
+			width = 24;
+			height = 40;
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		333
 		{
 			title = "Explosion Ring Panel";
-			sprite = "internal:PIKEA0";
+			sprite = "PIKEA0";
+			width = 24;
+			height = 40;
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		334
 		{
 			title = "Scatter Ring Panel";
-			sprite = "internal:PIKSA0";
+			sprite = "PIKSA0";
+			width = 24;
+			height = 40;
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 		335
 		{
 			title = "Grenade Ring Panel";
-			sprite = "internal:PIKGA0";
+			sprite = "PIKGA0";
+			width = 24;
+			height = 40;
+			arg0
+			{
+				title = "Float?";
+				type = 11;
+				enum = "yesno";
+			}
 		}
 	}
 
@@ -4452,7 +1385,7 @@ udmf
 		color = 10; // Light_Green
 		title = "Other Collectibles";
 		width = 16;
-		height = 32;
+		height = 24;
 		sort = 1;
 		sprite = "CEMGA0";
 
@@ -4522,6 +1455,7 @@ udmf
 		{
 			title = "Emerald Hunt Location";
 			sprite = "SHRDA0";
+			height = 32;
 			arg0
 			{
 				title = "Float?";
@@ -4562,16 +1496,16 @@ udmf
 		title = "Monitors";
 		width = 18;
 		height = 40;
-		arg0
-		{
-			title = "Death trigger tag";
-			type = 15;
-		}
 
 		400
 		{
 			title = "Super Ring (10 Rings)";
 			sprite = "TVRIA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4583,6 +1517,11 @@ udmf
 		{
 			title = "Pity Shield";
 			sprite = "TVPIA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4594,6 +1533,11 @@ udmf
 		{
 			title = "Attraction Shield";
 			sprite = "TVATA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4605,6 +1549,11 @@ udmf
 		{
 			title = "Force Shield";
 			sprite = "TVFOA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4616,6 +1565,11 @@ udmf
 		{
 			title = "Armageddon Shield";
 			sprite = "TVARA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4627,6 +1581,11 @@ udmf
 		{
 			title = "Whirlwind Shield";
 			sprite = "TVWWA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4638,6 +1597,11 @@ udmf
 		{
 			title = "Elemental Shield";
 			sprite = "TVELA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4649,6 +1613,11 @@ udmf
 		{
 			title = "Super Sneakers";
 			sprite = "TVSSA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4660,6 +1629,11 @@ udmf
 		{
 			title = "Invincibility";
 			sprite = "TVIVA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4671,6 +1645,11 @@ udmf
 		{
 			title = "Extra Life";
 			sprite = "TV1UA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4692,11 +1671,21 @@ udmf
 		{
 			title = "Eggman";
 			sprite = "TVEGA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		411
 		{
 			title = "Teleporter";
 			sprite = "TVMXA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4708,21 +1697,41 @@ udmf
 		{
 			title = "Gravity Boots";
 			sprite = "TVGVA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		414
 		{
 			title = "CTF Team Ring Monitor (Red)";
 			sprite = "TRRIA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		415
 		{
 			title = "CTF Team Ring Monitor (Blue)";
 			sprite = "TBRIA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		416
 		{
 			title = "Recycler";
 			sprite = "TVRCA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4734,16 +1743,31 @@ udmf
 		{
 			title = "Score (1,000 Points)";
 			sprite = "TV1KA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		419
 		{
 			title = "Score (10,000 Points)";
 			sprite = "TVTKA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		420
 		{
 			title = "Flame Shield";
 			sprite = "TVFLA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4755,6 +1779,11 @@ udmf
 		{
 			title = "Water Shield";
 			sprite = "TVBBA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4766,6 +1795,11 @@ udmf
 		{
 			title = "Lightning Shield";
 			sprite = "TVZPA0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 			arg1
 			{
 				title = "Respawn behavior";
@@ -4782,76 +1816,136 @@ udmf
 		title = "Monitors (Respawning)";
 		width = 20;
 		height = 44;
-		arg0
-		{
-			title = "Death trigger tag";
-			type = 15;
-		}
 
 		431
 		{
 			title = "Pity Shield (Respawn)";
 			sprite = "TVPIB0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		432
 		{
 			title = "Attraction Shield (Respawn)";
 			sprite = "TVATB0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		433
 		{
 			title = "Force Shield (Respawn)";
 			sprite = "TVFOB0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		434
 		{
 			title = "Armageddon Shield (Respawn)";
 			sprite = "TVARB0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		435
 		{
 			title = "Whirlwind Shield (Respawn)";
 			sprite = "TVWWB0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		436
 		{
 			title = "Elemental Shield (Respawn)";
 			sprite = "TVELB0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		437
 		{
 			title = "Super Sneakers (Respawn)";
 			sprite = "TVSSB0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		438
 		{
 			title = "Invincibility (Respawn)";
 			sprite = "TVIVB0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		440
 		{
 			title = "Eggman (Respawn)";
 			sprite = "TVEGB0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		443
 		{
 			title = "Gravity Boots (Respawn)";
 			sprite = "TVGVB0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		450
 		{
 			title = "Flame Shield (Respawn)";
 			sprite = "TVFLB0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		451
 		{
 			title = "Water Shield (Respawn)";
 			sprite = "TVBBB0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 		452
 		{
 			title = "Lightning Shield (Respawn)";
 			sprite = "TVZPB0";
+			arg0
+			{
+				title = "Death trigger tag";
+				type = 15;
+			}
 		}
 	}
 
@@ -5235,13 +2329,13 @@ udmf
 		{
 			arrow = 0;
 			title = "5 Vertical Rings (Yellow Spring)";
-			sprite = "RINGA0";
+			sprite = "internal:ringverticalyellow";
 		}
 		601
 		{
 			arrow = 0;
 			title = "5 Vertical Rings (Red Spring)";
-			sprite = "RINGA0";
+			sprite = "internal:ringverticalred";
 			height = 1024;
 		}
 		602
@@ -5259,41 +2353,47 @@ udmf
 		604
 		{
 			title = "Circle of Rings";
-			sprite = "RINGA0";
+			sprite = "internal:circlering";
 			width = 96;
 			height = 192;
+			centerhitbox = true;
 		}
 		605
 		{
 			title = "Circle of Rings (Big)";
-			sprite = "RINGA0";
+			sprite = "internal:circlebigring";
 			width = 192;
+			centerhitbox = true;
 		}
 		606
 		{
 			title = "Circle of Blue Spheres";
-			sprite = "SPHRA0";
+			sprite = "internal:circlesphere";
 			width = 96;
 			height = 192;
+			centerhitbox = true;
 		}
 		607
 		{
 			title = "Circle of Blue Spheres (Big)";
-			sprite = "SPHRA0";
+			sprite = "internal:circlebigsphere";
 			width = 192;
+			centerhitbox = true;
 		}
 		608
 		{
 			title = "Circle of Rings and Spheres";
-			sprite = "SPHRA0";
+			sprite = "internal:circleringsphere";
 			width = 96;
 			height = 192;
+			centerhitbox = true;
 		}
 		609
 		{
 			title = "Circle of Rings and Spheres (Big)";
-			sprite = "SPHRA0";
+			sprite = "internal:circlebigringsphere";
 			width = 192;
+			centerhitbox = true;
 		}
 		610
 		{
@@ -5322,6 +2422,7 @@ udmf
 			sprite = "RINGA0";
 			width = 96;
 			height = 192;
+			centerhitbox = true;
 			arg0
 			{
 				title = "Number of items";
@@ -5429,7 +2530,7 @@ udmf
 		756
 		{
 			title = "Blast Linedef Executor";
-			sprite = "TOADA0";
+			sprite = "internal:blastexec";
 			width = 32;
 			height = 16;
 			arg0
@@ -5441,7 +2542,7 @@ udmf
 		757
 		{
 			title = "Fan Particle Generator";
-			sprite = "PRTLA0";
+			sprite = "internal:fanparticles";
 			width = 8;
 			height = 16;
 			arg0
@@ -5490,11 +2591,6 @@ udmf
 			title = "PolyObject Spawn Point";
 			sprite = "internal:polycenter";
 		}
-		762
-		{
-			title = "PolyObject Spawn Point (Crush)";
-			sprite = "internal:polycentercrush";
-		}
 		780
 		{
 			title = "Skybox View Point";
@@ -5514,7 +2610,7 @@ udmf
 
 	greenflower
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Greenflower";
 
 		800
@@ -5619,7 +2715,7 @@ udmf
 
 	technohill
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Techno Hill";
 
 		900
@@ -5663,7 +2759,7 @@ udmf
 
 	deepsea
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Deep Sea";
 
 		1000
@@ -5823,7 +2919,7 @@ udmf
 
 	castleeggman
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Castle Eggman";
 
 		1100
@@ -6386,7 +3482,7 @@ udmf
 
 	aridcanyon
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Arid Canyon";
 
 		1200
@@ -6511,6 +3607,7 @@ udmf
 			sprite = "WWSGAR";
 			width = 22;
 			height = 64;
+			wallsprite = true;
 		}
 		1213
 		{
@@ -6518,6 +3615,7 @@ udmf
 			sprite = "WWS2AR";
 			width = 22;
 			height = 64;
+			wallsprite = true;
 		}
 		1214
 		{
@@ -6525,6 +3623,7 @@ udmf
 			sprite = "WWS3ALAR";
 			width = 16;
 			height = 192;
+			wallsprite = true;
 		}
 		1215
 		{
@@ -6644,7 +3743,7 @@ udmf
 
 	redvolcano
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Red Volcano";
 
 		1300
@@ -6789,7 +3888,7 @@ udmf
 
 	botanicserenity
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Botanic Serenity";
 		width = 16;
 		height = 32;
@@ -7032,7 +4131,7 @@ udmf
 
 	azuretemple
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Azure Temple";
 
 		1500
@@ -7144,7 +4243,7 @@ udmf
 
 	dreamhill
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Dream Hill";
 
 		1600
@@ -7178,8 +4277,8 @@ udmf
 
 	nightstrk
 	{
-		color = 13; // Pink
-		title = "NiGHTS Track";
+		color = 16; // Light Pink
+		title = "NiGHTS Track & Misc.";
 		width = 8;
 		height = 4096;
 		sprite = "UNKNA0";
@@ -7207,8 +4306,8 @@ udmf
 				type = 11;
 				enum
 				{
-					0 = "Clockwise";
-					1 = "Counterclockwise";
+					0 = "Counterclockwise";
+					1 = "Clockwise";
 				}
 			}
 		}
@@ -7238,30 +4337,6 @@ udmf
 				title = "Order";
 			}
 		}
-		1710
-		{
-			title = "Ideya Capture";
-			sprite = "CAPSA0";
-			width = 72;
-			height = 144;
-			arg0
-			{
-				title = "Mare";
-			}
-			arg1
-			{
-				title = "Required spheres";
-			}
-		}
-	}
-
-	nights
-	{
-		color = 13; // Pink
-		title = "NiGHTS Items";
-		width = 16;
-		height = 32;
-
 		1703
 		{
 			title = "Ideya Drone";
@@ -7294,11 +4369,46 @@ udmf
 			}
 			arg4
 			{
-				title = "Die upon time up?";
-				type = 11;
-				enum = "noyes";
+				title = "Die upon time up?";
+				type = 11;
+				enum = "noyes";
+			}
+		}
+		1710
+		{
+			title = "Ideya Capture";
+			sprite = "CAPSA0";
+			width = 72;
+			height = 144;
+			arg0
+			{
+				title = "Mare";
+			}
+			arg1
+			{
+				title = "Required spheres";
+			}
+		}
+		1714
+		{
+			title = "Ideya Anchor Point";
+			sprite = "internal:ideya";
+			width = 8;
+			height = 16;
+			arg0
+			{
+				title = "Mare";
 			}
 		}
+	}
+
+	nights
+	{
+		color = 13; // Pink
+		title = "NiGHTS Items";
+		width = 16;
+		height = 32;
+		
 		1704
 		{
 			arrow = 1;
@@ -7399,25 +4509,15 @@ udmf
 		{
 			arrow = 1;
 			title = "Hoop";
-			sprite = "HOOPA0";
+			sprite = "internal:nightshoop";
 			width = 80;
 			height = 160;
+			centerhitbox = true;
 			arg0
 			{
 				title = "Radius";
 			}
 		}
-		1714
-		{
-			title = "Ideya Anchor Point";
-			sprite = "internal:axis1";
-			width = 8;
-			height = 16;
-			arg0
-			{
-				title = "Mare";
-			}
-		}
 	}
 
 	mario
@@ -7528,7 +4628,7 @@ udmf
 
 	christmasdisco
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Christmas & Disco";
 
 		1850
@@ -7643,7 +4743,7 @@ udmf
 
 	stalagmites
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Stalagmites";
 		width = 16;
 		height = 40;
@@ -7722,7 +4822,7 @@ udmf
 
 	hauntedheights
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Haunted Heights";
 
 		2000
@@ -7828,7 +4928,7 @@ udmf
 
 	frozenhillside
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Frozen Hillside";
 
 		2100
@@ -7883,7 +4983,7 @@ udmf
 
 	tutorial
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Tutorial";
 
 		799
@@ -7901,65 +5001,170 @@ udmf
 
 	flickies
 	{
-		color = 10; // Green
+		color = 2; // Green
 		title = "Flickies";
 		width = 8;
 		height = 20;
-		arg0
-		{
-			title = "Radius";
-		}
-		arg1
-		{
-			title = "Flags";
-			type = 12;
-			enum
-			{
-				1 = "Move aimlessly";
-				2 = "No movement";
-				4 = "Hop";
-			}
-		}
 
 		2200
 		{
 			title = "Bluebird";
 			sprite = "FL01A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2201
 		{
 			title = "Rabbit";
 			sprite = "FL02A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2202
 		{
 			title = "Chicken";
 			sprite = "FL03A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2203
 		{
 			title = "Seal";
 			sprite = "FL04A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2204
 		{
 			title = "Pig";
 			sprite = "FL05A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2205
 		{
 			title = "Chipmunk";
 			sprite = "FL06A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2206
 		{
 			title = "Penguin";
 			sprite = "FL07A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2207
 		{
 			title = "Fish";
 			sprite = "FL08A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 			arg2
 			{
 				title = "Color";
@@ -7989,51 +5194,214 @@ udmf
 		{
 			title = "Ram";
 			sprite = "FL09A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2209
 		{
 			title = "Puffin";
 			sprite = "FL10A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2210
 		{
 			title = "Cow";
 			sprite = "FL11A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2211
 		{
 			title = "Rat";
 			sprite = "FL12A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2212
 		{
 			title = "Bear";
 			sprite = "FL13A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2213
 		{
 			title = "Dove";
 			sprite = "FL14A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2214
 		{
 			title = "Cat";
 			sprite = "FL15A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2215
 		{
 			title = "Canary";
 			sprite = "FL16A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2216
 		{
 			title = "Spider";
 			sprite = "FS01A1";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 		2217
 		{
 			title = "Bat";
 			sprite = "FS02A0";
+			arg0
+			{
+				title = "Radius";
+			}
+			arg1
+			{
+				title = "Flags";
+				type = 12;
+				enum
+				{
+					1 = "Move aimlessly";
+					2 = "No movement";
+					4 = "Hop";
+				}
+			}
 		}
 	}
-}
+	
+	editor
+	{
+		color = 15; // White
+		arrow = 1;
+		title = "3D Mode Start";
+		error = -1;
+		width = 8;
+		height = 16;
+		sort = 1;
+
+		3328 = "3D Mode Start";
+	}
+}
\ No newline at end of file
diff --git a/extras/conf/udb/SRB2_22Doom.cfg b/extras/conf/udb/SRB2_22Doom.cfg
deleted file mode 100644
index 9e733aa394581a8d48dab2c592e46008b2142f11..0000000000000000000000000000000000000000
--- a/extras/conf/udb/SRB2_22Doom.cfg
+++ /dev/null
@@ -1,32 +0,0 @@
-/************************************************************************\
-	Ultimate Doom Builder Game Configuration for Sonic Robo Blast 2 Version 2.2
-\************************************************************************/
-
-// This is required to prevent accidental use of a different configuration
-type = "Doom Builder 2 Game Configuration";
-
-// This is the title to show for this game
-game = "Sonic Robo Blast 2 - 2.2 (Doom format)";
-
-// This is the simplified game engine/sourceport name
-engine = "zdoom";
-
-// Settings common to all games and all map formats
-include("Includes\\SRB222_common.cfg", "common");
-
-// Settings common to Doom map format
-include("Includes\\SRB222_common.cfg", "mapformat_doom");
-
-include("Includes\\Game_SRB222.cfg");
-
-// Script lumps detection
-scriptlumpnames
-{
-	include("Includes\\SRB222_misc.cfg", "scriptlumpnames");
-}
-
-//Default things filters
-thingsfilters
-{
-	include("Includes\\SRB222_misc.cfg", "thingsfilters");
-}
\ No newline at end of file
diff --git a/src/Android.mk b/src/Android.mk
index a461da2242c7ab813831c95c1d442353756b0907..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 \
@@ -76,7 +71,7 @@ LOCAL_SRC_FILES :=      am_map.c \
                         android/i_system.c \
                         android/i_video.c
 
-LOCAL_CFLAGS += -DPLATFORM_ANDROID -DNONX86 -DLINUX -DDEBUGMODE -DNOASM -DNOPIX -DUNIXCOMMON -DNOTERMIOS
+LOCAL_CFLAGS += -DPLATFORM_ANDROID -DNONX86 -DLINUX -DDEBUGMODE -DNOPIX -DUNIXCOMMON -DNOTERMIOS
 
 LOCAL_MODULE := libsrb2
 
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 2f4467a322026aa73b4281b97daeae8800855deb..22c1def2752bd3b50aea361684b8707d5c078c30 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,27 +1,149 @@
-add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32)
+include(clang-tidy-default)
+
+add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
+	comptime.c
+	md5.c
+	config.h.in
+	string.c
+	d_main.c
+	dehacked.c
+	deh_soc.c
+	deh_lua.c
+	deh_tables.c
+	z_zone.c
+	f_finale.c
+	f_wipe.c
+	g_demo.c
+	g_game.c
+	g_input.c
+	am_map.c
+	command.c
+	console.c
+	hu_stuff.c
+	i_time.c
+	y_inter.c
+	st_stuff.c
+	m_aatree.c
+	m_anigif.c
+	m_argv.c
+	m_bbox.c
+	m_cheat.c
+	m_cond.c
+	m_easing.c
+	m_fixed.c
+	m_menu.c
+	m_misc.c
+	m_perfstats.c
+	m_random.c
+	m_queue.c
+	info.c
+	p_ceilng.c
+	p_enemy.c
+	p_floor.c
+	p_inter.c
+	p_lights.c
+	p_map.c
+	p_maputl.c
+	p_mobj.c
+	p_polyobj.c
+	p_saveg.c
+	p_setup.c
+	p_sight.c
+	p_spec.c
+	p_telept.c
+	p_tick.c
+	p_user.c
+	p_slopes.c
+	tables.c
+	r_bsp.c
+	r_data.c
+	r_draw.c
+	r_fps.c
+	r_main.c
+	r_plane.c
+	r_segs.c
+	r_skins.c
+	r_sky.c
+	r_splats.c
+	r_things.c
+	r_bbox.c
+	r_textures.c
+	r_patch.c
+	r_patchrotation.c
+	r_picformats.c
+	r_portal.c
+	screen.c
+	taglist.c
+	v_video.c
+	s_sound.c
+	sounds.c
+	w_wad.c
+	filesrch.c
+	lzf.c
+	b_bot.c
+	u_list.c
+	snake.c
+	lua_script.c
+	lua_baselib.c
+	lua_mathlib.c
+	lua_hooklib.c
+	lua_consolelib.c
+	lua_infolib.c
+	lua_mobjlib.c
+	lua_playerlib.c
+	lua_skinlib.c
+	lua_thinkerlib.c
+	lua_maplib.c
+	lua_taglib.c
+	lua_polyobjlib.c
+	lua_blockmaplib.c
+	lua_hudlib.c
+	lua_hudlib_drawlist.c
+	lua_inputlib.c
+)
 
-if("${CMAKE_COMPILER_IS_GNUCC}" AND "${CMAKE_SYSTEM_NAME}" MATCHES "Windows" AND NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}" AND NOT "${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}")
-	# On MinGW with internal libraries, link the standard library statically
-	target_link_options(SRB2SDL2 PRIVATE "-static")
-endif()
+# This updates the modification time for comptime.c at the
+# end of building so when the build system is ran next time,
+# that file gets flagged. comptime.c will always be rebuilt.
+#
+# This begs the question, why always rebuild comptime.c?
+# Some things like the git commit must be checked each time
+# the program is built. But the build system determines which
+# files should be rebuilt before anything else. So
+# comptime.c, which only needs to be rebuilt based on
+# information known at build time, must be told to rebuild
+# before that information can be ascertained.
+add_custom_command(
+	TARGET SRB2SDL2
+	POST_BUILD
+	COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${CMAKE_CURRENT_SOURCE_DIR}/comptime.c
+)
 
-# Core sources
-target_sourcefile(c)
-target_sources(SRB2SDL2 PRIVATE comptime.c md5.c config.h.in)
+# config.h is generated by this command. It should be done at
+# build time for accurate git information and before anything
+# that needs it, obviously.
+add_custom_target(_SRB2_reconf ALL
+	COMMAND ${CMAKE_COMMAND} -DGIT_EXECUTABLE=${GIT_EXECUTABLE} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}/.. -P ${CMAKE_CURRENT_SOURCE_DIR}/../cmake/Comptime.cmake
+	WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/.."
+)
+add_dependencies(SRB2SDL2 _SRB2_reconf)
 
-set(SRB2_ASM_SOURCES vid_copy.s)
+if("${CMAKE_COMPILER_IS_GNUCC}" AND "${CMAKE_SYSTEM_NAME}" MATCHES "Windows")
+	target_link_options(SRB2SDL2 PRIVATE "-Wl,--disable-dynamicbase")
+	if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}" AND NOT "${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}")
+		# On MinGW with internal libraries, link the standard library statically
+		target_link_options(SRB2SDL2 PRIVATE "-static")
+	endif()
+endif()
 
-set(SRB2_NASM_SOURCES tmap_mmx.nas tmap.nas)
+target_compile_features(SRB2SDL2 PRIVATE c_std_11 cxx_std_17)
 
 ### Configuration
-set(SRB2_CONFIG_USEASM OFF CACHE BOOL
-	"Enable NASM tmap implementation for software mode speedup.")
-set(SRB2_CONFIG_YASM OFF CACHE BOOL
-	"Use YASM in place of NASM.")
 set(SRB2_CONFIG_DEV_BUILD OFF CACHE BOOL
 	"Compile a development build of SRB2.")
 
 add_subdirectory(blua)
+add_subdirectory(netcode)
 
 # OS macros
 if (UNIX)
@@ -74,33 +196,6 @@ if("${SRB2_CONFIG_HWRENDER}")
 	endif()
 endif()
 
-if(${SRB2_CONFIG_USEASM})
-	#SRB2_ASM_FLAGS can be used to pass flags to either nasm or yasm.
-	if("${CMAKE_SYSTEM_NAME}" MATCHES "Linux")
-		set(SRB2_ASM_FLAGS "-DLINUX ${SRB2_ASM_FLAGS}")
-	endif()
-
-	if(${SRB2_CONFIG_YASM})
-		set(CMAKE_ASM_YASM_SOURCE_FILE_EXTENSIONS ${CMAKE_ASM_YASM_SOURCE_FILE_EXTENSIONS} nas)
-		set(CMAKE_ASM_YASM_FLAGS "${SRB2_ASM_FLAGS}" CACHE STRING "Flags used by the assembler during all build types.")
-		enable_language(ASM_YASM)
-	else()
-		set(CMAKE_ASM_NASM_SOURCE_FILE_EXTENSIONS ${CMAKE_ASM_NASM_SOURCE_FILE_EXTENSIONS} nas)
-		set(CMAKE_ASM_NASM_FLAGS "${SRB2_ASM_FLAGS}" CACHE STRING "Flags used by the assembler during all build types.")
-		enable_language(ASM_NASM)
-	endif()
-
-	set(SRB2_USEASM ON)
-	target_compile_definitions(SRB2SDL2 PRIVATE -DUSEASM)
-	target_compile_options(SRB2SDL2 PRIVATE -msse3 -mfpmath=sse)
-
-	target_sources(SRB2SDL2 PRIVATE ${SRB2_ASM_SOURCES}
-		${SRB2_NASM_SOURCES})
-else()
-	set(SRB2_USEASM OFF)
-	target_compile_definitions(SRB2SDL2 PRIVATE -DNONX86 -DNORUSEASM)
-endif()
-
 # Targets
 
 # If using CCACHE, then force it.
@@ -289,6 +384,9 @@ if(SRB2_CONFIG_PROFILEMODE AND "${CMAKE_C_COMPILER_ID}" STREQUAL "GNU")
 endif()
 
 add_subdirectory(sdl)
+if(SRB2_CONFIG_ENABLE_TESTS)
+	add_subdirectory(tests)
+endif()
 
 # strip debug symbols into separate file when using gcc.
 # to be consistent with Makefile, don't generate for OS X.
@@ -329,3 +427,11 @@ if("${CMAKE_SYSTEM_NAME}" STREQUAL Windows AND NOT "${SRB2_CONFIG_INTERNAL_LIBRA
 		COMMENT "Copying runtime DLLs"
 	)
 endif()
+
+# Setup clang-tidy
+if(SRB2_CONFIG_ENABLE_CLANG_TIDY_C)
+	target_set_default_clang_tidy(SRB2SDL2 C "-*,clang-analyzer-*,-clang-analyzer-cplusplus-*")
+endif()
+if(SRB2_CONFIG_ENABLE_CLANG_TIDY_CXX)
+	target_set_default_clang_tidy(SRB2SDL2 CXX "-*,clang-analyzer-*,modernize-*")
+endif()
diff --git a/src/Makefile b/src/Makefile
index 36b1a7efabea7416f42c624fc03ab93d2a15c05f..a0a18be769e0cc64b98d8774b4b3a1aeb3c177ad 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -47,8 +47,6 @@
 # HAVE_MINIUPNPC=1 - Enable automated port forwarding.
 #                    Already enabled by default for 32-bit
 #                    Windows.
-# NOASM=1 - Disable hand optimized assembly code for the
-#           Software renderer.
 # NOPNG=1 - Disable PNG graphics support. (TODO: double
 #           check netplay compatible.)
 # NOCURL=1 - Disable libcurl--HTTP capability.
@@ -64,7 +62,6 @@
 #
 # Netplay incompatible
 # --------------------
-# NONET=1 - Disable online capability.
 # NOMD5=1 - Disable MD5 checksum (validation tool).
 # NOPOSTPROCESSING=1 - ?
 # MOBJCONSISTANCY=1 - ??
@@ -88,7 +85,6 @@
 #        executable.
 # WINDOWSHELL=1 - Use Windows commands.
 # PREFIX= - Prefix to many commands, for cross compiling.
-# YASM=1 - Use Yasm instead of NASM assembler.
 # STABS=1 - ?
 # ECHO=1 - Print out each command in the build process.
 # NOECHOFILENAMES=1 - Don't print out each that is being
@@ -144,25 +140,9 @@ endif
 
 OBJDUMP_OPTS?=--wide --source --line-numbers
 
-OBJCOPY:=$(call Prefix,objcopy)
-OBJDUMP:=$(call Prefix,objdump)
-WINDRES:=$(call Prefix,windres)
-
-ifdef YASM
-NASM?=yasm
-else
-NASM?=nasm
-endif
-
-ifdef YASM
-ifdef STABS
-NASMOPTS?=-g stabs
-else
-NASMOPTS?=-g dwarf2
-endif
-else
-NASMOPTS?=-g
-endif
+OBJCOPY?=$(call Prefix,objcopy)
+OBJDUMP?=$(call Prefix,objdump)
+WINDRES?=$(call Prefix,windres)
 
 GZIP?=gzip
 GZIP_OPTS?=-9 -f -n
@@ -187,8 +167,6 @@ makedir:=../make
 opts:=-DCOMPVERSION -g
 libs:=
 
-nasm_format:=
-
 # This is a list of variables names, of which if defined,
 # also defines the name as a macro to the compiler.
 passthru_opts:=
@@ -208,6 +186,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)))
@@ -316,7 +295,6 @@ endif
 
 LD:=$(CC)
 cc:=$(cc) $(opts)
-nasm=$(NASM) $(NASMOPTS) -f $(nasm_format)
 ifdef UPX
 upx=$(UPX) $(UPX_OPTS)
 endif
@@ -393,7 +371,6 @@ $(objdir)/%.$(1) : %.$(2) | $$$$(@D)/
 endef
 
 $(eval $(call _recipe,o,c,$(cc) -c -o $$@ $$<))
-$(eval $(call _recipe,o,nas,$(nasm) -o $$@ $$<))
 $(eval $(call _recipe,o,s,$(cc) $(asflags) -c -o $$@ $$<))
 $(eval $(call _recipe,res,rc,$(windres) -i $$< -o $$@))
 
@@ -414,3 +391,5 @@ ifdef WINDOWSHELL
 else
 	@:
 endif
+
+#$(warning The handwritten GNU Makefile for SRB2 is deprecated, and may be removed in the future. Please consider switching to CMake.)
diff --git a/src/Makefile.d/detect.mk b/src/Makefile.d/detect.mk
index aca4987213419adb6d93056a8413c2d0330ae057..9e27369462b00c407f02cb95dbda22ef284a91d1 100644
--- a/src/Makefile.d/detect.mk
+++ b/src/Makefile.d/detect.mk
@@ -56,15 +56,18 @@ endif
 
 # This must have high to low order.
 gcc_versions:=\
-	102 101\
-	93 92 91\
-	84 83 82 81\
-	75 74 73 72 71\
-	64 63 62 61\
-	55 54 53 52 51\
+	132 131 130\
+	123 122 121 120\
+	114 113 112 111 110\
+	105 104 103 102 101 100\
+	95 94 93 92 91 90\
+	85 84 83 82 81 80\
+	75 74 73 72 71 70\
+	64 63 62 61 60\
+	55 54 53 52 51 50\
 	49 48 47 46 45 44 43 42 41 40
 
-latest_gcc_version:=10.2
+latest_gcc_version:=13.2
 
 # Automatically set version flag, but not if one was
 # manually set. And don't bother if this is a clean only
@@ -74,13 +77,18 @@ ifeq (,$(call Wildvar,GCC% destructive))
 # can't use $(CC) --version here since that uses argv[0] to display the name
 # also gcc outputs the information to stderr, so I had to do 2>&1
 # this program really doesn't like identifying itself
-version:=$(shell $(CC) -v 2>&1)
+shellversion:=$(shell $(CC) -v 2>&1)
+# Try to remove "-win32"
+version:=$(subst -win32,.0,$(shellversion))
 
 # check if this is in fact GCC
 ifneq (,$(findstring gcc version,$(version)))
 
 # in stark contrast to the name, gcc will give me a nicely formatted version number for free
-version:=$(shell $(CC) -dumpfullversion)
+shellversion:=$(shell $(CC) -dumpfullversion)
+
+# Try to remove "-win32"
+version:=$(subst -win32,.0,$(shellversion))
 
 # Turn version into words of major, minor
 v:=$(subst ., ,$(version))
diff --git a/src/Makefile.d/features.mk b/src/Makefile.d/features.mk
index 8ba33383bb2f0c8169e92f91c6c8fea25c6c852f..8b713718cdd604ff83193e560566455453f4a7ff 100644
--- a/src/Makefile.d/features.mk
+++ b/src/Makefile.d/features.mk
@@ -1,75 +1,66 @@
-#
-# Makefile for feature flags.
-#
-
-passthru_opts+=\
-	NONET NO_IPV6 NOHW NOMD5 NOPOSTPROCESSING\
-	MOBJCONSISTANCY PACKETDROP ZDEBUG\
-	HAVE_MINIUPNPC\
-
-# build with debugging information
-ifdef DEBUGMODE
-PACKETDROP=1
-opts+=-DPARANOIA -DRANGECHECK
-endif
-
-ifndef NOHW
-opts+=-DHWRENDER
-sources+=$(call List,hardware/Sourcefile)
-endif
-
-ifndef NOASM
-ifndef NONX86
-sources+=tmap.nas tmap_mmx.nas
-opts+=-DUSEASM
-endif
-endif
-
-ifndef NOMD5
-sources+=md5.c
-endif
-
-ifndef NOZLIB
-ifndef NOPNG
-ifdef PNG_PKGCONFIG
-$(eval $(call Use_pkg_config,PNG_PKGCONFIG))
-else
-PNG_CONFIG?=$(call Prefix,libpng-config)
-$(eval $(call Configure,PNG,$(PNG_CONFIG) \
-	$(if $(PNG_STATIC),--static),,--ldflags))
-endif
-ifdef LINUX
-opts+=-D_LARGEFILE64_SOURCE
-endif
-opts+=-DHAVE_PNG
-sources+=apng.c
-endif
-endif
-
-ifndef NONET
-ifndef NOCURL
-CURLCONFIG?=curl-config
-$(eval $(call Configure,CURL,$(CURLCONFIG)))
-opts+=-DHAVE_CURL
-endif
-endif
-
-ifdef HAVE_MINIUPNPC
-libs+=-lminiupnpc
-endif
-
-# (Valgrind is a memory debugger.)
-ifdef VALGRIND
-VALGRIND_PKGCONFIG?=valgrind
-$(eval $(call Use_pkg_config,VALGRIND))
-ZDEBUG=1
-opts+=-DHAVE_VALGRIND
-endif
-
-default_packages:=\
-	GME/libgme/LIBGME\
-	OPENMPT/libopenmpt/LIBOPENMPT\
-	ZLIB/zlib\
-
-$(foreach p,$(default_packages),\
-	$(eval $(call Check_pkg_config,$(p))))
+#
+# Makefile for feature flags.
+#
+
+passthru_opts+=\
+	NO_IPV6 NOHW NOMD5 NOPOSTPROCESSING\
+	MOBJCONSISTANCY PACKETDROP ZDEBUG\
+	HAVE_MINIUPNPC\
+
+# build with debugging information
+ifdef DEBUGMODE
+PACKETDROP=1
+opts+=-DPARANOIA -DRANGECHECK
+endif
+
+ifndef NOHW
+opts+=-DHWRENDER
+sources+=$(call List,hardware/Sourcefile)
+endif
+
+ifndef NOMD5
+sources+=md5.c
+endif
+
+ifndef NOZLIB
+ifndef NOPNG
+ifdef PNG_PKGCONFIG
+$(eval $(call Use_pkg_config,PNG_PKGCONFIG))
+else
+PNG_CONFIG?=$(call Prefix,libpng-config)
+$(eval $(call Configure,PNG,$(PNG_CONFIG) \
+	$(if $(PNG_STATIC),--static),,--ldflags))
+endif
+ifdef LINUX
+opts+=-D_LARGEFILE64_SOURCE
+endif
+opts+=-DHAVE_PNG
+sources+=apng.c
+endif
+endif
+
+ifndef NOCURL
+CURLCONFIG?=curl-config
+$(eval $(call Configure,CURL,$(CURLCONFIG)))
+opts+=-DHAVE_CURL
+endif
+
+ifdef HAVE_MINIUPNPC
+libs+=-lminiupnpc
+endif
+
+# (Valgrind is a memory debugger.)
+ifdef VALGRIND
+VALGRIND_PKGCONFIG?=valgrind
+$(eval $(call Use_pkg_config,VALGRIND))
+ZDEBUG=1
+opts+=-DHAVE_VALGRIND
+endif
+
+default_packages:=\
+	GME/libgme/LIBGME\
+	OPENMPT/libopenmpt/LIBOPENMPT\
+	ZLIB/zlib\
+
+$(foreach p,$(default_packages),\
+	$(eval $(call Check_pkg_config,$(p))))
diff --git a/src/Makefile.d/nix.mk b/src/Makefile.d/nix.mk
index 767b64c12be4bf42fede8e07e80cba68151ef92a..9adf3f0f14fdc2b052e48053cabedee48a1a7edd 100644
--- a/src/Makefile.d/nix.mk
+++ b/src/Makefile.d/nix.mk
@@ -9,10 +9,6 @@ opts+=-DUNIXCOMMON -DLUA_USE_POSIX
 # instead of addresses
 libs+=-lm -rdynamic
 
-ifndef nasm_format
-nasm_format:=elf -DLINUX
-endif
-
 ifndef NOHW
 opts+=-I/usr/X11R6/include
 libs+=-L/usr/X11R6/lib
@@ -29,13 +25,12 @@ endif
 # Tested by Steel, as of release 2.2.8.
 ifdef FREEBSD
 opts+=-I/usr/X11R6/include -DLINUX -DFREEBSD
-libs+=-L/usr/X11R6/lib -lipx -lkvm
+libs+=-L/usr/X11R6/lib -lkvm -lexecinfo
 endif
 
 # FIXME: UNTESTED
 #ifdef SOLARIS
 #NOIPX=1
-#NOASM=1
 #opts+=-I/usr/local/include -I/opt/sfw/include \
 #		-DSOLARIS -DINADDR_NONE=INADDR_ANY -DBSD_COMP
 #libs+=-L/opt/sfw/lib -lsocket -lnsl
diff --git a/src/Makefile.d/platform.mk b/src/Makefile.d/platform.mk
index c5ac71a20adc24766a961cb544af54cf3a1b59b1..d19143e4cf6040dc161b201553db3942b123ee39 100644
--- a/src/Makefile.d/platform.mk
+++ b/src/Makefile.d/platform.mk
@@ -39,7 +39,6 @@ else ifdef SOLARIS # FIXME: UNTESTED
 UNIX=1
 platform=solaris
 else ifdef CYGWIN32 # FIXME: UNTESTED
-nasm_format=win32
 platform=cygwin
 else ifdef MINGW
 ifdef MINGW64
diff --git a/src/Makefile.d/sdl.mk b/src/Makefile.d/sdl.mk
index 99ca624e69f2f18c10625c93585f14681636f36e..a1bfa33038bbacebada85790a7565fceb9440985 100644
--- a/src/Makefile.d/sdl.mk
+++ b/src/Makefile.d/sdl.mk
@@ -56,13 +56,6 @@ SDL_LDFLAGS?=$(shell $(SDL_CONFIG) \
 $(eval $(call Propogate_flags,SDL))
 endif
 
-# use the x86 asm code
-ifndef CYGWIN32
-ifndef NOASM
-USEASM=1
-endif
-endif
-
 ifdef MINGW
 ifndef NOSDLMAIN
 SDLMAIN=1
diff --git a/src/Makefile.d/win32.mk b/src/Makefile.d/win32.mk
index 0e48ed68359523e70b4ba0a6d8eade4b81d1d9ca..a858881c0a0508c0eb3addfc456bdd042656108d 100644
--- a/src/Makefile.d/win32.mk
+++ b/src/Makefile.d/win32.mk
@@ -17,8 +17,6 @@ sources+=win32/Srb2win.rc
 opts+=-DSTDC_HEADERS
 libs+=-ladvapi32 -lkernel32 -lmsvcrt -luser32
 
-nasm_format:=win32
-
 SDL?=1
 
 ifndef NOHW
@@ -35,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 de90bb60910286242ae4b62b41af5d9ad57706ad..6ed1f3b4c03119f44c877e5ca1013e7bba581605 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
@@ -64,6 +60,7 @@ r_skins.c
 r_sky.c
 r_splats.c
 r_things.c
+r_bbox.c
 r_textures.c
 r_patch.c
 r_patchrotation.c
@@ -76,12 +73,10 @@ s_sound.c
 sounds.c
 w_wad.c
 filesrch.c
-mserv.c
-http-mserv.c
-i_tcp.c
 lzf.c
-vid_copy.s
 b_bot.c
+u_list.c
+snake.c
 lua_script.c
 lua_baselib.c
 lua_mathlib.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/android/i_system.c b/src/android/i_system.c
index ff8b88de539353bfd93ae612a3739770b267c416..9d798d452fb1b36460553fd35870894be2a3e406 100644
--- a/src/android/i_system.c
+++ b/src/android/i_system.c
@@ -278,4 +278,26 @@ char *I_ClipboardPaste(void)
 
 void I_RegisterSysCommands(void) {}
 
+// This is identical to the SDL implementation.
+size_t I_GetRandomBytes(char *destination, size_t count)
+{
+  FILE *rndsource;
+  size_t actual_bytes;
+
+  if (!(rndsource = fopen("/dev/urandom", "r")))
+	  if (!(rndsource = fopen("/dev/random", "r")))
+		  actual_bytes = 0;
+
+  if (rndsource)
+  {
+	  actual_bytes = fread(destination, 1, count, rndsource);
+	  fclose(rndsource);
+  }
+
+  if (actual_bytes == 0)
+    I_OutputMsg("I_GetRandomBytes(): couldn't get any random bytes");
+
+  return actual_bytes;
+}
+
 #include "../sdl/dosstr.c"
diff --git a/src/asm_defs.inc b/src/asm_defs.inc
deleted file mode 100644
index 48f8da0d8f582f28ad09674eec97b4af840f40b9..0000000000000000000000000000000000000000
--- a/src/asm_defs.inc
+++ /dev/null
@@ -1,43 +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  asm_defs.inc
-/// \brief must match the C structures
-
-#ifndef __ASM_DEFS__
-#define __ASM_DEFS__
-
-// this makes variables more noticable,
-// and make the label match with C code
-
-// Linux, unlike DOS, has no "_" 19990119 by Kin
-// and nasm needs .data code segs under linux 20010210 by metzgermeister
-// FIXME: nasm ignores these settings, so I put the macros into the makefile
-#ifdef __ELF__
-#define C(label) label
-#define CODE_SEG .data
-#else
-#define C(label) _##label
-#define CODE_SEG .text
-#endif
-
-/* This is a more readable way to access the arguments passed from C code   */
-/* PLEASE NOTE: it is supposed that all arguments passed from C code are    */
-/*              32bit integer (INT32, long, and most *pointers)               */
-#define ARG1      8(%ebp)
-#define ARG2      12(%ebp)
-#define ARG3      16(%ebp)
-#define ARG4      20(%ebp)
-#define ARG5      24(%ebp)
-#define ARG6      28(%ebp)
-#define ARG7      32(%ebp)
-#define ARG8      36(%ebp)
-#define ARG9      40(%ebp)      //(c)tm ... Allegro by Shawn Hargreaves.
-
-#endif
diff --git a/src/b_bot.c b/src/b_bot.c
index d1465f891d481dd8314507b8f6b5e2c795abb1dd..57f7623042d318df981af823ac59d1865c44fb75 100644
--- a/src/b_bot.c
+++ b/src/b_bot.c
@@ -631,7 +631,8 @@ void B_HandleFlightIndicator(player_t *player)
 	}
 
 	// otherwise, update its visibility
-	if (P_IsLocalPlayer(player->botleader))
+	tails->hnext->drawonlyforplayer = player->botleader; // Hide it from the other player in splitscreen, and yourself when spectating
+	if (P_IsLocalPlayer(player->botleader)) // Only display it on your own view. Don't display it for spectators
 		tails->hnext->flags2 &= ~MF2_DONTDRAW;
 	else
 		tails->hnext->flags2 |= MF2_DONTDRAW;
diff --git a/src/blua/CMakeLists.txt b/src/blua/CMakeLists.txt
index 4e9c67d2f348a8bfed899e4002d25136284b031f..892bf534addc810434bdd00960ebe7a92ef7bde4 100644
--- a/src/blua/CMakeLists.txt
+++ b/src/blua/CMakeLists.txt
@@ -1 +1,28 @@
-target_sourcefile(c)
+target_sources(SRB2SDL2 PRIVATE
+	lapi.c
+	lbaselib.c
+	ldo.c
+	lfunc.c
+	linit.c
+	liolib.c
+	llex.c
+	lmem.c
+	lobject.c
+	lstate.c
+	lstrlib.c
+	ltablib.c
+	lundump.c
+	lzio.c
+	lauxlib.c
+	lcode.c
+	ldebug.c
+	ldump.c
+	lgc.c
+	lopcodes.c
+	lparser.c
+	lstring.c
+	ltable.c
+	ltm.c
+	lvm.c
+	loslib.c
+)
diff --git a/src/blua/Sourcefile b/src/blua/Sourcefile
index f99c89c8dfb8e8b5da643cb2c8625a764e84580d..dae94310981fce03d6aefd30b6e7e5781a4a4eae 100644
--- a/src/blua/Sourcefile
+++ b/src/blua/Sourcefile
@@ -23,3 +23,4 @@ lstring.c
 ltable.c
 ltm.c
 lvm.c
+loslib.c
diff --git a/src/blua/linit.c b/src/blua/linit.c
index d17390b20a01dca0cff486c4c25cc5e31cd9570a..dcf05d9f2c925e49457514c57f99afe7bf66c1d8 100644
--- a/src/blua/linit.c
+++ b/src/blua/linit.c
@@ -18,6 +18,7 @@ static const luaL_Reg lualibs[] = {
   {"", luaopen_base},
   {LUA_TABLIBNAME, luaopen_table},
   {LUA_IOLIBNAME, luaopen_io},
+  {LUA_OSLIBNAME, luaopen_os},
   {LUA_STRLIBNAME, luaopen_string},
   {NULL, NULL}
 };
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/blua/loslib.c b/src/blua/loslib.c
new file mode 100644
index 0000000000000000000000000000000000000000..ba2e1e3846c77809d0fe06673243b537417ea4ea
--- /dev/null
+++ b/src/blua/loslib.c
@@ -0,0 +1,167 @@
+/*
+** $Id: loslib.c,v 1.19.1.3 2008/01/18 16:38:18 roberto Exp $
+** Standard Operating System library
+** See Copyright Notice in lua.h
+*/
+
+
+#include <errno.h>
+#include <locale.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#define loslib_c
+#define LUA_LIB
+
+#include "lua.h"
+
+#include "lauxlib.h"
+#include "lualib.h"
+
+
+static int os_clock (lua_State *L) {
+  lua_pushnumber(L, ((lua_Number)clock())/(lua_Number)CLOCKS_PER_SEC);
+  return 1;
+}
+
+
+/*
+** {======================================================
+** Time/Date operations
+** { year=%Y, month=%m, day=%d, hour=%H, min=%M, sec=%S,
+**   wday=%w+1, yday=%j, isdst=? }
+** =======================================================
+*/
+
+static void setfield (lua_State *L, const char *key, int value) {
+  lua_pushinteger(L, value);
+  lua_setfield(L, -2, key);
+}
+
+static void setboolfield (lua_State *L, const char *key, int value) {
+  if (value < 0)  /* undefined? */
+    return;  /* does not set field */
+  lua_pushboolean(L, value);
+  lua_setfield(L, -2, key);
+}
+
+static int getboolfield (lua_State *L, const char *key) {
+  int res;
+  lua_getfield(L, -1, key);
+  res = lua_isnil(L, -1) ? -1 : lua_toboolean(L, -1);
+  lua_pop(L, 1);
+  return res;
+}
+
+
+static int getfield (lua_State *L, const char *key, int d) {
+  int res;
+  lua_getfield(L, -1, key);
+  if (lua_isnumber(L, -1))
+    res = (int)lua_tointeger(L, -1);
+  else {
+    if (d < 0)
+      return luaL_error(L, "field " LUA_QS " missing in date table", key);
+    res = d;
+  }
+  lua_pop(L, 1);
+  return res;
+}
+
+
+static int os_date (lua_State *L) {
+  const char *s = luaL_optstring(L, 1, "%c");
+  time_t t = luaL_opt(L, (time_t)luaL_checknumber, 2, time(NULL));
+  struct tm *stm;
+  if (*s == '!') {  /* UTC? */
+    stm = gmtime(&t);
+    s++;  /* skip `!' */
+  }
+  else
+    stm = localtime(&t);
+  if (stm == NULL)  /* invalid date? */
+    lua_pushnil(L);
+  else if (strcmp(s, "*t") == 0) {
+    lua_createtable(L, 0, 9);  /* 9 = number of fields */
+    setfield(L, "sec", stm->tm_sec);
+    setfield(L, "min", stm->tm_min);
+    setfield(L, "hour", stm->tm_hour);
+    setfield(L, "day", stm->tm_mday);
+    setfield(L, "month", stm->tm_mon+1);
+    setfield(L, "year", stm->tm_year+1900);
+    setfield(L, "wday", stm->tm_wday+1);
+    setfield(L, "yday", stm->tm_yday+1);
+    setboolfield(L, "isdst", stm->tm_isdst);
+  }
+  else {
+    char cc[3];
+    luaL_Buffer b;
+    cc[0] = '%'; cc[2] = '\0';
+    luaL_buffinit(L, &b);
+    for (; *s; s++) {
+      if (*s != '%' || *(s + 1) == '\0')  /* no conversion specifier? */
+        luaL_addchar(&b, *s);
+      else {
+        size_t reslen;
+        char buff[200];  /* should be big enough for any conversion result */
+        cc[1] = *(++s);
+        reslen = strftime(buff, sizeof(buff), cc, stm);
+        luaL_addlstring(&b, buff, reslen);
+      }
+    }
+    luaL_pushresult(&b);
+  }
+  return 1;
+}
+
+
+static int os_time (lua_State *L) {
+  time_t t;
+  if (lua_isnoneornil(L, 1))  /* called without args? */
+    t = time(NULL);  /* get current time */
+  else {
+    struct tm ts;
+    luaL_checktype(L, 1, LUA_TTABLE);
+    lua_settop(L, 1);  /* make sure table is at the top */
+    ts.tm_sec = getfield(L, "sec", 0);
+    ts.tm_min = getfield(L, "min", 0);
+    ts.tm_hour = getfield(L, "hour", 12);
+    ts.tm_mday = getfield(L, "day", -1);
+    ts.tm_mon = getfield(L, "month", -1) - 1;
+    ts.tm_year = getfield(L, "year", -1) - 1900;
+    ts.tm_isdst = getboolfield(L, "isdst");
+    t = mktime(&ts);
+  }
+  if (t == (time_t)(-1))
+    lua_pushnil(L);
+  else
+    lua_pushnumber(L, (lua_Number)t);
+  return 1;
+}
+
+
+static int os_difftime (lua_State *L) {
+  lua_pushnumber(L, difftime((time_t)(luaL_checknumber(L, 1)),
+                             (time_t)(luaL_optnumber(L, 2, 0))));
+  return 1;
+}
+
+/* }====================================================== */
+
+static const luaL_Reg syslib[] = {
+  {"clock",     os_clock},
+  {"date",      os_date},
+  {"difftime",  os_difftime},
+  {"time",      os_time},
+  {NULL, NULL}
+};
+
+/* }====================================================== */
+
+
+
+LUALIB_API int luaopen_os (lua_State *L) {
+  luaL_register(L, LUA_OSLIBNAME, syslib);
+  return 1;
+}
diff --git a/src/blua/lualib.h b/src/blua/lualib.h
index 4ea97edf3d22d585b57390b2db435b4ceec73330..7127e4d77e56c1d2aee9c7079b59c5a3adc959a2 100644
--- a/src/blua/lualib.h
+++ b/src/blua/lualib.h
@@ -24,6 +24,9 @@ LUALIB_API int (luaopen_table) (lua_State *L);
 #define LUA_IOLIBNAME	"io"
 LUALIB_API int (luaopen_io) (lua_State *L);
 
+#define LUA_OSLIBNAME	"os"
+LUALIB_API int (luaopen_os) (lua_State *L);
+
 #define LUA_STRLIBNAME	"string"
 LUALIB_API int (luaopen_string) (lua_State *L);
 
diff --git a/src/command.c b/src/command.c
index a34d9694a1c4f5026225780bca1d2e1d47b77c25..e0deff8e1cc995109c7a704d16c92bd578914190 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
 
@@ -76,6 +77,7 @@ CV_PossibleValue_t CV_OnOff[] = {{0, "Off"}, {1, "On"}, {0, NULL}};
 CV_PossibleValue_t CV_YesNo[] = {{0, "No"}, {1, "Yes"}, {0, NULL}};
 CV_PossibleValue_t CV_Unsigned[] = {{0, "MIN"}, {999999999, "MAX"}, {0, NULL}};
 CV_PossibleValue_t CV_Natural[] = {{1, "MIN"}, {999999999, "MAX"}, {0, NULL}};
+CV_PossibleValue_t CV_TrueFalse[] = {{0, "False"}, {1, "True"}, {0, NULL}};
 
 // Filter consvars by EXECVERSION
 // First implementation is 26 (2.1.21), so earlier configs default at 25 (2.1.20)
@@ -680,25 +682,58 @@ static void COM_ExecuteString(char *ptext)
 //                            SCRIPT COMMANDS
 // =========================================================================
 
+static void print_alias(void)
+{
+	cmdalias_t *a;
+
+	CONS_Printf("\x82""Current alias commands:\n");
+	for (a = com_alias; a; a = a->next)
+	{
+		CONS_Printf("%s : %s", a->name, a->value);
+	}
+}
+
+static void add_alias(char *newname, char *newcmd)
+{
+	cmdalias_t *a;
+
+	// Check for existing aliases first
+	for (a = com_alias; a; a = a->next)
+	{
+		if (!stricmp(newname, a->name))
+		{
+			Z_Free(a->value); // Free old cmd 
+			a->value = newcmd;
+			return;
+		}
+	}
+
+	// No alias found, add it instead
+	a = ZZ_Alloc(sizeof *a);
+	a->next = com_alias;
+	com_alias = a;
+
+	a->name = newname;
+	a->value = newcmd;
+}
+
 /** Creates a command name that replaces another command.
   */
 static void COM_Alias_f(void)
 {
-	cmdalias_t *a;
+	char *name;
+	char *zcmd;
 	char cmd[1024];
 	size_t i, c;
 
 	if (COM_Argc() < 3)
 	{
 		CONS_Printf(M_GetText("alias <name> <command>: create a shortcut command that executes other command(s)\n"));
+		print_alias();
 		return;
 	}
 
-	a = ZZ_Alloc(sizeof *a);
-	a->next = com_alias;
-	com_alias = a;
-
-	a->name = Z_StrDup(COM_Argv(1));
+	name = Z_StrDup(COM_Argv(1));
 
 	// copy the rest of the command line
 	cmd[0] = 0; // start out with a null string
@@ -710,8 +745,8 @@ static void COM_Alias_f(void)
 			strcat(cmd, " ");
 	}
 	strcat(cmd, "\n");
-
-	a->value = Z_StrDup(cmd);
+	zcmd = Z_StrDup(cmd);
+	add_alias(name, zcmd);
 }
 
 /** Prints a line of text to the console.
@@ -863,9 +898,11 @@ static void COM_Help_f(void)
 			{
 				CONS_Printf(" Possible values:\n");
 				if (cvar->PossibleValue == CV_YesNo)
-					CONS_Printf("  Yes or No (On or Off, 1 or 0)\n");
+					CONS_Printf("  Yes or No (On or Off, True or False, 1 or 0)\n");
 				else if (cvar->PossibleValue == CV_OnOff)
-					CONS_Printf("  On or Off (Yes or No, 1 or 0)\n");
+					CONS_Printf("  On or Off (Yes or No, True or False, 1 or 0)\n");
+				else if (cvar->PossibleValue == CV_TrueFalse)
+					CONS_Printf("  True or False (On or Off, Yes or No, 1 or 0)\n");
 				else if (cvar->PossibleValue == Color_cons_t)
 				{
 					for (i = 1; i < numskincolors; ++i)
@@ -1016,7 +1053,7 @@ static void COM_Toggle_f(void)
 	if (CV_Immutable(cvar))
 		return;
 
-	if (!(cvar->PossibleValue == CV_YesNo || cvar->PossibleValue == CV_OnOff))
+	if (!(cvar->PossibleValue == CV_YesNo || cvar->PossibleValue == CV_OnOff || cvar->PossibleValue == CV_TrueFalse))
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("%s is not a boolean value\n"), COM_Argv(1));
 		return;
@@ -1507,12 +1544,12 @@ static void Setvalue(consvar_t *var, const char *valstr, boolean stealth)
 						goto found;
 			}
 			// Not found ... but wait, there's hope!
-			if (var->PossibleValue == CV_OnOff || var->PossibleValue == CV_YesNo)
+			if (var->PossibleValue == CV_OnOff || var->PossibleValue == CV_YesNo || var->PossibleValue == CV_TrueFalse)
 			{
 				overrideval = -1;
-				if (!stricmp(valstr, "on") || !stricmp(valstr, "yes"))
+				if (!stricmp(valstr, "on") || !stricmp(valstr, "yes") || !stricmp(valstr, "true"))
 					overrideval = 1;
-				else if (!stricmp(valstr, "off") || !stricmp(valstr, "no"))
+				else if (!stricmp(valstr, "off") || !stricmp(valstr, "no") || !stricmp(valstr, "false"))
 					overrideval = 0;
 
 				if (overrideval != -1)
diff --git a/src/command.h b/src/command.h
index 69d1890d34d068ac67eb48c10e5c0ec5b8764b7f..619d8c1dcab07a7ab5dfbe20e98cae409c77fb9d 100644
--- a/src/command.h
+++ b/src/command.h
@@ -177,6 +177,7 @@ extern CV_PossibleValue_t CV_OnOff[];
 extern CV_PossibleValue_t CV_YesNo[];
 extern CV_PossibleValue_t CV_Unsigned[];
 extern CV_PossibleValue_t CV_Natural[];
+extern CV_PossibleValue_t CV_TrueFalse[];
 
 // Filter consvars by version
 extern consvar_t cv_execversion;
diff --git a/src/comptime.c b/src/comptime.c
index 398eda0743706cecb4a22d1996f8949564f1fe07..386b53f46904df87fc5f64749cc11db6e6821e3f 100644
--- a/src/comptime.c
+++ b/src/comptime.c
@@ -11,6 +11,9 @@
 #include "config.h"
 const char *compbranch = SRB2_COMP_BRANCH;
 const char *comprevision = SRB2_COMP_REVISION;
+const char *compnote = SRB2_COMP_NOTE;
+const char *comptype = CMAKE_BUILD_TYPE;
+const int compoptimized = SRB2_COMP_OPTIMIZED;
 
 #elif (defined(COMPVERSION))
 #include "comptime.h"
@@ -21,5 +24,12 @@ const char *comprevision = "illegal";
 
 #endif
 
+const int compuncommitted =
+#if (defined(COMPVERSION_UNCOMMITTED))
+1;
+#else
+0;
+#endif
+
 const char *compdate = __DATE__;
 const char *comptime = __TIME__;
diff --git a/src/config.h.in b/src/config.h.in
index 3d6d98375693b220329ebd7b8fe1619def9fe792..6d49a698934bf8b498f6bfb402bbaa7f8ff93191 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -11,8 +11,18 @@
 
 #ifdef CMAKECONFIG
 
-#define SRB2_COMP_REVISION    "${SRB2_COMP_REVISION}"
-#define SRB2_COMP_BRANCH      "${SRB2_COMP_BRANCH}"
+#define SRB2_COMP_REVISION       "${SRB2_COMP_REVISION}"
+#define SRB2_COMP_BRANCH         "${SRB2_COMP_BRANCH}"
+#define SRB2_COMP_NOTE           "${SRB2_COMP_NOTE}"
+// This is done with configure_file instead of defines in order to avoid
+// recompiling the whole target whenever the working directory state changes
+#cmakedefine SRB2_COMP_UNCOMMITTED
+#ifdef SRB2_COMP_UNCOMMITTED
+#define COMPVERSION_UNCOMMITTED
+#endif
+
+#define CMAKE_BUILD_TYPE         "${CMAKE_BUILD_TYPE}"
+#cmakedefine01 SRB2_COMP_OPTIMIZED
 
 #endif
 
@@ -28,12 +38,14 @@
  * Last updated 2021 / 05 / 06 - v2.2.9 - patch.pk3 & zones.pk3
  * Last updated 2022 / 03 / 06 - v2.2.10 - main assets
  * Last updated 2023 / 05 / 02 - v2.2.11 - patch.pk3 & zones.pk3
+ * Last updated 2023 / 09 / 06 - v2.2.12 - patch.pk3
+ * Last updated 2023 / 09 / 09 - v2.2.13 - none
  */
 #define ASSET_HASH_SRB2_PK3   "ad911f29a28a18968ee5b2d11c2acb39"
 #define ASSET_HASH_ZONES_PK3  "1c8adf8d079ecb87d00081f158acf3c7"
 #define ASSET_HASH_PLAYER_DTA "2e7aaae8a6b1b77d90ffe7606ceadb6c"
 #ifdef USE_PATCH_DTA
-#define ASSET_HASH_PATCH_PK3  "2e69558bce3b9610624549a75e29e19b"
+#define ASSET_HASH_PATCH_PK3  "3c7b73f34af7e9a7bceb2d5260f76172"
 #endif
 
 #endif
diff --git a/src/console.c b/src/console.c
index 88e7bc2aed4dda92efd15d9cfb62c7933b2cfd69..21b608ce4df88ac5719cb8c43bb739e314450cbd 100644
--- a/src/console.c
+++ b/src/console.c
@@ -72,8 +72,8 @@ static INT32 con_curlines;  // vid lines currently used by console
 
        INT32 con_clipviewtop; // (useless)
 
-static INT32 con_hudlines;        // number of console heads up message lines
-static INT32 con_hudtime[MAXHUDLINES];      // remaining time of display for hud msg lines
+static UINT8  con_hudlines;             // number of console heads up message lines
+static UINT32 con_hudtime[MAXHUDLINES]; // remaining time of display for hud msg lines
 
        INT32 con_clearlines;      // top screen lines to refresh when view reduced
        boolean con_hudupdate;   // when messages scroll, we need a backgrnd refresh
@@ -126,10 +126,13 @@ static void CONS_backcolor_Change(void);
 static char con_buffer[CON_BUFFERSIZE];
 
 // how many seconds the hud messages lasts on the screen
-static consvar_t cons_msgtimeout = CVAR_INIT ("con_hudtime", "5", CV_SAVE, CV_Unsigned, NULL);
+// CV_Unsigned can overflow when multiplied by TICRATE later, so let's use a 3-year limit instead
+static CV_PossibleValue_t hudtime_cons_t[] = {{0, "MIN"}, {99999999, "MAX"}, {0, NULL}};
+static consvar_t cons_hudtime = CVAR_INIT ("con_hudtime", "5", CV_SAVE, hudtime_cons_t, NULL);
 
 // number of lines displayed on the HUD
-static consvar_t cons_hudlines = CVAR_INIT ("con_hudlines", "5", CV_CALL|CV_SAVE, CV_Unsigned, CONS_hudlines_Change);
+static CV_PossibleValue_t hudlines_cons_t[] = {{0, "MIN"}, {MAXHUDLINES, "MAX"}, {0, NULL}};
+static consvar_t cons_hudlines = CVAR_INIT ("con_hudlines", "5", CV_CALL|CV_SAVE, hudlines_cons_t, CONS_hudlines_Change);
 
 // number of lines console move per frame
 // (con_speed needs a limit, apparently)
@@ -181,11 +184,6 @@ static void CONS_hudlines_Change(void)
 	for (i = 0; i < con_hudlines; i++)
 		con_hudtime[i] = 0;
 
-	if (cons_hudlines.value < 1)
-		cons_hudlines.value = 1;
-	else if (cons_hudlines.value > MAXHUDLINES)
-		cons_hudlines.value = MAXHUDLINES;
-
 	con_hudlines = cons_hudlines.value;
 
 	Unlock_state();
@@ -395,16 +393,16 @@ static void CON_SetupColormaps(void)
 
 	//                      0x1       0x3                           0x9                           0xF
 	colset(magentamap, 177, 177, 178, 178, 178, 180, 180, 180, 182, 182, 182, 182, 184, 184, 184, 185);
-	colset(yellowmap,   82,  82,  73,  73,  73,  64,  64,  64,  66,  66,  66,  66,  67,  67,  67,  68);
-	colset(lgreenmap,   96,  96,  98,  98,  98, 101, 101, 101, 104, 104, 104, 104, 106, 106, 106, 107);
-	colset(bluemap,    146, 146, 147, 147, 147, 149, 149, 149, 152, 152, 152, 152, 155, 155, 155, 157);
-	colset(redmap,      32,  32,  33,  33,  33,  35,  35,  35,  39,  39,  39,  39,  42,  42,  42,  44);
+	colset(yellowmap,   82,  82,  73,  73,  73,  74,  74,  74,  66,  66,  66,  66,  67,  67,  67,  68);
+	colset(lgreenmap,   96,  96,  98,  98,  98, 100, 100, 100, 103, 103, 103, 103, 105, 105, 105, 107);
+	colset(bluemap,    146, 146, 147, 147, 147, 148, 148, 148, 149, 149, 149, 149, 150, 150, 150, 151);
+	colset(redmap,      32,  32,  33,  33,  33,  34,  34,  34,  35,  35,  35,  35,  37,  37,  37,  39);
 	colset(graymap,      8,   9,  10,  11,  12,  13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23);
 	colset(orangemap,   50,  50,  52,  52,  52,  54,  54,  54,  56,  56,  56,  56,  59,  59,  59,  60);
 	colset(skymap,     129, 129, 130, 130, 130, 131, 131, 131, 133, 133, 133, 133, 135, 135, 135, 136);
 	colset(purplemap,  160, 160, 161, 161, 161, 162, 162, 162, 163, 163, 163, 163, 164, 164, 164, 165);
 	colset(aquamap,    120, 120, 121, 121, 121, 122, 122, 122, 123, 123, 123, 123, 124, 124, 124, 125);
-	colset(peridotmap,  72,  72, 188, 188, 189, 189, 189, 189, 190, 190, 190, 190, 191, 191, 191,  94);
+	colset(peridotmap,  73,  73, 188, 188, 188, 189, 189, 189, 190, 190, 190, 190, 191, 191, 191,  94);
 	colset(azuremap,   144, 144, 145, 145, 145, 146, 146, 146, 170, 170, 170, 170, 171, 171, 171, 172);
 	colset(brownmap,   219, 219, 221, 221, 221, 222, 222, 222, 224, 224, 224, 224, 227, 227, 227, 229);
 	colset(rosymap,    200, 200, 201, 201, 201, 202, 202, 202, 203, 203, 203, 203, 204, 204, 204, 205);
@@ -477,7 +475,7 @@ void CON_Init(void)
 
 		Unlock_state();
 
-		CV_RegisterVar(&cons_msgtimeout);
+		CV_RegisterVar(&cons_hudtime);
 		CV_RegisterVar(&cons_hudlines);
 		CV_RegisterVar(&cons_speed);
 		CV_RegisterVar(&cons_height);
@@ -792,9 +790,8 @@ void CON_Ticker(void)
 	// make overlay messages disappear after a while
 	for (i = 0; i < con_hudlines; i++)
 	{
-		con_hudtime[i]--;
-		if (con_hudtime[i] < 0)
-			con_hudtime[i] = 0;
+		if (con_hudtime[i])
+			con_hudtime[i]--;
 	}
 
 	Unlock_state();
@@ -1343,7 +1340,8 @@ boolean CON_Responder(event_t *ev)
 static void CON_Linefeed(void)
 {
 	// set time for heads up messages
-	con_hudtime[con_cy%con_hudlines] = cons_msgtimeout.value*TICRATE;
+	if (con_hudlines)
+		con_hudtime[con_cy%con_hudlines] = cons_hudtime.value*TICRATE;
 
 	con_cy++;
 	con_cx = 0;
@@ -1699,7 +1697,7 @@ static void CON_DrawHudlines(void)
 	INT32 charwidth = 8 * con_scalefactor;
 	INT32 charheight = 8 * con_scalefactor;
 
-	if (con_hudlines <= 0)
+	if (!con_hudlines)
 		return;
 
 	if (chat_on && OLDCHAT)
@@ -1707,7 +1705,7 @@ static void CON_DrawHudlines(void)
 	else
 		y = 0;
 
-	for (i = con_cy - con_hudlines+1; i <= con_cy; i++)
+	for (i = con_cy - con_hudlines; i <= con_cy; i++)
 	{
 		size_t c;
 		INT32 x;
@@ -1889,7 +1887,7 @@ void CON_Drawer(void)
 		CON_DrawConsole();
 	else if (gamestate == GS_LEVEL
 	|| gamestate == GS_INTERMISSION || gamestate == GS_ENDING || gamestate == GS_CUTSCENE
-	|| gamestate == GS_CREDITS || gamestate == GS_EVALUATION)
+	|| gamestate == GS_CREDITS || gamestate == GS_EVALUATION || gamestate == GS_WAITINGPLAYERS)
 		CON_DrawHudlines();
 
 	Unlock_state();
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
deleted file mode 100755
index 64e5aff6b295e464997b98ea7ff9ee1e0e1e2730..0000000000000000000000000000000000000000
--- a/src/d_clisrv.c
+++ /dev/null
@@ -1,5658 +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;
-
-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;
-	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 */
-
-	strncpy(netbuffer->u.clientcfg.names[0], cv_playername.zstring, MAXPLAYERNAME);
-	strncpy(netbuffer->u.clientcfg.names[1], cv_playername2.zstring, MAXPLAYERNAME);
-
-	return HSendPacket(servernode, true, 0, sizeof (clientconfig_pak));
-}
-
-static INT32 FindRejoinerNum(SINT8 node)
-{
-	char strippednodeaddress[64];
-	const char *nodeaddress;
-	char *port;
-	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(strippednodeaddress, nodeaddress);
-	port = strchr(strippednodeaddress, ':');
-	if (port)
-		*port = '\0';
-
-	// 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(true); // title sky
-				F_TitleScreenTicker(true);
-				F_TitleScreenDrawer();
-			}
-			CL_DrawConnectionStatus();
-#ifdef HAVE_THREADS
-			I_lock_mutex(&m_menu_mutex);
-#endif
-			M_Drawer(); //Needed for drawing messageboxes on the connection screen
-#ifdef HAVE_THREADS
-			I_unlock_mutex(m_menu_mutex);
-#endif
-			I_UpdateNoVsync(); // page flip or blit buffer
-			if (moviemode)
-				M_SaveFrame();
-			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))));
-
-	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;
-	size_t i;
-	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 (i=0; fgets(buffer, (int)sizeof(buffer), f); i++)
-	{
-		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();
-
-	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)
-		{
-			const char *address = I_GetNodeAddress(node);
-			char *port = NULL;
-			if (address) // MI: fix msvcrt.dll!_mbscat crash?
-			{
-				strcpy(playeraddress[newplayernum], address);
-				port = strchr(playeraddress[newplayernum], ':');
-				if (port)
-					*port = '\0';
-			}
-		}
-	}
-
-	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;
-
-			// Don't do anything for packets of type NODEKEEPALIVE?
-			if (netconsole == -1 || netbuffer->packettype == PT_NODEKEEPALIVE
-				|| netbuffer->packettype == PT_NODEKEEPALIVEMIS)
-				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;
-
-			// Copy ticcmd
-			G_MoveTiccmd(&netcmds[maketic%BACKUPTICS][netconsole], &netbuffer->u.clientpak.cmd, 1);
-
-			// Check ticcmd for "speed hacks"
-			if (netcmds[maketic%BACKUPTICS][netconsole].forwardmove > MAXPLMOVE || netcmds[maketic%BACKUPTICS][netconsole].forwardmove < -MAXPLMOVE
-				|| netcmds[maketic%BACKUPTICS][netconsole].sidemove > MAXPLMOVE || netcmds[maketic%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[maketic%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_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);
-}
-
-// send the client packet to the server
-static void CL_SendClientCmd(void)
-{
-	size_t packetsize = 0;
-
-	netbuffer->packettype = PT_CLIENTCMD;
-
-	if (cl_packetmissed)
-		netbuffer->packettype++;
-	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 += 4;
-		packetsize = sizeof (clientcmd_pak) - sizeof (ticcmd_t) - sizeof (INT16);
-		HSendPacket(servernode, false, 0, packetsize);
-	}
-	else if (gamestate != GS_NULL && (addedtogame || dedicated))
-	{
-		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 += 2;
-			G_MoveTiccmd(&netbuffer->u.client2pak.cmd2, &localcmds2, 1);
-			packetsize = sizeof (client2cmd_pak);
-		}
-		else
-			packetsize = sizeof (clientcmd_pak);
-
-		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
-}
-
-void NetUpdate(void)
-{
-	static tic_t gametime = 0;
-	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;
-	}
-
-	gametime = nowtime;
-
-	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++;
-	}
-
-	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)
-		{
-			INT32 counts;
-
-			hu_redownloadinggamestate = false;
-
-			firstticstosend = gametic;
-			for (i = 0; i < MAXNETNODES; i++)
-				if (nodeingame[i] && nettics[i] < firstticstosend)
-				{
-					firstticstosend = nettics[i];
-
-					if (maketic + 1 >= nettics[i] + BACKUPTICS)
-						Net_ConnectionTimeout(i);
-				}
-
-			// Don't erase tics not acknowledged
-			counts = realtics;
-
-			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();
-
-	// Handle timeouts to prevent definitive freezes from happenning
-	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);
-	}
-
-	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_main.c b/src/d_main.c
index 6506c9d4ee40687a557ad3cc031ec08451413d73..80907a013d9c6586c3b0d7cb828404e8d5214d67 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
@@ -70,6 +70,8 @@
 #include "filesrch.h" // refreshdirmenu
 #include "g_input.h" // tutorial mode control scheming
 #include "m_perfstats.h"
+#include "m_random.h"
+#include "command.h"
 
 #ifdef CMAKECONFIG
 #include "config.h"
@@ -458,6 +460,13 @@ static void D_Display(void)
 
 		case GS_WAITINGPLAYERS:
 			// The clientconnect drawer is independent...
+			if (netgame)
+			{
+				// I don't think HOM from nothing drawing is independent...
+				F_WaitingPlayersDrawer();
+				HU_Erase();
+				HU_Drawer();
+			}
 		case GS_DEDICATEDSERVER:
 		case GS_NULL:
 			break;
@@ -972,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
@@ -1208,6 +1218,15 @@ D_ConvertVersionNumbers (void)
 #endif
 }
 
+static void Command_assert(void)
+{
+#if !defined(NDEBUG) || defined(PARANOIA)
+	CONS_Printf("Yes, assertions are enabled.\n");
+#else
+	CONS_Printf("No, assertions are NOT enabled.\n");
+#endif
+}
+
 //
 // D_SRB2Main
 //
@@ -1221,6 +1240,11 @@ void D_SRB2Main(void)
 	/* break the version string into version numbers, for netplay */
 	D_ConvertVersionNumbers();
 
+	if (!strcmp(compbranch, ""))
+	{
+		compbranch = "detached HEAD";
+	}
+
 	// Print GPL notice for our console users (Linux)
 	CONS_Printf(
 	"\n\nSonic Robo Blast 2\n"
@@ -1334,11 +1358,12 @@ void D_SRB2Main(void)
 	snprintf(addonsdir, sizeof addonsdir, "%s%s%s", srb2home, PATHSEP, "addons");
 	I_mkdir(addonsdir, 0755);
 
-	// rand() needs seeded regardless of password
-	srand((unsigned int)time(NULL));
-	rand();
-	rand();
-	rand();
+	// seed M_Random because it is necessary; seed P_Random for scripts that
+	// might want to use random numbers immediately at start
+	if (!M_RandomSeedFromOS())
+		M_RandomSeed((UINT32)time(NULL)); // less good but serviceable
+
+	P_SetRandSeed(M_RandomizedSeed());
 
 	if (M_CheckParm("-password") && M_IsNextParm())
 		D_SetPassword(M_GetNextParm());
@@ -1350,9 +1375,14 @@ void D_SRB2Main(void)
 	CONS_Printf("Z_Init(): Init zone memory allocation daemon. \n");
 	Z_Init();
 
+	clientGamedata = M_NewGameDataStruct();
+	serverGamedata = M_NewGameDataStruct();
+
 	// Do this up here so that WADs loaded through the command line can use ExecCfg
 	COM_Init();
 
+	COM_AddCommand("assert", Command_assert, COM_LUA);
+
 	// Add any files specified on the command line with
 	// "-file <file>" or "-folder <folder>" to the add-on list
 	if (!((M_GetUrlProtocolArg() || M_CheckParm("-connect")) && !M_CheckParm("-server")))
@@ -1479,7 +1509,9 @@ void D_SRB2Main(void)
 		// confusion issues when loading mods.
 		strlcpy(gamedatafilename, M_GetNextParm(), sizeof gamedatafilename);
 	}
-	G_LoadGameData();
+
+	G_LoadGameData(clientGamedata);
+	M_CopyGameData(serverGamedata, clientGamedata);
 
 #if defined (__unix__) || defined (UNIXCOMMON) || defined (HAVE_SDL)
 	VID_PrepareModeList(); // Regenerate Modelist according to cv_fullscreen
@@ -1595,6 +1627,8 @@ void D_SRB2Main(void)
 		autostart = true;
 	}
 
+	pickedchar = R_SkinAvailable(cv_defaultskin.string);
+
 	// user settings come before "+" parameters.
 	if (dedicated)
 		COM_ImmedExecute(va("exec \"%s"PATHSEP"adedserv.cfg\"\n", srb2home));
@@ -1706,14 +1740,15 @@ void D_SRB2Main(void)
 			// Prevent warping to nonexistent levels
 			if (W_CheckNumForName(G_BuildMapName(pstartmap)) == LUMPERROR)
 				I_Error("Could not warp to %s (map not found)\n", G_BuildMapName(pstartmap));
-			// Prevent warping to locked levels
-			// ... unless you're in a dedicated server.  Yes, technically this means you can view any level by
-			// running a dedicated server and joining it yourself, but that's better than making dedicated server's
-			// lives hell.
-			else if (!dedicated && M_MapLocked(pstartmap))
-				I_Error("You need to unlock this level before you can warp to it!\n");
 			else
 			{
+				if (M_CampaignWarpIsCheat(gametype, pstartmap, serverGamedata))
+				{
+					// If you're warping via command line, you know what you're doing.
+					// No need to I_Error over this.
+					G_SetUsedCheats(false);
+				}
+
 				D_MapChange(pstartmap, gametype, ultimatemode, true, 0, false, false);
 			}
 		}
diff --git a/src/d_player.h b/src/d_player.h
index 756c7141e3707c37a6f2ea0dde7a723aac12e834..7ad5b9f811962cb56ffcf55030f2ff1d2aea331d 100644
--- a/src/d_player.h
+++ b/src/d_player.h
@@ -249,6 +249,38 @@ typedef enum
 	CR_FAN
 } carrytype_t; // pw_carry
 
+typedef enum
+{
+	STR_NONE = 0, // All strong powers can stack onto each other
+
+	// Attack powers
+	STR_ANIM = 0x1, // remove powers when leaving current animation
+	STR_PUNCH = 0x2, // frontal attack (knuckles glide)
+	STR_TAIL = 0x4, // rear attack
+	STR_STOMP = 0x8, // falling onto object (fang bounce)
+	STR_UPPER = 0x10, // moving upwards into object (tails fly)
+	STR_GUARD = 0x20, //protect against damage
+	STR_HEAVY = 0x40, // ignore vertical rebound
+	STR_DASH = 0x80, // special type for machine dashmode, automatically removes your powers when leaving dashmode
+
+	// Environment powers
+	STR_WALL = 0x100, // fof busting
+	STR_FLOOR = 0x200,
+	STR_CEILING = 0x400,
+	STR_SPRING = 0x800, // power up hit springs
+	STR_SPIKE = 0x1000, // break spikes
+
+	// Shortcuts
+	STR_ATTACK = STR_PUNCH|STR_TAIL|STR_STOMP|STR_UPPER,
+	STR_BUST = STR_WALL|STR_FLOOR|STR_CEILING,
+	STR_FLY = STR_ANIM|STR_UPPER,
+	STR_GLIDE = STR_ANIM|STR_PUNCH,
+	STR_TWINSPIN = STR_ANIM|STR_ATTACK|STR_BUST|STR_SPRING|STR_SPIKE,
+	STR_MELEE = STR_ANIM|STR_PUNCH|STR_HEAVY|STR_WALL|STR_FLOOR|STR_SPRING|STR_SPIKE,
+	STR_BOUNCE = STR_ANIM|STR_STOMP|STR_FLOOR,
+	STR_METAL = STR_DASH|STR_SPIKE
+} strongtype_t; // pw_strong
+
 // Player powers. (don't edit this comment)
 typedef enum
 {
@@ -293,6 +325,8 @@ typedef enum
 
 	pw_ignorelatch, // Don't grab onto CR_GENERIC, add 32768 (powers[pw_ignorelatch] & 1<<15) to avoid ALL not-NiGHTS CR_ types
 
+	pw_strong, // Additional properties for powerful attacks
+
 	NUMPOWERS
 } powertype_t;
 
@@ -407,6 +441,7 @@ typedef struct player_s
 
 	// playing animation.
 	panim_t panim;
+	UINT8 stronganim;
 
 	// For screen flashing (bright).
 	UINT16 flashcount;
@@ -418,7 +453,8 @@ typedef struct player_s
 	INT32 skin;
 	UINT32 availabilities;
 
-	UINT32 score; // player score
+	UINT32 score; // player score (total)
+	UINT32 recordscore; // player score (per life / map)
 	fixed_t dashspeed; // dashing speed
 
 	fixed_t normalspeed; // Normal ground
diff --git a/src/d_think.h b/src/d_think.h
index bdb5db3f54135545d27b0018943f0c995fffdcd4..efc1589bf62e277a67ab309f05d33d66af741865 100644
--- a/src/d_think.h
+++ b/src/d_think.h
@@ -17,6 +17,8 @@
 #ifndef __D_THINK__
 #define __D_THINK__
 
+#include "doomdef.h"
+
 #ifdef __GNUG__
 #pragma interface
 #endif
@@ -49,6 +51,11 @@ typedef struct thinker_s
 	// killough 11/98: count of how many other objects reference
 	// this one using pointers. Used for garbage collection.
 	INT32 references;
+
+#ifdef PARANOIA
+	INT32 debug_mobjtype;
+	tic_t debug_time;
+#endif
 } thinker_t;
 
 #endif
diff --git a/src/deh_lua.c b/src/deh_lua.c
index 068fe4f23806b70504318c27c81b9f27e1bbdbe7..0b789547b266aeb238701ca6b34c4f8702c334fb 100644
--- a/src/deh_lua.c
+++ b/src/deh_lua.c
@@ -10,20 +10,7 @@
 /// \file  deh_lua.c
 /// \brief Lua SOC library
 
-#include "g_game.h"
-#include "s_sound.h"
-#include "z_zone.h"
-#include "m_menu.h"
-#include "m_misc.h"
-#include "p_local.h"
-#include "st_stuff.h"
-#include "fastcmp.h"
-#include "lua_script.h"
-#include "lua_libs.h"
-
-#include "dehacked.h"
 #include "deh_lua.h"
-#include "deh_tables.h"
 
 // freeslot takes a name (string only!)
 // and allocates it to the appropriate free slot.
@@ -89,6 +76,8 @@ static inline int lib_freeslot(lua_State *L)
 				strncpy(sprnames[j],word,4);
 				//sprnames[j][4] = 0;
 				used_spr[(j-SPR_FIRSTFREESLOT)/8] |= 1<<(j%8); // Okay, this sprite slot has been named now.
+				// Lua needs to update the value in _G if it exists
+				LUA_UpdateSprName(word, j);
 				lua_pushinteger(L, j);
 				r++;
 				break;
@@ -198,39 +187,54 @@ static inline int lib_freeslot(lua_State *L)
 // Arguments: mobj_t actor, int var1, int var2
 static int action_call(lua_State *L)
 {
-	//actionf_t *action = lua_touserdata(L,lua_upvalueindex(1));
 	actionf_t *action = *((actionf_t **)luaL_checkudata(L, 1, META_ACTION));
 	mobj_t *actor = *((mobj_t **)luaL_checkudata(L, 2, META_MOBJ));
+
 	var1 = (INT32)luaL_optinteger(L, 3, 0);
 	var2 = (INT32)luaL_optinteger(L, 4, 0);
+
 	if (!actor)
+	{
 		return LUA_ErrInvalid(L, "mobj_t");
+	}
+
 	action->acp1(actor);
 	return 0;
 }
 
 // Hardcoded A_Action name to call for super() or NULL if super() would be invalid.
 // Set in lua_infolib.
-const char *superactions[MAXRECURSION];
-UINT8 superstack = 0;
+const char *luaactions[MAX_ACTION_RECURSION];
+UINT8 luaactionstack = 0;
 
 static int lib_dummysuper(lua_State *L)
 {
-	return luaL_error(L, "Can't call super() outside of hardcode-replacing A_Action functions being called by state changes!"); // convoluted, I know. @_@;;
+	// TODO: Now that the restriction on only being allowed in state changes was lifted,
+	// it'd be nice to have super extend to Lua A_ functions too :)
+	return luaL_error(L, "Can't call super() outside of hardcode-replacing A_Action functions!");
 }
 
-static inline int lib_getenum(lua_State *L)
+static void CacheAndPushConstant(lua_State *L, const char *name, lua_Integer value)
 {
-	const char *word, *p;
+	// "cache" into _G
+	lua_pushstring(L, name);
+	lua_pushinteger(L, value);
+	lua_rawset(L, LUA_GLOBALSINDEX);
+	// push
+	lua_pushinteger(L, value);
+}
+
+// Search for a matching constant variable.
+// Result is stored into _G for faster subsequent use. (Except for SPR_ in the SOC parser)
+static int ScanConstants(lua_State *L, boolean mathlib, const char *word)
+{
+	const char *p;
 	fixed_t i;
-	boolean mathlib = lua_toboolean(L, lua_upvalueindex(1));
-	if (lua_type(L,2) != LUA_TSTRING)
-		return 0;
-	word = lua_tostring(L,2);
+
 	if (strlen(word) == 1) { // Assume sprite frame if length 1.
 		if (*word >= 'A' && *word <= '~')
 		{
-			lua_pushinteger(L, *word-'A');
+			CacheAndPushConstant(L, word, *word-'A');
 			return 1;
 		}
 		if (mathlib) return luaL_error(L, "constant '%s' could not be parsed.\n", word);
@@ -240,7 +244,7 @@ static inline int lib_getenum(lua_State *L)
 		p = word+3;
 		for (i = 0; MOBJFLAG_LIST[i]; i++)
 			if (fastcmp(p, MOBJFLAG_LIST[i])) {
-				lua_pushinteger(L, ((lua_Integer)1<<i));
+				CacheAndPushConstant(L, word, ((lua_Integer)1<<i));
 				return 1;
 			}
 		if (mathlib) return luaL_error(L, "mobjflag '%s' could not be found.\n", word);
@@ -250,7 +254,7 @@ static inline int lib_getenum(lua_State *L)
 		p = word+4;
 		for (i = 0; MOBJFLAG2_LIST[i]; i++)
 			if (fastcmp(p, MOBJFLAG2_LIST[i])) {
-				lua_pushinteger(L, ((lua_Integer)1<<i));
+				CacheAndPushConstant(L, word, ((lua_Integer)1<<i));
 				return 1;
 			}
 		if (mathlib) return luaL_error(L, "mobjflag2 '%s' could not be found.\n", word);
@@ -260,12 +264,12 @@ static inline int lib_getenum(lua_State *L)
 		p = word+4;
 		for (i = 0; MOBJEFLAG_LIST[i]; i++)
 			if (fastcmp(p, MOBJEFLAG_LIST[i])) {
-				lua_pushinteger(L, ((lua_Integer)1<<i));
+				CacheAndPushConstant(L, word, ((lua_Integer)1<<i));
 				return 1;
 			}
 		if (fastcmp(p, "REVERSESUPER"))
 		{
-			lua_pushinteger(L, (lua_Integer)MFE_REVERSESUPER);
+			CacheAndPushConstant(L, word, (lua_Integer)MFE_REVERSESUPER);
 			return 1;
 		}
 		if (mathlib) return luaL_error(L, "mobjeflag '%s' could not be found.\n", word);
@@ -273,9 +277,9 @@ static inline int lib_getenum(lua_State *L)
 	}
 	else if (fastncmp("MTF_", word, 4)) {
 		p = word+4;
-		for (i = 0; i < 4; i++)
-			if (MAPTHINGFLAG_LIST[i] && fastcmp(p, MAPTHINGFLAG_LIST[i])) {
-				lua_pushinteger(L, ((lua_Integer)1<<i));
+		for (i = 0; MAPTHINGFLAG_LIST[i]; i++)
+			if (fastcmp(p, MAPTHINGFLAG_LIST[i])) {
+				CacheAndPushConstant(L, word, ((lua_Integer)1<<i));
 				return 1;
 			}
 		if (mathlib) return luaL_error(L, "mapthingflag '%s' could not be found.\n", word);
@@ -285,17 +289,17 @@ static inline int lib_getenum(lua_State *L)
 		p = word+3;
 		for (i = 0; PLAYERFLAG_LIST[i]; i++)
 			if (fastcmp(p, PLAYERFLAG_LIST[i])) {
-				lua_pushinteger(L, ((lua_Integer)1<<i));
+				CacheAndPushConstant(L, word, ((lua_Integer)1<<i));
 				return 1;
 			}
 		if (fastcmp(p, "FULLSTASIS"))
 		{
-			lua_pushinteger(L, (lua_Integer)PF_FULLSTASIS);
+			CacheAndPushConstant(L, word, (lua_Integer)PF_FULLSTASIS);
 			return 1;
 		}
 		else if (fastcmp(p, "USEDOWN")) // Remove case when 2.3 nears release...
 		{
-			lua_pushinteger(L, (lua_Integer)PF_SPINDOWN);
+			CacheAndPushConstant(L, word, (lua_Integer)PF_SPINDOWN);
 			return 1;
 		}
 		if (mathlib) return luaL_error(L, "playerflag '%s' could not be found.\n", word);
@@ -305,7 +309,7 @@ static inline int lib_getenum(lua_State *L)
 		p = word;
 		for (i = 0; Gametype_ConstantNames[i]; i++)
 			if (fastcmp(p, Gametype_ConstantNames[i])) {
-				lua_pushinteger(L, i);
+				CacheAndPushConstant(L, word, i);
 				return 1;
 			}
 		if (mathlib) return luaL_error(L, "gametype '%s' could not be found.\n", word);
@@ -315,7 +319,7 @@ static inline int lib_getenum(lua_State *L)
 		p = word+4;
 		for (i = 0; GAMETYPERULE_LIST[i]; i++)
 			if (fastcmp(p, GAMETYPERULE_LIST[i])) {
-				lua_pushinteger(L, ((lua_Integer)1<<i));
+				CacheAndPushConstant(L, word, ((lua_Integer)1<<i));
 				return 1;
 			}
 		if (mathlib) return luaL_error(L, "game type rule '%s' could not be found.\n", word);
@@ -325,7 +329,7 @@ static inline int lib_getenum(lua_State *L)
 		p = word+4;
 		for (i = 0; TYPEOFLEVEL[i].name; i++)
 			if (fastcmp(p, TYPEOFLEVEL[i].name)) {
-				lua_pushinteger(L, TYPEOFLEVEL[i].flag);
+				CacheAndPushConstant(L, word, TYPEOFLEVEL[i].flag);
 				return 1;
 			}
 		if (mathlib) return luaL_error(L, "typeoflevel '%s' could not be found.\n", word);
@@ -333,9 +337,10 @@ static inline int lib_getenum(lua_State *L)
 	}
 	else if (fastncmp("ML_", word, 3)) {
 		p = word+3;
+
 		for (i = 0; ML_LIST[i]; i++)
 			if (fastcmp(p, ML_LIST[i])) {
-				lua_pushinteger(L, ((lua_Integer)1<<i));
+				CacheAndPushConstant(L, word, ((lua_Integer)1<<i));
 				return 1;
 			}
 		// Aliases
@@ -418,13 +423,13 @@ static inline int lib_getenum(lua_State *L)
 			if (!FREE_STATES[i])
 				break;
 			if (fastcmp(p, FREE_STATES[i])) {
-				lua_pushinteger(L, S_FIRSTFREESLOT+i);
+				CacheAndPushConstant(L, word, S_FIRSTFREESLOT+i);
 				return 1;
 			}
 		}
 		for (i = 0; i < S_FIRSTFREESLOT; i++)
 			if (fastcmp(p, STATE_LIST[i]+2)) {
-				lua_pushinteger(L, i);
+				CacheAndPushConstant(L, word, i);
 				return 1;
 			}
 		return luaL_error(L, "state '%s' does not exist.\n", word);
@@ -435,13 +440,13 @@ static inline int lib_getenum(lua_State *L)
 			if (!FREE_MOBJS[i])
 				break;
 			if (fastcmp(p, FREE_MOBJS[i])) {
-				lua_pushinteger(L, MT_FIRSTFREESLOT+i);
+				CacheAndPushConstant(L, word, MT_FIRSTFREESLOT+i);
 				return 1;
 			}
 		}
 		for (i = 0; i < MT_FIRSTFREESLOT; i++)
 			if (fastcmp(p, MOBJTYPE_LIST[i]+3)) {
-				lua_pushinteger(L, i);
+				CacheAndPushConstant(L, word, i);
 				return 1;
 			}
 		return luaL_error(L, "mobjtype '%s' does not exist.\n", word);
@@ -450,7 +455,12 @@ static inline int lib_getenum(lua_State *L)
 		p = word+4;
 		for (i = 0; i < NUMSPRITES; i++)
 			if (!sprnames[i][4] && fastncmp(p,sprnames[i],4)) {
-				lua_pushinteger(L, i);
+				// updating overridden sprnames is not implemented for soc parser,
+				// so don't use cache
+				if (mathlib)
+					lua_pushinteger(L, i);
+				else
+					CacheAndPushConstant(L, word, i);
 				return 1;
 			}
 		if (mathlib) return luaL_error(L, "sprite '%s' could not be found.\n", word);
@@ -465,12 +475,12 @@ static inline int lib_getenum(lua_State *L)
 				// the spr2names entry will have "_" on the end, as in "RUN_"
 				if (spr2names[i][3] == '_' && !p[3]) {
 					if (fastncmp(p,spr2names[i],3)) {
-						lua_pushinteger(L, i);
+						CacheAndPushConstant(L, word, i);
 						return 1;
 					}
 				}
 				else if (fastncmp(p,spr2names[i],4)) {
-					lua_pushinteger(L, i);
+					CacheAndPushConstant(L, word, i);
 					return 1;
 				}
 			}
@@ -481,7 +491,7 @@ static inline int lib_getenum(lua_State *L)
 		p = word+4;
 		for (i = 0; i < NUMSFX; i++)
 			if (S_sfx[i].name && fastcmp(p, S_sfx[i].name)) {
-				lua_pushinteger(L, i);
+				CacheAndPushConstant(L, word, i);
 				return 1;
 			}
 		return 0;
@@ -490,7 +500,7 @@ static inline int lib_getenum(lua_State *L)
 		p = word+4;
 		for (i = 0; i < NUMSFX; i++)
 			if (S_sfx[i].name && fasticmp(p, S_sfx[i].name)) {
-				lua_pushinteger(L, i);
+				CacheAndPushConstant(L, word, i);
 				return 1;
 			}
 		return luaL_error(L, "sfx '%s' could not be found.\n", word);
@@ -499,7 +509,7 @@ static inline int lib_getenum(lua_State *L)
 		p = word+2;
 		for (i = 0; i < NUMSFX; i++)
 			if (S_sfx[i].name && fasticmp(p, S_sfx[i].name)) {
-				lua_pushinteger(L, i);
+				CacheAndPushConstant(L, word, i);
 				return 1;
 			}
 		if (mathlib) return luaL_error(L, "sfx '%s' could not be found.\n", word);
@@ -509,7 +519,7 @@ static inline int lib_getenum(lua_State *L)
 		p = word+3;
 		for (i = 0; i < NUMPOWERS; i++)
 			if (fasticmp(p, POWERS_LIST[i])) {
-				lua_pushinteger(L, i);
+				CacheAndPushConstant(L, word, i);
 				return 1;
 			}
 		return 0;
@@ -518,7 +528,7 @@ static inline int lib_getenum(lua_State *L)
 		p = word+3;
 		for (i = 0; i < NUMPOWERS; i++)
 			if (fastcmp(p, POWERS_LIST[i])) {
-				lua_pushinteger(L, i);
+				CacheAndPushConstant(L, word, i);
 				return 1;
 			}
 		return luaL_error(L, "power '%s' could not be found.\n", word);
@@ -527,7 +537,7 @@ static inline int lib_getenum(lua_State *L)
 		p = word+4;
 		for (i = 0; i < NUMHUDITEMS; i++)
 			if (fastcmp(p, HUDITEMS_LIST[i])) {
-				lua_pushinteger(L, i);
+				CacheAndPushConstant(L, word, i);
 				return 1;
 			}
 		if (mathlib) return luaL_error(L, "huditem '%s' could not be found.\n", word);
@@ -539,13 +549,13 @@ static inline int lib_getenum(lua_State *L)
 			if (!FREE_SKINCOLORS[i])
 				break;
 			if (fastcmp(p, FREE_SKINCOLORS[i])) {
-				lua_pushinteger(L, SKINCOLOR_FIRSTFREESLOT+i);
+				CacheAndPushConstant(L, word, SKINCOLOR_FIRSTFREESLOT+i);
 				return 1;
 			}
 		}
 		for (i = 0; i < SKINCOLOR_FIRSTFREESLOT; i++)
 			if (fastcmp(p, COLOR_ENUMS[i])) {
-				lua_pushinteger(L, i);
+				CacheAndPushConstant(L, word, i);
 				return 1;
 			}
 		return luaL_error(L, "skincolor '%s' could not be found.\n", word);
@@ -556,7 +566,7 @@ static inline int lib_getenum(lua_State *L)
 		for (i = 0; NIGHTSGRADE_LIST[i]; i++)
 			if (*p == NIGHTSGRADE_LIST[i])
 			{
-				lua_pushinteger(L, i);
+				CacheAndPushConstant(L, word, i);
 				return 1;
 			}
 		if (mathlib) return luaL_error(L, "NiGHTS grade '%s' could not be found.\n", word);
@@ -566,70 +576,127 @@ static inline int lib_getenum(lua_State *L)
 		p = word+3;
 		for (i = 0; i < NUMMENUTYPES; i++)
 			if (fastcmp(p, MENUTYPES_LIST[i])) {
-				lua_pushinteger(L, i);
+				CacheAndPushConstant(L, word, i);
 				return 1;
 			}
 		if (mathlib) return luaL_error(L, "menutype '%s' could not be found.\n", word);
 		return 0;
 	}
-	else if (!mathlib && fastncmp("A_",word,2)) {
+
+	if (fastcmp(word, "BT_USE")) // Remove case when 2.3 nears release...
+	{
+		CacheAndPushConstant(L, word, (lua_Integer)BT_SPIN);
+		return 1;
+	}
+
+	for (i = 0; INT_CONST[i].n; i++)
+		if (fastcmp(word,INT_CONST[i].n)) {
+			CacheAndPushConstant(L, word, INT_CONST[i].v);
+			return 1;
+		}
+
+	return 0;
+}
+
+static inline int lib_getenum(lua_State *L)
+{
+	const char *word;
+	fixed_t i;
+	boolean mathlib = lua_toboolean(L, lua_upvalueindex(1));
+	if (lua_type(L,2) != LUA_TSTRING)
+		return 0;
+	word = lua_tostring(L,2);
+
+	// check actions, super and globals first, as they don't have _G caching implemented
+	// so they benefit from being checked first
+
+	if (!mathlib && fastncmp("A_",word,2)) {
 		char *caps;
-		// Try to get a Lua action first.
-		/// \todo Push a closure that sets superactions[] and superstack.
+
+		// Hardcoded actions come first.
+		// Trying to call them will invoke LUA_CallAction, which will handle super properly.
+		// Retrieving them from this metatable allows them to be case-insensitive!
+		for (i = 0; actionpointers[i].name; i++)
+		{
+			if (fasticmp(word, actionpointers[i].name))
+			{
+				// We push the actionf_t* itself as userdata!
+				LUA_PushUserdata(L, &actionpointers[i].action, META_ACTION);
+				return 1;
+			}
+		}
+
+		// Now try to get Lua actions.
+		/// \todo Push a closure that sets luaactions[] and luaactionstack.
+		/// This would be part one of a step to get super functions working for custom A_ functions.
+		/// Custom functions.
 		lua_getfield(L, LUA_REGISTRYINDEX, LREG_ACTIONS);
+
 		// actions are stored in all uppercase.
 		caps = Z_StrDup(word);
 		strupr(caps);
 		lua_getfield(L, -1, caps);
 		Z_Free(caps);
+
 		if (!lua_isnil(L, -1))
+		{
 			return 1; // Success! :D That was easy.
+		}
+
 		// Welp, that failed.
 		lua_pop(L, 2); // pop nil and LREG_ACTIONS
-
-		// Hardcoded actions as callable Lua functions!
-		// Retrieving them from this metatable allows them to be case-insensitive!
-		for (i = 0; actionpointers[i].name; i++)
-			if (fasticmp(word, actionpointers[i].name)) {
-				// We push the actionf_t* itself as userdata!
-				LUA_PushUserdata(L, &actionpointers[i].action, META_ACTION);
-				return 1;
-			}
 		return 0;
 	}
 	else if (!mathlib && fastcmp("super",word))
 	{
-		if (!superstack)
+		if (!luaactionstack)
 		{
+			// Not in A_ action routine
 			lua_pushcfunction(L, lib_dummysuper);
 			return 1;
 		}
+
 		for (i = 0; actionpointers[i].name; i++)
-			if (fasticmp(superactions[superstack-1], actionpointers[i].name)) {
+		{
+			if (fasticmp(luaactions[luaactionstack-1], actionpointers[i].name))
+			{
 				LUA_PushUserdata(L, &actionpointers[i].action, META_ACTION);
 				return 1;
 			}
-		return 0;
-	}
+		}
 
-	if (fastcmp(word, "BT_USE")) // Remove case when 2.3 nears release...
-	{
-		lua_pushinteger(L, (lua_Integer)BT_SPIN);
+		// Not a hardcoded A_ action.
+		lua_pushcfunction(L, lib_dummysuper);
 		return 1;
 	}
-
-	for (i = 0; INT_CONST[i].n; i++)
-		if (fastcmp(word,INT_CONST[i].n)) {
-			lua_pushinteger(L, INT_CONST[i].v);
-			return 1;
-		}
+	else if ((!mathlib && LUA_PushGlobals(L, word)) || ScanConstants(L, mathlib, word))
+		return 1;
 
 	if (mathlib) return luaL_error(L, "constant '%s' could not be parsed.\n", word);
 
-	// DYNAMIC variables too!!
-	// Try not to add anything that would break netgames or timeattack replays here.
-	// You know, like consoleplayer, displayplayer, secondarydisplayplayer, or gametime.
-	return LUA_PushGlobals(L, word);
+	return 0;
+}
+
+// If a sprname has been "cached" to _G, update it to a new value.
+void LUA_UpdateSprName(const char *name, lua_Integer value)
+{
+	char fullname[9] = "SPR_XXXX";
+
+	if (!gL)
+		return;
+
+	strncpy(&fullname[4], name, 4);
+	lua_pushstring(gL, fullname);
+	lua_rawget(gL, LUA_GLOBALSINDEX);
+
+	if (!lua_isnil(gL, -1))
+	{
+		lua_pushstring(gL, name);
+		lua_pushinteger(gL, value);
+		lua_rawset(gL, LUA_GLOBALSINDEX);
+	}
+
+	lua_pop(gL, 1); // pop the rawget result
 }
 
 int LUA_EnumLib(lua_State *L)
diff --git a/src/deh_lua.h b/src/deh_lua.h
index c400351b8c576969f9c5293b2756a97be446f2d4..1bec371ccb42d5b76549103d9068c5e471825c98 100644
--- a/src/deh_lua.h
+++ b/src/deh_lua.h
@@ -13,6 +13,21 @@
 #ifndef __DEH_LUA_H__
 #define __DEH_LUA_H__
 
+#include "g_game.h"
+#include "s_sound.h"
+#include "z_zone.h"
+#include "m_menu.h"
+#include "m_misc.h"
+#include "p_local.h"
+#include "st_stuff.h"
+#include "fastcmp.h"
+#include "lua_script.h"
+#include "lua_libs.h"
+
+#include "dehacked.h"
+#include "deh_tables.h"
+
+void LUA_UpdateSprName(const char *name, lua_Integer value);
 boolean LUA_SetLuaAction(void *state, const char *actiontocompare);
 const char *LUA_GetActionName(void *action);
 void LUA_SetActionByName(void *state, const char *actiontocompare);
diff --git a/src/deh_soc.c b/src/deh_soc.c
index 3a3942c14f2fb7e6544bd35fd6b0625a8b373896..59eb0a9bd93e12e86f78a251408f401698ebc724 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"
@@ -45,6 +45,7 @@
 #include "dehacked.h"
 #include "deh_soc.h"
 #include "deh_lua.h" // included due to some LUA_SetLuaAction hack smh
+// also used for LUA_UpdateSprName
 #include "deh_tables.h"
 
 // Loops through every constant and operation in word and performs its calculations, returning the final value.
@@ -439,6 +440,8 @@ void readfreeslots(MYFILE *f)
 					strncpy(sprnames[i],word,4);
 					//sprnames[i][4] = 0;
 					used_spr[(i-SPR_FIRSTFREESLOT)/8] |= 1<<(i%8); // Okay, this sprite slot has been named now.
+					// Lua needs to update the value in _G if it exists
+					LUA_UpdateSprName(word, i);
 					break;
 				}
 			}
@@ -909,7 +912,7 @@ static void readspriteframe(MYFILE *f, spriteinfo_t *sprinfo, UINT8 frame)
 			else if (fastcmp(word, "YPIVOT"))
 				sprinfo->pivot[frame].y = value;
 			else if (fastcmp(word, "ROTAXIS"))
-				sprinfo->pivot[frame].rotaxis = value;
+				deh_warning("SpriteInfo: ROTAXIS is deprecated and will be removed.");
 			else
 			{
 				f->curpos = lastline;
@@ -3839,6 +3842,10 @@ void readmaincfg(MYFILE *f)
 			{
 				useContinues = (UINT8)(value || word2[0] == 'T' || word2[0] == 'Y');
 			}
+			else if (fastcmp(word, "SHAREEMBLEMS"))
+			{
+				shareEmblems = (UINT8)(value || word2[0] == 'T' || word2[0] == 'Y');
+			}
 
 			else if (fastcmp(word, "GAMEDATA"))
 			{
@@ -3849,7 +3856,7 @@ void readmaincfg(MYFILE *f)
 				if (!GoodDataFileName(word2))
 					I_Error("Maincfg: bad data file name '%s'\n", word2);
 
-				G_SaveGameData();
+				G_SaveGameData(clientGamedata);
 				strlcpy(gamedatafilename, word2, sizeof (gamedatafilename));
 				strlwr(gamedatafilename);
 				savemoddata = true;
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 c3b2cfccd64aae8601e045c4587d3b435036ab97..7012ede466b2a1734fc9a47d9e63591ed900a05d 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -198,6 +198,7 @@ actionpointer_t actionpointers[] =
 	{{A_Boss3TakeDamage},        "A_BOSS3TAKEDAMAGE"},
 	{{A_Boss3Path},              "A_BOSS3PATH"},
 	{{A_Boss3ShockThink},        "A_BOSS3SHOCKTHINK"},
+	{{A_Shockwave},              "A_SHOCKWAVE"},
 	{{A_LinedefExecute},         "A_LINEDEFEXECUTE"},
 	{{A_LinedefExecuteFromArg},  "A_LINEDEFEXECUTEFROMARG"},
 	{{A_PlaySeeSound},           "A_PLAYSEESOUND"},
@@ -4409,11 +4410,12 @@ const char *const MOBJEFLAG_LIST[] = {
 	NULL
 };
 
-const char *const MAPTHINGFLAG_LIST[4] = {
+const char *const MAPTHINGFLAG_LIST[] = {
 	"EXTRA", // Extra flag for objects.
 	"OBJECTFLIP", // Reverse gravity flag for objects.
 	"OBJECTSPECIAL", // Special flag used with certain objects.
-	"AMBUSH" // Deaf monsters/do not react to sound.
+	"AMBUSH", // Deaf monsters/do not react to sound.
+	"ABSOLUTEZ" // Absolute spawn height flag for objects.
 };
 
 const char *const PLAYERFLAG_LIST[] = {
@@ -4550,6 +4552,7 @@ const char *const MSF_LIST[] = {
 const char *const SSF_LIST[] = {
 	"OUTERSPACE",
 	"DOUBLESTEPUP",
+	"NOSTEPDOWN",
 	"WINDCURRENT",
 	"CONVEYOR",
 	"SPEEDPAD",
@@ -4566,6 +4569,8 @@ const char *const SSF_LIST[] = {
 	"ZOOMTUBEEND",
 	"FINISHLINE",
 	"ROPEHANG",
+	"JUMPFLIP",
+	"GRAVITYOVERRIDE",
 	NULL
 };
 
@@ -4609,66 +4614,111 @@ const char *COLOR_ENUMS[] = {
 	// Desaturated
 	"AETHER",     	// SKINCOLOR_AETHER,
 	"SLATE",     	// SKINCOLOR_SLATE,
+	"MOONSTONE",   	// SKINCOLOR_MOONSTONE,
 	"BLUEBELL",   	// SKINCOLOR_BLUEBELL,
 	"PINK",     	// SKINCOLOR_PINK,
+	"ROSEWOOD",   	// SKINCOLOR_ROSEWOOD,
 	"YOGURT",     	// SKINCOLOR_YOGURT,
+	"LATTE",     	// SKINCOLOR_LATTE,
 	"BROWN",     	// SKINCOLOR_BROWN,
+	"BOULDER",   	// SKINCOLOR_BOULDER
 	"BRONZE",     	// SKINCOLOR_BRONZE,
-	"TAN",     		// SKINCOLOR_TAN,
+	"SEPIA",     	// SKINCOLOR_SEPIA,
+	"ECRU",     	// SKINCOLOR_ECRU,
+	"TAN",      	// SKINCOLOR_TAN,
 	"BEIGE",     	// SKINCOLOR_BEIGE,
+	"ROSEBUSH",  	// SKINCOLOR_ROSEBUSH,
 	"MOSS",     	// SKINCOLOR_MOSS,
 	"AZURE",     	// SKINCOLOR_AZURE,
-	"LAVENDER",     // SKINCOLOR_LAVENDER,
+	"EGGPLANT",  	// SKINCOLOR_EGGPLANT,
+	"LAVENDER",   	// SKINCOLOR_LAVENDER,
 
 	// Viv's vivid colours (toast 21/07/17)
+	// Tweaks & additions (Lach, sphere, Alice, MotorRoach 26/10/22)
 	"RUBY",     	// SKINCOLOR_RUBY,
+	"CHERRY",     	// SKINCOLOR_CHERRY,
 	"SALMON",     	// SKINCOLOR_SALMON,
+	"PEPPER",     	// SKINCOLOR_PEPPER,
 	"RED",     		// SKINCOLOR_RED,
 	"CRIMSON",     	// SKINCOLOR_CRIMSON,
 	"FLAME",     	// SKINCOLOR_FLAME,
+	"GARNET",      	// SKINCOLOR_GARNET,
 	"KETCHUP",     	// SKINCOLOR_KETCHUP,
 	"PEACHY",     	// SKINCOLOR_PEACHY,
 	"QUAIL",     	// SKINCOLOR_QUAIL,
+	"FOUNDATION",   // SKINCOLOR_FOUNDATION,
 	"SUNSET",     	// SKINCOLOR_SUNSET,
 	"COPPER",     	// SKINCOLOR_COPPER,
 	"APRICOT",     	// SKINCOLOR_APRICOT,
 	"ORANGE",     	// SKINCOLOR_ORANGE,
 	"RUST",     	// SKINCOLOR_RUST,
+	"TANGERINE",   	// SKINCOLOR_TANGERINE,
+	"TOPAZ",     	// SKINCOLOR_TOPAZ,
 	"GOLD",     	// SKINCOLOR_GOLD,
 	"SANDY",     	// SKINCOLOR_SANDY,
+	"GOLDENROD",   	// SKINCOLOR_GOLDENROD,
 	"YELLOW",     	// SKINCOLOR_YELLOW,
 	"OLIVE",     	// SKINCOLOR_OLIVE,
+	"PEAR",     	// SKINCOLOR_PEAR,
+	"LEMON",     	// SKINCOLOR_LEMON,
 	"LIME",     	// SKINCOLOR_LIME,
 	"PERIDOT",     	// SKINCOLOR_PERIDOT,
 	"APPLE",     	// SKINCOLOR_APPLE,
+	"HEADLIGHT",	// SKINCOLOR_HEADLIGHT,
+	"CHARTREUSE",   // SKINCOLOR_CHARTREUSE,
 	"GREEN",     	// SKINCOLOR_GREEN,
 	"FOREST",     	// SKINCOLOR_FOREST,
-	"EMERALD",     	// SKINCOLOR_EMERALD,
+	"SHAMROCK",    	// SKINCOLOR_SHAMROCK,
+	"JADE",     	// SKINCOLOR_JADE,
 	"MINT",     	// SKINCOLOR_MINT,
+	"MASTER",     	// SKINCOLOR_MASTER,
+	"EMERALD",     	// SKINCOLOR_EMERALD,
 	"SEAFOAM",     	// SKINCOLOR_SEAFOAM,
+	"ISLAND",     	// SKINCOLOR_ISLAND,
+	"BOTTLE",     	// SKINCOLOR_BOTTLE,
 	"AQUA",     	// SKINCOLOR_AQUA,
 	"TEAL",     	// SKINCOLOR_TEAL,
+	"OCEAN",     	// SKINCOLOR_OCEAN,
 	"WAVE",     	// SKINCOLOR_WAVE,
 	"CYAN",     	// SKINCOLOR_CYAN,
+	"TURQUOISE",    // SKINCOLOR_TURQUOISE,
+	"AQUAMARINE",  	// SKINCOLOR_AQUAMARINE,
 	"SKY",     		// SKINCOLOR_SKY,
+	"MARINE",     	// SKINCOLOR_MARINE,
 	"CERULEAN",     // SKINCOLOR_CERULEAN,
+	"DREAM",     	// SKINCOLOR_DREAM,
 	"ICY",     		// SKINCOLOR_ICY,
+	"DAYBREAK",     // SKINCOLOR_DAYBREAK,
 	"SAPPHIRE",     // SKINCOLOR_SAPPHIRE,
+	"ARCTIC",     	// SKINCOLOR_ARCTIC,
 	"CORNFLOWER",   // SKINCOLOR_CORNFLOWER,
 	"BLUE",     	// SKINCOLOR_BLUE,
 	"COBALT",     	// SKINCOLOR_COBALT,
+	"MIDNIGHT",     // SKINCOLOR_MIDNIGHT,
+	"GALAXY",     	// SKINCOLOR_GALAXY,
 	"VAPOR",     	// SKINCOLOR_VAPOR,
 	"DUSK",     	// SKINCOLOR_DUSK,
+	"MAJESTY",     	// SKINCOLOR_MAJESTY,
 	"PASTEL",     	// SKINCOLOR_PASTEL,
 	"PURPLE",     	// SKINCOLOR_PURPLE,
-	"BUBBLEGUM",    // SKINCOLOR_BUBBLEGUM,
+	"NOBLE",     	// SKINCOLOR_NOBLE,
+	"FUCHSIA",     	// SKINCOLOR_FUCHSIA,
+	"BUBBLEGUM",   	// SKINCOLOR_BUBBLEGUM,
+	"SIBERITE",   	// SKINCOLOR_SIBERITE,
 	"MAGENTA",     	// SKINCOLOR_MAGENTA,
 	"NEON",     	// SKINCOLOR_NEON,
 	"VIOLET",     	// SKINCOLOR_VIOLET,
+	"ROYAL",     	// SKINCOLOR_ROYAL,
 	"LILAC",     	// SKINCOLOR_LILAC,
+	"MAUVE",     	// SKINCOLOR_MAUVE,
+	"EVENTIDE",   	// SKINCOLOR_EVENTIDE,
 	"PLUM",     	// SKINCOLOR_PLUM,
 	"RASPBERRY",  	// SKINCOLOR_RASPBERRY,
+	"TAFFY",     	// SKINCOLOR_TAFFY,
 	"ROSY",     	// SKINCOLOR_ROSY,
+	"FANCY",  		// SKINCOLOR_FANCY,
+	"SANGRIA",     	// SKINCOLOR_SANGRIA,
+	"VOLCANIC",    	// SKINCOLOR_VOLCANIC,
 
 	// Super special awesome Super flashing colors!
 	"SUPERSILVER1",	// SKINCOLOR_SUPERSILVER1
@@ -4768,7 +4818,9 @@ const char *const POWERS_LIST[] = {
 
 	"JUSTLAUNCHED",
 
-	"IGNORELATCH"
+	"IGNORELATCH",
+
+	"STRONG"
 };
 
 const char *const HUDITEMS_LIST[] = {
@@ -4831,7 +4883,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
@@ -5121,6 +5173,30 @@ struct int_const_s const INT_CONST[] = {
 	{"CR_DUSTDEVIL",CR_DUSTDEVIL},
 	{"CR_FAN",CR_FAN},
 
+	// Strong powers
+	{"STR_NONE",STR_NONE},
+	{"STR_ANIM",STR_ANIM},
+	{"STR_PUNCH",STR_PUNCH},
+	{"STR_TAIL",STR_TAIL},
+	{"STR_STOMP",STR_STOMP},
+	{"STR_UPPER",STR_UPPER},
+	{"STR_GUARD",STR_GUARD},
+	{"STR_HEAVY",STR_HEAVY},
+	{"STR_DASH",STR_DASH},
+	{"STR_WALL",STR_WALL},
+	{"STR_FLOOR",STR_FLOOR},
+	{"STR_CEILING",STR_CEILING},
+	{"STR_SPRING",STR_SPRING},
+	{"STR_SPIKE",STR_SPIKE},
+	{"STR_ATTACK",STR_ATTACK},
+	{"STR_BUST",STR_BUST},
+	{"STR_FLY",STR_FLY},
+	{"STR_GLIDE",STR_GLIDE},
+	{"STR_TWINSPIN",STR_TWINSPIN},
+	{"STR_MELEE",STR_MELEE},
+	{"STR_BOUNCE",STR_BOUNCE},
+	{"STR_METAL",STR_METAL},
+
 	// Ring weapons (ringweapons_t)
 	// Useful for A_GiveWeapon
 	{"RW_AUTO",RW_AUTO},
diff --git a/src/deh_tables.h b/src/deh_tables.h
index 8943ab71a4a2c29e5740f707ee240c6d6f0bba7c..42716f9b4bd271c98aca83737f419670d862d7e5 100644
--- a/src/deh_tables.h
+++ b/src/deh_tables.h
@@ -30,6 +30,7 @@ extern UINT8 used_spr[(NUMSPRITEFREESLOTS / 8) + 1]; // Bitwise flag for sprite
 	memset(FREE_MOBJS,0,sizeof(char *) * NUMMOBJFREESLOTS);\
 	memset(FREE_SKINCOLORS,0,sizeof(char *) * NUMCOLORFREESLOTS);\
 	memset(used_spr,0,sizeof(UINT8) * ((NUMSPRITEFREESLOTS / 8) + 1));\
+	memset(actionsoverridden, LUA_REFNIL, sizeof(actionsoverridden));\
 }
 
 struct flickytypes_s {
@@ -61,7 +62,7 @@ extern const char *const MOBJTYPE_LIST[];
 extern const char *const MOBJFLAG_LIST[];
 extern const char *const MOBJFLAG2_LIST[]; // \tMF2_(\S+).*// (.+) --> \t"\1", // \2
 extern const char *const MOBJEFLAG_LIST[];
-extern const char *const MAPTHINGFLAG_LIST[4];
+extern const char *const MAPTHINGFLAG_LIST[];
 extern const char *const PLAYERFLAG_LIST[];
 extern const char *const GAMETYPERULE_LIST[];
 extern const char *const ML_LIST[]; // Linedef flags
diff --git a/src/dehacked.c b/src/dehacked.c
index 17768eb7f4496107503a50fedbbffc0da0812b0b..fd2a701715e17ce7dab0b6c829147113844972fe 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -575,7 +575,7 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile)
 	} // end while
 
 	if (gamedataadded)
-		G_LoadGameData();
+		G_LoadGameData(clientGamedata);
 
 	if (gamestate == GS_TITLESCREEN)
 	{
diff --git a/src/dehacked.h b/src/dehacked.h
index 902404df7e0b1dffa533a3222554c2e564646aa9..b39f090352adf6b7e7e5d744d5644e78da48ee58 100644
--- a/src/dehacked.h
+++ b/src/dehacked.h
@@ -40,9 +40,9 @@ extern boolean gamedataadded;
 extern boolean titlechanged;
 extern boolean introchanged;
 
-#define MAXRECURSION 30
-extern const char *superactions[MAXRECURSION];
-extern UINT8 superstack;
+#define MAX_ACTION_RECURSION 30
+extern const char *luaactions[MAX_ACTION_RECURSION];
+extern UINT8 luaactionstack;
 
 // If the dehacked patch does not match this version, we throw a warning
 #define PATCHVERSION 220
diff --git a/src/doomdata.h b/src/doomdata.h
index 4c5bdefaf968f073462ab37b295cf85b5185954e..276e03297b6f0d453ad0d0c65fc8bd9196d9680c 100644
--- a/src/doomdata.h
+++ b/src/doomdata.h
@@ -62,6 +62,10 @@ enum
 #define MTF_AMBUSH 8
 
 // Do not use bit five or after, as they are used for object z-offsets.
+// Unless it's exclusive to UDMF.
+
+// Flag to use Z as absolute spawn height, ignoring the floor and ceiling.
+#define MTF_ABSOLUTEZ 16
 
 #if defined(_MSC_VER)
 #pragma pack(1)
@@ -211,6 +215,7 @@ typedef struct
 	UINT8 extrainfo;
 	taglist_t tags;
 	fixed_t scale;
+	fixed_t spritexscale, spriteyscale;
 	INT32 args[NUMMAPTHINGARGS];
 	char *stringargs[NUMMAPTHINGSTRINGARGS];
 	struct mobj_s *mobj;
diff --git a/src/doomdef.h b/src/doomdef.h
index d521d4fbbb0ef978edddd6696328b45de64c55ab..b382d0ecb4bbaa09a0da1180e481e2f979014e32 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
@@ -108,6 +107,14 @@ FILE *fopenfile(const char*, const char*);
 
 //#define NOMD5
 
+// If you don't disable ALL debug first, you get ALL debug enabled
+#if !defined (NDEBUG)
+#define PACKETDROP
+#define PARANOIA
+#define RANGECHECK
+#define ZDEBUG
+#endif
+
 // Uncheck this to compile debugging code
 //#define RANGECHECK
 //#ifndef PARANOIA
@@ -261,66 +268,111 @@ typedef enum
 	// Desaturated
 	SKINCOLOR_AETHER,
 	SKINCOLOR_SLATE,
+	SKINCOLOR_MOONSTONE,
 	SKINCOLOR_BLUEBELL,
 	SKINCOLOR_PINK,
+	SKINCOLOR_ROSEWOOD,
 	SKINCOLOR_YOGURT,
+	SKINCOLOR_LATTE,
 	SKINCOLOR_BROWN,
+	SKINCOLOR_BOULDER,
 	SKINCOLOR_BRONZE,
+	SKINCOLOR_SEPIA,
+	SKINCOLOR_ECRU,
 	SKINCOLOR_TAN,
 	SKINCOLOR_BEIGE,
+	SKINCOLOR_ROSEBUSH,
 	SKINCOLOR_MOSS,
 	SKINCOLOR_AZURE,
+	SKINCOLOR_EGGPLANT,
 	SKINCOLOR_LAVENDER,
 
 	// Viv's vivid colours (toast 21/07/17)
+	// Tweaks & additions (Lach, sphere, Alice, MotorRoach 26/10/22)
 	SKINCOLOR_RUBY,
+	SKINCOLOR_CHERRY,
 	SKINCOLOR_SALMON,
+	SKINCOLOR_PEPPER,
 	SKINCOLOR_RED,
 	SKINCOLOR_CRIMSON,
 	SKINCOLOR_FLAME,
+	SKINCOLOR_GARNET,
 	SKINCOLOR_KETCHUP,
 	SKINCOLOR_PEACHY,
 	SKINCOLOR_QUAIL,
+	SKINCOLOR_FOUNDATION,
 	SKINCOLOR_SUNSET,
 	SKINCOLOR_COPPER,
 	SKINCOLOR_APRICOT,
 	SKINCOLOR_ORANGE,
 	SKINCOLOR_RUST,
+	SKINCOLOR_TANGERINE,
+	SKINCOLOR_TOPAZ,
 	SKINCOLOR_GOLD,
 	SKINCOLOR_SANDY,
+	SKINCOLOR_GOLDENROD,
 	SKINCOLOR_YELLOW,
 	SKINCOLOR_OLIVE,
+	SKINCOLOR_PEAR,
+	SKINCOLOR_LEMON,
 	SKINCOLOR_LIME,
 	SKINCOLOR_PERIDOT,
 	SKINCOLOR_APPLE,
+	SKINCOLOR_HEADLIGHT,
+	SKINCOLOR_CHARTREUSE,
 	SKINCOLOR_GREEN,
 	SKINCOLOR_FOREST,
-	SKINCOLOR_EMERALD,
+	SKINCOLOR_SHAMROCK,
+	SKINCOLOR_JADE,
 	SKINCOLOR_MINT,
+	SKINCOLOR_MASTER,
+	SKINCOLOR_EMERALD,
 	SKINCOLOR_SEAFOAM,
+	SKINCOLOR_ISLAND,
+	SKINCOLOR_BOTTLE,
 	SKINCOLOR_AQUA,
 	SKINCOLOR_TEAL,
+	SKINCOLOR_OCEAN,
 	SKINCOLOR_WAVE,
 	SKINCOLOR_CYAN,
+	SKINCOLOR_TURQUOISE,
+	SKINCOLOR_AQUAMARINE,
 	SKINCOLOR_SKY,
+	SKINCOLOR_MARINE,
 	SKINCOLOR_CERULEAN,
+	SKINCOLOR_DREAM,
 	SKINCOLOR_ICY,
+	SKINCOLOR_DAYBREAK,
 	SKINCOLOR_SAPPHIRE, // sweet mother, i cannot weave – slender aphrodite has overcome me with longing for a girl
+	SKINCOLOR_ARCTIC,
 	SKINCOLOR_CORNFLOWER,
 	SKINCOLOR_BLUE,
 	SKINCOLOR_COBALT,
+	SKINCOLOR_MIDNIGHT,
+	SKINCOLOR_GALAXY,
 	SKINCOLOR_VAPOR,
 	SKINCOLOR_DUSK,
+	SKINCOLOR_MAJESTY,
 	SKINCOLOR_PASTEL,
 	SKINCOLOR_PURPLE,
+	SKINCOLOR_NOBLE,
+	SKINCOLOR_FUCHSIA,
 	SKINCOLOR_BUBBLEGUM,
+	SKINCOLOR_SIBERITE,
 	SKINCOLOR_MAGENTA,
 	SKINCOLOR_NEON,
 	SKINCOLOR_VIOLET,
+	SKINCOLOR_ROYAL,
 	SKINCOLOR_LILAC,
+	SKINCOLOR_MAUVE,
+	SKINCOLOR_EVENTIDE,
 	SKINCOLOR_PLUM,
 	SKINCOLOR_RASPBERRY,
+	SKINCOLOR_TAFFY,
 	SKINCOLOR_ROSY,
+	SKINCOLOR_FANCY,
+	SKINCOLOR_SANGRIA,
+	SKINCOLOR_VOLCANIC,
 
 	FIRSTSUPERCOLOR,
 
@@ -592,7 +644,16 @@ UINT32 quickncasehash (const char *p, size_t n)
 #define PUNCTUATION "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
 
 // Compile date and time and revision.
-extern const char *compdate, *comptime, *comprevision, *compbranch;
+extern const char
+	*compdate,
+	*comptime,
+	*comprevision,
+	*compbranch,
+	*compnote,
+	*comptype;
+extern int
+	compuncommitted,
+	compoptimized;
 
 // Disabled code and code under testing
 // None of these that are disabled in the normal build are guaranteed to work perfectly
@@ -662,7 +723,7 @@ extern const char *compdate, *comptime, *comprevision, *compbranch;
 /// 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 847c10b8c9835f737d47f4fd2b49412a27e03d76..6a2d6acf00f816804c1ec8830b69c9848deadcbe 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -132,8 +132,6 @@ extern INT32 postimgparam2;
 extern INT32 viewwindowx, viewwindowy;
 extern INT32 viewwidth, scaledviewwidth;
 
-extern boolean gamedataloaded;
-
 // Player taking events, and displaying.
 extern INT32 consoleplayer;
 extern INT32 displayplayer;
@@ -251,6 +249,7 @@ extern textprompt_t *textprompts[MAX_PROMPTS];
 // For the Custom Exit linedef.
 extern INT16 nextmapoverride;
 extern UINT8 skipstats;
+extern INT16 nextgametype;
 
 extern UINT32 ssspheres; //  Total # of spheres in a level
 
@@ -495,8 +494,6 @@ typedef struct
 extern tolinfo_t TYPEOFLEVEL[NUMTOLNAMES];
 extern UINT32 lastcustomtol;
 
-extern tic_t totalplaytime;
-
 extern boolean stagefailed;
 
 // Emeralds stored as bits to throw savegame hackers off.
@@ -515,52 +512,6 @@ extern INT32 luabanks[NUM_LUABANKS];
 
 extern INT32 nummaprings; //keep track of spawned rings/coins
 
-/** Time attack information, currently a very small structure.
-  */
-typedef struct
-{
-	tic_t time;   ///< Time in which the level was finished.
-	UINT32 score; ///< Score when the level was finished.
-	UINT16 rings; ///< Rings when the level was finished.
-} recorddata_t;
-
-/** Setup for one NiGHTS map.
-  * These are dynamically allocated because I am insane
-  */
-#define GRADE_F 0
-#define GRADE_E 1
-#define GRADE_D 2
-#define GRADE_C 3
-#define GRADE_B 4
-#define GRADE_A 5
-#define GRADE_S 6
-
-typedef struct
-{
-	// 8 mares, 1 overall (0)
-	UINT8	nummares;
-	UINT32	score[9];
-	UINT8	grade[9];
-	tic_t	time[9];
-} nightsdata_t;
-
-extern nightsdata_t *nightsrecords[NUMMAPS];
-extern recorddata_t *mainrecords[NUMMAPS];
-
-// mapvisited is now a set of flags that says what we've done in the map.
-#define MV_VISITED      1
-#define MV_BEATEN       2
-#define MV_ALLEMERALDS  4
-#define MV_ULTIMATE     8
-#define MV_PERFECT     16
-#define MV_PERFECTRA   32
-#define MV_MAX         63 // used in gamedata check, update whenever MV's are added
-#define MV_MP         128
-extern UINT8 mapvisited[NUMMAPS];
-
-// Temporary holding place for nights data for the current map
-extern nightsdata_t ntemprecords;
-
 extern UINT32 token; ///< Number of tokens collected in a level
 extern UINT32 tokenlist; ///< List of tokens collected
 extern boolean gottoken; ///< Did you get a token? Used for end of act
@@ -593,9 +544,12 @@ extern UINT8 useBlackRock;
 
 extern UINT8 use1upSound;
 extern UINT8 maxXtraLife; // Max extra lives from rings
+
 extern UINT8 useContinues;
 #define continuesInSession (!multiplayer && (ultimatemode || (useContinues && !marathonmode) || (!modeattacking && !(cursaveslot > 0))))
 
+extern UINT8 shareEmblems;
+
 extern mobj_t *hunt1, *hunt2, *hunt3; // Emerald hunt locations
 
 // For racing
@@ -616,10 +570,6 @@ extern INT32 cheats;
 
 extern tic_t hidetime;
 
-extern UINT32 timesBeaten; // # of times the game has been beaten.
-extern UINT32 timesBeatenWithEmeralds;
-extern UINT32 timesBeatenUltimate;
-
 // ===========================
 // Internal parameters, fixed.
 // ===========================
@@ -684,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/doomtype.h b/src/doomtype.h
index f6c236e20bf3979ec9f1ef2353ed80b647e45b73..4070e346a1b13dd516a91504f056cebc4defb20d 100644
--- a/src/doomtype.h
+++ b/src/doomtype.h
@@ -17,6 +17,10 @@
 #ifndef __DOOMTYPE__
 #define __DOOMTYPE__
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #ifdef _WIN32
 //#define WIN32_LEAN_AND_MEAN
 #define RPC_NO_WINDOWS_H
@@ -78,7 +82,9 @@ typedef long ssize_t;
 #endif
 	#define strncasecmp             strnicmp
 	#define strcasecmp              stricmp
+#ifndef __cplusplus
 	#define inline                  __inline
+#endif
 #elif defined (__WATCOMC__)
 	#include <dos.h>
 	#include <sys\types.h>
@@ -94,32 +100,26 @@ typedef long ssize_t;
 	#define strnicmp(x,y,n) strncasecmp(x,y,n)
 #endif
 
-char *strcasestr(const char *in, const char *what);
+char *nongnu_strcasestr(const char *in, const char *what);
+#ifndef _GNU_SOURCE
+#define strcasestr nongnu_strcasestr
+#endif
 #define stristr strcasestr
 
 int startswith (const char *base, const char *tag);
 int endswith (const char *base, const char *tag);
 
-#if defined (macintosh) //|| defined (__APPLE__) //skip all boolean/Boolean crap
-	#define true 1
-	#define false 0
-	#define min(x,y) (((x)<(y)) ? (x) : (y))
-	#define max(x,y) (((x)>(y)) ? (x) : (y))
-
-#ifdef macintosh
-	#define stricmp strcmp
-	#define strnicmp strncmp
+#if defined (_WIN32) || defined (__HAIKU__)
+#define HAVE_DOSSTR_FUNCS
 #endif
 
-	#define boolean INT32
-
-	#ifndef O_BINARY
-	#define O_BINARY 0
+#if defined (__APPLE__)
+	#define SRB2_HAVE_STRLCPY
+#elif defined (__GLIBC_PREREQ)
+	// glibc 2.38: added strlcpy and strlcat to _DEFAULT_SOURCE
+	#if __GLIBC_PREREQ(2, 38)
+		#define SRB2_HAVE_STRLCPY
 	#endif
-#endif //macintosh
-
-#if defined (_WIN32) || defined (__HAIKU__)
-#define HAVE_DOSSTR_FUNCS
 #endif
 
 #ifndef HAVE_DOSSTR_FUNCS
@@ -129,7 +129,7 @@ int strlwr(char *n); // from dosstr.c
 
 #include <stddef.h> // for size_t
 
-#ifndef __APPLE__
+#ifndef SRB2_HAVE_STRLCPY
 size_t strlcat(char *dst, const char *src, size_t siz);
 size_t strlcpy(char *dst, const char *src, size_t siz);
 #endif
@@ -144,22 +144,24 @@ size_t strlcpy(char *dst, const char *src, size_t siz);
 
 /* Boolean type definition */
 
-// \note __BYTEBOOL__ used to be set above if "macintosh" was defined,
-// if macintosh's version of boolean type isn't needed anymore, then isn't this macro pointless now?
-#ifndef __BYTEBOOL__
-	#define __BYTEBOOL__
-
-	//faB: clean that up !!
-	#if defined( _MSC_VER)  && (_MSC_VER >= 1800) // MSVC 2013 and forward
-		#include "stdbool.h"
-	#elif defined (_WIN32)
-		#define false   FALSE           // use windows types
-		#define true    TRUE
-		#define boolean BOOL
-	#else
-		typedef enum {false, true} boolean;
-	#endif
-#endif // __BYTEBOOL__
+// Note: C++ bool and C99/C11 _Bool are NOT compatible.
+// Historically, boolean was win32 BOOL on Windows. For equivalence, it's now
+// int32_t. "true" and "false" are only declared for C code; in C++, conversion
+// between "bool" and "int32_t" takes over.
+#ifndef _WIN32
+typedef int32_t boolean;
+#else
+#define boolean BOOL
+#endif
+
+#ifndef __cplusplus
+#ifndef _WIN32
+enum {false = 0, true = 1};
+#else
+#define false FALSE
+#define true TRUE
+#endif
+#endif
 
 /* 7.18.2.1  Limits of exact-width integer types */
 
@@ -387,4 +389,8 @@ unset_bit_array (bitarray_t * const array, const int value)
 
 typedef UINT64 precise_t;
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif //__DOOMTYPE__
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_system.c b/src/dummy/i_system.c
index 8556c0248651d04469b4c1114a780bba47824421..70e1ef4ec2b7aba9699731c0d743733750bc26fc 100644
--- a/src/dummy/i_system.c
+++ b/src/dummy/i_system.c
@@ -14,13 +14,18 @@ size_t I_GetFreeMem(size_t *total)
 	return 0;
 }
 
-void I_Sleep(UINT32 ms){}
+void I_Sleep(UINT32 ms)
+{
+	(void)ms;
+}
 
-precise_t I_GetPreciseTime(void) {
+precise_t I_GetPreciseTime(void)
+{
 	return 0;
 }
 
-UINT64 I_GetPrecisePrecision(void) {
+UINT64 I_GetPrecisePrecision(void)
+{
 	return 1000000;
 }
 
@@ -180,7 +185,14 @@ const char *I_ClipboardPaste(void)
 	return NULL;
 }
 
-void I_RegisterSysCommands(void) {}
+size_t I_GetRandomBytes(char *destination, size_t amount)
+{
+	(void)destination;
+	(void)amount;
+	return 0;
+}
+
+void I_RegisterSysCommands(void){}
 
 void I_GetCursorPosition(INT32 *x, INT32 *y)
 {
diff --git a/src/dummy/i_video.c b/src/dummy/i_video.c
index 3b0a12a328df587e1cd20d0312cf96ce6c8df847..bb796b6767a1d55990209ba46cd8245377b013d9 100644
--- a/src/dummy/i_video.c
+++ b/src/dummy/i_video.c
@@ -57,6 +57,8 @@ const char *VID_GetModeName(INT32 modenum)
 	return NULL;
 }
 
+UINT32 I_GetRefreshRate(void) { return 35; }
+
 void I_UpdateNoBlit(void){}
 
 void I_FinishUpdate(void){}
diff --git a/src/f_finale.c b/src/f_finale.c
index 68b2641a1d0bd248d1eb94e9a19de3945d9f2c58..7afed27d07055038328eb44ce09ea6fe15355de3 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"
@@ -63,7 +63,6 @@ static tic_t stoptimer;
 static boolean keypressed = false;
 
 // (no longer) De-Demo'd Title Screen
-static INT32 menuanimtimer; // Title screen: background animation timing
 mobj_t *titlemapcameraref = NULL;
 
 // menu presentation state
@@ -75,6 +74,8 @@ INT32 curbgyspeed;
 boolean curbghide;
 boolean hidetitlemap;		// WARNING: set to false by M_SetupNextMenu and M_ClearMenus
 
+static fixed_t curbgx = 0;
+static fixed_t curbgy = 0;
 static UINT8  curDemo = 0;
 static UINT32 demoDelayLeft;
 static UINT32 demoIdleLeft;
@@ -223,7 +224,6 @@ static INT32 cutscene_writeptr = 0;
 static INT32 cutscene_textcount = 0;
 static INT32 cutscene_textspeed = 0;
 static UINT8 cutscene_boostspeed = 0;
-static tic_t cutscene_lasttextwrite = 0;
 
 // STJR Intro
 char stjrintro[9] = "STJRI000";
@@ -239,11 +239,6 @@ static UINT8 F_WriteText(void)
 {
 	INT32 numtowrite = 1;
 	const char *c;
-	tic_t ltw = I_GetTime();
-
-	if (cutscene_lasttextwrite == ltw)
-		return 1; // singletics prevention
-	cutscene_lasttextwrite = ltw;
 
 	if (cutscene_boostspeed)
 	{
@@ -1067,12 +1062,14 @@ static const char *credits[] = {
 	"\"Golden\"",
 	"Vivian \"toaster\" Grannell",
 	"Julio \"Chaos Zero 64\" Guir",
+	"\"Hanicef\"",
 	"\"Hannu_Hanhi\"", // For many OpenGL performance improvements!
 	"Kepa \"Nev3r\" Iceta",
 	"Thomas \"Shadow Hog\" Igoe",
 	"Iestyn \"Monster Iestyn\" Jealous",
 	"\"Kaito Sinclaire\"",
 	"\"Kalaron\"", // Coded some of Sryder13's collection of OpenGL fixes, especially fog
+	"\"katsy\"",
 	"Ronald \"Furyhunter\" Kinard", // The SDL2 port
 	"\"Lat'\"", // SRB2-CHAT, the chat window from Kart
 	"\"LZA\"",
@@ -1095,6 +1092,7 @@ static const char *credits[] = {
 	"Ben \"Cue\" Woodford",
 	"Lachlan \"Lach\" Wright",
 	"Marco \"mazmazz\" Zafra",
+	"\"Zwip-Zwap Zapony\"",
 	"",
 	"\1Art",
 	"Victor \"VAdaPEGA\" Ara\x1Fjo", // Araújo -- sorry for our limited font! D:
@@ -1202,6 +1200,7 @@ static const char *credits[] = {
 	"FreeDoom Project", // Used some of the mancubus and rocket launcher sprites for Brak
 	"Kart Krew",
 	"Alex \"MistaED\" Fuller",
+	"Howard Drossin", // Virtual Sonic - Sonic & Knuckles Theme
 	"Pascal \"CodeImp\" vd Heiden", // Doom Builder developer
 	"Randi Heit (<!>)", // For their MSPaint <!> sprite that we nicked
 	"Simon \"sirjuddington\" Judd", // SLADE developer
@@ -1279,14 +1278,23 @@ void F_CreditDrawer(void)
 	UINT16 i;
 	INT16 zagpos = (timetonext - finalecount - animtimer) % 32;
 	fixed_t y = (80<<FRACBITS) - (animtimer<<FRACBITS>>1);
+	UINT8 colornum;
+	const UINT8 *colormap;
+
+	if (players[consoleplayer].skincolor)
+		colornum = players[consoleplayer].skincolor;
+	else
+		colornum = cv_playercolor.value;
+
+	colormap = R_GetTranslationColormap(TC_DEFAULT, colornum, GTC_CACHE);
 
 	V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
 
 	// Zig Zagz
-	V_DrawScaledPatch(-16,               zagpos,       V_SNAPTOLEFT,         W_CachePatchName("LTZIGZAG", PU_PATCH_LOWPRIORITY));
-	V_DrawScaledPatch(-16,               zagpos - 320, V_SNAPTOLEFT,         W_CachePatchName("LTZIGZAG", PU_PATCH_LOWPRIORITY));
-	V_DrawScaledPatch(BASEVIDWIDTH + 16, zagpos,       V_SNAPTORIGHT|V_FLIP, W_CachePatchName("LTZIGZAG", PU_PATCH_LOWPRIORITY));
-	V_DrawScaledPatch(BASEVIDWIDTH + 16, zagpos - 320, V_SNAPTORIGHT|V_FLIP, W_CachePatchName("LTZIGZAG", PU_PATCH_LOWPRIORITY));
+	V_DrawFixedPatch(-16*FRACUNIT, zagpos<<FRACBITS, FRACUNIT, V_SNAPTOLEFT, W_CachePatchName("LTZIGZAG", PU_PATCH_LOWPRIORITY), colormap);
+	V_DrawFixedPatch(-16*FRACUNIT, (zagpos - 320)<<FRACBITS, FRACUNIT, V_SNAPTOLEFT, W_CachePatchName("LTZIGZAG", PU_PATCH_LOWPRIORITY), colormap);
+	V_DrawFixedPatch((BASEVIDWIDTH + 16)*FRACUNIT, zagpos<<FRACBITS, FRACUNIT, V_SNAPTORIGHT|V_FLIP, W_CachePatchName("LTZIGZAG", PU_PATCH_LOWPRIORITY), colormap);
+	V_DrawFixedPatch((BASEVIDWIDTH + 16)*FRACUNIT, (zagpos - 320)<<FRACBITS, FRACUNIT, V_SNAPTORIGHT|V_FLIP, W_CachePatchName("LTZIGZAG", PU_PATCH_LOWPRIORITY), colormap);
 
 	// Draw background pictures first
 	for (i = 0; credits_pics[i].patch; i++)
@@ -1411,7 +1419,7 @@ boolean F_CreditResponder(event_t *event)
 			break;
 	}
 
-	if (!(timesBeaten) && !(netgame || multiplayer) && !cv_debug)
+	if (!(serverGamedata->timesBeaten) && !(netgame || multiplayer) && !cv_debug)
 		return false;
 
 	if (event->type != ev_keydown)
@@ -1573,23 +1581,17 @@ void F_GameEvaluationDrawer(void)
 #if 0 // the following looks like hot garbage the more unlockables we add, and we now have a lot of unlockables
 	if (finalecount >= 5*TICRATE)
 	{
+		INT32 startcoord = 32;
 		V_DrawString(8, 16, V_YELLOWMAP, "Unlocked:");
 
-		if (netgame)
-			V_DrawString(8, 96, V_YELLOWMAP, "Multiplayer games\ncan't unlock\nextras!");
-		else
+		for (i = 0; i < MAXUNLOCKABLES; i++)
 		{
-			INT32 startcoord = 32;
-
-			for (i = 0; i < MAXUNLOCKABLES; i++)
+			if (unlockables[i].conditionset && unlockables[i].conditionset < MAXCONDITIONSETS
+				&& unlockables[i].type && !unlockables[i].nocecho)
 			{
-				if (unlockables[i].conditionset && unlockables[i].conditionset < MAXCONDITIONSETS
-					&& unlockables[i].type && !unlockables[i].nocecho)
-				{
-					if (unlockables[i].unlocked)
-						V_DrawString(8, startcoord, 0, unlockables[i].name);
-					startcoord += 8;
-				}
+				if (clientGamedata->unlocked[i])
+					V_DrawString(8, startcoord, 0, unlockables[i].name);
+				startcoord += 8;
 			}
 		}
 	}
@@ -1646,30 +1648,29 @@ void F_GameEvaluationTicker(void)
 		sparklloop = 0;
 	}
 
-	if (finalecount == 5*TICRATE)
+	if (G_CoopGametype() && !stagefailed && finalecount == 5*TICRATE)
 	{
-		if (netgame || multiplayer) // modify this when we finally allow unlocking stuff in 2P
+		serverGamedata->timesBeaten++;
+		clientGamedata->timesBeaten++;
+
+		if (ALL7EMERALDS(emeralds))
 		{
-			HU_SetCEchoFlags(V_YELLOWMAP|V_RETURN8);
-			HU_SetCEchoDuration(6);
-			HU_DoCEcho("\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\Multiplayer games can't unlock extras!");
-			S_StartSound(NULL, sfx_s3k68);
+			serverGamedata->timesBeatenWithEmeralds++;
+			clientGamedata->timesBeatenWithEmeralds++;
 		}
-		else
-		{
-			++timesBeaten;
 
-			if (ALL7EMERALDS(emeralds))
-				++timesBeatenWithEmeralds;
+		if (ultimatemode)
+		{
+			serverGamedata->timesBeatenUltimate++;
+			clientGamedata->timesBeatenUltimate++;
+		}
 
-			if (ultimatemode)
-				++timesBeatenUltimate;
+		M_SilentUpdateUnlockablesAndEmblems(serverGamedata);
 
-			if (M_UpdateUnlockablesAndExtraEmblems())
-				S_StartSound(NULL, sfx_s3k68);
+		if (M_UpdateUnlockablesAndExtraEmblems(clientGamedata))
+			S_StartSound(NULL, sfx_s3k68);
 
-			G_SaveGameData();
-		}
+		G_SaveGameData(clientGamedata);
 	}
 }
 
@@ -2183,7 +2184,7 @@ void F_EndingDrawer(void)
 			//colset(linkmap,  164, 165, 169); -- the ideal purple colour to represent a clicked in-game link, but not worth it just for a soundtest-controlled secret
 			V_DrawCenteredString(BASEVIDWIDTH/2, 8, V_ALLOWLOWERCASE|(trans<<V_ALPHASHIFT), str);
 			V_DrawCharacter(32, BASEVIDHEIGHT-16, '>'|(trans<<V_ALPHASHIFT), false);
-			V_DrawString(40, ((finalecount == STOPPINGPOINT-(20+TICRATE)) ? 1 : 0)+BASEVIDHEIGHT-16, ((timesBeaten || finalecount >= STOPPINGPOINT-TICRATE) ? V_PURPLEMAP : V_BLUEMAP)|(trans<<V_ALPHASHIFT), " [S] ===>");
+			V_DrawString(40, ((finalecount == STOPPINGPOINT-(20+TICRATE)) ? 1 : 0)+BASEVIDHEIGHT-16, ((serverGamedata->timesBeaten || finalecount >= STOPPINGPOINT-TICRATE) ? V_PURPLEMAP : V_BLUEMAP)|(trans<<V_ALPHASHIFT), " [S] ===>");
 		}
 
 		if (finalecount > STOPPINGPOINT-(20+(2*TICRATE)))
@@ -2249,7 +2250,8 @@ void F_GameEndTicker(void)
 
 void F_InitMenuPresValues(void)
 {
-	menuanimtimer = 0;
+	curbgx = 0;
+	curbgy = 0;
 	prevMenuId = 0;
 	activeMenuId = MainDef.menuid;
 
@@ -2258,7 +2260,7 @@ void F_InitMenuPresValues(void)
 	curfadevalue = 16;
 	curbgcolor = -1;
 	curbgxspeed = (gamestate == GS_TIMEATTACK) ? 0 : titlescrollxspeed;
-	curbgyspeed = (gamestate == GS_TIMEATTACK) ? 22 : titlescrollyspeed;
+	curbgyspeed = (gamestate == GS_TIMEATTACK) ? 18 : titlescrollyspeed;
 	curbghide = (gamestate == GS_TIMEATTACK) ? false : true;
 
 	curhidepics = hidetitlepics;
@@ -2282,17 +2284,11 @@ void F_InitMenuPresValues(void)
 //
 // F_SkyScroll
 //
-void F_SkyScroll(INT32 scrollxspeed, INT32 scrollyspeed, const char *patchname)
+void F_SkyScroll(const char *patchname)
 {
-	INT32 xscrolled, x, xneg = (scrollxspeed > 0) - (scrollxspeed < 0), tilex;
-	INT32 yscrolled, y, yneg = (scrollyspeed > 0) - (scrollyspeed < 0), tiley;
-	boolean xispos = (scrollxspeed >= 0), yispos = (scrollyspeed >= 0);
+	INT32 x, basey = 0;
 	INT32 dupz = (vid.dupx < vid.dupy ? vid.dupx : vid.dupy);
-	INT16 patwidth, patheight;
-	INT32 pw, ph; // scaled by dupz
 	patch_t *pat;
-	INT32 i, j;
-	fixed_t fracmenuanimtimer, xscrolltimer, yscrolltimer;
 
 	if (rendermode == render_none)
 		return;
@@ -2303,43 +2299,34 @@ void F_SkyScroll(INT32 scrollxspeed, INT32 scrollyspeed, const char *patchname)
 		return;
 	}
 
-	if (!scrollxspeed && !scrollyspeed)
+	pat = W_CachePatchName(patchname, PU_PATCH_LOWPRIORITY);
+
+	if (!curbgxspeed && !curbgyspeed)
 	{
-		V_DrawPatchFill(W_CachePatchName(patchname, PU_PATCH_LOWPRIORITY));
+		V_DrawPatchFill(pat);
+		W_UnlockCachedPatch(pat);
 		return;
 	}
 
-	pat = W_CachePatchName(patchname, PU_PATCH_LOWPRIORITY);
-
-	patwidth = pat->width;
-	patheight = pat->height;
-	pw = patwidth * dupz;
-	ph = patheight * dupz;
-
-	tilex = max(FixedCeil(FixedDiv(vid.width, pw)) >> FRACBITS, 1)+2; // one tile on both sides of center
-	tiley = max(FixedCeil(FixedDiv(vid.height, ph)) >> FRACBITS, 1)+2;
+	// Modulo the background scrolling to prevent jumps from integer overflows
+	// We already load the background patch here, so we can modulo it here
+	// to avoid also having to load the patch in F_MenuPresTicker
+	curbgx %= pat->width  * 16;
+	curbgy %= pat->height * 16;
 
-	fracmenuanimtimer = (menuanimtimer * FRACUNIT) - (FRACUNIT - rendertimefrac);
-	xscrolltimer = ((fracmenuanimtimer*scrollxspeed)/16 + patwidth*xneg*FRACUNIT) % (patwidth * FRACUNIT);
-	yscrolltimer = ((fracmenuanimtimer*scrollyspeed)/16 + patheight*yneg*FRACUNIT) % (patheight * FRACUNIT);
+	// Ooh, fancy frame interpolation
+	x     = ((curbgx*dupz) + FixedInt((rendertimefrac-FRACUNIT) * curbgxspeed*dupz)) / 16;
+	basey = ((curbgy*dupz) + FixedInt((rendertimefrac-FRACUNIT) * curbgyspeed*dupz)) / 16;
 
-	// coordinate offsets
-	xscrolled = FixedInt(xscrolltimer * dupz);
-	yscrolled = FixedInt(yscrolltimer * dupz);
+	if (x     > 0) // Make sure that we don't leave the left or top sides empty
+		x     -= pat->width  * dupz;
+	if (basey > 0)
+		basey -= pat->height * dupz;
 
-	for (x = (xispos) ? -pw*(tilex-1)+pw : 0, i = 0;
-		i < tilex;
-		x += pw, i++)
+	for (; x < vid.width; x += pat->width * dupz)
 	{
-		for (y = (yispos) ? -ph*(tiley-1)+ph : 0, j = 0;
-			j < tiley;
-			y += ph, j++)
-		{
-			V_DrawScaledPatch(
-				(xispos) ? xscrolled - x : x + xscrolled,
-				(yispos) ? yscrolled - y : y + yscrolled,
-				V_NOSCALESTART, pat);
-		}
+		for (INT32 y = basey; y < vid.height; y += pat->height * dupz)
+			V_DrawScaledPatch(x, y, V_NOSCALESTART, pat);
 	}
 
 	W_UnlockCachedPatch(pat);
@@ -2662,7 +2649,7 @@ void F_TitleScreenDrawer(void)
 	if (curbgcolor >= 0)
 		V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, curbgcolor);
 	else if (!curbghide || !titlemapinaction || gamestate == GS_WAITINGPLAYERS)
-		F_SkyScroll(curbgxspeed, curbgyspeed, curbgname);
+		F_SkyScroll(curbgname);
 
 	// Don't draw outside of the title screen, or if the patch isn't there.
 	if (gamestate != GS_TITLESCREEN && gamestate != GS_WAITINGPLAYERS)
@@ -3417,10 +3404,10 @@ luahook:
 
 // separate animation timer for backgrounds, since we also count
 // during GS_TIMEATTACK
-void F_MenuPresTicker(boolean run)
+void F_MenuPresTicker(void)
 {
-	if (run)
-		menuanimtimer++;
+	curbgx += curbgxspeed;
+	curbgy += curbgyspeed;
 }
 
 // (no longer) De-Demo'd Title Screen
@@ -3526,6 +3513,7 @@ void F_TitleScreenTicker(boolean run)
 		}
 
 		titledemo = true;
+		demofileoverride = DFILE_OVERRIDE_NONE;
 		G_DoPlayDemo(dname);
 	}
 }
@@ -4686,3 +4674,36 @@ void F_TextPromptTicker(void)
 			animtimer--;
 	}
 }
+
+// ================
+//  WAITINGPLAYERS
+// ================
+
+void F_StartWaitingPlayers(void)
+{
+	wipegamestate = GS_TITLESCREEN; // technically wiping from title screen
+	finalecount = 0;
+}
+
+void F_WaitingPlayersTicker(void)
+{
+	if (paused)
+		return;
+
+	finalecount++;
+
+	// dumb hack, only start the music on the 1st tick so if you instantly go into the map you aren't hearing a tic of music
+	if (finalecount == 2)
+		S_ChangeMusicInternal("_CHSEL", true);
+}
+
+void F_WaitingPlayersDrawer(void)
+{
+	const char *waittext1 = "You will join";
+	const char *waittext2 = "next level...";
+
+	V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+
+	V_DrawCreditString((160 - (V_CreditStringWidth(waittext1)>>1))<<FRACBITS, 48<<FRACBITS, 0, waittext1);
+	V_DrawCreditString((160 - (V_CreditStringWidth(waittext2)>>1))<<FRACBITS, 64<<FRACBITS, 0, waittext2);
+}
diff --git a/src/f_finale.h b/src/f_finale.h
index 6ea1b5537c46f8ab58ad9fb9c74d2d95c241d169..cb71775d05fc109aa4c3468c19cfe381eff72635 100644
--- a/src/f_finale.h
+++ b/src/f_finale.h
@@ -40,7 +40,7 @@ void F_TextPromptTicker(void);
 void F_GameEndDrawer(void);
 void F_IntroDrawer(void);
 void F_TitleScreenDrawer(void);
-void F_SkyScroll(INT32 scrollxspeed, INT32 scrollyspeed, const char *patchname);
+void F_SkyScroll(const char *patchname);
 
 void F_GameEvaluationDrawer(void);
 void F_StartGameEvaluation(void);
@@ -74,6 +74,10 @@ void F_StartContinue(void);
 void F_ContinueTicker(void);
 void F_ContinueDrawer(void);
 
+void F_StartWaitingPlayers(void);
+void F_WaitingPlayersTicker(void);
+void F_WaitingPlayersDrawer(void);
+
 extern INT32 finalecount;
 extern INT32 titlescrollxspeed;
 extern INT32 titlescrollyspeed;
@@ -131,7 +135,7 @@ extern UINT16 curtttics;
 #define TITLEBACKGROUNDACTIVE (curfadevalue >= 0 || curbgname[0])
 
 void F_InitMenuPresValues(void);
-void F_MenuPresTicker(boolean run);
+void F_MenuPresTicker(void);
 
 //
 // WIPE
diff --git a/src/f_wipe.c b/src/f_wipe.c
index 6014fb7f9471d851532a3df4a94b01c1b04ef6e8..4bcfb029b2ed1815e895ff126127bb628c6709fb 100644
--- a/src/f_wipe.c
+++ b/src/f_wipe.c
@@ -614,6 +614,8 @@ void F_RunWipe(UINT8 wipetype, boolean drawMenu)
 
 		if (moviemode)
 			M_SaveFrame();
+
+		NetKeepAlive(); // Update the network so we don't cause timeouts
 	}
 
 	WipeInAction = false;
diff --git a/src/filesrch.c b/src/filesrch.c
index 9ee64f5ba658752e40a57c9ff0c0d2302ed85ecf..313f286e1f29dad11f8b6e5ec29d9fa39a4613ae 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
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 0403da16da95c5d4e41a614fa2640797ece217f2..cb168dfd97ceed20b9eedd193f5406dd2fc7c361 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,6 +39,7 @@
 #include "v_video.h"
 #include "lua_hook.h"
 #include "md5.h" // demo checksums
+#include "netcode/d_netfil.h" // G_CheckDemoExtraFiles
 
 boolean timingdemo; // if true, exit with report on completion
 boolean nodrawers; // for comparative timing purposes
@@ -49,6 +50,7 @@ static char demoname[64];
 boolean demorecording;
 boolean demoplayback;
 boolean titledemo; // Title Screen demo can be cancelled by any key
+demo_file_override_e demofileoverride;
 static UINT8 *demobuffer = NULL;
 static UINT8 *demo_p, *demotime_p;
 static UINT8 *demoend;
@@ -56,6 +58,7 @@ static UINT8 demoflags;
 static UINT16 demoversion;
 boolean singledemo; // quit after playing a demo from cmdline
 boolean demo_start; // don't start playing demo right away
+boolean demo_forwardmove_rng; // old demo backwards compatibility
 boolean demosynced = true; // console warning message
 
 boolean metalrecording; // recording as metal sonic
@@ -95,7 +98,7 @@ demoghost *ghosts = NULL;
 // DEMO RECORDING
 //
 
-#define DEMOVERSION 0x000f
+#define DEMOVERSION 0x0010
 #define DEMOHEADER  "\xF0" "SRB2Replay" "\x0F"
 
 #define DF_GHOST        0x01 // This demo contains ghost data too!
@@ -1413,6 +1416,10 @@ void G_BeginRecording(void)
 	char name[MAXCOLORNAME+1];
 	player_t *player = &players[consoleplayer];
 
+	char *filename;
+	UINT16 totalfiles;
+	UINT8 *m;
+
 	if (demo_p)
 		return;
 	memset(name,0,sizeof(name));
@@ -1435,23 +1442,43 @@ void G_BeginRecording(void)
 	M_Memcpy(demo_p, mapmd5, 16); demo_p += 16;
 
 	WRITEUINT8(demo_p,demoflags);
+
+	// file list
+	m = demo_p;/* file count */
+	demo_p += 2;
+
+	totalfiles = 0;
+	for (i = mainwads; ++i < numwadfiles; )
+	{
+		if (wadfiles[i]->important)
+		{
+			nameonly(( filename = va("%s", wadfiles[i]->filename) ));
+			WRITESTRINGL(demo_p, filename, MAX_WADPATH);
+			WRITEMEM(demo_p, wadfiles[i]->md5sum, 16);
+
+			totalfiles++;
+		}
+	}
+
+	WRITEUINT16(m, totalfiles);
+
 	switch ((demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
 	{
-	case ATTACKING_NONE: // 0
-		break;
-	case ATTACKING_RECORD: // 1
-		demotime_p = demo_p;
-		WRITEUINT32(demo_p,UINT32_MAX); // time
-		WRITEUINT32(demo_p,0); // score
-		WRITEUINT16(demo_p,0); // rings
-		break;
-	case ATTACKING_NIGHTS: // 2
-		demotime_p = demo_p;
-		WRITEUINT32(demo_p,UINT32_MAX); // time
-		WRITEUINT32(demo_p,0); // score
-		break;
-	default: // 3
-		break;
+		case ATTACKING_NONE: // 0
+			break;
+		case ATTACKING_RECORD: // 1
+			demotime_p = demo_p;
+			WRITEUINT32(demo_p,UINT32_MAX); // time
+			WRITEUINT32(demo_p,0); // score
+			WRITEUINT16(demo_p,0); // rings
+			break;
+		case ATTACKING_NIGHTS: // 2
+			demotime_p = demo_p;
+			WRITEUINT32(demo_p,UINT32_MAX); // time
+			WRITEUINT32(demo_p,0); // score
+			break;
+		default: // 3
+			break;
 	}
 
 	WRITEUINT32(demo_p,P_GetInitSeed());
@@ -1473,8 +1500,12 @@ void G_BeginRecording(void)
 	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);
@@ -1483,18 +1514,18 @@ void G_BeginRecording(void)
 	// Stats
 	WRITEUINT8(demo_p,player->charability);
 	WRITEUINT8(demo_p,player->charability2);
-	WRITEUINT8(demo_p,player->actionspd>>FRACBITS);
-	WRITEUINT8(demo_p,player->mindash>>FRACBITS);
-	WRITEUINT8(demo_p,player->maxdash>>FRACBITS);
-	WRITEUINT8(demo_p,player->normalspeed>>FRACBITS);
-	WRITEUINT8(demo_p,player->runspeed>>FRACBITS);
+	WRITEFIXED(demo_p,player->actionspd);
+	WRITEFIXED(demo_p,player->mindash);
+	WRITEFIXED(demo_p,player->maxdash);
+	WRITEFIXED(demo_p,player->normalspeed);
+	WRITEFIXED(demo_p,player->runspeed);
 	WRITEUINT8(demo_p,player->thrustfactor);
 	WRITEUINT8(demo_p,player->accelstart);
 	WRITEUINT8(demo_p,player->acceleration);
 	WRITEFIXED(demo_p,player->height);
 	WRITEFIXED(demo_p,player->spinheight);
-	WRITEUINT8(demo_p,player->camerascale>>FRACBITS);
-	WRITEUINT8(demo_p,player->shieldscale>>FRACBITS);
+	WRITEFIXED(demo_p,player->camerascale);
+	WRITEFIXED(demo_p,player->shieldscale);
 
 	// Trying to convert it back to % causes demo desync due to precision loss.
 	// Don't do it.
@@ -1590,6 +1621,183 @@ void G_BeginMetal(void)
 	oldmetal.angle = mo->angle>>24;
 }
 
+static void G_LoadDemoExtraFiles(UINT8 **pp, UINT16 this_demo_version)
+{
+	UINT16 totalfiles;
+	char filename[MAX_WADPATH];
+	UINT8 md5sum[16];
+	filestatus_t ncs;
+	boolean toomany = false;
+	boolean alreadyloaded;
+	UINT16 i, j;
+
+	if (this_demo_version < 0x0010)
+	{
+		// demo has no file list
+		return;
+	}
+
+	totalfiles = READUINT16((*pp));
+	for (i = 0; i < totalfiles; ++i)
+	{
+		if (toomany)
+			SKIPSTRING((*pp));
+		else
+		{
+			strlcpy(filename, (char *)(*pp), sizeof filename);
+			SKIPSTRING((*pp));
+		}
+		READMEM((*pp), md5sum, 16);
+
+		if (!toomany)
+		{
+			alreadyloaded = false;
+
+			for (j = 0; j < numwadfiles; ++j)
+			{
+				if (memcmp(md5sum, wadfiles[j]->md5sum, 16) == 0)
+				{
+					alreadyloaded = true;
+					break;
+				}
+			}
+
+			if (alreadyloaded)
+				continue;
+
+			if (numwadfiles >= MAX_WADFILES)
+				toomany = true;
+			else
+				ncs = findfile(filename, md5sum, false);
+
+			if (toomany)
+			{
+				CONS_Alert(CONS_WARNING, M_GetText("Too many files loaded to add anymore for demo playback\n"));
+				if (!CON_Ready())
+					M_StartMessage(M_GetText("There are too many files loaded to add this demo's addons.\n\nDemo playback may desync.\n\nPress ESC\n"), NULL, MM_NOTHING);
+			}
+			else if (ncs != FS_FOUND)
+			{
+				if (ncs == FS_NOTFOUND)
+					CONS_Alert(CONS_NOTICE, M_GetText("You do not have a copy of %s\n"), filename);
+				else if (ncs == FS_MD5SUMBAD)
+					CONS_Alert(CONS_NOTICE, M_GetText("Checksum mismatch on %s\n"), filename);
+				else
+					CONS_Alert(CONS_NOTICE, M_GetText("Unknown error finding file %s\n"), filename);
+
+				if (!CON_Ready())
+					M_StartMessage(M_GetText("There were errors trying to add this demo's addons. Check the console for more information.\n\nDemo playback may desync.\n\nPress ESC\n"), NULL, MM_NOTHING);
+			}
+			else
+			{
+				P_AddWadFile(filename);
+			}
+		}
+	}
+}
+
+static void G_SkipDemoExtraFiles(UINT8 **pp, UINT16 this_demo_version)
+{
+	UINT16 totalfiles;
+	UINT16 i;
+
+	if (this_demo_version < 0x0010)
+	{
+		// demo has no file list
+		return;
+	}
+
+	totalfiles = READUINT16((*pp));
+	for (i = 0; i < totalfiles; ++i)
+	{
+		SKIPSTRING((*pp));// file name
+		(*pp) += 16;// md5
+	}
+}
+
+// G_CheckDemoExtraFiles: checks if our loaded WAD list matches the demo's.
+// Enabling quick prevents filesystem checks to see if needed files are available to load.
+static UINT8 G_CheckDemoExtraFiles(UINT8 **pp, boolean quick, UINT16 this_demo_version)
+{
+	UINT16 totalfiles, filesloaded, nmusfilecount;
+	char filename[MAX_WADPATH];
+	UINT8 md5sum[16];
+	boolean toomany = false;
+	boolean alreadyloaded;
+	UINT16 i, j;
+	UINT8 error = DFILE_ERROR_NONE;
+
+	if (this_demo_version < 0x0010)
+	{
+		// demo has no file list
+		return DFILE_ERROR_NONE;
+	}
+
+	totalfiles = READUINT16((*pp));
+	filesloaded = 0;
+	for (i = 0; i < totalfiles; ++i)
+	{
+		if (toomany)
+			SKIPSTRING((*pp));
+		else
+		{
+			strlcpy(filename, (char *)(*pp), sizeof filename);
+			SKIPSTRING((*pp));
+		}
+		READMEM((*pp), md5sum, 16);
+
+		if (!toomany)
+		{
+			alreadyloaded = false;
+			nmusfilecount = 0;
+
+			for (j = 0; j < numwadfiles; ++j)
+			{
+				if (wadfiles[j]->important && j > mainwads)
+					nmusfilecount++;
+				else
+					continue;
+
+				if (memcmp(md5sum, wadfiles[j]->md5sum, 16) == 0)
+				{
+					alreadyloaded = true;
+
+					if (i != nmusfilecount-1 && error < DFILE_ERROR_OUTOFORDER)
+						error |= DFILE_ERROR_OUTOFORDER;
+
+					break;
+				}
+			}
+
+			if (alreadyloaded)
+			{
+				filesloaded++;
+				continue;
+			}
+
+			if (numwadfiles >= MAX_WADFILES)
+				error = DFILE_ERROR_CANNOTLOAD;
+			else if (!quick && findfile(filename, md5sum, false) != FS_FOUND)
+				error = DFILE_ERROR_CANNOTLOAD;
+			else if (error < DFILE_ERROR_INCOMPLETEOUTOFORDER)
+				error |= DFILE_ERROR_NOTLOADED;
+		} else
+			error = DFILE_ERROR_CANNOTLOAD;
+	}
+
+	// Get final file count
+	nmusfilecount = 0;
+
+	for (j = 0; j < numwadfiles; ++j)
+		if (wadfiles[j]->important && j > mainwads)
+			nmusfilecount++;
+
+	if (!error && filesloaded < nmusfilecount)
+		error = DFILE_ERROR_EXTRAFILES;
+
+	return error;
+}
+
 void G_SetDemoTime(UINT32 ptime, UINT32 pscore, UINT16 prings)
 {
 	if (!demorecording || !demotime_p)
@@ -1618,10 +1826,9 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
 	UINT8 *buffer,*p;
 	UINT8 flags;
 	UINT32 oldtime, newtime, oldscore, newscore;
-	UINT16 oldrings, newrings, oldversion;
+	UINT16 oldrings, newrings, oldversion, newversion;
 	size_t bufsize ATTRUNUSED;
 	UINT8 c;
-	UINT16 s ATTRUNUSED;
 	UINT8 aflags = 0;
 
 	// load the new file
@@ -1637,15 +1844,15 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
 	I_Assert(c == VERSION);
 	c = READUINT8(p); // SUBVERSION
 	I_Assert(c == SUBVERSION);
-	s = READUINT16(p);
-	I_Assert(s >= 0x000c);
+	newversion = READUINT16(p);
+	I_Assert(newversion == DEMOVERSION);
 	p += 16; // demo checksum
 	I_Assert(!memcmp(p, "PLAY", 4));
 	p += 4; // PLAY
 	p += 2; // gamemap
 	p += 16; // map md5
 	flags = READUINT8(p); // demoflags
-
+	G_SkipDemoExtraFiles(&p, newversion);
 	aflags = flags & (DF_RECORDATTACK|DF_NIGHTSATTACK);
 	I_Assert(aflags);
 	if (flags & DF_RECORDATTACK)
@@ -1684,15 +1891,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 0x000e: // The previous demoversions also supported
-	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;
@@ -1710,6 +1911,7 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
 		p += 2; // gamemap
 	p += 16; // mapmd5
 	flags = READUINT8(p);
+	G_SkipDemoExtraFiles(&p, oldversion);
 	if (!(flags & aflags))
 	{
 		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' not from same game mode. It will be overwritten.\n"), oldname);
@@ -1764,14 +1966,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';
@@ -1829,22 +2028,14 @@ void G_DoPlayDemo(char *defdemoname)
 	version = READUINT8(demo_p);
 	subversion = READUINT8(demo_p);
 	demoversion = READUINT16(demo_p);
-	switch(demoversion)
-	{
-	case 0x000d:
-	case 0x000e:
-	case DEMOVERSION: // latest always supported
-		cnamelen = MAXCOLORNAME;
-		break;
+	demo_forwardmove_rng = (demoversion < 0x0010);
 #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);
@@ -1871,6 +2062,69 @@ void G_DoPlayDemo(char *defdemoname)
 	demo_p += 16; // mapmd5
 
 	demoflags = READUINT8(demo_p);
+
+	if (titledemo)
+	{
+		// Titledemos should always play and ought to always be compatible with whatever wadlist is running.
+		G_SkipDemoExtraFiles(&demo_p, demoversion);
+	}
+	else if (demofileoverride == DFILE_OVERRIDE_LOAD)
+	{
+		G_LoadDemoExtraFiles(&demo_p, demoversion);
+	}
+	else if (demofileoverride == DFILE_OVERRIDE_SKIP)
+	{
+		G_SkipDemoExtraFiles(&demo_p, demoversion);
+	}
+	else
+	{
+		UINT8 error = G_CheckDemoExtraFiles(&demo_p, false, demoversion);
+
+		if (error)
+		{
+			switch (error)
+			{
+				case DFILE_ERROR_NOTLOADED:
+					snprintf(msg, 1024,
+						M_GetText("Required files for this demo are not loaded.\n\nUse\n\"playdemo %s -addfiles\"\nto load them and play the demo.\n"),
+					pdemoname);
+					break;
+
+				case DFILE_ERROR_OUTOFORDER:
+					snprintf(msg, 1024,
+						M_GetText("Required files for this demo are loaded out of order.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"),
+					pdemoname);
+					break;
+
+				case DFILE_ERROR_INCOMPLETEOUTOFORDER:
+					snprintf(msg, 1024,
+						M_GetText("Required files for this demo are not loaded, and some are out of order.\n\nUse\n\"playdemo %s -addfiles\"\nto load needed files and play the demo.\n"),
+					pdemoname);
+					break;
+
+				case DFILE_ERROR_CANNOTLOAD:
+					snprintf(msg, 1024,
+						M_GetText("Required files for this demo cannot be loaded.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"),
+					pdemoname);
+					break;
+
+				case DFILE_ERROR_EXTRAFILES:
+					snprintf(msg, 1024,
+						M_GetText("You have additional files loaded beyond the demo's file list.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"),
+					pdemoname);
+					break;
+			}
+
+			CONS_Alert(CONS_ERROR, "%s", msg);
+			M_StartMessage(msg, NULL, MM_NOTHING);
+			Z_Free(pdemoname);
+			Z_Free(demobuffer);
+			demoplayback = false;
+			titledemo = false;
+			return;
+		}
+	}
+
 	modeattacking = (demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT;
 	CON_ToggleOff();
 
@@ -1908,23 +2162,23 @@ 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);
-	actionspd = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	mindash = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	maxdash = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	normalspeed = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	runspeed = (fixed_t)READUINT8(demo_p)<<FRACBITS;
+	actionspd = (demoversion < 0x0010) ? (fixed_t)READUINT8(demo_p)<<FRACBITS : READFIXED(demo_p);
+	mindash = (demoversion < 0x0010) ? (fixed_t)READUINT8(demo_p)<<FRACBITS : READFIXED(demo_p);
+	maxdash = (demoversion < 0x0010) ? (fixed_t)READUINT8(demo_p)<<FRACBITS : READFIXED(demo_p);
+	normalspeed = (demoversion < 0x0010) ? (fixed_t)READUINT8(demo_p)<<FRACBITS : READFIXED(demo_p);
+	runspeed = (demoversion < 0x0010) ? (fixed_t)READUINT8(demo_p)<<FRACBITS : READFIXED(demo_p);
 	thrustfactor = READUINT8(demo_p);
 	accelstart = READUINT8(demo_p);
 	acceleration = READUINT8(demo_p);
 	height = (demoversion < 0x000e) ? (fixed_t)READUINT8(demo_p)<<FRACBITS : READFIXED(demo_p);
 	spinheight = (demoversion < 0x000e) ? (fixed_t)READUINT8(demo_p)<<FRACBITS : READFIXED(demo_p);
-	camerascale = (fixed_t)READUINT8(demo_p)<<FRACBITS;
-	shieldscale = (fixed_t)READUINT8(demo_p)<<FRACBITS;
+	camerascale = (demoversion < 0x0010) ? (fixed_t)READUINT8(demo_p)<<FRACBITS : READFIXED(demo_p);
+	shieldscale = (demoversion < 0x0010) ? (fixed_t)READUINT8(demo_p)<<FRACBITS : READFIXED(demo_p);
 	jumpfactor = READFIXED(demo_p);
 	followitem = READUINT32(demo_p);
 
@@ -1945,7 +2199,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
@@ -1993,7 +2247,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;
@@ -2026,12 +2279,87 @@ void G_DoPlayDemo(char *defdemoname)
 	demo_start = true;
 }
 
+//
+// Check if a replay can be loaded from the menu
+//
+UINT8 G_CheckDemoForError(char *defdemoname)
+{
+	lumpnum_t l;
+	char *n,*pdemoname;
+	UINT16 our_demo_version;
+
+	if (titledemo)
+	{
+		// Don't do anything with files for these.
+		return DFILE_ERROR_NONE;
+	}
+
+	n = defdemoname+strlen(defdemoname);
+	while (*n != '/' && *n != '\\' && n != defdemoname)
+		n--;
+	if (n != defdemoname)
+		n++;
+	pdemoname = ZZ_Alloc(strlen(n)+1);
+	strcpy(pdemoname,n);
+
+	// Internal if no extension, external if one exists
+	if (FIL_CheckExtension(defdemoname))
+	{
+		//FIL_DefaultExtension(defdemoname, ".lmp");
+		if (!FIL_ReadFile(defdemoname, &demobuffer))
+		{
+			return DFILE_ERROR_NOTDEMO;
+		}
+		demo_p = demobuffer;
+	}
+	// load demo resource from WAD
+	else if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR)
+	{
+		return DFILE_ERROR_NOTDEMO;
+	}
+	else // it's an internal demo
+	{
+		demobuffer = demo_p = W_CacheLumpNum(l, PU_STATIC);
+	}
+
+	// read demo header
+	if (memcmp(demo_p, DEMOHEADER, 12))
+	{
+		return DFILE_ERROR_NOTDEMO;
+	}
+	demo_p += 12; // DEMOHEADER
+
+	demo_p++; // version
+	demo_p++; // subversion
+	our_demo_version = READUINT16(demo_p);
+#ifdef OLD22DEMOCOMPAT
+	if (our_demo_version < 0x000c || our_demo_version > DEMOVERSION)
+#else
+	if (our_demo_version < 0x000d || our_demo_version > DEMOVERSION)
+#endif
+	{
+		// too old (or new), cannot support
+		return DFILE_ERROR_NOTDEMO;
+	}
+	demo_p += 16; // demo checksum
+	if (memcmp(demo_p, "PLAY", 4))
+	{
+		return DFILE_ERROR_NOTDEMO;
+	}
+	demo_p += 4; // "PLAY"
+	demo_p += 2; // gamemap
+	demo_p += 16; // mapmd5
+
+	demo_p++; // demoflags
+
+	return G_CheckDemoExtraFiles(&demo_p, true, our_demo_version);
+}
+
 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;
@@ -2083,19 +2411,9 @@ void G_AddGhost(char *defdemoname)
 	p++; // VERSION
 	subversion = READUINT8(p); // SUBVERSION
 	ghostversion = READUINT16(p);
-	switch(ghostversion)
+	if (ghostversion < 0x000c || ghostversion > DEMOVERSION)
 	{
-	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);
@@ -2130,6 +2448,9 @@ void G_AddGhost(char *defdemoname)
 		Z_Free(buffer);
 		return;
 	}
+
+	G_SkipDemoExtraFiles(&p, ghostversion); // Don't wanna modify the file list for ghosts.
+
 	switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
 	{
 	case ATTACKING_NONE: // 0
@@ -2155,23 +2476,18 @@ 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
 	p++; // charability2
-	p++; // actionspd
-	p++; // mindash
-	p++; // maxdash
-	p++; // normalspeed
-	p++; // runspeed
+	p += (ghostversion < 0x0010) ? 5 : 5 * sizeof(fixed_t); // actionspd, mindash, maxdash, normalspeed, and runspeed
 	p++; // thrustfactor
 	p++; // accelstart
 	p++; // acceleration
 	p += (ghostversion < 0x000e) ? 2 : 2 * sizeof(fixed_t); // height and spinheight
-	p++; // camerascale
-	p++; // shieldscale
+	p += (ghostversion < 0x0010) ? 2 : 2 * sizeof(fixed_t); // camerascale and shieldscale
 	p += 4; // jumpfactor
 	p += 4; // followitem
 
@@ -2344,15 +2660,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 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_demo.h b/src/g_demo.h
index f25315a58c9b6040f38baca3d7f50abc9050a69e..379c57428a6db9daeef1b5d6ab6368436224c828 100644
--- a/src/g_demo.h
+++ b/src/g_demo.h
@@ -26,9 +26,19 @@
 extern boolean demoplayback, titledemo, demorecording, timingdemo;
 extern tic_t demostarttime;
 
+typedef enum
+{
+	DFILE_OVERRIDE_NONE = 0, // Show errors normally
+	DFILE_OVERRIDE_LOAD, // Forcefully load demo, add files beforehand
+	DFILE_OVERRIDE_SKIP, // Forcefully load demo, skip file list
+} demo_file_override_e;
+
+extern demo_file_override_e demofileoverride;
+
 // Quit after playing a demo from cmdline.
 extern boolean singledemo;
 extern boolean demo_start;
+extern boolean demo_forwardmove_rng;
 extern boolean demosynced;
 
 extern mobj_t *metalplayback;
@@ -53,6 +63,18 @@ typedef enum
 	GHC_RETURNSKIN // ditto
 } ghostcolor_t;
 
+// G_CheckDemoExtraFiles: checks if our loaded WAD list matches the demo's.
+typedef enum
+{
+	DFILE_ERROR_NONE = 0, // No file error
+	DFILE_ERROR_NOTLOADED, // Files are not loaded, but can be without a restart.
+	DFILE_ERROR_OUTOFORDER, // Files are loaded, but out of order.
+	DFILE_ERROR_INCOMPLETEOUTOFORDER, // Some files are loaded out of order, but others are not.
+	DFILE_ERROR_CANNOTLOAD, // Files are missing and cannot be loaded.
+	DFILE_ERROR_EXTRAFILES, // Extra files outside of the replay's file list are loaded.
+	DFILE_ERROR_NOTDEMO = UINT8_MAX, // This replay isn't even a replay...
+} demo_file_error_e;
+
 // Record/playback tics
 void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum);
 void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum);
@@ -83,5 +105,6 @@ ATTRNORETURN void FUNCNORETURN G_StopMetalRecording(boolean kill);
 void G_StopDemo(void);
 boolean G_CheckDemoStatus(void);
 INT32 G_ConvertOldFrameFlags(INT32 frame);
+UINT8 G_CheckDemoForError(char *defdemoname);
 
 #endif // __G_DEMO__
diff --git a/src/g_game.c b/src/g_game.c
index 7386b2a84c79450a887c75a56175135a008fc7eb..fae311694c0caa193b13131f31891a4b46e8df82 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"
@@ -55,6 +56,8 @@ gameaction_t gameaction;
 gamestate_t gamestate = GS_NULL;
 UINT8 ultimatemode = false;
 
+INT32 pickedchar;
+
 boolean botingame;
 UINT8 botskin;
 UINT16 botcolor;
@@ -156,6 +159,7 @@ textprompt_t *textprompts[MAX_PROMPTS];
 
 INT16 nextmapoverride;
 UINT8 skipstats;
+INT16 nextgametype = -1;
 
 // Pointers to each CTF flag
 mobj_t *redflag;
@@ -185,18 +189,6 @@ INT32 tokenbits; // Used for setting token bits
 // Old Special Stage
 INT32 sstimer; // Time allotted in the special stage
 
-tic_t totalplaytime;
-boolean gamedataloaded = false;
-
-// Time attack data for levels
-// These are dynamically allocated for space reasons now
-recorddata_t *mainrecords[NUMMAPS]   = {NULL};
-nightsdata_t *nightsrecords[NUMMAPS] = {NULL};
-UINT8 mapvisited[NUMMAPS];
-
-// Temporary holding place for nights data for the current map
-nightsdata_t ntemprecords;
-
 UINT32 bluescore, redscore; // CTF and Team Match team scores
 
 // ring count... for PERFECT!
@@ -227,6 +219,7 @@ UINT8 ammoremovaltics = 2*TICRATE;
 UINT8 use1upSound = 0;
 UINT8 maxXtraLife = 2; // Max extra lives from rings
 UINT8 useContinues = 0; // Set to 1 to enable continues outside of no-save scenarioes
+UINT8 shareEmblems = 0; // Set to 1 to share all picked up emblems in multiplayer
 
 UINT8 introtoplay;
 UINT8 creditscutscene;
@@ -252,11 +245,6 @@ INT32 cheats; //for multiplayer cheat commands
 
 tic_t hidetime;
 
-// Grading
-UINT32 timesBeaten;
-UINT32 timesBeatenWithEmeralds;
-UINT32 timesBeatenUltimate;
-
 typedef struct joystickvector2_s
 {
 	INT32 xaxis;
@@ -331,6 +319,8 @@ consvar_t cv_consolechat = CVAR_INIT ("chatmode", "Window", CV_SAVE, consolechat
 // Pause game upon window losing focus
 consvar_t cv_pauseifunfocused = CVAR_INIT ("pauseifunfocused", "Yes", CV_SAVE, CV_YesNo, NULL);
 
+consvar_t cv_instantretry = CVAR_INIT ("instantretry", "No", CV_SAVE, CV_YesNo, NULL);
+
 consvar_t cv_crosshair = CVAR_INIT ("crosshair", "Cross", CV_SAVE, crosshair_cons_t, NULL);
 consvar_t cv_crosshair2 = CVAR_INIT ("crosshair2", "Cross", CV_SAVE, crosshair_cons_t, NULL);
 consvar_t cv_invertmouse = CVAR_INIT ("invertmouse", "Off", CV_SAVE, CV_OnOff, NULL);
@@ -452,295 +442,323 @@ INT16 rw_maximums[NUM_WEAPONS] =
 };
 
 // Allocation for time and nights data
-void G_AllocMainRecordData(INT16 i)
+void G_AllocMainRecordData(INT16 i, gamedata_t *data)
 {
-	if (!mainrecords[i])
-		mainrecords[i] = Z_Malloc(sizeof(recorddata_t), PU_STATIC, NULL);
-	memset(mainrecords[i], 0, sizeof(recorddata_t));
+	if (!data->mainrecords[i])
+		data->mainrecords[i] = Z_Malloc(sizeof(recorddata_t), PU_STATIC, NULL);
+	memset(data->mainrecords[i], 0, sizeof(recorddata_t));
 }
 
-void G_AllocNightsRecordData(INT16 i)
+void G_AllocNightsRecordData(INT16 i, gamedata_t *data)
 {
-	if (!nightsrecords[i])
-		nightsrecords[i] = Z_Malloc(sizeof(nightsdata_t), PU_STATIC, NULL);
-	memset(nightsrecords[i], 0, sizeof(nightsdata_t));
+	if (!data->nightsrecords[i])
+		data->nightsrecords[i] = Z_Malloc(sizeof(nightsdata_t), PU_STATIC, NULL);
+	memset(data->nightsrecords[i], 0, sizeof(nightsdata_t));
 }
 
 // MAKE SURE YOU SAVE DATA BEFORE CALLING THIS
-void G_ClearRecords(void)
+void G_ClearRecords(gamedata_t *data)
 {
 	INT16 i;
 	for (i = 0; i < NUMMAPS; ++i)
 	{
-		if (mainrecords[i])
+		if (data->mainrecords[i])
 		{
-			Z_Free(mainrecords[i]);
-			mainrecords[i] = NULL;
+			Z_Free(data->mainrecords[i]);
+			data->mainrecords[i] = NULL;
 		}
-		if (nightsrecords[i])
+		if (data->nightsrecords[i])
 		{
-			Z_Free(nightsrecords[i]);
-			nightsrecords[i] = NULL;
+			Z_Free(data->nightsrecords[i]);
+			data->nightsrecords[i] = NULL;
 		}
 	}
 }
 
 // For easy retrieval of records
-UINT32 G_GetBestScore(INT16 map)
+UINT32 G_GetBestScore(INT16 map, gamedata_t *data)
 {
-	if (!mainrecords[map-1])
+	if (!data->mainrecords[map-1])
 		return 0;
 
-	return mainrecords[map-1]->score;
+	return data->mainrecords[map-1]->score;
 }
 
-tic_t G_GetBestTime(INT16 map)
+tic_t G_GetBestTime(INT16 map, gamedata_t *data)
 {
-	if (!mainrecords[map-1] || mainrecords[map-1]->time <= 0)
+	if (!data->mainrecords[map-1] || data->mainrecords[map-1]->time <= 0)
 		return (tic_t)UINT32_MAX;
 
-	return mainrecords[map-1]->time;
+	return data->mainrecords[map-1]->time;
 }
 
-UINT16 G_GetBestRings(INT16 map)
+UINT16 G_GetBestRings(INT16 map, gamedata_t *data)
 {
-	if (!mainrecords[map-1])
+	if (!data->mainrecords[map-1])
 		return 0;
 
-	return mainrecords[map-1]->rings;
+	return data->mainrecords[map-1]->rings;
 }
 
-UINT32 G_GetBestNightsScore(INT16 map, UINT8 mare)
+UINT32 G_GetBestNightsScore(INT16 map, UINT8 mare, gamedata_t *data)
 {
-	if (!nightsrecords[map-1])
+	if (!data->nightsrecords[map-1])
 		return 0;
 
-	return nightsrecords[map-1]->score[mare];
+	return data->nightsrecords[map-1]->score[mare];
 }
 
-tic_t G_GetBestNightsTime(INT16 map, UINT8 mare)
+tic_t G_GetBestNightsTime(INT16 map, UINT8 mare, gamedata_t *data)
 {
-	if (!nightsrecords[map-1] || nightsrecords[map-1]->time[mare] <= 0)
+	if (!data->nightsrecords[map-1] || data->nightsrecords[map-1]->time[mare] <= 0)
 		return (tic_t)UINT32_MAX;
 
-	return nightsrecords[map-1]->time[mare];
+	return data->nightsrecords[map-1]->time[mare];
 }
 
-UINT8 G_GetBestNightsGrade(INT16 map, UINT8 mare)
+UINT8 G_GetBestNightsGrade(INT16 map, UINT8 mare, gamedata_t *data)
 {
-	if (!nightsrecords[map-1])
+	if (!data->nightsrecords[map-1])
 		return 0;
 
-	return nightsrecords[map-1]->grade[mare];
+	return data->nightsrecords[map-1]->grade[mare];
 }
 
 // For easy adding of NiGHTS records
-void G_AddTempNightsRecords(UINT32 pscore, tic_t ptime, UINT8 mare)
+void G_AddTempNightsRecords(player_t *player, UINT32 pscore, tic_t ptime, UINT8 mare)
 {
-	ntemprecords.score[mare] = pscore;
-	ntemprecords.grade[mare] = P_GetGrade(pscore, gamemap, mare - 1);
-	ntemprecords.time[mare] = ptime;
+	const UINT8 playerID = player - players;
+
+	I_Assert(player != NULL);
+
+	ntemprecords[playerID].score[mare] = pscore;
+	ntemprecords[playerID].grade[mare] = P_GetGrade(pscore, gamemap, mare - 1);
+	ntemprecords[playerID].time[mare] = ptime;
 
 	// Update nummares
 	// Note that mare "0" is overall, mare "1" is the first real mare
-	if (ntemprecords.nummares < mare)
-		ntemprecords.nummares = mare;
+	if (ntemprecords[playerID].nummares < mare)
+		ntemprecords[playerID].nummares = mare;
 }
 
 //
-// G_UpdateRecordReplays
+// G_SetMainRecords
 //
 // Update replay files/data, etc. for Record Attack
 // See G_SetNightsRecords for NiGHTS Attack.
 //
-static void G_UpdateRecordReplays(void)
+static void G_SetMainRecords(gamedata_t *data, player_t *player)
 {
-	const size_t glen = strlen(srb2home)+1+strlen("replay")+1+strlen(timeattackfolder)+1+strlen("MAPXX")+1;
-	char *gpath;
-	char lastdemo[256], bestdemo[256];
 	UINT8 earnedEmblems;
 
+	I_Assert(player != NULL);
+
 	// Record new best time
-	if (!mainrecords[gamemap-1])
-		G_AllocMainRecordData(gamemap-1);
+	if (!data->mainrecords[gamemap-1])
+		G_AllocMainRecordData(gamemap-1, data);
 
-	if (players[consoleplayer].score > mainrecords[gamemap-1]->score)
-		mainrecords[gamemap-1]->score = players[consoleplayer].score;
+	if (player->recordscore > data->mainrecords[gamemap-1]->score)
+		data->mainrecords[gamemap-1]->score = player->recordscore;
 
-	if ((mainrecords[gamemap-1]->time == 0) || (players[consoleplayer].realtime < mainrecords[gamemap-1]->time))
-		mainrecords[gamemap-1]->time = players[consoleplayer].realtime;
+	if ((data->mainrecords[gamemap-1]->time == 0) || (player->realtime < data->mainrecords[gamemap-1]->time))
+		data->mainrecords[gamemap-1]->time = player->realtime;
 
-	if ((UINT16)(players[consoleplayer].rings) > mainrecords[gamemap-1]->rings)
-		mainrecords[gamemap-1]->rings = (UINT16)(players[consoleplayer].rings);
+	if ((UINT16)(player->rings) > data->mainrecords[gamemap-1]->rings)
+		data->mainrecords[gamemap-1]->rings = (UINT16)(player->rings);
 
-	// Save demo!
-	bestdemo[255] = '\0';
-	lastdemo[255] = '\0';
-	G_SetDemoTime(players[consoleplayer].realtime, players[consoleplayer].score, (UINT16)(players[consoleplayer].rings));
-	G_CheckDemoStatus();
+	if (modeattacking)
+	{
+		const size_t glen = strlen(srb2home)+1+strlen("replay")+1+strlen(timeattackfolder)+1+strlen("MAPXX")+1;
+		char *gpath;
+		char lastdemo[256], bestdemo[256];
 
-	I_mkdir(va("%s"PATHSEP"replay", srb2home), 0755);
-	I_mkdir(va("%s"PATHSEP"replay"PATHSEP"%s", srb2home, timeattackfolder), 0755);
+		// Save demo!
+		bestdemo[255] = '\0';
+		lastdemo[255] = '\0';
+		G_SetDemoTime(player->realtime, player->recordscore, (UINT16)(player->rings));
+		G_CheckDemoStatus();
 
-	if ((gpath = malloc(glen)) == NULL)
-		I_Error("Out of memory for replay filepath\n");
+		I_mkdir(va("%s"PATHSEP"replay", srb2home), 0755);
+		I_mkdir(va("%s"PATHSEP"replay"PATHSEP"%s", srb2home, timeattackfolder), 0755);
 
-	sprintf(gpath,"%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s", srb2home, timeattackfolder, G_BuildMapName(gamemap));
-	snprintf(lastdemo, 255, "%s-%s-last.lmp", gpath, skins[cv_chooseskin.value-1].name);
+		if ((gpath = malloc(glen)) == NULL)
+			I_Error("Out of memory for replay filepath\n");
 
-	if (FIL_FileExists(lastdemo))
-	{
-		UINT8 *buf;
-		size_t len = FIL_ReadFile(lastdemo, &buf);
+		sprintf(gpath,"%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s", srb2home, timeattackfolder, G_BuildMapName(gamemap));
+		snprintf(lastdemo, 255, "%s-%s-last.lmp", gpath, skins[cv_chooseskin.value-1].name);
 
-		snprintf(bestdemo, 255, "%s-%s-time-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
-		if (!FIL_FileExists(bestdemo) || G_CmpDemoTime(bestdemo, lastdemo) & 1)
-		{ // Better time, save this demo.
-			if (FIL_FileExists(bestdemo))
-				remove(bestdemo);
-			FIL_WriteFile(bestdemo, buf, len);
-			CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW RECORD TIME!"), M_GetText("Saved replay as"), bestdemo);
-		}
+		if (FIL_FileExists(lastdemo))
+		{
+			UINT8 *buf;
+			size_t len = FIL_ReadFile(lastdemo, &buf);
+
+			snprintf(bestdemo, 255, "%s-%s-time-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
+			if (!FIL_FileExists(bestdemo) || G_CmpDemoTime(bestdemo, lastdemo) & 1)
+			{ // Better time, save this demo.
+				if (FIL_FileExists(bestdemo))
+					remove(bestdemo);
+				FIL_WriteFile(bestdemo, buf, len);
+				CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW RECORD TIME!"), M_GetText("Saved replay as"), bestdemo);
+			}
 
-		snprintf(bestdemo, 255, "%s-%s-score-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
-		if (!FIL_FileExists(bestdemo) || (G_CmpDemoTime(bestdemo, lastdemo) & (1<<1)))
-		{ // Better score, save this demo.
-			if (FIL_FileExists(bestdemo))
-				remove(bestdemo);
-			FIL_WriteFile(bestdemo, buf, len);
-			CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW HIGH SCORE!"), M_GetText("Saved replay as"), bestdemo);
-		}
+			snprintf(bestdemo, 255, "%s-%s-score-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
+			if (!FIL_FileExists(bestdemo) || (G_CmpDemoTime(bestdemo, lastdemo) & (1<<1)))
+			{ // Better score, save this demo.
+				if (FIL_FileExists(bestdemo))
+					remove(bestdemo);
+				FIL_WriteFile(bestdemo, buf, len);
+				CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW HIGH SCORE!"), M_GetText("Saved replay as"), bestdemo);
+			}
 
-		snprintf(bestdemo, 255, "%s-%s-rings-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
-		if (!FIL_FileExists(bestdemo) || (G_CmpDemoTime(bestdemo, lastdemo) & (1<<2)))
-		{ // Better rings, save this demo.
-			if (FIL_FileExists(bestdemo))
-				remove(bestdemo);
-			FIL_WriteFile(bestdemo, buf, len);
-			CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW MOST RINGS!"), M_GetText("Saved replay as"), bestdemo);
-		}
+			snprintf(bestdemo, 255, "%s-%s-rings-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
+			if (!FIL_FileExists(bestdemo) || (G_CmpDemoTime(bestdemo, lastdemo) & (1<<2)))
+			{ // Better rings, save this demo.
+				if (FIL_FileExists(bestdemo))
+					remove(bestdemo);
+				FIL_WriteFile(bestdemo, buf, len);
+				CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW MOST RINGS!"), M_GetText("Saved replay as"), bestdemo);
+			}
 
-		//CONS_Printf("%s '%s'\n", M_GetText("Saved replay as"), lastdemo);
+			//CONS_Printf("%s '%s'\n", M_GetText("Saved replay as"), lastdemo);
 
-		Z_Free(buf);
+			Z_Free(buf);
+		}
+		free(gpath);
 	}
-	free(gpath);
 
 	// Check emblems when level data is updated
-	if ((earnedEmblems = M_CheckLevelEmblems()))
+	if ((earnedEmblems = M_CheckLevelEmblems(data)))
+	{
 		CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for Record Attack records.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : "");
+	}
 
 	// Update timeattack menu's replay availability.
 	Nextmap_OnChange();
 }
 
-void G_SetNightsRecords(void)
+static void G_SetNightsRecords(gamedata_t *data, player_t *player)
 {
-	INT32 i;
+	nightsdata_t *const ntemprecord = &ntemprecords[player - players];
 	UINT32 totalscore = 0;
 	tic_t totaltime = 0;
-	UINT8 earnedEmblems;
+	INT32 i;
 
-	const size_t glen = strlen(srb2home)+1+strlen("replay")+1+strlen(timeattackfolder)+1+strlen("MAPXX")+1;
-	char *gpath;
-	char lastdemo[256], bestdemo[256];
+	UINT8 earnedEmblems;
 
-	if (!ntemprecords.nummares)
+	if (!ntemprecord->nummares)
+	{
 		return;
+	}
 
 	// Set overall
 	{
 		UINT8 totalrank = 0, realrank = 0;
 
-		for (i = 1; i <= ntemprecords.nummares; ++i)
+		for (i = 1; i <= ntemprecord->nummares; ++i)
 		{
-			totalscore += ntemprecords.score[i];
-			totalrank += ntemprecords.grade[i];
-			totaltime += ntemprecords.time[i];
+			totalscore += ntemprecord->score[i];
+			totalrank += ntemprecord->grade[i];
+			totaltime += ntemprecord->time[i];
 		}
 
 		// Determine overall grade
-		realrank = (UINT8)((FixedDiv((fixed_t)totalrank << FRACBITS, ntemprecords.nummares << FRACBITS) + (FRACUNIT/2)) >> FRACBITS);
+		realrank = (UINT8)((FixedDiv((fixed_t)totalrank << FRACBITS, ntemprecord->nummares << FRACBITS) + (FRACUNIT/2)) >> FRACBITS);
 
 		// You need ALL rainbow As to get a rainbow A overall
-		if (realrank == GRADE_S && (totalrank / ntemprecords.nummares) != GRADE_S)
+		if (realrank == GRADE_S && (totalrank / ntemprecord->nummares) != GRADE_S)
+		{
 			realrank = GRADE_A;
+		}
 
-		ntemprecords.score[0] = totalscore;
-		ntemprecords.grade[0] = realrank;
-		ntemprecords.time[0] = totaltime;
+		ntemprecord->score[0] = totalscore;
+		ntemprecord->grade[0] = realrank;
+		ntemprecord->time[0] = totaltime;
 	}
 
 	// Now take all temp records and put them in the actual records
 	{
 		nightsdata_t *maprecords;
 
-		if (!nightsrecords[gamemap-1])
-			G_AllocNightsRecordData(gamemap-1);
-		maprecords = nightsrecords[gamemap-1];
+		if (!data->nightsrecords[gamemap-1])
+		{
+			G_AllocNightsRecordData(gamemap-1, data);
+		}
 
-		if (maprecords->nummares != ntemprecords.nummares)
-			maprecords->nummares = ntemprecords.nummares;
+		maprecords = data->nightsrecords[gamemap-1];
 
-		for (i = 0; i < ntemprecords.nummares + 1; ++i)
+		if (maprecords->nummares != ntemprecord->nummares)
 		{
-			if (maprecords->score[i] < ntemprecords.score[i])
-				maprecords->score[i] = ntemprecords.score[i];
-			if (maprecords->grade[i] < ntemprecords.grade[i])
-				maprecords->grade[i] = ntemprecords.grade[i];
-			if (!maprecords->time[i] || maprecords->time[i] > ntemprecords.time[i])
-				maprecords->time[i] = ntemprecords.time[i];
+			maprecords->nummares = ntemprecord->nummares;
+		}
+
+		for (i = 0; i < ntemprecord->nummares + 1; ++i)
+		{
+			if (maprecords->score[i] < ntemprecord->score[i])
+				maprecords->score[i] = ntemprecord->score[i];
+			if (maprecords->grade[i] < ntemprecord->grade[i])
+				maprecords->grade[i] = ntemprecord->grade[i];
+			if (!maprecords->time[i] || maprecords->time[i] > ntemprecord->time[i])
+				maprecords->time[i] = ntemprecord->time[i];
 		}
 	}
 
-	memset(&ntemprecords, 0, sizeof(nightsdata_t));
+	memset(&ntemprecords[player - players], 0, sizeof(nightsdata_t));
 
-	// Save demo!
-	bestdemo[255] = '\0';
-	lastdemo[255] = '\0';
-	G_SetDemoTime(totaltime, totalscore, 0);
-	G_CheckDemoStatus();
+	if (modeattacking)
+	{
+		const size_t glen = strlen(srb2home)+1+strlen("replay")+1+strlen(timeattackfolder)+1+strlen("MAPXX")+1;
+		char *gpath;
+		char lastdemo[256], bestdemo[256];
 
-	I_mkdir(va("%s"PATHSEP"replay", srb2home), 0755);
-	I_mkdir(va("%s"PATHSEP"replay"PATHSEP"%s", srb2home, timeattackfolder), 0755);
+		// Save demo!
+		bestdemo[255] = '\0';
+		lastdemo[255] = '\0';
+		G_SetDemoTime(totaltime, totalscore, 0);
+		G_CheckDemoStatus();
 
-	if ((gpath = malloc(glen)) == NULL)
-		I_Error("Out of memory for replay filepath\n");
+		I_mkdir(va("%s"PATHSEP"replay", srb2home), 0755);
+		I_mkdir(va("%s"PATHSEP"replay"PATHSEP"%s", srb2home, timeattackfolder), 0755);
 
-	sprintf(gpath,"%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s", srb2home, timeattackfolder, G_BuildMapName(gamemap));
-	snprintf(lastdemo, 255, "%s-%s-last.lmp", gpath, skins[cv_chooseskin.value-1].name);
+		if ((gpath = malloc(glen)) == NULL)
+			I_Error("Out of memory for replay filepath\n");
 
-	if (FIL_FileExists(lastdemo))
-	{
-		UINT8 *buf;
-		size_t len = FIL_ReadFile(lastdemo, &buf);
+		sprintf(gpath,"%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s", srb2home, timeattackfolder, G_BuildMapName(gamemap));
+		snprintf(lastdemo, 255, "%s-%s-last.lmp", gpath, skins[cv_chooseskin.value-1].name);
 
-		snprintf(bestdemo, 255, "%s-%s-time-best.lmp", gpath, skins[cv_chooseskin.value-1].name);;
-		if (!FIL_FileExists(bestdemo) || G_CmpDemoTime(bestdemo, lastdemo) & 1)
-		{ // Better time, save this demo.
-			if (FIL_FileExists(bestdemo))
-				remove(bestdemo);
-			FIL_WriteFile(bestdemo, buf, len);
-			CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW RECORD TIME!"), M_GetText("Saved replay as"), bestdemo);
-		}
+		if (FIL_FileExists(lastdemo))
+		{
+			UINT8 *buf;
+			size_t len = FIL_ReadFile(lastdemo, &buf);
+
+			snprintf(bestdemo, 255, "%s-%s-time-best.lmp", gpath, skins[cv_chooseskin.value-1].name);;
+			if (!FIL_FileExists(bestdemo) || G_CmpDemoTime(bestdemo, lastdemo) & 1)
+			{ // Better time, save this demo.
+				if (FIL_FileExists(bestdemo))
+					remove(bestdemo);
+				FIL_WriteFile(bestdemo, buf, len);
+				CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW RECORD TIME!"), M_GetText("Saved replay as"), bestdemo);
+			}
 
-		snprintf(bestdemo, 255, "%s-%s-score-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
-		if (!FIL_FileExists(bestdemo) || (G_CmpDemoTime(bestdemo, lastdemo) & (1<<1)))
-		{ // Better score, save this demo.
-			if (FIL_FileExists(bestdemo))
-				remove(bestdemo);
-			FIL_WriteFile(bestdemo, buf, len);
-			CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW HIGH SCORE!"), M_GetText("Saved replay as"), bestdemo);
-		}
+			snprintf(bestdemo, 255, "%s-%s-score-best.lmp", gpath, skins[cv_chooseskin.value-1].name);
+			if (!FIL_FileExists(bestdemo) || (G_CmpDemoTime(bestdemo, lastdemo) & (1<<1)))
+			{ // Better score, save this demo.
+				if (FIL_FileExists(bestdemo))
+					remove(bestdemo);
+				FIL_WriteFile(bestdemo, buf, len);
+				CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW HIGH SCORE!"), M_GetText("Saved replay as"), bestdemo);
+			}
 
-		//CONS_Printf("%s '%s'\n", M_GetText("Saved replay as"), lastdemo);
+			//CONS_Printf("%s '%s'\n", M_GetText("Saved replay as"), lastdemo);
 
-		Z_Free(buf);
+			Z_Free(buf);
+		}
+		free(gpath);
 	}
-	free(gpath);
 
-	if ((earnedEmblems = M_CheckLevelEmblems()))
+	if ((earnedEmblems = M_CheckLevelEmblems(data)))
+	{
 		CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for NiGHTS records.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : "");
+	}
 
 	// If the mare count changed, this will update the score display
 	Nextmap_OnChange();
@@ -1429,6 +1447,7 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
 			// I assume this is netgame-safe because gunslinger spawns this for only the local player...... *sweats intensely*
 			newtarget = P_SpawnMobj(ticcmd_ztargetfocus[forplayer]->x, ticcmd_ztargetfocus[forplayer]->y, ticcmd_ztargetfocus[forplayer]->z, MT_LOCKON); // positioning, flip handled in P_SceneryThinker
 			P_SetTarget(&newtarget->target, ticcmd_ztargetfocus[forplayer]);
+			newtarget->drawonlyforplayer = player; // Hide it from the other player in splitscreen, and yourself when spectating
 
 			if (player->mo && P_AproxDistance(
 				player->mo->x - ticcmd_ztargetfocus[forplayer]->x,
@@ -1931,6 +1950,7 @@ void G_PreLevelTitleCard(void)
 		ST_runTitleCard();
 		ST_preLevelTitleCardDrawer();
 		I_FinishUpdate(); // page flip or blit buffer
+		NetKeepAlive(); // Prevent timeouts
 
 		if (moviemode)
 			M_SaveFrame();
@@ -2065,7 +2085,7 @@ boolean G_Responder(event_t *ev)
 	if (gameaction == ga_nothing && !singledemo &&
 		((demoplayback && !modeattacking && !titledemo) || gamestate == GS_TITLESCREEN))
 	{
-		if (ev->type == ev_keydown && ev->key != 301 && !(gamestate == GS_TITLESCREEN && finalecount < TICRATE))
+		if (ev->type == ev_keydown && ev->key != 301 && !(gamestate == GS_TITLESCREEN && finalecount < (cv_tutorialprompt.value ? TICRATE : 0)))
 		{
 			M_StartControlPanel();
 			return true;
@@ -2123,7 +2143,7 @@ boolean G_Responder(event_t *ev)
 			if (! netgame)
 				F_StartGameEvaluation();
 			else if (server || IsPlayerAdmin(consoleplayer))
-				SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+				D_SendExitLevel(false);
 			return true;
 		}
 	}
@@ -2158,9 +2178,9 @@ boolean G_Responder(event_t *ev)
 					if (menuactive || pausedelay < 0 || leveltime < 2)
 						return true;
 
-					if (pausedelay < 1+(NEWTICRATE/2))
+					if (!cv_instantretry.value && pausedelay < 1+(NEWTICRATE/2))
 						pausedelay = 1+(NEWTICRATE/2);
-					else if (++pausedelay > 1+(NEWTICRATE/2)+(NEWTICRATE/3))
+					else if (cv_instantretry.value || ++pausedelay > 1+(NEWTICRATE/2)+(NEWTICRATE/3))
 					{
 						G_SetModeAttackRetryFlag();
 						return true;
@@ -2303,7 +2323,7 @@ void G_Ticker(boolean run)
 					p->lives = startinglivesbalance[0];
 					p->continues = 1;
 
-					p->score = 0;
+					p->score = p->recordscore = 0;
 
 					// The latter two should clear by themselves, but just in case
 					p->pflags &= ~(PF_TAGIT|PF_GAMETYPEOVER|PF_FULLSTASIS);
@@ -2407,7 +2427,8 @@ void G_Ticker(boolean run)
 			break;
 
 		case GS_TIMEATTACK:
-			F_MenuPresTicker(run);
+			if (run)
+				F_MenuPresTicker();
 			break;
 
 		case GS_INTRO:
@@ -2452,13 +2473,17 @@ void G_Ticker(boolean run)
 		case GS_TITLESCREEN:
 			if (titlemapinaction)
 				P_Ticker(run);
-				// then intentionally fall through
-			/* FALLTHRU */
-		case GS_WAITINGPLAYERS:
-			F_MenuPresTicker(run);
+			if (run)
+				F_MenuPresTicker();
 			F_TitleScreenTicker(run);
 			break;
 
+		case GS_WAITINGPLAYERS:
+			if (netgame)
+				F_WaitingPlayersTicker();
+			HU_Ticker();
+			break;
+
 		case GS_DEDICATEDSERVER:
 		case GS_NULL:
 			break; // do nothing
@@ -2729,25 +2754,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;
 
@@ -3169,6 +3175,9 @@ void G_DoReborn(INT32 playernum)
 
 	if (resetlevel)
 	{
+		// Don't give completion emblems for reloading the level...
+		stagefailed = true;
+
 		// reload the level from scratch
 		if (countdowntimeup)
 		{
@@ -3176,6 +3185,7 @@ void G_DoReborn(INT32 playernum)
 			{
 				if (!playeringame[i])
 					continue;
+				players[i].recordscore = 0;
 				players[i].starpostscale = 0;
 				players[i].starpostangle = 0;
 				players[i].starposttime = 0;
@@ -3438,9 +3448,7 @@ UINT32 gametypedefaultrules[NUMGAMETYPES] =
 };
 
 //
-// G_SetGametype
-//
-// Set a new gametype, also setting gametype rules accordingly. Yay!
+// Sets a new gametype.
 //
 void G_SetGametype(INT16 gtype)
 {
@@ -3838,7 +3846,7 @@ static INT16 RandMap(UINT32 tolflags, INT16 pprevmap)
 	for (ix = 0; ix < NUMMAPS; ix++)
 		if (mapheaderinfo[ix] && (mapheaderinfo[ix]->typeoflevel & tolflags) == tolflags
 		 && ix != pprevmap // Don't pick the same map.
-		 && (dedicated || !M_MapLocked(ix+1)) // Don't pick locked maps.
+		 && (!M_MapLocked(ix+1, serverGamedata)) // Don't pick locked maps.
 		)
 			okmaps[numokmaps++] = ix;
 
@@ -3855,41 +3863,80 @@ static INT16 RandMap(UINT32 tolflags, INT16 pprevmap)
 //
 // G_UpdateVisited
 //
-static void G_UpdateVisited(void)
+static void G_UpdateVisited(gamedata_t *data, player_t *player, boolean silent)
 {
-	boolean spec = G_IsSpecialStage(gamemap);
 	// Update visitation flags?
-	if (!multiplayer && !demoplayback && (gametype == GT_COOP) // SP/RA/NiGHTS mode
+	if (!demoplayback
+		&& G_CoopGametype() // Campaign mode
 		&& !stagefailed) // Did not fail the stage
 	{
 		UINT8 earnedEmblems;
+		UINT16 totalrings = 0;
+		INT32 i;
 
 		// Update visitation flags
-		mapvisited[gamemap-1] |= MV_BEATEN;
+		data->mapvisited[gamemap-1] |= MV_BEATEN;
+
 		// eh, what the hell
 		if (ultimatemode)
-			mapvisited[gamemap-1] |= MV_ULTIMATE;
+			data->mapvisited[gamemap-1] |= MV_ULTIMATE;
+
+		for (i = 0; i < MAXPLAYERS; i++)
+		{
+			if (!playeringame[i])
+			{
+				continue;
+			}
+
+			totalrings += players[i].rings;
+		}
+
 		// may seem incorrect but IS possible in what the main game uses as mp special stages, and nummaprings will be -1 in NiGHTS
-		if (nummaprings > 0 && players[consoleplayer].rings >= nummaprings)
+		if (nummaprings > 0 && totalrings >= nummaprings)
 		{
-			mapvisited[gamemap-1] |= MV_PERFECT;
+			data->mapvisited[gamemap-1] |= MV_PERFECT;
 			if (modeattacking)
-				mapvisited[gamemap-1] |= MV_PERFECTRA;
+				data->mapvisited[gamemap-1] |= MV_PERFECTRA;
 		}
-		if (!spec)
+
+		if (!G_IsSpecialStage(gamemap))
 		{
 			// not available to special stages because they can only really be done in one order in an unmodified game, so impossible for first six and trivial for seventh
 			if (ALL7EMERALDS(emeralds))
-				mapvisited[gamemap-1] |= MV_ALLEMERALDS;
+				data->mapvisited[gamemap-1] |= MV_ALLEMERALDS;
 		}
 
-		if (modeattacking == ATTACKING_RECORD)
-			G_UpdateRecordReplays();
-		else if (modeattacking == ATTACKING_NIGHTS)
-			G_SetNightsRecords();
-
-		if ((earnedEmblems = M_CompletionEmblems()))
+		if ((earnedEmblems = M_CompletionEmblems(data)) && !silent)
+		{
 			CONS_Printf(M_GetText("\x82" "Earned %hu emblem%s for level completion.\n"), (UINT16)earnedEmblems, earnedEmblems > 1 ? "s" : "");
+		}
+
+		if (silent)
+		{
+			M_CheckLevelEmblems(data);
+		}
+		else
+		{
+			if (mapheaderinfo[gamemap-1]->menuflags & LF2_RECORDATTACK)
+				G_SetMainRecords(data, player);
+			else if (mapheaderinfo[gamemap-1]->menuflags & LF2_NIGHTSATTACK)
+				G_SetNightsRecords(data, player);
+		}
+	}
+}
+
+static void G_UpdateAllVisited(void)
+{
+	// Update server
+	G_UpdateVisited(serverGamedata, &players[serverplayer], true);
+
+	// Update client
+	G_UpdateVisited(clientGamedata, &players[consoleplayer], false);
+
+	if (splitscreen)
+	{
+		// Allow P2 to get emblems too, why not :)
+		G_UpdateVisited(clientGamedata, &players[secondarydisplayplayer], false);
 	}
 }
 
@@ -3913,6 +3960,9 @@ static boolean CanSaveLevel(INT32 mapnum)
 
 static void G_HandleSaveLevel(void)
 {
+	// Update records & emblems
+	G_UpdateAllVisited();
+
 	// do this before running the intermission or custom cutscene, mostly for the sake of marathon mode but it also massively reduces redundant file save events in f_finale.c
 	if (nextmap >= 1100-1)
 	{
@@ -3988,6 +4038,13 @@ static void G_DoCompleted(void)
 			nextmap = 1100-1; // No infinite loop for you
 	}
 
+	INT16 gametype_to_use;
+
+	if (nextgametype >= 0 && nextgametype < gametypecount)
+		gametype_to_use = nextgametype;
+	else
+		gametype_to_use = gametype;
+
 	// If nextmap is actually going to get used, make sure it points to
 	// a map of the proper gametype -- skip levels that don't support
 	// the current gametype. (Helps avoid playing boss levels in Race,
@@ -3996,8 +4053,8 @@ static void G_DoCompleted(void)
 	{
 		if (nextmap >= 0 && nextmap < NUMMAPS)
 		{
-			register INT16 cm = nextmap;
-			UINT32 tolflag = G_TOLFlag(gametype);
+			INT16 cm = nextmap;
+			UINT32 tolflag = G_TOLFlag(gametype_to_use);
 			UINT8 visitedmap[(NUMMAPS+7)/8];
 
 			memset(visitedmap, 0, sizeof (visitedmap));
@@ -4052,7 +4109,7 @@ static void G_DoCompleted(void)
 	{
 		token--;
 
-		if (!nextmapoverride)
+//		if (!nextmapoverride) // Having a token should pull the player into the special stage before going to the overridden map (Issue #933)
 			for (i = 0; i < 7; i++)
 				if (!(emeralds & (1<<i)))
 				{
@@ -4077,7 +4134,7 @@ static void G_DoCompleted(void)
 		if (cv_advancemap.value == 0) // Stay on same map.
 			nextmap = prevmap;
 		else if (cv_advancemap.value == 2) // Go to random map.
-			nextmap = RandMap(G_TOLFlag(gametype), prevmap);
+			nextmap = RandMap(G_TOLFlag(gametype_to_use), prevmap);
 	}
 
 	// We are committed to this map now.
@@ -4086,12 +4143,10 @@ static void G_DoCompleted(void)
 	if (nextmap < NUMMAPS && !mapheaderinfo[nextmap])
 		P_AllocMapHeader(nextmap);
 
-	// If the current gametype has no intermission screen set, then don't start it.
 	Y_DetermineIntermissionType();
 
 	if ((skipstats && !modeattacking) || (modeattacking && stagefailed) || (intertype == int_none))
 	{
-		G_UpdateVisited();
 		G_HandleSaveLevel();
 		G_AfterIntermission();
 	}
@@ -4100,7 +4155,6 @@ static void G_DoCompleted(void)
 		G_SetGamestate(GS_INTERMISSION);
 		Y_StartIntermission();
 		Y_LoadIntermissionData();
-		G_UpdateVisited();
 		G_HandleSaveLevel();
 	}
 }
@@ -4154,12 +4208,21 @@ static void G_DoWorldDone(void)
 {
 	if (server)
 	{
+		INT16 gametype_to_use;
+
+		if (nextgametype >= 0 && nextgametype < gametypecount)
+			gametype_to_use = nextgametype;
+		else
+			gametype_to_use = gametype;
+
 		if (gametyperules & GTR_CAMPAIGN)
 			// don't reset player between maps
-			D_MapChange(nextmap+1, gametype, ultimatemode, false, 0, false, false);
+			D_MapChange(nextmap+1, gametype_to_use, ultimatemode, false, 0, false, false);
 		else
 			// resetplayer in match/chaos/tag/CTF/race for more equality
-			D_MapChange(nextmap+1, gametype, ultimatemode, true, 0, false, false);
+			D_MapChange(nextmap+1, gametype_to_use, ultimatemode, true, 0, false, false);
+
+		nextgametype = -1;
 	}
 
 	gameaction = ga_nothing;
@@ -4202,7 +4265,7 @@ static void G_DoContinued(void)
 {
 	player_t *pl = &players[consoleplayer];
 	I_Assert(!netgame && !multiplayer);
-	I_Assert(pl->continues > 0);
+	//I_Assert(pl->continues > 0);
 
 	if (pl->continues)
 		pl->continues--;
@@ -4287,7 +4350,7 @@ void G_LoadGameSettings(void)
 
 // G_LoadGameData
 // Loads the main data file, which stores information such as emblems found, etc.
-void G_LoadGameData(void)
+void G_LoadGameData(gamedata_t *data)
 {
 	size_t length;
 	INT32 i, j;
@@ -4304,13 +4367,19 @@ void G_LoadGameData(void)
 	INT32 curmare;
 
 	// Stop saving, until we successfully load it again.
-	gamedataloaded = false;
+	data->loaded = false;
+
+	// Backwards compat stuff
+	INT32 max_emblems = MAXEMBLEMS;
+	INT32 max_extraemblems = MAXEXTRAEMBLEMS;
+	INT32 max_unlockables = MAXUNLOCKABLES;
+	INT32 max_conditionsets = MAXCONDITIONSETS;
 
 	// Clear things so previously read gamedata doesn't transfer
 	// to new gamedata
-	G_ClearRecords(); // main and nights records
-	M_ClearSecrets(); // emblems, unlocks, maps visited, etc
-	totalplaytime = 0; // total play time (separate from all)
+	G_ClearRecords(data); // main and nights records
+	M_ClearSecrets(data); // emblems, unlocks, maps visited, etc
+	data->totalplaytime = 0; // total play time (separate from all)
 
 	if (M_CheckParm("-nodata"))
 	{
@@ -4321,15 +4390,15 @@ void G_LoadGameData(void)
 	if (M_CheckParm("-resetdata"))
 	{
 		// Don't load, but do save. (essentially, reset)
-		gamedataloaded = true;
-		return; 
+		data->loaded = true;
+		return;
 	}
 
 	length = FIL_ReadFile(va(pandf, srb2home, gamedatafilename), &savebuffer);
 	if (!length)
 	{
 		// No gamedata. We can save a new one.
-		gamedataloaded = true;
+		data->loaded = true;
 		return;
 	}
 
@@ -4352,7 +4421,15 @@ void G_LoadGameData(void)
 		I_Error("Game data is from another version of SRB2.\nDelete %s(maybe in %s) and try again.", gamedatafilename, gdfolder);
 	}
 
-	totalplaytime = READUINT32(save_p);
+#ifdef COMPAT_GAMEDATA_ID // Account for lower MAXUNLOCKABLES and MAXEXTRAEMBLEMS from older versions
+	if (versionID == COMPAT_GAMEDATA_ID)
+	{
+		max_extraemblems = 16;
+		max_unlockables = 32;
+	}
+#endif
+
+	data->totalplaytime = READUINT32(save_p);
 
 #ifdef COMPAT_GAMEDATA_ID
 	if (versionID == COMPAT_GAMEDATA_ID)
@@ -4369,6 +4446,17 @@ void G_LoadGameData(void)
 		{
 			goto datacorrupt;
 		}
+
+		// make a backup of the old data
+		char currentfilename[64];
+		char backupfilename[69];
+		char bak[5];
+
+		strcpy(bak, ".bak");
+		strcpy(currentfilename, gamedatafilename);
+		STRBUFCPY(backupfilename, strcat(currentfilename, bak));
+
+		FIL_WriteFile(va(pandf, srb2home, backupfilename), savebuffer, length);
 	}
 	else
 #endif
@@ -4386,42 +4474,42 @@ void G_LoadGameData(void)
 
 	// TODO put another cipher on these things? meh, I don't care...
 	for (i = 0; i < NUMMAPS; i++)
-		if ((mapvisited[i] = READUINT8(save_p)) > MV_MAX)
+		if ((data->mapvisited[i] = READUINT8(save_p)) > MV_MAX)
 			goto datacorrupt;
 
 	// To save space, use one bit per collected/achieved/unlocked flag
-	for (i = 0; i < MAXEMBLEMS;)
+	for (i = 0; i < max_emblems;)
 	{
 		rtemp = READUINT8(save_p);
-		for (j = 0; j < 8 && j+i < MAXEMBLEMS; ++j)
-			emblemlocations[j+i].collected = ((rtemp >> j) & 1);
+		for (j = 0; j < 8 && j+i < max_emblems; ++j)
+			data->collected[j+i] = ((rtemp >> j) & 1);
 		i += j;
 	}
-	for (i = 0; i < MAXEXTRAEMBLEMS;)
+	for (i = 0; i < max_extraemblems;)
 	{
 		rtemp = READUINT8(save_p);
-		for (j = 0; j < 8 && j+i < MAXEXTRAEMBLEMS; ++j)
-			extraemblems[j+i].collected = ((rtemp >> j) & 1);
+		for (j = 0; j < 8 && j+i < max_extraemblems; ++j)
+			data->extraCollected[j+i] = ((rtemp >> j) & 1);
 		i += j;
 	}
-	for (i = 0; i < MAXUNLOCKABLES;)
+	for (i = 0; i < max_unlockables;)
 	{
 		rtemp = READUINT8(save_p);
-		for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j)
-			unlockables[j+i].unlocked = ((rtemp >> j) & 1);
+		for (j = 0; j < 8 && j+i < max_unlockables; ++j)
+			data->unlocked[j+i] = ((rtemp >> j) & 1);
 		i += j;
 	}
-	for (i = 0; i < MAXCONDITIONSETS;)
+	for (i = 0; i < max_conditionsets;)
 	{
 		rtemp = READUINT8(save_p);
-		for (j = 0; j < 8 && j+i < MAXCONDITIONSETS; ++j)
-			conditionSets[j+i].achieved = ((rtemp >> j) & 1);
+		for (j = 0; j < 8 && j+i < max_conditionsets; ++j)
+			data->achieved[j+i] = ((rtemp >> j) & 1);
 		i += j;
 	}
 
-	timesBeaten = READUINT32(save_p);
-	timesBeatenWithEmeralds = READUINT32(save_p);
-	timesBeatenUltimate = READUINT32(save_p);
+	data->timesBeaten = READUINT32(save_p);
+	data->timesBeatenWithEmeralds = READUINT32(save_p);
+	data->timesBeatenUltimate = READUINT32(save_p);
 
 	// Main records
 	for (i = 0; i < NUMMAPS; ++i)
@@ -4436,10 +4524,10 @@ void G_LoadGameData(void)
 
 		if (recscore || rectime || recrings)
 		{
-			G_AllocMainRecordData((INT16)i);
-			mainrecords[i]->score = recscore;
-			mainrecords[i]->time = rectime;
-			mainrecords[i]->rings = recrings;
+			G_AllocMainRecordData((INT16)i, data);
+			data->mainrecords[i]->score = recscore;
+			data->mainrecords[i]->time = rectime;
+			data->mainrecords[i]->rings = recrings;
 		}
 	}
 
@@ -4449,19 +4537,21 @@ void G_LoadGameData(void)
 		if ((recmares = READUINT8(save_p)) == 0)
 			continue;
 
-		G_AllocNightsRecordData((INT16)i);
+		G_AllocNightsRecordData((INT16)i, data);
 
 		for (curmare = 0; curmare < (recmares+1); ++curmare)
 		{
-			nightsrecords[i]->score[curmare] = READUINT32(save_p);
-			nightsrecords[i]->grade[curmare] = READUINT8(save_p);
-			nightsrecords[i]->time[curmare] = (tic_t)READUINT32(save_p);
+			data->nightsrecords[i]->score[curmare] = READUINT32(save_p);
+			data->nightsrecords[i]->grade[curmare] = READUINT8(save_p);
+			data->nightsrecords[i]->time[curmare] = (tic_t)READUINT32(save_p);
 
-			if (nightsrecords[i]->grade[curmare] > GRADE_S)
+			if (data->nightsrecords[i]->grade[curmare] > GRADE_S)
+			{
 				goto datacorrupt;
+			}
 		}
 
-		nightsrecords[i]->nummares = recmares;
+		data->nightsrecords[i]->nummares = recmares;
 	}
 
 	// done
@@ -4472,10 +4562,11 @@ void G_LoadGameData(void)
 	// It used to do this much earlier, but this would cause the gamedata to
 	// save over itself when it I_Errors from the corruption landing point below,
 	// which can accidentally delete players' legitimate data if the code ever has any tiny mistakes!
-	gamedataloaded = true;
+	data->loaded = true;
 
 	// Silent update unlockables in case they're out of sync with conditions
-	M_SilentUpdateUnlockablesAndEmblems();
+	M_SilentUpdateUnlockablesAndEmblems(data);
+	M_SilentUpdateSkinAvailabilites();
 
 	return;
 
@@ -4495,7 +4586,7 @@ void G_LoadGameData(void)
 
 // G_SaveGameData
 // Saves the main data file, which stores information such as emblems found, etc.
-void G_SaveGameData(void)
+void G_SaveGameData(gamedata_t *data)
 {
 	size_t length;
 	INT32 i, j;
@@ -4503,7 +4594,7 @@ void G_SaveGameData(void)
 
 	INT32 curmare;
 
-	if (!gamedataloaded)
+	if (!data->loaded)
 		return; // If never loaded (-nodata), don't save
 
 	save_p = savebuffer = (UINT8 *)malloc(GAMEDATASIZE);
@@ -4523,20 +4614,20 @@ void G_SaveGameData(void)
 	// Version test
 	WRITEUINT32(save_p, GAMEDATA_ID);
 
-	WRITEUINT32(save_p, totalplaytime);
+	WRITEUINT32(save_p, data->totalplaytime);
 
 	WRITEUINT32(save_p, quickncasehash(timeattackfolder, sizeof timeattackfolder));
 
 	// TODO put another cipher on these things? meh, I don't care...
 	for (i = 0; i < NUMMAPS; i++)
-		WRITEUINT8(save_p, (mapvisited[i] & MV_MAX));
+		WRITEUINT8(save_p, (data->mapvisited[i] & MV_MAX));
 
 	// To save space, use one bit per collected/achieved/unlocked flag
 	for (i = 0; i < MAXEMBLEMS;)
 	{
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXEMBLEMS; ++j)
-			btemp |= (emblemlocations[j+i].collected << j);
+			btemp |= (data->collected[j+i] << j);
 		WRITEUINT8(save_p, btemp);
 		i += j;
 	}
@@ -4544,7 +4635,7 @@ void G_SaveGameData(void)
 	{
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXEXTRAEMBLEMS; ++j)
-			btemp |= (extraemblems[j+i].collected << j);
+			btemp |= (data->extraCollected[j+i] << j);
 		WRITEUINT8(save_p, btemp);
 		i += j;
 	}
@@ -4552,7 +4643,7 @@ void G_SaveGameData(void)
 	{
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j)
-			btemp |= (unlockables[j+i].unlocked << j);
+			btemp |= (data->unlocked[j+i] << j);
 		WRITEUINT8(save_p, btemp);
 		i += j;
 	}
@@ -4560,23 +4651,23 @@ void G_SaveGameData(void)
 	{
 		btemp = 0;
 		for (j = 0; j < 8 && j+i < MAXCONDITIONSETS; ++j)
-			btemp |= (conditionSets[j+i].achieved << j);
+			btemp |= (data->achieved[j+i] << j);
 		WRITEUINT8(save_p, btemp);
 		i += j;
 	}
 
-	WRITEUINT32(save_p, timesBeaten);
-	WRITEUINT32(save_p, timesBeatenWithEmeralds);
-	WRITEUINT32(save_p, timesBeatenUltimate);
+	WRITEUINT32(save_p, data->timesBeaten);
+	WRITEUINT32(save_p, data->timesBeatenWithEmeralds);
+	WRITEUINT32(save_p, data->timesBeatenUltimate);
 
 	// Main records
 	for (i = 0; i < NUMMAPS; i++)
 	{
-		if (mainrecords[i])
+		if (data->mainrecords[i])
 		{
-			WRITEUINT32(save_p, mainrecords[i]->score);
-			WRITEUINT32(save_p, mainrecords[i]->time);
-			WRITEUINT16(save_p, mainrecords[i]->rings);
+			WRITEUINT32(save_p, data->mainrecords[i]->score);
+			WRITEUINT32(save_p, data->mainrecords[i]->time);
+			WRITEUINT16(save_p, data->mainrecords[i]->rings);
 		}
 		else
 		{
@@ -4590,19 +4681,19 @@ void G_SaveGameData(void)
 	// NiGHTS records
 	for (i = 0; i < NUMMAPS; i++)
 	{
-		if (!nightsrecords[i] || !nightsrecords[i]->nummares)
+		if (!data->nightsrecords[i] || !data->nightsrecords[i]->nummares)
 		{
 			WRITEUINT8(save_p, 0);
 			continue;
 		}
 
-		WRITEUINT8(save_p, nightsrecords[i]->nummares);
+		WRITEUINT8(save_p, data->nightsrecords[i]->nummares);
 
-		for (curmare = 0; curmare < (nightsrecords[i]->nummares + 1); ++curmare)
+		for (curmare = 0; curmare < (data->nightsrecords[i]->nummares + 1); ++curmare)
 		{
-			WRITEUINT32(save_p, nightsrecords[i]->score[curmare]);
-			WRITEUINT8(save_p, nightsrecords[i]->grade[curmare]);
-			WRITEUINT32(save_p, nightsrecords[i]->time[curmare]);
+			WRITEUINT32(save_p, data->nightsrecords[i]->score[curmare]);
+			WRITEUINT8(save_p, data->nightsrecords[i]->grade[curmare]);
+			WRITEUINT32(save_p, data->nightsrecords[i]->time[curmare]);
 		}
 	}
 
@@ -4698,12 +4789,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();
 
@@ -4896,9 +4984,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)
@@ -4919,10 +5007,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);
@@ -4956,12 +5041,20 @@ void G_InitNew(UINT8 pultmode, const char *mapname, boolean resetplayer, boolean
 		numgameovers = tokenlist = token = sstimer = redscore = bluescore = lastmap = 0;
 		countdown = countdown2 = exitfadestarted = 0;
 
+		if (!FLS)
+		{
+			emeralds = 0;
+			memset(&luabanks, 0, sizeof(luabanks));
+		}
+
 		for (i = 0; i < MAXPLAYERS; i++)
 		{
 			players[i].playerstate = PST_REBORN;
 			players[i].starpostscale = players[i].starpostangle = players[i].starpostnum = players[i].starposttime = 0;
 			players[i].starpostx = players[i].starposty = players[i].starpostz = 0;
+			players[i].recordscore = 0;
 
+			// default lives, continues and score
 			if (netgame || multiplayer)
 			{
 				if (!FLS || (players[i].lives < 1))
@@ -4994,6 +5087,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....
@@ -5021,6 +5118,22 @@ 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)
+	{
+		numgameovers = savedata.numgameovers;
+		players[consoleplayer].continues = savedata.continues;
+		players[consoleplayer].lives = savedata.lives;
+		players[consoleplayer].score = savedata.score;
+		if ((botingame = ((botskin = savedata.botskin) != 0)))
+			botcolor = skins[botskin-1].prefcolor;
+		emeralds = savedata.emeralds;
+		savedata.lives = 0;
+	}
+
 	if ((gametyperules & GTR_CUTSCENES) && !skipprecutscene && mapheaderinfo[gamemap-1]->precutscenenum && !modeattacking && !(marathonmode & MA_NOCUTSCENES)) // Start a custom cutscene.
 		F_StartCustomCutscene(mapheaderinfo[gamemap-1]->precutscenenum-1, true, resetplayer, FLS);
 	else
diff --git a/src/g_game.h b/src/g_game.h
index 144360db4f73626bfce0a78adfce13895aac7afb..4d86a0455e560e22afc9d75b24f5d97acaf4e95c 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -19,6 +19,7 @@
 #include "d_event.h"
 #include "g_demo.h"
 #include "m_cheat.h" // objectplacing
+#include "m_cond.h"
 
 extern char gamedatafilename[64];
 extern char timeattackfolder[64];
@@ -48,6 +49,8 @@ extern boolean promptactive;
 
 extern consvar_t cv_pauseifunfocused;
 
+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;
@@ -173,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);
@@ -183,7 +185,7 @@ boolean G_IsTitleCardAvailable(void);
 // Can be called by the startup code or M_Responder, calls P_SetupLevel.
 void G_LoadGame(UINT32 slot, INT16 mapoverride);
 
-void G_SaveGameData(void);
+void G_SaveGameData(gamedata_t *data);
 
 void G_SaveGame(UINT32 slot, INT16 mapnum);
 
@@ -239,7 +241,7 @@ void G_SetModeAttackRetryFlag(void);
 void G_ClearModeAttackRetryFlag(void);
 boolean G_GetModeAttackRetryFlag(void);
 
-void G_LoadGameData(void);
+void G_LoadGameData(gamedata_t *data);
 void G_LoadGameSettings(void);
 
 void G_SetGameModified(boolean silent);
@@ -248,19 +250,18 @@ void G_SetUsedCheats(boolean silent);
 void G_SetGamestate(gamestate_t newstate);
 
 // Gamedata record shit
-void G_AllocMainRecordData(INT16 i);
-void G_AllocNightsRecordData(INT16 i);
-void G_ClearRecords(void);
-
-UINT32 G_GetBestScore(INT16 map);
-tic_t G_GetBestTime(INT16 map);
-UINT16 G_GetBestRings(INT16 map);
-UINT32 G_GetBestNightsScore(INT16 map, UINT8 mare);
-tic_t G_GetBestNightsTime(INT16 map, UINT8 mare);
-UINT8 G_GetBestNightsGrade(INT16 map, UINT8 mare);
-
-void G_AddTempNightsRecords(UINT32 pscore, tic_t ptime, UINT8 mare);
-void G_SetNightsRecords(void);
+void G_AllocMainRecordData(INT16 i, gamedata_t *data);
+void G_AllocNightsRecordData(INT16 i, gamedata_t *data);
+void G_ClearRecords(gamedata_t *data);
+
+UINT32 G_GetBestScore(INT16 map, gamedata_t *data);
+tic_t G_GetBestTime(INT16 map, gamedata_t *data);
+UINT16 G_GetBestRings(INT16 map, gamedata_t *data);
+UINT32 G_GetBestNightsScore(INT16 map, UINT8 mare, gamedata_t *data);
+tic_t G_GetBestNightsTime(INT16 map, UINT8 mare, gamedata_t *data);
+UINT8 G_GetBestNightsGrade(INT16 map, UINT8 mare, gamedata_t *data);
+
+void G_AddTempNightsRecords(player_t *player, UINT32 pscore, tic_t ptime, UINT8 mare);
 
 FUNCMATH INT32 G_TicsToHours(tic_t tics);
 FUNCMATH INT32 G_TicsToMinutes(tic_t tics, boolean full);
diff --git a/src/g_input.c b/src/g_input.c
index 826dcecbdf22b6ff833fce6e65bfb71ef051d05d..fa30c1984f6bcd02e9befaad37631b2be04849d2 100644
--- a/src/g_input.c
+++ b/src/g_input.c
@@ -16,7 +16,7 @@
 #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"
 
 #define MAXMOUSESENSITIVITY 100 // sensitivity steps
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/CMakeLists.txt b/src/hardware/CMakeLists.txt
index 4e9c67d2f348a8bfed899e4002d25136284b031f..e7819aba97e2065d36f6f920d4725d7b294505f3 100644
--- a/src/hardware/CMakeLists.txt
+++ b/src/hardware/CMakeLists.txt
@@ -1 +1,14 @@
-target_sourcefile(c)
+target_sources(SRB2SDL2 PRIVATE
+	hw_bsp.c
+	hw_draw.c
+	hw_light.c
+	hw_main.c
+	hw_clip.c
+	hw_md2.c
+	hw_cache.c
+	hw_md2load.c
+	hw_md3load.c
+	hw_model.c
+	hw_batching.c
+	r_opengl/r_opengl.c
+)
diff --git a/src/hardware/Sourcefile b/src/hardware/Sourcefile
index 1c05de76cca6d71251023e3e9e7bdde7d8cffaab..6c374621d7b1de61f2b5a5c6fd9171f0685eccbf 100644
--- a/src/hardware/Sourcefile
+++ b/src/hardware/Sourcefile
@@ -8,6 +8,5 @@ hw_cache.c
 hw_md2load.c
 hw_md3load.c
 hw_model.c
-u_list.c
 hw_batching.c
 r_opengl/r_opengl.c
diff --git a/src/hardware/hw_batching.c b/src/hardware/hw_batching.c
index d1b84a5eecb4aff512bb75cf7562327675622bf1..dc0b5ee5b00fae1b28591f78ab3cb39627f11645 100644
--- a/src/hardware/hw_batching.c
+++ b/src/hardware/hw_batching.c
@@ -42,10 +42,10 @@ int unsortedVertexArrayAllocSize = 65536;
 // Call HWR_RenderBatches to render all the collected geometry.
 void HWR_StartBatching(void)
 {
-    if (currently_batching)
-        I_Error("Repeat call to HWR_StartBatching without HWR_RenderBatches");
+	if (currently_batching)
+		I_Error("Repeat call to HWR_StartBatching without HWR_RenderBatches");
 
-    // init arrays if that has not been done yet
+	// init arrays if that has not been done yet
 	if (!finalVertexArray)
 	{
 		finalVertexArray = malloc(finalVertexArrayAllocSize * sizeof(FOutVector));
@@ -55,7 +55,7 @@ void HWR_StartBatching(void)
 		unsortedVertexArray = malloc(unsortedVertexArrayAllocSize * sizeof(FOutVector));
 	}
 
-    currently_batching = true;
+	currently_batching = true;
 }
 
 // This replaces the direct calls to pfnSetTexture in cases where batching is available.
diff --git a/src/hardware/hw_defs.h b/src/hardware/hw_defs.h
index b0859f478bd37d2315d742d0f7c80a580a5300bc..74c4ed7d2eec91d1c846ca0a43f23d636a1024f8 100644
--- a/src/hardware/hw_defs.h
+++ b/src/hardware/hw_defs.h
@@ -93,33 +93,22 @@ typedef struct FVector
 //Hurdler: Transform (coords + angles)
 //BP: transform order : scale(rotation_x(rotation_y(translation(v))))
 
-// Kart features
-//#define USE_FTRANSFORM_ANGLEZ
-//#define USE_FTRANSFORM_MIRROR
-
 // Vanilla features
 #define USE_MODEL_NEXTFRAME
 
 typedef struct
 {
 	FLOAT       x,y,z;           // position
-#ifdef USE_FTRANSFORM_ANGLEZ
 	FLOAT       anglex,angley,anglez;   // aimingangle / viewangle
-#else
-	FLOAT       anglex,angley;   // aimingangle / viewangle
-#endif
 	FLOAT       scalex,scaley,scalez;
 	FLOAT       fovxangle, fovyangle;
 	UINT8       splitscreen;
 	boolean     flip;            // screenflip
 	boolean     roll;
-	SINT8       rollflip;
 	FLOAT       rollangle; // done to not override USE_FTRANSFORM_ANGLEZ
-	UINT8       rotaxis;
 	FLOAT       centerx, centery;
-#ifdef USE_FTRANSFORM_MIRROR
+	FLOAT       rollx, rollz;
 	boolean     mirror;          // SRB2Kart: Encore Mode
-#endif
 	boolean     shearing;        // 14042019
 	float       viewaiming;      // 17052019
 } FTransform;
@@ -136,6 +125,7 @@ typedef struct
 // Predefined shader types
 enum
 {
+	SHADER_NONE = -1,
 	SHADER_DEFAULT = 0,
 
 	SHADER_FLOOR,
@@ -237,7 +227,8 @@ enum EPolyFlags
 	PF_RemoveYWrap      = 0x00010000,   // Forces clamp texture on Y
 	PF_ForceWrapX       = 0x00020000,   // Forces repeat texture on X
 	PF_ForceWrapY       = 0x00040000,   // Forces repeat texture on Y
-	PF_Ripple           = 0x00100000    // Water ripple effect. The current backend doesn't use it for anything.
+	PF_Ripple           = 0x00100000,   // Water ripple effect. The current backend doesn't use it for anything.
+	PF_WireFrame        = 0x00200000,   // Draws vertices as lines instead of triangles
 };
 
 
diff --git a/src/hardware/hw_drv.h b/src/hardware/hw_drv.h
index 426d2f283b35668e70fe12520ad2dab0b6d23fd5..1c4cd99ab03d34498fa13d132a01ef53af9e6e61 100644
--- a/src/hardware/hw_drv.h
+++ b/src/hardware/hw_drv.h
@@ -51,7 +51,7 @@ EXPORT void HWRAPI(ClearMipMapCache) (void);
 EXPORT void HWRAPI(SetSpecialState) (hwdspecialstate_t IdState, INT32 Value);
 
 //Hurdler: added for new development
-EXPORT void HWRAPI(DrawModel) (model_t *model, INT32 frameIndex, float duration, float tics, INT32 nextFrameIndex, FTransform *pos, float scale, UINT8 flipped, UINT8 hflipped, FSurfaceInfo *Surface);
+EXPORT void HWRAPI(DrawModel) (model_t *model, INT32 frameIndex, float duration, float tics, INT32 nextFrameIndex, FTransform *pos, float hscale, float vscale, UINT8 flipped, UINT8 hflipped, FSurfaceInfo *Surface);
 EXPORT void HWRAPI(CreateModelVBOs) (model_t *model);
 EXPORT void HWRAPI(SetTransform) (FTransform *ptransform);
 EXPORT INT32 HWRAPI(GetTextureUsed) (void);
@@ -136,4 +136,3 @@ extern struct hwdriver_s hwdriver;
 #endif //not defined _CREATE_DLL_
 
 #endif //__HWR_DRV_H__
-
diff --git a/src/hardware/hw_glob.h b/src/hardware/hw_glob.h
index b5daba82261ce46cbe0393ca765146b4118fcfff..d391c415670846fad27d4a22e1e33f46ccb367b3 100644
--- a/src/hardware/hw_glob.h
+++ b/src/hardware/hw_glob.h
@@ -81,6 +81,7 @@ typedef struct gl_vissprite_s
 
 	boolean flip, vflip;
 	boolean precip; // Tails 08-25-2002
+	boolean bbox;
 	boolean rotated;
 	UINT8 translucency;       //alpha level 0-255
 
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index 0b5d41c4f57015db09b996107fb3e54f1d14778c..dfb80e5021ff46bc68a638bff7e48df5145fc604 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"
@@ -66,6 +66,7 @@ static void HWR_ProjectSprite(mobj_t *thing);
 #ifdef HWPRECIP
 static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing);
 #endif
+static void HWR_ProjectBoundingBox(mobj_t *thing);
 
 void HWR_AddTransparentFloor(levelflat_t *levelflat, extrasubsector_t *xsub, boolean isceiling, fixed_t fixedheight, INT32 lightlevel, INT32 alpha, sector_t *FOFSector, FBITFIELD blend, boolean fogplane, extracolormap_t *planecolormap);
 void HWR_AddTransparentPolyobjectFloor(levelflat_t *levelflat, polyobj_t *polysector, boolean isceiling, fixed_t fixedheight,
@@ -139,7 +140,7 @@ static fixed_t dup_viewx, dup_viewy, dup_viewz;
 static angle_t dup_viewangle;
 
 static float gl_viewx, gl_viewy, gl_viewz;
-static float gl_viewsin, gl_viewcos;
+float gl_viewsin, gl_viewcos;
 
 // Maybe not necessary with the new T&L code (needs to be checked!)
 static float gl_viewludsin, gl_viewludcos; // look up down kik test
@@ -459,30 +460,30 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 	{
 		if (!isceiling) // it's a floor
 		{
-			scrollx = FIXED_TO_FLOAT(FOFsector->floor_xoffs)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(FOFsector->floor_yoffs)/fflatheight;
-			angle = FOFsector->floorpic_angle;
+			scrollx = FIXED_TO_FLOAT(FOFsector->floorxoffset)/fflatwidth;
+			scrolly = FIXED_TO_FLOAT(FOFsector->flooryoffset)/fflatheight;
+			angle = FOFsector->floorangle;
 		}
 		else // it's a ceiling
 		{
-			scrollx = FIXED_TO_FLOAT(FOFsector->ceiling_xoffs)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(FOFsector->ceiling_yoffs)/fflatheight;
-			angle = FOFsector->ceilingpic_angle;
+			scrollx = FIXED_TO_FLOAT(FOFsector->ceilingxoffset)/fflatwidth;
+			scrolly = FIXED_TO_FLOAT(FOFsector->ceilingyoffset)/fflatheight;
+			angle = FOFsector->ceilingangle;
 		}
 	}
 	else if (gl_frontsector)
 	{
 		if (!isceiling) // it's a floor
 		{
-			scrollx = FIXED_TO_FLOAT(gl_frontsector->floor_xoffs)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(gl_frontsector->floor_yoffs)/fflatheight;
-			angle = gl_frontsector->floorpic_angle;
+			scrollx = FIXED_TO_FLOAT(gl_frontsector->floorxoffset)/fflatwidth;
+			scrolly = FIXED_TO_FLOAT(gl_frontsector->flooryoffset)/fflatheight;
+			angle = gl_frontsector->floorangle;
 		}
 		else // it's a ceiling
 		{
-			scrollx = FIXED_TO_FLOAT(gl_frontsector->ceiling_xoffs)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(gl_frontsector->ceiling_yoffs)/fflatheight;
-			angle = gl_frontsector->ceilingpic_angle;
+			scrollx = FIXED_TO_FLOAT(gl_frontsector->ceilingxoffset)/fflatwidth;
+			scrolly = FIXED_TO_FLOAT(gl_frontsector->ceilingyoffset)/fflatheight;
+			angle = gl_frontsector->ceilingangle;
 		}
 	}
 
@@ -1153,7 +1154,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 			else
 				texturevpeg = gl_backsector->ceilingheight + textureheight[gl_toptexture] - gl_frontsector->ceilingheight;
 
-			texturevpeg += gl_sidedef->rowoffset;
+			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];
@@ -1162,8 +1163,8 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 
 			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 * grTex->scaleX;
-			wallVerts[2].s = wallVerts[1].s = cliphigh * grTex->scaleX;
+			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;
 
 			// Adjust t value for sloped walls
 			if (!(gl_linedef->flags & ML_SKEWTD))
@@ -1213,7 +1214,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 			else
 				texturevpeg = gl_frontsector->floorheight - gl_backsector->floorheight;
 
-			texturevpeg += gl_sidedef->rowoffset;
+			texturevpeg += gl_sidedef->rowoffset + gl_sidedef->offsety_bot;
 
 			// 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];
@@ -1222,8 +1223,8 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 
 			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 * grTex->scaleX;
-			wallVerts[2].s = wallVerts[1].s = cliphigh * grTex->scaleX;
+			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;
 
 			// Adjust t value for sloped walls
 			if (!(gl_linedef->flags & ML_SKEWTD))
@@ -1333,13 +1334,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;
+					polybottom = max(front->floorheight, back->floorheight) + gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
 					polytop = polybottom + midtexheight;
 				}
 				// Peg it to the ceiling
 				else
 				{
-					polytop = min(front->ceilingheight, back->ceilingheight) + gl_sidedef->rowoffset;
+					polytop = min(front->ceilingheight, back->ceilingheight) + gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
 					polybottom = polytop - midtexheight;
 				}
 
@@ -1350,9 +1351,9 @@ 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;
+				polybottom = popenbottom + gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
 				polytop = polybottom + midtexheight;
-				polybottomslope = popenbottomslope + gl_sidedef->rowoffset;
+				polybottomslope = popenbottomslope + gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
 				polytopslope = polybottomslope + midtexheight;
 			}
 			// Skew it according to the ceiling's slope
@@ -1407,12 +1408,12 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 			// Left side
 			wallVerts[3].t = texturevpeg * grTex->scaleY;
 			wallVerts[0].t = (h - l + texturevpeg) * grTex->scaleY;
-			wallVerts[0].s = wallVerts[3].s = cliplow * grTex->scaleX;
+			wallVerts[0].s = wallVerts[3].s = (cliplow + 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 * grTex->scaleX;
+			wallVerts[2].s = wallVerts[1].s = (cliphigh + gl_sidedef->offsetx_mid) * grTex->scaleX;
 
 			// set top/bottom coords
 			// Take the texture peg into account, rather than changing the offsets past
@@ -1474,19 +1475,19 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 
 			// 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;
+				texturevpeg = gl_frontsector->floorheight + textureheight[gl_sidedef->midtexture] - gl_frontsector->ceilingheight + gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
 			else if (gl_linedef->flags & ML_DONTPEGBOTTOM)
-				texturevpeg = worldbottom + textureheight[gl_sidedef->midtexture] - worldtop + gl_sidedef->rowoffset;
+				texturevpeg = worldbottom + textureheight[gl_sidedef->midtexture] - worldtop + gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
 			else
 				// top of texture at top
-				texturevpeg = gl_sidedef->rowoffset;
+				texturevpeg = gl_sidedef->rowoffset + gl_sidedef->offsety_mid;
 
 			grTex = HWR_GetTexture(gl_midtexture);
 
 			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 * grTex->scaleX;
-			wallVerts[2].s = wallVerts[1].s = cliphigh * grTex->scaleX;
+			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;
 
 			// Texture correction for slopes
 			if (gl_linedef->flags & ML_NOSKEW) {
@@ -1634,13 +1635,13 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 					// -- Monster Iestyn 26/06/18
 					if (newline)
 					{
-						texturevpeg = sides[newline->sidenum[0]].rowoffset;
+						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;
+						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);
 					}
@@ -1672,8 +1673,8 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 						}
 					}
 
-					wallVerts[0].s = wallVerts[3].s = cliplow * grTex->scaleX;
-					wallVerts[2].s = wallVerts[1].s = cliphigh * grTex->scaleX;
+					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;
 				}
 				if (rover->fofflags & FOF_FOG)
 				{
@@ -1698,7 +1699,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 					if ((rover->fofflags & FOF_TRANSLUCENT && !(rover->fofflags & FOF_SPLAT)) || rover->blend)
 					{
 						blendmode = rover->blend ? HWR_GetBlendModeFlag(rover->blend) : PF_Translucent;
-						Surf.PolyColor.s.alpha = (UINT8)rover->alpha-1 > 255 ? 255 : rover->alpha-1;
+						Surf.PolyColor.s.alpha = max(0, min(rover->alpha, 255));
 					}
 
 					if (gl_frontsector->numlights)
@@ -1785,17 +1786,17 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 
 					if (newline)
 					{
-						wallVerts[3].t = wallVerts[2].t = (*rover->topheight - h + sides[newline->sidenum[0]].rowoffset) * grTex->scaleY;
-						wallVerts[0].t = wallVerts[1].t = (h - l + (*rover->topheight - h + sides[newline->sidenum[0]].rowoffset)) * grTex->scaleY;
+						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) * grTex->scaleY;
-						wallVerts[0].t = wallVerts[1].t = (h - l + (*rover->topheight - h + sides[rover->master->sidenum[0]].rowoffset)) * grTex->scaleY;
+						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;
 					}
 
-					wallVerts[0].s = wallVerts[3].s = cliplow * grTex->scaleX;
-					wallVerts[2].s = wallVerts[1].s = cliphigh * grTex->scaleX;
+					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;
 				}
 
 				if (rover->fofflags & FOF_FOG)
@@ -1821,7 +1822,7 @@ static void HWR_ProcessSeg(void) // Sort of like GLWall::Process in GZDoom
 					if ((rover->fofflags & FOF_TRANSLUCENT && !(rover->fofflags & FOF_SPLAT)) || rover->blend)
 					{
 						blendmode = rover->blend ? HWR_GetBlendModeFlag(rover->blend) : PF_Translucent;
-						Surf.PolyColor.s.alpha = (UINT8)rover->alpha-1 > 255 ? 255 : rover->alpha-1;
+						Surf.PolyColor.s.alpha = max(0, min(rover->alpha, 255));
 					}
 
 					if (gl_backsector->numlights)
@@ -2719,30 +2720,30 @@ static void HWR_RenderPolyObjectPlane(polyobj_t *polysector, boolean isceiling,
 	{
 		if (!isceiling) // it's a floor
 		{
-			scrollx = FIXED_TO_FLOAT(FOFsector->floor_xoffs)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(FOFsector->floor_yoffs)/fflatheight;
-			angle = FOFsector->floorpic_angle;
+			scrollx = FIXED_TO_FLOAT(FOFsector->floorxoffset)/fflatwidth;
+			scrolly = FIXED_TO_FLOAT(FOFsector->flooryoffset)/fflatheight;
+			angle = FOFsector->floorangle;
 		}
 		else // it's a ceiling
 		{
-			scrollx = FIXED_TO_FLOAT(FOFsector->ceiling_xoffs)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(FOFsector->ceiling_yoffs)/fflatheight;
-			angle = FOFsector->ceilingpic_angle;
+			scrollx = FIXED_TO_FLOAT(FOFsector->ceilingxoffset)/fflatwidth;
+			scrolly = FIXED_TO_FLOAT(FOFsector->ceilingyoffset)/fflatheight;
+			angle = FOFsector->ceilingangle;
 		}
 	}
 	else if (gl_frontsector)
 	{
 		if (!isceiling) // it's a floor
 		{
-			scrollx = FIXED_TO_FLOAT(gl_frontsector->floor_xoffs)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(gl_frontsector->floor_yoffs)/fflatheight;
-			angle = gl_frontsector->floorpic_angle;
+			scrollx = FIXED_TO_FLOAT(gl_frontsector->floorxoffset)/fflatwidth;
+			scrolly = FIXED_TO_FLOAT(gl_frontsector->flooryoffset)/fflatheight;
+			angle = gl_frontsector->floorangle;
 		}
 		else // it's a ceiling
 		{
-			scrollx = FIXED_TO_FLOAT(gl_frontsector->ceiling_xoffs)/fflatwidth;
-			scrolly = FIXED_TO_FLOAT(gl_frontsector->ceiling_yoffs)/fflatheight;
-			angle = gl_frontsector->ceilingpic_angle;
+			scrollx = FIXED_TO_FLOAT(gl_frontsector->ceilingxoffset)/fflatwidth;
+			scrolly = FIXED_TO_FLOAT(gl_frontsector->ceilingyoffset)/fflatheight;
+			angle = gl_frontsector->ceilingangle;
 		}
 	}
 
@@ -3094,7 +3095,7 @@ static void HWR_Subsector(size_t num)
 										   false,
 					                       *rover->bottomheight,
 					                       *gl_frontsector->lightlist[light].lightlevel,
-					                       rover->alpha-1 > 255 ? 255 : rover->alpha-1, rover->master->frontsector,
+					                       max(0, min(rover->alpha, 255)), rover->master->frontsector,
 					                       HWR_RippleBlend(gl_frontsector, rover, false) | (rover->blend ? HWR_GetBlendModeFlag(rover->blend) : PF_Translucent),
 					                       false, *gl_frontsector->lightlist[light].extra_colormap);
 				}
@@ -3140,7 +3141,7 @@ static void HWR_Subsector(size_t num)
 											true,
 					                        *rover->topheight,
 					                        *gl_frontsector->lightlist[light].lightlevel,
-					                        rover->alpha-1 > 255 ? 255 : rover->alpha-1, rover->master->frontsector,
+					                        max(0, min(rover->alpha, 255)), rover->master->frontsector,
 					                        HWR_RippleBlend(gl_frontsector, rover, false) | (rover->blend ? HWR_GetBlendModeFlag(rover->blend) : PF_Translucent),
 					                        false, *gl_frontsector->lightlist[light].extra_colormap);
 				}
@@ -3594,7 +3595,7 @@ static void HWR_DrawDropShadow(mobj_t *thing, fixed_t scale)
 			return;
 	}
 
-	floordiff = abs((flip < 0 ? thing->height : 0) + interp.z - groundz);
+	floordiff = abs((flip < 0 ? interp.height : 0) + interp.z - groundz);
 
 	alpha = floordiff / (4*FRACUNIT) + 75;
 	if (alpha >= 255) return;
@@ -3605,7 +3606,7 @@ static void HWR_DrawDropShadow(mobj_t *thing, fixed_t scale)
 	HWR_GetPatch(gpatch);
 
 	scalemul = FixedMul(FRACUNIT - floordiff/640, scale);
-	scalemul = FixedMul(scalemul, (thing->radius*2) / gpatch->height);
+	scalemul = FixedMul(scalemul, (interp.radius*2) / gpatch->height);
 
 	fscale = FIXED_TO_FLOAT(scalemul);
 	fx = FIXED_TO_FLOAT(interp.x);
@@ -3717,7 +3718,7 @@ static void HWR_RotateSpritePolyToAim(gl_vissprite_t *spr, FOutVector *wallVerts
 
 		if (P_MobjFlip(spr->mobj) == -1)
 		{
-			basey = FIXED_TO_FLOAT(interp.z + spr->mobj->height);
+			basey = FIXED_TO_FLOAT(interp.z + interp.height);
 		}
 		else
 		{
@@ -4036,6 +4037,54 @@ static void HWR_SplitSprite(gl_vissprite_t *spr)
 		HWR_LinkDrawHackAdd(wallVerts, spr);
 }
 
+static void HWR_DrawBoundingBox(gl_vissprite_t *vis)
+{
+	FOutVector v[24];
+	FSurfaceInfo Surf = {0};
+
+	//
+	// create a cube (side view)
+	//
+	//  5--4  3
+	//        |
+	//        |
+	//  0--1  2
+	//
+	// repeat this 4 times (overhead)
+	//
+	//
+	// 15    16  17    09
+	//    14 13  12 08
+	// 23 18  *--*  07 10
+	//        |  |
+	// 22 19  *--*  06 11
+	//    20 00  01 02
+	// 21    05  04    03
+	//
+
+	v[ 0].x = v[ 5].x = v[13].x = v[14].x = v[15].x = v[16].x =
+		v[18].x = v[19].x = v[20].x = v[21].x = v[22].x = v[23].x = vis->x1; // west
+
+	v[ 1].x = v[ 2].x = v[ 3].x = v[ 4].x = v[ 6].x = v[ 7].x =
+		v[ 8].x = v[ 9].x = v[10].x = v[11].x = v[12].x = v[17].x = vis->x2; // east
+
+	v[ 0].z = v[ 1].z = v[ 2].z = v[ 3].z = v[ 4].z = v[ 5].z =
+		v[ 6].z = v[11].z = v[19].z = v[20].z = v[21].z = v[22].z = vis->z1; // south
+
+	v[ 7].z = v[ 8].z = v[ 9].z = v[10].z = v[12].z = v[13].z =
+		v[14].z = v[15].z = v[16].z = v[17].z = v[18].z = v[23].z = vis->z2; // north
+
+	v[ 0].y = v[ 1].y = v[ 2].y = v[ 6].y = v[ 7].y = v[ 8].y =
+		v[12].y = v[13].y = v[14].y = v[18].y = v[19].y = v[20].y = vis->gz; // bottom
+
+	v[ 3].y = v[ 4].y = v[ 5].y = v[ 9].y = v[10].y = v[11].y =
+		v[15].y = v[16].y = v[17].y = v[21].y = v[22].y = v[23].y = vis->gzt; // top
+
+	Surf.PolyColor = V_GetColor(R_GetBoundingBoxColor(vis->mobj));
+	
+	HWR_ProcessPolygon(&Surf, v, 24, (cv_renderhitboxgldepth.value ? 0 : PF_NoDepthTest)|PF_Modulated|PF_NoTexture|PF_WireFrame, SHADER_NONE, false);
+}
+
 // -----------------+
 // HWR_DrawSprite   : Draw flat sprites
 //                  : (monsters, bonuses, weapons, lights, ...)
@@ -4092,14 +4141,11 @@ static void HWR_DrawSprite(gl_vissprite_t *spr)
 		float xscale, yscale;
 		float xoffset, yoffset;
 		float leftoffset, topoffset;
-		float scale = spr->scale;
 		float zoffset = (P_MobjFlip(spr->mobj) * 0.05f);
 		pslope_t *splatslope = NULL;
 		INT32 i;
 
 		renderflags_t renderflags = spr->renderflags;
-		if (renderflags & RF_SHADOWEFFECTS)
-			scale *= spr->shadowscale;
 
 		if (spr->rotateflags & SRF_3D || renderflags & RF_NOSPLATBILLBOARD)
 			angle = spr->mobj->angle;
@@ -4107,7 +4153,7 @@ static void HWR_DrawSprite(gl_vissprite_t *spr)
 			angle = viewangle;
 
 		if (!spr->rotated)
-			angle += spr->mobj->rollangle;
+			angle += spr->mobj->spriteroll;
 
 		angle = -angle;
 		angle += ANGLE_90;
@@ -4480,9 +4526,16 @@ static int CompareVisSprites(const void *p1, const void *p2)
 	int transparency1;
 	int transparency2;
 
+	int linkdraw1;
+	int linkdraw2;
+
+	// draw bbox after everything else
+	if (spr1->bbox || spr2->bbox)
+		return (spr1->bbox - spr2->bbox);
+
 	// check for precip first, because then sprX->mobj is actually a precipmobj_t and does not have flags2 or tracer
-	int linkdraw1 = !spr1->precip && (spr1->mobj->flags2 & MF2_LINKDRAW) && spr1->mobj->tracer;
-	int linkdraw2 = !spr2->precip && (spr2->mobj->flags2 & MF2_LINKDRAW) && spr2->mobj->tracer;
+	linkdraw1 = !spr1->precip && (spr1->mobj->flags2 & MF2_LINKDRAW) && spr1->mobj->tracer;
+	linkdraw2 = !spr2->precip && (spr2->mobj->flags2 & MF2_LINKDRAW) && spr2->mobj->tracer;
 
 	// ^ is the XOR operation
 	// if comparing a linkdraw and non-linkdraw sprite or 2 linkdraw sprites with different tracers, then use
@@ -4852,6 +4905,9 @@ static void HWR_DrawSprites(void)
 	for (i = 0; i < gl_visspritecount; i++)
 	{
 		gl_vissprite_t *spr = gl_vsprorder[i];
+		if (spr->bbox)
+			HWR_DrawBoundingBox(spr);
+		else
 #ifdef HWPRECIP
 		if (spr->precip)
 			HWR_DrawPrecipitationSprite(spr);
@@ -4951,8 +5007,15 @@ static void HWR_AddSprites(sector_t *sec)
 	hoop_limit_dist = (fixed_t)(cv_drawdist_nights.value) << FRACBITS;
 	for (thing = sec->thinglist; thing; thing = thing->snext)
 	{
-		if (R_ThingVisibleWithinDist(thing, limit_dist, hoop_limit_dist))
-			HWR_ProjectSprite(thing);
+		if (R_ThingWithinDist(thing, limit_dist, hoop_limit_dist))
+		{
+			if (R_ThingVisible(thing))
+			{
+				HWR_ProjectSprite(thing);
+			}
+
+			HWR_ProjectBoundingBox(thing);
+		}
 	}
 
 #ifdef HWPRECIP
@@ -5010,6 +5073,7 @@ static void HWR_ProjectSprite(mobj_t *thing)
 #ifdef ROTSPRITE
 	patch_t *rotsprite = NULL;
 	INT32 rollangle = 0;
+	angle_t spriterotangle = 0;
 #endif
 
 	// uncapped/interpolation
@@ -5177,18 +5241,21 @@ static void HWR_ProjectSprite(mobj_t *thing)
 	spr_topoffset = spritecachedinfo[lumpoff].topoffset;
 
 #ifdef ROTSPRITE
-	if (thing->rollangle
+	spriterotangle = R_SpriteRotationAngle(&interp);
+
+	if (spriterotangle != 0
 	&& !(splat && !(thing->renderflags & RF_NOSPLATROLLANGLE)))
 	{
 		if (papersprite)
 		{
 			// a positive rollangle should should pitch papersprites upwards relative to their facing angle
-			rollangle = R_GetRollAngle(InvAngle(thing->rollangle));
+			rollangle = R_GetRollAngle(InvAngle(spriterotangle));
 		}
 		else
 		{
-			rollangle = R_GetRollAngle(thing->rollangle);
+			rollangle = R_GetRollAngle(spriterotangle);
 		}
+
 		rotsprite = Patch_GetRotatedSprite(sprframe, (thing->frame & FF_FRAMEMASK), rot, flip, false, sprinfo, rollangle);
 
 		if (rotsprite != NULL)
@@ -5254,7 +5321,7 @@ static void HWR_ProjectSprite(mobj_t *thing)
 			}
 
 			groundz = R_GetShadowZ(thing, NULL);
-			floordiff = abs(((thing->eflags & MFE_VERTICALFLIP) ? caster->height : 0) + casterinterp.z - groundz);
+			floordiff = abs(((thing->eflags & MFE_VERTICALFLIP) ? casterinterp.height : 0) + casterinterp.z - groundz);
 
 			shadowheight = FIXED_TO_FLOAT(floordiff);
 			shadowscale = FIXED_TO_FLOAT(FixedMul(FRACUNIT - floordiff/640, casterinterp.scale));
@@ -5306,7 +5373,7 @@ static void HWR_ProjectSprite(mobj_t *thing)
 
 		if (vflip)
 		{
-			gz = FIXED_TO_FLOAT(interp.z + thing->height) - (FIXED_TO_FLOAT(spr_topoffset) * this_yscale);
+			gz = FIXED_TO_FLOAT(interp.z + interp.height) - (FIXED_TO_FLOAT(spr_topoffset) * this_yscale);
 			gzt = gz + (FIXED_TO_FLOAT(spr_height) * this_yscale);
 		}
 		else
@@ -5470,6 +5537,7 @@ static void HWR_ProjectSprite(mobj_t *thing)
 	vis->vflip = vflip;
 
 	vis->precip = false;
+	vis->bbox = false;
 
 	vis->angle = interp.angle;
 }
@@ -5592,6 +5660,7 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 	vis->gz = vis->gzt - FIXED_TO_FLOAT(spritecachedinfo[lumpoff].height);
 
 	vis->precip = true;
+	vis->bbox = false;
 
 	// okay... this is a hack, but weather isn't networked, so it should be ok
 	if (!(thing->precipflags & PCF_THUNK))
@@ -5605,6 +5674,58 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 }
 #endif
 
+static void HWR_ProjectBoundingBox(mobj_t *thing)
+{
+	gl_vissprite_t *vis;
+	float tr_x, tr_y;
+	float tz;
+
+	if (!thing)
+		return;
+
+	if (!R_ThingBoundingBoxVisible(thing))
+		return;
+
+	// uncapped/interpolation
+	boolean interpolate = cv_renderhitboxinterpolation.value;
+	interpmobjstate_t interp = {0};
+
+	if (R_UsingFrameInterpolation() && !paused && interpolate)
+	{
+		R_InterpolateMobjState(thing, rendertimefrac, &interp);
+	}
+	else
+	{
+		R_InterpolateMobjState(thing, FRACUNIT, &interp);
+	}
+
+	// transform the origin point
+	tr_x = FIXED_TO_FLOAT(interp.x) - gl_viewx;
+	tr_y = FIXED_TO_FLOAT(interp.y) - gl_viewy;
+
+	// rotation around vertical axis
+	tz = (tr_x * gl_viewcos) + (tr_y * gl_viewsin);
+
+	// thing is behind view plane?
+	if (tz < ZCLIP_PLANE)
+		return;
+
+	tr_x += gl_viewx;
+	tr_y += gl_viewy;
+
+	vis = HWR_NewVisSprite();
+	vis->x1 = tr_x - FIXED_TO_FLOAT(interp.radius);
+	vis->x2 = tr_x + FIXED_TO_FLOAT(interp.radius);
+	vis->z1 = tr_y - FIXED_TO_FLOAT(interp.radius);
+	vis->z2 = tr_y + FIXED_TO_FLOAT(interp.radius);
+	vis->gz = FIXED_TO_FLOAT(interp.z);
+	vis->gzt = vis->gz + FIXED_TO_FLOAT(interp.height);
+	vis->mobj = thing;
+
+	vis->precip = false;
+	vis->bbox = true;
+}
+
 // ==========================================================================
 // Sky dome rendering, ported from PrBoom+
 // ==========================================================================
@@ -5788,6 +5909,8 @@ static void HWR_DrawSkyBackground(player_t *player)
 			fixed_t rol = AngleFixed(player->viewrollangle);
 			dometransform.rollangle = FIXED_TO_FLOAT(rol);
 			dometransform.roll = true;
+			dometransform.rollx = 1.0f;
+			dometransform.rollz = 0.0f;
 		}
 		dometransform.splitscreen = splitscreen;
 
@@ -6066,6 +6189,8 @@ void HWR_RenderSkyboxView(INT32 viewnumber, player_t *player)
 		fixed_t rol = AngleFixed(player->viewrollangle);
 		atransform.rollangle = FIXED_TO_FLOAT(rol);
 		atransform.roll = true;
+		atransform.rollx = 1.0f;
+		atransform.rollz = 0.0f;
 	}
 	atransform.splitscreen = splitscreen;
 
@@ -6280,6 +6405,8 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 		fixed_t rol = AngleFixed(player->viewrollangle);
 		atransform.rollangle = FIXED_TO_FLOAT(rol);
 		atransform.roll = true;
+		atransform.rollx = 1.0f;
+		atransform.rollz = 0.0f;
 	}
 	atransform.splitscreen = splitscreen;
 
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index 4a1b412fc6539e64b86ac63a9b20288ac506b52f..9450ca2c56ed56d52916f587136e2e9352f3f4b4 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -115,6 +115,7 @@ extern float gl_viewwindowx, gl_basewindowcentery;
 // BP: big hack for a test in lighting ref : 1249753487AB
 extern fixed_t *hwbbox;
 extern FTransform atransform;
+extern float gl_viewsin, gl_viewcos;
 
 
 // Render stats
diff --git a/src/hardware/hw_md2.c b/src/hardware/hw_md2.c
index 6b1d0c6fcbd95376c420739b2e4fb763e07644cc..0f834213592e041c866ca05a90d2599476c81c76 100644
--- a/src/hardware/hw_md2.c
+++ b/src/hardware/hw_md2.c
@@ -486,7 +486,7 @@ void HWR_InitModels(void)
 	size_t i;
 	INT32 s;
 	FILE *f;
-	char name[24], filename[32];
+	char name[26], filename[32];
 	float scale, offset;
 	size_t prefixlen;
 
@@ -585,7 +585,7 @@ modelfound:
 void HWR_AddPlayerModel(int skin) // For skins that were added after startup
 {
 	FILE *f;
-	char name[24], filename[32];
+	char name[26], filename[32];
 	float scale, offset;
 	size_t prefixlen;
 
@@ -644,7 +644,7 @@ void HWR_AddSpriteModel(size_t spritenum) // For sprites that were added after s
 	// name[24] is used to check for names in the models.dat file that match with sprites or player skins
 	// sprite names are always 4 characters long, and names is for player skins can be up to 19 characters long
 	// PLAYERMODELPREFIX is 6 characters long
-	char name[24], filename[32];
+	char name[26], filename[32];
 	float scale, offset;
 
 	if (nomd2s)
@@ -1146,7 +1146,7 @@ static void HWR_GetBlendedTexture(patch_t *patch, patch_t *blendpatch, INT32 ski
 static boolean HWR_AllowModel(mobj_t *mobj)
 {
 	// Signpost overlay. Not needed.
-	if (mobj->state-states == S_PLAY_SIGN)
+	if (mobj->sprite2 == SPR2_SIGN || mobj->state-states == S_PLAY_SIGN)
 		return false;
 
 	// Otherwise, render the model.
@@ -1346,10 +1346,7 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 		const UINT8 hflip = (UINT8)(!(spr->mobj->mirrored) != !R_ThingHorizontallyFlipped(spr->mobj));
 		spritedef_t *sprdef;
 		spriteframe_t *sprframe;
-		spriteinfo_t *sprinfo;
-		angle_t ang;
 		INT32 mod;
-		float finalscale;
 		interpmobjstate_t interp;
 
 		if (R_UsingFrameInterpolation() && !paused)
@@ -1388,12 +1385,10 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 		{
 			md2 = &md2_playermodels[(skin_t*)spr->mobj->skin-skins];
 			md2->skin = (skin_t*)spr->mobj->skin-skins;
-			sprinfo = &((skin_t *)spr->mobj->skin)->sprinfo[spr->mobj->sprite2];
 		}
 		else
 		{
 			md2 = &md2_models[spr->mobj->sprite];
-			sprinfo = &spriteinfo[spr->mobj->sprite];
 		}
 
 		// texture loading before model init, so it knows if sprite graphics are used, which
@@ -1455,7 +1450,6 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 		}
 
 		//HWD.pfnSetBlend(blend); // This seems to actually break translucency?
-		finalscale = md2->scale;
 		//Hurdler: arf, I don't like that implementation at all... too much crappy
 
 		if (gpatch && hwrPatch && hwrPatch->mipmap->format) // else if meant that if a texture couldn't be loaded, it would just end up using something else's texture
@@ -1591,7 +1585,7 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 		p.y = FIXED_TO_FLOAT(interp.y)+md2->offset;
 
 		if (flip)
-			p.z = FIXED_TO_FLOAT(interp.z + spr->mobj->height);
+			p.z = FIXED_TO_FLOAT(interp.z + interp.height);
 		else
 			p.z = FIXED_TO_FLOAT(interp.z);
 
@@ -1614,61 +1608,56 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 			p.angley = FIXED_TO_FLOAT(anglef);
 		}
 
-		p.rollangle = 0.0f;
-		p.rollflip = 1;
-		p.rotaxis = 0;
-		if (spr->mobj->rollangle)
 		{
-			fixed_t anglef = AngleFixed(spr->mobj->rollangle);
-			p.rollangle = FIXED_TO_FLOAT(anglef);
-			p.roll = true;
-
-			// rotation pivot
-			p.centerx = FIXED_TO_FLOAT(spr->mobj->radius/2);
-			p.centery = FIXED_TO_FLOAT(spr->mobj->height/(flip ? -2 : 2));
-
-			// rotation axis
-			if (sprinfo->available)
-				p.rotaxis = (UINT8)(sprinfo->pivot[(spr->mobj->frame & FF_FRAMEMASK)].rotaxis);
-
-			// for NiGHTS specifically but should work everywhere else
-			ang = R_PointToAngle (interp.x, interp.y) - interp.angle;
-			if ((sprframe->rotate & SRF_RIGHT) && (ang < ANGLE_180)) // See from right
-				p.rollflip = 1;
-			else if ((sprframe->rotate & SRF_LEFT) && (ang >= ANGLE_180)) // See from left
-				p.rollflip = -1;
-
-			if (flip)
-				p.rollflip *= -1;
-		}
+			fixed_t anglef = AngleFixed(R_ModelRotationAngle(&interp));
 
-		p.anglex = 0.0f;
+			p.rollangle = 0.0f;
 
-#ifdef USE_FTRANSFORM_ANGLEZ
-		// Slope rotation from Kart
-		p.anglez = 0.0f;
-		if (spr->mobj->standingslope)
-		{
-			fixed_t tempz = spr->mobj->standingslope->normal.z;
-			fixed_t tempy = spr->mobj->standingslope->normal.y;
-			fixed_t tempx = spr->mobj->standingslope->normal.x;
-			fixed_t tempangle = AngleFixed(R_PointToAngle2(0, 0, FixedSqrt(FixedMul(tempy, tempy) + FixedMul(tempz, tempz)), tempx));
-			p.anglez = FIXED_TO_FLOAT(tempangle);
-			tempangle = -AngleFixed(R_PointToAngle2(0, 0, tempz, tempy));
-			p.anglex = FIXED_TO_FLOAT(tempangle);
+			if (anglef)
+			{
+				fixed_t camAngleDiff = AngleFixed(viewangle) - FLOAT_TO_FIXED(p.angley); // dumb reconversion back, I know
+
+				p.rollangle = FIXED_TO_FLOAT(anglef);
+				p.roll = true;
+
+				// rotation pivot
+				p.centerx = FIXED_TO_FLOAT(interp.radius / 2);
+				p.centery = FIXED_TO_FLOAT(interp.height / 2);
+
+				// rotation axes relative to camera
+				p.rollx = FIXED_TO_FLOAT(FINECOSINE(FixedAngle(camAngleDiff) >> ANGLETOFINESHIFT));
+				p.rollz = FIXED_TO_FLOAT(FINESINE(FixedAngle(camAngleDiff) >> ANGLETOFINESHIFT));
+			}
 		}
-#endif
 
-		// SRB2CBTODO: MD2 scaling support
-		finalscale *= FIXED_TO_FLOAT(spr->mobj->scale);
+#if 0
+		p.anglez = FIXED_TO_FLOAT(AngleFixed(interp.pitch));
+		p.anglex = FIXED_TO_FLOAT(AngleFixed(interp.roll));
+#else
+		p.anglez = 0.f;
+		p.anglex = 0.f;
+#endif
 
 		p.flip = atransform.flip;
-#ifdef USE_FTRANSFORM_MIRROR
-		p.mirror = atransform.mirror; // from Kart
-#endif
+		p.mirror = atransform.mirror;
 
 		HWD.pfnSetShader(SHADER_MODEL);	// model shader
-		HWD.pfnDrawModel(md2->model, frame, durs, tics, nextFrame, &p, finalscale, flip, hflip, &Surf);
+		{
+			float this_scale = FIXED_TO_FLOAT(interp.scale);
+
+			float xs = this_scale * FIXED_TO_FLOAT(interp.spritexscale);
+			float ys = this_scale * FIXED_TO_FLOAT(interp.spriteyscale);
+
+			float ox = xs * FIXED_TO_FLOAT(interp.spritexoffset);
+			float oy = ys * FIXED_TO_FLOAT(interp.spriteyoffset);
+
+			// offset perpendicular to the camera angle
+			p.x -= ox * gl_viewsin;
+			p.y += ox * gl_viewcos;
+			p.z += oy;
+
+			HWD.pfnDrawModel(md2->model, frame, durs, tics, nextFrame, &p, md2->scale * xs, md2->scale * ys, flip, hflip, &Surf);
+		}
 	}
 
 	return true;
diff --git a/src/hardware/hw_model.c b/src/hardware/hw_model.c
index 4ed03744bfe072db597c0e2d120fd72ccd77a6ca..4b6bce6f7b55834167d496a361c3936cd22cc8ac 100644
--- a/src/hardware/hw_model.c
+++ b/src/hardware/hw_model.c
@@ -15,7 +15,7 @@
 #include "hw_md2load.h"
 #include "hw_md3load.h"
 #include "hw_md2.h"
-#include "u_list.h"
+#include "../u_list.h"
 #include <string.h>
 
 static float PI = (3.1415926535897932384626433832795f);
@@ -672,6 +672,9 @@ void GeneratePolygonNormals(model_t *model, int ztag)
 
 			for (k = 0; k < mesh->numTriangles; k++)
 			{
+				/// TODO: normalize vectors
+				(void)vertices;
+				(void)polyNormals;
 //				Vector::Normal(vertices, polyNormals);
 				vertices += 3 * 3;
 				polyNormals++;
diff --git a/src/hardware/r_opengl/r_opengl.c b/src/hardware/r_opengl/r_opengl.c
index 569ddfee84032f9c88650216fa5f2127ce54df33..71cb5ca70375629ee06c7943e0c5be881555a0c1 100644
--- a/src/hardware/r_opengl/r_opengl.c
+++ b/src/hardware/r_opengl/r_opengl.c
@@ -1030,6 +1030,12 @@ EXPORT void HWRAPI(LoadCustomShader) (int number, char *code, size_t size, boole
 EXPORT void HWRAPI(SetShader) (int type)
 {
 #ifdef GL_SHADERS
+	if (type == SHADER_NONE)
+	{
+		UnSetShader();
+		return;
+	}
+
 	if (gl_allowshaders != HWD_SHADEROPTION_OFF)
 	{
 		gl_shader_t *shader = gl_shaderstate.current;
@@ -2290,7 +2296,7 @@ EXPORT void HWRAPI(DrawPolygon) (FSurfaceInfo *pSurf, FOutVector *pOutVerts, FUI
 
 	pglVertexPointer(3, GL_FLOAT, sizeof(FOutVector), &pOutVerts[0].x);
 	pglTexCoordPointer(2, GL_FLOAT, sizeof(FOutVector), &pOutVerts[0].s);
-	pglDrawArrays(GL_TRIANGLE_FAN, 0, iNumPts);
+	pglDrawArrays(PolyFlags & PF_WireFrame ? GL_LINES : GL_TRIANGLE_FAN, 0, iNumPts);
 
 	if (PolyFlags & PF_RemoveYWrap)
 		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
@@ -2673,7 +2679,7 @@ EXPORT void HWRAPI(CreateModelVBOs) (model_t *model)
 
 #define BUFFER_OFFSET(i) ((void*)(i))
 
-static void DrawModelEx(model_t *model, INT32 frameIndex, float duration, float tics, INT32 nextFrameIndex, FTransform *pos, float scale, UINT8 flipped, UINT8 hflipped, FSurfaceInfo *Surface)
+static void DrawModelEx(model_t *model, INT32 frameIndex, float duration, float tics, INT32 nextFrameIndex, FTransform *pos, float hscale, float vscale, UINT8 flipped, UINT8 hflipped, FSurfaceInfo *Surface)
 {
 	static GLRGBAFloat poly = {0,0,0,0};
 	static GLRGBAFloat tint = {0,0,0,0};
@@ -2697,10 +2703,11 @@ static void DrawModelEx(model_t *model, INT32 frameIndex, float duration, float
 #endif
 
 	// Affect input model scaling
-	scale *= 0.5f;
-	scalex = scale;
-	scaley = scale;
-	scalez = scale;
+	hscale *= 0.5f;
+	vscale *= 0.5f;
+	scalex = hscale;
+	scaley = vscale;
+	scalez = hscale;
 
 	if (duration > 0.0 && tics >= 0.0) // don't interpolate if instantaneous or infinite in length
 	{
@@ -2776,7 +2783,6 @@ static void DrawModelEx(model_t *model, INT32 frameIndex, float duration, float
 	pglEnable(GL_CULL_FACE);
 	pglEnable(GL_NORMALIZE);
 
-#ifdef USE_FTRANSFORM_MIRROR
 	// flipped is if the object is vertically flipped
 	// hflipped is if the object is horizontally flipped
 	// pos->flip is if the screen is flipped vertically
@@ -2789,17 +2795,6 @@ static void DrawModelEx(model_t *model, INT32 frameIndex, float duration, float
 		else
 			pglCullFace(GL_BACK);
 	}
-#else
-	// pos->flip is if the screen is flipped too
-	if (flipped ^ hflipped ^ pos->flip) // If one or three of these are active, but not two, invert the model's culling
-	{
-		pglCullFace(GL_FRONT);
-	}
-	else
-	{
-		pglCullFace(GL_BACK);
-	}
-#endif
 
 	pglPushMatrix(); // should be the same as glLoadIdentity
 	//Hurdler: now it seems to work
@@ -2809,22 +2804,14 @@ static void DrawModelEx(model_t *model, INT32 frameIndex, float duration, float
 	if (hflipped)
 		scalez = -scalez;
 
-#ifdef USE_FTRANSFORM_ANGLEZ
-	pglRotatef(pos->anglez, 0.0f, 0.0f, -1.0f); // rotate by slope from Kart
-#endif
-	pglRotatef(pos->angley, 0.0f, -1.0f, 0.0f);
+	pglRotatef(pos->anglez, 0.0f, 0.0f, -1.0f);
 	pglRotatef(pos->anglex, 1.0f, 0.0f, 0.0f);
+	pglRotatef(pos->angley, 0.0f, -1.0f, 0.0f);
 
 	if (pos->roll)
 	{
-		float roll = (1.0f * pos->rollflip);
 		pglTranslatef(pos->centerx, pos->centery, 0);
-		if (pos->rotaxis == 2) // Z
-			pglRotatef(pos->rollangle, 0.0f, 0.0f, roll);
-		else if (pos->rotaxis == 1) // Y
-			pglRotatef(pos->rollangle, 0.0f, roll, 0.0f);
-		else // X
-			pglRotatef(pos->rollangle, roll, 0.0f, 0.0f);
+		pglRotatef(pos->rollangle, pos->rollx, 0.0f, pos->rollz);
 		pglTranslatef(-pos->centerx, -pos->centery, 0);
 	}
 
@@ -2978,9 +2965,9 @@ static void DrawModelEx(model_t *model, INT32 frameIndex, float duration, float
 // -----------------+
 // HWRAPI DrawModel : Draw a model
 // -----------------+
-EXPORT void HWRAPI(DrawModel) (model_t *model, INT32 frameIndex, float duration, float tics, INT32 nextFrameIndex, FTransform *pos, float scale, UINT8 flipped, UINT8 hflipped, FSurfaceInfo *Surface)
+EXPORT void HWRAPI(DrawModel) (model_t *model, INT32 frameIndex, float duration, float tics, INT32 nextFrameIndex, FTransform *pos, float hscale, float vscale, UINT8 flipped, UINT8 hflipped, FSurfaceInfo *Surface)
 {
-	DrawModelEx(model, frameIndex, duration, tics, nextFrameIndex, pos, scale, flipped, hflipped, Surface);
+	DrawModelEx(model, frameIndex, duration, tics, nextFrameIndex, pos, hscale, vscale, flipped, hflipped, Surface);
 }
 
 // -----------------+
@@ -2997,13 +2984,9 @@ EXPORT void HWRAPI(SetTransform) (FTransform *stransform)
 	if (stransform)
 	{
 		used_fov = stransform->fovxangle;
-#ifdef USE_FTRANSFORM_MIRROR
-		// mirroring from Kart
 		if (stransform->mirror)
 			pglScalef(-stransform->scalex, stransform->scaley, -stransform->scalez);
-		else
-#endif
-		if (stransform->flip)
+		else if (stransform->flip)
 			pglScalef(stransform->scalex, -stransform->scaley, -stransform->scalez);
 		else
 			pglScalef(stransform->scalex, stransform->scaley, -stransform->scalez);
diff --git a/src/hu_stuff.c b/src/hu_stuff.c
index 55ebe0d4d9e3380fcc45708f5c3e8061644de5cb..eb2915d801caa69df14f912e772238a57169d3b7 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"
@@ -175,14 +177,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 +327,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 +361,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 +408,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 +432,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.
@@ -865,8 +853,6 @@ static void Got_Saycmd(UINT8 **p, INT32 playernum)
 #endif
 }
 
-#endif
-
 //
 //
 void HU_Ticker(void)
@@ -882,7 +868,6 @@ void HU_Ticker(void)
 	else
 		hu_showscores = false;
 
-#ifndef NONET
 	if (chat_on)
 	{
 		// count down the scroll timer.
@@ -910,7 +895,6 @@ void HU_Ticker(void)
 				HU_removeChatText_Mini();
 		}
 	}
-#endif
 
 	if (cechotimer > 0) --cechotimer;
 
@@ -918,8 +902,6 @@ void HU_Ticker(void)
 		resynch_ticker++;
 }
 
-#ifndef NONET
-
 static boolean teamtalk = false;
 static boolean justscrolleddown;
 static boolean justscrolledup;
@@ -1027,8 +1009,6 @@ static void HU_sendChatMessage(void)
 	}
 }
 
-#endif
-
 void HU_clearChatChars(void)
 {
 	memset(w_chat, '\0', sizeof(w_chat));
@@ -1043,9 +1023,7 @@ void HU_clearChatChars(void)
 //
 boolean HU_Responder(event_t *ev)
 {
-#ifndef NONET
 	INT32 c=0;
-#endif
 
 	if (ev->type != ev_keydown)
 		return false;
@@ -1072,7 +1050,6 @@ 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)
@@ -1222,7 +1199,6 @@ boolean HU_Responder(event_t *ev)
 
 		return true;
 	}
-#endif
 
 	return false;
 }
@@ -1232,8 +1208,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 +1787,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 +1926,6 @@ static void HU_DrawDemoInfo(void)
 //
 void HU_Drawer(void)
 {
-#ifndef NONET
 	// draw chat string plus cursor
 	if (chat_on)
 	{
@@ -1970,7 +1942,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();
@@ -2025,7 +1996,7 @@ void HU_Drawer(void)
 		V_DrawCenteredString(BASEVIDWIDTH/2, 180, V_YELLOWMAP | V_ALLOWLOWERCASE, resynch_text);
 	}
 
-	if (modeattacking && pausedelay > 0 && !pausebreakkey)
+	if (modeattacking && pausedelay > 0 && !(pausebreakkey || cv_instantretry.value))
 	{
 		INT32 strength = ((pausedelay - 1 - NEWTICRATE/2)*10)/(NEWTICRATE/3);
 		INT32 y = hudinfo[HUD_LIVES].y - 13;
@@ -2862,18 +2833,6 @@ static void HU_DrawRankings(void)
 			V_DrawCenteredString(256, 16, 0, va("%d", cv_pointlimit.value));
 		}
 	}
-	else if (gametyperankings[gametype] == GT_COOP)
-	{
-		INT32 totalscore = 0;
-		for (i = 0; i < MAXPLAYERS; i++)
-		{
-			if (playeringame[i])
-				totalscore += players[i].score;
-		}
-
-		V_DrawCenteredString(256, 8, 0, "TOTAL SCORE");
-		V_DrawCenteredString(256, 16, 0, va("%u", totalscore));
-	}
 	else
 	{
 		if (circuitmap)
@@ -2996,7 +2955,7 @@ static void HU_DrawCoopOverlay(void)
 
 	if (LUA_HudEnabled(hud_tabemblems))
 	{
-		V_DrawString(160, 144, 0, va("- %d/%d", M_CountEmblems(), numemblems+numextraemblems));
+		V_DrawString(160, 144, 0, va("- %d/%d", M_CountEmblems(clientGamedata), numemblems+numextraemblems));
 		V_DrawScaledPatch(128, 144 - emblemicon->height/4, 0, emblemicon);
 	}
 
@@ -3029,6 +2988,15 @@ static void HU_DrawNetplayCoopOverlay(void)
 		V_DrawSmallScaledPatch(148, 6, 0, tokenicon);
 	}
 
+	if (G_CoopGametype() && LUA_HudEnabled(hud_tabemblems))
+	{
+		V_DrawCenteredString(256, 14, 0, "/");
+		V_DrawString(256 + 4, 14, 0, va("%d", numemblems + numextraemblems));
+		V_DrawRightAlignedString(256 - 4, 14, 0, va("%d", M_CountEmblems(clientGamedata)));
+
+		V_DrawSmallScaledPatch(256 - (emblemicon->width / 4), 6, 0, emblemicon);
+	}
+
 	if (!LUA_HudEnabled(hud_coopemeralds))
 		return;
 
diff --git a/src/i_system.h b/src/i_system.h
index deea9f8a8de8b0a5128336a03c4ca4d6069b0867..834dd4091487b295fd1faed9d11018e498d79b12 100644
--- a/src/i_system.h
+++ b/src/i_system.h
@@ -49,6 +49,10 @@ size_t I_GetFreeMem(size_t *total);
   */
 precise_t I_GetPreciseTime(void);
 
+/**	\brief	Fills a buffer with random data, returns amount of data obtained.
+  */
+size_t I_GetRandomBytes(char *destination, size_t count);
+
 /** \brief  Get the precision of precise_t in units per second. Invocations of
             this function for the program's duration MUST return the same value.
   */
diff --git a/src/i_time.c b/src/i_time.c
index 2a22503f1b8b5c70a8145d2c45696aa2c50f0922..fae26abede3f646252abd7c04ff826bbf89ae2ba 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"
 
diff --git a/src/info.c b/src/info.c
index 09452a74f54b04dc2a1c702ea515b793e25b311b..36389d849a38e21064e0f4e94b803cccfc9f7ef0 100644
--- a/src/info.c
+++ b/src/info.c
@@ -7194,7 +7194,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_cgot,       // deathsound
 		EMERALD1,       // speed
 		16*FRACUNIT,    // radius
-		32*FRACUNIT,    // height
+		24*FRACUNIT,    // height
 		0,              // display offset
 		16,             // mass
 		0,              // damage
@@ -7220,7 +7220,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_cgot,       // deathsound
 		EMERALD2,       // speed
 		16*FRACUNIT,    // radius
-		32*FRACUNIT,    // height
+		24*FRACUNIT,    // height
 		0,              // display offset
 		16,             // mass
 		0,              // damage
@@ -7246,7 +7246,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_cgot,       // deathsound
 		EMERALD3,       // speed
 		16*FRACUNIT,    // radius
-		32*FRACUNIT,    // height
+		24*FRACUNIT,    // height
 		0,              // display offset
 		16,             // mass
 		0,              // damage
@@ -7272,7 +7272,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_cgot,       // deathsound
 		EMERALD4,       // speed
 		16*FRACUNIT,    // radius
-		32*FRACUNIT,    // height
+		24*FRACUNIT,    // height
 		0,              // display offset
 		16,             // mass
 		0,              // damage
@@ -7298,7 +7298,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_cgot,       // deathsound
 		EMERALD5,       // speed
 		16*FRACUNIT,    // radius
-		32*FRACUNIT,    // height
+		24*FRACUNIT,    // height
 		0,              // display offset
 		16,             // mass
 		0,              // damage
@@ -7324,7 +7324,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_cgot,       // deathsound
 		EMERALD6,       // speed
 		16*FRACUNIT,    // radius
-		32*FRACUNIT,    // height
+		24*FRACUNIT,    // height
 		0,              // display offset
 		16,             // mass
 		0,              // damage
@@ -7350,7 +7350,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_cgot,       // deathsound
 		EMERALD7,       // speed
 		16*FRACUNIT,    // radius
-		32*FRACUNIT,    // height
+		24*FRACUNIT,    // height
 		0,              // display offset
 		16,             // mass
 		0,              // damage
@@ -18344,7 +18344,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL,         // xdeathstate
 		sfx_itemup,     // deathsound
 		60*FRACUNIT,    // speed
-		24*FRACUNIT,    // radius
+		16*FRACUNIT,    // radius
 		24*FRACUNIT,    // height
 		0,              // display offset
 		pw_bouncering,  // mass
@@ -18371,7 +18371,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL,         // xdeathstate
 		sfx_itemup,     // deathsound
 		60*FRACUNIT,    // speed
-		24*FRACUNIT,    // radius
+		16*FRACUNIT,    // radius
 		24*FRACUNIT,    // height
 		0,              // display offset
 		pw_railring,    // mass
@@ -18425,7 +18425,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL,         // xdeathstate
 		sfx_itemup,     // deathsound
 		60*FRACUNIT,    // speed
-		24*FRACUNIT,    // radius
+		16*FRACUNIT,    // radius
 		24*FRACUNIT,    // height
 		0,              // display offset
 		pw_automaticring, // mass
@@ -18452,7 +18452,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL,         // xdeathstate
 		sfx_itemup,     // deathsound
 		60*FRACUNIT,    // speed
-		24*FRACUNIT,    // radius
+		16*FRACUNIT,    // radius
 		24*FRACUNIT,    // height
 		0,              // display offset
 		pw_explosionring, // mass
@@ -18479,7 +18479,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL,         // xdeathstate
 		sfx_itemup,     // deathsound
 		60*FRACUNIT,    // speed
-		24*FRACUNIT,    // radius
+		16*FRACUNIT,    // radius
 		24*FRACUNIT,    // height
 		0,              // display offset
 		pw_scatterring, // mass
@@ -18506,7 +18506,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		S_NULL,         // xdeathstate
 		sfx_itemup,     // deathsound
 		60*FRACUNIT,    // speed
-		24*FRACUNIT,    // radius
+		16*FRACUNIT,    // radius
 		24*FRACUNIT,    // height
 		0,              // display offset
 		pw_grenadering, // mass
@@ -18535,7 +18535,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_ncitem,     // deathsound
 		60*FRACUNIT,    // speed
 		24*FRACUNIT,    // radius
-		24*FRACUNIT,    // height
+		40*FRACUNIT,    // height
 		0,              // display offset
 		pw_bouncering,  // mass
 		2*TICRATE,      // damage
@@ -18562,7 +18562,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_ncitem,     // deathsound
 		60*FRACUNIT,    // speed
 		24*FRACUNIT,    // radius
-		24*FRACUNIT,    // height
+		40*FRACUNIT,    // height
 		0,              // display offset
 		pw_railring,    // mass
 		2*TICRATE,      // damage
@@ -18589,7 +18589,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_ncitem,     // deathsound
 		60*FRACUNIT,    // speed
 		24*FRACUNIT,    // radius
-		24*FRACUNIT,    // height
+		40*FRACUNIT,    // height
 		0,              // display offset
 		pw_automaticring, // mass
 		2*TICRATE,      // damage
@@ -18616,7 +18616,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_ncitem,     // deathsound
 		60*FRACUNIT,    // speed
 		24*FRACUNIT,    // radius
-		24*FRACUNIT,    // height
+		40*FRACUNIT,    // height
 		0,              // display offset
 		pw_explosionring, // mass
 		2*TICRATE,      // damage
@@ -18643,7 +18643,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_ncitem,     // deathsound
 		60*FRACUNIT,    // speed
 		24*FRACUNIT,    // radius
-		24*FRACUNIT,    // height
+		40*FRACUNIT,    // height
 		0,              // display offset
 		pw_scatterring, // mass
 		2*TICRATE,      // damage
@@ -18670,7 +18670,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
 		sfx_ncitem,     // deathsound
 		60*FRACUNIT,    // speed
 		24*FRACUNIT,    // radius
-		24*FRACUNIT,    // height
+		40*FRACUNIT,    // height
 		0,              // display offset
 		pw_grenadering, // mass
 		2*TICRATE,      // damage
@@ -21584,68 +21584,113 @@ skincolor_t skincolors[MAXSKINCOLORS] = {
 	{"Black",  {0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1b, 0x1b, 0x1c, 0x1d, 0x1d, 0x1e, 0x1e, 0x1f, 0x1f}, SKINCOLOR_WHITE,  7,  V_GRAYMAP, true}, // SKINCOLOR_BLACK
 
 	// Desaturated
-	{"Aether",   {0x00, 0x00, 0x01, 0x02, 0x02, 0x03, 0x91, 0x91, 0x91, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xaf}, SKINCOLOR_GREY,    15, 0,           true}, // SKINCOLOR_AETHER
-	{"Slate",    {0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0xaa, 0xaa, 0xaa, 0xab, 0xac, 0xac, 0xad, 0xad, 0xae, 0xaf}, SKINCOLOR_SILVER,  12, 0,           true}, // SKINCOLOR_SLATE
-	{"Bluebell", {0x90, 0x91, 0x92, 0x93, 0x94, 0x94, 0x95, 0xac, 0xac, 0xad, 0xad, 0xa8, 0xa8, 0xa9, 0xfd, 0xfe}, SKINCOLOR_COPPER,  4,  V_BLUEMAP,   true}, // SKINCOLOR_BLUEBELL
-	{"Pink",     {0xd0, 0xd0, 0xd1, 0xd1, 0xd2, 0xd2, 0xd3, 0xd3, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0x2b, 0x2c, 0x2e}, SKINCOLOR_AZURE,   9,  V_REDMAP,    true}, // SKINCOLOR_PINK
-	{"Yogurt",   {0xd0, 0x30, 0xd8, 0xd9, 0xda, 0xdb, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe3, 0xe6, 0xe8, 0xe9}, SKINCOLOR_RUST,    7,  V_BROWNMAP,  true}, // SKINCOLOR_YOGURT
-	{"Brown",    {0xdf, 0xe0, 0xe1, 0xe2, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef}, SKINCOLOR_TAN,     2,  V_BROWNMAP,  true}, // SKINCOLOR_BROWN
-	{"Bronze",   {0xde, 0xe0, 0xe1, 0xe4, 0xe7, 0xe9, 0xeb, 0xec, 0xed, 0xed, 0xed, 0x19, 0x19, 0x1b, 0x1d, 0x1e}, SKINCOLOR_KETCHUP, 0,  V_BROWNMAP,  true}, // SKINCOLOR_BRONZE
-	{"Tan",      {0x51, 0x51, 0x54, 0x54, 0x55, 0x55, 0x56, 0x56, 0x56, 0x57, 0xf5, 0xf5, 0xf9, 0xf9, 0xed, 0xed}, SKINCOLOR_BROWN,   12, V_BROWNMAP,  true}, // SKINCOLOR_TAN
-	{"Beige",    {0x54, 0x55, 0x56, 0x56, 0xf2, 0xf3, 0xf3, 0xf4, 0xf5, 0xf6, 0xf8, 0xf9, 0xfa, 0xfb, 0xed, 0xed}, SKINCOLOR_MOSS,    5,  V_BROWNMAP,  true}, // SKINCOLOR_BEIGE
-	{"Moss",     {0x58, 0x58, 0x59, 0x59, 0x5a, 0x5a, 0x5b, 0x5b, 0x5b, 0x5c, 0x5d, 0x5d, 0x5e, 0x5e, 0x5f, 0x5f}, SKINCOLOR_BEIGE,   13, V_GREENMAP,  true}, // SKINCOLOR_MOSS
-	{"Azure",    {0x90, 0x90, 0x91, 0x91, 0xaa, 0xaa, 0xab, 0xab, 0xab, 0xac, 0xad, 0xad, 0xae, 0xae, 0xaf, 0xaf}, SKINCOLOR_PINK,    5,  V_AZUREMAP,  true}, // SKINCOLOR_AZURE
-	{"Lavender", {0xc0, 0xc0, 0xc1, 0xc1, 0xc2, 0xc2, 0xc3, 0xc3, 0xc3, 0xc4, 0xc5, 0xc5, 0xc6, 0xc6, 0xc7, 0xc7}, SKINCOLOR_GOLD,    4,  V_PURPLEMAP, true}, // SKINCOLOR_LAVENDER
+	{"Aether",    {0x00, 0x00, 0x01, 0x01, 0x90, 0x90, 0x91, 0x91, 0x92, 0xaa, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xae}, SKINCOLOR_GREY,      15, 0,           true}, // SKINCOLOR_AETHER
+	{"Slate",     {0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0xaa, 0xaa, 0xaa, 0xab, 0xac, 0xac, 0xad, 0xad, 0xae, 0xaf}, SKINCOLOR_SILVER,    12, 0,           true}, // SKINCOLOR_SLATE
+	{"Moonstone", {   0,    4,    8,    9,   11,   12,   14,   15,  171,  172,  173,  174,  175,   27,   29,   31}, SKINCOLOR_TOPAZ,     15, V_GRAYMAP,   true}, // SKINCOLOR_MOONSTONE
+	{"Bluebell",  {0x90, 0x91, 0x92, 0x93, 0x94, 0x94, 0x95, 0xac, 0xac, 0xad, 0xad, 0xa8, 0xa8, 0xa9, 0xfd, 0xfe}, SKINCOLOR_COPPER,    4,  V_BLUEMAP,   true}, // SKINCOLOR_BLUEBELL
+	{"Pink",      {0xd0, 0xd0, 0xd1, 0xd1, 0xd2, 0xd2, 0xd3, 0xd3, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0x2b, 0x2c, 0x2e}, SKINCOLOR_AZURE,     9,  V_REDMAP,    true}, // SKINCOLOR_PINK
+	{"Rosewood",  { 209,  210,  211,  212,  213,  214,  228,  230,  232,  234,  235,  237,   26,   27,   28,   29}, SKINCOLOR_SEPIA,     5,  V_BROWNMAP,  true}, // SKINCOLOR_ROSEWOOD
+	{"Yogurt",    {0xd0, 0x30, 0xd8, 0xd9, 0xda, 0xdb, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe3, 0xe6, 0xe8, 0xe9}, SKINCOLOR_RUST,      7,  V_BROWNMAP,  true}, // SKINCOLOR_YOGURT
+	{"Latte",     {  48,  217,  219,  221,  223,  224,  226,  228,   68,   69,   70,   70,   44,   45,   46,   47}, SKINCOLOR_BOTTLE,    12, V_BROWNMAP,  true}, // SKINCOLOR_LATTE
+	{"Brown",     {0xdf, 0xe0, 0xe1, 0xe2, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef}, SKINCOLOR_TAN,       2,  V_BROWNMAP,  true}, // SKINCOLOR_BROWN
+	{"Boulder",   {0xde, 0xe0, 0xe1, 0xe4, 0xe7, 0xe9, 0xeb, 0xec, 0xed, 0xed, 0xed, 0x19, 0x19, 0x1b, 0x1d, 0x1e}, SKINCOLOR_KETCHUP,   0,  V_BROWNMAP,  true}, // SKINCOLOR_BOULDER
+	{"Bronze",    {  82,   84,   50,   51,  223,  228,  230,  232,  234,  236,  237,  238,  239,  239,   30,   31}, SKINCOLOR_VOLCANIC,  9,  V_BROWNMAP,  true}, // SKINCOLOR_BRONZE
+	{"Sepia",     {  88,   84,   85,   86,  224,  226,  228,  230,  232,  235,  236,  237,  238,  239,   28,   28}, SKINCOLOR_ROSEWOOD,  5,  V_BROWNMAP,  true}, // SKINCOLOR_SEPIA
+	{"Ecru",      {  80,   83,   84,   85,   86,  242,  243,  245,  230,  232,  234,  236,  238,  239,   47,   47}, SKINCOLOR_ARCTIC,    12, V_BROWNMAP,  true}, // SKINCOLOR_ECRU
+	{"Tan",       {0x51, 0x51, 0x54, 0x54, 0x55, 0x55, 0x56, 0x56, 0x56, 0x57, 0xf5, 0xf5, 0xf9, 0xf9, 0xed, 0xed}, SKINCOLOR_BROWN,     12, V_BROWNMAP,  true}, // SKINCOLOR_TAN
+	{"Beige",     {0x54, 0x55, 0x56, 0x56, 0xf2, 0xf3, 0xf3, 0xf4, 0xf5, 0xf6, 0xf8, 0xf9, 0xfa, 0xfb, 0xed, 0xed}, SKINCOLOR_MOSS,      5,  V_BROWNMAP,  true}, // SKINCOLOR_BEIGE
+	{"Rosebush",  { 208,  216,  209,   85,   90,   91,   91,   92,  191,   93,   94,  107,  109,  110,  111,  111}, SKINCOLOR_EGGPLANT,  5,  V_GREENMAP,  true}, // SKINCOLOR_ROSEBUSH
+	{"Moss",      {0x58, 0x58, 0x59, 0x59, 0x5a, 0x5a, 0x5b, 0x5b, 0x5b, 0x5c, 0x5d, 0x5d, 0x5e, 0x5e, 0x5f, 0x5f}, SKINCOLOR_BEIGE,     13, V_GREENMAP,  true}, // SKINCOLOR_MOSS
+	{"Azure",     {0x90, 0x90, 0x91, 0x91, 0xaa, 0xaa, 0xab, 0xab, 0xab, 0xac, 0xad, 0xad, 0xae, 0xae, 0xaf, 0xaf}, SKINCOLOR_PINK,      5,  V_AZUREMAP,  true}, // SKINCOLOR_AZURE
+	{"Eggplant",  {   4,   8,    11,   11,   16,  195,  195,  195,  196,  186,  187,  187,  254,  254,   30,   31}, SKINCOLOR_ROSEBUSH,  5,  V_PURPLEMAP, true}, // SKINCOLOR_EGGPLANT
+	{"Lavender",  {0xc0, 0xc0, 0xc1, 0xc1, 0xc2, 0xc2, 0xc3, 0xc3, 0xc3, 0xc4, 0xc5, 0xc5, 0xc6, 0xc6, 0xc7, 0xc7}, SKINCOLOR_GOLD,      4,  V_PURPLEMAP, true}, // SKINCOLOR_LAVENDER
 
 	// Viv's vivid colours (toast 21/07/17)
+	// Tweaks & additions (Lach, Chrispy, sphere, Alice, MotorRoach & Saneko 26/10/22)
 	{"Ruby",       {0xb0, 0xb0, 0xc9, 0xca, 0xcc, 0x26, 0x27, 0x28, 0x29, 0x2a, 0xb9, 0xb9, 0xba, 0xba, 0xbb, 0xfd}, SKINCOLOR_EMERALD,    10, V_REDMAP,     true}, // SKINCOLOR_RUBY
+	{"Cherry",     { 202,  203,  204,  205,  206,   40,   41,   42,   43,   44,  186,  187,   28,   29,   30,   31}, SKINCOLOR_MIDNIGHT,   10, V_REDMAP,     true}, // SKINCOLOR_CHERRY
 	{"Salmon",     {0xd0, 0xd0, 0xd1, 0xd2, 0x20, 0x21, 0x24, 0x25, 0x26, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e}, SKINCOLOR_FOREST,     6,  V_REDMAP,     true}, // SKINCOLOR_SALMON
+	{"Pepper",     { 210,   32,   33,   34,   35,   35,   36,   37,   38,   39,   41,   43,   45,   45,   46,   47}, SKINCOLOR_MASTER,     8,  V_REDMAP,     true}, // SKINCOLOR_PEPPER
 	{"Red",        {0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x47, 0x2e, 0x2f}, SKINCOLOR_GREEN,      10, V_REDMAP,     true}, // SKINCOLOR_RED
 	{"Crimson",    {0x27, 0x27, 0x28, 0x28, 0x29, 0x2a, 0x2b, 0x2b, 0x2c, 0x2d, 0x2e, 0x2e, 0x2e, 0x2f, 0x2f, 0x1f}, SKINCOLOR_ICY,        10, V_REDMAP,     true}, // SKINCOLOR_CRIMSON
 	{"Flame",      {0x31, 0x32, 0x33, 0x36, 0x22, 0x22, 0x25, 0x25, 0x25, 0xcd, 0xcf, 0xcf, 0xc5, 0xc5, 0xc7, 0xc7}, SKINCOLOR_PURPLE,     8,  V_REDMAP,     true}, // SKINCOLOR_FLAME
-	{"Ketchup",    {0x48, 0x49, 0x40, 0x33, 0x34, 0x36, 0x22, 0x24, 0x26, 0x28, 0x2a, 0x2b, 0x2c, 0x47, 0x2e, 0x2f}, SKINCOLOR_BRONZE,     8,  V_REDMAP,     true}, // SKINCOLOR_KETCHUP
+	{"Garnet",     {   0,   83,   50,   53,   34,   35,   37,   38,   39,   40,   42,   44,   45,   46,   47,   47}, SKINCOLOR_AQUAMARINE, 6,  V_REDMAP,     true}, // SKINCOLOR_GARNET
+	{"Ketchup",    {0x48, 0x49, 0x40, 0x33, 0x34, 0x36, 0x22, 0x24, 0x26, 0x28, 0x2a, 0x2b, 0x2c, 0x47, 0x2e, 0x2f}, SKINCOLOR_BOULDER,    8,  V_REDMAP,     true}, // SKINCOLOR_KETCHUP
 	{"Peachy",     {0xd0, 0x30, 0x31, 0x31, 0x32, 0x32, 0xdc, 0xdc, 0xdc, 0xd3, 0xd4, 0xd4, 0xcc, 0xcd, 0xce, 0xcf}, SKINCOLOR_TEAL,       7,  V_ROSYMAP,    true}, // SKINCOLOR_PEACHY
 	{"Quail",      {0xd8, 0xd9, 0xdb, 0xdc, 0xde, 0xdf, 0xd5, 0xd5, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0x1d, 0x1f}, SKINCOLOR_WAVE,       5,  V_BROWNMAP,   true}, // SKINCOLOR_QUAIL
+	{"Foundation", {  80,   81,   82,   84,  219,  221,  221,  212,  213,  214,  215,  197,  186,  187,  187,   30}, SKINCOLOR_DREAM,      6,  V_ORANGEMAP,  true}, // SKINCOLOR_FOUNDATION
 	{"Sunset",     {0x51, 0x52, 0x40, 0x40, 0x34, 0x36, 0xd5, 0xd5, 0xd6, 0xd7, 0xcf, 0xcf, 0xc6, 0xc6, 0xc7, 0xfe}, SKINCOLOR_SAPPHIRE,   5,  V_ORANGEMAP,  true}, // SKINCOLOR_SUNSET
 	{"Copper",     {0x58, 0x54, 0x40, 0x34, 0x35, 0x38, 0x3a, 0x3c, 0x3d, 0x2a, 0x2b, 0x2c, 0x2c, 0xba, 0xba, 0xbb}, SKINCOLOR_BLUEBELL,   5,  V_ORANGEMAP,  true}, // SKINCOLOR_COPPER
 	{"Apricot",    {0x00, 0xd8, 0xd9, 0xda, 0xdb, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e}, SKINCOLOR_CYAN,       4,  V_ORANGEMAP,  true}, // SKINCOLOR_APRICOT
-	{"Orange",     {0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x2c}, SKINCOLOR_BLUE,       4,  V_ORANGEMAP,  true}, // SKINCOLOR_ORANGE
+	{"Orange",     {  49,   50,   51,   52,   53,   54,   55,   57,   58,   59,   60,   42,   44,   45,   46,   46}, SKINCOLOR_BLUE,       4,  V_ORANGEMAP,  true}, // SKINCOLOR_ORANGE
 	{"Rust",       {0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3c, 0x3d, 0x3d, 0x3d, 0x3f, 0x2c, 0x2d, 0x47, 0x2e, 0x2f, 0x2f}, SKINCOLOR_YOGURT,     8,  V_ORANGEMAP,  true}, // SKINCOLOR_RUST
+	{"Tangerine",  {  81,   83,   64,   64,   51,   52,   53,   54,   56,   58,   60,   61,   63,   45,   46,   47}, SKINCOLOR_OCEAN,      12, V_ORANGEMAP,  true}, // SKINCOLOR_TANGERINE
+	{"Topaz",      {   0,   81,   83,   73,   74,   74,   65,   52,   53,   54,   56,   58,   60,   42,   43,   45}, SKINCOLOR_MOONSTONE,  10, V_YELLOWMAP,  true}, // SKINCOLOR_TOPAZ
 	{"Gold",       {0x51, 0x51, 0x54, 0x54, 0x41, 0x42, 0x43, 0x43, 0x44, 0x45, 0x46, 0x3f, 0x2d, 0x2e, 0x2f, 0x2f}, SKINCOLOR_LAVENDER,   10, V_YELLOWMAP,  true}, // SKINCOLOR_GOLD
 	{"Sandy",      {0x53, 0x40, 0x41, 0x42, 0x43, 0xe6, 0xe9, 0xe9, 0xea, 0xec, 0xec, 0xc6, 0xc6, 0xc7, 0xc7, 0xfe}, SKINCOLOR_SKY,        8,  V_YELLOWMAP,  true}, // SKINCOLOR_SANDY
+	{"Goldenrod",  {   0,   80,   81,   81,   83,   73,   73,   64,   65,   66,   67,   68,   69,   62,   44,   45}, SKINCOLOR_MAJESTY,    8,  V_YELLOWMAP,  true}, // SKINCOLOR_GOLDENROD
 	{"Yellow",     {0x52, 0x53, 0x49, 0x49, 0x4a, 0x4a, 0x4b, 0x4b, 0x4b, 0x4c, 0x4d, 0x4d, 0x4e, 0x4e, 0x4f, 0xed}, SKINCOLOR_CORNFLOWER, 8,  V_YELLOWMAP,  true}, // SKINCOLOR_YELLOW
 	{"Olive",      {0x4b, 0x4b, 0x4c, 0x4c, 0x4d, 0x4e, 0xe7, 0xe7, 0xe9, 0xc5, 0xc5, 0xc6, 0xc6, 0xc7, 0xc7, 0xfd}, SKINCOLOR_DUSK,       3,  V_YELLOWMAP,  true}, // SKINCOLOR_OLIVE
+	{"Pear",       {  88,   89,  188,  189,  189,   76,   76,   67,   67,   68,   69,   70,   45,   46,   47,   47}, SKINCOLOR_MARINE,     9,  V_PERIDOTMAP, true}, // SKINCOLOR_PEAR
+	{"Lemon",      {   0,   80,   81,   83,   73,   73,   74,   74,   76,   76,  191,  191,   79,   79,  110,  111}, SKINCOLOR_FUCHSIA,    8,  V_YELLOWMAP,  true}, // SKINCOLOR_LEMON
 	{"Lime",       {0x50, 0x51, 0x52, 0x53, 0x48, 0xbc, 0xbd, 0xbe, 0xbe, 0xbf, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f}, SKINCOLOR_MAGENTA,    9,  V_PERIDOTMAP, true}, // SKINCOLOR_LIME
 	{"Peridot",    {0x58, 0x58, 0xbc, 0xbc, 0xbd, 0xbd, 0xbe, 0xbe, 0xbe, 0xbf, 0x5e, 0x5e, 0x5f, 0x5f, 0x77, 0x77}, SKINCOLOR_COBALT,     2,  V_PERIDOTMAP, true}, // SKINCOLOR_PERIDOT
 	{"Apple",      {0x49, 0x49, 0xbc, 0xbd, 0xbe, 0xbe, 0xbe, 0x67, 0x69, 0x6a, 0x6b, 0x6b, 0x6c, 0x6d, 0x6d, 0x6d}, SKINCOLOR_RASPBERRY,  13, V_PERIDOTMAP, true}, // SKINCOLOR_APPLE
+	{"Headlight",  {   0,   80,   81,   82,   73,   84,   64,   65,   91,   91,  124,  125,  126,  137,  138,  139}, SKINCOLOR_MAUVE,      8,  V_YELLOWMAP,  true}, // SKINCOLOR_HEADLIGHT
+	{"Chartreuse", {  80,   82,   72,   73,  188,  188,  113,  114,  114,  125,  126,  137,  138,  139,  253,  254}, SKINCOLOR_NOBLE,      9,  V_PERIDOTMAP, true}, // SKINCOLOR_CHARTREUSE
 	{"Green",      {0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f}, SKINCOLOR_RED,        6,  V_GREENMAP,   true}, // SKINCOLOR_GREEN
 	{"Forest",     {0x65, 0x66, 0x67, 0x68, 0x69, 0x69, 0x6a, 0x6b, 0x6b, 0x6c, 0x6d, 0x6d, 0x6e, 0x6e, 0x6e, 0x6f}, SKINCOLOR_SALMON,     9,  V_GREENMAP,   true}, // SKINCOLOR_FOREST
-	{"Emerald",    {0x70, 0x70, 0x71, 0x71, 0x72, 0x72, 0x73, 0x73, 0x73, 0x74, 0x75, 0x75, 0x76, 0x76, 0x77, 0x77}, SKINCOLOR_RUBY,       4,  V_GREENMAP,   true}, // SKINCOLOR_EMERALD
+	{"Shamrock",   {0x70, 0x70, 0x71, 0x71, 0x72, 0x72, 0x73, 0x73, 0x73, 0x74, 0x75, 0x75, 0x76, 0x76, 0x77, 0x77}, SKINCOLOR_SIBERITE,   10, V_GREENMAP,   true}, // SKINCOLOR_SHAMROCK
+	{"Jade",       { 128,  120,  121,  122,  122,  113,  114,  114,  115,  116,  117,  118,  119,  110,  111,   30}, SKINCOLOR_TAFFY,      10, V_GREENMAP,   true}, // SKINCOLOR_JADE
 	{"Mint",       {0x00, 0x00, 0x58, 0x58, 0x59, 0x62, 0x62, 0x62, 0x64, 0x67, 0x7e, 0x7e, 0x8f, 0x8f, 0x8a, 0x8a}, SKINCOLOR_VIOLET,     5,  V_GREENMAP,   true}, // SKINCOLOR_MINT
-	{"Seafoam",    {0x01, 0x58, 0x59, 0x5a, 0x7d, 0x7d, 0x7e, 0x7e, 0x7e, 0x8f, 0x8f, 0x8a, 0x8a, 0x8a, 0xfd, 0xfd}, SKINCOLOR_PLUM,       6,  V_AQUAMAP,    true}, // SKINCOLOR_SEAFOAM
+	{"Master",     {   0,   80,   88,   96,  112,  113,   99,  100,  124,  125,  126,  117,  107,  118,  119,  111}, SKINCOLOR_PEPPER,     8,  V_GREENMAP,   true}, // SKINCOLOR_MASTER
+	{"Emerald",    {  80,   96,  112,  113,  114,  114,  125,  125,  126,  126,  137,  137,  138,  138,  139,  139}, SKINCOLOR_RUBY,       9,  V_GREENMAP,   true}, // SKINCOLOR_EMERALD
+	{"Seafoam",    {0x01, 0x58, 0x59, 0x5a, 0x7c, 0x7d, 0x7d, 0x7e, 0x7e, 0x8f, 0x8f, 0x8a, 0x8a, 0x8b, 0xfd, 0xfd}, SKINCOLOR_PLUM,       6,  V_AQUAMAP,    true}, // SKINCOLOR_SEAFOAM
+	{"Island",     {  96,   97,  113,  113,  114,  124,  142,  136,  136,  150,  151,  153,  168,  168,  169,  169}, SKINCOLOR_GALAXY,     7,  V_AQUAMAP,    true}, // SKINCOLOR_ISLAND
+	{"Bottle",     {   0,    1,    3,    4,    5,  140,  141,  141,  124,  125,  126,  127,  118,  119,  111,  111}, SKINCOLOR_LATTE,      14, V_AQUAMAP,    true}, // SKINCOLOR_BOTTLE
 	{"Aqua",       {0x78, 0x79, 0x7a, 0x7a, 0x7b, 0x7b, 0x7c, 0x7c, 0x7c, 0x7d, 0x7e, 0x7e, 0x7f, 0x7f, 0x76, 0x77}, SKINCOLOR_ROSY,       7,  V_AQUAMAP,    true}, // SKINCOLOR_AQUA
 	{"Teal",       {0x78, 0x78, 0x8c, 0x8c, 0x8d, 0x8d, 0x8d, 0x8e, 0x8e, 0x8f, 0x8f, 0x8f, 0x8a, 0x8a, 0x8a, 0x8a}, SKINCOLOR_PEACHY,     7,  V_SKYMAP,     true}, // SKINCOLOR_TEAL
+	{"Ocean",      { 120,  121,  122,  122,  123,  141,  142,  142,  136,  137,  138,  138,  139,  139,  253,  253}, SKINCOLOR_TANGERINE,  4,  V_AQUAMAP,    true}, // SKINCOLOR_OCEAN
 	{"Wave",       {0x00, 0x78, 0x78, 0x79, 0x8d, 0x87, 0x88, 0x89, 0x89, 0xae, 0xa8, 0xa8, 0xa9, 0xa9, 0xfd, 0xfd}, SKINCOLOR_QUAIL,      5,  V_SKYMAP,     true}, // SKINCOLOR_WAVE
 	{"Cyan",       {0x80, 0x81, 0xff, 0xff, 0x83, 0x83, 0x8d, 0x8d, 0x8d, 0x8e, 0x7e, 0x7f, 0x76, 0x76, 0x77, 0x6e}, SKINCOLOR_APRICOT,    6,  V_SKYMAP,     true}, // SKINCOLOR_CYAN
+	{"Turquoise",  {  0,   120,  121,  122,  123,  141,  141,  135,  136,  136,  150,  153,  155,  157,  159,  253}, SKINCOLOR_SANGRIA,    12, V_SKYMAP,     true}, // SKINCOLOR_TURQUOISE
+	{"Aquamarine", {   0,  120,  121,  131,  132,  133,  134,  134,  135,  135,  149,  149,  172,  173,  174,  175}, SKINCOLOR_GARNET,     8,  V_SKYMAP,     true}, // SKINCOLOR_AQUAMARINE
 	{"Sky",        {0x80, 0x80, 0x81, 0x82, 0x83, 0x83, 0x84, 0x85, 0x85, 0x86, 0x87, 0x88, 0x89, 0x89, 0x8a, 0x8b}, SKINCOLOR_SANDY,      1,  V_SKYMAP,     true}, // SKINCOLOR_SKY
+	{"Marine",     { 144,  146,  147,  147,  148,  135,  136,  136,  137,  137,  127,  118,  119,  111,  111,  111}, SKINCOLOR_PEAR,       13, V_SKYMAP,     true}, // SKINCOLOR_MARINE
 	{"Cerulean",   {0x85, 0x86, 0x87, 0x88, 0x88, 0x89, 0x89, 0x89, 0x8a, 0x8a, 0xfd, 0xfd, 0xfd, 0x1f, 0x1f, 0x1f}, SKINCOLOR_NEON,       4,  V_SKYMAP,     true}, // SKINCOLOR_CERULEAN
+	{"Dream",      {  80,  208,  200,  200,  146,  146,  133,  134,  135,  136,  137,  138,  139,  139,  254,  254}, SKINCOLOR_FOUNDATION, 9,  V_SKYMAP,     true}, // SKINCOLOR_DREAM
 	{"Icy",        {0x00, 0x00, 0x00, 0x00, 0x80, 0x81, 0x83, 0x83, 0x86, 0x87, 0x95, 0x95, 0xad, 0xad, 0xae, 0xaf}, SKINCOLOR_CRIMSON,    0,  V_SKYMAP,     true}, // SKINCOLOR_ICY
-	{"Sapphire",   {0x80, 0x83, 0x86, 0x87, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xfd, 0xfe}, SKINCOLOR_SUNSET,     5,  V_SKYMAP,     true}, // SKINCOLOR_SAPPHIRE
+	{"Daybreak",   {  80,   81,   82,   72,   64,    9,   11,  171,  149,  150,  151,  153,  156,  157,  159,  253}, SKINCOLOR_EVENTIDE,   12, V_BLUEMAP,    true}, // SKINCOLOR_DAYBREAK
+	{"Sapphire",   {0x80, 0x82, 0x86, 0x87, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xfd, 0xfe}, SKINCOLOR_SUNSET,     5,  V_BLUEMAP,    true}, // SKINCOLOR_SAPPHIRE
+	{"Arctic",     {   0,    1,    3,    4,  145,  146,  147,  148,  148,  149,  150,  153,  156,  159,  253,  254}, SKINCOLOR_ECRU,       15, V_BLUEMAP,    true}, // SKINCOLOR_ARCTIC
 	{"Cornflower", {0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x9a, 0x9c, 0x9d, 0x9d, 0x9e, 0x9e, 0x9e}, SKINCOLOR_YELLOW,     4,  V_BLUEMAP,    true}, // SKINCOLOR_CORNFLOWER
 	{"Blue",       {0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xfd, 0xfe}, SKINCOLOR_ORANGE,     5,  V_BLUEMAP,    true}, // SKINCOLOR_BLUE
-	{"Cobalt",     {0x93, 0x94, 0x95, 0x96, 0x98, 0x9a, 0x9b, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xfd, 0xfd, 0xfe, 0xfe}, SKINCOLOR_PERIDOT,    5,  V_BLUEMAP,    true}, // SKINCOLOR_COBALT
+	{"Cobalt",     { 145,  147,  149,  150,  151,  153,  154,  155,  156,  157,  158,  159,  253,  253,  254,  254}, SKINCOLOR_PERIDOT,    5,  V_BLUEMAP,    true}, // SKINCOLOR_COBALT
+	{"Midnight",   { 171,  171,  172,  173,  173,  174,  175,  157,  158,  159,  253,  253,  254,  254,   31,   31}, SKINCOLOR_CHERRY,     10, V_GRAYMAP,    true}, // SKINCOLOR_MIDNIGHT
+	{"Galaxy",     { 160,  161,  162,  163,  164,  165,  166,  166,  154,  155,  156,  157,  159,  253,  254,   31}, SKINCOLOR_ISLAND,     7,  V_PURPLEMAP,  true}, // SKINCOLOR_GALAXY
 	{"Vapor",      {0x80, 0x81, 0x83, 0x86, 0x94, 0x94, 0xa3, 0xa3, 0xa4, 0xa6, 0xa6, 0xa6, 0xa8, 0xa8, 0xa9, 0xa9}, SKINCOLOR_LILAC,      4,  V_SKYMAP,     true}, // SKINCOLOR_VAPOR
 	{"Dusk",       {0x92, 0x93, 0x94, 0x94, 0xac, 0xad, 0xad, 0xad, 0xae, 0xae, 0xaf, 0xaf, 0xa9, 0xa9, 0xfd, 0xfd}, SKINCOLOR_OLIVE,      0,  V_BLUEMAP,    true}, // SKINCOLOR_DUSK
+	{"Majesty",    {   0,    1,  176,  160,  160,  161,  162,  162,  163,  172,  173,  174,  174,  175,  139,  139}, SKINCOLOR_GOLDENROD,  9,  V_PURPLEMAP,  true}, // SKINCOLOR_MAJESTY
 	{"Pastel",     {0x90, 0x90, 0xa0, 0xa0, 0xa1, 0xa1, 0xa2, 0xa2, 0xa2, 0xa3, 0xa4, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8}, SKINCOLOR_BUBBLEGUM,  9,  V_PURPLEMAP,  true}, // SKINCOLOR_PASTEL
 	{"Purple",     {0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa4, 0xa5, 0xa5, 0xa5, 0xa6, 0xa7, 0xa7, 0xa8, 0xa8, 0xa9, 0xa9}, SKINCOLOR_FLAME,      7,  V_PURPLEMAP,  true}, // SKINCOLOR_PURPLE
-	{"Bubblegum",  {0x00, 0xd0, 0xd0, 0xc8, 0xc8, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8}, SKINCOLOR_PASTEL,     8,  V_MAGENTAMAP, true}, // SKINCOLOR_BUBBLEGUM
+	{"Noble",      { 144,  146,  147,  148,  149,  164,  164,  165,  166,  185,  186,  186,  187,  187,   28,   29}, SKINCOLOR_CHARTREUSE, 12, V_PURPLEMAP,  true}, // SKINCOLOR_NOBLE
+	{"Fuchsia",    { 200,  201,  203,  204,  204,  183,  184,  184,  165,  166,  167,  168,  169,  159,  253,  254}, SKINCOLOR_LEMON,      10, V_PURPLEMAP,  true}, // SKINCOLOR_FUCHSIA
+	{"Bubblegum",  {   0,  208,  208,  176,  177,  178,  179,  180,  181,  182,  164,  166,  167,  168,  169,  253}, SKINCOLOR_PASTEL,     8,  V_MAGENTAMAP, true}, // SKINCOLOR_BUBBLEGUM
+	{"Siberite",   { 252,  177,  179,  180,  181,  181,  182,  182,  183,  164,  166,  167,  167,  168,  169,  159}, SKINCOLOR_EMERALD,    8,  V_MAGENTAMAP, true}, // SKINCOLOR_SIBERITE
 	{"Magenta",    {0xb3, 0xb3, 0xb4, 0xb5, 0xb6, 0xb6, 0xb7, 0xb7, 0xb7, 0xb8, 0xb9, 0xb9, 0xba, 0xba, 0xbb, 0xbb}, SKINCOLOR_LIME,       6,  V_MAGENTAMAP, true}, // SKINCOLOR_MAGENTA
 	{"Neon",       {0xb3, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xb9, 0xba, 0xba, 0xbb, 0xbb, 0xc7, 0xc7, 0x1d, 0x1d, 0x1e}, SKINCOLOR_CERULEAN,   2,  V_MAGENTAMAP, true}, // SKINCOLOR_NEON
 	{"Violet",     {0xd0, 0xd1, 0xd2, 0xca, 0xcc, 0xb8, 0xb9, 0xb9, 0xba, 0xa8, 0xa8, 0xa9, 0xa9, 0xfd, 0xfe, 0xfe}, SKINCOLOR_MINT,       6,  V_MAGENTAMAP, true}, // SKINCOLOR_VIOLET
+	{"Royal",      { 208,  209,  192,  192,  192,  193,  193,  194,  194,  172,  173,  174,  175,  175,  139,  139}, SKINCOLOR_FANCY,      9,  V_PURPLEMAP,  true}, // SKINCOLOR_ROYAL
 	{"Lilac",      {0x00, 0xd0, 0xd1, 0xd2, 0xd3, 0xc1, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc5, 0xc6, 0xc6, 0xfe, 0x1f}, SKINCOLOR_VAPOR,      4,  V_ROSYMAP,    true}, // SKINCOLOR_LILAC
+	{"Mauve",      { 176,  177,  178,  192,  193,  194,  195,  195,  196,  185,  185,  186,  186,  187,  187,  253}, SKINCOLOR_HEADLIGHT,  8,  V_PURPLEMAP,  true}, // SKINCOLOR_MAUVE
+	{"Eventide",   {  51,   52,   53,   33,   34,  204,  183,  183,  184,  184,  166,  167,  168,  169,  253,  254}, SKINCOLOR_DAYBREAK,   13, V_MAGENTAMAP, true}, // SKINCOLOR_EVENTIDE
 	{"Plum",       {0xc8, 0xd3, 0xd5, 0xd6, 0xd7, 0xce, 0xcf, 0xb9, 0xb9, 0xba, 0xba, 0xa9, 0xa9, 0xa9, 0xfd, 0xfe}, SKINCOLOR_MINT,       7,  V_ROSYMAP,    true}, // SKINCOLOR_PLUM
 	{"Raspberry",  {0xc8, 0xc9, 0xca, 0xcb, 0xcb, 0xcc, 0xcd, 0xcd, 0xce, 0xb9, 0xb9, 0xba, 0xba, 0xbb, 0xfe, 0xfe}, SKINCOLOR_APPLE,      13, V_ROSYMAP,    true}, // SKINCOLOR_RASPBERRY
+	{"Taffy",      {   1,  176,  176,  177,  178,  179,  202,  203,  204,  204,  205,  206,  207,   44,   45,   46}, SKINCOLOR_JADE,       8,  V_ROSYMAP,    true}, // SKINCOLOR_TAFFY
 	{"Rosy",       {0xfc, 0xc8, 0xc8, 0xc9, 0xc9, 0xca, 0xca, 0xcb, 0xcb, 0xcc, 0xcc, 0xcd, 0xcd, 0xce, 0xce, 0xcf}, SKINCOLOR_AQUA,       1,  V_ROSYMAP,    true}, // SKINCOLOR_ROSY
+	{"Fancy",      {   0,  208,   49,  210,  210,  202,  202,  203,  204,  204,  205,  206,  207,  207,  186,  186}, SKINCOLOR_ROYAL,      9,  V_ROSYMAP,    true}, // SKINCOLOR_FANCY
+	{"Sangria",    { 210,   32,   33,   34,   34,  215,  215,  207,  207,  185,  186,  186,  186,  169,  169,  253}, SKINCOLOR_TURQUOISE,  12, V_ROSYMAP,    true}, // SKINCOLOR_SANGRIA
+	{"Volcanic",   {  54,   36,   42,   44,   45,   46,   46,   47,   28,  253,  253,  254,  254,   30,   31,   31}, SKINCOLOR_BRONZE,     9,  V_REDMAP,     true}, // SKINCOLOR_VOLCANIC
 
 	// super
 	{"Super Silver 1", {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x03}, SKINCOLOR_BLACK, 15, 0,         false}, // SKINCOLOR_SUPERSILVER1
diff --git a/src/info.h b/src/info.h
index 502ecd726006ecae626f44a0657bcd4245a01eaa..5c7a9f3fd428579c674c7f969db44f7d71b88f07 100644
--- a/src/info.h
+++ b/src/info.h
@@ -18,6 +18,7 @@
 #include "d_think.h"
 #include "sounds.h"
 #include "m_fixed.h"
+#include "dehacked.h" // MAX_ACTION_RECURSION
 
 // deh_tables.c now has lists for the more named enums! PLEASE keep them up to date!
 // For great modding!!
@@ -151,6 +152,7 @@ enum actionnum
 	A_BOSS3TAKEDAMAGE,
 	A_BOSS3PATH,
 	A_BOSS3SHOCKTHINK,
+	A_SHOCKWAVE,
 	A_LINEDEFEXECUTE,
 	A_LINEDEFEXECUTEFROMARG,
 	A_PLAYSEESOUND,
@@ -415,6 +417,7 @@ void A_Boss1Spikeballs();
 void A_Boss3TakeDamage();
 void A_Boss3Path();
 void A_Boss3ShockThink();
+void A_Shockwave();
 void A_LinedefExecute();
 void A_LinedefExecuteFromArg();
 void A_PlaySeeSound();
@@ -564,7 +567,7 @@ void A_DragonWing();
 void A_DragonSegment();
 void A_ChangeHeight();
 
-extern boolean actionsoverridden[NUMACTIONS];
+extern int actionsoverridden[NUMACTIONS][MAX_ACTION_RECURSION];
 
 // ratio of states to sprites to mobj types is roughly 6 : 1 : 1
 #define NUMMOBJFREESLOTS 512
diff --git a/src/libdivide.h b/src/libdivide.h
index 1a589c7e5508957b0c4a8e15d37cd02c0402f349..71a5ae6be16589bbf08e1a8a764217fd121681de 100644
--- a/src/libdivide.h
+++ b/src/libdivide.h
@@ -581,6 +581,11 @@ static inline void libdivide_u128_shift(uint64_t *u1, uint64_t *u0, int32_t sign
 
 ////////// UINT32
 
+#if defined(__GNUC__) || defined(__clang__) // Suppress intentional compiler warnings
+    #pragma GCC diagnostic push
+    #pragma GCC diagnostic ignored "-Waggregate-return"
+#endif
+
 static inline struct libdivide_u32_t libdivide_internal_u32_gen(uint32_t d, int branchfree) {
     struct libdivide_u32_t result;
     uint32_t floor_log_2_d;
@@ -647,6 +652,10 @@ struct libdivide_u32_t libdivide_u32_gen(uint32_t d) {
     return ret;
 }*/
 
+#if defined(__GNUC__) || defined(__clang__) // Stop suppressing intentional compiler warnings
+    #pragma GCC diagnostic pop
+#endif
+
 uint32_t libdivide_u32_do(uint32_t numer, const struct libdivide_u32_t *denom) {
     uint8_t more = denom->more;
     if (!denom->magic) {
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 25fa38769aed22267fe68a05eaa6bd59db343905..1821550b86ff194995346ea7a4fbb4479444e2bb 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"
@@ -689,11 +689,12 @@ static int lib_pSpawnLockOn(lua_State *L)
 		return LUA_ErrInvalid(L, "player_t");
 	if (state >= NUMSTATES)
 		return luaL_error(L, "state %d out of range (0 - %d)", state, NUMSTATES-1);
-	if (P_IsLocalPlayer(player)) // Only display it on your own view.
+	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);
 		visual->flags2 |= MF2_DONTDRAW;
+		visual->drawonlyforplayer = player; // Hide it from the other player in splitscreen, and yourself when spectating
 		P_SetMobjStateNF(visual, state);
 	}
 	return 0;
@@ -1078,7 +1079,8 @@ static int lib_pZMovement(lua_State *L)
 	if (!actor)
 		return LUA_ErrInvalid(L, "mobj_t");
 	lua_pushboolean(L, P_ZMovement(actor));
-	P_CheckPosition(actor, actor->x, actor->y);
+	if (!P_MobjWasRemoved(actor))
+		P_CheckPosition(actor, actor->x, actor->y);
 	P_SetTarget(&tmthing, ptmthing);
 	return 1;
 }
@@ -1106,7 +1108,8 @@ static int lib_pSceneryZMovement(lua_State *L)
 	if (!actor)
 		return LUA_ErrInvalid(L, "mobj_t");
 	lua_pushboolean(L, P_SceneryZMovement(actor));
-	P_CheckPosition(actor, actor->x, actor->y);
+	if (!P_MobjWasRemoved(actor))
+		P_CheckPosition(actor, actor->x, actor->y);
 	P_SetTarget(&tmthing, ptmthing);
 	return 1;
 }
@@ -2453,7 +2456,7 @@ static int lib_pFadeLight(lua_State *L)
 static int lib_pIsFlagAtBase(lua_State *L)
 {
 	mobjtype_t flag = luaL_checkinteger(L, 1);
-	NOHUD
+	//HUDSAFE
 	INLEVEL
 	if (flag >= NUMMOBJTYPES)
 		return luaL_error(L, "mobj type %d out of range (0 - %d)", flag, NUMMOBJTYPES-1);
@@ -2846,6 +2849,22 @@ static int lib_rTextureNumForName(lua_State *L)
 	return 1;
 }
 
+static int lib_rCheckTextureNameForNum(lua_State *L)
+{
+	INT32 num = (INT32)luaL_checkinteger(L, 1);
+	//HUDSAFE
+	lua_pushstring(L, R_CheckTextureNameForNum(num));
+	return 1;
+}
+
+static int lib_rTextureNameForNum(lua_State *L)
+{
+	INT32 num = (INT32)luaL_checkinteger(L, 1);
+	//HUDSAFE
+	lua_pushstring(L, R_TextureNameForNum(num));
+	return 1;
+}
+
 // R_DRAW
 ////////////
 static int lib_rGetColorByName(lua_State *L)
@@ -3525,7 +3544,7 @@ static int lib_gAddGametype(lua_State *L)
 // Partly lifted from Got_AddPlayer
 static int lib_gAddPlayer(lua_State *L)
 {
-	INT16 i, newplayernum, botcount = 1;
+	INT16 i, newplayernum;
 	player_t *newplayer;
 	SINT8 skinnum = 0, bot;
 
@@ -3533,10 +3552,8 @@ static int lib_gAddPlayer(lua_State *L)
 	{
 		if (!playeringame[i])
 			break;
-
-		if (players[i].bot)
-			botcount++; // How many of us are there already?
 	}
+
 	if (i >= MAXPLAYERS)
 	{
 		lua_pushnil(L);
@@ -3807,7 +3824,7 @@ static int lib_gDoReborn(lua_State *L)
 }
 
 // Another Lua function that doesn't actually exist!
-// Sets nextmapoverride & skipstats without instantly ending the level, for instances where other sources should be exiting the level, like normal signposts.
+// Sets nextmapoverride, skipstats and nextgametype without instantly ending the level, for instances where other sources should be exiting the level, like normal signposts.
 static int lib_gSetCustomExitVars(lua_State *L)
 {
 	int n = lua_gettop(L); // Num arguments
@@ -3816,18 +3833,21 @@ static int lib_gSetCustomExitVars(lua_State *L)
 
 	// LUA EXTENSION: Custom exit like support
 	// Supported:
-	//	G_SetCustomExitVars();			[reset to defaults]
-	//	G_SetCustomExitVars(int)		[nextmap override only]
-	//	G_SetCustomExitVars(nil, int)	[skipstats only]
-	//	G_SetCustomExitVars(int, int)	[both of the above]
+	//	G_SetCustomExitVars();               [reset to defaults]
+	//	G_SetCustomExitVars(int)             [nextmap override only]
+	//	G_SetCustomExitVars(nil, int)        [skipstats only]
+	//	G_SetCustomExitVars(int, int)        [both of the above]
+	//	G_SetCustomExitVars(int, int, int)   [nextmapoverride, skipstats and nextgametype]
 
 	nextmapoverride = 0;
 	skipstats = 0;
+	nextgametype = -1;
 
 	if (n >= 1)
 	{
 		nextmapoverride = (INT16)luaL_optinteger(L, 1, 0);
 		skipstats = (INT16)luaL_optinteger(L, 2, 0);
+		nextgametype = (INT16)luaL_optinteger(L, 3, -1);
 	}
 
 	return 0;
@@ -4203,6 +4223,8 @@ static luaL_Reg lib[] = {
 	// r_data
 	{"R_CheckTextureNumForName",lib_rCheckTextureNumForName},
 	{"R_TextureNumForName",lib_rTextureNumForName},
+	{"R_CheckTextureNameForNum", lib_rCheckTextureNameForNum},
+	{"R_TextureNameForNum", lib_rTextureNameForNum},
 
 	// r_draw
 	{"R_GetColorByName", lib_rGetColorByName},
diff --git a/src/lua_consolelib.c b/src/lua_consolelib.c
index dcaad1416011941230285723037a193ade75102e..0ab8ad9c348a14de25bb8cfc0fd2ad809bab15ee 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"
@@ -357,7 +358,7 @@ static int lib_cvRegisterVar(lua_State *L)
 			if (lua_islightuserdata(L, 4))
 			{
 				CV_PossibleValue_t *pv = lua_touserdata(L, 4);
-				if (pv == CV_OnOff || pv == CV_YesNo || pv == CV_Unsigned || pv == CV_Natural)
+				if (pv == CV_OnOff || pv == CV_YesNo || pv == CV_Unsigned || pv == CV_Natural || pv == CV_TrueFalse)
 					cvar->PossibleValue = pv;
 				else
 					FIELDERROR("PossibleValue", "CV_PossibleValue_t expected, got unrecognised pointer")
@@ -566,27 +567,59 @@ static luaL_Reg lib[] = {
 	{NULL, NULL}
 };
 
+enum cvar_e
+{
+	cvar_name,
+	cvar_defaultvalue,
+	cvar_flags,
+	cvar_value,
+	cvar_string,
+	cvar_changed,
+};
+
+static const char *const cvar_opt[] = {
+	"name",
+	"defaultvalue",
+	"flags",
+	"value",
+	"string",
+	"changed",
+	NULL,
+};
+
+static int cvar_fields_ref = LUA_NOREF;
+
 static int cvar_get(lua_State *L)
 {
 	consvar_t *cvar = *(consvar_t **)luaL_checkudata(L, 1, META_CVAR);
-	const char *field = luaL_checkstring(L, 2);
+	enum cvar_e field = Lua_optoption(L, 2, -1, cvar_fields_ref);
 
-	if(fastcmp(field,"name"))
+	switch (field)
+	{
+	case cvar_name:
 		lua_pushstring(L, cvar->name);
-	else if(fastcmp(field,"defaultvalue"))
+		break;
+	case cvar_defaultvalue:
 		lua_pushstring(L, cvar->defaultvalue);
-	else if(fastcmp(field,"flags"))
+		break;
+	case cvar_flags:
 		lua_pushinteger(L, cvar->flags);
-	else if(fastcmp(field,"value"))
+		break;
+	case cvar_value:
 		lua_pushinteger(L, cvar->value);
-	else if(fastcmp(field,"string"))
+		break;
+	case cvar_string:
 		lua_pushstring(L, cvar->string);
-	else if(fastcmp(field,"changed"))
+		break;
+	case cvar_changed:
 		lua_pushboolean(L, cvar->changed);
-	else if (devparm)
-		return luaL_error(L, LUA_QL("consvar_t") " has no field named " LUA_QS, field);
-	else
-		return 0;
+		break;
+	default:
+		if (devparm)
+			return luaL_error(L, LUA_QL("consvar_t") " has no field named " LUA_QS ".", lua_tostring(L, 2));
+		else
+			return 0;
+	}
 	return 1;
 }
 
@@ -598,6 +631,8 @@ int LUA_ConsoleLib(lua_State *L)
 		lua_setfield(L, -2, "__index");
 	lua_pop(L,1);
 
+	cvar_fields_ref = Lua_CreateFieldTable(L, cvar_opt);
+
 	// Set empty registry tables
 	lua_newtable(L);
 	lua_setfield(L, LUA_REGISTRYINDEX, "COM_Command");
@@ -618,6 +653,8 @@ int LUA_ConsoleLib(lua_State *L)
 	lua_setglobal(L, "CV_Unsigned");
 	lua_pushlightuserdata(L, CV_Natural);
 	lua_setglobal(L, "CV_Natural");
+	lua_pushlightuserdata(L, CV_TrueFalse);
+	lua_setglobal(L, "CV_TrueFalse");
 
 	// Set global functions
 	lua_pushvalue(L, LUA_GLOBALSINDEX);
diff --git a/src/lua_hooklib.c b/src/lua_hooklib.c
index 039a9677f421dad1ffc4fac29a2335c88c866afa..0fc25ee6c9927b96e893a3fc1a0be9470b8f4938 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
 
 /* =========================================================================
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index 1a05997572f60542ca12d195b697f7aad87da564..6eec91273352db4adc598a80b7e0138795589640 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -95,6 +95,8 @@ static const char *const patch_opt[] = {
 	"topoffset",
 	NULL};
 
+static int patch_fields_ref = LUA_NOREF;
+
 // alignment types for v.drawString
 enum align {
 	align_left = 0,
@@ -196,6 +198,8 @@ static const char *const camera_opt[] = {
 	"momz",
 	NULL};
 
+static int camera_fields_ref = LUA_NOREF;
+
 static int lib_getHudInfo(lua_State *L)
 {
 	UINT32 i;
@@ -276,9 +280,8 @@ static int colormap_get(lua_State *L)
 static int patch_get(lua_State *L)
 {
 	patch_t *patch = *((patch_t **)luaL_checkudata(L, 1, META_PATCH));
-	enum patch field = luaL_checkoption(L, 2, NULL, patch_opt);
+	enum patch field = Lua_optoption(L, 2, -1, patch_fields_ref);
 
-	// patches are invalidated when switching renderers
 	if (!patch) {
 		if (field == patch_valid) {
 			lua_pushboolean(L, 0);
@@ -316,7 +319,7 @@ static int patch_set(lua_State *L)
 static int camera_get(lua_State *L)
 {
 	camera_t *cam = *((camera_t **)luaL_checkudata(L, 1, META_CAMERA));
-	enum cameraf field = luaL_checkoption(L, 2, NULL, camera_opt);
+	enum cameraf field = Lua_optoption(L, 2, -1, camera_fields_ref);
 
 	// cameras should always be valid unless I'm a nutter
 	I_Assert(cam != NULL);
@@ -372,7 +375,7 @@ static int camera_get(lua_State *L)
 static int camera_set(lua_State *L)
 {
 	camera_t *cam = *((camera_t **)luaL_checkudata(L, 1, META_CAMERA));
-	enum cameraf field = luaL_checkoption(L, 2, NULL, camera_opt);
+	enum cameraf field = Lua_optoption(L, 2, -1, camera_fields_ref);
 
 	I_Assert(cam != NULL);
 
@@ -432,7 +435,7 @@ static int camera_set(lua_State *L)
 		cam->momz = luaL_checkfixed(L, 3);
 		break;
 	default:
-		return luaL_error(L, LUA_QL("camera_t") " has no field named " LUA_QS, camera_opt[field]);
+		return luaL_error(L, LUA_QL("camera_t") " has no field named " LUA_QS ".", lua_tostring(L, 2));
 	}
 	return 0;
 }
@@ -1252,8 +1255,6 @@ static int libd_RandomKey(lua_State *L)
 	INT32 a = (INT32)luaL_checkinteger(L, 1);
 
 	HUDONLY
-	if (a > 65536)
-		LUA_UsageWarning(L, "v.RandomKey: range > 65536 is undefined behavior");
 	lua_pushinteger(L, M_RandomKey(a));
 	return 1;
 }
@@ -1264,13 +1265,6 @@ static int libd_RandomRange(lua_State *L)
 	INT32 b = (INT32)luaL_checkinteger(L, 2);
 
 	HUDONLY
-	if (b < a) {
-		INT32 c = a;
-		a = b;
-		b = c;
-	}
-	if ((b-a+1) > 65536)
-		LUA_UsageWarning(L, "v.RandomRange: range > 65536 is undefined behavior");
 	lua_pushinteger(L, M_RandomRange(a, b));
 	return 1;
 }
@@ -1444,6 +1438,8 @@ int LUA_HudLib(lua_State *L)
 		lua_setfield(L, -2, "__newindex");
 	lua_pop(L,1);
 
+	patch_fields_ref = Lua_CreateFieldTable(L, patch_opt);
+
 	luaL_newmetatable(L, META_CAMERA);
 		lua_pushcfunction(L, camera_get);
 		lua_setfield(L, -2, "__index");
@@ -1452,6 +1448,8 @@ int LUA_HudLib(lua_State *L)
 		lua_setfield(L, -2, "__newindex");
 	lua_pop(L,1);
 
+	camera_fields_ref = Lua_CreateFieldTable(L, camera_opt);
+
 	luaL_register(L, "hud", lib_hud);
 	return 0;
 }
@@ -1488,7 +1486,6 @@ void LUA_SetHudHook(int hook, huddrawlist_h list)
 			break;
 
 		case HUD_HOOK(intermission):
-			lua_pushboolean(gL, intertype == int_spec &&
-					stagefailed);
+			lua_pushboolean(gL, stagefailed);
 	}
 }
diff --git a/src/lua_hudlib_drawlist.c b/src/lua_hudlib_drawlist.c
index f46f207c100dc03e1256938dceda2e4e31b6eff7..c518ba52540ff874480d7cda2c8314a31987e993 100644
--- a/src/lua_hudlib_drawlist.c
+++ b/src/lua_hudlib_drawlist.c
@@ -103,7 +103,7 @@ huddrawlist_h LUA_HUD_CreateDrawList(void)
 {
 	huddrawlist_h drawlist;
 
-	drawlist = (huddrawlist_h) Z_CallocAlign(sizeof(struct huddrawlist_s), PU_STATIC, NULL, 64);
+	drawlist = (huddrawlist_h) Z_Calloc(sizeof(struct huddrawlist_s), PU_STATIC, NULL);
 	drawlist->items = NULL;
 	drawlist->items_capacity = 0;
 	drawlist->items_len = 0;
@@ -160,7 +160,7 @@ static size_t AllocateDrawItem(huddrawlist_h list)
 	{
 		if (list->items_capacity == 0) list->items_capacity = 128;
 		else list->items_capacity *= 2;
-		list->items = (drawitem_t *) Z_ReallocAlign(list->items, sizeof(struct drawitem_s) * list->items_capacity, PU_STATIC, NULL, 64);
+		list->items = (drawitem_t *) Z_Realloc(list->items, sizeof(struct drawitem_s) * list->items_capacity, PU_STATIC, NULL);
 	}
 
 	return list->items_len++;
@@ -177,9 +177,18 @@ static const char *CopyString(huddrawlist_h list, const char* str)
 	lenstr = strlen(str);
 	if (list->strbuf_capacity <= list->strbuf_len + lenstr + 1)
 	{
+		const char *old_offset = list->strbuf;
+		size_t i;
 		if (list->strbuf_capacity == 0) list->strbuf_capacity = 256;
 		else list->strbuf_capacity *= 2;
-		list->strbuf = (char*) Z_ReallocAlign(list->strbuf, sizeof(char) * list->strbuf_capacity, PU_STATIC, NULL, 8);
+		list->strbuf = (char*) Z_Realloc(list->strbuf, sizeof(char) * list->strbuf_capacity, PU_STATIC, NULL);
+
+		// align the string pointers to make sure old pointers don't point towards invalid addresses
+		// this is necessary since Z_ReallocAlign might actually move the string buffer in memory
+		for (i = 0; i < list->items_len; i++)
+		{
+			list->items[i].str += list->strbuf - old_offset;
+		}
 	}
 	const char *result = (const char *) &list->strbuf[list->strbuf_len];
 	strncpy(&list->strbuf[list->strbuf_len], str, lenstr + 1);
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index 7388632d3c6cac8e74619ff4b5ac9bfca8ddb2e8..3764acf6a40945489506ce8d94b1b976fa771752 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -66,7 +66,7 @@ const char *const sfxinfo_wopt[] = {
 	"caption",
 	NULL};
 
-boolean actionsoverridden[NUMACTIONS] = {false};
+int actionsoverridden[NUMACTIONS][MAX_ACTION_RECURSION];
 
 //
 // Sprite Names
@@ -319,7 +319,7 @@ static int PopPivotSubTable(spriteframepivot_t *pivot, lua_State *L, int stk, in
 				else if (ikey == 2 || (key && fastcmp(key, "y")))
 					pivot[idx].y = (INT32)value;
 				else if (ikey == 3 || (key && fastcmp(key, "rotaxis")))
-					pivot[idx].rotaxis = (UINT8)value;
+					LUA_UsageWarning(L, "\"rotaxis\" is deprecated and will be removed.")
 				else if (ikey == -1 && (key != NULL))
 					FIELDERROR("pivot key", va("invalid option %s", key));
 				okcool = 1;
@@ -508,8 +508,6 @@ static int pivotlist_get(lua_State *L)
 	const char *field = luaL_checkstring(L, 2);
 	UINT8 frame;
 
-	I_Assert(framepivot != NULL);
-
 	frame = R_Char2Frame(field[0]);
 	if (frame == 255)
 		luaL_error(L, "invalid frame %s", field);
@@ -539,8 +537,6 @@ static int pivotlist_set(lua_State *L)
 	if (hook_cmd_running)
 		return luaL_error(L, "Do not alter spriteframepivot_t in CMD building code!");
 
-	I_Assert(pivotlist != NULL);
-
 	frame = R_Char2Frame(field[0]);
 	if (frame == 255)
 		luaL_error(L, "invalid frame %s", field);
@@ -576,7 +572,10 @@ static int framepivot_get(lua_State *L)
 	else if (fastcmp("y", field))
 		lua_pushinteger(L, framepivot->y);
 	else if (fastcmp("rotaxis", field))
-		lua_pushinteger(L, (UINT8)framepivot->rotaxis);
+	{
+		LUA_UsageWarning(L, "\"rotaxis\" is deprecated and will be removed.");
+		lua_pushinteger(L, 0);
+	}
 	else
 		return luaL_error(L, va("Field %s does not exist in spriteframepivot_t", field));
 
@@ -602,7 +601,7 @@ static int framepivot_set(lua_State *L)
 	else if (fastcmp("y", field))
 		framepivot->y = luaL_checkinteger(L, 3);
 	else if (fastcmp("rotaxis", field))
-		framepivot->rotaxis = luaL_checkinteger(L, 3);
+		LUA_UsageWarning(L, "\"rotaxis\" is deprecated and will be removed.")
 	else
 		return luaL_error(L, va("Field %s does not exist in spriteframepivot_t", field));
 
@@ -645,8 +644,8 @@ static void A_Lua(mobj_t *actor)
 		if (lua_rawequal(gL, -1, -4))
 		{
 			found = true;
-			superactions[superstack] = lua_tostring(gL, -2); // "A_ACTION"
-			++superstack;
+			luaactions[luaactionstack] = lua_tostring(gL, -2); // "A_ACTION"
+			++luaactionstack;
 			lua_pop(gL, 2); // pop the name and function
 			break;
 		}
@@ -661,8 +660,8 @@ static void A_Lua(mobj_t *actor)
 
 	if (found)
 	{
-		--superstack;
-		superactions[superstack] = NULL;
+		--luaactionstack;
+		luaactions[luaactionstack] = NULL;
 	}
 }
 
@@ -812,22 +811,54 @@ boolean LUA_SetLuaAction(void *stv, const char *action)
 	return true; // action successfully set.
 }
 
+static UINT8 superstack[NUMACTIONS];
 boolean LUA_CallAction(enum actionnum actionnum, mobj_t *actor)
 {
 	I_Assert(actor != NULL);
 
-	if (!actionsoverridden[actionnum]) // The action is not overriden,
-		return false; // action not called.
+	if (actionsoverridden[actionnum][0] == LUA_REFNIL)
+	{
+		// The action was not overridden at all,
+		// so just call the hardcoded version.
+		return false;
+	}
+
+	if (luaactionstack && fasticmp(actionpointers[actionnum].name, luaactions[luaactionstack-1]))
+	{
+		// The action is calling itself,
+		// so look up the next Lua reference in its stack.
 
-	if (superstack && fasticmp(actionpointers[actionnum].name, superactions[superstack-1])) // the action is calling itself,
-		return false; // let it call the hardcoded function instead.
+		// 0 is just the reference to the one we're calling,
+		// so we increment here.
+		superstack[actionnum]++;
 
+		if (superstack[actionnum] >= MAX_ACTION_RECURSION)
+		{
+			CONS_Alert(CONS_WARNING, "Max Lua super recursion reached! Cool it on calling super!\n");
+			superstack[actionnum] = 0;
+			return false;
+		}
+	}
+
+	if (actionsoverridden[actionnum][superstack[actionnum]] == LUA_REFNIL)
+	{
+		// No Lua reference beyond this point.
+		// Let it call the hardcoded function instead.
+
+		if (superstack[actionnum])
+		{
+			// Decrement super stack
+			superstack[actionnum]--;
+		}
+
+		return false;
+	}
+
+	// Push error function
 	lua_pushcfunction(gL, LUA_GetErrorMessage);
 
-	// grab function by uppercase name.
-	lua_getfield(gL, LUA_REGISTRYINDEX, LREG_ACTIONS);
-	lua_getfield(gL, -1, actionpointers[actionnum].name);
-	lua_remove(gL, -2); // pop LREG_ACTIONS
+	// Push function by reference.
+	lua_getref(gL, actionsoverridden[actionnum][superstack[actionnum]]);
 
 	if (lua_isnil(gL, -1)) // no match
 	{
@@ -835,7 +866,7 @@ boolean LUA_CallAction(enum actionnum actionnum, mobj_t *actor)
 		return false; // action not called.
 	}
 
-	if (superstack == MAXRECURSION)
+	if (luaactionstack >= MAX_ACTION_RECURSION)
 	{
 		CONS_Alert(CONS_WARNING, "Max Lua Action recursion reached! Cool it on the calling A_Action functions from inside A_Action functions!\n");
 		lua_pop(gL, 2); // pop function and error handler
@@ -849,14 +880,20 @@ boolean LUA_CallAction(enum actionnum actionnum, mobj_t *actor)
 	lua_pushinteger(gL, var1);
 	lua_pushinteger(gL, var2);
 
-	superactions[superstack] = actionpointers[actionnum].name;
-	++superstack;
+	luaactions[luaactionstack] = actionpointers[actionnum].name;
+	++luaactionstack;
 
 	LUA_Call(gL, 3, 0, -(2 + 3));
 	lua_pop(gL, -1); // Error handler
 
-	--superstack;
-	superactions[superstack] = NULL;
+	if (superstack[actionnum])
+	{
+		// Decrement super stack
+		superstack[actionnum]--;
+	}
+
+	--luaactionstack;
+	luaactions[luaactionstack] = NULL;
 	return true; // action successfully called.
 }
 
@@ -1106,75 +1143,161 @@ static int lib_mobjinfolen(lua_State *L)
 	return 1;
 }
 
+enum mobjinfo_e
+{
+	mobjinfo_doomednum,
+	mobjinfo_spawnstate,
+	mobjinfo_spawnhealth,
+	mobjinfo_seestate,
+	mobjinfo_seesound,
+	mobjinfo_reactiontime,
+	mobjinfo_attacksound,
+	mobjinfo_painstate,
+	mobjinfo_painchance,
+	mobjinfo_painsound,
+	mobjinfo_meleestate,
+	mobjinfo_missilestate,
+	mobjinfo_deathstate,
+	mobjinfo_xdeathstate,
+	mobjinfo_deathsound,
+	mobjinfo_speed,
+	mobjinfo_radius,
+	mobjinfo_height,
+	mobjinfo_dispoffset,
+	mobjinfo_mass,
+	mobjinfo_damage,
+	mobjinfo_activesound,
+	mobjinfo_flags,
+	mobjinfo_raisestate,
+};
+
+const char *const mobjinfo_opt[] = {
+	"doomednum",
+	"spawnstate",
+	"spawnhealth",
+	"seestate",
+	"seesound",
+	"reactiontime",
+	"attacksound",
+	"painstate",
+	"painchance",
+	"painsound",
+	"meleestate",
+	"missilestate",
+	"deathstate",
+	"xdeathstate",
+	"deathsound",
+	"speed",
+	"radius",
+	"height",
+	"dispoffset",
+	"mass",
+	"damage",
+	"activesound",
+	"flags",
+	"raisestate",
+	NULL,
+};
+
+static int mobjinfo_fields_ref = LUA_NOREF;
+
 // mobjinfo_t *, field -> number
 static int mobjinfo_get(lua_State *L)
 {
 	mobjinfo_t *info = *((mobjinfo_t **)luaL_checkudata(L, 1, META_MOBJINFO));
-	const char *field = luaL_checkstring(L, 2);
+	enum mobjinfo_e field = Lua_optoption(L, 2, mobjinfo_doomednum, mobjinfo_fields_ref);
 
 	I_Assert(info != NULL);
 	I_Assert(info >= mobjinfo);
 
-	if (fastcmp(field,"doomednum"))
+	switch (field)
+	{
+	case mobjinfo_doomednum:
 		lua_pushinteger(L, info->doomednum);
-	else if (fastcmp(field,"spawnstate"))
+		break;
+	case mobjinfo_spawnstate:
 		lua_pushinteger(L, info->spawnstate);
-	else if (fastcmp(field,"spawnhealth"))
+		break;
+	case mobjinfo_spawnhealth:
 		lua_pushinteger(L, info->spawnhealth);
-	else if (fastcmp(field,"seestate"))
+		break;
+	case mobjinfo_seestate:
 		lua_pushinteger(L, info->seestate);
-	else if (fastcmp(field,"seesound"))
+		break;
+	case mobjinfo_seesound:
 		lua_pushinteger(L, info->seesound);
-	else if (fastcmp(field,"reactiontime"))
+		break;
+	case mobjinfo_reactiontime:
 		lua_pushinteger(L, info->reactiontime);
-	else if (fastcmp(field,"attacksound"))
+		break;
+	case mobjinfo_attacksound:
 		lua_pushinteger(L, info->attacksound);
-	else if (fastcmp(field,"painstate"))
+		break;
+	case mobjinfo_painstate:
 		lua_pushinteger(L, info->painstate);
-	else if (fastcmp(field,"painchance"))
+		break;
+	case mobjinfo_painchance:
 		lua_pushinteger(L, info->painchance);
-	else if (fastcmp(field,"painsound"))
+		break;
+	case mobjinfo_painsound:
 		lua_pushinteger(L, info->painsound);
-	else if (fastcmp(field,"meleestate"))
+		break;
+	case mobjinfo_meleestate:
 		lua_pushinteger(L, info->meleestate);
-	else if (fastcmp(field,"missilestate"))
+		break;
+	case mobjinfo_missilestate:
 		lua_pushinteger(L, info->missilestate);
-	else if (fastcmp(field,"deathstate"))
+		break;
+	case mobjinfo_deathstate:
 		lua_pushinteger(L, info->deathstate);
-	else if (fastcmp(field,"xdeathstate"))
+		break;
+	case mobjinfo_xdeathstate:
 		lua_pushinteger(L, info->xdeathstate);
-	else if (fastcmp(field,"deathsound"))
+		break;
+	case mobjinfo_deathsound:
 		lua_pushinteger(L, info->deathsound);
-	else if (fastcmp(field,"speed"))
+		break;
+	case mobjinfo_speed:
 		lua_pushinteger(L, info->speed); // sometimes it's fixed_t, sometimes it's not...
-	else if (fastcmp(field,"radius"))
+		break;
+	case mobjinfo_radius:
 		lua_pushfixed(L, info->radius);
-	else if (fastcmp(field,"height"))
+		break;
+	case mobjinfo_height:
 		lua_pushfixed(L, info->height);
-	else if (fastcmp(field,"dispoffset"))
+		break;
+	case mobjinfo_dispoffset:
 		lua_pushinteger(L, info->dispoffset);
-	else if (fastcmp(field,"mass"))
+		break;
+	case mobjinfo_mass:
 		lua_pushinteger(L, info->mass);
-	else if (fastcmp(field,"damage"))
+		break;
+	case mobjinfo_damage:
 		lua_pushinteger(L, info->damage);
-	else if (fastcmp(field,"activesound"))
+		break;
+	case mobjinfo_activesound:
 		lua_pushinteger(L, info->activesound);
-	else if (fastcmp(field,"flags"))
+		break;
+	case mobjinfo_flags:
 		lua_pushinteger(L, info->flags);
-	else if (fastcmp(field,"raisestate"))
+		break;
+	case mobjinfo_raisestate:
 		lua_pushinteger(L, info->raisestate);
-	else {
+		break;
+	default:
 		lua_getfield(L, LUA_REGISTRYINDEX, LREG_EXTVARS);
 		I_Assert(lua_istable(L, -1));
 		lua_pushlightuserdata(L, info);
 		lua_rawget(L, -2);
 		if (!lua_istable(L, -1)) { // no extra values table
-			CONS_Debug(DBG_LUA, M_GetText("'%s' has no field named '%s'; returning nil.\n"), "mobjinfo_t", field);
+			CONS_Debug(DBG_LUA, M_GetText("'%s' has no field named '%s'; returning nil.\n"), "mobjinfo_t", lua_tostring(L, 2));
 			return 0;
 		}
-		lua_getfield(L, -1, field);
+		lua_pushvalue(L, 2); // field name
+		lua_gettable(L, -2);
 		if (lua_isnil(L, -1)) // no value for this field
-			CONS_Debug(DBG_LUA, M_GetText("'%s' has no field named '%s'; returning nil.\n"), "mobjinfo_t", field);
+			CONS_Debug(DBG_LUA, M_GetText("'%s' has no field named '%s'; returning nil.\n"), "mobjinfo_t", lua_tostring(L, 2));
+		break;
 	}
 	return 1;
 }
@@ -1183,7 +1306,7 @@ static int mobjinfo_get(lua_State *L)
 static int mobjinfo_set(lua_State *L)
 {
 	mobjinfo_t *info = *((mobjinfo_t **)luaL_checkudata(L, 1, META_MOBJINFO));
-	const char *field = luaL_checkstring(L, 2);
+	enum mobjinfo_e field = Lua_optoption(L, 2, -1, mobjinfo_fields_ref);
 
 	if (hud_running)
 		return luaL_error(L, "Do not alter mobjinfo in HUD rendering code!");
@@ -1193,55 +1316,81 @@ static int mobjinfo_set(lua_State *L)
 	I_Assert(info != NULL);
 	I_Assert(info >= mobjinfo);
 
-	if (fastcmp(field,"doomednum"))
+	switch (field)
+	{
+	case mobjinfo_doomednum:
 		info->doomednum = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"spawnstate"))
+		break;
+	case mobjinfo_spawnstate:
 		info->spawnstate = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"spawnhealth"))
+		break;
+	case mobjinfo_spawnhealth:
 		info->spawnhealth = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"seestate"))
+		break;
+	case mobjinfo_seestate:
 		info->seestate = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"seesound"))
+		break;
+	case mobjinfo_seesound:
 		info->seesound = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"reactiontime"))
+		break;
+	case mobjinfo_reactiontime:
 		info->reactiontime = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"attacksound"))
+		break;
+	case mobjinfo_attacksound:
 		info->attacksound = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"painstate"))
+		break;
+	case mobjinfo_painstate:
 		info->painstate = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"painchance"))
+		break;
+	case mobjinfo_painchance:
 		info->painchance = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"painsound"))
+		break;
+	case mobjinfo_painsound:
 		info->painsound = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"meleestate"))
+		break;
+	case mobjinfo_meleestate:
 		info->meleestate = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"missilestate"))
+		break;
+	case mobjinfo_missilestate:
 		info->missilestate = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"deathstate"))
+		break;
+	case mobjinfo_deathstate:
 		info->deathstate = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"xdeathstate"))
+		break;
+	case mobjinfo_xdeathstate:
 		info->xdeathstate = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"deathsound"))
+		break;
+	case mobjinfo_deathsound:
 		info->deathsound = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"speed"))
+		break;
+	case mobjinfo_speed:
 		info->speed = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"radius"))
+		break;
+	case mobjinfo_radius:
 		info->radius = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"height"))
+		break;
+	case mobjinfo_height:
 		info->height = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"dispoffset"))
+		break;
+	case mobjinfo_dispoffset:
 		info->dispoffset = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"mass"))
+		break;
+	case mobjinfo_mass:
 		info->mass = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"damage"))
+		break;
+	case mobjinfo_damage:
 		info->damage = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"activesound"))
+		break;
+	case mobjinfo_activesound:
 		info->activesound = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"flags"))
+		break;
+	case mobjinfo_flags:
 		info->flags = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"raisestate"))
+		break;
+	case mobjinfo_raisestate:
 		info->raisestate = luaL_checkinteger(L, 3);
-	else {
+		break;
+	default:
 		lua_getfield(L, LUA_REGISTRYINDEX, LREG_EXTVARS);
 		I_Assert(lua_istable(L, -1));
 		lua_pushlightuserdata(L, info);
@@ -1249,18 +1398,17 @@ static int mobjinfo_set(lua_State *L)
 		if (lua_isnil(L, -1)) {
 			// This index doesn't have a table for extra values yet, let's make one.
 			lua_pop(L, 1);
-			CONS_Debug(DBG_LUA, M_GetText("'%s' has no field named '%s'; adding it as Lua data.\n"), "mobjinfo_t", field);
+			CONS_Debug(DBG_LUA, M_GetText("'%s' has no field named '%s'; adding it as Lua data.\n"), "mobjinfo_t", lua_tostring(L, 2));
 			lua_newtable(L);
 			lua_pushlightuserdata(L, info);
 			lua_pushvalue(L, -2); // ext value table
 			lua_rawset(L, -4); // LREG_EXTVARS table
 		}
+		lua_pushvalue(L, 2); // key
 		lua_pushvalue(L, 3); // value to store
-		lua_setfield(L, -2, field);
+		lua_settable(L, -3);
 		lua_pop(L, 2);
 	}
-	//else
-		//return luaL_error(L, LUA_QL("mobjinfo_t") " has no field named " LUA_QS, field);
 	return 0;
 }
 
@@ -1598,7 +1746,7 @@ static int lib_setSkinColor(lua_State *L)
 		else if (i == 6 || (str && fastcmp(str,"accessible"))) {
 			boolean v = lua_toboolean(L, 3);
 			if (cnum < FIRSTSUPERCOLOR && v != skincolors[cnum].accessible)
-				return luaL_error(L, "skincolors[] index %d is a standard color; accessibility changes are prohibited.", cnum);
+				CONS_Alert(CONS_WARNING, "skincolors[] index %d is a standard color; accessibility changes are prohibited.", cnum);
 			else
 				info->accessible = v;
 		}
@@ -1693,7 +1841,7 @@ static int skincolor_set(lua_State *L)
 	else if (fastcmp(field,"accessible")) {
 		boolean v = lua_toboolean(L, 3);
 		if (cnum < FIRSTSUPERCOLOR && v != skincolors[cnum].accessible)
-			return luaL_error(L, "skincolors[] index %d is a standard color; accessibility changes are prohibited.", cnum);
+			CONS_Alert(CONS_WARNING, "skincolors[] index %d is a standard color; accessibility changes are prohibited.", cnum);
 		else
 			info->accessible = v;
 	} else
@@ -1788,6 +1936,8 @@ int LUA_InfoLib(lua_State *L)
 		lua_setfield(L, -2, "__len");
 	lua_pop(L, 1);
 
+	mobjinfo_fields_ref = Lua_CreateFieldTable(L, mobjinfo_opt);
+
 	luaL_newmetatable(L, META_SKINCOLOR);
 		lua_pushcfunction(L, skincolor_get);
 		lua_setfield(L, -2, "__index");
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 898651520d77ead06f05ccbe767fe618708e4c20..e343979939056ec3c18b482a3bfd3423c446d70e 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -33,12 +33,20 @@ enum sector_e {
 	sector_floorheight,
 	sector_ceilingheight,
 	sector_floorpic,
+	sector_floorxoffset,
+	sector_flooryoffset,
+	sector_floorangle,	
 	sector_ceilingpic,
+	sector_ceilingxoffset,
+	sector_ceilingyoffset,
+	sector_ceilingangle,
 	sector_lightlevel,
 	sector_floorlightlevel,
 	sector_floorlightabsolute,
+	sector_floorlightsec,	
 	sector_ceilinglightlevel,
 	sector_ceilinglightabsolute,
+	sector_ceilinglightsec,
 	sector_special,
 	sector_tag,
 	sector_taglist,
@@ -63,12 +71,20 @@ static const char *const sector_opt[] = {
 	"floorheight",
 	"ceilingheight",
 	"floorpic",
+	"floorxoffset",
+	"flooryoffset",
+	"floorangle",
 	"ceilingpic",
+	"ceilingxoffset",
+	"ceilingyoffset",
+	"ceilingangle",	
 	"lightlevel",
 	"floorlightlevel",
 	"floorlightabsolute",
+	"floorlightsec",
 	"ceilinglightlevel",
 	"ceilinglightabsolute",
+	"ceilinglightsec",	
 	"special",
 	"tag",
 	"taglist",
@@ -88,6 +104,8 @@ static const char *const sector_opt[] = {
 	"gravity",
 	NULL};
 
+static int sector_fields_ref = LUA_NOREF;
+
 enum subsector_e {
 	subsector_valid = 0,
 	subsector_sector,
@@ -104,6 +122,8 @@ static const char *const subsector_opt[] = {
 	"polyList",
 	NULL};
 
+static int subsector_fields_ref = LUA_NOREF;
+
 enum line_e {
 	line_valid = 0,
 	line_v1,
@@ -156,10 +176,18 @@ static const char *const line_opt[] = {
 	"callcount",
 	NULL};
 
+static int line_fields_ref = LUA_NOREF;
+
 enum side_e {
 	side_valid = 0,
 	side_textureoffset,
 	side_rowoffset,
+	side_offsetx_top,
+	side_offsety_top,
+	side_offsetx_mid,
+	side_offsety_mid,
+	side_offsetx_bot,
+	side_offsety_bot,
 	side_toptexture,
 	side_bottomtexture,
 	side_midtexture,
@@ -174,6 +202,12 @@ static const char *const side_opt[] = {
 	"valid",
 	"textureoffset",
 	"rowoffset",
+	"offsetx_top",
+	"offsety_top",
+	"offsetx_mid",
+	"offsety_mid",
+	"offsetx_bot",
+	"offsety_bot",
 	"toptexture",
 	"bottomtexture",
 	"midtexture",
@@ -184,6 +218,8 @@ static const char *const side_opt[] = {
 	"text",
 	NULL};
 
+static int side_fields_ref = LUA_NOREF;
+
 enum vertex_e {
 	vertex_valid = 0,
 	vertex_x,
@@ -204,6 +240,8 @@ static const char *const vertex_opt[] = {
 	"ceilingzset",
 	NULL};
 
+static int vertex_fields_ref = LUA_NOREF;
+
 enum ffloor_e {
 	ffloor_valid = 0,
 	ffloor_topheight,
@@ -256,6 +294,8 @@ static const char *const ffloor_opt[] = {
 	"bouncestrength",
 	NULL};
 
+static int ffloor_fields_ref = LUA_NOREF;
+
 #ifdef HAVE_LUA_SEGS
 enum seg_e {
 	seg_valid = 0,
@@ -285,6 +325,8 @@ static const char *const seg_opt[] = {
 	"polyseg",
 	NULL};
 
+static int seg_fields_ref = LUA_NOREF;
+
 enum node_e {
 	node_valid = 0,
 	node_x,
@@ -305,6 +347,8 @@ static const char *const node_opt[] = {
 	"children",
 	NULL};
 
+static int node_fields_ref = LUA_NOREF;
+
 enum nodechild_e {
 	nodechild_valid = 0,
 	nodechild_right,
@@ -356,6 +400,8 @@ static const char *const slope_opt[] = {
 	"flags",
 	NULL};
 
+static int slope_fields_ref = LUA_NOREF;
+
 // shared by both vector2_t and vector3_t
 enum vector_e {
 	vector_x = 0,
@@ -575,7 +621,7 @@ static int sectorlines_num(lua_State *L)
 static int sector_get(lua_State *L)
 {
 	sector_t *sector = *((sector_t **)luaL_checkudata(L, 1, META_SECTOR));
-	enum sector_e field = luaL_checkoption(L, 2, sector_opt[0], sector_opt);
+	enum sector_e field = Lua_optoption(L, 2, sector_valid, sector_fields_ref);
 	INT16 i;
 
 	if (!sector)
@@ -607,6 +653,21 @@ static int sector_get(lua_State *L)
 		lua_pushlstring(L, levelflat->name, i);
 		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: 
+	{
+		lua_pushangle(L, sector->floorangle);
+		return 1;
+	}	
 	case sector_ceilingpic: // ceilingpic
 	{
 		levelflat_t *levelflat = &levelflats[sector->ceilingpic];
@@ -616,6 +677,21 @@ static int sector_get(lua_State *L)
 		lua_pushlstring(L, levelflat->name, i);
 		return 1;
 	}
+	case sector_ceilingxoffset:
+	{
+		lua_pushfixed(L, sector->ceilingxoffset);
+		return 1;
+	}
+	case sector_ceilingyoffset:
+	{
+		lua_pushfixed(L, sector->ceilingyoffset);
+		return 1;
+	}
+	case sector_ceilingangle:
+	{
+		lua_pushangle(L, sector->ceilingangle);
+		return 1;
+	}	
 	case sector_lightlevel:
 		lua_pushinteger(L, sector->lightlevel);
 		return 1;
@@ -625,12 +701,18 @@ static int sector_get(lua_State *L)
 	case sector_floorlightabsolute:
 		lua_pushboolean(L, sector->floorlightabsolute);
 		return 1;
+	case sector_floorlightsec:
+		lua_pushinteger(L, sector->floorlightsec);
+		return 1;		
 	case sector_ceilinglightlevel:
 		lua_pushinteger(L, sector->ceilinglightlevel);
 		return 1;
 	case sector_ceilinglightabsolute:
 		lua_pushboolean(L, sector->ceilinglightabsolute);
 		return 1;
+	case sector_ceilinglightsec:
+		lua_pushinteger(L, sector->ceilinglightsec);
+		return 1;		
 	case sector_special:
 		lua_pushinteger(L, sector->special);
 		return 1;
@@ -697,7 +779,7 @@ static int sector_get(lua_State *L)
 static int sector_set(lua_State *L)
 {
 	sector_t *sector = *((sector_t **)luaL_checkudata(L, 1, META_SECTOR));
-	enum sector_e field = luaL_checkoption(L, 2, sector_opt[0], sector_opt);
+	enum sector_e field = Lua_optoption(L, 2, sector_valid, sector_fields_ref);
 
 	if (!sector)
 		return luaL_error(L, "accessed sector_t doesn't exist anymore.");
@@ -718,8 +800,9 @@ static int sector_set(lua_State *L)
 	case sector_fslope: // f_slope
 	case sector_cslope: // c_slope
 	case sector_friction: // friction
-	default:
 		return luaL_error(L, "sector_t field " LUA_QS " cannot be set.", sector_opt[field]);
+	default:
+		return luaL_error(L, "sector_t has no field named " LUA_QS ".", lua_tostring(L, 2));
 	case sector_floorheight: { // floorheight
 		boolean flag;
 		mobj_t *ptmthing = tmthing;
@@ -751,9 +834,27 @@ static int sector_set(lua_State *L)
 	case sector_floorpic:
 		sector->floorpic = P_AddLevelFlatRuntime(luaL_checkstring(L, 3));
 		break;
+	case sector_floorxoffset:
+		sector->floorxoffset = luaL_checkfixed(L, 3);
+		break;
+	case sector_flooryoffset:
+		sector->flooryoffset = luaL_checkfixed(L, 3);
+		break;
+	case sector_floorangle:
+		sector->floorangle = luaL_checkangle(L, 3);
+		break;				
 	case sector_ceilingpic:
 		sector->ceilingpic = P_AddLevelFlatRuntime(luaL_checkstring(L, 3));
 		break;
+	case sector_ceilingxoffset:
+		sector->ceilingxoffset = luaL_checkfixed(L, 3);
+		break;
+	case sector_ceilingyoffset:
+		sector->ceilingyoffset = luaL_checkfixed(L, 3);
+		break;
+	case sector_ceilingangle:
+		sector->ceilingangle = luaL_checkangle(L, 3);
+		break;
 	case sector_lightlevel:
 		sector->lightlevel = (INT16)luaL_checkinteger(L, 3);
 		break;
@@ -763,12 +864,18 @@ static int sector_set(lua_State *L)
 	case sector_floorlightabsolute:
 		sector->floorlightabsolute = luaL_checkboolean(L, 3);
 		break;
+	case sector_floorlightsec:
+		sector->floorlightsec = (INT32)luaL_checkinteger(L, 3);
+		break;		
 	case sector_ceilinglightlevel:
 		sector->ceilinglightlevel = (INT16)luaL_checkinteger(L, 3);
 		break;
 	case sector_ceilinglightabsolute:
 		sector->ceilinglightabsolute = luaL_checkboolean(L, 3);
 		break;
+	case sector_ceilinglightsec:
+		sector->ceilinglightsec = (INT32)luaL_checkinteger(L, 3);
+		break;		
 	case sector_special:
 		sector->special = (INT16)luaL_checkinteger(L, 3);
 		break;
@@ -814,7 +921,7 @@ static int sector_num(lua_State *L)
 static int subsector_get(lua_State *L)
 {
 	subsector_t *subsector = *((subsector_t **)luaL_checkudata(L, 1, META_SUBSECTOR));
-	enum subsector_e field = luaL_checkoption(L, 2, subsector_opt[0], subsector_opt);
+	enum subsector_e field = Lua_optoption(L, 2, subsector_valid, subsector_fields_ref);
 
 	if (!subsector)
 	{
@@ -898,7 +1005,7 @@ static int linestringargs_len(lua_State *L)
 static int line_get(lua_State *L)
 {
 	line_t *line = *((line_t **)luaL_checkudata(L, 1, META_LINE));
-	enum line_e field = luaL_checkoption(L, 2, line_opt[0], line_opt);
+	enum line_e field = Lua_optoption(L, 2, line_valid, line_fields_ref);
 
 	if (!line)
 	{
@@ -1002,8 +1109,24 @@ static int line_get(lua_State *L)
 		LUA_PushUserdata(L, line->polyobj, META_POLYOBJ);
 		return 1;
 	case line_text:
-		lua_pushstring(L, line->text);
-		return 1;
+		{
+			if (udmf)
+			{
+				LUA_Deprecated(L, "(linedef_t).text", "(linedef_t).stringargs");
+				lua_pushnil(L);
+				return 1;
+			}
+
+			if (line->special == 331 || line->special == 443)
+			{
+				// See P_ProcessLinedefsAfterSidedefs, P_ConvertBinaryLinedefTypes
+				lua_pushstring(L, line->stringargs[0]);
+			}
+			else
+				lua_pushnil(L);
+
+			return 1;
+		}
 	case line_callcount:
 		lua_pushinteger(L, line->callcount);
 		return 1;
@@ -1057,7 +1180,7 @@ static int sidenum_get(lua_State *L)
 static int side_get(lua_State *L)
 {
 	side_t *side = *((side_t **)luaL_checkudata(L, 1, META_SIDE));
-	enum side_e field = luaL_checkoption(L, 2, side_opt[0], side_opt);
+	enum side_e field = Lua_optoption(L, 2, side_valid, side_fields_ref);
 
 	if (!side)
 	{
@@ -1079,6 +1202,24 @@ static int side_get(lua_State *L)
 	case side_rowoffset:
 		lua_pushfixed(L, side->rowoffset);
 		return 1;
+	case side_offsetx_top:
+		lua_pushfixed(L, side->offsetx_top);
+		return 1;
+	case side_offsety_top:
+		lua_pushfixed(L, side->offsety_top);
+		return 1;
+	case side_offsetx_mid:
+		lua_pushfixed(L, side->offsetx_mid);
+		return 1;
+	case side_offsety_mid:
+		lua_pushfixed(L, side->offsety_mid);
+		return 1;
+	case side_offsetx_bot:
+		lua_pushfixed(L, side->offsetx_bot);
+		return 1;
+	case side_offsety_bot:
+		lua_pushfixed(L, side->offsety_bot);
+		return 1;
 	case side_toptexture:
 		lua_pushinteger(L, side->toptexture);
 		return 1;
@@ -1101,8 +1242,19 @@ static int side_get(lua_State *L)
 		lua_pushinteger(L, side->repeatcnt);
 		return 1;
 	case side_text:
-		lua_pushstring(L, side->text);
-		return 1;
+		{
+			if (udmf)
+			{
+				LUA_Deprecated(L, "(sidedef_t).text", "(sidedef_t).line.stringargs");
+				lua_pushnil(L);
+				return 1;
+			}
+
+			boolean isfrontside = side->line->sidenum[0] == side-sides;
+
+			lua_pushstring(L, side->line->stringargs[isfrontside ? 0 : 1]);
+			return 1;
+		}
 	}
 	return 0;
 }
@@ -1110,7 +1262,7 @@ static int side_get(lua_State *L)
 static int side_set(lua_State *L)
 {
 	side_t *side = *((side_t **)luaL_checkudata(L, 1, META_SIDE));
-	enum side_e field = luaL_checkoption(L, 2, side_opt[0], side_opt);
+	enum side_e field = Lua_optoption(L, 2, side_valid, side_fields_ref);
 
 	if (!side)
 	{
@@ -1128,14 +1280,33 @@ static int side_set(lua_State *L)
 	case side_sector:
 	case side_special:
 	case side_text:
-	default:
 		return luaL_error(L, "side_t field " LUA_QS " cannot be set.", side_opt[field]);
+	default:
+		return luaL_error(L, "side_t has no field named " LUA_QS ".", lua_tostring(L, 2));
 	case side_textureoffset:
 		side->textureoffset = luaL_checkfixed(L, 3);
 		break;
 	case side_rowoffset:
 		side->rowoffset = luaL_checkfixed(L, 3);
 		break;
+	case side_offsetx_top:
+		side->offsetx_top = luaL_checkfixed(L, 3);
+		break;
+	case side_offsety_top:
+		side->offsety_top = luaL_checkfixed(L, 3);
+		break;
+	case side_offsetx_mid:
+		side->offsetx_mid = luaL_checkfixed(L, 3);
+		break;
+	case side_offsety_mid:
+		side->offsety_mid = luaL_checkfixed(L, 3);
+		break;
+	case side_offsetx_bot:
+		side->offsetx_bot = luaL_checkfixed(L, 3);
+		break;
+	case side_offsety_bot:
+		side->offsety_bot = luaL_checkfixed(L, 3);
+		break;
 	case side_toptexture:
 		side->toptexture = luaL_checkinteger(L, 3);
 		break;
@@ -1166,7 +1337,7 @@ static int side_num(lua_State *L)
 static int vertex_get(lua_State *L)
 {
 	vertex_t *vertex = *((vertex_t **)luaL_checkudata(L, 1, META_VERTEX));
-	enum vertex_e field = luaL_checkoption(L, 2, vertex_opt[0], vertex_opt);
+	enum vertex_e field = Lua_optoption(L, 2, vertex_valid, vertex_fields_ref);
 
 	if (!vertex)
 	{
@@ -1220,7 +1391,7 @@ static int vertex_num(lua_State *L)
 static int seg_get(lua_State *L)
 {
 	seg_t *seg = *((seg_t **)luaL_checkudata(L, 1, META_SEG));
-	enum seg_e field = luaL_checkoption(L, 2, seg_opt[0], seg_opt);
+	enum seg_e field = Lua_optoption(L, 2, seg_valid, seg_fields_ref);
 
 	if (!seg)
 	{
@@ -1284,7 +1455,7 @@ static int seg_num(lua_State *L)
 static int node_get(lua_State *L)
 {
 	node_t *node = *((node_t **)luaL_checkudata(L, 1, META_NODE));
-	enum node_e field = luaL_checkoption(L, 2, node_opt[0], node_opt);
+	enum node_e field = Lua_optoption(L, 2, node_valid, node_fields_ref);
 
 	if (!node)
 	{
@@ -1920,7 +2091,7 @@ static INT32 P_GetOldFOFFlags(ffloor_t *fflr)
 static int ffloor_get(lua_State *L)
 {
 	ffloor_t *ffloor = *((ffloor_t **)luaL_checkudata(L, 1, META_FFLOOR));
-	enum ffloor_e field = luaL_checkoption(L, 2, ffloor_opt[0], ffloor_opt);
+	enum ffloor_e field = Lua_optoption(L, 2, ffloor_valid, ffloor_fields_ref);
 	INT16 i;
 
 	if (!ffloor)
@@ -2102,7 +2273,7 @@ static void P_SetOldFOFFlags(ffloor_t *fflr, oldffloortype_e oldflags)
 static int ffloor_set(lua_State *L)
 {
 	ffloor_t *ffloor = *((ffloor_t **)luaL_checkudata(L, 1, META_FFLOOR));
-	enum ffloor_e field = luaL_checkoption(L, 2, ffloor_opt[0], ffloor_opt);
+	enum ffloor_e field = Lua_optoption(L, 2, ffloor_valid, ffloor_fields_ref);
 
 	if (!ffloor)
 		return luaL_error(L, "accessed ffloor_t doesn't exist anymore.");
@@ -2122,8 +2293,9 @@ static int ffloor_set(lua_State *L)
 	case ffloor_target: // target
 	case ffloor_next: // next
 	case ffloor_prev: // prev
-	default:
 		return luaL_error(L, "ffloor_t field " LUA_QS " cannot be set.", ffloor_opt[field]);
+	default:
+		return luaL_error(L, "ffloor_t has no field named " LUA_QS ".", lua_tostring(L, 2));
 	case ffloor_topheight: { // topheight
 		boolean flag;
 		fixed_t lastpos = *ffloor->topheight;
@@ -2197,7 +2369,7 @@ static int ffloor_set(lua_State *L)
 static int slope_get(lua_State *L)
 {
 	pslope_t *slope = *((pslope_t **)luaL_checkudata(L, 1, META_SLOPE));
-	enum slope_e field = luaL_checkoption(L, 2, slope_opt[0], slope_opt);
+	enum slope_e field = Lua_optoption(L, 2, slope_valid, slope_fields_ref);
 
 	if (!slope)
 	{
@@ -2241,7 +2413,7 @@ static int slope_get(lua_State *L)
 static int slope_set(lua_State *L)
 {
 	pslope_t *slope = *((pslope_t **)luaL_checkudata(L, 1, META_SLOPE));
-	enum slope_e field = luaL_checkoption(L, 2, slope_opt[0], slope_opt);
+	enum slope_e field = Lua_optoption(L, 2, slope_valid, slope_fields_ref);
 
 	if (!slope)
 		return luaL_error(L, "accessed pslope_t doesn't exist anymore.");
@@ -2257,8 +2429,9 @@ static int slope_set(lua_State *L)
 	case slope_d: // d
 	case slope_flags: // flags
 	case slope_normal: // normal
-	default:
 		return luaL_error(L, "pslope_t field " LUA_QS " cannot be set.", slope_opt[field]);
+	default:
+		return luaL_error(L, "pslope_t has no field named " LUA_QS ".", lua_tostring(L, 2));
 	case slope_o: { // o
 		luaL_checktype(L, 3, LUA_TTABLE);
 
@@ -2405,116 +2578,266 @@ static int lib_nummapheaders(lua_State *L)
 // mapheader_t //
 /////////////////
 
+enum mapheaderinfo_e
+{
+	mapheaderinfo_lvlttl,
+	mapheaderinfo_subttl,
+	mapheaderinfo_actnum,
+	mapheaderinfo_typeoflevel,
+	mapheaderinfo_nextlevel,
+	mapheaderinfo_marathonnext,
+	mapheaderinfo_keywords,
+	mapheaderinfo_musname,
+	mapheaderinfo_mustrack,
+	mapheaderinfo_muspos,
+	mapheaderinfo_musinterfadeout,
+	mapheaderinfo_musintername,
+	mapheaderinfo_muspostbossname,
+	mapheaderinfo_muspostbosstrack,
+	mapheaderinfo_muspostbosspos,
+	mapheaderinfo_muspostbossfadein,
+	mapheaderinfo_musforcereset,
+	mapheaderinfo_forcecharacter,
+	mapheaderinfo_weather,
+	mapheaderinfo_skynum,
+	mapheaderinfo_skybox_scalex,
+	mapheaderinfo_skybox_scaley,
+	mapheaderinfo_skybox_scalez,
+	mapheaderinfo_interscreen,
+	mapheaderinfo_runsoc,
+	mapheaderinfo_scriptname,
+	mapheaderinfo_precutscenenum,
+	mapheaderinfo_cutscenenum,
+	mapheaderinfo_countdown,
+	mapheaderinfo_palette,
+	mapheaderinfo_numlaps,
+	mapheaderinfo_unlockrequired,
+	mapheaderinfo_levelselect,
+	mapheaderinfo_bonustype,
+	mapheaderinfo_ltzzpatch,
+	mapheaderinfo_ltzztext,
+	mapheaderinfo_ltactdiamond,
+	mapheaderinfo_maxbonuslives,
+	mapheaderinfo_levelflags,
+	mapheaderinfo_menuflags,
+	mapheaderinfo_selectheading,
+	mapheaderinfo_startrings,
+	mapheaderinfo_sstimer,
+	mapheaderinfo_ssspheres,
+	mapheaderinfo_gravity,
+};
+
+static const char *const mapheaderinfo_opt[] = {
+	"lvlttl",
+	"subttl",
+	"actnum",
+	"typeoflevel",
+	"nextlevel",
+	"marathonnext",
+	"keywords",
+	"musname",
+	"mustrack",
+	"muspos",
+	"musinterfadeout",
+	"musintername",
+	"muspostbossname",
+	"muspostbosstrack",
+	"muspostbosspos",
+	"muspostbossfadein",
+	"musforcereset",
+	"forcecharacter",
+	"weather",
+	"skynum",
+	"skybox_scalex",
+	"skybox_scaley",
+	"skybox_scalez",
+	"interscreen",
+	"runsoc",
+	"scriptname",
+	"precutscenenum",
+	"cutscenenum",
+	"countdown",
+	"palette",
+	"numlaps",
+	"unlockrequired",
+	"levelselect",
+	"bonustype",
+	"ltzzpatch",
+	"ltzztext",
+	"ltactdiamond",
+	"maxbonuslives",
+	"levelflags",
+	"menuflags",
+	"selectheading",
+	"startrings",
+	"sstimer",
+	"ssspheres",
+	"gravity",
+	NULL,
+};
+
+static int mapheaderinfo_fields_ref = LUA_NOREF;
+
 static int mapheaderinfo_get(lua_State *L)
 {
 	mapheader_t *header = *((mapheader_t **)luaL_checkudata(L, 1, META_MAPHEADER));
-	const char *field = luaL_checkstring(L, 2);
+	enum mapheaderinfo_e field = Lua_optoption(L, 2, -1, mapheaderinfo_fields_ref);
 	INT16 i;
-	if (fastcmp(field,"lvlttl"))
+
+	switch (field)
+	{
+	case mapheaderinfo_lvlttl:
 		lua_pushstring(L, header->lvlttl);
-	else if (fastcmp(field,"subttl"))
+		break;
+	case mapheaderinfo_subttl:
 		lua_pushstring(L, header->subttl);
-	else if (fastcmp(field,"actnum"))
+		break;
+	case mapheaderinfo_actnum:
 		lua_pushinteger(L, header->actnum);
-	else if (fastcmp(field,"typeoflevel"))
+		break;
+	case mapheaderinfo_typeoflevel:
 		lua_pushinteger(L, header->typeoflevel);
-	else if (fastcmp(field,"nextlevel"))
+		break;
+	case mapheaderinfo_nextlevel:
 		lua_pushinteger(L, header->nextlevel);
-	else if (fastcmp(field,"marathonnext"))
+		break;
+	case mapheaderinfo_marathonnext:
 		lua_pushinteger(L, header->marathonnext);
-	else if (fastcmp(field,"keywords"))
+		break;
+	case mapheaderinfo_keywords:
 		lua_pushstring(L, header->keywords);
-	else if (fastcmp(field,"musname"))
+		break;
+	case mapheaderinfo_musname:
 		lua_pushstring(L, header->musname);
-	else if (fastcmp(field,"mustrack"))
+		break;
+	case mapheaderinfo_mustrack:
 		lua_pushinteger(L, header->mustrack);
-	else if (fastcmp(field,"muspos"))
+		break;
+	case mapheaderinfo_muspos:
 		lua_pushinteger(L, header->muspos);
-	else if (fastcmp(field,"musinterfadeout"))
+		break;
+	case mapheaderinfo_musinterfadeout:
 		lua_pushinteger(L, header->musinterfadeout);
-	else if (fastcmp(field,"musintername"))
+		break;
+	case mapheaderinfo_musintername:
 		lua_pushstring(L, header->musintername);
-	else if (fastcmp(field,"muspostbossname"))
+		break;
+	case mapheaderinfo_muspostbossname:
 		lua_pushstring(L, header->muspostbossname);
-	else if (fastcmp(field,"muspostbosstrack"))
+		break;
+	case mapheaderinfo_muspostbosstrack:
 		lua_pushinteger(L, header->muspostbosstrack);
-	else if (fastcmp(field,"muspostbosspos"))
+		break;
+	case mapheaderinfo_muspostbosspos:
 		lua_pushinteger(L, header->muspostbosspos);
-	else if (fastcmp(field,"muspostbossfadein"))
+		break;
+	case mapheaderinfo_muspostbossfadein:
 		lua_pushinteger(L, header->muspostbossfadein);
-	else if (fastcmp(field,"musforcereset"))
+		break;
+	case mapheaderinfo_musforcereset:
 		lua_pushinteger(L, header->musforcereset);
-	else if (fastcmp(field,"forcecharacter"))
+		break;
+	case mapheaderinfo_forcecharacter:
 		lua_pushstring(L, header->forcecharacter);
-	else if (fastcmp(field,"weather"))
+		break;
+	case mapheaderinfo_weather:
 		lua_pushinteger(L, header->weather);
-	else if (fastcmp(field,"skynum"))
+		break;
+	case mapheaderinfo_skynum:
 		lua_pushinteger(L, header->skynum);
-	else if (fastcmp(field,"skybox_scalex"))
+		break;
+	case mapheaderinfo_skybox_scalex:
 		lua_pushinteger(L, header->skybox_scalex);
-	else if (fastcmp(field,"skybox_scaley"))
+		break;
+	case mapheaderinfo_skybox_scaley:
 		lua_pushinteger(L, header->skybox_scaley);
-	else if (fastcmp(field,"skybox_scalez"))
+		break;
+	case mapheaderinfo_skybox_scalez:
 		lua_pushinteger(L, header->skybox_scalez);
-	else if (fastcmp(field,"interscreen")) {
+		break;
+	case mapheaderinfo_interscreen:
 		for (i = 0; i < 8; i++)
 			if (!header->interscreen[i])
 				break;
 		lua_pushlstring(L, header->interscreen, i);
-	} else if (fastcmp(field,"runsoc"))
+		break;
+	case mapheaderinfo_runsoc:
 		lua_pushstring(L, header->runsoc);
-	else if (fastcmp(field,"scriptname"))
+		break;
+	case mapheaderinfo_scriptname:
 		lua_pushstring(L, header->scriptname);
-	else if (fastcmp(field,"precutscenenum"))
+		break;
+	case mapheaderinfo_precutscenenum:
 		lua_pushinteger(L, header->precutscenenum);
-	else if (fastcmp(field,"cutscenenum"))
+		break;
+	case mapheaderinfo_cutscenenum:
 		lua_pushinteger(L, header->cutscenenum);
-	else if (fastcmp(field,"countdown"))
+		break;
+	case mapheaderinfo_countdown:
 		lua_pushinteger(L, header->countdown);
-	else if (fastcmp(field,"palette"))
+		break;
+	case mapheaderinfo_palette:
 		lua_pushinteger(L, header->palette);
-	else if (fastcmp(field,"numlaps"))
+		break;
+	case mapheaderinfo_numlaps:
 		lua_pushinteger(L, header->numlaps);
-	else if (fastcmp(field,"unlockrequired"))
+		break;
+	case mapheaderinfo_unlockrequired:
 		lua_pushinteger(L, header->unlockrequired);
-	else if (fastcmp(field,"levelselect"))
+		break;
+	case mapheaderinfo_levelselect:
 		lua_pushinteger(L, header->levelselect);
-	else if (fastcmp(field,"bonustype"))
+		break;
+	case mapheaderinfo_bonustype:
 		lua_pushinteger(L, header->bonustype);
-	else if (fastcmp(field,"ltzzpatch"))
+		break;
+	case mapheaderinfo_ltzzpatch:
 		lua_pushstring(L, header->ltzzpatch);
-	else if (fastcmp(field,"ltzztext"))
+		break;
+	case mapheaderinfo_ltzztext:
 		lua_pushstring(L, header->ltzztext);
-	else if (fastcmp(field,"ltactdiamond"))
+		break;
+	case mapheaderinfo_ltactdiamond:
 		lua_pushstring(L, header->ltactdiamond);
-	else if (fastcmp(field,"maxbonuslives"))
+		break;
+	case mapheaderinfo_maxbonuslives:
 		lua_pushinteger(L, header->maxbonuslives);
-	else if (fastcmp(field,"levelflags"))
+		break;
+	case mapheaderinfo_levelflags:
 		lua_pushinteger(L, header->levelflags);
-	else if (fastcmp(field,"menuflags"))
+		break;
+	case mapheaderinfo_menuflags:
 		lua_pushinteger(L, header->menuflags);
-	else if (fastcmp(field,"selectheading"))
+		break;
+	case mapheaderinfo_selectheading:
 		lua_pushstring(L, header->selectheading);
-	else if (fastcmp(field,"startrings"))
+		break;
+	case mapheaderinfo_startrings:
 		lua_pushinteger(L, header->startrings);
-	else if (fastcmp(field, "sstimer"))
+		break;
+	case mapheaderinfo_sstimer:
 		lua_pushinteger(L, header->sstimer);
-	else if (fastcmp(field, "ssspheres"))
+		break;
+	case mapheaderinfo_ssspheres:
 		lua_pushinteger(L, header->ssspheres);
-	else if (fastcmp(field, "gravity"))
+		break;
+	case mapheaderinfo_gravity:
 		lua_pushfixed(L, header->gravity);
+		break;
 	// TODO add support for reading numGradedMares and grades
-	else {
+	default:
+	{
 		// Read custom vars now
 		// (note: don't include the "LUA." in your lua scripts!)
 		UINT8 j = 0;
-		for (;j < header->numCustomOptions && !fastcmp(field, header->customopts[j].option); ++j);
+		for (;j < header->numCustomOptions && !fastcmp(lua_tostring(L, 2), header->customopts[j].option); ++j);
 
 		if(j < header->numCustomOptions)
 			lua_pushstring(L, header->customopts[j].value);
 		else
 			lua_pushnil(L);
 	}
+	}
 	return 1;
 }
 
@@ -2539,6 +2862,8 @@ int LUA_MapLib(lua_State *L)
 		lua_setfield(L, -2, "__len");
 	lua_pop(L, 1);
 
+	sector_fields_ref = Lua_CreateFieldTable(L, sector_opt);
+
 	luaL_newmetatable(L, META_SUBSECTOR);
 		lua_pushcfunction(L, subsector_get);
 		lua_setfield(L, -2, "__index");
@@ -2547,6 +2872,8 @@ int LUA_MapLib(lua_State *L)
 		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");
@@ -2555,6 +2882,8 @@ int LUA_MapLib(lua_State *L)
 		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");
@@ -2587,6 +2916,8 @@ int LUA_MapLib(lua_State *L)
 		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");
@@ -2595,6 +2926,8 @@ int LUA_MapLib(lua_State *L)
 		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");
@@ -2603,6 +2936,8 @@ int LUA_MapLib(lua_State *L)
 		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);
@@ -2612,6 +2947,8 @@ int LUA_MapLib(lua_State *L)
 		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");
@@ -2620,6 +2957,8 @@ int LUA_MapLib(lua_State *L)
 		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");
@@ -2646,6 +2985,8 @@ int LUA_MapLib(lua_State *L)
 		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");
@@ -2664,6 +3005,8 @@ int LUA_MapLib(lua_State *L)
 		//lua_setfield(L, -2, "__len");
 	lua_pop(L, 1);
 
+	mapheaderinfo_fields_ref = Lua_CreateFieldTable(L, mapheaderinfo_opt);
+
 	LUA_PushTaggableObjectArray(L, "sectors",
 			lib_iterateSectors,
 			lib_getSector,
diff --git a/src/lua_mobjlib.c b/src/lua_mobjlib.c
index 5c2b8b4d3cec0be91593807114346c77f454d879..fddf958beb783e27671cb966c3afbc3e20b0c620 100644
--- a/src/lua_mobjlib.c
+++ b/src/lua_mobjlib.c
@@ -33,7 +33,8 @@ enum mobj_e {
 	mobj_angle,
 	mobj_pitch,
 	mobj_roll,
-	mobj_rollangle,
+	mobj_spriteroll,
+	mobj_rollangle, // backwards compat
 	mobj_sprite,
 	mobj_frame,
 	mobj_sprite2,
@@ -43,6 +44,8 @@ enum mobj_e {
 	mobj_spritexoffset,
 	mobj_spriteyoffset,
 	mobj_floorspriteslope,
+	mobj_drawonlyforplayer,
+	mobj_dontdrawforviewmobj,
 	mobj_touching_sectorlist,
 	mobj_subsector,
 	mobj_floorz,
@@ -110,7 +113,8 @@ static const char *const mobj_opt[] = {
 	"angle",
 	"pitch",
 	"roll",
-	"rollangle",
+	"spriteroll",
+	"rollangle", // backwards compat
 	"sprite",
 	"frame",
 	"sprite2",
@@ -120,6 +124,8 @@ static const char *const mobj_opt[] = {
 	"spritexoffset",
 	"spriteyoffset",
 	"floorspriteslope",
+	"drawonlyforplayer",
+	"dontdrawforviewmobj",
 	"touching_sectorlist",
 	"subsector",
 	"floorz",
@@ -179,10 +185,12 @@ static const char *const mobj_opt[] = {
 
 #define UNIMPLEMENTED luaL_error(L, LUA_QL("mobj_t") " field " LUA_QS " is not implemented for Lua and cannot be accessed.", mobj_opt[field])
 
+static int mobj_fields_ref = LUA_NOREF;
+
 static int mobj_get(lua_State *L)
 {
 	mobj_t *mo = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
-	enum mobj_e field = Lua_optoption(L, 2, NULL, mobj_opt);
+	enum mobj_e field = Lua_optoption(L, 2, -1, mobj_fields_ref);
 	lua_settop(L, 2);
 
 	if (!mo || !ISINLEVEL) {
@@ -227,8 +235,9 @@ static int mobj_get(lua_State *L)
 	case mobj_roll:
 		lua_pushangle(L, mo->roll);
 		break;
-	case mobj_rollangle:
-		lua_pushangle(L, mo->rollangle);
+	case mobj_spriteroll:
+	case mobj_rollangle: // backwards compat
+		lua_pushangle(L, mo->spriteroll);
 		break;
 	case mobj_sprite:
 		lua_pushinteger(L, mo->sprite);
@@ -257,6 +266,17 @@ static int mobj_get(lua_State *L)
 	case mobj_floorspriteslope:
 		LUA_PushUserdata(L, mo->floorspriteslope, META_SLOPE);
 		break;
+	case mobj_drawonlyforplayer:
+		LUA_PushUserdata(L, mo->drawonlyforplayer, META_PLAYER);
+		break;
+	case mobj_dontdrawforviewmobj:
+		if (mo->dontdrawforviewmobj && P_MobjWasRemoved(mo->dontdrawforviewmobj))
+		{ // don't put invalid mobj back into Lua.
+			P_SetTarget(&mo->dontdrawforviewmobj, NULL);
+			return 0;
+		}
+		LUA_PushUserdata(L, mo->dontdrawforviewmobj, META_MOBJ);
+		break;
 	case mobj_touching_sectorlist:
 		return UNIMPLEMENTED;
 	case mobj_subsector:
@@ -467,7 +487,7 @@ static int mobj_get(lua_State *L)
 static int mobj_set(lua_State *L)
 {
 	mobj_t *mo = *((mobj_t **)luaL_checkudata(L, 1, META_MOBJ));
-	enum mobj_e field = Lua_optoption(L, 2, mobj_opt[0], mobj_opt);
+	enum mobj_e field = Lua_optoption(L, 2, mobj_valid, mobj_fields_ref);
 	lua_settop(L, 3);
 
 	INLEVEL
@@ -516,8 +536,9 @@ static int mobj_set(lua_State *L)
 	case mobj_roll:
 		mo->roll = luaL_checkangle(L, 3);
 		break;
-	case mobj_rollangle:
-		mo->rollangle = luaL_checkangle(L, 3);
+	case mobj_spriteroll:
+	case mobj_rollangle: // backwards compat
+		mo->spriteroll = luaL_checkangle(L, 3);
 		break;
 	case mobj_sprite:
 		mo->sprite = luaL_checkinteger(L, 3);
@@ -545,6 +566,24 @@ static int mobj_set(lua_State *L)
 		break;
 	case mobj_floorspriteslope:
 		return NOSET;
+	case mobj_drawonlyforplayer:
+		if (lua_isnil(L, 3))
+			mo->drawonlyforplayer = NULL;
+		else
+		{
+			player_t *drawonlyforplayer = *((player_t **)luaL_checkudata(L, 3, META_PLAYER));
+			mo->drawonlyforplayer = drawonlyforplayer;
+		}
+		break;
+	case mobj_dontdrawforviewmobj:
+		if (lua_isnil(L, 3))
+			P_SetTarget(&mo->dontdrawforviewmobj, NULL);
+		else
+		{
+			mobj_t *dontdrawforviewmobj = *((mobj_t **)luaL_checkudata(L, 3, META_MOBJ));
+			P_SetTarget(&mo->dontdrawforviewmobj, dontdrawforviewmobj);
+		}
+		break;
 	case mobj_touching_sectorlist:
 		return UNIMPLEMENTED;
 	case mobj_subsector:
@@ -876,14 +915,59 @@ static int thingstringargs_len(lua_State *L)
 	return 1;
 }
 
+enum mapthing_e {
+	mapthing_valid = 0,
+	mapthing_x,
+	mapthing_y,
+	mapthing_angle,
+	mapthing_pitch,
+	mapthing_roll,
+	mapthing_type,
+	mapthing_options,
+	mapthing_scale,
+	mapthing_spritexscale,
+	mapthing_spriteyscale,
+	mapthing_z,
+	mapthing_extrainfo,
+	mapthing_tag,
+	mapthing_taglist,
+	mapthing_args,
+	mapthing_stringargs,
+	mapthing_mobj,
+};
+
+const char *const mapthing_opt[] = {
+	"valid",
+	"x",
+	"y",
+	"angle",
+	"pitch",
+	"roll",
+	"type",
+	"options",
+	"scale",
+	"spritexscale",
+	"spriteyscale",
+	"z",
+	"extrainfo",
+	"tag",
+	"taglist",
+	"args",
+	"stringargs",
+	"mobj",
+	NULL,
+};
+
+static int mapthing_fields_ref = LUA_NOREF;
+
 static int mapthing_get(lua_State *L)
 {
 	mapthing_t *mt = *((mapthing_t **)luaL_checkudata(L, 1, META_MAPTHING));
-	const char *field = luaL_checkstring(L, 2);
-	lua_Integer number;
+	enum mapthing_e field = Lua_optoption(L, 2, -1, mapthing_fields_ref);
+	lua_settop(L, 2);
 
 	if (!mt) {
-		if (fastcmp(field,"valid")) {
+		if (field == mapthing_valid) {
 			lua_pushboolean(L, false);
 			return 1;
 		}
@@ -892,62 +976,77 @@ static int mapthing_get(lua_State *L)
 		return 0;
 	}
 
-	if (fastcmp(field,"valid")) {
-		lua_pushboolean(L, true);
-		return 1;
-	} else if(fastcmp(field,"x"))
-		number = mt->x;
-	else if(fastcmp(field,"y"))
-		number = mt->y;
-	else if(fastcmp(field,"angle"))
-		number = mt->angle;
-	else if(fastcmp(field,"pitch"))
-		number = mt->pitch;
-	else if(fastcmp(field,"roll"))
-		number = mt->roll;
-	else if(fastcmp(field,"type"))
-		number = mt->type;
-	else if(fastcmp(field,"options"))
-		number = mt->options;
-	else if(fastcmp(field,"scale"))
-		number = mt->scale;
-	else if(fastcmp(field,"z"))
-		number = mt->z;
-	else if(fastcmp(field,"extrainfo"))
-		number = mt->extrainfo;
-	else if(fastcmp(field,"tag"))
-		number = Tag_FGet(&mt->tags);
-	else if(fastcmp(field,"taglist"))
-	{
-		LUA_PushUserdata(L, &mt->tags, META_TAGLIST);
-		return 1;
-	}
-	else if(fastcmp(field,"args"))
+	switch (field)
 	{
-		LUA_PushUserdata(L, mt->args, META_THINGARGS);
-		return 1;
-	}
-	else if(fastcmp(field,"stringargs"))
-	{
-		LUA_PushUserdata(L, mt->stringargs, META_THINGSTRINGARGS);
-		return 1;
+		case mapthing_valid:
+			lua_pushboolean(L, true);
+			break;
+		case mapthing_x:
+			lua_pushinteger(L, mt->x);
+			break;
+		case mapthing_y:
+			lua_pushinteger(L, mt->y);
+			break;
+		case mapthing_angle:
+			lua_pushinteger(L, mt->angle);
+			break;
+		case mapthing_pitch:
+			lua_pushinteger(L, mt->pitch);
+			break;
+		case mapthing_roll:
+			lua_pushinteger(L, mt->roll);
+			break;
+		case mapthing_type:
+			lua_pushinteger(L, mt->type);
+			break;
+		case mapthing_options:
+			lua_pushinteger(L, mt->options);
+			break;
+		case mapthing_scale:
+			lua_pushfixed(L, mt->scale);
+			break;
+		case mapthing_spritexscale:
+			lua_pushfixed(L, mt->spritexscale);
+			break;
+		case mapthing_spriteyscale:
+			lua_pushfixed(L, mt->spriteyscale);
+			break;
+		case mapthing_z:
+			lua_pushinteger(L, mt->z);
+			break;
+		case mapthing_extrainfo:
+			lua_pushinteger(L, mt->extrainfo);
+			break;
+		case mapthing_tag:
+			lua_pushinteger(L, Tag_FGet(&mt->tags));
+			break;
+		case mapthing_taglist:
+			LUA_PushUserdata(L, &mt->tags, META_TAGLIST);
+			break;
+		case mapthing_args:
+			LUA_PushUserdata(L, mt->args, META_THINGARGS);
+			break;
+		case mapthing_stringargs:
+			LUA_PushUserdata(L, mt->stringargs, META_THINGSTRINGARGS);
+			break;
+		case mapthing_mobj:
+			LUA_PushUserdata(L, mt->mobj, META_MOBJ);
+			break;
+		default:
+			if (devparm)
+				return luaL_error(L, LUA_QL("mapthing_t") " has no field named " LUA_QS, field);
+			else
+				return 0;
 	}
-	else if(fastcmp(field,"mobj")) {
-		LUA_PushUserdata(L, mt->mobj, META_MOBJ);
-		return 1;
-	} else if (devparm)
-		return luaL_error(L, LUA_QL("mapthing_t") " has no field named " LUA_QS, field);
-	else
-		return 0;
 
-	lua_pushinteger(L, number);
 	return 1;
 }
 
 static int mapthing_set(lua_State *L)
 {
 	mapthing_t *mt = *((mapthing_t **)luaL_checkudata(L, 1, META_MAPTHING));
-	const char *field = luaL_checkstring(L, 2);
+	enum mapthing_e field = Lua_optoption(L, 2, -1, mapthing_fields_ref);
+	lua_settop(L, 3);
 
 	if (!mt)
 		return luaL_error(L, "accessed mapthing_t doesn't exist anymore.");
@@ -957,39 +1056,60 @@ static int mapthing_set(lua_State *L)
 	if (hook_cmd_running)
 		return luaL_error(L, "Do not alter mapthing_t in CMD building code!");
 
-	if(fastcmp(field,"x"))
-		mt->x = (INT16)luaL_checkinteger(L, 3);
-	else if(fastcmp(field,"y"))
-		mt->y = (INT16)luaL_checkinteger(L, 3);
-	else if(fastcmp(field,"angle"))
-		mt->angle = (INT16)luaL_checkinteger(L, 3);
-	else if(fastcmp(field,"pitch"))
-		mt->pitch = (INT16)luaL_checkinteger(L, 3);
-	else if(fastcmp(field,"roll"))
-		mt->roll = (INT16)luaL_checkinteger(L, 3);
-	else if(fastcmp(field,"type"))
-		mt->type = (UINT16)luaL_checkinteger(L, 3);
-	else if(fastcmp(field,"options"))
-		mt->options = (UINT16)luaL_checkinteger(L, 3);
-	else if(fastcmp(field,"scale"))
-		mt->scale = luaL_checkfixed(L, 3);
-	else if(fastcmp(field,"z"))
-		mt->z = (INT16)luaL_checkinteger(L, 3);
-	else if(fastcmp(field,"extrainfo"))
+	switch (field)
 	{
-		INT32 extrainfo = luaL_checkinteger(L, 3);
-		if (extrainfo & ~15)
-			return luaL_error(L, "mapthing_t extrainfo set %d out of range (%d - %d)", extrainfo, 0, 15);
-		mt->extrainfo = (UINT8)extrainfo;
+		case mapthing_x:
+			mt->x = (INT16)luaL_checkinteger(L, 3);
+			break;
+		case mapthing_y:
+			mt->y = (INT16)luaL_checkinteger(L, 3);
+			break;
+		case mapthing_angle:
+			mt->angle = (INT16)luaL_checkinteger(L, 3);
+			break;
+		case mapthing_pitch:
+			mt->pitch = (INT16)luaL_checkinteger(L, 3);
+			break;
+		case mapthing_roll:
+			mt->roll = (INT16)luaL_checkinteger(L, 3);
+			break;
+		case mapthing_type:
+			mt->type = (UINT16)luaL_checkinteger(L, 3);
+			break;
+		case mapthing_options:
+			mt->options = (UINT16)luaL_checkinteger(L, 3);
+			break;
+		case mapthing_scale:
+			mt->scale = luaL_checkfixed(L, 3);
+			break;
+		case mapthing_spritexscale:
+			mt->spritexscale = luaL_checkfixed(L, 3);
+			break;
+		case mapthing_spriteyscale:
+			mt->spriteyscale = luaL_checkfixed(L, 3);
+			break;
+		case mapthing_z:
+			mt->z = (INT16)luaL_checkinteger(L, 3);
+			break;
+		case mapthing_extrainfo:
+		{
+			INT32 extrainfo = luaL_checkinteger(L, 3);
+			if (extrainfo & ~15)
+				return luaL_error(L, "mapthing_t extrainfo set %d out of range (%d - %d)", extrainfo, 0, 15);
+			mt->extrainfo = (UINT8)extrainfo;
+			break;
+		}
+		case mapthing_tag:
+			Tag_FSet(&mt->tags, (INT16)luaL_checkinteger(L, 3));
+			break;
+		case mapthing_taglist:
+			return LUA_ErrSetDirectly(L, "mapthing_t", "taglist");
+		case mapthing_mobj:
+			mt->mobj = *((mobj_t **)luaL_checkudata(L, 3, META_MOBJ));
+			break;
+		default:
+			return luaL_error(L, LUA_QL("mapthing_t") " has no field named " LUA_QS, field);
 	}
-	else if (fastcmp(field,"tag"))
-		Tag_FSet(&mt->tags, (INT16)luaL_checkinteger(L, 3));
-	else if (fastcmp(field,"taglist"))
-		return LUA_ErrSetDirectly(L, "mapthing_t", "taglist");
-	else if(fastcmp(field,"mobj"))
-		mt->mobj = *((mobj_t **)luaL_checkudata(L, 3, META_MOBJ));
-	else
-		return luaL_error(L, LUA_QL("mapthing_t") " has no field named " LUA_QS, field);
 
 	return 0;
 }
@@ -1051,6 +1171,8 @@ int LUA_MobjLib(lua_State *L)
 		lua_setfield(L, -2, "__newindex");
 	lua_pop(L,1);
 
+	mobj_fields_ref = Lua_CreateFieldTable(L, mobj_opt);
+
 	luaL_newmetatable(L, META_THINGARGS);
 		lua_pushcfunction(L, thingargs_get);
 		lua_setfield(L, -2, "__index");
@@ -1078,6 +1200,8 @@ int LUA_MobjLib(lua_State *L)
 		lua_setfield(L, -2, "__len");
 	lua_pop(L,1);
 
+	mapthing_fields_ref = Lua_CreateFieldTable(L, mapthing_opt);
+
 	LUA_PushTaggableObjectArray(L, "mapthings",
 			lib_iterateMapthings,
 			lib_getMapthing,
diff --git a/src/lua_playerlib.c b/src/lua_playerlib.c
index f7e14e78f96a89e490069da4fcadfffdfc59dd7c..827e5a405fffb7fa1dda25f58aea049dc244bf38 100644
--- a/src/lua_playerlib.c
+++ b/src/lua_playerlib.c
@@ -80,332 +80,784 @@ static int lib_lenPlayer(lua_State *L)
 	return 1;
 }
 
+enum player_e
+{
+	player_valid,
+	player_name,
+	player_realmo,
+	player_mo,
+	player_cmd,
+	player_playerstate,
+	player_camerascale,
+	player_shieldscale,
+	player_viewz,
+	player_viewheight,
+	player_deltaviewheight,
+	player_bob,
+	player_viewrollangle,
+	player_aiming,
+	player_drawangle,
+	player_rings,
+	player_spheres,
+	player_pity,
+	player_currentweapon,
+	player_ringweapons,
+	player_ammoremoval,
+	player_ammoremovaltimer,
+	player_ammoremovalweapon,
+	player_powers,
+	player_pflags,
+	player_panim,
+	player_flashcount,
+	player_flashpal,
+	player_skincolor,
+	player_skin,
+	player_availabilities,
+	player_score,
+	player_recordscore,
+	player_dashspeed,
+	player_normalspeed,
+	player_runspeed,
+	player_thrustfactor,
+	player_accelstart,
+	player_acceleration,
+	player_charability,
+	player_charability2,
+	player_charflags,
+	player_thokitem,
+	player_spinitem,
+	player_revitem,
+	player_followitem,
+	player_followmobj,
+	player_actionspd,
+	player_mindash,
+	player_maxdash,
+	player_jumpfactor,
+	player_height,
+	player_spinheight,
+	player_lives,
+	player_continues,
+	player_xtralife,
+	player_gotcontinue,
+	player_speed,
+	player_secondjump,
+	player_fly1,
+	player_scoreadd,
+	player_glidetime,
+	player_climbing,
+	player_deadtimer,
+	player_exiting,
+	player_homing,
+	player_dashmode,
+	player_skidtime,
+	player_cmomx,
+	player_cmomy,
+	player_rmomx,
+	player_rmomy,
+	player_numboxes,
+	player_totalring,
+	player_realtime,
+	player_laps,
+	player_ctfteam,
+	player_gotflag,
+	player_weapondelay,
+	player_tossdelay,
+	player_starpostx,
+	player_starposty,
+	player_starpostz,
+	player_starpostnum,
+	player_starposttime,
+	player_starpostangle,
+	player_starpostscale,
+	player_angle_pos,
+	player_old_angle_pos,
+	player_axis1,
+	player_axis2,
+	player_bumpertime,
+	player_flyangle,
+	player_drilltimer,
+	player_linkcount,
+	player_linktimer,
+	player_anotherflyangle,
+	player_nightstime,
+	player_drillmeter,
+	player_drilldelay,
+	player_bonustime,
+	player_capsule,
+	player_drone,
+	player_oldscale,
+	player_mare,
+	player_marelap,
+	player_marebonuslap,
+	player_marebegunat,
+	player_startedtime,
+	player_finishedtime,
+	player_lapbegunat,
+	player_lapstartedtime,
+	player_finishedspheres,
+	player_finishedrings,
+	player_marescore,
+	player_lastmarescore,
+	player_totalmarescore,
+	player_lastmare,
+	player_lastmarelap,
+	player_lastmarebonuslap,
+	player_totalmarelap,
+	player_totalmarebonuslap,
+	player_maxlink,
+	player_texttimer,
+	player_textvar,
+	player_lastsidehit,
+	player_lastlinehit,
+	player_losstime,
+	player_timeshit,
+	player_onconveyor,
+	player_awayviewmobj,
+	player_awayviewtics,
+	player_awayviewaiming,
+	player_spectator,
+	player_outofcoop,
+	player_bot,
+	player_botleader,
+	player_lastbuttons,
+	player_blocked,
+	player_jointime,
+	player_quittime,
+	player_ping,
+#ifdef HWRENDER
+	player_fovadd,
+#endif
+};
+
+static const char *const player_opt[] = {
+	"valid",
+	"name",
+	"realmo",
+	"mo",
+	"cmd",
+	"playerstate",
+	"camerascale",
+	"shieldscale",
+	"viewz",
+	"viewheight",
+	"deltaviewheight",
+	"bob",
+	"viewrollangle",
+	"aiming",
+	"drawangle",
+	"rings",
+	"spheres",
+	"pity",
+	"currentweapon",
+	"ringweapons",
+	"ammoremoval",
+	"ammoremovaltimer",
+	"ammoremovalweapon",
+	"powers",
+	"pflags",
+	"panim",
+	"flashcount",
+	"flashpal",
+	"skincolor",
+	"skin",
+	"availabilities",
+	"score",
+	"recordscore",
+	"dashspeed",
+	"normalspeed",
+	"runspeed",
+	"thrustfactor",
+	"accelstart",
+	"acceleration",
+	"charability",
+	"charability2",
+	"charflags",
+	"thokitem",
+	"spinitem",
+	"revitem",
+	"followitem",
+	"followmobj",
+	"actionspd",
+	"mindash",
+	"maxdash",
+	"jumpfactor",
+	"height",
+	"spinheight",
+	"lives",
+	"continues",
+	"xtralife",
+	"gotcontinue",
+	"speed",
+	"secondjump",
+	"fly1",
+	"scoreadd",
+	"glidetime",
+	"climbing",
+	"deadtimer",
+	"exiting",
+	"homing",
+	"dashmode",
+	"skidtime",
+	"cmomx",
+	"cmomy",
+	"rmomx",
+	"rmomy",
+	"numboxes",
+	"totalring",
+	"realtime",
+	"laps",
+	"ctfteam",
+	"gotflag",
+	"weapondelay",
+	"tossdelay",
+	"starpostx",
+	"starposty",
+	"starpostz",
+	"starpostnum",
+	"starposttime",
+	"starpostangle",
+	"starpostscale",
+	"angle_pos",
+	"old_angle_pos",
+	"axis1",
+	"axis2",
+	"bumpertime",
+	"flyangle",
+	"drilltimer",
+	"linkcount",
+	"linktimer",
+	"anotherflyangle",
+	"nightstime",
+	"drillmeter",
+	"drilldelay",
+	"bonustime",
+	"capsule",
+	"drone",
+	"oldscale",
+	"mare",
+	"marelap",
+	"marebonuslap",
+	"marebegunat",
+	"startedtime",
+	"finishedtime",
+	"lapbegunat",
+	"lapstartedtime",
+	"finishedspheres",
+	"finishedrings",
+	"marescore",
+	"lastmarescore",
+	"totalmarescore",
+	"lastmare",
+	"lastmarelap",
+	"lastmarebonuslap",
+	"totalmarelap",
+	"totalmarebonuslap",
+	"maxlink",
+	"texttimer",
+	"textvar",
+	"lastsidehit",
+	"lastlinehit",
+	"losstime",
+	"timeshit",
+	"onconveyor",
+	"awayviewmobj",
+	"awayviewtics",
+	"awayviewaiming",
+	"spectator",
+	"outofcoop",
+	"bot",
+	"botleader",
+	"lastbuttons",
+	"blocked",
+	"jointime",
+	"quittime",
+	"ping",
+#ifdef HWRENDER
+	"fovadd",
+#endif
+	NULL,
+};
+
+static int player_fields_ref = LUA_NOREF;
+
 static int player_get(lua_State *L)
 {
 	player_t *plr = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
-	const char *field = luaL_checkstring(L, 2);
+	enum player_e field = Lua_optoption(L, 2, -1, player_fields_ref);
+	lua_settop(L, 2);
 
-	if (!plr) {
-		if (fastcmp(field,"valid")) {
+	if (!plr)
+	{
+		if (field == player_valid)
+		{
 			lua_pushboolean(L, false);
 			return 1;
 		}
 		return LUA_ErrInvalid(L, "player_t");
 	}
 
-	if (fastcmp(field,"valid"))
+	switch (field)
+	{
+	case player_valid:
 		lua_pushboolean(L, true);
-	else if (fastcmp(field,"name"))
+		break;
+	case player_name:
 		lua_pushstring(L, player_names[plr-players]);
-	else if (fastcmp(field,"realmo"))
+		break;
+	case player_realmo:
 		LUA_PushUserdata(L, plr->mo, META_MOBJ);
+		break;
 	// Kept for backward-compatibility
 	// Should be fixed to work like "realmo" later
-	else if (fastcmp(field,"mo"))
-	{
+	case player_mo:
 		if (plr->spectator)
 			lua_pushnil(L);
 		else
 			LUA_PushUserdata(L, plr->mo, META_MOBJ);
-	}
-	else if (fastcmp(field,"cmd"))
+		break;
+	case player_cmd:
 		LUA_PushUserdata(L, &plr->cmd, META_TICCMD);
-	else if (fastcmp(field,"playerstate"))
+		break;
+	case player_playerstate:
 		lua_pushinteger(L, plr->playerstate);
-	else if (fastcmp(field,"camerascale"))
+		break;
+	case player_camerascale:
 		lua_pushfixed(L, plr->camerascale);
-	else if (fastcmp(field,"shieldscale"))
+		break;
+	case player_shieldscale:
 		lua_pushfixed(L, plr->shieldscale);
-	else if (fastcmp(field,"viewz"))
+		break;
+	case player_viewz:
 		lua_pushfixed(L, plr->viewz);
-	else if (fastcmp(field,"viewheight"))
+		break;
+	case player_viewheight:
 		lua_pushfixed(L, plr->viewheight);
-	else if (fastcmp(field,"deltaviewheight"))
+		break;
+	case player_deltaviewheight:
 		lua_pushfixed(L, plr->deltaviewheight);
-	else if (fastcmp(field,"bob"))
+		break;
+	case player_bob:
 		lua_pushfixed(L, plr->bob);
-	else if (fastcmp(field,"viewrollangle"))
+		break;
+	case player_viewrollangle:
 		lua_pushangle(L, plr->viewrollangle);
-	else if (fastcmp(field,"aiming"))
+		break;
+	case player_aiming:
 		lua_pushangle(L, plr->aiming);
-	else if (fastcmp(field,"drawangle"))
+		break;
+	case player_drawangle:
 		lua_pushangle(L, plr->drawangle);
-	else if (fastcmp(field,"rings"))
+		break;
+	case player_rings:
 		lua_pushinteger(L, plr->rings);
-	else if (fastcmp(field,"spheres"))
+		break;
+	case player_spheres:
 		lua_pushinteger(L, plr->spheres);
-	else if (fastcmp(field,"pity"))
+		break;
+	case player_pity:
 		lua_pushinteger(L, plr->pity);
-	else if (fastcmp(field,"currentweapon"))
+		break;
+	case player_currentweapon:
 		lua_pushinteger(L, plr->currentweapon);
-	else if (fastcmp(field,"ringweapons"))
+		break;
+	case player_ringweapons:
 		lua_pushinteger(L, plr->ringweapons);
-	else if (fastcmp(field,"ammoremoval"))
+		break;
+	case player_ammoremoval:
 		lua_pushinteger(L, plr->ammoremoval);
-	else if (fastcmp(field,"ammoremovaltimer"))
+		break;
+	case player_ammoremovaltimer:
 		lua_pushinteger(L, plr->ammoremovaltimer);
-	else if (fastcmp(field,"ammoremovalweapon"))
+		break;
+	case player_ammoremovalweapon:
 		lua_pushinteger(L, plr->ammoremovalweapon);
-	else if (fastcmp(field,"powers"))
+		break;
+	case player_powers:
 		LUA_PushUserdata(L, plr->powers, META_POWERS);
-	else if (fastcmp(field,"pflags"))
+		break;
+	case player_pflags:
 		lua_pushinteger(L, plr->pflags);
-	else if (fastcmp(field,"panim"))
+		break;
+	case player_panim:
 		lua_pushinteger(L, plr->panim);
-	else if (fastcmp(field,"flashcount"))
+		break;
+	case player_flashcount:
 		lua_pushinteger(L, plr->flashcount);
-	else if (fastcmp(field,"flashpal"))
+		break;
+	case player_flashpal:
 		lua_pushinteger(L, plr->flashpal);
-	else if (fastcmp(field,"skincolor"))
+		break;
+	case player_skincolor:
 		lua_pushinteger(L, plr->skincolor);
-	else if (fastcmp(field,"skin"))
+		break;
+	case player_skin:
 		lua_pushinteger(L, plr->skin);
-	else if (fastcmp(field,"availabilities"))
+		break;
+	case player_availabilities:
 		lua_pushinteger(L, plr->availabilities);
-	else if (fastcmp(field,"score"))
+		break;
+	case player_score:
 		lua_pushinteger(L, plr->score);
-	else if (fastcmp(field,"dashspeed"))
+		break;
+	case player_recordscore:
+		lua_pushinteger(L, plr->recordscore);
+		break;
+	case player_dashspeed:
 		lua_pushfixed(L, plr->dashspeed);
-	else if (fastcmp(field,"normalspeed"))
+		break;
+	case player_normalspeed:
 		lua_pushfixed(L, plr->normalspeed);
-	else if (fastcmp(field,"runspeed"))
+		break;
+	case player_runspeed:
 		lua_pushfixed(L, plr->runspeed);
-	else if (fastcmp(field,"thrustfactor"))
+		break;
+	case player_thrustfactor:
 		lua_pushinteger(L, plr->thrustfactor);
-	else if (fastcmp(field,"accelstart"))
+		break;
+	case player_accelstart:
 		lua_pushinteger(L, plr->accelstart);
-	else if (fastcmp(field,"acceleration"))
+		break;
+	case player_acceleration:
 		lua_pushinteger(L, plr->acceleration);
-	else if (fastcmp(field,"charability"))
+		break;
+	case player_charability:
 		lua_pushinteger(L, plr->charability);
-	else if (fastcmp(field,"charability2"))
+		break;
+	case player_charability2:
 		lua_pushinteger(L, plr->charability2);
-	else if (fastcmp(field,"charflags"))
+		break;
+	case player_charflags:
 		lua_pushinteger(L, plr->charflags);
-	else if (fastcmp(field,"thokitem"))
+		break;
+	case player_thokitem:
 		lua_pushinteger(L, plr->thokitem);
-	else if (fastcmp(field,"spinitem"))
+		break;
+	case player_spinitem:
 		lua_pushinteger(L, plr->spinitem);
-	else if (fastcmp(field,"revitem"))
+		break;
+	case player_revitem:
 		lua_pushinteger(L, plr->revitem);
-	else if (fastcmp(field,"followitem"))
+		break;
+	case player_followitem:
 		lua_pushinteger(L, plr->followitem);
-	else if (fastcmp(field,"followmobj"))
+		break;
+	case player_followmobj:
 		LUA_PushUserdata(L, plr->followmobj, META_MOBJ);
-	else if (fastcmp(field,"actionspd"))
+		break;
+	case player_actionspd:
 		lua_pushfixed(L, plr->actionspd);
-	else if (fastcmp(field,"mindash"))
+		break;
+	case player_mindash:
 		lua_pushfixed(L, plr->mindash);
-	else if (fastcmp(field,"maxdash"))
+		break;
+	case player_maxdash:
 		lua_pushfixed(L, plr->maxdash);
-	else if (fastcmp(field,"jumpfactor"))
+		break;
+	case player_jumpfactor:
 		lua_pushfixed(L, plr->jumpfactor);
-	else if (fastcmp(field,"height"))
+		break;
+	case player_height:
 		lua_pushfixed(L, plr->height);
-	else if (fastcmp(field,"spinheight"))
+		break;
+	case player_spinheight:
 		lua_pushfixed(L, plr->spinheight);
-	else if (fastcmp(field,"lives"))
+		break;
+	case player_lives:
 		lua_pushinteger(L, plr->lives);
-	else if (fastcmp(field,"continues"))
+		break;
+	case player_continues:
 		lua_pushinteger(L, plr->continues);
-	else if (fastcmp(field,"xtralife"))
+		break;
+	case player_xtralife:
 		lua_pushinteger(L, plr->xtralife);
-	else if (fastcmp(field,"gotcontinue"))
+		break;
+	case player_gotcontinue:
 		lua_pushinteger(L, plr->gotcontinue);
-	else if (fastcmp(field,"speed"))
+		break;
+	case player_speed:
 		lua_pushfixed(L, plr->speed);
-	else if (fastcmp(field,"secondjump"))
+		break;
+	case player_secondjump:
 		lua_pushinteger(L, plr->secondjump);
-	else if (fastcmp(field,"fly1"))
+		break;
+	case player_fly1:
 		lua_pushinteger(L, plr->fly1);
-	else if (fastcmp(field,"scoreadd"))
+		break;
+	case player_scoreadd:
 		lua_pushinteger(L, plr->scoreadd);
-	else if (fastcmp(field,"glidetime"))
+		break;
+	case player_glidetime:
 		lua_pushinteger(L, plr->glidetime);
-	else if (fastcmp(field,"climbing"))
+		break;
+	case player_climbing:
 		lua_pushinteger(L, plr->climbing);
-	else if (fastcmp(field,"deadtimer"))
+		break;
+	case player_deadtimer:
 		lua_pushinteger(L, plr->deadtimer);
-	else if (fastcmp(field,"exiting"))
+		break;
+	case player_exiting:
 		lua_pushinteger(L, plr->exiting);
-	else if (fastcmp(field,"homing"))
+		break;
+	case player_homing:
 		lua_pushinteger(L, plr->homing);
-	else if (fastcmp(field,"dashmode"))
+		break;
+	case player_dashmode:
 		lua_pushinteger(L, plr->dashmode);
-	else if (fastcmp(field,"skidtime"))
+		break;
+	case player_skidtime:
 		lua_pushinteger(L, plr->skidtime);
-	else if (fastcmp(field,"cmomx"))
+		break;
+	case player_cmomx:
 		lua_pushfixed(L, plr->cmomx);
-	else if (fastcmp(field,"cmomy"))
+		break;
+	case player_cmomy:
 		lua_pushfixed(L, plr->cmomy);
-	else if (fastcmp(field,"rmomx"))
+		break;
+	case player_rmomx:
 		lua_pushfixed(L, plr->rmomx);
-	else if (fastcmp(field,"rmomy"))
+		break;
+	case player_rmomy:
 		lua_pushfixed(L, plr->rmomy);
-	else if (fastcmp(field,"numboxes"))
+		break;
+	case player_numboxes:
 		lua_pushinteger(L, plr->numboxes);
-	else if (fastcmp(field,"totalring"))
+		break;
+	case player_totalring:
 		lua_pushinteger(L, plr->totalring);
-	else if (fastcmp(field,"realtime"))
+		break;
+	case player_realtime:
 		lua_pushinteger(L, plr->realtime);
-	else if (fastcmp(field,"laps"))
+		break;
+	case player_laps:
 		lua_pushinteger(L, plr->laps);
-	else if (fastcmp(field,"ctfteam"))
+		break;
+	case player_ctfteam:
 		lua_pushinteger(L, plr->ctfteam);
-	else if (fastcmp(field,"gotflag"))
+		break;
+	case player_gotflag:
 		lua_pushinteger(L, plr->gotflag);
-	else if (fastcmp(field,"weapondelay"))
+		break;
+	case player_weapondelay:
 		lua_pushinteger(L, plr->weapondelay);
-	else if (fastcmp(field,"tossdelay"))
+		break;
+	case player_tossdelay:
 		lua_pushinteger(L, plr->tossdelay);
-	else if (fastcmp(field,"starpostx"))
+		break;
+	case player_starpostx:
 		lua_pushinteger(L, plr->starpostx);
-	else if (fastcmp(field,"starposty"))
+		break;
+	case player_starposty:
 		lua_pushinteger(L, plr->starposty);
-	else if (fastcmp(field,"starpostz"))
+		break;
+	case player_starpostz:
 		lua_pushinteger(L, plr->starpostz);
-	else if (fastcmp(field,"starpostnum"))
+		break;
+	case player_starpostnum:
 		lua_pushinteger(L, plr->starpostnum);
-	else if (fastcmp(field,"starposttime"))
+		break;
+	case player_starposttime:
 		lua_pushinteger(L, plr->starposttime);
-	else if (fastcmp(field,"starpostangle"))
+		break;
+	case player_starpostangle:
 		lua_pushangle(L, plr->starpostangle);
-	else if (fastcmp(field,"starpostscale"))
+		break;
+	case player_starpostscale:
 		lua_pushfixed(L, plr->starpostscale);
-	else if (fastcmp(field,"angle_pos"))
+		break;
+	case player_angle_pos:
 		lua_pushangle(L, plr->angle_pos);
-	else if (fastcmp(field,"old_angle_pos"))
+		break;
+	case player_old_angle_pos:
 		lua_pushangle(L, plr->old_angle_pos);
-	else if (fastcmp(field,"axis1"))
+		break;
+	case player_axis1:
 		LUA_PushUserdata(L, plr->axis1, META_MOBJ);
-	else if (fastcmp(field,"axis2"))
+		break;
+	case player_axis2:
 		LUA_PushUserdata(L, plr->axis2, META_MOBJ);
-	else if (fastcmp(field,"bumpertime"))
+		break;
+	case player_bumpertime:
 		lua_pushinteger(L, plr->bumpertime);
-	else if (fastcmp(field,"flyangle"))
+		break;
+	case player_flyangle:
 		lua_pushinteger(L, plr->flyangle);
-	else if (fastcmp(field,"drilltimer"))
+		break;
+	case player_drilltimer:
 		lua_pushinteger(L, plr->drilltimer);
-	else if (fastcmp(field,"linkcount"))
+		break;
+	case player_linkcount:
 		lua_pushinteger(L, plr->linkcount);
-	else if (fastcmp(field,"linktimer"))
+		break;
+	case player_linktimer:
 		lua_pushinteger(L, plr->linktimer);
-	else if (fastcmp(field,"anotherflyangle"))
+		break;
+	case player_anotherflyangle:
 		lua_pushinteger(L, plr->anotherflyangle);
-	else if (fastcmp(field,"nightstime"))
+		break;
+	case player_nightstime:
 		lua_pushinteger(L, plr->nightstime);
-	else if (fastcmp(field,"drillmeter"))
+		break;
+	case player_drillmeter:
 		lua_pushinteger(L, plr->drillmeter);
-	else if (fastcmp(field,"drilldelay"))
+		break;
+	case player_drilldelay:
 		lua_pushinteger(L, plr->drilldelay);
-	else if (fastcmp(field,"bonustime"))
+		break;
+	case player_bonustime:
 		lua_pushboolean(L, plr->bonustime);
-	else if (fastcmp(field,"capsule"))
+		break;
+	case player_capsule:
 		LUA_PushUserdata(L, plr->capsule, META_MOBJ);
-	else if (fastcmp(field,"drone"))
+		break;
+	case player_drone:
 		LUA_PushUserdata(L, plr->drone, META_MOBJ);
-	else if (fastcmp(field,"oldscale"))
+		break;
+	case player_oldscale:
 		lua_pushfixed(L, plr->oldscale);
-	else if (fastcmp(field,"mare"))
+		break;
+	case player_mare:
 		lua_pushinteger(L, plr->mare);
-	else if (fastcmp(field,"marelap"))
+		break;
+	case player_marelap:
 		lua_pushinteger(L, plr->marelap);
-	else if (fastcmp(field,"marebonuslap"))
+		break;
+	case player_marebonuslap:
 		lua_pushinteger(L, plr->marebonuslap);
-	else if (fastcmp(field,"marebegunat"))
+		break;
+	case player_marebegunat:
 		lua_pushinteger(L, plr->marebegunat);
-	else if (fastcmp(field,"startedtime"))
+		break;
+	case player_startedtime:
 		lua_pushinteger(L, plr->startedtime);
-	else if (fastcmp(field,"finishedtime"))
+		break;
+	case player_finishedtime:
 		lua_pushinteger(L, plr->finishedtime);
-	else if (fastcmp(field,"lapbegunat"))
+		break;
+	case player_lapbegunat:
 		lua_pushinteger(L, plr->lapbegunat);
-	else if (fastcmp(field,"lapstartedtime"))
+		break;
+	case player_lapstartedtime:
 		lua_pushinteger(L, plr->lapstartedtime);
-	else if (fastcmp(field,"finishedspheres"))
+		break;
+	case player_finishedspheres:
 		lua_pushinteger(L, plr->finishedspheres);
-	else if (fastcmp(field,"finishedrings"))
+		break;
+	case player_finishedrings:
 		lua_pushinteger(L, plr->finishedrings);
-	else if (fastcmp(field,"marescore"))
+		break;
+	case player_marescore:
 		lua_pushinteger(L, plr->marescore);
-	else if (fastcmp(field,"lastmarescore"))
+		break;
+	case player_lastmarescore:
 		lua_pushinteger(L, plr->lastmarescore);
-	else if (fastcmp(field,"totalmarescore"))
+		break;
+	case player_totalmarescore:
 		lua_pushinteger(L, plr->totalmarescore);
-	else if (fastcmp(field,"lastmare"))
+		break;
+	case player_lastmare:
 		lua_pushinteger(L, plr->lastmare);
-	else if (fastcmp(field,"lastmarelap"))
+		break;
+	case player_lastmarelap:
 		lua_pushinteger(L, plr->lastmarelap);
-	else if (fastcmp(field,"lastmarebonuslap"))
+		break;
+	case player_lastmarebonuslap:
 		lua_pushinteger(L, plr->lastmarebonuslap);
-	else if (fastcmp(field,"totalmarelap"))
+		break;
+	case player_totalmarelap:
 		lua_pushinteger(L, plr->totalmarelap);
-	else if (fastcmp(field,"totalmarebonuslap"))
+		break;
+	case player_totalmarebonuslap:
 		lua_pushinteger(L, plr->totalmarebonuslap);
-	else if (fastcmp(field,"maxlink"))
+		break;
+	case player_maxlink:
 		lua_pushinteger(L, plr->maxlink);
-	else if (fastcmp(field,"texttimer"))
+		break;
+	case player_texttimer:
 		lua_pushinteger(L, plr->texttimer);
-	else if (fastcmp(field,"textvar"))
+		break;
+	case player_textvar:
 		lua_pushinteger(L, plr->textvar);
-	else if (fastcmp(field,"lastsidehit"))
+		break;
+	case player_lastsidehit:
 		lua_pushinteger(L, plr->lastsidehit);
-	else if (fastcmp(field,"lastlinehit"))
+		break;
+	case player_lastlinehit:
 		lua_pushinteger(L, plr->lastlinehit);
-	else if (fastcmp(field,"losstime"))
+		break;
+	case player_losstime:
 		lua_pushinteger(L, plr->losstime);
-	else if (fastcmp(field,"timeshit"))
+		break;
+	case player_timeshit:
 		lua_pushinteger(L, plr->timeshit);
-	else if (fastcmp(field,"onconveyor"))
+		break;
+	case player_onconveyor:
 		lua_pushinteger(L, plr->onconveyor);
-	else if (fastcmp(field,"awayviewmobj"))
+		break;
+	case player_awayviewmobj:
 		LUA_PushUserdata(L, plr->awayviewmobj, META_MOBJ);
-	else if (fastcmp(field,"awayviewtics"))
+		break;
+	case player_awayviewtics:
 		lua_pushinteger(L, plr->awayviewtics);
-	else if (fastcmp(field,"awayviewaiming"))
+		break;
+	case player_awayviewaiming:
 		lua_pushangle(L, plr->awayviewaiming);
-	else if (fastcmp(field,"spectator"))
+		break;
+	case player_spectator:
 		lua_pushboolean(L, plr->spectator);
-	else if (fastcmp(field,"outofcoop"))
+		break;
+	case player_outofcoop:
 		lua_pushboolean(L, plr->outofcoop);
-	else if (fastcmp(field,"bot"))
+		break;
+	case player_bot:
 		lua_pushinteger(L, plr->bot);
-	else if (fastcmp(field,"botleader"))
+		break;
+	case player_botleader:
 		LUA_PushUserdata(L, plr->botleader, META_PLAYER);
-	else if (fastcmp(field,"lastbuttons"))
+		break;
+	case player_lastbuttons:
 		lua_pushinteger(L, plr->lastbuttons);
-	else if (fastcmp(field,"blocked"))
+		break;
+	case player_blocked:
 		lua_pushboolean(L, plr->blocked);
-	else if (fastcmp(field,"jointime"))
+		break;
+	case player_jointime:
 		lua_pushinteger(L, plr->jointime);
-	else if (fastcmp(field,"quittime"))
+		break;
+	case player_quittime:
 		lua_pushinteger(L, plr->quittime);
+		break;
+	case player_ping:
+		lua_pushinteger(L, playerpingtable[plr - players]);
+		break;
 #ifdef HWRENDER
-	else if (fastcmp(field,"fovadd"))
+	case player_fovadd:
 		lua_pushfixed(L, plr->fovadd);
+		break;
 #endif
-	else {
+	default:
 		lua_getfield(L, LUA_REGISTRYINDEX, LREG_EXTVARS);
 		I_Assert(lua_istable(L, -1));
 		lua_pushlightuserdata(L, plr);
 		lua_rawget(L, -2);
 		if (!lua_istable(L, -1)) { // no extra values table
-			CONS_Debug(DBG_LUA, M_GetText("'%s' has no extvars table or field named '%s'; returning nil.\n"), "player_t", field);
+			CONS_Debug(DBG_LUA, M_GetText("'%s' has no extvars table or field named '%s'; returning nil.\n"), "player_t", lua_tostring(L, 2));
 			return 0;
 		}
-		lua_getfield(L, -1, field);
+		lua_pushvalue(L, 2); // field name
+		lua_gettable(L, -2);
 		if (lua_isnil(L, -1)) // no value for this field
-			CONS_Debug(DBG_LUA, M_GetText("'%s' has no field named '%s'; returning nil.\n"), "player_t", field);
+			CONS_Debug(DBG_LUA, M_GetText("'%s' has no field named '%s'; returning nil.\n"), "player_t", lua_tostring(L, 2));
+		break;
 	}
 
 	return 1;
 }
 
-#define NOSET luaL_error(L, LUA_QL("player_t") " field " LUA_QS " should not be set directly.", field)
+#define NOSET luaL_error(L, LUA_QL("player_t") " field " LUA_QS " should not be set directly.", player_opt[field])
 static int player_set(lua_State *L)
 {
 	player_t *plr = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
-	const char *field = luaL_checkstring(L, 2);
+	enum player_e field = Lua_optoption(L, 2, player_cmd, player_fields_ref);
 	if (!plr)
 		return LUA_ErrInvalid(L, "player_t");
 
@@ -414,337 +866,495 @@ static int player_set(lua_State *L)
 	if (hook_cmd_running)
 		return luaL_error(L, "Do not alter player_t in CMD building code!");
 
-	if (fastcmp(field,"mo") || fastcmp(field,"realmo")) {
+	switch (field)
+	{
+	case player_mo:
+	case player_realmo:
+	{
 		mobj_t *newmo = *((mobj_t **)luaL_checkudata(L, 3, META_MOBJ));
 		plr->mo->player = NULL; // remove player pointer from old mobj
 		(newmo->player = plr)->mo = newmo; // set player pointer for new mobj, and set new mobj as the player's mobj
+		break;
 	}
-	else if (fastcmp(field,"cmd"))
+	case player_cmd:
 		return NOSET;
-	else if (fastcmp(field,"playerstate"))
+	case player_playerstate:
 		plr->playerstate = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"camerascale"))
+		break;
+	case player_camerascale:
 		plr->camerascale = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"shieldscale"))
+		break;
+	case player_shieldscale:
 		plr->shieldscale = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"viewz"))
+		break;
+	case player_viewz:
 		plr->viewz = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"viewheight"))
+		break;
+	case player_viewheight:
 		plr->viewheight = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"deltaviewheight"))
+		break;
+	case player_deltaviewheight:
 		plr->deltaviewheight = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"bob"))
+		break;
+	case player_bob:
 		plr->bob = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"viewrollangle"))
+		break;
+	case player_viewrollangle:
 		plr->viewrollangle = luaL_checkangle(L, 3);
-	else if (fastcmp(field,"aiming")) {
+		break;
+	case player_aiming:
+	{
 		plr->aiming = luaL_checkangle(L, 3);
 		if (plr == &players[consoleplayer])
 			localaiming = plr->aiming;
 		else if (plr == &players[secondarydisplayplayer])
 			localaiming2 = plr->aiming;
+		break;
 	}
-	else if (fastcmp(field,"drawangle"))
+	case player_drawangle:
 		plr->drawangle = luaL_checkangle(L, 3);
-	else if (fastcmp(field,"rings"))
+		break;
+	case player_rings:
 		plr->rings = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"spheres"))
+		break;
+	case player_spheres:
 		plr->spheres = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"pity"))
+		break;
+	case player_pity:
 		plr->pity = (SINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"currentweapon"))
+		break;
+	case player_currentweapon:
 		plr->currentweapon = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"ringweapons"))
+		break;
+	case player_ringweapons:
 		plr->ringweapons = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"ammoremoval"))
+		break;
+	case player_ammoremoval:
 		plr->ammoremoval = (UINT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"ammoremovaltimer"))
+		break;
+	case player_ammoremovaltimer:
 		plr->ammoremovaltimer = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"ammoremovalweapon"))
+		break;
+	case player_ammoremovalweapon:
 		plr->ammoremovalweapon = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"powers"))
+		break;
+	case player_powers:
 		return NOSET;
-	else if (fastcmp(field,"pflags"))
+	case player_pflags:
 		plr->pflags = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"panim"))
+		break;
+	case player_panim:
 		plr->panim = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"flashcount"))
+		break;
+	case player_flashcount:
 		plr->flashcount = (UINT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"flashpal"))
+		break;
+	case player_flashpal:
 		plr->flashpal = (UINT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"skincolor"))
+		break;
+	case player_skincolor:
 	{
 		UINT16 newcolor = (UINT16)luaL_checkinteger(L,3);
 		if (newcolor >= numskincolors)
 			return luaL_error(L, "player.skincolor %d out of range (0 - %d).", newcolor, numskincolors-1);
 		plr->skincolor = newcolor;
+		break;
 	}
-	else if (fastcmp(field,"skin"))
+	case player_skin:
 		return NOSET;
-	else if (fastcmp(field,"availabilities"))
+	case player_availabilities:
 		return NOSET;
-	else if (fastcmp(field,"score"))
+	case player_score:
 		plr->score = (UINT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"dashspeed"))
+		break;
+	case player_recordscore:
+		plr->recordscore = (UINT32)luaL_checkinteger(L, 3);
+		break;
+	case player_dashspeed:
 		plr->dashspeed = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"normalspeed"))
+		break;
+	case player_normalspeed:
 		plr->normalspeed = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"runspeed"))
+		break;
+	case player_runspeed:
 		plr->runspeed = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"thrustfactor"))
+		break;
+	case player_thrustfactor:
 		plr->thrustfactor = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"accelstart"))
+		break;
+	case player_accelstart:
 		plr->accelstart = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"acceleration"))
+		break;
+	case player_acceleration:
 		plr->acceleration = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"charability"))
+		break;
+	case player_charability:
 		plr->charability = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"charability2"))
+		break;
+	case player_charability2:
 		plr->charability2 = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"charflags"))
+		break;
+	case player_charflags:
 		plr->charflags = (UINT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"thokitem"))
+		break;
+	case player_thokitem:
 		plr->thokitem = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"spinitem"))
+		break;
+	case player_spinitem:
 		plr->spinitem = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"revitem"))
+		break;
+	case player_revitem:
 		plr->revitem = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"followitem"))
+		break;
+	case player_followitem:
 		plr->followitem = luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"followmobj"))
+		break;
+	case player_followmobj:
 	{
 		mobj_t *mo = NULL;
 		if (!lua_isnil(L, 3))
 			mo = *((mobj_t **)luaL_checkudata(L, 3, META_MOBJ));
 		P_SetTarget(&plr->followmobj, mo);
+		break;
 	}
-	else if (fastcmp(field,"actionspd"))
+	case player_actionspd:
 		plr->actionspd = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"mindash"))
+		break;
+	case player_mindash:
 		plr->mindash = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"maxdash"))
+		break;
+	case player_maxdash:
 		plr->maxdash = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"jumpfactor"))
+		break;
+	case player_jumpfactor:
 		plr->jumpfactor = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"height"))
+		break;
+	case player_height:
 		plr->height = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"spinheight"))
+		break;
+	case player_spinheight:
 		plr->spinheight = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"lives"))
+		break;
+	case player_lives:
 		plr->lives = (SINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"continues"))
+		break;
+	case player_continues:
 		plr->continues = (SINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"xtralife"))
+		break;
+	case player_xtralife:
 		plr->xtralife = (SINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"gotcontinue"))
+		break;
+	case player_gotcontinue:
 		plr->gotcontinue = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"speed"))
+		break;
+	case player_speed:
 		plr->speed = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"secondjump"))
+		break;
+	case player_secondjump:
 		plr->secondjump = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"fly1"))
+		break;
+	case player_fly1:
 		plr->fly1 = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"scoreadd"))
+		break;
+	case player_scoreadd:
 		plr->scoreadd = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"glidetime"))
+		break;
+	case player_glidetime:
 		plr->glidetime = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"climbing"))
+		break;
+	case player_climbing:
 		plr->climbing = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"deadtimer"))
+		break;
+	case player_deadtimer:
 		plr->deadtimer = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"exiting"))
+		break;
+	case player_exiting:
 		plr->exiting = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"homing"))
+		break;
+	case player_homing:
 		plr->homing = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"dashmode"))
+		break;
+	case player_dashmode:
 		plr->dashmode = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"skidtime"))
+		break;
+	case player_skidtime:
 		plr->skidtime = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"cmomx"))
+		break;
+	case player_cmomx:
 		plr->cmomx = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"cmomy"))
+		break;
+	case player_cmomy:
 		plr->cmomy = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"rmomx"))
+		break;
+	case player_rmomx:
 		plr->rmomx = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"rmomy"))
+		break;
+	case player_rmomy:
 		plr->rmomy = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"numboxes"))
+		break;
+	case player_numboxes:
 		plr->numboxes = (INT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"totalring"))
+		break;
+	case player_totalring:
 		plr->totalring = (INT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"realtime"))
+		break;
+	case player_realtime:
 		plr->realtime = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"laps"))
+		break;
+	case player_laps:
 		plr->laps = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"ctfteam"))
+		break;
+	case player_ctfteam:
 		plr->ctfteam = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"gotflag"))
+		break;
+	case player_gotflag:
 		plr->gotflag = (UINT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"weapondelay"))
+		break;
+	case player_weapondelay:
 		plr->weapondelay = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"tossdelay"))
+		break;
+	case player_tossdelay:
 		plr->tossdelay = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"starpostx"))
+		break;
+	case player_starpostx:
 		plr->starpostx = (INT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"starposty"))
+		break;
+	case player_starposty:
 		plr->starposty = (INT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"starpostz"))
+		break;
+	case player_starpostz:
 		plr->starpostz = (INT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"starpostnum"))
+		break;
+	case player_starpostnum:
 		plr->starpostnum = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"starposttime"))
+		break;
+	case player_starposttime:
 		plr->starposttime = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"starpostangle"))
+		break;
+	case player_starpostangle:
 		plr->starpostangle = luaL_checkangle(L, 3);
-	else if (fastcmp(field,"starpostscale"))
+		break;
+	case player_starpostscale:
 		plr->starpostscale = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"angle_pos"))
+		break;
+	case player_angle_pos:
 		plr->angle_pos = luaL_checkangle(L, 3);
-	else if (fastcmp(field,"old_angle_pos"))
+		break;
+	case player_old_angle_pos:
 		plr->old_angle_pos = luaL_checkangle(L, 3);
-	else if (fastcmp(field,"axis1"))
+		break;
+	case player_axis1:
 	{
 		mobj_t *mo = NULL;
 		if (!lua_isnil(L, 3))
 			mo = *((mobj_t **)luaL_checkudata(L, 3, META_MOBJ));
 		P_SetTarget(&plr->axis1, mo);
+		break;
 	}
-	else if (fastcmp(field,"axis2"))
+	case player_axis2:
 	{
 		mobj_t *mo = NULL;
 		if (!lua_isnil(L, 3))
 			mo = *((mobj_t **)luaL_checkudata(L, 3, META_MOBJ));
 		P_SetTarget(&plr->axis2, mo);
+		break;
 	}
-	else if (fastcmp(field,"bumpertime"))
+	case player_bumpertime:
 		plr->bumpertime = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"flyangle"))
+		break;
+	case player_flyangle:
 		plr->flyangle = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"drilltimer"))
+		break;
+	case player_drilltimer:
 		plr->drilltimer = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"linkcount"))
+		break;
+	case player_linkcount:
 		plr->linkcount = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"linktimer"))
+		break;
+	case player_linktimer:
 		plr->linktimer = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"anotherflyangle"))
+		break;
+	case player_anotherflyangle:
 		plr->anotherflyangle = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"nightstime"))
+		break;
+	case player_nightstime:
 		plr->nightstime = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"drillmeter"))
+		break;
+	case player_drillmeter:
 		plr->drillmeter = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"drilldelay"))
+		break;
+	case player_drilldelay:
 		plr->drilldelay = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"bonustime"))
+		break;
+	case player_bonustime:
 		plr->bonustime = luaL_checkboolean(L, 3);
-	else if (fastcmp(field,"capsule"))
+		break;
+	case player_capsule:
 	{
 		mobj_t *mo = NULL;
 		if (!lua_isnil(L, 3))
 			mo = *((mobj_t **)luaL_checkudata(L, 3, META_MOBJ));
 		P_SetTarget(&plr->capsule, mo);
+		break;
 	}
-	else if (fastcmp(field,"drone"))
+	case player_drone:
 	{
 		mobj_t *mo = NULL;
 		if (!lua_isnil(L, 3))
 			mo = *((mobj_t **)luaL_checkudata(L, 3, META_MOBJ));
 		P_SetTarget(&plr->drone, mo);
+		break;
 	}
-	else if (fastcmp(field,"oldscale"))
+	case player_oldscale:
 		plr->oldscale = luaL_checkfixed(L, 3);
-	else if (fastcmp(field,"mare"))
+		break;
+	case player_mare:
 		plr->mare = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"marelap"))
+		break;
+	case player_marelap:
 		plr->marelap = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"marebonuslap"))
+		break;
+	case player_marebonuslap:
 		plr->marebonuslap = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"marebegunat"))
+		break;
+	case player_marebegunat:
 		plr->marebegunat = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"startedtime"))
+		break;
+	case player_startedtime:
 		plr->startedtime = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"finishedtime"))
+		break;
+	case player_finishedtime:
 		plr->finishedtime = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"lapbegunat"))
+		break;
+	case player_lapbegunat:
 		plr->lapbegunat = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"lapstartedtime"))
+		break;
+	case player_lapstartedtime:
 		plr->lapstartedtime = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"finishedspheres"))
+		break;
+	case player_finishedspheres:
 		plr->finishedspheres = (INT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"finishedrings"))
+		break;
+	case player_finishedrings:
 		plr->finishedrings = (INT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"marescore"))
+		break;
+	case player_marescore:
 		plr->marescore = (UINT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"lastmarescore"))
+		break;
+	case player_lastmarescore:
 		plr->lastmarescore = (UINT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"totalmarescore"))
+		break;
+	case player_totalmarescore:
 		plr->totalmarescore = (UINT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"lastmare"))
+		break;
+	case player_lastmare:
 		plr->lastmare = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"lastmarelap"))
+		break;
+	case player_lastmarelap:
 		plr->lastmarelap = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"lastmarebonuslap"))
+		break;
+	case player_lastmarebonuslap:
 		plr->lastmarebonuslap = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"totalmarelap"))
+		break;
+	case player_totalmarelap:
 		plr->totalmarelap = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"totalmarebonuslap"))
+		break;
+	case player_totalmarebonuslap:
 		plr->totalmarebonuslap = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"maxlink"))
+		break;
+	case player_maxlink:
 		plr->maxlink = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"texttimer"))
+		break;
+	case player_texttimer:
 		plr->texttimer = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"textvar"))
+		break;
+	case player_textvar:
 		plr->textvar = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"lastsidehit"))
+		break;
+	case player_lastsidehit:
 		plr->lastsidehit = (INT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"lastlinehit"))
+		break;
+	case player_lastlinehit:
 		plr->lastlinehit = (INT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"losstime"))
+		break;
+	case player_losstime:
 		plr->losstime = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"timeshit"))
+		break;
+	case player_timeshit:
 		plr->timeshit = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"onconveyor"))
+		break;
+	case player_onconveyor:
 		plr->onconveyor = (INT32)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"awayviewmobj"))
+		break;
+	case player_awayviewmobj:
 	{
 		mobj_t *mo = NULL;
 		if (!lua_isnil(L, 3))
 			mo = *((mobj_t **)luaL_checkudata(L, 3, META_MOBJ));
-		P_SetTarget(&plr->awayviewmobj, mo);
+		if (plr->awayviewmobj != mo) {
+			P_SetTarget(&plr->awayviewmobj, mo);
+			if (plr->awayviewtics) {
+				if (!plr->awayviewmobj)
+					plr->awayviewtics = 0; // can't have a NULL awayviewmobj with awayviewtics!
+				if (plr == &players[displayplayer])
+					P_ResetCamera(plr, &camera); // reset p1 camera on p1 getting an awayviewmobj
+				else if (splitscreen && plr == &players[secondarydisplayplayer])
+					P_ResetCamera(plr, &camera2);  // reset p2 camera on p2 getting an awayviewmobj
+			}
+		}
+		break;
 	}
-	else if (fastcmp(field,"awayviewtics"))
+	case player_awayviewtics:
 	{
-		plr->awayviewtics = (INT32)luaL_checkinteger(L, 3);
-		if (plr->awayviewtics && !plr->awayviewmobj) // awayviewtics must ALWAYS have an awayviewmobj set!!
+		INT32 tics = (INT32)luaL_checkinteger(L, 3);
+		if (tics && !plr->awayviewmobj) // awayviewtics must ALWAYS have an awayviewmobj set!!
 			P_SetTarget(&plr->awayviewmobj, plr->mo); // but since the script might set awayviewmobj immediately AFTER setting awayviewtics, use player mobj as filler for now.
+		if ((tics && !plr->awayviewtics) || (!tics && plr->awayviewtics)) {
+			if (plr == &players[displayplayer])
+				P_ResetCamera(plr, &camera); // reset p1 camera on p1 transitioning to/from zero awayviewtics
+			else if (splitscreen && plr == &players[secondarydisplayplayer])
+				P_ResetCamera(plr, &camera2);  // reset p2 camera on p2 transitioning to/from zero awayviewtics
+		}
+		plr->awayviewtics = tics;
+		break;
 	}
-	else if (fastcmp(field,"awayviewaiming"))
+	case player_awayviewaiming:
 		plr->awayviewaiming = luaL_checkangle(L, 3);
-	else if (fastcmp(field,"spectator"))
+		break;
+	case player_spectator:
 		plr->spectator = lua_toboolean(L, 3);
-	else if (fastcmp(field,"outofcoop"))
+		break;
+	case player_outofcoop:
 		plr->outofcoop = lua_toboolean(L, 3);
-	else if (fastcmp(field,"bot"))
+		break;
+	case player_bot:
 		return NOSET;
-	else if (fastcmp(field,"botleader"))
+	case player_botleader:
 	{
 		player_t *player = NULL;
 		if (!lua_isnil(L, 3))
 			player = *((player_t **)luaL_checkudata(L, 3, META_PLAYER));
 		plr->botleader = player;
+		break;
 	}
-	else if (fastcmp(field,"lastbuttons"))
+	case player_lastbuttons:
 		plr->lastbuttons = (UINT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"blocked"))
+		break;
+	case player_blocked:
 		plr->blocked = (UINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"jointime"))
+		break;
+	case player_jointime:
 		plr->jointime = (tic_t)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"quittime"))
+		break;
+	case player_quittime:
 		plr->quittime = (tic_t)luaL_checkinteger(L, 3);
+		break;
 #ifdef HWRENDER
-	else if (fastcmp(field,"fovadd"))
+	case player_fovadd:
 		plr->fovadd = luaL_checkfixed(L, 3);
+		break;
 #endif
-	else {
+	default:
 		lua_getfield(L, LUA_REGISTRYINDEX, LREG_EXTVARS);
 		I_Assert(lua_istable(L, -1));
 		lua_pushlightuserdata(L, plr);
@@ -752,15 +1362,17 @@ static int player_set(lua_State *L)
 		if (lua_isnil(L, -1)) {
 			// This index doesn't have a table for extra values yet, let's make one.
 			lua_pop(L, 1);
-			CONS_Debug(DBG_LUA, M_GetText("'%s' has no field named '%s'; adding it as Lua data.\n"), "player_t", field);
+			CONS_Debug(DBG_LUA, M_GetText("'%s' has no field named '%s'; adding it as Lua data.\n"), "player_t", lua_tostring(L, 2));
 			lua_newtable(L);
 			lua_pushlightuserdata(L, plr);
 			lua_pushvalue(L, -2); // ext value table
 			lua_rawset(L, -4); // LREG_EXTVARS table
 		}
+		lua_pushvalue(L, 2); // key
 		lua_pushvalue(L, 3); // value to store
-		lua_setfield(L, -2, field);
+		lua_settable(L, -3);
 		lua_pop(L, 2);
+		break;
 	}
 
 	return 0;
@@ -812,29 +1424,60 @@ static int power_len(lua_State *L)
 }
 
 #define NOFIELD luaL_error(L, LUA_QL("ticcmd_t") " has no field named " LUA_QS, field)
-#define NOSET luaL_error(L, LUA_QL("ticcmd_t") " field " LUA_QS " should not be set directly.", field)
+#define NOSET luaL_error(L, LUA_QL("ticcmd_t") " field " LUA_QS " should not be set directly.", ticcmd_opt[field])
+
+enum ticcmd_e
+{
+	ticcmd_forwardmove,
+	ticcmd_sidemove,
+	ticcmd_angleturn,
+	ticcmd_aiming,
+	ticcmd_buttons,
+	ticcmd_latency,
+};
+
+static const char *const ticcmd_opt[] = {
+	"forwardmove",
+	"sidemove",
+	"angleturn",
+	"aiming",
+	"buttons",
+	"latency",
+	NULL,
+};
+
+static int ticcmd_fields_ref = LUA_NOREF;
 
 static int ticcmd_get(lua_State *L)
 {
 	ticcmd_t *cmd = *((ticcmd_t **)luaL_checkudata(L, 1, META_TICCMD));
-	const char *field = luaL_checkstring(L, 2);
+	enum ticcmd_e field = Lua_optoption(L, 2, -1, ticcmd_fields_ref);
 	if (!cmd)
 		return LUA_ErrInvalid(L, "player_t");
 
-	if (fastcmp(field,"forwardmove"))
+	switch (field)
+	{
+	case ticcmd_forwardmove:
 		lua_pushinteger(L, cmd->forwardmove);
-	else if (fastcmp(field,"sidemove"))
+		break;
+	case ticcmd_sidemove:
 		lua_pushinteger(L, cmd->sidemove);
-	else if (fastcmp(field,"angleturn"))
+		break;
+	case ticcmd_angleturn:
 		lua_pushinteger(L, cmd->angleturn);
-	else if (fastcmp(field,"aiming"))
+		break;
+	case ticcmd_aiming:
 		lua_pushinteger(L, cmd->aiming);
-	else if (fastcmp(field,"buttons"))
+		break;
+	case ticcmd_buttons:
 		lua_pushinteger(L, cmd->buttons);
-	else if (fastcmp(field,"latency"))
+		break;
+	case ticcmd_latency:
 		lua_pushinteger(L, cmd->latency);
-	else
+		break;
+	default:
 		return NOFIELD;
+	}
 
 	return 1;
 }
@@ -842,27 +1485,35 @@ static int ticcmd_get(lua_State *L)
 static int ticcmd_set(lua_State *L)
 {
 	ticcmd_t *cmd = *((ticcmd_t **)luaL_checkudata(L, 1, META_TICCMD));
-	const char *field = luaL_checkstring(L, 2);
+	enum ticcmd_e field = Lua_optoption(L, 2, -1, ticcmd_fields_ref);
 	if (!cmd)
 		return LUA_ErrInvalid(L, "ticcmd_t");
 
 	if (hud_running)
 		return luaL_error(L, "Do not alter player_t in HUD rendering code!");
 
-	if (fastcmp(field,"forwardmove"))
+	switch (field)
+	{
+	case ticcmd_forwardmove:
 		cmd->forwardmove = (SINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"sidemove"))
+		break;
+	case ticcmd_sidemove:
 		cmd->sidemove = (SINT8)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"angleturn"))
+		break;
+	case ticcmd_angleturn:
 		cmd->angleturn = (INT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"aiming"))
+		break;
+	case ticcmd_aiming:
 		cmd->aiming = (INT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"buttons"))
+		break;
+	case ticcmd_buttons:
 		cmd->buttons = (UINT16)luaL_checkinteger(L, 3);
-	else if (fastcmp(field,"latency"))
+		break;
+	case ticcmd_latency:
 		return NOSET;
-	else
+	default:
 		return NOFIELD;
+	}
 
 	return 0;
 }
@@ -883,6 +1534,8 @@ int LUA_PlayerLib(lua_State *L)
 		lua_setfield(L, -2, "__len");
 	lua_pop(L,1);
 
+	player_fields_ref = Lua_CreateFieldTable(L, player_opt);
+
 	luaL_newmetatable(L, META_POWERS);
 		lua_pushcfunction(L, power_get);
 		lua_setfield(L, -2, "__index");
@@ -902,6 +1555,8 @@ int LUA_PlayerLib(lua_State *L)
 		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);
diff --git a/src/lua_script.c b/src/lua_script.c
index 9c7636ebe6a99a5e35abc0b7b49dc49a9e57c6fa..392935b4fdfbb06202b797b65b95566ca10e6409 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"
@@ -225,6 +225,18 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 	} else if (fastcmp(word,"pointlimit")) {
 		lua_pushinteger(L, cv_pointlimit.value);
 		return 1;
+	} else if (fastcmp(word, "redflag")) {
+		LUA_PushUserdata(L, redflag, META_MOBJ);
+		return 1;
+	} else if (fastcmp(word, "blueflag")) {
+		LUA_PushUserdata(L, blueflag, META_MOBJ);
+		return 1;
+	} else if (fastcmp(word, "rflagpoint")) {
+		LUA_PushUserdata(L, rflagpoint, META_MAPTHING);
+		return 1;
+	} else if (fastcmp(word, "bflagpoint")) {
+		LUA_PushUserdata(L, bflagpoint, META_MAPTHING);
+		return 1;
 	// begin map vars
 	} else if (fastcmp(word,"spstage_start")) {
 		lua_pushinteger(L, spstage_start);
@@ -306,6 +318,18 @@ int LUA_PushGlobals(lua_State *L, const char *word)
 		lua_pushinteger(L, ammoremovaltics);
 		return 1;
 	// end timers
+	} else if (fastcmp(word,"use1upSound")) {
+		lua_pushinteger(L, use1upSound);
+		return 1;
+	} else if (fastcmp(word,"maxXtraLife")) {
+		lua_pushinteger(L, maxXtraLife);
+		return 1;
+	} else if (fastcmp(word,"useContinues")) {
+		lua_pushinteger(L, useContinues);
+		return 1;
+	} else if (fastcmp(word,"shareEmblems")) {
+		lua_pushinteger(L, shareEmblems);
+		return 1;
 	} else if (fastcmp(word,"gametype")) {
 		lua_pushinteger(L, gametype);
 		return 1;
@@ -489,7 +513,19 @@ static int setglobals(lua_State *L)
 
 		actionnum = LUA_GetActionNumByName(name);
 		if (actionnum < NUMACTIONS)
-			actionsoverridden[actionnum] = true;
+		{
+			int i;
+
+			for (i = MAX_ACTION_RECURSION-1; i > 0; i--)
+			{
+				// Move other references deeper.
+				actionsoverridden[actionnum][i] = actionsoverridden[actionnum][i - 1];
+			}
+
+			// Add the new reference.
+			lua_pushvalue(L, 2);
+			actionsoverridden[actionnum][0] = luaL_ref(L, LUA_REGISTRYINDEX);
+		}
 
 		Z_Free(name);
 		return 0;
@@ -698,20 +734,23 @@ void LUA_DumpFile(const char *filename)
 
 fixed_t LUA_EvalMath(const char *word)
 {
-	lua_State *L = NULL;
+	static lua_State *L = NULL;
 	char buf[1024], *b;
 	const char *p;
 	fixed_t res = 0;
 
-	// make a new state so SOC can't interefere with scripts
-	// allocate state
-	L = lua_newstate(LUA_Alloc, NULL);
-	lua_atpanic(L, LUA_Panic);
-
-	// open only enum lib
-	lua_pushcfunction(L, LUA_EnumLib);
-	lua_pushboolean(L, true);
-	lua_call(L, 1, 0);
+	if (!L)
+	{
+		// make a new state so SOC can't interefere with scripts
+		// allocate state
+		L = lua_newstate(LUA_Alloc, NULL);
+		lua_atpanic(L, LUA_Panic);
+
+		// open only enum lib
+		lua_pushcfunction(L, LUA_EnumLib);
+		lua_pushboolean(L, true);
+		lua_call(L, 1, 0);
+	}
 
 	// change ^ into ^^ for Lua.
 	strcpy(buf, "return ");
@@ -736,8 +775,6 @@ fixed_t LUA_EvalMath(const char *word)
 	else
 		res = lua_tointeger(L, -1);
 
-	// clean up and return.
-	lua_close(L);
 	return res;
 }
 
@@ -952,6 +989,7 @@ enum
 	ARCH_MAPHEADER,
 	ARCH_SKINCOLOR,
 	ARCH_MOUSE,
+	ARCH_SKIN,
 
 	ARCH_TEND=0xFF,
 };
@@ -980,6 +1018,7 @@ static const struct {
 	{META_MAPHEADER,   ARCH_MAPHEADER},
 	{META_SKINCOLOR,   ARCH_SKINCOLOR},
 	{META_MOUSE,    ARCH_MOUSE},
+	{META_SKIN,     ARCH_SKIN},
 	{NULL,          ARCH_NULL}
 };
 
@@ -1301,6 +1340,13 @@ static UINT8 ArchiveValue(int TABLESINDEX, int myindex)
 			WRITEUINT8(save_p, m == &mouse ? 1 : 2);
 			break;
 		}
+		case ARCH_SKIN:
+		{
+			skin_t *skin = *((skin_t **)lua_touserdata(gL, myindex));
+			WRITEUINT8(save_p, ARCH_SKIN);
+			WRITEUINT8(save_p, skin - skins); // UINT8 because MAXSKINS is only 32
+			break;
+		}
 		default:
 			WRITEUINT8(save_p, ARCH_NULL);
 			return 2;
@@ -1547,6 +1593,9 @@ static UINT8 UnArchiveValue(int TABLESINDEX)
 	case ARCH_MOUSE:
 		LUA_PushUserdata(gL, READUINT16(save_p) == 1 ? &mouse : &mouse2, META_MOUSE);
 		break;
+	case ARCH_SKIN:
+		LUA_PushUserdata(gL, &skins[READUINT8(save_p)], META_SKIN);
+		break;
 	case ARCH_TEND:
 		return 1;
 	}
@@ -1716,17 +1765,39 @@ void LUA_UnArchive(void)
 }
 
 // For mobj_t, player_t, etc. to take custom variables.
-int Lua_optoption(lua_State *L, int narg,
-	const char *def, const char *const lst[])
+int Lua_optoption(lua_State *L, int narg, int def, int list_ref)
 {
-	const char *name = (def) ? luaL_optstring(L, narg, def) :  luaL_checkstring(L, narg);
-	int i;
-	for (i=0; lst[i]; i++)
-		if (fastcmp(lst[i], name))
-			return i;
+	if (lua_isnoneornil(L, narg))
+		return def;
+
+	I_Assert(lua_checkstack(L, 2));
+	luaL_checkstring(L, narg);
+
+	lua_rawgeti(L, LUA_REGISTRYINDEX, list_ref);
+	I_Assert(lua_istable(L, -1));
+	lua_pushvalue(L, narg);
+	lua_rawget(L, -2);
+
+	if (lua_isnumber(L, -1))
+		return lua_tointeger(L, -1);
 	return -1;
 }
 
+int Lua_CreateFieldTable(lua_State *L, const char *const lst[])
+{
+	int i;
+
+	lua_newtable(L);
+	for (i = 0; lst[i] != NULL; i++)
+	{
+		lua_pushstring(L, lst[i]);
+		lua_pushinteger(L, i);
+		lua_settable(L, -3);
+	}
+
+	return luaL_ref(L, LUA_REGISTRYINDEX);
+}
+
 void LUA_PushTaggableObjectArray
 (		lua_State *L,
 		const char *field,
diff --git a/src/lua_script.h b/src/lua_script.h
index fe04e5e608fdeca20690799e5df2aa1c3b145db6..d0b06a719e32a254ba56c5f860252aba15484da0 100644
--- a/src/lua_script.h
+++ b/src/lua_script.h
@@ -57,8 +57,8 @@ int LUA_PushGlobals(lua_State *L, const char *word);
 int LUA_CheckGlobals(lua_State *L, const char *word);
 void Got_Luacmd(UINT8 **cp, INT32 playernum); // lua_consolelib.c
 void LUA_CVarChanged(void *cvar); // lua_consolelib.c
-int Lua_optoption(lua_State *L, int narg,
-	const char *def, const char *const lst[]);
+int Lua_optoption(lua_State *L, int narg, int def, int list_ref);
+int Lua_CreateFieldTable(lua_State *L, const char *const lst[]);
 void LUA_HookNetArchive(lua_CFunction archFunc);
 
 void LUA_PushTaggableObjectArray
diff --git a/src/lua_skinlib.c b/src/lua_skinlib.c
index 5c21b04c3a31ea4d29d0d73049dbd54d5ab1dc04..041c5d59851f3669b03d45c899c20b3440e193f0 100644
--- a/src/lua_skinlib.c
+++ b/src/lua_skinlib.c
@@ -25,6 +25,7 @@ enum skin {
 	skin_flags,
 	skin_realname,
 	skin_hudname,
+	skin_supername,
 	skin_ability,
 	skin_ability2,
 	skin_thokitem,
@@ -55,6 +56,7 @@ enum skin {
 	skin_soundsid,
 	skin_sprites
 };
+
 static const char *const skin_opt[] = {
 	"valid",
 	"name",
@@ -62,6 +64,7 @@ static const char *const skin_opt[] = {
 	"flags",
 	"realname",
 	"hudname",
+	"supername",
 	"ability",
 	"ability2",
 	"thokitem",
@@ -95,10 +98,12 @@ static const char *const skin_opt[] = {
 
 #define UNIMPLEMENTED luaL_error(L, LUA_QL("skin_t") " field " LUA_QS " is not implemented for Lua and cannot be accessed.", skin_opt[field])
 
+static int skin_fields_ref = LUA_NOREF;
+
 static int skin_get(lua_State *L)
 {
 	skin_t *skin = *((skin_t **)luaL_checkudata(L, 1, META_SKIN));
-	enum skin field = luaL_checkoption(L, 2, NULL, skin_opt);
+	enum skin field = Lua_optoption(L, 2, -1, skin_fields_ref);
 
 	// skins are always valid, only added, never removed
 	I_Assert(skin != NULL);
@@ -123,6 +128,9 @@ static int skin_get(lua_State *L)
 	case skin_hudname:
 		lua_pushstring(L, skin->hudname);
 		break;
+	case skin_supername:
+		lua_pushstring(L, skin->supername);
+		break;
 	case skin_ability:
 		lua_pushinteger(L, skin->ability);
 		break;
@@ -331,13 +339,13 @@ static const char *const sprites_opt[] = {
 // skin.sprites[i] -> sprites[i]
 static int lib_getSkinSprite(lua_State *L)
 {
-	spritedef_t *sprites = *(spritedef_t **)luaL_checkudata(L, 1, META_SKINSPRITES);
+	spritedef_t *sksprites = *(spritedef_t **)luaL_checkudata(L, 1, META_SKINSPRITES);
 	playersprite_t i = luaL_checkinteger(L, 2);
 
 	if (i < 0 || i >= NUMPLAYERSPRITES*2)
 		return luaL_error(L, LUA_QL("skin_t") " field 'sprites' index %d out of range (0 - %d)", i, (NUMPLAYERSPRITES*2)-1);
 
-	LUA_PushUserdata(L, &sprites[i], META_SKINSPRITESLIST);
+	LUA_PushUserdata(L, &sksprites[i], META_SKINSPRITESLIST);
 	return 1;
 }
 
@@ -376,6 +384,8 @@ int LUA_SkinLib(lua_State *L)
 		lua_setfield(L, -2, "__len");
 	lua_pop(L,1);
 
+	skin_fields_ref = Lua_CreateFieldTable(L, skin_opt);
+
 	luaL_newmetatable(L, META_SOUNDSID);
 		lua_pushcfunction(L, soundsid_get);
 		lua_setfield(L, -2, "__index");
diff --git a/src/m_cheat.c b/src/m_cheat.c
index 9d257b48b6452d57788e2f45edba5a6f28c7252b..2bcf43ad1ee1971c13ab3a769ef8b0fafc708883 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"
@@ -79,9 +79,9 @@ static UINT8 cheatf_warp(void)
 
 	// Temporarily unlock stuff.
 	G_SetUsedCheats(false);
-	unlockables[31].unlocked = true; // credits
-	unlockables[30].unlocked = true; // sound test
-	unlockables[28].unlocked = true; // level select
+	clientGamedata->unlocked[31] = true; // credits
+	clientGamedata->unlocked[30] = true; // sound test
+	clientGamedata->unlocked[28] = true; // level select
 
 	// Refresh secrets menu existing.
 	M_ClearMenus(true);
@@ -102,7 +102,7 @@ static UINT8 cheatf_devmode(void)
 	// Just unlock all the things and turn on -debug and console devmode.
 	G_SetUsedCheats(false);
 	for (i = 0; i < MAXUNLOCKABLES; i++)
-		unlockables[i].unlocked = true;
+		clientGamedata->unlocked[i] = true;
 	devparm = true;
 	cv_debug |= 0x8000;
 
@@ -238,7 +238,7 @@ boolean cht_Responder(event_t *ev)
 }
 
 // Console cheat commands rely on these a lot...
-#define REQUIRE_PANDORA if (!M_SecretUnlocked(SECRET_PANDORA) && !cv_debug)\
+#define REQUIRE_PANDORA if (!M_SecretUnlocked(SECRET_PANDORA, serverGamedata) && !cv_debug)\
 { CONS_Printf(M_GetText("You haven't earned this yet.\n")); return; }
 
 #define REQUIRE_DEVMODE if (!cv_debug)\
@@ -1102,6 +1102,8 @@ static mapthing_t *OP_CreateNewMapThing(player_t *player, UINT16 type, boolean c
 
 	mt->options = (mt->z << ZSHIFT) | (UINT16)cv_opflags.value;
 	mt->scale = player->mo->scale;
+	mt->spritexscale = player->mo->spritexscale;
+	mt->spriteyscale = player->mo->spriteyscale;
 	memset(mt->args, 0, NUMMAPTHINGARGS*sizeof(*mt->args));
 	memset(mt->stringargs, 0x00, NUMMAPTHINGSTRINGARGS*sizeof(*mt->stringargs));
 	mt->pitch = mt->roll = 0;
diff --git a/src/m_cond.c b/src/m_cond.c
index bd6faadfd3991aab0a6adcd3d7c1cd4a9fd33708..706a1b5106f487e4ed285880304a87335f7f716e 100644
--- a/src/m_cond.c
+++ b/src/m_cond.c
@@ -21,6 +21,9 @@
 #include "r_skins.h" // numskins
 #include "r_draw.h" // R_GetColorByName
 
+gamedata_t *clientGamedata; // Our gamedata
+gamedata_t *serverGamedata; // Server's gamedata
+
 // Map triggers for linedef executors
 // 32 triggers, one bit each
 UINT32 unlocktriggers;
@@ -41,12 +44,76 @@ unlockable_t unlockables[MAXUNLOCKABLES];
 INT32 numemblems = 0;
 INT32 numextraemblems = 0;
 
+// Temporary holding place for nights data for the current map
+nightsdata_t ntemprecords[MAXPLAYERS];
+
+// Create a new gamedata_t, for start-up
+gamedata_t *M_NewGameDataStruct(void)
+{
+	gamedata_t *data = Z_Calloc(sizeof (*data), PU_STATIC, NULL);
+	M_ClearSecrets(data);
+	G_ClearRecords(data);
+	return data;
+}
+
+void M_CopyGameData(gamedata_t *dest, gamedata_t *src)
+{
+	INT32 i, j;
+
+	M_ClearSecrets(dest);
+	G_ClearRecords(dest);
+
+	dest->loaded = src->loaded;
+	dest->totalplaytime = src->totalplaytime;
+
+	dest->timesBeaten = src->timesBeaten;
+	dest->timesBeatenWithEmeralds = src->timesBeatenWithEmeralds;
+	dest->timesBeatenUltimate = src->timesBeatenUltimate;
+
+	memcpy(dest->achieved, src->achieved, sizeof(dest->achieved));
+	memcpy(dest->collected, src->collected, sizeof(dest->collected));
+	memcpy(dest->extraCollected, src->extraCollected, sizeof(dest->extraCollected));
+	memcpy(dest->unlocked, src->unlocked, sizeof(dest->unlocked));
+
+	memcpy(dest->mapvisited, src->mapvisited, sizeof(dest->mapvisited));
+
+	// Main records
+	for (i = 0; i < NUMMAPS; ++i)
+	{
+		if (!src->mainrecords[i])
+			continue;
+
+		G_AllocMainRecordData((INT16)i, dest);
+		dest->mainrecords[i]->score = src->mainrecords[i]->score;
+		dest->mainrecords[i]->time = src->mainrecords[i]->time;
+		dest->mainrecords[i]->rings = src->mainrecords[i]->rings;
+	}
+
+	// Nights records
+	for (i = 0; i < NUMMAPS; ++i)
+	{
+		if (!src->nightsrecords[i] || !src->nightsrecords[i]->nummares)
+			continue;
+
+		G_AllocNightsRecordData((INT16)i, dest);
+
+		for (j = 0; j < (src->nightsrecords[i]->nummares + 1); j++)
+		{
+			dest->nightsrecords[i]->score[j] = src->nightsrecords[i]->score[j];
+			dest->nightsrecords[i]->grade[j] = src->nightsrecords[i]->grade[j];
+			dest->nightsrecords[i]->time[j] = src->nightsrecords[i]->time[j];
+		}
+
+		dest->nightsrecords[i]->nummares = src->nightsrecords[i]->nummares;
+	}
+}
+
 void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1, INT16 x2)
 {
 	condition_t *cond;
 	UINT32 num, wnum;
 
-	I_Assert(set && set <= MAXCONDITIONSETS);
+	I_Assert(set < MAXCONDITIONSETS);
 
 	wnum = conditionSets[set - 1].numconditions;
 	num = ++conditionSets[set - 1].numconditions;
@@ -70,89 +137,90 @@ void M_ClearConditionSet(UINT8 set)
 		conditionSets[set - 1].condition = NULL;
 		conditionSets[set - 1].numconditions = 0;
 	}
-	conditionSets[set - 1].achieved = false;
+	clientGamedata->achieved[set - 1] = serverGamedata->achieved[set - 1] = false;
 }
 
 // Clear ALL secrets.
-void M_ClearSecrets(void)
+void M_ClearSecrets(gamedata_t *data)
 {
 	INT32 i;
 
-	memset(mapvisited, 0, sizeof(mapvisited));
+	memset(data->mapvisited, 0, sizeof(data->mapvisited));
 
 	for (i = 0; i < MAXEMBLEMS; ++i)
-		emblemlocations[i].collected = false;
+		data->collected[i] = false;
 	for (i = 0; i < MAXEXTRAEMBLEMS; ++i)
-		extraemblems[i].collected = false;
+		data->extraCollected[i] = false;
 	for (i = 0; i < MAXUNLOCKABLES; ++i)
-		unlockables[i].unlocked = false;
+		data->unlocked[i] = false;
 	for (i = 0; i < MAXCONDITIONSETS; ++i)
-		conditionSets[i].achieved = false;
+		data->achieved[i] = false;
 
-	timesBeaten = timesBeatenWithEmeralds = timesBeatenUltimate = 0;
+	data->timesBeaten = data->timesBeatenWithEmeralds = data->timesBeatenUltimate = 0;
 
 	// Re-unlock any always unlocked things
-	M_SilentUpdateUnlockablesAndEmblems();
+	M_SilentUpdateUnlockablesAndEmblems(data);
+	M_SilentUpdateSkinAvailabilites();
 }
 
 // ----------------------
 // Condition set checking
 // ----------------------
-static UINT8 M_CheckCondition(condition_t *cn)
+static UINT8 M_CheckCondition(condition_t *cn, gamedata_t *data)
 {
 	switch (cn->type)
 	{
 		case UC_PLAYTIME: // Requires total playing time >= x
-			return (totalplaytime >= (unsigned)cn->requirement);
+			return (data->totalplaytime >= (unsigned)cn->requirement);
 		case UC_GAMECLEAR: // Requires game beaten >= x times
-			return (timesBeaten >= (unsigned)cn->requirement);
+			return (data->timesBeaten >= (unsigned)cn->requirement);
 		case UC_ALLEMERALDS: // Requires game beaten with all 7 emeralds >= x times
-			return (timesBeatenWithEmeralds >= (unsigned)cn->requirement);
+			return (data->timesBeatenWithEmeralds >= (unsigned)cn->requirement);
 		case UC_ULTIMATECLEAR: // Requires game beaten on ultimate >= x times (in other words, never)
-			return (timesBeatenUltimate >= (unsigned)cn->requirement);
+			return (data->timesBeatenUltimate >= (unsigned)cn->requirement);
 		case UC_OVERALLSCORE: // Requires overall score >= x
-			return (M_GotHighEnoughScore(cn->requirement));
+			return (M_GotHighEnoughScore(cn->requirement, data));
 		case UC_OVERALLTIME: // Requires overall time <= x
-			return (M_GotLowEnoughTime(cn->requirement));
+			return (M_GotLowEnoughTime(cn->requirement, data));
 		case UC_OVERALLRINGS: // Requires overall rings >= x
-			return (M_GotHighEnoughRings(cn->requirement));
+			return (M_GotHighEnoughRings(cn->requirement, data));
 		case UC_MAPVISITED: // Requires map x to be visited
-			return ((mapvisited[cn->requirement - 1] & MV_VISITED) == MV_VISITED);
+			return ((data->mapvisited[cn->requirement - 1] & MV_VISITED) == MV_VISITED);
 		case UC_MAPBEATEN: // Requires map x to be beaten
-			return ((mapvisited[cn->requirement - 1] & MV_BEATEN) == MV_BEATEN);
+			return ((data->mapvisited[cn->requirement - 1] & MV_BEATEN) == MV_BEATEN);
 		case UC_MAPALLEMERALDS: // Requires map x to be beaten with all emeralds in possession
-			return ((mapvisited[cn->requirement - 1] & MV_ALLEMERALDS) == MV_ALLEMERALDS);
+			return ((data->mapvisited[cn->requirement - 1] & MV_ALLEMERALDS) == MV_ALLEMERALDS);
 		case UC_MAPULTIMATE: // Requires map x to be beaten on ultimate
-			return ((mapvisited[cn->requirement - 1] & MV_ULTIMATE) == MV_ULTIMATE);
+			return ((data->mapvisited[cn->requirement - 1] & MV_ULTIMATE) == MV_ULTIMATE);
 		case UC_MAPPERFECT: // Requires map x to be beaten with a perfect bonus
-			return ((mapvisited[cn->requirement - 1] & MV_PERFECT) == MV_PERFECT);
+			return ((data->mapvisited[cn->requirement - 1] & MV_PERFECT) == MV_PERFECT);
 		case UC_MAPSCORE: // Requires score on map >= x
-			return (G_GetBestScore(cn->extrainfo1) >= (unsigned)cn->requirement);
+			return (G_GetBestScore(cn->extrainfo1, data) >= (unsigned)cn->requirement);
 		case UC_MAPTIME: // Requires time on map <= x
-			return (G_GetBestTime(cn->extrainfo1) <= (unsigned)cn->requirement);
+			return (G_GetBestTime(cn->extrainfo1, data) <= (unsigned)cn->requirement);
 		case UC_MAPRINGS: // Requires rings on map >= x
-			return (G_GetBestRings(cn->extrainfo1) >= cn->requirement);
+			return (G_GetBestRings(cn->extrainfo1, data) >= cn->requirement);
 		case UC_NIGHTSSCORE:
-			return (G_GetBestNightsScore(cn->extrainfo1, (UINT8)cn->extrainfo2) >= (unsigned)cn->requirement);
+			return (G_GetBestNightsScore(cn->extrainfo1, (UINT8)cn->extrainfo2, data) >= (unsigned)cn->requirement);
 		case UC_NIGHTSTIME:
-			return (G_GetBestNightsTime(cn->extrainfo1, (UINT8)cn->extrainfo2) <= (unsigned)cn->requirement);
+			return (G_GetBestNightsTime(cn->extrainfo1, (UINT8)cn->extrainfo2, data) <= (unsigned)cn->requirement);
 		case UC_NIGHTSGRADE:
-			return (G_GetBestNightsGrade(cn->extrainfo1, (UINT8)cn->extrainfo2) >= cn->requirement);
+			return (G_GetBestNightsGrade(cn->extrainfo1, (UINT8)cn->extrainfo2, data) >= cn->requirement);
 		case UC_TRIGGER: // requires map trigger set
 			return !!(unlocktriggers & (1 << cn->requirement));
 		case UC_TOTALEMBLEMS: // Requires number of emblems >= x
-			return (M_GotEnoughEmblems(cn->requirement));
+			return (M_GotEnoughEmblems(cn->requirement, data));
 		case UC_EMBLEM: // Requires emblem x to be obtained
-			return emblemlocations[cn->requirement-1].collected;
+			return data->collected[cn->requirement-1];
 		case UC_EXTRAEMBLEM: // Requires extra emblem x to be obtained
-			return extraemblems[cn->requirement-1].collected;
+			return data->extraCollected[cn->requirement-1];
 		case UC_CONDITIONSET: // requires condition set x to already be achieved
-			return M_Achieved(cn->requirement-1);
+			return M_Achieved(cn->requirement-1, data);
 	}
 	return false;
 }
 
-static UINT8 M_CheckConditionSet(conditionset_t *c)
+static UINT8 M_CheckConditionSet(conditionset_t *c, gamedata_t *data)
 {
 	UINT32 i;
 	UINT32 lastID = 0;
@@ -173,13 +241,13 @@ static UINT8 M_CheckConditionSet(conditionset_t *c)
 			continue;
 
 		lastID = cn->id;
-		achievedSoFar = M_CheckCondition(cn);
+		achievedSoFar = M_CheckCondition(cn, data);
 	}
 
 	return achievedSoFar;
 }
 
-void M_CheckUnlockConditions(void)
+void M_CheckUnlockConditions(gamedata_t *data)
 {
 	INT32 i;
 	conditionset_t *c;
@@ -187,27 +255,27 @@ void M_CheckUnlockConditions(void)
 	for (i = 0; i < MAXCONDITIONSETS; ++i)
 	{
 		c = &conditionSets[i];
-		if (!c->numconditions || c->achieved)
+		if (!c->numconditions || data->achieved[i])
 			continue;
 
-		c->achieved = (M_CheckConditionSet(c));
+		data->achieved[i] = (M_CheckConditionSet(c, data));
 	}
 }
 
-UINT8 M_UpdateUnlockablesAndExtraEmblems(void)
+UINT8 M_UpdateUnlockablesAndExtraEmblems(gamedata_t *data)
 {
 	INT32 i;
 	char cechoText[992] = "";
 	UINT8 cechoLines = 0;
 
-	M_CheckUnlockConditions();
+	M_CheckUnlockConditions(data);
 
 	// Go through extra emblems
 	for (i = 0; i < numextraemblems; ++i)
 	{
-		if (extraemblems[i].collected || !extraemblems[i].conditionset)
+		if (data->extraCollected[i] || !extraemblems[i].conditionset)
 			continue;
-		if ((extraemblems[i].collected = M_Achieved(extraemblems[i].conditionset - 1)) != false)
+		if ((data->extraCollected[i] = M_Achieved(extraemblems[i].conditionset - 1, data)) != false)
 		{
 			strcat(cechoText, va(M_GetText("Got \"%s\" emblem!\\"), extraemblems[i].name));
 			++cechoLines;
@@ -217,14 +285,14 @@ UINT8 M_UpdateUnlockablesAndExtraEmblems(void)
 	// Fun part: if any of those unlocked we need to go through the
 	// unlock conditions AGAIN just in case an emblem reward was reached
 	if (cechoLines)
-		M_CheckUnlockConditions();
+		M_CheckUnlockConditions(data);
 
 	// Go through unlockables
 	for (i = 0; i < MAXUNLOCKABLES; ++i)
 	{
-		if (unlockables[i].unlocked || !unlockables[i].conditionset)
+		if (data->unlocked[i] || !unlockables[i].conditionset)
 			continue;
-		if ((unlockables[i].unlocked = M_Achieved(unlockables[i].conditionset - 1)) != false)
+		if ((data->unlocked[i] = M_Achieved(unlockables[i].conditionset - 1, data)) != false)
 		{
 			if (unlockables[i].nocecho)
 				continue;
@@ -248,45 +316,50 @@ UINT8 M_UpdateUnlockablesAndExtraEmblems(void)
 		HU_DoCEcho(slashed);
 		return true;
 	}
+
 	return false;
 }
 
 // Used when loading gamedata to make sure all unlocks are synched with conditions
-void M_SilentUpdateUnlockablesAndEmblems(void)
+void M_SilentUpdateUnlockablesAndEmblems(gamedata_t *data)
 {
 	INT32 i;
 	boolean checkAgain = false;
 
 	// Just in case they aren't to sync
-	M_CheckUnlockConditions();
-	M_CheckLevelEmblems();
+	M_CheckUnlockConditions(data);
+	M_CheckLevelEmblems(data);
+	M_CompletionEmblems(data);
 
 	// Go through extra emblems
 	for (i = 0; i < numextraemblems; ++i)
 	{
-		if (extraemblems[i].collected || !extraemblems[i].conditionset)
+		if (data->extraCollected[i] || !extraemblems[i].conditionset)
 			continue;
-		if ((extraemblems[i].collected = M_Achieved(extraemblems[i].conditionset - 1)) != false)
+		if ((data->extraCollected[i] = M_Achieved(extraemblems[i].conditionset - 1, data)) != false)
 			checkAgain = true;
 	}
 
 	// check again if extra emblems unlocked, blah blah, etc
 	if (checkAgain)
-		M_CheckUnlockConditions();
+		M_CheckUnlockConditions(data);
 
 	// Go through unlockables
 	for (i = 0; i < MAXUNLOCKABLES; ++i)
 	{
-		if (unlockables[i].unlocked || !unlockables[i].conditionset)
+		if (data->unlocked[i] || !unlockables[i].conditionset)
 			continue;
-		unlockables[i].unlocked = M_Achieved(unlockables[i].conditionset - 1);
+		data->unlocked[i] = M_Achieved(unlockables[i].conditionset - 1, data);
 	}
+}
 
+void M_SilentUpdateSkinAvailabilites(void)
+{
 	players[consoleplayer].availabilities = players[1].availabilities = R_GetSkinAvailabilities(); // players[1] is supposed to be for 2p
 }
 
 // Emblem unlocking shit
-UINT8 M_CheckLevelEmblems(void)
+UINT8 M_CheckLevelEmblems(gamedata_t *data)
 {
 	INT32 i;
 	INT32 valToReach;
@@ -297,7 +370,7 @@ UINT8 M_CheckLevelEmblems(void)
 	// Update Score, Time, Rings emblems
 	for (i = 0; i < numemblems; ++i)
 	{
-		if (emblemlocations[i].type <= ET_SKIN || emblemlocations[i].type == ET_MAP || emblemlocations[i].collected)
+		if (emblemlocations[i].type <= ET_SKIN || emblemlocations[i].type == ET_MAP || data->collected[i])
 			continue;
 
 		levelnum = emblemlocations[i].level;
@@ -306,32 +379,32 @@ UINT8 M_CheckLevelEmblems(void)
 		switch (emblemlocations[i].type)
 		{
 			case ET_SCORE: // Requires score on map >= x
-				res = (G_GetBestScore(levelnum) >= (unsigned)valToReach);
+				res = (G_GetBestScore(levelnum, data) >= (unsigned)valToReach);
 				break;
 			case ET_TIME: // Requires time on map <= x
-				res = (G_GetBestTime(levelnum) <= (unsigned)valToReach);
+				res = (G_GetBestTime(levelnum, data) <= (unsigned)valToReach);
 				break;
 			case ET_RINGS: // Requires rings on map >= x
-				res = (G_GetBestRings(levelnum) >= valToReach);
+				res = (G_GetBestRings(levelnum, data) >= valToReach);
 				break;
 			case ET_NGRADE: // Requires NiGHTS grade on map >= x
-				res = (G_GetBestNightsGrade(levelnum, 0) >= valToReach);
+				res = (G_GetBestNightsGrade(levelnum, 0, data) >= valToReach);
 				break;
 			case ET_NTIME: // Requires NiGHTS time on map <= x
-				res = (G_GetBestNightsTime(levelnum, 0) <= (unsigned)valToReach);
+				res = (G_GetBestNightsTime(levelnum, 0, data) <= (unsigned)valToReach);
 				break;
 			default: // unreachable but shuts the compiler up.
 				continue;
 		}
 
-		emblemlocations[i].collected = res;
+		data->collected[i] = res;
 		if (res)
 			++somethingUnlocked;
 	}
 	return somethingUnlocked;
 }
 
-UINT8 M_CompletionEmblems(void) // Bah! Duplication sucks, but it's for a separate print when awarding emblems and it's sorta different enough.
+UINT8 M_CompletionEmblems(gamedata_t *data) // Bah! Duplication sucks, but it's for a separate print when awarding emblems and it's sorta different enough.
 {
 	INT32 i;
 	INT32 embtype;
@@ -342,7 +415,7 @@ UINT8 M_CompletionEmblems(void) // Bah! Duplication sucks, but it's for a separa
 
 	for (i = 0; i < numemblems; ++i)
 	{
-		if (emblemlocations[i].type != ET_MAP || emblemlocations[i].collected)
+		if (emblemlocations[i].type != ET_MAP || data->collected[i])
 			continue;
 
 		levelnum = emblemlocations[i].level;
@@ -358,9 +431,9 @@ UINT8 M_CompletionEmblems(void) // Bah! Duplication sucks, but it's for a separa
 		if (embtype & ME_PERFECT)
 			flags |= MV_PERFECT;
 
-		res = ((mapvisited[levelnum - 1] & flags) == flags);
+		res = ((data->mapvisited[levelnum - 1] & flags) == flags);
 
-		emblemlocations[i].collected = res;
+		data->collected[i] = res;
 		if (res)
 			++somethingUnlocked;
 	}
@@ -370,48 +443,105 @@ UINT8 M_CompletionEmblems(void) // Bah! Duplication sucks, but it's for a separa
 // -------------------
 // Quick unlock checks
 // -------------------
-UINT8 M_AnySecretUnlocked(void)
+UINT8 M_AnySecretUnlocked(gamedata_t *data)
 {
 	INT32 i;
 	for (i = 0; i < MAXUNLOCKABLES; ++i)
 	{
-		if (!unlockables[i].nocecho && unlockables[i].unlocked)
+		if (!unlockables[i].nocecho && data->unlocked[i])
 			return true;
 	}
 	return false;
 }
 
-UINT8 M_SecretUnlocked(INT32 type)
+UINT8 M_SecretUnlocked(INT32 type, gamedata_t *data)
 {
 	INT32 i;
 	for (i = 0; i < MAXUNLOCKABLES; ++i)
 	{
-		if (unlockables[i].type == type && unlockables[i].unlocked)
+		if (unlockables[i].type == type && data->unlocked[i])
 			return true;
 	}
 	return false;
 }
 
-UINT8 M_MapLocked(INT32 mapnum)
+UINT8 M_MapLocked(INT32 mapnum, gamedata_t *data)
 {
+	if (dedicated)
+	{
+		// If you're in a dedicated server, every level is unlocked.
+		// Yes, technically this means you can view any level by
+		// running a dedicated server and joining it yourself, but
+		// that's better than making dedicated server's lives hell.
+		return false;
+	}
+
 	if (!mapheaderinfo[mapnum-1] || mapheaderinfo[mapnum-1]->unlockrequired < 0)
+	{
 		return false;
-	if (!unlockables[mapheaderinfo[mapnum-1]->unlockrequired].unlocked)
+	}
+
+	if (!data->unlocked[mapheaderinfo[mapnum-1]->unlockrequired])
+	{
 		return true;
+	}
+
 	return false;
 }
 
-INT32 M_CountEmblems(void)
+UINT8 M_CampaignWarpIsCheat(INT32 gt, INT32 mapnum, gamedata_t *data)
+{
+	if (M_MapLocked(mapnum, data) == true)
+	{
+		// Warping to locked maps is definitely always a cheat
+		return true;
+	}
+
+	if ((gametypedefaultrules[gt] & GTR_CAMPAIGN) == 0)
+	{
+		// Not a campaign, do whatever you want.
+		return false;
+	}
+
+	if (G_IsSpecialStage(mapnum))
+	{
+		// Warping to special stages is a cheat
+		return true;
+	}
+
+	if (!mapheaderinfo[mapnum-1] || mapheaderinfo[mapnum-1]->menuflags & LF2_HIDEINMENU)
+	{
+		// You're never allowed to warp to this level.
+		return true;
+	}
+
+	if (mapheaderinfo[mapnum-1]->menuflags & LF2_NOVISITNEEDED)
+	{
+		// You're always allowed to warp to this level.
+		return false;
+	}
+
+	if (mapnum == spstage_start)
+	{
+		// Warping to the first level is never a cheat
+		return false;
+	}
+
+	// It's only a cheat if you've never been there.
+	return (!(data->mapvisited[mapnum-1]));
+}
+
+INT32 M_CountEmblems(gamedata_t *data)
 {
 	INT32 found = 0, i;
 	for (i = 0; i < numemblems; ++i)
 	{
-		if (emblemlocations[i].collected)
+		if (data->collected[i])
 			found++;
 	}
 	for (i = 0; i < numextraemblems; ++i)
 	{
-		if (extraemblems[i].collected)
+		if (data->extraCollected[i])
 			found++;
 	}
 	return found;
@@ -423,23 +553,23 @@ INT32 M_CountEmblems(void)
 
 // Theoretically faster than using M_CountEmblems()
 // Stops when it reaches the target number of emblems.
-UINT8 M_GotEnoughEmblems(INT32 number)
+UINT8 M_GotEnoughEmblems(INT32 number, gamedata_t *data)
 {
 	INT32 i, gottenemblems = 0;
 	for (i = 0; i < numemblems; ++i)
 	{
-		if (emblemlocations[i].collected)
+		if (data->collected[i])
 			if (++gottenemblems >= number) return true;
 	}
 	for (i = 0; i < numextraemblems; ++i)
 	{
-		if (extraemblems[i].collected)
+		if (data->extraCollected[i])
 			if (++gottenemblems >= number) return true;
 	}
 	return false;
 }
 
-UINT8 M_GotHighEnoughScore(INT32 tscore)
+UINT8 M_GotHighEnoughScore(INT32 tscore, gamedata_t *data)
 {
 	INT32 mscore = 0;
 	INT32 i;
@@ -448,16 +578,16 @@ UINT8 M_GotHighEnoughScore(INT32 tscore)
 	{
 		if (!mapheaderinfo[i] || !(mapheaderinfo[i]->menuflags & LF2_RECORDATTACK))
 			continue;
-		if (!mainrecords[i])
+		if (!data->mainrecords[i])
 			continue;
 
-		if ((mscore += mainrecords[i]->score) > tscore)
+		if ((mscore += data->mainrecords[i]->score) > tscore)
 			return true;
 	}
 	return false;
 }
 
-UINT8 M_GotLowEnoughTime(INT32 tictime)
+UINT8 M_GotLowEnoughTime(INT32 tictime, gamedata_t *data)
 {
 	INT32 curtics = 0;
 	INT32 i;
@@ -467,15 +597,15 @@ UINT8 M_GotLowEnoughTime(INT32 tictime)
 		if (!mapheaderinfo[i] || !(mapheaderinfo[i]->menuflags & LF2_RECORDATTACK))
 			continue;
 
-		if (!mainrecords[i] || !mainrecords[i]->time)
+		if (!data->mainrecords[i] || !data->mainrecords[i]->time)
 			return false;
-		else if ((curtics += mainrecords[i]->time) > tictime)
+		else if ((curtics += data->mainrecords[i]->time) > tictime)
 			return false;
 	}
 	return true;
 }
 
-UINT8 M_GotHighEnoughRings(INT32 trings)
+UINT8 M_GotHighEnoughRings(INT32 trings, gamedata_t *data)
 {
 	INT32 mrings = 0;
 	INT32 i;
@@ -484,10 +614,10 @@ UINT8 M_GotHighEnoughRings(INT32 trings)
 	{
 		if (!mapheaderinfo[i] || !(mapheaderinfo[i]->menuflags & LF2_RECORDATTACK))
 			continue;
-		if (!mainrecords[i])
+		if (!data->mainrecords[i])
 			continue;
 
-		if ((mrings += mainrecords[i]->rings) > trings)
+		if ((mrings += data->mainrecords[i]->rings) > trings)
 			return true;
 	}
 	return false;
diff --git a/src/m_cond.h b/src/m_cond.h
index d49dc920b30433b5d0ac416186ec9ba1cf9b7894..2491a384c02aa5f34ba47933eba689811ac599d0 100644
--- a/src/m_cond.h
+++ b/src/m_cond.h
@@ -10,7 +10,11 @@
 /// \file  m_cond.h
 /// \brief Unlockable condition system for SRB2 version 2.1
 
+#ifndef __M_COND__
+#define __M_COND__
+
 #include "doomdef.h"
+#include "doomdata.h"
 
 // --------
 // Typedefs
@@ -61,8 +65,6 @@ typedef struct
 {
 	UINT32 numconditions;   /// <- number of conditions.
 	condition_t *condition; /// <- All conditionals to be checked.
-	UINT8 achieved;         /// <- Whether this conditional has been achieved already or not.
-	                        ///    (Conditional checking is skipped if true -- it's assumed you can't relock an unlockable)
 } conditionset_t;
 
 // Emblem information
@@ -94,7 +96,6 @@ typedef struct
 	INT32 var;       ///< If needed, specifies information on the target amount to achieve (or target skin)
 	char *stringVar; ///< String version
 	char hint[110];  ///< Hint for emblem hints menu
-	UINT8 collected; ///< Do you have this emblem?
 } emblem_t;
 typedef struct
 {
@@ -104,7 +105,6 @@ typedef struct
 	UINT8 showconditionset; ///< Condition set that shows this emblem.
 	UINT8 sprite;           ///< emblem sprite to use, 0 - 25
 	UINT16 color;           ///< skincolor to use
-	UINT8 collected;        ///< Do you have this emblem?
 } extraemblem_t;
 
 // Unlockable information
@@ -120,7 +120,6 @@ typedef struct
 	char *stringVar;
 	UINT8 nocecho;
 	UINT8 nochecklist;
-	UINT8 unlocked;
 } unlockable_t;
 
 #define SECRET_NONE         -6 // Does nil.  Use with levels locked by UnlockRequired
@@ -140,8 +139,85 @@ typedef struct
 // you seriously need to get a life.
 #define MAXCONDITIONSETS 128
 #define MAXEMBLEMS       512
-#define MAXEXTRAEMBLEMS   16
-#define MAXUNLOCKABLES    32
+#define MAXEXTRAEMBLEMS   48
+#define MAXUNLOCKABLES    80
+
+/** Time attack information, currently a very small structure.
+  */
+typedef struct
+{
+	tic_t time;   ///< Time in which the level was finished.
+	UINT32 score; ///< Score when the level was finished.
+	UINT16 rings; ///< Rings when the level was finished.
+} recorddata_t;
+
+/** Setup for one NiGHTS map.
+  * These are dynamically allocated because I am insane
+  */
+#define GRADE_F 0
+#define GRADE_E 1
+#define GRADE_D 2
+#define GRADE_C 3
+#define GRADE_B 4
+#define GRADE_A 5
+#define GRADE_S 6
+
+typedef struct
+{
+	// 8 mares, 1 overall (0)
+	UINT8	nummares;
+	UINT32	score[9];
+	UINT8	grade[9];
+	tic_t	time[9];
+} nightsdata_t;
+
+// mapvisited is now a set of flags that says what we've done in the map.
+#define MV_VISITED      1
+#define MV_BEATEN       2
+#define MV_ALLEMERALDS  4
+#define MV_ULTIMATE     8
+#define MV_PERFECT     16
+#define MV_PERFECTRA   32
+#define MV_MAX         63 // used in gamedata check, update whenever MV's are added
+
+// Temporary holding place for nights data for the current map
+extern nightsdata_t ntemprecords[MAXPLAYERS];
+
+// GAMEDATA STRUCTURE
+// Everything that would get saved in gamedata.dat
+typedef struct
+{
+	// WHENEVER OR NOT WE'RE READY TO SAVE
+	boolean loaded;
+
+	// CONDITION SETS ACHIEVED
+	boolean achieved[MAXCONDITIONSETS];
+
+	// EMBLEMS COLLECTED
+	boolean collected[MAXEMBLEMS];
+
+	// EXTRA EMBLEMS COLLECTED
+	boolean extraCollected[MAXEXTRAEMBLEMS];
+
+	// UNLOCKABLES UNLOCKED
+	boolean unlocked[MAXUNLOCKABLES];
+
+	// TIME ATTACK DATA
+	recorddata_t *mainrecords[NUMMAPS];
+	nightsdata_t *nightsrecords[NUMMAPS];
+	UINT8 mapvisited[NUMMAPS];
+
+	// # OF TIMES THE GAME HAS BEEN BEATEN
+	UINT32 timesBeaten;
+	UINT32 timesBeatenWithEmeralds;
+	UINT32 timesBeatenUltimate;
+
+	// PLAY TIME
+	UINT32 totalplaytime;
+} gamedata_t;
+
+extern gamedata_t *clientGamedata;
+extern gamedata_t *serverGamedata;
 
 extern conditionset_t conditionSets[MAXCONDITIONSETS];
 extern emblem_t emblemlocations[MAXEMBLEMS];
@@ -153,25 +229,31 @@ extern INT32 numextraemblems;
 
 extern UINT32 unlocktriggers;
 
+gamedata_t *M_NewGameDataStruct(void);
+void M_CopyGameData(gamedata_t *dest, gamedata_t *src);
+
 // Condition set setup
 void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1, INT16 x2);
 
 // Clearing secrets
 void M_ClearConditionSet(UINT8 set);
-void M_ClearSecrets(void);
+void M_ClearSecrets(gamedata_t *data);
 
 // Updating conditions and unlockables
-void M_CheckUnlockConditions(void);
-UINT8 M_UpdateUnlockablesAndExtraEmblems(void);
-void M_SilentUpdateUnlockablesAndEmblems(void);
-UINT8 M_CheckLevelEmblems(void);
-UINT8 M_CompletionEmblems(void);
+void M_CheckUnlockConditions(gamedata_t *data);
+UINT8 M_UpdateUnlockablesAndExtraEmblems(gamedata_t *data);
+void M_SilentUpdateUnlockablesAndEmblems(gamedata_t *data);
+UINT8 M_CheckLevelEmblems(gamedata_t *data);
+UINT8 M_CompletionEmblems(gamedata_t *data);
+
+void M_SilentUpdateSkinAvailabilites(void);
 
 // Checking unlockable status
-UINT8 M_AnySecretUnlocked(void);
-UINT8 M_SecretUnlocked(INT32 type);
-UINT8 M_MapLocked(INT32 mapnum);
-INT32 M_CountEmblems(void);
+UINT8 M_AnySecretUnlocked(gamedata_t *data);
+UINT8 M_SecretUnlocked(INT32 type, gamedata_t *data);
+UINT8 M_MapLocked(INT32 mapnum, gamedata_t *data);
+UINT8 M_CampaignWarpIsCheat(INT32 gt, INT32 mapnum, gamedata_t *data);
+INT32 M_CountEmblems(gamedata_t *data);
 
 // Emblem shit
 emblem_t *M_GetLevelEmblems(INT32 mapnum);
@@ -183,12 +265,14 @@ const char *M_GetExtraEmblemPatch(extraemblem_t *em, boolean big);
 // If you're looking to compare stats for unlocks or what not, use these
 // They stop checking upon reaching the target number so they
 // should be (theoretically?) slightly faster.
-UINT8 M_GotEnoughEmblems(INT32 number);
-UINT8 M_GotHighEnoughScore(INT32 tscore);
-UINT8 M_GotLowEnoughTime(INT32 tictime);
-UINT8 M_GotHighEnoughRings(INT32 trings);
+UINT8 M_GotEnoughEmblems(INT32 number, gamedata_t *data);
+UINT8 M_GotHighEnoughScore(INT32 tscore, gamedata_t *data);
+UINT8 M_GotLowEnoughTime(INT32 tictime, gamedata_t *data);
+UINT8 M_GotHighEnoughRings(INT32 trings, gamedata_t *data);
 
 INT32 M_UnlockableSkinNum(unlockable_t *unlock);
 INT32 M_EmblemSkinNum(emblem_t *emblem);
 
-#define M_Achieved(a) ((a) >= MAXCONDITIONSETS || conditionSets[a].achieved)
+#define M_Achieved(a, data) ((a) >= MAXCONDITIONSETS || data->achieved[a])
+
+#endif
diff --git a/src/m_fixed.c b/src/m_fixed.c
index ad283119646b75abf514360bcf64d96972e86214..b674e3b2c8e230524d57ea540dada83b8bf75eb6 100644
--- a/src/m_fixed.c
+++ b/src/m_fixed.c
@@ -21,50 +21,6 @@
 #include "doomdef.h"
 #include "m_fixed.h"
 
-#ifdef __USE_C_FIXEDMUL__
-
-/**	\brief	The FixedMul function
-
-	\param	a	fixed_t number
-	\param	b	fixed_t number
-
-	\return	a*b>>FRACBITS
-
-*/
-fixed_t FixedMul(fixed_t a, fixed_t b)
-{
-	// Need to cast to unsigned before shifting to avoid undefined behaviour
-	// for negative integers
-	return (fixed_t)(((UINT64)((INT64)a * b)) >> FRACBITS);
-}
-
-#endif //__USE_C_FIXEDMUL__
-
-#ifdef __USE_C_FIXEDDIV__
-/**	\brief	The FixedDiv2 function
-
-	\param	a	fixed_t number
-	\param	b	fixed_t number
-
-	\return	a/b * FRACUNIT
-
-*/
-fixed_t FixedDiv2(fixed_t a, fixed_t b)
-{
-	INT64 ret;
-
-	if (b == 0)
-		I_Error("FixedDiv: divide by zero");
-
-	ret = (((INT64)a * FRACUNIT)) / b;
-
-	if ((ret > INT32_MAX) || (ret < INT32_MIN))
-		I_Error("FixedDiv: divide by zero");
-	return (fixed_t)ret;
-}
-
-#endif // __USE_C_FIXEDDIV__
-
 fixed_t FixedSqrt(fixed_t x)
 {
 #ifdef HAVE_SQRT
diff --git a/src/m_fixed.h b/src/m_fixed.h
index 4a5b7ce2adf863571000b2134948d0c8e4e3e86f..94bd6a16bbf86e70c4f38c86a4758424da325db1 100644
--- a/src/m_fixed.h
+++ b/src/m_fixed.h
@@ -53,127 +53,35 @@ FUNCMATH FUNCINLINE static ATTRINLINE fixed_t FloatToFixed(float f)
 #define FIXED_TO_FLOAT(x) FixedToFloat(x) // (((float)(x)) / ((float)FRACUNIT))
 #define FLOAT_TO_FIXED(f) FloatToFixed(f) // (fixed_t)((f) * ((float)FRACUNIT))
 
+/**	\brief	The FixedMul function
 
-#if defined (__WATCOMC__) && FRACBITS == 16
-	#pragma aux FixedMul =  \
-		"imul ebx",         \
-		"shrd eax,edx,16"   \
-		parm    [eax] [ebx] \
-		value   [eax]       \
-		modify exact [eax edx]
-
-	#pragma aux FixedDiv2 = \
-		"cdq",              \
-		"shld edx,eax,16",  \
-		"sal eax,16",       \
-		"idiv ebx"          \
-		parm    [eax] [ebx] \
-		value   [eax]       \
-		modify exact [eax edx]
-#elif defined (__GNUC__) && defined (__i386__) && !defined (NOASM)
-	// i386 linux, cygwin or mingw
-	FUNCMATH FUNCINLINE static inline fixed_t FixedMul(fixed_t a, fixed_t b) // asm
-	{
-		fixed_t ret;
-		asm
-		(
-			 "imull %2;"           // a*b
-			 "shrdl %3,%%edx,%0;"  // shift logical right FRACBITS bits
-			:"=a" (ret)            // eax is always the result and the first operand (%0,%1)
-			:"0" (a), "r" (b)      // and %2 is what we use imull on with what in %1
-			, "I" (FRACBITS)       // %3 holds FRACBITS (normally 16)
-			:"cc", "%edx"         // edx and condition codes clobbered
-		);
-		return ret;
-	}
+	\param	a	fixed_t number
+	\param	b	fixed_t number
 
-	FUNCMATH FUNCINLINE static inline fixed_t FixedDiv2(fixed_t a, fixed_t b)
-	{
-		fixed_t ret;
-		asm
-		(
-			  "movl  %1,%%edx;"    // these two instructions allow the next two to pair, on the Pentium processor.
-			  "sarl  $31,%%edx;"   // shift arithmetic right 31 on EDX
-			  "shldl %3,%1,%%edx;" // DP shift logical left FRACBITS on EDX
-			  "sall  %3,%0;"       // shift arithmetic left FRACBITS on EAX
-			  "idivl %2;"          // EDX/b = EAX
-			: "=a" (ret)
-			: "0" (a), "r" (b)
-			, "I" (FRACBITS)
-			: "%edx"
-		);
-		return ret;
-	}
-#elif defined (__GNUC__) && defined (__arm__) && !defined(__thumb__) && !defined(NOASM) //ARMv4 ASM
-	FUNCMATH FUNCINLINE static inline fixed_t FixedMul(fixed_t a, fixed_t b) // let abuse smull
-	{
-		fixed_t ret;
-		asm
-		(
-			  "smull %[lo], r1, %[a], %[b];"
-			  "mov %[lo], %[lo], lsr %3;"
-			  "orr %[lo], %[lo], r1, lsl %3;"
-			: [lo] "=&r" (ret) // rhi, rlo and rm must be distinct registers
-			: [a] "r" (a), [b] "r" (b)
-			, "i" (FRACBITS)
-			: "r1"
-		);
-		return ret;
-	}
+	\return	a*b>>FRACBITS
 
-	#define __USE_C_FIXEDDIV__ // no double or asm div in ARM land
-#elif defined (__GNUC__) && defined (__ppc__) && !defined(NOASM) && 0 // WII: PPC CPU
-	FUNCMATH FUNCINLINE static inline fixed_t FixedMul(fixed_t a, fixed_t b) // asm
-	{
-		fixed_t ret, hi, lo;
-		asm
-		(
-			  "mullw %0, %2, %3;"
-			  "mulhw %1, %2, %3"
-			: "=r" (hi), "=r" (lo)
-			: "r" (a), "r" (b)
-			, "I" (FRACBITS)
-		);
-		ret = (INT64)((hi>>FRACBITS)+lo)<<FRACBITS;
-		return ret;
-	}
+*/
+FUNCMATH FUNCINLINE static ATTRINLINE fixed_t FixedMul(fixed_t a, fixed_t b)
+{
+	// Need to cast to unsigned before shifting to avoid undefined behaviour
+	// for negative integers
+	return (fixed_t)(((UINT64)((INT64)a * b)) >> FRACBITS);
+}
 
-	#define __USE_C_FIXEDDIV__// Alam: I am lazy
-#elif defined (__GNUC__) && defined (__mips__) && !defined(NOASM) && 0 // PSP: MIPS CPU
-	FUNCMATH FUNCINLINE static inline fixed_t FixedMul(fixed_t a, fixed_t b) // asm
-	{
-		fixed_t ret;
-		asm
-		(
-			  "mult %3, %4;"    // a*b=h<32+l
-			: "=r" (ret), "=l" (a), "=h" (b) //todo: abuse shr opcode
-			: "0" (a), "r" (b)
-			, "I" (FRACBITS)
-			//: "+l", "+h"
-		);
-		ret = (INT64)((a>>FRACBITS)+b)<<FRACBITS;
-		return ret;
-	}
+/**	\brief	The FixedDiv2 function
 
-	#define __USE_C_FIXEDDIV__ // no 64b asm div in MIPS land
-#elif defined (__GNUC__) && defined (__sh__) && 0 // DC: SH4 CPU
-#elif defined (__GNUC__) && defined (__m68k__) && 0 // DEAD: Motorola 6800 CPU
-#elif defined (_MSC_VER) && defined(USEASM) && FRACBITS == 16
-	// Microsoft Visual C++ (no asm inline)
-	fixed_t __cdecl FixedMul(fixed_t a, fixed_t b);
-	fixed_t __cdecl FixedDiv2(fixed_t a, fixed_t b);
-#else
-	#define __USE_C_FIXEDMUL__
-	#define __USE_C_FIXEDDIV__
-#endif
+	\param	a	fixed_t number
+	\param	b	fixed_t number
 
-#ifdef __USE_C_FIXEDMUL__
-FUNCMATH fixed_t FixedMul(fixed_t a, fixed_t b);
-#endif
+	\return	a/b * FRACUNIT
 
-#ifdef __USE_C_FIXEDDIV__
-FUNCMATH fixed_t FixedDiv2(fixed_t a, fixed_t b);
-#endif
+*/
+FUNCMATH FUNCINLINE static ATTRINLINE fixed_t FixedDiv2(fixed_t a, fixed_t b)
+{
+	// This does not check for division overflow or division by 0!
+	// That is the caller's responsibility.
+	return (fixed_t)(((INT64)a * FRACUNIT) / b);
+}
 
 /**	\brief	The FixedInt function
 
diff --git a/src/m_menu.c b/src/m_menu.c
index ba11795c98592643522821c8b4097731f3b0702e..ff57fdebdfaaa7622a9bdfe2a056737e88b18bc8 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?
 // ==========================================================================
@@ -241,6 +239,7 @@ static void M_EmblemHints(INT32 choice);
 static void M_HandleEmblemHints(INT32 choice);
 UINT32 hintpage = 1;
 static void M_HandleChecklist(INT32 choice);
+static void M_PauseLevelSelect(INT32 choice);
 menu_t SR_MainDef, SR_UnlockChecklistDef;
 
 static UINT8 check_on;
@@ -295,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);
@@ -303,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
@@ -381,13 +378,12 @@ 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);
 
 // Handling functions
 static boolean M_ExitPandorasBox(void);
@@ -399,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);
 
@@ -501,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},
@@ -554,26 +544,30 @@ static menuitem_t MPauseMenu[] =
 {
 	{IT_STRING | IT_CALL,    NULL, "Add-ons...",                M_Addons,               8},
 	{IT_STRING | IT_SUBMENU, NULL, "Scramble Teams...",         &MISC_ScrambleTeamDef, 16},
-	{IT_STRING | IT_CALL,    NULL, "Switch Gametype/Level...",  M_MapChange,           24},
+	{IT_STRING | IT_CALL,    NULL, "Emblem Hints...",           M_EmblemHints,         24},
+	{IT_STRING | IT_CALL,    NULL, "Switch Gametype/Level...",  M_MapChange,           32},
 
-	{IT_STRING | IT_CALL,    NULL, "Continue",                  M_SelectableClearMenus,40},
-	{IT_STRING | IT_CALL,    NULL, "Player 1 Setup",            M_SetupMultiPlayer,    48}, // splitscreen
-	{IT_STRING | IT_CALL,    NULL, "Player 2 Setup",            M_SetupMultiPlayer2,   56}, // splitscreen
+	{IT_STRING | IT_CALL,    NULL, "Continue",                  M_SelectableClearMenus,48},
 
-	{IT_STRING | IT_CALL,    NULL, "Spectate",                  M_ConfirmSpectate,     48},
-	{IT_STRING | IT_CALL,    NULL, "Enter Game",                M_ConfirmEnterGame,    48},
-	{IT_STRING | IT_SUBMENU, NULL, "Switch Team...",            &MISC_ChangeTeamDef,   48},
-	{IT_STRING | IT_CALL,    NULL, "Player Setup",              M_SetupMultiPlayer,    56}, // alone
-	{IT_STRING | IT_CALL,    NULL, "Options",                   M_Options,             64},
+	{IT_STRING | IT_CALL,    NULL, "Player 1 Setup",            M_SetupMultiPlayer,    56}, // splitscreen
+	{IT_STRING | IT_CALL,    NULL, "Player 2 Setup",            M_SetupMultiPlayer2,   64},
 
-	{IT_STRING | IT_CALL,    NULL, "Return to Title",           M_EndGame,             80},
-	{IT_STRING | IT_CALL,    NULL, "Quit Game",                 M_QuitSRB2,            88},
+	{IT_STRING | IT_CALL,    NULL, "Spectate",                  M_ConfirmSpectate,     56}, // alone
+	{IT_STRING | IT_CALL,    NULL, "Enter Game",                M_ConfirmEnterGame,    56},
+	{IT_STRING | IT_SUBMENU, NULL, "Switch Team...",            &MISC_ChangeTeamDef,   56},
+	{IT_STRING | IT_CALL,    NULL, "Player Setup",              M_SetupMultiPlayer,    64},
+
+	{IT_STRING | IT_CALL,    NULL, "Options",                   M_Options,             72},
+
+	{IT_STRING | IT_CALL,    NULL, "Return to Title",           M_EndGame,             88},
+	{IT_STRING | IT_CALL,    NULL, "Quit Game",                 M_QuitSRB2,            96},
 };
 
 typedef enum
 {
 	mpause_addons = 0,
 	mpause_scramble,
+	mpause_hints,
 	mpause_switchmap,
 
 	mpause_continue,
@@ -597,7 +591,7 @@ static menuitem_t SPauseMenu[] =
 	// Pandora's Box will be shifted up if both options are available
 	{IT_CALL | IT_STRING,    NULL, "Pandora's Box...",     M_PandorasBox,         16},
 	{IT_CALL | IT_STRING,    NULL, "Emblem Hints...",      M_EmblemHints,         24},
-	{IT_CALL | IT_STRING,    NULL, "Level Select...",      M_LoadGameLevelSelect, 32},
+	{IT_CALL | IT_STRING,    NULL, "Level Select...",      M_PauseLevelSelect,    32},
 
 	{IT_CALL | IT_STRING,    NULL, "Continue",             M_SelectableClearMenus,48},
 	{IT_CALL | IT_STRING,    NULL, "Retry",                M_Retry,               56},
@@ -688,42 +682,10 @@ static menuitem_t SR_PandorasBox[] =
 };
 
 // Sky Room Custom Unlocks
-static menuitem_t SR_MainMenu[] =
+static menuitem_t SR_MainMenu[MAXUNLOCKABLES+1] =
 {
 	{IT_STRING|IT_SUBMENU,NULL, "Extras Checklist", &SR_UnlockChecklistDef, 0},
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom1
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom2
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom3
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom4
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom5
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom6
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom7
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom8
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom9
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom10
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom11
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom12
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom13
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom14
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom15
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom16
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom17
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom18
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom19
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom20
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom21
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom22
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom23
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom24
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom25
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom26
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom27
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom28
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom29
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom30
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom31
-	{IT_DISABLED,         NULL, "",   NULL,                 0}, // Custom32
-
+	// The remaining (MAXUNLOCKABLES) items are now initialized in M_SecretsMenu
 };
 
 static menuitem_t SR_LevelSelectMenu[] =
@@ -956,21 +918,15 @@ 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},
 	{IT_STRING|IT_CALL,       NULL, "Server browser...",     M_ConnectMenuModChecks,          12},
-	{IT_STRING|IT_KEYHANDLER, NULL, "Specify IPv4 address:", M_HandleConnectIP,      22},
+	{IT_STRING|IT_KEYHANDLER, NULL, "Specify server address:", M_HandleConnectIP,    22},
 	{IT_HEADER, NULL, "Host a game", NULL, 54},
 	{IT_STRING|IT_CALL,       NULL, "Internet/LAN...",       M_StartServerMenu,      66},
 	{IT_STRING|IT_CALL,       NULL, "Splitscreen...",        M_StartSplitServerMenu, 76},
@@ -1052,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
@@ -1340,17 +1294,17 @@ static menuitem_t OP_VideoOptionsMenu[] =
 	{IT_STRING | IT_CALL,  NULL, "Set Resolution...",       M_VideoModeMenu,          6},
 
 #if defined (__unix__) || defined (UNIXCOMMON) || defined (HAVE_SDL)
-	{IT_STRING|IT_CVAR,      NULL, "Fullscreen",             &cv_fullscreen,         11},
+	{IT_STRING|IT_CVAR,      NULL, "Fullscreen (F11)",          &cv_fullscreen,      11},
 #endif
 	{IT_STRING | IT_CVAR, NULL, "Vertical Sync",                &cv_vidwait,         16},
 #ifdef HWRENDER
-	{IT_STRING | IT_CVAR, NULL, "Renderer",                     &cv_renderer,        21},
+	{IT_STRING | IT_CVAR, NULL, "Renderer (F10)",               &cv_renderer,        21},
 #else
-	{IT_TRANSTEXT | IT_PAIR, "Renderer", "Software",            &cv_renderer,           21},
+	{IT_TRANSTEXT | IT_PAIR, "Renderer", "Software",            &cv_renderer,        21},
 #endif
 
 	{IT_HEADER, NULL, "Color Profile", NULL, 30},
-	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Brightness (F11)", &cv_globalgamma,36},
+	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Brightness", &cv_globalgamma,36},
 	{IT_STRING | IT_CVAR | IT_CV_SLIDER, NULL, "Saturation", &cv_globalsaturation, 41},
 	{IT_SUBMENU|IT_STRING, NULL, "Advanced Settings...",     &OP_ColorOptionsDef,  46},
 
@@ -1612,14 +1566,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},
 
@@ -1658,7 +1610,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},
 
@@ -1666,7 +1617,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[] =
@@ -1824,6 +1774,10 @@ menu_t SP_LevelSelectDef = MAPPLATTERMENUSTYLE(
 	MTREE4(MN_SP_MAIN, MN_SP_LOAD, MN_SP_PLAYER, MN_SP_LEVELSELECT),
 	NULL, SP_LevelSelectMenu);
 
+menu_t SP_PauseLevelSelectDef = MAPPLATTERMENUSTYLE(
+	MTREE4(MN_SP_MAIN, MN_SP_LOAD, MN_SP_PLAYER, MN_SP_LEVELSELECT),
+	NULL, SP_LevelSelectMenu);
+
 menu_t SP_LevelStatsDef =
 {
 	MTREE2(MN_SP_MAIN, MN_SP_LEVELSTATS),
@@ -1976,11 +1930,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,
@@ -1988,8 +1938,6 @@ menu_t MP_SplitServerDef =
 	NULL
 };
 
-#ifndef NONET
-
 menu_t MP_MainDef =
 {
 	MN_MP_MAIN,
@@ -2041,15 +1989,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
@@ -2161,7 +2104,7 @@ static void M_VideoOptions(INT32 choice)
 	{
 		OP_VideoOptionsMenu[op_video_renderer].status = (IT_STRING | IT_CVAR);
 		OP_VideoOptionsMenu[op_video_renderer].patch = NULL;
-		OP_VideoOptionsMenu[op_video_renderer].text = "Renderer";
+		OP_VideoOptionsMenu[op_video_renderer].text = "Renderer (F10)";
 	}
 #endif
 
@@ -2284,6 +2227,7 @@ static boolean M_CanShowLevelOnPlatter(INT32 mapnum, INT32 gt);
 // Nextmap.  Used for Level select.
 void Nextmap_OnChange(void)
 {
+	gamedata_t *data = clientGamedata;
 	char *leveltitle;
 	char tabase[256];
 #ifdef OLDNREPLAYNAME
@@ -2301,7 +2245,7 @@ void Nextmap_OnChange(void)
 	{
 		CV_StealthSetValue(&cv_dummymares, 0);
 		// Hide the record changing CVAR if only one mare is available.
-		if (!nightsrecords[cv_nextmap.value-1] || nightsrecords[cv_nextmap.value-1]->nummares < 2)
+		if (!data->nightsrecords[cv_nextmap.value-1] || data->nightsrecords[cv_nextmap.value-1]->nummares < 2)
 			SP_NightsAttackMenu[narecords].status = IT_DISABLED;
 		else
 			SP_NightsAttackMenu[narecords].status = IT_STRING|IT_CVAR;
@@ -2432,14 +2376,15 @@ void Nextmap_OnChange(void)
 
 static void Dummymares_OnChange(void)
 {
-	if (!nightsrecords[cv_nextmap.value-1])
+	gamedata_t *data = clientGamedata;
+	if (!data->nightsrecords[cv_nextmap.value-1])
 	{
 		CV_StealthSetValue(&cv_dummymares, 0);
 		return;
 	}
 	else
 	{
-		UINT8 mares = nightsrecords[cv_nextmap.value-1]->nummares;
+		UINT8 mares = data->nightsrecords[cv_nextmap.value-1]->nummares;
 
 		if (cv_dummymares.value < 0)
 			CV_StealthSetValue(&cv_dummymares, mares);
@@ -2669,7 +2614,7 @@ static boolean MIT_SetCurBackground(UINT32 menutype, INT32 level, INT32 *retval,
 		{
 			strncpy(curbgname, defaultname, 9);
 			curbgxspeed = (gamestate == GS_TIMEATTACK) ? 0 : titlescrollxspeed;
-			curbgyspeed = (gamestate == GS_TIMEATTACK) ? 0 : titlescrollyspeed;
+			curbgyspeed = (gamestate == GS_TIMEATTACK) ? 18 : titlescrollyspeed;
 		}
 	}
 	return false;
@@ -2807,6 +2752,7 @@ void M_SetMenuCurBackground(const char *defaultname)
 {
 	char name[9];
 	strncpy(name, defaultname, 8);
+	name[8] = '\0';
 	M_IterateMenuTree(MIT_SetCurBackground, &name);
 }
 
@@ -2863,8 +2809,8 @@ static void M_HandleMenuPresState(menu_t *newMenu)
 	curfadevalue = 16;
 	curhidepics = hidetitlepics;
 	curbgcolor = -1;
-	curbgxspeed = titlescrollxspeed;
-	curbgyspeed = titlescrollyspeed;
+	curbgxspeed = (gamestate == GS_TIMEATTACK) ? 0 : titlescrollxspeed;
+	curbgyspeed = (gamestate == GS_TIMEATTACK) ? 18 : titlescrollyspeed;
 	curbghide = (gamestate != GS_TIMEATTACK); // show in time attack, hide in other menus
 
 	curttmode = ttmode;
@@ -3216,7 +3162,7 @@ boolean M_Responder(event_t *ev)
 	|| gamestate == GS_CREDITS || gamestate == GS_EVALUATION || gamestate == GS_GAMEEND)
 		return false;
 
-	if (gamestate == GS_TITLESCREEN && finalecount < TICRATE)
+	if (gamestate == GS_TITLESCREEN && finalecount < (cv_tutorialprompt.value ? TICRATE : 0))
 		return false;
 
 	if (CON_Ready() && gamestate != GS_WAITINGPLAYERS)
@@ -3399,12 +3345,12 @@ boolean M_Responder(event_t *ev)
 			// Screenshots on F8 now handled elsewhere
 			// Same with Moviemode on F9
 
-			case KEY_F10: // Quit SRB2
-				M_QuitSRB2(0);
+			case KEY_F10: // Renderer toggle, also processed inside menus
+				CV_AddValue(&cv_renderer, 1);
 				return true;
 
-			case KEY_F11: // Gamma Level
-				CV_AddValue(&cv_globalgamma, 1);
+			case KEY_F11: // Fullscreen toggle, also processed inside menus
+				CV_SetValue(&cv_fullscreen, !cv_fullscreen.value);
 				return true;
 
 			// Spymode on F12 handled in game logic
@@ -3520,10 +3466,10 @@ boolean M_Responder(event_t *ev)
 #ifndef DEVELOP
 					// TODO: Replays are scary, so I left the remaining instances of this alone.
 					// It'd be nice to get rid of this once and for all though!
-					if (((currentMenu->menuitems[itemOn].status & IT_CALLTYPE) & IT_CALL_NOTMODIFIED) && (modifiedgame && !savemoddata) && !usedCheats)
+					if (((currentMenu->menuitems[itemOn].status & IT_CALLTYPE) & IT_CALL_NOTMODIFIED) && usedCheats)
 					{
 						S_StartSound(NULL, sfx_skid);
-						M_StartMessage(M_GetText("This cannot be done in a modified game.\n\n(Press a key)\n"), NULL, MM_NOTHING);
+						M_StartMessage(M_GetText("This cannot be done in a cheated game.\n\n(Press a key)\n"), NULL, MM_NOTHING);
 						return true;
 					}
 #endif
@@ -3585,6 +3531,14 @@ boolean M_Responder(event_t *ev)
 			//	M_SetupNextMenu(currentMenu->prevMenu);
 			return false;
 
+		case KEY_F10: // Renderer toggle, also processed outside menus
+			CV_AddValue(&cv_renderer, 1);
+			return true;
+
+		case KEY_F11: // Fullscreen toggle, also processed outside menus
+			CV_SetValue(&cv_fullscreen, !cv_fullscreen.value);
+			return true;
+
 		default:
 			CON_Responder(ev);
 			break;
@@ -3670,9 +3624,9 @@ void M_StartControlPanel(void)
 	if (!Playing())
 	{
 		// Secret menu!
-		MainMenu[singleplr].alphaKey = (M_AnySecretUnlocked()) ? 76 : 84;
-		MainMenu[multiplr].alphaKey = (M_AnySecretUnlocked()) ? 84 : 92;
-		MainMenu[secrets].status = (M_AnySecretUnlocked()) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
+		MainMenu[singleplr].alphaKey = (M_AnySecretUnlocked(clientGamedata)) ? 76 : 84;
+		MainMenu[multiplr].alphaKey = (M_AnySecretUnlocked(clientGamedata)) ? 84 : 92;
+		MainMenu[secrets].status = (M_AnySecretUnlocked(clientGamedata)) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 
 		currentMenu = &MainDef;
 		itemOn = singleplr;
@@ -3680,14 +3634,14 @@ void M_StartControlPanel(void)
 	else if (modeattacking)
 	{
 		currentMenu = &MAPauseDef;
-		MAPauseMenu[mapause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS)) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
+		MAPauseMenu[mapause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS, clientGamedata)) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 		itemOn = mapause_continue;
 	}
 	else if (!(netgame || multiplayer)) // Single Player
 	{
 		if (gamestate != GS_LEVEL || ultimatemode) // intermission, so gray out stuff.
 		{
-			SPauseMenu[spause_pandora].status = (M_SecretUnlocked(SECRET_PANDORA)) ? (IT_GRAYEDOUT) : (IT_DISABLED);
+			SPauseMenu[spause_pandora].status = (M_SecretUnlocked(SECRET_PANDORA, serverGamedata)) ? (IT_GRAYEDOUT) : (IT_DISABLED);
 			SPauseMenu[spause_retry].status = IT_GRAYEDOUT;
 		}
 		else
@@ -3696,7 +3650,7 @@ void M_StartControlPanel(void)
 			if (players[consoleplayer].playerstate != PST_LIVE)
 				++numlives;
 
-			SPauseMenu[spause_pandora].status = (M_SecretUnlocked(SECRET_PANDORA) && !marathonmode) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
+			SPauseMenu[spause_pandora].status = (M_SecretUnlocked(SECRET_PANDORA, serverGamedata) && !marathonmode) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 
 			// The list of things that can disable retrying is (was?) a little too complex
 			// for me to want to use the short if statement syntax
@@ -3707,10 +3661,10 @@ void M_StartControlPanel(void)
 		}
 
 		// We can always use level select though. :33
-		SPauseMenu[spause_levelselect].status = (gamecomplete == 1) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
+		SPauseMenu[spause_levelselect].status = (maplistoption != 0) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 
 		// And emblem hints.
-		SPauseMenu[spause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS) && !marathonmode) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
+		SPauseMenu[spause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS, clientGamedata) && !marathonmode) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 
 		// Shift up Pandora's Box if both pandora and levelselect are active
 		/*if (SPauseMenu[spause_pandora].status != (IT_DISABLED)
@@ -3745,12 +3699,10 @@ void M_StartControlPanel(void)
 		if (splitscreen)
 		{
 			MPauseMenu[mpause_psetupsplit].status = MPauseMenu[mpause_psetupsplit2].status = IT_STRING | IT_CALL;
-			MPauseMenu[mpause_psetup].text = "Player 1 Setup";
 		}
 		else
 		{
 			MPauseMenu[mpause_psetup].status = IT_STRING | IT_CALL;
-			MPauseMenu[mpause_psetup].text = "Player Setup";
 
 			if (G_GametypeHasTeams())
 				MPauseMenu[mpause_switchteam].status = IT_STRING | IT_SUBMENU;
@@ -3760,6 +3712,8 @@ void M_StartControlPanel(void)
 				MPauseMenu[mpause_spectate].status = IT_GRAYEDOUT;
 		}
 
+		MPauseMenu[mpause_hints].status = (M_SecretUnlocked(SECRET_EMBLEMHINTS, clientGamedata) && G_CoopGametype()) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
+
 		currentMenu = &MPauseDef;
 		itemOn = mpause_continue;
 	}
@@ -3968,9 +3922,7 @@ void M_Init(void)
 		OP_JoystickSetMenu[i].itemaction = M_AssignJoystick;
 	}
 
-#ifndef NONET
 	CV_RegisterVar(&cv_serversort);
-#endif
 }
 
 void M_InitCharacterTables(void)
@@ -4281,15 +4233,6 @@ static void M_DrawSaveLoadBorder(INT32 x,INT32 y)
 }
 #endif
 
-// horizontally centered text
-static void M_CentreText(INT32 y, const char *string)
-{
-	INT32 x;
-	//added : 02-02-98 : centre on 320, because V_DrawString centers on vid.width...
-	x = (BASEVIDWIDTH - V_StringWidth(string, V_OLDSPACING))>>1;
-	V_DrawString(x,y,V_OLDSPACING,string);
-}
-
 //
 // M_DrawMapEmblems
 //
@@ -4325,7 +4268,7 @@ static void M_DrawMapEmblems(INT32 mapnum, INT32 x, INT32 y, boolean norecordatt
 			x -= 4;
 		lasttype = curtype;
 
-		if (emblem->collected)
+		if (clientGamedata->collected[emblem - emblemlocations])
 			V_DrawSmallMappedPatch(x, y, 0, W_CachePatchName(M_GetEmblemPatch(emblem, false), PU_PATCH),
 			                       R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(emblem), GTC_CACHE));
 		else
@@ -4758,7 +4701,9 @@ static void M_DrawGenericScrollMenu(void)
 
 static void M_DrawPauseMenu(void)
 {
-	if (!netgame && !multiplayer && (gamestate == GS_LEVEL || gamestate == GS_INTERMISSION))
+	gamedata_t *data = clientGamedata;
+
+	if (gamestate == GS_LEVEL || gamestate == GS_INTERMISSION)
 	{
 		emblem_t *emblem_detail[3] = {NULL, NULL, NULL};
 		char emblem_text[3][20];
@@ -4786,7 +4731,7 @@ static void M_DrawPauseMenu(void)
 				{
 					case ET_SCORE:
 						snprintf(targettext, 9, "%d", emblem->var);
-						snprintf(currenttext, 9, "%u", G_GetBestScore(gamemap));
+						snprintf(currenttext, 9, "%u", G_GetBestScore(gamemap, data));
 
 						targettext[8] = 0;
 						currenttext[8] = 0;
@@ -4800,7 +4745,7 @@ static void M_DrawPauseMenu(void)
 							G_TicsToSeconds((tic_t)emblemslot),
 							G_TicsToCentiseconds((tic_t)emblemslot));
 
-						emblemslot = (INT32)G_GetBestTime(gamemap); // dumb hack pt ii
+						emblemslot = (INT32)G_GetBestTime(gamemap, data); // dumb hack pt ii
 						if ((tic_t)emblemslot == UINT32_MAX)
 							snprintf(currenttext, 9, "-:--.--");
 						else
@@ -4816,7 +4761,7 @@ static void M_DrawPauseMenu(void)
 						break;
 					case ET_RINGS:
 						snprintf(targettext, 9, "%d", emblem->var);
-						snprintf(currenttext, 9, "%u", G_GetBestRings(gamemap));
+						snprintf(currenttext, 9, "%u", G_GetBestRings(gamemap, data));
 
 						targettext[8] = 0;
 						currenttext[8] = 0;
@@ -4824,8 +4769,8 @@ static void M_DrawPauseMenu(void)
 						emblemslot = 2;
 						break;
 					case ET_NGRADE:
-						snprintf(targettext, 9, "%u", P_GetScoreForGradeOverall(gamemap, emblem->var));
-						snprintf(currenttext, 9, "%u", G_GetBestNightsScore(gamemap, 0));
+						snprintf(targettext, 9, "%u", P_GetScoreForGrade(gamemap, 0, emblem->var));
+						snprintf(currenttext, 9, "%u", G_GetBestNightsScore(gamemap, 0, data));
 
 						targettext[8] = 0;
 						currenttext[8] = 0;
@@ -4839,7 +4784,7 @@ static void M_DrawPauseMenu(void)
 							G_TicsToSeconds((tic_t)emblemslot),
 							G_TicsToCentiseconds((tic_t)emblemslot));
 
-						emblemslot = (INT32)G_GetBestNightsTime(gamemap, 0); // dumb hack pt iv
+						emblemslot = (INT32)G_GetBestNightsTime(gamemap, 0, data); // dumb hack pt iv
 						if ((tic_t)emblemslot == UINT32_MAX)
 							snprintf(currenttext, 9, "-:--.--");
 						else
@@ -4873,7 +4818,7 @@ static void M_DrawPauseMenu(void)
 			if (!emblem)
 				continue;
 
-			if (emblem->collected)
+			if (data->collected[emblem - emblemlocations])
 				V_DrawSmallMappedPatch(40, 44 + (i*8), 0, W_CachePatchName(M_GetEmblemPatch(emblem, false), PU_PATCH),
 				                       R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(emblem), GTC_CACHE));
 			else
@@ -5085,7 +5030,9 @@ static void M_PatchSkinNameTable(void)
 //
 static boolean M_LevelAvailableOnPlatter(INT32 mapnum)
 {
-	if (M_MapLocked(mapnum+1))
+	gamedata_t *data = serverGamedata;
+
+	if (M_MapLocked(mapnum+1, data))
 		return false; // not unlocked
 
 	switch (levellistmode)
@@ -5098,7 +5045,7 @@ static boolean M_LevelAvailableOnPlatter(INT32 mapnum)
 				return true;
 
 #ifndef DEVELOP
-			if (mapvisited[mapnum]) // MV_MP
+			if (data->mapvisited[mapnum])
 #endif
 				return true;
 
@@ -5106,7 +5053,7 @@ static boolean M_LevelAvailableOnPlatter(INT32 mapnum)
 		case LLM_RECORDATTACK:
 		case LLM_NIGHTSATTACK:
 #ifndef DEVELOP
-			if (mapvisited[mapnum] & MV_MAX)
+			if (data->mapvisited[mapnum])
 				return true;
 
 			if (mapheaderinfo[mapnum]->menuflags & LF2_NOVISITNEEDED)
@@ -5137,7 +5084,7 @@ static boolean M_CanShowLevelOnPlatter(INT32 mapnum, INT32 gt)
 	if (!mapheaderinfo[mapnum]->lvlttl[0])
 		return false;
 
-	/*if (M_MapLocked(mapnum+1))
+	/*if (M_MapLocked(mapnum+1, serverGamedata))
 		return false; // not unlocked*/
 
 	switch (levellistmode)
@@ -6032,7 +5979,7 @@ static void M_DrawLevelPlatterMenu(void)
 			V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, curbgcolor);
 		else if (!curbghide || !titlemapinaction)
 		{
-			F_SkyScroll(curbgxspeed, curbgyspeed, curbgname);
+			F_SkyScroll(curbgname);
 			// Draw and animate foreground
 			if (!strncmp("RECATKBG", curbgname, 8))
 				M_DrawRecordAttackForeground();
@@ -6294,7 +6241,7 @@ static void M_DrawMessageMenu(void)
 			}
 			else
 			{
-				F_SkyScroll(curbgxspeed, curbgyspeed, curbgname);
+				F_SkyScroll(curbgname);
 				if (!strncmp("RECATKBG", curbgname, 8))
 					M_DrawRecordAttackForeground();
 			}
@@ -6970,7 +6917,7 @@ static void M_HandleAddons(INT32 choice)
 		closefilemenu(true);
 
 		// secrets disabled by addfile...
-		MainMenu[secrets].status = (M_AnySecretUnlocked()) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
+		MainMenu[secrets].status = (M_AnySecretUnlocked(clientGamedata)) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
 
 		if (currentMenu->prevMenu)
 			M_SetupNextMenu(currentMenu->prevMenu);
@@ -7183,8 +7130,6 @@ static void M_DestroyRobots(INT32 choice)
 
 static void M_LevelSelectWarp(INT32 choice)
 {
-	boolean fromloadgame = (currentMenu == &SP_LevelSelectDef);
-
 	(void)choice;
 
 	if (W_CheckNumForName(G_BuildMapName(cv_nextmap.value)) == LUMPERROR)
@@ -7194,12 +7139,25 @@ static void M_LevelSelectWarp(INT32 choice)
 	}
 
 	startmap = (INT16)(cv_nextmap.value);
-
 	fromlevelselect = true;
 
-	if (fromloadgame)
-		G_LoadGame((UINT32)cursaveslot, startmap);
-	else
+	if (currentMenu == &SP_LevelSelectDef || currentMenu == &SP_PauseLevelSelectDef)
+	{
+		if (cursaveslot > 0) // do we have a save slot to load?
+			G_LoadGame((UINT32)cursaveslot, startmap); // reload from SP save data: this is needed to keep score/lives/continues from reverting to defaults
+		else // no save slot, start new game but keep the current skin
+		{
+			M_ClearMenus(true);
+
+			G_DeferedInitNew(false, G_BuildMapName(startmap), cv_skin.value, false, fromlevelselect); // Not sure about using cv_skin here, but it seems fine in testing.
+			COM_BufAddText("dummyconsvar 1\n"); // G_DeferedInitNew doesn't do this
+
+			if (levelselect.rows)
+				Z_Free(levelselect.rows);
+			levelselect.rows = NULL;
+		}
+	}
+	else // start new game
 	{
 		cursaveslot = 0;
 		M_SetupChoosePlayer(0);
@@ -7216,7 +7174,9 @@ static boolean checklist_cangodown; // uuuueeerggghhhh HACK
 
 static void M_HandleChecklist(INT32 choice)
 {
+	gamedata_t *data = clientGamedata;
 	INT32 j;
+
 	switch (choice)
 	{
 		case KEY_DOWNARROW:
@@ -7233,7 +7193,7 @@ static void M_HandleChecklist(INT32 choice)
 						continue;
 					if (unlockables[j].conditionset > MAXCONDITIONSETS)
 						continue;
-					if (!unlockables[j].unlocked && unlockables[j].showconditionset && !M_Achieved(unlockables[j].showconditionset))
+					if (!data->unlocked[j] && unlockables[j].showconditionset && !M_Achieved(unlockables[j].showconditionset, data))
 						continue;
 					if (unlockables[j].conditionset == unlockables[check_on].conditionset)
 						continue;
@@ -7258,7 +7218,7 @@ static void M_HandleChecklist(INT32 choice)
 						continue;
 					if (unlockables[j].conditionset > MAXCONDITIONSETS)
 						continue;
-					if (!unlockables[j].unlocked && unlockables[j].showconditionset && !M_Achieved(unlockables[j].showconditionset))
+					if (!data->unlocked[j] && unlockables[j].showconditionset && !M_Achieved(unlockables[j].showconditionset, data))
 						continue;
 					if (j && unlockables[j].conditionset == unlockables[j-1].conditionset)
 						continue;
@@ -7284,6 +7244,9 @@ static void M_HandleChecklist(INT32 choice)
 
 static void M_DrawChecklist(void)
 {
+	gamedata_t *data = clientGamedata;
+	INT32 emblemCount = M_CountEmblems(data);
+
 	INT32 i = check_on, j = 0, y = currentMenu->y, emblems = numemblems+numextraemblems;
 	UINT32 condnum, previd, maxcond;
 	condition_t *cond;
@@ -7294,7 +7257,7 @@ static void M_DrawChecklist(void)
 	// draw emblem counter
 	if (emblems > 0)
 	{
-		V_DrawString(42, 20, (emblems == M_CountEmblems()) ? V_GREENMAP : 0, va("%d/%d", M_CountEmblems(), emblems));
+		V_DrawString(42, 20, (emblems == emblemCount) ? V_GREENMAP : 0, va("%d/%d", emblemCount, emblems));
 		V_DrawSmallScaledPatch(28, 20, 0, W_CachePatchName("EMBLICON", PU_PATCH));
 	}
 
@@ -7305,13 +7268,13 @@ static void M_DrawChecklist(void)
 	{
 		if (unlockables[i].name[0] == 0 //|| unlockables[i].nochecklist
 		|| !unlockables[i].conditionset || unlockables[i].conditionset > MAXCONDITIONSETS
-		|| (!unlockables[i].unlocked && unlockables[i].showconditionset && !M_Achieved(unlockables[i].showconditionset)))
+		|| (!data->unlocked[i] && unlockables[i].showconditionset && !M_Achieved(unlockables[i].showconditionset, data)))
 		{
 			i += 1;
 			continue;
 		}
 
-		V_DrawString(currentMenu->x, y, ((unlockables[i].unlocked) ? V_GREENMAP : V_TRANSLUCENT)|V_ALLOWLOWERCASE, ((unlockables[i].unlocked || !unlockables[i].nochecklist) ? unlockables[i].name : M_CreateSecretMenuOption(unlockables[i].name)));
+		V_DrawString(currentMenu->x, y, ((data->unlocked[i]) ? V_GREENMAP : V_TRANSLUCENT)|V_ALLOWLOWERCASE, ((data->unlocked[i] || !unlockables[i].nochecklist) ? unlockables[i].name : M_CreateSecretMenuOption(unlockables[i].name)));
 
 		for (j = i+1; j < MAXUNLOCKABLES; j++)
 		{
@@ -7389,7 +7352,7 @@ static void M_DrawChecklist(void)
 
 									if (title)
 									{
-										const char *level = ((M_MapLocked(cond[condnum].requirement) || !((mapheaderinfo[cond[condnum].requirement-1]->menuflags & LF2_NOVISITNEEDED) || (mapvisited[cond[condnum].requirement-1] & MV_MAX))) ? M_CreateSecretMenuOption(title) : title);
+										const char *level = ((M_MapLocked(cond[condnum].requirement, data) || !((mapheaderinfo[cond[condnum].requirement-1]->menuflags & LF2_NOVISITNEEDED) || (data->mapvisited[cond[condnum].requirement-1] & MV_MAX))) ? M_CreateSecretMenuOption(title) : title);
 
 										switch (cond[condnum].type)
 										{
@@ -7422,7 +7385,7 @@ static void M_DrawChecklist(void)
 
 									if (title)
 									{
-										const char *level = ((M_MapLocked(cond[condnum].extrainfo1) || !((mapheaderinfo[cond[condnum].extrainfo1-1]->menuflags & LF2_NOVISITNEEDED) || (mapvisited[cond[condnum].extrainfo1-1] & MV_MAX))) ? M_CreateSecretMenuOption(title) : title);
+										const char *level = ((M_MapLocked(cond[condnum].extrainfo1, data) || !((mapheaderinfo[cond[condnum].extrainfo1-1]->menuflags & LF2_NOVISITNEEDED) || (data->mapvisited[cond[condnum].extrainfo1-1] & MV_MAX))) ? M_CreateSecretMenuOption(title) : title);
 
 										switch (cond[condnum].type)
 										{
@@ -7491,7 +7454,7 @@ static void M_DrawChecklist(void)
 
 									if (title)
 									{
-										const char *level = ((M_MapLocked(cond[condnum].extrainfo1) || !((mapheaderinfo[cond[condnum].extrainfo1-1]->menuflags & LF2_NOVISITNEEDED) || (mapvisited[cond[condnum].extrainfo1-1] & MV_MAX))) ? M_CreateSecretMenuOption(title) : title);
+										const char *level = ((M_MapLocked(cond[condnum].extrainfo1, data) || !((mapheaderinfo[cond[condnum].extrainfo1-1]->menuflags & LF2_NOVISITNEEDED) || (data->mapvisited[cond[condnum].extrainfo1-1] & MV_MAX))) ? M_CreateSecretMenuOption(title) : title);
 
 										switch (cond[condnum].type)
 										{
@@ -7554,7 +7517,7 @@ static void M_DrawChecklist(void)
 
 		/*V_DrawString(160, 8+(24*j), V_RETURN8, V_WordWrap(160, 292, 0, unlockables[i].objective));
 
-		if (unlockables[i].unlocked)
+		if (data->unlocked[i])
 			V_DrawString(308, 8+(24*j), V_YELLOWMAP, "Y");
 		else
 			V_DrawString(308, 8+(24*j), V_YELLOWMAP, "N");*/
@@ -7583,7 +7546,7 @@ static void M_EmblemHints(INT32 choice)
 
 	(void)choice;
 	SR_EmblemHintMenu[0].status = (local > NUMHINTS*2) ? (IT_STRING | IT_ARROWS) : (IT_DISABLED);
-	SR_EmblemHintMenu[1].status = (M_SecretUnlocked(SECRET_ITEMFINDER)) ? (IT_CVAR|IT_STRING) : (IT_SECRET);
+	SR_EmblemHintMenu[1].status = (M_SecretUnlocked(SECRET_ITEMFINDER, clientGamedata)) ? (IT_CVAR|IT_STRING) : (IT_SECRET);
 	hintpage = 1;
 	SR_EmblemHintDef.prevMenu = currentMenu;
 	M_SetupNextMenu(&SR_EmblemHintDef);
@@ -7643,7 +7606,7 @@ static void M_DrawEmblemHints(void)
 
 		if (totalemblems >= ((hintpage-1)*(NUMHINTS*2) + 1) && totalemblems < (hintpage*NUMHINTS*2)+1){
 
-			if (emblem->collected)
+			if (clientGamedata->collected[i])
 			{
 				collected = V_GREENMAP;
 				V_DrawMappedPatch(x, y+4, 0, W_CachePatchName(M_GetEmblemPatch(emblem, false), PU_PATCH),
@@ -7713,6 +7676,26 @@ static void M_HandleEmblemHints(INT32 choice)
 
 }
 
+static void M_PauseLevelSelect(INT32 choice)
+{
+	(void)choice;
+
+	SP_PauseLevelSelectDef.prevMenu = currentMenu;
+	levellistmode = LLM_LEVELSELECT;
+
+	// maplistoption is NOT specified, so that this
+	// transfers the level select list from the menu
+	// used to enter the game to the pause menu.
+
+	if (!M_PrepareLevelPlatter(-1, true))
+	{
+		M_StartMessage(M_GetText("No selectable levels found.\n"),NULL,MM_NOTHING);
+		return;
+	}
+
+	M_SetupNextMenu(&SP_PauseLevelSelectDef);
+}
+
 /*static void M_DrawSkyRoom(void)
 {
 	INT32 i, y = 0;
@@ -8142,14 +8125,15 @@ static void M_SecretsMenu(INT32 choice)
 
 	(void)choice;
 
-	// Clear all before starting
-	for (i = 1; i < MAXUNLOCKABLES+1; ++i)
-		SR_MainMenu[i].status = IT_DISABLED;
+	// Initialize array with placeholder entries
+	menuitem_t placeholder = {IT_DISABLED, NULL, "", NULL, 0};
+	for (i = 1; i <= MAXUNLOCKABLES; ++i)
+		SR_MainMenu[i] = placeholder;
 
 	memset(skyRoomMenuTranslations, 0, sizeof(skyRoomMenuTranslations));
 	memset(done, 0, sizeof(done));
 
-	for (i = 1; i < MAXUNLOCKABLES+1; ++i)
+	for (i = 1; i <= MAXUNLOCKABLES; ++i)
 	{
 		curheight = UINT16_MAX;
 		ul = -1;
@@ -8183,7 +8167,7 @@ static void M_SecretsMenu(INT32 choice)
 
 		SR_MainMenu[i].status = IT_SECRET;
 
-		if (unlockables[ul].unlocked)
+		if (clientGamedata->unlocked[ul])
 		{
 			switch (unlockables[ul].type)
 			{
@@ -8220,6 +8204,7 @@ INT32 ultimate_selectable = false;
 static void M_NewGame(void)
 {
 	fromlevelselect = false;
+	maplistoption = 0;
 
 	startmap = spstage_start;
 	CV_SetValue(&cv_newgametype, GT_COOP); // Graue 09-08-2004
@@ -8231,6 +8216,7 @@ static void M_CustomWarp(INT32 choice)
 {
 	INT32 ul = skyRoomMenuTranslations[choice-1];
 
+	maplistoption = 0;
 	startmap = (INT16)(unlockables[ul].variable);
 
 	M_SetupChoosePlayer(0);
@@ -8282,7 +8268,7 @@ static void M_SinglePlayerMenu(INT32 choice)
 
 	levellistmode = LLM_RECORDATTACK;
 	if (M_GametypeHasLevels(-1))
-		SP_MainMenu[sprecordattack].status = (M_SecretUnlocked(SECRET_RECORDATTACK)) ? IT_CALL|IT_STRING : IT_SECRET;
+		SP_MainMenu[sprecordattack].status = (M_SecretUnlocked(SECRET_RECORDATTACK, clientGamedata)) ? IT_CALL|IT_STRING : IT_SECRET;
 	else // If Record Attack is nonexistent in the current add-on...
 	{
 		SP_MainMenu[sprecordattack].status = IT_NOTHING|IT_DISABLED; // ...hide and disable the Record Attack option...
@@ -8292,7 +8278,7 @@ static void M_SinglePlayerMenu(INT32 choice)
 
 	levellistmode = LLM_NIGHTSATTACK;
 	if (M_GametypeHasLevels(-1))
-		SP_MainMenu[spnightsmode].status = (M_SecretUnlocked(SECRET_NIGHTSMODE)) ? IT_CALL|IT_STRING : IT_SECRET;
+		SP_MainMenu[spnightsmode].status = (M_SecretUnlocked(SECRET_NIGHTSMODE, clientGamedata)) ? IT_CALL|IT_STRING : IT_SECRET;
 	else // If NiGHTS Mode is nonexistent in the current add-on...
 	{
 		SP_MainMenu[spnightsmode].status = IT_NOTHING|IT_DISABLED; // ...hide and disable the NiGHTS Mode option...
@@ -8315,7 +8301,7 @@ static void M_SinglePlayerMenu(INT32 choice)
 		SP_MainMenu[spnightsmode]  .alphaKey += 8;
 	}
 	else // Otherwise, if Marathon Run is allowed and Record Attack is unlocked, unlock Marathon Run!
-		SP_MainMenu[spmarathon].status = (M_SecretUnlocked(SECRET_RECORDATTACK)) ? IT_CALL|IT_STRING|IT_CALL_NOTMODIFIED : IT_SECRET;
+		SP_MainMenu[spmarathon].status = (M_SecretUnlocked(SECRET_RECORDATTACK, clientGamedata)) ? IT_CALL|IT_STRING|IT_CALL_NOTMODIFIED : IT_SECRET;
 
 
 	if (tutorialmap) // If there's a tutorial available in the current add-on...
@@ -8423,6 +8409,7 @@ static void M_StartTutorial(INT32 choice)
 	M_ClearMenus(true);
 	gamecomplete = 0;
 	cursaveslot = 0;
+	maplistoption = 0;
 	G_DeferedInitNew(false, G_BuildMapName(tutorialmap), 0, false, false);
 }
 
@@ -8530,7 +8517,7 @@ static void M_DrawLoadGameData(void)
 					if (charskin->prefoppositecolor)
 					{
 						col = charskin->prefoppositecolor;
-						col = skincolors[col].ramp[skincolors[skincolors[col].invcolor].invshade];
+						col = skincolors[col].ramp[skincolors[charskin->prefcolor].invshade];
 					}
 					else
 					{
@@ -8785,6 +8772,10 @@ static void M_LoadSelect(INT32 choice)
 {
 	(void)choice;
 
+	// Reset here, if we want a level select
+	// M_LoadGameLevelSelect will set it for us.
+	maplistoption = 0;
+
 	if (saveSlotSelected == NOSAVESLOT) //last slot is play without saving
 	{
 		M_NewGame();
@@ -9692,7 +9683,7 @@ static void M_Statistics(INT32 choice)
 		if (!(mapheaderinfo[i]->typeoflevel & TOL_SP) || (mapheaderinfo[i]->menuflags & LF2_HIDEINSTATS))
 			continue;
 
-		if (!(mapvisited[i] & MV_MAX))
+		if (!(clientGamedata->mapvisited[i] & MV_MAX))
 			continue;
 
 		statsMapList[j++] = i;
@@ -9709,6 +9700,7 @@ static void M_Statistics(INT32 choice)
 
 static void M_DrawStatsMaps(int location)
 {
+	gamedata_t *data = clientGamedata;
 	INT32 y = 80, i = -1;
 	INT16 mnum;
 	extraemblem_t *exemblem;
@@ -9776,14 +9768,14 @@ static void M_DrawStatsMaps(int location)
 		{
 			exemblem = &extraemblems[i];
 
-			if (exemblem->collected)
+			if (data->extraCollected[i])
 				V_DrawSmallMappedPatch(292, y, 0, W_CachePatchName(M_GetExtraEmblemPatch(exemblem, false), PU_PATCH),
 				                       R_GetTranslationColormap(TC_DEFAULT, M_GetExtraEmblemColor(exemblem), GTC_CACHE));
 			else
 				V_DrawSmallScaledPatch(292, y, 0, W_CachePatchName("NEEDIT", PU_PATCH));
 
 			V_DrawString(20, y, V_YELLOWMAP|V_ALLOWLOWERCASE,
-				(!exemblem->collected && exemblem->showconditionset && !M_Achieved(exemblem->showconditionset))
+				(!data->extraCollected[i] && exemblem->showconditionset && !M_Achieved(exemblem->showconditionset, data))
 				? M_CreateSecretMenuOption(exemblem->description)
 				: exemblem->description);
 		}
@@ -9800,6 +9792,7 @@ bottomarrow:
 
 static void M_DrawLevelStats(void)
 {
+	gamedata_t *data = clientGamedata;
 	char beststr[40];
 
 	tic_t besttime = 0;
@@ -9814,9 +9807,9 @@ static void M_DrawLevelStats(void)
 
 	V_DrawString(20, 24, V_YELLOWMAP, "Total Play Time:");
 	V_DrawCenteredString(BASEVIDWIDTH/2, 32, 0, va("%i hours, %i minutes, %i seconds",
-	                         G_TicsToHours(totalplaytime),
-	                         G_TicsToMinutes(totalplaytime, false),
-	                         G_TicsToSeconds(totalplaytime)));
+	                         G_TicsToHours(data->totalplaytime),
+	                         G_TicsToMinutes(data->totalplaytime, false),
+	                         G_TicsToSeconds(data->totalplaytime)));
 
 	for (i = 0; i < NUMMAPS; i++)
 	{
@@ -9825,25 +9818,25 @@ static void M_DrawLevelStats(void)
 		if (!mapheaderinfo[i] || !(mapheaderinfo[i]->menuflags & LF2_RECORDATTACK))
 			continue;
 
-		if (!mainrecords[i])
+		if (!data->mainrecords[i])
 		{
 			mapsunfinished++;
 			bestunfinished[0] = bestunfinished[1] = bestunfinished[2] = true;
 			continue;
 		}
 
-		if (mainrecords[i]->score > 0)
-			bestscore += mainrecords[i]->score;
+		if (data->mainrecords[i]->score > 0)
+			bestscore += data->mainrecords[i]->score;
 		else
 			mapunfinished = bestunfinished[0] = true;
 
-		if (mainrecords[i]->time > 0)
-			besttime += mainrecords[i]->time;
+		if (data->mainrecords[i]->time > 0)
+			besttime += data->mainrecords[i]->time;
 		else
 			mapunfinished = bestunfinished[1] = true;
 
-		if (mainrecords[i]->rings > 0)
-			bestrings += mainrecords[i]->rings;
+		if (data->mainrecords[i]->rings > 0)
+			bestrings += data->mainrecords[i]->rings;
 		else
 			mapunfinished = bestunfinished[2] = true;
 
@@ -9858,7 +9851,7 @@ static void M_DrawLevelStats(void)
 	else
 		V_DrawString(20, 56, V_GREENMAP, "(complete)");
 
-	V_DrawString(36, 64, 0, va("x %d/%d", M_CountEmblems(), numemblems+numextraemblems));
+	V_DrawString(36, 64, 0, va("x %d/%d", M_CountEmblems(data), numemblems+numextraemblems));
 	V_DrawSmallScaledPatch(20, 64, 0, W_CachePatchName("EMBLICON", PU_PATCH));
 
 	sprintf(beststr, "%u", bestscore);
@@ -9925,6 +9918,7 @@ static void M_HandleLevelStats(INT32 choice)
 // Drawing function for Time Attack
 void M_DrawTimeAttackMenu(void)
 {
+	gamedata_t *data = clientGamedata;
 	INT32 i, x, y, empatx, empaty, cursory = 0;
 	UINT16 dispstatus;
 	patch_t *PictureOfUrFace;	// my WHAT
@@ -9941,7 +9935,7 @@ void M_DrawTimeAttackMenu(void)
 		V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, curbgcolor);
 	else if (!curbghide || !titlemapinaction)
 	{
-		F_SkyScroll(curbgxspeed, curbgyspeed, curbgname);
+		F_SkyScroll(curbgname);
 		// Draw and animate foreground
 		if (!strncmp("RECATKBG", curbgname, 8))
 			M_DrawRecordAttackForeground();
@@ -10083,7 +10077,7 @@ void M_DrawTimeAttackMenu(void)
 			empatx = empatch->leftoffset / 2;
 			empaty = empatch->topoffset / 2;
 
-			if (em->collected)
+			if (data->collected[em - emblemlocations])
 				V_DrawSmallMappedPatch(104+76+empatx, yHeight+lsheadingheight/2+empaty, 0, empatch,
 				                       R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(em), GTC_CACHE));
 			else
@@ -10096,34 +10090,34 @@ void M_DrawTimeAttackMenu(void)
 		// Draw in-level emblems.
 		M_DrawMapEmblems(cv_nextmap.value, 288, 28, true);
 
-		if (!mainrecords[cv_nextmap.value-1] || !mainrecords[cv_nextmap.value-1]->score)
+		if (!data->mainrecords[cv_nextmap.value-1] || !data->mainrecords[cv_nextmap.value-1]->score)
 			sprintf(beststr, "(none)");
 		else
-			sprintf(beststr, "%u", mainrecords[cv_nextmap.value-1]->score);
+			sprintf(beststr, "%u", data->mainrecords[cv_nextmap.value-1]->score);
 
 		V_DrawString(104-72, 33+lsheadingheight/2, V_YELLOWMAP, "SCORE:");
 		V_DrawRightAlignedString(104+64, 33+lsheadingheight/2, V_ALLOWLOWERCASE, beststr);
 		V_DrawRightAlignedString(104+72, 43+lsheadingheight/2, V_ALLOWLOWERCASE, reqscore);
 
-		if (!mainrecords[cv_nextmap.value-1] || !mainrecords[cv_nextmap.value-1]->time)
+		if (!data->mainrecords[cv_nextmap.value-1] || !data->mainrecords[cv_nextmap.value-1]->time)
 			sprintf(beststr, "(none)");
 		else
-			sprintf(beststr, "%i:%02i.%02i", G_TicsToMinutes(mainrecords[cv_nextmap.value-1]->time, true),
-			                                 G_TicsToSeconds(mainrecords[cv_nextmap.value-1]->time),
-			                                 G_TicsToCentiseconds(mainrecords[cv_nextmap.value-1]->time));
+			sprintf(beststr, "%i:%02i.%02i", G_TicsToMinutes(data->mainrecords[cv_nextmap.value-1]->time, true),
+			                                 G_TicsToSeconds(data->mainrecords[cv_nextmap.value-1]->time),
+			                                 G_TicsToCentiseconds(data->mainrecords[cv_nextmap.value-1]->time));
 
 		V_DrawString(104-72, 53+lsheadingheight/2, V_YELLOWMAP, "TIME:");
 		V_DrawRightAlignedString(104+64, 53+lsheadingheight/2, V_ALLOWLOWERCASE, beststr);
 		V_DrawRightAlignedString(104+72, 63+lsheadingheight/2, V_ALLOWLOWERCASE, reqtime);
 
-		if (!mainrecords[cv_nextmap.value-1] || !mainrecords[cv_nextmap.value-1]->rings)
+		if (!data->mainrecords[cv_nextmap.value-1] || !data->mainrecords[cv_nextmap.value-1]->rings)
 			sprintf(beststr, "(none)");
 		else
-			sprintf(beststr, "%hu", mainrecords[cv_nextmap.value-1]->rings);
+			sprintf(beststr, "%hu", data->mainrecords[cv_nextmap.value-1]->rings);
 
 		V_DrawString(104-72, 73+lsheadingheight/2, V_YELLOWMAP, "RINGS:");
 
-		V_DrawRightAlignedString(104+64, 73+lsheadingheight/2, V_ALLOWLOWERCASE|((mapvisited[cv_nextmap.value-1] & MV_PERFECTRA) ? V_YELLOWMAP : 0), beststr);
+		V_DrawRightAlignedString(104+64, 73+lsheadingheight/2, V_ALLOWLOWERCASE|((data->mapvisited[cv_nextmap.value-1] & MV_PERFECTRA) ? V_YELLOWMAP : 0), beststr);
 
 		V_DrawRightAlignedString(104+72, 83+lsheadingheight/2, V_ALLOWLOWERCASE, reqrings);
 	}
@@ -10217,6 +10211,7 @@ static void M_TimeAttack(INT32 choice)
 // Drawing function for Nights Attack
 void M_DrawNightsAttackMenu(void)
 {
+	gamedata_t *data = clientGamedata;
 	INT32 i, x, y, cursory = 0;
 	UINT16 dispstatus;
 
@@ -10283,10 +10278,10 @@ void M_DrawNightsAttackMenu(void)
 		lumpnum_t lumpnum;
 		char beststr[40];
 
-		//UINT8 bestoverall	= G_GetBestNightsGrade(cv_nextmap.value, 0);
-		UINT8 bestgrade		= G_GetBestNightsGrade(cv_nextmap.value, cv_dummymares.value);
-		UINT32 bestscore	= G_GetBestNightsScore(cv_nextmap.value, cv_dummymares.value);
-		tic_t besttime		= G_GetBestNightsTime(cv_nextmap.value, cv_dummymares.value);
+		//UINT8 bestoverall	= G_GetBestNightsGrade(cv_nextmap.value, 0, data);
+		UINT8 bestgrade		= G_GetBestNightsGrade(cv_nextmap.value, cv_dummymares.value, data);
+		UINT32 bestscore	= G_GetBestNightsScore(cv_nextmap.value, cv_dummymares.value, data);
+		tic_t besttime		= G_GetBestNightsTime(cv_nextmap.value, cv_dummymares.value, data);
 
 		M_DrawLevelPlatterHeader(32-lsheadingheight/2, cv_nextmap.string, true, false);
 
@@ -10367,7 +10362,7 @@ void M_DrawNightsAttackMenu(void)
 						goto skipThisOne;
 				}
 
-				if (em->collected)
+				if (data->collected[em - emblemlocations])
 					V_DrawSmallMappedPatch(xpos, yHeight+lsheadingheight/2, 0, W_CachePatchName(M_GetEmblemPatch(em, false), PU_PATCH),
 																 R_GetTranslationColormap(TC_DEFAULT, M_GetEmblemColor(em), GTC_CACHE));
 				else
@@ -10482,13 +10477,23 @@ static void M_ChooseTimeAttack(INT32 choice)
 	G_DeferedInitNew(false, G_BuildMapName(cv_nextmap.value), (UINT8)(cv_chooseskin.value-1), false, false);
 }
 
+static char ra_demoname[1024];
+
+static void M_StartTimeAttackReplay(INT32 choice)
+{
+	if (choice == 'y' || choice == KEY_ENTER)
+	{
+		M_ClearMenus(true);
+		modeattacking = ATTACKING_RECORD; // set modeattacking before G_DoPlayDemo so the map loader knows
+		G_DoPlayDemo(ra_demoname);
+	}
+}
+
 // Player has selected the "REPLAY" from the time attack screen
 static void M_ReplayTimeAttack(INT32 choice)
 {
 	const char *which;
-	char *demoname;
-	M_ClearMenus(true);
-	modeattacking = ATTACKING_RECORD; // set modeattacking before G_DoPlayDemo so the map loader knows
+	UINT8 error = DFILE_ERROR_NONE;
 
 	if (currentMenu == &SP_ReplayDef)
 	{
@@ -10508,11 +10513,15 @@ static void M_ReplayTimeAttack(INT32 choice)
 			break;
 		case 4: // guest
 			// srb2/replay/main/map01-guest.lmp
-			G_DoPlayDemo(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value)));
-			return;
+			snprintf(ra_demoname, 1024, "%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value));
+			break;
+		}
+
+		if (choice != 4)
+		{
+			// srb2/replay/main/map01-sonic-time-best.lmp
+			snprintf(ra_demoname, 1024, "%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value-1].name, which);
 		}
-		// srb2/replay/main/map01-sonic-time-best.lmp
-		G_DoPlayDemo(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value-1].name, which));
 	}
 	else if (currentMenu == &SP_NightsReplayDef)
 	{
@@ -10528,19 +10537,78 @@ static void M_ReplayTimeAttack(INT32 choice)
 			which = "last";
 			break;
 		case 3: // guest
-			G_DoPlayDemo(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value)));
-			return;
+			snprintf(ra_demoname, 1024, "%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value));
+			break;
 		}
 
-		demoname = va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value-1].name, which);
+		if (choice != 3)
+		{
+			snprintf(ra_demoname, 1024, "%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value-1].name, which);
 
 #ifdef OLDNREPLAYNAME // Check for old style named NiGHTS replay if a new style replay doesn't exist.
-		if (!FIL_FileExists(demoname))
-			demoname = va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), which);
+			if (!FIL_FileExists(ra_demoname))
+			{
+				snprintf(ra_demoname, 1024, "%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), which);
+			}
 #endif
+		}
+	}
+
+	demofileoverride = DFILE_OVERRIDE_NONE;
+	error = G_CheckDemoForError(ra_demoname);
+
+	if (error)
+	{
+		S_StartSound(NULL, sfx_skid);
+
+		switch (error)
+		{
+			case DFILE_ERROR_NOTDEMO:
+				M_StartMessage(M_GetText("An error occurred loading this replay.\n\n(Press a key)\n"), NULL, MM_NOTHING);
+				break;
+
+			case DFILE_ERROR_NOTLOADED:
+				demofileoverride = DFILE_OVERRIDE_LOAD;
+				M_StartMessage(M_GetText("Add-ons for this replay\nhave not been loaded.\n\nAttempt to load files?\n\n(Press 'Y' to confirm)\n"), M_StartTimeAttackReplay, MM_YESNO);
+				break;
+
+			case DFILE_ERROR_OUTOFORDER:
+				/*
+				demofileoverride = DFILE_OVERRIDE_SKIP;
+				M_StartMessage(M_GetText("Add-ons for this replay\nwere loaded out of order.\n\nAttempt to playback anyway?\n\n(Press 'Y' to confirm)\n"), M_StartTimeAttackReplay, MM_YESNO);
+				*/
+				M_StartMessage(M_GetText("Add-ons for this replay\nwere loaded out of order.\n\n(Press a key)\n"), NULL, MM_NOTHING);
+				break;
+
+			case DFILE_ERROR_INCOMPLETEOUTOFORDER:
+				/*
+				demofileoverride = DFILE_OVERRIDE_LOAD;
+				M_StartMessage(M_GetText("Add-ons for this replay\nhave not been loaded,\nand some are in the wrong order.\n\nAttempt to load files?\n\n(Press 'Y' to confirm)\n"), M_StartTimeAttackReplay, MM_YESNO);
+				*/
+				M_StartMessage(M_GetText("Add-ons for this replay\nhave not been loaded,\nand some are in the wrong order.\n\n(Press a key)\n"), NULL, MM_NOTHING);
+				break;
+
+			case DFILE_ERROR_CANNOTLOAD:
+				/*
+				demofileoverride = DFILE_OVERRIDE_SKIP;
+				M_StartMessage(M_GetText("Add-ons for this replay\ncould not be loaded.\n\nAttempt to playback anyway?\n\n(Press 'Y' to confirm)\n"), M_StartTimeAttackReplay, MM_YESNO);
+				*/
+				M_StartMessage(M_GetText("Add-ons for this replay\ncould not be loaded.\n\n(Press a key)\n"), NULL, MM_NOTHING);
+				break;
 
-		G_DoPlayDemo(demoname);
+			case DFILE_ERROR_EXTRAFILES:
+				/*
+				demofileoverride = DFILE_OVERRIDE_SKIP;
+				M_StartMessage(M_GetText("You have more files loaded\nthan the replay does.\n\nAttempt to playback anyway?\n\n(Press 'Y' to confirm)\n"), M_StartTimeAttackReplay, MM_YESNO);
+				*/
+				M_StartMessage(M_GetText("You have more files loaded\nthan the replay does.\n\n(Press a key)\n"), NULL, MM_NOTHING);
+				break;
+		}
+
+		return;
 	}
+
+	M_StartTimeAttackReplay(KEY_ENTER);
 }
 
 static void M_EraseGuest(INT32 choice)
@@ -10721,6 +10789,7 @@ static void M_Marathon(INT32 choice)
 	}
 
 	fromlevelselect = false;
+	maplistoption = 0;
 
 	startmap = spmarathon_start;
 	CV_SetValue(&cv_newgametype, GT_COOP); // Graue 09-08-2004
@@ -11069,7 +11138,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)
@@ -11341,11 +11409,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.
@@ -11367,10 +11433,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)
 {
@@ -11569,7 +11633,6 @@ static void M_ChooseRoom(INT32 choice)
 	if (currentMenu == &MP_ConnectDef)
 		M_Refresh(0);
 }
-#endif //NONET
 
 //===========================================================================
 // Start Server Menu
@@ -11617,7 +11680,6 @@ static void M_DrawServerMenu(void)
 {
 	M_DrawGenericMenu();
 
-#ifndef NONET
 	// Room name
 	if (currentMenu == &MP_ServerDef)
 	{
@@ -11629,15 +11691,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];
@@ -11689,7 +11746,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
@@ -11710,7 +11766,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 */
@@ -11722,7 +11777,6 @@ static void M_ServerOptions(INT32 choice)
 	M_SetupNextMenu(&OP_ServerOptionsDef);
 }
 
-#ifndef NONET
 static void M_StartServerMenu(INT32 choice)
 {
 	(void)choice;
@@ -11739,35 +11793,19 @@ static void M_StartServerMenu(INT32 choice)
 
 #define CONNIP_LEN 128
 static char setupm_ip[CONNIP_LEN];
-
 #define DOTS "... "
 
-// Draw the funky Connect IP menu. Tails 11-19-2002
-// So much work for such a little thing!
-static void M_DrawMPMainMenu(void)
+static void M_DrawConnectIP(void)
 {
 	INT32 x = currentMenu->x;
-	INT32 y = currentMenu->y;
+	INT32 y = currentMenu->y + 22;
+
 	const INT32 boxwidth = /*16*8 + 6*/ (BASEVIDWIDTH - 2*(x+5));
 	const INT32 maxstrwidth = boxwidth - 5;
 	char *drawnstr = malloc(sizeof(setupm_ip));
 	char *drawnstr_orig = drawnstr;
 	boolean drawthin, shorten = false;
 
-	// use generic drawer for cursor, items and title
-	M_DrawGenericMenu();
-
-	V_DrawRightAlignedString(BASEVIDWIDTH-x, y+66,
-		((itemOn == 4) ? V_YELLOWMAP : 0), va("(2-%d players)", MAXPLAYERS));
-
-	V_DrawRightAlignedString(BASEVIDWIDTH-x, y+76,
-		((itemOn == 5) ? V_YELLOWMAP : 0), "(2 players)");
-
-	V_DrawRightAlignedString(BASEVIDWIDTH-x, y+116,
-		((itemOn == 8) ? V_YELLOWMAP : 0), "(splitscreen)");
-
-	y += 22;
-
 	V_DrawFill(x+5, y+4+5, boxwidth, 8+6, 159);
 
 	strcpy(drawnstr, setupm_ip);
@@ -11815,6 +11853,28 @@ static void M_DrawMPMainMenu(void)
 	free(drawnstr_orig);
 }
 
+// Draw the funky Connect IP menu. Tails 11-19-2002
+// So much work for such a little thing!
+static void M_DrawMPMainMenu(void)
+{
+	INT32 x = currentMenu->x;
+	INT32 y = currentMenu->y;
+
+	// use generic drawer for cursor, items and title
+	M_DrawGenericMenu();
+
+	V_DrawRightAlignedString(BASEVIDWIDTH-x, y+66,
+		((itemOn == 4) ? V_YELLOWMAP : 0), va("(2-%d players)", MAXPLAYERS));
+
+	V_DrawRightAlignedString(BASEVIDWIDTH-x, y+76,
+		((itemOn == 5) ? V_YELLOWMAP : 0), "(2 players)");
+
+	V_DrawRightAlignedString(BASEVIDWIDTH-x, y+116,
+		((itemOn == 8) ? V_YELLOWMAP : 0), "(splitscreen)");
+
+	M_DrawConnectIP();
+}
+
 #undef DOTS
 
 // Tails 11-19-2002
@@ -11952,7 +12012,11 @@ static void M_HandleConnectIP(INT32 choice)
 				break;
 
 			// Rudimentary number and period enforcing - also allows letters so hostnames can be used instead
-			if ((choice >= '-' && choice <= ':') || (choice >= 'A' && choice <= 'Z') || (choice >= 'a' && choice <= 'z'))
+			// and square brackets for RFC 2732 IPv6 addresses
+			if ((choice >= '-' && choice <= ':') ||
+					(choice == '[' || choice == ']') ||
+					(choice >= 'A' && choice <= 'Z') ||
+					(choice >= 'a' && choice <= 'z'))
 			{
 				S_StartSound(NULL,sfx_menu1); // Tails
 				setupm_ip[l] = (char)choice;
@@ -11979,7 +12043,6 @@ static void M_HandleConnectIP(INT32 choice)
 			M_ClearMenus(true);
 	}
 }
-#endif //!NONET
 
 // ========================
 // MULTIPLAYER PLAYER SETUP
@@ -11989,6 +12052,8 @@ static void M_HandleConnectIP(INT32 choice)
 static fixed_t    multi_tics;
 static UINT8      multi_frame;
 static UINT8      multi_spr2;
+static boolean    multi_paused;
+static boolean    multi_invcolor;
 
 // this is set before entering the MultiPlayer setup menu,
 // for either player 1 or 2
@@ -12001,6 +12066,137 @@ static consvar_t   *setupm_cvdefaultskin;
 static consvar_t   *setupm_cvdefaultcolor;
 static INT32        setupm_fakeskin;
 static menucolor_t *setupm_fakecolor;
+static boolean      colorgrid;
+
+#define COLOR_GRID_ROW_SIZE (16)
+
+static UINT16 M_GetColorGridIndex(UINT16 color)
+{
+	menucolor_t *look;
+	UINT16 i = 0;
+
+	if (!skincolors[color].accessible)
+	{
+		return 0;
+	}
+
+	for (look = menucolorhead; ; i++, look = look->next)
+	{
+		while (!skincolors[look->color].accessible) // skip inaccessible colors
+		{
+			if (look == menucolortail)
+			{
+				return 0;
+			}
+
+			look = look->next;
+		}
+
+		if (look->color == color)
+		{
+			return i;
+		}
+
+		if (look == menucolortail)
+		{
+			return 0;
+		}
+	}
+}
+
+static INT32 M_GridIndexToX(UINT16 index)
+{
+	return (index % COLOR_GRID_ROW_SIZE);
+}
+
+static INT32 M_GridIndexToY(UINT16 index)
+{
+	return (index / COLOR_GRID_ROW_SIZE);
+}
+
+static UINT16 M_ColorGridLen(void)
+{
+	menucolor_t *look;
+	UINT16 i = 0;
+
+	for (look = menucolorhead; ; i++)
+	{
+		do
+		{
+			if (look == menucolortail)
+			{
+				return i;
+			}
+
+			look = look->next;
+		}
+		while (!skincolors[look->color].accessible); // skip inaccessible colors
+	}
+}
+
+static UINT16 M_GridPosToGridIndex(INT32 x, INT32 y)
+{
+	const UINT16 grid_len = M_ColorGridLen();
+	const UINT16 grid_height = ((grid_len - 1) / COLOR_GRID_ROW_SIZE) + 1;
+	const UINT16 last_row_len = COLOR_GRID_ROW_SIZE - ((grid_height * COLOR_GRID_ROW_SIZE) - grid_len);
+
+	UINT16 row_len = COLOR_GRID_ROW_SIZE;
+	UINT16 new_index = 0;
+
+	while (y < 0)
+	{
+		y += grid_height;
+	}
+	y = (y % grid_height);
+
+	if (y >= grid_height-1 && last_row_len > 0)
+	{
+		row_len = last_row_len;
+	}
+
+	while (x < 0)
+	{
+		x += row_len;
+	}
+	x = (x % row_len);
+
+	new_index = (y * COLOR_GRID_ROW_SIZE) + x;
+	if (new_index >= grid_len)
+	{
+		new_index = grid_len - 1;
+	}
+
+	return new_index;
+}
+
+static menucolor_t *M_GridIndexToMenuColor(UINT16 index)
+{
+	menucolor_t *look = menucolorhead;
+	UINT16 i = 0;
+
+	for (look = menucolorhead; ; i++, look = look->next)
+	{
+		while (!skincolors[look->color].accessible) // skip inaccessible colors
+		{
+			if (look == menucolortail)
+			{
+				return menucolorhead;
+			}
+
+			look = look->next;
+		}
+
+		if (i == index)
+		{
+			return look;
+		}
+
+		if (look == menucolortail)
+		{
+			return menucolorhead;
+		}
+	}
+}
 
 static void M_DrawSetupMultiPlayerMenu(void)
 {
@@ -12050,12 +12246,12 @@ static void M_DrawSetupMultiPlayerMenu(void)
 			'\x1D' | V_YELLOWMAP, false);
 	}
 
-	x = BASEVIDWIDTH/2;
+	x = colorgrid ? 92 : BASEVIDWIDTH/2;
 	y += 11;
 
 	// anim the player in the box
 	multi_tics -= renderdeltatics;
-	while (multi_tics <= 0)
+	while (!multi_paused && multi_tics <= 0)
 	{
 		multi_frame++;
 		multi_tics += 4*FRACUNIT;
@@ -12064,7 +12260,8 @@ static void M_DrawSetupMultiPlayerMenu(void)
 #define charw 74
 
 	// draw box around character
-	V_DrawFill(x-(charw/2), y, charw, 84, 159);
+	V_DrawFill(x-(charw/2), y, charw, 84,
+		multi_invcolor ?skincolors[skincolors[setupm_fakecolor->color].invcolor].ramp[skincolors[setupm_fakecolor->color].invshade] : 159);
 
 	sprdef = &skins[setupm_fakeskin].sprites[multi_spr2];
 
@@ -12107,61 +12304,143 @@ faildraw:
 #undef chary
 
 colordraw:
-	x = MP_PlayerSetupDef.x;
-	y += 75;
-
-	M_DrawLevelPlatterHeader(y - (lsheadingheight - 12), "Color", true, false);
-	if (itemOn == 2)
-		cursory = y;
 
-	// draw color string
-	V_DrawRightAlignedString(BASEVIDWIDTH - x, y,
-	             ((MP_PlayerSetupMenu[2].status & IT_TYPE) == IT_SPACE ? V_TRANSLUCENT : 0)|(itemOn == 2 ? V_YELLOWMAP : 0)|V_ALLOWLOWERCASE,
-	             skincolors[setupm_fakecolor->color].name);
+#define indexwidth 8
 
-	if (itemOn == 2 && (MP_PlayerSetupMenu[2].status & IT_TYPE) != IT_SPACE)
+	if (colorgrid) // Draw color grid & skip the later options
 	{
-		V_DrawCharacter(BASEVIDWIDTH - x - 10 - V_StringWidth(skincolors[setupm_fakecolor->color].name, V_ALLOWLOWERCASE) - (skullAnimCounter/5), y,
-			'\x1C' | V_YELLOWMAP, false);
-		V_DrawCharacter(BASEVIDWIDTH - x + 2 + (skullAnimCounter/5), y,
-			'\x1D' | V_YELLOWMAP, false);
-	}
+		UINT16 pos;
+		INT16 cx = 96, cy = 66;
+		INT16 i, j;
+		INT32 w = indexwidth; // Width of a singular color block
+		boolean stoprow = false;
+		menucolor_t *mc; // Last accessed color
 
-	y += 11;
+		const UINT16 grid_len = M_ColorGridLen();
+		const UINT16 grid_end_y = M_GridIndexToY(grid_len - 1);
 
-#define indexwidth 8
+		INT32 grid_select = M_GetColorGridIndex(setupm_fakecolor->color);
+		INT32 grid_select_y = M_GridIndexToY(grid_select);
+
+		x = 132;
+		y = 66;
+
+		pos = M_GridPosToGridIndex(0, max(0, min(grid_select_y - 3, grid_end_y - 7)));
+		mc = M_GridIndexToMenuColor(pos);
+
+		// Draw grid
+		V_DrawFill(x-2, y-2, 132, 132, 159);
+		for (j = 0; j < 8; j++)
+		{
+			for (i = 0; i < COLOR_GRID_ROW_SIZE; i++)
+			{
+				if (skincolors[mc->color].accessible)
+				{
+					M_DrawColorRamp(x + i*w, y + j*16, w, 1, skincolors[mc->color]);
+
+					if (mc == setupm_fakecolor) // store current color position
+					{
+						cx = x + i*w;
+						cy = y + j*16;
+					}
+				}
+
+				if (stoprow)
+				{
+					break;
+				}
+
+				// Find accessible color after this one
+				do
+				{
+					mc = mc->next;
+					if (mc == menucolortail)
+					{
+						stoprow = true;
+					}
+				} while (!skincolors[mc->color].accessible && !stoprow);
+			}
+
+			if (stoprow)
+			{
+				break;
+			}
+		}
+
+		// Draw arrows, if needed
+		if (pos > 0)
+			V_DrawCharacter(264, y - (skullAnimCounter/5), '\x1A' | V_YELLOWMAP, false);
+		if (!stoprow)
+			V_DrawCharacter(264, y+120 + (skullAnimCounter/5), '\x1B' | V_YELLOWMAP, false);
+
+		// Draw cursor & current skincolor
+		V_DrawFill(cx - 2, cy - 2, 12, 20, 0);
+		V_DrawFill(cx - 1, cy - 1, 11, 19, 31);
+		V_DrawFill(    cx,     cy,  9, 17, 0);
+		M_DrawColorRamp(cx, cy, w, 1, skincolors[setupm_fakecolor->color]);
+
+		// Draw color string (with background)
+		V_DrawFill(55, 148,  74, 1, 73);
+		V_DrawFill(55, 149,  74, 1, 26);
+		M_DrawColorRamp(55, 150, 74, 1, skincolors[setupm_fakecolor->color]);
+		V_DrawRightAlignedString(x-2,166,
+								 ((MP_PlayerSetupMenu[2].status & IT_TYPE) == IT_SPACE ? V_TRANSLUCENT : 0)|(itemOn == 2 ? V_YELLOWMAP : 0)|V_ALLOWLOWERCASE,
+								 skincolors[setupm_fakecolor->color].name);
+
+		return; // Don't draw anything after this
+	}
+	else // Draw color strip & the rest of the menu options
 	{
 		const INT32 numcolors = (282-charw)/(2*indexwidth); // Number of colors per side
 		INT32 w = indexwidth; // Width of a singular color block
 		menucolor_t *mc = setupm_fakecolor->prev; // Last accessed color
-		UINT8 h;
 		INT16 i;
 
+		x = MP_PlayerSetupDef.x;
+		y += 75;
+
+		// Draw color header & string
+		M_DrawLevelPlatterHeader(y - (lsheadingheight - 12), "Color...", true, false);
+		V_DrawRightAlignedString(BASEVIDWIDTH - x, y,
+								 ((MP_PlayerSetupMenu[2].status & IT_TYPE) == IT_SPACE ? V_TRANSLUCENT : 0)|(itemOn == 2 ? V_YELLOWMAP : 0)|V_ALLOWLOWERCASE,
+								 skincolors[setupm_fakecolor->color].name);
+
+		// Draw horizontal arrows
+		if (itemOn == 2)
+		{
+			cursory = y;
+			if ((MP_PlayerSetupMenu[2].status & IT_TYPE) != IT_SPACE)
+			{
+				V_DrawCharacter(BASEVIDWIDTH - x - 10 - V_StringWidth(skincolors[setupm_fakecolor->color].name, V_ALLOWLOWERCASE) - (skullAnimCounter/5), y,
+					'\x1C' | V_YELLOWMAP, false);
+				V_DrawCharacter(BASEVIDWIDTH - x + 2 + (skullAnimCounter/5), y,
+					'\x1D' | V_YELLOWMAP, false);
+			}
+		}
+
 		// Draw color in the middle
 		x += numcolors*w;
-		for (h = 0; h < 16; h++)
-			V_DrawFill(x, y+h, charw, 1, skincolors[setupm_fakecolor->color].ramp[h]);
+		y += 11;
+		M_DrawColorRamp(x, y, charw, 1, skincolors[setupm_fakecolor->color]);
 
-		//Draw colors from middle to left
-		for (i=0; i<numcolors; i++) {
+		// Draw colors from middle to left
+		for (i = 0; i < numcolors; i++)
+		{
 			x -= w;
-			// Find accessible color before this one
-			while (!skincolors[mc->color].accessible)
+			while (!skincolors[mc->color].accessible) // Find accessible color before this one
 				mc = mc->prev;
-			for (h = 0; h < 16; h++)
-				V_DrawFill(x, y+h, w, 1, skincolors[mc->color].ramp[h]);
+			M_DrawColorRamp(x, y, w, 1, skincolors[mc->color]);
 			mc = mc->prev;
 		}
 
 		// Draw colors from middle to right
 		mc = setupm_fakecolor->next;
 		x += numcolors*w + charw;
-		for (i=0; i<numcolors; i++) {
-			// Find accessible color after this one
-			while (!skincolors[mc->color].accessible)
+		for (i = 0; i < numcolors; i++)
+		{
+			while (!skincolors[mc->color].accessible) // Find accessible color after this one
 				mc = mc->next;
-			for (h = 0; h < 16; h++)
-				V_DrawFill(x, y+h, w, 1, skincolors[mc->color].ramp[h]);
+			M_DrawColorRamp(x, y, w, 1, skincolors[mc->color]);
 			x += w;
 			mc = mc->next;
 		}
@@ -12186,6 +12465,13 @@ colordraw:
 		W_CachePatchName("M_CURSOR", PU_PATCH));
 }
 
+static void M_DrawColorRamp(INT32 x, INT32 y, INT32 w, INT32 h, skincolor_t color)
+{
+	UINT8 i;
+	for (i = 0; i < 16; i++)
+		V_DrawFill(x, y+(i*h), w, h, color.ramp[i]);
+}
+
 // Handle 1P/2P MP Setup
 static void M_HandleSetupMultiPlayer(INT32 choice)
 {
@@ -12196,13 +12482,26 @@ static void M_HandleSetupMultiPlayer(INT32 choice)
 	switch (choice)
 	{
 		case KEY_DOWNARROW:
-			M_NextOpt();
-			S_StartSound(NULL,sfx_menu1); // Tails
-			break;
-
 		case KEY_UPARROW:
-			M_PrevOpt();
-			S_StartSound(NULL,sfx_menu1); // Tails
+			{
+				if (itemOn == 2 && colorgrid)
+				{
+					UINT16 index = M_GetColorGridIndex(setupm_fakecolor->color);
+					INT32 x = M_GridIndexToX(index);
+					INT32 y = M_GridIndexToY(index);
+
+					y += (choice == KEY_UPARROW) ? -1 : 1;
+
+					index = M_GridPosToGridIndex(x, y);
+					setupm_fakecolor = M_GridIndexToMenuColor(index);
+				}
+				else if (choice == KEY_UPARROW)
+					M_PrevOpt();
+				else
+					M_NextOpt();
+
+				S_StartSound(NULL,sfx_menu1);
+			}
 			break;
 
 		case KEY_LEFTARROW:
@@ -12221,8 +12520,8 @@ static void M_HandleSetupMultiPlayer(INT32 choice)
 			}
 			else if (itemOn == 2) // player color
 			{
-				S_StartSound(NULL,sfx_menu1); // Tails
 				setupm_fakecolor = setupm_fakecolor->prev;
+				S_StartSound(NULL,sfx_menu1); // Tails
 			}
 			break;
 
@@ -12237,6 +12536,13 @@ static void M_HandleSetupMultiPlayer(INT32 choice)
 				COM_BufAddText (va("%s %d\n",setupm_cvdefaultcolor->name,setupm_fakecolor->color));
 				break;
 			}
+			else if (itemOn == 2)
+			{
+				if (!colorgrid)
+					S_StartSound(NULL,sfx_menu1);
+				colorgrid = !colorgrid;
+				break;
+			}
 			/* FALLTHRU */
 		case KEY_RIGHTARROW:
 			if (itemOn == 1)       //player skin
@@ -12254,13 +12560,48 @@ static void M_HandleSetupMultiPlayer(INT32 choice)
 			}
 			else if (itemOn == 2) // player color
 			{
-				S_StartSound(NULL,sfx_menu1); // Tails
 				setupm_fakecolor = setupm_fakecolor->next;
+				S_StartSound(NULL,sfx_menu1); // Tails
+			}
+			break;
+
+		case KEY_PGUP:
+		case KEY_PGDN:
+			{
+				UINT8 i;
+				if (itemOn == 2) // player color
+				{
+					if (colorgrid)
+					{
+						UINT16 index = M_GetColorGridIndex(setupm_fakecolor->color);
+						INT32 x = M_GridIndexToX(index);
+						INT32 y = M_GridIndexToY(index);
+
+						y += (choice == KEY_UPARROW) ? -4 : 4;
+
+						index = M_GridPosToGridIndex(x, y);
+						setupm_fakecolor = M_GridIndexToMenuColor(index);
+					}
+					else
+					{
+						for (i = 0; i < 13; i++) // or (282-charw)/(2*indexwidth)
+						{
+							setupm_fakecolor = (choice == KEY_PGUP) ? setupm_fakecolor->prev : setupm_fakecolor->next;
+							while (!skincolors[setupm_fakecolor->color].accessible) // skip inaccessible colors
+								setupm_fakecolor = (choice == KEY_PGUP) ? setupm_fakecolor->prev : setupm_fakecolor->next;
+						}
+					}
+
+					S_StartSound(NULL, sfx_menu1); // Tails
+				}
 			}
 			break;
 
 		case KEY_ESCAPE:
-			exitmenu = true;
+			if (itemOn == 2 && colorgrid)
+				colorgrid = false;
+			else
+				exitmenu = true;
 			break;
 
 		case KEY_BACKSPACE:
@@ -12281,7 +12622,6 @@ static void M_HandleSetupMultiPlayer(INT32 choice)
 				}
 			}
 			break;
-			break;
 
 		case KEY_DEL:
 			if (itemOn == 0 && (l = strlen(setupm_name))!=0)
@@ -12291,6 +12631,14 @@ static void M_HandleSetupMultiPlayer(INT32 choice)
 			}
 			break;
 
+		case KEY_PAUSE:
+			multi_paused = !multi_paused;
+			break;
+
+		case KEY_INS:
+			multi_invcolor = !multi_invcolor;
+			break;
+
 		default:
 			if (itemOn != 0 || choice < 32 || choice > 127)
 				break;
@@ -12330,6 +12678,7 @@ static void M_SetupMultiPlayer(INT32 choice)
 
 	multi_frame = 0;
 	multi_tics = 4*FRACUNIT;
+
 	strcpy(setupm_name, cv_playername.string);
 
 	// set for player 1
@@ -12355,11 +12704,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);
 
@@ -12374,6 +12719,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
@@ -12399,11 +12745,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);
 
@@ -12570,6 +12912,42 @@ UINT16 M_GetColorAfter(UINT16 color) {
 	}
 }
 
+UINT16 M_GetColorIndex(UINT16 color) {
+	menucolor_t *look;
+	UINT16 i = 0;
+
+	if (color >= numskincolors) {
+		CONS_Printf("M_GetColorIndex: color %d does not exist.\n",color);
+		return 0;
+	}
+
+	for (look=menucolorhead;;look=look->next) {
+		if (look->color == color)
+			return i;
+		if (look==menucolortail)
+			return 0;
+		i++;
+	}
+}
+
+menucolor_t* M_GetColorFromIndex(UINT16 index) {
+	menucolor_t *look = menucolorhead;
+	UINT16 i = 0;
+
+	if (index >= numskincolors) {
+		CONS_Printf("M_GetColorIndex: index %d does not exist.\n",index);
+		return 0;
+	}
+
+	for (i = 0; i <= index; i++) {
+		if (look==menucolortail)
+			return menucolorhead;
+		look=look->next;
+	}
+
+	return look;
+}
+
 void M_InitPlayerSetupColors(void) {
 	UINT8 i;
 	numskincolors = SKINCOLOR_FIRSTFREESLOT;
@@ -12610,12 +12988,12 @@ static void M_EraseDataResponse(INT32 ch)
 
 	// Delete the data
 	if (erasecontext != 1)
-		G_ClearRecords();
+		G_ClearRecords(clientGamedata);
 	if (erasecontext != 0)
-		M_ClearSecrets();
+		M_ClearSecrets(clientGamedata);
 	if (erasecontext == 2)
 	{
-		totalplaytime = 0;
+		clientGamedata->totalplaytime = 0;
 		F_StartIntro();
 	}
 	BwehHehHe();
@@ -12971,12 +13349,12 @@ static void M_DrawControl(void)
 	if (tutorialmode && tutorialgcs)
 	{
 		if ((gametic / TICRATE) % 2)
-			M_CentreText(30, "\202EXIT THE TUTORIAL TO CHANGE THE CONTROLS");
+			V_DrawCenteredString(BASEVIDWIDTH/2, 30, 0, "\202EXIT THE TUTORIAL TO CHANGE THE CONTROLS");
 		else
-			M_CentreText(30, "EXIT THE TUTORIAL TO CHANGE THE CONTROLS");
+			V_DrawCenteredString(BASEVIDWIDTH/2, 30, 0, "EXIT THE TUTORIAL TO CHANGE THE CONTROLS");
 	}
 	else
-		M_CentreText(30,
+		V_DrawCenteredString(BASEVIDWIDTH/2, 30, 0,
 		    (setupcontrols_secondaryplayer ? "SET CONTROLS FOR SECONDARY PLAYER" :
 		                                     "PRESS ENTER TO CHANGE, BACKSPACE TO CLEAR"));
 
@@ -13341,11 +13719,11 @@ static void M_DrawVideoMode(void)
 	// draw title
 	M_DrawMenuTitle();
 
-	V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y,
-		V_YELLOWMAP, "Choose mode, reselect to change default");
+	V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y, V_YELLOWMAP, "Choose mode, reselect to change default");
+	V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y+8, V_YELLOWMAP, "Press F11 to toggle fullscreen");
 
 	row = 41;
-	col = OP_VideoModeDef.y + 14;
+	col = OP_VideoModeDef.y + 24;
 	for (i = 0; i < vidm_nummodes; i++)
 	{
 		if (i == vidm_selected)
@@ -13358,7 +13736,7 @@ static void M_DrawVideoMode(void)
 		if ((i % vidm_column_size) == (vidm_column_size-1))
 		{
 			row += 7*13;
-			col = OP_VideoModeDef.y + 14;
+			col = OP_VideoModeDef.y + 24;
 		}
 	}
 
@@ -13366,28 +13744,34 @@ static void M_DrawVideoMode(void)
 	{
 		INT32 testtime = (vidm_testingmode/TICRATE) + 1;
 
-		M_CentreText(OP_VideoModeDef.y + 116,
+		V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 116, 0,
 			va("Previewing mode %c%dx%d",
 				(SCR_IsAspectCorrect(vid.width, vid.height)) ? 0x83 : 0x80,
 				vid.width, vid.height));
-		M_CentreText(OP_VideoModeDef.y + 138,
+		V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 138, 0,
 			"Press ENTER again to keep this mode");
-		M_CentreText(OP_VideoModeDef.y + 150,
+		V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 150, 0,
 			va("Wait %d second%s", testtime, (testtime > 1) ? "s" : ""));
-		M_CentreText(OP_VideoModeDef.y + 158,
+		V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 158, 0,
 			"or press ESC to return");
-
 	}
 	else
 	{
-		M_CentreText(OP_VideoModeDef.y + 116,
+		V_DrawFill(60, OP_VideoModeDef.y + 98, 200, 12, 159);
+		V_DrawFill(60, OP_VideoModeDef.y + 114, 200, 20, 159);
+
+		V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 100, 0,
 			va("Current mode is %c%dx%d",
 				(SCR_IsAspectCorrect(vid.width, vid.height)) ? 0x83 : 0x80,
 				vid.width, vid.height));
-		M_CentreText(OP_VideoModeDef.y + 124,
+		V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 116, (cv_fullscreen.value ? 0 : V_TRANSLUCENT),
 			va("Default mode is %c%dx%d",
-				(SCR_IsAspectCorrect(cv_scr_width.value, cv_scr_height.value)) ? 0x83 : 0x80,
+				(SCR_IsAspectCorrect(cv_scr_width.value, cv_scr_height.value)) ? 0x83 : (!(VID_GetModeForSize(cv_scr_width.value, cv_scr_height.value)+1) ? 0x85 : 0x80),
 				cv_scr_width.value, cv_scr_height.value));
+		V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 124, (cv_fullscreen.value ? V_TRANSLUCENT : 0),
+			va("Windowed mode is %c%dx%d",
+				(SCR_IsAspectCorrect(cv_scr_width_w.value, cv_scr_height_w.value)) ? 0x83 : (!(VID_GetModeForSize(cv_scr_width_w.value, cv_scr_height_w.value)+1) ? 0x85 : 0x80),
+				cv_scr_width_w.value, cv_scr_height_w.value));
 
 		V_DrawCenteredString(BASEVIDWIDTH/2, OP_VideoModeDef.y + 138,
 			V_GREENMAP, "Green modes are recommended.");
@@ -13399,7 +13783,7 @@ static void M_DrawVideoMode(void)
 
 	// Draw the cursor for the VidMode menu
 	i = 41 - 10 + ((vidm_selected / vidm_column_size)*7*13);
-	j = OP_VideoModeDef.y + 14 + ((vidm_selected % vidm_column_size)*8);
+	j = OP_VideoModeDef.y + 24 + ((vidm_selected % vidm_column_size)*8);
 
 	V_DrawScaledPatch(i - 8, j, 0,
 		W_CachePatchName("M_CURSOR", PU_PATCH));
@@ -13582,11 +13966,14 @@ static void M_HandleVideoMode(INT32 ch)
 			break;
 
 		case KEY_ENTER:
-			S_StartSound(NULL, sfx_menu1);
 			if (vid.modenum == modedescs[vidm_selected].modenum)
+			{
+				S_StartSound(NULL, sfx_strpst);
 				SCR_SetDefaultMode();
+			}
 			else
 			{
+				S_StartSound(NULL, sfx_menu1);
 				vidm_testingmode = 15*TICRATE;
 				vidm_previousmode = vid.modenum;
 				if (!setmodeneeded) // in case the previous setmode was not finished
@@ -13601,6 +13988,27 @@ static void M_HandleVideoMode(INT32 ch)
 				M_ClearMenus(true);
 			break;
 
+		case KEY_BACKSPACE:
+			S_StartSound(NULL, sfx_menu1);
+			CV_Set(&cv_scr_width, cv_scr_width.defaultvalue);
+			CV_Set(&cv_scr_height, cv_scr_height.defaultvalue);
+			CV_Set(&cv_scr_width_w, cv_scr_width_w.defaultvalue);
+			CV_Set(&cv_scr_height_w, cv_scr_height_w.defaultvalue);
+			if (cv_fullscreen.value)
+				setmodeneeded = VID_GetModeForSize(cv_scr_width.value, cv_scr_height.value)+1;
+			else
+				setmodeneeded = VID_GetModeForSize(cv_scr_width_w.value, cv_scr_height_w.value)+1;
+			break;
+
+		case KEY_F10: // Renderer toggle, also processed inside menus
+			CV_AddValue(&cv_renderer, 1);
+			break;
+
+		case KEY_F11:
+			S_StartSound(NULL, sfx_menu1);
+			CV_SetValue(&cv_fullscreen, !cv_fullscreen.value);
+			break;
+
 		default:
 			break;
 	}
diff --git a/src/m_menu.h b/src/m_menu.h
index 35c77cc438a8c295c3d86d5caa5b127548cc0df8..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
@@ -482,6 +482,8 @@ void M_MoveColorBefore(UINT16 color, UINT16 targ);
 void M_MoveColorAfter(UINT16 color, UINT16 targ);
 UINT16 M_GetColorBefore(UINT16 color);
 UINT16 M_GetColorAfter(UINT16 color);
+UINT16 M_GetColorIndex(UINT16 color);
+menucolor_t* M_GetColorFromIndex(UINT16 index);
 void M_InitPlayerSetupColors(void);
 void M_FreePlayerSetupColors(void);
 
diff --git a/src/m_misc.c b/src/m_misc.c
index c24896600e1b69533073c2bee6433d3988ae98c4..f547f5c41ac8fb0fea405f8ed760ffe4070fdeb2 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -63,7 +63,7 @@ typedef off_t off64_t;
 
 #if defined(__MINGW32__) && ((__GNUC__ > 7) || (__GNUC__ == 6 && __GNUC_MINOR__ >= 3)) && (__GNUC__ < 8)
 #define PRIdS "u"
-#elif defined (_WIN32)
+#elif defined(_WIN32) && !defined(__MINGW64__)
 #define PRIdS "Iu"
 #else
 #define PRIdS "zu"
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/m_random.c b/src/m_random.c
index 3d0774a60b6c73895a31cfcb2f5d69d6fbfda02f..536fbfbbd1077abf6ae400bd4d8773a7574e4b08 100644
--- a/src/m_random.c
+++ b/src/m_random.c
@@ -3,6 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh.
+// Copyright (C) 2022-2023 by tertu marybig.
 // Copyright (C) 1999-2023 by Sonic Team Junior.
 //
 // This program is free software distributed under the
@@ -14,12 +15,122 @@
 
 #include "doomdef.h"
 #include "doomtype.h"
-#include "doomstat.h" // totalplaytime
+#include "i_system.h" // I_GetRandomBytes
 
 #include "m_random.h"
 #include "m_fixed.h"
 
+// SFC32 random number generator implementation
 
+typedef struct rnstate_s {
+	UINT32 data[3];
+	UINT32 counter;
+} rnstate_t;
+
+/** Generate a raw uniform random number using a particular state.
+  *
+  * \param state The RNG state to use.
+  * \return A random UINT32.
+  */
+static inline UINT32 RandomState_Get32(rnstate_t *state) {
+	UINT32 result, b, c;
+
+	b = state->data[1];
+	c = state->data[2];
+	result = state->data[0] + b + state->counter++;
+
+	state->data[0] = b ^ (b >> 9);
+	state->data[1] = c * 9;
+	state->data[2] = ((c << 21) | (c >> 11)) + result;
+
+	return result;
+}
+
+/** Seed an SFC32 RNG state with up to 96 bits of seed data.
+  *
+  * \param state The RNG state to seed.
+  * \param seeds A pointer to up to 3 UINT32s to use as seed data.
+  * \param seed_count The number of seed words.
+  */
+static inline void RandomState_Seed(rnstate_t *state, UINT32 *seeds, size_t seed_count)
+{
+	size_t i;
+
+	state->counter = 1;
+
+	for(i = 0; i < 3; i++)
+	{
+		UINT32 seed_word;
+
+		if(i < seed_count)
+			seed_word = seeds[i];
+		else
+			seed_word = 0;
+
+		// For SFC32, seed data should be stored in the state in reverse order.
+		state->data[2-i] = seed_word;
+	}
+
+	for(i = 0; i < 16; i++)
+		RandomState_Get32(state);
+}
+
+/** Gets a uniform number in the range [0, limit).
+  * Technique is based on a combination of scaling and rejection sampling
+  * and is adapted from Daniel Lemire.
+  *
+  * \note Any UINT32 is a valid argument for limit.
+  *
+  * \param state The RNG state to use.
+  * \param limit The upper limit of the range.
+  * \return A UINT32 in the range [0, limit).
+  */
+static inline UINT32 RandomState_GetKey32(rnstate_t *state, const UINT32 limit)
+{
+	UINT32 raw_random, scaled_lower_word;
+	UINT64 scaled_random;
+
+	// This algorithm won't work correctly if passed a 0.
+	if (limit == 0) return 0;
+
+	raw_random = RandomState_Get32(state);
+	scaled_random = (UINT64)raw_random * (UINT64)limit;
+
+	/*The high bits of scaled_random now contain the number we want, but it is
+	possible, depending on the number we generated and the value of limit,
+	that there is bias in the result. The rest of this code is for ensuring
+	that does not happen.
+	*/
+	scaled_lower_word = (UINT32)scaled_random;
+
+	// If we're lucky, we can bail out now and avoid the division
+	if (scaled_lower_word < limit)
+	{
+		// Scale the limit to improve the chance of success.
+		// After this, the first result might turn out to be good enough.
+		UINT32 scaled_limit;
+		// An explanation for this trick: scaled_limit should be
+		// (UINT32_MAX+1)%range, but if that was computed directly the result
+		// would need to be computed as a UINT64. This trick allows it to be
+		// computed using 32-bit arithmetic.
+		scaled_limit = (-limit) % limit;
+
+		while (scaled_lower_word < scaled_limit)
+		{
+			raw_random = RandomState_Get32(state);
+			scaled_random = (UINT64)raw_random * (UINT64)limit;
+			scaled_lower_word = (UINT32)scaled_random;
+		}
+	}
+
+	return scaled_random >> 32;
+}
+
+// The default seed is the hexadecimal digits of pi, though it will be overwritten.
+static rnstate_t m_randomstate = {
+	.data = {0x4A3B6035U, 0x99555606U, 0x6F603421U},
+	.counter = 16
+};
 
 // ---------------------------
 // RNG functions (not synched)
@@ -32,13 +143,7 @@
   */
 fixed_t M_RandomFixed(void)
 {
-#if RAND_MAX < 65535
-	// Compensate for insufficient randomness.
-	fixed_t rndv = (rand()&1)<<15;
-	return rand()^rndv;
-#else
-	return (rand() & 0xFFFF);
-#endif
+	return RandomState_Get32(&m_randomstate) >> (32-FRACBITS);
 }
 
 /** Provides a random byte. Distribution is uniform.
@@ -48,7 +153,7 @@ fixed_t M_RandomFixed(void)
   */
 UINT8 M_RandomByte(void)
 {
-	return (rand() & 0xFF);
+	return RandomState_Get32(&m_randomstate) >> 24;
 }
 
 /** Provides a random integer for picking random elements from an array.
@@ -60,7 +165,22 @@ UINT8 M_RandomByte(void)
   */
 INT32 M_RandomKey(INT32 a)
 {
-	return (INT32)((rand()/((float)RAND_MAX+1.0f))*a);
+	boolean range_is_negative;
+	INT64 range;
+	INT32 random_result;
+
+	range = a;
+	range_is_negative = range < 0;
+
+	if(range_is_negative)
+		range = -range;
+
+	random_result = RandomState_GetKey32(&m_randomstate, (UINT32)range);
+
+	if(range_is_negative)
+		random_result = -random_result;
+
+	return random_result;
 }
 
 /** Provides a random integer in a given range.
@@ -73,7 +193,46 @@ INT32 M_RandomKey(INT32 a)
   */
 INT32 M_RandomRange(INT32 a, INT32 b)
 {
-	return (INT32)((rand()/((float)RAND_MAX+1.0f))*(b-a+1))+a;
+  	if (b < a)
+	{
+    	INT32 temp;
+
+		temp = a;
+		a = b;
+		b = temp;
+	}
+
+	const UINT32 spread = b-a+1;
+	return (INT32)((INT64)RandomState_GetKey32(&m_randomstate, spread) + a);
+}
+
+/** Attempts to seed the unsynched RNG from a good random number source
+  * provided by the operating system.
+  * \return true on success, false on failure.
+  */
+boolean M_RandomSeedFromOS(void)
+{
+	UINT32 complete_word_count;
+
+	union {
+		UINT32 words[3];
+		char bytes[sizeof(UINT32[3])];
+	} seed_data;
+
+	complete_word_count = I_GetRandomBytes((char *)&seed_data.bytes, sizeof(seed_data)) / sizeof(UINT32);
+
+	// If we get even 1 word of seed, it's fine, but any less probably is not fine.
+	if (complete_word_count == 0)
+		return false;
+
+	RandomState_Seed(&m_randomstate, (UINT32 *)&seed_data.words, complete_word_count);
+
+	return true;
+}
+
+void M_RandomSeed(UINT32 seed)
+{
+	RandomState_Seed(&m_randomstate, &seed, 1);
 }
 
 
@@ -247,10 +406,18 @@ void P_SetRandSeedD(const char *rfile, INT32 rline, UINT32 seed)
 }
 
 /** Gets a randomized seed for setting the random seed.
+  * This function will never return 0, as the current P_Random implementation
+  * cannot handle a zero seed. Any other seed is equally likely.
   *
   * \sa P_GetRandSeed
   */
 UINT32 M_RandomizedSeed(void)
 {
-	return ((totalplaytime & 0xFFFF) << 16)|M_RandomFixed();
+	UINT32 seed;
+
+	do {
+		seed = RandomState_Get32(&m_randomstate);
+	} while(seed == 0);
+
+	return seed;
 }
diff --git a/src/m_random.h b/src/m_random.h
index 824287e27d486906c60d976e60817cb5ac9ffb73..a7c07a46b5e2c951548d67f409287e80345c20dd 100644
--- a/src/m_random.h
+++ b/src/m_random.h
@@ -3,6 +3,7 @@
 // Copyright (C) 1993-1996 by id Software, Inc.
 // Copyright (C) 1998-2000 by DooM Legacy Team.
 // Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh.
+// Copyright (C) 2022-2023 by tertu marybig.
 // Copyright (C) 1999-2023 by Sonic Team Junior.
 //
 // This program is free software distributed under the
@@ -29,6 +30,8 @@ fixed_t M_RandomFixed(void);
 UINT8   M_RandomByte(void);
 INT32   M_RandomKey(INT32 a);
 INT32   M_RandomRange(INT32 a, INT32 b);
+boolean M_RandomSeedFromOS(void);
+void    M_RandomSeed(UINT32 a);
 
 // PRNG functions
 #ifdef DEBUGRANDOM
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..4b67198babe4157aefa615fc0fb3b319f5f1b47b
--- /dev/null
+++ b/src/netcode/commands.c
@@ -0,0 +1,484 @@
+// 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;
+	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);
+}
+
+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..f06192f2c8500bc1a35600b43ed502e69a222400
--- /dev/null
+++ b/src/netcode/d_clisrv.c
@@ -0,0 +1,1736 @@
+// 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];
+boolean adminpasswordset = false;
+
+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_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;
+
+	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;
+	}
+
+	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)
+			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? */
+
+	if (doomcom->datalength < 16)/* ignore partial sends */
+		return;
+
+	if (!adminpasswordset)
+	{
+		CONS_Printf(M_GetText("Password from %s failed (no password set).\n"), player_names[netconsole]);
+		return;
+	}
+
+	// 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]);
+#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++;
+	}
+}
+
+// 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();
+
+#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
+
+	// 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;
+}
+
+/** 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..d87ead9ecbf5e71f280f13f3598ada1a1592a524
--- /dev/null
+++ b/src/netcode/d_clisrv.h
@@ -0,0 +1,135 @@
+// 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
+
+} 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_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_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 boolean adminpasswordset;
+
+extern boolean hu_stopped;
+
+#endif
diff --git a/src/d_net.c b/src/netcode/d_net.c
similarity index 92%
rename from src/d_net.c
rename to src/netcode/d_net.c
index 7de3dba56ab9b660a89a09005485fe809810c306..cfb1963b9f6c2dd4699321470676bf78354cec21 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
@@ -318,7 +315,6 @@ static void RemoveAck(INT32 i)
 // We have got a packet, proceed the ack request and ack return
 static boolean Processackpak(void)
 {
-	INT32 i;
 	boolean goodpacket = true;
 	node_t *node = &nodes[doomcom->remotenode];
 
@@ -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)
 			{
@@ -349,7 +345,7 @@ static boolean Processackpak(void)
 		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));
@@ -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)
@@ -426,28 +422,20 @@ static boolean Processackpak(void)
 	}
 	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 +451,6 @@ static void GotAcks(void)
 						}
 				}
 }
-#endif
 
 void Net_ConnectionTimeout(INT32 node)
 {
@@ -472,14 +459,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 +472,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 +500,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 +517,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 +554,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 +563,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 +577,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 +592,6 @@ void Net_WaitAllAckReceived(UINT32 timeout)
 		HGetPacket();
 		Net_AckTicker();
 	}
-#endif
 }
 
 static void InitNode(node_t *node)
@@ -637,14 +605,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 +619,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 +634,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 +663,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 +677,8 @@ void Net_CloseConnection(INT32 node)
 	if (server)
 		SV_AbortLuaFileTransfer(node);
 	I_NetFreeNodenum(node);
-#endif
 }
 
-#ifndef NONET
 //
 // Checksum
 //
@@ -734,23 +687,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 +760,8 @@ static const char *packettypename[NUMPACKETTYPE] =
 	"ASKLUAFILE",
 	"HASLUAFILE",
 
+	"PT_BASICKEEPALIVE",
+
 	"FILEFRAGMENT",
 	"FILEACK",
 	"FILERECEIVED",
@@ -817,7 +769,6 @@ static const char *packettypename[NUMPACKETTYPE] =
 	"TEXTCMD",
 	"TEXTCMD2",
 	"CLIENTJOIN",
-	"NODETIMEOUT",
 	"LOGIN",
 	"TELLFILESNEEDED",
 	"MOREFILESNEEDED",
@@ -869,6 +820,9 @@ static void DebugPrintpacket(const char *header)
 				(UINT32)ExpandTics(netbuffer->u.clientpak.client_tic, doomcom->remotenode),
 				(UINT32)ExpandTics (netbuffer->u.clientpak.resendfrom, doomcom->remotenode));
 			break;
+		case PT_BASICKEEPALIVE:
+			fprintf(debugfile, "    wipetime\n");
+			break;
 		case PT_TEXTCMD:
 		case PT_TEXTCMD2:
 			fprintf(debugfile, "    length %d\n    ", netbuffer->u.textcmd[0]);
@@ -918,7 +872,6 @@ void Command_Drop(void)
 {
 	INT32 packetquantity;
 	const char *packetname;
-	size_t i;
 
 	if (COM_Argc() < 2)
 	{
@@ -948,11 +901,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;
@@ -983,14 +936,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
@@ -1025,11 +976,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;
@@ -1090,8 +1036,6 @@ boolean HSendPacket(INT32 node, boolean reliable, UINT8 acknum, size_t packetlen
 	}
 #endif
 
-#endif // ndef NONET
-
 	return true;
 }
 
@@ -1109,10 +1053,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
@@ -1125,8 +1066,6 @@ boolean HGetPacket(void)
 	if (!netgame)
 		return false;
 
-#ifndef NONET
-
 	while(true)
 	{
 		//nodejustjoined = I_NetGet();
@@ -1186,7 +1125,6 @@ boolean HGetPacket(void)
 		}
 		break;
 	}
-#endif // ndef NONET
 
 	return true;
 }
@@ -1207,26 +1145,32 @@ static void Internal_FreeNodenum(INT32 nodenum)
 	(void)nodenum;
 }
 
+char *I_NetSplitAddress(char *host, char **port)
+{
+	boolean v4 = (strchr(host, '.') != NULL);
+
+	host = strtok(host, v4 ? ":" : "[]");
+
+	if (port)
+		*port = strtok(NULL, ":");
+
+	return host;
+}
+
 SINT8 I_NetMakeNode(const char *hostname)
 {
 	SINT8 newnode = -1;
 	if (I_NetMakeNodewPort)
 	{
 		char *localhostname = strdup(hostname);
-		char  *t = localhostname;
-		const char *port;
+		char *port;
 		if (!localhostname)
 			return newnode;
-		// retrieve portnum from address!
-		strtok(localhostname, ":");
-		port = strtok(NULL, ":");
 
-		// remove the port in the hostname as we've it already
-		while ((*t != ':') && (*t != '\0'))
-			t++;
-		*t = '\0';
+		// retrieve portnum from address!
+		hostname = I_NetSplitAddress(localhostname, &port);
 
-		newnode = I_NetMakeNodewPort(localhostname, port);
+		newnode = I_NetMakeNodewPort(hostname, port);
 		free(localhostname);
 	}
 	return newnode;
@@ -1390,13 +1334,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;
@@ -1416,7 +1359,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,
@@ -1432,15 +1375,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 74%
rename from src/d_net.h
rename to src/netcode/d_net.h
index ddedbef4a806df597a0e56b29a51b7c6e7f3c606..549f2b93c18dc86222e39d359d05ff2e5763ba27 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,9 +68,6 @@ 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);
 void Net_UnAcknowledgePacket(INT32 node);
diff --git a/src/d_netcmd.c b/src/netcode/d_netcmd.c
similarity index 93%
rename from src/d_netcmd.c
rename to src/netcode/d_netcmd.c
index 80a084e1677208ac892648dd15c4ad3ed0d5585b..5afa955610a6aee452790fe0ca74b13fc7007d86 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 "hardware/u_list.h" // TODO: this should be a standard utility class
+#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
@@ -225,6 +227,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
@@ -594,12 +597,10 @@ 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);
-#endif
+	CV_RegisterVar(&cv_dedicatedidletime);
 
 	COM_AddCommand("ping", Command_Ping_f, COM_LUA);
 	CV_RegisterVar(&cv_nettimeout);
@@ -775,6 +776,8 @@ void D_RegisterClientCommands(void)
 	CV_RegisterVar(&cv_showfocuslost);
 	CV_RegisterVar(&cv_pauseifunfocused);
 
+	CV_RegisterVar(&cv_instantretry);
+
 	// g_input.c
 	CV_RegisterVar(&cv_sideaxis);
 	CV_RegisterVar(&cv_sideaxis2);
@@ -873,10 +876,15 @@ void D_RegisterClientCommands(void)
 	// screen.c
 	CV_RegisterVar(&cv_fullscreen);
 	CV_RegisterVar(&cv_renderview);
+	CV_RegisterVar(&cv_renderhitboxinterpolation);
+	CV_RegisterVar(&cv_renderhitboxgldepth);
+	CV_RegisterVar(&cv_renderhitbox);
 	CV_RegisterVar(&cv_renderer);
 	CV_RegisterVar(&cv_scr_depth);
 	CV_RegisterVar(&cv_scr_width);
 	CV_RegisterVar(&cv_scr_height);
+	CV_RegisterVar(&cv_scr_width_w);
+	CV_RegisterVar(&cv_scr_height_w);
 
 	CV_RegisterVar(&cv_soundtest);
 
@@ -1197,26 +1205,37 @@ 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
 //
@@ -1227,15 +1246,6 @@ static void SendNameAndColor(void)
 
 	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)
 	{
@@ -1266,50 +1276,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);
+		SetColorLocal(consoleplayer, cv_playercolor.value);
 
-			/*if (notsame)
-			{
-				CV_StealthSetValue(&cv_playercolor, skins[cv_skin.value].prefcolor);
-
-				players[consoleplayer].skincolor = cv_playercolor.value % numskincolors;
-
-				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;
 	}
 
@@ -1326,10 +1301,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);
@@ -1361,16 +1332,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)
@@ -1392,63 +1353,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);
-
-			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);
+		SetColorLocal(secondplaya, cv_playercolor2.value);
 
-				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;
 	}
 
@@ -1492,7 +1414,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]))
@@ -1501,15 +1423,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;
@@ -1550,16 +1463,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);
 }
@@ -1637,9 +1543,14 @@ static void Command_Playdemo_f(void)
 {
 	char name[256];
 
-	if (COM_Argc() != 2)
+	if (COM_Argc() < 2)
 	{
-		CONS_Printf(M_GetText("playdemo <demoname>: playback a demo\n"));
+		CONS_Printf("playdemo <demoname> [-addfiles / -force]:\n");
+		CONS_Printf(M_GetText(
+					"Play back a demo file. The full path from your SRB2 directory must be given.\n\n"
+
+					"* With \"-addfiles\", any required files are added from a list contained within the demo file.\n"
+					"* With \"-force\", the demo is played even if the necessary files have not been added.\n"));
 		return;
 	}
 
@@ -1661,6 +1572,16 @@ static void Command_Playdemo_f(void)
 
 	CONS_Printf(M_GetText("Playing back demo '%s'.\n"), name);
 
+	demofileoverride = DFILE_OVERRIDE_NONE;
+	if (strcmp(COM_Argv(2), "-addfiles") == 0)
+	{
+		demofileoverride = DFILE_OVERRIDE_LOAD;
+	}
+	else if (strcmp(COM_Argv(2), "-force") == 0)
+	{
+		demofileoverride = DFILE_OVERRIDE_SKIP;
+	}
+
 	// Internal if no extension, external if one exists
 	// If external, convert the file name to a path in SRB2's home directory
 	if (FIL_CheckExtension(name))
@@ -1820,8 +1741,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;
 		}
@@ -1881,8 +1801,8 @@ static void Command_Map_f(void)
 	size_t option_gametype;
 	const char *gametypename;
 	boolean newresetplayers;
-
-	boolean wouldSetCheats;
+	boolean prevent_cheat;
+	boolean set_cheated;
 
 	INT32 newmapnum;
 
@@ -1903,21 +1823,34 @@ static void Command_Map_f(void)
 	option_gametype =   COM_CheckPartialParm("-g");
 	newresetplayers = ! COM_CheckParm("-noresetplayers");
 
-	wouldSetCheats =
-		!( netgame || multiplayer ) &&
-		!( usedCheats );
+	prevent_cheat = !( usedCheats ) && !( option_force || cv_debug );
+	set_cheated = false;
 
-	if (wouldSetCheats && !option_force)
+	if (!( netgame || multiplayer ))
 	{
-		/* May want to be more descriptive? */
-		CONS_Printf(M_GetText("Sorry, level change disabled in single player.\n"));
-		return;
+		if (prevent_cheat)
+		{
+			/* May want to be more descriptive? */
+			CONS_Printf(M_GetText("Cheats must be enabled to level change in single player.\n"));
+			return;
+		}
+		else
+		{
+			set_cheated = true;
+		}
 	}
 
-	if (!newresetplayers && !cv_debug)
+	if (!newresetplayers)
 	{
-		CONS_Printf(M_GetText("DEVMODE must be enabled.\n"));
-		return;
+		if (prevent_cheat)
+		{
+			CONS_Printf(M_GetText("Cheats must be enabled to use -noresetplayers.\n"));
+			return;
+		}
+		else
+		{
+			set_cheated = true;
+		}
 	}
 
 	if (option_gametype)
@@ -1925,7 +1858,7 @@ static void Command_Map_f(void)
 		if (!multiplayer)
 		{
 			CONS_Printf(M_GetText(
-						"You can't switch gametypes in single player!\n"));
+				"You can't switch gametypes in single player!\n"));
 			return;
 		}
 		else if (COM_Argc() < option_gametype + 2)/* no argument after? */
@@ -1938,7 +1871,9 @@ static void Command_Map_f(void)
 	}
 
 	if (!( first_option = COM_FirstOption() ))
+	{
 		first_option = COM_Argc();
+	}
 
 	if (first_option < 2)
 	{
@@ -1961,11 +1896,6 @@ static void Command_Map_f(void)
 		return;
 	}
 
-	if (wouldSetCheats && option_force)
-	{
-		G_SetUsedCheats(false);
-	}
-
 	// new gametype value
 	// use current one by default
 	if (option_gametype)
@@ -2007,15 +1937,13 @@ static void Command_Map_f(void)
 	}
 
 	// don't use a gametype the map doesn't support
-	if (cv_debug || option_force || cv_skipmapcheck.value)
-		fromlevelselect = false; // The player wants us to trek on anyway.  Do so.
 	// G_TOLFlag handles both multiplayer gametype and ignores it for !multiplayer
-	else
+	if (!(
+			mapheaderinfo[newmapnum-1] &&
+			mapheaderinfo[newmapnum-1]->typeoflevel & G_TOLFlag(newgametype)
+	))
 	{
-		if (!(
-					mapheaderinfo[newmapnum-1] &&
-					mapheaderinfo[newmapnum-1]->typeoflevel & G_TOLFlag(newgametype)
-		))
+		if (prevent_cheat && !cv_skipmapcheck.value)
 		{
 			CONS_Alert(CONS_WARNING, M_GetText("%s (%s) doesn't support %s mode!\n(Use -force to override)\n"), realmapname, G_BuildMapName(newmapnum),
 				(multiplayer ? gametype_cons_t[newgametype].strvalue : "Single Player"));
@@ -2025,23 +1953,33 @@ static void Command_Map_f(void)
 		}
 		else
 		{
-			fromlevelselect =
-				( netgame || multiplayer ) &&
-				newgametype == gametype    &&
-				gametypedefaultrules[newgametype] & GTR_CAMPAIGN;
+			// The player wants us to trek on anyway.  Do so.
+			fromlevelselect = false;
+			set_cheated = ((gametypedefaultrules[newgametype] & GTR_CAMPAIGN) == GTR_CAMPAIGN);
 		}
 	}
+	else
+	{
+		fromlevelselect =
+			( netgame || multiplayer ) &&
+			newgametype == gametype    &&
+			(gametypedefaultrules[newgametype] & GTR_CAMPAIGN);
+	}
 
 	// Prevent warping to locked levels
-	// ... unless you're in a dedicated server.  Yes, technically this means you can view any level by
-	// running a dedicated server and joining it yourself, but that's better than making dedicated server's
-	// lives hell.
-	if (!dedicated && M_MapLocked(newmapnum))
+	if (M_CampaignWarpIsCheat(newgametype, newmapnum, serverGamedata))
 	{
-		CONS_Alert(CONS_NOTICE, M_GetText("You need to unlock this level before you can warp to it!\n"));
-		Z_Free(realmapname);
-		Z_Free(mapname);
-		return;
+		if (prevent_cheat)
+		{
+			CONS_Alert(CONS_NOTICE, M_GetText("Cheats must be enabled to warp to a locked level!\n"));
+			Z_Free(realmapname);
+			Z_Free(mapname);
+			return;
+		}
+		else
+		{
+			set_cheated = true;
+		}
 	}
 
 	// Ultimate Mode only in SP via menu
@@ -2058,6 +1996,11 @@ static void Command_Map_f(void)
 	}
 	tutorialmode = false; // warping takes us out of tutorial mode
 
+	if (set_cheated && !usedCheats)
+	{
+		G_SetUsedCheats(false);
+	}
+
 	D_MapChange(newmapnum, newgametype, false, newresetplayers, 0, false, fromlevelselect);
 
 	Z_Free(realmapname);
@@ -2099,11 +2042,13 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 
 	lastgametype = gametype;
 	gametype = READUINT8(*cp);
-	G_SetGametype(gametype); // I fear putting that macro as an argument
 
 	if (gametype < 0 || gametype >= gametypecount)
 		gametype = lastgametype;
-	else if (gametype != lastgametype)
+	else
+		G_SetGametype(gametype);
+
+	if (gametype != lastgametype)
 		D_GameTypeChanged(lastgametype); // emulate consvar_t behavior for gametype
 
 	skipprecutscene = ((flags & (1<<2)) != 0);
@@ -2125,17 +2070,10 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 	if (demoplayback && !timingdemo)
 		precache = false;
 
-	if (resetplayer && !FLS)
-	{
-		emeralds = 0;
-		memset(&luabanks, 0, sizeof(luabanks));
-	}
-
 	if (modeattacking)
 	{
 		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]);
@@ -2171,7 +2109,7 @@ static void Command_Pause(void)
 
 	if (cv_pause.value || server || (IsPlayerAdmin(consoleplayer)))
 	{
-		if (modeattacking || !(gamestate == GS_LEVEL || gamestate == GS_INTERMISSION) || (marathonmode && gamestate == GS_INTERMISSION))
+		if (modeattacking || !(gamestate == GS_LEVEL || gamestate == GS_INTERMISSION || gamestate == GS_WAITINGPLAYERS) || (marathonmode && gamestate == GS_INTERMISSION))
 		{
 			CONS_Printf(M_GetText("You can't pause here.\n"));
 			return;
@@ -2334,7 +2272,7 @@ static void Got_Clearscores(UINT8 **cp, INT32 playernum)
 	}
 
 	for (i = 0; i < MAXPLAYERS; i++)
-		players[i].score = 0;
+		players[i].score = players[i].recordscore = 0;
 
 	CONS_Printf(M_GetText("Scores have been reset by the server.\n"));
 }
@@ -2889,17 +2827,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();
@@ -3838,7 +3765,7 @@ static void Command_ListWADS_f(void)
 static void Command_Version_f(void)
 {
 #ifdef DEVELOP
-	CONS_Printf("Sonic Robo Blast 2 %s-%s (%s %s) ", compbranch, comprevision, compdate, comptime);
+	CONS_Printf("Sonic Robo Blast 2 %s %s %s (%s %s) ", compbranch, comprevision, compnote, compdate, comptime);
 #else
 	CONS_Printf("Sonic Robo Blast 2 %s (%s %s %s %s) ", VERSIONSTRING, compdate, comptime, comprevision, compbranch);
 #endif
@@ -3872,11 +3799,6 @@ static void Command_Version_f(void)
 	else // 16-bit? 128-bit?
 		CONS_Printf("Bits Unknown ");
 
-	// No ASM?
-#ifdef NOASM
-	CONS_Printf("\x85" "NOASM " "\x80");
-#endif
-
 	// Debug build
 #ifdef _DEBUG
 	CONS_Printf("\x85" "DEBUG " "\x80");
@@ -3945,18 +3867,12 @@ void ItemFinder_OnChange(void)
 	if (!cv_itemfinder.value)
 		return; // it's fine.
 
-	if (!M_SecretUnlocked(SECRET_ITEMFINDER))
+	if (!M_SecretUnlocked(SECRET_ITEMFINDER, clientGamedata))
 	{
 		CONS_Printf(M_GetText("You haven't earned this yet.\n"));
 		CV_StealthSetValue(&cv_itemfinder, 0);
 		return;
 	}
-	else if (netgame || multiplayer)
-	{
-		CONS_Printf(M_GetText("This only works in single player.\n"));
-		CV_StealthSetValue(&cv_itemfinder, 0);
-		return;
-	}
 }
 
 /** Deals with a pointlimit change by printing the change to the console.
@@ -4248,9 +4164,6 @@ void D_GameTypeChanged(INT32 lastgametype)
 	else if (!multiplayer && !netgame)
 	{
 		G_SetGametype(GT_COOP);
-		// These shouldn't matter anymore
-		//CV_Set(&cv_itemrespawntime, cv_itemrespawntime.defaultvalue);
-		//CV_SetValue(&cv_itemrespawn, 0);
 	}
 
 	// reset timelimit and pointlimit in race/coop, prevent stupid cheats
@@ -4305,7 +4218,7 @@ void D_GameTypeChanged(INT32 lastgametype)
 
 static void Ringslinger_OnChange(void)
 {
-	if (!M_SecretUnlocked(SECRET_PANDORA) && !netgame && cv_ringslinger.value && !cv_debug)
+	if (!M_SecretUnlocked(SECRET_PANDORA, serverGamedata) && !netgame && cv_ringslinger.value && !cv_debug)
 	{
 		CONS_Printf(M_GetText("You haven't earned this yet.\n"));
 		CV_StealthSetValue(&cv_ringslinger, 0);
@@ -4318,7 +4231,7 @@ static void Ringslinger_OnChange(void)
 
 static void Gravity_OnChange(void)
 {
-	if (!M_SecretUnlocked(SECRET_PANDORA) && !netgame && !cv_debug
+	if (!M_SecretUnlocked(SECRET_PANDORA, serverGamedata) && !netgame && !cv_debug
 		&& strcmp(cv_gravity.string, cv_gravity.defaultvalue))
 	{
 		CONS_Printf(M_GetText("You haven't earned this yet.\n"));
@@ -4551,25 +4464,37 @@ static void Command_Mapmd5_f(void)
 		CONS_Printf(M_GetText("You must be in a level to use this.\n"));
 }
 
+void D_SendExitLevel(boolean cheat)
+{
+	UINT8 buf[8];
+	UINT8 *buf_p = buf;
+
+	WRITEUINT8(buf_p, cheat);
+
+	SendNetXCmd(XD_EXITLEVEL, &buf, buf_p - buf);
+}
+
 static void Command_ExitLevel_f(void)
 {
-	if (!(netgame || (multiplayer && gametype != GT_COOP)) && !cv_debug)
-		CONS_Printf(M_GetText("This only works in a netgame.\n"));
-	else if (!(server || (IsPlayerAdmin(consoleplayer))))
+	if (!(server || (IsPlayerAdmin(consoleplayer))))
 		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
 	else if (( gamestate != GS_LEVEL && gamestate != GS_CREDITS ) || demoplayback)
 		CONS_Printf(M_GetText("You must be in a level to use this.\n"));
 	else
-		SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+		D_SendExitLevel(true);
 }
 
 static void Got_ExitLevelcmd(UINT8 **cp, INT32 playernum)
 {
-	(void)cp;
+	boolean cheat = false;
+
+	cheat = (boolean)READUINT8(*cp);
 
 	// Ignore duplicate XD_EXITLEVEL commands.
 	if (gameaction == ga_completed)
+	{
 		return;
+	}
 
 	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
 	{
@@ -4579,6 +4504,11 @@ static void Got_ExitLevelcmd(UINT8 **cp, INT32 playernum)
 		return;
 	}
 
+	if (G_CoopGametype() && cheat)
+	{
+		G_SetUsedCheats(false);
+	}
+
 	G_ExitLevel();
 }
 
@@ -4615,6 +4545,7 @@ void Command_ExitGame_f(void)
 	botskin = 0;
 	cv_debug = 0;
 	emeralds = 0;
+	automapactive = false;
 	memset(&luabanks, 0, sizeof(luabanks));
 
 	if (dirmenu)
@@ -4774,11 +4705,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);
 	}
 }
 
@@ -4792,7 +4728,6 @@ static void Name_OnChange(void)
 	}
 	else
 		SendNameAndColor();
-
 }
 
 static void Name2_OnChange(void)
@@ -4815,19 +4750,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]);
 	}
 }
 
@@ -4841,12 +4790,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]);
 	}
 }
 
@@ -4856,15 +4812,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 26bf4d5c6bfbcfe9e481b35b84f38c44360ec3fa..4849079d0ce622ae6388dfdec9984686c963b614 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;
@@ -201,6 +201,7 @@ void D_SendPlayerConfig(void);
 void Command_ExitGame_f(void);
 void Command_Retry_f(void);
 void D_GameTypeChanged(INT32 lastgametype); // not a real _OnChange function anymore
+void D_SendExitLevel(boolean cheat);
 void D_MapChange(INT32 pmapnum, INT32 pgametype, boolean pultmode, boolean presetplayers, INT32 pdelay, boolean pskipprecutscene, boolean pfromlevelselect);
 boolean IsPlayerAdmin(INT32 playernum);
 void SetAdminPlayer(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 e60af2c2c2db2de4b7271b343a1982c40d98b2aa..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
@@ -498,7 +504,7 @@ INT32 CL_CheckFiles(void)
 		CONS_Debug(DBG_NETPLAY, "searching for '%s' ", fileneeded[i].filename);
 
 		// Check in already loaded files
-		for (j = mainwads; wadfiles[j]; j++)
+		for (j = mainwads; j < numwadfiles; j++)
 		{
 			nameonly(strcpy(wadfilename, wadfiles[j]->filename));
 			if (!stricmp(wadfilename, fileneeded[i].filename) &&
@@ -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 97%
rename from src/http-mserv.c
rename to src/netcode/http-mserv.c
index b4dba0db9335da0a07ddbda1b0d280da9ed418b2..f8bd5da218efc57bb3384d2f9375919760254817 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)
@@ -95,7 +96,7 @@ init_user_agent_once(void)
 {
 	if (hms_useragent[0] != '\0')
 		return;
-	
+
 	get_user_agent(hms_useragent, 512);
 }
 
@@ -159,7 +160,7 @@ HMS_connect (const char *format, ...)
 		return NULL;
 	}
 
-	if (cv_masterserver_token.string[0])
+	if (cv_masterserver_token.string && cv_masterserver_token.string[0])
 	{
 		quack_token = curl_easy_escape(curl, cv_masterserver_token.string, 0);
 		token_length = ( sizeof "?token="-1 )+ strlen(quack_token);
@@ -216,7 +217,11 @@ HMS_connect (const char *format, ...)
 
 	curl_easy_setopt(curl, CURLOPT_URL, url);
 	curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
-	curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+
+#ifndef NO_IPV6
+	if (M_CheckParm("-noipv6"))
+#endif
+		curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
 
 	curl_easy_setopt(curl, CURLOPT_TIMEOUT, cv_masterserver_timeout.value);
 	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, HMS_on_read);
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 92%
rename from src/i_net.h
rename to src/netcode/i_net.h
index 9f2c38c7b5309c6520705ac5cc35a586f8d7e8e3..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
@@ -109,6 +109,17 @@ extern boolean (*I_NetCanSend)(void);
 */
 extern void (*I_NetFreeNodenum)(INT32 nodenum);
 
+/**
+	\brief	split a string into address and port
+
+	\param	address	string to split
+
+	\param	port	double pointer to hold port component (optional)
+
+	\return	address component
+*/
+extern char *I_NetSplitAddress(char *address, char **port);
+
 /**	\brief	open a connection with specified address
 
 	\param	address	address to connect to
diff --git a/src/i_tcp.c b/src/netcode/i_tcp.c
similarity index 85%
rename from src/i_tcp.c
rename to src/netcode/i_tcp.c
index 3820155b83bd4c60b98ece7a8f04638a2bf1f7a1..698234579160dc76d201de5e73ca2e7fe8b2cf66 100644
--- a/src/i_tcp.c
+++ b/src/netcode/i_tcp.c
@@ -36,109 +36,100 @@
 	#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
+	#define ETIMEDOUT WSAETIMEDOUT
+	#ifndef IOC_VENDOR
+	#define IOC_VENDOR 0x18000000
+	#endif
+	#ifndef _WSAIOW
+	#define _WSAIOW(x,y) (IOC_IN|(x)|(y))
 	#endif
-	} mysockaddr_t;
+	#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 +142,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 +154,20 @@
 	#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 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 +176,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)
@@ -340,8 +328,14 @@ static inline void I_UPnP_rem(const char *port, const char * servicetype)
 
 static const char *SOCK_AddrToStr(mysockaddr_t *sk)
 {
-	static char s[64]; // 255.255.255.255:65535 or IPv6:65535
+	static char s[64]; // 255.255.255.255:65535 or
+	// [ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]:65535
 #ifdef HAVE_NTOP
+#ifdef HAVE_IPV6
+	int v6 = (sk->any.sa_family == AF_INET6);
+#else
+	int v6 = 0;
+#endif
 	void *addr;
 
 	if(sk->any.sa_family == AF_INET)
@@ -355,14 +349,21 @@ static const char *SOCK_AddrToStr(mysockaddr_t *sk)
 
 	if(addr == NULL)
 		sprintf(s, "No address");
-	else if(inet_ntop(sk->any.sa_family, addr, s, sizeof (s)) == NULL)
+	else if(inet_ntop(sk->any.sa_family, addr, &s[v6], sizeof (s) - v6) == NULL)
 		sprintf(s, "Unknown family type, error #%u", errno);
 #ifdef HAVE_IPV6
-	else if(sk->any.sa_family == AF_INET6 && sk->ip6.sin6_port != 0)
-		strcat(s, va(":%d", ntohs(sk->ip6.sin6_port)));
+	else if(sk->any.sa_family == AF_INET6)
+	{
+		s[0] = '[';
+		strcat(s, "]");
+
+		if (sk->ip6.sin6_port != 0)
+			strcat(s, va(":%d", ntohs(sk->ip6.sin6_port)));
+	}
 #endif
 	else if(sk->any.sa_family == AF_INET  && sk->ip4.sin_port  != 0)
 		strcat(s, va(":%d", ntohs(sk->ip4.sin_port)));
+
 #else
 	if (sk->any.sa_family == AF_INET)
 	{
@@ -374,47 +375,33 @@ 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
 static boolean SOCK_cmpaddr(mysockaddr_t *a, mysockaddr_t *b, UINT8 mask)
 {
 	UINT32 bitmask = INADDR_NONE;
@@ -427,7 +414,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 !memcmp(&a->ip6.sin6_addr, &b->ip6.sin6_addr, sizeof(b->ip6.sin6_addr))
 			&& (b->ip6.sin6_port == 0 || (a->ip6.sin6_port == b->ip6.sin6_port));
 #endif
 	else
@@ -442,24 +429,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;
@@ -472,8 +455,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;
@@ -484,28 +467,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(" -       ");
@@ -518,19 +500,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,
@@ -583,20 +563,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
@@ -636,9 +613,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);
@@ -662,16 +637,15 @@ static inline ssize_t SOCK_SendToAddr(SOCKET_TYPE socket, mysockaddr_t *sockaddr
 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]);
@@ -681,7 +655,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]);
@@ -701,9 +675,7 @@ static void SOCK_Send(void)
 				SOCK_GetNodeAddress(doomcom->remotenode), e, strerror(e));
 	}
 }
-#endif
 
-#ifndef NONET
 static void SOCK_FreeNodenum(INT32 numnode)
 {
 	// can't disconnect from self :)
@@ -718,12 +690,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)
@@ -735,8 +705,7 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 	unsigned long trueval = true;
 #endif
 	mysockaddr_t straddr;
-	struct sockaddr_in sin;
-	socklen_t len = sizeof(sin);
+	socklen_t len = sizeof(straddr);
 
 	if (s == (SOCKET_TYPE)ERRSOCKET)
 		return (SOCKET_TYPE)ERRSOCKET;
@@ -754,14 +723,12 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 	}
 #endif
 
-	straddr.any = *addr;
+	memcpy(&straddr, addr, addrlen);
 	I_OutputMsg("Binding to %s\n", SOCK_AddrToStr(&straddr));
 
 	if (family == AF_INET)
 	{
-		mysockaddr_t tmpaddr;
-		tmpaddr.any = *addr ;
-		if (tmpaddr.ip4.sin_addr.s_addr == htonl(INADDR_ANY))
+		if (straddr.ip4.sin_addr.s_addr == htonl(INADDR_ANY))
 		{
 			opt = true;
 			opts = (socklen_t)sizeof(opt);
@@ -778,7 +745,7 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 #ifdef HAVE_IPV6
 	else if (family == AF_INET6)
 	{
-		if (memcmp(addr, &in6addr_any, sizeof(in6addr_any)) == 0) //IN6_ARE_ADDR_EQUAL
+		if (memcmp(&straddr.ip6.sin6_addr, &in6addr_any, sizeof(in6addr_any)) == 0) //IN6_ARE_ADDR_EQUAL
 		{
 			opt = true;
 			opts = (socklen_t)sizeof(opt);
@@ -788,7 +755,7 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 		// make it IPv6 ony
 		opt = true;
 		opts = (socklen_t)sizeof(opt);
-		if (setsockopt(s, SOL_SOCKET, IPV6_V6ONLY, (char *)&opt, opts))
+		if (setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&opt, opts))
 		{
 			CONS_Alert(CONS_WARNING, M_GetText("Could not limit IPv6 bind\n")); // I do not care anymore
 		}
@@ -830,10 +797,17 @@ static SOCKET_TYPE UDP_Bind(int family, struct sockaddr *addr, socklen_t addrlen
 			CONS_Printf(M_GetText("Network system buffer set to: %dKb\n"), opt>>10);
 	}
 
-	if (getsockname(s, (struct sockaddr *)&sin, &len) == -1)
+	if (getsockname(s, &straddr.any, &len) == -1)
 		CONS_Alert(CONS_WARNING, M_GetText("Failed to get port number\n"));
 	else
-		current_port = (UINT16)ntohs(sin.sin_port);
+	{
+		if (family == AF_INET)
+			current_port = (UINT16)ntohs(straddr.ip4.sin_port);
+#ifdef HAVE_IPV6
+		else if (family == AF_INET6)
+			current_port = (UINT16)ntohs(straddr.ip6.sin6_port);
+#endif
+	}
 
 	return s;
 }
@@ -844,7 +818,7 @@ static boolean UDP_Socket(void)
 	struct my_addrinfo *ai, *runp, hints;
 	int gaie;
 #ifdef HAVE_IPV6
-	const INT32 b_ipv6 = M_CheckParm("-ipv6");
+	const INT32 b_ipv6 = !M_CheckParm("-noipv6");
 #endif
 	const char *serv;
 
@@ -1044,12 +1018,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
@@ -1104,7 +1076,7 @@ boolean I_InitTcpDriver(void)
 #endif
 		init_tcp_driver = true;
 	}
-#endif
+
 	if (!tcp_was_up && init_tcp_driver)
 	{
 		I_AddExitFunc(I_ShutdownTcpDriver);
@@ -1118,11 +1090,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))
@@ -1133,11 +1103,9 @@ static void SOCK_CloseSocket(void)
 		mysockets[i] = ERRSOCKET;
 	}
 }
-#endif
 
 void I_ShutdownTcpDriver(void)
 {
-#ifndef NONET
 	SOCK_CloseSocket();
 
 	CONS_Printf("I_ShutdownTcpDriver: ");
@@ -1147,15 +1115,14 @@ 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;
 	struct my_addrinfo *ai = NULL, *runp, hints;
 	int gaie;
+	size_t i;
 
 	 if (!port || !port[0])
 		port = DEFAULTPORT;
@@ -1183,28 +1150,35 @@ static SINT8 SOCK_NetMakeNodewPort(const char *address, const char *port)
 
 	while (runp != NULL)
 	{
-		// find ip of the server
-		if (sendto(mysockets[0], NULL, 0, 0, runp->ai_addr, runp->ai_addrlen) == 0)
+		// test ip address of server
+		for (i = 0; i < mysocketses; ++i)
 		{
-			memcpy(&clientaddress[newnode], runp->ai_addr, runp->ai_addrlen);
-			break;
+			/* sendto tests that there is a network to this
+				address */
+			if (runp->ai_addr->sa_family == myfamily[i] &&
+					sendto(mysockets[i], NULL, 0, 0,
+						runp->ai_addr, runp->ai_addrlen) == 0)
+			{
+				memcpy(&clientaddress[newnode], runp->ai_addr, runp->ai_addrlen);
+				break;
+			}
 		}
-		runp = runp->ai_next;
+
+		if (i < mysocketses)
+			runp = runp->ai_next;
+		else
+			break;
 	}
 	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;
@@ -1222,18 +1196,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;
 
@@ -1252,16 +1220,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;
 
@@ -1306,7 +1268,6 @@ static boolean SOCK_SetBanAddress(const char *address, const char *mask)
 	I_freeaddrinfo(ai);
 
 	return true;
-#endif
 }
 
 static void SOCK_ClearBans(void)
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 98%
rename from src/mserv.c
rename to src/netcode/mserv.c
index 62cda96e45baf7b8e2e785ef5ee6f51437272610..1c7f3e08d4fa67094657de7887d635b67ba0708f 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*/
 
@@ -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)
diff --git a/src/mserv.h b/src/netcode/mserv.h
similarity index 96%
rename from src/mserv.h
rename to src/netcode/mserv.h
index 1c8d742d818915a61cc90389a067ca44a1752f2a..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
@@ -33,7 +33,7 @@ typedef union
 typedef struct
 {
 	msg_header_t header;
-	char ip[16];
+	char ip[sizeof "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"];
 	char port[8];
 	char name[32];
 	INT32 room;
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 60%
rename from src/d_clisrv.h
rename to src/netcode/protocol.h
index 04a7b5ba2f40e648f3eae21e4f48f89d0c0aa1e9..a992e3b69db1ac1eb981c15b24d41e4ffac156eb 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,6 +72,8 @@ 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
+
 	// Add non-PT_CANFAIL packet types here to avoid breaking MS compatibility.
 
 	PT_CANFAIL,       // This is kind of a priority. Anything bigger than CANFAIL
@@ -90,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.
 
@@ -101,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
@@ -137,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
@@ -213,6 +200,7 @@ enum {
 
 #define MAXSERVERNAME 32
 #define MAXFILENEEDED 915
+
 // This packet is too large
 typedef struct
 {
@@ -273,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
@@ -284,7 +272,7 @@ typedef struct
 	UINT32 pflags;
 	UINT32 score;
 	UINT8 ctfteam;
-} ATTRPACK plrconfig;
+} ATTRPACK plrconfig_pak;
 
 typedef struct
 {
@@ -307,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;
 
@@ -333,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,103 +333,4 @@ extern consvar_t cv_playbackspeed;
 #define KICK_MSG_CUSTOM_BAN  8
 #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;
-
-// 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);
-
-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..2164f411a227527dba862e80bdc48e1019f5b526
--- /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_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)
+{
+	netbuffer->packettype = PT_PLAYERINFO;
+
+	for (UINT8 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_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/p5prof.h b/src/p5prof.h
deleted file mode 100644
index a9ed3965e9691f9931cd360da7cc41efc2ee5c55..0000000000000000000000000000000000000000
--- a/src/p5prof.h
+++ /dev/null
@@ -1,278 +0,0 @@
-/*********************************************************
- *
- * File:  p5prof.h
- * By:    Kevin Baca
- *
- * MODIFIED BY Fab SO THAT RDMSR(...) WRITES EDX : EAX TO A LONG LONG
- * (WHICH MEANS WRITE THE LOW DWORD FIRST)
- *
- * Now in yer code do:
- *   INT64 count,total;
- *
- *   ...
- *   RDMSR(0x10,&count);        //inner loop count
- *   total += count;
- *   ...
- *
- *   printf("0x%x %x", (INT32)total, *((INT32 *)&total+1));
- *   //                  HIGH        LOW
- *
- *********************************************************/
-/**\file
-	\brief  This file provides macros to profile your code.
-
- Here's how they work...
-
- As you may or may not know, the Pentium class of
- processors provides extremely fine grained profiling
- capabilities through the use of what are called
- Machine Specific Registers (MSRs). These registers
- can provide information about almost any aspect of
- CPU performance down to a single cycle.
-
- The MSRs of interest for profiling are specified by
- indices 0x10, 0x11, 0x12, and 0x13.  Here is a brief
- description of each of these registers:
-
- MSR 0x10
-    This register is simple a cycle counter.
-
- MSR 0x11
-    This register controls what type of profiling data
- will be gathered.
-
- MSRs 0x12 and 0x13
-    These registers gather the profiling data specified in
- MSR 0x11.
-
- Each MSR is 64 bits wide.  For the Pentium processor,
- only the lower 32 bits of MSR 0x11 are valid.  Bits 0-15
- specify what data will be gathered in MSR 0x12.  Bits 16-31
- specify what data will be gathered in MSR 0x13.  Both sets
- of bits have the same format:
-
- Bits 0-5 specify which hardware event will be tracked.
- Bit 6, if set, indicates events will be tracked in
- rings 0-2.
- Bit 7, if set, indicates events will be tracked in
- ring 3.
- Bit 8, if set, indicates cycles should be counted for
- the specified event.  If clear, it indicates the
- number of events should be counted.
-
- Two instructions are provided for manupulating the MSRs.
- RDMSR (Read Machine Specific Register) and WRMSR
- (Write Machine Specific Register).  These opcodes were
- originally undocumented and therefore most assemblers don't
- recognize them.  Their byte codes are provided in the
- macros below.
-
- RDMSR takes the MSR index in ecx and the profiling criteria
- in edx : eax.
-
- WRMSR takes the MSR index in ecx and returns the profile data
- in edx : eax.
-
- Two profiling registers limits profiling capability to
- gathering only two types of information.  The register
- usage can, however, be combined in interesting ways.
- For example, you can set one register to gather the
- number of a specific type of event while the other gathers
- the number of cycles for the same event.  Or you can
- gather the number of two separate events while using
- MSR 0x10 to gather the number of cycles.
-
- The enumerated list provides somewhat readable labels for
- the types of events that can be tracked.
-
- For more information, get ahold of appendix H from the
- Intel Pentium programmer's manual (I don't remember the
- order number) or go to
- http://green.kaist.ac.kr/jwhahn/art3.htm.
- That's an article by Terje Mathisen where I got most of
- my information.
-
- You may use this code however you wish.  I hope it's
- useful and I hope I got everything right.
-
- -Kevin
-
- kbaca@skygames.com
-
-*/
-
-#ifdef __GNUC__
-
-#define RDTSC(_dst) \
-__asm__("
-     .byte 0x0F,0x31
-     movl %%edx,(%%edi)
-     movl %%eax,4(%%edi)"\
-: : "D" (_dst) : "eax", "edx", "edi")
-
-// the old code... swapped it
-//     movl %%edx,(%%edi)
-//     movl %%eax,4(%%edi)"
-#define RDMSR(_msri, _msrd) \
-__asm__("
-     .byte 0x0F,0x32
-     movl %%eax,(%%edi)
-     movl %%edx,4(%%edi)"\
-: : "c" (_msri), "D" (_msrd) : "eax", "ecx", "edx", "edi")
-
-#define WRMSR(_msri, _msrd) \
-__asm__("
-     xorl %%edx,%%edx
-     .byte 0x0F,0x30"\
-: : "c" (_msri), "a" (_msrd) : "eax", "ecx", "edx")
-
-#define RDMSR_0x12_0x13(_msr12, _msr13) \
-__asm__("
-     movl $0x12,%%ecx
-     .byte 0x0F,0x32
-     movl %%edx,(%%edi)
-     movl %%eax,4(%%edi)
-     movl $0x13,%%ecx
-     .byte 0x0F,0x32
-     movl %%edx,(%%esi)
-     movl %%eax,4(%%esi)"\
-: : "D" (_msr12), "S" (_msr13) : "eax", "ecx", "edx", "edi")
-
-#define ZERO_MSR_0x12_0x13() \
-__asm__("
-     xorl %%edx,%%edx
-     xorl %%eax,%%eax
-     movl $0x12,%%ecx
-     .byte 0x0F,0x30
-     movl $0x13,%%ecx
-     .byte 0x0F,0x30"\
-: : : "eax", "ecx", "edx")
-
-#elif defined (__WATCOMC__)
-
-extern void RDTSC(UINT32 *dst);
-#pragma aux RDTSC =\
-   "db 0x0F,0x31"\
-   "mov [edi],edx"\
-   "mov [4+edi],eax"\
-   parm [edi]\
-   modify [eax edx edi];
-
-extern void RDMSR(UINT32 msri, UINT32 *msrd);
-#pragma aux RDMSR =\
-   "db 0x0F,0x32"\
-   "mov [edi],edx"\
-   "mov [4+edi],eax"\
-   parm [ecx] [edi]\
-   modify [eax ecx edx edi];
-
-extern void WRMSR(UINT32 msri, UINT32 msrd);
-#pragma aux WRMSR =\
-   "xor edx,edx"\
-   "db 0x0F,0x30"\
-   parm [ecx] [eax]\
-   modify [eax ecx edx];
-
-extern void RDMSR_0x12_0x13(UINT32 *msr12, UINT32 *msr13);
-#pragma aux RDMSR_0x12_0x13 =\
-   "mov ecx,0x12"\
-   "db 0x0F,0x32"\
-   "mov [edi],edx"\
-   "mov [4+edi],eax"\
-   "mov ecx,0x13"\
-   "db 0x0F,0x32"\
-   "mov [esi],edx"\
-   "mov [4+esi],eax"\
-   parm [edi] [esi]\
-   modify [eax ecx edx edi esi];
-
-extern void ZERO_MSR_0x12_0x13(void);
-#pragma aux ZERO_MSR_0x12_0x13 =\
-   "xor edx,edx"\
-   "xor eax,eax"\
-   "mov ecx,0x12"\
-   "db 0x0F,0x30"\
-   "mov ecx,0x13"\
-   "db 0x0F,0x30"\
-   modify [eax ecx edx];
-
-#endif
-
-typedef enum
-{
-   DataRead,
-     DataWrite,
-     DataTLBMiss,
-     DataReadMiss,
-     DataWriteMiss,
-     WriteHitEM,
-     DataCacheLinesWritten,
-     DataCacheSnoops,
-     DataCacheSnoopHit,
-     MemAccessBothPipes,
-     BankConflict,
-     MisalignedDataRef,
-     CodeRead,
-     CodeTLBMiss,
-     CodeCacheMiss,
-     SegRegLoad,
-     RESERVED0,
-     RESERVED1,
-     Branch,
-     BTBHit,
-     TakenBranchOrBTBHit,
-     PipelineFlush,
-     InstructionsExeced,
-     InstructionsExecedVPipe,
-     BusUtilizationClocks,
-     PipelineStalledWriteBackup,
-     PipelineStalledDateMemRead,
-     PipeLineStalledWriteEM,
-     LockedBusCycle,
-     IOReadOrWriteCycle,
-     NonCacheableMemRef,
-     AGI,
-     RESERVED2,
-     RESERVED3,
-     FPOperation,
-     Breakpoint0Match,
-     Breakpoint1Match,
-     Breakpoint2Match,
-     Breakpoint3Match,
-     HWInterrupt,
-     DataReadOrWrite,
-     DataReadOrWriteMiss
-};
-
-#define PROF_CYCLES (0x100)
-#define PROF_EVENTS (0x000)
-#define RING_012    (0x40)
-#define RING_3      (0x80)
-#define RING_0123   (RING_012 | RING_3)
-
-/*void ProfSetProfiles(UINT32 msr12, UINT32 msr13);*/
-#define ProfSetProfiles(_msr12, _msr13)\
-{\
-   UINT32 prof;\
-\
-   prof = (_msr12) | ((_msr13) << 16);\
-   WRMSR(0x11, prof);\
-}
-
-/*void ProfBeginProfiles(void);*/
-#define ProfBeginProfiles()\
-   ZERO_MSR_0x12_0x13();
-
-/*void ProfGetProfiles(UINT32 msr12[2], UINT32 msr13[2]);*/
-#define ProfGetProfiles(_msr12, _msr13)\
-   RDMSR_0x12_0x13(_msr12, _msr13);
-
-/*void ProfZeroTimer(void);*/
-#define ProfZeroTimer()\
-   WRMSR(0x10, 0);
-
-/*void ProfReadTimer(UINT32 timer[2]);*/
-#define ProfReadTimer(timer)\
-   RDMSR(0x10, timer);
-
-/*EOF*/
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 c7d87b88ada52c2ab1c45c947431f0a984243d20..93c828fbecd1394e1adbeac6aa3fd43cc43437b6 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -174,6 +174,7 @@ void A_Boss1Spikeballs(mobj_t *actor);
 void A_Boss3TakeDamage(mobj_t *actor);
 void A_Boss3Path(mobj_t *actor);
 void A_Boss3ShockThink(mobj_t *actor);
+void A_Shockwave(mobj_t *actor);
 void A_LinedefExecute(mobj_t *actor);
 void A_LinedefExecuteFromArg(mobj_t *actor);
 void A_PlaySeeSound(mobj_t *actor);
@@ -828,7 +829,7 @@ static boolean P_LookForShield(mobj_t *actor)
 			continue;
 
 		if ((player->powers[pw_shield] & SH_PROTECTELECTRIC)
-			&& (P_AproxDistance(P_AproxDistance(actor->x-player->mo->x, actor->y-player->mo->y), actor->z-player->mo->z) < FixedMul(RING_DIST, player->mo->scale)))
+			&& (R_PointToDist2(0, 0, R_PointToDist2(0, 0, actor->x-player->mo->x, actor->y-player->mo->y), actor->z-player->mo->z) < FixedMul(RING_DIST, player->mo->scale)))
 		{
 			P_SetTarget(&actor->tracer, player->mo);
 
@@ -3938,7 +3939,7 @@ static void P_DoBossVictory(mobj_t *mo)
 	}
 
 	// victory!
-	if (mo->spawnpoint)
+	if (mo->spawnpoint && mo->spawnpoint->args[3])
 		P_LinedefExecute(mo->spawnpoint->args[3], mo, NULL);
 
 	if (stoppedclock && modeattacking) // if you're just time attacking, skip making the capsule appear since you don't need to step on it anyways.
@@ -4157,7 +4158,7 @@ void A_BossDeath(mobj_t *mo)
 	if (LUA_CallAction(A_BOSSDEATH, mo))
 		return;
 
-	if (mo->spawnpoint)
+	if (mo->spawnpoint && mo->spawnpoint->args[2])
 		P_LinedefExecute(mo->spawnpoint->args[2], mo, NULL);
 	mo->health = 0;
 
@@ -4886,7 +4887,9 @@ void A_FishJump(mobj_t *actor)
 			jumpval = locvar1;
 		else
 		{
-			if (actor->spawnpoint && actor->spawnpoint->args[0])
+			if (!udmf && actor->angle)
+				jumpval = AngleFixed(actor->angle)>>2;
+			else if (actor->spawnpoint && actor->spawnpoint->args[0])
 				jumpval = actor->spawnpoint->args[0] << (FRACBITS - 2);
 			else
 				jumpval = 44 << (FRACBITS - 2);
@@ -5245,7 +5248,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)
 			;
@@ -5321,7 +5324,7 @@ void A_SignPlayer(mobj_t *actor)
 
 	actor->tracer->color = signcolor;
 	if (signcolor && signcolor < numskincolors)
-		signframe += (15 - skincolors[skincolors[signcolor].invcolor].invshade);
+		signframe += (15 - skincolors[facecolor].invshade);
 	actor->tracer->frame = signframe;
 }
 
@@ -7136,7 +7139,7 @@ void A_Boss1Chase(mobj_t *actor)
 		}
 		else
 		{
-			if (actor->spawnpoint)
+			if (actor->spawnpoint && actor->spawnpoint->args[4])
 				P_LinedefExecute(actor->spawnpoint->args[4], actor, NULL);
 			P_SetMobjState(actor, actor->info->raisestate);
 		}
@@ -8377,6 +8380,56 @@ void A_Boss3ShockThink(mobj_t *actor)
 	}
 }
 
+// Function: A_Shockwave
+//
+// Description: Spawns a shockwave of objects. Best used to spawn objects that call A_Boss3ShockThink.
+//
+// var1 = object spawned
+// var2 = amount of objects spawned
+//
+void A_Shockwave(mobj_t *actor)
+{
+	INT32 locvar1 = var1;
+	INT32 locvar2 = var2;
+	INT32 i;
+
+	angle_t ang = 0, interval;
+	mobj_t *shock = NULL, *sfirst = NULL, *sprev = NULL;
+
+	if (LUA_CallAction(A_SHOCKWAVE, actor))
+		return;
+
+	if (locvar2 == 0)
+		locvar2 = 24; // a sensible default, just in case
+
+	interval = FixedAngle((360 << FRACBITS) / locvar2);
+
+	for (i = 0; i < locvar2; i++)
+	{
+		shock = P_SpawnMobj(actor->x, actor->y, actor->z, locvar1);
+		P_SetTarget(&shock->target, actor);
+		shock->fuse = shock->info->painchance;
+
+		if (i % 2 == 0)
+			P_SetMobjState(shock, shock->state->nextstate);
+
+		if (!sprev)
+			sfirst = shock;
+		else
+		{
+			if (i == locvar2 - 1)
+				P_SetTarget(&shock->hnext, sfirst);
+			P_SetTarget(&sprev->hnext, shock);
+		}
+
+		P_Thrust(shock, ang, shock->info->speed);
+		ang += interval;
+		sprev = shock;
+	}
+	
+	S_StartSound(actor, shock->info->seesound);
+}
+
 // Function: A_LinedefExecute
 //
 // Description: Object's location is used to set the calling sector. The tag used is var1. Optionally, if var2 is set, the actor's angle (multiplied by var2) is added to the tag number as well.
@@ -8685,10 +8738,10 @@ void A_RollAngle(mobj_t *actor)
 
 	// relative (default)
 	if (!locvar2)
-		actor->rollangle += angle;
+		actor->spriteroll += angle;
 	// absolute
 	else
-		actor->rollangle = angle;
+		actor->spriteroll = angle;
 }
 
 // Function: A_ChangeRollAngleRelative
@@ -8713,7 +8766,7 @@ void A_ChangeRollAngleRelative(mobj_t *actor)
 		I_Error("A_ChangeRollAngleRelative: var1 is greater than var2");
 #endif
 
-	actor->rollangle += FixedAngle(P_RandomRange(amin, amax));
+	actor->spriteroll += FixedAngle(P_RandomRange(amin, amax));
 }
 
 // Function: A_ChangeRollAngleAbsolute
@@ -8738,7 +8791,7 @@ void A_ChangeRollAngleAbsolute(mobj_t *actor)
 		I_Error("A_ChangeRollAngleAbsolute: var1 is greater than var2");
 #endif
 
-	actor->rollangle = FixedAngle(P_RandomRange(amin, amax));
+	actor->spriteroll = FixedAngle(P_RandomRange(amin, amax));
 }
 
 // Function: A_PlaySound
@@ -9006,7 +9059,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))
 	{
@@ -12583,8 +12636,7 @@ void A_MineRange(mobj_t *actor)
 void A_ConnectToGround(mobj_t *actor)
 {
 	mobj_t *work;
-	fixed_t workz;
-	fixed_t workh;
+	fixed_t endz;
 	angle_t ang;
 	INT32 locvar1 = var1;
 	INT32 locvar2 = var2;
@@ -12595,38 +12647,42 @@ void A_ConnectToGround(mobj_t *actor)
 	if (actor->subsector->sector->ffloors)
 		P_AdjustMobjFloorZ_FFloors(actor, actor->subsector->sector, 2);
 
+	endz = actor->z;
 	if (actor->flags2 & MF2_OBJECTFLIP)
-		workz = (actor->z + actor->height) - actor->ceilingz;
+		actor->z = actor->ceilingz - actor->height; // Ensures perfect ceiling connection
 	else
-		workz = actor->floorz - actor->z;
+		actor->z = actor->floorz; // Ensures perfect floor connection
 
 	if (locvar2)
 	{
-		workh = FixedMul(mobjinfo[locvar2].height, actor->scale);
-		if (actor->flags2 & MF2_OBJECTFLIP)
-			workz += workh;
-		work = P_SpawnMobjFromMobj(actor, 0, 0, workz, locvar2);
-		workz += workh;
-	}
+		work = P_SpawnMobjFromMobj(actor, 0, 0, 0, locvar2);
+		if (work)
+			work->old_z = work->z; // Don't copy old_z from the actor
 
-	if (!locvar1)
-		return;
+		actor->z += P_MobjFlip(actor) * FixedMul(mobjinfo[locvar2].height, actor->scale);
+	}
 
-	if (!(workh = FixedMul(mobjinfo[locvar1].height, actor->scale)))
+	if (!locvar1 || !mobjinfo[locvar1].height) // Can't tile the middle object?
+	{
+		actor->z = endz;
 		return;
+	}
 
 	ang = actor->angle + ANGLE_45;
-	while (workz < 0)
+	while ((actor->flags2 & MF2_OBJECTFLIP) ? (actor->z > endz) : (actor->z < endz))
 	{
-		work = P_SpawnMobjFromMobj(actor, 0, 0, workz, locvar1);
+		work = P_SpawnMobjFromMobj(actor, 0, 0, 0, locvar1);
 		if (work)
-			work->angle = ang;
+		{
+			work->angle = work->old_angle = ang;
+			work->old_z = work->z; // Don't copy old_z from the actor
+		}
+
 		ang += ANGLE_90;
-		workz += workh;
+		actor->z += P_MobjFlip(actor) * FixedMul(mobjinfo[locvar1].height, actor->scale);
 	}
 
-	if (workz != 0)
-		actor->z += P_MobjFlip(actor)*workz;
+	actor->old_z = actor->z; // Reset Z interpolation - the spawned objects intentionally don't have any Z interpolation either, after all
 }
 
 // Function: A_SpawnParticleRelative
diff --git a/src/p_floor.c b/src/p_floor.c
index 9c24f585141f3223c223c902f80fbba21896c4f3..38f0c5a0fbc496564e7b69c96f7cacd8a93287f6 100644
--- a/src/p_floor.c
+++ b/src/p_floor.c
@@ -155,7 +155,7 @@ result_e T_MovePlane(sector_t *sector, fixed_t speed, fixed_t dest, boolean crus
 		}
 	}
 
-	return ok;
+	return planeok;
 }
 
 //
@@ -1128,7 +1128,7 @@ void T_ThwompSector(thwomp_t *thwomp)
 				thwomp->direction         // direction
 			);
 
-			if (res == ok || res == pastdest)
+			if (res == planeok || res == pastdest)
 				T_MovePlane
 				(
 					thwomp->sector,             // sector
@@ -1160,7 +1160,7 @@ void T_ThwompSector(thwomp_t *thwomp)
 				thwomp->direction // direction
 			);
 
-			if (res == ok || res == pastdest)
+			if (res == planeok || res == pastdest)
 				T_MovePlane
 				(
 					thwomp->sector,   // sector
@@ -1465,7 +1465,7 @@ void T_RaiseSector(raise_t *raise)
 		direction           // direction
 	);
 
-	if (res == ok || res == pastdest)
+	if (res == planeok || res == pastdest)
 		T_MovePlane
 		(
 			raise->sector,    // sector
diff --git a/src/p_inter.c b/src/p_inter.c
index 873448dcd614c62a368f7c53200e7403dea7a587..c3811cbe4e827f41133f2c1693a932d33d36a48d 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") : ""
@@ -164,6 +165,62 @@ boolean P_CanPickupItem(player_t *player, boolean weapon)
 	return true;
 }
 
+boolean P_CanPickupEmblem(player_t *player, INT32 emblemID)
+{
+	emblem_t *emblem = NULL;
+
+	if (emblemID < 0 || emblemID >= numemblems)
+	{
+		// Invalid emblem ID, can't pickup.
+		return false;
+	}
+
+	emblem = &emblemlocations[emblemID];
+
+	if (demoplayback)
+	{
+		// Never collect emblems in replays.
+		return false;
+	}
+
+	if (player->bot && player->bot != BOT_MPAI)
+	{
+		// Your little lap-dog can't grab these for you.
+		return false;
+	}
+
+	if (emblem->type == ET_SKIN)
+	{
+		INT32 skinnum = M_EmblemSkinNum(emblem);
+
+		if (player->skin != skinnum)
+		{
+			// Incorrect skin to pick up this emblem.
+			return false;
+		}
+	}
+
+	return true;
+}
+
+boolean P_EmblemWasCollected(INT32 emblemID)
+{
+	if (emblemID < 0 || emblemID >= numemblems)
+	{
+		// Invalid emblem ID, can't pickup.
+		return true;
+	}
+
+	if (shareEmblems && !serverGamedata->collected[emblemID])
+	{
+		// It can be worth collecting again if we're sharing emblems
+		// and the server doesn't have it.
+		return false;
+	}
+
+	return clientGamedata->collected[emblemID];
+}
+
 //
 // P_DoNightsScore
 //
@@ -199,42 +256,19 @@ void P_DoNightsScore(player_t *player)
 		player->linktimer = nightslinktics;
 	}
 
-	if (player->linkcount < 10)
-	{
-		if (player->bonustime)
-		{
-			P_AddPlayerScore(player, player->linkcount*20);
-			P_SetMobjState(dummymo, dummymo->info->xdeathstate+player->linkcount-1);
-		}
-		else
-		{
-			P_AddPlayerScore(player, player->linkcount*10);
-			P_SetMobjState(dummymo, dummymo->info->spawnstate+player->linkcount-1);
-		}
-	}
-	else
-	{
-		if (player->bonustime)
-		{
-			P_AddPlayerScore(player, 200);
-			P_SetMobjState(dummymo, dummymo->info->xdeathstate+9);
-		}
-		else
-		{
-			P_AddPlayerScore(player, 100);
-			P_SetMobjState(dummymo, dummymo->info->spawnstate+9);
-		}
-	}
+	// 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);
 
-	// Hoops are the only things that should add to your drill meter
-	//player->drillmeter += TICRATE;
+	// Make objects slowly rise & scale up
 	dummymo->momz = FRACUNIT;
 	dummymo->fuse = 3*TICRATE;
-
-	// What?! NO, don't use the camera! Scale up instead!
-	//P_InstaThrust(dummymo, R_PointToAngle2(dummymo->x, dummymo->y, camera.x, camera.y), 3*FRACUNIT);
 	dummymo->scalespeed = FRACUNIT/25;
 	dummymo->destscale = 2*FRACUNIT;
+
+	// Add extra values used for color variety
+	dummymo->extravalue1 = player->linkcount-1;
+	dummymo->extravalue2 = ((player->linkcount-1 >= 300) ? (player->linkcount-1 >= 600) ? 2 : 1 : 0);
 }
 
 //
@@ -465,23 +499,20 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			if (special->type == MT_PTERABYTE && special->target == player->mo && special->extravalue1 == 1)
 				return; // Can't hurt a Pterabyte if it's trying to pick you up
 
-			if ((P_MobjFlip(toucher)*toucher->momz < 0) && (elementalpierce != 1))
+			if ((P_MobjFlip(toucher)*toucher->momz < 0) && (elementalpierce != 1) && (!(player->powers[pw_strong] & STR_HEAVY)))
 			{
-				if (!(player->charability2 == CA2_MELEE && player->panim == PA_ABILITY2))
+				fixed_t setmomz = -toucher->momz; // Store this, momz get changed by P_DoJump within P_DoBubbleBounce
+				
+				if (elementalpierce == 2) // Reset bubblewrap, part 1
+					P_DoBubbleBounce(player);
+				toucher->momz = setmomz;
+				if (elementalpierce == 2) // Reset bubblewrap, part 2
 				{
-					fixed_t setmomz = -toucher->momz; // Store this, momz get changed by P_DoJump within P_DoBubbleBounce
-
-					if (elementalpierce == 2) // Reset bubblewrap, part 1
-						P_DoBubbleBounce(player);
-					toucher->momz = setmomz;
-					if (elementalpierce == 2) // Reset bubblewrap, part 2
-					{
-						boolean underwater = toucher->eflags & MFE_UNDERWATER;
-
-						if (underwater)
-							toucher->momz /= 2;
-						toucher->momz -= (toucher->momz/(underwater ? 8 : 4)); // Cap the height!
-					}
+					boolean underwater = toucher->eflags & MFE_UNDERWATER;
+							
+					if (underwater)
+						toucher->momz /= 2;
+					toucher->momz -= (toucher->momz/(underwater ? 8 : 4)); // Cap the height!
 				}
 			}
 			if (player->pflags & PF_BOUNCING)
@@ -500,8 +531,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 					toucher->momx = 7*toucher->momx>>3;
 					toucher->momy = 7*toucher->momy>>3;
 				}
-				else if (player->dashmode >= DASHMODE_THRESHOLD && (player->charflags & (SF_DASHMODE|SF_MACHINE)) == (SF_DASHMODE|SF_MACHINE)
-					&& player->panim == PA_DASH)
+				else if ((player->powers[pw_strong] & STR_DASH) && player->panim == PA_DASH)
 					P_DoPlayerPain(player, special, special);
 			}
 			P_DamageMobj(special, toucher, toucher, 1, 0);
@@ -563,7 +593,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 			special->momx = special->momy = special->momz = 0;
 			P_GivePlayerSpheres(player, 1);
 
-			if (special->type == MT_BLUESPHERE)
+			if (special->type == MT_BLUESPHERE || special->type == MT_FLINGBLUESPHERE)
 			{
 				special->destscale = ((player->powers[pw_carry] == CR_NIGHTSMODE) ? 4 : 2)*special->scale;
 				if (states[special->info->deathstate].tics > 0)
@@ -738,13 +768,70 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 		// Secret emblem thingy
 		case MT_EMBLEM:
 			{
-				if (demoplayback || (player->bot && player->bot != BOT_MPAI) || special->health <= 0 || special->health > MAXEMBLEMS)
+				const boolean toucherIsServer = ((player - players) == serverplayer);
+				const boolean consoleIsServer = (consoleplayer == serverplayer);
+				boolean prevCollected = false;
+
+				if ((special->flags2 & MF2_NIGHTSPULL)
+					&& (toucher == special->tracer))
+				{
+					// Since collecting may not remove the object,
+					// we need to manually stop it from chasing.
+					P_SetTarget(&special->tracer, NULL);
+					special->flags2 &= ~MF2_NIGHTSPULL;
+					special->movefactor = 0;
+					special->momx = special->momy = special->momz = 0;
+				}
+
+				if (!P_CanPickupEmblem(player, special->health - 1))
+				{
 					return;
-				emblemlocations[special->health-1].collected = true;
+				}
 
-				M_UpdateUnlockablesAndExtraEmblems();
-				G_SaveGameData();
-				break;
+				prevCollected = P_EmblemWasCollected(special->health - 1);
+
+				if (toucherIsServer || shareEmblems)
+				{
+					serverGamedata->collected[special->health-1] = true;
+					M_SilentUpdateUnlockablesAndEmblems(serverGamedata);
+				}
+
+				if (P_IsLocalPlayer(player) || (consoleIsServer && shareEmblems))
+				{
+					clientGamedata->collected[special->health-1] = true;
+					M_UpdateUnlockablesAndExtraEmblems(clientGamedata);
+					G_SaveGameData(clientGamedata);
+				}
+
+				if (netgame)
+				{
+					// This always spawns the object to prevent mobjnum issues,
+					// but makes the effect invisible to whoever it doesn't matter to.
+					mobj_t *spark = P_SpawnMobjFromMobj(special, 0, 0, 0, MT_SPARK);
+
+					if (prevCollected == false && P_EmblemWasCollected(special->health - 1) == true)
+					{
+						// Play the sound if it was collected.
+						S_StartSound((shareEmblems ? NULL : special), special->info->deathsound);
+					}
+					else
+					{
+						// We didn't collect it, make it invisible to us.
+						spark->flags2 |= MF2_DONTDRAW;
+					}
+
+					return;
+				}
+				else
+				{
+					if (prevCollected == false && P_EmblemWasCollected(special->health - 1) == true)
+					{
+						// Disappear when collecting for local games.
+						break;
+					}
+
+					return;
+				}
 			}
 
 		// CTF Flags
@@ -1058,7 +1145,7 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
 					if (!(mo2->type == MT_RING || mo2->type == MT_COIN
 						|| mo2->type == MT_BLUESPHERE || mo2->type == MT_BOMBSPHERE
 						|| mo2->type == MT_NIGHTSCHIP || mo2->type == MT_NIGHTSSTAR
-						|| ((mo2->type == MT_EMBLEM) && (mo2->reactiontime & GE_NIGHTSPULL))))
+						|| ((mo2->type == MT_EMBLEM) && (mo2->reactiontime & GE_NIGHTSPULL) && P_CanPickupEmblem(player, mo2->health - 1) && !P_EmblemWasCollected(mo2->health - 1))))
 						continue;
 
 					// Yay! The thing's in reach! Pull it in!
@@ -2149,7 +2236,7 @@ void P_CheckTimeLimit(void)
 		}
 
 		if (server)
-			SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+			D_SendExitLevel(false);
 	}
 
 	//Optional tie-breaker for Match/CTF
@@ -2212,11 +2299,11 @@ void P_CheckTimeLimit(void)
 			}
 		}
 		if (server)
-			SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+			D_SendExitLevel(false);
 	}
 
 	if (server)
-		SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+		D_SendExitLevel(false);
 }
 
 /** Checks if a player's score is over the pointlimit and the round should end.
@@ -2245,7 +2332,7 @@ void P_CheckPointLimit(void)
 		if ((UINT32)cv_pointlimit.value <= redscore || (UINT32)cv_pointlimit.value <= bluescore)
 		{
 			if (server)
-				SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+				D_SendExitLevel(false);
 		}
 	}
 	else
@@ -2258,7 +2345,7 @@ void P_CheckPointLimit(void)
 			if ((UINT32)cv_pointlimit.value <= players[i].score)
 			{
 				if (server)
-					SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+					D_SendExitLevel(false);
 				return;
 			}
 		}
@@ -2302,7 +2389,7 @@ void P_CheckSurvivors(void)
 		{
 			CONS_Printf(M_GetText("The IT player has left the game.\n"));
 			if (server)
-				SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+				D_SendExitLevel(false);
 
 			return;
 		}
@@ -2322,7 +2409,7 @@ void P_CheckSurvivors(void)
 			{
 				CONS_Printf(M_GetText("All players have been tagged!\n"));
 				if (server)
-					SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+					D_SendExitLevel(false);
 			}
 
 			return;
@@ -2334,7 +2421,7 @@ void P_CheckSurvivors(void)
 		{
 			CONS_Printf(M_GetText("There are no players able to become IT.\n"));
 			if (server)
-				SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+				D_SendExitLevel(false);
 		}
 
 		return;
@@ -2346,7 +2433,7 @@ void P_CheckSurvivors(void)
 	{
 		CONS_Printf(M_GetText("All players have been tagged!\n"));
 		if (server)
-			SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+			D_SendExitLevel(false);
 	}
 }
 
@@ -2544,7 +2631,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);
 
@@ -3031,7 +3118,7 @@ static void P_NiGHTSDamage(mobj_t *target, mobj_t *source)
 		P_SetPlayerMobjState(target, S_PLAY_NIGHTS_STUN);
 		S_StartSound(target, sfx_nghurt);
 
-		player->mo->rollangle = 0;
+		player->mo->spriteroll = 0;
 
 		if (oldnightstime > 10*TICRATE
 			&& player->nightstime < 10*TICRATE)
@@ -3213,7 +3300,7 @@ static boolean P_PlayerHitsPlayer(mobj_t *target, mobj_t *inflictor, mobj_t *sou
 		return false;
 
 	// Add pity.
-	if (!player->powers[pw_flashing] && !player->powers[pw_invulnerability] && !player->powers[pw_super]
+	if (!player->powers[pw_flashing] && !player->powers[pw_invulnerability] && !player->powers[pw_super] && !(player->powers[pw_strong] & STR_GUARD)
 	&& source->player->score > player->score)
 		player->pity++;
 
@@ -3237,7 +3324,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;
@@ -3354,7 +3441,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;
@@ -3685,7 +3772,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
 			}
 			return false;
 		}
-		else if (player->powers[pw_invulnerability] || player->powers[pw_flashing] || player->powers[pw_super]) // ignore bouncing & such in invulnerability
+		else if (player->powers[pw_invulnerability] || player->powers[pw_flashing] || player->powers[pw_super] || (player->powers[pw_strong] & STR_GUARD)) // ignore bouncing & such in invulnerability
 		{
 			if (force
 			|| (inflictor && inflictor->flags & MF_MISSILE && inflictor->flags2 & MF2_SUPERFIRE)) // Super Sonic is stunned!
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 cc060e4eee3c9b45f5b0da4e1c9c43a61f5c8ccf..4b330184bddd915ea155ca1741b6b03bf80f43d8 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -146,6 +146,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);
@@ -510,6 +511,8 @@ void P_ClearStarPost(INT32 postnum);
 void P_ResetStarposts(void);
 
 boolean P_CanPickupItem(player_t *player, boolean weapon);
+boolean P_CanPickupEmblem(player_t *player, INT32 emblemID);
+boolean P_EmblemWasCollected(INT32 emblemID);
 void P_DoNightsScore(player_t *player);
 void P_DoMatchSuper(player_t *player);
 
@@ -535,5 +538,6 @@ void P_Thrust(mobj_t *mo, angle_t angle, fixed_t move);
 void P_DoSuperTransformation(player_t *player, boolean giverings);
 void P_ExplodeMissile(mobj_t *mo);
 void P_CheckGravity(mobj_t *mo, boolean affect);
+void P_SetPitchRollFromSlope(mobj_t *mo, pslope_t *slope);
 
 #endif // __P_LOCAL__
diff --git a/src/p_map.c b/src/p_map.c
index 27d5ea7810e2d630ef1aad9647e8379168f63887..251837876caab9f979b92ef1ece83a1750f24768 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -177,10 +177,8 @@ boolean P_DoSpring(mobj_t *spring, mobj_t *object)
 	{
 		if (spring->info->painchance == 3)
 			;
-		else if (object->player->charability == CA_TWINSPIN && object->player->panim == PA_ABILITY)
+		else if (object->player->powers[pw_strong] & STR_SPRING)
 			strong = 1;
-		else if (object->player->charability2 == CA2_MELEE && object->player->panim == PA_ABILITY2)
-			strong = 2;
 	}
 
 	if (spring->info->painchance == -1) // Pinball bumper mode.
@@ -495,7 +493,8 @@ springstate:
 
 		if (strong)
 		{
-			P_TwinSpinRejuvenate(object->player, (strong == 1 ? object->player->thokitem : object->player->revitem));
+			if (object->player->charability == CA_TWINSPIN || object->player->charability2 == CA2_MELEE)
+				P_TwinSpinRejuvenate(object->player, (object->player->charability == CA_TWINSPIN ? object->player->thokitem : object->player->revitem));
 			S_StartSound(object, sfx_sprong); // strong spring. sprong.
 		}
 	}
@@ -836,43 +835,25 @@ static boolean PIT_CheckThing(mobj_t *thing)
 		return true;
 	}
 
-	// SF_DASHMODE users destroy spikes and monitors, CA_TWINSPIN users and CA2_MELEE users destroy spikes.
-	if ((tmthing->player)
-		&& ((((tmthing->player->charflags & (SF_DASHMODE|SF_MACHINE)) == (SF_DASHMODE|SF_MACHINE)) && (tmthing->player->dashmode >= DASHMODE_THRESHOLD)
-		&& (thing->flags & (MF_MONITOR)
-		|| (thing->type == MT_SPIKE
-		|| thing->type == MT_WALLSPIKE)))
-	|| ((((tmthing->player->charability == CA_TWINSPIN) && (tmthing->player->panim == PA_ABILITY))
-	|| (tmthing->player->charability2 == CA2_MELEE && tmthing->player->panim == PA_ABILITY2))
-		&& (thing->type == MT_SPIKE
-		|| thing->type == MT_WALLSPIKE))))
-	{
-		if ((thing->flags & (MF_MONITOR)) && (thing->health <= 0 || !(thing->flags & MF_SHOOTABLE)))
-			return true;
-		blockdist = thing->radius + tmthing->radius;
-		if (abs(thing->x - tmx) >= blockdist || abs(thing->y - tmy) >= blockdist)
-			return true; // didn't hit it
-		// see if it went over / under
-		if (tmthing->z > thing->z + thing->height)
-			return true; // overhead
-		if (tmthing->z + tmthing->height < thing->z)
-			return true; // underneath
-		if (thing->type == MT_SPIKE
-		|| thing->type == MT_WALLSPIKE)
-		{
-			mobj_t *iter;
-			if (thing->flags & MF_SOLID)
-				S_StartSound(tmthing, thing->info->deathsound);
-			for (iter = thing->subsector->sector->thinglist; iter; iter = iter->snext)
-				if (iter->type == thing->type && iter->health > 0 && iter->flags & MF_SOLID && (iter == thing || P_AproxDistance(P_AproxDistance(thing->x - iter->x, thing->y - iter->y), thing->z - iter->z) < 56*thing->scale))//FixedMul(56*FRACUNIT, thing->scale))
-					P_KillMobj(iter, tmthing, tmthing, 0);
-			return true;
-		}
-		else
-		{
-			if (P_DamageMobj(thing, tmthing, tmthing, 1, 0))
-				return true;
-		}
+	// STR_SPIKE users destroy spikes
+	if ((tmthing->player) && ((tmthing->player->powers[pw_strong] & STR_SPIKE) && (thing->type == MT_SPIKE || thing->type == MT_WALLSPIKE)))
+	{
+		mobj_t *iter;
+        	blockdist = thing->radius + tmthing->radius;
+        	if (abs(thing->x - tmx) >= blockdist || abs(thing->y - tmy) >= blockdist)
+            		return true; // didn't hit it
+        	// see if it went over / under
+        	if (tmthing->z > thing->z + thing->height)
+            		return true; // overhead
+        	if (tmthing->z + tmthing->height < thing->z)
+            		return true; // underneath
+
+		if (thing->flags & MF_SOLID)
+			S_StartSound(tmthing, thing->info->deathsound);
+		for (iter = thing->subsector->sector->thinglist; iter; iter = iter->snext)
+			if (iter->type == thing->type && iter->health > 0 && iter->flags & MF_SOLID && (iter == thing || P_AproxDistance(P_AproxDistance(thing->x - iter->x, thing->y - iter->y), thing->z - iter->z) < 56*thing->scale))//FixedMul(56*FRACUNIT, thing->scale))
+				P_KillMobj(iter, tmthing, tmthing, 0);
+		return true;
 	}
 
 	// vectorise metal - done in a special case as at this point neither has the right flags for touching
@@ -1185,7 +1166,7 @@ static boolean PIT_CheckThing(mobj_t *thing)
 	}
 
 	// When solid spikes move, assume they just popped up and teleport things on top of them to hurt.
-	if (tmthing->type == MT_SPIKE && tmthing->flags & MF_SOLID)
+	if (tmthing->type == MT_SPIKE && (thing->flags & MF_SOLID) && (tmthing->flags & MF_SOLID))
 	{
 		if (thing->z > tmthing->z + tmthing->height)
 			return true; // overhead
@@ -1727,25 +1708,22 @@ static boolean PIT_CheckThing(mobj_t *thing)
 				// Going down? Then bounce back up.
 				if (P_DamageMobj(thing, tmthing, tmthing, 1, 0) // break the monitor
 				&& (flipval*(*momz) < 0) // monitor is on the floor and you're going down, or on the ceiling and you're going up
-				&& (elementalpierce != 1)) // you're not piercing through the monitor...
+				&& (elementalpierce != 1) && (!(player->powers[pw_strong] & STR_HEAVY))) // you're not piercing through the monitor...
 				{
-					if (!(player->charability2 == CA2_MELEE && player->panim == PA_ABILITY2))
+					fixed_t setmomz = -*momz; // Store this, momz get changed by P_DoJump within P_DoBubbleBounce
+
+					if (elementalpierce == 2) // Reset bubblewrap, part 1
+						P_DoBubbleBounce(player);
+					*momz = setmomz; // Therefore, you should be thrust in the opposite direction, vertically.
+					if (player->charability == CA_TWINSPIN && player->panim == PA_ABILITY)
+						P_TwinSpinRejuvenate(player, player->thokitem);
+					if (elementalpierce == 2) // Reset bubblewrap, part 2
 					{
-						fixed_t setmomz = -*momz; // Store this, momz get changed by P_DoJump within P_DoBubbleBounce
-
-						if (elementalpierce == 2) // Reset bubblewrap, part 1
-							P_DoBubbleBounce(player);
-						*momz = setmomz; // Therefore, you should be thrust in the opposite direction, vertically.
-						if (player->charability == CA_TWINSPIN && player->panim == PA_ABILITY)
-							P_TwinSpinRejuvenate(player, player->thokitem);
-						if (elementalpierce == 2) // Reset bubblewrap, part 2
-						{
-							boolean underwater = tmthing->eflags & MFE_UNDERWATER;
+						boolean underwater = tmthing->eflags & MFE_UNDERWATER;
 
-							if (underwater)
-								*momz /= 2;
-							*momz -= (*momz/(underwater ? 8 : 4)); // Cap the height!
-						}
+						if (underwater)
+							*momz /= 2;
+						*momz -= (*momz/(underwater ? 8 : 4)); // Cap the height!
 					}
 				}
 				if (!(elementalpierce == 1 && thing->flags & MF_GRENADEBOUNCE)) // prevent gold monitor clipthrough.
@@ -2546,7 +2524,6 @@ boolean P_CheckCameraPosition(fixed_t x, fixed_t y, camera_t *thiscam)
 boolean P_TryCameraMove(fixed_t x, fixed_t y, camera_t *thiscam)
 {
 	subsector_t *s = R_PointInSubsector(x, y);
-	boolean retval = true;
 	boolean itsatwodlevel = false;
 
 	floatok = false;
@@ -2561,8 +2538,8 @@ boolean P_TryCameraMove(fixed_t x, fixed_t y, camera_t *thiscam)
 		fixed_t tryx = thiscam->x;
 		fixed_t tryy = thiscam->y;
 
-		if ((thiscam == &camera && (players[displayplayer].pflags & PF_NOCLIP))
-		|| (thiscam == &camera2 && (players[secondarydisplayplayer].pflags & PF_NOCLIP)))
+		if ((thiscam == &camera && (players[displayplayer].pflags & PF_NOCLIP || players[displayplayer].powers[pw_carry] == CR_NIGHTSMODE))
+		|| (thiscam == &camera2 && (players[secondarydisplayplayer].pflags & PF_NOCLIP || players[secondarydisplayplayer].powers[pw_carry] == CR_NIGHTSMODE)))
 		{ // Noclipping player camera noclips too!!
 			floatok = true;
 			thiscam->floorz = thiscam->z;
@@ -2630,7 +2607,7 @@ boolean P_TryCameraMove(fixed_t x, fixed_t y, camera_t *thiscam)
 	thiscam->y = y;
 	thiscam->subsector = s;
 
-	return retval;
+	return true;
 }
 
 //
@@ -2728,8 +2705,22 @@ increment_move
 	fixed_t thingtop;
 	floatok = false;
 
-	if (radius < MAXRADIUS/2)
-		radius = MAXRADIUS/2;
+	// This makes sure that there are no freezes from computing extremely small movements.
+	// Originally was MAXRADIUS/2, but that can cause some bad inconsistencies for small players.
+	radius = max(radius, thing->scale);
+
+	// And we also have to prevent Big Large (tm) movements, as those can skip too far
+	// across slopes and cause us to fail step up checks on them when we otherwise shouldn't.
+	radius = min(radius, 16 * thing->scale);
+
+	// (This whole "step" system is flawed; it was OK before, but the addition of slopes has
+	// exposed the problems with doing it like this. The right thing to do would be to use
+	// raycasting for physics to fix colliding in weird order, double-checking collisions,
+	// randomly colliding with slopes instead of going up them, etc. I don't feel like porting
+	// that from RR, as its both a huge sweeping change and still incomplete at the time of
+	// writing. Clamping radius to make our steps more precise will work just fine as long
+	// as you keep all of your crazy intentions to poke any of the other deep-rooted movement
+	// code to yourself. -- Sal 6/5/2023)
 
 	do {
 		if (thing->flags & MF_NOCLIP) {
@@ -2936,6 +2927,8 @@ boolean P_TryMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff)
 			if (thing->momz <= 0)
 			{
 				thing->standingslope = tmfloorslope;
+				P_SetPitchRollFromSlope(thing, thing->standingslope);
+
 				if (thing->momz == 0 && thing->player && !startingonground)
 					P_PlayerHitFloor(thing->player, true);
 			}
@@ -2947,6 +2940,8 @@ boolean P_TryMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff)
 			if (thing->momz >= 0)
 			{
 				thing->standingslope = tmceilingslope;
+				P_SetPitchRollFromSlope(thing, thing->standingslope);
+
 				if (thing->momz == 0 && thing->player && !startingonground)
 					P_PlayerHitFloor(thing->player, true);
 			}
@@ -3735,7 +3730,9 @@ void P_SlideMove(mobj_t *mo)
 
 	boolean papercol = false;
 	vertex_t v1, v2; // fake vertexes
-	line_t junk; // fake linedef
+	static line_t junk; // fake linedef
+
+	memset(&junk, 0x00, sizeof(junk));
 
 	if (tmhitthing && mo->z + mo->height > tmhitthing->z && mo->z < tmhitthing->z + tmhitthing->height)
 	{
@@ -4013,131 +4010,135 @@ void P_BounceMove(mobj_t *mo)
 	slidemo = mo;
 	hitcount = 0;
 
-retry:
-	if (++hitcount == 3)
-		goto bounceback; // don't loop forever
-
-	if (mo->player)
-	{
-		mmomx = mo->player->rmomx;
-		mmomy = mo->player->rmomy;
-	}
-	else
+	do
 	{
-		mmomx = mo->momx;
-		mmomy = mo->momy;
-	}
+		if (++hitcount == 3)
+			goto bounceback; // don't loop forever
 
-	// trace along the three leading corners
-	if (mo->momx > 0)
-	{
-		leadx = mo->x + mo->radius;
-		trailx = mo->x - mo->radius;
-	}
-	else
-	{
-		leadx = mo->x - mo->radius;
-		trailx = mo->x + mo->radius;
-	}
+		if (mo->player)
+		{
+			mmomx = mo->player->rmomx;
+			mmomy = mo->player->rmomy;
+		}
+		else
+		{
+			mmomx = mo->momx;
+			mmomy = mo->momy;
+		}
 
-	if (mo->momy > 0)
-	{
-		leady = mo->y + mo->radius;
-		traily = mo->y - mo->radius;
-	}
-	else
-	{
-		leady = mo->y - mo->radius;
-		traily = mo->y + mo->radius;
-	}
+		// trace along the three leading corners
+		if (mo->momx > 0)
+		{
+			leadx = mo->x + mo->radius;
+			trailx = mo->x - mo->radius;
+		}
+		else
+		{
+			leadx = mo->x - mo->radius;
+			trailx = mo->x + mo->radius;
+		}
+
+		if (mo->momy > 0)
+		{
+			leady = mo->y + mo->radius;
+			traily = mo->y - mo->radius;
+		}
+		else
+		{
+			leady = mo->y - mo->radius;
+			traily = mo->y + mo->radius;
+		}
 
-	bestslidefrac = FRACUNIT + 1;
+		bestslidefrac = FRACUNIT + 1;
 
-	P_PathTraverse(leadx, leady, leadx + mmomx, leady + mmomy, PT_ADDLINES, PTR_SlideTraverse);
-	P_PathTraverse(trailx, leady, trailx + mmomx, leady + mmomy, PT_ADDLINES, PTR_SlideTraverse);
-	P_PathTraverse(leadx, traily, leadx + mmomx, traily + mmomy, PT_ADDLINES, PTR_SlideTraverse);
+		P_PathTraverse(leadx, leady, leadx + mmomx, leady + mmomy, PT_ADDLINES, PTR_SlideTraverse);
+		P_PathTraverse(trailx, leady, trailx + mmomx, leady + mmomy, PT_ADDLINES, PTR_SlideTraverse);
+		P_PathTraverse(leadx, traily, leadx + mmomx, traily + mmomy, PT_ADDLINES, PTR_SlideTraverse);
 
-	// move up to the wall
-	if (bestslidefrac == FRACUNIT + 1)
-	{
-		// the move must have hit the middle, so bounce straight back
-bounceback:
-		if (P_TryMove(mo, mo->x - mmomx, mo->y - mmomy, true))
+		// move up to the wall
+		if (bestslidefrac == FRACUNIT + 1)
 		{
-			mo->momx *= -1;
-			mo->momy *= -1;
-			mo->momx = FixedMul(mo->momx, (FRACUNIT - (FRACUNIT>>2) - (FRACUNIT>>3)));
-			mo->momy = FixedMul(mo->momy, (FRACUNIT - (FRACUNIT>>2) - (FRACUNIT>>3)));
-
-			if (mo->player)
+			// the move must have hit the middle, so bounce straight back
+bounceback:
+			if (P_TryMove(mo, mo->x - mmomx, mo->y - mmomy, true))
 			{
-				mo->player->cmomx *= -1;
-				mo->player->cmomy *= -1;
-				mo->player->cmomx = FixedMul(mo->player->cmomx,
-					(FRACUNIT - (FRACUNIT>>2) - (FRACUNIT>>3)));
-				mo->player->cmomy = FixedMul(mo->player->cmomy,
-					(FRACUNIT - (FRACUNIT>>2) - (FRACUNIT>>3)));
+				mo->momx *= -1;
+				mo->momy *= -1;
+				mo->momx = FixedMul(mo->momx, (FRACUNIT - (FRACUNIT>>2) - (FRACUNIT>>3)));
+				mo->momy = FixedMul(mo->momy, (FRACUNIT - (FRACUNIT>>2) - (FRACUNIT>>3)));
+
+				if (mo->player)
+				{
+					mo->player->cmomx *= -1;
+					mo->player->cmomy *= -1;
+					mo->player->cmomx = FixedMul(mo->player->cmomx,
+						(FRACUNIT - (FRACUNIT>>2) - (FRACUNIT>>3)));
+					mo->player->cmomy = FixedMul(mo->player->cmomy,
+						(FRACUNIT - (FRACUNIT>>2) - (FRACUNIT>>3)));
+				}
 			}
+			return;
 		}
-		return;
-	}
 
-	// fudge a bit to make sure it doesn't hit
-	bestslidefrac -= 0x800;
-	if (bestslidefrac > 0)
-	{
-		newx = FixedMul(mmomx, bestslidefrac);
-		newy = FixedMul(mmomy, bestslidefrac);
+		// fudge a bit to make sure it doesn't hit
+		bestslidefrac -= 0x800;
+		if (bestslidefrac > 0)
+		{
+			newx = FixedMul(mmomx, bestslidefrac);
+			newy = FixedMul(mmomy, bestslidefrac);
 
-		if (!P_TryMove(mo, mo->x + newx, mo->y + newy, true))
-			goto bounceback;
-	}
+			if (!P_TryMove(mo, mo->x + newx, mo->y + newy, true))
+			{
+				if (P_MobjWasRemoved(mo))
+					return;
+				goto bounceback;
+			}
+		}
 
-	// Now continue along the wall.
-	// First calculate remainder.
-	bestslidefrac = FRACUNIT - bestslidefrac;
+		// Now continue along the wall.
+		// First calculate remainder.
+		bestslidefrac = FRACUNIT - bestslidefrac;
 
-	if (bestslidefrac > FRACUNIT)
-		bestslidefrac = FRACUNIT;
+		if (bestslidefrac > FRACUNIT)
+			bestslidefrac = FRACUNIT;
 
-	if (bestslidefrac <= 0)
-		return;
+		if (bestslidefrac <= 0)
+			return;
 
-	if (mo->type == MT_SHELL)
-	{
-		tmxmove = mmomx;
-		tmymove = mmomy;
-	}
-	else if (mo->type == MT_THROWNBOUNCE)
-	{
-		tmxmove = FixedMul(mmomx, (FRACUNIT - (FRACUNIT>>6) - (FRACUNIT>>5)));
-		tmymove = FixedMul(mmomy, (FRACUNIT - (FRACUNIT>>6) - (FRACUNIT>>5)));
-	}
-	else if (mo->type == MT_THROWNGRENADE || mo->type == MT_CYBRAKDEMON_NAPALM_BOMB_LARGE)
-	{
-		// Quickly decay speed as it bounces
-		tmxmove = FixedDiv(mmomx, 2*FRACUNIT);
-		tmymove = FixedDiv(mmomy, 2*FRACUNIT);
-	}
-	else
-	{
-		tmxmove = FixedMul(mmomx, (FRACUNIT - (FRACUNIT>>2) - (FRACUNIT>>3)));
-		tmymove = FixedMul(mmomy, (FRACUNIT - (FRACUNIT>>2) - (FRACUNIT>>3)));
-	}
+		if (mo->type == MT_SHELL)
+		{
+			tmxmove = mmomx;
+			tmymove = mmomy;
+		}
+		else if (mo->type == MT_THROWNBOUNCE)
+		{
+			tmxmove = FixedMul(mmomx, (FRACUNIT - (FRACUNIT>>6) - (FRACUNIT>>5)));
+			tmymove = FixedMul(mmomy, (FRACUNIT - (FRACUNIT>>6) - (FRACUNIT>>5)));
+		}
+		else if (mo->type == MT_THROWNGRENADE || mo->type == MT_CYBRAKDEMON_NAPALM_BOMB_LARGE)
+		{
+			// Quickly decay speed as it bounces
+			tmxmove = FixedDiv(mmomx, 2*FRACUNIT);
+			tmymove = FixedDiv(mmomy, 2*FRACUNIT);
+		}
+		else
+		{
+			tmxmove = FixedMul(mmomx, (FRACUNIT - (FRACUNIT>>2) - (FRACUNIT>>3)));
+			tmymove = FixedMul(mmomy, (FRACUNIT - (FRACUNIT>>2) - (FRACUNIT>>3)));
+		}
 
-	P_HitBounceLine(bestslideline); // clip the moves
+		P_HitBounceLine(bestslideline); // clip the moves
 
-	mo->momx = tmxmove;
-	mo->momy = tmymove;
+		mo->momx = tmxmove;
+		mo->momy = tmymove;
 
-	if (mo->player)
-	{
-		mo->player->cmomx = tmxmove;
-		mo->player->cmomy = tmymove;
+		if (mo->player)
+		{
+			mo->player->cmomx = tmxmove;
+			mo->player->cmomy = tmymove;
+		}
 	}
-
-	if (!P_TryMove(mo, mo->x + tmxmove, mo->y + tmymove, true))
-		goto retry;
+	while (!P_TryMove(mo, mo->x + tmxmove, mo->y + tmymove, true) && !P_MobjWasRemoved(mo));
 }
 
 //
@@ -4242,13 +4243,11 @@ void P_RadiusAttack(mobj_t *spot, mobj_t *source, fixed_t damagedist, UINT8 dama
 //  the way it was and call P_CheckSector (? was P_ChangeSector - Graue) again
 //  to undo the changes.
 //
-static boolean crushchange;
-static boolean nofit;
 
 //
 // PIT_ChangeSector
 //
-static boolean PIT_ChangeSector(mobj_t *thing, boolean realcrush)
+static boolean PIT_ChangeSector(mobj_t *thing, boolean realcrush, boolean crunch)
 {
 	mobj_t *killer = NULL;
 	//If a thing is both pushable and vulnerable, it doesn't block the crusher because it gets killed.
@@ -4272,11 +4271,7 @@ static boolean PIT_ChangeSector(mobj_t *thing, boolean realcrush)
 	if (thing->z + thing->height > thing->ceilingz && thing->z <= thing->ceilingz)
 	{
 		if (immunepushable && thing->z + thing->height > thing->subsector->sector->ceilingheight)
-		{
-			//Thing is a pushable and blocks the moving ceiling
-			nofit = true;
-			return false;
-		}
+			return false; //Thing is a pushable and blocks the moving ceiling
 
 		//Check FOFs in the sector
 		if (thing->subsector->sector->ffloors && (realcrush || immunepushable))
@@ -4288,47 +4283,54 @@ static boolean PIT_ChangeSector(mobj_t *thing, boolean realcrush)
 
 			for (rover = thing->subsector->sector->ffloors; rover; rover = rover->next)
 			{
-				if (!(((rover->fofflags & FOF_BLOCKPLAYER) && thing->player)
-				|| ((rover->fofflags & FOF_BLOCKOTHERS) && !thing->player)) || !(rover->fofflags & FOF_EXISTS))
+				thinker_t *think;
+
+				if (!(rover->fofflags & FOF_EXISTS))
+					continue;
+				if (thing->player && !(rover->fofflags & FOF_BLOCKPLAYER))
+					continue;
+				if (!thing->player && !(rover->fofflags & FOF_BLOCKOTHERS))
 					continue;
 
 				topheight = *rover->topheight;
 				bottomheight = *rover->bottomheight;
-				//topheight    = P_GetFFloorTopZAt   (rover, thing->x, thing->y);
-				//bottomheight = P_GetFFloorBottomZAt(rover, thing->x, thing->y);
+
+				if (bottomheight > thing->ceilingz)
+					continue;
 
 				delta1 = thing->z - (bottomheight + topheight)/2;
 				delta2 = thingtop - (bottomheight + topheight)/2;
-				if (bottomheight <= thing->ceilingz && abs(delta1) >= abs(delta2))
+				if (abs(delta1) < abs(delta2))
+					continue;
+
+				if (immunepushable)
+					return false; //FOF is blocked by pushable
+
+				if (!realcrush)
+					continue;
+
+				//If the thing was crushed by a crumbling FOF, reward the player who made it crumble!
+				for (think = thlist[THINK_MAIN].next; think != &thlist[THINK_MAIN]; think = think->next)
 				{
-					if (immunepushable)
-					{
-						//FOF is blocked by pushable
-						nofit = true;
-						return false;
-					}
-					else
-					{
-						//If the thing was crushed by a crumbling FOF, reward the player who made it crumble!
-						thinker_t *think;
-						crumble_t *crumbler;
+					crumble_t *crumbler;
 
-						for (think = thlist[THINK_MAIN].next; think != &thlist[THINK_MAIN]; think = think->next)
-						{
-							if (think->function.acp1 != (actionf_p1)T_StartCrumble)
-								continue;
-
-							crumbler = (crumble_t *)think;
-
-							if (crumbler->player && crumbler->player->mo
-								&& crumbler->player->mo != thing
-								&& crumbler->actionsector == thing->subsector->sector
-								&& crumbler->sector == rover->master->frontsector)
-							{
-								killer = crumbler->player->mo;
-							}
-						}
-					}
+					if (think->function.acp1 != (actionf_p1)T_StartCrumble)
+						continue;
+
+					crumbler = (crumble_t *)think;
+
+					if (!crumbler->player)
+						continue;
+					if (!crumbler->player->mo)
+						continue;
+					if (crumbler->player->mo == thing)
+						continue;
+					if (crumbler->actionsector != thing->subsector->sector)
+						continue;
+					if (crumbler->sector != rover->master->frontsector)
+						continue;
+
+					killer = crumbler->player->mo;
 				}
 			}
 		}
@@ -4344,121 +4346,68 @@ static boolean PIT_ChangeSector(mobj_t *thing, boolean realcrush)
 		}
 	}
 
-	if (realcrush && crushchange)
+	if (realcrush && crunch)
 		P_DamageMobj(thing, NULL, NULL, 1, 0);
 
 	// keep checking (crush other things)
 	return true;
 }
 
-//
-// P_CheckSector
-//
-boolean P_CheckSector(sector_t *sector, boolean crunch)
+static boolean P_CheckSectorPolyObjects(sector_t *sector, boolean realcrush, boolean crunch)
 {
-	msecnode_t *n;
 	size_t i;
 
-	nofit = false;
-	crushchange = crunch;
-
-	// killough 4/4/98: scan list front-to-back until empty or exhausted,
-	// restarting from beginning after each thing is processed. Avoids
-	// crashes, and is sure to examine all things in the sector, and only
-	// the things which are in the sector, until a steady-state is reached.
-	// Things can arbitrarily be inserted and removed and it won't mess up.
-	//
-	// killough 4/7/98: simplified to avoid using complicated counter
-
-
-	// First, let's see if anything will keep it from crushing.
-
 	// Sal: This stupid function chain is required to fix polyobjects not being able to crush.
 	// Monster Iestyn: don't use P_CheckSector actually just look for objects in the blockmap instead
 	validcount++;
 
 	for (i = 0; i < sector->linecount; i++)
 	{
-		if (sector->lines[i]->polyobj)
+		INT32 x, y;
+		polyobj_t *po = sector->lines[i]->polyobj;
+
+		if (!po)
+			continue;
+		if (po->validcount == validcount)
+			continue; // skip if already checked
+		if (!(po->flags & POF_SOLID))
+			continue;
+		if (po->lines[0]->backsector != sector) // Make sure you're currently checking the control sector
+			continue;
+
+		po->validcount = validcount;
+
+		for (y = po->blockbox[BOXBOTTOM]; y <= po->blockbox[BOXTOP]; ++y)
 		{
-			polyobj_t *po = sector->lines[i]->polyobj;
-			if (po->validcount == validcount)
-				continue; // skip if already checked
-			if (!(po->flags & POF_SOLID))
-				continue;
-			if (po->lines[0]->backsector == sector) // Make sure you're currently checking the control sector
+			for (x = po->blockbox[BOXLEFT]; x <= po->blockbox[BOXRIGHT]; ++x)
 			{
-				INT32 x, y;
-				po->validcount = validcount;
+				mobj_t *mo;
 
-				for (y = po->blockbox[BOXBOTTOM]; y <= po->blockbox[BOXTOP]; ++y)
-				{
-					for (x = po->blockbox[BOXLEFT]; x <= po->blockbox[BOXRIGHT]; ++x)
-					{
-						mobj_t *mo;
-
-						if (x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight)
-							continue;
+				if (x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight)
+					continue;
 
-						mo = blocklinks[y * bmapwidth + x];
+				mo = blocklinks[y * bmapwidth + x];
 
-						for (; mo; mo = mo->bnext)
-						{
-							// Monster Iestyn: do we need to check if a mobj has already been checked? ...probably not I suspect
+				for (; mo; mo = mo->bnext)
+				{
+					// Monster Iestyn: do we need to check if a mobj has already been checked? ...probably not I suspect
 
-							if (!P_MobjInsidePolyobj(po, mo))
-								continue;
+					if (!P_MobjInsidePolyobj(po, mo))
+						continue;
 
-							if (!PIT_ChangeSector(mo, false))
-							{
-								nofit = true;
-								return nofit;
-							}
-						}
-					}
+					if (!PIT_ChangeSector(mo, realcrush, crunch) && !realcrush)
+						return false;
 				}
 			}
 		}
 	}
 
-	if (sector->numattached)
-	{
-		sector_t *sec;
-		for (i = 0; i < sector->numattached; i++)
-		{
-			sec = &sectors[sector->attached[i]];
-			for (n = sec->touching_thinglist; n; n = n->m_thinglist_next)
-				n->visited = false;
-
-			sec->moved = true;
-
-			P_RecalcPrecipInSector(sec);
-
-			if (!sector->attachedsolid[i])
-				continue;
-
-			do
-			{
-				for (n = sec->touching_thinglist; n; n = n->m_thinglist_next)
-				if (!n->visited)
-				{
-					n->visited = true;
-					if (!(n->m_thing->flags & MF_NOBLOCKMAP))
-					{
-						if (!PIT_ChangeSector(n->m_thing, false))
-						{
-							nofit = true;
-							return nofit;
-						}
-					}
-					break;
-				}
-			} while (n);
-		}
-	}
+	return true;
+}
 
-	// Mark all things invalid
-	sector->moved = true;
+static boolean P_CheckTouchingThinglist(sector_t *sector, boolean realcrush, boolean crunch)
+{
+	msecnode_t *n;
 
 	for (n = sector->touching_thinglist; n; n = n->m_thinglist_next)
 		n->visited = false;
@@ -4466,122 +4415,86 @@ boolean P_CheckSector(sector_t *sector, boolean crunch)
 	do
 	{
 		for (n = sector->touching_thinglist; n; n = n->m_thinglist_next) // go through list
-			if (!n->visited) // unprocessed thing found
-			{
-				n->visited = true; // mark thing as processed
-				if (!(n->m_thing->flags & MF_NOBLOCKMAP)) //jff 4/7/98 don't do these
-				{
-					if (!PIT_ChangeSector(n->m_thing, false)) // process it
-					{
-						nofit = true;
-						return nofit;
-					}
-				}
-				break; // exit and start over
-			}
-	} while (n); // repeat from scratch until all things left are marked valid
-
-	// Nothing blocked us, so lets crush for real!
+		{
+			if (n->visited)
+				continue;
 
-	// Sal: This stupid function chain is required to fix polyobjects not being able to crush.
-	// Monster Iestyn: don't use P_CheckSector actually just look for objects in the blockmap instead
-	validcount++;
+			n->visited = true; // mark thing as processed
 
-	for (i = 0; i < sector->linecount; i++)
-	{
-		if (sector->lines[i]->polyobj)
-		{
-			polyobj_t *po = sector->lines[i]->polyobj;
-			if (po->validcount == validcount)
-				continue; // skip if already checked
-			if (!(po->flags & POF_SOLID))
+			if (n->m_thing->flags & MF_NOBLOCKMAP) //jff 4/7/98 don't do these
 				continue;
-			if (po->lines[0]->backsector == sector) // Make sure you're currently checking the control sector
-			{
-				INT32 x, y;
-				po->validcount = validcount;
 
-				for (y = po->blockbox[BOXBOTTOM]; y <= po->blockbox[BOXTOP]; ++y)
-				{
-					for (x = po->blockbox[BOXLEFT]; x <= po->blockbox[BOXRIGHT]; ++x)
-					{
-						mobj_t *mo;
+			if (!PIT_ChangeSector(n->m_thing, realcrush, crunch) && !realcrush) // process it
+				return false;
 
-						if (x < 0 || y < 0 || x >= bmapwidth || y >= bmapheight)
-							continue;
+			break; // exit and start over
+		}
+	} while (n); // repeat from scratch until all things left are marked valid
 
-						mo = blocklinks[y * bmapwidth + x];
+	return true;
+}
 
-						for (; mo; mo = mo->bnext)
-						{
-							// Monster Iestyn: do we need to check if a mobj has already been checked? ...probably not I suspect
+static boolean P_CheckSectorFFloors(sector_t *sector, boolean realcrush, boolean crunch)
+{
+	sector_t *sec;
+	size_t i;
 
-							if (!P_MobjInsidePolyobj(po, mo))
-								continue;
+	if (!sector->numattached)
+		return true;
 
-							PIT_ChangeSector(mo, true);
-							return nofit;
-						}
-					}
-				}
-			}
-		}
-	}
-	if (sector->numattached)
+	for (i = 0; i < sector->numattached; i++)
 	{
-		sector_t *sec;
-		for (i = 0; i < sector->numattached; i++)
-		{
-			sec = &sectors[sector->attached[i]];
-			for (n = sec->touching_thinglist; n; n = n->m_thinglist_next)
-				n->visited = false;
+		sec = &sectors[sector->attached[i]];
 
-			sec->moved = true;
+		sec->moved = true;
 
-			P_RecalcPrecipInSector(sec);
+		P_RecalcPrecipInSector(sec);
 
-			if (!sector->attachedsolid[i])
-				continue;
+		if (!sector->attachedsolid[i])
+			continue;
 
-			do
-			{
-				for (n = sec->touching_thinglist; n; n = n->m_thinglist_next)
-				if (!n->visited)
-				{
-					n->visited = true;
-					if (!(n->m_thing->flags & MF_NOBLOCKMAP))
-					{
-						PIT_ChangeSector(n->m_thing, true);
-						return nofit;
-					}
-					break;
-				}
-			} while (n);
-		}
+		if (!P_CheckTouchingThinglist(sec, realcrush, crunch))
+			return false;
 	}
 
+	return true;
+}
+
+static boolean P_CheckSectorHelper(sector_t *sector, boolean realcrush, boolean crunch)
+{
+	if (!P_CheckSectorPolyObjects(sector, realcrush, crunch))
+		return false;
+
+	if (!P_CheckSectorFFloors(sector, realcrush, crunch))
+		return false;
+
 	// Mark all things invalid
 	sector->moved = true;
 
-	for (n = sector->touching_thinglist; n; n = n->m_thinglist_next)
-		n->visited = false;
+	return P_CheckTouchingThinglist(sector, realcrush, crunch);
+}
 
-	do
-	{
-		for (n = sector->touching_thinglist; n; n = n->m_thinglist_next) // go through list
-			if (!n->visited) // unprocessed thing found
-			{
-				n->visited = true; // mark thing as processed
-				if (!(n->m_thing->flags & MF_NOBLOCKMAP)) //jff 4/7/98 don't do these
-				{
-					PIT_ChangeSector(n->m_thing, true); // process it
-					return nofit;
-				}
-				break; // exit and start over
-			}
-	} while (n); // repeat from scratch until all things left are marked valid
+//
+// P_CheckSector
+//
+boolean P_CheckSector(sector_t *sector, boolean crunch)
+{
+	// killough 4/4/98: scan list front-to-back until empty or exhausted,
+	// restarting from beginning after each thing is processed. Avoids
+	// crashes, and is sure to examine all things in the sector, and only
+	// the things which are in the sector, until a steady-state is reached.
+	// Things can arbitrarily be inserted and removed and it won't mess up.
+	//
+	// killough 4/7/98: simplified to avoid using complicated counter
 
-	return nofit;
+	// First, let's see if anything will keep it from crushing.
+	if (!P_CheckSectorHelper(sector, false, crunch))
+		return true;
+
+	// Nothing blocked us, so lets crush for real!
+	P_CheckSectorHelper(sector, true, crunch);
+
+	return false;
 }
 
 /*
diff --git a/src/p_maputl.c b/src/p_maputl.c
index b6a3207308f3c5c7af8b434e50c2920d09b8f859..e36d5fd724fd152346b34b8ec047277e25735ecb 100644
--- a/src/p_maputl.c
+++ b/src/p_maputl.c
@@ -509,26 +509,26 @@ void P_LineOpening(line_t *linedef, mobj_t *mobj)
 				// on non-solid polyobjects should NEVER happen in the future
 				if (linedef->polyobj && (linedef->polyobj->flags & POF_TESTHEIGHT)) {
 					if (linedef->flags & ML_WRAPMIDTEX && !side->repeatcnt) { // "infinite" repeat
-						texbottom = back->floorheight + side->rowoffset;
-						textop = back->ceilingheight + side->rowoffset;
+						texbottom = back->floorheight + side->rowoffset + side->offsety_mid;
+						textop = back->ceilingheight + side->rowoffset + side->offsety_mid;
 					} else if (linedef->flags & ML_MIDTEX) {
-						texbottom = back->floorheight + side->rowoffset;
+						texbottom = back->floorheight + side->rowoffset + side->offsety_mid;
 						textop = texbottom + texheight*(side->repeatcnt+1);
 					} else {
-						textop = back->ceilingheight + side->rowoffset;
+						textop = back->ceilingheight + side->rowoffset + side->offsety_mid;
 						texbottom = textop - texheight*(side->repeatcnt+1);
 					}
 				} else
 #endif
 				{
 					if (linedef->flags & ML_WRAPMIDTEX && !side->repeatcnt) { // "infinite" repeat
-						texbottom = openbottom + side->rowoffset;
-						textop = opentop + side->rowoffset;
+						texbottom = openbottom + side->rowoffset + side->offsety_mid;
+						textop = opentop + side->rowoffset + side->offsety_mid;
 					} else if (linedef->flags & ML_MIDPEG) {
-						texbottom = openbottom + side->rowoffset;
+						texbottom = openbottom + side->rowoffset + side->offsety_mid;
 						textop = texbottom + texheight*(side->repeatcnt+1);
 					} else {
-						textop = opentop + side->rowoffset;
+						textop = opentop + side->rowoffset + side->offsety_mid;
 						texbottom = textop - texheight*(side->repeatcnt+1);
 					}
 				}
diff --git a/src/p_mobj.c b/src/p_mobj.c
index eeaf547769fdbbae34a11cee3afac385addd690c..a81845918b64f5ceb74f41f303c43da579bafc55 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);
@@ -1551,6 +1552,31 @@ void P_CheckGravity(mobj_t *mo, boolean affect)
 	}
 }
 
+//
+// P_SetPitchRollFromSlope
+//
+void P_SetPitchRollFromSlope(mobj_t *mo, pslope_t *slope)
+{
+#if 0
+	if (slope)
+	{
+		fixed_t tempz = slope->normal.z;
+		fixed_t tempy = slope->normal.y;
+		fixed_t tempx = slope->normal.x;
+
+		mo->pitch = R_PointToAngle2(0, 0, FixedSqrt(FixedMul(tempy, tempy) + FixedMul(tempz, tempz)), tempx);
+		mo->roll = R_PointToAngle2(0, 0, tempz, tempy);
+	}
+	else
+	{
+		mo->pitch = mo->roll = 0;
+	}
+#else
+	(void)mo;
+	(void)slope;
+#endif
+}
+
 #define STOPSPEED (FRACUNIT)
 
 //
@@ -1985,6 +2011,7 @@ void P_XYMovement(mobj_t *mo)
 			// Now compare the Zs of the different quantizations
 			if (oldangle-newangle > ANG30 && oldangle-newangle < ANGLE_180) { // Allow for a bit of sticking - this value can be adjusted later
 				mo->standingslope = oldslope;
+				P_SetPitchRollFromSlope(mo, mo->standingslope);
 				P_SlopeLaunch(mo);
 
 				//CONS_Printf("launched off of slope - ");
@@ -2557,6 +2584,7 @@ boolean P_ZMovement(mobj_t *mo)
 		if (((mo->eflags & MFE_VERTICALFLIP) ? tmceilingslope : tmfloorslope) && (mo->type != MT_STEAM))
 		{
 			mo->standingslope = (mo->eflags & MFE_VERTICALFLIP) ? tmceilingslope : tmfloorslope;
+			P_SetPitchRollFromSlope(mo, mo->standingslope);
 			P_ReverseQuantizeMomentumToSlope(&mom, mo->standingslope);
 		}
 
@@ -3139,7 +3167,8 @@ boolean P_SceneryZMovement(mobj_t *mo)
 
 	if (P_CheckDeathPitCollide(mo))
 	{
-		P_RemoveMobj(mo);
+		if (mo->type != MT_GHOST)  // ghosts play death animations instead, so don't remove them
+			P_RemoveMobj(mo);
 		return false;
 	}
 
@@ -3660,7 +3689,7 @@ boolean P_CameraThinker(player_t *player, camera_t *thiscam, boolean resetcalled
 			dummy.y = thiscam->y;
 			dummy.z = thiscam->z;
 			dummy.height = thiscam->height;
-			if (!resetcalled && !(player->pflags & PF_NOCLIP) && !P_CheckSight(&dummy, player->mo)) // TODO: "P_CheckCameraSight" instead.
+			if (!resetcalled && !(player->pflags & PF_NOCLIP || player->powers[pw_carry] == CR_NIGHTSMODE) && !P_CheckSight(&dummy, player->mo)) // TODO: "P_CheckCameraSight" instead.
 				P_ResetCamera(player, thiscam);
 			else
 			{
@@ -3691,7 +3720,7 @@ boolean P_CameraThinker(player_t *player, camera_t *thiscam, boolean resetcalled
 		// adjust height
 		thiscam->z += thiscam->momz + player->mo->pmomz;
 
-		if (!itsatwodlevel && !(player->pflags & PF_NOCLIP))
+		if (!itsatwodlevel && !(player->pflags & PF_NOCLIP || player->powers[pw_carry] == CR_NIGHTSMODE))
 		{
 			// clip movement
 			if (thiscam->z <= thiscam->floorz) // hit the floor
@@ -7923,11 +7952,6 @@ static void P_MobjSceneryThink(mobj_t *mobj)
 			return;
 		}
 
-		if (!camera.chase)
-			mobj->flags2 |= MF2_DONTDRAW;
-		else
-			mobj->flags2 &= ~MF2_DONTDRAW;
-
 		P_UnsetThingPosition(mobj);
 		{
 			fixed_t radius = FixedMul(10*mobj->info->speed, mobj->target->scale);
@@ -9204,7 +9228,7 @@ static void P_DragonbomberThink(mobj_t *mobj)
 		else
 		{
 			fixed_t vspeed = FixedMul(mobj->info->speed >> 3, mobj->scale);
-			fixed_t z = mobj->target->z + (mobj->height >> 1) + (mobj->flags & MFE_VERTICALFLIP ? -128*mobj->scale : 128*mobj->scale + mobj->target->height);
+			fixed_t z = mobj->target->z + (mobj->height >> 1) + (mobj->eflags & MFE_VERTICALFLIP ? -128*mobj->scale : (128*mobj->scale + mobj->target->height));
 			angle_t diff = R_PointToAngle2(mobj->x, mobj->y, mobj->target->x, mobj->target->y) - mobj->angle;
 			if (diff > ANGLE_180)
 				mobj->angle -= DRAGONTURNSPEED;
@@ -9602,7 +9626,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
 	case MT_BOSSFLYPOINT:
 		return false;
 	case MT_NIGHTSCORE:
-		mobj->color = (UINT16)(leveltime % SKINCOLOR_WHITE);
+		mobj->color = linkColor[mobj->extravalue2][(leveltime + mobj->extravalue1) % NUMLINKCOLORS];
 		break;
 	case MT_JETFUME1:
 		if (!P_JetFume1Think(mobj))
@@ -9716,6 +9740,11 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
 			A_AttractChase(mobj);
 		break;
 	case MT_EMBLEM:
+		if (P_EmblemWasCollected(mobj->health - 1) || !P_CanPickupEmblem(&players[consoleplayer], mobj->health - 1))
+			mobj->frame |= (tr_trans50 << FF_TRANSSHIFT);
+		else
+			mobj->frame &= ~FF_TRANSMASK;
+
 		if (mobj->flags2 & MF2_NIGHTSPULL)
 			P_NightsItemChase(mobj);
 		break;
@@ -9840,9 +9869,9 @@ static boolean P_MobjRegularThink(mobj_t *mobj)
 		break;
 	case MT_MINUS:
 		if (P_IsObjectOnGround(mobj))
-			mobj->rollangle = 0;
+			mobj->spriteroll = 0;
 		else
-			mobj->rollangle = R_PointToAngle2(0, 0, P_MobjFlip(mobj)*mobj->momz, (mobj->scale << 1) - min(abs(mobj->momz), mobj->scale << 1));
+			mobj->spriteroll = R_PointToAngle2(0, 0, P_MobjFlip(mobj)*mobj->momz, (mobj->scale << 1) - min(abs(mobj->momz), mobj->scale << 1));
 		break;
 	case MT_PUSH:
 		P_PointPushThink(mobj);
@@ -10535,6 +10564,29 @@ static fixed_t P_DefaultMobjShadowScale (mobj_t *thing)
 		case MT_REDFLAG:
 		case MT_BLUEFLAG:
 
+		case MT_BOUNCERING:
+		case MT_AUTOMATICRING:
+		case MT_INFINITYRING:
+		case MT_RAILRING:
+		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:
+		case MT_THROWNAUTOMATIC:
+		case MT_THROWNSCATTER:
+		case MT_THROWNEXPLOSION:
+		case MT_THROWNGRENADE:
+
 		case MT_EMBLEM:
 
 		case MT_TOKEN:
@@ -11152,12 +11204,6 @@ void P_RemoveMobj(mobj_t *mobj)
 
 	P_SetTarget(&mobj->hnext, P_SetTarget(&mobj->hprev, NULL));
 
-	// DBG: set everything in mobj_t to 0xFF instead of leaving it. debug memory error.
-#ifdef SCRAMBLE_REMOVED
-	// Invalidate mobj_t data to cause crashes if accessed!
-	memset((UINT8 *)mobj + sizeof(thinker_t), 0xff, sizeof(mobj_t) - sizeof(thinker_t));
-#endif
-
 	R_RemoveMobjInterpolator(mobj);
 
 	// free block
@@ -11176,6 +11222,17 @@ void P_RemoveMobj(mobj_t *mobj)
 	}
 
 	P_RemoveThinker((thinker_t *)mobj);
+
+#ifdef PARANOIA
+	// Saved to avoid being scrambled like below...
+	mobj->thinker.debug_mobjtype = mobj->type;
+#endif
+
+	// DBG: set everything in mobj_t to 0xFF instead of leaving it. debug memory error.
+#ifdef SCRAMBLE_REMOVED
+	// Invalidate mobj_t data to cause crashes if accessed!
+	memset((UINT8 *)mobj + sizeof(thinker_t), 0xff, sizeof(mobj_t) - sizeof(thinker_t));
+#endif
 }
 
 // This does not need to be added to Lua.
@@ -11530,8 +11587,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;
@@ -11551,13 +11606,6 @@ 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))
@@ -11569,7 +11617,7 @@ void P_SpawnPlayer(INT32 playernum)
 	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),
@@ -11807,7 +11855,7 @@ void P_MovePlayerToStarpost(INT32 playernum)
 mapthing_t *huntemeralds[MAXHUNTEMERALDS];
 INT32 numhuntemeralds;
 
-fixed_t P_GetMobjSpawnHeight(const mobjtype_t mobjtype, const fixed_t x, const fixed_t y, const fixed_t dz, const fixed_t offset, const boolean flip, const fixed_t scale)
+fixed_t P_GetMobjSpawnHeight(const mobjtype_t mobjtype, const fixed_t x, const fixed_t y, const fixed_t dz, const fixed_t offset, const boolean flip, const fixed_t scale, const boolean absolutez)
 {
 	const subsector_t *ss = R_PointInSubsector(x, y);
 
@@ -11817,9 +11865,9 @@ fixed_t P_GetMobjSpawnHeight(const mobjtype_t mobjtype, const fixed_t x, const f
 
 	// Establish height.
 	if (flip)
-		return P_GetSectorCeilingZAt(ss->sector, x, y) - dz - FixedMul(scale, offset + mobjinfo[mobjtype].height);
+		return (absolutez ? dz : P_GetSectorCeilingZAt(ss->sector, x, y) - dz) - FixedMul(scale, offset + mobjinfo[mobjtype].height);
 	else
-		return P_GetSectorFloorZAt(ss->sector, x, y) + dz + FixedMul(scale, offset);
+		return (absolutez ? dz : P_GetSectorFloorZAt(ss->sector, x, y) + dz) + FixedMul(scale, offset);
 }
 
 fixed_t P_GetMapThingSpawnHeight(const mobjtype_t mobjtype, const mapthing_t* mthing, const fixed_t x, const fixed_t y)
@@ -11827,6 +11875,7 @@ fixed_t P_GetMapThingSpawnHeight(const mobjtype_t mobjtype, const mapthing_t* mt
 	fixed_t dz = mthing->z << FRACBITS; // Base offset from the floor.
 	fixed_t offset = 0; // Specific scaling object offset.
 	boolean flip = (!!(mobjinfo[mobjtype].flags & MF_SPAWNCEILING) ^ !!(mthing->options & MTF_OBJECTFLIP));
+	boolean absolutez = !!(mthing->options & MTF_ABSOLUTEZ);
 
 	switch (mobjtype)
 	{
@@ -11882,7 +11931,7 @@ fixed_t P_GetMapThingSpawnHeight(const mobjtype_t mobjtype, const mapthing_t* mt
 			offset += mthing->args[0] ? 0 : 24*FRACUNIT;
 	}
 
-	if (!(dz + offset)) // Snap to the surfaces when there's no offset set.
+	if (!(dz + offset) && !absolutez) // Snap to the surfaces when there's no offset set.
 	{
 		if (flip)
 			return ONCEILINGZ;
@@ -11890,7 +11939,7 @@ fixed_t P_GetMapThingSpawnHeight(const mobjtype_t mobjtype, const mapthing_t* mt
 			return ONFLOORZ;
 	}
 
-	return P_GetMobjSpawnHeight(mobjtype, x, y, dz, offset, flip, mthing->scale);
+	return P_GetMobjSpawnHeight(mobjtype, x, y, dz, offset, flip, mthing->scale, absolutez);
 }
 
 static boolean P_SpawnNonMobjMapThing(mapthing_t *mthing)
@@ -12005,8 +12054,8 @@ static boolean P_AllowMobjSpawn(mapthing_t* mthing, mobjtype_t i)
 
 		break;
 	case MT_EMBLEM:
-		if (netgame || multiplayer)
-			return false; // Single player (You're next on my shit list)
+		if (!G_CoopGametype())
+			return false; // Gametype's not right
 		break;
 	default:
 		break;
@@ -12150,7 +12199,6 @@ static boolean P_SetupEmblem(mapthing_t *mthing, mobj_t *mobj)
 	INT32 j;
 	emblem_t* emblem = M_GetLevelEmblems(gamemap);
 	skincolornum_t emcolor;
-	boolean validEmblem = true;
 
 	while (emblem)
 	{
@@ -12175,42 +12223,19 @@ static boolean P_SetupEmblem(mapthing_t *mthing, mobj_t *mobj)
 	emcolor = M_GetEmblemColor(&emblemlocations[j]); // workaround for compiler complaint about bad function casting
 	mobj->color = (UINT16)emcolor;
 
-	validEmblem = !emblemlocations[j].collected;
+	mobj->frame &= ~FF_TRANSMASK;
 
-	if (emblemlocations[j].type == ET_SKIN)
+	if (emblemlocations[j].type == ET_GLOBAL)
 	{
-		INT32 skinnum = M_EmblemSkinNum(&emblemlocations[j]);
-
-		if (players[0].skin != skinnum)
+		mobj->reactiontime = emblemlocations[j].var;
+		if (emblemlocations[j].var & GE_NIGHTSITEM)
 		{
-			validEmblem = false;
+			mobj->flags |= MF_NIGHTSITEM;
+			mobj->flags &= ~MF_SPECIAL;
+			mobj->flags2 |= MF2_DONTDRAW;
 		}
 	}
 
-	if (validEmblem == false)
-	{
-		P_UnsetThingPosition(mobj);
-		mobj->flags |= MF_NOCLIP;
-		mobj->flags &= ~MF_SPECIAL;
-		mobj->flags |= MF_NOBLOCKMAP;
-		mobj->frame |= (tr_trans50 << FF_TRANSSHIFT);
-		P_SetThingPosition(mobj);
-	}
-	else
-	{
-		mobj->frame &= ~FF_TRANSMASK;
-
-		if (emblemlocations[j].type == ET_GLOBAL)
-		{
-			mobj->reactiontime = emblemlocations[j].var;
-			if (emblemlocations[j].var & GE_NIGHTSITEM)
-			{
-				mobj->flags |= MF_NIGHTSITEM;
-				mobj->flags &= ~MF_SPECIAL;
-				mobj->flags2 |= MF2_DONTDRAW;
-			}
-		}
-	}
 	return true;
 }
 
@@ -12583,7 +12608,7 @@ static boolean P_SetupNiGHTSDrone(mapthing_t *mthing, mobj_t *mobj)
 	dronemangoaldiff = max(mobjinfo[MT_NIGHTSDRONE_MAN].height - mobjinfo[MT_NIGHTSDRONE_GOAL].height, 0);
 
 	if (flip && mobj->height != oldheight)
-		P_MoveOrigin(mobj, mobj->x, mobj->y, mobj->z - (mobj->height - oldheight));
+		P_SetOrigin(mobj, mobj->x, mobj->y, mobj->z - (mobj->height - oldheight));
 
 	if (!flip)
 	{
@@ -13322,6 +13347,9 @@ static mobj_t *P_SpawnMobjFromMapThing(mapthing_t *mthing, fixed_t x, fixed_t y,
 	P_SetScale(mobj, FixedMul(mobj->scale, mthing->scale));
 	mobj->destscale = FixedMul(mobj->destscale, mthing->scale);
 
+	mobj->spritexscale = mthing->spritexscale;
+	mobj->spriteyscale = mthing->spriteyscale;
+
 	if (!P_SetupSpawnedMapThing(mthing, mobj, &doangle))
 		return mobj;
 
@@ -13408,7 +13436,7 @@ void P_SpawnHoop(mapthing_t *mthing)
 	vector4_t v, res;
 	fixed_t x = mthing->x << FRACBITS;
 	fixed_t y = mthing->y << FRACBITS;
-	fixed_t z = P_GetMobjSpawnHeight(MT_HOOP, x, y, mthing->z << FRACBITS, 0, false, mthing->scale);
+	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);
 	hoopcenter->spawnpoint = mthing;
@@ -13535,7 +13563,7 @@ static void P_SpawnItemRow(mapthing_t *mthing, mobjtype_t *itemtypes, UINT8 numi
 			itemtypes[r] = P_GetMobjtypeSubstitute(&dummything, itemtypes[r]);
 		}
 	}
-	z = P_GetMobjSpawnHeight(itemtypes[0], x, y, z, 0, mthing->options & MTF_OBJECTFLIP, mthing->scale);
+	z = P_GetMobjSpawnHeight(itemtypes[0], x, y, z, 0, mthing->options & MTF_OBJECTFLIP, mthing->scale, mthing->options & MTF_ABSOLUTEZ);
 
 	for (r = 0; r < numitems; r++)
 	{
@@ -13594,7 +13622,7 @@ static void P_SpawnItemCircle(mapthing_t *mthing, mobjtype_t *itemtypes, UINT8 n
 			itemtypes[i] = P_GetMobjtypeSubstitute(&dummything, itemtypes[i]);
 		}
 	}
-	z = P_GetMobjSpawnHeight(itemtypes[0], x, y, z, 0, false, mthing->scale);
+	z = P_GetMobjSpawnHeight(itemtypes[0], x, y, z, 0, false, mthing->scale, mthing->options & MTF_ABSOLUTEZ);
 
 	for (i = 0; i < numitems; i++)
 	{
@@ -13706,7 +13734,6 @@ void P_SpawnItemPattern(mapthing_t *mthing, boolean bonustime)
 		UINT8 numitemtypes;
 		if (!udmf)
 			return;
-		CONS_Printf("Itemstring: %s\n", mthing->stringargs[0]);
 		P_ParseItemTypes(mthing->stringargs[0], itemtypes, &numitemtypes);
 		P_SpawnItemCircle(mthing, itemtypes, numitemtypes, mthing->args[0], mthing->args[1] << FRACBITS, bonustime);
 		return;
@@ -14164,6 +14191,13 @@ mobj_t *P_SpawnMobjFromMobj(mobj_t *mobj, fixed_t xofs, fixed_t yofs, fixed_t zo
 		newmobj->old_angle = mobj->old_angle;
 	}
 
+	newmobj->old_pitch2 = mobj->old_pitch2;
+	newmobj->old_pitch = mobj->old_pitch;
+	newmobj->old_roll2 = mobj->old_roll2;
+	newmobj->old_roll = mobj->old_roll;
+	newmobj->old_spriteroll2 = mobj->old_spriteroll2;
+	newmobj->old_spriteroll = mobj->old_spriteroll;
+
 	newmobj->old_scale2 = mobj->old_scale2;
 	newmobj->old_scale = mobj->old_scale;
 	newmobj->old_spritexscale = mobj->old_spritexscale;
diff --git a/src/p_mobj.h b/src/p_mobj.h
index 6717c4add0ab016d32821e3e6b1de7c44c06d205..a980691beb8b296b4fd16415141e4f9e5f484af8 100644
--- a/src/p_mobj.h
+++ b/src/p_mobj.h
@@ -122,7 +122,7 @@ typedef enum
 	MF_AMBIENT          = 1<<10,
 	// Slide this object when it hits a wall.
 	MF_SLIDEME          = 1<<11,
-	// Player cheat.
+	// Don't collide with walls or solid objects. Two MF_NOCLIP objects can't touch each other at all!
 	MF_NOCLIP           = 1<<12,
 	// Allow moves to any height, no gravity. For active floaters.
 	MF_FLOAT            = 1<<13,
@@ -292,7 +292,7 @@ typedef struct mobj_s
 	angle_t angle, pitch, roll; // orientation
 	angle_t old_angle, old_pitch, old_roll; // orientation interpolation
 	angle_t old_angle2, old_pitch2, old_roll2;
-	angle_t rollangle;
+	angle_t spriteroll, old_spriteroll, old_spriteroll2;
 	spritenum_t sprite; // used to find patch_t and flip value
 	UINT32 frame; // frame number, plus bits see p_pspr.h
 	UINT8 sprite2; // player sprites
@@ -334,6 +334,8 @@ typedef struct mobj_s
 	// Player and mobj sprites in multiplayer modes are modified
 	//  using an internal color lookup table for re-indexing.
 	UINT16 color; // This replaces MF_TRANSLATION. Use 0 for default (no translation).
+	struct player_s *drawonlyforplayer; // If set, hides the mobj for everyone except this player and their spectators
+	struct mobj_s *dontdrawforviewmobj; // If set, hides the mobj if dontdrawforviewmobj is the current camera (first-person player or awayviewmobj)
 
 	// Interaction info, by BLOCKMAP.
 	// Links in blocks (if needed).
@@ -429,7 +431,7 @@ typedef struct precipmobj_s
 	angle_t angle, pitch, roll; // orientation
 	angle_t old_angle, old_pitch, old_roll; // orientation interpolation
 	angle_t old_angle2, old_pitch2, old_roll2;
-	angle_t rollangle;
+	angle_t spriteroll, old_spriteroll, old_spriteroll2;
 	spritenum_t sprite; // used to find patch_t and flip value
 	UINT32 frame; // frame number, plus bits see p_pspr.h
 	UINT8 sprite2; // player sprites
@@ -489,7 +491,7 @@ void P_MovePlayerToSpawn(INT32 playernum, mapthing_t *mthing);
 void P_MovePlayerToStarpost(INT32 playernum);
 void P_AfterPlayerSpawn(INT32 playernum);
 
-fixed_t P_GetMobjSpawnHeight(const mobjtype_t mobjtype, const fixed_t x, const fixed_t y, const fixed_t dz, const fixed_t offset, const boolean flip, const fixed_t scale);
+fixed_t P_GetMobjSpawnHeight(const mobjtype_t mobjtype, const fixed_t x, const fixed_t y, const fixed_t dz, const fixed_t offset, const boolean flip, const fixed_t scale, const boolean absolutez);
 fixed_t P_GetMapThingSpawnHeight(const mobjtype_t mobjtype, const mapthing_t* mthing, const fixed_t x, const fixed_t y);
 
 mobj_t *P_SpawnMapThing(mapthing_t *mthing);
diff --git a/src/p_saveg.c b/src/p_saveg.c
index 8c8a7832252a7531641051812a865c80308d2915..faecd13770b3d81b992017af51b4b663487aa50d 100644
--- a/src/p_saveg.c
+++ b/src/p_saveg.c
@@ -47,6 +47,7 @@ UINT8 *save_p;
 #define ARCHIVEBLOCK_POBJS    0x7F928546
 #define ARCHIVEBLOCK_THINKERS 0x7F37037C
 #define ARCHIVEBLOCK_SPECIALS 0x7F228378
+#define ARCHIVEBLOCK_EMBLEMS  0x7F4A5445
 
 // Note: This cannot be bigger
 // than an UINT16
@@ -168,6 +169,7 @@ static void P_NetArchivePlayers(void)
 		WRITEUINT8(save_p, players[i].playerstate);
 		WRITEUINT32(save_p, players[i].pflags);
 		WRITEUINT8(save_p, players[i].panim);
+		WRITEUINT8(save_p, players[i].stronganim);
 		WRITEUINT8(save_p, players[i].spectator);
 
 		WRITEUINT16(save_p, players[i].flashpal);
@@ -177,6 +179,7 @@ static void P_NetArchivePlayers(void)
 		WRITEINT32(save_p, players[i].skin);
 		WRITEUINT32(save_p, players[i].availabilities);
 		WRITEUINT32(save_p, players[i].score);
+		WRITEUINT32(save_p, players[i].recordscore);
 		WRITEFIXED(save_p, players[i].dashspeed);
 		WRITESINT8(save_p, players[i].lives);
 		WRITESINT8(save_p, players[i].continues);
@@ -395,6 +398,7 @@ static void P_NetUnArchivePlayers(void)
 		players[i].playerstate = READUINT8(save_p);
 		players[i].pflags = READUINT32(save_p);
 		players[i].panim = READUINT8(save_p);
+		players[i].stronganim = READUINT8(save_p);
 		players[i].spectator = READUINT8(save_p);
 
 		players[i].flashpal = READUINT16(save_p);
@@ -404,6 +408,7 @@ static void P_NetUnArchivePlayers(void)
 		players[i].skin = READINT32(save_p);
 		players[i].availabilities = READUINT32(save_p);
 		players[i].score = READUINT32(save_p);
+		players[i].recordscore = READUINT32(save_p);
 		players[i].dashspeed = READFIXED(save_p); // dashing speed
 		players[i].lives = READSINT8(save_p);
 		players[i].continues = READSINT8(save_p); // continues that player has acquired
@@ -1022,17 +1027,17 @@ static void ArchiveSectors(void)
 		if (ss->special != spawnss->special)
 			diff |= SD_SPECIAL;
 
-		if (ss->floor_xoffs != spawnss->floor_xoffs)
+		if (ss->floorxoffset != spawnss->floorxoffset)
 			diff2 |= SD_FXOFFS;
-		if (ss->floor_yoffs != spawnss->floor_yoffs)
+		if (ss->flooryoffset != spawnss->flooryoffset)
 			diff2 |= SD_FYOFFS;
-		if (ss->ceiling_xoffs != spawnss->ceiling_xoffs)
+		if (ss->ceilingxoffset != spawnss->ceilingxoffset)
 			diff2 |= SD_CXOFFS;
-		if (ss->ceiling_yoffs != spawnss->ceiling_yoffs)
+		if (ss->ceilingyoffset != spawnss->ceilingyoffset)
 			diff2 |= SD_CYOFFS;
-		if (ss->floorpic_angle != spawnss->floorpic_angle)
+		if (ss->floorangle != spawnss->floorangle)
 			diff2 |= SD_FLOORANG;
-		if (ss->ceilingpic_angle != spawnss->ceilingpic_angle)
+		if (ss->ceilingangle != spawnss->ceilingangle)
 			diff2 |= SD_CEILANG;
 
 		if (!Tag_Compare(&ss->tags, &spawnss->tags))
@@ -1095,17 +1100,17 @@ static void ArchiveSectors(void)
 			if (diff & SD_SPECIAL)
 				WRITEINT16(save_p, ss->special);
 			if (diff2 & SD_FXOFFS)
-				WRITEFIXED(save_p, ss->floor_xoffs);
+				WRITEFIXED(save_p, ss->floorxoffset);
 			if (diff2 & SD_FYOFFS)
-				WRITEFIXED(save_p, ss->floor_yoffs);
+				WRITEFIXED(save_p, ss->flooryoffset);
 			if (diff2 & SD_CXOFFS)
-				WRITEFIXED(save_p, ss->ceiling_xoffs);
+				WRITEFIXED(save_p, ss->ceilingxoffset);
 			if (diff2 & SD_CYOFFS)
-				WRITEFIXED(save_p, ss->ceiling_yoffs);
+				WRITEFIXED(save_p, ss->ceilingyoffset);
 			if (diff2 & SD_FLOORANG)
-				WRITEANGLE(save_p, ss->floorpic_angle);
+				WRITEANGLE(save_p, ss->floorangle);
 			if (diff2 & SD_CEILANG)
-				WRITEANGLE(save_p, ss->ceilingpic_angle);
+				WRITEANGLE(save_p, ss->ceilingangle);
 			if (diff2 & SD_TAG)
 			{
 				WRITEUINT32(save_p, ss->tags.count);
@@ -1196,17 +1201,17 @@ static void UnArchiveSectors(void)
 			sectors[i].special = READINT16(save_p);
 
 		if (diff2 & SD_FXOFFS)
-			sectors[i].floor_xoffs = READFIXED(save_p);
+			sectors[i].floorxoffset = READFIXED(save_p);
 		if (diff2 & SD_FYOFFS)
-			sectors[i].floor_yoffs = READFIXED(save_p);
+			sectors[i].flooryoffset = READFIXED(save_p);
 		if (diff2 & SD_CXOFFS)
-			sectors[i].ceiling_xoffs = READFIXED(save_p);
+			sectors[i].ceilingxoffset = READFIXED(save_p);
 		if (diff2 & SD_CYOFFS)
-			sectors[i].ceiling_yoffs = READFIXED(save_p);
+			sectors[i].ceilingyoffset = READFIXED(save_p);
 		if (diff2 & SD_FLOORANG)
-			sectors[i].floorpic_angle  = READANGLE(save_p);
+			sectors[i].floorangle  = READANGLE(save_p);
 		if (diff2 & SD_CEILANG)
-			sectors[i].ceilingpic_angle = READANGLE(save_p);
+			sectors[i].ceilingangle = READANGLE(save_p);
 		if (diff2 & SD_TAG)
 		{
 			size_t ncount = READUINT32(save_p);
@@ -1548,30 +1553,32 @@ typedef enum
 
 typedef enum
 {
-	MD2_CUSVAL      = 1,
-	MD2_CVMEM       = 1<<1,
-	MD2_SKIN        = 1<<2,
-	MD2_COLOR       = 1<<3,
-	MD2_SCALESPEED  = 1<<4,
-	MD2_EXTVAL1     = 1<<5,
-	MD2_EXTVAL2     = 1<<6,
-	MD2_HNEXT       = 1<<7,
-	MD2_HPREV       = 1<<8,
-	MD2_FLOORROVER  = 1<<9,
-	MD2_CEILINGROVER = 1<<10,
-	MD2_SLOPE        = 1<<11,
-	MD2_COLORIZED    = 1<<12,
-	MD2_MIRRORED     = 1<<13,
-	MD2_ROLLANGLE    = 1<<14,
-	MD2_SHADOWSCALE  = 1<<15,
-	MD2_RENDERFLAGS  = 1<<16,
-	MD2_BLENDMODE    = 1<<17,
-	MD2_SPRITEXSCALE = 1<<18,
-	MD2_SPRITEYSCALE = 1<<19,
-	MD2_SPRITEXOFFSET = 1<<20,
-	MD2_SPRITEYOFFSET = 1<<21,
-	MD2_FLOORSPRITESLOPE = 1<<22,
-	MD2_DISPOFFSET = 1<<23
+	MD2_CUSVAL              = 1,
+	MD2_CVMEM               = 1<<1,
+	MD2_SKIN                = 1<<2,
+	MD2_COLOR               = 1<<3,
+	MD2_SCALESPEED          = 1<<4,
+	MD2_EXTVAL1             = 1<<5,
+	MD2_EXTVAL2             = 1<<6,
+	MD2_HNEXT               = 1<<7,
+	MD2_HPREV               = 1<<8,
+	MD2_FLOORROVER          = 1<<9,
+	MD2_CEILINGROVER        = 1<<10,
+	MD2_SLOPE               = 1<<11,
+	MD2_COLORIZED           = 1<<12,
+	MD2_MIRRORED            = 1<<13,
+	MD2_SPRITEROLL          = 1<<14,
+	MD2_SHADOWSCALE         = 1<<15,
+	MD2_RENDERFLAGS         = 1<<16,
+	MD2_BLENDMODE           = 1<<17,
+	MD2_SPRITEXSCALE        = 1<<18,
+	MD2_SPRITEYSCALE        = 1<<19,
+	MD2_SPRITEXOFFSET       = 1<<20,
+	MD2_SPRITEYOFFSET       = 1<<21,
+	MD2_FLOORSPRITESLOPE    = 1<<22,
+	MD2_DISPOFFSET          = 1<<23,
+	MD2_DRAWONLYFORPLAYER   = 1<<24,
+	MD2_DONTDRAWFORVIEWMOBJ = 1<<25
 } mobj_diff2_t;
 
 typedef enum
@@ -1780,8 +1787,8 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 		diff2 |= MD2_COLORIZED;
 	if (mobj->mirrored)
 		diff2 |= MD2_MIRRORED;
-	if (mobj->rollangle)
-		diff2 |= MD2_ROLLANGLE;
+	if (mobj->spriteroll)
+		diff2 |= MD2_SPRITEROLL;
 	if (mobj->shadowscale)
 		diff2 |= MD2_SHADOWSCALE;
 	if (mobj->renderflags)
@@ -1806,6 +1813,10 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 		|| (slope->normal.z != FRACUNIT))
 			diff2 |= MD2_FLOORSPRITESLOPE;
 	}
+	if (mobj->drawonlyforplayer)
+		diff2 |= MD2_DRAWONLYFORPLAYER;
+	if (mobj->dontdrawforviewmobj)
+		diff2 |= MD2_DONTDRAWFORVIEWMOBJ;
 	if (mobj->dispoffset != mobj->info->dispoffset)
 		diff2 |= MD2_DISPOFFSET;
 
@@ -1948,8 +1959,8 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 		WRITEUINT8(save_p, mobj->colorized);
 	if (diff2 & MD2_MIRRORED)
 		WRITEUINT8(save_p, mobj->mirrored);
-	if (diff2 & MD2_ROLLANGLE)
-		WRITEANGLE(save_p, mobj->rollangle);
+	if (diff2 & MD2_SPRITEROLL)
+		WRITEANGLE(save_p, mobj->spriteroll);
 	if (diff2 & MD2_SHADOWSCALE)
 		WRITEFIXED(save_p, mobj->shadowscale);
 	if (diff2 & MD2_RENDERFLAGS)
@@ -1983,6 +1994,10 @@ static void SaveMobjThinker(const thinker_t *th, const UINT8 type)
 		WRITEFIXED(save_p, slope->normal.y);
 		WRITEFIXED(save_p, slope->normal.z);
 	}
+	if (diff2 & MD2_DRAWONLYFORPLAYER)
+		WRITEUINT8(save_p, mobj->drawonlyforplayer-players);
+	if (diff2 & MD2_DONTDRAWFORVIEWMOBJ)
+		WRITEUINT32(save_p, mobj->dontdrawforviewmobj->mobjnum);
 	if (diff2 & MD2_DISPOFFSET)
 		WRITEINT32(save_p, mobj->dispoffset);
 
@@ -2701,8 +2716,8 @@ static void P_NetArchiveThinkers(void)
 				continue;
 			}
 #ifdef PARANOIA
-			else if (th->function.acp1 != (actionf_p1)P_RemoveThinkerDelayed) // wait garbage collection
-				I_Error("unknown thinker type %p", th->function.acp1);
+			else
+				I_Assert(th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed); // wait garbage collection
 #endif
 		}
 
@@ -2998,8 +3013,8 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 		mobj->colorized = READUINT8(save_p);
 	if (diff2 & MD2_MIRRORED)
 		mobj->mirrored = READUINT8(save_p);
-	if (diff2 & MD2_ROLLANGLE)
-		mobj->rollangle = READANGLE(save_p);
+	if (diff2 & MD2_SPRITEROLL)
+		mobj->spriteroll = READANGLE(save_p);
 	if (diff2 & MD2_SHADOWSCALE)
 		mobj->shadowscale = READFIXED(save_p);
 	if (diff2 & MD2_RENDERFLAGS)
@@ -3039,6 +3054,10 @@ static thinker_t* LoadMobjThinker(actionf_p1 thinker)
 		slope->normal.y = READFIXED(save_p);
 		slope->normal.z = READFIXED(save_p);
 	}
+	if (diff2 & MD2_DRAWONLYFORPLAYER)
+		mobj->drawonlyforplayer = &players[READUINT8(save_p)];
+	if (diff2 & MD2_DONTDRAWFORVIEWMOBJ)
+		mobj->dontdrawforviewmobj = (mobj_t *)(size_t)READUINT32(save_p);
 	if (diff2 & MD2_DISPOFFSET)
 		mobj->dispoffset = READINT32(save_p);
 	else
@@ -4058,6 +4077,13 @@ static void P_RelinkPointers(void)
 		if (mobj->type == MT_HOOP || mobj->type == MT_HOOPCOLLIDE || mobj->type == MT_HOOPCENTER)
 			continue;
 
+		if (mobj->dontdrawforviewmobj)
+		{
+			temp = (UINT32)(size_t)mobj->dontdrawforviewmobj;
+			mobj->dontdrawforviewmobj = NULL;
+			if (!P_SetTarget(&mobj->dontdrawforviewmobj, P_FindNewPosition(temp)))
+				CONS_Debug(DBG_GAMELOGIC, "dontdrawforviewmobj not found on %d\n", mobj->type);
+		}
 		if (mobj->tracer)
 		{
 			temp = (UINT32)(size_t)mobj->tracer;
@@ -4276,7 +4302,11 @@ static void P_NetArchiveMisc(boolean resending)
 	if (resending)
 		WRITEUINT32(save_p, gametic);
 	WRITEINT16(save_p, gamemap);
-	WRITEINT16(save_p, gamestate);
+
+	if (gamestate != GS_LEVEL)
+		WRITEINT16(save_p, GS_WAITINGPLAYERS); // nice hack to put people back into waitingplayers
+	else
+		WRITEINT16(save_p, gamestate);
 	WRITEINT16(save_p, gametype);
 
 	{
@@ -4444,6 +4474,267 @@ static inline boolean P_NetUnArchiveMisc(boolean reloading)
 	return true;
 }
 
+static inline void P_NetArchiveEmblems(void)
+{
+	gamedata_t *data = serverGamedata;
+	INT32 i, j;
+	UINT8 btemp;
+	INT32 curmare;
+
+	WRITEUINT32(save_p, ARCHIVEBLOCK_EMBLEMS);
+
+	// These should be synchronized before savegame loading by the wad files being the same anyway,
+	// but just in case, for now, we'll leave them here for testing. It would be very bad if they mismatch.
+	WRITEUINT8(save_p, (UINT8)savemoddata);
+	WRITEINT32(save_p, numemblems);
+	WRITEINT32(save_p, numextraemblems);
+
+	// The rest of this is lifted straight from G_SaveGameData in g_game.c
+	// TODO: Optimize this to only send information about emblems, unlocks, etc. which actually exist
+	//       There is no need to go all the way up to MAXEMBLEMS when wads are guaranteed to be the same.
+
+	WRITEUINT32(save_p, data->totalplaytime);
+
+	// TODO put another cipher on these things? meh, I don't care...
+	for (i = 0; i < NUMMAPS; i++)
+		WRITEUINT8(save_p, (data->mapvisited[i] & MV_MAX));
+
+	// To save space, use one bit per collected/achieved/unlocked flag
+	for (i = 0; i < MAXEMBLEMS;)
+	{
+		btemp = 0;
+		for (j = 0; j < 8 && j+i < MAXEMBLEMS; ++j)
+			btemp |= (data->collected[j+i] << j);
+		WRITEUINT8(save_p, btemp);
+		i += j;
+	}
+	for (i = 0; i < MAXEXTRAEMBLEMS;)
+	{
+		btemp = 0;
+		for (j = 0; j < 8 && j+i < MAXEXTRAEMBLEMS; ++j)
+			btemp |= (data->extraCollected[j+i] << j);
+		WRITEUINT8(save_p, btemp);
+		i += j;
+	}
+	for (i = 0; i < MAXUNLOCKABLES;)
+	{
+		btemp = 0;
+		for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j)
+			btemp |= (data->unlocked[j+i] << j);
+		WRITEUINT8(save_p, btemp);
+		i += j;
+	}
+	for (i = 0; i < MAXCONDITIONSETS;)
+	{
+		btemp = 0;
+		for (j = 0; j < 8 && j+i < MAXCONDITIONSETS; ++j)
+			btemp |= (data->achieved[j+i] << j);
+		WRITEUINT8(save_p, btemp);
+		i += j;
+	}
+
+	WRITEUINT32(save_p, data->timesBeaten);
+	WRITEUINT32(save_p, data->timesBeatenWithEmeralds);
+	WRITEUINT32(save_p, data->timesBeatenUltimate);
+
+	// Main records
+	for (i = 0; i < NUMMAPS; i++)
+	{
+		if (data->mainrecords[i])
+		{
+			WRITEUINT32(save_p, data->mainrecords[i]->score);
+			WRITEUINT32(save_p, data->mainrecords[i]->time);
+			WRITEUINT16(save_p, data->mainrecords[i]->rings);
+		}
+		else
+		{
+			WRITEUINT32(save_p, 0);
+			WRITEUINT32(save_p, 0);
+			WRITEUINT16(save_p, 0);
+		}
+	}
+
+	// NiGHTS records
+	for (i = 0; i < NUMMAPS; i++)
+	{
+		if (!data->nightsrecords[i] || !data->nightsrecords[i]->nummares)
+		{
+			WRITEUINT8(save_p, 0);
+			continue;
+		}
+
+		WRITEUINT8(save_p, data->nightsrecords[i]->nummares);
+
+		for (curmare = 0; curmare < (data->nightsrecords[i]->nummares + 1); ++curmare)
+		{
+			WRITEUINT32(save_p, data->nightsrecords[i]->score[curmare]);
+			WRITEUINT8(save_p, data->nightsrecords[i]->grade[curmare]);
+			WRITEUINT32(save_p, data->nightsrecords[i]->time[curmare]);
+		}
+	}
+
+	// Mid-map stuff
+	WRITEUINT32(save_p, unlocktriggers);
+
+	for (i = 0; i < MAXPLAYERS; i++)
+	{
+		if (!ntemprecords[i].nummares)
+		{
+			WRITEUINT8(save_p, 0);
+			continue;
+		}
+
+		WRITEUINT8(save_p, ntemprecords[i].nummares);
+
+		for (curmare = 0; curmare < (ntemprecords[i].nummares + 1); ++curmare)
+		{
+			WRITEUINT32(save_p, ntemprecords[i].score[curmare]);
+			WRITEUINT8(save_p, ntemprecords[i].grade[curmare]);
+			WRITEUINT32(save_p, ntemprecords[i].time[curmare]);
+		}
+	}
+}
+
+static inline void P_NetUnArchiveEmblems(void)
+{
+	gamedata_t *data = serverGamedata;
+	INT32 i, j;
+	UINT8 rtemp;
+	UINT32 recscore;
+	tic_t rectime;
+	UINT16 recrings;
+	UINT8 recmares;
+	INT32 curmare;
+
+	if (READUINT32(save_p) != ARCHIVEBLOCK_EMBLEMS)
+		I_Error("Bad $$$.sav at archive block Emblems");
+
+	savemoddata = (boolean)READUINT8(save_p); // this one is actually necessary because savemoddata stays false otherwise for some reason.
+
+	if (numemblems != READINT32(save_p))
+		I_Error("Bad $$$.sav dearchiving Emblems (numemblems mismatch)");
+	if (numextraemblems != READINT32(save_p))
+		I_Error("Bad $$$.sav dearchiving Emblems (numextraemblems mismatch)");
+
+	// This shouldn't happen, but if something really fucked up happens and you transfer
+	// the SERVER player's gamedata over your own CLIENT gamedata,
+	// then this prevents it from being saved over yours.
+	data->loaded = false;
+
+	M_ClearSecrets(data);
+	G_ClearRecords(data);
+
+	// The rest of this is lifted straight from G_LoadGameData in g_game.c
+	// TODO: Optimize this to only read information about emblems, unlocks, etc. which actually exist
+	//       There is no need to go all the way up to MAXEMBLEMS when wads are guaranteed to be the same.
+
+	data->totalplaytime = READUINT32(save_p);
+
+	// TODO put another cipher on these things? meh, I don't care...
+	for (i = 0; i < NUMMAPS; i++)
+		if ((data->mapvisited[i] = READUINT8(save_p)) > MV_MAX)
+			I_Error("Bad $$$.sav dearchiving Emblems (invalid visit flags)");
+
+	// To save space, use one bit per collected/achieved/unlocked flag
+	for (i = 0; i < MAXEMBLEMS;)
+	{
+		rtemp = READUINT8(save_p);
+		for (j = 0; j < 8 && j+i < MAXEMBLEMS; ++j)
+			data->collected[j+i] = ((rtemp >> j) & 1);
+		i += j;
+	}
+	for (i = 0; i < MAXEXTRAEMBLEMS;)
+	{
+		rtemp = READUINT8(save_p);
+		for (j = 0; j < 8 && j+i < MAXEXTRAEMBLEMS; ++j)
+			data->extraCollected[j+i] = ((rtemp >> j) & 1);
+		i += j;
+	}
+	for (i = 0; i < MAXUNLOCKABLES;)
+	{
+		rtemp = READUINT8(save_p);
+		for (j = 0; j < 8 && j+i < MAXUNLOCKABLES; ++j)
+			data->unlocked[j+i] = ((rtemp >> j) & 1);
+		i += j;
+	}
+	for (i = 0; i < MAXCONDITIONSETS;)
+	{
+		rtemp = READUINT8(save_p);
+		for (j = 0; j < 8 && j+i < MAXCONDITIONSETS; ++j)
+			data->achieved[j+i] = ((rtemp >> j) & 1);
+		i += j;
+	}
+
+	data->timesBeaten = READUINT32(save_p);
+	data->timesBeatenWithEmeralds = READUINT32(save_p);
+	data->timesBeatenUltimate = READUINT32(save_p);
+
+	// Main records
+	for (i = 0; i < NUMMAPS; ++i)
+	{
+		recscore = READUINT32(save_p);
+		rectime  = (tic_t)READUINT32(save_p);
+		recrings = READUINT16(save_p);
+
+		if (recrings > 10000 || recscore > MAXSCORE)
+			I_Error("Bad $$$.sav dearchiving Emblems (invalid score)");
+
+		if (recscore || rectime || recrings)
+		{
+			G_AllocMainRecordData((INT16)i, data);
+			data->mainrecords[i]->score = recscore;
+			data->mainrecords[i]->time = rectime;
+			data->mainrecords[i]->rings = recrings;
+		}
+	}
+
+	// Nights records
+	for (i = 0; i < NUMMAPS; ++i)
+	{
+		if ((recmares = READUINT8(save_p)) == 0)
+			continue;
+
+		G_AllocNightsRecordData((INT16)i, data);
+
+		for (curmare = 0; curmare < (recmares+1); ++curmare)
+		{
+			data->nightsrecords[i]->score[curmare] = READUINT32(save_p);
+			data->nightsrecords[i]->grade[curmare] = READUINT8(save_p);
+			data->nightsrecords[i]->time[curmare] = (tic_t)READUINT32(save_p);
+
+			if (data->nightsrecords[i]->grade[curmare] > GRADE_S)
+			{
+				I_Error("Bad $$$.sav dearchiving Emblems (invalid grade)");
+			}
+		}
+
+		data->nightsrecords[i]->nummares = recmares;
+	}
+
+	// Mid-map stuff
+	unlocktriggers = READUINT32(save_p);
+
+	for (i = 0; i < MAXPLAYERS; ++i)
+	{
+		if ((recmares = READUINT8(save_p)) == 0)
+			continue;
+
+		for (curmare = 0; curmare < (recmares+1); ++curmare)
+		{
+			ntemprecords[i].score[curmare] = READUINT32(save_p);
+			ntemprecords[i].grade[curmare] = READUINT8(save_p);
+			ntemprecords[i].time[curmare] = (tic_t)READUINT32(save_p);
+
+			if (ntemprecords[i].grade[curmare] > GRADE_S)
+			{
+				I_Error("Bad $$$.sav dearchiving Emblems (invalid temp grade)");
+			}
+		}
+
+		ntemprecords[i].nummares = recmares;
+	}
+}
+
 static inline void P_ArchiveLuabanksAndConsistency(void)
 {
 	UINT8 i, banksinuse = NUM_LUABANKS;
@@ -4507,6 +4798,7 @@ void P_SaveNetGame(boolean resending)
 
 	CV_SaveNetVars(&save_p);
 	P_NetArchiveMisc(resending);
+	P_NetArchiveEmblems();
 
 	// Assign the mobjnumber for pointer tracking
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
@@ -4559,6 +4851,7 @@ boolean P_LoadNetGame(boolean reloading)
 	CV_LoadNetVars(&save_p);
 	if (!P_NetUnArchiveMisc(reloading))
 		return false;
+	P_NetUnArchiveEmblems();
 	P_NetUnArchivePlayers();
 	if (gamestate == GS_LEVEL)
 	{
diff --git a/src/p_setup.c b/src/p_setup.c
index c8b0936b802928392f08c49ec9107bfde8d11c5c..0390761b61a5e064b0b9b55daead7ebd1488ac49 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -50,6 +50,7 @@
 #include "m_random.h"
 
 #include "dehacked.h" // for map headers
+#include "deh_tables.h" // FREE_SKINCOLORS
 #include "r_main.h"
 #include "m_cond.h" // for emblems
 
@@ -87,6 +88,8 @@
 
 #include "taglist.h"
 
+#include "netcode/net_command.h"
+
 //
 // Map MD5, calculated on level load.
 // Sent to clients in PT_SERVERINFO.
@@ -876,7 +879,7 @@ static void P_SpawnMapThings(boolean spawnemblems)
 	size_t i;
 	mapthing_t *mt;
 
-        // Spawn axis points first so they are at the front of the list for fast searching.
+	// Spawn axis points first so they are at the front of the list for fast searching.
 	for (i = 0, mt = mapthings; i < nummapthings; i++, mt++)
 	{
 		switch (mt->type)
@@ -1049,10 +1052,10 @@ static void P_LoadSectors(UINT8 *data)
 		ss->special = SHORT(ms->special);
 		Tag_FSet(&ss->tags, SHORT(ms->tag));
 
-		ss->floor_xoffs = ss->floor_yoffs = 0;
-		ss->ceiling_xoffs = ss->ceiling_yoffs = 0;
+		ss->floorxoffset = ss->flooryoffset = 0;
+		ss->ceilingxoffset = ss->ceilingyoffset = 0;
 
-		ss->floorpic_angle = ss->ceilingpic_angle = 0;
+		ss->floorangle = ss->ceilingangle = 0;
 
 		ss->floorlightlevel = ss->ceilinglightlevel = 0;
 		ss->floorlightabsolute = ss->ceilinglightabsolute = false;
@@ -1103,7 +1106,6 @@ static void P_InitializeLinedef(line_t *ld)
 	ld->validcount = 0;
 	ld->polyobj = NULL;
 
-	ld->text = NULL;
 	ld->callcount = 0;
 
 	// cph 2006/09/30 - fix sidedef errors right away.
@@ -1207,10 +1209,115 @@ static void P_InitializeSidedef(side_t *sd)
 		sd->special = sd->line->special;
 	}
 
-	sd->text = NULL;
 	sd->colormap_data = NULL;
 }
 
+static boolean contextdrift = false;
+
+// In Ring Racers, this is just the reference implementation.
+// But in SRB2, backwards compatibility is a design goal!?
+// So we let the map function on load, but report that there's
+// an issue which could prevent saving it as a UDMF map.
+//
+// ... "context drift" is a term I learned from the story "Lena",
+// styled after a wikipedia article on a man whose brain scan
+// spawns an industry of digital brain emulation. It is a term
+// to describe that the understanding of the world MMAcevedo has
+// is rooted in a specific moment and time and will lose relevance
+// as societal norms, languages, and events change and are changed.
+// It is on the real wikipedia, but under the name "concept drift".
+// I am connecting the idea of the rooted-in-time brainstate with
+// the numerical evaluation of get_number(), the ground truth
+// CONTEXT for all these silly integers, that will trip up future
+// builds of SRB2 that have modified states and object lists.
+// Golden statues rotating in place of carousel rides is perhaps
+// not quite what qntm meant, but is a TEXTMAP, a human-readable
+// document for imparting a sense of place made/read by machines,
+// not its own language providing a vulnerable context?
+// ~toast 200723, see https://qntm.org/mmacevedo
+//
+static void P_WriteConstant(INT32 constant, char **target, const char *desc, UINT32 id)
+{
+	char buffer[12];
+	size_t len;
+
+	CONS_Alert(
+		CONS_WARNING, M_GetText(
+		"P_WriteConstant has been called for %s %d, "
+		"this level is vulnerable to context drift "
+		"if -writetextmap is used.\n"),
+		desc, id
+	);
+
+	contextdrift = true;
+
+	sprintf(buffer, "%d", constant);
+	len = strlen(buffer) + 1;
+	*target = Z_Malloc(len, PU_LEVEL, NULL);
+	M_Memcpy(*target, buffer, len);
+}
+
+static void P_WriteDuplicateText(const char *text, char **target)
+{
+	if (text == NULL || text[0] == '\0')
+		return;
+
+	size_t len = strlen(text) + 1;
+	*target = Z_Malloc(len, PU_LEVEL, NULL);
+	M_Memcpy(*target, text, len);
+}
+
+static void P_WriteSkincolor(INT32 constant, char **target)
+{
+	const char *color_name;
+
+	if (constant <= SKINCOLOR_NONE
+	|| constant >= (INT32)numskincolors)
+		return;
+
+	if (constant >= SKINCOLOR_FIRSTFREESLOT)
+		color_name = FREE_SKINCOLORS[constant - SKINCOLOR_FIRSTFREESLOT];
+	else
+		color_name = COLOR_ENUMS[constant];
+
+	P_WriteDuplicateText(
+		va("SKINCOLOR_%s", color_name),
+		target
+	);
+}
+
+static void P_WriteSfx(INT32 constant, char **target)
+{
+	if (constant <= sfx_None
+	|| constant >= (INT32)sfxfree)
+		return;
+
+	P_WriteDuplicateText(
+		va("SFX_%s", S_sfx[constant].name),
+		target
+	);
+}
+
+static void P_WriteTics(INT32 tics, char **target)
+{
+	if (!tics)
+		return;
+
+	INT32 seconds = (tics / TICRATE);
+	tics %= TICRATE;
+
+	const char *text;
+
+	if (tics && !seconds)
+		text = va("%d", tics);
+	else if (seconds && !tics)
+		text = va("(%d*TICRATE)", seconds);
+	else
+		text = va("(%d*TICRATE)%s%d", seconds, (tics > 0) ? "+" : "", tics);
+
+	P_WriteDuplicateText(text, target);
+}
+
 static void P_LoadSidedefs(UINT8 *data)
 {
 	mapsidedef_t *msd = (mapsidedef_t*)data;
@@ -1240,6 +1347,9 @@ 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;
+
 		P_SetSidedefSector(i, SHORT(msd->sector));
 
 		// Special info stored in texture fields!
@@ -1256,9 +1366,13 @@ static void P_LoadSidedefs(UINT8 *data)
 
 			case 413: // Change music
 			{
+				sd->toptexture = sd->midtexture = sd->bottomtexture = 0;
+
+				if (!isfrontside)
+					break;
+
 				char process[8+1];
 
-				sd->toptexture = sd->midtexture = sd->bottomtexture = 0;
 				if (msd->bottomtexture[0] != '-' || msd->bottomtexture[1] != '\0')
 				{
 					M_Memcpy(process,msd->bottomtexture,8);
@@ -1273,48 +1387,39 @@ static void P_LoadSidedefs(UINT8 *data)
 					sd->midtexture = get_number(process);
 				}
 
-				sd->text = Z_Malloc(7, PU_LEVEL, NULL);
-				if (isfrontside && !(msd->toptexture[0] == '-' && msd->toptexture[1] == '\0'))
+				if (msd->toptexture[0] != '-' && msd->toptexture[1] != '\0')
 				{
+					sd->line->stringargs[0] = Z_Malloc(7, PU_LEVEL, NULL);
+
 					M_Memcpy(process,msd->toptexture,8);
 					process[8] = '\0';
 
 					// If they type in O_ or D_ and their music name, just shrug,
 					// then copy the rest instead.
 					if ((process[0] == 'O' || process[0] == 'D') && process[7])
-						M_Memcpy(sd->text, process+2, 6);
+						M_Memcpy(sd->line->stringargs[0], process+2, 6);
 					else // Assume it's a proper music name.
-						M_Memcpy(sd->text, process, 6);
-					sd->text[6] = 0;
+						M_Memcpy(sd->line->stringargs[0], process, 6);
+					sd->line->stringargs[0][6] = '\0';
 				}
-				else
-					sd->text[0] = 0;
-				break;
-			}
 
-			case 4: // Speed pad parameters
-			{
-				sd->toptexture = sd->midtexture = sd->bottomtexture = 0;
-				if (msd->toptexture[0] != '-' || msd->toptexture[1] != '\0')
-				{
-					char process[8+1];
-					M_Memcpy(process,msd->toptexture,8);
-					process[8] = '\0';
-					sd->toptexture = get_number(process);
-				}
 				break;
 			}
 
+			case 4: // Speed pad parameters
 			case 414: // Play SFX
 			{
 				sd->toptexture = sd->midtexture = sd->bottomtexture = 0;
+
+				if (!isfrontside)
+					break;
+
 				if (msd->toptexture[0] != '-' || msd->toptexture[1] != '\0')
 				{
 					char process[8 + 1];
 					M_Memcpy(process, msd->toptexture, 8);
 					process[8] = '\0';
-					sd->text = Z_Malloc(strlen(process) + 1, PU_LEVEL, NULL);
-					M_Memcpy(sd->text, process, strlen(process) + 1);
+					P_WriteDuplicateText(process, &sd->line->stringargs[0]);
 				}
 				break;
 			}
@@ -1323,21 +1428,13 @@ static void P_LoadSidedefs(UINT8 *data)
 			case 14: // Bustable block parameters
 			case 15: // Fan particle spawner parameters
 			{
-				char process[8*3+1];
-				memset(process,0,8*3+1);
-				sd->toptexture = sd->midtexture = sd->bottomtexture = 0;
-				if (msd->toptexture[0] == '-' && msd->toptexture[1] == '\0')
+				if (msd->toptexture[7] == '\0' && strcasecmp(msd->toptexture, "MT_NULL") == 0)
+				{
+					// Don't bulk the conversion with irrelevant types
 					break;
-				else
-					M_Memcpy(process,msd->toptexture,8);
-				if (msd->midtexture[0] != '-' || msd->midtexture[1] != '\0')
-					M_Memcpy(process+strlen(process), msd->midtexture, 8);
-				if (msd->bottomtexture[0] != '-' || msd->bottomtexture[1] != '\0')
-					M_Memcpy(process+strlen(process), msd->bottomtexture, 8);
-				sd->toptexture = get_number(process);
-				break;
+				}
 			}
-
+			// FALLTHRU
 			case 331: // Trigger linedef executor: Skin - Continuous
 			case 332: // Trigger linedef executor: Skin - Each time
 			case 333: // Trigger linedef executor: Skin - Once
@@ -1363,8 +1460,11 @@ static void P_LoadSidedefs(UINT8 *data)
 					M_Memcpy(process+strlen(process), msd->midtexture, 8);
 				if (msd->bottomtexture[0] != '-' || msd->bottomtexture[1] != '\0')
 					M_Memcpy(process+strlen(process), msd->bottomtexture, 8);
-				sd->text = Z_Malloc(strlen(process)+1, PU_LEVEL, NULL);
-				M_Memcpy(sd->text, process, strlen(process)+1);
+
+				P_WriteDuplicateText(
+					process,
+					&sd->line->stringargs[(isfrontside) ? 0 : 1]
+				);
 				break;
 			}
 
@@ -1420,6 +1520,7 @@ static void P_LoadThings(UINT8 *data)
 		mt->extrainfo = (UINT8)(mt->type >> 12);
 		Tag_FSet(&mt->tags, 0);
 		mt->scale = FRACUNIT;
+		mt->spritexscale = mt->spriteyscale = FRACUNIT;
 		memset(mt->args, 0, NUMMAPTHINGARGS*sizeof(*mt->args));
 		memset(mt->stringargs, 0x00, NUMMAPTHINGSTRINGARGS*sizeof(*mt->stringargs));
 		mt->pitch = mt->roll = 0;
@@ -1578,19 +1679,19 @@ static void ParseTextmapSectorParameter(UINT32 i, const char *param, const char
 			if ((id = strchr(id, ' ')))
 				id++;
 		}
-	}
+	}	
 	else if (fastcmp(param, "xpanningfloor"))
-		sectors[i].floor_xoffs = FLOAT_TO_FIXED(atof(val));
+		sectors[i].floorxoffset = FLOAT_TO_FIXED(atof(val));
 	else if (fastcmp(param, "ypanningfloor"))
-		sectors[i].floor_yoffs = FLOAT_TO_FIXED(atof(val));
+		sectors[i].flooryoffset = FLOAT_TO_FIXED(atof(val));
 	else if (fastcmp(param, "xpanningceiling"))
-		sectors[i].ceiling_xoffs = FLOAT_TO_FIXED(atof(val));
+		sectors[i].ceilingxoffset = FLOAT_TO_FIXED(atof(val));
 	else if (fastcmp(param, "ypanningceiling"))
-		sectors[i].ceiling_yoffs = FLOAT_TO_FIXED(atof(val));
+		sectors[i].ceilingyoffset = FLOAT_TO_FIXED(atof(val));
 	else if (fastcmp(param, "rotationfloor"))
-		sectors[i].floorpic_angle = FixedAngle(FLOAT_TO_FIXED(atof(val)));
+		sectors[i].floorangle = FixedAngle(FLOAT_TO_FIXED(atof(val)));
 	else if (fastcmp(param, "rotationceiling"))
-		sectors[i].ceilingpic_angle = FixedAngle(FLOAT_TO_FIXED(atof(val)));
+		sectors[i].ceilingangle = FixedAngle(FLOAT_TO_FIXED(atof(val)));
 	else if (fastcmp(param, "floorplane_a"))
 	{
 		textmap_planefloor.defined |= PD_A;
@@ -1777,6 +1878,18 @@ static void ParseTextmapSidedefParameter(UINT32 i, const char *param, const char
 		sides[i].textureoffset = atol(val)<<FRACBITS;
 	else if (fastcmp(param, "offsety"))
 		sides[i].rowoffset = atol(val)<<FRACBITS;
+	else if (fastcmp(param, "offsetx_top"))
+		sides[i].offsetx_top = atol(val) << FRACBITS;
+	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;
+	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;
 	else if (fastcmp(param, "texturetop"))
 		sides[i].toptexture = R_TextureNumForName(val);
 	else if (fastcmp(param, "texturebottom"))
@@ -1911,11 +2024,19 @@ static void ParseTextmapThingParameter(UINT32 i, const char *param, const char *
 		mapthings[i].roll = atol(val);
 	else if (fastcmp(param, "type"))
 		mapthings[i].type = atol(val);
-	else if (fastcmp(param, "scale") || fastcmp(param, "scalex") || fastcmp(param, "scaley"))
+	else if (fastcmp(param, "scale"))
+		mapthings[i].spritexscale = mapthings[i].spriteyscale = FLOAT_TO_FIXED(atof(val));
+	else if (fastcmp(param, "scalex"))
+		mapthings[i].spritexscale = FLOAT_TO_FIXED(atof(val));
+	else if (fastcmp(param, "scaley"))
+		mapthings[i].spriteyscale = FLOAT_TO_FIXED(atof(val));
+	else if (fastcmp(param, "mobjscale"))
 		mapthings[i].scale = FLOAT_TO_FIXED(atof(val));
 	// Flags
 	else if (fastcmp(param, "flip") && fastcmp("true", val))
 		mapthings[i].options |= MTF_OBJECTFLIP;
+	else if (fastcmp(param, "absolutez") && fastcmp("true", val))
+		mapthings[i].options |= MTF_ABSOLUTEZ;
 
 	else if (fastncmp(param, "stringarg", 9) && strlen(param) > 9)
 	{
@@ -1966,47 +2087,47 @@ static void TextmapParse(UINT32 dataPos, size_t num, void (*parser)(UINT32, cons
  */
 static void TextmapFixFlatOffsets(sector_t *sec)
 {
-	if (sec->floorpic_angle)
+	if (sec->floorangle)
 	{
-		fixed_t pc = FINECOSINE(sec->floorpic_angle>>ANGLETOFINESHIFT);
-		fixed_t ps = FINESINE  (sec->floorpic_angle>>ANGLETOFINESHIFT);
-		fixed_t xoffs = sec->floor_xoffs;
-		fixed_t yoffs = sec->floor_yoffs;
-		sec->floor_xoffs = (FixedMul(xoffs, pc) % MAXFLATSIZE) - (FixedMul(yoffs, ps) % MAXFLATSIZE);
-		sec->floor_yoffs = (FixedMul(xoffs, ps) % MAXFLATSIZE) + (FixedMul(yoffs, pc) % MAXFLATSIZE);
+		fixed_t pc = FINECOSINE(sec->floorangle>>ANGLETOFINESHIFT);
+		fixed_t ps = FINESINE  (sec->floorangle>>ANGLETOFINESHIFT);
+		fixed_t xoffs = sec->floorxoffset;
+		fixed_t yoffs = sec->flooryoffset;
+		sec->floorxoffset = (FixedMul(xoffs, pc) % MAXFLATSIZE) - (FixedMul(yoffs, ps) % MAXFLATSIZE);
+		sec->flooryoffset = (FixedMul(xoffs, ps) % MAXFLATSIZE) + (FixedMul(yoffs, pc) % MAXFLATSIZE);
 	}
 
-	if (sec->ceilingpic_angle)
+	if (sec->ceilingangle)
 	{
-		fixed_t pc = FINECOSINE(sec->ceilingpic_angle>>ANGLETOFINESHIFT);
-		fixed_t ps = FINESINE  (sec->ceilingpic_angle>>ANGLETOFINESHIFT);
-		fixed_t xoffs = sec->ceiling_xoffs;
-		fixed_t yoffs = sec->ceiling_yoffs;
-		sec->ceiling_xoffs = (FixedMul(xoffs, pc) % MAXFLATSIZE) - (FixedMul(yoffs, ps) % MAXFLATSIZE);
-		sec->ceiling_yoffs = (FixedMul(xoffs, ps) % MAXFLATSIZE) + (FixedMul(yoffs, pc) % MAXFLATSIZE);
+		fixed_t pc = FINECOSINE(sec->ceilingangle>>ANGLETOFINESHIFT);
+		fixed_t ps = FINESINE  (sec->ceilingangle>>ANGLETOFINESHIFT);
+		fixed_t xoffs = sec->ceilingxoffset;
+		fixed_t yoffs = sec->ceilingyoffset;
+		sec->ceilingxoffset = (FixedMul(xoffs, pc) % MAXFLATSIZE) - (FixedMul(yoffs, ps) % MAXFLATSIZE);
+		sec->ceilingyoffset = (FixedMul(xoffs, ps) % MAXFLATSIZE) + (FixedMul(yoffs, pc) % MAXFLATSIZE);
 	}
 }
 
 static void TextmapUnfixFlatOffsets(sector_t *sec)
 {
-	if (sec->floorpic_angle)
+	if (sec->floorangle)
 	{
-		fixed_t pc = FINECOSINE(sec->floorpic_angle >> ANGLETOFINESHIFT);
-		fixed_t ps = FINESINE(sec->floorpic_angle >> ANGLETOFINESHIFT);
-		fixed_t xoffs = sec->floor_xoffs;
-		fixed_t yoffs = sec->floor_yoffs;
-		sec->floor_xoffs = (FixedMul(xoffs, ps) % MAXFLATSIZE) + (FixedMul(yoffs, pc) % MAXFLATSIZE);
-		sec->floor_yoffs = (FixedMul(xoffs, pc) % MAXFLATSIZE) - (FixedMul(yoffs, ps) % MAXFLATSIZE);
+		fixed_t pc = FINECOSINE(sec->floorangle >> ANGLETOFINESHIFT);
+		fixed_t ps = FINESINE(sec->floorangle >> ANGLETOFINESHIFT);
+		fixed_t xoffs = sec->floorxoffset;
+		fixed_t yoffs = sec->flooryoffset;
+		sec->floorxoffset = (FixedMul(xoffs, ps) % MAXFLATSIZE) + (FixedMul(yoffs, pc) % MAXFLATSIZE);
+		sec->flooryoffset = (FixedMul(xoffs, pc) % MAXFLATSIZE) - (FixedMul(yoffs, ps) % MAXFLATSIZE);
 	}
 
-	if (sec->ceilingpic_angle)
+	if (sec->ceilingangle)
 	{
-		fixed_t pc = FINECOSINE(sec->ceilingpic_angle >> ANGLETOFINESHIFT);
-		fixed_t ps = FINESINE(sec->ceilingpic_angle >> ANGLETOFINESHIFT);
-		fixed_t xoffs = sec->ceiling_xoffs;
-		fixed_t yoffs = sec->ceiling_yoffs;
-		sec->ceiling_xoffs = (FixedMul(xoffs, ps) % MAXFLATSIZE) + (FixedMul(yoffs, pc) % MAXFLATSIZE);
-		sec->ceiling_yoffs = (FixedMul(xoffs, pc) % MAXFLATSIZE) - (FixedMul(yoffs, ps) % MAXFLATSIZE);
+		fixed_t pc = FINECOSINE(sec->ceilingangle >> ANGLETOFINESHIFT);
+		fixed_t ps = FINESINE(sec->ceilingangle >> ANGLETOFINESHIFT);
+		fixed_t xoffs = sec->ceilingxoffset;
+		fixed_t yoffs = sec->ceilingyoffset;
+		sec->ceilingxoffset = (FixedMul(xoffs, ps) % MAXFLATSIZE) + (FixedMul(yoffs, pc) % MAXFLATSIZE);
+		sec->ceilingyoffset = (FixedMul(xoffs, pc) % MAXFLATSIZE) - (FixedMul(yoffs, ps) % MAXFLATSIZE);
 	}
 }
 
@@ -2329,8 +2450,12 @@ static void P_WriteTextmap(void)
 			fprintf(f, "roll = %d;\n", wmapthings[i].roll);
 		if (wmapthings[i].type != 0)
 			fprintf(f, "type = %d;\n", wmapthings[i].type);
+		if (wmapthings[i].spritexscale != FRACUNIT)
+			fprintf(f, "scalex = %f;\n", FIXED_TO_FLOAT(wmapthings[i].spritexscale));
+		if (wmapthings[i].spriteyscale != FRACUNIT)
+			fprintf(f, "scaley = %f;\n", FIXED_TO_FLOAT(wmapthings[i].spriteyscale));
 		if (wmapthings[i].scale != FRACUNIT)
-			fprintf(f, "scale = %f;\n", FIXED_TO_FLOAT(wmapthings[i].scale));
+			fprintf(f, "mobjscale = %f;\n", FIXED_TO_FLOAT(wmapthings[i].scale));
 		if (wmapthings[i].options & MTF_OBJECTFLIP)
 			fprintf(f, "flip = true;\n");
 		for (j = 0; j < NUMMAPTHINGARGS; j++)
@@ -2461,6 +2586,18 @@ static void P_WriteTextmap(void)
 			fprintf(f, "offsetx = %d;\n", wsides[i].textureoffset >> FRACBITS);
 		if (wsides[i].rowoffset != 0)
 			fprintf(f, "offsety = %d;\n", wsides[i].rowoffset >> FRACBITS);
+		if (wsides[i].offsetx_top != 0)
+			fprintf(f, "offsetx_top = %d;\n", wsides[i].offsetx_top >> FRACBITS);
+		if (wsides[i].offsety_top != 0)
+			fprintf(f, "offsety_top = %d;\n", wsides[i].offsety_top >> FRACBITS);
+		if (wsides[i].offsetx_mid != 0)
+			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].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)
@@ -2508,18 +2645,18 @@ static void P_WriteTextmap(void)
 		}
 		sector_t tempsec = wsectors[i];
 		TextmapUnfixFlatOffsets(&tempsec);
-		if (tempsec.floor_xoffs != 0)
-			fprintf(f, "xpanningfloor = %f;\n", FIXED_TO_FLOAT(tempsec.floor_xoffs));
-		if (tempsec.floor_yoffs != 0)
-			fprintf(f, "ypanningfloor = %f;\n", FIXED_TO_FLOAT(tempsec.floor_yoffs));
-		if (tempsec.ceiling_xoffs != 0)
-			fprintf(f, "xpanningceiling = %f;\n", FIXED_TO_FLOAT(tempsec.ceiling_xoffs));
-		if (tempsec.ceiling_yoffs != 0)
-			fprintf(f, "ypanningceiling = %f;\n", FIXED_TO_FLOAT(tempsec.ceiling_yoffs));
-		if (wsectors[i].floorpic_angle != 0)
-			fprintf(f, "rotationfloor = %f;\n", FIXED_TO_FLOAT(AngleFixed(wsectors[i].floorpic_angle)));
-		if (wsectors[i].ceilingpic_angle != 0)
-			fprintf(f, "rotationceiling = %f;\n", FIXED_TO_FLOAT(AngleFixed(wsectors[i].ceilingpic_angle)));
+		if (tempsec.floorxoffset != 0)
+			fprintf(f, "xpanningfloor = %f;\n", FIXED_TO_FLOAT(tempsec.floorxoffset));
+		if (tempsec.flooryoffset != 0)
+			fprintf(f, "ypanningfloor = %f;\n", FIXED_TO_FLOAT(tempsec.flooryoffset));
+		if (tempsec.ceilingxoffset != 0)
+			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 (wsectors[i].floorangle != 0)
+			fprintf(f, "rotationfloor = %f;\n", FIXED_TO_FLOAT(AngleFixed(wsectors[i].floorangle)));
+		if (wsectors[i].ceilingangle != 0)
+			fprintf(f, "rotationceiling = %f;\n", FIXED_TO_FLOAT(AngleFixed(wsectors[i].ceilingangle)));
         if (wsectors[i].extra_colormap)
 		{
 			INT32 lightcolor = P_RGBAToColor(wsectors[i].extra_colormap->rgba);
@@ -2689,6 +2826,15 @@ static void P_WriteTextmap(void)
 	Z_Free(wlines);
 	Z_Free(wsides);
 	Z_Free(specialthings);
+
+	if (contextdrift)
+	{
+		CONS_Alert(
+			CONS_WARNING, M_GetText(
+			"Some elements of this level are vulnerable to context drift."
+			"Please check the console log for more information.\n")
+		);
+	}
 }
 
 /** Loads the textmap data, after obtaining the elements count and allocating their respective space.
@@ -2739,10 +2885,10 @@ static void P_LoadTextmap(void)
 		sc->special = 0;
 		Tag_FSet(&sc->tags, 0);
 
-		sc->floor_xoffs = sc->floor_yoffs = 0;
-		sc->ceiling_xoffs = sc->ceiling_yoffs = 0;
+		sc->floorxoffset = sc->flooryoffset = 0;
+		sc->ceilingxoffset = sc->ceilingyoffset = 0;
 
-		sc->floorpic_angle = sc->ceilingpic_angle = 0;
+		sc->floorangle = sc->ceilingangle = 0;
 
 		sc->floorlightlevel = sc->ceilinglightlevel = 0;
 		sc->floorlightabsolute = sc->ceilinglightabsolute = false;
@@ -2828,6 +2974,8 @@ 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->toptexture = R_TextureNumForName("-");
 		sd->midtexture = R_TextureNumForName("-");
 		sd->bottomtexture = R_TextureNumForName("-");
@@ -2853,6 +3001,7 @@ static void P_LoadTextmap(void)
 		mt->extrainfo = 0;
 		Tag_FSet(&mt->tags, 0);
 		mt->scale = FRACUNIT;
+		mt->spritexscale = mt->spriteyscale = FRACUNIT;
 		memset(mt->args, 0, NUMMAPTHINGARGS*sizeof(*mt->args));
 		memset(mt->stringargs, 0x00, NUMMAPTHINGSTRINGARGS*sizeof(*mt->stringargs));
 		mt->mobj = NULL;
@@ -2880,15 +3029,20 @@ static void P_ProcessLinedefsAfterSidedefs(void)
 		case 332: // Trigger linedef executor: Skin - Each time
 		case 333: // Trigger linedef executor: Skin - Once
 		case 443: // Calls a named Lua function
-			if (sides[ld->sidenum[0]].text)
+			if (ld->stringargs[0] && ld->stringargs[1])
 			{
-				size_t len = strlen(sides[ld->sidenum[0]].text) + 1;
-				if (ld->sidenum[1] != 0xffff && sides[ld->sidenum[1]].text)
-					len += strlen(sides[ld->sidenum[1]].text);
-				ld->text = Z_Malloc(len, PU_LEVEL, NULL);
-				M_Memcpy(ld->text, sides[ld->sidenum[0]].text, strlen(sides[ld->sidenum[0]].text) + 1);
-				if (ld->sidenum[1] != 0xffff && sides[ld->sidenum[1]].text)
-					M_Memcpy(ld->text + strlen(ld->text) + 1, sides[ld->sidenum[1]].text, strlen(sides[ld->sidenum[1]].text) + 1);
+				size_t len[2];
+				len[0] = strlen(ld->stringargs[0]);
+				len[1] = strlen(ld->stringargs[1]);
+
+				if (len[1])
+				{
+					ld->stringargs[0] = Z_Realloc(ld->stringargs[0], len[0] + len[1] + 1, PU_LEVEL, NULL);
+					M_Memcpy(ld->stringargs[0] + len[0] + 1, ld->stringargs[1], len[1] + 1);
+				}
+
+				Z_Free(ld->stringargs[1]);
+				ld->stringargs[1] = NULL;
 			}
 			break;
 		case 447: // Change colormap
@@ -3176,10 +3330,17 @@ static nodetype_t P_GetNodetype(const virtres_t *virt, UINT8 **nodedata)
 	nodetype_t nodetype = NT_UNSUPPORTED;
 	char signature[4 + 1];
 
+	*nodedata = NULL;
+
 	if (udmf)
 	{
-		*nodedata = vres_Find(virt, "ZNODES")->data;
-		supported[NT_XGLN] = supported[NT_XGL3] = true;
+		virtlump_t *virtznodes = vres_Find(virt, "ZNODES");
+
+		if (virtznodes && virtznodes->size)
+		{
+			*nodedata = virtznodes->data;
+			supported[NT_XGLN] = supported[NT_XGL3] = true;
+		}
 	}
 	else
 	{
@@ -3188,24 +3349,39 @@ static nodetype_t P_GetNodetype(const virtres_t *virt, UINT8 **nodedata)
 
 		if (virtsegs && virtsegs->size)
 		{
-			*nodedata = vres_Find(virt, "NODES")->data;
-			return NT_DOOM; // Traditional map format BSP tree.
-		}
-
-		virtssectors = vres_Find(virt, "SSECTORS");
-
-		if (virtssectors && virtssectors->size)
-		{ // Possibly GL nodes: NODES ignored, SSECTORS takes precedence as nodes lump, (It is confusing yeah) and has a signature.
-			*nodedata = virtssectors->data;
-			supported[NT_XGLN] = supported[NT_ZGLN] = supported[NT_XGL3] = true;
+			virtlump_t *virtnodes = vres_Find(virt, "NODES");
+			if (virtnodes && virtnodes->size)
+			{
+				*nodedata = virtnodes->data;
+				return NT_DOOM; // Traditional map format BSP tree.
+			}
 		}
 		else
-		{ // Possibly ZDoom extended nodes: SSECTORS is empty, NODES has a signature.
-			*nodedata = vres_Find(virt, "NODES")->data;
-			supported[NT_XNOD] = supported[NT_ZNOD] = true;
+		{
+			virtssectors = vres_Find(virt, "SSECTORS");
+
+			if (virtssectors && virtssectors->size)
+			{ // Possibly GL nodes: NODES ignored, SSECTORS takes precedence as nodes lump, (It is confusing yeah) and has a signature.
+				*nodedata = virtssectors->data;
+				supported[NT_XGLN] = supported[NT_ZGLN] = supported[NT_XGL3] = true;
+			}
+			else
+			{ // Possibly ZDoom extended nodes: SSECTORS is empty, NODES has a signature.
+				virtlump_t *virtnodes = vres_Find(virt, "NODES");
+				if (virtnodes && virtnodes->size)
+				{
+					*nodedata = virtnodes->data;
+					supported[NT_XNOD] = supported[NT_ZNOD] = true;
+				}
+			}
 		}
 	}
 
+	if (*nodedata == NULL)
+	{
+		I_Error("Level has no nodes (does your map have at least 2 sectors?)");
+	}
+
 	M_Memcpy(signature, *nodedata, 4);
 	signature[4] = '\0';
 	(*nodedata) += 4;
@@ -3982,14 +4158,6 @@ static void P_AddBinaryMapTags(void)
 	}
 }
 
-static void P_WriteConstant(INT32 constant, char **target)
-{
-	char buffer[12];
-	sprintf(buffer, "%d", constant);
-	*target = Z_Malloc(strlen(buffer) + 1, PU_LEVEL, NULL);
-	M_Memcpy(*target, buffer, strlen(buffer) + 1);
-}
-
 static line_t *P_FindPointPushLine(taglist_t *list)
 {
 	INT32 i, l;
@@ -4132,7 +4300,6 @@ static void P_ConvertBinaryLinedefTypes(void)
 				lines[i].args[1] |= TMSP_NOTELEPORT;
 			if (lines[i].flags & ML_WRAPMIDTEX)
 				lines[i].args[1] |= TMSP_FORCESPIN;
-			P_WriteConstant(sides[lines[i].sidenum[0]].toptexture ? sides[lines[i].sidenum[0]].toptexture : sfx_spdpad, &lines[i].stringargs[0]);
 			break;
 		case 7: //Sector flat alignment
 			lines[i].args[0] = tag;
@@ -4218,8 +4385,6 @@ static void P_ConvertBinaryLinedefTypes(void)
 			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].flags & ML_SKEWTD);
-			if (sides[lines[i].sidenum[0]].toptexture)
-				P_WriteConstant(sides[lines[i].sidenum[0]].toptexture, &lines[i].stringargs[0]);
 			break;
 		case 16: //Minecart parameters
 			lines[i].args[0] = sides[lines[i].sidenum[0]].textureoffset >> FRACBITS;
@@ -4682,7 +4847,7 @@ static void P_ConvertBinaryLinedefTypes(void)
 				lines[i].args[2] = 16;
 			}
 			if (lines[i].flags & ML_MIDSOLID)
-				P_WriteConstant(sides[lines[i].sidenum[0]].textureoffset >> FRACBITS, &lines[i].stringargs[0]);
+				P_WriteSfx(sides[lines[i].sidenum[0]].textureoffset >> FRACBITS, &lines[i].stringargs[0]);
 			break;
 		case 252: //FOF: Shatter block
 		case 253: //FOF: Shatter block, translucent
@@ -4949,11 +5114,6 @@ static void P_ConvertBinaryLinedefTypes(void)
 			else
 				lines[i].args[0] = TMT_CONTINUOUS;
 			lines[i].args[1] = !!(lines[i].flags & ML_NOCLIMB);
-			if (lines[i].text)
-			{
-				lines[i].stringargs[0] = Z_Malloc(strlen(lines[i].text) + 1, PU_LEVEL, NULL);
-				M_Memcpy(lines[i].stringargs[0], lines[i].text, strlen(lines[i].text) + 1);
-			}
 			lines[i].special = 331;
 			break;
 		case 334: // Object dye - continuous
@@ -4966,11 +5126,6 @@ static void P_ConvertBinaryLinedefTypes(void)
 			else
 				lines[i].args[0] = TMT_CONTINUOUS;
 			lines[i].args[1] = !!(lines[i].flags & ML_NOCLIMB);
-			if (sides[lines[i].sidenum[0]].text)
-			{
-				lines[i].stringargs[0] = Z_Malloc(strlen(sides[lines[i].sidenum[0]].text) + 1, PU_LEVEL, NULL);
-				M_Memcpy(lines[i].stringargs[0], sides[lines[i].sidenum[0]].text, strlen(sides[lines[i].sidenum[0]].text) + 1);
-			}
 			lines[i].special = 334;
 			break;
 		case 337: //Emerald check - continuous
@@ -5117,11 +5272,6 @@ static void P_ConvertBinaryLinedefTypes(void)
 			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[6] = sides[lines[i].sidenum[0]].bottomtexture;
-			if (sides[lines[i].sidenum[0]].text)
-			{
-				lines[i].stringargs[0] = Z_Malloc(strlen(sides[lines[i].sidenum[0]].text) + 1, PU_LEVEL, NULL);
-				M_Memcpy(lines[i].stringargs[0], sides[lines[i].sidenum[0]].text, strlen(sides[lines[i].sidenum[0]].text) + 1);
-			}
 			break;
 		case 414: //Play sound effect
 			lines[i].args[2] = tag;
@@ -5161,11 +5311,6 @@ static void P_ConvertBinaryLinedefTypes(void)
 					lines[i].args[1] = TMSL_EVERYONE;
 				}
 			}
-			if (sides[lines[i].sidenum[0]].text)
-			{
-				lines[i].stringargs[0] = Z_Malloc(strlen(sides[lines[i].sidenum[0]].text) + 1, PU_LEVEL, NULL);
-				M_Memcpy(lines[i].stringargs[0], sides[lines[i].sidenum[0]].text, strlen(sides[lines[i].sidenum[0]].text) + 1);
-			}
 			break;
 		case 415: //Run script
 		{
@@ -5256,13 +5401,6 @@ static void P_ConvertBinaryLinedefTypes(void)
 			lines[i].args[0] = sides[lines[i].sidenum[0]].textureoffset >> FRACBITS;
 			lines[i].args[1] = !!(lines[i].flags & ML_NOCLIMB);
 			break;
-		case 425: //Change object state
-			if (sides[lines[i].sidenum[0]].text)
-			{
-				lines[i].stringargs[0] = Z_Malloc(strlen(sides[lines[i].sidenum[0]].text) + 1, PU_LEVEL, NULL);
-				M_Memcpy(lines[i].stringargs[0], sides[lines[i].sidenum[0]].text, strlen(sides[lines[i].sidenum[0]].text) + 1);
-			}
-			break;
 		case 426: //Stop object
 			lines[i].args[0] = !!(lines[i].flags & ML_NOCLIMB);
 			break;
@@ -5302,18 +5440,18 @@ static void P_ConvertBinaryLinedefTypes(void)
 			lines[i].args[2] = !!(lines[i].flags & ML_BLOCKMONSTERS);
 			break;
 		case 434: //Award power-up
-			if (sides[lines[i].sidenum[0]].text)
-			{
-				lines[i].stringargs[0] = Z_Malloc(strlen(sides[lines[i].sidenum[0]].text) + 1, PU_LEVEL, NULL);
-				M_Memcpy(lines[i].stringargs[0], sides[lines[i].sidenum[0]].text, strlen(sides[lines[i].sidenum[0]].text) + 1);
-			}
-			if (lines[i].sidenum[1] != 0xffff && lines[i].flags & ML_BLOCKMONSTERS) // read power from back sidedef
+			if ((lines[i].flags & ML_BLOCKMONSTERS) == 0)
 			{
-				lines[i].stringargs[1] = Z_Malloc(strlen(sides[lines[i].sidenum[1]].text) + 1, PU_LEVEL, NULL);
-				M_Memcpy(lines[i].stringargs[1], sides[lines[i].sidenum[1]].text, strlen(sides[lines[i].sidenum[1]].text) + 1);
+				// do NOT read power from back sidedef if this flag is not present
+				if (lines[i].stringargs[1] != NULL)
+				{
+					Z_Free(lines[i].stringargs[1]);
+					lines[i].stringargs[1] = NULL;
+				}
+
+				// Instead... write the desired time!
+				P_WriteTics((lines[i].flags & ML_NOCLIMB) ? -1 : (sides[lines[i].sidenum[0]].textureoffset >> FRACBITS), &lines[i].stringargs[1]);
 			}
-			else
-				P_WriteConstant((lines[i].flags & ML_NOCLIMB) ? -1 : (sides[lines[i].sidenum[0]].textureoffset >> FRACBITS), &lines[i].stringargs[1]);
 			break;
 		case 435: //Change plane scroller direction
 			lines[i].args[0] = tag;
@@ -5341,30 +5479,10 @@ static void P_ConvertBinaryLinedefTypes(void)
 			break;
 		case 442: //Change object type state
 			lines[i].args[0] = tag;
-			if (sides[lines[i].sidenum[0]].text)
-			{
-				lines[i].stringargs[0] = Z_Malloc(strlen(sides[lines[i].sidenum[0]].text) + 1, PU_LEVEL, NULL);
-				M_Memcpy(lines[i].stringargs[0], sides[lines[i].sidenum[0]].text, strlen(sides[lines[i].sidenum[0]].text) + 1);
-			}
-			if (lines[i].sidenum[1] == 0xffff)
-				lines[i].args[1] = 1;
-			else
-			{
-				lines[i].args[1] = 0;
-				if (sides[lines[i].sidenum[1]].text)
-				{
-					lines[i].stringargs[1] = Z_Malloc(strlen(sides[lines[i].sidenum[1]].text) + 1, PU_LEVEL, NULL);
-					M_Memcpy(lines[i].stringargs[1], sides[lines[i].sidenum[1]].text, strlen(sides[lines[i].sidenum[1]].text) + 1);
-				}
-			}
+			lines[i].args[1] = (lines[i].sidenum[1] == 0xffff) ? 1 : 0;
 			break;
 		case 443: //Call Lua function
-			if (lines[i].text)
-			{
-				lines[i].stringargs[0] = Z_Malloc(strlen(lines[i].text) + 1, PU_LEVEL, NULL);
-				M_Memcpy(lines[i].stringargs[0], lines[i].text, strlen(lines[i].text) + 1);
-			}
-			else
+			if (lines[i].stringargs[0] == NULL)
 				CONS_Alert(CONS_WARNING, "Linedef %s is missing the hook name of the Lua function to call! (This should be given in the front texture fields)\n", sizeu1(i));
 			break;
 		case 444: //Earthquake
@@ -5517,11 +5635,6 @@ static void P_ConvertBinaryLinedefTypes(void)
 			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;
-			if (sides[lines[i].sidenum[0]].text)
-			{
-				lines[i].stringargs[0] = Z_Malloc(strlen(sides[lines[i].sidenum[0]].text) + 1, PU_LEVEL, NULL);
-				M_Memcpy(lines[i].stringargs[0], sides[lines[i].sidenum[0]].text, strlen(sides[lines[i].sidenum[0]].text) + 1);
-			}
 			break;
 		case 460: //Award rings
 			lines[i].args[0] = sides[lines[i].sidenum[0]].textureoffset >> FRACBITS;
@@ -5549,18 +5662,6 @@ static void P_ConvertBinaryLinedefTypes(void)
 			}
 			else
 				lines[i].args[4] = 0;
-			if (sides[lines[i].sidenum[0]].text)
-			{
-				lines[i].stringargs[0] = Z_Malloc(strlen(sides[lines[i].sidenum[0]].text) + 1, PU_LEVEL, NULL);
-				M_Memcpy(lines[i].stringargs[0], sides[lines[i].sidenum[0]].text, strlen(sides[lines[i].sidenum[0]].text) + 1);
-			}
-			break;
-		case 463: //Dye object
-			if (sides[lines[i].sidenum[0]].text)
-			{
-				lines[i].stringargs[0] = Z_Malloc(strlen(sides[lines[i].sidenum[0]].text) + 1, PU_LEVEL, NULL);
-				M_Memcpy(lines[i].stringargs[0], sides[lines[i].sidenum[0]].text, strlen(sides[lines[i].sidenum[0]].text) + 1);
-			}
 			break;
 		case 464: //Trigger egg capsule
 			lines[i].args[0] = tag;
@@ -6389,7 +6490,7 @@ static void P_ConvertBinaryThingTypes(void)
 			break;
 		case 543: //Balloon
 			if (mapthings[i].angle > 0)
-				P_WriteConstant(((mapthings[i].angle - 1) % (numskincolors - 1)) + 1, &mapthings[i].stringargs[0]);
+				P_WriteSkincolor(((mapthings[i].angle - 1) % (numskincolors - 1)) + 1, &mapthings[i].stringargs[0]);
 			mapthings[i].args[0] = !!(mapthings[i].options & MTF_AMBUSH);
 			break;
 		case 555: //Diagonal yellow spring
@@ -6414,22 +6515,22 @@ static void P_ConvertBinaryThingTypes(void)
 		case 706: //Water ambience G
 		case 707: //Water ambience H
 			mapthings[i].args[0] = 35;
-			P_WriteConstant(sfx_amwtr1 + mapthings[i].type - 700, &mapthings[i].stringargs[0]);
+			P_WriteSfx(sfx_amwtr1 + mapthings[i].type - 700, &mapthings[i].stringargs[0]);
 			mapthings[i].type = 700;
 			break;
 		case 708: //Disco ambience
 			mapthings[i].args[0] = 512;
-			P_WriteConstant(sfx_ambint, &mapthings[i].stringargs[0]);
+			P_WriteSfx(sfx_ambint, &mapthings[i].stringargs[0]);
 			mapthings[i].type = 700;
 			break;
 		case 709: //Volcano ambience
 			mapthings[i].args[0] = 220;
-			P_WriteConstant(sfx_ambin2, &mapthings[i].stringargs[0]);
+			P_WriteSfx(sfx_ambin2, &mapthings[i].stringargs[0]);
 			mapthings[i].type = 700;
 			break;
 		case 710: //Machine ambience
 			mapthings[i].args[0] = 24;
-			P_WriteConstant(sfx_ambmac, &mapthings[i].stringargs[0]);
+			P_WriteSfx(sfx_ambmac, &mapthings[i].stringargs[0]);
 			mapthings[i].type = 700;
 			break;
 		case 750: //Slope vertex
@@ -6492,8 +6593,7 @@ static void P_ConvertBinaryThingTypes(void)
 			mapthings[i].args[3] = sides[lines[j].sidenum[0]].rowoffset >> FRACBITS;
 			mapthings[i].args[4] = lines[j].backsector ? sides[lines[j].sidenum[1]].textureoffset >> FRACBITS : 0;
 			mapthings[i].args[6] = mapthings[i].angle;
-			if (sides[lines[j].sidenum[0]].toptexture)
-				P_WriteConstant(sides[lines[j].sidenum[0]].toptexture, &mapthings[i].stringargs[0]);
+			P_WriteDuplicateText(lines[j].stringargs[0], &mapthings[i].stringargs[0]);
 			break;
 		}
 		case 762: //PolyObject spawn point (crush)
@@ -6582,8 +6682,8 @@ static void P_ConvertBinaryThingTypes(void)
 				mapthings[i].args[8] |= TMM_ALWAYSTHINK;
 			if (mapthings[i].type == 1110)
 			{
-				P_WriteConstant(sides[lines[j].sidenum[0]].toptexture, &mapthings[i].stringargs[0]);
-				P_WriteConstant(lines[j].backsector ? sides[lines[j].sidenum[1]].toptexture : MT_NULL, &mapthings[i].stringargs[1]);
+				P_WriteDuplicateText(lines[j].stringargs[0], &mapthings[i].stringargs[0]);
+				P_WriteDuplicateText(lines[j].stringargs[1], &mapthings[i].stringargs[1]);
 			}
 			break;
 		}
@@ -6624,7 +6724,13 @@ static void P_ConvertBinaryThingTypes(void)
 			mapthings[i].args[0] = P_AproxDistance(lines[j].dx, lines[j].dy) >> FRACBITS;
 			mapthings[i].args[1] = sides[lines[j].sidenum[0]].textureoffset >> FRACBITS;
 			mapthings[i].args[2] = !!(lines[j].flags & ML_NOCLIMB);
-			P_WriteConstant(MT_ROCKCRUMBLE1 + (sides[lines[j].sidenum[0]].rowoffset >> FRACBITS), &mapthings[i].stringargs[0]);
+			INT32 id = (sides[lines[j].sidenum[0]].rowoffset >> FRACBITS);
+			// Rather than introduce deh_tables.h as a dependency for literally one
+			// conversion, we just... recreate the string expected to be produced.
+			if (id > 0 && id < 16)
+				P_WriteDuplicateText(va("MT_ROCKCRUMBLE%d", id+1), &mapthings[i].stringargs[0]);
+			else
+				P_WriteConstant(MT_ROCKCRUMBLE1 + id, &mapthings[i].stringargs[0], "Mapthing", i);
 			break;
 		}
 		case 1221: //Minecart saloon door
@@ -6716,6 +6822,9 @@ static void P_ConvertBinaryThingTypes(void)
 		default:
 			break;
 		}
+		
+		// Clear binary thing height hacks, to prevent interfering with UDMF-only flags
+		mapthings[i].options &= 0xF;
 	}
 }
 
@@ -6757,6 +6866,7 @@ static void P_ConvertBinaryLinedefFlags(void)
 //For maps in binary format, converts setup of specials to UDMF format.
 static void P_ConvertBinaryMap(void)
 {
+	contextdrift = false;
 	P_ConvertBinaryLinedefTypes();
 	P_ConvertBinarySectorTypes();
 	P_ConvertBinaryThingTypes();
@@ -6956,7 +7066,7 @@ static void P_InitLevelSettings(void)
 	stagefailed = G_IsSpecialStage(gamemap);
 
 	// Reset temporary record data
-	memset(&ntemprecords, 0, sizeof(nightsdata_t));
+	memset(&ntemprecords, 0, sizeof(ntemprecords));
 
 	// earthquake camera
 	memset(&quake,0,sizeof(struct quake));
@@ -7066,37 +7176,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;
 	}
 }
 
@@ -7407,6 +7503,7 @@ static void P_RunSpecialStageWipe(void)
 		lastwipetic = nowtime;
 		if (moviemode) // make sure we save frames for the white hold too
 			M_SaveFrame();
+		NetKeepAlive(); // Prevent timeout
 	}
 }
 
@@ -7460,7 +7557,7 @@ static void P_WriteLetter(void)
 {
 	char *buf, *b;
 
-	if (!unlockables[28].unlocked) // pandora's box
+	if (!serverGamedata->unlocked[28]) // pandora's box
 		return;
 
 	if (modeattacking)
@@ -7711,18 +7808,6 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	R_InitMobjInterpolators();
 	P_InitCachedActions();
 
-	if (!fromnetsave && savedata.lives > 0)
-	{
-		numgameovers = savedata.numgameovers;
-		players[consoleplayer].continues = savedata.continues;
-		players[consoleplayer].lives = savedata.lives;
-		players[consoleplayer].score = savedata.score;
-		if ((botingame = ((botskin = savedata.botskin) != 0)))
-			botcolor = skins[botskin-1].prefcolor;
-		emeralds = savedata.emeralds;
-		savedata.lives = 0;
-	}
-
 	// internal game map
 	maplumpname = G_BuildMapName(gamemap);
 	lastloadedmaplumpnum = W_CheckNumForMap(maplumpname);
@@ -7804,10 +7889,11 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	nextmapoverride = 0;
 	skipstats = 0;
 
-	if (!(netgame || multiplayer || demoplayback))
-		mapvisited[gamemap-1] |= MV_VISITED;
-	else if (netgame || multiplayer)
-		mapvisited[gamemap-1] |= MV_MP; // you want to record that you've been there this session, but not permanently
+	if (!demoplayback)
+	{
+		clientGamedata->mapvisited[gamemap-1] |= MV_VISITED;
+		serverGamedata->mapvisited[gamemap-1] |= MV_VISITED;
+	}
 
 	levelloading = false;
 
@@ -8153,7 +8239,7 @@ static boolean P_LoadAddon(UINT16 numlumps)
 	{
 		CONS_Printf(M_GetText("Current map %d replaced by added file, ending the level to ensure consistency.\n"), gamemap);
 		if (server)
-			SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+			D_SendExitLevel(false);
 	}
 
 	return true;
diff --git a/src/p_slopes.c b/src/p_slopes.c
index 48a13a07d61bc88b45a67ce4d5a0e181b31b53f6..1c0ee81a7e9453d204049d2f0ab5986a07849edb 100644
--- a/src/p_slopes.c
+++ b/src/p_slopes.c
@@ -328,7 +328,7 @@ static void line_SpawnViaLine(const int linenum, const boolean spawnthinker)
 
 	if(!frontfloor && !backfloor && !frontceil && !backceil)
 	{
-		CONS_Printf("line_SpawnViaLine: Slope special with nothing to do.\n");
+		CONS_Printf("line_SpawnViaLine: Unused slope special with nothing to do on line number %i\n", linenum);
 		return;
 	}
 
@@ -566,6 +566,7 @@ static void line_SpawnViaMapthingVertexes(const int linenum, const boolean spawn
 	case TMSP_BACKCEILING:
 		slopetoset = &line->backsector->c_slope;
 		side = &sides[line->sidenum[1]];
+		break;
 	default:
 		return;
 	}
@@ -899,6 +900,8 @@ void P_HandleSlopeLanding(mobj_t *thing, pslope_t *slope)
 		if (P_MobjFlip(thing)*(thing->momz) < 0) // falling, land on slope
 		{
 			thing->standingslope = slope;
+			P_SetPitchRollFromSlope(thing, slope);
+
 			if (!thing->player || !(thing->player->pflags & PF_BOUNCING))
 				thing->momz = -P_MobjFlip(thing);
 		}
@@ -915,6 +918,7 @@ void P_HandleSlopeLanding(mobj_t *thing, pslope_t *slope)
 		thing->momx = mom.x;
 		thing->momy = mom.y;
 		thing->standingslope = slope;
+		P_SetPitchRollFromSlope(thing, slope);
 		if (!thing->player || !(thing->player->pflags & PF_BOUNCING))
 			thing->momz = -P_MobjFlip(thing);
 	}
diff --git a/src/p_spec.c b/src/p_spec.c
index aa04a723eb95be92cd6d323742300921c634be9a..28ecc60f4dedb5f67f9aa33e092232eb0b3782f6 100644
--- a/src/p_spec.c
+++ b/src/p_spec.c
@@ -1741,14 +1741,13 @@ static boolean P_ActivateLinedefExecutorsInSector(line_t *triggerline, mobj_t *a
 
 /** Used by P_LinedefExecute to check a trigger linedef's conditions
   * The linedef executor specials in the trigger linedef's sector are run if all conditions are met.
-  * Return false cancels P_LinedefExecute, this happens if a condition is not met.
   *
   * \param triggerline Trigger linedef to check conditions for; should NEVER be NULL.
   * \param actor Object initiating the action; should not be NULL.
   * \param caller Sector in which the action was started. May be NULL.
   * \sa P_ProcessLineSpecial, P_LinedefExecute
   */
-boolean P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller)
+void P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller)
 {
 	INT16 specialtype = triggerline->special;
 
@@ -1761,12 +1760,12 @@ boolean P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller
 		if (caller->triggerer == TO_PLAYEREMERALDS)
 		{
 			if (!(ALL7EMERALDS(emeralds)))
-				return false;
+				return;
 		}
 		else if (caller->triggerer == TO_PLAYERNIGHTS)
 		{
 			if (!P_CheckPlayerMareOld(triggerline))
-				return false;
+				return;
 		}
 	}
 
@@ -1774,51 +1773,47 @@ boolean P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller
 	{
 		case 303:
 			if (!P_CheckPlayerRings(triggerline, actor))
-				return false;
+				return;
 			break;
 		case 305:
 			if (!(actor && actor->player && actor->player->charability == triggerline->args[1]))
-				return false;
+				return;
 			break;
 		case 309:
 			// Only red/blue team members can activate this.
 			if (!(actor && actor->player))
-				return false;
+				return;
 			if (actor->player->ctfteam != ((triggerline->args[1] == TMT_RED) ? 1 : 2))
-				return false;
+				return;
 			break;
 		case 314:
 			if (!P_CheckPushables(triggerline, caller))
-				return false;
+				return;
 			break;
 		case 317:
 			{ // Unlockable triggers required
 				INT32 trigid = triggerline->args[1];
 
-				if (netgame || multiplayer)
-					return false;
-				else if (trigid < 0 || trigid > 31) // limited by 32 bit variable
+				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);
-					return false;
+					return;
 				}
 				else if (!(unlocktriggers & (1 << trigid)))
-					return false;
+					return;
 			}
 			break;
 		case 319:
 			{ // An unlockable itself must be unlocked!
 				INT32 unlockid = triggerline->args[1];
 
-				if (netgame || multiplayer)
-					return false;
-				else if (unlockid < 0 || unlockid >= MAXUNLOCKABLES) // limited by unlockable count
+				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);
-					return false;
+					return;
 				}
-				else if (!(unlockables[unlockid-1].unlocked))
-					return false;
+				else if (!(serverGamedata->unlocked[unlockid-1]))
+					return;
 			}
 			break;
 		case 321:
@@ -1826,7 +1821,7 @@ boolean P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller
 			if (triggerline->callcount > 0)
 			{
 				if (--triggerline->callcount > 0)
-					return false;
+					return;
 			}
 			break;
 		case 323: // nightserize
@@ -1834,15 +1829,15 @@ boolean P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller
 		case 327: // nights lap
 		case 329: // nights egg capsule touch
 			if (!P_CheckNightsTriggerLine(triggerline, actor))
-				return false;
+				return;
 			break;
 		case 331:
 			if (!(actor && actor->player))
-				return false;
+				return;
 			if (!triggerline->stringargs[0])
-				return false;
+				return;
 			if (!(stricmp(triggerline->stringargs[0], skins[actor->player->skin].name) == 0) ^ !!(triggerline->args[1]))
-				return false;
+				return;
 			break;
 		case 334: // object dye
 			{
@@ -1850,22 +1845,22 @@ boolean P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller
 				UINT16 color = (actor->player ? actor->player->powers[pw_dye] : actor->color);
 
 				if (!!(triggerline->args[1]) ^ (triggercolor != color))
-					return false;
+					return;
 			}
 			break;
 		case 337: // emerald check
 			if (!P_CheckEmeralds(triggerline->args[2], (UINT16)triggerline->args[1]))
-				return false;
+				return;
 			break;
 		case 340: // NiGHTS mare
 			if (!P_CheckPlayerMare(triggerline))
-				return false;
+				return;
 			break;
 		case 343: // gravity check
 			if (triggerline->args[1] == TMG_TEMPREVERSE && (!(actor->flags2 & MF2_OBJECTFLIP) != !(actor->player->powers[pw_gravityboots])))
-				return false;
+				return;
 			if ((triggerline->args[1] == TMG_NORMAL) != !(actor->eflags & MFE_VERTICALFLIP))
-				return false;
+				return;
 			break;
 		default:
 			break;
@@ -1876,7 +1871,7 @@ boolean P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller
 	/////////////////////////////////
 
 	if (!P_ActivateLinedefExecutorsInSector(triggerline, actor, caller))
-		return false;
+		return;
 
 	// "Trigger on X calls" linedefs reset if args[2] is set
 	if (specialtype == 321 && triggerline->args[2])
@@ -1909,8 +1904,6 @@ boolean P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller
 			&& triggerline->args[0] == TMT_ONCE)
 			triggerline->special = 0;
 	}
-
-	return true;
 }
 
 /** Runs a linedef executor.
@@ -1963,8 +1956,7 @@ void P_LinedefExecute(INT16 tag, mobj_t *actor, sector_t *caller)
 		if (lines[masterline].special == 321 && lines[masterline].args[0] > TMXT_EACHTIMEMASK) // Trigger after X calls
 			continue;
 
-		if (!P_RunTriggerLinedef(&lines[masterline], actor, caller))
-			return; // cancel P_LinedefExecute if function returns false
+		P_RunTriggerLinedef(&lines[masterline], actor, caller); // Even if it fails, there might be more linedefs to trigger
 	}
 }
 
@@ -2569,11 +2561,13 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				// Change the music and apply position/fade operations
 				else
 				{
-					if (!line->stringargs[0])
-						break;
-
-					strncpy(mapmusname, line->stringargs[0], 7);
-					mapmusname[6] = 0;
+					if (!line->stringargs[0] || !strcmp(line->stringargs[0], "-"))
+						strcpy(mapmusname, "");
+					else
+					{
+						strncpy(mapmusname, line->stringargs[0], 7);
+						mapmusname[6] = 0;
+					}
 
 					mapmusflags = tracknum & MUSIC_TRACKMASK;
 					if (!(line->args[0] & TMM_NORELOAD))
@@ -2665,10 +2659,13 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				// This is not revoked until overwritten; awayviewtics is ignored
 				if (titlemapinaction)
 					titlemapcameraref = altview;
-				else
-				{
+				else if (!mo->player->awayviewtics || mo->player->awayviewmobj != altview) {
 					P_SetTarget(&mo->player->awayviewmobj, altview);
-					mo->player->awayviewtics = line->args[1];
+					
+					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])
+						P_ResetCamera(mo->player, &camera2);  // reset p2 camera on p2 getting an awayviewmobj
 				}
 
 				aim = udmf ? altview->spawnpoint->pitch : line->args[2];
@@ -2678,8 +2675,10 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 				aim <<= 8;
 				if (titlemapinaction)
 					titlemapcameraref->cusval = (angle_t)aim;
-				else
+				else {
 					mo->player->awayviewaiming = (angle_t)aim;
+					mo->player->awayviewtics = line->args[1];
+				}
 			}
 			break;
 
@@ -2937,7 +2936,6 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 			break;
 
 		case 441: // Trigger unlockable
-			if (!(netgame || multiplayer))
 			{
 				INT32 trigid = line->args[0];
 
@@ -2948,10 +2946,12 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 					unlocktriggers |= 1 << trigid;
 
 					// Unlocked something?
-					if (M_UpdateUnlockablesAndExtraEmblems())
+					M_SilentUpdateUnlockablesAndEmblems(serverGamedata);
+
+					if (M_UpdateUnlockablesAndExtraEmblems(clientGamedata))
 					{
 						S_StartSound(NULL, sfx_s3k68);
-						G_SaveGameData(); // only save if unlocked something
+						G_SaveGameData(clientGamedata); // only save if unlocked something
 					}
 				}
 			}
@@ -3286,19 +3286,18 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 						foundrover = true;
 
 						// If fading an invisible FOF whose render flags we did not yet set,
-						// initialize its alpha to 1
-						// for relative alpha calc
+						// initialize its alpha to 0 for relative alpha calculation
 						if (!(line->args[3] & TMST_DONTDOTRANSLUCENT) &&      // do translucent
 							(rover->spawnflags & FOF_NOSHADE) && // do not include light blocks, which don't set FOF_NOSHADE
 							!(rover->spawnflags & FOF_RENDERSIDES) &&
 							!(rover->spawnflags & FOF_RENDERPLANES) &&
 							!(rover->fofflags & FOF_RENDERALL))
-							rover->alpha = 1;
+							rover->alpha = 0;
 
 						P_RemoveFakeFloorFader(rover);
 						P_FadeFakeFloor(rover,
 							rover->alpha,
-							max(1, min(256, (line->args[3] & TMST_RELATIVE) ? rover->alpha + destvalue : destvalue)),
+							max(0, min(255, (line->args[3] & TMST_RELATIVE) ? rover->alpha + destvalue : destvalue)),
 							0,                                         // set alpha immediately
 							false, NULL,                               // tic-based logic
 							false,                                     // do not handle FOF_EXISTS
@@ -3372,19 +3371,18 @@ static void P_ProcessLineSpecial(line_t *line, mobj_t *mo, sector_t *callsec)
 						else
 						{
 							// If fading an invisible FOF whose render flags we did not yet set,
-							// initialize its alpha to 1
-							// for relative alpha calc
+							// initialize its alpha to 1 for relative alpha calculation
 							if (!(line->args[4] & TMFT_DONTDOTRANSLUCENT) &&      // do translucent
 								(rover->spawnflags & FOF_NOSHADE) && // do not include light blocks, which don't set FOF_NOSHADE
 								!(rover->spawnflags & FOF_RENDERSIDES) &&
 								!(rover->spawnflags & FOF_RENDERPLANES) &&
 								!(rover->fofflags & FOF_RENDERALL))
-								rover->alpha = 1;
+								rover->alpha = 0;
 
 							P_RemoveFakeFloorFader(rover);
 							P_FadeFakeFloor(rover,
 								rover->alpha,
-								max(1, min(256, (line->args[4] & TMFT_RELATIVE) ? rover->alpha + destvalue : destvalue)),
+								max(0, min(255, (line->args[4] & TMFT_RELATIVE) ? rover->alpha + destvalue : destvalue)),
 								0,                                         // set alpha immediately
 								false, NULL,                               // tic-based logic
 								!(line->args[4] & TMFT_DONTDOEXISTS),      // do not handle FOF_EXISTS
@@ -5596,17 +5594,17 @@ static ffloor_t *P_AddFakeFloor(sector_t *sec, sector_t *sec2, line_t *master, I
 	fflr->target = sec;
 	fflr->bottomheight = &sec2->floorheight;
 	fflr->bottompic = &sec2->floorpic;
-	fflr->bottomxoffs = &sec2->floor_xoffs;
-	fflr->bottomyoffs = &sec2->floor_yoffs;
-	fflr->bottomangle = &sec2->floorpic_angle;
+	fflr->bottomxoffs = &sec2->floorxoffset;
+	fflr->bottomyoffs = &sec2->flooryoffset;
+	fflr->bottomangle = &sec2->floorangle;
 
 	// Add the ceiling
 	fflr->topheight = &sec2->ceilingheight;
 	fflr->toppic = &sec2->ceilingpic;
 	fflr->toplightlevel = &sec2->lightlevel;
-	fflr->topxoffs = &sec2->ceiling_xoffs;
-	fflr->topyoffs = &sec2->ceiling_yoffs;
-	fflr->topangle = &sec2->ceilingpic_angle;
+	fflr->topxoffs = &sec2->ceilingxoffset;
+	fflr->topyoffs = &sec2->ceilingyoffset;
+	fflr->topangle = &sec2->ceilingangle;
 
 	// Add slopes
 	fflr->t_slope = &sec2->c_slope;
@@ -6122,16 +6120,16 @@ void P_ApplyFlatAlignment(sector_t *sector, angle_t flatangle, fixed_t xoffs, fi
 {
 	if (floor)
 	{
-		sector->floorpic_angle = flatangle;
-		sector->floor_xoffs += xoffs;
-		sector->floor_yoffs += yoffs;
+		sector->floorangle = flatangle;
+		sector->floorxoffset += xoffs;
+		sector->flooryoffset += yoffs;
 	}
 
 	if (ceiling)
 	{
-		sector->ceilingpic_angle = flatangle;
-		sector->ceiling_xoffs += xoffs;
-		sector->ceiling_yoffs += yoffs;
+		sector->ceilingangle = flatangle;
+		sector->ceilingxoffset += xoffs;
+		sector->ceilingyoffset += yoffs;
 	}
 
 }
@@ -6565,10 +6563,10 @@ void P_SpawnSpecials(boolean fromnetsave)
 				//Cutting options
 				if (ffloorflags & FOF_RENDERALL)
 				{
-					//If inside is visible, cut inner walls
-					if ((lines[i].args[1] < 255) || (lines[i].args[3] & TMFA_SPLAT) || (lines[i].args[4] & TMFT_VISIBLEFROMINSIDE))
+					//If inside is visible from the outside, cut inner walls
+					if (lines[i].args[1] < 255 || (lines[i].args[3] & TMFA_SPLAT))
 						ffloorflags |= FOF_CUTEXTRA|FOF_EXTRA;
-					else
+					else if (!(lines[i].args[4] & TMFT_VISIBLEFROMINSIDE))
 						ffloorflags |= FOF_CUTLEVEL;
 				}
 
@@ -6624,20 +6622,19 @@ void P_SpawnSpecials(boolean fromnetsave)
 				if (lines[i].args[4] & TMFC_SPLAT)
 					ffloorflags |= FOF_SPLAT;
 
-				//If inside is visible, cut inner walls
-				if (lines[i].args[1] < 0xff || (lines[i].args[3] & TMFT_VISIBLEFROMINSIDE) || (lines[i].args[4] & TMFC_SPLAT))
+				//If inside is visible from the outside, cut inner walls
+				if (lines[i].args[1] < 255 || (lines[i].args[4] & TMFC_SPLAT))
 					ffloorflags |= FOF_CUTEXTRA|FOF_EXTRA;
-				else
-					ffloorflags |= FOF_CUTLEVEL;
-
-				//If player can enter it, render insides
-				if (lines[i].args[3] & TMFT_VISIBLEFROMINSIDE)
+				//If player can view it from the inside, render insides
+				else if (lines[i].args[3] & TMFT_VISIBLEFROMINSIDE)
 				{
 					if (ffloorflags & FOF_RENDERPLANES)
 						ffloorflags |= FOF_BOTHPLANES;
 					if (ffloorflags & FOF_RENDERSIDES)
 						ffloorflags |= FOF_ALLSIDES;
 				}
+				else
+					ffloorflags |= FOF_CUTLEVEL;
 
 				P_AddFakeFloorsByLine(i, lines[i].args[1], lines[i].args[2], ffloorflags, secthinkers);
 				if (lines[i].args[4] & TMFC_AIRBOB)
@@ -6688,10 +6685,10 @@ void P_SpawnSpecials(boolean fromnetsave)
 				//Cutting options
 				if (ffloorflags & FOF_RENDERALL)
 				{
-					//If inside is visible, cut inner walls
-					if ((lines[i].args[1] < 255) || (lines[i].args[3] & TMFA_SPLAT) || (lines[i].args[4] & TMFT_VISIBLEFROMINSIDE))
+					//If inside is visible from the outside, cut inner walls
+					if (lines[i].args[1] < 255 || (lines[i].args[3] & TMFA_SPLAT))
 						ffloorflags |= FOF_CUTEXTRA|FOF_EXTRA;
-					else
+					else if (!(lines[i].args[4] & TMFT_VISIBLEFROMINSIDE))
 						ffloorflags |= FOF_CUTLEVEL;
 				}
 
@@ -6883,71 +6880,6 @@ void P_SpawnSpecials(boolean fromnetsave)
 				}
 				break;
 
-			case 260: // GZDoom-like 3D Floor.
-				{
-					UINT8 dtype = lines[i].args[1] & 3;
-					UINT8 dflags1 = lines[i].args[1] - dtype;
-					UINT8 dflags2 = lines[i].args[2];
-					UINT8 dopacity = lines[i].args[3];
-					boolean isfog = false;
-
-					if (dtype == 0)
-						dtype = 1;
-
-					ffloorflags = FOF_EXISTS;
-
-					if (dflags2 & 1) ffloorflags |= FOF_NOSHADE; // Disable light effects (Means no shadowcast)
-					if (dflags2 & 2) ffloorflags |= FOF_DOUBLESHADOW; // Restrict light inside (Means doubleshadow)
-					if (dflags2 & 4) isfog = true; // Fog effect (Explicitly render like a fog block)
-
-					if (dflags1 & 4) ffloorflags |= FOF_BOTHPLANES|FOF_ALLSIDES; // Render-inside
-					if (dflags1 & 16) ffloorflags |= FOF_INVERTSIDES|FOF_INVERTPLANES; // Invert visibility rules
-
-					// Fog block
-					if (isfog)
-						ffloorflags |= FOF_RENDERALL|FOF_CUTEXTRA|FOF_CUTSPRITES|FOF_BOTHPLANES|FOF_EXTRA|FOF_FOG|FOF_INVERTPLANES|FOF_ALLSIDES|FOF_INVERTSIDES;
-					else
-					{
-						ffloorflags |= FOF_RENDERALL;
-
-						// Solid
-						if (dtype == 1)
-							ffloorflags |= FOF_SOLID|FOF_CUTLEVEL;
-						// Water
-						else if (dtype == 2)
-							ffloorflags |= FOF_SWIMMABLE|FOF_CUTEXTRA|FOF_CUTSPRITES|FOF_EXTRA|FOF_RIPPLE;
-						// Intangible
-						else if (dtype == 3)
-							ffloorflags |= FOF_CUTEXTRA|FOF_CUTSPRITES|FOF_EXTRA;
-					}
-
-					// Non-opaque
-					if (dopacity < 255)
-					{
-						// Invisible
-						if (dopacity == 0)
-						{
-							// True invisible
-							if (ffloorflags & FOF_NOSHADE)
-								ffloorflags &= ~(FOF_RENDERALL|FOF_CUTEXTRA|FOF_CUTSPRITES|FOF_EXTRA|FOF_BOTHPLANES|FOF_ALLSIDES|FOF_CUTLEVEL);
-							// Shadow block
-							else
-							{
-								ffloorflags |= FOF_CUTSPRITES;
-								ffloorflags &= ~(FOF_RENDERALL|FOF_CUTEXTRA|FOF_EXTRA|FOF_BOTHPLANES|FOF_ALLSIDES|FOF_CUTLEVEL);
-							}
-						}
-						else
-						{
-							ffloorflags |= FOF_TRANSLUCENT|FOF_CUTEXTRA|FOF_EXTRA;
-							ffloorflags &= ~FOF_CUTLEVEL;
-						}
-					}
-
-					P_AddFakeFloorsByLine(i, dopacity, TMB_TRANSLUCENT, ffloorflags, secthinkers);
-				}
-				break;
-
 			case 300: // Trigger linedef executor
 			case 303: // Count rings
 			case 305: // Character ability
@@ -7366,14 +7298,14 @@ void T_Scroll(scroll_t *s)
 
 		case sc_floor: // scroll floor texture
 			sec = sectors + s->affectee;
-			sec->floor_xoffs += dx;
-			sec->floor_yoffs += dy;
+			sec->floorxoffset += dx;
+			sec->flooryoffset += dy;
 			break;
 
 		case sc_ceiling: // scroll ceiling texture
 			sec = sectors + s->affectee;
-			sec->ceiling_xoffs += dx;
-			sec->ceiling_yoffs += dy;
+			sec->ceilingxoffset += dx;
+			sec->ceilingyoffset += dy;
 			break;
 
 		case sc_carry:
@@ -7821,15 +7753,14 @@ static boolean P_FadeFakeFloor(ffloor_t *rover, INT16 sourcevalue, INT16 destval
 	if (rover->master->special == 258) // Laser block
 		return false;
 
-	// If fading an invisible FOF whose render flags we did not yet set,
-	// initialize its alpha to 1
+	// If fading an invisible FOF whose render flags we did not yet set, initialize its alpha to 1
 	if (dotranslucent &&
 		(rover->spawnflags & FOF_NOSHADE) && // do not include light blocks, which don't set FOF_NOSHADE
 		!(rover->fofflags & FOF_FOG) && // do not include fog
 		!(rover->spawnflags & FOF_RENDERSIDES) &&
 		!(rover->spawnflags & FOF_RENDERPLANES) &&
 		!(rover->fofflags & FOF_RENDERALL))
-		rover->alpha = 1;
+		rover->alpha = 0;
 
 	if (fadingdata)
 		alpha = fadingdata->alpha;
@@ -7915,7 +7846,7 @@ static boolean P_FadeFakeFloor(ffloor_t *rover, INT16 sourcevalue, INT16 destval
 	{
 		if (doexists && !(rover->spawnflags & FOF_BUSTUP))
 		{
-			if (alpha <= 1)
+			if (alpha <= 0)
 				rover->fofflags &= ~FOF_EXISTS;
 			else
 				rover->fofflags |= FOF_EXISTS;
@@ -7927,7 +7858,7 @@ static boolean P_FadeFakeFloor(ffloor_t *rover, INT16 sourcevalue, INT16 destval
 
 		if (dotranslucent && !(rover->fofflags & FOF_FOG))
 		{
-			if (alpha >= 256)
+			if (alpha >= 255)
 			{
 				if (!(rover->fofflags & FOF_CUTSOLIDS) &&
 					(rover->spawnflags & FOF_CUTSOLIDS))
@@ -8027,11 +7958,11 @@ static boolean P_FadeFakeFloor(ffloor_t *rover, INT16 sourcevalue, INT16 destval
 		else // clamp fadingdata->alpha to software's alpha levels
 		{
 			if (alpha < 12)
-				rover->alpha = destvalue < 12 ? destvalue : 1; // Don't even draw it
+				rover->alpha = destvalue < 12 ? destvalue : 0; // Don't even draw it
 			else if (alpha < 38)
 				rover->alpha = destvalue >= 12 && destvalue < 38 ? destvalue : 25;
 			else if (alpha < 64)
-				rover->alpha = destvalue >=38 && destvalue < 64 ? destvalue : 51;
+				rover->alpha = destvalue >= 38 && destvalue < 64 ? destvalue : 51;
 			else if (alpha < 89)
 				rover->alpha = destvalue >= 64 && destvalue < 89 ? destvalue : 76;
 			else if (alpha < 115)
@@ -8047,7 +7978,7 @@ static boolean P_FadeFakeFloor(ffloor_t *rover, INT16 sourcevalue, INT16 destval
 			else if (alpha < 243)
 				rover->alpha = destvalue >= 217 && destvalue < 243 ? destvalue : 230;
 			else // Opaque
-				rover->alpha = destvalue >= 243 ? destvalue : 256;
+				rover->alpha = destvalue >= 243 ? destvalue : 255;
 		}
 	}
 
@@ -8077,17 +8008,16 @@ static void P_AddFakeFloorFader(ffloor_t *rover, size_t sectornum, size_t ffloor
 {
 	fade_t *d;
 
-	// If fading an invisible FOF whose render flags we did not yet set,
-	// initialize its alpha to 1
+	// If fading an invisible FOF whose render flags we did not yet set, initialize its alpha to 1
 	if (dotranslucent &&
 		(rover->spawnflags & FOF_NOSHADE) && // do not include light blocks, which don't set FOF_NOSHADE
 		!(rover->spawnflags & FOF_RENDERSIDES) &&
 		!(rover->spawnflags & FOF_RENDERPLANES) &&
 		!(rover->fofflags & FOF_RENDERALL))
-		rover->alpha = 1;
+		rover->alpha = 0;
 
 	// already equal, nothing to do
-	if (rover->alpha == max(1, min(256, relative ? rover->alpha + destvalue : destvalue)))
+	if (rover->alpha == max(0, min(255, relative ? rover->alpha + destvalue : destvalue)))
 		return;
 
 	d = Z_Malloc(sizeof *d, PU_LEVSPEC, NULL);
@@ -8098,7 +8028,7 @@ static void P_AddFakeFloorFader(ffloor_t *rover, size_t sectornum, size_t ffloor
 	d->ffloornum = (UINT32)ffloornum;
 
 	d->alpha = d->sourcevalue = rover->alpha;
-	d->destvalue = max(1, min(256, relative ? rover->alpha + destvalue : destvalue)); // rover->alpha is 1-256
+	d->destvalue = max(0, min(255, relative ? rover->alpha + destvalue : destvalue)); // rover->alpha is 0-255
 
 	if (ticbased)
 	{
diff --git a/src/p_spec.h b/src/p_spec.h
index 91dfccb70fabb355f1111f5e38ce9cec35876fca..50ab6410f145b0d2ad180b492525cec51d786365 100644
--- a/src/p_spec.h
+++ b/src/p_spec.h
@@ -527,7 +527,7 @@ boolean P_IsMobjTouchingPolyobj(mobj_t *mo, polyobj_t *po, sector_t *polysec);
 
 void P_SwitchWeather(INT32 weathernum);
 
-boolean P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller);
+void P_RunTriggerLinedef(line_t *triggerline, mobj_t *actor, sector_t *caller);
 void P_LinedefExecute(INT16 tag, mobj_t *actor, sector_t *caller);
 void P_RunNightserizeExecutors(mobj_t *actor);
 void P_RunDeNightserizeExecutors(mobj_t *actor);
@@ -895,7 +895,7 @@ typedef struct
 
 typedef enum
 {
-	ok,
+	planeok,
 	crushed,
 	pastdest
 } result_e;
diff --git a/src/p_tick.c b/src/p_tick.c
index 0357258e812a57d05668f8221371098a0fa52373..444b68d2fda3d5ca60ab958715754b9e164fb82c 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -26,10 +26,16 @@
 #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"
 
+#ifdef PARANOIA
+#include "deh_tables.h" // MOBJTYPE_LIST
+#endif
+
 tic_t leveltime;
 
 //
@@ -211,8 +217,49 @@ 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
+
+#ifdef PARANOIA
+	thinker->debug_mobjtype = MT_NULL;
+#endif
 }
 
+#ifdef PARANOIA
+static const char *MobjTypeName(const mobj_t *mobj)
+{
+	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];
+		}
+	}
+
+	return "<Not a mobj>";
+}
+
+static const char *MobjThinkerName(const mobj_t *mobj)
+{
+	actionf_p1 p1 = mobj->thinker.function.acp1;
+
+	if (p1 == (actionf_p1)P_MobjThinker)
+	{
+		return "P_MobjThinker";
+	}
+	else if (p1 == (actionf_p1)P_RemoveThinkerDelayed)
+	{
+		return "P_RemoveThinkerDelayed";
+	}
+
+	return "<Unknown Thinker>";
+}
+#endif
+
 //
 // killough 11/98:
 //
@@ -234,20 +281,34 @@ static thinker_t *currentthinker;
 void P_RemoveThinkerDelayed(thinker_t *thinker)
 {
 	thinker_t *next;
-#ifdef PARANOIA
-#define BEENAROUNDBIT (0x40000000) // has to be sufficiently high that it's unlikely to happen in regular gameplay. If you change this, pay attention to the bit pattern of INT32_MIN.
-	if (thinker->references & ~BEENAROUNDBIT)
+
+	if (thinker->references != 0)
 	{
-		if (thinker->references & BEENAROUNDBIT) // Usually gets cleared up in one frame; what's going on here, then?
-			CONS_Printf("Number of potentially faulty references: %d\n", (thinker->references & ~BEENAROUNDBIT));
-		thinker->references |= BEENAROUNDBIT;
+#ifdef PARANOIA
+		if (thinker->debug_time > leveltime)
+		{
+			thinker->debug_time = leveltime + 2; // do not print errors again
+		}
+		// Removed mobjs can be the target of another mobj. In
+		// that case, the other mobj will manage its reference
+		// to the removed mobj in P_MobjThinker. However, if
+		// the removed mobj is removed after the other object
+		// thinks, the reference management is delayed by one
+		// tic.
+		else if (thinker->debug_time < leveltime)
+		{
+			CONS_Printf(
+					"PARANOIA/P_RemoveThinkerDelayed: %p %s references=%d\n",
+					(void*)thinker,
+					MobjTypeName((mobj_t*)thinker),
+					thinker->references
+			);
+
+			thinker->debug_time = leveltime + 2; // do not print this error again
+		}
+#endif
 		return;
 	}
-#undef BEENAROUNDBIT
-#else
-	if (thinker->references)
-		return;
-#endif
 
 	/* Remove from main thinker list */
 	next = thinker->next;
@@ -291,12 +352,45 @@ void P_RemoveThinker(thinker_t *thinker)
  * references, and delay removal until the count is 0.
  */
 
-mobj_t *P_SetTarget(mobj_t **mop, mobj_t *targ)
+mobj_t *P_SetTarget2(mobj_t **mop, mobj_t *targ
+#ifdef PARANOIA
+		, const char *source_file, int source_line
+#endif
+)
 {
-	if (*mop)              // If there was a target already, decrease its refcount
+	if (*mop) // If there was a target already, decrease its refcount
+	{
 		(*mop)->thinker.references--;
-if ((*mop = targ) != NULL) // Set new target and if non-NULL, increase its counter
+
+#ifdef PARANOIA
+		if ((*mop)->thinker.references < 0)
+		{
+			CONS_Printf(
+					"PARANOIA/P_SetTarget: %p %s %s references=%d, references go negative! (%s:%d)\n",
+					(void*)*mop,
+					MobjTypeName(*mop),
+					MobjThinkerName(*mop),
+					(*mop)->thinker.references,
+					source_file,
+					source_line
+			);
+		}
+
+		(*mop)->thinker.debug_time = leveltime;
+#endif
+	}
+
+	if (targ != NULL) // Set new target and if non-NULL, increase its counter
+	{
 		targ->thinker.references++;
+
+#ifdef PARANOIA
+		targ->thinker.debug_time = leveltime;
+#endif
+	}
+
+	*mop = targ;
+
 	return targ;
 }
 
@@ -675,7 +769,10 @@ void P_Ticker(boolean run)
 
 	// Keep track of how long they've been playing!
 	if (!demoplayback) // Don't increment if a demo is playing.
-		totalplaytime++;
+	{
+		clientGamedata->totalplaytime++;
+		serverGamedata->totalplaytime++;
+	}
 
 	if (!(maptol & TOL_NIGHTS) && G_IsSpecialStage(gamemap))
 		P_DoSpecialStageStuff();
diff --git a/src/p_tick.h b/src/p_tick.h
index 594bbc7afb608ce88a54b8f63ae7e5494f96af4f..bbc227e081433652a9a3d79b8ab0513c531677bc 100644
--- a/src/p_tick.h
+++ b/src/p_tick.h
@@ -14,6 +14,8 @@
 #ifndef __P_TICK__
 #define __P_TICK__
 
+#include "doomdef.h"
+
 #ifdef __GNUG__
 #pragma interface
 #endif
@@ -28,6 +30,17 @@ void P_Ticker(boolean run);
 void P_PreTicker(INT32 frames);
 void P_DoTeamscrambling(void);
 void P_RemoveThinkerDelayed(thinker_t *thinker); //killed
-mobj_t *P_SetTarget(mobj_t **mo, mobj_t *target);   // killough 11/98
+
+mobj_t *P_SetTarget2(mobj_t **mo, mobj_t *target
+#ifdef PARANOIA
+		, const char *source_file, int source_line
+#endif
+);
+
+#ifdef PARANOIA
+#define P_SetTarget(...) P_SetTarget2(__VA_ARGS__, __FILE__, __LINE__)
+#else
+#define P_SetTarget P_SetTarget2
+#endif
 
 #endif
diff --git a/src/p_user.c b/src/p_user.c
index 0f3282da5582f122c9f383ba9509ea28fa6d458a..5730d476d551d84039e42e942e49236a6dab25d8 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"
@@ -398,6 +399,7 @@ void P_GiveFinishFlags(player_t *player)
 		angle += FixedAngle(120*FRACUNIT);
 
 		P_SetTarget(&flag->target, player->mo);
+		P_SetTarget(&flag->dontdrawforviewmobj, player->mo); // Hide the flag in first-person
 	}
 }
 
@@ -676,7 +678,7 @@ static void P_DeNightserizePlayer(player_t *player)
 	player->marebonuslap = 0;
 	player->flyangle = 0;
 	player->anotherflyangle = 0;
-	player->mo->rollangle = 0;
+	player->mo->spriteroll = 0;
 
 	P_SetTarget(&player->mo->target, NULL);
 	P_SetTarget(&player->axis1, P_SetTarget(&player->axis2, NULL));
@@ -685,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
@@ -802,7 +804,7 @@ void P_NightserizePlayer(player_t *player, INT32 nighttime)
 	player->secondjump = 0;
 	player->flyangle = 0;
 	player->anotherflyangle = 0;
-	player->mo->rollangle = 0;
+	player->mo->spriteroll = 0;
 
 	player->powers[pw_shield] = SH_NONE;
 	player->powers[pw_super] = 0;
@@ -882,8 +884,7 @@ void P_NightserizePlayer(player_t *player, INT32 nighttime)
 			}
 
 			// Add score to leaderboards now
-			if (!(netgame||multiplayer) && P_IsLocalPlayer(&players[i]))
-				G_AddTempNightsRecords(players[i].marescore, leveltime - player->marebegunat, players[i].mare + 1);
+			G_AddTempNightsRecords(player, players[i].marescore, leveltime - player->marebegunat, players[i].mare + 1);
 
 			// transfer scores anyway
 			players[i].totalmarescore += players[i].marescore;
@@ -909,8 +910,7 @@ void P_NightserizePlayer(player_t *player, INT32 nighttime)
 		player->finishedrings = (INT16)(player->rings);
 
 		// Add score to temp leaderboards
-		if (!(netgame||multiplayer) && P_IsLocalPlayer(player))
-			G_AddTempNightsRecords(player->marescore, leveltime - player->marebegunat, (UINT8)(oldmare + 1));
+		G_AddTempNightsRecords(player, player->marescore, leveltime - player->marebegunat, (UINT8)(oldmare + 1));
 
 		// Starting a new mare, transfer scores
 		player->totalmarescore += player->marescore;
@@ -989,6 +989,8 @@ void P_DoPlayerPain(player_t *player, mobj_t *source, mobj_t *inflictor)
 	if (player->powers[pw_carry] == CR_ROPEHANG)
 		P_SetTarget(&player->mo->tracer, NULL);
 
+	player->powers[pw_strong] = STR_NONE;
+
 	{
 		angle_t ang;
 		fixed_t fallbackspeed;
@@ -1106,6 +1108,7 @@ void P_ResetPlayer(player_t *player)
 boolean P_PlayerCanDamage(player_t *player, mobj_t *thing)
 {
 	fixed_t bottomheight, topheight;
+	boolean allatk = ((player->powers[pw_strong] & STR_PUNCH) && (player->powers[pw_strong] & STR_TAIL) && (player->powers[pw_strong] & STR_STOMP) && (player->powers[pw_strong] & STR_UPPER));
 
 	if (!player->mo || player->spectator || !thing || P_MobjWasRemoved(thing))
 		return false;
@@ -1130,22 +1133,33 @@ boolean P_PlayerCanDamage(player_t *player, mobj_t *thing)
 
 	// Jumping.
 	if ((player->pflags & PF_JUMPED)
-	&& (!(player->pflags & PF_NOJUMPDAMAGE)
-		|| (player->charability == CA_TWINSPIN && player->panim == PA_ABILITY)))
+	&& (!(player->pflags & PF_NOJUMPDAMAGE)))
 		return true;
 
 	// Spinning.
 	if (player->pflags & PF_SPINNING)
 		return true;
 
-	if (player->dashmode >= DASHMODE_THRESHOLD && (player->charflags & (SF_DASHMODE|SF_MACHINE)) == (SF_DASHMODE|SF_MACHINE))
+	// Shield stomp.
+	if (((player->powers[pw_shield] & SH_NOSTACK) == SH_ELEMENTAL || (player->powers[pw_shield] & SH_NOSTACK) == SH_BUBBLEWRAP) && (player->pflags & PF_SHIELDABILITY))
+		return true;
+
+	// pw_strong checks below here
+
+	// Omnidirectional attacks.
+	if (allatk || (player->powers[pw_strong] & STR_DASH))
 		return true;
 
 	// From the front.
-	if (((player->pflags & PF_GLIDING) || (player->charability2 == CA2_MELEE && player->panim == PA_ABILITY2))
+	if ((player->powers[pw_strong] & STR_PUNCH)
 	&& (player->drawangle - R_PointToAngle2(player->mo->x - player->mo->momx, player->mo->y - player->mo->momy, thing->x, thing->y) +  + ANGLE_90) < ANGLE_180)
 		return true;
 
+	// From the back.
+	if ((player->powers[pw_strong] & STR_TAIL)
+	&& (player->drawangle - R_PointToAngle2(player->mo->x - player->mo->momx, player->mo->y - player->mo->momy, thing->x, thing->y) +  + ANGLE_90) >= ANGLE_180)
+		return true;
+
 	// From the top/bottom.
 	bottomheight = player->mo->z;
 	topheight = player->mo->z + player->mo->height;
@@ -1159,19 +1173,15 @@ boolean P_PlayerCanDamage(player_t *player, mobj_t *thing)
 
 	if (P_MobjFlip(player->mo)*(bottomheight - (thing->z + thing->height/2)) > 0)
 	{
-		if ((player->charflags & SF_STOMPDAMAGE || player->pflags & PF_BOUNCING) && (P_MobjFlip(player->mo)*(player->mo->momz - thing->momz) < 0))
+		if ((player->charflags & SF_STOMPDAMAGE || player->powers[pw_strong] & STR_STOMP) && (P_MobjFlip(player->mo)*(player->mo->momz - thing->momz) < 0))
 			return true;
 	}
 	else if (P_MobjFlip(player->mo)*(topheight - (thing->z + thing->height/2)) < 0)
 	{
-		if (player->charability == CA_FLY && player->panim == PA_ABILITY && !(player->mo->eflags & MFE_UNDERWATER) && (P_MobjFlip(player->mo)*(player->mo->momz - thing->momz) > 0))
+		if ((player->powers[pw_strong] & STR_UPPER) && (player->mo->sprite2 != SPR2_SWIM) && (P_MobjFlip(player->mo)*(player->mo->momz - thing->momz) > 0))
 			return true;
 	}
 
-	// Shield stomp.
-	if (((player->powers[pw_shield] & SH_NOSTACK) == SH_ELEMENTAL || (player->powers[pw_shield] & SH_NOSTACK) == SH_BUBBLEWRAP) && (player->pflags & PF_SHIELDABILITY))
-		return true;
-
 	return false;
 }
 
@@ -1430,6 +1440,10 @@ void P_AddPlayerScore(player_t *player, UINT32 amount)
 	if (player->score > MAXSCORE)
 		player->score = MAXSCORE;
 
+	player->recordscore += amount;
+	if (player->recordscore > MAXSCORE)
+		player->recordscore = MAXSCORE;
+
 	// check for extra lives every 50000 pts
 	if (!ultimatemode && !modeattacking && player->score > oldscore && player->score % 50000 < amount && (gametyperules & GTR_LIVES))
 	{
@@ -1828,6 +1842,7 @@ void P_SpawnShieldOrb(player_t *player)
 	shieldobj = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, orbtype);
 	shieldobj->flags2 |= MF2_SHIELD;
 	P_SetTarget(&shieldobj->target, player->mo);
+	P_SetTarget(&shieldobj->dontdrawforviewmobj, player->mo); // Hide the shield in first-person
 	if ((player->powers[pw_shield] & SH_NOSTACK) == SH_PINK)
 	{
 		shieldobj->color = SKINCOLOR_PINK;
@@ -1841,6 +1856,7 @@ void P_SpawnShieldOrb(player_t *player)
 	{
 		ov = P_SpawnMobj(shieldobj->x, shieldobj->y, shieldobj->z, MT_OVERLAY);
 		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);
 	}
@@ -1848,12 +1864,14 @@ void P_SpawnShieldOrb(player_t *player)
 	{
 		ov = P_SpawnMobj(shieldobj->x, shieldobj->y, shieldobj->z, MT_OVERLAY);
 		P_SetTarget(&ov->target, shieldobj);
+		P_SetTarget(&ov->dontdrawforviewmobj, player->mo); // Hide the shield in first-person
 		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);
+		P_SetTarget(&ov->dontdrawforviewmobj, player->mo); // Hide the shield in first-person
 		P_SetMobjState(ov, shieldobj->info->missilestate);
 	}
 	if (player->powers[pw_shield] & SH_FORCE)
@@ -1952,6 +1970,7 @@ mobj_t *P_SpawnGhostMobj(mobj_t *mobj)
 	mobj_t *ghost = P_SpawnMobj(mobj->x, mobj->y, mobj->z, MT_GHOST);
 
 	P_SetTarget(&ghost->target, mobj);
+	P_SetTarget(&ghost->dontdrawforviewmobj, mobj); // Hide the ghost in first-person
 
 	P_SetScale(ghost, mobj->scale);
 	ghost->destscale = mobj->scale;
@@ -1966,7 +1985,9 @@ mobj_t *P_SpawnGhostMobj(mobj_t *mobj)
 	ghost->colorized = mobj->colorized; // alternatively, "true" for sonic advance style colourisation
 
 	ghost->angle = (mobj->player ? mobj->player->drawangle : mobj->angle);
-	ghost->rollangle = mobj->rollangle;
+	ghost->roll = mobj->roll;
+	ghost->pitch = mobj->pitch;
+	ghost->spriteroll = mobj->spriteroll;
 
 	ghost->sprite = mobj->sprite;
 	ghost->sprite2 = mobj->sprite2;
@@ -1985,15 +2006,17 @@ mobj_t *P_SpawnGhostMobj(mobj_t *mobj)
 
 	ghost->fuse = ghost->info->damage;
 	ghost->skin = mobj->skin;
+	ghost->standingslope = mobj->standingslope;
 
 	if (mobj->flags2 & MF2_OBJECTFLIP)
-		ghost->flags |= MF2_OBJECTFLIP;
+		ghost->flags2 |= MF2_OBJECTFLIP;
 
 	if (mobj->player && mobj->player->followmobj)
 	{
 		mobj_t *ghost2 = P_SpawnGhostMobj(mobj->player->followmobj);
 		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);
 	}
 
@@ -2004,6 +2027,7 @@ mobj_t *P_SpawnGhostMobj(mobj_t *mobj)
 	ghost->old_angle = (mobj->player ? mobj->player->old_drawangle2 : mobj->old_angle2);
 	ghost->old_pitch = mobj->old_pitch2;
 	ghost->old_roll = mobj->old_roll2;
+	ghost->old_spriteroll = mobj->old_spriteroll2;
 
 	return ghost;
 }
@@ -2333,6 +2357,7 @@ boolean P_PlayerHitFloor(player_t *player, boolean dorollstuff)
 					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;
+					player->powers[pw_strong] = STR_MELEE;
 
 					// hearticles
 					if (type)
@@ -2557,17 +2582,13 @@ static boolean P_PlayerCanBust(player_t *player, ffloor_t *rover)
 			return true;
 
 		// Passive wall breaking
-		if (player->charflags & SF_CANBUSTWALLS)
+		if (player->charflags & SF_CANBUSTWALLS || player->powers[pw_strong] & (STR_WALL|STR_FLOOR|STR_CEILING|STR_DASH))
 			return true;
 
 		// Super
 		if (player->powers[pw_super])
 			return true;
 
-		// Dashmode
-		if ((player->charflags & (SF_DASHMODE|SF_MACHINE)) == (SF_DASHMODE|SF_MACHINE) && player->dashmode >= DASHMODE_THRESHOLD)
-			return true;
-
 		// NiGHTS drill
 		if (player->pflags & PF_DRILLING)
 			return true;
@@ -2578,21 +2599,11 @@ static boolean P_PlayerCanBust(player_t *player, ffloor_t *rover)
 
 		/* FALLTHRU */
 	case BT_STRONG: // Requires a "strong ability"
-		if (player->charflags & SF_CANBUSTWALLS)
-			return true;
-
-		if (player->pflags & PF_BOUNCING)
-			return true;
-
-		if (player->charability == CA_TWINSPIN && player->panim == PA_ABILITY)
-			return true;
-
-		if (player->charability2 == CA2_MELEE && player->panim == PA_ABILITY2)
+		if (player->charflags & SF_CANBUSTWALLS || player->powers[pw_strong] & (STR_WALL|STR_FLOOR|STR_CEILING))
 			return true;
 
 		break;
 	}
-
 	return false;
 }
 
@@ -2608,7 +2619,7 @@ static void P_CheckBustableBlocks(player_t *player)
 	oldx = player->mo->x;
 	oldy = player->mo->y;
 
-	if (!(player->pflags & PF_BOUNCING)) // Bouncers only get to break downwards, not sideways
+	if (!((player->powers[pw_strong] & (STR_FLOOR|STR_CEILING)) && (!(player->powers[pw_strong] & STR_WALL)) && (!(player->charflags & SF_CANBUSTWALLS)))) // Don't break sideways without wall powers
 	{
 		P_UnsetThingPosition(player->mo);
 		player->mo->x += player->mo->momx;
@@ -2635,8 +2646,24 @@ static void P_CheckBustableBlocks(player_t *player)
 			topheight = P_GetFOFTopZ(player->mo, node->m_sector, rover, player->mo->x, player->mo->y, NULL);
 			bottomheight = P_GetFOFBottomZ(player->mo, node->m_sector, rover, player->mo->x, player->mo->y, NULL);
 
-			if (((player->charability == CA_TWINSPIN) && (player->panim == PA_ABILITY))
-			|| ((P_MobjFlip(player->mo)*player->mo->momz < 0) && (player->pflags & PF_BOUNCING || ((player->charability2 == CA2_MELEE) && (player->panim == PA_ABILITY2)))))
+			// Height checks
+			if (player->mo->eflags & MFE_VERTICALFLIP)
+			{
+				if ((player->powers[pw_strong] & STR_FLOOR) && (!(player->powers[pw_strong] & STR_CEILING)) && player->mo->z > topheight)
+					continue;
+
+				if ((player->powers[pw_strong] & STR_CEILING) && (!(player->powers[pw_strong] & STR_FLOOR)) && player->mo->z + player->mo->height < bottomheight)
+					continue;
+			}
+			else
+			{
+				if ((player->powers[pw_strong] & STR_FLOOR) && (!(player->powers[pw_strong] & STR_CEILING)) && player->mo->z < bottomheight)
+					continue;
+
+				if ((player->powers[pw_strong] & STR_CEILING) && (!(player->powers[pw_strong] & STR_FLOOR)) && player->mo->z + player->mo->height > topheight)
+					continue;
+			}
+			if (player->powers[pw_strong] & (STR_FLOOR|STR_CEILING))
 			{
 				topheight -= player->mo->momz;
 				bottomheight -= player->mo->momz;
@@ -2704,7 +2731,7 @@ static void P_CheckBustableBlocks(player_t *player)
 		}
 	}
 bustupdone:
-	if (!(player->pflags & PF_BOUNCING))
+	if (!((player->powers[pw_strong] & (STR_FLOOR|STR_CEILING)) && (!(player->powers[pw_strong] & STR_WALL)) && (!(player->charflags & SF_CANBUSTWALLS))))
 	{
 		P_UnsetThingPosition(player->mo);
 		player->mo->x = oldx;
@@ -2984,7 +3011,7 @@ static void P_CheckInvincibilityTimer(player_t *player)
 		return;
 
 	if (mariomode && !player->powers[pw_super])
-		player->mo->color = (UINT16)(SKINCOLOR_RUBY + (leveltime % (numskincolors - SKINCOLOR_RUBY))); // Passes through all saturated colours
+		player->mo->color = (UINT16)(SKINCOLOR_RUBY + (leveltime % (FIRSTSUPERCOLOR - SKINCOLOR_RUBY))); // Passes through all saturated colours
 	else if (leveltime % (TICRATE/7) == 0)
 	{
 		mobj_t *sparkle = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_IVSP);
@@ -3006,7 +3033,7 @@ static void P_CheckInvincibilityTimer(player_t *player)
 				}
 				else
 				{
-					player->mo->color = player->skincolor;
+					player->mo->color = P_GetPlayerColor(player);
 					G_GhostAddColor(GHC_NORMAL);
 				}
 			}
@@ -3104,39 +3131,41 @@ static void P_DoBubbleBreath(player_t *player)
 //
 static void P_DoPlayerHeadSigns(player_t *player)
 {
+	mobj_t *sign = NULL;
+
 	if (G_TagGametype())
 	{
-		// If you're "IT", show a big "IT" over your head for others to see.
-		if (player->pflags & PF_TAGIT && !P_IsLocalPlayer(player))
+		// If you're "IT", show a big "IT" over your head for others to see, including spectators
+		// (and even yourself if you spectate someone else).
+		if (player->pflags & PF_TAGIT && (!P_IsLocalPlayer(player) || consoleplayer != displayplayer || splitscreen))
 		{
-			mobj_t* it = P_SpawnMobjFromMobj(player->mo, 0, 0, 0, MT_TAG);
-			it->x = player->mo->x;
-			it->y = player->mo->y;
-			it->z = player->mo->z;
-			it->old_x = player->mo->old_x;
-			it->old_y = player->mo->old_y;
-			it->old_z = player->mo->old_z;
+			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))
 			{
-				it->z += player->mo->height;
-				it->old_z += player->mo->height;
+				sign->z += player->mo->height;
+				sign->old_z += player->mo->height;
 			}
 			else
 			{
-				it->z -= mobjinfo[MT_TAG].height;
-				it->old_z -= mobjinfo[MT_TAG].height;
+				sign->z -= mobjinfo[MT_TAG].height;
+				sign->old_z -= mobjinfo[MT_TAG].height;
 			}
 		}
 	}
 	else if ((gametyperules & GTR_TEAMFLAGS) && (player->gotflag & (GF_REDFLAG|GF_BLUEFLAG))) // If you have the flag (duh).
 	{
-		// Spawn a got-flag message over the head of the player that
-		// has it (but not on your own screen if you have the flag).
-		if (splitscreen || player != &players[consoleplayer])
+		// Spawn a got-flag message over the head of the player that has it
+		// (but not on your own screen if you have the flag, unless you're spectating).
+		if (!P_IsLocalPlayer(player) || consoleplayer != displayplayer || splitscreen)
 		{
 			fixed_t zofs;
-			mobj_t *sign;
 			boolean player_is_flipped = (player->mo->eflags & MFE_VERTICALFLIP) > 0;
 
 			zofs = player->mo->momz;
@@ -3168,6 +3197,43 @@ static void P_DoPlayerHeadSigns(player_t *player)
 				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
+	{
+		if (player == &players[displayplayer])
+			sign->drawonlyforplayer = &players[secondarydisplayplayer];
+		else
+			sign->drawonlyforplayer = &players[displayplayer];
+
+#ifdef QUADS
+		if (splitscreen > 1) // Can be seen by at least two local views, so we need an extra copy of the sign
+		{
+			UINT32 signframe = sign->frame; // Copy the flag frame
+			sign = P_SpawnMobjFromMobj(sign, 0, 0, 0, MT_TAG);
+			if (P_MobjWasRemoved(sign))
+				return;
+
+			sign->frame = signframe;
+			if (player == &players[displayplayer] || player == &players[secondarydisplayplayer])
+				sign->drawonlyforplayer = &players[thirddisplayplayer];
+			else
+				sign->drawonlyforplayer = &players[secondarydisplayplayer];
+
+			if (splitscreen > 2) // Can be seen by three local views
+			{
+				sign = P_SpawnMobjFromMobj(sign, 0, 0, 0, MT_TAG);
+				if (P_MobjWasRemoved(sign))
+					return;
+
+				sign->frame = signframe;
+				if (player != &players[fourthdisplayplayer])
+					sign->drawonlyforplayer = &players[fourthdisplayplayer];
+				else
+					sign->drawonlyforplayer = &players[thirddisplayplayer];
+			}
+		}
+#endif
+	}
 }
 
 //
@@ -4240,7 +4306,7 @@ static void P_DoSuperStuff(player_t *player)
 			}
 			else
 			{
-				player->mo->color = player->skincolor;
+				player->mo->color = P_GetPlayerColor(player);
 				G_GhostAddColor(GHC_NORMAL);
 			}
 
@@ -4290,7 +4356,7 @@ static void P_DoSuperStuff(player_t *player)
 			}
 			else
 			{
-				player->mo->color = player->skincolor;
+				player->mo->color = P_GetPlayerColor(player);
 				G_GhostAddColor(GHC_NORMAL);
 			}
 
@@ -4688,10 +4754,11 @@ static void P_DoSpinAbility(player_t *player, ticcmd_t *cmd)
 						mobj_t *lockon = P_LookForEnemies(player, false, true);
 						if (lockon)
 						{
-							if (P_IsLocalPlayer(player)) // Only display it on your own view.
+							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);
+								visual->drawonlyforplayer = player; // Hide it from the other player in splitscreen, and yourself when spectating
 							}
 						}
 						if ((cmd->buttons & BT_SPIN) && !(player->pflags & PF_SPINDOWN))
@@ -4769,6 +4836,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);
+						player->powers[pw_strong] = STR_MELEE;
 						S_StartSound(player->mo, sfx_s3k42);
 					}
 					player->pflags |= PF_SPINDOWN;
@@ -5016,6 +5084,7 @@ static void P_DoTwinSpin(player_t *player)
 	S_StartSound(player->mo, sfx_s3k42);
 	player->mo->frame = 0;
 	P_SetPlayerMobjState(player->mo, S_PLAY_TWINSPIN);
+	player->powers[pw_strong] = STR_TWINSPIN;
 }
 
 //
@@ -5033,7 +5102,7 @@ static boolean P_PlayerShieldThink(player_t *player, ticcmd_t *cmd, mobj_t *lock
 		{
 			if ((lockonshield = P_LookForEnemies(player, false, false)))
 			{
-				if (P_IsLocalPlayer(player)) // Only display it on your own view.
+				if (P_IsLocalPlayer(player)) // Only display it on your own view. Don't display it for spectators
 				{
 					boolean dovis = true;
 					if (lockonshield == lockonthok)
@@ -5047,6 +5116,7 @@ static boolean P_PlayerShieldThink(player_t *player, ticcmd_t *cmd, mobj_t *lock
 					{
 						visual = P_SpawnMobj(lockonshield->x, lockonshield->y, lockonshield->z, MT_LOCKON); // positioning, flip handled in P_SceneryThinker
 						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);
 					}
 				}
@@ -5150,10 +5220,11 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 
 	if ((player->charability == CA_HOMINGTHOK) && !player->homing && (player->pflags & PF_JUMPED) && (!(player->pflags & PF_THOKKED) || (player->charflags & SF_MULTIABILITY)) && (lockonthok = P_LookForEnemies(player, true, false)))
 	{
-		if (P_IsLocalPlayer(player)) // Only display it on your own view.
+		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);
+			visual->drawonlyforplayer = player; // Hide it from the other player in splitscreen, and yourself when spectating
 		}
 	}
 
@@ -5383,6 +5454,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 							player->pflags |= PF_THOKKED;
 						else
 							player->pflags |= (PF_THOKKED|PF_CANCARRY);
+						player->powers[pw_strong] = STR_FLY;
 					}
 					break;
 				case CA_GLIDEANDCLIMB:
@@ -5409,7 +5481,8 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 						P_SetPlayerMobjState(player->mo, S_PLAY_GLIDE);
 						if (playerspeed < glidespeed)
 							P_Thrust(player->mo, player->mo->angle, glidespeed - playerspeed);
-						player->pflags &= ~(PF_SPINNING|PF_STARTDASH);
+						player->pflags &= ~(PF_JUMPED|PF_SPINNING|PF_STARTDASH);
+						player->powers[pw_strong] = STR_GLIDE;
 					}
 					break;
 				case CA_DOUBLEJUMP: // Double-Jump
@@ -5469,6 +5542,7 @@ static void P_DoJumpStuff(player_t *player, ticcmd_t *cmd)
 						P_SetPlayerMobjState(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;
 						player->mo->momx >>= 1;
 						player->mo->momy >>= 1;
 						player->mo->momz >>= 1;
@@ -6772,9 +6846,9 @@ static void P_DoNiGHTSCapsule(player_t *player)
 	{
 		if ((player->mo->state == &states[S_PLAY_NIGHTS_PULL])
 		&& (player->mo->sprite2 == SPR2_NPUL))
-			player->mo->rollangle -= ANG30;
+			player->mo->spriteroll -= ANG30;
 		else
-			player->mo->rollangle = 0;
+			player->mo->spriteroll = 0;
 	}
 
 	if (G_IsSpecialStage(gamemap))
@@ -7227,7 +7301,7 @@ static void P_NiGHTSMovement(player_t *player)
 		&& player->mo->state <= &states[S_PLAY_NIGHTS_TRANS6])
 	{
 		player->mo->momx = player->mo->momy = player->mo->momz = 0;
-		player->mo->rollangle = 0;
+		player->mo->spriteroll = 0;
 		return;
 	}
 
@@ -7245,7 +7319,7 @@ static void P_NiGHTSMovement(player_t *player)
 		{
 			if (player->mo->state != &states[S_PLAY_NIGHTS_DRILL])
 				P_SetPlayerMobjState(player->mo, S_PLAY_NIGHTS_DRILL);
-			player->mo->rollangle = ANGLE_90;
+			player->mo->spriteroll = ANGLE_90;
 		}
 		else
 #endif
@@ -7578,9 +7652,9 @@ static void P_NiGHTSMovement(player_t *player)
 		P_SetPlayerMobjState(player->mo, flystate);
 
 	if (player->charflags & SF_NONIGHTSROTATION)
-		player->mo->rollangle = 0;
+		player->mo->spriteroll = 0;
 	else
-		player->mo->rollangle = rollangle;
+		player->mo->spriteroll = rollangle;
 
 	P_SetPlayerAngle(player, player->mo->angle);
 
@@ -8647,7 +8721,10 @@ void P_MovePlayer(player_t *player)
 			player->mo->height = P_GetPlayerHeight(player);
 
 		if (player->mo->eflags & MFE_VERTICALFLIP && player->mo->height != oldheight) // adjust z height for reverse gravity, similar to how it's done for scaling
-			player->mo->z -= player->mo->height - oldheight;
+		{
+			player->mo->z     -= player->mo->height - oldheight;
+			player->mo->old_z -= player->mo->height - oldheight; // Snap the Z adjustment, while keeping the Z interpolation
+		}
 
 		// Crush test...
 		if ((player->mo->ceilingz - player->mo->floorz < player->mo->height)
@@ -9217,7 +9294,7 @@ mobj_t *P_LookForEnemies(player_t *player, boolean nonenemies, boolean bullet)
 
 		{
 			fixed_t zdist = (player->mo->z + player->mo->height/2) - (mo->z + mo->height/2);
-			dist = P_AproxDistance(player->mo->x-mo->x, player->mo->y-mo->y);
+			dist = R_PointToDist2(0, 0, player->mo->x-mo->x, player->mo->y-mo->y);
 			if (bullet)
 			{
 				if ((R_PointToAngle2(0, 0, dist, zdist) + span) > span*2)
@@ -9234,7 +9311,7 @@ mobj_t *P_LookForEnemies(player_t *player, boolean nonenemies, boolean bullet)
 					continue;
 			}
 
-			dist = P_AproxDistance(dist, zdist);
+			dist = R_PointToDist2(0, 0, dist, zdist);
 			if (dist > maxdist)
 				continue; // out of range
 		}
@@ -11060,17 +11137,30 @@ static void P_MinecartThink(player_t *player)
 
 			// Mark interpolation; the old positions need to be relative to the displacement from the minecart _after_ it's moved.
 			// This isn't quite correct (it captures the landing wobble) but it works well enough
+			// Additionally, hide other players' marks
 			if (detleft)
 			{
-				detleft->old_x = detleft->x - (minecart->old_x - minecart->old_x2);
-				detleft->old_y = detleft->y - (minecart->old_y - minecart->old_y2);
-				detleft->old_z = detleft->z - (minecart->old_z - minecart->old_z2);
+				if (P_IsLocalPlayer(player))
+				{
+					detleft->old_x = detleft->x - (minecart->old_x - minecart->old_x2);
+					detleft->old_y = detleft->y - (minecart->old_y - minecart->old_y2);
+					detleft->old_z = detleft->z - (minecart->old_z - minecart->old_z2);
+					detleft->drawonlyforplayer = player; // Hide it from the other player in splitscreen, and yourself when spectating
+				}
+				else // Don't see others' marks when spectating others
+					P_RemoveMobj(detleft); // Lock-on markers are only spawned client-side, so this SHOULD be safe too...
 			}
 			if (detright)
 			{
-				detright->old_x = detright->x - (minecart->old_x - minecart->old_x2);
-				detright->old_y = detright->y - (minecart->old_y - minecart->old_y2);
-				detright->old_z = detright->z - (minecart->old_z - minecart->old_z2);
+				if (P_IsLocalPlayer(player))
+				{
+					detright->old_x = detright->x - (minecart->old_x - minecart->old_x2);
+					detright->old_y = detright->y - (minecart->old_y - minecart->old_y2);
+					detright->old_z = detright->z - (minecart->old_z - minecart->old_z2);
+					detright->drawonlyforplayer = player;
+				}
+				else
+					P_RemoveMobj(detright);
 			}
 		}
 		else
@@ -11362,12 +11452,15 @@ static void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 
 			for (i = -1; i < 2; i += 2)
 			{
+				mobj_t *bubble;
 				offsetH = i*P_ReturnThrustX(fume, fume->movedir, radiusV);
 				offsetV = i*P_ReturnThrustY(fume, fume->movedir, radiusV);
 				x = mo->x + radiusX + FixedMul(offsetH, factorX);
 				y = mo->y + radiusY + FixedMul(offsetH, factorY);
 				z = mo->z + heightoffset + offsetV;
-				P_SpawnMobj(x, y, z, MT_SMALLBUBBLE)->scale = mo->scale >> 1;
+				bubble = P_SpawnMobj(x, y, z, MT_SMALLBUBBLE);
+				bubble->scale = mo->scale >> 1;
+				P_SetTarget(&bubble->dontdrawforviewmobj, mo); // Hide the bubble in first-person
 			}
 
 			fume->movefactor = 0;
@@ -11421,7 +11514,7 @@ static void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 	}
 
 	fume->movecount = dashmode; // keeps track of previous dashmode value so we know whether Metal is entering or leaving it
-	fume->eflags = (fume->flags2 & ~MF2_OBJECTFLIP) | (mo->flags2 & MF2_OBJECTFLIP); // Make sure to flip in reverse gravity!
+	fume->flags2 = (fume->flags2 & ~MF2_OBJECTFLIP) | (mo->flags2 & MF2_OBJECTFLIP); // Make sure to flip in reverse gravity!
 	fume->eflags = (fume->eflags & ~MFE_VERTICALFLIP) | (mo->eflags & MFE_VERTICALFLIP); // Make sure to flip in reverse gravity!
 
 	// Finally, set its position
@@ -11436,7 +11529,11 @@ static void P_DoMetalJetFume(player_t *player, mobj_t *fume)
 
 	// If dashmode is high enough, spawn a trail
 	if (player->normalspeed >= skins[player->skin].normalspeed*2)
-		P_SpawnGhostMobj(fume);
+	{
+		mobj_t *ghost = P_SpawnGhostMobj(fume);
+		if (!P_MobjWasRemoved(ghost))
+			P_SetTarget(&ghost->dontdrawforviewmobj, mo); // Hide the trail in first-person
+	}
 }
 
 //
@@ -11507,15 +11604,24 @@ void P_PlayerThink(player_t *player)
 	if (player->awayviewmobj && P_MobjWasRemoved(player->awayviewmobj))
 	{
 		P_SetTarget(&player->awayviewmobj, NULL); // remove awayviewmobj asap if invalid
-		player->awayviewtics = 0; // reset to zero
+		player->awayviewtics = 1; // reset to one, the below code will immediately set it to zero
+	}
+	
+	if (player->awayviewtics && player->awayviewtics != -1)
+	{
+		player->awayviewtics--;
+		if (!(player->awayviewtics))
+		{
+			if (player == &players[displayplayer])
+				P_ResetCamera(player, &camera); // reset p1 camera on p1 running out of awayviewtics
+			else if (splitscreen && player == &players[secondarydisplayplayer])
+				P_ResetCamera(player, &camera2);  // reset p2 camera on p2 running out of awayviewtics
+		}
 	}
 
 	if (player->flashcount)
 		player->flashcount--;
 
-	if (player->awayviewtics && player->awayviewtics != -1)
-		player->awayviewtics--;
-
 	/// \note do this in the cheat code
 	if (player->pflags & PF_NOCLIP)
 		player->mo->flags |= MF_NOCLIP;
@@ -11524,9 +11630,12 @@ void P_PlayerThink(player_t *player)
 
 	cmd = &player->cmd;
 
-	// Add some extra randomization.
-	if (cmd->forwardmove)
-		P_RandomFixed();
+	if (demoplayback && demo_forwardmove_rng)
+	{
+		// Smelly demo backwards compatibility
+		if (cmd->forwardmove)
+			P_RandomFixed();
+	}
 
 #ifdef PARANOIA
 	if (player->playerstate == PST_REBORN)
@@ -11656,7 +11765,7 @@ void P_PlayerThink(player_t *player)
 			if (!total || ((4*exiting)/total) >= numneeded)
 			{
 				if (server)
-					SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+					D_SendExitLevel(false);
 			}
 			else
 				player->exiting = 3;
@@ -11664,7 +11773,7 @@ void P_PlayerThink(player_t *player)
 		else
 		{
 			if (server)
-				SendNetXCmd(XD_EXITLEVEL, NULL, 0);
+				D_SendExitLevel(false);
 		}
 	}
 
@@ -11705,7 +11814,7 @@ void P_PlayerThink(player_t *player)
 	if (player->spectator)
 	{
 		if (!(gametyperules & GTR_CAMPAIGN))
-			player->score = 0;
+			player->score = player->recordscore = 0;
 	}
 	else if ((netgame || multiplayer) && player->lives <= 0 && !G_CoopGametype())
 	{
@@ -11762,7 +11871,7 @@ void P_PlayerThink(player_t *player)
 			mo2 = (mobj_t *)th;
 
 			if (!(mo2->type == MT_RING || mo2->type == MT_COIN
-				|| mo2->type == MT_BLUESPHERE || mo2->type == MT_BOMBSPHERE
+				|| mo2->type == MT_BLUESPHERE // || mo2->type == MT_BOMBSPHERE
 				|| mo2->type == MT_NIGHTSCHIP || mo2->type == MT_NIGHTSSTAR))
 				continue;
 
@@ -12071,14 +12180,6 @@ void P_PlayerThink(player_t *player)
 				gmobj->tracer->frame |= tr_trans70<<FF_TRANSSHIFT;
 			}
 		}
-
-		// Hide the mobj from our sights if we're the displayplayer and chasecam is off,
-		// or secondarydisplayplayer and chasecam2 is off.
-		// Why not just not spawn the mobj?  Well, I'd rather only flirt with
-		// consistency so much...
-		if ((player == &players[displayplayer] && !camera.chase)
-		|| (splitscreen && player == &players[secondarydisplayplayer] && !camera2.chase))
-			gmobj->flags2 |= MF2_DONTDRAW;
 	}
 #endif
 
@@ -12172,6 +12273,16 @@ void P_PlayerThink(player_t *player)
 	else
 		player->powers[pw_ignorelatch] = 0;
 
+	if (player->powers[pw_strong] & STR_ANIM)
+	{
+		if (!(player->stronganim))
+			player->stronganim = player->panim;
+		else if (player->panim != player->stronganim)
+			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]
@@ -12266,6 +12377,8 @@ void P_PlayerThink(player_t *player)
 			{
 				player->normalspeed = skins[player->skin].normalspeed; // Reset to default if not capable of entering dash mode.
 				player->jumpfactor = skins[player->skin].jumpfactor;
+				if (player->powers[pw_strong] & STR_DASH)
+					player->powers[pw_strong] = STR_NONE;
 			}
 		}
 		else if (P_IsObjectOnGround(player->mo)) // Activate dash mode if we're on the ground.
@@ -12275,6 +12388,9 @@ 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))) 
+					player->powers[pw_strong] = STR_METAL;
 		}
 
 		if (player->normalspeed >= skins[player->skin].normalspeed*2)
@@ -12292,6 +12408,8 @@ void P_PlayerThink(player_t *player)
 			player->normalspeed = skins[player->skin].normalspeed;
 			player->jumpfactor = skins[player->skin].jumpfactor;
 			S_StartSound(player->mo, sfx_kc65);
+			if (player->powers[pw_strong] & STR_DASH)
+				player->powers[pw_strong] = STR_NONE;
 		}
 		dashmode = 0;
 	}
@@ -13002,3 +13120,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
new file mode 100644
index 0000000000000000000000000000000000000000..cf417ec37639477b43a5a5e5035b059dc81490b4
--- /dev/null
+++ b/src/r_bbox.c
@@ -0,0 +1,321 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1993-1996 by id Software, Inc.
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C)      2022 by Kart Krew.
+// 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  r_bbox.c
+/// \brief Boundary box (cube) renderer
+
+#include "doomdef.h"
+#include "command.h"
+#include "r_local.h"
+#include "screen.h" // cv_renderhitbox
+#include "v_video.h" // V_DrawFill
+
+enum {
+	RENDERHITBOX_OFF,
+	RENDERHITBOX_TANGIBLE,
+	RENDERHITBOX_ALL,
+	RENDERHITBOX_INTANGIBLE,
+	RENDERHITBOX_RINGS,
+};
+
+static CV_PossibleValue_t renderhitbox_cons_t[] = {
+	{RENDERHITBOX_OFF, "Off"},
+	{RENDERHITBOX_TANGIBLE, "Tangible"},
+	{RENDERHITBOX_ALL, "All"},
+	{RENDERHITBOX_INTANGIBLE, "Intangible"},
+	{RENDERHITBOX_RINGS, "Rings"},
+	{0}};
+
+consvar_t cv_renderhitbox = CVAR_INIT ("renderhitbox", "Off", CV_CHEAT|CV_NOTINNET, renderhitbox_cons_t, NULL);
+consvar_t cv_renderhitboxinterpolation = CVAR_INIT ("renderhitbox_interpolation", "On", CV_SAVE, CV_OnOff, NULL);
+consvar_t cv_renderhitboxgldepth = CVAR_INIT ("renderhitbox_gldepth", "Off", CV_SAVE, CV_OnOff, NULL);
+
+struct bbox_col {
+	INT32 x;
+	INT32 y;
+	INT32 h;
+};
+
+struct bbox_config {
+	fixed_t height;
+	fixed_t tz;
+	struct bbox_col col[4];
+	UINT8 color;
+};
+
+static inline void
+raster_bbox_seg
+(		INT32 x,
+		fixed_t y,
+		fixed_t h,
+		UINT8 pixel)
+{
+	y /= FRACUNIT;
+
+	if (y < 0)
+		y = 0;
+
+	h = y + (FixedCeil(abs(h)) / FRACUNIT);
+
+	if (h >= viewheight)
+		h = viewheight;
+
+	while (y < h)
+	{
+		topleft[x + y * vid.width] = pixel;
+		y++;
+	}
+}
+
+static void
+draw_bbox_col
+(		struct bbox_config * bb,
+		size_t p,
+		fixed_t tx,
+		fixed_t ty)
+{
+	struct bbox_col *col = &bb->col[p];
+
+	fixed_t xscale, yscale;
+
+	if (ty < FRACUNIT) // projection breaks down here
+		ty = FRACUNIT;
+
+	xscale = FixedDiv(projection, ty);
+	yscale = FixedDiv(projectiony, ty);
+
+	col->x = (centerxfrac + FixedMul(tx, xscale)) / FRACUNIT;
+	col->y = (centeryfrac - FixedMul(bb->tz, yscale));
+	col->h = FixedMul(bb->height, yscale);
+
+	// Using this function is TOO EASY!
+	V_DrawFill(
+			viewwindowx + col->x,
+			viewwindowy + col->y / FRACUNIT, 1,
+			col->h / FRACUNIT, V_NOSCALESTART | bb->color);
+}
+
+static void
+draw_bbox_row
+(		struct bbox_config * bb,
+		size_t p1,
+		size_t p2)
+{
+	struct bbox_col
+		*a = &bb->col[p1],
+		*b = &bb->col[p2];
+
+	INT32 x1, x2; // left, right
+	INT32 dx; // width
+
+	fixed_t y1, y2; // top, bottom
+	fixed_t s1, s2; // top and bottom increment
+
+	if (a->x > b->x)
+	{
+		struct bbox_col *c = a;
+		a = b;
+		b = c;
+	}
+
+	x1 = a->x;
+	x2 = b->x;
+
+	if (x1 == x2 || x1 >= viewwidth || x2 < 0)
+		return;
+
+	dx = x2 - x1;
+
+	y1 = a->y;
+	y2 = b->y;
+	s1 = (y2 - y1) / dx;
+
+	y2 = y1 + a->h;
+	s2 = ((b->y + b->h) - y2) / dx;
+
+	// FixedCeil needs a minimum!!! :D :D
+
+	if (s1 == 0)
+		s1 = 1;
+
+	if (s2 == 0)
+		s2 = 1;
+
+	if (x1 < 0)
+	{
+		y1 -= x1 * s1;
+		y2 -= x1 * s2;
+		x1 = 0;
+	}
+
+	if (x2 >= viewwidth)
+		x2 = viewwidth - 1;
+
+	while (x1 < x2)
+	{
+		raster_bbox_seg(x1, y1, s1, bb->color);
+		raster_bbox_seg(x1, y2, s2, bb->color);
+
+		y1 += s1;
+		y2 += s2;
+
+		x1++;
+	}
+}
+
+UINT8 R_GetBoundingBoxColor(mobj_t *thing)
+{
+	UINT32 flags = thing->flags;
+
+	if (thing->player)
+		return 255; // 0FF
+
+	if (flags & (MF_NOCLIPTHING))
+		return 7; // BFBFBF
+
+	if (flags & (MF_BOSS|MF_ENEMY))
+		return 35; // F00
+
+	if (flags & (MF_MISSILE|MF_PAIN))
+		return 54; // F70
+
+	if (flags & (MF_SPECIAL|MF_MONITOR))
+		return 73; // FF0
+
+	if (flags & MF_PUSHABLE)
+		return 112; // 0F0
+
+	if (flags & (MF_SPRING))
+		return 181; // F0F
+
+	if (flags & (MF_NOCLIP))
+		return 152; // 00F
+
+	return 0; // FFF
+}
+
+void R_DrawThingBoundingBox(vissprite_t *vis)
+{
+	// radius offsets
+	fixed_t rs = vis->scale;
+	fixed_t rc = vis->xscale;
+
+	// translated coordinates
+	fixed_t tx = vis->gx;
+	fixed_t ty = vis->gy;
+
+	struct bbox_config bb = {
+		.height = vis->thingheight,
+		.tz = vis->texturemid,
+		.color = R_GetBoundingBoxColor(vis->mobj),
+	};
+
+	// 1--3
+	// |  |
+	// 0--2
+
+	// left
+
+	draw_bbox_col(&bb, 0, tx, ty); // bottom
+	draw_bbox_col(&bb, 1, tx - rc, ty + rs); // top
+
+	// right
+
+	tx += rs;
+	ty += rc;
+
+	draw_bbox_col(&bb, 2, tx, ty); // bottom
+	draw_bbox_col(&bb, 3, tx - rc, ty + rs); // top
+
+	// connect all four columns
+
+	draw_bbox_row(&bb, 0, 1);
+	draw_bbox_row(&bb, 1, 3);
+	draw_bbox_row(&bb, 3, 2);
+	draw_bbox_row(&bb, 2, 0);
+}
+
+static boolean is_tangible (mobj_t *thing)
+{
+	// These objects can never touch another
+	if (thing->flags & (MF_NOCLIPTHING))
+	{
+		return false;
+	}
+
+	// These objects probably do nothing! :D
+	if ((thing->flags & (MF_SPECIAL|MF_SOLID|MF_SHOOTABLE
+					|MF_PUSHABLE|MF_BOSS|MF_MISSILE|MF_SPRING
+					|MF_BOUNCE|MF_MONITOR|MF_FIRE|MF_ENEMY
+					|MF_PAIN|MF_STICKY
+					|MF_GRENADEBOUNCE)) == 0U)
+	{
+		return false;
+	}
+
+	return true;
+}
+
+boolean R_ThingBoundingBoxVisible(mobj_t *thing)
+{
+	INT32 cvmode = cv_renderhitbox.value;
+
+	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!
+			if (thing != r_viewmobj)
+			{
+				break;
+			}
+			// FALLTHRU
+
+		case MT_SKYBOX:
+			// Ditto for skybox viewpoint but because they
+			// are rendered using portals in Software,
+			// r_viewmobj does not point here.
+			return false;
+	}
+
+	switch (cvmode)
+	{
+		case RENDERHITBOX_OFF:
+			return false;
+
+		case RENDERHITBOX_ALL:
+			return true;
+
+		case RENDERHITBOX_INTANGIBLE:
+			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);
+
+		case RENDERHITBOX_RINGS:
+			return (thing->type == MT_RING || thing->type == MT_BLUESPHERE);
+
+		default:
+			return false;
+	}
+}
diff --git a/src/r_bsp.c b/src/r_bsp.c
index 121ddaae5ae1340b5c0b2e33b983b061be060f86..42e050adf831f15a7b3e653c57e265cab781b6cc 100644
--- a/src/r_bsp.c
+++ b/src/r_bsp.c
@@ -275,9 +275,9 @@ sector_t *R_FakeFlat(sector_t *sec, sector_t *tempsec, INT32 *floorlightlevel,
 			tempsec->ceilingheight = s->floorheight - 1, !back)) || viewz <= s->floorheight)
 		{ // head-below-floor hack
 			tempsec->floorpic = s->floorpic;
-			tempsec->floor_xoffs = s->floor_xoffs;
-			tempsec->floor_yoffs = s->floor_yoffs;
-			tempsec->floorpic_angle = s->floorpic_angle;
+			tempsec->floorxoffset = s->floorxoffset;
+			tempsec->flooryoffset = s->flooryoffset;
+			tempsec->floorangle = s->floorangle;
 
 			if (underwater)
 			{
@@ -285,16 +285,16 @@ sector_t *R_FakeFlat(sector_t *sec, sector_t *tempsec, INT32 *floorlightlevel,
 				{
 					tempsec->floorheight = tempsec->ceilingheight+1;
 					tempsec->ceilingpic = tempsec->floorpic;
-					tempsec->ceiling_xoffs = tempsec->floor_xoffs;
-					tempsec->ceiling_yoffs = tempsec->floor_yoffs;
-					tempsec->ceilingpic_angle = tempsec->floorpic_angle;
+					tempsec->ceilingxoffset = tempsec->floorxoffset;
+					tempsec->ceilingyoffset = tempsec->flooryoffset;
+					tempsec->ceilingangle = tempsec->floorangle;
 				}
 				else
 				{
 					tempsec->ceilingpic = s->ceilingpic;
-					tempsec->ceiling_xoffs = s->ceiling_xoffs;
-					tempsec->ceiling_yoffs = s->ceiling_yoffs;
-					tempsec->ceilingpic_angle = s->ceilingpic_angle;
+					tempsec->ceilingxoffset = s->ceilingxoffset;
+					tempsec->ceilingyoffset = s->ceilingyoffset;
+					tempsec->ceilingangle = s->ceilingangle;
 				}
 			}
 
@@ -315,25 +315,25 @@ sector_t *R_FakeFlat(sector_t *sec, sector_t *tempsec, INT32 *floorlightlevel,
 			tempsec->floorheight = s->ceilingheight + 1;
 
 			tempsec->floorpic = tempsec->ceilingpic = s->ceilingpic;
-			tempsec->floor_xoffs = tempsec->ceiling_xoffs = s->ceiling_xoffs;
-			tempsec->floor_yoffs = tempsec->ceiling_yoffs = s->ceiling_yoffs;
-			tempsec->floorpic_angle = tempsec->ceilingpic_angle = s->ceilingpic_angle;
+			tempsec->floorxoffset = tempsec->ceilingxoffset = s->ceilingxoffset;
+			tempsec->flooryoffset = tempsec->ceilingyoffset = s->ceilingyoffset;
+			tempsec->floorangle = tempsec->ceilingangle = s->ceilingangle;
 
 			if (s->floorpic == skyflatnum) // SKYFIX?
 			{
 				tempsec->ceilingheight = tempsec->floorheight-1;
 				tempsec->floorpic = tempsec->ceilingpic;
-				tempsec->floor_xoffs = tempsec->ceiling_xoffs;
-				tempsec->floor_yoffs = tempsec->ceiling_yoffs;
-				tempsec->floorpic_angle = tempsec->ceilingpic_angle;
+				tempsec->floorxoffset = tempsec->ceilingxoffset;
+				tempsec->flooryoffset = tempsec->ceilingyoffset;
+				tempsec->floorangle = tempsec->ceilingangle;
 			}
 			else
 			{
 				tempsec->ceilingheight = sec->ceilingheight;
 				tempsec->floorpic = s->floorpic;
-				tempsec->floor_xoffs = s->floor_xoffs;
-				tempsec->floor_yoffs = s->floor_yoffs;
-				tempsec->floorpic_angle = s->floorpic_angle;
+				tempsec->floorxoffset = s->floorxoffset;
+				tempsec->flooryoffset = s->flooryoffset;
+				tempsec->floorangle = s->floorangle;
 			}
 
 			tempsec->lightlevel = s->lightlevel;
@@ -363,12 +363,12 @@ boolean R_IsEmptyLine(seg_t *line, sector_t *front, sector_t *back)
 		&& back->lightlevel == front->lightlevel
 		&& !line->sidedef->midtexture
 		// Check offsets too!
-		&& back->floor_xoffs == front->floor_xoffs
-		&& back->floor_yoffs == front->floor_yoffs
-		&& back->floorpic_angle == front->floorpic_angle
-		&& back->ceiling_xoffs == front->ceiling_xoffs
-		&& back->ceiling_yoffs == front->ceiling_yoffs
-		&& back->ceilingpic_angle == front->ceilingpic_angle
+		&& back->floorxoffset == front->floorxoffset
+		&& back->flooryoffset == front->flooryoffset
+		&& back->floorangle == front->floorangle
+		&& back->ceilingxoffset == front->ceilingxoffset
+		&& back->ceilingyoffset == front->ceilingyoffset
+		&& back->ceilingangle == front->ceilingangle
 		// Consider altered lighting.
 		&& back->floorlightlevel == front->floorlightlevel
 		&& back->floorlightabsolute == front->floorlightabsolute
@@ -909,7 +909,7 @@ static void R_Subsector(size_t num)
 		|| (frontsector->heightsec != -1 && sectors[frontsector->heightsec].ceilingpic == skyflatnum))
 	{
 		floorplane = R_FindPlane(frontsector->floorheight, frontsector->floorpic, floorlightlevel,
-			frontsector->floor_xoffs, frontsector->floor_yoffs, frontsector->floorpic_angle, floorcolormap, NULL, NULL, frontsector->f_slope);
+			frontsector->floorxoffset, frontsector->flooryoffset, frontsector->floorangle, floorcolormap, NULL, NULL, frontsector->f_slope);
 	}
 	else
 		floorplane = NULL;
@@ -919,7 +919,7 @@ static void R_Subsector(size_t num)
 		|| (frontsector->heightsec != -1 && sectors[frontsector->heightsec].floorpic == skyflatnum))
 	{
 		ceilingplane = R_FindPlane(frontsector->ceilingheight, frontsector->ceilingpic,
-			ceilinglightlevel, frontsector->ceiling_xoffs, frontsector->ceiling_yoffs, frontsector->ceilingpic_angle,
+			ceilinglightlevel, frontsector->ceilingxoffset, frontsector->ceilingyoffset, frontsector->ceilingangle,
 			ceilingcolormap, NULL, NULL, frontsector->c_slope);
 	}
 	else
@@ -1033,8 +1033,8 @@ 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->floor_xoffs, polysec->floor_yoffs,
-					polysec->floorpic_angle-po->angle,
+					(light == -1 ? frontsector->lightlevel : *frontsector->lightlist[light].lightlevel), polysec->floorxoffset, polysec->flooryoffset,
+					polysec->floorangle-po->angle,
 					(light == -1 ? frontsector->extra_colormap : *frontsector->lightlist[light].extra_colormap), NULL, po,
 					NULL); // will ffloors be slopable eventually?
 
@@ -1057,7 +1057,7 @@ 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->ceiling_xoffs, polysec->ceiling_yoffs, polysec->ceilingpic_angle-po->angle,
+					(light == -1 ? frontsector->lightlevel : *frontsector->lightlist[light].lightlevel), polysec->ceilingxoffset, polysec->ceilingyoffset, 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_defs.h b/src/r_defs.h
index 6d2b7d3d8ab0f1e2db9932e26c6e1ee188f218bb..dfd2d6d708f8a5a9dae0da401719ad1d3eb24235 100644
--- a/src/r_defs.h
+++ b/src/r_defs.h
@@ -423,12 +423,12 @@ typedef struct sector_s
 	void *fadecolormapdata; // fade colormap thinker
 
 	// floor and ceiling texture offsets
-	fixed_t floor_xoffs, floor_yoffs;
-	fixed_t ceiling_xoffs, ceiling_yoffs;
+	fixed_t floorxoffset, flooryoffset;
+	fixed_t ceilingxoffset, ceilingyoffset;
 
 	// flat angle
-	angle_t floorpic_angle;
-	angle_t ceilingpic_angle;
+	angle_t floorangle;
+	angle_t ceilingangle;
 
 	INT32 heightsec; // other sector, or -1 if no other sector
 	INT32 camsec; // used for camera clipping
@@ -547,7 +547,6 @@ typedef struct line_s
 	size_t validcount; // if == validcount, already checked
 	polyobj_t *polyobj; // Belongs to a polyobject?
 
-	char *text; // a concatenation of all front and back texture names, for linedef specials that require a string.
 	INT16 callcount; // no. of calls left before triggering, for the "X calls" linedef specials, defaults to 0
 } line_t;
 
@@ -559,6 +558,10 @@ typedef struct
 	// add this to the calculated texture top
 	fixed_t rowoffset;
 
+	// per-texture offsets for UDMF
+	fixed_t offsetx_top, offsetx_mid, offsetx_bot;
+	fixed_t offsety_top, offsety_mid, offsety_bot;
+
 	// Texture indices.
 	// We do not maintain names here.
 	INT32 toptexture, bottomtexture, midtexture;
@@ -572,8 +575,6 @@ typedef struct
 	INT16 special; // the special of the linedef this side belongs to
 	INT16 repeatcnt; // # of times to repeat midtexture
 
-	char *text; // a concatenation of all top, bottom, and mid texture names, for linedef specials that require a string.
-
 	extracolormap_t *colormap_data; // storage for colormaps; not applied to sectors.
 } side_t;
 
@@ -756,12 +757,12 @@ typedef struct drawseg_s
 	// Pointers to lists for sprite clipping, all three adjusted so [x1] is first value.
 	INT16 *sprtopclip;
 	INT16 *sprbottomclip;
-	INT16 *maskedtexturecol;
+	fixed_t *maskedtexturecol;
 
 	struct visplane_s *ffloorplanes[MAXFFLOORS];
 	INT32 numffloorplanes;
 	struct ffloor_s *thicksides[MAXFFLOORS];
-	INT16 *thicksidecol;
+	fixed_t *thicksidecol;
 	INT32 numthicksides;
 	fixed_t frontscale[MAXVIDWIDTH];
 
diff --git a/src/r_draw.c b/src/r_draw.c
index b0467e4f728d4cf757b53484a3d5ca4fda9d91cc..df9e1a4608b568706452df29bbc347adef075b01 100644
--- a/src/r_draw.c
+++ b/src/r_draw.c
@@ -179,8 +179,6 @@ CV_PossibleValue_t Color_cons_t[MAXSKINCOLORS+1];
 void R_InitTranslucencyTables(void)
 {
 	// Load here the transparency lookup tables 'TRANSx0'
-	// NOTE: the TRANSx0 resources MUST BE aligned on 64k for the asm
-	// optimised code (in other words, transtables pointer low word is 0)
 	transtables = Z_MallocAlign(NUMTRANSTABLES*0x10000, PU_STATIC,
 		NULL, 16);
 
diff --git a/src/r_draw.h b/src/r_draw.h
index ea03a8e3d53e059570822a0119ee6431f45d105a..0103ed82782b22c7a51beb10c20473a3e8ba3787 100644
--- a/src/r_draw.h
+++ b/src/r_draw.h
@@ -225,18 +225,6 @@ void R_DrawTiltedTransSolidColorSpan_8(void);
 void R_DrawWaterSolidColorSpan_8(void);
 void R_DrawTiltedWaterSolidColorSpan_8(void);
 
-#ifdef USEASM
-void ASMCALL R_DrawColumn_8_ASM(void);
-void ASMCALL R_DrawShadeColumn_8_ASM(void);
-void ASMCALL R_DrawTranslucentColumn_8_ASM(void);
-void ASMCALL R_Draw2sMultiPatchColumn_8_ASM(void);
-
-void ASMCALL R_DrawColumn_8_MMX(void);
-
-void ASMCALL R_Draw2sMultiPatchColumn_8_MMX(void);
-void ASMCALL R_DrawSpan_8_MMX(void);
-#endif
-
 // ------------------
 // 16bpp DRAWING CODE
 // ------------------
diff --git a/src/r_draw8_npo2.c b/src/r_draw8_npo2.c
index faf1cdba80bb1acda4b5c595378f0aa428043431..91f3b06c4270ba28901d35b561f68a002ddbc6f3 100644
--- a/src/r_draw8_npo2.c
+++ b/src/r_draw8_npo2.c
@@ -18,6 +18,11 @@
 #define SPANSIZE 16
 #define INVSPAN 0.0625f
 
+#if defined(__GNUC__) || defined(__clang__) // Suppress intentional libdivide compiler warnings - Also added to libdivide.h
+    #pragma GCC diagnostic push
+    #pragma GCC diagnostic ignored "-Waggregate-return"
+#endif
+
 /**	\brief The R_DrawSpan_NPO2_8 function
 	Draws the actual span.
 */
@@ -1572,3 +1577,7 @@ void R_DrawTiltedWaterSpan_NPO2_8(void)
 	}
 #endif
 }
+
+#if defined(__GNUC__) || defined(__clang__) // Stop suppressing intentional libdivide compiler warnings
+    #pragma GCC diagnostic pop
+#endif
diff --git a/src/r_fps.c b/src/r_fps.c
index 2d30c9f01920959c783da056d1db2f0858969209..de450aaa7f465b8d47891baec585c757de361c60 100644
--- a/src/r_fps.c
+++ b/src/r_fps.c
@@ -292,8 +292,13 @@ void R_InterpolateMobjState(mobj_t *mobj, fixed_t frac, interpmobjstate_t *out)
 		out->y = mobj->y;
 		out->z = mobj->z;
 		out->scale = mobj->scale;
+		out->radius = mobj->radius;
+		out->height = mobj->height;
 		out->subsector = mobj->subsector;
 		out->angle = mobj->player ? mobj->player->drawangle : mobj->angle;
+		out->pitch = mobj->pitch;
+		out->roll = mobj->roll;
+		out->spriteroll = mobj->spriteroll;
 		out->spritexscale = mobj->spritexscale;
 		out->spriteyscale = mobj->spriteyscale;
 		out->spritexoffset = mobj->spritexoffset;
@@ -304,10 +309,22 @@ void R_InterpolateMobjState(mobj_t *mobj, fixed_t frac, interpmobjstate_t *out)
 	out->x = R_LerpFixed(mobj->old_x, mobj->x, frac);
 	out->y = R_LerpFixed(mobj->old_y, mobj->y, frac);
 	out->z = R_LerpFixed(mobj->old_z, mobj->z, frac);
-	out->scale = mobj->resetinterp ? mobj->scale : R_LerpFixed(mobj->old_scale, mobj->scale, frac);
 	out->spritexscale = mobj->resetinterp ? mobj->spritexscale : R_LerpFixed(mobj->old_spritexscale, mobj->spritexscale, frac);
 	out->spriteyscale = mobj->resetinterp ? mobj->spriteyscale : R_LerpFixed(mobj->old_spriteyscale, mobj->spriteyscale, frac);
 
+	if (mobj->scale == mobj->old_scale) // Tiny optimisation - scale is usually unchanging, so let's skip a lerp, two FixedMuls, and two FixedDivs
+	{
+		out->scale = mobj->scale;
+		out->radius = mobj->radius;
+		out->height = mobj->height;
+	}
+	else
+	{
+		out->scale = R_LerpFixed(mobj->old_scale, mobj->scale, frac);
+		out->radius = FixedMul(mobj->radius, FixedDiv(out->scale, mobj->scale));
+		out->height = FixedMul(mobj->height, FixedDiv(out->scale, mobj->scale));
+	}
+
 	// Sprite offsets are not interpolated until we have a way to interpolate them explicitly in Lua.
 	// It seems existing mods visually break more often than not if it is interpolated.
 	out->spritexoffset = mobj->spritexoffset;
@@ -323,6 +340,10 @@ void R_InterpolateMobjState(mobj_t *mobj, fixed_t frac, interpmobjstate_t *out)
 	{
 		out->angle = mobj->resetinterp ? mobj->angle : R_LerpAngle(mobj->old_angle, mobj->angle, frac);
 	}
+
+	out->pitch = mobj->resetinterp ? mobj->pitch : R_LerpAngle(mobj->old_pitch, mobj->pitch, frac);
+	out->roll = mobj->resetinterp ? mobj->roll : R_LerpAngle(mobj->old_roll, mobj->roll, frac);
+	out->spriteroll = mobj->resetinterp ? mobj->spriteroll : R_LerpAngle(mobj->old_spriteroll, mobj->spriteroll, frac);
 }
 
 void R_InterpolatePrecipMobjState(precipmobj_t *mobj, fixed_t frac, interpmobjstate_t *out)
@@ -333,8 +354,13 @@ void R_InterpolatePrecipMobjState(precipmobj_t *mobj, fixed_t frac, interpmobjst
 		out->y = mobj->y;
 		out->z = mobj->z;
 		out->scale = FRACUNIT;
+		out->radius = mobj->radius;
+		out->height = mobj->height;
 		out->subsector = mobj->subsector;
 		out->angle = mobj->angle;
+		out->pitch = mobj->angle;
+		out->roll = mobj->roll;
+		out->spriteroll = mobj->spriteroll;
 		out->spritexscale = mobj->spritexscale;
 		out->spriteyscale = mobj->spriteyscale;
 		out->spritexoffset = mobj->spritexoffset;
@@ -346,6 +372,8 @@ void R_InterpolatePrecipMobjState(precipmobj_t *mobj, fixed_t frac, interpmobjst
 	out->y = R_LerpFixed(mobj->old_y, mobj->y, frac);
 	out->z = R_LerpFixed(mobj->old_z, mobj->z, frac);
 	out->scale = FRACUNIT;
+	out->radius = mobj->radius;
+	out->height = mobj->height;
 	out->spritexscale = R_LerpFixed(mobj->old_spritexscale, mobj->spritexscale, frac);
 	out->spriteyscale = R_LerpFixed(mobj->old_spriteyscale, mobj->spriteyscale, frac);
 	out->spritexoffset = R_LerpFixed(mobj->old_spritexoffset, mobj->spritexoffset, frac);
@@ -354,6 +382,9 @@ void R_InterpolatePrecipMobjState(precipmobj_t *mobj, fixed_t frac, interpmobjst
 	out->subsector = R_PointInSubsector(out->x, out->y);
 
 	out->angle = R_LerpAngle(mobj->old_angle, mobj->angle, frac);
+	out->pitch = R_LerpAngle(mobj->old_pitch, mobj->pitch, frac);
+	out->roll = R_LerpAngle(mobj->old_roll, mobj->roll, frac);
+	out->spriteroll = R_LerpAngle(mobj->old_spriteroll, mobj->spriteroll, frac);
 }
 
 static void AddInterpolator(levelinterpolator_t* interpolator)
@@ -369,12 +400,11 @@ static void AddInterpolator(levelinterpolator_t* interpolator)
 			levelinterpolators_size *= 2;
 		}
 
-		levelinterpolators = Z_ReallocAlign(
+		levelinterpolators = Z_Realloc(
 			(void*) levelinterpolators,
 			sizeof(levelinterpolator_t*) * levelinterpolators_size,
 			PU_LEVEL,
-			NULL,
-			sizeof(levelinterpolator_t*) * 8
+			NULL
 		);
 	}
 
@@ -384,11 +414,8 @@ static void AddInterpolator(levelinterpolator_t* interpolator)
 
 static levelinterpolator_t *CreateInterpolator(levelinterpolator_type_e type, thinker_t *thinker)
 {
-	levelinterpolator_t *ret = (levelinterpolator_t*) Z_CallocAlign(
-		sizeof(levelinterpolator_t),
-		PU_LEVEL,
-		NULL,
-		sizeof(levelinterpolator_t) * 8
+	levelinterpolator_t *ret = (levelinterpolator_t*) Z_Calloc(
+		sizeof(levelinterpolator_t), PU_LEVEL, NULL
 	);
 
 	ret->type = type;
@@ -421,13 +448,13 @@ void R_CreateInterpolator_SectorScroll(thinker_t *thinker, sector_t *sector, boo
 	interp->sectorscroll.ceiling = ceiling;
 	if (ceiling)
 	{
-		interp->sectorscroll.oldxoffs = interp->sectorscroll.bakxoffs = sector->ceiling_xoffs;
-		interp->sectorscroll.oldyoffs = interp->sectorscroll.bakyoffs = sector->ceiling_yoffs;
+		interp->sectorscroll.oldxoffs = interp->sectorscroll.bakxoffs = sector->ceilingxoffset;
+		interp->sectorscroll.oldyoffs = interp->sectorscroll.bakyoffs = sector->ceilingyoffset;
 	}
 	else
 	{
-		interp->sectorscroll.oldxoffs = interp->sectorscroll.bakxoffs = sector->floor_xoffs;
-		interp->sectorscroll.oldyoffs = interp->sectorscroll.bakyoffs = sector->floor_yoffs;
+		interp->sectorscroll.oldxoffs = interp->sectorscroll.bakxoffs = sector->floorxoffset;
+		interp->sectorscroll.oldyoffs = interp->sectorscroll.bakyoffs = sector->flooryoffset;
 	}
 }
 
@@ -490,9 +517,9 @@ static void UpdateLevelInterpolatorState(levelinterpolator_t *interp)
 		break;
 	case LVLINTERP_SectorScroll:
 		interp->sectorscroll.oldxoffs = interp->sectorscroll.bakxoffs;
-		interp->sectorscroll.bakxoffs = interp->sectorscroll.ceiling ? interp->sectorscroll.sector->ceiling_xoffs : interp->sectorscroll.sector->floor_xoffs;
+		interp->sectorscroll.bakxoffs = interp->sectorscroll.ceiling ? interp->sectorscroll.sector->ceilingxoffset : interp->sectorscroll.sector->floorxoffset;
 		interp->sectorscroll.oldyoffs = interp->sectorscroll.bakyoffs;
-		interp->sectorscroll.bakyoffs = interp->sectorscroll.ceiling ? interp->sectorscroll.sector->ceiling_yoffs : interp->sectorscroll.sector->floor_yoffs;
+		interp->sectorscroll.bakyoffs = interp->sectorscroll.ceiling ? interp->sectorscroll.sector->ceilingyoffset : interp->sectorscroll.sector->flooryoffset;
 		break;
 	case LVLINTERP_SideScroll:
 		interp->sidescroll.oldtextureoffset = interp->sidescroll.baktextureoffset;
@@ -578,13 +605,13 @@ void R_ApplyLevelInterpolators(fixed_t frac)
 		case LVLINTERP_SectorScroll:
 			if (interp->sectorscroll.ceiling)
 			{
-				interp->sectorscroll.sector->ceiling_xoffs = R_LerpFixed(interp->sectorscroll.oldxoffs, interp->sectorscroll.bakxoffs, frac);
-				interp->sectorscroll.sector->ceiling_yoffs = R_LerpFixed(interp->sectorscroll.oldyoffs, interp->sectorscroll.bakyoffs, frac);
+				interp->sectorscroll.sector->ceilingxoffset = R_LerpFixed(interp->sectorscroll.oldxoffs, interp->sectorscroll.bakxoffs, frac);
+				interp->sectorscroll.sector->ceilingyoffset = R_LerpFixed(interp->sectorscroll.oldyoffs, interp->sectorscroll.bakyoffs, frac);
 			}
 			else
 			{
-				interp->sectorscroll.sector->floor_xoffs = R_LerpFixed(interp->sectorscroll.oldxoffs, interp->sectorscroll.bakxoffs, frac);
-				interp->sectorscroll.sector->floor_yoffs = R_LerpFixed(interp->sectorscroll.oldyoffs, interp->sectorscroll.bakyoffs, frac);
+				interp->sectorscroll.sector->floorxoffset = R_LerpFixed(interp->sectorscroll.oldxoffs, interp->sectorscroll.bakxoffs, frac);
+				interp->sectorscroll.sector->flooryoffset = R_LerpFixed(interp->sectorscroll.oldyoffs, interp->sectorscroll.bakyoffs, frac);
 			}
 			break;
 		case LVLINTERP_SideScroll:
@@ -633,13 +660,13 @@ void R_RestoreLevelInterpolators(void)
 		case LVLINTERP_SectorScroll:
 			if (interp->sectorscroll.ceiling)
 			{
-				interp->sectorscroll.sector->ceiling_xoffs = interp->sectorscroll.bakxoffs;
-				interp->sectorscroll.sector->ceiling_yoffs = interp->sectorscroll.bakyoffs;
+				interp->sectorscroll.sector->ceilingxoffset = interp->sectorscroll.bakxoffs;
+				interp->sectorscroll.sector->ceilingyoffset = interp->sectorscroll.bakyoffs;
 			}
 			else
 			{
-				interp->sectorscroll.sector->floor_xoffs = interp->sectorscroll.bakxoffs;
-				interp->sectorscroll.sector->floor_yoffs = interp->sectorscroll.bakyoffs;
+				interp->sectorscroll.sector->floorxoffset = interp->sectorscroll.bakxoffs;
+				interp->sectorscroll.sector->flooryoffset = interp->sectorscroll.bakyoffs;
 			}
 			break;
 		case LVLINTERP_SideScroll:
@@ -703,12 +730,11 @@ void R_AddMobjInterpolator(mobj_t *mobj)
 			interpolated_mobjs_capacity *= 2;
 		}
 
-		interpolated_mobjs = Z_ReallocAlign(
+		interpolated_mobjs = Z_Realloc(
 			interpolated_mobjs,
 			sizeof(mobj_t *) * interpolated_mobjs_capacity,
 			PU_LEVEL,
-			NULL,
-			64
+			NULL
 		);
 	}
 
@@ -771,6 +797,7 @@ void R_ResetMobjInterpolationState(mobj_t *mobj)
 	mobj->old_angle2 = mobj->old_angle;
 	mobj->old_pitch2 = mobj->old_pitch;
 	mobj->old_roll2 = mobj->old_roll;
+	mobj->old_spriteroll2 = mobj->old_spriteroll;
 	mobj->old_scale2 = mobj->old_scale;
 	mobj->old_x = mobj->x;
 	mobj->old_y = mobj->y;
@@ -778,6 +805,7 @@ void R_ResetMobjInterpolationState(mobj_t *mobj)
 	mobj->old_angle = mobj->angle;
 	mobj->old_pitch = mobj->pitch;
 	mobj->old_roll = mobj->roll;
+	mobj->old_spriteroll = mobj->spriteroll;
 	mobj->old_scale = mobj->scale;
 	mobj->old_spritexscale = mobj->spritexscale;
 	mobj->old_spriteyscale = mobj->spriteyscale;
@@ -806,10 +834,14 @@ void R_ResetPrecipitationMobjInterpolationState(precipmobj_t *mobj)
 	mobj->old_angle2 = mobj->old_angle;
 	mobj->old_pitch2 = mobj->old_pitch;
 	mobj->old_roll2 = mobj->old_roll;
+	mobj->old_spriteroll2 = mobj->old_spriteroll;
 	mobj->old_x = mobj->x;
 	mobj->old_y = mobj->y;
 	mobj->old_z = mobj->z;
 	mobj->old_angle = mobj->angle;
+	mobj->old_pitch = mobj->pitch;
+	mobj->old_roll = mobj->roll;
+	mobj->old_spriteroll = mobj->spriteroll;
 	mobj->old_spritexscale = mobj->spritexscale;
 	mobj->old_spriteyscale = mobj->spriteyscale;
 	mobj->old_spritexoffset = mobj->spritexoffset;
diff --git a/src/r_fps.h b/src/r_fps.h
index 85c87a2f49ff1c3177f7fb8b8e9136e88316ebc9..f43d29f300a8a6707a3e4c6f5fa24e1e3f0ea37f 100644
--- a/src/r_fps.h
+++ b/src/r_fps.h
@@ -59,7 +59,12 @@ typedef struct {
 	fixed_t z;
 	subsector_t *subsector;
 	angle_t angle;
+	angle_t pitch;
+	angle_t roll;
+	angle_t spriteroll;
 	fixed_t scale;
+	fixed_t radius;
+	fixed_t height;
 	fixed_t spritexscale;
 	fixed_t spriteyscale;
 	fixed_t spritexoffset;
diff --git a/src/r_main.c b/src/r_main.c
index ebf7a28bf10b286a2453eb94dbdb52891eef4355..0655bd06f04536931dfe3baaa1435b9c847fe091 100644
--- a/src/r_main.c
+++ b/src/r_main.c
@@ -41,16 +41,6 @@
 #include "hardware/hw_main.h"
 #endif
 
-//profile stuff ---------------------------------------------------------
-//#define TIMING
-#ifdef TIMING
-#include "p5prof.h"
-INT64 mycount;
-INT64 mytotal = 0;
-//unsigned long  nombre = 100000;
-#endif
-//profile stuff ---------------------------------------------------------
-
 // Fineangles in the SCREENWIDTH wide window.
 #define FIELDOFVIEW 2048
 
@@ -157,7 +147,8 @@ consvar_t cv_flipcam2 = CVAR_INIT ("flipcam2", "No", CV_SAVE|CV_CALL|CV_NOINIT,
 
 consvar_t cv_shadow = CVAR_INIT ("shadow", "On", CV_SAVE, CV_OnOff, NULL);
 consvar_t cv_skybox = CVAR_INIT ("skybox", "On", CV_SAVE, CV_OnOff, NULL);
-consvar_t cv_ffloorclip = CVAR_INIT ("ffloorclip", "On", CV_SAVE, CV_OnOff, NULL);
+consvar_t cv_ffloorclip = CVAR_INIT ("r_ffloorclip", "On", CV_SAVE, CV_OnOff, NULL);
+consvar_t cv_spriteclip = CVAR_INIT ("r_spriteclip", "On", CV_SAVE, CV_OnOff, NULL);
 consvar_t cv_allowmlook = CVAR_INIT ("allowmlook", "Yes", CV_NETVAR|CV_ALLOWLUA, CV_YesNo, NULL);
 consvar_t cv_showhud = CVAR_INIT ("showhud", "Yes", CV_CALL|CV_ALLOWLUA,  CV_YesNo, R_SetViewSize);
 consvar_t cv_translucenthud = CVAR_INIT ("translucenthud", "10", CV_SAVE, translucenthud_cons_t, NULL);
@@ -258,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;
@@ -270,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
@@ -1092,34 +1084,12 @@ subsector_t *R_PointInSubsectorOrNull(fixed_t x, fixed_t y)
 void R_SetupFrame(player_t *player)
 {
 	camera_t *thiscam;
-	boolean chasecam = false;
-
-	if (splitscreen && player == &players[secondarydisplayplayer]
-		&& player != &players[consoleplayer])
-	{
+	boolean chasecam = R_ViewpointHasChasecam(player);
+	
+	if (splitscreen && player == &players[secondarydisplayplayer] && player != &players[consoleplayer])
 		thiscam = &camera2;
-		chasecam = (cv_chasecam2.value != 0);
-		R_SetViewContext(VIEWCONTEXT_PLAYER2);
-	}
 	else
-	{
 		thiscam = &camera;
-		chasecam = (cv_chasecam.value != 0);
-		R_SetViewContext(VIEWCONTEXT_PLAYER1);
-	}
-
-	if (player->climbing || (player->powers[pw_carry] == CR_NIGHTSMODE) || player->playerstate == PST_DEAD || gamestate == GS_TITLESCREEN || tutorialmode)
-		chasecam = true; // force chasecam on
-	else if (player->spectator) // no spectator chasecam
-		chasecam = false; // force chasecam off
-
-	if (chasecam && !thiscam->chase)
-	{
-		P_ResetCamera(player, thiscam);
-		thiscam->chase = true;
-	}
-	else if (!chasecam)
-		thiscam->chase = false;
 
 	newview->sky = false;
 
@@ -1348,11 +1318,37 @@ boolean R_ViewpointHasChasecam(player_t *player)
 {
 	camera_t *thiscam;
 	boolean chasecam = false;
+	boolean isplayer2 = (splitscreen && player == &players[secondarydisplayplayer] && player != &players[consoleplayer]);
 
-	if (splitscreen && player == &players[secondarydisplayplayer] && player != &players[consoleplayer])
+	if (isplayer2)
 	{
 		thiscam = &camera2;
 		chasecam = (cv_chasecam2.value != 0);
+	}
+	else
+	{
+		thiscam = &camera;
+		chasecam = (cv_chasecam.value != 0);
+	}
+
+	if (player->climbing || (player->powers[pw_carry] == CR_NIGHTSMODE) || player->playerstate == PST_DEAD || gamestate == GS_TITLESCREEN || tutorialmode)
+		chasecam = true; // force chasecam on
+	else if (player->spectator) // no spectator chasecam
+		chasecam = false; // force chasecam off
+		
+	if (chasecam && !thiscam->chase)
+	{
+		P_ResetCamera(player, thiscam);
+		thiscam->chase = true;
+	}
+	else if (!chasecam && thiscam->chase)
+	{
+		P_ResetCamera(player, thiscam);
+		thiscam->chase = false;
+	}
+	
+	if (isplayer2)
+	{
 		R_SetViewContext(VIEWCONTEXT_PLAYER2);
 		if (thiscam->reset)
 		{
@@ -1362,8 +1358,6 @@ boolean R_ViewpointHasChasecam(player_t *player)
 	}
 	else
 	{
-		thiscam = &camera;
-		chasecam = (cv_chasecam.value != 0);
 		R_SetViewContext(VIEWCONTEXT_PLAYER1);
 		if (thiscam->reset)
 		{
@@ -1372,11 +1366,6 @@ boolean R_ViewpointHasChasecam(player_t *player)
 		}
 	}
 
-	if (player->climbing || (player->powers[pw_carry] == CR_NIGHTSMODE) || player->playerstate == PST_DEAD || gamestate == GS_TITLESCREEN || tutorialmode)
-		chasecam = true; // force chasecam on
-	else if (player->spectator) // no spectator chasecam
-		chasecam = false; // force chasecam off
-
 	return chasecam;
 }
 
@@ -1482,6 +1471,7 @@ void R_RenderPlayerView(player_t *player)
 		R_ClearClipSegs();
 	}
 	R_ClearDrawSegs();
+	R_ClearSegTables();
 	R_ClearSprites();
 	Portal_InitList();
 
@@ -1492,29 +1482,17 @@ void R_RenderPlayerView(player_t *player)
 
 	Mask_Pre(&masks[nummasks - 1]);
 	curdrawsegs = ds_p;
-//profile stuff ---------------------------------------------------------
-#ifdef TIMING
-	mytotal = 0;
-	ProfZeroTimer();
-#endif
 	ps_numbspcalls.value.i = ps_numpolyobjects.value.i = ps_numdrawnodes.value.i = 0;
 	PS_START_TIMING(ps_bsptime);
 	R_RenderBSPNode((INT32)numnodes - 1);
 	PS_STOP_TIMING(ps_bsptime);
-	ps_numsprites.value.i = visspritecount;
-#ifdef TIMING
-	RDMSR(0x10, &mycount);
-	mytotal += mycount; // 64bit add
-
-	CONS_Debug(DBG_RENDER, "RenderBSPNode: 0x%d %d\n", *((INT32 *)&mytotal + 1), (INT32)mytotal);
-#endif
-//profile stuff ---------------------------------------------------------
 	Mask_Post(&masks[nummasks - 1]);
 
 	PS_START_TIMING(ps_sw_spritecliptime);
 	R_ClipSprites(drawsegs, NULL);
 	PS_STOP_TIMING(ps_sw_spritecliptime);
 
+	ps_numsprites.value.i = numvisiblesprites;
 
 	// Add skybox portals caused by sky visplanes.
 	if (cv_skybox.value && skyboxmo[0])
@@ -1605,6 +1583,7 @@ void R_RegisterEngineStuff(void)
 	CV_RegisterVar(&cv_shadow);
 	CV_RegisterVar(&cv_skybox);
 	CV_RegisterVar(&cv_ffloorclip);
+	CV_RegisterVar(&cv_spriteclip);
 
 	CV_RegisterVar(&cv_cam_dist);
 	CV_RegisterVar(&cv_cam_still);
diff --git a/src/r_main.h b/src/r_main.h
index f08070d0f387b544c9b0b5089e65b6251999db99..a6fb42ba2410ba42c8adc7ba84ef8fde21e0802f 100644
--- a/src/r_main.h
+++ b/src/r_main.h
@@ -114,7 +114,7 @@ extern consvar_t cv_chasecam, cv_chasecam2;
 extern consvar_t cv_flipcam, cv_flipcam2;
 
 extern consvar_t cv_shadow;
-extern consvar_t cv_ffloorclip;
+extern consvar_t cv_ffloorclip, cv_spriteclip;
 extern consvar_t cv_translucency;
 extern consvar_t cv_drawdist, cv_drawdist_nights, cv_drawdist_precip;
 extern consvar_t cv_fov;
diff --git a/src/r_patch.h b/src/r_patch.h
index d2106a390019b357702c573c414842a615ba555b..a0ab3e75ac0c81a7fa16b6129cd2deabb8d02d46 100644
--- a/src/r_patch.h
+++ b/src/r_patch.h
@@ -14,6 +14,7 @@
 
 #include "r_defs.h"
 #include "r_picformats.h"
+#include "r_fps.h"
 #include "doomdef.h"
 
 // Patch functions
@@ -38,6 +39,8 @@ patch_t *Patch_GetRotatedSprite(
 	size_t frame, size_t spriteangle,
 	boolean flip, boolean adjustfeet,
 	void *info, INT32 rotationangle);
+angle_t R_ModelRotationAngle(interpmobjstate_t *interp);
+angle_t R_SpriteRotationAngle(interpmobjstate_t *interp);
 INT32 R_GetRollAngle(angle_t rollangle);
 #endif
 
diff --git a/src/r_patchrotation.c b/src/r_patchrotation.c
index 3d3c6c512893d1670b4b93b96b32d670b6300e93..b0cbeaa42911d1506d9928b906198afdf50ff0be 100644
--- a/src/r_patchrotation.c
+++ b/src/r_patchrotation.c
@@ -13,11 +13,33 @@
 #include "r_things.h" // FEETADJUST
 #include "z_zone.h"
 #include "w_wad.h"
+#include "r_main.h" // R_PointToAngle
 
 #ifdef ROTSPRITE
 fixed_t rollcosang[ROTANGLES];
 fixed_t rollsinang[ROTANGLES];
 
+angle_t R_ModelRotationAngle(interpmobjstate_t *interp)
+{
+	return interp->spriteroll;
+}
+
+angle_t R_SpriteRotationAngle(interpmobjstate_t *interp)
+{
+#if 0
+	angle_t viewingAngle = R_PointToAngle(interp->x, interp->y);
+
+	fixed_t pitchMul = -FINESINE(viewingAngle >> ANGLETOFINESHIFT);
+	fixed_t rollMul = FINECOSINE(viewingAngle >> ANGLETOFINESHIFT);
+
+	angle_t rollOrPitch = FixedMul(interp->pitch, pitchMul) + FixedMul(interp->roll, rollMul);
+
+	return (rollOrPitch + R_ModelRotationAngle(interp));
+#else
+	return R_ModelRotationAngle(interp);
+#endif
+}
+
 INT32 R_GetRollAngle(angle_t rollangle)
 {
 	INT32 ra = AngleFixed(rollangle)>>FRACBITS;
diff --git a/src/r_picformats.c b/src/r_picformats.c
index 9aea8c6c1496430cac950ad7c7a7de408f6fbf62..3e817f4a06199bb47c38f198c6ceb6cd447a6452 100644
--- a/src/r_picformats.c
+++ b/src/r_picformats.c
@@ -1407,7 +1407,6 @@ static void R_ParseSpriteInfoFrame(spriteinfo_t *info)
 	UINT8 frameFrame = 0xFF;
 	INT16 frameXPivot = 0;
 	INT16 frameYPivot = 0;
-	rotaxis_t frameRotAxis = 0;
 
 	// Sprite identifier
 	sprinfoToken = M_GetToken(NULL);
@@ -1458,12 +1457,6 @@ static void R_ParseSpriteInfoFrame(spriteinfo_t *info)
 				{
 					Z_Free(sprinfoToken);
 					sprinfoToken = M_GetToken(NULL);
-					if ((stricmp(sprinfoToken, "X")==0) || (stricmp(sprinfoToken, "XAXIS")==0) || (stricmp(sprinfoToken, "ROLL")==0))
-						frameRotAxis = ROTAXIS_X;
-					else if ((stricmp(sprinfoToken, "Y")==0) || (stricmp(sprinfoToken, "YAXIS")==0) || (stricmp(sprinfoToken, "PITCH")==0))
-						frameRotAxis = ROTAXIS_Y;
-					else if ((stricmp(sprinfoToken, "Z")==0) || (stricmp(sprinfoToken, "ZAXIS")==0) || (stricmp(sprinfoToken, "YAW")==0))
-						frameRotAxis = ROTAXIS_Z;
 				}
 				Z_Free(sprinfoToken);
 
@@ -1480,7 +1473,6 @@ static void R_ParseSpriteInfoFrame(spriteinfo_t *info)
 	// set fields
 	info->pivot[frameFrame].x = frameXPivot;
 	info->pivot[frameFrame].y = frameYPivot;
-	info->pivot[frameFrame].rotaxis = frameRotAxis;
 }
 
 //
diff --git a/src/r_picformats.h b/src/r_picformats.h
index 4f9637460085b98ae3e5c629fff57c06678f9b02..4050a1b71dc01e8a60cbb14cbe017716c61038c3 100644
--- a/src/r_picformats.h
+++ b/src/r_picformats.h
@@ -95,7 +95,6 @@ typedef enum
 typedef struct
 {
 	INT32 x, y;
-	rotaxis_t rotaxis;
 } spriteframepivot_t;
 
 typedef struct
diff --git a/src/r_plane.c b/src/r_plane.c
index c568484b6ed7e71335a665a40710753d2608133f..29ce26b292e5ea529d937aded0028e41a8d294ba 100644
--- a/src/r_plane.c
+++ b/src/r_plane.c
@@ -53,10 +53,6 @@ INT32 numffloors;
 #define visplane_hash(picnum,lightlevel,height) \
   ((unsigned)((picnum)*3+(lightlevel)+(height)*7) & VISPLANEHASHMASK)
 
-//SoM: 3/23/2000: Use boom opening limit removal
-size_t maxopenings;
-INT16 *openings, *lastopening; /// \todo free leak
-
 //
 // Clip values are the solid pixel bounding the range.
 //  floorclip starts out SCREENHEIGHT
@@ -366,8 +362,6 @@ void R_ClearPlanes(void)
 		freehead = &(*freehead)->next;
 	}
 
-	lastopening = openings;
-
 	// texture calculation
 	memset(cachedheight, 0, sizeof (cachedheight));
 }
diff --git a/src/r_plane.h b/src/r_plane.h
index 9870a43e26286e5e888a13f20fa72ecf8b38be08..917e8b041b75775016dbc2a1e10b8cc6ad7a3778 100644
--- a/src/r_plane.h
+++ b/src/r_plane.h
@@ -60,9 +60,6 @@ extern visplane_t *floorplane;
 extern visplane_t *ceilingplane;
 
 // Visplane related.
-extern INT16 *lastopening, *openings;
-extern size_t maxopenings;
-
 extern INT16 floorclip[MAXVIDWIDTH], ceilingclip[MAXVIDWIDTH];
 extern fixed_t frontscale[MAXVIDWIDTH], yslopetab[MAXVIDHEIGHT*16];
 extern fixed_t cachedheight[MAXVIDHEIGHT];
diff --git a/src/r_segs.c b/src/r_segs.c
index 71fc9f9b2a9801643e646e603ca2f48721e76c28..019a0d5c6a9f7e316d984546e7cb7c7cd5c9bae5 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"
@@ -49,6 +49,7 @@ 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_scale, rw_scalestep;
 static fixed_t rw_midtexturemid, rw_toptexturemid, rw_bottomtexturemid;
@@ -70,9 +71,22 @@ static fixed_t topfrac, topstep;
 static fixed_t bottomfrac, bottomstep;
 
 static lighttable_t **walllights;
-static INT16 *maskedtexturecol;
+static fixed_t *maskedtexturecol;
 static fixed_t *maskedtextureheight = NULL;
 
+//SoM: 3/23/2000: Use boom opening limit removal
+static size_t numopenings;
+static INT16 *openings, *lastopening;
+
+static size_t texturecolumntablesize;
+static fixed_t *texturecolumntable, *curtexturecolumntable;
+
+void R_ClearSegTables(void)
+{
+	lastopening = openings;
+	curtexturecolumntable = texturecolumntable;
+}
+
 // ==========================================================================
 // R_RenderMaskedSegRange
 // ==========================================================================
@@ -349,170 +363,115 @@ void R_RenderMaskedSegRange(drawseg_t *ds, INT32 x1, INT32 x2)
 				dc_texturemid += (textureheight[texnum])*times + textureheight[texnum];
 			else
 				dc_texturemid -= (textureheight[texnum])*times;
-			// calculate lighting
-			if (maskedtexturecol[dc_x] != INT16_MAX)
+
+			// Check for overflows first
+			overflow_test = (INT64)centeryfrac - (((INT64)dc_texturemid*spryscale)>>FRACBITS);
+			if (overflow_test < 0) overflow_test = -overflow_test;
+			if ((UINT64)overflow_test&0xFFFFFFFF80000000ULL)
 			{
-				// Check for overflows first
-				overflow_test = (INT64)centeryfrac - (((INT64)dc_texturemid*spryscale)>>FRACBITS);
-				if (overflow_test < 0) overflow_test = -overflow_test;
-				if ((UINT64)overflow_test&0xFFFFFFFF80000000ULL)
+				// Eh, no, go away, don't waste our time
+				if (dc_numlights)
 				{
-					// Eh, no, go away, don't waste our time
-					if (dc_numlights)
+					for (i = 0; i < dc_numlights; i++)
 					{
-						for (i = 0; i < dc_numlights; i++)
-						{
-							rlight = &dc_lightlist[i];
-							rlight->height += rlight->heightstep;
-						}
+						rlight = &dc_lightlist[i];
+						rlight->height += rlight->heightstep;
 					}
-					spryscale += rw_scalestep;
-					continue;
 				}
+				spryscale += rw_scalestep;
+				continue;
+			}
 
-				if (dc_numlights)
-				{
-					lighttable_t **xwalllights;
-
-					sprbotscreen = INT32_MAX;
-					sprtopscreen = windowtop = (centeryfrac - FixedMul(dc_texturemid, spryscale));
-
-					realbot = windowbottom = FixedMul(textureheight[texnum], spryscale) + sprtopscreen;
-					dc_iscale = 0xffffffffu / (unsigned)spryscale;
+			// calculate lighting
+			if (dc_numlights)
+			{
+				lighttable_t **xwalllights;
 
-					// draw the texture
-					col = (column_t *)((UINT8 *)R_GetColumn(texnum, maskedtexturecol[dc_x]) - 3);
+				sprbotscreen = INT32_MAX;
+				sprtopscreen = windowtop = (centeryfrac - FixedMul(dc_texturemid, spryscale));
 
-					for (i = 0; i < dc_numlights; i++)
-					{
-						rlight = &dc_lightlist[i];
+				realbot = windowbottom = FixedMul(textureheight[texnum], spryscale) + sprtopscreen;
+				dc_iscale = 0xffffffffu / (unsigned)spryscale;
 
-						if ((rlight->flags & FOF_NOSHADE))
-							continue;
+				// draw the texture
+				col = (column_t *)((UINT8 *)R_GetColumn(texnum, (maskedtexturecol[dc_x] >> FRACBITS)) - 3);
 
-						if (rlight->lightnum < 0)
-							xwalllights = scalelight[0];
-						else if (rlight->lightnum >= LIGHTLEVELS)
-							xwalllights = scalelight[LIGHTLEVELS-1];
-						else
-							xwalllights = scalelight[rlight->lightnum];
+				for (i = 0; i < dc_numlights; i++)
+				{
+					rlight = &dc_lightlist[i];
 
-						pindex = FixedMul(spryscale, LIGHTRESOLUTIONFIX)>>LIGHTSCALESHIFT;
+					if ((rlight->flags & FOF_NOSHADE))
+						continue;
 
-						if (pindex >= MAXLIGHTSCALE)
-							pindex = MAXLIGHTSCALE - 1;
+					if (rlight->lightnum < 0)
+						xwalllights = scalelight[0];
+					else if (rlight->lightnum >= LIGHTLEVELS)
+						xwalllights = scalelight[LIGHTLEVELS-1];
+					else
+						xwalllights = scalelight[rlight->lightnum];
 
-						if (rlight->extra_colormap)
-							rlight->rcolormap = rlight->extra_colormap->colormap + (xwalllights[pindex] - colormaps);
-						else
-							rlight->rcolormap = xwalllights[pindex];
+					pindex = FixedMul(spryscale, LIGHTRESOLUTIONFIX)>>LIGHTSCALESHIFT;
 
-						height = rlight->height;
-						rlight->height += rlight->heightstep;
+					if (pindex >= MAXLIGHTSCALE)
+						pindex = MAXLIGHTSCALE - 1;
 
-						if (height <= windowtop)
-						{
-							dc_colormap = rlight->rcolormap;
-							continue;
-						}
+					if (rlight->extra_colormap)
+						rlight->rcolormap = rlight->extra_colormap->colormap + (xwalllights[pindex] - colormaps);
+					else
+						rlight->rcolormap = xwalllights[pindex];
 
-						windowbottom = height;
-						if (windowbottom >= realbot)
-						{
-							windowbottom = realbot;
-							colfunc_2s(col);
-							for (i++; i < dc_numlights; i++)
-							{
-								rlight = &dc_lightlist[i];
-								rlight->height += rlight->heightstep;
-							}
+					height = rlight->height;
+					rlight->height += rlight->heightstep;
 
-							continue;
-						}
-						colfunc_2s(col);
-						windowtop = windowbottom + 1;
+					if (height <= windowtop)
+					{
 						dc_colormap = rlight->rcolormap;
+						continue;
 					}
-					windowbottom = realbot;
-					if (windowtop < windowbottom)
+
+					windowbottom = height;
+					if (windowbottom >= realbot)
+					{
+						windowbottom = realbot;
 						colfunc_2s(col);
+						for (i++; i < dc_numlights; i++)
+						{
+							rlight = &dc_lightlist[i];
+							rlight->height += rlight->heightstep;
+						}
 
-					spryscale += rw_scalestep;
-					continue;
+						continue;
+					}
+					colfunc_2s(col);
+					windowtop = windowbottom + 1;
+					dc_colormap = rlight->rcolormap;
 				}
+				windowbottom = realbot;
+				if (windowtop < windowbottom)
+					colfunc_2s(col);
 
-				// calculate lighting
-				pindex = FixedMul(spryscale, LIGHTRESOLUTIONFIX)>>LIGHTSCALESHIFT;
-
-				if (pindex >= MAXLIGHTSCALE)
-					pindex = MAXLIGHTSCALE - 1;
-
-				dc_colormap = walllights[pindex];
-
-				if (frontsector->extra_colormap)
-					dc_colormap = frontsector->extra_colormap->colormap + (dc_colormap - colormaps);
-
-				sprtopscreen = centeryfrac - FixedMul(dc_texturemid, spryscale);
-				dc_iscale = 0xffffffffu / (unsigned)spryscale;
-
-				// draw the texture
-				col = (column_t *)((UINT8 *)R_GetColumn(texnum, maskedtexturecol[dc_x]) - 3);
-
-#if 0 // Disabling this allows inside edges to render below the planes, for until the clipping is fixed to work right when POs are near the camera. -Red
-				if (curline->dontrenderme && curline->polyseg && (curline->polyseg->flags & POF_RENDERPLANES))
-				{
-					fixed_t my_topscreen;
-					fixed_t my_bottomscreen;
-					fixed_t my_yl, my_yh;
+				spryscale += rw_scalestep;
+				continue;
+			}
 
-					my_topscreen = sprtopscreen + spryscale*col->topdelta;
-					my_bottomscreen = sprbotscreen == INT32_MAX ? my_topscreen + spryscale*col->length
-					                                         : sprbotscreen + spryscale*col->length;
+			// calculate lighting
+			pindex = FixedMul(spryscale, LIGHTRESOLUTIONFIX)>>LIGHTSCALESHIFT;
 
-					my_yl = (my_topscreen+FRACUNIT-1)>>FRACBITS;
-					my_yh = (my_bottomscreen-1)>>FRACBITS;
-	//				CONS_Debug(DBG_RENDER, "my_topscreen: %d\nmy_bottomscreen: %d\nmy_yl: %d\nmy_yh: %d\n", my_topscreen, my_bottomscreen, my_yl, my_yh);
+			if (pindex >= MAXLIGHTSCALE)
+				pindex = MAXLIGHTSCALE - 1;
 
-					if (numffloors)
-					{
-						INT32 top = my_yl;
-						INT32 bottom = my_yh;
+			dc_colormap = walllights[pindex];
 
-						for (i = 0; i < numffloors; i++)
-						{
-							if (!ffloor[i].polyobj || ffloor[i].polyobj != curline->polyseg)
-								continue;
+			if (frontsector->extra_colormap)
+				dc_colormap = frontsector->extra_colormap->colormap + (dc_colormap - colormaps);
 
-							if (ffloor[i].height < viewz)
-							{
-								INT32 top_w = ffloor[i].plane->top[dc_x];
+			sprtopscreen = centeryfrac - FixedMul(dc_texturemid, spryscale);
+			dc_iscale = 0xffffffffu / (unsigned)spryscale;
 
-	//							CONS_Debug(DBG_RENDER, "Leveltime : %d\n", leveltime);
-	//							CONS_Debug(DBG_RENDER, "Top is %d, top_w is %d\n", top, top_w);
-								if (top_w < top)
-								{
-									ffloor[i].plane->top[dc_x] = (INT16)top;
-									ffloor[i].plane->picnum = 0;
-								}
-	//							CONS_Debug(DBG_RENDER, "top_w is now %d\n", ffloor[i].plane->top[dc_x]);
-							}
-							else if (ffloor[i].height > viewz)
-							{
-								INT32 bottom_w = ffloor[i].plane->bottom[dc_x];
+			// draw the texture
+			col = (column_t *)((UINT8 *)R_GetColumn(texnum, (maskedtexturecol[dc_x] >> FRACBITS)) - 3);
+			colfunc_2s(col);
 
-								if (bottom_w > bottom)
-								{
-									ffloor[i].plane->bottom[dc_x] = (INT16)bottom;
-									ffloor[i].plane->picnum = 0;
-								}
-							}
-						}
-					}
-				}
-				else
-#endif
-					colfunc_2s(col);
-			}
 			spryscale += rw_scalestep;
 		}
 	}
@@ -778,7 +737,7 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 
 	if (newline)
 	{
-		offsetvalue = sides[newline->sidenum[0]].rowoffset;
+		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
@@ -790,7 +749,7 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 	}
 	else
 	{
-		offsetvalue = sides[pfloor->master->sidenum[0]].rowoffset;
+		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
@@ -856,183 +815,182 @@ void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pfloor)
 	// draw the columns
 	for (dc_x = x1; dc_x <= x2; dc_x++)
 	{
-		if (maskedtexturecol[dc_x] != INT16_MAX)
+		// skew FOF walls
+		if (ffloortextureslide)
 		{
-			if (ffloortextureslide) { // skew FOF walls
-				if (oldx != -1)
-					dc_texturemid += FixedMul(ffloortextureslide, (maskedtexturecol[oldx]-maskedtexturecol[dc_x])<<FRACBITS);
-				oldx = dc_x;
-			}
-			// Calculate bounds
-			// clamp the values if necessary to avoid overflows and rendering glitches caused by them
+			if (oldx != -1)
+				dc_texturemid += FixedMul(ffloortextureslide, maskedtexturecol[oldx]-maskedtexturecol[dc_x]);
+			oldx = dc_x;
+		}
 
-			if      (top_frac > (INT64)CLAMPMAX) sprtopscreen = windowtop = CLAMPMAX;
-			else if (top_frac > (INT64)CLAMPMIN) sprtopscreen = windowtop = (fixed_t)top_frac;
-			else                                 sprtopscreen = windowtop = CLAMPMIN;
-			if      (bottom_frac > (INT64)CLAMPMAX) sprbotscreen = windowbottom = CLAMPMAX;
-			else if (bottom_frac > (INT64)CLAMPMIN) sprbotscreen = windowbottom = (fixed_t)bottom_frac;
-			else                                    sprbotscreen = windowbottom = CLAMPMIN;
+		// Calculate bounds
+		// clamp the values if necessary to avoid overflows and rendering glitches caused by them
+		if      (top_frac > (INT64)CLAMPMAX) sprtopscreen = windowtop = CLAMPMAX;
+		else if (top_frac > (INT64)CLAMPMIN) sprtopscreen = windowtop = (fixed_t)top_frac;
+		else                                 sprtopscreen = windowtop = CLAMPMIN;
+		if      (bottom_frac > (INT64)CLAMPMAX) sprbotscreen = windowbottom = CLAMPMAX;
+		else if (bottom_frac > (INT64)CLAMPMIN) sprbotscreen = windowbottom = (fixed_t)bottom_frac;
+		else                                    sprbotscreen = windowbottom = CLAMPMIN;
 
-			top_frac += top_step;
-			bottom_frac += bottom_step;
+		top_frac += top_step;
+		bottom_frac += bottom_step;
 
-			// SoM: If column is out of range, why bother with it??
-			if (windowbottom < topbounds || windowtop > bottombounds)
+		// SoM: If column is out of range, why bother with it??
+		if (windowbottom < topbounds || windowtop > bottombounds)
+		{
+			if (dc_numlights)
 			{
-				if (dc_numlights)
+				for (i = 0; i < dc_numlights; i++)
 				{
-					for (i = 0; i < dc_numlights; i++)
-					{
-						rlight = &dc_lightlist[i];
-						rlight->height += rlight->heightstep;
-						if (rlight->flags & FOF_CUTLEVEL)
-							rlight->botheight += rlight->botheightstep;
-					}
+					rlight = &dc_lightlist[i];
+					rlight->height += rlight->heightstep;
+					if (rlight->flags & FOF_CUTLEVEL)
+						rlight->botheight += rlight->botheightstep;
 				}
-				spryscale += rw_scalestep;
-				continue;
 			}
+			spryscale += rw_scalestep;
+			continue;
+		}
 
-			dc_iscale = 0xffffffffu / (unsigned)spryscale;
+		dc_iscale = 0xffffffffu / (unsigned)spryscale;
 
-			// Get data for the column
-			col = (column_t *)((UINT8 *)R_GetColumn(texnum,maskedtexturecol[dc_x]) - 3);
+		// Get data for the column
+		col = (column_t *)((UINT8 *)R_GetColumn(texnum, (maskedtexturecol[dc_x] >> FRACBITS)) - 3);
 
-			// SoM: New code does not rely on R_DrawColumnShadowed_8 which
-			// will (hopefully) put less strain on the stack.
-			if (dc_numlights)
-			{
-				lighttable_t **xwalllights;
-				fixed_t height;
-				fixed_t bheight = 0;
-				INT32 solid = 0;
-				INT32 lighteffect = 0;
+		// SoM: New code does not rely on R_DrawColumnShadowed_8 which
+		// will (hopefully) put less strain on the stack.
+		if (dc_numlights)
+		{
+			lighttable_t **xwalllights;
+			fixed_t height;
+			fixed_t bheight = 0;
+			INT32 solid = 0;
+			INT32 lighteffect = 0;
 
-				for (i = 0; i < dc_numlights; i++)
+			for (i = 0; i < dc_numlights; i++)
+			{
+				// Check if the current light effects the colormap/lightlevel
+				rlight = &dc_lightlist[i];
+				lighteffect = !(dc_lightlist[i].flags & FOF_NOSHADE);
+				if (lighteffect)
 				{
-					// Check if the current light effects the colormap/lightlevel
-					rlight = &dc_lightlist[i];
-					lighteffect = !(dc_lightlist[i].flags & FOF_NOSHADE);
-					if (lighteffect)
-					{
-						lightnum = rlight->lightnum;
-
-						if (lightnum < 0)
-							xwalllights = scalelight[0];
-						else if (lightnum >= LIGHTLEVELS)
-							xwalllights = scalelight[LIGHTLEVELS-1];
-						else
-							xwalllights = scalelight[lightnum];
-
-						pindex = FixedMul(spryscale, LIGHTRESOLUTIONFIX)>>LIGHTSCALESHIFT;
+					lightnum = rlight->lightnum;
 
-						if (pindex >= MAXLIGHTSCALE)
-							pindex = MAXLIGHTSCALE-1;
+					if (lightnum < 0)
+						xwalllights = scalelight[0];
+					else if (lightnum >= LIGHTLEVELS)
+						xwalllights = scalelight[LIGHTLEVELS-1];
+					else
+						xwalllights = scalelight[lightnum];
 
-						if (pfloor->fofflags & FOF_FOG)
-						{
-							if (pfloor->master->frontsector->extra_colormap)
-								rlight->rcolormap = pfloor->master->frontsector->extra_colormap->colormap + (xwalllights[pindex] - colormaps);
-							else
-								rlight->rcolormap = xwalllights[pindex];
-						}
-						else
-						{
-							if (rlight->extra_colormap)
-								rlight->rcolormap = rlight->extra_colormap->colormap + (xwalllights[pindex] - colormaps);
-							else
-								rlight->rcolormap = xwalllights[pindex];
-						}
-					}
+					pindex = FixedMul(spryscale, LIGHTRESOLUTIONFIX)>>LIGHTSCALESHIFT;
 
-					solid = 0; // don't carry over solid-cutting flag from the previous light
+					if (pindex >= MAXLIGHTSCALE)
+						pindex = MAXLIGHTSCALE-1;
 
-					// Check if the current light can cut the current 3D floor.
-					if (rlight->flags & FOF_CUTSOLIDS && !(pfloor->fofflags & FOF_EXTRA))
-						solid = 1;
-					else if (rlight->flags & FOF_CUTEXTRA && pfloor->fofflags & FOF_EXTRA)
+					if (pfloor->fofflags & FOF_FOG)
 					{
-						if (rlight->flags & FOF_EXTRA)
-						{
-							// The light is from an extra 3D floor... Check the flags so
-							// there are no undesired cuts.
-							if ((rlight->flags & (FOF_FOG|FOF_SWIMMABLE)) == (pfloor->fofflags & (FOF_FOG|FOF_SWIMMABLE)))
-								solid = 1;
-						}
+						if (pfloor->master->frontsector->extra_colormap)
+							rlight->rcolormap = pfloor->master->frontsector->extra_colormap->colormap + (xwalllights[pindex] - colormaps);
 						else
-							solid = 1;
+							rlight->rcolormap = xwalllights[pindex];
 					}
 					else
-						solid = 0;
-
-					height = rlight->height;
-					rlight->height += rlight->heightstep;
-
-					if (solid)
 					{
-						bheight = rlight->botheight - (FRACUNIT >> 1);
-						rlight->botheight += rlight->botheightstep;
+						if (rlight->extra_colormap)
+							rlight->rcolormap = rlight->extra_colormap->colormap + (xwalllights[pindex] - colormaps);
+						else
+							rlight->rcolormap = xwalllights[pindex];
 					}
+				}
 
-					if (height <= windowtop)
-					{
-						if (lighteffect)
-							dc_colormap = rlight->rcolormap;
-						if (solid && windowtop < bheight)
-							windowtop = bheight;
-						continue;
-					}
+				solid = 0; // don't carry over solid-cutting flag from the previous light
 
-					windowbottom = height;
-					if (windowbottom >= sprbotscreen)
+				// Check if the current light can cut the current 3D floor.
+				if (rlight->flags & FOF_CUTSOLIDS && !(pfloor->fofflags & FOF_EXTRA))
+					solid = 1;
+				else if (rlight->flags & FOF_CUTEXTRA && pfloor->fofflags & FOF_EXTRA)
+				{
+					if (rlight->flags & FOF_EXTRA)
 					{
-						windowbottom = sprbotscreen;
-						// draw the texture
-						colfunc_2s (col);
-						for (i++; i < dc_numlights; i++)
-						{
-							rlight = &dc_lightlist[i];
-							rlight->height += rlight->heightstep;
-							if (rlight->flags & FOF_CUTLEVEL)
-								rlight->botheight += rlight->botheightstep;
-						}
-						continue;
+						// The light is from an extra 3D floor... Check the flags so
+						// there are no undesired cuts.
+						if ((rlight->flags & (FOF_FOG|FOF_SWIMMABLE)) == (pfloor->fofflags & (FOF_FOG|FOF_SWIMMABLE)))
+							solid = 1;
 					}
-					// draw the texture
-					colfunc_2s (col);
-					if (solid)
-						windowtop = bheight;
 					else
-						windowtop = windowbottom + 1;
+						solid = 1;
+				}
+				else
+					solid = 0;
+
+				height = rlight->height;
+				rlight->height += rlight->heightstep;
+
+				if (solid)
+				{
+					bheight = rlight->botheight - (FRACUNIT >> 1);
+					rlight->botheight += rlight->botheightstep;
+				}
+
+				if (height <= windowtop)
+				{
 					if (lighteffect)
 						dc_colormap = rlight->rcolormap;
+					if (solid && windowtop < bheight)
+						windowtop = bheight;
+					continue;
 				}
-				windowbottom = sprbotscreen;
-				// draw the texture, if there is any space left
-				if (windowtop < windowbottom)
-					colfunc_2s (col);
 
-				spryscale += rw_scalestep;
-				continue;
+				windowbottom = height;
+				if (windowbottom >= sprbotscreen)
+				{
+					windowbottom = sprbotscreen;
+					// draw the texture
+					colfunc_2s (col);
+					for (i++; i < dc_numlights; i++)
+					{
+						rlight = &dc_lightlist[i];
+						rlight->height += rlight->heightstep;
+						if (rlight->flags & FOF_CUTLEVEL)
+							rlight->botheight += rlight->botheightstep;
+					}
+					continue;
+				}
+				// draw the texture
+				colfunc_2s (col);
+				if (solid)
+					windowtop = bheight;
+				else
+					windowtop = windowbottom + 1;
+				if (lighteffect)
+					dc_colormap = rlight->rcolormap;
 			}
+			windowbottom = sprbotscreen;
+			// draw the texture, if there is any space left
+			if (windowtop < windowbottom)
+				colfunc_2s (col);
 
-			// calculate lighting
-			pindex = FixedMul(spryscale, LIGHTRESOLUTIONFIX)>>LIGHTSCALESHIFT;
+			spryscale += rw_scalestep;
+			continue;
+		}
 
-			if (pindex >= MAXLIGHTSCALE)
-				pindex = MAXLIGHTSCALE - 1;
+		// calculate lighting
+		pindex = FixedMul(spryscale, LIGHTRESOLUTIONFIX)>>LIGHTSCALESHIFT;
 
-			dc_colormap = walllights[pindex];
+		if (pindex >= MAXLIGHTSCALE)
+			pindex = MAXLIGHTSCALE - 1;
 
-			if (pfloor->fofflags & FOF_FOG && pfloor->master->frontsector->extra_colormap)
-				dc_colormap = pfloor->master->frontsector->extra_colormap->colormap + (dc_colormap - colormaps);
-			else if (frontsector->extra_colormap)
-				dc_colormap = frontsector->extra_colormap->colormap + (dc_colormap - colormaps);
+		dc_colormap = walllights[pindex];
 
-			// draw the texture
-			colfunc_2s (col);
-			spryscale += rw_scalestep;
-		}
+		if (pfloor->fofflags & FOF_FOG && pfloor->master->frontsector->extra_colormap)
+			dc_colormap = pfloor->master->frontsector->extra_colormap->colormap + (dc_colormap - colormaps);
+		else if (frontsector->extra_colormap)
+			dc_colormap = frontsector->extra_colormap->colormap + (dc_colormap - colormaps);
+
+		// draw the texture
+		colfunc_2s (col);
+		spryscale += rw_scalestep;
 	}
 	colfunc = colfuncs[BASEDRAWFUNC];
 
@@ -1269,7 +1227,7 @@ static void R_RenderSegLoop (void)
 		}
 		oldtexturecolumn = texturecolumn;
 
-		texturecolumn >>= FRACBITS;
+		INT32 itexturecolumn = texturecolumn >> FRACBITS;
 
 		// texturecolumn and lighting are independent of wall tiers
 		if (segtextured)
@@ -1335,7 +1293,7 @@ static void R_RenderSegLoop (void)
 				dc_yl = yl;
 				dc_yh = yh;
 				dc_texturemid = rw_midtexturemid;
-				dc_source = R_GetColumn(midtexture,texturecolumn);
+				dc_source = R_GetColumn(midtexture, itexturecolumn + (rw_offset_mid>>FRACBITS));
 				dc_texheight = textureheight[midtexture]>>FRACBITS;
 
 				//profile stuff ---------------------------------------------------------
@@ -1396,7 +1354,7 @@ static void R_RenderSegLoop (void)
 						dc_yl = yl;
 						dc_yh = mid;
 						dc_texturemid = rw_toptexturemid;
-						dc_source = R_GetColumn(toptexture,texturecolumn);
+						dc_source = R_GetColumn(toptexture, itexturecolumn + (rw_offset_top>>FRACBITS));
 						dc_texheight = textureheight[toptexture]>>FRACBITS;
 						colfunc();
 						ceilingclip[rw_x] = (INT16)mid;
@@ -1432,8 +1390,7 @@ static void R_RenderSegLoop (void)
 						dc_yl = mid;
 						dc_yh = yh;
 						dc_texturemid = rw_bottomtexturemid;
-						dc_source = R_GetColumn(bottomtexture,
-							texturecolumn);
+						dc_source = R_GetColumn(bottomtexture, itexturecolumn + (rw_offset_bot>>FRACBITS));
 						dc_texheight = textureheight[bottomtexture]>>FRACBITS;
 						colfunc();
 						floorclip[rw_x] = (INT16)mid;
@@ -1452,7 +1409,7 @@ static void R_RenderSegLoop (void)
 		{
 			// save texturecol
 			//  for backdrawing of masked mid texture
-			maskedtexturecol[rw_x] = (INT16)texturecolumn;
+			maskedtexturecol[rw_x] = texturecolumn + rw_offset_mid;
 
 			if (maskedtextureheight != NULL) {
 				maskedtextureheight[rw_x] = (curline->linedef->flags & ML_MIDPEG) ?
@@ -1511,6 +1468,73 @@ static INT64 R_CalcSegDist(seg_t* seg, INT64 x2, INT64 y2)
 	}
 }
 
+//SoM: Code to remove limits on openings.
+static void R_AllocClippingTables(size_t range)
+{
+	size_t pos = lastopening - openings;
+	size_t need = range * 2; // for both sprtopclip and sprbottomclip
+
+	if (pos + need < numopenings)
+		return;
+
+	INT16 *oldopenings = openings;
+	INT16 *oldlast = lastopening;
+
+	if (numopenings == 0)
+		numopenings = 16384;
+
+	numopenings += need;
+	openings = Z_Realloc(openings, numopenings * sizeof (*openings), PU_STATIC, NULL);
+	lastopening = openings + pos;
+
+	if (oldopenings == NULL)
+		return;
+
+	// borrowed fix from *cough* zdoom *cough*
+	// [RH] We also need to adjust the openings pointers that
+	//    were already stored in drawsegs.
+	for (drawseg_t *ds = drawsegs; ds < ds_p; ds++)
+	{
+		// Check if it's in range of the openings
+		if (ds->sprtopclip + ds->x1 >= oldopenings && ds->sprtopclip + ds->x1 <= oldlast)
+			ds->sprtopclip = (ds->sprtopclip - oldopenings) + openings;
+		if (ds->sprbottomclip + ds->x1 >= oldopenings && ds->sprbottomclip + ds->x1 <= oldlast)
+			ds->sprbottomclip = (ds->sprbottomclip - oldopenings) + openings;
+	}
+}
+
+static void R_AllocTextureColumnTables(size_t range)
+{
+	size_t pos = curtexturecolumntable - texturecolumntable;
+
+	// 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)
+		return;
+
+	fixed_t *oldtable = texturecolumntable;
+	fixed_t *oldlast = curtexturecolumntable;
+
+	if (texturecolumntablesize == 0)
+		texturecolumntablesize = 16384;
+
+	texturecolumntablesize += range;
+	texturecolumntable = Z_Realloc(texturecolumntable, texturecolumntablesize * sizeof (*texturecolumntable), PU_STATIC, NULL);
+	curtexturecolumntable = texturecolumntable + pos;
+
+	if (oldtable == NULL)
+		return;
+
+	for (drawseg_t *ds = drawsegs; ds < ds_p; ds++)
+	{
+		// Check if it's in range of the tables
+		if (ds->maskedtexturecol + ds->x1 >= oldtable && ds->maskedtexturecol + ds->x1 <= oldlast)
+			ds->maskedtexturecol = (ds->maskedtexturecol - oldtable) + texturecolumntable;
+		if (ds->thicksidecol + ds->x1 >= oldtable && ds->thicksidecol + ds->x1 <= oldlast)
+			ds->thicksidecol = (ds->thicksidecol - oldtable) + texturecolumntable;
+	}
+}
+
 //
 // R_StoreWallRange
 // A wall segment will be drawn
@@ -1579,37 +1603,6 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 	ds_p->curline = curline;
 	rw_stopx = stop+1;
 
-	//SoM: Code to remove limits on openings.
-	{
-		size_t pos = lastopening - openings;
-		size_t need = (rw_stopx - start)*4 + pos;
-		if (need > maxopenings)
-		{
-			drawseg_t *ds;  //needed for fix from *cough* zdoom *cough*
-			INT16 *oldopenings = openings;
-			INT16 *oldlast = lastopening;
-
-			do
-				maxopenings = maxopenings ? maxopenings*2 : 16384;
-			while (need > maxopenings);
-			openings = Z_Realloc(openings, maxopenings * sizeof (*openings), PU_STATIC, NULL);
-			lastopening = openings + pos;
-
-			// borrowed fix from *cough* zdoom *cough*
-			// [RH] We also need to adjust the openings pointers that
-			//    were already stored in drawsegs.
-			for (ds = drawsegs; ds < ds_p; ds++)
-			{
-#define ADJUST(p) if (ds->p + ds->x1 >= oldopenings && ds->p + ds->x1 <= oldlast) ds->p = ds->p - oldopenings + openings;
-				ADJUST(maskedtexturecol);
-				ADJUST(sprtopclip);
-				ADJUST(sprbottomclip);
-				ADJUST(thicksidecol);
-#undef ADJUST
-			}
-		}
-	}  // end of code to remove limits on openings
-
 	// calculate scale at both ends and step
 	ds_p->scale1 = rw_scale = R_ScaleFromGlobalAngle(viewangle + xtoviewangle[start]);
 
@@ -1783,7 +1776,7 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			rw_midtexturemid = worldtop;
 			rw_midtextureslide = ceilingfrontslide;
 		}
-		rw_midtexturemid += sidedef->rowoffset;
+		rw_midtexturemid += sidedef->rowoffset + sidedef->offsety_mid;
 
 		ds_p->silhouette = SIL_BOTH;
 		ds_p->sprtopclip = screenheightarray;
@@ -1906,9 +1899,9 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			|| backsector->floorpic != frontsector->floorpic
 			|| backsector->lightlevel != frontsector->lightlevel
 			//SoM: 3/22/2000: Check floor x and y offsets.
-			|| backsector->floor_xoffs != frontsector->floor_xoffs
-			|| backsector->floor_yoffs != frontsector->floor_yoffs
-			|| backsector->floorpic_angle != frontsector->floorpic_angle
+			|| backsector->floorxoffset != frontsector->floorxoffset
+			|| backsector->flooryoffset != frontsector->flooryoffset
+			|| backsector->floorangle != frontsector->floorangle
 			//SoM: 3/22/2000: Prevents bleeding.
 			|| (frontsector->heightsec != -1 && frontsector->floorpic != skyflatnum)
 			|| backsector->floorlightlevel != frontsector->floorlightlevel
@@ -1939,9 +1932,9 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			|| backsector->ceilingpic != frontsector->ceilingpic
 			|| backsector->lightlevel != frontsector->lightlevel
 			//SoM: 3/22/2000: Check floor x and y offsets.
-			|| backsector->ceiling_xoffs != frontsector->ceiling_xoffs
-			|| backsector->ceiling_yoffs != frontsector->ceiling_yoffs
-			|| backsector->ceilingpic_angle != frontsector->ceilingpic_angle
+			|| backsector->ceilingxoffset != frontsector->ceilingxoffset
+			|| backsector->ceilingyoffset != frontsector->ceilingyoffset
+			|| backsector->ceilingangle != frontsector->ceilingangle
 			//SoM: 3/22/2000: Prevents bleeding.
 			|| (frontsector->heightsec != -1 && frontsector->ceilingpic != skyflatnum)
 			|| backsector->ceilinglightlevel != frontsector->ceilinglightlevel
@@ -2022,8 +2015,10 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			}
 		}
 
-		rw_toptexturemid += sidedef->rowoffset;
-		rw_bottomtexturemid += sidedef->rowoffset;
+		rw_toptexturemid += sidedef->rowoffset + sidedef->offsety_top;
+		rw_bottomtexturemid += sidedef->rowoffset + sidedef->offsety_bot;
+
+		R_AllocTextureColumnTables(rw_stopx - start);
 
 		// allocate space for masked texture tables
 		if (frontsector && backsector && !Tag_Compare(&frontsector->tags, &backsector->tags) && (backsector->ffloors || frontsector->ffloors))
@@ -2039,8 +2034,8 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			//markceiling = markfloor = true;
 			maskedtexture = true;
 
-			ds_p->thicksidecol = maskedtexturecol = lastopening - rw_x;
-			lastopening += rw_stopx - rw_x;
+			ds_p->thicksidecol = maskedtexturecol = curtexturecolumntable - rw_x;
+			curtexturecolumntable += rw_stopx - rw_x;
 
 			lowcut = max(worldbottom, worldlow) + viewz;
 			highcut = min(worldtop, worldhigh) + viewz;
@@ -2223,8 +2218,8 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 			// masked midtexture
 			if (!ds_p->thicksidecol)
 			{
-				ds_p->maskedtexturecol = maskedtexturecol = lastopening - rw_x;
-				lastopening += rw_stopx - rw_x;
+				ds_p->maskedtexturecol = maskedtexturecol = curtexturecolumntable - rw_x;
+				curtexturecolumntable += rw_stopx - rw_x;
 			}
 			else
 				ds_p->maskedtexturecol = ds_p->thicksidecol;
@@ -2266,8 +2261,8 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 					rw_midtexturebackslide = ceilingbackslide;
 				}
 			}
-			rw_midtexturemid += sidedef->rowoffset;
-			rw_midtextureback += sidedef->rowoffset;
+			rw_midtexturemid += sidedef->rowoffset + sidedef->offsety_mid;
+			rw_midtextureback += sidedef->rowoffset + sidedef->offsety_mid;
 
 			maskedtexture = true;
 		}
@@ -2305,6 +2300,9 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 		/// 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_centerangle = ANGLE_90 + viewangle - rw_normalangle;
 
 		// calculate light table
@@ -2733,29 +2731,34 @@ void R_StoreWallRange(INT32 start, INT32 stop)
 		ds_p->portalpass = 0;
 
 	// save sprite clipping info
-	if (((ds_p->silhouette & SIL_TOP) || maskedtexture) && !ds_p->sprtopclip)
+	if (maskedtexture || (ds_p->silhouette & (SIL_TOP | SIL_BOTTOM)))
 	{
-		M_Memcpy(lastopening, ceilingclip+start, 2*(rw_stopx - start));
-		ds_p->sprtopclip = lastopening - start;
-		lastopening += rw_stopx - start;
-	}
+		R_AllocClippingTables(rw_stopx - start);
 
-	if (((ds_p->silhouette & SIL_BOTTOM) || maskedtexture) && !ds_p->sprbottomclip)
-	{
-		M_Memcpy(lastopening, floorclip + start, 2*(rw_stopx-start));
-		ds_p->sprbottomclip = lastopening - start;
-		lastopening += rw_stopx - start;
+		if (((ds_p->silhouette & SIL_TOP) || maskedtexture) && !ds_p->sprtopclip)
+		{
+			M_Memcpy(lastopening, ceilingclip + start, 2*(rw_stopx - start));
+			ds_p->sprtopclip = lastopening - start;
+			lastopening += rw_stopx - start;
+		}
+
+		if (((ds_p->silhouette & SIL_BOTTOM) || maskedtexture) && !ds_p->sprbottomclip)
+		{
+			M_Memcpy(lastopening, floorclip + start, 2*(rw_stopx - start));
+			ds_p->sprbottomclip = lastopening - start;
+			lastopening += rw_stopx - start;
+		}
 	}
 
 	if (maskedtexture && !(ds_p->silhouette & SIL_TOP))
 	{
 		ds_p->silhouette |= SIL_TOP;
-		ds_p->tsilheight = (sidedef->midtexture > 0 && sidedef->midtexture < numtextures) ? INT32_MIN: INT32_MAX;
+		ds_p->tsilheight = (sidedef->midtexture > 0 && sidedef->midtexture < numtextures) ? INT32_MIN : INT32_MAX;
 	}
 	if (maskedtexture && !(ds_p->silhouette & SIL_BOTTOM))
 	{
 		ds_p->silhouette |= SIL_BOTTOM;
-		ds_p->bsilheight = (sidedef->midtexture > 0 && sidedef->midtexture < numtextures) ? INT32_MAX: INT32_MIN;
+		ds_p->bsilheight = (sidedef->midtexture > 0 && sidedef->midtexture < numtextures) ? INT32_MAX : INT32_MIN;
 	}
 	ds_p++;
 }
diff --git a/src/r_segs.h b/src/r_segs.h
index 09c68b27e95eb34deb1302762d9d8a50e72e44dc..cad0146748bb4ab77325285a6290fc195d4399bc 100644
--- a/src/r_segs.h
+++ b/src/r_segs.h
@@ -22,5 +22,6 @@ transnum_t R_GetLinedefTransTable(fixed_t alpha);
 void R_RenderMaskedSegRange(drawseg_t *ds, INT32 x1, INT32 x2);
 void R_RenderThickSideRange(drawseg_t *ds, INT32 x1, INT32 x2, ffloor_t *pffloor);
 void R_StoreWallRange(INT32 start, INT32 stop);
+void R_ClearSegTables(void);
 
 #endif
diff --git a/src/r_skins.c b/src/r_skins.c
index e59e085b8c1c0681a175ec537daf16d42d8a87fb..308cee8d6050496b361385c0595334b647927ee1 100644
--- a/src/r_skins.c
+++ b/src/r_skins.c
@@ -113,6 +113,7 @@ static void Sk_SetDefaultValue(skin_t *skin)
 
 	strcpy(skin->realname, "Someone");
 	strcpy(skin->hudname, "???");
+	strcpy(skin->supername, "Someone super");
 
 	skin->starttranscolor = 96;
 	skin->prefcolor = SKINCOLOR_GREEN;
@@ -190,11 +191,12 @@ UINT32 R_GetSkinAvailabilities(void)
 			// This crash is impossible to trigger as is,
 			// but it could happen if MAXUNLOCKABLES is ever made higher than 32,
 			// and someone makes a mod that has 33+ unlockable characters. :V
-			I_Error("Too many unlockable characters\n");
+			// 2022/03/15: MAXUNLOCKABLES is now higher than 32
+			I_Error("Too many unlockable characters! (maximum is 32)\n");
 			return 0;
 		}
 
-		if (unlockables[i].unlocked)
+		if (clientGamedata->unlocked[i])
 		{
 			response |= (1 << unlockShift);
 		}
@@ -242,11 +244,12 @@ boolean R_SkinUsable(INT32 playernum, INT32 skinnum)
 		// Force 3.
 		return true;
 	}
+
 	if (playernum != -1 && players[playernum].bot)
-    {
-        //Force 4.
-        return true;
-    }
+	{
+		// Force 4.
+		return true;
+	}
 
 	// We will now check if this skin is supposed to be locked or not.
 
@@ -284,7 +287,7 @@ boolean R_SkinUsable(INT32 playernum, INT32 skinnum)
 	else
 	{
 		// We want to check our global unlockables.
-		return (unlockables[unlockID].unlocked);
+		return (clientGamedata->unlocked[unlockID]);
 	}
 }
 
@@ -303,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)
 {
@@ -344,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)
 		{
@@ -533,7 +547,7 @@ static void R_LoadSkinSprites(UINT16 wadnum, UINT16 *lump, UINT16 *lastlump, ski
 		R_AddSingleSpriteDef(spr2names[sprite2], &skin->sprites[sprite2], wadnum, *lump, *lastlump);
 
 	if (skin->sprites[0].numframes == 0)
-		I_Error("R_LoadSkinSprites: no frames found for sprite SPR2_%s\n", spr2names[0]);
+		CONS_Alert(CONS_ERROR, M_GetText("No frames found for sprite SPR2_%s\n"), spr2names[0]);
 }
 
 // returns whether found appropriate property
@@ -680,7 +694,7 @@ void R_AddSkins(UINT16 wadnum, boolean mainfile)
 	char *value;
 	size_t size;
 	skin_t *skin;
-	boolean hudname, realname;
+	boolean hudname, realname, supername;
 
 	//
 	// search for all skin markers in pwad
@@ -710,7 +724,7 @@ void R_AddSkins(UINT16 wadnum, boolean mainfile)
 		skin = &skins[numskins];
 		Sk_SetDefaultValue(skin);
 		skin->wadnum = wadnum;
-		hudname = realname = false;
+		hudname = realname = supername = false;
 		// parse
 		stoken = strtok (buf2, "\r\n= ");
 		while (stoken)
@@ -753,7 +767,7 @@ void R_AddSkins(UINT16 wadnum, boolean mainfile)
 					Z_Free(value2);
 				}
 
-				// copy to hudname and fullname as a default.
+				// copy to hudname, realname, and supername as a default.
 				if (!realname)
 				{
 					STRBUFCPY(skin->realname, skin->name);
@@ -769,6 +783,19 @@ void R_AddSkins(UINT16 wadnum, boolean mainfile)
 					strupr(skin->hudname);
 					SYMBOLCONVERT(skin->hudname)
 				}
+				if (!supername)
+				{
+					char superstring[SKINNAMESIZE+7];
+					strcpy(superstring, "Super ");
+					strlcat(superstring, skin->name, sizeof(superstring));
+					STRBUFCPY(skin->supername, superstring);
+				}
+			}
+			else if (!stricmp(stoken, "supername"))
+			{ // Super name (eg. "Super Knuckles")
+				supername = true;
+				STRBUFCPY(skin->supername, value);
+				SYMBOLCONVERT(skin->supername)
 			}
 			else if (!stricmp(stoken, "realname"))
 			{ // Display name (eg. "Knuckles")
@@ -777,6 +804,13 @@ void R_AddSkins(UINT16 wadnum, boolean mainfile)
 				SYMBOLCONVERT(skin->realname)
 				if (!hudname)
 					HUDNAMEWRITE(skin->realname);
+				if (!supername) //copy over default to capitalise the name
+				{
+					char superstring[SKINNAMESIZE+7];
+					strcpy(superstring, "Super ");
+					strlcat(superstring, skin->realname, sizeof(superstring));
+					STRBUFCPY(skin->supername, superstring);
+				}
 			}
 			else if (!stricmp(stoken, "hudname"))
 			{ // Life icon name (eg. "K.T.E")
@@ -829,7 +863,7 @@ void R_PatchSkins(UINT16 wadnum, boolean mainfile)
 	char *value;
 	size_t size;
 	skin_t *skin;
-	boolean noskincomplain, realname, hudname;
+	boolean noskincomplain, realname, hudname, supername;
 
 	//
 	// search for all skin patch markers in pwad
@@ -853,7 +887,7 @@ void R_PatchSkins(UINT16 wadnum, boolean mainfile)
 		buf2[size] = '\0';
 
 		skin = NULL;
-		noskincomplain = realname = hudname = false;
+		noskincomplain = realname = hudname = supername = false;
 
 		/*
 		Parse. Has more phases than the parser in R_AddSkins because it needs to have the patching name first (no default skin name is acceptible for patching, unlike skin creation)
@@ -892,13 +926,26 @@ void R_PatchSkins(UINT16 wadnum, boolean mainfile)
 			else // Get the properties!
 			{
 				// Some of these can't go in R_ProcessPatchableFields because they have side effects for future lines.
-				if (!stricmp(stoken, "realname"))
+				if (!stricmp(stoken, "supername"))
+				{ // Super name (eg. "Super Knuckles")
+					supername = true;
+					STRBUFCPY(skin->supername, value);
+					SYMBOLCONVERT(skin->supername)
+				}
+				else if (!stricmp(stoken, "realname"))
 				{ // Display name (eg. "Knuckles")
 					realname = true;
 					STRBUFCPY(skin->realname, value);
 					SYMBOLCONVERT(skin->realname)
 					if (!hudname)
 						HUDNAMEWRITE(skin->realname);
+					if (!supername) //copy over default to capitalise the name
+					{
+						char superstring[SKINNAMESIZE+7];
+						strcpy(superstring, "Super ");
+						strlcat(superstring, skin->realname, sizeof(superstring));
+						STRBUFCPY(skin->supername, superstring);
+					}
 				}
 				else if (!stricmp(stoken, "hudname"))
 				{ // Life icon name (eg. "K.T.E")
diff --git a/src/r_skins.h b/src/r_skins.h
index 082b690dda8294ed2f250a5aaba118a765073a21..bf2275a49a795a1a984343dc82d881b769a89592 100644
--- a/src/r_skins.h
+++ b/src/r_skins.h
@@ -35,8 +35,9 @@ typedef struct
 	UINT16 wadnum;
 	skinflags_t flags;
 
-	char realname[SKINNAMESIZE+1]; // Display name for level completion.
+	char realname[SKINNAMESIZE+1]; // Display name for level completion
 	char hudname[SKINNAMESIZE+1]; // HUD name to display (officially exactly 5 characters long)
+	char supername[SKINNAMESIZE+7]; // Super name to display when collecting all emeralds
 
 	UINT8 ability; // ability definition
 	UINT8 ability2; // secondary ability definition
@@ -95,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_splats.c b/src/r_splats.c
index 72bfe74b3a7be6ac50313793f4ea28e7f6bc8c5e..0b482d7988cc0e3a32f719a0f84a272fdf63d2f3 100644
--- a/src/r_splats.c
+++ b/src/r_splats.c
@@ -31,20 +31,8 @@ static void prepare_rastertab(void);
 
 static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, vissprite_t *vis);
 
-#ifdef USEASM
-void ASMCALL rasterize_segment_tex_asm(INT32 x1, INT32 y1, INT32 x2, INT32 y2, INT32 tv1, INT32 tv2, INT32 tc, INT32 dir);
-#endif
-
 static void rasterize_segment_tex(INT32 x1, INT32 y1, INT32 x2, INT32 y2, INT32 tv1, INT32 tv2, INT32 tc, INT32 dir)
 {
-#ifdef USEASM
-	if (R_ASM)
-	{
-		rasterize_segment_tex_asm(x1, y1, x2, y2, tv1, tv2, tc, dir);
-		return;
-	}
-	else
-#endif
 	{
 		fixed_t xs, xe, count;
 		fixed_t dx0, dx1;
@@ -191,7 +179,7 @@ void R_DrawFloorSplat(vissprite_t *spr)
 		splatangle = spr->viewpoint.angle;
 
 	if (!(spr->cut & SC_ISROTATED))
-		splatangle += mobj->rollangle;
+		splatangle += mobj->spriteroll;
 
 	splat.angle = -splatangle;
 	splat.angle += ANGLE_90;
@@ -326,9 +314,7 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 	fixed_t planeheight = 0;
 	fixed_t step;
 
-	int spanfunctype = SPANDRAWFUNC_SPRITE;
-
-	prepare_rastertab();
+	int spanfunctype;
 
 #define RASTERPARAMS(vnum1, vnum2, tv1, tv2, tc, dir) \
     x1 = verts[vnum1].x; \
@@ -379,21 +365,15 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
     if (ry1 > maxy) \
         maxy = ry1;
 
-	// do segment a -> top of texture
-	RASTERPARAMS(3,2,0,pSplat->width-1,0,0);
-	// do segment b -> right side of texture
-	RASTERPARAMS(2,1,0,pSplat->width-1,pSplat->height-1,0);
-	// do segment c -> bottom of texture
-	RASTERPARAMS(1,0,pSplat->width-1,0,pSplat->height-1,0);
-	// do segment d -> left side of texture
-	RASTERPARAMS(0,3,pSplat->width-1,0,0,1);
-
 	ds_source = (UINT8 *)pSplat->pic;
 	ds_flatwidth = pSplat->width;
 	ds_flatheight = pSplat->height;
-	ds_powersoftwo = false;
 
-	if (R_CheckPowersOfTwo())
+	ds_powersoftwo = ds_solidcolor = false;
+
+	if (R_CheckSolidColorFlat())
+		ds_solidcolor = true;
+	else if (R_CheckPowersOfTwo())
 	{
 		R_SetFlatVars(ds_flatwidth * ds_flatheight);
 		ds_powersoftwo = true;
@@ -404,9 +384,8 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 		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();
-		spanfunctype = SPANDRAWFUNC_TILTEDSPRITE;
 	}
-	else
+	else if (!ds_solidcolor)
 	{
 		planeheight = abs(pSplat->z - vis->viewpoint.z);
 
@@ -441,23 +420,70 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 			ds_colormap = &vis->extra_colormap->colormap[ds_colormap - colormaps];
 	}
 
-	if (vis->transmap)
+	ds_transmap = vis->transmap;
+
+	// Determine which R_DrawWhatever to use
+
+	// Solid color
+	if (ds_solidcolor)
 	{
-		ds_transmap = vis->transmap;
+		UINT16 px = *(UINT16 *)ds_source;
+
+		// Uh, it's not visible.
+		if (!(px & 0xFF00))
+			return;
+
+		// Pixel color is contained in the lower 8 bits (upper 8 are the opacity), so advance the pointer
+		ds_source++;
 
+		if (pSplat->slope)
+		{
+			if (ds_transmap)
+				spanfunctype = SPANDRAWFUNC_TILTEDTRANSSOLID;
+			else
+				spanfunctype = SPANDRAWFUNC_TILTEDSOLID;
+		}
+		else
+		{
+			if (ds_transmap)
+				spanfunctype = SPANDRAWFUNC_TRANSSOLID;
+			else
+				spanfunctype = SPANDRAWFUNC_SOLID;
+		}
+	}
+	// Transparent
+	else if (ds_transmap)
+	{
 		if (pSplat->slope)
 			spanfunctype = SPANDRAWFUNC_TILTEDTRANSSPRITE;
 		else
 			spanfunctype = SPANDRAWFUNC_TRANSSPRITE;
 	}
+	// Opaque
 	else
-		ds_transmap = NULL;
+	{
+		if (pSplat->slope)
+			spanfunctype = SPANDRAWFUNC_TILTEDSPRITE;
+		else
+			spanfunctype = SPANDRAWFUNC_SPRITE;
+	}
 
-	if (ds_powersoftwo)
+	if (ds_powersoftwo || ds_solidcolor)
 		spanfunc = spanfuncs[spanfunctype];
 	else
 		spanfunc = spanfuncs_npo2[spanfunctype];
 
+	prepare_rastertab();
+
+	// do segment a -> top of texture
+	RASTERPARAMS(3,2,0,pSplat->width-1,0,0);
+	// do segment b -> right side of texture
+	RASTERPARAMS(2,1,0,pSplat->width-1,pSplat->height-1,0);
+	// do segment c -> bottom of texture
+	RASTERPARAMS(1,0,pSplat->width-1,0,pSplat->height-1,0);
+	// do segment d -> left side of texture
+	RASTERPARAMS(0,3,pSplat->width-1,0,0,1);
+
 	if (maxy >= vid.height)
 		maxy = vid.height-1;
 
@@ -512,7 +538,7 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 		if (x2 < x1)
 			continue;
 
-		if (!pSplat->slope)
+		if (!ds_solidcolor && !pSplat->slope)
 		{
 			fixed_t xstep, ystep;
 			fixed_t distance, span;
@@ -561,7 +587,7 @@ static void R_RasterizeFloorSplat(floorsplat_t *pSplat, vector2_t *verts, visspr
 		rastertab[y].maxx = INT32_MIN;
 	}
 
-	if (pSplat->angle && !pSplat->slope)
+	if (!ds_solidcolor && pSplat->angle && !pSplat->slope)
 		memset(cachedheight, 0, sizeof(cachedheight));
 }
 
diff --git a/src/r_textures.c b/src/r_textures.c
index 69e64074d8acefa5e73e52e6311cc3a776d191da..8b47f455e8bc7e2e3a9cb766fb38b357b96d4091 100644
--- a/src/r_textures.c
+++ b/src/r_textures.c
@@ -1662,6 +1662,35 @@ INT32 R_CheckTextureNumForName(const char *name)
 	return -1;
 }
 
+//
+// R_CheckTextureNameForNum
+//
+// because sidedefs use numbers and sometimes you want names
+// returns no texture marker if no texture was found
+//
+const char *R_CheckTextureNameForNum(INT32 num)
+{
+	if (num > 0 && num < numtextures)
+		return textures[num]->name;
+	
+	return "-";
+}
+
+//
+// R_TextureNameForNum
+//
+// calls R_CheckTextureNameForNum and returns REDWALL if result is a no texture marker
+//
+const char *R_TextureNameForNum(INT32 num)
+{
+	const char *result = R_CheckTextureNameForNum(num);
+
+	if (strcmp(result, "-") == 0)
+		return "REDWALL";
+
+	return result;
+}
+
 //
 // R_TextureNumForName
 //
diff --git a/src/r_textures.h b/src/r_textures.h
index 4a3c10b9ec999fc133a5f5ffe220f03f86ce2ff6..394b4f8243a7fa7a0de120f2ff893bc1dc9f0702 100644
--- a/src/r_textures.h
+++ b/src/r_textures.h
@@ -104,6 +104,10 @@ INT32 R_TextureNumForName(const char *name);
 INT32 R_CheckTextureNumForName(const char *name);
 lumpnum_t R_GetFlatNumForName(const char *name);
 
+// Returns the texture name for the texture number (in case you ever needed it)
+const char *R_CheckTextureNameForNum(INT32 num);
+const char *R_TextureNameForNum(INT32 num);
+
 extern INT32 numtextures;
 
 #endif
diff --git a/src/r_things.c b/src/r_things.c
index 916a7ee4ba85f8cf35fe07237cdba3c526045da4..89c4f35ebdf7f5f19cac40be0f65fa652b800bbd 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"
@@ -80,6 +80,33 @@ static spriteframe_t sprtemp[64];
 static size_t maxframe;
 static const char *spritename;
 
+//
+// Clipping against drawsegs optimization, from prboom-plus
+//
+// TODO: This should be done with proper subsector pass through
+// sprites which would ideally remove the need to do it at all.
+// Unfortunately, SRB2's drawing loop has lots of annoying
+// changes from Doom for portals, which make it hard to implement.
+
+typedef struct drawseg_xrange_item_s
+{
+	INT16 x1, x2;
+	drawseg_t *user;
+} drawseg_xrange_item_t;
+
+typedef struct drawsegs_xrange_s
+{
+	drawseg_xrange_item_t *items;
+	INT32 count;
+} drawsegs_xrange_t;
+
+#define DS_RANGES_COUNT 3
+static drawsegs_xrange_t drawsegs_xranges[DS_RANGES_COUNT];
+
+static drawseg_xrange_item_t *drawsegs_xrange;
+static size_t drawsegs_xrange_size = 0;
+static INT32 drawsegs_xrange_count = 0;
+
 // ==========================================================================
 //
 // Sprite loading routines: support sprites in pwad, dehacked sprite renaming,
@@ -497,7 +524,8 @@ void R_AddSpriteDefs(UINT16 wadnum)
 //
 // GAME FUNCTIONS
 //
-UINT32 visspritecount;
+UINT32 visspritecount, numvisiblesprites;
+
 static UINT32 clippedvissprites;
 static vissprite_t *visspritechunks[MAXVISSPRITES >> VISSPRITECHUNKBITS] = {NULL};
 
@@ -571,7 +599,7 @@ void R_InitSprites(void)
 //
 void R_ClearSprites(void)
 {
-	visspritecount = clippedvissprites = 0;
+	visspritecount = numvisiblesprites = clippedvissprites = 0;
 }
 
 //
@@ -817,6 +845,15 @@ static void R_DrawVisSprite(vissprite_t *vis)
 		if ((UINT64)overflow_test&0xFFFFFFFF80000000ULL) return; // ditto
 	}
 
+	// TODO This check should not be necessary. But Papersprites near to the camera will sometimes create invalid values
+	// for the vissprite's startfrac. This happens because they are not depth culled like other sprites.
+	// Someone who is more familiar with papersprites pls check and try to fix <3
+	if (vis->startfrac < 0 || vis->startfrac > (patch->width << FRACBITS))
+	{
+		// never draw vissprites with startfrac out of patch range
+		return;
+	}
+
 	colfunc = colfuncs[BASEDRAWFUNC]; // hack: this isn't resetting properly somewhere.
 	dc_colormap = vis->colormap;
 	dc_translation = R_GetSpriteTranslation(vis);
@@ -860,7 +897,7 @@ static void R_DrawVisSprite(vissprite_t *vis)
 	frac = vis->startfrac;
 	windowtop = windowbottom = sprbotscreen = INT32_MAX;
 
-	if (!(vis->cut & SC_PRECIP) && vis->mobj->skin && ((skin_t *)vis->mobj->skin)->flags & SF_HIRES)
+	if (vis->cut & SC_SHADOW && vis->mobj->skin && ((skin_t *)vis->mobj->skin)->flags & SF_HIRES)
 		this_scale = FixedMul(this_scale, ((skin_t *)vis->mobj->skin)->highresscale);
 	if (this_scale <= 0)
 		this_scale = 1;
@@ -870,10 +907,10 @@ static void R_DrawVisSprite(vissprite_t *vis)
 		{
 			vis->scale = FixedMul(vis->scale, this_scale);
 			vis->scalestep = FixedMul(vis->scalestep, this_scale);
-			vis->xiscale = FixedDiv(vis->xiscale,this_scale);
+			vis->xiscale = FixedDiv(vis->xiscale, this_scale);
 			vis->cut |= SC_ISSCALED;
 		}
-		dc_texturemid = FixedDiv(dc_texturemid,this_scale);
+		dc_texturemid = FixedDiv(dc_texturemid, this_scale);
 	}
 
 	spryscale = vis->scale;
@@ -1157,7 +1194,7 @@ fixed_t R_GetShadowZ(mobj_t *thing, pslope_t **shadowslope)
 		R_InterpolateMobjState(thing, FRACUNIT, &interp);
 	}
 
-	halfHeight = interp.z + (thing->height >> 1);
+	halfHeight = interp.z + (interp.height >> 1);
 	floorz = P_GetFloorZ(thing, interp.subsector->sector, interp.x, interp.y, NULL);
 	ceilingz = P_GetCeilingZ(thing, interp.subsector->sector, interp.x, interp.y, NULL);
 
@@ -1221,8 +1258,8 @@ fixed_t R_GetShadowZ(mobj_t *thing, pslope_t **shadowslope)
 		}
 	}
 
-	if (isflipped ? (ceilingz < groundz - (!groundslope ? 0 : FixedMul(abs(groundslope->zdelta), thing->radius*3/2)))
-		: (floorz > groundz + (!groundslope ? 0 : FixedMul(abs(groundslope->zdelta), thing->radius*3/2))))
+	if (isflipped ? (ceilingz < groundz - (!groundslope ? 0 : FixedMul(abs(groundslope->zdelta), interp.radius*3/2)))
+		: (floorz > groundz + (!groundslope ? 0 : FixedMul(abs(groundslope->zdelta), interp.radius*3/2))))
 	{
 		groundz = isflipped ? ceilingz : floorz;
 		groundslope = NULL;
@@ -1265,9 +1302,9 @@ static void R_SkewShadowSprite(
 	//CONS_Printf("Shadow is sloped by %d %d\n", xslope, zslope);
 
 	if (viewz < groundz)
-		*shadowyscale += FixedMul(FixedMul(thing->radius*2 / spriteheight, scalemul), zslope);
+		*shadowyscale += FixedMul(FixedMul(interp.radius*2 / spriteheight, scalemul), zslope);
 	else
-		*shadowyscale -= FixedMul(FixedMul(thing->radius*2 / spriteheight, scalemul), zslope);
+		*shadowyscale -= FixedMul(FixedMul(interp.radius*2 / spriteheight, scalemul), zslope);
 
 	*shadowyscale = abs((*shadowyscale));
 	*shadowskew = xslope;
@@ -1318,7 +1355,7 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 			return;
 	}
 
-	floordiff = abs((isflipped ? thing->height : 0) + interp.z - groundz);
+	floordiff = abs((isflipped ? interp.height : 0) + interp.z - groundz);
 
 	trans = floordiff / (100*FRACUNIT) + 3;
 	if (trans >= 9) return;
@@ -1328,8 +1365,8 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 	patch = W_CachePatchName("DSHADOW", PU_SPRITE);
 	xscale = FixedDiv(projection, tz);
 	yscale = FixedDiv(projectiony, tz);
-	shadowxscale = FixedMul(thing->radius*2, scalemul);
-	shadowyscale = FixedMul(FixedMul(thing->radius*2, scalemul), FixedDiv(abs(groundz - viewz), tz));
+	shadowxscale = FixedMul(interp.radius*2, scalemul);
+	shadowyscale = FixedMul(FixedMul(interp.radius*2, scalemul), FixedDiv(abs(groundz - viewz), tz));
 	shadowyscale = min(shadowyscale, shadowxscale) / patch->height;
 	shadowxscale /= patch->width;
 	shadowskew = 0;
@@ -1424,6 +1461,105 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 	objectsdrawn++;
 }
 
+static void R_ProjectBoundingBox(mobj_t *thing, vissprite_t *vis)
+{
+	fixed_t gx, gy;
+	fixed_t tx, tz;
+
+	vissprite_t *box;
+
+	if (!R_ThingBoundingBoxVisible(thing))
+	{
+		return;
+	}
+
+	// uncapped/interpolation
+	boolean interpolate = cv_renderhitboxinterpolation.value;
+	interpmobjstate_t interp = {0};
+
+	// do interpolation
+	if (R_UsingFrameInterpolation() && !paused && interpolate)
+	{
+		R_InterpolateMobjState(thing, rendertimefrac, &interp);
+	}
+	else
+	{
+		R_InterpolateMobjState(thing, FRACUNIT, &interp);
+	}
+
+	// 1--3
+	// |  |
+	// 0--2
+
+	// start in the (0) corner
+	gx = interp.x - interp.radius - viewx;
+	gy = interp.y - interp.radius - viewy;
+
+	tz = FixedMul(gx, viewcos) + FixedMul(gy, viewsin);
+
+	// thing is behind view plane?
+	// if parent vis is visible, ignore this
+	if (!vis && (tz < FixedMul(MINZ, interp.scale)))
+	{
+		return;
+	}
+
+	tx = FixedMul(gx, viewsin) - FixedMul(gy, viewcos);
+
+	// too far off the side?
+	if (!vis && abs(tx) > FixedMul(tz, fovtan)<<2)
+	{
+		return;
+	}
+
+	box = R_NewVisSprite();
+	box->mobj = thing;
+	box->mobjflags = thing->flags;
+	box->thingheight = interp.height;
+	box->cut = SC_BBOX;
+
+	box->gx = tx;
+	box->gy = tz;
+
+	box->scale  = 2 * FixedMul(interp.radius, viewsin);
+	box->xscale = 2 * FixedMul(interp.radius, viewcos);
+
+	box->pz = interp.z;
+	box->pzt = box->pz + box->thingheight;
+
+	box->gzt = box->pzt;
+	box->gz = box->pz;
+	box->texturemid = box->gzt - viewz;
+
+	if (vis)
+	{
+		box->x1 = vis->x1;
+		box->x2 = vis->x2;
+		box->szt = vis->szt;
+		box->sz = vis->sz;
+
+		box->sortscale = vis->sortscale; // link sorting to sprite
+		box->dispoffset = vis->dispoffset + 5;
+
+		box->cut |= SC_LINKDRAW;
+	}
+	else
+	{
+		fixed_t xscale = FixedDiv(projection, tz);
+		fixed_t yscale = FixedDiv(projectiony, tz);
+		fixed_t top = (centeryfrac - FixedMul(box->texturemid, yscale));
+
+		box->x1 = (centerxfrac + FixedMul(box->gx, xscale)) / FRACUNIT;
+		box->x2 = box->x1;
+
+		box->szt = top / FRACUNIT;
+		box->sz = (top + FixedMul(box->thingheight, yscale)) / FRACUNIT;
+
+		box->sortscale = yscale;
+		box->dispoffset = 0;
+	}
+}
+
 //
 // R_ProjectSprite
 // Generates a vissprite for a thing
@@ -1435,6 +1571,7 @@ static void R_ProjectSprite(mobj_t *thing)
 	fixed_t tr_x, tr_y;
 	fixed_t tx, tz;
 	fixed_t xscale, yscale; //added : 02-02-98 : aaargll..if I were a math-guy!!!
+	fixed_t radius, height; // For drop shadows
 	fixed_t sortscale, sortsplat = 0;
 	fixed_t linkscale = 0;
 	fixed_t sort_x = 0, sort_y = 0, sort_z;
@@ -1493,6 +1630,7 @@ static void R_ProjectSprite(mobj_t *thing)
 #ifdef ROTSPRITE
 	patch_t *rotsprite = NULL;
 	INT32 rollangle = 0;
+	angle_t spriterotangle = 0;
 #endif
 
 	// uncapped/interpolation
@@ -1509,6 +1647,8 @@ static void R_ProjectSprite(mobj_t *thing)
 	}
 
 	this_scale = interp.scale;
+	radius = interp.radius; // For drop shadows
+	height = interp.height; // Ditto
 
 	// transform the origin point
 	tr_x = interp.x - viewx;
@@ -1629,9 +1769,6 @@ static void R_ProjectSprite(mobj_t *thing)
 
 	I_Assert(lump < max_spritelumps);
 
-	if (thing->skin && ((skin_t *)thing->skin)->flags & SF_HIRES)
-		this_scale = FixedMul(this_scale, ((skin_t *)thing->skin)->highresscale);
-
 	spr_width = spritecachedinfo[lump].width;
 	spr_height = spritecachedinfo[lump].height;
 	spr_offset = spritecachedinfo[lump].offset;
@@ -1642,17 +1779,19 @@ static void R_ProjectSprite(mobj_t *thing)
 	patch = W_CachePatchNum(sprframe->lumppat[rot], PU_SPRITE);
 
 #ifdef ROTSPRITE
-	if (thing->rollangle
+	spriterotangle = R_SpriteRotationAngle(&interp);
+
+	if (spriterotangle != 0
 	&& !(splat && !(thing->renderflags & RF_NOSPLATROLLANGLE)))
 	{
 		if (papersprite && ang >= ANGLE_180)
 		{
 			// Makes Software act much more sane like OpenGL
-			rollangle = R_GetRollAngle(InvAngle(thing->rollangle));
+			rollangle = R_GetRollAngle(InvAngle(spriterotangle));
 		}
 		else
 		{
-			rollangle = R_GetRollAngle(thing->rollangle);
+			rollangle = R_GetRollAngle(spriterotangle);
 		}
 
 		rotsprite = Patch_GetRotatedSprite(sprframe, (thing->frame & FF_FRAMEMASK), rot, flip, false, sprinfo, rollangle);
@@ -1679,6 +1818,14 @@ static void R_ProjectSprite(mobj_t *thing)
 	// calculate edges of the shape
 	spritexscale = interp.spritexscale;
 	spriteyscale = interp.spriteyscale;
+
+	if (thing->skin && ((skin_t *)thing->skin)->flags & SF_HIRES)
+	{
+		fixed_t highresscale = ((skin_t *)thing->skin)->highresscale;
+		spritexscale = FixedMul(spritexscale, highresscale);
+		spriteyscale = FixedMul(spriteyscale, highresscale);
+	}
+
 	if (spritexscale < 1 || spriteyscale < 1)
 		return;
 
@@ -1846,6 +1993,8 @@ static void R_ProjectSprite(mobj_t *thing)
 		{
 			R_InterpolateMobjState(thing, FRACUNIT, &tracer_interp);
 		}
+		radius = tracer_interp.radius; // For drop shadows
+		height = tracer_interp.height; // Ditto
 
 		tr_x = (tracer_interp.x + sort_x) - viewx;
 		tr_y = (tracer_interp.y + sort_y) - viewy;
@@ -1937,7 +2086,7 @@ static void R_ProjectSprite(mobj_t *thing)
 				if (abs(groundz-viewz)/tz > 4)
 					return; // Prevent stretchy shadows and possible crashes
 
-				floordiff = abs((isflipped ? caster->height : 0) + casterinterp.z - groundz);
+				floordiff = abs((isflipped ? casterinterp.height : 0) + casterinterp.z - groundz);
 				trans += ((floordiff / (100*FRACUNIT)) + 3);
 				shadowscale = FixedMul(FRACUNIT - floordiff/640, casterinterp.scale);
 			}
@@ -1952,8 +2101,8 @@ static void R_ProjectSprite(mobj_t *thing)
 
 		if (shadowdraw)
 		{
-			spritexscale = FixedMul(thing->radius * 2, FixedMul(shadowscale, spritexscale));
-			spriteyscale = FixedMul(thing->radius * 2, FixedMul(shadowscale, spriteyscale));
+			spritexscale = FixedMul(radius * 2, FixedMul(shadowscale, spritexscale));
+			spriteyscale = FixedMul(radius * 2, FixedMul(shadowscale, spriteyscale));
 			spriteyscale = FixedMul(spriteyscale, FixedDiv(abs(groundz - viewz), tz));
 			spriteyscale = min(spriteyscale, spritexscale) / patch->height;
 			spritexscale /= patch->width;
@@ -1968,7 +2117,7 @@ static void R_ProjectSprite(mobj_t *thing)
 		{
 			R_SkewShadowSprite(thing, thing->standingslope, groundz, patch->height, shadowscale, &spriteyscale, &sheartan);
 
-			gzt = (isflipped ? (interp.z + thing->height) : interp.z) + patch->height * spriteyscale / 2;
+			gzt = (isflipped ? (interp.z + height) : interp.z) + patch->height * spriteyscale / 2;
 			gz = gzt - patch->height * spriteyscale;
 
 			cut |= SC_SHEAR;
@@ -1983,7 +2132,7 @@ static void R_ProjectSprite(mobj_t *thing)
 			// When vertical flipped, draw sprites from the top down, at least as far as offsets are concerned.
 			// sprite height - sprite topoffset is the proper inverse of the vertical offset, of course.
 			// remember gz and gzt should be seperated by sprite height, not thing height - thing height can be shorter than the sprite itself sometimes!
-			gz = interp.z + oldthing->height - FixedMul(spr_topoffset, FixedMul(spriteyscale, this_scale));
+			gz = interp.z + interp.height - FixedMul(spr_topoffset, FixedMul(spriteyscale, this_scale));
 			gzt = gz + FixedMul(spr_height, FixedMul(spriteyscale, this_scale));
 		}
 		else
@@ -2062,7 +2211,7 @@ static void R_ProjectSprite(mobj_t *thing)
 	vis->gy = interp.y;
 	vis->gz = gz;
 	vis->gzt = gzt;
-	vis->thingheight = thing->height;
+	vis->thingheight = height;
 	vis->pz = interp.z;
 	vis->pzt = vis->pz + vis->thingheight;
 	vis->texturemid = FixedDiv(gzt - viewz, spriteyscale);
@@ -2185,6 +2334,8 @@ static void R_ProjectSprite(mobj_t *thing)
 	if (oldthing->shadowscale && cv_shadow.value)
 		R_ProjectDropShadow(oldthing, vis, oldthing->shadowscale, basetx, basetz);
 
+	R_ProjectBoundingBox(oldthing, vis);
+
 	// Debug
 	++objectsdrawn;
 }
@@ -2410,8 +2561,26 @@ void R_AddSprites(sector_t *sec, INT32 lightlevel)
 	hoop_limit_dist = (fixed_t)(cv_drawdist_nights.value) << FRACBITS;
 	for (thing = sec->thinglist; thing; thing = thing->snext)
 	{
-		if (R_ThingVisibleWithinDist(thing, limit_dist, hoop_limit_dist))
-			R_ProjectSprite(thing);
+		if (R_ThingWithinDist(thing, limit_dist, hoop_limit_dist))
+		{
+			const INT32 oldobjectsdrawn = objectsdrawn;
+
+			if (R_ThingVisible(thing))
+			{
+				R_ProjectSprite(thing);
+			}
+
+			// I'm so smart :^)
+			if (objectsdrawn == oldobjectsdrawn)
+			{
+				/*
+				Object is invisible OR is off screen but
+				render its bbox even if the latter because
+				radius could be bigger than sprite.
+				*/
+				R_ProjectBoundingBox(thing, NULL);
+			}
+		}
 	}
 
 	// no, no infinite draw distance for precipitation. this option at zero is supposed to turn it off
@@ -2482,6 +2651,14 @@ static void R_SortVisSprites(vissprite_t* vsprsortedhead, UINT32 start, UINT32 e
 	// bundle linkdraw
 	for (ds = unsorted.prev; ds != &unsorted; ds = ds->prev)
 	{
+		// Remove this sprite if it was determined to not be visible
+		if (ds->cut & SC_NOTVISIBLE)
+		{
+			ds->next->prev = ds->prev;
+			ds->prev->next = ds->next;
+			continue;
+		}
+
 		if (!(ds->cut & SC_LINKDRAW))
 			continue;
 
@@ -2499,26 +2676,36 @@ static void R_SortVisSprites(vissprite_t* vsprsortedhead, UINT32 start, UINT32 e
 			if (dsfirst->cut & SC_SHADOW)
 				continue;
 
+			// don't connect to your bounding box!
+			if (dsfirst->cut & SC_BBOX)
+				continue;
+
 			// don't connect if it's not the tracer
 			if (dsfirst->mobj != ds->mobj)
 				continue;
 
 			// don't connect if the tracer's top is cut off, but lower than the link's top
-			if ((dsfirst->cut & SC_TOP)
-			&& dsfirst->szt > ds->szt)
+			if ((dsfirst->cut & SC_TOP) && dsfirst->szt > ds->szt)
 				continue;
 
 			// don't connect if the tracer's bottom is cut off, but higher than the link's bottom
-			if ((dsfirst->cut & SC_BOTTOM)
-			&& dsfirst->sz < ds->sz)
+			if ((dsfirst->cut & SC_BOTTOM) && dsfirst->sz < ds->sz)
 				continue;
 
+			// If the object isn't visible, then the bounding box isn't either
+			if (ds->cut & SC_BBOX && dsfirst->cut & SC_NOTVISIBLE)
+				ds->cut |= SC_NOTVISIBLE;
+
 			break;
 		}
 
 		// remove from chain
 		ds->next->prev = ds->prev;
 		ds->prev->next = ds->next;
+
+		if (ds->cut & SC_NOTVISIBLE)
+			continue;
+
 		linkedvissprites++;
 
 		if (dsfirst != &unsorted)
@@ -2570,12 +2757,15 @@ static void R_SortVisSprites(vissprite_t* vsprsortedhead, UINT32 start, UINT32 e
 				best = ds;
 			}
 		}
-		best->next->prev = best->prev;
-		best->prev->next = best->next;
-		best->next = vsprsortedhead;
-		best->prev = vsprsortedhead->prev;
-		vsprsortedhead->prev->next = best;
-		vsprsortedhead->prev = best;
+		if (best)
+		{
+			best->next->prev = best->prev;
+			best->prev->next = best->next;
+			best->next = vsprsortedhead;
+			best->prev = vsprsortedhead->prev;
+			vsprsortedhead->prev->next = best;
+			vsprsortedhead->prev = best;
+		}
 	}
 }
 
@@ -2939,7 +3129,9 @@ static void R_DrawSprite(vissprite_t *spr)
 	mfloorclip = spr->clipbot;
 	mceilingclip = spr->cliptop;
 
-	if (spr->cut & SC_SPLAT)
+	if (spr->cut & SC_BBOX)
+		R_DrawThingBoundingBox(spr);
+	else if (spr->cut & SC_SPLAT)
 		R_DrawFloorSplat(spr);
 	else
 		R_DrawVisSprite(spr);
@@ -3004,9 +3196,47 @@ static void R_HeightSecClip(vissprite_t *spr, INT32 x1, INT32 x2)
 	}
 }
 
+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;
+
+	if (scalestep)
+	{
+		height = spr->patch->height;
+		yscale = spr->scale;
+		scalestep = FixedMul(scalestep, spr->spriteyscale);
+
+		if (spr->thingscale != FRACUNIT)
+			texturemid = FixedDiv(spr->texturemid, max(spr->thingscale, 1));
+		else
+			texturemid = spr->texturemid;
+	}
+
+	for (INT32 x = x1; x <= x2; x++)
+	{
+		if (scalestep)
+		{
+			fixed_t top = centeryfrac - FixedMul(texturemid, yscale);
+			fixed_t bottom = top + (height * yscale);
+			szt = (INT16)(top >> FRACBITS);
+			sz = (INT16)(bottom >> FRACBITS);
+			yscale += scalestep;
+		}
+
+		if (spr->cliptop[x] < spr->clipbot[x] && sz > spr->cliptop[x] && szt < spr->clipbot[x])
+			return true;
+	}
+
+	return false;
+}
+
 // R_ClipVisSprite
 // Clips vissprites without drawing, so that portals can work. -Red
-void R_ClipVisSprite(vissprite_t *spr, INT32 x1, INT32 x2, drawseg_t* dsstart, portal_t* portal)
+static void R_ClipVisSprite(vissprite_t *spr, INT32 x1, INT32 x2, portal_t* portal)
 {
 	drawseg_t *ds;
 	INT32		x;
@@ -3026,21 +3256,23 @@ void R_ClipVisSprite(vissprite_t *spr, INT32 x1, INT32 x2, drawseg_t* dsstart, p
 	// Pointer check was originally nonportable
 	// and buggy, by going past LEFT end of array:
 
-	//    for (ds = ds_p-1; ds >= drawsegs; ds--)    old buggy code
-	for (ds = ds_p; ds-- > dsstart;)
+	// e6y: optimization
+	if (drawsegs_xrange_size)
 	{
-		// determine if the drawseg obscures the sprite
-		if (ds->x1 > x2 ||
-			ds->x2 < x1 ||
-			(!ds->silhouette
-			 && !ds->maskedtexturecol))
-		{
-			// does not cover sprite
-			continue;
-		}
+		const drawseg_xrange_item_t *last = &drawsegs_xrange[drawsegs_xrange_count - 1];
+		drawseg_xrange_item_t *curr = &drawsegs_xrange[-1];
 
-		if (ds->portalpass != 66)
+		while (++curr <= last)
 		{
+			// determine if the drawseg obscures the sprite
+			if (curr->x1 > x2 || curr->x2 < x1)
+			{
+				// does not cover sprite
+				continue;
+			}
+
+			ds = curr->user;
+
 			if (ds->portalpass > 0 && ds->portalpass <= portalrender)
 				continue; // is a portal
 
@@ -3065,43 +3297,43 @@ void R_ClipVisSprite(vissprite_t *spr, INT32 x1, INT32 x2, drawseg_t* dsstart, p
 				// seg is behind sprite
 				continue;
 			}
-		}
 
-		r1 = ds->x1 < x1 ? x1 : ds->x1;
-		r2 = ds->x2 > x2 ? x2 : ds->x2;
+			r1 = ds->x1 < x1 ? x1 : ds->x1;
+			r2 = ds->x2 > x2 ? x2 : ds->x2;
 
-		// clip this piece of the sprite
-		silhouette = ds->silhouette;
+			// clip this piece of the sprite
+			silhouette = ds->silhouette;
 
-		if (spr->gz >= ds->bsilheight)
-			silhouette &= ~SIL_BOTTOM;
+			if (spr->gz >= ds->bsilheight)
+				silhouette &= ~SIL_BOTTOM;
 
-		if (spr->gzt <= ds->tsilheight)
-			silhouette &= ~SIL_TOP;
+			if (spr->gzt <= ds->tsilheight)
+				silhouette &= ~SIL_TOP;
 
-		if (silhouette == SIL_BOTTOM)
-		{
-			// bottom sil
-			for (x = r1; x <= r2; x++)
-				if (spr->clipbot[x] == -2)
-					spr->clipbot[x] = ds->sprbottomclip[x];
-		}
-		else if (silhouette == SIL_TOP)
-		{
-			// top sil
-			for (x = r1; x <= r2; x++)
-				if (spr->cliptop[x] == -2)
-					spr->cliptop[x] = ds->sprtopclip[x];
-		}
-		else if (silhouette == (SIL_TOP|SIL_BOTTOM))
-		{
-			// both
-			for (x = r1; x <= r2; x++)
+			if (silhouette == SIL_BOTTOM)
+			{
+				// bottom sil
+				for (x = r1; x <= r2; x++)
+					if (spr->clipbot[x] == -2)
+						spr->clipbot[x] = ds->sprbottomclip[x];
+			}
+			else if (silhouette == SIL_TOP)
 			{
-				if (spr->clipbot[x] == -2)
-					spr->clipbot[x] = ds->sprbottomclip[x];
-				if (spr->cliptop[x] == -2)
-					spr->cliptop[x] = ds->sprtopclip[x];
+				// top sil
+				for (x = r1; x <= r2; x++)
+					if (spr->cliptop[x] == -2)
+						spr->cliptop[x] = ds->sprtopclip[x];
+			}
+			else if (silhouette == (SIL_TOP|SIL_BOTTOM))
+			{
+				// both
+				for (x = r1; x <= r2; x++)
+				{
+					if (spr->clipbot[x] == -2)
+						spr->clipbot[x] = ds->sprbottomclip[x];
+					if (spr->cliptop[x] == -2)
+						spr->cliptop[x] = ds->sprtopclip[x];
+				}
 			}
 		}
 	}
@@ -3145,8 +3377,7 @@ void R_ClipVisSprite(vissprite_t *spr, INT32 x1, INT32 x2, drawseg_t* dsstart, p
 			spr->clipbot[x] = (INT16)viewheight;
 
 		if (spr->cliptop[x] == -2)
-			//Fab : 26-04-98: was -1, now clips against console bottom
-			spr->cliptop[x] = (INT16)con_clipviewtop;
+			spr->cliptop[x] = -1;
 	}
 
 	if (portal)
@@ -3171,16 +3402,119 @@ void R_ClipVisSprite(vissprite_t *spr, INT32 x1, INT32 x2, drawseg_t* dsstart, p
 			spr->cliptop[x] = -1;
 		}
 	}
+
+	// Check if it'll be visible
+	// Not done for floorsprites.
+	if (cv_spriteclip.value && (spr->cut & SC_SPLAT) == 0)
+	{
+		if (!R_CheckSpriteVisible(spr, x1, x2))
+			spr->cut |= SC_NOTVISIBLE;
+	}
 }
 
 void R_ClipSprites(drawseg_t* dsstart, portal_t* portal)
 {
+	const size_t maxdrawsegs = ds_p - drawsegs;
+	const INT32 cx = viewwidth / 2;
+	drawseg_t* ds;
+	INT32 i;
+
+	// e6y
+	// Reducing of cache misses in the following R_DrawSprite()
+	// Makes sense for scenes with huge amount of drawsegs.
+	// ~12% of speed improvement on epic.wad map05
+	for (i = 0; i < DS_RANGES_COUNT; i++)
+	{
+		drawsegs_xranges[i].count = 0;
+	}
+
+	if (visspritecount - clippedvissprites <= 0)
+	{
+		return;
+	}
+
+	if (drawsegs_xrange_size < maxdrawsegs)
+	{
+		drawsegs_xrange_size = 2 * maxdrawsegs;
+
+		for (i = 0; i < DS_RANGES_COUNT; i++)
+		{
+			drawsegs_xranges[i].items = Z_Realloc(
+				drawsegs_xranges[i].items,
+				drawsegs_xrange_size * sizeof(drawsegs_xranges[i].items[0]),
+				PU_STATIC, NULL
+			);
+		}
+	}
+
+	for (ds = ds_p; ds-- > dsstart;)
+	{
+		if (ds->silhouette || ds->maskedtexturecol)
+		{
+			drawsegs_xranges[0].items[drawsegs_xranges[0].count].x1 = ds->x1;
+			drawsegs_xranges[0].items[drawsegs_xranges[0].count].x2 = ds->x2;
+			drawsegs_xranges[0].items[drawsegs_xranges[0].count].user = ds;
+
+			// e6y: ~13% of speed improvement on sunder.wad map10
+			if (ds->x1 < cx)
+			{
+				drawsegs_xranges[1].items[drawsegs_xranges[1].count] =
+					drawsegs_xranges[0].items[drawsegs_xranges[0].count];
+				drawsegs_xranges[1].count++;
+			}
+
+			if (ds->x2 >= cx)
+			{
+				drawsegs_xranges[2].items[drawsegs_xranges[2].count] =
+					drawsegs_xranges[0].items[drawsegs_xranges[0].count];
+				drawsegs_xranges[2].count++;
+			}
+
+			drawsegs_xranges[0].count++;
+		}
+	}
+
 	for (; clippedvissprites < visspritecount; clippedvissprites++)
 	{
 		vissprite_t *spr = R_GetVisSprite(clippedvissprites);
+
+		if (cv_spriteclip.value
+		&& (spr->szt > vid.height || spr->sz < 0)
+		&& !((spr->cut & SC_SPLAT) || spr->scalestep))
+		{
+			spr->cut |= SC_NOTVISIBLE;
+			continue;
+		}
+
+		if (spr->cut & SC_BBOX)
+		{
+			numvisiblesprites++;
+			continue;
+		}
+
 		INT32 x1 = (spr->cut & SC_SPLAT) ? 0 : spr->x1;
 		INT32 x2 = (spr->cut & SC_SPLAT) ? viewwidth : spr->x2;
-		R_ClipVisSprite(spr, x1, x2, dsstart, portal);
+
+		if (x2 < cx)
+		{
+			drawsegs_xrange = drawsegs_xranges[1].items;
+			drawsegs_xrange_count = drawsegs_xranges[1].count;
+		}
+		else if (x1 >= cx)
+		{
+			drawsegs_xrange = drawsegs_xranges[2].items;
+			drawsegs_xrange_count = drawsegs_xranges[2].count;
+		}
+		else
+		{
+			drawsegs_xrange = drawsegs_xranges[0].items;
+			drawsegs_xrange_count = drawsegs_xranges[0].count;
+		}
+
+		R_ClipVisSprite(spr, x1, x2, portal);
+
+		if ((spr->cut & SC_NOTVISIBLE) == 0)
+			numvisiblesprites++;
 	}
 }
 
@@ -3188,22 +3522,22 @@ void R_ClipSprites(drawseg_t* dsstart, portal_t* portal)
 boolean R_ThingVisible (mobj_t *thing)
 {
 	return (!(
-				thing->sprite == SPR_NULL ||
-				( thing->flags2 & (MF2_DONTDRAW) ) ||
-				(r_viewmobj && (thing == r_viewmobj || (r_viewmobj->player && r_viewmobj->player->followmobj == thing)))
+		(thing->sprite == SPR_NULL) || // Don't draw null-sprites
+		(thing->flags2 & MF2_DONTDRAW) || // Don't draw MF2_LINKDRAW objects
+		(thing->drawonlyforplayer && thing->drawonlyforplayer != viewplayer) || // Don't draw other players' personal objects
+		(r_viewmobj && (
+		  (r_viewmobj == thing) || // Don't draw first-person players or awayviewmobj objects
+		  (r_viewmobj->player && r_viewmobj->player->followmobj == thing) || // Don't draw first-person players' followmobj
+		  (r_viewmobj == thing->dontdrawforviewmobj) // Don't draw objects that are hidden for the current view
+		))
 	));
 }
 
-boolean R_ThingVisibleWithinDist (mobj_t *thing,
+boolean R_ThingWithinDist (mobj_t *thing,
 		fixed_t      limit_dist,
 		fixed_t hoop_limit_dist)
 {
-	fixed_t approx_dist;
-
-	if (! R_ThingVisible(thing))
-		return false;
-
-	approx_dist = P_AproxDistance(viewx-thing->x, viewy-thing->y);
+	const fixed_t approx_dist = P_AproxDistance(viewx-thing->x, viewy-thing->y);
 
 	if (thing->sprite == SPR_HOOP)
 	{
diff --git a/src/r_things.h b/src/r_things.h
index 22a37a0eaccbd622bcb92ec6c4fa2836a5aa9456..318234886bbbfa2b603f1883d50bbfea4c67d370 100644
--- a/src/r_things.h
+++ b/src/r_things.h
@@ -66,9 +66,12 @@ void R_AddSprites(sector_t *sec, INT32 lightlevel);
 void R_InitSprites(void);
 void R_ClearSprites(void);
 
+UINT8 R_GetBoundingBoxColor(mobj_t *thing);
+boolean R_ThingBoundingBoxVisible(mobj_t *thing);
+
 boolean R_ThingVisible (mobj_t *thing);
 
-boolean R_ThingVisibleWithinDist (mobj_t *thing,
+boolean R_ThingWithinDist (mobj_t *thing,
 		fixed_t        draw_dist,
 		fixed_t nights_draw_dist);
 
@@ -120,20 +123,22 @@ typedef enum
 	SC_NONE       = 0,
 	SC_TOP        = 1,
 	SC_BOTTOM     = 1<<1,
+	SC_NOTVISIBLE = 1<<2,
 	// other flags
-	SC_PRECIP     = 1<<2,
-	SC_LINKDRAW   = 1<<3,
-	SC_FULLBRIGHT = 1<<4,
-	SC_SEMIBRIGHT = 1<<5,
-	SC_FULLDARK   = 1<<6,
-	SC_VFLIP      = 1<<7,
-	SC_ISSCALED   = 1<<8,
-	SC_ISROTATED  = 1<<9,
-	SC_SHADOW     = 1<<10,
-	SC_SHEAR      = 1<<11,
-	SC_SPLAT      = 1<<12,
+	SC_PRECIP     = 1<<3,
+	SC_LINKDRAW   = 1<<4,
+	SC_FULLBRIGHT = 1<<5,
+	SC_SEMIBRIGHT = 1<<6,
+	SC_FULLDARK   = 1<<7,
+	SC_VFLIP      = 1<<8,
+	SC_ISSCALED   = 1<<9,
+	SC_ISROTATED  = 1<<10,
+	SC_SHADOW     = 1<<11,
+	SC_SHEAR      = 1<<12,
+	SC_SPLAT      = 1<<13,
+	SC_BBOX       = 1<<14,
 	// masks
-	SC_CUTMASK    = SC_TOP|SC_BOTTOM,
+	SC_CUTMASK    = SC_TOP|SC_BOTTOM|SC_NOTVISIBLE,
 	SC_FLAGMASK   = ~SC_CUTMASK
 } spritecut_e;
 
@@ -215,12 +220,14 @@ typedef struct vissprite_s
 	INT32 dispoffset; // copy of mobj->dispoffset, affects ordering but not drawing
 } vissprite_t;
 
-extern UINT32 visspritecount;
+extern UINT32 visspritecount, numvisiblesprites;
 
 void R_ClipSprites(drawseg_t* dsstart, portal_t* portal);
-void R_ClipVisSprite(vissprite_t *spr, INT32 x1, INT32 x2, drawseg_t* dsstart, portal_t* portal);
 
 boolean R_SpriteIsFlashing(vissprite_t *vis);
+
+void R_DrawThingBoundingBox(vissprite_t *spr);
+
 UINT8 *R_GetSpriteTranslation(vissprite_t *vis);
 
 // ----------
diff --git a/src/s_sound.c b/src/s_sound.c
index 111b6ce256661d9f0adac594dd03f4d63573afd7..ada1a0fd2f399f09849a7330cf14ca283c7b1784 100644
--- a/src/s_sound.c
+++ b/src/s_sound.c
@@ -1692,6 +1692,7 @@ UINT8 soundtestpage = 1;
 //
 boolean S_PrepareSoundTest(void)
 {
+	gamedata_t *data = clientGamedata;
 	musicdef_t *def;
 	INT32 pos = numsoundtestdefs = 0;
 
@@ -1717,9 +1718,9 @@ boolean S_PrepareSoundTest(void)
 		if (!(def->soundtestpage & soundtestpage))
 			continue;
 		soundtestdefs[pos++] = def;
-		if (def->soundtestcond > 0 && !(mapvisited[def->soundtestcond-1] & MV_BEATEN))
+		if (def->soundtestcond > 0 && !(data->mapvisited[def->soundtestcond-1] & MV_BEATEN))
 			continue;
-		if (def->soundtestcond < 0 && !M_Achieved(-1-def->soundtestcond))
+		if (def->soundtestcond < 0 && !M_Achieved(-1-def->soundtestcond, data))
 			continue;
 		def->allowed = true;
 	}
diff --git a/src/screen.c b/src/screen.c
index f8af4c504a74d2d6b779bd6befdee2ab2277c544..3f249f8d314c481dbd911e70b7c1f24ce6ca8c56 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
@@ -44,10 +44,6 @@
 // SRB2Kart
 #include "r_fps.h" // R_GetFramerateCap
 
-#if defined (USEASM) && !defined (NORUSEASM)//&& (!defined (_MSC_VER) || (_MSC_VER <= 1200))
-#define RUSEASM //MSC.NET can't patch itself
-#endif
-
 // --------------------------------------------
 // assembly or c drawer routines for 8bpp/16bpp
 // --------------------------------------------
@@ -70,6 +66,8 @@ static CV_PossibleValue_t scr_depth_cons_t[] = {{8, "8 bits"}, {16, "16 bits"},
 //added : 03-02-98: default screen mode, as loaded/saved in config
 consvar_t cv_scr_width = CVAR_INIT ("scr_width", "1280", CV_SAVE, CV_Unsigned, NULL);
 consvar_t cv_scr_height = CVAR_INIT ("scr_height", "800", CV_SAVE, CV_Unsigned, NULL);
+consvar_t cv_scr_width_w = CVAR_INIT ("scr_width_w", "640", CV_SAVE, CV_Unsigned, NULL);
+consvar_t cv_scr_height_w = CVAR_INIT ("scr_height_w", "400", CV_SAVE, CV_Unsigned, NULL);
 consvar_t cv_scr_depth = CVAR_INIT ("scr_depth", "16 bits", CV_SAVE, scr_depth_cons_t, NULL);
 
 consvar_t cv_renderview = CVAR_INIT ("renderview", "On", 0, CV_OnOff, NULL);
@@ -100,7 +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_ASM = true;
 boolean R_486 = false;
 boolean R_586 = false;
 boolean R_MMX = false;
@@ -167,26 +164,6 @@ void SCR_SetDrawFuncs(void)
 		spanfuncs_npo2[SPANDRAWFUNC_WATER] = R_DrawWaterSpan_NPO2_8;
 		spanfuncs_npo2[SPANDRAWFUNC_TILTEDWATER] = R_DrawTiltedWaterSpan_NPO2_8;
 
-#ifdef RUSEASM
-		if (R_ASM)
-		{
-			if (R_MMX)
-			{
-				colfuncs[BASEDRAWFUNC] = R_DrawColumn_8_MMX;
-				//colfuncs[COLDRAWFUNC_SHADE] = R_DrawShadeColumn_8_ASM;
-				//colfuncs[COLDRAWFUNC_FUZZY] = R_DrawTranslucentColumn_8_ASM;
-				colfuncs[COLDRAWFUNC_TWOSMULTIPATCH] = R_Draw2sMultiPatchColumn_8_MMX;
-				spanfuncs[BASEDRAWFUNC] = R_DrawSpan_8_MMX;
-			}
-			else
-			{
-				colfuncs[BASEDRAWFUNC] = R_DrawColumn_8_ASM;
-				//colfuncs[COLDRAWFUNC_SHADE] = R_DrawShadeColumn_8_ASM;
-				//colfuncs[COLDRAWFUNC_FUZZY] = R_DrawTranslucentColumn_8_ASM;
-				colfuncs[COLDRAWFUNC_TWOSMULTIPATCH] = R_Draw2sMultiPatchColumn_8_ASM;
-			}
-		}
-#endif
 	}
 /*	else if (vid.bpp > 1)
 	{
@@ -269,8 +246,6 @@ void SCR_Startup(void)
 		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("-noASM"))
-		R_ASM = false;
 	if (M_CheckParm("-486"))
 		R_486 = true;
 	if (M_CheckParm("-586"))
@@ -377,10 +352,16 @@ void SCR_CheckDefaultMode(void)
 	}
 	else
 	{
-		CONS_Printf(M_GetText("Default resolution: %d x %d (%d bits)\n"), cv_scr_width.value,
-			cv_scr_height.value, cv_scr_depth.value);
-		// see note above
-		setmodeneeded = VID_GetModeForSize(cv_scr_width.value, cv_scr_height.value) + 1;
+		CONS_Printf(M_GetText("Default resolution: %d x %d\n"), cv_scr_width.value, cv_scr_height.value);
+		CONS_Printf(M_GetText("Windowed resolution: %d x %d\n"), cv_scr_width_w.value, cv_scr_height_w.value);
+		CONS_Printf(M_GetText("Default bit depth: %d bits\n"), cv_scr_depth.value);
+		if (cv_fullscreen.value)
+			setmodeneeded = VID_GetModeForSize(cv_scr_width.value, cv_scr_height.value) + 1; // see note above
+		else
+			setmodeneeded = VID_GetModeForSize(cv_scr_width_w.value, cv_scr_height_w.value) + 1; // see note above
+
+		if (setmodeneeded <= 0)
+			CONS_Alert(CONS_WARNING, "Invalid resolution given, defaulting to base resolution\n");
 	}
 
 	if (cv_renderer.value != (signed)rendermode)
@@ -398,9 +379,8 @@ void SCR_CheckDefaultMode(void)
 // sets the modenum as the new default video mode to be saved in the config file
 void SCR_SetDefaultMode(void)
 {
-	// remember the default screen size
-	CV_SetValue(&cv_scr_width, vid.width);
-	CV_SetValue(&cv_scr_height, vid.height);
+	CV_SetValue(cv_fullscreen.value ? &cv_scr_width : &cv_scr_width_w, vid.width);
+	CV_SetValue(cv_fullscreen.value ? &cv_scr_height : &cv_scr_height_w, vid.height);
 }
 
 // Change fullscreen on/off according to cv_fullscreen
@@ -415,7 +395,16 @@ void SCR_ChangeFullscreen(void)
 	if (graphics_started)
 	{
 		VID_PrepareModeList();
-		setmodeneeded = VID_GetModeForSize(vid.width, vid.height) + 1;
+		if (cv_fullscreen.value)
+			setmodeneeded = VID_GetModeForSize(cv_scr_width.value, cv_scr_height.value) + 1;
+		else
+			setmodeneeded = VID_GetModeForSize(cv_scr_width_w.value, cv_scr_height_w.value) + 1;
+
+		if (setmodeneeded <= 0) // hacky safeguard
+		{
+			CONS_Alert(CONS_WARNING, "Invalid resolution given, defaulting to base resolution.\n");
+			setmodeneeded = VID_GetModeForSize(BASEVIDWIDTH, BASEVIDHEIGHT) + 1;
+		}
 	}
 	return;
 #endif
@@ -462,11 +451,11 @@ double averageFPS = 0.0f;
 #define USE_FPS_SAMPLES
 
 #ifdef USE_FPS_SAMPLES
-#define FPS_SAMPLE_RATE (0.05) // How often to update FPS samples, in seconds
+#define MAX_FRAME_TIME 0.05
 #define NUM_FPS_SAMPLES (16) // Number of samples to store
 
-static double fps_samples[NUM_FPS_SAMPLES];
-static double updateElapsed = 0.0;
+static double total_frame_time = 0.0;
+static int frame_index;
 #endif
 
 static boolean fps_init = false;
@@ -489,34 +478,12 @@ void SCR_CalculateFPS(void)
 	fps_enter = fps_finish;
 
 #ifdef USE_FPS_SAMPLES
-	updateElapsed += frameElapsed;
-
-	if (updateElapsed >= FPS_SAMPLE_RATE)
-	{
-		static int sampleIndex = 0;
-		int i;
-
-		fps_samples[sampleIndex] = frameElapsed;
-
-		sampleIndex++;
-		if (sampleIndex >= NUM_FPS_SAMPLES)
-			sampleIndex = 0;
-
-		averageFPS = 0.0;
-		for (i = 0; i < NUM_FPS_SAMPLES; i++)
-		{
-			averageFPS += fps_samples[i];
-		}
-
-		if (averageFPS > 0.0)
-		{
-			averageFPS = 1.0 / (averageFPS / NUM_FPS_SAMPLES);
-		}
-	}
-
-	while (updateElapsed >= FPS_SAMPLE_RATE)
+	total_frame_time += frameElapsed;
+	if (frame_index++ >= NUM_FPS_SAMPLES || total_frame_time >= MAX_FRAME_TIME)
 	{
-		updateElapsed -= FPS_SAMPLE_RATE;
+		averageFPS = 1.0 / (total_frame_time / frame_index);
+		total_frame_time = 0.0;
+		frame_index = 0;
 	}
 #else
 	// Direct, unsampled counter.
diff --git a/src/screen.h b/src/screen.h
index 19103b0df59036c80ea85112f47a7673596ded67..65e82ff4df2bff3701a0f57d4667ee32a34784f5 100644
--- a/src/screen.h
+++ b/src/screen.h
@@ -199,7 +199,9 @@ extern CV_PossibleValue_t cv_renderer_t[];
 extern INT32 scr_bpp;
 extern UINT8 *scr_borderpatch; // patch used to fill the view borders
 
-extern consvar_t cv_scr_width, cv_scr_height, cv_scr_depth, cv_renderview, cv_renderer, cv_fullscreen;
+extern consvar_t cv_scr_width, cv_scr_height, cv_scr_width_w, cv_scr_height_w, cv_scr_depth, cv_fullscreen;
+extern consvar_t cv_renderview, cv_renderer;
+extern consvar_t cv_renderhitbox, cv_renderhitboxinterpolation, cv_renderhitboxgldepth;
 // wait for page flipping to end or not
 extern consvar_t cv_vidwait;
 extern consvar_t cv_timescale;
diff --git a/src/sdl/CMakeLists.txt b/src/sdl/CMakeLists.txt
index be540b778b733976a9ceb08c636bcd30d36d29d2..4c4cdafb640e7806ee1efb79380ee10eaf26e119 100644
--- a/src/sdl/CMakeLists.txt
+++ b/src/sdl/CMakeLists.txt
@@ -1,17 +1,17 @@
 # Declare SDL2 interface sources
 
-target_sources(SRB2SDL2 PRIVATE mixer_sound.c)
-
-target_sourcefile(c)
-
-target_sources(SRB2SDL2 PRIVATE ogl_sdl.c)
-
-target_sources(SRB2SDL2 PRIVATE i_threads.c)
-
-if(${SRB2_USEASM})
-	set_source_files_properties(${SRB2_ASM_SOURCES} PROPERTIES LANGUAGE C)
-	set_source_files_properties(${SRB2_ASM_SOURCES} PROPERTIES COMPILE_FLAGS "-x assembler-with-cpp")
-endif()
+target_sources(SRB2SDL2 PRIVATE
+	mixer_sound.c
+	ogl_sdl.c
+	i_threads.c
+	i_net.c
+	i_system.c
+	i_main.c
+	i_video.c
+	dosstr.c
+	endtxt.c
+	hwsym_sdl.c
+)
 
 if("${CMAKE_SYSTEM_NAME}" MATCHES Windows)
 	target_sources(SRB2SDL2 PRIVATE
@@ -68,18 +68,6 @@ if("${CMAKE_SYSTEM_NAME}" MATCHES Linux)
 	target_link_libraries(SRB2SDL2 PRIVATE m rt)
 endif()
 
-if(${SRB2_USEASM})
-	if(${SRB2_CONFIG_YASM})
-		set(ASM_ASSEMBLER_TEMP ${CMAKE_ASM_YASM_COMPILER})
-		set(ASM_ASSEMBLER_OBJFORMAT ${CMAKE_ASM_YASM_OBJECT_FORMAT})
-		set_source_files_properties(${SRB2_NASM_SOURCES} LANGUAGE ASM_YASM)
-	else()
-		set(ASM_ASSEMBLER_TEMP ${CMAKE_ASM_NASM_COMPILER})
-		set(ASM_ASSEMBLER_OBJFORMAT ${CMAKE_ASM_NASM_OBJECT_FORMAT})
-		set_source_files_properties(${SRB2_NASM_SOURCES} LANGUAGE ASM_NASM)
-	endif()
-endif()
-
 if("${CMAKE_SYSTEM_NAME}" MATCHES Windows)
 	target_link_libraries(SRB2SDL2 PRIVATE
 		ws2_32
diff --git a/src/sdl/MakeCYG.cfg b/src/sdl/MakeCYG.cfg
index 5907579c1bc9d16abb0e338cb709566fd75b6c61..b78316b00142dfcb96188f6fb45baa047a628ed0 100644
--- a/src/sdl/MakeCYG.cfg
+++ b/src/sdl/MakeCYG.cfg
@@ -7,7 +7,6 @@
 
 	NOHW=1
 	NOHS=1
-	NOASM=1
 
 	OPTS+=-DLINUX
 
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index c20265ed17cde4a7d17844029489a713ba5ad617..9b51cfb8094a1d6897e0685e9531ccd21df0fbdb 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -259,10 +259,9 @@
     <ClInclude Include="..\hardware\hw_md2load.h" />
     <ClInclude Include="..\hardware\hw_md3load.h" />
     <ClInclude Include="..\hardware\hw_model.h" />
-    <ClInclude Include="..\hardware\u_list.h" />
+    <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" />
@@ -424,13 +435,9 @@
     <ClCompile Include="..\hardware\hw_md3load.c" />
     <ClCompile Include="..\hardware\hw_model.c" />
     <ClCompile Include="..\hardware\r_opengl\r_opengl.c" />
-    <ClCompile Include="..\hardware\u_list.c" />
+    <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" />
@@ -556,4 +578,4 @@
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
   <ImportGroup Label="ExtensionTargets">
   </ImportGroup>
-</Project>
\ No newline at end of file
+</Project>
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_main.c b/src/sdl/i_main.c
index 1dee379c0d8d95db186f1e9e3bb301554ed0549f..3eeacd83569188bd3bfdb5c8f2b9e720b5dce9f4 100644
--- a/src/sdl/i_main.c
+++ b/src/sdl/i_main.c
@@ -70,39 +70,6 @@ char logfilename[1024];
 typedef BOOL (WINAPI *p_IsDebuggerPresent)(VOID);
 #endif
 
-#if defined (_WIN32)
-static inline VOID MakeCodeWritable(VOID)
-{
-#ifdef USEASM // Disable write-protection of code segment
-	DWORD OldRights;
-	const DWORD NewRights = PAGE_EXECUTE_READWRITE;
-	PBYTE pBaseOfImage = (PBYTE)GetModuleHandle(NULL);
-	PIMAGE_DOS_HEADER dosH =(PIMAGE_DOS_HEADER)pBaseOfImage;
-	PIMAGE_NT_HEADERS ntH = (PIMAGE_NT_HEADERS)(pBaseOfImage + dosH->e_lfanew);
-	PIMAGE_OPTIONAL_HEADER oH = (PIMAGE_OPTIONAL_HEADER)
-		((PBYTE)ntH + sizeof (IMAGE_NT_SIGNATURE) + sizeof (IMAGE_FILE_HEADER));
-	LPVOID pA = pBaseOfImage+oH->BaseOfCode;
-	SIZE_T pS = oH->SizeOfCode;
-#if 1 // try to find the text section
-	PIMAGE_SECTION_HEADER ntS = IMAGE_FIRST_SECTION (ntH);
-	WORD s;
-	for (s = 0; s < ntH->FileHeader.NumberOfSections; s++)
-	{
-		if (memcmp (ntS[s].Name, ".text\0\0", 8) == 0)
-		{
-			pA = pBaseOfImage+ntS[s].VirtualAddress;
-			pS = ntS[s].Misc.VirtualSize;
-			break;
-		}
-	}
-#endif
-
-	if (!VirtualProtect(pA,pS,NewRights,&OldRights))
-		I_Error("Could not make code writable\n");
-#endif
-}
-#endif
-
 #ifdef LOGMESSAGES
 static void InitLogging(void)
 {
@@ -243,7 +210,6 @@ int main(int argc, char **argv)
 #ifndef __MINGW32__
 	prevExceptionFilter = SetUnhandledExceptionFilter(RecordExceptionInfo);
 #endif
-	MakeCodeWritable();
 #endif
 
 	// startup SRB2
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 67ee8d6684268943c9f2d7e07aab332b5d63189c..b05f40ee375376bd7c585c3952daad2446580fca 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -23,12 +23,6 @@
 /// \file
 /// \brief SRB2 system stuff for SDL
 
-#ifdef CMAKECONFIG
-#include "config.h"
-#else
-#include "../config.h.in"
-#endif
-
 #include <signal.h>
 
 #ifdef _WIN32
@@ -41,6 +35,12 @@ typedef DWORD (WINAPI *p_timeGetTime) (void);
 typedef UINT (WINAPI *p_timeEndPeriod) (UINT);
 typedef HANDLE (WINAPI *p_OpenFileMappingA) (DWORD, BOOL, LPCSTR);
 typedef LPVOID (WINAPI *p_MapViewOfFile) (HANDLE, DWORD, DWORD, DWORD, SIZE_T);
+
+// This is for RtlGenRandom.
+#define SystemFunction036 NTAPI SystemFunction036
+#include <ntsecapi.h>
+#undef SystemFunction036
+
 #endif
 #include <stdio.h>
 #include <stdlib.h>
@@ -90,7 +90,7 @@ typedef LPVOID (WINAPI *p_MapViewOfFile) (HANDLE, DWORD, DWORD, DWORD, SIZE_T);
 #include <kvm.h>
 #endif
 #include <nlist.h>
-#include <sys/vmmeter.h>
+#include <sys/sysctl.h>
 #endif
 #endif
 
@@ -187,7 +187,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 +209,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
 
@@ -259,10 +260,10 @@ UINT8 keyboard_started = false;
 
 #ifdef UNIXBACKTRACE
 #define STDERR_WRITE(string) if (fd != -1) I_OutputMsg("%s", string)
-#define CRASHLOG_WRITE(string) if (fd != -1) write(fd, string, strlen(string))
+#define CRASHLOG_WRITE(string) if (fd != -1) junk = write(fd, string, strlen(string))
 #define CRASHLOG_STDERR_WRITE(string) \
 	if (fd != -1)\
-		write(fd, string, strlen(string));\
+		junk = write(fd, string, strlen(string));\
 	I_OutputMsg("%s", string)
 
 static void write_backtrace(INT32 signal)
@@ -271,6 +272,7 @@ static void write_backtrace(INT32 signal)
 	size_t size;
 	time_t rawtime;
 	struct tm timeinfo;
+	ssize_t junk;
 
 	enum { BT_SIZE = 1024, STR_SIZE = 32 };
 	void *array[BT_SIZE];
@@ -314,7 +316,7 @@ static void write_backtrace(INT32 signal)
 	backtrace_symbols_fd(array, size, STDERR_FILENO);
 
 	CRASHLOG_WRITE("\n"); // Write another newline to the log so it looks nice :)
-
+	(void)junk;
 	close(fd);
 }
 #undef STDERR_WRITE
@@ -325,56 +327,90 @@ static void write_backtrace(INT32 signal)
 static void I_ReportSignal(int num, int coredumped)
 {
 	//static char msg[] = "oh no! back to reality!\r\n";
-	const char *      sigmsg;
-	char msg[128];
+	const char *sigmsg, *signame;
+	char ttl[128];
+	char sigttl[512] = "Process killed by signal: ";
+	const char *reportmsg = "\n\nTo help us figure out the cause, you can visit our official Discord server\nwhere you will find more instructions on how to submit a crash report.\n\nSorry for the inconvenience!";
 
 	switch (num)
 	{
 //	case SIGINT:
-//		sigmsg = "SIGINT - interrupted";
+//		sigttl = "SIGINT"
+//		sigmsg = "SRB2 was interrupted prematurely by the user.";
 //		break;
 	case SIGILL:
-		sigmsg = "SIGILL - illegal instruction - invalid function image";
+		sigmsg = "SRB2 has attempted to execute an illegal instruction and needs to close.";
+		signame = "SIGILL"; // illegal instruction - invalid function image
 		break;
 	case SIGFPE:
-		sigmsg = "SIGFPE - mathematical exception";
+		sigmsg = "SRB2 has encountered a mathematical exception and needs to close.";
+		signame = "SIGFPE"; // mathematical exception
 		break;
 	case SIGSEGV:
-		sigmsg = "SIGSEGV - segment violation";
+		sigmsg = "SRB2 has attempted to access a memory location that it shouldn't and needs to close.";
+		signame = "SIGSEGV"; // segment violation
 		break;
 //	case SIGTERM:
-//		sigmsg = "SIGTERM - Software termination signal from kill";
+//		sigmsg = "SRB2 was terminated by a kill signal.";
+//		sigttl = "SIGTERM"; // Software termination signal from kill
 //		break;
 //	case SIGBREAK:
-//		sigmsg = "SIGBREAK - Ctrl-Break sequence";
+//		sigmsg = "SRB2 was terminated by a Ctrl-Break sequence.";
+//		sigttl = "SIGBREAK" // Ctrl-Break sequence
 //		break;
 	case SIGABRT:
-		sigmsg = "SIGABRT - abnormal termination triggered by abort call";
+		sigmsg = "SRB2 was terminated by an abort signal.";
+		signame = "SIGABRT"; // abnormal termination triggered by abort call
 		break;
 	default:
-		sprintf(msg,"signal number %d", num);
+		sigmsg = "SRB2 was terminated by an unknown signal.";
+
+		sprintf(ttl, "number %d", num);
 		if (coredumped)
-			sigmsg = 0;
+			signame = 0;
 		else
-			sigmsg = msg;
+			signame = ttl;
 	}
 
 	if (coredumped)
 	{
-		if (sigmsg)
-			sprintf(msg, "%s (core dumped)", sigmsg);
+		if (signame)
+			sprintf(ttl, "%s (core dumped)", signame);
 		else
-			strcat(msg, " (core dumped)");
+			strcat(ttl, " (core dumped)");
 
-		sigmsg = msg;
+		signame = ttl;
 	}
 
-	I_OutputMsg("\nProcess killed by signal: %s\n\n", sigmsg);
+	strcat(sigttl, signame);
+	I_OutputMsg("%s\n", sigttl);
 
-	if (!M_CheckParm("-dedicated"))
-		SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,
-			"Process killed by signal",
-			sigmsg, NULL);
+	if (M_CheckParm("-dedicated"))
+		return;
+
+	const SDL_MessageBoxButtonData buttons[] = {
+		{ SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 0,		"OK" },
+		{ 										0, 1,  "Discord" },
+	};
+
+	const SDL_MessageBoxData messageboxdata = {
+		SDL_MESSAGEBOX_ERROR, /* .flags */
+		NULL, /* .window */
+		sigttl, /* .title */
+		va("%s %s", sigmsg, reportmsg), /* .message */
+		SDL_arraysize(buttons), /* .numbuttons */
+		buttons, /* .buttons */
+		NULL /* .colorScheme */
+	};
+
+	int buttonid;
+
+	SDL_ShowMessageBox(&messageboxdata, &buttonid);
+
+#if SDL_VERSION_ATLEAST(2,0,14)
+	if (buttonid == 1)
+		SDL_OpenURL("https://www.srb2.org/discord");
+#endif
 }
 
 #ifndef NEWSIGNALHANDLER
@@ -2230,7 +2266,7 @@ void I_Sleep(UINT32 ms)
 }
 
 #ifdef NEWSIGNALHANDLER
-static void newsignalhandler_Warn(const char *pr)
+ATTRNORETURN static FUNCNORETURN void newsignalhandler_Warn(const char *pr)
 {
 	char text[128];
 
@@ -2322,7 +2358,10 @@ INT32 I_StartupSystem(void)
 #endif
 	I_StartupConsole();
 #ifdef NEWSIGNALHANDLER
-	I_Fork();
+	// This is useful when debugging. It lets GDB attach to
+	// the correct process easily.
+	if (!M_CheckParm("-nofork"))
+		I_Fork();
 #endif
 	I_RegisterSignals();
 	I_OutputMsg("Compiled for SDL version: %d.%d.%d\n",
@@ -2349,10 +2388,8 @@ 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(); // Tails 12-08-2002
+	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.
 
@@ -2436,7 +2473,7 @@ void I_Error(const char *error, ...)
 		if (errorcount == 8)
 		{
 			M_SaveConfig(NULL);
-			G_SaveGameData();
+			G_SaveGameData(clientGamedata);
 		}
 		if (errorcount > 20)
 		{
@@ -2466,10 +2503,8 @@ void I_Error(const char *error, ...)
 	// ---
 
 	M_SaveConfig(NULL); // save game config, cvars..
-#ifndef NONET
 	D_SaveBan(); // save the ban list
-#endif
-	G_SaveGameData(); // Tails 12-08-2002
+	G_SaveGameData(clientGamedata); // Tails 12-08-2002
 
 	// Shutdown. Here might be other errors.
 	if (demorecording)
@@ -2608,9 +2643,10 @@ void I_ShutdownSystem(void)
 {
 	INT32 c;
 
-#ifndef NEWSIGNALHANDLER
-	I_ShutdownConsole();
+#ifdef NEWSIGNALHANDLER
+	if (M_CheckParm("-nofork"))
 #endif
+		I_ShutdownConsole();
 
 	for (c = MAX_QUIT_FUNCS-1; c >= 0; c--)
 		if (quit_funcs[c])
@@ -2742,6 +2778,38 @@ INT32 I_PutEnv(char *variable)
 #endif
 }
 
+size_t I_GetRandomBytes(char *destination, size_t count)
+{
+#if defined (__unix__) || defined (UNIXCOMMON) || defined(__APPLE__)
+	FILE *rndsource;
+	size_t actual_bytes;
+
+	if (!(rndsource = fopen("/dev/urandom", "r")))
+		if (!(rndsource = fopen("/dev/random", "r")))
+			actual_bytes = 0;
+
+	if (rndsource)
+	{
+		actual_bytes = fread(destination, 1, count, rndsource);
+		fclose(rndsource);
+	}
+
+	if (actual_bytes == 0)
+		I_OutputMsg("I_GetRandomBytes(): couldn't get any random bytes");
+
+	return actual_bytes;
+#elif defined (_WIN32)
+	if (RtlGenRandom(destination, count))
+		return count;
+
+	I_OutputMsg("I_GetRandomBytes(): couldn't get any random bytes");
+	return 0;
+#else
+	#warning SDL I_GetRandomBytes is not implemented on this platform.
+	return 0;
+#endif
+}
+
 INT32 I_ClipboardCopy(const char *data, size_t size)
 {
 	char storage[256];
@@ -2965,40 +3033,17 @@ static long get_entry(const char* name, const char* buf)
 size_t I_GetFreeMem(size_t *total)
 {
 #ifdef FREEBSD
-	struct vmmeter sum;
-	kvm_t *kd;
-	struct nlist namelist[] =
-	{
-#define X_SUM   0
-		{"_cnt"},
-		{NULL}
-	};
-	if ((kd = kvm_open(NULL, NULL, NULL, O_RDONLY, "kvm_open")) == NULL)
-	{
-		if (total)
-			*total = 0L;
-		return 0;
-	}
-	if (kvm_nlist(kd, namelist) != 0)
-	{
-		kvm_close (kd);
-		if (total)
-			*total = 0L;
-		return 0;
-	}
-	if (kvm_read(kd, namelist[X_SUM].n_value, &sum,
-		sizeof (sum)) != sizeof (sum))
-	{
-		kvm_close(kd);
-		if (total)
-			*total = 0L;
-		return 0;
-	}
-	kvm_close(kd);
+	u_int v_free_count, v_page_size, v_page_count;
+	size_t size = sizeof(v_free_count);
+	sysctlbyname("vm.stats.vm.v_free_count", &v_free_count, &size, NULL, 0);
+	size = sizeof(v_page_size);
+	sysctlbyname("vm.stats.vm.v_page_size", &v_page_size, &size, NULL, 0);
+	size = sizeof(v_page_count);
+	sysctlbyname("vm.stats.vm.v_page_count", &v_page_count, &size, NULL, 0);
 
 	if (total)
-		*total = sum.v_page_count * sum.v_page_size;
-	return sum.v_free_count * sum.v_page_size;
+		*total = v_page_count * v_page_size;
+	return v_free_count * v_page_size;
 #elif defined (SOLARIS)
 	/* Just guess */
 	if (total)
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 d2fbb90063f13f5ed663ee7a2665b95f8aac296e..590d7d142a7a536678bfb96d3891c78264a19fba 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -1593,7 +1593,6 @@ boolean VID_CheckRenderer(void)
 			else if (vid.glstate == VID_GL_LIBRARY_ERROR)
 				rendererchanged = false;
 		}
-		else
 #endif
 
 		if (!contextcreated)
@@ -1859,7 +1858,7 @@ void I_StartupGraphics(void)
 	borderlesswindow = M_CheckParm("-borderless");
 
 	//SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY>>1,SDL_DEFAULT_REPEAT_INTERVAL<<2);
-	VID_Command_ModeList_f();
+	//VID_Command_ModeList_f();
 
 #ifdef HWRENDER
 	if (rendermode == render_opengl)
@@ -1907,7 +1906,7 @@ void I_StartupGraphics(void)
 	realwidth = (Uint16)vid.width;
 	realheight = (Uint16)vid.height;
 
-	VID_Command_Info_f();
+	//VID_Command_Info_f();
 	SDLdoUngrabMouse();
 
 	SDL_RaiseWindow(window);
diff --git a/src/sdl/mixer_sound.c b/src/sdl/mixer_sound.c
index f13aaef5d660f7e626f3e9c4c522b007fa6abd2d..0a39c7f286f8dc3e4b563709de240b0370e1b2b9 100644
--- a/src/sdl/mixer_sound.c
+++ b/src/sdl/mixer_sound.c
@@ -108,6 +108,7 @@ static UINT32 fading_timer;
 static UINT32 fading_duration;
 static INT32 fading_id;
 static void (*fading_callback)(void);
+static boolean fading_do_callback;
 static boolean fading_nocleanup;
 
 #ifdef HAVE_GME
@@ -213,7 +214,10 @@ static void var_cleanup(void)
 	// HACK: See music_loop, where we want the fade timing to proceed after a non-looping
 	// song has stopped playing
 	if (!fading_nocleanup)
+	{
 		fading_callback = NULL;
+		fading_do_callback = false;
+	}
 	else
 		fading_nocleanup = false; // use it once, set it back immediately
 
@@ -330,6 +334,13 @@ void I_ShutdownSound(void)
 
 void I_UpdateSound(void)
 {
+	if (fading_do_callback)
+	{
+		if (fading_callback)
+			(*fading_callback)();
+		fading_callback = NULL;
+		fading_do_callback = false;
+	}
 }
 
 /// ------------------------
@@ -654,9 +665,8 @@ static UINT32 get_adjusted_position(UINT32 position)
 
 static void do_fading_callback(void)
 {
-	if (fading_callback)
-		(*fading_callback)();
-	fading_callback = NULL;
+	// TODO: Should I use a mutex here or something?
+	fading_do_callback = true;
 }
 
 /// ------------------------
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 206c93273329238b5df39523fccf7cd4d2628752..de19c468fcdcc4cc09ea3fb06eb34002b1fdf143 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -108,6 +108,9 @@ static patch_t *sneakers;
 static patch_t *gravboots;
 static patch_t *nonicon;
 static patch_t *nonicon2;
+static patch_t *nightopianhelper;
+static patch_t *linkfreeze;
+static patch_t *superparaloop;
 static patch_t *bluestat;
 static patch_t *byelstat;
 static patch_t *orngstat;
@@ -167,6 +170,15 @@ hudinfo_t hudinfo[NUMHUDITEMS] =
 static huddrawlist_h luahuddrawlist_game[2];
 static huddrawlist_h luahuddrawlist_titlecard;
 
+// NiGHTS link colors; 3 sets with increasingly fancy colors (1 to 299, 300 to 599, 600 and above)
+skincolornum_t linkColor[3][NUMLINKCOLORS] = {
+{SKINCOLOR_SHAMROCK, SKINCOLOR_AQUA, SKINCOLOR_SKY, SKINCOLOR_BLUE, SKINCOLOR_PURPLE, SKINCOLOR_MAGENTA,
+ SKINCOLOR_ROSY, SKINCOLOR_RED, SKINCOLOR_ORANGE, SKINCOLOR_GOLD, SKINCOLOR_YELLOW, SKINCOLOR_PERIDOT},
+{SKINCOLOR_EMERALD, SKINCOLOR_OCEAN, SKINCOLOR_AQUAMARINE, SKINCOLOR_SAPPHIRE, SKINCOLOR_GALAXY, SKINCOLOR_SIBERITE,
+ SKINCOLOR_TAFFY, SKINCOLOR_RUBY, SKINCOLOR_GARNET, SKINCOLOR_TOPAZ, SKINCOLOR_LEMON, SKINCOLOR_LIME},
+{SKINCOLOR_ISLAND, SKINCOLOR_TURQUOISE, SKINCOLOR_DREAM, SKINCOLOR_DAYBREAK, SKINCOLOR_VAPOR, SKINCOLOR_FUCHSIA,
+ SKINCOLOR_VIOLET, SKINCOLOR_EVENTIDE, SKINCOLOR_KETCHUP, SKINCOLOR_FOUNDATION, SKINCOLOR_HEADLIGHT, SKINCOLOR_CHARTREUSE}};
+
 //
 // STATUS BAR CODE
 //
@@ -313,6 +325,10 @@ void ST_LoadGraphics(void)
 	nonicon2 = W_CachePatchName("NONICON2", PU_HUDGFX);
 
 	// NiGHTS HUD things
+	nightopianhelper = W_CachePatchName("NHLPICON", PU_HUDGFX);
+	linkfreeze = W_CachePatchName("NLFZICON", PU_HUDGFX);
+	superparaloop = W_CachePatchName("NSPRICON", PU_HUDGFX);
+
 	bluestat = W_CachePatchName("BLUESTAT", PU_HUDGFX);
 	byelstat = W_CachePatchName("BYELSTAT", PU_HUDGFX);
 	orngstat = W_CachePatchName("ORNGSTAT", PU_HUDGFX);
@@ -799,7 +815,7 @@ static inline void ST_drawRings(void)
 static void ST_drawLivesArea(void)
 {
 	INT32 v_colmap = V_YELLOWMAP, livescount;
-	boolean notgreyedout;
+	boolean notgreyedout = false;
 
 	if (!stplyr->skincolor)
 		return; // Just joined a server, skin isn't loaded yet!
@@ -811,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)
 	{
@@ -840,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);
 	}
@@ -852,39 +870,36 @@ static void ST_drawLivesArea(void)
 	if (metalrecording)
 	{
 		if (((2*leveltime)/TICRATE) & 1)
+		{
 			V_DrawRightAlignedString(hudinfo[HUD_LIVES].x+58, hudinfo[HUD_LIVES].y+8,
 				hudinfo[HUD_LIVES].f|V_PERPLAYER|V_REDMAP|V_HUDTRANS, "REC");
+		}
 	}
 	// Spectator
 	else if (stplyr->spectator)
-		v_colmap = V_GRAYMAP;
-	// Tag
-	else if (gametyperules & GTR_TAG)
 	{
-		if (stplyr->pflags & PF_TAGIT)
-		{
-			V_DrawRightAlignedString(hudinfo[HUD_LIVES].x+58, hudinfo[HUD_LIVES].y+8, V_HUDTRANS|hudinfo[HUD_LIVES].f|V_PERPLAYER, "IT!");
-			v_colmap = V_ORANGEMAP;
-		}
+		v_colmap = V_GRAYMAP;
 	}
-	// Team name
-	else if (G_GametypeHasTeams())
+	else
 	{
-		if (stplyr->ctfteam == 1)
+		boolean candrawlives = false;
+
+		// Set the player's name color.
+		if (G_TagGametype() && (stplyr->pflags & PF_TAGIT))
 		{
-			V_DrawRightAlignedString(hudinfo[HUD_LIVES].x+58, hudinfo[HUD_LIVES].y+8, V_HUDTRANS|hudinfo[HUD_LIVES].f|V_PERPLAYER, "RED");
-			v_colmap = V_REDMAP;
+			v_colmap = V_ORANGEMAP;
 		}
-		else if (stplyr->ctfteam == 2)
+		else if (G_GametypeHasTeams())
 		{
-			V_DrawRightAlignedString(hudinfo[HUD_LIVES].x+58, hudinfo[HUD_LIVES].y+8, V_HUDTRANS|hudinfo[HUD_LIVES].f|V_PERPLAYER, "BLUE");
-			v_colmap = V_BLUEMAP;
+			if (stplyr->ctfteam == 1)
+			{
+				v_colmap = V_REDMAP;
+			}
+			else if (stplyr->ctfteam == 2)
+			{
+				v_colmap = V_BLUEMAP;
+			}
 		}
-	}
-	// Lives number
-	else
-	{
-		boolean candrawlives = true;
 
 		// Co-op and Competition, normal life counter
 		if (G_GametypeUsesLives())
@@ -920,12 +935,15 @@ static void ST_drawLivesArea(void)
 				livescount = (((netgame || multiplayer) && G_GametypeUsesCoopLives() && cv_cooplives.value == 0) ? INFLIVES : stplyr->lives);
 				notgreyedout = true;
 			}
+
+			candrawlives = true;
 		}
 		// Infinity symbol (Race)
 		else if (G_PlatformGametype() && !(gametyperules & GTR_LIVES))
 		{
 			livescount = INFLIVES;
 			notgreyedout = true;
+			candrawlives = true;
 		}
 		// Otherwise nothing, sorry.
 		// Special Stages keep not showing lives,
@@ -934,8 +952,6 @@ static void ST_drawLivesArea(void)
 		// cannot show up because Special Stages
 		// still have the GTR_LIVES gametype rule
 		// by default.
-		else
-			candrawlives = false;
 
 		// Draw the lives counter here.
 		if (candrawlives)
@@ -943,8 +959,10 @@ static void ST_drawLivesArea(void)
 			// x
 			V_DrawScaledPatch(hudinfo[HUD_LIVES].x+22, hudinfo[HUD_LIVES].y+10, hudinfo[HUD_LIVES].f|V_PERPLAYER|V_HUDTRANS, stlivex);
 			if (livescount == INFLIVES)
+			{
 				V_DrawCharacter(hudinfo[HUD_LIVES].x+50, hudinfo[HUD_LIVES].y+8,
 					'\x16' | 0x80 | hudinfo[HUD_LIVES].f|V_PERPLAYER|V_HUDTRANS, false);
+			}
 			else
 			{
 				if (stplyr->playerstate == PST_DEAD && !(stplyr->spectator) && (livescount || stplyr->deadtimer < (TICRATE<<1)) && !(stplyr->pflags & PF_FINISHED))
@@ -955,6 +973,25 @@ static void ST_drawLivesArea(void)
 					hudinfo[HUD_LIVES].f|V_PERPLAYER|(notgreyedout ? V_HUDTRANS : V_HUDTRANSHALF), va("%d",livescount));
 			}
 		}
+		else
+		{
+			// Draw team name instead of lives, if possible.
+			if (G_TagGametype() && (stplyr->pflags & PF_TAGIT))
+			{
+				V_DrawRightAlignedString(hudinfo[HUD_LIVES].x+58, hudinfo[HUD_LIVES].y+8, V_HUDTRANS|hudinfo[HUD_LIVES].f|V_PERPLAYER, "IT!");
+			}
+			else if (G_GametypeHasTeams())
+			{
+				if (stplyr->ctfteam == 1)
+				{
+					V_DrawRightAlignedString(hudinfo[HUD_LIVES].x+58, hudinfo[HUD_LIVES].y+8, V_HUDTRANS|hudinfo[HUD_LIVES].f|V_PERPLAYER, "RED");
+				}
+				else if (stplyr->ctfteam == 2)
+				{
+					V_DrawRightAlignedString(hudinfo[HUD_LIVES].x+58, hudinfo[HUD_LIVES].y+8, V_HUDTRANS|hudinfo[HUD_LIVES].f|V_PERPLAYER, "BLUE");
+				}
+			}
+		}
 #undef ST_drawLivesX
 	}
 
@@ -995,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;
 
@@ -1448,6 +1486,21 @@ void ST_drawWipeTitleCard(void)
 	}
 }
 
+#define ICONSEP (16+4) // matches weapon rings HUD
+
+static INT32 ST_powerupHUDoffset(UINT16 timer)
+{
+	if (timer > 7)
+		return ICONSEP;
+	else
+	{
+		UINT8 a = ICONSEP, b = 7-timer;
+		while (b--)
+			a = 2*a/3;
+		return a;
+	}
+}
+
 static void ST_drawPowerupHUD(void)
 {
 	patch_t *p = NULL;
@@ -1455,7 +1508,6 @@ static void ST_drawPowerupHUD(void)
 	INT32 offs = hudinfo[HUD_POWERUPS].x;
 	const UINT8 q = ((splitscreen && stplyr == &players[secondarydisplayplayer]) ? 1 : 0);
 	static INT32 flagoffs[2] = {0, 0}, shieldoffs[2] = {0, 0}, finishoffs[2] = {0, 0};
-#define ICONSEP (16+4) // matches weapon rings HUD
 
 	if (F_GetPromptHideHud(hudinfo[HUD_POWERUPS].y))
 		return;
@@ -1567,15 +1619,7 @@ static void ST_drawPowerupHUD(void)
 		DRAWTIMERICON(invincibility, invulntime)
 	}
 
-	if (invulntime > 7)
-		offs -= ICONSEP;
-	else
-	{
-		UINT8 a = ICONSEP, b = 7-invulntime;
-		while (b--)
-			a = 2*a/3;
-		offs -= a;
-	}
+	offs -= ST_powerupHUDoffset(invulntime);
 
 	// Super Sneakers
 	if (stplyr->powers[pw_sneakers] > 3*TICRATE || (stplyr->powers[pw_sneakers] && leveltime & 1))
@@ -1583,15 +1627,7 @@ static void ST_drawPowerupHUD(void)
 		DRAWTIMERICON(sneakers, stplyr->powers[pw_sneakers])
 	}
 
-	if (stplyr->powers[pw_sneakers] > 7)
-		offs -= ICONSEP;
-	else
-	{
-		UINT8 a = ICONSEP, b = 7-stplyr->powers[pw_sneakers];
-		while (b--)
-			a = 2*a/3;
-		offs -= a;
-	}
+	offs -= ST_powerupHUDoffset(stplyr->powers[pw_sneakers]);
 
 	// Gravity Boots
 	if (stplyr->powers[pw_gravityboots] > 3*TICRATE || (stplyr->powers[pw_gravityboots] && leveltime & 1))
@@ -1599,6 +1635,36 @@ static void ST_drawPowerupHUD(void)
 		DRAWTIMERICON(gravboots, stplyr->powers[pw_gravityboots])
 	}
 
+	offs -= ST_powerupHUDoffset(stplyr->powers[pw_gravityboots]);
+
+// --------------------
+// NiGHTS timer-based powerups
+// --------------------
+
+	// Nightopian Helper
+	if (stplyr->powers[pw_nights_helper] > 3*TICRATE || (stplyr->powers[pw_nights_helper] && leveltime & 1))
+	{
+		DRAWTIMERICON(nightopianhelper, stplyr->powers[pw_nights_helper])
+	}
+
+	offs -= ST_powerupHUDoffset(stplyr->powers[pw_nights_helper]);
+
+	// Link Freeze
+	if (stplyr->powers[pw_nights_linkfreeze] > 3*TICRATE || (stplyr->powers[pw_nights_linkfreeze] && leveltime & 1))
+	{
+		DRAWTIMERICON(linkfreeze, stplyr->powers[pw_nights_linkfreeze])
+	}
+
+	offs -= ST_powerupHUDoffset(stplyr->powers[pw_nights_linkfreeze]);
+
+	// Super Paraloop
+	if (stplyr->powers[pw_nights_superloop] > 3*TICRATE || (stplyr->powers[pw_nights_superloop] && leveltime & 1))
+	{
+		DRAWTIMERICON(superparaloop, stplyr->powers[pw_nights_superloop])
+	}
+
+	//offs -= ST_powerupHUDoffset(stplyr->powers[pw_nights_superloop]);
+
 #undef DRAWTIMERICON
 #undef ICONSEP
 }
@@ -1697,7 +1763,7 @@ static void ST_drawNightsRecords(void)
 			ST_DrawNightsOverlayNum((BASEVIDWIDTH/2 + 56)<<FRACBITS, 160<<FRACBITS, FRACUNIT, aflag, stplyr->lastmarescore, nightsnum, SKINCOLOR_AZURE);
 
 			// If new record, say so!
-			if (!(netgame || multiplayer) && G_GetBestNightsScore(gamemap, stplyr->lastmare + 1) <= stplyr->lastmarescore)
+			if (!(netgame || multiplayer) && G_GetBestNightsScore(gamemap, stplyr->lastmare + 1, clientGamedata) <= stplyr->lastmarescore)
 			{
 				if (stplyr->texttimer & 16)
 					V_DrawCenteredString(BASEVIDWIDTH/2, 184, V_YELLOWMAP|aflag, "* NEW RECORD *");
@@ -1716,43 +1782,11 @@ static void ST_drawNightsRecords(void)
 	}
 }
 
-// 2.0-1: [21:42] <+Rob> Beige - Lavender - Steel Blue - Peach - Orange - Purple - Silver - Yellow - Pink - Red - Blue - Green - Cyan - Gold
-/*#define NUMLINKCOLORS 14
-static skincolornum_t linkColor[NUMLINKCOLORS] =
-{SKINCOLOR_BEIGE,  SKINCOLOR_LAVENDER, SKINCOLOR_AZURE, SKINCOLOR_PEACH, SKINCOLOR_ORANGE,
- SKINCOLOR_MAGENTA, SKINCOLOR_SILVER, SKINCOLOR_SUPERGOLD4, SKINCOLOR_PINK,  SKINCOLOR_RED,
- SKINCOLOR_BLUE, SKINCOLOR_GREEN, SKINCOLOR_CYAN, SKINCOLOR_GOLD};*/
-
-// 2.2 indev list: (unix time 1470866042) <Rob> Emerald, Aqua, Cyan, Blue, Pastel, Purple, Magenta, Rosy, Red, Orange, Gold, Yellow, Peridot
-/*#define NUMLINKCOLORS 13
-static skincolornum_t linkColor[NUMLINKCOLORS] =
-{SKINCOLOR_EMERALD, SKINCOLOR_AQUA, SKINCOLOR_CYAN, SKINCOLOR_BLUE, SKINCOLOR_PASTEL,
- SKINCOLOR_PURPLE, SKINCOLOR_MAGENTA, SKINCOLOR_ROSY, SKINCOLOR_RED,  SKINCOLOR_ORANGE,
- SKINCOLOR_GOLD, SKINCOLOR_YELLOW, SKINCOLOR_PERIDOT};*/
-
-// 2.2 indev list again: [19:59:52] <baldobo> Ruby > Red > Flame > Sunset > Orange > Gold > Yellow > Lime > Green > Aqua  > cyan > Sky > Blue > Pastel > Purple > Bubblegum > Magenta > Rosy > repeat
-// [20:00:25] <baldobo> Also Icy for the link freeze text color
-// [20:04:03] <baldobo> I would start it on lime
-/*#define NUMLINKCOLORS 18
-static skincolornum_t linkColor[NUMLINKCOLORS] =
-{SKINCOLOR_LIME, SKINCOLOR_EMERALD, SKINCOLOR_AQUA, SKINCOLOR_CYAN, SKINCOLOR_SKY,
- SKINCOLOR_SAPPHIRE, SKINCOLOR_PASTEL, SKINCOLOR_PURPLE, SKINCOLOR_BUBBLEGUM, SKINCOLOR_MAGENTA,
- SKINCOLOR_ROSY, SKINCOLOR_RUBY, SKINCOLOR_RED, SKINCOLOR_FLAME, SKINCOLOR_SUNSET,
- SKINCOLOR_ORANGE, SKINCOLOR_GOLD, SKINCOLOR_YELLOW};*/
-
-// 2.2+ list for real this time: https://wiki.srb2.org/wiki/User:Rob/Sandbox (check history around 31/10/17, spoopy)
-#define NUMLINKCOLORS 12
-static skincolornum_t linkColor[2][NUMLINKCOLORS] = {
-{SKINCOLOR_EMERALD, SKINCOLOR_AQUA, SKINCOLOR_SKY, SKINCOLOR_BLUE, SKINCOLOR_PURPLE, SKINCOLOR_MAGENTA,
- SKINCOLOR_ROSY, SKINCOLOR_RED, SKINCOLOR_ORANGE, SKINCOLOR_GOLD, SKINCOLOR_YELLOW, SKINCOLOR_PERIDOT},
-{SKINCOLOR_SEAFOAM, SKINCOLOR_CYAN, SKINCOLOR_WAVE, SKINCOLOR_SAPPHIRE, SKINCOLOR_VAPOR, SKINCOLOR_BUBBLEGUM,
- SKINCOLOR_VIOLET, SKINCOLOR_RUBY, SKINCOLOR_FLAME, SKINCOLOR_SUNSET, SKINCOLOR_SANDY, SKINCOLOR_LIME}};
-
 static void ST_drawNiGHTSLink(void)
 {
 	static INT32 prevsel[2] = {0, 0}, prevtime[2] = {0, 0};
 	const UINT8 q = ((splitscreen && stplyr == &players[secondarydisplayplayer]) ? 1 : 0);
-	INT32 sel = ((stplyr->linkcount-1) / 5) % NUMLINKCOLORS, aflag = V_PERPLAYER, mag = ((stplyr->linkcount-1 >= 300) ? 1 : 0);
+	INT32 sel = ((stplyr->linkcount-1) / 5) % NUMLINKCOLORS, aflag = V_PERPLAYER, mag = ((stplyr->linkcount-1 >= 300) ? (stplyr->linkcount-1 >= 600) ? 2 : 1 : 0);
 	skincolornum_t colornum;
 	fixed_t x, y, scale;
 
@@ -2481,6 +2515,8 @@ num:
 static INT32 ST_drawEmeraldHuntIcon(mobj_t *hunt, patch_t **patches, INT32 offset)
 {
 	INT32 interval, i;
+	if (stplyr->mo == NULL)
+		return 0;  // player just joined after spectating, can happen on custom gamemodes.
 	UINT32 dist = ((UINT32)P_AproxDistance(P_AproxDistance(stplyr->mo->x - hunt->x, stplyr->mo->y - hunt->y), stplyr->mo->z - hunt->z))>>FRACBITS;
 
 	if (dist < 128)
@@ -2545,7 +2581,7 @@ static void ST_doHuntIconsAndSound(void)
 		S_StartSound(NULL, sfx_emfind);
 }
 
-static void ST_doItemFinderIconsAndSound(void)
+static boolean ST_doItemFinderIconsAndSound(void)
 {
 	INT32 emblems[16];
 	thinker_t *th;
@@ -2556,6 +2592,12 @@ static void ST_doItemFinderIconsAndSound(void)
 	INT32 interval = 0, newinterval = 0;
 	INT32 soffset;
 
+	if (!(cv_itemfinder.value && M_SecretUnlocked(SECRET_ITEMFINDER, clientGamedata)))
+	{
+		// Not unlocked, or not enabled. Use emerald hunt radar.
+		return false;
+	}
+
 	for (i = 0; i < numemblems; ++i)
 	{
 		if (emblemlocations[i].type > ET_SKIN || emblemlocations[i].level != gamemap)
@@ -2563,15 +2605,21 @@ static void ST_doItemFinderIconsAndSound(void)
 
 		emblems[stemblems++] = i;
 
-		if (!emblemlocations[i].collected)
+		if (!P_EmblemWasCollected(i) && P_CanPickupEmblem(stplyr, i))
+		{
 			++stunfound;
+		}
 
 		if (stemblems >= 16)
 			break;
 	}
+
 	// Found all/none exist? Don't waste our time
 	if (!stunfound)
-		return;
+	{
+		// Allow emerald hunt radar to function after they're all collected.
+		return false;
+	}
 
 	// Scan thinkers to find emblem mobj with these ids
 	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
@@ -2591,6 +2639,9 @@ static void ST_doItemFinderIconsAndSound(void)
 		{
 			if (mo2->health == emblems[i] + 1)
 			{
+				if (P_EmblemWasCollected(emblems[i]) || !P_CanPickupEmblem(stplyr, emblems[i]))
+					break;
+
 				soffset = (i * 20) - ((stemblems - 1) * 10);
 
 				newinterval = ST_drawEmeraldHuntIcon(mo2, itemhoming, soffset);
@@ -2605,6 +2656,8 @@ static void ST_doItemFinderIconsAndSound(void)
 
 	if (!(P_AutoPause() || paused) && interval > 0 && leveltime && leveltime % interval == 0 && renderisnewtic)
 		S_StartSound(NULL, sfx_emfind);
+
+	return true;
 }
 
 //
@@ -2723,9 +2776,7 @@ static void ST_overlayDrawer(void)
 			ST_drawRaceHUD();
 
 		// Emerald Hunt Indicators
-		if (cv_itemfinder.value && M_SecretUnlocked(SECRET_ITEMFINDER))
-			ST_doItemFinderIconsAndSound();
-		else
+		if (!ST_doItemFinderIconsAndSound())
 			ST_doHuntIconsAndSound();
 
 		if(!P_IsLocalPlayer(stplyr))
@@ -2740,18 +2791,16 @@ static void ST_overlayDrawer(void)
 		}
 
 		// This is where we draw all the fun cheese if you have the chasecam off!
-		if (!(maptol & TOL_NIGHTS))
+		if ((stplyr == &players[displayplayer] && !camera.chase)
+		|| ((splitscreen && stplyr == &players[secondarydisplayplayer]) && !camera2.chase))
 		{
-			if ((stplyr == &players[displayplayer] && !camera.chase)
-			|| ((splitscreen && stplyr == &players[secondarydisplayplayer]) && !camera2.chase))
-			{
-				ST_drawFirstPersonHUD();
-				if (cv_powerupdisplay.value)
-					ST_drawPowerupHUD();  // same as it ever was...
-			}
-			else if (cv_powerupdisplay.value == 2)
+			ST_drawFirstPersonHUD();
+			if (cv_powerupdisplay.value)
 				ST_drawPowerupHUD();  // same as it ever was...
 		}
+		else if (cv_powerupdisplay.value == 2)
+			ST_drawPowerupHUD();  // same as it ever was...
+		
 	}
 	else if (!(netgame || multiplayer) && cv_powerupdisplay.value == 2)
 		ST_drawPowerupHUD(); // same as it ever was...
diff --git a/src/st_stuff.h b/src/st_stuff.h
index 68ac900f7724f4a64f27a0dc2a0a499707387d58..603be3c309c761b31fa269f2b0fbad795a489676 100644
--- a/src/st_stuff.h
+++ b/src/st_stuff.h
@@ -124,4 +124,7 @@ extern hudinfo_t hudinfo[NUMHUDITEMS];
 
 extern UINT16 objectsdrawn;
 
+#define NUMLINKCOLORS 12
+extern skincolornum_t linkColor[3][NUMLINKCOLORS];
+
 #endif
diff --git a/src/strcasestr.c b/src/strcasestr.c
index 2796f11d52658f2376b547a3d41d6b2d2974f15e..37899a8425b3d2082957c41cbdae07056798fe74 100644
--- a/src/strcasestr.c
+++ b/src/strcasestr.c
@@ -61,7 +61,7 @@ swapp (char ***ppap, char ***ppbp, char **cpap, char **cpbp)
 }
 
 char *
-strcasestr (const char *s, const char *q)
+nongnu_strcasestr (const char *s, const char *q)
 {
 	size_t  qn;
 
diff --git a/src/string.c b/src/string.c
index dd3080a979ca3ccffa811f27ee408bef7540bb26..2f16fa4c68a35fa287156f546fc2973e9d4ad426 100644
--- a/src/string.c
+++ b/src/string.c
@@ -15,7 +15,7 @@
 #include <string.h>
 #include "doomdef.h"
 
-#if !defined (__APPLE__)
+#ifndef SRB2_HAVE_STRLCPY
 
 // Like the OpenBSD version, but it doesn't check for src not being a valid
 // C string.
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..28c4ce492f2d8b4e7590e3ac10f9ce6a95d98a0f
--- /dev/null
+++ b/src/tests/CMakeLists.txt
@@ -0,0 +1,3 @@
+target_sources(srb2tests PRIVATE
+	boolcompat.cpp
+)
diff --git a/src/tests/boolcompat.cpp b/src/tests/boolcompat.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fee40cd36f2bce34217a875024d6b41fe71adbd1
--- /dev/null
+++ b/src/tests/boolcompat.cpp
@@ -0,0 +1,8 @@
+#include <catch2/catch_test_macros.hpp>
+
+#include "../doomtype.h"
+
+TEST_CASE("C++ bool is convertible to doomtype.h boolean") {
+	REQUIRE(static_cast<boolean>(true) == 1);
+	REQUIRE(static_cast<boolean>(false) == 0);
+}
diff --git a/src/tmap.nas b/src/tmap.nas
deleted file mode 100644
index 85091cbd5d8dd9b1ab33cdd4938325eefeb1922b..0000000000000000000000000000000000000000
--- a/src/tmap.nas
+++ /dev/null
@@ -1,957 +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:
-;;      tmap.nas
-;; DESCRIPTION:
-;;      Assembler optimised rendering code for software mode.
-;;      Draw wall columns.
-
-
-[BITS 32]
-
-%define FRACBITS 16
-%define TRANSPARENTPIXEL 255
-
-%ifdef LINUX
-%macro cextern 1
-[extern %1]
-%endmacro
-
-%macro cglobal 1
-[global %1]
-%endmacro
-
-%else
-%macro cextern 1
-%define %1 _%1
-[extern %1]
-%endmacro
-
-%macro cglobal 1
-%define %1 _%1
-[global %1]
-%endmacro
-
-%endif
-
-
-; The viddef_s structure. We only need the width field.
-struc viddef_s
-        resb 12
-.width: resb 4
-        resb 44
-endstruc
-
-;; externs
-;; columns
-cextern dc_x
-cextern dc_yl
-cextern dc_yh
-cextern ylookup
-cextern columnofs
-cextern dc_source
-cextern dc_texturemid
-cextern dc_texheight
-cextern dc_iscale
-cextern dc_hires
-cextern centery
-cextern centeryfrac
-cextern dc_colormap
-cextern dc_transmap
-cextern colormaps
-cextern vid
-cextern topleft
-
-; DELME
-cextern R_DrawColumn_8
-
-; polygon edge rasterizer
-cextern prastertab
-
-[SECTION .data]
-
-;;.align        4
-loopcount       dd      0
-pixelcount      dd      0
-tystep          dd      0
-
-[SECTION .text]
-
-;;----------------------------------------------------------------------
-;;
-;; R_DrawColumn : 8bpp column drawer
-;;
-;; New  optimised version 10-01-1998 by D.Fabrice and P.Boris
-;; Revised by G. Dick July 2010 to support the intervening twelve years'
-;; worth of changes to the renderer. Since I only vaguely know what I'm
-;; doing, this is probably rather suboptimal. Help appreciated!
-;;
-;;----------------------------------------------------------------------
-;; fracstep, vid.width in memory
-;; eax = accumulator
-;; ebx = colormap
-;; ecx = count
-;; edx = heightmask
-;; esi = source
-;; edi = dest
-;; ebp = frac
-;;----------------------------------------------------------------------
-
-cglobal R_DrawColumn_8_ASM
-;       align   16
-R_DrawColumn_8_ASM:
-        push    ebp                     ;; preserve caller's stack frame pointer
-        push    esi                     ;; preserve register variables
-        push    edi
-        push    ebx
-;;
-;; dest = ylookup[dc_yl] + columnofs[dc_x];
-;;
-        mov     ebp,[dc_yl]
-        mov     edi,[ylookup+ebp*4]
-        mov     ebx,[dc_x]
-        add     edi,[columnofs+ebx*4]  ;; edi = dest
-;;
-;; pixelcount = yh - yl + 1
-;;
-        mov     ecx,[dc_yh]
-        add     ecx,1
-        sub     ecx,ebp                 ;; pixel count
-        jle     near .done              ;; nothing to scale
-;;
-;; fracstep = dc_iscale;	// But we just use [dc_iscale]
-;; frac = (dc_texturemid + FixedMul((dc_yl << FRACBITS) - centeryfrac, fracstep));
-;;
-        mov     eax,ebp                 ;; dc_yl
-        shl     eax,FRACBITS
-        sub     eax,[centeryfrac]
-        imul    dword [dc_iscale]
-        shrd    eax,edx,FRACBITS
-        add     eax,[dc_texturemid]
-        mov     ebp,eax                 ;; ebp = frac
-
-        mov     ebx,[dc_colormap]
-
-        mov     esi,[dc_source]
-;;
-;; if (dc_hires) frac = 0;
-;;
-        test    byte [dc_hires],0x01
-        jz      .texheightcheck
-        xor     ebp,ebp
-
-;;
-;; Check for power of two
-;;
-.texheightcheck:
-        mov     edx,[dc_texheight]
-        sub     edx,1                   ;; edx = heightmask
-        test    edx,[dc_texheight]
-        jnz     .notpowertwo
-
-        test    ecx,0x01                ;; Test for odd no. pixels
-        jnz     .odd
-
-;;
-;; Texture height is a power of two, so we get modular arithmetic by
-;; masking
-;;
-.powertwo:
-        mov     eax,ebp                 ;; eax = frac
-        sar     eax,FRACBITS            ;; Integer part
-        and     eax,edx                 ;; eax &= heightmask
-        movzx   eax,byte [esi + eax]    ;; eax = texel
-        add     ebp,[dc_iscale]         ;; frac += fracstep
-        movzx   eax,byte [ebx+eax]      ;; Map through colormap
-        mov     [edi],al                ;; Write pixel
-                                        ;; dest += vid.width
-        add     edi,[vid + viddef_s.width]
-
-.odd:
-        mov     eax,ebp                 ;; eax = frac
-        sar     eax,FRACBITS            ;; Integer part
-        and     eax,edx                 ;; eax &= heightmask
-        movzx   eax,byte [esi + eax]    ;; eax = texel
-        add     ebp,[dc_iscale]         ;; frac += fracstep
-        movzx   eax,byte [ebx+eax]      ;; Map through colormap
-        mov     [edi],al                ;; Write pixel
-                                        ;; dest += vid.width
-        add     edi,[vid + viddef_s.width]
-
-
-        sub     ecx,2                   ;; count -= 2
-        jg      .powertwo
-
-        jmp     .done
-
-.notpowertwo:
-        add     edx,1
-        shl     edx,FRACBITS
-        test    ebp,ebp
-        jns     .notpowtwoloop
-
-.makefracpos:
-        add     ebp,edx                 ;; frac is negative; make it positive
-        js      .makefracpos
-
-.notpowtwoloop:
-        cmp     ebp,edx                 ;; Reduce mod height
-        jl      .writenonpowtwo
-        sub     ebp,edx
-        jmp     .notpowtwoloop
-
-.writenonpowtwo:
-        mov     eax,ebp                 ;; eax = frac
-        sar     eax,FRACBITS            ;; Integer part.
-        mov     bl,[esi + eax]          ;; ebx = colormap + texel
-        add     ebp,[dc_iscale]         ;; frac += fracstep
-        movzx   eax,byte [ebx]          ;; Map through colormap
-        mov     [edi],al                ;; Write pixel
-                                        ;; dest += vid.width
-        add     edi,[vid + viddef_s.width]
-
-        sub     ecx,1
-        jnz     .notpowtwoloop
-
-;;
-
-.done:
-        pop     ebx                     ;; restore register variables
-        pop     edi
-        pop     esi
-        pop     ebp                     ;; restore caller's stack frame pointer
-        ret
-
-
-;;----------------------------------------------------------------------
-;;
-;; R_Draw2sMultiPatchColumn : Like R_DrawColumn, but omits transparent
-;;                            pixels.
-;;
-;; New  optimised version 10-01-1998 by D.Fabrice and P.Boris
-;; Revised by G. Dick July 2010 to support the intervening twelve years'
-;; worth of changes to the renderer. Since I only vaguely know what I'm
-;; doing, this is probably rather suboptimal. Help appreciated!
-;;
-;;----------------------------------------------------------------------
-;; fracstep, vid.width in memory
-;; eax = accumulator
-;; ebx = colormap
-;; ecx = count
-;; edx = heightmask
-;; esi = source
-;; edi = dest
-;; ebp = frac
-;;----------------------------------------------------------------------
-
-cglobal R_Draw2sMultiPatchColumn_8_ASM
-;       align   16
-R_Draw2sMultiPatchColumn_8_ASM:
-        push    ebp                     ;; preserve caller's stack frame pointer
-        push    esi                     ;; preserve register variables
-        push    edi
-        push    ebx
-;;
-;; dest = ylookup[dc_yl] + columnofs[dc_x];
-;;
-        mov     ebp,[dc_yl]
-        mov     edi,[ylookup+ebp*4]
-        mov     ebx,[dc_x]
-        add     edi,[columnofs+ebx*4]  ;; edi = dest
-;;
-;; pixelcount = yh - yl + 1
-;;
-        mov     ecx,[dc_yh]
-        add     ecx,1
-        sub     ecx,ebp                 ;; pixel count
-        jle     near .done              ;; nothing to scale
-;;
-;; fracstep = dc_iscale;	// But we just use [dc_iscale]
-;; frac = (dc_texturemid + FixedMul((dc_yl << FRACBITS) - centeryfrac, fracstep));
-;;
-        mov     eax,ebp                 ;; dc_yl
-        shl     eax,FRACBITS
-        sub     eax,[centeryfrac]
-        imul    dword [dc_iscale]
-        shrd    eax,edx,FRACBITS
-        add     eax,[dc_texturemid]
-        mov     ebp,eax                 ;; ebp = frac
-
-        mov     ebx,[dc_colormap]
-
-        mov     esi,[dc_source]
-;;
-;; if (dc_hires) frac = 0;
-;;
-        test    byte [dc_hires],0x01
-        jz      .texheightcheck
-        xor     ebp,ebp
-
-;;
-;; Check for power of two
-;;
-.texheightcheck:
-        mov     edx,[dc_texheight]
-        sub     edx,1                   ;; edx = heightmask
-        test    edx,[dc_texheight]
-        jnz     .notpowertwo
-
-        test    ecx,0x01                ;; Test for odd no. pixels
-        jnz     .odd
-
-;;
-;; Texture height is a power of two, so we get modular arithmetic by
-;; masking
-;;
-.powertwo:
-        mov     eax,ebp                 ;; eax = frac
-        sar     eax,FRACBITS            ;; Integer part
-        and     eax,edx                 ;; eax &= heightmask
-        movzx   eax,byte [esi + eax]    ;; eax = texel
-        add     ebp,[dc_iscale]         ;; frac += fracstep
-        cmp     al,TRANSPARENTPIXEL     ;; Is pixel transparent?
-        je      .nextpowtwoeven         ;; If so, advance.
-        movzx   eax,byte [ebx+eax]      ;; Map through colormap
-        mov	    [edi],al                ;; Write pixel
-.nextpowtwoeven:
-                                        ;; dest += vid.width
-        add     edi,[vid + viddef_s.width]
-
-.odd:
-        mov     eax,ebp                 ;; eax = frac
-        sar     eax,FRACBITS            ;; Integer part
-        and     eax,edx                 ;; eax &= heightmask
-        movzx   eax,byte [esi + eax]    ;; eax = texel
-        add     ebp,[dc_iscale]         ;; frac += fracstep
-        cmp     al,TRANSPARENTPIXEL     ;; Is pixel transparent?
-        je      .nextpowtwoodd          ;; If so, advance.
-        movzx   eax,byte [ebx+eax]      ;; Map through colormap
-        mov     [edi],al                ;; Write pixel
-.nextpowtwoodd:
-                                        ;; dest += vid.width
-        add     edi,[vid + viddef_s.width]
-
-
-        sub     ecx,2                   ;; count -= 2
-        jg      .powertwo
-
-        jmp     .done
-
-.notpowertwo:
-        add     edx,1
-        shl     edx,FRACBITS
-        test    ebp,ebp
-        jns     .notpowtwoloop
-
-.makefracpos:
-        add     ebp,edx                 ;; frac is negative; make it positive
-        js      .makefracpos
-
-.notpowtwoloop:
-        cmp     ebp,edx                 ;; Reduce mod height
-        jl      .writenonpowtwo
-        sub     ebp,edx
-        jmp     .notpowtwoloop
-
-.writenonpowtwo:
-        mov     eax,ebp                 ;; eax = frac
-        sar     eax,FRACBITS            ;; Integer part.
-        mov     bl,[esi + eax]          ;; ebx = colormap + texel
-        add     ebp,[dc_iscale]         ;; frac += fracstep
-        cmp     bl,TRANSPARENTPIXEL     ;; Is pixel transparent?
-        je      .nextnonpowtwo          ;; If so, advance.
-        movzx   eax,byte [ebx]          ;; Map through colormap
-        mov     [edi],al                ;; Write pixel
-.nextnonpowtwo:
-                                        ;; dest += vid.width
-        add     edi,[vid + viddef_s.width]
-
-        sub     ecx,1
-        jnz     .notpowtwoloop
-
-;;
-
-.done:
-        pop     ebx                     ;; restore register variables
-        pop     edi
-        pop     esi
-        pop     ebp                     ;; restore caller's stack frame pointer
-        ret
-
-;;----------------------------------------------------------------------
-;; R_DrawTranslucentColumnA_8
-;;
-;; Vertical column texture drawer, with transparency. Replaces Doom2's
-;; 'fuzz' effect, which was not so beautiful.
-;; Transparency is always impressive in some way, don't know why...
-;;----------------------------------------------------------------------
-
-cglobal R_DrawTranslucentColumn_8_ASM
-R_DrawTranslucentColumn_8_ASM:
-        push    ebp                     ;; preserve caller's stack frame pointer
-        push    esi                     ;; preserve register variables
-        push    edi
-        push    ebx
-;;
-;; dest = ylookup[dc_yl] + columnofs[dc_x];
-;;
-        mov     ebp,[dc_yl]
-        mov     ebx,ebp
-        mov     edi,[ylookup+ebx*4]
-        mov     ebx,[dc_x]
-        add     edi,[columnofs+ebx*4]   ;; edi = dest
-;;
-;; pixelcount = yh - yl + 1
-;;
-        mov     eax,[dc_yh]
-        inc     eax
-        sub     eax,ebp                 ;; pixel count
-        mov     [pixelcount],eax        ;; save for final pixel
-        jle     near    vtdone         ;; nothing to scale
-;;
-;; frac = dc_texturemid - (centery-dc_yl)*fracstep;
-;;
-        mov     ecx,[dc_iscale]        ;; fracstep
-        mov     eax,[centery]
-        sub     eax,ebp
-        imul    eax,ecx
-        mov     edx,[dc_texturemid]
-        sub     edx,eax
-        mov     ebx,edx
-
-        shr     ebx,16                  ;; frac int.
-        and     ebx,0x7f
-        shl     edx,16                  ;; y frac up
-
-        mov     ebp,ecx
-        shl     ebp,16                  ;; fracstep f. up
-        shr     ecx,16                  ;; fracstep i. ->cl
-        and     cl,0x7f
-        push    cx
-        mov     ecx,edx
-        pop     cx
-        mov     edx,[dc_colormap]
-        mov     esi,[dc_source]
-;;
-;; lets rock :) !
-;;
-        mov     eax,[pixelcount]
-        shr     eax,0x2
-        test    byte [pixelcount],0x3
-        mov     ch,al                   ;; quad count
-        mov     eax,[dc_transmap]
-        je      vt4quadloop
-;;
-;;  do un-even pixel
-;;
-        test    byte [pixelcount],0x1
-        je      trf2
-
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        add     ecx,ebp
-        adc     bl,cl
-        mov     al,[edi]                ;; fetch dest  : index into colormap
-        and     bl,0x7f
-        mov     dl,[eax]
-        mov     dl,[edx]
-        mov     [edi],dl
-pf:     add     edi,0x12345678
-;;
-;;  do two non-quad-aligned pixels
-;;
-trf2:    test    byte [pixelcount],0x2
-        je      trf3
-
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        add     ecx,ebp
-        adc     bl,cl
-        mov     al,[edi]                ;; fetch dest  : index into colormap
-        and     bl,0x7f
-        mov     dl,[eax]
-        mov     dl,[edx]
-        mov     [edi],dl
-pg:     add     edi,0x12345678
-
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        add     ecx,ebp
-        adc     bl,cl
-        mov     al,[edi]                ;; fetch dest  : index into colormap
-        and     bl,0x7f
-        mov     dl,[eax]
-        mov     dl,[edx]
-        mov     [edi],dl
-ph:     add     edi,0x12345678
-;;
-;;  test if there was at least 4 pixels
-;;
-trf3:   test    ch,0xff                 ;; test quad count
-        je near vtdone
-
-;;
-;; ebp : ystep frac. upper 24 bits
-;; edx : y     frac. upper 24 bits
-;; ebx : y     i.    lower 7 bits,  masked for index
-;; ecx : ch = counter, cl = y step i.
-;; eax : colormap aligned 256
-;; esi : source texture column
-;; edi : dest screen
-;;
-vt4quadloop:
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        mov     [tystep],ebp
-pi:     add     edi,0x12345678
-        mov     al,[edi]                ;; fetch dest  : index into colormap
-pj:     sub     edi,0x12345678
-        mov     ebp,edi
-pk:     sub     edi,0x12345678
-        jmp short inloop
-align 4
-vtquadloop:
-        add     ecx,[tystep]
-        adc     bl,cl
-q1:     add     ebp,0x23456789
-        and     bl,0x7f
-        mov     dl,[eax]
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        mov     dl,[edx]
-        mov     [edi],dl
-        mov     al,[ebp]                ;; fetch dest   : index into colormap
-inloop:
-        add     ecx,[tystep]
-        adc     bl,cl
-q2:     add     edi,0x23456789
-        and     bl,0x7f
-        mov     dl,[eax]
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        mov     dl,[edx]
-        mov     [ebp+0x0],dl
-        mov     al,[edi]                ;; fetch dest   : index into colormap
-
-        add     ecx,[tystep]
-        adc     bl,cl
-q3:     add     ebp,0x23456789
-        and     bl,0x7f
-        mov     dl,[eax]
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        mov     dl,[edx]
-        mov     [edi],dl
-        mov     al,[ebp]                ;; fetch dest   : index into colormap
-
-        add     ecx,[tystep]
-        adc     bl,cl
-q4:     add     edi,0x23456789
-        and     bl,0x7f
-        mov     dl,[eax]
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        mov     dl,[edx]
-        mov     [ebp],dl
-        mov     al,[edi]                ;; fetch dest   : index into colormap
-
-        dec     ch
-        jne     vtquadloop
-vtdone:
-        pop     ebx
-        pop     edi
-        pop     esi
-        pop     ebp
-        ret
-
-;;----------------------------------------------------------------------
-;; R_DrawShadeColumn
-;;
-;;   for smoke..etc.. test.
-;;----------------------------------------------------------------------
-cglobal R_DrawShadeColumn_8_ASM
-R_DrawShadeColumn_8_ASM:
-        push    ebp                     ;; preserve caller's stack frame pointer
-        push    esi                     ;; preserve register variables
-        push    edi
-        push    ebx
-
-;;
-;; dest = ylookup[dc_yl] + columnofs[dc_x];
-;;
-        mov     ebp,[dc_yl]
-        mov     ebx,ebp
-        mov     edi,[ylookup+ebx*4]
-        mov     ebx,[dc_x]
-        add     edi,[columnofs+ebx*4]  ;; edi = dest
-;;
-;; pixelcount = yh - yl + 1
-;;
-        mov     eax,[dc_yh]
-        inc     eax
-        sub     eax,ebp                 ;; pixel count
-        mov     [pixelcount],eax       ;; save for final pixel
-        jle near shdone                ;; nothing to scale
-;;
-;; frac = dc_texturemid - (centery-dc_yl)*fracstep;
-;;
-        mov     ecx,[dc_iscale]        ;; fracstep
-        mov     eax,[centery]
-        sub     eax,ebp
-        imul    eax,ecx
-        mov     edx,[dc_texturemid]
-        sub     edx,eax
-        mov     ebx,edx
-        shr     ebx,16                  ;; frac int.
-        and     ebx,byte +0x7f
-        shl     edx,16                  ;; y frac up
-
-        mov     ebp,ecx
-        shl     ebp,16                  ;; fracstep f. up
-        shr     ecx,16                  ;; fracstep i. ->cl
-        and     cl,0x7f
-
-        mov     esi,[dc_source]
-;;
-;; lets rock :) !
-;;
-        mov     eax,[pixelcount]
-        mov     dh,al
-        shr     eax,2
-        mov     ch,al                   ;; quad count
-        mov     eax,[colormaps]
-        test    dh,3
-        je      sh4quadloop
-;;
-;;  do un-even pixel
-;;
-        test    dh,0x1
-        je      shf2
-
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        add     edx,ebp
-        adc     bl,cl
-        mov     al,[edi]                ;; fetch dest  : index into colormap
-        and     bl,0x7f
-        mov     dl,[eax]
-        mov     [edi],dl
-pl:     add     edi,0x12345678
-;;
-;;  do two non-quad-aligned pixels
-;;
-shf2:
-        test    dh,0x2
-        je      shf3
-
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        add     edx,ebp
-        adc     bl,cl
-        mov     al,[edi]                ;; fetch dest  : index into colormap
-        and     bl,0x7f
-        mov     dl,[eax]
-        mov     [edi],dl
-pm:     add     edi,0x12345678
-
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        add     edx,ebp
-        adc     bl,cl
-        mov     al,[edi]                ;; fetch dest  : index into colormap
-        and     bl,0x7f
-        mov     dl,[eax]
-        mov     [edi],dl
-pn:     add     edi,0x12345678
-;;
-;;  test if there was at least 4 pixels
-;;
-shf3:
-        test    ch,0xff                 ;; test quad count
-        je near shdone
-
-;;
-;; ebp : ystep frac. upper 24 bits
-;; edx : y     frac. upper 24 bits
-;; ebx : y     i.    lower 7 bits,  masked for index
-;; ecx : ch = counter, cl = y step i.
-;; eax : colormap aligned 256
-;; esi : source texture column
-;; edi : dest screen
-;;
-sh4quadloop:
-        mov     dh,0x7f                 ;; prep mask
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        mov     [tystep],ebp
-po:     add     edi,0x12345678
-        mov     al,[edi]                ;; fetch dest  : index into colormap
-pp:     sub     edi,0x12345678
-        mov     ebp,edi
-pq:     sub     edi,0x12345678
-        jmp short shinloop
-
-align  4
-shquadloop:
-        add     edx,[tystep]
-        adc     bl,cl
-        and     bl,dh
-q5:     add     ebp,0x12345678
-        mov     dl,[eax]
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        mov     [edi],dl
-        mov     al,[ebp]                ;; fetch dest : index into colormap
-shinloop:
-        add     edx,[tystep]
-        adc     bl,cl
-        and     bl,dh
-q6:     add     edi,0x12345678
-        mov     dl,[eax]
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        mov     [ebp],dl
-        mov     al,[edi]                ;; fetch dest : index into colormap
-
-        add     edx,[tystep]
-        adc     bl,cl
-        and     bl,dh
-q7:     add     ebp,0x12345678
-        mov     dl,[eax]
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        mov     [edi],dl
-        mov     al,[ebp]                ;; fetch dest : index into colormap
-
-        add     edx,[tystep]
-        adc     bl,cl
-        and     bl,dh
-q8:     add     edi,0x12345678
-        mov     dl,[eax]
-        mov     ah,[esi+ebx]            ;; fetch texel : colormap number
-        mov     [ebp],dl
-        mov     al,[edi]                ;; fetch dest : index into colormap
-
-        dec     ch
-        jne     shquadloop
-
-shdone:
-        pop     ebx                     ;; restore register variables
-        pop     edi
-        pop     esi
-        pop     ebp                     ;; restore caller's stack frame pointer
-        ret
-
-
-;; ========================================================================
-;;  Rasterization of the segments of a LINEAR polygne textur of manire.
-;;  It is thus a question of interpolating coordinate them at the edges of texture in
-;;  the time that the X-coordinates minx/maxx for each line.
-;;  the argument ' dir' indicates which edges of texture are Interpol?:
-;;    0:  segments associs at edge TOP? and BOTTOM? (constant TY)
-;;    1:  segments associs at the LEFT and RIGHT edge (constant TX)
-;; ========================================================================
-;;
-;;  void   rasterize_segment_tex( LONG x1, LONG y1, LONG x2, LONG y2, LONG tv1, LONG tv2, LONG tc, LONG dir );
-;;                                   ARG1     ARG2     ARG3     ARG4      ARG5      ARG6     ARG7       ARG8
-;;
-;;  Pour dir = 0, (tv1,tv2) = (tX1,tX2), tc = tY, en effet TY est constant.
-;;
-;;  Pour dir = 1, (tv1,tv2) = (tY1,tY2), tc = tX, en effet TX est constant.
-;;
-;;
-;;  Uses:  extern struct rastery *_rastertab;
-;;
-
-MINX            EQU    0
-MAXX            EQU    4
-TX1             EQU    8
-TY1             EQU    12
-TX2             EQU    16
-TY2             EQU    20
-RASTERY_SIZEOF  EQU    24
-
-cglobal rasterize_segment_tex_asm
-rasterize_segment_tex_asm:
-        push    ebp
-        mov     ebp,esp
-
-        sub     esp,byte +0x8           ;; allocate the local variables
-
-        push    ebx
-        push    esi
-        push    edi
-        o16 mov ax,es
-        push    eax
-
-;;        #define DX       [ebp-4]
-;;        #define TD       [ebp-8]
-
-        mov     eax,[ebp+0xc]           ;; y1
-        mov     ebx,[ebp+0x14]          ;; y2
-        cmp     ebx,eax
-        je near .L_finished             ;; special (y1==y2) segment horizontal, exit!
-
-        jg near .L_rasterize_right
-
-;;rasterize_left:       ;; one rasterize a segment LEFT of the polygne
-
-        mov     ecx,eax
-        sub     ecx,ebx
-        inc     ecx                     ;; y1-y2+1
-
-        mov     eax,RASTERY_SIZEOF
-        mul     ebx                     ;; * y2
-        mov     esi,[prastertab]
-        add     esi,eax                 ;; point into rastertab[y2]
-
-        mov     eax,[ebp+0x8]           ;; ARG1
-        sub     eax,[ebp+0x10]          ;; ARG3
-        shl     eax,0x10                ;;     ((x1-x2)<<PRE) ...
-        cdq
-        idiv    ecx                     ;; dx =     ...        / (y1-y2+1)
-        mov     [ebp-0x4],eax           ;; DX
-
-        mov     eax,[ebp+0x18]          ;; ARG5
-        sub     eax,[ebp+0x1c]          ;; ARG6
-        shl     eax,0x10
-        cdq
-        idiv    ecx                     ;;      tdx =((tx1-tx2)<<PRE) / (y1-y2+1)
-        mov     [ebp-0x8],eax           ;; idem tdy =((ty1-ty2)<<PRE) / (y1-y2+1)
-
-        mov     eax,[ebp+0x10]          ;; ARG3
-        shl     eax,0x10                ;; x = x2<<PRE
-
-        mov     ebx,[ebp+0x1c]          ;; ARG6
-        shl     ebx,0x10                ;; tx = tx2<<PRE    d0
-                                        ;; ty = ty2<<PRE    d1
-        mov     edx,[ebp+0x20]          ;; ARG7
-        shl     edx,0x10                ;; ty = ty<<PRE     d0
-                                        ;; tx = tx<<PRE     d1
-        push    ebp
-        mov     edi,[ebp-0x4]           ;; DX
-        cmp     dword [ebp+0x24],byte +0x0      ;; ARG8   direction ?
-
-        mov     ebp,[ebp-0x8]           ;; TD
-        je      .L_rleft_h_loop
-;;
-;; TY varies, TX is constant
-;;
-.L_rleft_v_loop:
-        mov     [esi+MINX],eax           ;; rastertab[y].minx = x
-          add     ebx,ebp
-        mov     [esi+TX1],edx           ;;             .tx1  = tx
-          add     eax,edi
-        mov     [esi+TY1],ebx           ;;             .ty1  = ty
-
-        ;;addl    DX, %eax        // x     += dx
-        ;;addl    TD, %ebx        // ty    += tdy
-
-        add     esi,RASTERY_SIZEOF      ;; next raster line into rastertab[]
-        dec     ecx
-        jne     .L_rleft_v_loop
-        pop     ebp
-        jmp     .L_finished
-;;
-;; TX varies, TY is constant
-;;
-.L_rleft_h_loop:
-        mov     [esi+MINX],eax           ;; rastertab[y].minx = x
-          add     eax,edi
-        mov     [esi+TX1],ebx           ;;             .tx1  = tx
-          add     ebx,ebp
-        mov     [esi+TY1],edx           ;;             .ty1  = ty
-
-        ;;addl    DX, %eax        // x     += dx
-        ;;addl    TD, %ebx        // tx    += tdx
-
-        add     esi,RASTERY_SIZEOF      ;; next raster line into rastertab[]
-        dec     ecx
-        jne     .L_rleft_h_loop
-        pop     ebp
-        jmp     .L_finished
-;;
-;; one rasterize a segment LINE of the polygne
-;;
-.L_rasterize_right:
-        mov     ecx,ebx
-        sub     ecx,eax
-        inc     ecx                     ;; y2-y1+1
-
-        mov     ebx,RASTERY_SIZEOF
-        mul     ebx                     ;;   * y1
-        mov     esi,[prastertab]
-        add     esi,eax                 ;;  point into rastertab[y1]
-
-        mov     eax,[ebp+0x10]          ;; ARG3
-        sub     eax,[ebp+0x8]           ;; ARG1
-        shl     eax,0x10                ;; ((x2-x1)<<PRE) ...
-        cdq
-        idiv    ecx                     ;;  dx =     ...        / (y2-y1+1)
-        mov     [ebp-0x4],eax           ;; DX
-
-        mov     eax,[ebp+0x1c]          ;; ARG6
-        sub     eax,[ebp+0x18]          ;; ARG5
-        shl     eax,0x10
-        cdq
-        idiv    ecx                     ;;       tdx =((tx2-tx1)<<PRE) / (y2-y1+1)
-        mov     [ebp-0x8],eax           ;;  idem tdy =((ty2-ty1)<<PRE) / (y2-y1+1)
-
-        mov     eax,[ebp+0x8]           ;; ARG1
-        shl     eax,0x10                ;; x  = x1<<PRE
-
-        mov     ebx,[ebp+0x18]          ;; ARG5
-        shl     ebx,0x10                ;; tx = tx1<<PRE    d0
-                                        ;; ty = ty1<<PRE    d1
-        mov     edx,[ebp+0x20]          ;; ARG7
-        shl     edx,0x10                ;; ty = ty<<PRE     d0
-                                        ;; tx = tx<<PRE     d1
-        push    ebp
-        mov     edi,[ebp-0x4]           ;; DX
-
-        cmp     dword [ebp+0x24], 0     ;; direction ?
-
-         mov     ebp,[ebp-0x8]          ;; TD
-        je      .L_rright_h_loop
-;;
-;; TY varies, TX is constant
-;;
-.L_rright_v_loop:
-
-        mov     [esi+MAXX],eax           ;; rastertab[y].maxx = x
-          add     ebx,ebp
-        mov     [esi+TX2],edx          ;;             .tx2  = tx
-          add     eax,edi
-        mov     [esi+TY2],ebx          ;;             .ty2  = ty
-
-        ;;addl    DX, %eax        // x     += dx
-        ;;addl    TD, %ebx        // ty    += tdy
-
-        add     esi,RASTERY_SIZEOF
-        dec     ecx
-        jne     .L_rright_v_loop
-
-        pop     ebp
-
-        jmp     short .L_finished
-;;
-;; TX varies, TY is constant
-;;
-.L_rright_h_loop:
-        mov     [esi+MAXX],eax           ;; rastertab[y].maxx = x
-          add     eax,edi
-        mov     [esi+TX2],ebx          ;;             .tx2  = tx
-          add     ebx,ebp
-        mov     [esi+TY2],edx          ;;             .ty2  = ty
-
-        ;;addl    DX, %eax        // x     += dx
-        ;;addl    TD, %ebx        // tx    += tdx
-
-        add     esi,RASTERY_SIZEOF
-        dec     ecx
-        jne     .L_rright_h_loop
-
-        pop     ebp
-
-.L_finished:
-        pop     eax
-        o16 mov es,ax
-        pop     edi
-        pop     esi
-        pop     ebx
-
-        mov     esp,ebp
-        pop     ebp
-        ret
diff --git a/src/tmap.s b/src/tmap.s
deleted file mode 100644
index d98d82e25cedbea383b71beb122e7f250e12d765..0000000000000000000000000000000000000000
--- a/src/tmap.s
+++ /dev/null
@@ -1,1587 +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  tmap.s
-/// \brief optimised drawing routines for span/column rendering
-
-// structures, must match the C structures!
-#include "asm_defs.inc"
-
-// Rappel: seuls EAX, ECX, EDX peuvent �tre �cras�s librement.
-//         il faut sauver esi,edi, cd...gs
-
-/* Attention aux comparaisons!                                              */
-/*                                                                          */
-/*      Intel_compare:                                                      */
-/*                                                                          */
-/*              cmp     A,B                     // A-B , set flags          */
-/*              jg      A_greater_than_B                                    */
-/*                                                                          */
-/*      AT&T_compare:                                                       */
-/*                                                                          */
-/*              cmp     A,B                     // B-A , set flags          */
-/*              jg      B_greater_than_A                                    */
-/*                                                                          */
-/*        (soustrait l'op�rande source DE l'op�rande destination,           */
-/*         comme sur Motorola! )                                            */
-
-// RAPPEL: Intel
-//         SECTION:[BASE+INDEX*SCALE+DISP]
-// devient SECTION:DISP(BASE,INDEX,SCALE)
-
-//----------------------------------------------------------------------
-//
-// R_DrawColumn
-//
-//   New optimised version 10-01-1998 by D.Fabrice and P.Boris
-//   TO DO: optimise it much farther... should take at most 3 cycles/pix
-//          once it's fixed, add code to patch the offsets so that it
-//          works in every screen width.
-//
-//----------------------------------------------------------------------
-
-    .data
-#ifdef LINUX
-    .align 2
-#else
-    .align 4
-#endif
-C(loopcount):   .long   0
-C(pixelcount):  .long   0
-C(tystep):      .long   0
-
-C(vidwidth):    .long   0       //use this one out of the inner loops
-                                //so you don't need to patch everywhere...
-
-#ifdef USEASM
-#if !defined( LINUX)
-    .text
-#endif
-.globl C(ASM_PatchRowBytes)
-C(ASM_PatchRowBytes):
-    pushl   %ebp
-    movl    %esp, %ebp      // assure l'"adressabilit� du stack"
-
-    movl    ARG1, %edx         // read first arg
-    movl    %edx, C(vidwidth)
-
-    // 1 * vidwidth
-    movl    %edx,p1+2
-    movl    %edx,w1+2   //water
-    movl    %edx,p1b+2  //sky
-
-    movl    %edx,p5+2
-      movl    %edx,sh5+2        //smokie test
-
-    // 2 * vidwidth
-    addl    ARG1,%edx
-
-    movl    %edx,p2+2
-    movl    %edx,w2+2   //water
-    movl    %edx,p2b+2  //sky
-
-    movl    %edx,p6+2
-    movl    %edx,p7+2
-    movl    %edx,p8+2
-    movl    %edx,p9+2
-      movl    %edx,sh6+2         //smokie test
-      movl    %edx,sh7+2
-      movl    %edx,sh8+2
-      movl    %edx,sh9+2
-
-    // 3 * vidwidth
-    addl    ARG1,%edx
-
-    movl    %edx,p3+2
-    movl    %edx,w3+2   //water
-    movl    %edx,p3b+2  //sky
-
-    // 4 * vidwidth
-    addl    ARG1,%edx
-
-    movl    %edx,p4+2
-    movl    %edx,w4+2   //water
-    movl    %edx,p4b+2  //sky
-
-    popl    %ebp
-    ret
-
-
-#ifdef LINUX
-    .align 2
-#else
-    .align 5
-#endif
-.globl C(R_DrawColumn_8)
-C(R_DrawColumn_8):
-    pushl   %ebp                // preserve caller's stack frame pointer
-    pushl   %esi                // preserve register variables
-    pushl   %edi
-    pushl   %ebx
-
-//
-// dest = ylookup[dc_yl] + columnofs[dc_x];
-//
-    movl     C(dc_yl),%ebp
-    movl     %ebp,%ebx
-    movl     C(ylookup)(,%ebx,4),%edi
-    movl     C(dc_x),%ebx
-    addl     C(columnofs)(,%ebx,4),%edi  // edi = dest
-
-//
-// pixelcount = yh - yl + 1
-//
-    movl     C(dc_yh),%eax
-    incl     %eax
-    subl     %ebp,%eax                   // pixel count
-    movl     %eax,C(pixelcount)          // save for final pixel
-    jle      vdone                       // nothing to scale
-
-//
-// frac = dc_texturemid - (centery-dc_yl)*fracstep;
-//
-    movl     C(dc_iscale),%ecx           // fracstep
-    movl     C(centery),%eax
-    subl     %ebp,%eax
-    imul     %ecx,%eax
-    movl     C(dc_texturemid),%edx
-    subl     %eax,%edx
-     movl     %edx,%ebx
-     shrl     $16,%ebx          // frac int.
-     andl     $0x0000007f,%ebx
-     shll     $16,%edx          // y frac up
-
-     movl     %ecx,%ebp
-     shll     $16,%ebp          // fracstep f. up
-     shrl     $16,%ecx          // fracstep i. ->cl
-     andb     $0x7f,%cl
-
-    movl     C(dc_source),%esi
-
-//
-// lets rock :) !
-//
-    movl    C(pixelcount),%eax
-    movb    %al,%dh
-    shrl    $2,%eax
-    movb    %al,%ch             // quad count
-    movl    C(dc_colormap),%eax
-    testb   $3,%dh
-    jz      v4quadloop
-
-//
-//  do un-even pixel
-//
-    testb   $1,%dh
-    jz      2f
-
-    movb    (%esi,%ebx),%al     // prep un-even loops
-     addl    %ebp,%edx            // ypos f += ystep f
-    adcb    %cl,%bl              // ypos i += ystep i
-     movb    (%eax),%dl           // colormap texel
-    andb    $0x7f,%bl            // mask 0-127 texture index
-     movb    %dl,(%edi)           // output pixel
-    addl    C(vidwidth),%edi
-
-//
-//  do two non-quad-aligned pixels
-//
-2:
-    testb   $2,%dh
-    jz      3f
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx            // ypos f += ystep f
-    adcb    %cl,%bl              // ypos i += ystep i
-     movb    (%eax),%dl           // colormap texel
-    andb    $0x7f,%bl            // mask 0-127 texture index
-     movb    %dl,(%edi)           // output pixel
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx            // ypos f += ystep f
-    adcb    %cl,%bl              // ypos i += ystep i
-     movb    (%eax),%dl           // colormap texel
-    andb    $0x7f,%bl            // mask 0-127 texture index
-    addl    C(vidwidth),%edi
-     movb    %dl,(%edi)           // output pixel
-
-    addl    C(vidwidth),%edi
-
-//
-//  test if there was at least 4 pixels
-//
-3:
-    testb   $0xFF,%ch           // test quad count
-    jz      vdone
-
-//
-// ebp : ystep frac. upper 24 bits
-// edx : y     frac. upper 24 bits
-// ebx : y     i.    lower 7 bits,  masked for index
-// ecx : ch = counter, cl = y step i.
-// eax : colormap aligned 256
-// esi : source texture column
-// edi : dest screen
-//
-v4quadloop:
-    movb    $0x7f,%dh           // prep mask
-//    .align  4
-vquadloop:
-    movb    (%esi,%ebx),%al     // prep loop
-     addl    %ebp,%edx            // ypos f += ystep f
-    adcb    %cl,%bl              // ypos i += ystep i
-     movb    (%eax),%dl           // colormap texel
-    movb    %dl,(%edi)           // output pixel
-     andb    $0x7f,%bl            // mask 0-127 texture index
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-     movb    (%eax),%dl
-p1:    movb    %dl,0x12345678(%edi)
-     andb    $0x7f,%bl
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-     movb    (%eax),%dl
-p2:    movb    %dl,2*0x12345678(%edi)
-     andb    $0x7f,%bl
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-     movb    (%eax),%dl
-p3:    movb    %dl,3*0x12345678(%edi)
-     andb    $0x7f,%bl
-
-p4:    addl    $4*0x12345678,%edi
-
-    decb   %ch
-     jnz    vquadloop
-
-vdone:
-    popl    %ebx                // restore register variables
-    popl    %edi
-    popl    %esi
-    popl    %ebp                // restore caller's stack frame pointer
-    ret
-
-#ifdef HORIZONTALDRAW
-// --------------------------------------------------------------------------
-// Horizontal Column Drawer Optimisation
-// --------------------------------------------------------------------------
-
-#ifdef LINUX
-    .align 2
-#else
-    .align 5
-#endif
-.globl C(R_DrawHColumn_8)
-C(R_DrawHColumn_8):
-    pushl   %ebp
-    pushl   %esi
-    pushl   %edi
-    pushl   %ebx
-
-//
-// dest = yhlookup[dc_x] + hcolumnofs[dc_yl];
-//
-    movl    C(dc_x),%ebx
-    movl    C(yhlookup)(,%ebx,4),%edi
-    movl    C(dc_yl),%ebp
-    movl    %ebp,%ebx
-    addl    C(hcolumnofs)(,%ebx,4),%edi  // edi = dest
-
-//
-// pixelcount = yh - yl + 1
-//
-    movl     C(dc_yh),%eax
-    incl     %eax
-    subl     %ebp,%eax                   // pixel count
-    movl     %eax,C(pixelcount)          // save for final pixel
-    jle      vhdone                      // nothing to scale
-
-//
-// frac = dc_texturemid - (centery-dc_yl)*fracstep;
-//
-    movl     C(dc_iscale),%ecx           // fracstep
-    movl     C(centery),%eax
-    subl     %ebp,%eax
-    imul     %ecx,%eax
-    movl     C(dc_texturemid),%edx
-    subl     %eax,%edx
-     movl     %edx,%ebx
-     shrl     $16,%ebx          // frac int.
-     andl     $0x0000007f,%ebx
-     shll     $16,%edx          // y frac up
-
-     movl     %ecx,%ebp
-     shll     $16,%ebp          // fracstep f. up
-     shrl     $16,%ecx          // fracstep i. ->cl
-     andb     $0x7f,%cl
-
-    movl     C(dc_source),%esi
-
-//
-// lets rock :) !
-//
-
-    movl    C(pixelcount),%eax
-    movb    %al,%dh
-    shrl    $2,%eax
-    movb    %al,%ch     // quad count
-
-    testb   %ch, %ch
-    jz      vhnearlydone
-
-    movl    C(dc_colormap),%eax
-    decl    %edi                  //-----
-
-vhloop:
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-     andb    $0x7f,%bl
-    incl    %edi                 //-----
-     movb    (%eax),%dh
-    movb    %dh,(%edi)           //-----
-
-     movb    (%esi,%ebx),%al      // fetch source texel
-    addl    %ebp,%edx
-     incl    %edi                //-----
-    adcb    %cl,%bl
-     movb    (%eax),%dl
-    andb    $0x7f,%bl
-     movb    %dl,(%edi)          //-----
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-//    shll    $16,%edx
-     andb    $0x7f,%bl
-    incl    %edi                //-----
-     movb    (%eax),%dh
-    movb    %dh,(%edi)          //-----
-
-     movb    (%esi,%ebx),%al      // fetch source texel
-    addl    %ebp,%edx
-     incl    %edi               //-----
-    adcb    %cl,%bl
-     movb    (%eax),%dl
-    andb    $0x7f,%bl
-     movb    %dl,(%edi)
-//     movl    %edx,(%edi)
-//    addl    $4,%edi
-
-    decb   %ch
-     jnz    vhloop
-
-vhnearlydone:
-//    movl    C(pixelcount)
-
-vhdone:
-    popl    %ebx
-    popl    %edi
-    popl    %esi
-    popl    %ebp
-    ret
-
-
-// --------------------------------------------------------------------------
-// Rotate a buffer 90 degree in clockwise order after horiz.col. draws
-// --------------------------------------------------------------------------
-
-#ifdef LINUX
-    .align 2
-#else
-    .align 5
-#endif
-.globl C(R_RotateBuffer)
-C(R_RotateBuffer):
-    pushl   %ebp
-    pushl   %esi
-    pushl   %edi
-    pushl   %ebx
-
-
-    movl    C(dc_source),%esi
-    movl    C(dc_colormap),%edi
-
-
-    movb    (%esi),%ah
-     addl    $200,%esi
-    movb    (%ebx),%al
-     addl    $200,%ebx
-    bswap    %eax
-    movb    (%esi),%ah
-     addl    $200,%esi
-    movb    (%ebx),%al
-     addl    $200,%ebx
-    movl    %eax,(%edi)
-     addl    $4,%edi
-
-
-    popl    %ebx
-    popl    %edi
-    popl    %esi
-    popl    %ebp
-    ret
-#endif
-
-//----------------------------------------------------------------------
-//13-02-98:
-//   R_DrawSkyColumn : same as R_DrawColumn but:
-//
-//            - wrap around 256 instead of 127.
-//   this is needed because we have a higher texture for mouselook,
-//   we need at least 200 lines for the sky.
-//
-//   NOTE: the sky should never wrap, so it could use a faster method.
-//         for the moment, we'll still use a wrapping method...
-//
-//  IT S JUST A QUICK CUT N PASTE, WAS NOT OPTIMISED AS IT SHOULD BE !!!
-//
-//----------------------------------------------------------------------
-
-#ifdef LINUX
-    .align 2
-#else
-    .align 5
-#endif
-.globl C(R_DrawSkyColumn_8)
-C(R_DrawSkyColumn_8):
-    pushl   %ebp
-    pushl   %esi
-    pushl   %edi
-    pushl   %ebx
-
-//
-// dest = ylookup[dc_yl] + columnofs[dc_x];
-//
-    movl     C(dc_yl),%ebp
-    movl     %ebp,%ebx
-    movl     C(ylookup)(,%ebx,4),%edi
-    movl     C(dc_x),%ebx
-    addl     C(columnofs)(,%ebx,4),%edi  // edi = dest
-
-//
-// pixelcount = yh - yl + 1
-//
-    movl     C(dc_yh),%eax
-    incl     %eax
-    subl     %ebp,%eax                   // pixel count
-    movl     %eax,C(pixelcount)          // save for final pixel
-    jle      vskydone                       // nothing to scale
-
-//
-// frac = dc_texturemid - (centery-dc_yl)*fracstep;
-//
-    movl     C(dc_iscale),%ecx           // fracstep
-    movl     C(centery),%eax
-    subl     %ebp,%eax
-    imul     %ecx,%eax
-    movl     C(dc_texturemid),%edx
-    subl     %eax,%edx
-     movl     %edx,%ebx
-     shrl     $16,%ebx          // frac int.
-     andl     $0x000000ff,%ebx
-     shll     $16,%edx          // y frac up
-
-     movl     %ecx,%ebp
-     shll     $16,%ebp          // fracstep f. up
-     shrl     $16,%ecx          // fracstep i. ->cl
-
-    movl     C(dc_source),%esi
-
-//
-// lets rock :) !
-//
-    movl    C(pixelcount),%eax
-    movb    %al,%dh
-    shrl    $2,%eax
-    movb    %al,%ch             // quad count
-    movl    C(dc_colormap),%eax
-    testb   $3,%dh
-    jz      v4skyquadloop
-
-//
-//  do un-even pixel
-//
-    testb   $1,%dh
-    jz      2f
-
-    movb    (%esi,%ebx),%al     // prep un-even loops
-     addl    %ebp,%edx            // ypos f += ystep f
-    adcb    %cl,%bl              // ypos i += ystep i
-     movb    (%eax),%dl           // colormap texel
-     movb    %dl,(%edi)           // output pixel
-    addl    C(vidwidth),%edi
-
-//
-//  do two non-quad-aligned pixels
-//
-2:
-    testb   $2,%dh
-    jz      3f
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx            // ypos f += ystep f
-    adcb    %cl,%bl              // ypos i += ystep i
-     movb    (%eax),%dl           // colormap texel
-     movb    %dl,(%edi)           // output pixel
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx            // ypos f += ystep f
-    adcb    %cl,%bl              // ypos i += ystep i
-     movb    (%eax),%dl           // colormap texel
-    addl    C(vidwidth),%edi
-     movb    %dl,(%edi)           // output pixel
-
-    addl    C(vidwidth),%edi
-
-//
-//  test if there was at least 4 pixels
-//
-3:
-    testb   $0xFF,%ch           // test quad count
-    jz      vskydone
-
-//
-// ebp : ystep frac. upper 24 bits
-// edx : y     frac. upper 24 bits
-// ebx : y     i.    lower 7 bits,  masked for index
-// ecx : ch = counter, cl = y step i.
-// eax : colormap aligned 256
-// esi : source texture column
-// edi : dest screen
-//
-v4skyquadloop:
-//    .align  4
-vskyquadloop:
-    movb    (%esi,%ebx),%al     // prep loop
-     addl    %ebp,%edx            // ypos f += ystep f
-    adcb    %cl,%bl              // ypos i += ystep i
-     movb    (%eax),%dl           // colormap texel
-    movb    %dl,(%edi)           // output pixel
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-     movb    (%eax),%dl
-p1b:    movb    %dl,0x12345678(%edi)
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-     movb    (%eax),%dl
-p2b:    movb    %dl,2*0x12345678(%edi)
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-     movb    (%eax),%dl
-p3b:    movb    %dl,3*0x12345678(%edi)
-
-p4b:    addl    $4*0x12345678,%edi
-
-    decb   %ch
-     jnz    vskyquadloop
-
-vskydone:
-    popl    %ebx                // restore register variables
-    popl    %edi
-    popl    %esi
-    popl    %ebp                // restore caller's stack frame pointer
-    ret
-
-
-
-//----------------------------------------------------------------------
-//
-// R_DrawSpan
-//
-// Horizontal texture mapping
-//
-//----------------------------------------------------------------------
-
-    .data
-
-ystep:          .long   0
-xstep:          .long   0
-C(texwidth):    .long   64      // texture width
-#if !defined( LINUX)
-    .text
-#endif
-#ifdef LINUX
-    .align 2
-#else
-    .align 4
-#endif
-.globl C(R_DrawSpan_8)
-C(R_DrawSpan_8):
-    pushl   %ebp                // preserve caller's stack frame pointer
-    pushl   %esi                // preserve register variables
-    pushl   %edi
-    pushl   %ebx
-
-
-//
-// find loop count
-//
-    movl    C(ds_x2),%eax
-    incl    %eax
-    subl    C(ds_x1),%eax               // pixel count
-    movl    %eax,C(pixelcount)          // save for final pixel
-    js      hdone                       // nothing to scale
-    shrl    $1,%eax                     // double pixel count
-    movl    %eax,C(loopcount)
-
-//
-// build composite position
-//
-    movl    C(ds_xfrac),%ebp
-    shll    $10,%ebp
-    andl    $0x0ffff0000,%ebp
-    movl    C(ds_yfrac),%eax
-    shrl    $6,%eax
-    andl    $0x0ffff,%eax
-    movl    C(ds_y),%edi
-    orl     %eax,%ebp
-
-    movl    C(ds_source),%esi
-
-//
-// calculate screen dest
-//
-
-    movl    C(ylookup)(,%edi,4),%edi
-    movl    C(ds_x1),%eax
-    addl    C(columnofs)(,%eax,4),%edi
-
-//
-// build composite step
-//
-    movl    C(ds_xstep),%ebx
-    shll    $10,%ebx
-    andl    $0x0ffff0000,%ebx
-    movl    C(ds_ystep),%eax
-    shrl    $6,%eax
-    andl    $0x0ffff,%eax
-    orl     %eax,%ebx
-
-    //movl        %eax,OFFSET hpatch1+2        // convice tasm to modify code...
-    movl    %ebx,hpatch1+2
-    //movl        %eax,OFFSET hpatch2+2        // convice tasm to modify code...
-    movl    %ebx,hpatch2+2
-    movl    %esi,hpatch3+2
-    movl    %esi,hpatch4+2
-// %eax      aligned colormap
-// %ebx      aligned colormap
-// %ecx,%edx  scratch
-// %esi      virtual source
-// %edi      moving destination pointer
-// %ebp      frac
-    movl    C(ds_colormap),%eax
-//    shld    $22,%ebp,%ecx           // begin calculating third pixel (y units)
-//    shld    $6,%ebp,%ecx            // begin calculating third pixel (x units)
-     movl    %ebp,%ecx
-    addl    %ebx,%ebp               // advance frac pointer
-     shrw    $10,%cx
-     roll    $6,%ecx
-    andl    $4095,%ecx              // finish calculation for third pixel
-//    shld    $22,%ebp,%edx           // begin calculating fourth pixel (y units)
-//    shld    $6,%ebp,%edx            // begin calculating fourth pixel (x units)
-     movl    %ebp,%edx
-     shrw    $10,%dx
-     roll    $6,%edx
-    addl    %ebx,%ebp               // advance frac pointer
-    andl    $4095,%edx              // finish calculation for fourth pixel
-    movl    %eax,%ebx
-    movb    (%esi,%ecx),%al         // get first pixel
-    movb    (%esi,%edx),%bl         // get second pixel
-    testl   $0x0fffffffe,C(pixelcount)
-    movb    (%eax),%dl             // color translate first pixel
-
-//    jnz hdoubleloop             // at least two pixels to map
-//    jmp hchecklast
-
-//    movw $0xf0f0,%dx //see visplanes start
-
-    jz      hchecklast
-    movb    (%ebx),%dh              // color translate second pixel
-    movl    C(loopcount),%esi
-//    .align  4
-hdoubleloop:
-//    shld    $22,%ebp,%ecx        // begin calculating third pixel (y units)
-//    shld    $6,%ebp,%ecx         // begin calculating third pixel (x units)
-    movl    %ebp,%ecx
-    shrw    $10,%cx
-    roll    $6,%ecx
-hpatch1:
-    addl    $0x012345678,%ebp    // advance frac pointer
-    movw    %dx,(%edi)           // write first pixel
-    andl    $4095,%ecx           // finish calculation for third pixel
-//    shld    $22,%ebp,%edx        // begin calculating fourth pixel (y units)
-//    shld    $6,%ebp,%edx         // begin calculating fourth pixel (x units)
-    movl    %ebp,%edx
-    shrw    $10,%dx
-    roll    $6,%edx
-hpatch3:
-    movb    0x012345678(%ecx),%al      // get third pixel
-//    movb    %bl,1(%edi)          // write second pixel
-    andl    $4095,%edx           // finish calculation for fourth pixel
-hpatch2:
-    addl    $0x012345678,%ebp    // advance frac pointer
-hpatch4:
-    movb    0x012345678(%edx),%bl      // get fourth pixel
-    movb    (%eax),%dl           // color translate third pixel
-    addl    $2,%edi              // advance to third pixel destination
-    decl    %esi                 // done with loop?
-    movb    (%ebx),%dh           // color translate fourth pixel
-    jnz hdoubleloop
-
-// check for final pixel
-hchecklast:
-    testl   $1,C(pixelcount)
-    jz      hdone
-    movb    %dl,(%edi)           // write final pixel
-
-hdone:
-    popl    %ebx                 // restore register variables
-    popl    %edi
-    popl    %esi
-    popl    %ebp                 // restore caller's stack frame pointer
-    ret
-
-
-//.endif
-
-
-//----------------------------------------------------------------------
-// R_DrawTransColumn
-//
-// Vertical column texture drawer, with transparency. Replaces Doom2's
-// 'fuzz' effect, which was not so beautiful.
-// Transparency is always impressive in some way, don't know why...
-//----------------------------------------------------------------------
-
-#ifdef LINUX
-    .align 2
-#else
-    .align 5
-#endif
-
-.globl C(R_DrawTranslucentColumn_8)
-C(R_DrawTranslucentColumn_8):
-    pushl   %ebp                // preserve caller's stack frame pointer
-    pushl   %esi                // preserve register variables
-    pushl   %edi
-    pushl   %ebx
-
-//
-// dest = ylookup[dc_yl] + columnofs[dc_x];
-//
-    movl     C(dc_yl),%ebp
-    movl     %ebp,%ebx
-    movl     C(ylookup)(,%ebx,4),%edi
-    movl     C(dc_x),%ebx
-    addl     C(columnofs)(,%ebx,4),%edi  // edi = dest
-
-//
-// pixelcount = yh - yl + 1
-//
-    movl     C(dc_yh),%eax
-    incl     %eax
-    subl     %ebp,%eax                   // pixel count
-    movl     %eax,C(pixelcount)          // save for final pixel
-    jle      vtdone                       // nothing to scale
-
-//
-// frac = dc_texturemid - (centery-dc_yl)*fracstep;
-//
-    movl     C(dc_iscale),%ecx           // fracstep
-    movl     C(centery),%eax
-    subl     %ebp,%eax
-    imul     %ecx,%eax
-    movl     C(dc_texturemid),%edx
-    subl     %eax,%edx
-    movl     %edx,%ebx
-
-    shrl     $16,%ebx          // frac int.
-    andl     $0x0000007f,%ebx
-    shll     $16,%edx          // y frac up
-
-    movl     %ecx,%ebp
-    shll     $16,%ebp          // fracstep f. up
-    shrl     $16,%ecx          // fracstep i. ->cl
-    andb     $0x7f,%cl
-    pushw    %cx
-    movl     %edx,%ecx
-    popw     %cx
-    movl     C(dc_colormap),%edx
-    movl     C(dc_source),%esi
-
-//
-// lets rock :) !
-//
-    movl    C(pixelcount),%eax
-    shrl    $2,%eax
-    testb   $0x03,C(pixelcount)
-    movb    %al,%ch             // quad count
-    movl    C(dc_transmap),%eax
-    jz      vt4quadloop
-//
-//  do un-even pixel
-//
-    testb   $1,C(pixelcount)
-    jz      2f
-
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-     addl    %ebp,%ecx
-    adcb    %cl,%bl
-     movb    (%edi),%al           // fetch dest  : index into colormap
-    andb    $0x7f,%bl
-     movb    (%eax),%dl
-    movb    (%edx), %dl          // use colormap now !
-    movb    %dl,(%edi)
-     addl    C(vidwidth),%edi
-//
-//  do two non-quad-aligned pixels
-//
-2:
-    testb   $2,C(pixelcount)
-    jz      3f
-
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-     addl    %ebp,%ecx
-    adcb    %cl,%bl
-     movb    (%edi),%al           // fetch dest  : index into colormap
-    andb    $0x7f,%bl
-     movb    (%eax),%dl
-    movb    (%edx), %dl          // use colormap now !
-    movb    %dl,(%edi)
-     addl    C(vidwidth),%edi
-
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-     addl    %ebp,%ecx
-    adcb    %cl,%bl
-     movb    (%edi),%al           // fetch dest  : index into colormap
-    andb    $0x7f,%bl
-     movb    (%eax),%dl
-    movb    (%edx), %dl          // use colormap now !
-    movb    %dl,(%edi)
-     addl    C(vidwidth),%edi
-
-//
-//  test if there was at least 4 pixels
-//
-3:
-    testb   $0xFF,%ch           // test quad count
-    jz      vtdone
-
-//
-// tystep : ystep frac. upper 24 bits
-// edx : upper 24 bit : colomap
-//  dl : tmp pixel to write
-// ebx : y     i.    lower 7 bits,  masked for index
-// ecx : y     frac. upper 16 bits
-// ecx : ch = counter, cl = y step i.
-// eax : transmap aligned 65535 (upper 16 bit)
-//  ah : background pixel (from the screen buffer)
-//  al : foreground pixel (from the texture)
-// esi : source texture column
-// ebp,edi : dest screen
-//
-vt4quadloop:
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-p5: movb    0x12345678(%edi),%al           // fetch dest  : index into colormap
-
-    movl    %ebp,C(tystep)
-    movl    %edi,%ebp
-    subl    C(vidwidth),%edi
-    jmp inloop
-//    .align  4
-vtquadloop:
-    addl    C(tystep),%ecx
-    adcb    %cl,%bl
-p6: addl    $2*0x12345678,%ebp
-    andb    $0x7f,%bl
-    movb    (%eax),%dl
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-    movb    (%edx), %dl          // use colormap now !
-    movb    %dl,(%edi)
-    movb    (%ebp),%al           // fetch dest  : index into colormap
-inloop:
-    addl    C(tystep),%ecx
-    adcb    %cl,%bl
-p7: addl    $2*0x12345678,%edi
-    andb    $0x7f,%bl
-    movb    (%eax),%dl
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-    movb    (%edx), %dl          // use colormap now !
-    movb    %dl,(%ebp)
-    movb    (%edi),%al           // fetch dest  : index into colormap
-
-    addl    C(tystep),%ecx
-    adcb    %cl,%bl
-p8: addl    $2*0x12345678,%ebp
-    andb    $0x7f,%bl
-    movb    (%eax),%dl
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-    movb    (%edx), %dl          // use colormap now !
-    movb    %dl,(%edi)
-    movb    (%ebp),%al           // fetch dest  : index into colormap
-
-    addl    C(tystep),%ecx
-    adcb    %cl,%bl
-p9: addl    $2*0x12345678,%edi
-    andb    $0x7f,%bl
-    movb    (%eax),%dl
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-    movb    (%edx), %dl          // use colormap now !
-    movb    %dl,(%ebp)
-    movb    (%edi),%al           // fetch dest  : index into colormap
-
-    decb   %ch
-     jnz    vtquadloop
-
-vtdone:
-    popl    %ebx                // restore register variables
-    popl    %edi
-    popl    %esi
-    popl    %ebp                // restore caller's stack frame pointer
-    ret
-
-#endif // ifdef USEASM
-
-
-
-//----------------------------------------------------------------------
-// R_DrawShadeColumn
-//
-//   for smoke..etc.. test.
-//----------------------------------------------------------------------
-
-#ifdef LINUX
-    .align 2
-#else
-    .align 5
-#endif
-.globl C(R_DrawShadeColumn_8)
-C(R_DrawShadeColumn_8):
-    pushl   %ebp                // preserve caller's stack frame pointer
-    pushl   %esi                // preserve register variables
-    pushl   %edi
-    pushl   %ebx
-
-//
-// dest = ylookup[dc_yl] + columnofs[dc_x];
-//
-    movl     C(dc_yl),%ebp
-    movl     %ebp,%ebx
-    movl     C(ylookup)(,%ebx,4),%edi
-    movl     C(dc_x),%ebx
-    addl     C(columnofs)(,%ebx,4),%edi  // edi = dest
-
-//
-// pixelcount = yh - yl + 1
-//
-    movl     C(dc_yh),%eax
-    incl     %eax
-    subl     %ebp,%eax                   // pixel count
-    movl     %eax,C(pixelcount)          // save for final pixel
-    jle      shdone                       // nothing to scale
-
-//
-// frac = dc_texturemid - (centery-dc_yl)*fracstep;
-//
-    movl     C(dc_iscale),%ecx           // fracstep
-    movl     C(centery),%eax
-    subl     %ebp,%eax
-    imul     %ecx,%eax
-    movl     C(dc_texturemid),%edx
-    subl     %eax,%edx
-     movl     %edx,%ebx
-     shrl     $16,%ebx          // frac int.
-     andl     $0x0000007f,%ebx
-     shll     $16,%edx          // y frac up
-
-     movl     %ecx,%ebp
-     shll     $16,%ebp          // fracstep f. up
-     shrl     $16,%ecx          // fracstep i. ->cl
-     andb     $0x7f,%cl
-
-    movl     C(dc_source),%esi
-
-//
-// lets rock :) !
-//
-    movl    C(pixelcount),%eax
-    movb    %al,%dh
-    shrl    $2,%eax
-    movb    %al,%ch             // quad count
-    movl    C(colormaps),%eax
-    testb   $0x03,%dh
-    jz      sh4quadloop
-
-//
-//  do un-even pixel
-//
-    testb   $1,%dh
-    jz      2f
-
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-     movb    (%edi),%al           // fetch dest  : index into colormap
-    andb    $0x7f,%bl
-     movb    (%eax),%dl
-    movb    %dl,(%edi)
-     addl    C(vidwidth),%edi
-
-//
-//  do two non-quad-aligned pixels
-//
-2:
-    testb   $2,%dh
-    jz      3f
-
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-     movb    (%edi),%al           // fetch dest  : index into colormap
-    andb    $0x7f,%bl
-     movb    (%eax),%dl
-    movb    %dl,(%edi)
-     addl    C(vidwidth),%edi
-
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-     movb    (%edi),%al           // fetch dest  : index into colormap
-    andb    $0x7f,%bl
-     movb    (%eax),%dl
-    movb    %dl,(%edi)
-     addl    C(vidwidth),%edi
-
-//
-//  test if there was at least 4 pixels
-//
-3:
-    testb   $0xFF,%ch           // test quad count
-    jz      shdone
-
-//
-// ebp : ystep frac. upper 24 bits
-// edx : y     frac. upper 24 bits
-// ebx : y     i.    lower 7 bits,  masked for index
-// ecx : ch = counter, cl = y step i.
-// eax : colormap aligned 256
-// esi : source texture column
-// edi : dest screen
-//
-sh4quadloop:
-    movb    $0x7f,%dh           // prep mask
-
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-sh5:    movb    0x12345678(%edi),%al           // fetch dest  : index into colormap
-
-    movl    %ebp,C(tystep)
-    movl    %edi,%ebp
-    subl    C(vidwidth),%edi
-    jmp shinloop
-//    .align  4
-shquadloop:
-    addl    C(tystep),%edx
-    adcb    %cl,%bl
-    andb    %dh,%bl
-sh6:    addl    $2*0x12345678,%ebp
-    movb    (%eax),%dl
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-    movb    %dl,(%edi)
-    movb    (%ebp),%al           // fetch dest  : index into colormap
-shinloop:
-    addl    C(tystep),%edx
-    adcb    %cl,%bl
-    andb    %dh,%bl
-sh7:    addl    $2*0x12345678,%edi
-    movb    (%eax),%dl
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-    movb    %dl,(%ebp)
-    movb    (%edi),%al           // fetch dest  : index into colormap
-
-    addl    C(tystep),%edx
-    adcb    %cl,%bl
-    andb    %dh,%bl
-sh8:    addl    $2*0x12345678,%ebp
-    movb    (%eax),%dl
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-    movb    %dl,(%edi)
-    movb    (%ebp),%al           // fetch dest  : index into colormap
-
-    addl    C(tystep),%edx
-    adcb    %cl,%bl
-    andb    %dh,%bl
-sh9:    addl    $2*0x12345678,%edi
-    movb    (%eax),%dl
-    movb    (%esi,%ebx),%ah      // fetch texel : colormap number
-    movb    %dl,(%ebp)
-    movb    (%edi),%al           // fetch dest  : index into colormap
-
-    decb   %ch
-     jnz    shquadloop
-
-shdone:
-    popl    %ebx                // restore register variables
-    popl    %edi
-    popl    %esi
-    popl    %ebp                // restore caller's stack frame pointer
-    ret
-
-
-
-//----------------------------------------------------------------------
-//
-//  R_DrawWaterColumn : basically it's just a copy of R_DrawColumn,
-//                      but it uses dc_colormap from dc_yl to dc_yw-1
-//                      then it uses dc_wcolormap from dc_yw to dc_yh
-//
-//  Thus, the 'underwater' part of the walls is remapped to 'water-like'
-//  colors.
-//
-//----------------------------------------------------------------------
-
-#ifdef LINUX
-    .align 2
-#else
-    .align 5
-#endif
-.globl C(R_DrawWaterColumn)
-C(R_DrawWaterColumn):
-    pushl   %ebp                // preserve caller's stack frame pointer
-    pushl   %esi                // preserve register variables
-    pushl   %edi
-    pushl   %ebx
-
-//
-// dest = ylookup[dc_yl] + columnofs[dc_x];
-//
-    movl     C(dc_yl),%ebp
-    movl     %ebp,%ebx
-    movl     C(ylookup)(,%ebx,4),%edi
-    movl     C(dc_x),%ebx
-    addl     C(columnofs)(,%ebx,4),%edi  // edi = dest
-
-//
-// pixelcount = yh - yl + 1
-//
-    movl     C(dc_yh),%eax
-    incl     %eax
-    subl     %ebp,%eax                   // pixel count
-    movl     %eax,C(pixelcount)          // save for final pixel
-    jle      wdone                       // nothing to scale
-
-//
-// frac = dc_texturemid - (centery-dc_yl)*fracstep;
-//
-    movl     C(dc_iscale),%ecx           // fracstep
-    movl     C(centery),%eax
-    subl     %ebp,%eax
-    imul     %ecx,%eax
-    movl     C(dc_texturemid),%edx
-    subl     %eax,%edx
-     movl     %edx,%ebx
-     shrl     $16,%ebx          // frac int.
-     andl     $0x0000007f,%ebx
-     shll     $16,%edx          // y frac up
-
-     movl     %ecx,%ebp
-     shll     $16,%ebp          // fracstep f. up
-     shrl     $16,%ecx          // fracstep i. ->cl
-     andb     $0x7f,%cl
-
-    movl     C(dc_source),%esi
-
-//
-// lets rock :) !
-//
-    movl    C(pixelcount),%eax
-    movb    %al,%dh
-    shrl    $2,%eax
-    movb    %al,%ch             // quad count
-    movl    C(dc_wcolormap),%eax
-    testb   $3,%dh
-    jz      w4quadloop
-
-//
-//  do un-even pixel
-//
-    testb   $1,%dh
-    jz      2f
-
-    movb    (%esi,%ebx),%al     // prep un-even loops
-     addl    %ebp,%edx            // ypos f += ystep f
-    adcb    %cl,%bl              // ypos i += ystep i
-     movb    (%eax),%dl           // colormap texel
-    andb    $0x7f,%bl            // mask 0-127 texture index
-     movb    %dl,(%edi)           // output pixel
-    addl    C(vidwidth),%edi
-
-//
-//  do two non-quad-aligned pixels
-//
-2:
-    testb   $2,%dh
-    jz      3f
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx            // ypos f += ystep f
-    adcb    %cl,%bl              // ypos i += ystep i
-     movb    (%eax),%dl           // colormap texel
-    andb    $0x7f,%bl            // mask 0-127 texture index
-     movb    %dl,(%edi)           // output pixel
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx            // ypos f += ystep f
-    adcb    %cl,%bl              // ypos i += ystep i
-     movb    (%eax),%dl           // colormap texel
-    andb    $0x7f,%bl            // mask 0-127 texture index
-    addl    C(vidwidth),%edi
-     movb    %dl,(%edi)           // output pixel
-
-    addl    C(vidwidth),%edi
-
-//
-//  test if there was at least 4 pixels
-//
-3:
-    testb   $0xFF,%ch           // test quad count
-    jz      wdone
-
-//
-// ebp : ystep frac. upper 24 bits
-// edx : y     frac. upper 24 bits
-// ebx : y     i.    lower 7 bits,  masked for index
-// ecx : ch = counter, cl = y step i.
-// eax : colormap aligned 256
-// esi : source texture column
-// edi : dest screen
-//
-w4quadloop:
-    movb    $0x7f,%dh           // prep mask
-//    .align  4
-wquadloop:
-    movb    (%esi,%ebx),%al     // prep loop
-     addl    %ebp,%edx            // ypos f += ystep f
-    adcb    %cl,%bl              // ypos i += ystep i
-     movb    (%eax),%dl           // colormap texel
-    movb    %dl,(%edi)           // output pixel
-     andb    $0x7f,%bl            // mask 0-127 texture index
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-     movb    (%eax),%dl
-w1:    movb    %dl,0x12345678(%edi)
-     andb    $0x7f,%bl
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-     movb    (%eax),%dl
-w2:    movb    %dl,2*0x12345678(%edi)
-     andb    $0x7f,%bl
-
-    movb    (%esi,%ebx),%al      // fetch source texel
-     addl    %ebp,%edx
-    adcb    %cl,%bl
-     movb    (%eax),%dl
-w3:    movb    %dl,3*0x12345678(%edi)
-     andb    $0x7f,%bl
-
-w4:    addl    $4*0x12345678,%edi
-
-    decb   %ch
-     jnz    wquadloop
-
-wdone:
-    popl    %ebx                // restore register variables
-    popl    %edi
-    popl    %esi
-    popl    %ebp                // restore caller's stack frame pointer
-    ret
-
-
-
-
-
-
-
-//----------------------------------------------------------------------
-//
-//  R_DrawSpanNoWrap
-//
-//      Horizontal texture mapping, does not remap colors,
-//      neither needs to wrap around the source texture.
-//
-//      Thus, a special optimisation can be used...
-//
-//----------------------------------------------------------------------
-
-    .data
-
-advancetable:   .long   0, 0
-#if !defined( LINUX)
-    .text
-#endif
-#ifdef LINUX
-    .align 2
-#else
-    .align 4
-#endif
-.globl C(R_DrawSpanNoWrap)
-C(R_DrawSpanNoWrap):
-    pushl   %ebp                // preserve caller's stack frame pointer
-    pushl   %esi                // preserve register variables
-    pushl   %edi
-    pushl   %ebx
-
-//
-// find loop count
-//
-
-    movl    C(ds_x2),%eax
-    incl    %eax
-    subl    C(ds_x1),%eax               // pixel count
-    movl    %eax,C(pixelcount)          // save for final pixel
-    jle     htvdone                       // nothing to scale
-//    shrl    $1,%eax                     // double pixel count
-//    movl    %eax,C(loopcount)
-
-//
-// calculate screen dest
-//
-
-    movl    C(ds_y),%edi        //full destination start address
-
-//
-// set up advancetable
-//
-
-    movl    C(ds_xstep),%ebp
-    movl    C(ds_ystep),%ecx
-    movl    %ecx,%eax
-    movl    %ebp,%edx
-    sarl    $16,%edx            // xstep >>= 16;
-    movl    C(vidwidth),%ebx
-    sarl    $16,%eax            // ystep >>= 16;
-    jz      0f
-    imull   %ebx,%eax           // (ystep >> 16) * texwidth;
-0:
-    addl    %edx,%eax           // add in xstep
-                                // (ystep >> 16) * texwidth + (xstep >> 16);
-
-    movl    %eax,advancetable+4 // advance base in y
-    addl    %ebx,%eax           // ((ystep >> 16) + 1) * texwidth +
-                                //  (xstep >> 16);
-    movl    %eax,advancetable   // advance extra in y
-
-    shll    $16,%ebp            // left-justify xstep fractional part
-    movl    %ebp,xstep
-    shll    $16,%ecx            // left-justify ystep fractional part
-    movl    %ecx,ystep
-
-//
-// calculate the texture starting address
-//
-    movl    C(ds_source),%esi       // texture source
-
-     movl    C(ds_yfrac),%eax
-     movl    %eax,%edx
-     sarl    $16,%eax
-    movl    C(ds_xfrac),%ecx
-     imull   %ebx,%eax               // (yfrac >> 16) * texwidth
-    movl    %ecx,%ebx
-    sarl    $16,%ecx
-    movl    %ecx,%ebp
-     addl    %eax,%ebp               // source = (xfrac >> 16) +
-                                    //           ((yfrac >> 16) * texwidth);
-
-//
-//  esi : texture source
-//  edi : screen dest
-//  eax : colormap aligned on 256 boundary, hehehe...
-//  ebx : xfrac << 16
-//  ecx : used in loop, contains either 0 or -1, *4, offset into advancetable
-//  edx : yfrac << 16
-//  ebp : offset into texture
-//
-
-    shll    $16,%edx             // yfrac upper word, lower byte will be used
-    movl    C(ds_colormap),%eax
-    shll    $16,%ebx             // xfrac upper word, lower unused
-
-    movl    C(pixelcount),%ecx
-    shrl    $2,%ecx
-    movb    %cl,%dh             // quad pixels count
-
-    movl    C(pixelcount),%ecx
-    andl    $3,%ecx
-    jz      htvquadloop         // pixelcount is multiple of 4
-    decl    %ecx
-    jz      1f
-    decl    %ecx
-    jz      2f
-
-//
-//  do one to three pixels first
-//
-    addl    ystep,%edx          // yfrac += ystep
-   sbbl    %ecx,%ecx           // turn carry into 0 or -1 if set
-    movb    (%esi,%ebp),%al          // get texture pixel
-   addl    xstep,%ebx           // xfrac += xstep
-//    movb    (%eax),%dl           // pixel goes through colormap
-   adcl    advancetable+4(,%ecx,4),%ebp       // advance source
-    movb    %al,(%edi)           // write pixel dest
-
-   incl    %edi
-
-2:
-    addl    ystep,%edx          // yfrac += ystep
-   sbbl    %ecx,%ecx           // turn carry into 0 or -1 if set
-    movb    (%esi,%ebp),%al          // get texture pixel
-   addl    xstep,%ebx           // xfrac += xstep
-//    movb    (%eax),%dl           // pixel goes through colormap
-   adcl    advancetable+4(,%ecx,4),%ebp       // advance source
-    movb    %al,(%edi)           // write pixel dest
-
-   incl    %edi
-
-1:
-    addl    ystep,%edx          // yfrac += ystep
-   sbbl    %ecx,%ecx           // turn carry into 0 or -1 if set
-    movb    (%esi,%ebp),%al          // get texture pixel
-   addl    xstep,%ebx           // xfrac += xstep
-//    movb    (%eax),%dl           // pixel goes through colormap
-   adcl    advancetable+4(,%ecx,4),%ebp       // advance source
-    movb    %al,(%edi)           // write pixel dest
-
-   incl    %edi
-
-//
-//  test if there was at least 4 pixels
-//
-    testb   $0xFF,%dh
-    jz      htvdone
-
-//
-//  two pixels per loop
-// U
-//  V
-htvquadloop:
-    addl    ystep,%edx             // yfrac += ystep
-   sbbl    %ecx,%ecx               // turn carry into 0 or -1 if set
-    movb    (%esi,%ebp),%al        // get texture pixel
-   addl    xstep,%ebx              // xfrac += xstep
-//    movb    (%eax),%dl             // pixel goes through colormap
-   adcl    advancetable+4(,%ecx,4),%ebp       // advance source
-    movb    %al,(%edi)             // write pixel dest
-
-    addl    ystep,%edx
-   sbbl    %ecx,%ecx
-    movb    (%esi,%ebp),%al
-   addl    xstep,%ebx
-//    movb    (%eax),%dl
-   adcl    advancetable+4(,%ecx,4),%ebp
-    movb    %al,1(%edi)
-
-    addl    ystep,%edx
-   sbbl    %ecx,%ecx
-    movb    (%esi,%ebp),%al
-   addl    xstep,%ebx
-//    movb    (%eax),%dl
-   adcl    advancetable+4(,%ecx,4),%ebp
-    movb    %al,2(%edi)
-
-    addl    ystep,%edx
-   sbbl    %ecx,%ecx
-    movb    (%esi,%ebp),%al
-   addl    xstep,%ebx
-//    movb    (%eax),%dl
-   adcl    advancetable+4(,%ecx,4),%ebp
-    movb    %al,3(%edi)
-
-   addl    $4, %edi
-    incl    %ecx    //dummy
-
-   decb   %dh
-    jnz    htvquadloop          // paire dans V-pipe
-
-htvdone:
-    popl    %ebx                // restore register variables
-    popl    %edi
-    popl    %esi
-    popl    %ebp                // restore caller's stack frame pointer
-    ret
-
-
-//.endif
-
-#ifdef HORIZONTALDRAW
-// void R_RotateBuffere (void)
-
-#ifdef LINUX
-    .align 2
-#else
-    .align 4
-#endif
-.globl C(R_RotateBufferasm)
-C(R_RotateBufferasm):
-    pushl   %ebp                // preserve caller's stack frame pointer
-    pushl   %esi                // preserve register variables
-    pushl   %edi
-    pushl   %ebx
-
-    movl    C(dc_source),%esi
-    movl    C(dc_colormap),%edi
-
-    movl    $200,%edx
-ra2:
-    movl    $40,%ecx
-ra:
-    movb    -2*200(%esi),%al
-    movb    -6*200(%esi),%bl
-    movb    -3*200(%esi),%ah
-    movb    -7*200(%esi),%bh
-    shll    $16,%eax
-    shll    $16,%ebx
-    movb    (%esi),%al
-    movb    -4*200(%esi),%bl
-    movb    -1*200(%esi),%ah
-    movb    -5*200(%esi),%bh
-    movl    %eax,(%edi)
-    subl    $8*200,%esi
-    movl    %ebx,4(%edi)
-    addl    $8,%edi
-    decl    %ecx
-    jnz     ra
-
-    addl    $320*200+1,%esi      //32*480 passe a la ligne suivante
-//    addl    320-32,%edi
-
-    decl    %edx
-    jnz     ra2
-
-    pop   %ebp                // preserve caller's stack frame pointer
-    pop   %esi                // preserve register variables
-    pop   %edi
-    pop   %ebx
-    ret
-#endif
diff --git a/src/tmap_asm.s b/src/tmap_asm.s
deleted file mode 100644
index d8967178cdf28e3b9bedbda863232ef0bf0978d4..0000000000000000000000000000000000000000
--- a/src/tmap_asm.s
+++ /dev/null
@@ -1,322 +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  tmap_asm.s
-/// \brief ???
-
-//.comm _dc_colormap,4
-//.comm _dc_x,4
-//.comm _dc_yl,4
-//.comm _dc_yh,4
-//.comm _dc_iscale,4
-//.comm _dc_texturemid,4
-//.comm _dc_source,4
-//.comm _ylookup,4
-//.comm _columnofs,4
-//.comm _loopcount,4
-//.comm _pixelcount,4
-.data
-_pixelcount:
-.long 0x00000000
-_loopcount:
-.long 0x00000000
-.align 8
-_mmxcomm:
-.long 0x00000000
-.text
-
-        .align 4
-.globl _R_DrawColumn8_NOMMX
-_R_DrawColumn8_NOMMX:
-   pushl %ebp
-   pushl %esi
-   pushl %edi
-   pushl %ebx
-	movl _dc_yl,%edx
-	movl _dc_yh,%eax
-	subl %edx,%eax
-	leal 1(%eax),%ebx
-	testl %ebx,%ebx
-	jle rdc8ndone
-	movl _dc_x,%eax
-        movl _ylookup, %edi
-	movl (%edi,%edx,4),%esi
-	movl _columnofs, %edi
-	addl (%edi,%eax,4),%esi
-	movl _dc_iscale,%edi
-	movl %edx,%eax
-	imull %edi,%eax
-	movl _dc_texturemid,%ecx
-	addl %eax,%ecx
-
-	movl _dc_source,%ebp
-   xorl %edx, %edx
-   subl $0x12345678, %esi
-.globl rdc8nwidth1
-rdc8nwidth1:
-	.align 4,0x90
-rdc8nloop:
-	movl %ecx,%eax
-	shrl $16,%eax
-	addl %edi,%ecx
-	andl $127,%eax
-	addl $0x12345678,%esi
-.globl rdc8nwidth2
-rdc8nwidth2:
-	movb (%eax,%ebp),%dl
-	movl _dc_colormap,%eax
-	movb (%eax,%edx),%al
-	movb %al,(%esi)
-	decl %ebx
-	jne rdc8nloop
-rdc8ndone:
-   popl %ebx
-   popl %edi
-   popl %esi
-   popl %ebp
-   ret
-
-//
-// Optimised specifically for P54C/P55C (aka Pentium with/without MMX)
-// By ES 1998/08/01
-//
-
-.globl _R_DrawColumn_8_Pentium
-_R_DrawColumn_8_Pentium:
-	pushl %ebp
-        pushl %ebx
-	pushl %esi
-        pushl %edi
-	movl _dc_yl,%eax        // Top pixel
-	movl _dc_yh,%ebx        // Bottom pixel
-        movl _ylookup, %edi
-	movl (%edi,%ebx,4),%ecx
-	subl %eax,%ebx          // ebx=number of pixels-1
-	jl rdc8pdone            // no pixel to draw, done
-	jnz rdc8pmany
-	movl _dc_x,%edx         // Special case: only one pixel
-        movl _columnofs, %edi
-	addl (%edi,%edx,4),%ecx // dest pixel at (%ecx)
-	movl _dc_iscale,%esi
-	imull %esi,%eax
-	movl _dc_texturemid,%edi
-	addl %eax,%edi          // texture index in edi
-	movl _dc_colormap,%edx
-   	shrl $16, %edi
-   	movl _dc_source,%ebp
-	andl $127,%edi
-	movb (%edi,%ebp),%dl    // read texture pixel
-	movb (%edx),%al	        // lookup for light
-	movb %al,0(%ecx) 	// write it
-	jmp rdc8pdone		// done!
-.align 4, 0x90
-rdc8pmany:			// draw >1 pixel
-	movl _dc_x,%edx
-        movl _columnofs, %edi
-	movl (%edi,%edx,4),%edx
-	leal 0x12345678(%edx, %ecx), %edi  // edi = two pixels above bottom
-.globl rdc8pwidth5
-rdc8pwidth5:  // DeadBeef = -2*SCREENWIDTH
-        movl _dc_iscale,%edx	// edx = fracstep
-	imull %edx,%eax
-   	shll $9, %edx           // fixme: Should get 7.25 fix as input
-	movl _dc_texturemid,%ecx
-	addl %eax,%ecx          // ecx = frac
-	movl _dc_colormap,%eax  // eax = lighting/special effects LUT
-   	shll $9, %ecx
-   	movl _dc_source,%esi    // esi = source ptr
-
-	imull $0x12345678, %ebx // ebx = negative offset to pixel
-.globl rdc8pwidth6
-rdc8pwidth6:  // DeadBeef = -SCREENWIDTH
-
-// Begin the calculation of the two first pixels
-        leal (%ecx, %edx), %ebp
-	shrl $25, %ecx
-	movb (%esi, %ecx), %al
-	leal (%edx, %ebp), %ecx
-	shrl $25, %ebp
-        movb (%eax), %dl
-
-// The main loop
-rdc8ploop:
-	movb (%esi,%ebp), %al		// load 1
-        leal (%ecx, %edx), %ebp         // calc frac 3
-
-	shrl $25, %ecx                  // shift frac 2
-        movb %dl, 0x12345678(%edi, %ebx)// store 0
-.globl rdc8pwidth1
-rdc8pwidth1:  // DeadBeef = 2*SCREENWIDTH
-
-        movb (%eax), %al                // lookup 1
-
-        movb %al, 0x12345678(%edi, %ebx)// store 1
-.globl rdc8pwidth2
-rdc8pwidth2:  // DeadBeef = 3*SCREENWIDTH
-        movb (%esi, %ecx), %al          // load 2
-
-        leal (%ebp, %edx), %ecx         // calc frac 4
-
-        shrl $25, %ebp                  // shift frac 3
-        movb (%eax), %dl                // lookup 2
-
-        addl $0x12345678, %ebx          // counter
-.globl rdc8pwidth3
-rdc8pwidth3:  // DeadBeef = 2*SCREENWIDTH
-        jl rdc8ploop                    // loop
-
-// End of loop. Write extra pixel or just exit.
-        jnz rdc8pdone
-        movb %dl, 0x12345678(%edi, %ebx)// Write odd pixel
-.globl rdc8pwidth4
-rdc8pwidth4:  // DeadBeef = 2*SCREENWIDTH
-
-rdc8pdone:
-
-        popl %edi
-	popl %esi
-        popl %ebx
-	popl %ebp
-        ret
-
-//
-// MMX asm version, optimised for K6
-// By ES 1998/07/05
-//
-
-.globl _R_DrawColumn_8_K6_MMX
-_R_DrawColumn_8_K6_MMX:
-	pushl %ebp
-        pushl %ebx
-	pushl %esi
-        pushl %edi
-
-        movl %esp, %eax // Push 8 or 12, so that (%esp) gets aligned by 8
-        andl $7,%eax
-        addl $8,%eax
-        movl %eax, _mmxcomm // Temp storage in mmxcomm: (%esp) is used instead
-        subl %eax,%esp
-
-	movl _dc_yl,%edx        // Top pixel
-	movl _dc_yh,%ebx        // Bottom pixel
-        movl _ylookup, %edi
-	movl (%edi,%ebx,4),%ecx
-	subl %edx,%ebx         // ebx=number of pixels-1
-	jl 0x12345678            // no pixel to draw, done
-.globl rdc8moffs1
-rdc8moffs1:
-	jnz rdc8mmany
-	movl _dc_x,%eax         // Special case: only one pixel
-        movl _columnofs, %edi
-	addl (%edi,%eax,4),%ecx  // dest pixel at (%ecx)
-	movl _dc_iscale,%esi
-	imull %esi,%edx
-	movl _dc_texturemid,%edi
-	addl %edx,%edi         // texture index in edi
-	movl _dc_colormap,%edx
-   	shrl $16, %edi
-   	movl _dc_source,%ebp
-	andl $127,%edi
-	movb (%edi,%ebp),%dl  // read texture pixel
-	movb (%edx),%al	 // lookup for light
-	movb %al,0(%ecx) 	 // write it
-	jmp rdc8mdone		 // done!
-.globl rdc8moffs2
-rdc8moffs2:
-.align 4, 0x90
-rdc8mmany:			 // draw >1 pixel
-	movl _dc_x,%eax
-        movl _columnofs, %edi
-	movl (%edi,%eax,4),%eax
-	leal 0x12345678(%eax, %ecx), %esi  // esi = two pixels above bottom
-.globl rdc8mwidth3
-rdc8mwidth3:  // DeadBeef = -2*SCREENWIDTH
-        movl _dc_iscale,%ecx	 // ecx = fracstep
-	imull %ecx,%edx
-   	shll $9, %ecx           // fixme: Should get 7.25 fix as input
-	movl _dc_texturemid,%eax
-	addl %edx,%eax         // eax = frac
-	movl _dc_colormap,%edx  // edx = lighting/special effects LUT
-   	shll $9, %eax
-	leal (%ecx, %ecx), %edi
-   	movl _dc_source,%ebp    // ebp = source ptr
-	movl %edi, 0(%esp)     // Start moving frac and fracstep to MMX regs
-
-	imull $0x12345678, %ebx  // ebx = negative offset to pixel
-.globl rdc8mwidth5
-rdc8mwidth5:  // DeadBeef = -SCREENWIDTH
-
-	movl %edi, 4(%esp)
-	leal (%eax, %ecx), %edi
-	movq 0(%esp), %mm1     // fracstep:fracstep in mm1
-	movl %eax, 0(%esp)
-	shrl $25, %eax
-	movl %edi, 4(%esp)
-	movzbl (%ebp, %eax), %eax
-	movq 0(%esp), %mm0     // frac:frac in mm0
-
-	paddd %mm1, %mm0
-	shrl $25, %edi
-	movq %mm0, %mm2
-	psrld $25, %mm2         // texture index in mm2
-	paddd %mm1, %mm0
-	movq %mm2, 0(%esp)
-
-.globl rdc8mloop
-rdc8mloop:                      		// The main loop
-	movq %mm0, %mm2                    // move 4-5 to temp reg
-	movzbl (%ebp, %edi), %edi 		// read 1
-
-	psrld $25, %mm2 			// shift 4-5
-	movb (%edx,%eax), %cl 		// lookup 0
-
-	movl 0(%esp), %eax 			// load 2
-	addl $0x12345678, %ebx 		// counter
-.globl rdc8mwidth2
-rdc8mwidth2:  // DeadBeef = 2*SCREENWIDTH
-
-	movb %cl, (%esi, %ebx)		// write 0
-	movb (%edx,%edi), %ch 		// lookup 1
-
-	movb %ch, 0x12345678(%esi, %ebx) 	// write 1
-.globl rdc8mwidth1
-rdc8mwidth1:  // DeadBeef = SCREENWIDTH
-	movl 4(%esp), %edi			// load 3
-
-	paddd %mm1, %mm0 			// frac 6-7
-	movzbl (%ebp, %eax), %eax 		// lookup 2
-
-	movq %mm2, 0(%esp) 		     // store texture index 4-5
-	jl rdc8mloop
-
-	jnz rdc8mno_odd
-	movb (%edx,%eax), %cl  // write the last odd pixel
-	movb %cl, 0x12345678(%esi)
-.globl rdc8mwidth4
-rdc8mwidth4:  // DeadBeef = 2*SCREENWIDTH
-rdc8mno_odd:
-
-.globl rdc8mdone
-rdc8mdone:
-        emms
-
-        addl _mmxcomm, %esp
-        popl %edi
-	popl %esi
-        popl %ebx
-	popl %ebp
-        ret
-
-// Need some extra space to align run-time
-.globl R_DrawColumn_8_K6_MMX_end
-R_DrawColumn_8_K6_MMX_end:
-nop;nop;nop;nop;nop;nop;nop;nop;
-nop;nop;nop;nop;nop;nop;nop;nop;
-nop;nop;nop;nop;nop;nop;nop;nop;
-nop;nop;nop;nop;nop;nop;nop;
diff --git a/src/tmap_mmx.nas b/src/tmap_mmx.nas
deleted file mode 100644
index a45667e23d539997193e0df23862dba71458c6f6..0000000000000000000000000000000000000000
--- a/src/tmap_mmx.nas
+++ /dev/null
@@ -1,674 +0,0 @@
-;; SONIC ROBO BLAST 2
-;;-----------------------------------------------------------------------------
-;; Copyright (C) 1998-2000 by DOSDOOM.
-;; Copyright (C) 2010-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:
-;;      tmap_mmx.nas
-;; DESCRIPTION:
-;;      Assembler optimised rendering code for software mode, using SIMD
-;;      instructions.
-;;      Draw wall columns.
-
-
-[BITS 32]
-
-%define FRACBITS 16
-%define TRANSPARENTPIXEL 255
-
-%ifdef LINUX
-%macro cextern 1
-[extern %1]
-%endmacro
-
-%macro cglobal 1
-[global %1]
-%endmacro
-
-%else
-%macro cextern 1
-%define %1 _%1
-[extern %1]
-%endmacro
-
-%macro cglobal 1
-%define %1 _%1
-[global %1]
-%endmacro
-
-%endif
-
-
-; The viddef_s structure. We only need the width field.
-struc viddef_s
-		resb 12
-.width: resb 4
-		resb 44
-endstruc
-
-
-;; externs
-;; columns
-cextern dc_colormap
-cextern dc_x
-cextern dc_yl
-cextern dc_yh
-cextern dc_iscale
-cextern dc_texturemid
-cextern dc_texheight
-cextern dc_source
-cextern dc_hires
-cextern centery
-cextern centeryfrac
-cextern dc_transmap
-
-cextern R_DrawColumn_8_ASM
-cextern R_Draw2sMultiPatchColumn_8_ASM
-
-;; spans
-cextern nflatshiftup
-cextern nflatxshift
-cextern nflatyshift
-cextern nflatmask
-cextern ds_xfrac
-cextern ds_yfrac
-cextern ds_xstep
-cextern ds_ystep
-cextern ds_x1
-cextern ds_x2
-cextern ds_y
-cextern ds_source
-cextern ds_colormap
-
-cextern ylookup
-cextern columnofs
-cextern vid
-
-[SECTION .data]
-
-nflatmask64		dq		0
-
-
-[SECTION .text]
-
-;;----------------------------------------------------------------------
-;;
-;; R_DrawColumn : 8bpp column drawer
-;;
-;; MMX column drawer.
-;;
-;;----------------------------------------------------------------------
-;; eax = accumulator
-;; ebx = colormap
-;; ecx = count
-;; edx = accumulator
-;; esi = source
-;; edi = dest
-;; ebp = vid.width
-;; mm0 = accumulator
-;; mm1 = heightmask, twice
-;; mm2 = 2 * fracstep, twice
-;; mm3 = pair of consecutive fracs
-;;----------------------------------------------------------------------
-
-
-cglobal R_DrawColumn_8_MMX
-R_DrawColumn_8_MMX:
-		push		ebp						;; preserve caller's stack frame pointer
-		push		esi						;; preserve register variables
-		push		edi
-		push		ebx
-
-;;
-;; Our algorithm requires that the texture height be a power of two.
-;; If not, fall back to the non-MMX drawer.
-;;
-.texheightcheck:
-		mov			edx, [dc_texheight]
-		sub			edx, 1					;; edx = heightmask
-		test		edx, [dc_texheight]
-		jnz			near .usenonMMX
-
-		mov			ebp, edx				;; Keep a copy of heightmask in a
-											;; GPR for the time being.
-
-;;
-;; Fill mm1 with heightmask
-;;
-		movd		mm1, edx				;; low dword = heightmask
-		punpckldq	mm1, mm1				;; copy low dword to high dword
-
-;;
-;; dest = ylookup[dc_yl] + columnofs[dc_x];
-;;
-		mov			eax, [dc_yl]
-		mov			edi, [ylookup+eax*4]
-		mov			ebx, [dc_x]
-		add			edi, [columnofs+ebx*4]	;; edi = dest
-
-
-;;
-;; pixelcount = yh - yl + 1
-;;
-		mov			ecx, [dc_yh]
-		add			ecx, 1
-		sub			ecx, eax				;; pixel count
-		jle			near .done				;; nothing to scale
-
-;;
-;; fracstep = dc_iscale;
-;;
-		movd		mm2, [dc_iscale]		;; fracstep in low dword
-		punpckldq	mm2, mm2				;; copy to high dword
-
-		mov			ebx, [dc_colormap]
-		mov			esi, [dc_source]
-
-;;
-;; frac = (dc_texturemid + FixedMul((dc_yl << FRACBITS) - centeryfrac, fracstep));
-;;
-											;; eax == dc_yl already
-		shl			eax, FRACBITS
-		sub			eax, [centeryfrac]
-		imul		dword [dc_iscale]
-		shrd		eax, edx, FRACBITS
-		add			eax, [dc_texturemid]
-
-;;
-;; if (dc_hires) frac = 0;
-;;
-		test		byte [dc_hires], 0x01
-		jz			.mod2
-		xor			eax, eax
-
-
-;;
-;; Do mod-2 pixel.
-;;
-.mod2:
-		test		ecx, 1
-		jz			.pairprepare
-		mov			edx, eax				;; edx = frac
-		add			eax, [dc_iscale]		;; eax += fracstep
-		sar			edx, FRACBITS
-		and			edx, ebp				;; edx &= heightmask
-		movzx		edx, byte [esi + edx]
-		movzx		edx, byte [ebx + edx]
-		mov			[edi], dl
-
-		add			edi, [vid + viddef_s.width]
-		sub			ecx, 1
-		jz			.done
-
-.pairprepare:
-;;
-;; Prepare for the main loop.
-;;
-		movd		mm3, eax				;; Low dword = frac
-		movq		mm4, mm3				;; Copy to intermediate register
-		paddd		mm4, mm2				;; dwords of mm4 += fracstep
-		punpckldq	mm3, mm4				;; Low dword = first frac, high = second
-		pslld		mm2, 1					;; fracstep *= 2
-
-;;
-;; ebp = vid.width
-;;
-		mov			ebp, [vid + viddef_s.width]
-
-		align		16
-.pairloop:
-		movq		mm0, mm3				;; 3B 1u.
-		psrad		mm0, FRACBITS			;; 4B 1u.
-		pand		mm0, mm1				;; 3B 1u. frac &= heightmask
-		paddd		mm3, mm2				;; 3B 1u. frac += fracstep
-
-		movd		eax, mm0				;; 3B 1u. Get first frac
-;; IFETCH boundary
-		movzx		eax, byte [esi + eax]	;; 4B 1u. Texture map
-		movzx		eax, byte [ebx + eax]	;; 4B 1u. Colormap
-
-		punpckhdq	mm0, mm0				;; 3B 1(2)u. low dword = high dword
-		movd		edx, mm0				;; 3B 1u. Get second frac
-		mov			[edi], al				;; 2B 1(2)u. First pixel
-;; IFETCH boundary
-
-		movzx		edx, byte [esi + edx]	;; 4B 1u. Texture map
-		movzx		edx, byte [ebx + edx]	;; 4B 1u. Colormap
-		mov			[edi + 1*ebp], dl		;; 3B 1(2)u. Second pixel
-
-		lea			edi, [edi + 2*ebp]		;; 3B 1u. edi += 2 * vid.width
-;; IFETCH boundary
-		sub			ecx, 2					;; 3B 1u. count -= 2
-		jnz			.pairloop				;; 2B 1u. if(count != 0) goto .pairloop
-
-
-.done:
-;;
-;; Clear MMX state, or else FPU operations will go badly awry.
-;;
-		emms
-
-		pop			ebx
-		pop			edi
-		pop			esi
-		pop			ebp
-		ret
-
-.usenonMMX:
-		call		R_DrawColumn_8_ASM
-		jmp			.done
-
-
-;;----------------------------------------------------------------------
-;;
-;; R_Draw2sMultiPatchColumn : Like R_DrawColumn, but omits transparent
-;;                            pixels.
-;;
-;; MMX column drawer.
-;;
-;;----------------------------------------------------------------------
-;; eax = accumulator
-;; ebx = colormap
-;; ecx = count
-;; edx = accumulator
-;; esi = source
-;; edi = dest
-;; ebp = vid.width
-;; mm0 = accumulator
-;; mm1 = heightmask, twice
-;; mm2 = 2 * fracstep, twice
-;; mm3 = pair of consecutive fracs
-;;----------------------------------------------------------------------
-
-
-cglobal R_Draw2sMultiPatchColumn_8_MMX
-R_Draw2sMultiPatchColumn_8_MMX:
-		push		ebp						;; preserve caller's stack frame pointer
-		push		esi						;; preserve register variables
-		push		edi
-		push		ebx
-
-;;
-;; Our algorithm requires that the texture height be a power of two.
-;; If not, fall back to the non-MMX drawer.
-;;
-.texheightcheck:
-		mov			edx, [dc_texheight]
-		sub			edx, 1					;; edx = heightmask
-		test		edx, [dc_texheight]
-		jnz			near .usenonMMX
-
-		mov			ebp, edx				;; Keep a copy of heightmask in a
-											;; GPR for the time being.
-
-;;
-;; Fill mm1 with heightmask
-;;
-		movd		mm1, edx				;; low dword = heightmask
-		punpckldq	mm1, mm1				;; copy low dword to high dword
-
-;;
-;; dest = ylookup[dc_yl] + columnofs[dc_x];
-;;
-		mov			eax, [dc_yl]
-		mov			edi, [ylookup+eax*4]
-		mov			ebx, [dc_x]
-		add			edi, [columnofs+ebx*4]	;; edi = dest
-
-
-;;
-;; pixelcount = yh - yl + 1
-;;
-		mov			ecx, [dc_yh]
-		add			ecx, 1
-		sub			ecx, eax				;; pixel count
-		jle			near .done				;; nothing to scale
-;;
-;; fracstep = dc_iscale;
-;;
-		movd		mm2, [dc_iscale]		;; fracstep in low dword
-		punpckldq	mm2, mm2				;; copy to high dword
-
-		mov			ebx, [dc_colormap]
-		mov			esi, [dc_source]
-
-;;
-;; frac = (dc_texturemid + FixedMul((dc_yl << FRACBITS) - centeryfrac, fracstep));
-;;
-											;; eax == dc_yl already
-		shl			eax, FRACBITS
-		sub			eax, [centeryfrac]
-		imul		dword [dc_iscale]
-		shrd		eax, edx, FRACBITS
-		add			eax, [dc_texturemid]
-
-;;
-;; if (dc_hires) frac = 0;
-;;
-		test		byte [dc_hires], 0x01
-		jz			.mod2
-		xor			eax, eax
-
-
-;;
-;; Do mod-2 pixel.
-;;
-.mod2:
-		test		ecx, 1
-		jz			.pairprepare
-		mov			edx, eax				;; edx = frac
-		add			eax, [dc_iscale]		;; eax += fracstep
-		sar			edx, FRACBITS
-		and			edx, ebp				;; edx &= heightmask
-		movzx		edx, byte [esi + edx]
-		cmp			dl, TRANSPARENTPIXEL
-		je			.nextmod2
-		movzx		edx, byte [ebx + edx]
-		mov			[edi], dl
-
-.nextmod2:
-		add			edi, [vid + viddef_s.width]
-		sub			ecx, 1
-		jz			.done
-
-.pairprepare:
-;;
-;; Prepare for the main loop.
-;;
-		movd		mm3, eax				;; Low dword = frac
-		movq		mm4, mm3				;; Copy to intermediate register
-		paddd		mm4, mm2				;; dwords of mm4 += fracstep
-		punpckldq	mm3, mm4				;; Low dword = first frac, high = second
-		pslld		mm2, 1					;; fracstep *= 2
-
-;;
-;; ebp = vid.width
-;;
-		mov			ebp, [vid + viddef_s.width]
-
-		align		16
-.pairloop:
-		movq		mm0, mm3				;; 3B 1u.
-		psrad		mm0, FRACBITS			;; 4B 1u.
-		pand		mm0, mm1				;; 3B 1u. frac &= heightmask
-		paddd		mm3, mm2				;; 3B 1u. frac += fracstep
-
-		movd		eax, mm0				;; 3B 1u. Get first frac
-;; IFETCH boundary
-		movzx		eax, byte [esi + eax]	;; 4B 1u. Texture map
-		punpckhdq	mm0, mm0				;; 3B 1(2)u. low dword = high dword
-		movd		edx, mm0				;; 3B 1u. Get second frac
-		cmp			al, TRANSPARENTPIXEL	;; 2B 1u.
-		je			.secondinpair			;; 2B 1u.
-;; IFETCH boundary
-		movzx		eax, byte [ebx + eax]	;; 4B 1u. Colormap
-		mov			[edi], al				;; 2B 1(2)u. First pixel
-
-.secondinpair:
-		movzx		edx, byte [esi + edx]	;; 4B 1u. Texture map
-		cmp			dl, TRANSPARENTPIXEL	;; 2B 1u.
-		je			.nextpair				;; 2B 1u.
-;; IFETCH boundary
-		movzx		edx, byte [ebx + edx]	;; 4B 1u. Colormap
-		mov			[edi + 1*ebp], dl		;; 3B 1(2)u. Second pixel
-
-.nextpair:
-		lea			edi, [edi + 2*ebp]		;; 3B 1u. edi += 2 * vid.width
-		sub			ecx, 2					;; 3B 1u. count -= 2
-		jnz			.pairloop				;; 2B 1u. if(count != 0) goto .pairloop
-
-
-.done:
-;;
-;; Clear MMX state, or else FPU operations will go badly awry.
-;;
-		emms
-
-		pop			ebx
-		pop			edi
-		pop			esi
-		pop			ebp
-		ret
-
-.usenonMMX:
-		call		R_Draw2sMultiPatchColumn_8_ASM
-		jmp			.done
-
-
-;;----------------------------------------------------------------------
-;;
-;; R_DrawSpan : 8bpp span drawer
-;;
-;; MMX span drawer.
-;;
-;;----------------------------------------------------------------------
-;; eax = accumulator
-;; ebx = colormap
-;; ecx = count
-;; edx = accumulator
-;; esi = source
-;; edi = dest
-;; ebp = two pixels
-;; mm0 = accumulator
-;; mm1 = xposition
-;; mm2 = yposition
-;; mm3 = 2 * xstep
-;; mm4 = 2 * ystep
-;; mm5 = nflatxshift
-;; mm6 = nflatyshift
-;; mm7 = accumulator
-;;----------------------------------------------------------------------
-
-cglobal R_DrawSpan_8_MMX
-R_DrawSpan_8_MMX:
-		push		ebp						;; preserve caller's stack frame pointer
-		push		esi						;; preserve register variables
-		push		edi
-		push		ebx
-
-;;
-;; esi = ds_source
-;; ebx = ds_colormap
-;;
-		mov			esi, [ds_source]
-		mov			ebx, [ds_colormap]
-
-;;
-;; edi = ylookup[ds_y] + columnofs[ds_x1]
-;;
-		mov			eax, [ds_y]
-		mov			edi, [ylookup + eax*4]
-		mov			edx, [ds_x1]
-		add			edi, [columnofs + edx*4]
-
-;;
-;; ecx = ds_x2 - ds_x1 + 1
-;;
-		mov			ecx, [ds_x2]
-		sub			ecx, edx
-		add			ecx, 1
-
-;;
-;; Needed for fracs and steps
-;;
-		movd		mm7, [nflatshiftup]
-
-;;
-;; mm3 = xstep
-;;
-		movd		mm3, [ds_xstep]
-		pslld		mm3, mm7
-		punpckldq	mm3, mm3
-
-;;
-;; mm4 = ystep
-;;
-		movd		mm4, [ds_ystep]
-		pslld		mm4, mm7
-		punpckldq	mm4, mm4
-
-;;
-;; mm1 = pair of consecutive xpositions
-;;
-		movd		mm1, [ds_xfrac]
-		pslld		mm1, mm7
-		movq		mm6, mm1
-		paddd		mm6, mm3
-		punpckldq	mm1, mm6
-
-;;
-;; mm2 = pair of consecutive ypositions
-;;
-		movd		mm2, [ds_yfrac]
-		pslld		mm2, mm7
-		movq		mm6, mm2
-		paddd		mm6, mm4
-		punpckldq	mm2, mm6
-
-;;
-;; mm5 = nflatxshift
-;; mm6 = nflatyshift
-;;
-		movd		mm5, [nflatxshift]
-		movd		mm6, [nflatyshift]
-
-;;
-;; Mask is in memory due to lack of registers.
-;;
-		mov			eax, [nflatmask]
-		mov			[nflatmask64], eax
-		mov			[nflatmask64 + 4], eax
-
-
-;;
-;; Go until we reach a dword boundary.
-;;
-.unaligned:
-		test		edi, 3
-		jz			.alignedprep
-.stragglers:
-		cmp			ecx, 0
-		je			.done					;; If ecx == 0, we're finished.
-
-;;
-;; eax = ((yposition >> nflatyshift) & nflatmask) | (xposition >> nflatxshift)
-;;
-		movq		mm0, mm1				;; mm0 = xposition
-		movq		mm7, mm2				;; mm7 = yposition
-		paddd		mm1, mm3				;; xposition += xstep (once!)
-		paddd		mm2, mm4				;; yposition += ystep (once!)
-		psrld		mm0, mm5				;; shift
-		psrld		mm7, mm6				;; shift
-		pand		mm7, [nflatmask64]		;; mask
-		por			mm0, mm7				;; or x and y together
-
-		movd		eax, mm0				;; eax = index of first pixel
-		movzx		eax, byte [esi + eax]	;; al = source[eax]
-		movzx		eax, byte [ebx + eax]	;; al = colormap[al]
-
-		mov			[edi], al
-		add			edi, 1
-
-		sub			ecx, 1
-		jmp			.unaligned
-
-
-.alignedprep:
-;;
-;; We can double the steps now.
-;;
-		pslld		mm3, 1
-		pslld		mm4, 1
-
-
-;;
-;; Generate chunks of four pixels.
-;;
-.alignedloop:
-
-;;
-;; Make sure we have at least four pixels.
-;;
-		cmp			ecx, 4
-		jl			.prestragglers
-
-;;
-;; First two pixels.
-;;
-		movq		mm0, mm1				;; mm0 = xposition
-		movq		mm7, mm2				;; mm7 = yposition
-		paddd		mm1, mm3				;; xposition += xstep
-		paddd		mm2, mm4				;; yposition += ystep
-		psrld		mm0, mm5				;; shift
-		psrld		mm7, mm6				;; shift
-		pand		mm7, [nflatmask64]		;; mask
-		por			mm0, mm7				;; or x and y together
-
-		movd		eax, mm0				;; eax = index of first pixel
-		movzx		eax, byte [esi + eax]	;; al = source[eax]
-		movzx		ebp, byte [ebx + eax]	;; ebp = colormap[al]
-
-		punpckhdq	mm0, mm0				;; both dwords = high dword
-		movd		eax, mm0				;; eax = index of second pixel
-		movzx		eax, byte [esi + eax]	;; al = source[eax]
-		movzx		eax, byte [ebx + eax]	;; al = colormap[al]
-		shl			eax, 8					;; get pixel in right byte
-		or			ebp, eax				;; put pixel in ebp
-
-;;
-;; Next two pixels.
-;;
-		movq		mm0, mm1				;; mm0 = xposition
-		movq		mm7, mm2				;; mm7 = yposition
-		paddd		mm1, mm3				;; xposition += xstep
-		paddd		mm2, mm4				;; yposition += ystep
-		psrld		mm0, mm5				;; shift
-		psrld		mm7, mm6				;; shift
-		pand		mm7, [nflatmask64]		;; mask
-		por			mm0, mm7				;; or x and y together
-
-		movd		eax, mm0				;; eax = index of third pixel
-		movzx		eax, byte [esi + eax]	;; al = source[eax]
-		movzx		eax, byte [ebx + eax]	;; al = colormap[al]
-		shl			eax, 16					;; get pixel in right byte
-		or			ebp, eax				;; put pixel in ebp
-
-		punpckhdq	mm0, mm0				;; both dwords = high dword
-		movd		eax, mm0				;; eax = index of second pixel
-		movzx		eax, byte [esi + eax]	;; al = source[eax]
-		movzx		eax, byte [ebx + eax]	;; al = colormap[al]
-		shl			eax, 24					;; get pixel in right byte
-		or			ebp, eax				;; put pixel in ebp
-
-;;
-;; Write pixels.
-;;
-		mov			[edi], ebp
-		add			edi, 4
-
-		sub			ecx, 4
-		jmp			.alignedloop
-
-.prestragglers:
-;;
-;; Back to one step at a time.
-;;
-		psrad		mm3, 1
-		psrad		mm4, 1
-		jmp			.stragglers
-
-.done:
-;;
-;; Clear MMX state, or else FPU operations will go badly awry.
-;;
-		emms
-
-		pop			ebx
-		pop			edi
-		pop			esi
-		pop			ebp
-		ret
diff --git a/src/tmap_vc.nas b/src/tmap_vc.nas
deleted file mode 100644
index c85cf70035f8588387420d479725242bb708cc42..0000000000000000000000000000000000000000
--- a/src/tmap_vc.nas
+++ /dev/null
@@ -1,48 +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:
-;;      tmap_vc.nas
-;; DESCRIPTION:
-;;      Assembler optimised math code for Visual C++.
-
-
-[BITS 32]
-
-%macro cglobal 1
-%define %1 _%1
-[global %1]
-%endmacro
-
-[SECTION .text write]
-
-;----------------------------------------------------------------------------
-;fixed_t FixedMul (fixed_t a, fixed_t b)
-;----------------------------------------------------------------------------
-cglobal FixedMul
-;       align   16
-FixedMul:
-        mov     eax,[esp+4]
-        imul    dword [esp+8]
-        shrd    eax,edx,16
-        ret
-
-;----------------------------------------------------------------------------
-;fixed_t FixedDiv2 (fixed_t a, fixed_t b);
-;----------------------------------------------------------------------------
-cglobal FixedDiv2
-;       align   16
-FixedDiv2:
-        mov     eax,[esp+4]
-        mov     edx,eax                 ;; these two instructions allow the next
-        sar     edx,31                  ;; two to pair, on the Pentium processor.
-        shld    edx,eax,16
-        sal     eax,16
-        idiv    dword [esp+8]
-        ret
diff --git a/src/hardware/u_list.c b/src/u_list.c
similarity index 99%
rename from src/hardware/u_list.c
rename to src/u_list.c
index dc49a74e7f7ab1a0502fbf5fb5f13cbbad888275..d5cfd5717ac2786e846abaf61c1dd91e2de184fe 100644
--- a/src/hardware/u_list.c
+++ b/src/u_list.c
@@ -8,7 +8,7 @@
 */
 
 #include "u_list.h"
-#include "../z_zone.h"
+#include "z_zone.h"
 
 // Utility for managing
 // structures in a linked
diff --git a/src/hardware/u_list.h b/src/u_list.h
similarity index 100%
rename from src/hardware/u_list.h
rename to src/u_list.h
diff --git a/src/v_video.c b/src/v_video.c
index 461a5e3bc7671684f5fdcc62a8c1a728ea913a55..3f958b286cdfdcc275a23c5822605812d218c242 100644
--- a/src/v_video.c
+++ b/src/v_video.c
@@ -447,12 +447,6 @@ static void CV_palette_OnChange(void)
 	V_SetPalette(0);
 }
 
-#if defined (__GNUC__) && defined (__i386__) && !defined (NOASM) && !defined (__APPLE__) && !defined (NORUSEASM)
-void VID_BlitLinearScreen_ASM(const UINT8 *srcptr, UINT8 *destptr, INT32 width, INT32 height, size_t srcrowbytes,
-	size_t destrowbytes);
-#define HAVE_VIDCOPY
-#endif
-
 static void CV_constextsize_OnChange(void)
 {
 	if (!con_refresh)
@@ -466,9 +460,6 @@ static void CV_constextsize_OnChange(void)
 void VID_BlitLinearScreen(const UINT8 *srcptr, UINT8 *destptr, INT32 width, INT32 height, size_t srcrowbytes,
 	size_t destrowbytes)
 {
-#ifdef HAVE_VIDCOPY
-    VID_BlitLinearScreen_ASM(srcptr,destptr,width,height,srcrowbytes,destrowbytes);
-#else
 	if (srcrowbytes == destrowbytes)
 		M_Memcpy(destptr, srcptr, srcrowbytes * height);
 	else
@@ -481,7 +472,6 @@ void VID_BlitLinearScreen(const UINT8 *srcptr, UINT8 *destptr, INT32 width, INT3
 			srcptr += srcrowbytes;
 		}
 	}
-#endif
 }
 
 static UINT8 hudplusalpha[11]  = { 10,  8,  6,  4,  2,  0,  0,  0,  0,  0,  0};
diff --git a/src/version.h b/src/version.h
index 083c531343cf0dd0628133110d338374fde71231..3242cad672df6757e74741ca482a403f7544e31b 100644
--- a/src/version.h
+++ b/src/version.h
@@ -1,4 +1,4 @@
-#define SRB2VERSION "2.2.11"/* this must be the first line, for cmake !! */
+#define SRB2VERSION "2.2.13"/* 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 52
+#define MODVERSION 54
 
 // Define this as a prerelease version suffix (pre#, RC#)
 //#define BETAVERSION "pre1"
diff --git a/src/vid_copy.s b/src/vid_copy.s
deleted file mode 100644
index 1473a3856f192145e3739738de85bd4f6cb96222..0000000000000000000000000000000000000000
--- a/src/vid_copy.s
+++ /dev/null
@@ -1,61 +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  vid_copy.s
-/// \brief code for updating the linear frame buffer screen.
-
-#include "asm_defs.inc"           // structures, must match the C structures!
-
-// DJGPPv2 is as fast as this one, but then someone may compile with a less
-// good version of DJGPP than mine, so this little asm will do the trick!
-
-#define srcptr          4+16
-#define destptr         8+16
-#define width           12+16
-#define height          16+16
-#define srcrowbytes     20+16
-#define destrowbytes    24+16
-
-// VID_BlitLinearScreen( src, dest, width, height, srcwidth, destwidth );
-//         width is given as BYTES
-
-#ifdef __i386__
-
-.globl C(VID_BlitLinearScreen_ASM)
-C(VID_BlitLinearScreen_ASM):
-    pushl   %ebp                // preserve caller's stack frame
-    pushl   %edi
-    pushl   %esi                // preserve register variables
-    pushl   %ebx
-
-    cld
-    movl    srcptr(%esp),%esi
-    movl    destptr(%esp),%edi
-    movl    width(%esp),%ebx
-    movl    srcrowbytes(%esp),%eax
-    subl    %ebx,%eax
-    movl    destrowbytes(%esp),%edx
-    subl    %ebx,%edx
-    shrl    $2,%ebx
-    movl    height(%esp),%ebp
-LLRowLoop:
-    movl    %ebx,%ecx
-    rep/movsl   (%esi),(%edi)
-    addl    %eax,%esi
-    addl    %edx,%edi
-    decl    %ebp
-    jnz     LLRowLoop
-
-    popl    %ebx                // restore register variables
-    popl    %esi
-    popl    %edi
-    popl    %ebp                // restore the caller's stack frame
-
-    ret
-#endif
diff --git a/src/w_wad.c b/src/w_wad.c
index 456afef7b9edd18fe26d188dd4326ea8469f2007..d012182f18208bd05435c9f7ee8e43d0f27256e2 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"
@@ -201,7 +201,7 @@ FILE *W_OpenWadFile(const char **filename, boolean useerrors)
 }
 
 // Look for all DEHACKED and Lua scripts inside a PK3 archive.
-static inline void W_LoadDehackedLumpsPK3(UINT16 wadnum, boolean mainfile)
+static void W_LoadDehackedLumpsPK3(UINT16 wadnum, boolean mainfile)
 {
 	UINT16 posStart, posEnd;
 
@@ -241,7 +241,7 @@ static inline void W_LoadDehackedLumpsPK3(UINT16 wadnum, boolean mainfile)
 }
 
 // search for all DEHACKED lump in all wads and load it
-static inline void W_LoadDehackedLumps(UINT16 wadnum, boolean mainfile)
+static void W_LoadDehackedLumps(UINT16 wadnum, boolean mainfile)
 {
 	UINT16 lump;
 
@@ -305,7 +305,7 @@ static inline void W_LoadDehackedLumps(UINT16 wadnum, boolean mainfile)
   * \param resblock resulting MD5 checksum
   * \return 0 if MD5 checksum was made, and is at resblock, 1 if error was found
   */
-static inline INT32 W_MakeFileMD5(const char *filename, void *resblock)
+static INT32 W_MakeFileMD5(const char *filename, void *resblock)
 {
 #ifdef NOMD5
 	(void)filename;
@@ -972,7 +972,7 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 	// add the wadfile
 	//
 	CONS_Printf(M_GetText("Added file %s (%u lumps)\n"), filename, numlumps);
-	wadfiles = Z_Realloc(wadfiles, sizeof(wadfile_t) * (numwadfiles + 1), PU_STATIC, NULL);
+	wadfiles = Z_Realloc(wadfiles, sizeof(wadfile_t *) * (numwadfiles + 1), PU_STATIC, NULL);
 	wadfiles[numwadfiles] = wadfile;
 	numwadfiles++; // must come BEFORE W_LoadDehackedLumps, so any addfile called by COM_BufInsertText called by Lua doesn't overwrite what we just loaded
 
@@ -1150,6 +1150,7 @@ UINT16 W_InitFolder(const char *path, boolean mainfile, boolean startup)
 	Z_Calloc(numlumps * sizeof (*wadfile->patchcache), PU_STATIC, &wadfile->patchcache);
 
 	CONS_Printf(M_GetText("Added folder %s (%u files, %u folders)\n"), fn, numlumps, foldercount);
+	wadfiles = Z_Realloc(wadfiles, sizeof(wadfile_t *) * (numwadfiles + 1), PU_STATIC, NULL);
 	wadfiles[numwadfiles] = wadfile;
 	numwadfiles++;
 
@@ -1956,7 +1957,7 @@ void *W_CacheLumpNumForce(lumpnum_t lumpnum, INT32 tag)
 // return false.
 //
 // no outside code uses the PWAD form, for now
-static inline boolean W_IsLumpCachedPWAD(UINT16 wad, UINT16 lump, void *ptr)
+static boolean W_IsLumpCachedPWAD(UINT16 wad, UINT16 lump, void *ptr)
 {
 	void *lcache;
 
@@ -1988,7 +1989,7 @@ boolean W_IsLumpCached(lumpnum_t lumpnum, void *ptr)
 // return false.
 //
 // no outside code uses the PWAD form, for now
-static inline boolean W_IsPatchCachedPWAD(UINT16 wad, UINT16 lump, void *ptr)
+static boolean W_IsPatchCachedPWAD(UINT16 wad, UINT16 lump, void *ptr)
 {
 	void *lcache;
 
diff --git a/src/win32/Srb2win.rc b/src/win32/Srb2win.rc
index 869c0e7d36e24a86f299d6013ec933c69a92ec4b..b699007463ad3f37527367b40e4a1c29411012ab 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,11,0
- PRODUCTVERSION 2,2,11,0
+ FILEVERSION 2,2,13,0
+ PRODUCTVERSION 2,2,13,0
  FILEFLAGSMASK 0x3fL
 #ifdef _DEBUG
  FILEFLAGS 0x1L
diff --git a/src/y_inter.c b/src/y_inter.c
index 02d01233e1136f7e332180de034484ffb73fb5d8..69dc931ba8cfde0c816baeb8508ce02f758c10bb 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"
@@ -998,8 +998,7 @@ void Y_Ticker(void)
 	if (paused || P_AutoPause())
 		return;
 
-	LUA_HookBool(intertype == int_spec && stagefailed,
-			HOOK(IntermissionThinker));
+	LUA_HookBool(stagefailed, HOOK(IntermissionThinker));
 
 	intertic++;
 
@@ -1092,12 +1091,14 @@ void Y_Ticker(void)
 			S_StartSound(NULL, (gottoken ? sfx_token : sfx_chchng)); // cha-ching!
 
 			// Update when done with tally
-			if (!(netgame || multiplayer) && !demoplayback)
+			if (!demoplayback)
 			{
-				if (M_UpdateUnlockablesAndExtraEmblems())
+				M_SilentUpdateUnlockablesAndEmblems(serverGamedata);
+
+				if (M_UpdateUnlockablesAndExtraEmblems(clientGamedata))
 					S_StartSound(NULL, sfx_s3k68);
 
-				G_SaveGameData();
+				G_SaveGameData(clientGamedata);
 			}
 		}
 		else if (!(intertic & 1))
@@ -1228,12 +1229,14 @@ void Y_Ticker(void)
 			S_StartSound(NULL, (gottoken ? sfx_token : sfx_chchng)); // cha-ching!
 
 			// Update when done with tally
-			if (!(netgame || multiplayer) && !demoplayback)
+			if (!demoplayback)
 			{
-				if (M_UpdateUnlockablesAndExtraEmblems())
+				M_SilentUpdateUnlockablesAndEmblems(serverGamedata);
+
+				if (M_UpdateUnlockablesAndExtraEmblems(clientGamedata))
 					S_StartSound(NULL, sfx_s3k68);
 
-				G_SaveGameData();
+				G_SaveGameData(clientGamedata);
 			}
 		}
 		else if (!(intertic & 1))
@@ -1473,10 +1476,10 @@ void Y_StartIntermission(void)
 				if (players[consoleplayer].charflags & SF_SUPER)
 				{
 					strcpy(data.spec.passed3, "can now become");
-					snprintf(data.spec.passed4,
-						sizeof data.spec.passed4, "Super %s",
-						skins[players[consoleplayer].skin].realname);
-					data.spec.passed4[sizeof data.spec.passed4 - 1] = '\0';
+					if (strlen(skins[players[consoleplayer].skin].supername) > 20) //too long, use generic
+						strcpy(data.spec.passed4, "their super form");
+					else
+						strcpy(data.spec.passed4, skins[players[consoleplayer].skin].supername);
 				}
 			}
 			else
@@ -2040,7 +2043,7 @@ static void Y_AwardCoopBonuses(void)
 	y_bonus_t localbonuses[4];
 
 	// set score/total first
-	data.coop.total = 0;
+	data.coop.total = players[consoleplayer].recordscore;
 	data.coop.score = players[consoleplayer].score;
 	data.coop.gotperfbonus = -1;
 	memset(data.coop.bonuses, 0, sizeof(data.coop.bonuses));
@@ -2061,6 +2064,9 @@ static void Y_AwardCoopBonuses(void)
 			players[i].score += localbonuses[j].points;
 			if (players[i].score > MAXSCORE)
 				players[i].score = MAXSCORE;
+			players[i].recordscore += localbonuses[j].points;
+			if (players[i].recordscore > MAXSCORE)
+				players[i].recordscore = MAXSCORE;
 		}
 
 		ptlives = min(
@@ -2117,6 +2123,10 @@ static void Y_AwardSpecialStageBonus(void)
 		players[i].score += localbonuses[1].points;
 		if (players[i].score > MAXSCORE)
 			players[i].score = MAXSCORE;
+		players[i].recordscore += localbonuses[0].points;
+		players[i].recordscore += localbonuses[1].points;
+		if (players[i].recordscore > MAXSCORE)
+			players[i].recordscore = MAXSCORE;
 
 		// grant extra lives right away since tally is faked
 		ptlives = min(
diff --git a/src/z_zone.c b/src/z_zone.c
index 11c4bcb2c789f6d58715ace2a59198ce4d83c98b..5750f8ae016becd9c9548b690b0e7fd5b32ec7cb 100644
--- a/src/z_zone.c
+++ b/src/z_zone.c
@@ -51,27 +51,11 @@ static boolean Z_calloc = false;
 //#define ZDEBUG2
 #endif
 
-struct memblock_s;
-
-typedef struct
-{
-	struct memblock_s *block; // Describing this memory
-	UINT32 id; // Should be ZONEID
-} ATTRPACK memhdr_t;
-
-// Some code might want aligned memory. Assume it wants memory n bytes
-// aligned -- then we allocate n-1 extra bytes and return a pointer to
-// the first byte aligned as requested.
-// Thus, "real" is the pointer we get from malloc() and will free()
-// later, but "hdr" is where the memhdr_t starts.
-// For non-aligned allocations they will be the same.
 typedef struct memblock_s
 {
-	void *real;
-	memhdr_t *hdr;
-
 	void **user;
 	INT32 tag; // purgelevel
+	UINT32 id; // Should be ZONEID
 
 	size_t size; // including the header and blocks
 	size_t realsize; // size of real data only
@@ -82,7 +66,10 @@ typedef struct memblock_s
 #endif
 
 	struct memblock_s *next, *prev;
-} ATTRPACK memblock_t;
+} memblock_t;
+
+#define MEMORY(x) (void *)((uintptr_t)(x) + sizeof(memblock_t))
+#define MEMBLOCK(x) (memblock_t *)((uintptr_t)(x) - sizeof(memblock_t))
 
 // both the head and tail of the zone memory block list
 static memblock_t head;
@@ -128,64 +115,6 @@ void Z_Init(void)
 // Zone memory allocation
 // ----------------------
 
-/** Returns the corresponding memblock_t for a given memory block.
-  *
-  * \param ptr A pointer to allocated memory,
-  *             assumed to have been allocated with Z_Malloc/Z_Calloc.
-  * \param func A string containing the name of the function that called this,
-  *              to be printed if the function I_Errors
-  * \return A pointer to the memblock_t for the given memory.
-  * \sa Z_Free, Z_ReallocAlign
-  */
-#ifdef ZDEBUG
-#define Ptr2Memblock(s, f) Ptr2Memblock2(s, f, __FILE__, __LINE__)
-static memblock_t *Ptr2Memblock2(void *ptr, const char* func, const char *file, INT32 line)
-#else
-static memblock_t *Ptr2Memblock(void *ptr, const char* func)
-#endif
-{
-	memhdr_t *hdr;
-	memblock_t *block;
-
-	if (ptr == NULL)
-		return NULL;
-
-#ifdef ZDEBUG2
-	CONS_Printf("%s %s:%d\n", func, file, line);
-#endif
-
-	hdr = (memhdr_t *)((UINT8 *)ptr - sizeof *hdr);
-
-#ifdef VALGRIND_MAKE_MEM_DEFINED
-	VALGRIND_MAKE_MEM_DEFINED(hdr, sizeof *hdr);
-#endif
-
-#ifdef VALGRIND_MEMPOOL_EXISTS
-	if (!VALGRIND_MEMPOOL_EXISTS(hdr->block))
-	{
-#ifdef ZDEBUG
-		I_Error("%s: bad memblock from %s:%d", func, file, line);
-#else
-		I_Error("%s: bad memblock", func);
-#endif
-	}
-#endif
-	if (hdr->id != ZONEID)
-	{
-#ifdef ZDEBUG
-		I_Error("%s: wrong id from %s:%d", func, file, line);
-#else
-		I_Error("%s: wrong id", func);
-#endif
-	}
-	block = hdr->block;
-#ifdef VALGRIND_MAKE_MEM_NOACCESS
-	VALGRIND_MAKE_MEM_NOACCESS(hdr, sizeof *hdr);
-#endif
-	return block;
-
-}
-
 /** Frees allocated memory.
   *
   * \param ptr A pointer to allocated memory,
@@ -207,10 +136,14 @@ void Z_Free(void *ptr)
 	CONS_Debug(DBG_MEMORY, "Z_Free %s:%d\n", file, line);
 #endif
 
+	block = MEMBLOCK(ptr);
+#ifdef PARANOIA
+	if (block->id != ZONEID)
 #ifdef ZDEBUG
-	block = Ptr2Memblock2(ptr, "Z_Free", file, line);
+		I_Error("Z_Free at %s:%d: wrong id", file, line);
 #else
-	block = Ptr2Memblock(ptr, "Z_Free");
+		I_Error("Z_Free: wrong id");
+#endif
 #endif
 
 #ifdef ZDEBUG
@@ -229,8 +162,6 @@ void Z_Free(void *ptr)
 	if (block->user != NULL)
 		*block->user = NULL;
 
-	// Free the memory and get rid of the block.
-	free(block->real);
 #ifdef VALGRIND_DESTROY_MEMPOOL
 	VALGRIND_DESTROY_MEMPOOL(block);
 #endif
@@ -287,34 +218,17 @@ void *Z_Malloc2(size_t size, INT32 tag, void *user, INT32 alignbits,
 void *Z_MallocAlign(size_t size, INT32 tag, void *user, INT32 alignbits)
 #endif
 {
-	size_t extrabytes = (1<<alignbits) - 1;
-	size_t padsize = 0;
 	memblock_t *block;
 	void *ptr;
-	memhdr_t *hdr;
-	void *given;
-	size_t blocksize = extrabytes + sizeof *hdr + size;
+	(void)(alignbits); // no longer used, so silence warnings.
 
 #ifdef ZDEBUG2
 	CONS_Debug(DBG_MEMORY, "Z_Malloc %s:%d\n", file, line);
 #endif
 
-	if (blocksize < size)/* overflow check */
-		I_Error("You are allocating memory too large!");
-
-	block = xm(sizeof *block);
-#ifdef HAVE_VALGRIND
-	padsize += (1<<sizeof(size_t))*2;
-#endif
-	ptr = xm(blocksize + padsize*2);
-
-	// This horrible calculation makes sure that "given" is aligned
-	// properly.
-	given = (void *)((size_t)((UINT8 *)ptr + extrabytes + sizeof *hdr + padsize/2)
-		& ~extrabytes);
-
-	// The mem header lives 'sizeof (memhdr_t)' bytes before given.
-	hdr = (memhdr_t *)((UINT8 *)given - sizeof *hdr);
+	block = xm(sizeof (memblock_t) + size);
+	ptr = MEMORY(block);
+	I_Assert((intptr_t)ptr % sizeof (void *) == 0);
 
 #ifdef HAVE_VALGRIND
 	Z_calloc = false;
@@ -325,41 +239,31 @@ void *Z_MallocAlign(size_t size, INT32 tag, void *user, INT32 alignbits)
 	head.next = block;
 	block->next->prev = block;
 
-	block->real = ptr;
-	block->hdr = hdr;
 	block->tag = tag;
 	block->user = NULL;
 #ifdef ZDEBUG
 	block->ownerline = line;
 	block->ownerfile = file;
 #endif
-	block->size = blocksize;
+	block->size = sizeof (memblock_t) + size;
 	block->realsize = size;
 
 #ifdef VALGRIND_CREATE_MEMPOOL
-	VALGRIND_CREATE_MEMPOOL(block, padsize, Z_calloc);
+	VALGRIND_CREATE_MEMPOOL(block, size, Z_calloc);
 #endif
-//#ifdef VALGRIND_MEMPOOL_ALLOC
-//	VALGRIND_MEMPOOL_ALLOC(block, hdr, size + sizeof *hdr);
-//#endif
 
-	hdr->id = ZONEID;
-	hdr->block = block;
-
-#ifdef VALGRIND_MAKE_MEM_NOACCESS
-	VALGRIND_MAKE_MEM_NOACCESS(hdr, sizeof *hdr);
-#endif
+	block->id = ZONEID;
 
 	if (user != NULL)
 	{
 		block->user = user;
-		*(void **)user = given;
+		*(void **)user = ptr;
 	}
 	else if (tag >= PU_PURGELEVEL)
 		I_Error("Z_Malloc: attempted to allocate purgable block "
 			"(size %s) with no user", sizeu1(size));
 
-	return given;
+	return ptr;
 }
 
 /** The Z_CallocAlign function.
@@ -436,10 +340,14 @@ void *Z_ReallocAlign(void *ptr, size_t size, INT32 tag, void *user, INT32 alignb
 #endif
 	}
 
+	block = MEMBLOCK(ptr);
+#ifdef PARANOIA
+	if (block->id != ZONEID)
 #ifdef ZDEBUG
-	block = Ptr2Memblock2(ptr, "Z_Realloc", file, line);
+		I_Error("Z_ReallocAlign at %s:%d: wrong id", file, line);
 #else
-	block = Ptr2Memblock(ptr, "Z_Realloc");
+		I_Error("Z_ReallocAlign: wrong id");
+#endif
 #endif
 
 	if (block == NULL)
@@ -490,9 +398,8 @@ void Z_FreeTags(INT32 lowtag, INT32 hightag)
 	for (block = head.next; block != &head; block = next)
 	{
 		next = block->next; // get link before freeing
-
 		if (block->tag >= lowtag && block->tag <= hightag)
-			Z_Free((UINT8 *)block->hdr + sizeof *block->hdr);
+			Z_Free(MEMORY(block));
 	}
 }
 
@@ -515,7 +422,7 @@ void Z_IterateTags(INT32 lowtag, INT32 hightag, boolean (*iterfunc)(void *))
 
 		if (block->tag >= lowtag && block->tag <= hightag)
 		{
-			void *mem = (UINT8 *)block->hdr + sizeof *block->hdr;
+			void *mem = MEMORY(block);
 			boolean free = iterfunc(mem);
 			if (free)
 				Z_Free(mem);
@@ -560,15 +467,13 @@ void Z_CheckMemCleanup(void)
 void Z_CheckHeap(INT32 i)
 {
 	memblock_t *block;
-	memhdr_t *hdr;
 	UINT32 blocknumon = 0;
 	void *given;
 
 	for (block = head.next; block != &head; block = block->next)
 	{
 		blocknumon++;
-		hdr = block->hdr;
-		given = (UINT8 *)hdr + sizeof *hdr;
+		given = MEMORY(block);
 #ifdef ZDEBUG2
 		CONS_Debug(DBG_MEMORY, "block %u owned by %s:%d\n",
 			blocknumon, block->ownerfile, block->ownerline);
@@ -584,7 +489,7 @@ void Z_CheckHeap(INT32 i)
 #ifdef ZDEBUG
 				, block->ownerfile, block->ownerline
 #endif
-			        );
+				);
 		}
 #endif
 		if (block->user != NULL && *(block->user) != given)
@@ -597,7 +502,7 @@ void Z_CheckHeap(INT32 i)
 #ifdef ZDEBUG
 				, block->ownerfile, block->ownerline
 #endif
-			       );
+				);
 		}
 		if (block->next->prev != block)
 		{
@@ -609,7 +514,7 @@ void Z_CheckHeap(INT32 i)
 #ifdef ZDEBUG
 				, block->ownerfile, block->ownerline
 #endif
-			       );
+				);
 		}
 		if (block->prev->next != block)
 		{
@@ -621,25 +526,9 @@ void Z_CheckHeap(INT32 i)
 #ifdef ZDEBUG
 				, block->ownerfile, block->ownerline
 #endif
-			       );
+				);
 		}
-#ifdef VALGRIND_MAKE_MEM_DEFINED
-		VALGRIND_MAKE_MEM_DEFINED(hdr, sizeof *hdr);
-#endif
-		if (hdr->block != block)
-		{
-			I_Error("Z_CheckHeap %d: block %u"
-#ifdef ZDEBUG
-				"(owned by %s:%d)"
-#endif
-				" doesn't have linkback from allocated memory",
-				i, blocknumon
-#ifdef ZDEBUG
-				, block->ownerfile, block->ownerline
-#endif
-					);
-		}
-		if (hdr->id != ZONEID)
+		if (block->id != ZONEID)
 		{
 			I_Error("Z_CheckHeap %d: block %u"
 #ifdef ZDEBUG
@@ -649,11 +538,8 @@ void Z_CheckHeap(INT32 i)
 #ifdef ZDEBUG
 				, block->ownerfile, block->ownerline
 #endif
-					);
+				);
 		}
-#ifdef VALGRIND_MAKE_MEM_NOACCESS
-	VALGRIND_MAKE_MEM_NOACCESS(hdr, sizeof *hdr);
-#endif
 	}
 }
 
@@ -675,35 +561,14 @@ void Z_ChangeTag(void *ptr, INT32 tag)
 #endif
 {
 	memblock_t *block;
-	memhdr_t *hdr;
 
 	if (ptr == NULL)
 		return;
 
-	hdr = (memhdr_t *)((UINT8 *)ptr - sizeof *hdr);
-
-#ifdef VALGRIND_MAKE_MEM_DEFINED
-	VALGRIND_MAKE_MEM_DEFINED(hdr, sizeof *hdr);
-#endif
+	block = MEMBLOCK(ptr);
 
-#ifdef VALGRIND_MEMPOOL_EXISTS
-	if (!VALGRIND_MEMPOOL_EXISTS(hdr->block))
-	{
-#ifdef PARANOIA
-		I_Error("Z_CT at %s:%d: bad memblock", file, line);
-#else
-		I_Error("Z_CT: bad memblock");
-#endif
-	}
-#endif
 #ifdef PARANOIA
-	if (hdr->id != ZONEID) I_Error("Z_CT at %s:%d: wrong id", file, line);
-#endif
-
-	block = hdr->block;
-
-#ifdef VALGRIND_MAKE_MEM_NOACCESS
-	VALGRIND_MAKE_MEM_NOACCESS(hdr, sizeof *hdr);
+	if (block->id != ZONEID) I_Error("Z_ChangeTag at %s:%d: wrong id", file, line);
 #endif
 
 	if (tag >= PU_PURGELEVEL && block->user == NULL)
@@ -727,25 +592,14 @@ void Z_SetUser(void *ptr, void **newuser)
 #endif
 {
 	memblock_t *block;
-	memhdr_t *hdr;
 
 	if (ptr == NULL)
 		return;
 
-	hdr = (memhdr_t *)((UINT8 *)ptr - sizeof *hdr);
-
-#ifdef VALGRIND_MAKE_MEM_DEFINED
-	VALGRIND_MAKE_MEM_DEFINED(hdr, sizeof *hdr);
-#endif
+	block = MEMBLOCK(ptr);
 
 #ifdef PARANOIA
-	if (hdr->id != ZONEID) I_Error("Z_CT at %s:%d: wrong id", file, line);
-#endif
-
-	block = hdr->block;
-
-#ifdef VALGRIND_MAKE_MEM_NOACCESS
-	VALGRIND_MAKE_MEM_NOACCESS(hdr, sizeof *hdr);
+	if (block->id != ZONEID) I_Error("Z_SetUser at %s:%d: wrong id", file, line);
 #endif
 
 	if (block->tag >= PU_PURGELEVEL && newuser == NULL)
diff --git a/src/z_zone.h b/src/z_zone.h
index c3cd4f01157b3871c6727c2a223a97d49a34bfdd..ce7af4a159555e3a6c2be83e8d0eadcf8a7a69eb 100644
--- a/src/z_zone.h
+++ b/src/z_zone.h
@@ -15,6 +15,7 @@
 #define __Z_ZONE__
 
 #include <stdio.h>
+#include "doomdef.h"
 #include "doomtype.h"
 
 #ifdef __GNUC__ // __attribute__ ((X))
@@ -101,10 +102,10 @@ void *Z_CallocAlign(size_t size, INT32 tag, void *user, INT32 alignbits) FUNCALL
 void *Z_ReallocAlign(void *ptr, size_t size, INT32 tag, void *user, INT32 alignbits) FUNCALLOC(2);
 #endif
 
-// Alloc with no alignment
-#define Z_Malloc(s,t,u)    Z_MallocAlign(s, t, u, 0)
-#define Z_Calloc(s,t,u)    Z_CallocAlign(s, t, u, 0)
-#define Z_Realloc(p,s,t,u) Z_ReallocAlign(p, s, t, u, 0)
+// Alloc with standard alignment
+#define Z_Malloc(s,t,u)    Z_MallocAlign(s, t, u, sizeof(void *))
+#define Z_Calloc(s,t,u)    Z_CallocAlign(s, t, u, sizeof(void *))
+#define Z_Realloc(p,s,t,u) Z_ReallocAlign(p, s, t, u, sizeof(void *))
 
 // Free all memory by tag
 // these don't give line numbers for ZDEBUG currently though
diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt
index 7aff16601efd49d71693a137a4aab7b98721e7fa..f33b3bf3f836b86b98fda8e78f73ec5be19758d8 100644
--- a/thirdparty/CMakeLists.txt
+++ b/thirdparty/CMakeLists.txt
@@ -9,521 +9,13 @@ else()
 	set(NOT_SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES ON)
 endif()
 
-
-if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}")
-	CPMAddPackage(
-		NAME SDL2
-		VERSION 2.24.2
-		URL "https://github.com/libsdl-org/SDL/archive/refs/tags/release-2.24.2.zip"
-		EXCLUDE_FROM_ALL ON
-		OPTIONS
-			"BUILD_SHARED_LIBS ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
-			"SDL_SHARED ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
-			"SDL_STATIC ${NOT_SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
-			"SDL_TEST OFF"
-			"SDL2_DISABLE_SDL2MAIN ON"
-			"SDL2_DISABLE_INSTALL ON"
-	)
-endif()
-
-if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}")
-	CPMAddPackage(
-		NAME SDL2_mixer
-		VERSION 2.6.2
-		URL "https://github.com/libsdl-org/SDL_mixer/archive/refs/tags/release-2.6.2.zip"
-		EXCLUDE_FROM_ALL ON
-		OPTIONS
-			"BUILD_SHARED_LIBS ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
-			"SDL2MIXER_INSTALL OFF"
-			"SDL2MIXER_DEPS_SHARED OFF"
-			"SDL2MIXER_SAMPLES OFF"
-			"SDL2MIXER_VENDORED ON"
-			"SDL2MIXER_FLAC ON"
-			"SDL2MIXER_FLAC_LIBFLAC OFF"
-			"SDL2MIXER_FLAC_DRFLAC ON"
-			"SDL2MIXER_MOD OFF"
-			"SDL2MIXER_MP3 ON"
-			"SDL2MIXER_MP3_DRMP3 ON"
-			"SDL2MIXER_MIDI ON"
-			"SDL2MIXER_OPUS OFF"
-			"SDL2MIXER_VORBIS STB"
-			"SDL2MIXER_WAVE ON"
-	)
-endif()
-
-if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}")
-	CPMAddPackage(
-		NAME ZLIB
-		VERSION 1.2.13
-		URL "https://github.com/madler/zlib/archive/refs/tags/v1.2.13.zip"
-		EXCLUDE_FROM_ALL
-		DOWNLOAD_ONLY YES
-	)
-	if(ZLIB_ADDED)
-		set(ZLIB_SRCS
-			crc32.h
-			deflate.h
-			gzguts.h
-			inffast.h
-			inffixed.h
-			inflate.h
-			inftrees.h
-			trees.h
-			zutil.h
-
-			adler32.c
-			compress.c
-			crc32.c
-			deflate.c
-			gzclose.c
-			gzlib.c
-			gzread.c
-			gzwrite.c
-			inflate.c
-			infback.c
-			inftrees.c
-			inffast.c
-			trees.c
-			uncompr.c
-			zutil.c
-		)
-		list(TRANSFORM ZLIB_SRCS PREPEND "${ZLIB_SOURCE_DIR}/")
-
-		configure_file("${ZLIB_SOURCE_DIR}/zlib.pc.cmakein" "${ZLIB_BINARY_DIR}/zlib.pc" @ONLY)
-		configure_file("${ZLIB_SOURCE_DIR}/zconf.h.cmakein" "${ZLIB_BINARY_DIR}/include/zconf.h" @ONLY)
-		configure_file("${ZLIB_SOURCE_DIR}/zlib.h" "${ZLIB_BINARY_DIR}/include/zlib.h" @ONLY)
-
-		add_library(ZLIB ${SRB2_INTERNAL_LIBRARY_TYPE} ${ZLIB_SRCS})
-		set_target_properties(ZLIB PROPERTIES
-			VERSION 1.2.13
-			OUTPUT_NAME "z"
-		)
-		target_include_directories(ZLIB PRIVATE "${ZLIB_SOURCE_DIR}")
-		target_include_directories(ZLIB PUBLIC "${ZLIB_BINARY_DIR}/include")
-		if(MSVC)
-			target_compile_definitions(ZLIB PRIVATE -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE)
-		endif()
-		add_library(ZLIB::ZLIB ALIAS ZLIB)
-	endif()
-endif()
-
 if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}")
-	CPMAddPackage(
-		NAME png
-		VERSION 1.6.38
-		URL "https://github.com/glennrp/libpng/archive/refs/tags/v1.6.38.zip"
-		# png cmake build is broken on msys/mingw32
-		DOWNLOAD_ONLY YES
-	)
-
-	if(png_ADDED)
-		# Since png's cmake build is broken, we're going to create a target manually
-		set(
-			PNG_SOURCES
-
-			png.h
-			pngconf.h
-
-			pngpriv.h
-			pngdebug.h
-			pnginfo.h
-			pngstruct.h
-
-			png.c
-			pngerror.c
-			pngget.c
-			pngmem.c
-			pngpread.c
-			pngread.c
-			pngrio.c
-			pngrtran.c
-			pngrutil.c
-			pngset.c
-			pngtrans.c
-			pngwio.c
-			pngwrite.c
-			pngwtran.c
-			pngwutil.c
-		)
-		list(TRANSFORM PNG_SOURCES PREPEND "${png_SOURCE_DIR}/")
-
-		add_custom_command(
-			OUTPUT "${png_BINARY_DIR}/include/png.h" "${png_BINARY_DIR}/include/pngconf.h"
-			COMMAND ${CMAKE_COMMAND} -E copy "${png_SOURCE_DIR}/png.h" "${png_SOURCE_DIR}/pngconf.h" "${png_BINARY_DIR}/include"
-			DEPENDS "${png_SOURCE_DIR}/png.h" "${png_SOURCE_DIR}/pngconf.h"
-			VERBATIM
-		)
-		add_custom_command(
-			OUTPUT "${png_BINARY_DIR}/include/pnglibconf.h"
-			COMMAND ${CMAKE_COMMAND} -E copy "${png_SOURCE_DIR}/scripts/pnglibconf.h.prebuilt" "${png_BINARY_DIR}/include/pnglibconf.h"
-			DEPENDS "${png_SOURCE_DIR}/scripts/pnglibconf.h.prebuilt"
-			VERBATIM
-		)
-		list(
-			APPEND PNG_SOURCES
-			"${png_BINARY_DIR}/include/png.h"
-			"${png_BINARY_DIR}/include/pngconf.h"
-			"${png_BINARY_DIR}/include/pnglibconf.h"
-		)
-		add_library(png "${SRB2_INTERNAL_LIBRARY_TYPE}" ${PNG_SOURCES})
-
-		# Disable ARM NEON since having it automatic breaks libpng external build on clang for some reason
-		target_compile_definitions(png PRIVATE -DPNG_ARM_NEON_OPT=0)
-
-		# The png includes need to be available to consumers
-		target_include_directories(png PUBLIC "${png_BINARY_DIR}/include")
-
-		# ... and these also need to be present only for png build
-		target_include_directories(png PRIVATE "${ZLIB_SOURCE_DIR}")
-		target_include_directories(png PRIVATE "${ZLIB_BINARY_DIR}")
-		target_include_directories(png PRIVATE "${png_BINARY_DIR}")
-
-		target_link_libraries(png PRIVATE ZLIB::ZLIB)
-		add_library(PNG::PNG ALIAS png)
-	endif()
+	include("cpm-sdl2.cmake")
+	include("cpm-sdl2-mixer.cmake")
+	include("cpm-zlib.cmake")
+	include("cpm-png.cmake")
+	include("cpm-curl.cmake")
+	include("cpm-openmpt.cmake")
 endif()
 
-if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}")
-	set(
-		internal_curl_options
-
-		"BUILD_CURL_EXE OFF"
-		"BUILD_SHARED_LIBS ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
-		"CURL_DISABLE_TESTS ON"
-		"HTTP_ONLY ON"
-		"CURL_DISABLE_CRYPTO_AUTH ON"
-		"CURL_DISABLE_NTLM ON"
-		"ENABLE_MANUAL OFF"
-		"ENABLE_THREADED_RESOLVER OFF"
-		"CURL_USE_LIBPSL OFF"
-		"CURL_USE_LIBSSH2 OFF"
-		"USE_LIBIDN2 OFF"
-		"CURL_ENABLE_EXPORT_TARGET OFF"
-	)
-	if(${CMAKE_SYSTEM} MATCHES Windows)
-		list(APPEND internal_curl_options "CURL_USE_OPENSSL OFF")
-		list(APPEND internal_curl_options "CURL_USE_SCHANNEL ON")
-	endif()
-	if(${CMAKE_SYSTEM} MATCHES Darwin)
-		list(APPEND internal_curl_options "CURL_USE_OPENSSL OFF")
-		list(APPEND internal_curl_options "CURL_USE_SECTRANSP ON")
-	endif()
-	if(${CMAKE_SYSTEM} MATCHES Linux)
-		list(APPEND internal_curl_options "CURL_USE_OPENSSL ON")
-	endif()
-
-	CPMAddPackage(
-		NAME curl
-		VERSION 7.86.0
-		URL "https://github.com/curl/curl/archive/refs/tags/curl-7_86_0.zip"
-		EXCLUDE_FROM_ALL ON
-		OPTIONS ${internal_curl_options}
-	)
-endif()
-
-if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}")
-	CPMAddPackage(
-		NAME openmpt
-		VERSION 0.4.30
-		URL "https://github.com/OpenMPT/openmpt/archive/refs/tags/libopenmpt-0.4.30.zip"
-		DOWNLOAD_ONLY ON
-	)
-
-	if(openmpt_ADDED)
-		set(
-			openmpt_SOURCES
-
-			# minimp3
-			# -DMPT_WITH_MINIMP3
-			include/minimp3/minimp3.c
-
-			common/mptStringParse.cpp
-			common/mptLibrary.cpp
-			common/Logging.cpp
-			common/Profiler.cpp
-			common/version.cpp
-			common/mptCPU.cpp
-			common/ComponentManager.cpp
-			common/mptOS.cpp
-			common/serialization_utils.cpp
-			common/mptStringFormat.cpp
-			common/FileReader.cpp
-			common/mptWine.cpp
-			common/mptPathString.cpp
-			common/mptAlloc.cpp
-			common/mptUUID.cpp
-			common/mptTime.cpp
-			common/mptString.cpp
-			common/mptFileIO.cpp
-			common/mptStringBuffer.cpp
-			common/mptRandom.cpp
-			common/mptIO.cpp
-			common/misc_util.cpp
-
-			common/mptCRC.h
-			common/mptLibrary.h
-			common/mptIO.h
-			common/version.h
-			common/stdafx.h
-			common/ComponentManager.h
-			common/Endianness.h
-			common/mptStringFormat.h
-			common/mptMutex.h
-			common/mptUUID.h
-			common/mptExceptionText.h
-			common/BuildSettings.h
-			common/mptAlloc.h
-			common/mptTime.h
-			common/FileReaderFwd.h
-			common/Logging.h
-			common/mptException.h
-			common/mptWine.h
-			common/mptStringBuffer.h
-			common/misc_util.h
-			common/mptBaseMacros.h
-			common/mptMemory.h
-			common/mptFileIO.h
-			common/serialization_utils.h
-			common/mptSpan.h
-			common/mptThread.h
-			common/FlagSet.h
-			common/mptString.h
-			common/mptStringParse.h
-			common/mptBaseUtils.h
-			common/mptRandom.h
-			common/CompilerDetect.h
-			common/FileReader.h
-			common/mptAssert.h
-			common/mptPathString.h
-			common/Profiler.h
-			common/mptOS.h
-			common/mptBaseTypes.h
-			common/mptCPU.h
-			common/mptBufferIO.h
-			common/versionNumber.h
-
-			soundlib/WAVTools.cpp
-			soundlib/ITTools.cpp
-			soundlib/AudioCriticalSection.cpp
-			soundlib/Load_stm.cpp
-			soundlib/MixerLoops.cpp
-			soundlib/Load_dbm.cpp
-			soundlib/ModChannel.cpp
-			soundlib/Load_gdm.cpp
-			soundlib/Snd_fx.cpp
-			soundlib/Load_mid.cpp
-			soundlib/mod_specifications.cpp
-			soundlib/Snd_flt.cpp
-			soundlib/Load_psm.cpp
-			soundlib/Load_far.cpp
-			soundlib/patternContainer.cpp
-			soundlib/Load_med.cpp
-			soundlib/Load_dmf.cpp
-			soundlib/Paula.cpp
-			soundlib/modcommand.cpp
-			soundlib/Message.cpp
-			soundlib/SoundFilePlayConfig.cpp
-			soundlib/Load_uax.cpp
-			soundlib/plugins/PlugInterface.cpp
-			soundlib/plugins/LFOPlugin.cpp
-			soundlib/plugins/PluginManager.cpp
-			soundlib/plugins/DigiBoosterEcho.cpp
-			soundlib/plugins/dmo/DMOPlugin.cpp
-			soundlib/plugins/dmo/Flanger.cpp
-			soundlib/plugins/dmo/Distortion.cpp
-			soundlib/plugins/dmo/ParamEq.cpp
-			soundlib/plugins/dmo/Gargle.cpp
-			soundlib/plugins/dmo/I3DL2Reverb.cpp
-			soundlib/plugins/dmo/Compressor.cpp
-			soundlib/plugins/dmo/WavesReverb.cpp
-			soundlib/plugins/dmo/Echo.cpp
-			soundlib/plugins/dmo/Chorus.cpp
-			soundlib/Load_ams.cpp
-			soundlib/tuningbase.cpp
-			soundlib/ContainerUMX.cpp
-			soundlib/Load_ptm.cpp
-			soundlib/ContainerXPK.cpp
-			soundlib/SampleFormatMP3.cpp
-			soundlib/tuning.cpp
-			soundlib/Sndfile.cpp
-			soundlib/ContainerMMCMP.cpp
-			soundlib/Load_amf.cpp
-			soundlib/Load_669.cpp
-			soundlib/modsmp_ctrl.cpp
-			soundlib/Load_mtm.cpp
-			soundlib/OggStream.cpp
-			soundlib/Load_plm.cpp
-			soundlib/Tables.cpp
-			soundlib/Load_c67.cpp
-			soundlib/Load_mod.cpp
-			soundlib/Load_sfx.cpp
-			soundlib/Sndmix.cpp
-			soundlib/load_j2b.cpp
-			soundlib/ModSequence.cpp
-			soundlib/SampleFormatFLAC.cpp
-			soundlib/ModInstrument.cpp
-			soundlib/Load_mo3.cpp
-			soundlib/ModSample.cpp
-			soundlib/Dlsbank.cpp
-			soundlib/Load_itp.cpp
-			soundlib/UpgradeModule.cpp
-			soundlib/MIDIMacros.cpp
-			soundlib/ContainerPP20.cpp
-			soundlib/RowVisitor.cpp
-			soundlib/Load_imf.cpp
-			soundlib/SampleFormatVorbis.cpp
-			soundlib/Load_dsm.cpp
-			soundlib/Load_mt2.cpp
-			soundlib/MixerSettings.cpp
-			soundlib/S3MTools.cpp
-			soundlib/Load_xm.cpp
-			soundlib/MIDIEvents.cpp
-			soundlib/pattern.cpp
-			soundlib/Load_digi.cpp
-			soundlib/Load_s3m.cpp
-			soundlib/tuningCollection.cpp
-			soundlib/SampleIO.cpp
-			soundlib/Dither.cpp
-			soundlib/Load_mdl.cpp
-			soundlib/OPL.cpp
-			soundlib/WindowedFIR.cpp
-			soundlib/SampleFormats.cpp
-			soundlib/Load_wav.cpp
-			soundlib/Load_it.cpp
-			soundlib/UMXTools.cpp
-			soundlib/Load_stp.cpp
-			soundlib/Load_okt.cpp
-			soundlib/Load_ult.cpp
-			soundlib/MixFuncTable.cpp
-			soundlib/SampleFormatOpus.cpp
-			soundlib/Fastmix.cpp
-			soundlib/Tagging.cpp
-			soundlib/ITCompression.cpp
-			soundlib/Load_dtm.cpp
-			soundlib/MPEGFrame.cpp
-			soundlib/XMTools.cpp
-			soundlib/SampleFormatMediaFoundation.cpp
-			soundlib/InstrumentExtensions.cpp
-
-			soundlib/MixerInterface.h
-			soundlib/SoundFilePlayConfig.h
-			soundlib/ModSample.h
-			soundlib/MIDIEvents.h
-			soundlib/ModSampleCopy.h
-			soundlib/patternContainer.h
-			soundlib/ChunkReader.h
-			soundlib/ITCompression.h
-			soundlib/Dither.h
-			soundlib/S3MTools.h
-			soundlib/MPEGFrame.h
-			soundlib/WAVTools.h
-			soundlib/mod_specifications.h
-			soundlib/ITTools.h
-			soundlib/RowVisitor.h
-			soundlib/plugins/PluginMixBuffer.h
-			soundlib/plugins/PluginStructs.h
-			soundlib/plugins/LFOPlugin.h
-			soundlib/plugins/PlugInterface.h
-			soundlib/plugins/DigiBoosterEcho.h
-			soundlib/plugins/OpCodes.h
-			soundlib/plugins/dmo/Echo.h
-			soundlib/plugins/dmo/I3DL2Reverb.h
-			soundlib/plugins/dmo/WavesReverb.h
-			soundlib/plugins/dmo/ParamEq.h
-			soundlib/plugins/dmo/Gargle.h
-			soundlib/plugins/dmo/DMOPlugin.h
-			soundlib/plugins/dmo/Chorus.h
-			soundlib/plugins/dmo/Compressor.h
-			soundlib/plugins/dmo/Distortion.h
-			soundlib/plugins/dmo/Flanger.h
-			soundlib/plugins/PluginManager.h
-			soundlib/SampleIO.h
-			soundlib/Container.h
-			soundlib/ModSequence.h
-			soundlib/UMXTools.h
-			soundlib/Message.h
-			soundlib/modcommand.h
-			soundlib/XMTools.h
-			soundlib/Snd_defs.h
-			soundlib/MixFuncTable.h
-			soundlib/pattern.h
-			soundlib/modsmp_ctrl.h
-			soundlib/Tagging.h
-			soundlib/tuningcollection.h
-			soundlib/Mixer.h
-			soundlib/FloatMixer.h
-			soundlib/AudioCriticalSection.h
-			soundlib/Tables.h
-			soundlib/tuningbase.h
-			soundlib/WindowedFIR.h
-			soundlib/Sndfile.h
-			soundlib/Paula.h
-			soundlib/ModInstrument.h
-			soundlib/Dlsbank.h
-			soundlib/IntMixer.h
-			soundlib/OPL.h
-			soundlib/Resampler.h
-			soundlib/ModChannel.h
-			soundlib/MixerSettings.h
-			soundlib/AudioReadTarget.h
-			soundlib/MixerLoops.h
-			soundlib/tuning.h
-			soundlib/MIDIMacros.h
-			soundlib/OggStream.h
-			soundlib/Loaders.h
-			soundlib/BitReader.h
-			soundlib/opal.h
-
-			sounddsp/AGC.cpp
-			sounddsp/EQ.cpp
-			sounddsp/DSP.cpp
-			sounddsp/Reverb.cpp
-			sounddsp/Reverb.h
-			sounddsp/EQ.h
-			sounddsp/DSP.h
-			sounddsp/AGC.h
-
-			libopenmpt/libopenmpt_c.cpp
-			libopenmpt/libopenmpt_cxx.cpp
-			libopenmpt/libopenmpt_impl.cpp
-			libopenmpt/libopenmpt_ext_impl.cpp
-		)
-		list(TRANSFORM openmpt_SOURCES PREPEND "${openmpt_SOURCE_DIR}/")
-
-		# -DLIBOPENMPT_BUILD
-		configure_file("openmpt_svn_version.h" "svn_version.h")
-		add_library(openmpt "${SRB2_INTERNAL_LIBRARY_TYPE}" ${openmpt_SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/svn_version.h)
-		if("${CMAKE_C_COMPILER_ID}" STREQUAL GNU OR "${CMAKE_C_COMPILER_ID}" STREQUAL Clang OR "${CMAKE_C_COMPILER_ID}" STREQUAL AppleClang)
-			target_compile_options(openmpt PRIVATE "-g0")
-		endif()
-		if("${CMAKE_SYSTEM_NAME}" STREQUAL Windows AND "${CMAKE_C_COMPILER_ID}" STREQUAL MSVC)
-			target_link_libraries(openmpt PRIVATE Rpcrt4)
-		endif()
-		target_compile_features(openmpt PRIVATE cxx_std_11)
-		target_compile_definitions(openmpt PRIVATE -DLIBOPENMPT_BUILD)
-
-		target_include_directories(openmpt PRIVATE "${openmpt_SOURCE_DIR}/common")
-		target_include_directories(openmpt PRIVATE "${openmpt_SOURCE_DIR}/src")
-		target_include_directories(openmpt PRIVATE "${openmpt_SOURCE_DIR}/include")
-		target_include_directories(openmpt PRIVATE "${openmpt_SOURCE_DIR}")
-		target_include_directories(openmpt PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
-
-		# I wish this wasn't necessary, but it is
-		target_include_directories(openmpt PUBLIC "${openmpt_SOURCE_DIR}")
-	endif()
-endif()
-
-if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}")
-	CPMAddPackage(
-		NAME libgme
-		VERSION 0.6.3
-		URL "https://bitbucket.org/mpyne/game-music-emu/get/e76bdc0cb916e79aa540290e6edd0c445879d3ba.zip"
-		EXCLUDE_FROM_ALL ON
-		OPTIONS
-			"BUILD_SHARED_LIBS ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
-			"ENABLE_UBSAN OFF"
-			"GME_YM2612_EMU MAME"
-	)
-	target_compile_features(gme PRIVATE cxx_std_11)
-	target_link_libraries(gme PRIVATE ZLIB::ZLIB)
-endif()
+include("cpm-libgme.cmake")
diff --git a/thirdparty/cpm-curl.cmake b/thirdparty/cpm-curl.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..3d8c6e61d46dee6f06f4e6d69beb09d1605f6584
--- /dev/null
+++ b/thirdparty/cpm-curl.cmake
@@ -0,0 +1,35 @@
+set(
+	internal_curl_options
+
+	"BUILD_CURL_EXE OFF"
+	"BUILD_SHARED_LIBS ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
+	"CURL_DISABLE_TESTS ON"
+	"HTTP_ONLY ON"
+	"CURL_DISABLE_CRYPTO_AUTH ON"
+	"CURL_DISABLE_NTLM ON"
+	"ENABLE_MANUAL OFF"
+	"ENABLE_THREADED_RESOLVER OFF"
+	"CURL_USE_LIBPSL OFF"
+	"CURL_USE_LIBSSH2 OFF"
+	"USE_LIBIDN2 OFF"
+	"CURL_ENABLE_EXPORT_TARGET OFF"
+)
+if(${CMAKE_SYSTEM} MATCHES Windows)
+	list(APPEND internal_curl_options "CURL_USE_OPENSSL OFF")
+	list(APPEND internal_curl_options "CURL_USE_SCHANNEL ON")
+endif()
+if(${CMAKE_SYSTEM} MATCHES Darwin)
+	list(APPEND internal_curl_options "CURL_USE_OPENSSL OFF")
+	list(APPEND internal_curl_options "CURL_USE_SECTRANSP ON")
+endif()
+if(${CMAKE_SYSTEM} MATCHES Linux)
+	list(APPEND internal_curl_options "CURL_USE_OPENSSL ON")
+endif()
+
+CPMAddPackage(
+	NAME curl
+	VERSION 7.86.0
+	URL "https://github.com/curl/curl/archive/refs/tags/curl-7_86_0.zip"
+	EXCLUDE_FROM_ALL ON
+	OPTIONS ${internal_curl_options}
+)
diff --git a/thirdparty/cpm-libgme.cmake b/thirdparty/cpm-libgme.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..f15bc3b31cb23ed7663ed950c14c1d6a0dc36567
--- /dev/null
+++ b/thirdparty/cpm-libgme.cmake
@@ -0,0 +1,16 @@
+CPMAddPackage(
+	NAME libgme
+	VERSION 0.6.3
+	URL "https://bitbucket.org/mpyne/game-music-emu/get/e76bdc0cb916e79aa540290e6edd0c445879d3ba.zip"
+	EXCLUDE_FROM_ALL ON
+	OPTIONS
+		"BUILD_SHARED_LIBS ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
+		"ENABLE_UBSAN OFF"
+		"GME_YM2612_EMU MAME"
+)
+
+if(libgme_ADDED)
+	target_compile_features(gme PRIVATE cxx_std_11)
+	# libgme's CMakeLists.txt already links this
+	#target_link_libraries(gme PRIVATE ZLIB::ZLIB)
+endif()
diff --git a/thirdparty/cpm-openmpt.cmake b/thirdparty/cpm-openmpt.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..01f7ff75f64d915799e432f2923a2c7e8c344466
--- /dev/null
+++ b/thirdparty/cpm-openmpt.cmake
@@ -0,0 +1,289 @@
+CPMAddPackage(
+	NAME openmpt
+	VERSION 0.4.30
+	URL "https://github.com/OpenMPT/openmpt/archive/refs/tags/libopenmpt-0.4.30.zip"
+	DOWNLOAD_ONLY ON
+)
+
+if(openmpt_ADDED)
+	set(
+		openmpt_SOURCES
+
+		# minimp3
+		# -DMPT_WITH_MINIMP3
+		include/minimp3/minimp3.c
+
+		common/mptStringParse.cpp
+		common/mptLibrary.cpp
+		common/Logging.cpp
+		common/Profiler.cpp
+		common/version.cpp
+		common/mptCPU.cpp
+		common/ComponentManager.cpp
+		common/mptOS.cpp
+		common/serialization_utils.cpp
+		common/mptStringFormat.cpp
+		common/FileReader.cpp
+		common/mptWine.cpp
+		common/mptPathString.cpp
+		common/mptAlloc.cpp
+		common/mptUUID.cpp
+		common/mptTime.cpp
+		common/mptString.cpp
+		common/mptFileIO.cpp
+		common/mptStringBuffer.cpp
+		common/mptRandom.cpp
+		common/mptIO.cpp
+		common/misc_util.cpp
+
+		common/mptCRC.h
+		common/mptLibrary.h
+		common/mptIO.h
+		common/version.h
+		common/stdafx.h
+		common/ComponentManager.h
+		common/Endianness.h
+		common/mptStringFormat.h
+		common/mptMutex.h
+		common/mptUUID.h
+		common/mptExceptionText.h
+		common/BuildSettings.h
+		common/mptAlloc.h
+		common/mptTime.h
+		common/FileReaderFwd.h
+		common/Logging.h
+		common/mptException.h
+		common/mptWine.h
+		common/mptStringBuffer.h
+		common/misc_util.h
+		common/mptBaseMacros.h
+		common/mptMemory.h
+		common/mptFileIO.h
+		common/serialization_utils.h
+		common/mptSpan.h
+		common/mptThread.h
+		common/FlagSet.h
+		common/mptString.h
+		common/mptStringParse.h
+		common/mptBaseUtils.h
+		common/mptRandom.h
+		common/CompilerDetect.h
+		common/FileReader.h
+		common/mptAssert.h
+		common/mptPathString.h
+		common/Profiler.h
+		common/mptOS.h
+		common/mptBaseTypes.h
+		common/mptCPU.h
+		common/mptBufferIO.h
+		common/versionNumber.h
+
+		soundlib/WAVTools.cpp
+		soundlib/ITTools.cpp
+		soundlib/AudioCriticalSection.cpp
+		soundlib/Load_stm.cpp
+		soundlib/MixerLoops.cpp
+		soundlib/Load_dbm.cpp
+		soundlib/ModChannel.cpp
+		soundlib/Load_gdm.cpp
+		soundlib/Snd_fx.cpp
+		soundlib/Load_mid.cpp
+		soundlib/mod_specifications.cpp
+		soundlib/Snd_flt.cpp
+		soundlib/Load_psm.cpp
+		soundlib/Load_far.cpp
+		soundlib/patternContainer.cpp
+		soundlib/Load_med.cpp
+		soundlib/Load_dmf.cpp
+		soundlib/Paula.cpp
+		soundlib/modcommand.cpp
+		soundlib/Message.cpp
+		soundlib/SoundFilePlayConfig.cpp
+		soundlib/Load_uax.cpp
+		soundlib/plugins/PlugInterface.cpp
+		soundlib/plugins/LFOPlugin.cpp
+		soundlib/plugins/PluginManager.cpp
+		soundlib/plugins/DigiBoosterEcho.cpp
+		soundlib/plugins/dmo/DMOPlugin.cpp
+		soundlib/plugins/dmo/Flanger.cpp
+		soundlib/plugins/dmo/Distortion.cpp
+		soundlib/plugins/dmo/ParamEq.cpp
+		soundlib/plugins/dmo/Gargle.cpp
+		soundlib/plugins/dmo/I3DL2Reverb.cpp
+		soundlib/plugins/dmo/Compressor.cpp
+		soundlib/plugins/dmo/WavesReverb.cpp
+		soundlib/plugins/dmo/Echo.cpp
+		soundlib/plugins/dmo/Chorus.cpp
+		soundlib/Load_ams.cpp
+		soundlib/tuningbase.cpp
+		soundlib/ContainerUMX.cpp
+		soundlib/Load_ptm.cpp
+		soundlib/ContainerXPK.cpp
+		soundlib/SampleFormatMP3.cpp
+		soundlib/tuning.cpp
+		soundlib/Sndfile.cpp
+		soundlib/ContainerMMCMP.cpp
+		soundlib/Load_amf.cpp
+		soundlib/Load_669.cpp
+		soundlib/modsmp_ctrl.cpp
+		soundlib/Load_mtm.cpp
+		soundlib/OggStream.cpp
+		soundlib/Load_plm.cpp
+		soundlib/Tables.cpp
+		soundlib/Load_c67.cpp
+		soundlib/Load_mod.cpp
+		soundlib/Load_sfx.cpp
+		soundlib/Sndmix.cpp
+		soundlib/load_j2b.cpp
+		soundlib/ModSequence.cpp
+		soundlib/SampleFormatFLAC.cpp
+		soundlib/ModInstrument.cpp
+		soundlib/Load_mo3.cpp
+		soundlib/ModSample.cpp
+		soundlib/Dlsbank.cpp
+		soundlib/Load_itp.cpp
+		soundlib/UpgradeModule.cpp
+		soundlib/MIDIMacros.cpp
+		soundlib/ContainerPP20.cpp
+		soundlib/RowVisitor.cpp
+		soundlib/Load_imf.cpp
+		soundlib/SampleFormatVorbis.cpp
+		soundlib/Load_dsm.cpp
+		soundlib/Load_mt2.cpp
+		soundlib/MixerSettings.cpp
+		soundlib/S3MTools.cpp
+		soundlib/Load_xm.cpp
+		soundlib/MIDIEvents.cpp
+		soundlib/pattern.cpp
+		soundlib/Load_digi.cpp
+		soundlib/Load_s3m.cpp
+		soundlib/tuningCollection.cpp
+		soundlib/SampleIO.cpp
+		soundlib/Dither.cpp
+		soundlib/Load_mdl.cpp
+		soundlib/OPL.cpp
+		soundlib/WindowedFIR.cpp
+		soundlib/SampleFormats.cpp
+		soundlib/Load_wav.cpp
+		soundlib/Load_it.cpp
+		soundlib/UMXTools.cpp
+		soundlib/Load_stp.cpp
+		soundlib/Load_okt.cpp
+		soundlib/Load_ult.cpp
+		soundlib/MixFuncTable.cpp
+		soundlib/SampleFormatOpus.cpp
+		soundlib/Fastmix.cpp
+		soundlib/Tagging.cpp
+		soundlib/ITCompression.cpp
+		soundlib/Load_dtm.cpp
+		soundlib/MPEGFrame.cpp
+		soundlib/XMTools.cpp
+		soundlib/SampleFormatMediaFoundation.cpp
+		soundlib/InstrumentExtensions.cpp
+
+		soundlib/MixerInterface.h
+		soundlib/SoundFilePlayConfig.h
+		soundlib/ModSample.h
+		soundlib/MIDIEvents.h
+		soundlib/ModSampleCopy.h
+		soundlib/patternContainer.h
+		soundlib/ChunkReader.h
+		soundlib/ITCompression.h
+		soundlib/Dither.h
+		soundlib/S3MTools.h
+		soundlib/MPEGFrame.h
+		soundlib/WAVTools.h
+		soundlib/mod_specifications.h
+		soundlib/ITTools.h
+		soundlib/RowVisitor.h
+		soundlib/plugins/PluginMixBuffer.h
+		soundlib/plugins/PluginStructs.h
+		soundlib/plugins/LFOPlugin.h
+		soundlib/plugins/PlugInterface.h
+		soundlib/plugins/DigiBoosterEcho.h
+		soundlib/plugins/OpCodes.h
+		soundlib/plugins/dmo/Echo.h
+		soundlib/plugins/dmo/I3DL2Reverb.h
+		soundlib/plugins/dmo/WavesReverb.h
+		soundlib/plugins/dmo/ParamEq.h
+		soundlib/plugins/dmo/Gargle.h
+		soundlib/plugins/dmo/DMOPlugin.h
+		soundlib/plugins/dmo/Chorus.h
+		soundlib/plugins/dmo/Compressor.h
+		soundlib/plugins/dmo/Distortion.h
+		soundlib/plugins/dmo/Flanger.h
+		soundlib/plugins/PluginManager.h
+		soundlib/SampleIO.h
+		soundlib/Container.h
+		soundlib/ModSequence.h
+		soundlib/UMXTools.h
+		soundlib/Message.h
+		soundlib/modcommand.h
+		soundlib/XMTools.h
+		soundlib/Snd_defs.h
+		soundlib/MixFuncTable.h
+		soundlib/pattern.h
+		soundlib/modsmp_ctrl.h
+		soundlib/Tagging.h
+		soundlib/tuningcollection.h
+		soundlib/Mixer.h
+		soundlib/FloatMixer.h
+		soundlib/AudioCriticalSection.h
+		soundlib/Tables.h
+		soundlib/tuningbase.h
+		soundlib/WindowedFIR.h
+		soundlib/Sndfile.h
+		soundlib/Paula.h
+		soundlib/ModInstrument.h
+		soundlib/Dlsbank.h
+		soundlib/IntMixer.h
+		soundlib/OPL.h
+		soundlib/Resampler.h
+		soundlib/ModChannel.h
+		soundlib/MixerSettings.h
+		soundlib/AudioReadTarget.h
+		soundlib/MixerLoops.h
+		soundlib/tuning.h
+		soundlib/MIDIMacros.h
+		soundlib/OggStream.h
+		soundlib/Loaders.h
+		soundlib/BitReader.h
+		soundlib/opal.h
+
+		sounddsp/AGC.cpp
+		sounddsp/EQ.cpp
+		sounddsp/DSP.cpp
+		sounddsp/Reverb.cpp
+		sounddsp/Reverb.h
+		sounddsp/EQ.h
+		sounddsp/DSP.h
+		sounddsp/AGC.h
+
+		libopenmpt/libopenmpt_c.cpp
+		libopenmpt/libopenmpt_cxx.cpp
+		libopenmpt/libopenmpt_impl.cpp
+		libopenmpt/libopenmpt_ext_impl.cpp
+	)
+	list(TRANSFORM openmpt_SOURCES PREPEND "${openmpt_SOURCE_DIR}/")
+
+	# -DLIBOPENMPT_BUILD
+	configure_file("openmpt_svn_version.h" "svn_version.h")
+	add_library(openmpt "${SRB2_INTERNAL_LIBRARY_TYPE}" ${openmpt_SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/svn_version.h)
+	if("${CMAKE_C_COMPILER_ID}" STREQUAL GNU OR "${CMAKE_C_COMPILER_ID}" STREQUAL Clang OR "${CMAKE_C_COMPILER_ID}" STREQUAL AppleClang)
+		target_compile_options(openmpt PRIVATE "-g0")
+	endif()
+	if("${CMAKE_SYSTEM_NAME}" STREQUAL Windows AND "${CMAKE_C_COMPILER_ID}" STREQUAL MSVC)
+		target_link_libraries(openmpt PRIVATE Rpcrt4)
+	endif()
+	target_compile_features(openmpt PRIVATE cxx_std_11)
+	target_compile_definitions(openmpt PRIVATE -DLIBOPENMPT_BUILD)
+
+	target_include_directories(openmpt PRIVATE "${openmpt_SOURCE_DIR}/common")
+	target_include_directories(openmpt PRIVATE "${openmpt_SOURCE_DIR}/src")
+	target_include_directories(openmpt PRIVATE "${openmpt_SOURCE_DIR}/include")
+	target_include_directories(openmpt PRIVATE "${openmpt_SOURCE_DIR}")
+	target_include_directories(openmpt PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
+
+	# I wish this wasn't necessary, but it is
+	target_include_directories(openmpt PUBLIC "${openmpt_SOURCE_DIR}")
+endif()
diff --git a/thirdparty/cpm-png.cmake b/thirdparty/cpm-png.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..f16ac037b0cc9c077f3ef315b67b430e68cf9b40
--- /dev/null
+++ b/thirdparty/cpm-png.cmake
@@ -0,0 +1,69 @@
+CPMAddPackage(
+	NAME png
+	VERSION 1.6.38
+	URL "https://github.com/glennrp/libpng/archive/refs/tags/v1.6.38.zip"
+	# png cmake build is broken on msys/mingw32
+	DOWNLOAD_ONLY YES
+)
+
+if(png_ADDED)
+	# Since png's cmake build is broken, we're going to create a target manually
+	set(
+		PNG_SOURCES
+		png.h
+		pngconf.h
+		pngpriv.h
+		pngdebug.h
+		pnginfo.h
+		pngstruct.h
+		png.c
+		pngerror.c
+		pngget.c
+		pngmem.c
+		pngpread.c
+		pngread.c
+		pngrio.c
+		pngrtran.c
+		pngrutil.c
+		pngset.c
+		pngtrans.c
+		pngwio.c
+		pngwrite.c
+		pngwtran.c
+		pngwutil.c
+	)
+	list(TRANSFORM PNG_SOURCES PREPEND "${png_SOURCE_DIR}/")
+
+	add_custom_command(
+		OUTPUT "${png_BINARY_DIR}/include/png.h" "${png_BINARY_DIR}/include/pngconf.h"
+		COMMAND ${CMAKE_COMMAND} -E copy "${png_SOURCE_DIR}/png.h" "${png_SOURCE_DIR}/pngconf.h" "${png_BINARY_DIR}/include"
+		DEPENDS "${png_SOURCE_DIR}/png.h" "${png_SOURCE_DIR}/pngconf.h"
+		VERBATIM
+	)
+	add_custom_command(
+		OUTPUT "${png_BINARY_DIR}/include/pnglibconf.h"
+		COMMAND ${CMAKE_COMMAND} -E copy "${png_SOURCE_DIR}/scripts/pnglibconf.h.prebuilt" "${png_BINARY_DIR}/include/pnglibconf.h"
+		DEPENDS "${png_SOURCE_DIR}/scripts/pnglibconf.h.prebuilt"
+		VERBATIM
+	)
+	list(
+		APPEND PNG_SOURCES
+		"${png_BINARY_DIR}/include/png.h"
+		"${png_BINARY_DIR}/include/pngconf.h"
+		"${png_BINARY_DIR}/include/pnglibconf.h"
+	)
+	add_library(png "${SRB2_INTERNAL_LIBRARY_TYPE}" ${PNG_SOURCES})
+
+	# Disable ARM NEON since having it automatic breaks libpng external build on clang for some reason
+	target_compile_definitions(png PRIVATE -DPNG_ARM_NEON_OPT=0)
+
+	# The png includes need to be available to consumers
+	target_include_directories(png PUBLIC "${png_BINARY_DIR}/include")
+
+	# ... and these also need to be present only for png build
+	target_include_directories(png PRIVATE "${ZLIB_SOURCE_DIR}")
+	target_include_directories(png PRIVATE "${ZLIB_BINARY_DIR}")
+	target_include_directories(png PRIVATE "${png_BINARY_DIR}")
+	target_link_libraries(png PRIVATE ZLIB::ZLIB)
+	add_library(PNG::PNG ALIAS png)
+endif()
diff --git a/thirdparty/cpm-sdl2-mixer.cmake b/thirdparty/cpm-sdl2-mixer.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..b7dfeae0d3eab254651f0c5e6e69e7af0626012e
--- /dev/null
+++ b/thirdparty/cpm-sdl2-mixer.cmake
@@ -0,0 +1,22 @@
+CPMAddPackage(
+	NAME SDL2_mixer
+	VERSION 2.6.2
+	URL "https://github.com/libsdl-org/SDL_mixer/archive/refs/tags/release-2.6.2.zip"
+	EXCLUDE_FROM_ALL ON
+	OPTIONS
+		"BUILD_SHARED_LIBS ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
+		"SDL2MIXER_INSTALL OFF"
+		"SDL2MIXER_DEPS_SHARED OFF"
+		"SDL2MIXER_SAMPLES OFF"
+		"SDL2MIXER_VENDORED ON"
+		"SDL2MIXER_FLAC ON"
+		"SDL2MIXER_FLAC_LIBFLAC OFF"
+		"SDL2MIXER_FLAC_DRFLAC ON"
+		"SDL2MIXER_MOD OFF"
+		"SDL2MIXER_MP3 ON"
+		"SDL2MIXER_MP3_DRMP3 ON"
+		"SDL2MIXER_MIDI ON"
+		"SDL2MIXER_OPUS OFF"
+		"SDL2MIXER_VORBIS STB"
+		"SDL2MIXER_WAVE ON"
+)
diff --git a/thirdparty/cpm-sdl2.cmake b/thirdparty/cpm-sdl2.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..58cf9afc2a8c9f443379625049ebd28446382b84
--- /dev/null
+++ b/thirdparty/cpm-sdl2.cmake
@@ -0,0 +1,13 @@
+CPMAddPackage(
+	NAME SDL2
+	VERSION 2.24.2
+	URL "https://github.com/libsdl-org/SDL/archive/refs/tags/release-2.24.2.zip"
+	EXCLUDE_FROM_ALL ON
+	OPTIONS
+		"BUILD_SHARED_LIBS ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
+		"SDL_SHARED ${SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
+		"SDL_STATIC ${NOT_SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES}"
+		"SDL_TEST OFF"
+		"SDL2_DISABLE_SDL2MAIN ON"
+		"SDL2_DISABLE_INSTALL ON"
+)
diff --git a/thirdparty/cpm-zlib.cmake b/thirdparty/cpm-zlib.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..5368366fd14a4246da476e48bb98ce708812646e
--- /dev/null
+++ b/thirdparty/cpm-zlib.cmake
@@ -0,0 +1,53 @@
+CPMAddPackage(
+	NAME ZLIB
+	VERSION 1.2.13
+	URL "https://github.com/madler/zlib/archive/refs/tags/v1.2.13.zip"
+	EXCLUDE_FROM_ALL
+	DOWNLOAD_ONLY YES
+)
+
+if(ZLIB_ADDED)
+	set(ZLIB_SRCS
+		crc32.h
+		deflate.h
+		gzguts.h
+		inffast.h
+		inffixed.h
+		inflate.h
+		inftrees.h
+		trees.h
+		zutil.h
+		adler32.c
+		compress.c
+		crc32.c
+		deflate.c
+		gzclose.c
+		gzlib.c
+		gzread.c
+		gzwrite.c
+		inflate.c
+		infback.c
+		inftrees.c
+		inffast.c
+		trees.c
+		uncompr.c
+		zutil.c
+	)
+	list(TRANSFORM ZLIB_SRCS PREPEND "${ZLIB_SOURCE_DIR}/")
+
+	configure_file("${ZLIB_SOURCE_DIR}/zlib.pc.cmakein" "${ZLIB_BINARY_DIR}/zlib.pc" @ONLY)
+	configure_file("${ZLIB_SOURCE_DIR}/zconf.h.cmakein" "${ZLIB_BINARY_DIR}/include/zconf.h" @ONLY)
+	configure_file("${ZLIB_SOURCE_DIR}/zlib.h" "${ZLIB_BINARY_DIR}/include/zlib.h" @ONLY)
+
+	add_library(ZLIB ${SRB2_INTERNAL_LIBRARY_TYPE} ${ZLIB_SRCS})
+	set_target_properties(ZLIB PROPERTIES
+		VERSION 1.2.13
+		OUTPUT_NAME "z"
+	)
+	target_include_directories(ZLIB PRIVATE "${ZLIB_SOURCE_DIR}")
+	target_include_directories(ZLIB PUBLIC "${ZLIB_BINARY_DIR}/include")
+	if(MSVC)
+		target_compile_definitions(ZLIB PRIVATE -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE)
+	endif()
+	add_library(ZLIB::ZLIB ALIAS ZLIB)
+endif()
diff --git a/tools/anglechk.c b/tools/anglechk.c
index 4a67069bf744772082afeac5d8875991f2075903..7f56abff7e56336090af76e81335d76951dcec39 100644
--- a/tools/anglechk.c
+++ b/tools/anglechk.c
@@ -22,7 +22,6 @@
 #ifdef _MSC_VER
 #include <assert.h>
 #endif
-#define NOASM
 #include "../src/tables.h"
 #define NO_M
 #include "../src/m_fixed.c"