diff --git a/.circleci/config.yml b/.circleci/config.yml
index 3faca372cd6fb0f92d05fdea87de12a297705395..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 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 2b4d9d2fbf20194317e43f8d5c64c624f271e705..268e3632906a9b840747e6c015b32848ba45b50f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,4 @@ Win32_LIB_ASM_Release
 /bin
 /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/appveyor.yml b/appveyor.yml
index 9770cb37df07fd9be9ae685f3770ca7410915c9f..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:
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/src/Android.mk b/src/Android.mk
index 035d48887727c2a6d6b63a6acb38fc7ec65a9342..235c4b81e5b4603cecffe56e962a973a7469977a 100644
--- a/src/Android.mk
+++ b/src/Android.mk
@@ -8,11 +8,7 @@ LOCAL_SRC_FILES :=      am_map.c \
                         command.c \
                         comptime.c \
                         console.c \
-                        d_clisrv.c \
                         d_main.c \
-                        d_net.c \
-                        d_netcmd.c \
-                        d_netfil.c \
                         dehacked.c \
                         f_finale.c \
                         f_wipe.c \
@@ -20,7 +16,6 @@ LOCAL_SRC_FILES :=      am_map.c \
                         g_game.c \
                         g_input.c \
                         hu_stuff.c \
-                        i_tcp.c \
                         info.c \
                         lzf.c \
                         m_argv.c \
@@ -32,7 +27,6 @@ LOCAL_SRC_FILES :=      am_map.c \
                         m_queue.c \
                         m_random.c \
                         md5.c \
-                        mserv.c \
                         p_ceilng.c \
                         p_enemy.c \
                         p_fab.c \
@@ -61,6 +55,7 @@ LOCAL_SRC_FILES :=      am_map.c \
                         r_things.c \
                         s_sound.c \
                         screen.c \
+                        snake.c \
                         sounds.c \
                         st_stuff.c \
                         string.c \
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index b926b3b7a3372d206df781fd65bbf7a947ddaef3..22c1def2752bd3b50aea361684b8707d5c078c30 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -6,10 +6,6 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
 	config.h.in
 	string.c
 	d_main.c
-	d_clisrv.c
-	d_net.c
-	d_netfil.c
-	d_netcmd.c
 	dehacked.c
 	deh_soc.c
 	deh_lua.c
@@ -83,12 +79,10 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
 	sounds.c
 	w_wad.c
 	filesrch.c
-	mserv.c
-	http-mserv.c
-	i_tcp.c
 	lzf.c
 	b_bot.c
 	u_list.c
+	snake.c
 	lua_script.c
 	lua_baselib.c
 	lua_mathlib.c
@@ -149,6 +143,7 @@ set(SRB2_CONFIG_DEV_BUILD OFF CACHE BOOL
 	"Compile a development build of SRB2.")
 
 add_subdirectory(blua)
+add_subdirectory(netcode)
 
 # OS macros
 if (UNIX)
diff --git a/src/Makefile b/src/Makefile
index 41cef2a179383670247847bcd572a651c1b8aad6..a0a18be769e0cc64b98d8774b4b3a1aeb3c177ad 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -62,7 +62,6 @@
 #
 # Netplay incompatible
 # --------------------
-# NONET=1 - Disable online capability.
 # NOMD5=1 - Disable MD5 checksum (validation tool).
 # NOPOSTPROCESSING=1 - ?
 # MOBJCONSISTANCY=1 - ??
@@ -141,9 +140,9 @@ endif
 
 OBJDUMP_OPTS?=--wide --source --line-numbers
 
-OBJCOPY:=$(call Prefix,objcopy)
-OBJDUMP:=$(call Prefix,objdump)
-WINDRES:=$(call Prefix,windres)
+OBJCOPY?=$(call Prefix,objcopy)
+OBJDUMP?=$(call Prefix,objdump)
+WINDRES?=$(call Prefix,windres)
 
 GZIP?=gzip
 GZIP_OPTS?=-9 -f -n
@@ -187,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)))
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 1787f94cb8988e6b27d5bb334c5a978ef2b31947..8b713718cdd604ff83193e560566455453f4a7ff 100644
--- a/src/Makefile.d/features.mk
+++ b/src/Makefile.d/features.mk
@@ -1,68 +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 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/win32.mk b/src/Makefile.d/win32.mk
index 73a3d9e453ecaa0a01e32e15f71326bd7be57920..a858881c0a0508c0eb3addfc456bdd042656108d 100644
--- a/src/Makefile.d/win32.mk
+++ b/src/Makefile.d/win32.mk
@@ -33,12 +33,10 @@ libs+=-lws2_32
 endif
 endif
 
-ifndef NONET
 ifndef MINGW64 # miniupnc is broken with MINGW64
 opts+=-I../libs -DSTATIC_MINIUPNPC
 libs+=-L../libs/miniupnpc/mingw$(32) -lws2_32 -liphlpapi
 endif
-endif
 
 ifndef MINGW64
 32=32
diff --git a/src/Sourcefile b/src/Sourcefile
index f2b408c665d28306ce9c778646f6139b6874099a..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
@@ -77,12 +73,10 @@ s_sound.c
 sounds.c
 w_wad.c
 filesrch.c
-mserv.c
-http-mserv.c
-i_tcp.c
 lzf.c
 b_bot.c
 u_list.c
+snake.c
 lua_script.c
 lua_baselib.c
 lua_mathlib.c
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/blua/liolib.c b/src/blua/liolib.c
index 545f9c144c3ba798c96609e4bcb0e6ca22f1f79e..6b44e2eb8d484597588736e1e074a946be8c12ca 100644
--- a/src/blua/liolib.c
+++ b/src/blua/liolib.c
@@ -19,7 +19,8 @@
 #include "lualib.h"
 #include "../i_system.h"
 #include "../g_game.h"
-#include "../d_netfil.h"
+#include "../netcode/d_netfil.h"
+#include "../netcode/net_command.h"
 #include "../lua_libs.h"
 #include "../byteptr.h"
 #include "../lua_script.h"
diff --git a/src/command.c b/src/command.c
index e1a43522da9946fe73b10efcf4f250008318fb50..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
 
diff --git a/src/config.h.in b/src/config.h.in
index 0ca0f26e905a0fdddf3741bb9d878517751d5bc9..6d49a698934bf8b498f6bfb402bbaa7f8ff93191 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -38,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/d_clisrv.c b/src/d_clisrv.c
deleted file mode 100755
index 83482b527c3354f5c2b6f81cc2e47f66b108c2b1..0000000000000000000000000000000000000000
--- a/src/d_clisrv.c
+++ /dev/null
@@ -1,5841 +0,0 @@
-// SONIC ROBO BLAST 2
-//-----------------------------------------------------------------------------
-// Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
-//
-// This program is free software distributed under the
-// terms of the GNU General Public License, version 2.
-// See the 'LICENSE' file for more details.
-//-----------------------------------------------------------------------------
-/// \file  d_clisrv.c
-/// \brief SRB2 Network game communication and protocol, all OS independent parts.
-
-#include <time.h>
-#ifdef __GNUC__
-#include <unistd.h> //for unlink
-#endif
-
-#include "i_time.h"
-#include "i_net.h"
-#include "i_system.h"
-#include "i_video.h"
-#include "d_net.h"
-#include "d_main.h"
-#include "g_game.h"
-#include "st_stuff.h"
-#include "hu_stuff.h"
-#include "keys.h"
-#include "g_input.h" // JOY1
-#include "m_menu.h"
-#include "console.h"
-#include "d_netfil.h"
-#include "byteptr.h"
-#include "p_saveg.h"
-#include "z_zone.h"
-#include "p_local.h"
-#include "m_misc.h"
-#include "am_map.h"
-#include "m_random.h"
-#include "mserv.h"
-#include "y_inter.h"
-#include "r_local.h"
-#include "m_argv.h"
-#include "p_setup.h"
-#include "lzf.h"
-#include "lua_script.h"
-#include "lua_hook.h"
-#include "lua_libs.h"
-#include "md5.h"
-#include "m_perfstats.h"
-
-// aaaaaa
-#include "i_joy.h"
-
-#ifndef NONET
-// cl loading screen
-#include "v_video.h"
-#include "f_finale.h"
-#endif
-
-//
-// NETWORKING
-//
-// gametic is the tic about to (or currently being) run
-// Server:
-//   maketic is the tic that hasn't had control made for it yet
-//   nettics is the tic for each node
-//   firstticstosend is the lowest value of nettics
-// Client:
-//   neededtic is the tic needed by the client to run the game
-//   firstticstosend is used to optimize a condition
-// Normally maketic >= gametic > 0
-
-#define PREDICTIONQUEUE BACKUPTICS
-#define PREDICTIONMASK (PREDICTIONQUEUE-1)
-#define MAX_REASONLENGTH 30
-
-boolean server = true; // true or false but !server == client
-#define client (!server)
-boolean nodownload = false;
-boolean serverrunning = false;
-INT32 serverplayer = 0;
-char motd[254], server_context[8]; // Message of the Day, Unique Context (even without Mumble support)
-
-// Server specific vars
-UINT8 playernode[MAXPLAYERS];
-char playeraddress[MAXPLAYERS][64];
-
-// Minimum timeout for sending the savegame
-// The actual timeout will be longer depending on the savegame length
-tic_t jointimeout = (10*TICRATE);
-static boolean sendingsavegame[MAXNETNODES]; // Are we sending the savegame?
-static boolean resendingsavegame[MAXNETNODES]; // Are we resending the savegame?
-static tic_t savegameresendcooldown[MAXNETNODES]; // How long before we can resend again?
-static tic_t freezetimeout[MAXNETNODES]; // Until when can this node freeze the server before getting a timeout?
-
-// Incremented by cv_joindelay when a client joins, decremented each tic.
-// If higher than cv_joindelay * 2 (3 joins in a short timespan), joins are temporarily disabled.
-static tic_t joindelay = 0;
-
-UINT16 pingmeasurecount = 1;
-UINT32 realpingtable[MAXPLAYERS]; //the base table of ping where an average will be sent to everyone.
-UINT32 playerpingtable[MAXPLAYERS]; //table of player latency values.
-SINT8 nodetoplayer[MAXNETNODES];
-SINT8 nodetoplayer2[MAXNETNODES]; // say the numplayer for this node if any (splitscreen)
-UINT8 playerpernode[MAXNETNODES]; // used specialy for scplitscreen
-boolean nodeingame[MAXNETNODES]; // set false as nodes leave game
-tic_t servermaxping = 800; // server's max ping. Defaults to 800
-static tic_t nettics[MAXNETNODES]; // what tic the client have received
-static tic_t supposedtics[MAXNETNODES]; // nettics prevision for smaller packet
-static UINT8 nodewaiting[MAXNETNODES];
-static tic_t firstticstosend; // min of the nettics
-static tic_t tictoclear = 0; // optimize d_clearticcmd
-static tic_t maketic;
-
-static INT16 consistancy[BACKUPTICS];
-
-static UINT8 player_joining = false;
-UINT8 hu_redownloadinggamestate = 0;
-
-// true when a player is connecting or disconnecting so that the gameplay has stopped in its tracks
-boolean hu_stopped = false;
-
-consvar_t cv_dedicatedidletime = CVAR_INIT ("dedicatedidletime", "10", CV_SAVE, CV_Unsigned, NULL);
-
-UINT8 adminpassmd5[16];
-boolean adminpasswordset = false;
-
-// Client specific
-static ticcmd_t localcmds;
-static ticcmd_t localcmds2;
-static boolean cl_packetmissed;
-// here it is for the secondary local player (splitscreen)
-static UINT8 mynode; // my address pointofview server
-static boolean cl_redownloadinggamestate = false;
-
-static UINT8 localtextcmd[MAXTEXTCMD];
-static UINT8 localtextcmd2[MAXTEXTCMD]; // splitscreen
-static tic_t neededtic;
-SINT8 servernode = 0; // the number of the server node
-
-/// \brief do we accept new players?
-/// \todo WORK!
-boolean acceptnewnode = true;
-
-static boolean serverisfull = false; //lets us be aware if the server was full after we check files, but before downloading, so we can ask if the user still wants to download or not
-static tic_t firstconnectattempttime = 0;
-
-// engine
-
-// Must be a power of two
-#define TEXTCMD_HASH_SIZE 4
-
-typedef struct textcmdplayer_s
-{
-	INT32 playernum;
-	UINT8 cmd[MAXTEXTCMD];
-	struct textcmdplayer_s *next;
-} textcmdplayer_t;
-
-typedef struct textcmdtic_s
-{
-	tic_t tic;
-	textcmdplayer_t *playercmds[TEXTCMD_HASH_SIZE];
-	struct textcmdtic_s *next;
-} textcmdtic_t;
-
-ticcmd_t netcmds[BACKUPTICS][MAXPLAYERS];
-static textcmdtic_t *textcmds[TEXTCMD_HASH_SIZE] = {NULL};
-
-
-consvar_t cv_showjoinaddress = CVAR_INIT ("showjoinaddress", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
-
-static CV_PossibleValue_t playbackspeed_cons_t[] = {{1, "MIN"}, {10, "MAX"}, {0, NULL}};
-consvar_t cv_playbackspeed = CVAR_INIT ("playbackspeed", "1", 0, playbackspeed_cons_t, NULL);
-
-static inline void *G_DcpyTiccmd(void* dest, const ticcmd_t* src, const size_t n)
-{
-	const size_t d = n / sizeof(ticcmd_t);
-	const size_t r = n % sizeof(ticcmd_t);
-	UINT8 *ret = dest;
-
-	if (r)
-		M_Memcpy(dest, src, n);
-	else if (d)
-		G_MoveTiccmd(dest, src, d);
-	return ret+n;
-}
-
-static inline void *G_ScpyTiccmd(ticcmd_t* dest, void* src, const size_t n)
-{
-	const size_t d = n / sizeof(ticcmd_t);
-	const size_t r = n % sizeof(ticcmd_t);
-	UINT8 *ret = src;
-
-	if (r)
-		M_Memcpy(dest, src, n);
-	else if (d)
-		G_MoveTiccmd(dest, src, d);
-	return ret+n;
-}
-
-
-
-// Some software don't support largest packet
-// (original sersetup, not exactely, but the probability of sending a packet
-// of 512 bytes is like 0.1)
-UINT16 software_MAXPACKETLENGTH;
-
-/** Guesses the full value of a tic from its lowest byte, for a specific node
-  *
-  * \param low The lowest byte of the tic value
-  * \param node The node to deduce the tic for
-  * \return The full tic value
-  *
-  */
-tic_t ExpandTics(INT32 low, INT32 node)
-{
-	INT32 delta;
-
-	delta = low - (nettics[node] & UINT8_MAX);
-
-	if (delta >= -64 && delta <= 64)
-		return (nettics[node] & ~UINT8_MAX) + low;
-	else if (delta > 64)
-		return (nettics[node] & ~UINT8_MAX) - 256 + low;
-	else //if (delta < -64)
-		return (nettics[node] & ~UINT8_MAX) + 256 + low;
-}
-
-// -----------------------------------------------------------------
-// Some extra data function for handle textcmd buffer
-// -----------------------------------------------------------------
-
-static void (*listnetxcmd[MAXNETXCMD])(UINT8 **p, INT32 playernum);
-
-void RegisterNetXCmd(netxcmd_t id, void (*cmd_f)(UINT8 **p, INT32 playernum))
-{
-#ifdef PARANOIA
-	if (id >= MAXNETXCMD)
-		I_Error("Command id %d too big", id);
-	if (listnetxcmd[id] != 0)
-		I_Error("Command id %d already used", id);
-#endif
-	listnetxcmd[id] = cmd_f;
-}
-
-void SendNetXCmd(netxcmd_t id, const void *param, size_t nparam)
-{
-	if (localtextcmd[0]+2+nparam > MAXTEXTCMD)
-	{
-		// for future reference: if (cv_debug) != debug disabled.
-		CONS_Alert(CONS_ERROR, M_GetText("NetXCmd buffer full, cannot add netcmd %d! (size: %d, needed: %s)\n"), id, localtextcmd[0], sizeu1(nparam));
-		return;
-	}
-	localtextcmd[0]++;
-	localtextcmd[localtextcmd[0]] = (UINT8)id;
-	if (param && nparam)
-	{
-		M_Memcpy(&localtextcmd[localtextcmd[0]+1], param, nparam);
-		localtextcmd[0] = (UINT8)(localtextcmd[0] + (UINT8)nparam);
-	}
-}
-
-// splitscreen player
-void SendNetXCmd2(netxcmd_t id, const void *param, size_t nparam)
-{
-	if (localtextcmd2[0]+2+nparam > MAXTEXTCMD)
-	{
-		I_Error("No more place in the buffer for netcmd %d\n",id);
-		return;
-	}
-	localtextcmd2[0]++;
-	localtextcmd2[localtextcmd2[0]] = (UINT8)id;
-	if (param && nparam)
-	{
-		M_Memcpy(&localtextcmd2[localtextcmd2[0]+1], param, nparam);
-		localtextcmd2[0] = (UINT8)(localtextcmd2[0] + (UINT8)nparam);
-	}
-}
-
-UINT8 GetFreeXCmdSize(void)
-{
-	// -1 for the size and another -1 for the ID.
-	return (UINT8)(localtextcmd[0] - 2);
-}
-
-// Frees all textcmd memory for the specified tic
-static void D_FreeTextcmd(tic_t tic)
-{
-	textcmdtic_t **tctprev = &textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
-	textcmdtic_t *textcmdtic = *tctprev;
-
-	while (textcmdtic && textcmdtic->tic != tic)
-	{
-		tctprev = &textcmdtic->next;
-		textcmdtic = textcmdtic->next;
-	}
-
-	if (textcmdtic)
-	{
-		INT32 i;
-
-		// Remove this tic from the list.
-		*tctprev = textcmdtic->next;
-
-		// Free all players.
-		for (i = 0; i < TEXTCMD_HASH_SIZE; i++)
-		{
-			textcmdplayer_t *textcmdplayer = textcmdtic->playercmds[i];
-
-			while (textcmdplayer)
-			{
-				textcmdplayer_t *tcpnext = textcmdplayer->next;
-				Z_Free(textcmdplayer);
-				textcmdplayer = tcpnext;
-			}
-		}
-
-		// Free this tic's own memory.
-		Z_Free(textcmdtic);
-	}
-}
-
-// Gets the buffer for the specified ticcmd, or NULL if there isn't one
-static UINT8* D_GetExistingTextcmd(tic_t tic, INT32 playernum)
-{
-	textcmdtic_t *textcmdtic = textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
-	while (textcmdtic && textcmdtic->tic != tic) textcmdtic = textcmdtic->next;
-
-	// Do we have an entry for the tic? If so, look for player.
-	if (textcmdtic)
-	{
-		textcmdplayer_t *textcmdplayer = textcmdtic->playercmds[playernum & (TEXTCMD_HASH_SIZE - 1)];
-		while (textcmdplayer && textcmdplayer->playernum != playernum) textcmdplayer = textcmdplayer->next;
-
-		if (textcmdplayer) return textcmdplayer->cmd;
-	}
-
-	return NULL;
-}
-
-// Gets the buffer for the specified ticcmd, creating one if necessary
-static UINT8* D_GetTextcmd(tic_t tic, INT32 playernum)
-{
-	textcmdtic_t *textcmdtic = textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
-	textcmdtic_t **tctprev = &textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
-	textcmdplayer_t *textcmdplayer, **tcpprev;
-
-	// Look for the tic.
-	while (textcmdtic && textcmdtic->tic != tic)
-	{
-		tctprev = &textcmdtic->next;
-		textcmdtic = textcmdtic->next;
-	}
-
-	// If we don't have an entry for the tic, make it.
-	if (!textcmdtic)
-	{
-		textcmdtic = *tctprev = Z_Calloc(sizeof (textcmdtic_t), PU_STATIC, NULL);
-		textcmdtic->tic = tic;
-	}
-
-	tcpprev = &textcmdtic->playercmds[playernum & (TEXTCMD_HASH_SIZE - 1)];
-	textcmdplayer = *tcpprev;
-
-	// Look for the player.
-	while (textcmdplayer && textcmdplayer->playernum != playernum)
-	{
-		tcpprev = &textcmdplayer->next;
-		textcmdplayer = textcmdplayer->next;
-	}
-
-	// If we don't have an entry for the player, make it.
-	if (!textcmdplayer)
-	{
-		textcmdplayer = *tcpprev = Z_Calloc(sizeof (textcmdplayer_t), PU_STATIC, NULL);
-		textcmdplayer->playernum = playernum;
-	}
-
-	return textcmdplayer->cmd;
-}
-
-static void ExtraDataTicker(void)
-{
-	INT32 i;
-
-	for (i = 0; i < MAXPLAYERS; i++)
-		if (playeringame[i] || i == 0)
-		{
-			UINT8 *bufferstart = D_GetExistingTextcmd(gametic, i);
-
-			if (bufferstart)
-			{
-				UINT8 *curpos = bufferstart;
-				UINT8 *bufferend = &curpos[curpos[0]+1];
-
-				curpos++;
-				while (curpos < bufferend)
-				{
-					if (*curpos < MAXNETXCMD && listnetxcmd[*curpos])
-					{
-						const UINT8 id = *curpos;
-						curpos++;
-						DEBFILE(va("executing x_cmd %s ply %u ", netxcmdnames[id - 1], i));
-						(listnetxcmd[id])(&curpos, i);
-						DEBFILE("done\n");
-					}
-					else
-					{
-						if (server)
-						{
-							SendKick(i, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
-							DEBFILE(va("player %d kicked [gametic=%u] reason as follows:\n", i, gametic));
-						}
-						CONS_Alert(CONS_WARNING, M_GetText("Got unknown net command [%s]=%d (max %d)\n"), sizeu1(curpos - bufferstart), *curpos, bufferstart[0]);
-						break;
-					}
-				}
-			}
-		}
-
-	// If you are a client, you can safely forget the net commands for this tic
-	// If you are the server, you need to remember them until every client has been acknowledged,
-	// because if you need to resend a PT_SERVERTICS packet, you will need to put the commands in it
-	if (client)
-		D_FreeTextcmd(gametic);
-}
-
-static void D_Clearticcmd(tic_t tic)
-{
-	INT32 i;
-
-	D_FreeTextcmd(tic);
-
-	for (i = 0; i < MAXPLAYERS; i++)
-		netcmds[tic%BACKUPTICS][i].angleturn = 0;
-
-	DEBFILE(va("clear tic %5u (%2u)\n", tic, tic%BACKUPTICS));
-}
-
-void D_ResetTiccmds(void)
-{
-	INT32 i;
-
-	memset(&localcmds, 0, sizeof(ticcmd_t));
-	memset(&localcmds2, 0, sizeof(ticcmd_t));
-
-	// Reset the net command list
-	for (i = 0; i < TEXTCMD_HASH_SIZE; i++)
-		while (textcmds[i])
-			D_Clearticcmd(textcmds[i]->tic);
-}
-
-void SendKick(UINT8 playernum, UINT8 msg)
-{
-	UINT8 buf[2];
-
-	if (!(server && cv_rejointimeout.value))
-		msg &= ~KICK_MSG_KEEP_BODY;
-
-	buf[0] = playernum;
-	buf[1] = msg;
-	SendNetXCmd(XD_KICK, &buf, 2);
-}
-
-// -----------------------------------------------------------------
-// end of extra data function
-// -----------------------------------------------------------------
-
-// -----------------------------------------------------------------
-// extra data function for lmps
-// -----------------------------------------------------------------
-
-// if extradatabit is set, after the ziped tic you find this:
-//
-//   type   |  description
-// ---------+--------------
-//   byte   | size of the extradata
-//   byte   | the extradata (xd) bits: see XD_...
-//            with this byte you know what parameter folow
-// if (xd & XDNAMEANDCOLOR)
-//   byte   | color
-//   char[MAXPLAYERNAME] | name of the player
-// endif
-// if (xd & XD_WEAPON_PREF)
-//   byte   | original weapon switch: boolean, true if use the old
-//          | weapon switch methode
-//   char[NUMWEAPONS] | the weapon switch priority
-//   byte   | autoaim: true if use the old autoaim system
-// endif
-/*boolean AddLmpExtradata(UINT8 **demo_point, INT32 playernum)
-{
-	UINT8 *textcmd = D_GetExistingTextcmd(gametic, playernum);
-
-	if (!textcmd)
-		return false;
-
-	M_Memcpy(*demo_point, textcmd, textcmd[0]+1);
-	*demo_point += textcmd[0]+1;
-	return true;
-}
-
-void ReadLmpExtraData(UINT8 **demo_pointer, INT32 playernum)
-{
-	UINT8 nextra;
-	UINT8 *textcmd;
-
-	if (!demo_pointer)
-		return;
-
-	textcmd = D_GetTextcmd(gametic, playernum);
-	nextra = **demo_pointer;
-	M_Memcpy(textcmd, *demo_pointer, nextra + 1);
-	// increment demo pointer
-	*demo_pointer += nextra + 1;
-}*/
-
-// -----------------------------------------------------------------
-// end extra data function for lmps
-// -----------------------------------------------------------------
-
-static INT16 Consistancy(void);
-
-typedef enum
-{
-	CL_SEARCHING,
-	CL_CHECKFILES,
-	CL_DOWNLOADFILES,
-	CL_ASKJOIN,
-	CL_LOADFILES,
-	CL_WAITJOINRESPONSE,
-	CL_DOWNLOADSAVEGAME,
-	CL_CONNECTED,
-	CL_ABORTED,
-	CL_ASKFULLFILELIST,
-	CL_CONFIRMCONNECT
-} cl_mode_t;
-
-static void GetPackets(void);
-
-static cl_mode_t cl_mode = CL_SEARCHING;
-
-static UINT16 cl_lastcheckedfilecount = 0;	// used for full file list
-
-#ifndef NONET
-#define SNAKE_SPEED 5
-
-#define SNAKE_NUM_BLOCKS_X 20
-#define SNAKE_NUM_BLOCKS_Y 10
-#define SNAKE_BLOCK_SIZE 12
-#define SNAKE_BORDER_SIZE 12
-
-#define SNAKE_MAP_WIDTH  (SNAKE_NUM_BLOCKS_X * SNAKE_BLOCK_SIZE)
-#define SNAKE_MAP_HEIGHT (SNAKE_NUM_BLOCKS_Y * SNAKE_BLOCK_SIZE)
-
-#define SNAKE_LEFT_X ((BASEVIDWIDTH - SNAKE_MAP_WIDTH) / 2 - SNAKE_BORDER_SIZE)
-#define SNAKE_RIGHT_X (SNAKE_LEFT_X + SNAKE_MAP_WIDTH + SNAKE_BORDER_SIZE * 2 - 1)
-#define SNAKE_BOTTOM_Y (BASEVIDHEIGHT - 48)
-#define SNAKE_TOP_Y (SNAKE_BOTTOM_Y - SNAKE_MAP_HEIGHT - SNAKE_BORDER_SIZE * 2 + 1)
-
-enum snake_bonustype_s {
-	SNAKE_BONUS_NONE = 0,
-	SNAKE_BONUS_SLOW,
-	SNAKE_BONUS_FAST,
-	SNAKE_BONUS_GHOST,
-	SNAKE_BONUS_NUKE,
-	SNAKE_BONUS_SCISSORS,
-	SNAKE_BONUS_REVERSE,
-	SNAKE_BONUS_EGGMAN,
-	SNAKE_NUM_BONUSES,
-};
-
-static const char *snake_bonuspatches[] = {
-	NULL,
-	"DL_SLOW",
-	"TVSSC0",
-	"TVIVC0",
-	"TVARC0",
-	"DL_SCISSORS",
-	"TVRCC0",
-	"TVEGC0",
-};
-
-static const char *snake_backgrounds[] = {
-	"RVPUMICF",
-	"FRSTRCKF",
-	"TAR",
-	"MMFLRB4",
-	"RVDARKF1",
-	"RVZWALF1",
-	"RVZWALF4",
-	"RVZWALF5",
-	"RVZGRS02",
-	"RVZGRS04",
-};
-
-typedef struct snake_s
-{
-	boolean paused;
-	boolean pausepressed;
-	tic_t time;
-	tic_t nextupdate;
-	boolean gameover;
-	UINT8 background;
-
-	UINT16 snakelength;
-	enum snake_bonustype_s snakebonus;
-	tic_t snakebonustime;
-	UINT8 snakex[SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y];
-	UINT8 snakey[SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y];
-	UINT8 snakedir[SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y];
-
-	UINT8 applex;
-	UINT8 appley;
-
-	enum snake_bonustype_s bonustype;
-	UINT8 bonusx;
-	UINT8 bonusy;
-} snake_t;
-
-static snake_t *snake = NULL;
-
-static void Snake_Initialise(void)
-{
-	if (!snake)
-		snake = malloc(sizeof(snake_t));
-
-	snake->paused = false;
-	snake->pausepressed = false;
-	snake->time = 0;
-	snake->nextupdate = SNAKE_SPEED;
-	snake->gameover = false;
-	snake->background = M_RandomKey(sizeof(snake_backgrounds) / sizeof(*snake_backgrounds));
-
-	snake->snakelength = 1;
-	snake->snakebonus = SNAKE_BONUS_NONE;
-	snake->snakex[0] = M_RandomKey(SNAKE_NUM_BLOCKS_X);
-	snake->snakey[0] = M_RandomKey(SNAKE_NUM_BLOCKS_Y);
-	snake->snakedir[0] = 0;
-	snake->snakedir[1] = 0;
-
-	snake->applex = M_RandomKey(SNAKE_NUM_BLOCKS_X);
-	snake->appley = M_RandomKey(SNAKE_NUM_BLOCKS_Y);
-
-	snake->bonustype = SNAKE_BONUS_NONE;
-}
-
-static UINT8 Snake_GetOppositeDir(UINT8 dir)
-{
-	if (dir == 1 || dir == 3)
-		return dir + 1;
-	else if (dir == 2 || dir == 4)
-		return dir - 1;
-	else
-		return 12 + 5 - dir;
-}
-
-event_t *snakejoyevents[MAXEVENTS];
-UINT16 joyeventcount = 0;
-
-// I'm screaming the hack is clean - ashi
-static boolean Snake_Joy_Grabber(event_t *ev)
-{
-	if (ev->type == ev_joystick  && ev->key == 0)
-	{
-		snakejoyevents[joyeventcount] = ev;
-		joyeventcount++;
-		return true;
-	}
-	else
-		return false;
-}
-
-static void Snake_FindFreeSlot(UINT8 *freex, UINT8 *freey, UINT8 headx, UINT8 heady)
-{
-	UINT8 x, y;
-	UINT16 i;
-
-	do
-	{
-		x = M_RandomKey(SNAKE_NUM_BLOCKS_X);
-		y = M_RandomKey(SNAKE_NUM_BLOCKS_Y);
-
-		for (i = 0; i < snake->snakelength; i++)
-			if (x == snake->snakex[i] && y == snake->snakey[i])
-				break;
-	} while (i < snake->snakelength || (x == headx && y == heady)
-		|| (x == snake->applex && y == snake->appley)
-		|| (snake->bonustype != SNAKE_BONUS_NONE && x == snake->bonusx && y == snake->bonusy));
-
-	*freex = x;
-	*freey = y;
-}
-
-static void Snake_Handle(void)
-{
-	UINT8 x, y;
-	UINT8 oldx, oldy;
-	UINT16 i;
-	UINT16 j;
-	UINT16 joystate = 0;
-	static INT32 pjoyx = 0, pjoyy = 0;
-
-	// Handle retry
-	if (snake->gameover && (PLAYER1INPUTDOWN(GC_JUMP) || gamekeydown[KEY_ENTER]))
-	{
-		Snake_Initialise();
-		snake->pausepressed = true; // Avoid accidental pause on respawn
-	}
-
-	// Handle pause
-	if (PLAYER1INPUTDOWN(GC_PAUSE) || gamekeydown[KEY_ENTER])
-	{
-		if (!snake->pausepressed)
-			snake->paused = !snake->paused;
-		snake->pausepressed = true;
-	}
-	else
-		snake->pausepressed = false;
-
-	if (snake->paused)
-		return;
-
-	snake->time++;
-
-	x = snake->snakex[0];
-	y = snake->snakey[0];
-	oldx = snake->snakex[1];
-	oldy = snake->snakey[1];
-
-	// process the input events in here dear lord
-	for (j = 0; j < joyeventcount; j++)
-	{
-		event_t *ev = snakejoyevents[j];
-		const INT32 jdeadzone = (JOYAXISRANGE * cv_digitaldeadzone.value) / FRACUNIT;
-		if (ev->y != INT32_MAX)
-		{
-			if (Joystick.bGamepadStyle || abs(ev->y) > jdeadzone)
-			{
-				if (ev->y < 0 && pjoyy >= 0)
-					joystate = 1;
-				else if (ev->y > 0 && pjoyy <= 0)
-					joystate = 2;
-				pjoyy = ev->y;
-			}
-			else
-				pjoyy = 0;
-		}
-
-		if (ev->x != INT32_MAX)
-		{
-			if (Joystick.bGamepadStyle || abs(ev->x) > jdeadzone)
-			{
-				if (ev->x < 0 && pjoyx >= 0)
-					joystate = 3;
-				else if (ev->x > 0 && pjoyx <= 0)
-					joystate = 4;
-				pjoyx = ev->x;
-			}
-			else
-				pjoyx = 0;
-		}
-	}
-	joyeventcount = 0;
-
-	// Update direction
-	if (PLAYER1INPUTDOWN(GC_STRAFELEFT) || gamekeydown[KEY_LEFTARROW] || joystate == 3)
-	{
-		if (snake->snakelength < 2 || x <= oldx)
-			snake->snakedir[0] = 1;
-	}
-	else if (PLAYER1INPUTDOWN(GC_STRAFERIGHT) || gamekeydown[KEY_RIGHTARROW] || joystate == 4)
-	{
-		if (snake->snakelength < 2 || x >= oldx)
-			snake->snakedir[0] = 2;
-	}
-	else if (PLAYER1INPUTDOWN(GC_FORWARD) || gamekeydown[KEY_UPARROW] || joystate == 1)
-	{
-		if (snake->snakelength < 2 || y <= oldy)
-			snake->snakedir[0] = 3;
-	}
-	else if (PLAYER1INPUTDOWN(GC_BACKWARD) || gamekeydown[KEY_DOWNARROW] || joystate == 2)
-	{
-		if (snake->snakelength < 2 || y >= oldy)
-			snake->snakedir[0] = 4;
-	}
-
-	if (snake->snakebonustime)
-	{
-		snake->snakebonustime--;
-		if (!snake->snakebonustime)
-			snake->snakebonus = SNAKE_BONUS_NONE;
-	}
-
-	snake->nextupdate--;
-	if (snake->nextupdate)
-		return;
-	if (snake->snakebonus == SNAKE_BONUS_SLOW)
-		snake->nextupdate = SNAKE_SPEED * 2;
-	else if (snake->snakebonus == SNAKE_BONUS_FAST)
-		snake->nextupdate = SNAKE_SPEED * 2 / 3;
-	else
-		snake->nextupdate = SNAKE_SPEED;
-
-	if (snake->gameover)
-		return;
-
-	// Find new position
-	switch (snake->snakedir[0])
-	{
-		case 1:
-			if (x > 0)
-				x--;
-			else
-				snake->gameover = true;
-			break;
-		case 2:
-			if (x < SNAKE_NUM_BLOCKS_X - 1)
-				x++;
-			else
-				snake->gameover = true;
-			break;
-		case 3:
-			if (y > 0)
-				y--;
-			else
-				snake->gameover = true;
-			break;
-		case 4:
-			if (y < SNAKE_NUM_BLOCKS_Y - 1)
-				y++;
-			else
-				snake->gameover = true;
-			break;
-	}
-
-	// Check collision with snake
-	if (snake->snakebonus != SNAKE_BONUS_GHOST)
-		for (i = 1; i < snake->snakelength - 1; i++)
-			if (x == snake->snakex[i] && y == snake->snakey[i])
-			{
-				if (snake->snakebonus == SNAKE_BONUS_SCISSORS)
-				{
-					snake->snakebonus = SNAKE_BONUS_NONE;
-					snake->snakelength = i;
-					S_StartSound(NULL, sfx_adderr);
-				}
-				else
-					snake->gameover = true;
-			}
-
-	if (snake->gameover)
-	{
-		S_StartSound(NULL, sfx_lose);
-		return;
-	}
-
-	// Check collision with apple
-	if (x == snake->applex && y == snake->appley)
-	{
-		if (snake->snakelength + 3 < SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y)
-		{
-			snake->snakelength++;
-			snake->snakex  [snake->snakelength - 1] = snake->snakex  [snake->snakelength - 2];
-			snake->snakey  [snake->snakelength - 1] = snake->snakey  [snake->snakelength - 2];
-			snake->snakedir[snake->snakelength - 1] = snake->snakedir[snake->snakelength - 2];
-		}
-
-		// Spawn new apple
-		Snake_FindFreeSlot(&snake->applex, &snake->appley, x, y);
-
-		// Spawn new bonus
-		if (!(snake->snakelength % 5))
-		{
-			do
-			{
-				snake->bonustype = M_RandomKey(SNAKE_NUM_BONUSES - 1) + 1;
-			} while (snake->snakelength > SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y * 3 / 4
-				&& (snake->bonustype == SNAKE_BONUS_EGGMAN || snake->bonustype == SNAKE_BONUS_FAST || snake->bonustype == SNAKE_BONUS_REVERSE));
-
-			Snake_FindFreeSlot(&snake->bonusx, &snake->bonusy, x, y);
-		}
-
-		S_StartSound(NULL, sfx_s3k6b);
-	}
-
-	if (snake->snakelength > 1 && snake->snakedir[0])
-	{
-		UINT8 dir = snake->snakedir[0];
-
-		oldx = snake->snakex[1];
-		oldy = snake->snakey[1];
-
-		// Move
-		for (i = snake->snakelength - 1; i > 0; i--)
-		{
-			snake->snakex[i] = snake->snakex[i - 1];
-			snake->snakey[i] = snake->snakey[i - 1];
-			snake->snakedir[i] = snake->snakedir[i - 1];
-		}
-
-		// Handle corners
-		if      (x < oldx && dir == 3)
-			dir = 5;
-		else if (x > oldx && dir == 3)
-			dir = 6;
-		else if (x < oldx && dir == 4)
-			dir = 7;
-		else if (x > oldx && dir == 4)
-			dir = 8;
-		else if (y < oldy && dir == 1)
-			dir = 9;
-		else if (y < oldy && dir == 2)
-			dir = 10;
-		else if (y > oldy && dir == 1)
-			dir = 11;
-		else if (y > oldy && dir == 2)
-			dir = 12;
-		snake->snakedir[1] = dir;
-	}
-
-	snake->snakex[0] = x;
-	snake->snakey[0] = y;
-
-	// Check collision with bonus
-	if (snake->bonustype != SNAKE_BONUS_NONE && x == snake->bonusx && y == snake->bonusy)
-	{
-		S_StartSound(NULL, sfx_ncchip);
-
-		switch (snake->bonustype)
-		{
-		case SNAKE_BONUS_SLOW:
-			snake->snakebonus = SNAKE_BONUS_SLOW;
-			snake->snakebonustime = 20 * TICRATE;
-			break;
-		case SNAKE_BONUS_FAST:
-			snake->snakebonus = SNAKE_BONUS_FAST;
-			snake->snakebonustime = 20 * TICRATE;
-			break;
-		case SNAKE_BONUS_GHOST:
-			snake->snakebonus = SNAKE_BONUS_GHOST;
-			snake->snakebonustime = 10 * TICRATE;
-			break;
-		case SNAKE_BONUS_NUKE:
-			for (i = 0; i < snake->snakelength; i++)
-			{
-				snake->snakex  [i] = snake->snakex  [0];
-				snake->snakey  [i] = snake->snakey  [0];
-				snake->snakedir[i] = snake->snakedir[0];
-			}
-
-			S_StartSound(NULL, sfx_bkpoof);
-			break;
-		case SNAKE_BONUS_SCISSORS:
-			snake->snakebonus = SNAKE_BONUS_SCISSORS;
-			snake->snakebonustime = 60 * TICRATE;
-			break;
-		case SNAKE_BONUS_REVERSE:
-			for (i = 0; i < (snake->snakelength + 1) / 2; i++)
-			{
-				UINT16 i2 = snake->snakelength - 1 - i;
-				UINT8 tmpx   = snake->snakex  [i];
-				UINT8 tmpy   = snake->snakey  [i];
-				UINT8 tmpdir = snake->snakedir[i];
-
-				// Swap first segment with last segment
-				snake->snakex  [i] = snake->snakex  [i2];
-				snake->snakey  [i] = snake->snakey  [i2];
-				snake->snakedir[i] = Snake_GetOppositeDir(snake->snakedir[i2]);
-				snake->snakex  [i2] = tmpx;
-				snake->snakey  [i2] = tmpy;
-				snake->snakedir[i2] = Snake_GetOppositeDir(tmpdir);
-			}
-
-			snake->snakedir[0] = 0;
-
-			S_StartSound(NULL, sfx_gravch);
-			break;
-		default:
-			if (snake->snakebonus != SNAKE_BONUS_GHOST)
-			{
-				snake->gameover = true;
-				S_StartSound(NULL, sfx_lose);
-			}
-		}
-
-		snake->bonustype = SNAKE_BONUS_NONE;
-	}
-}
-
-static void Snake_Draw(void)
-{
-	INT16 i;
-
-	// Background
-	V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
-
-	V_DrawFlatFill(
-		SNAKE_LEFT_X + SNAKE_BORDER_SIZE,
-		SNAKE_TOP_Y  + SNAKE_BORDER_SIZE,
-		SNAKE_MAP_WIDTH,
-		SNAKE_MAP_HEIGHT,
-		W_GetNumForName(snake_backgrounds[snake->background])
-	);
-
-	// Borders
-	V_DrawFill(SNAKE_LEFT_X, SNAKE_TOP_Y, SNAKE_BORDER_SIZE + SNAKE_MAP_WIDTH, SNAKE_BORDER_SIZE, 242); // Top
-	V_DrawFill(SNAKE_LEFT_X + SNAKE_BORDER_SIZE + SNAKE_MAP_WIDTH, SNAKE_TOP_Y, SNAKE_BORDER_SIZE, SNAKE_BORDER_SIZE + SNAKE_MAP_HEIGHT, 242); // Right
-	V_DrawFill(SNAKE_LEFT_X + SNAKE_BORDER_SIZE, SNAKE_TOP_Y + SNAKE_BORDER_SIZE + SNAKE_MAP_HEIGHT, SNAKE_BORDER_SIZE + SNAKE_MAP_WIDTH, SNAKE_BORDER_SIZE, 242); // Bottom
-	V_DrawFill(SNAKE_LEFT_X, SNAKE_TOP_Y + SNAKE_BORDER_SIZE, SNAKE_BORDER_SIZE, SNAKE_BORDER_SIZE + SNAKE_MAP_HEIGHT, 242); // Left
-
-	// Apple
-	V_DrawFixedPatch(
-		(SNAKE_LEFT_X + SNAKE_BORDER_SIZE + snake->applex * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2) * FRACUNIT,
-		(SNAKE_TOP_Y  + SNAKE_BORDER_SIZE + snake->appley * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2) * FRACUNIT,
-		FRACUNIT / 4,
-		0,
-		W_CachePatchLongName("DL_APPLE", PU_HUDGFX),
-		NULL
-	);
-
-	// Bonus
-	if (snake->bonustype != SNAKE_BONUS_NONE)
-		V_DrawFixedPatch(
-			(SNAKE_LEFT_X + SNAKE_BORDER_SIZE + snake->bonusx * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2    ) * FRACUNIT,
-			(SNAKE_TOP_Y  + SNAKE_BORDER_SIZE + snake->bonusy * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2 + 4) * FRACUNIT,
-			FRACUNIT / 2,
-			0,
-			W_CachePatchLongName(snake_bonuspatches[snake->bonustype], PU_HUDGFX),
-			NULL
-		);
-
-	// Snake
-	if (!snake->gameover || snake->time % 8 < 8 / 2) // Blink if game over
-	{
-		for (i = snake->snakelength - 1; i >= 0; i--)
-		{
-			const char *patchname;
-			UINT8 dir = snake->snakedir[i];
-
-			if (i == 0) // Head
-			{
-				switch (dir)
-				{
-					case  1: patchname = "DL_SNAKEHEAD_L"; break;
-					case  2: patchname = "DL_SNAKEHEAD_R"; break;
-					case  3: patchname = "DL_SNAKEHEAD_T"; break;
-					case  4: patchname = "DL_SNAKEHEAD_B"; break;
-					default: patchname = "DL_SNAKEHEAD_M";
-				}
-			}
-			else // Body
-			{
-				switch (dir)
-				{
-					case  1: patchname = "DL_SNAKEBODY_L"; break;
-					case  2: patchname = "DL_SNAKEBODY_R"; break;
-					case  3: patchname = "DL_SNAKEBODY_T"; break;
-					case  4: patchname = "DL_SNAKEBODY_B"; break;
-					case  5: patchname = "DL_SNAKEBODY_LT"; break;
-					case  6: patchname = "DL_SNAKEBODY_RT"; break;
-					case  7: patchname = "DL_SNAKEBODY_LB"; break;
-					case  8: patchname = "DL_SNAKEBODY_RB"; break;
-					case  9: patchname = "DL_SNAKEBODY_TL"; break;
-					case 10: patchname = "DL_SNAKEBODY_TR"; break;
-					case 11: patchname = "DL_SNAKEBODY_BL"; break;
-					case 12: patchname = "DL_SNAKEBODY_BR"; break;
-					default: patchname = "DL_SNAKEBODY_B";
-				}
-			}
-
-			V_DrawFixedPatch(
-				(SNAKE_LEFT_X + SNAKE_BORDER_SIZE + snake->snakex[i] * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2) * FRACUNIT,
-				(SNAKE_TOP_Y  + SNAKE_BORDER_SIZE + snake->snakey[i] * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2) * FRACUNIT,
-				i == 0 && dir == 0 ? FRACUNIT / 5 : FRACUNIT / 2,
-				snake->snakebonus == SNAKE_BONUS_GHOST ? V_TRANSLUCENT : 0,
-				W_CachePatchLongName(patchname, PU_HUDGFX),
-				NULL
-			);
-		}
-	}
-
-	// Length
-	V_DrawString(SNAKE_RIGHT_X + 4, SNAKE_TOP_Y, V_MONOSPACE, va("%u", snake->snakelength));
-
-	// Bonus
-	if (snake->snakebonus != SNAKE_BONUS_NONE
-	&& (snake->snakebonustime >= 3 * TICRATE || snake->time % 4 < 4 / 2))
-		V_DrawFixedPatch(
-			(SNAKE_RIGHT_X + 10) * FRACUNIT,
-			(SNAKE_TOP_Y + 24) * FRACUNIT,
-			FRACUNIT / 2,
-			0,
-			W_CachePatchLongName(snake_bonuspatches[snake->snakebonus], PU_HUDGFX),
-			NULL
-		);
-}
-
-static void CL_DrawConnectionStatusBox(void)
-{
-	M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
-	if (cl_mode != CL_CONFIRMCONNECT)
-		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
-}
-
-//
-// CL_DrawConnectionStatus
-//
-// Keep the local client informed of our status.
-//
-static inline void CL_DrawConnectionStatus(void)
-{
-	INT32 ccstime = I_GetTime();
-
-	// Draw background fade
-	V_DrawFadeScreen(0xFF00, 16); // force default
-
-	if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_LOADFILES)
-	{
-		INT32 i, animtime = ((ccstime / 4) & 15) + 16;
-		UINT8 palstart;
-		const char *cltext;
-
-		// Draw the bottom box.
-		CL_DrawConnectionStatusBox();
-
-		if (cl_mode == CL_SEARCHING)
-			palstart = 32; // Red
-		else if (cl_mode == CL_CONFIRMCONNECT)
-			palstart = 48; // Orange
-		else
-			palstart = 96; // Green
-
-		if (!(cl_mode == CL_DOWNLOADSAVEGAME && lastfilenum != -1))
-			for (i = 0; i < 16; ++i) // 15 pal entries total.
-				V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-16, 16, 8, palstart + ((animtime - i) & 15));
-
-		switch (cl_mode)
-		{
-			case CL_DOWNLOADSAVEGAME:
-				if (fileneeded && lastfilenum != -1)
-				{
-					UINT32 currentsize = fileneeded[lastfilenum].currentsize;
-					UINT32 totalsize = fileneeded[lastfilenum].totalsize;
-					INT32 dldlength;
-
-					cltext = M_GetText("Downloading game state...");
-					Net_GetNetStat();
-
-					dldlength = (INT32)((currentsize/(double)totalsize) * 256);
-					if (dldlength > 256)
-						dldlength = 256;
-					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
-					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
-
-					V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-						va(" %4uK/%4uK",currentsize>>10,totalsize>>10));
-
-					V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-						va("%3.1fK/s ", ((double)getbps)/1024));
-				}
-				else
-					cltext = M_GetText("Waiting to download game state...");
-				break;
-			case CL_ASKFULLFILELIST:
-			case CL_CHECKFILES:
-				cltext = M_GetText("Checking server addon list...");
-				break;
-			case CL_CONFIRMCONNECT:
-				cltext = "";
-				break;
-			case CL_LOADFILES:
-				cltext = M_GetText("Loading server addons...");
-				break;
-			case CL_ASKJOIN:
-			case CL_WAITJOINRESPONSE:
-				if (serverisfull)
-					cltext = M_GetText("Server full, waiting for a slot...");
-				else
-					cltext = M_GetText("Requesting to join...");
-				break;
-			default:
-				cltext = M_GetText("Connecting to server...");
-				break;
-		}
-		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, cltext);
-	}
-	else
-	{
-		if (cl_mode == CL_LOADFILES)
-		{
-			INT32 totalfileslength;
-			INT32 loadcompletednum = 0;
-			INT32 i;
-
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
-
-			//ima just count files here
-			if (fileneeded)
-			{
-				for (i = 0; i < fileneedednum; i++)
-					if (fileneeded[i].status == FS_OPEN)
-						loadcompletednum++;
-			}
-
-			// Loading progress
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, "Loading server addons...");
-			totalfileslength = (INT32)((loadcompletednum/(double)(fileneedednum)) * 256);
-			M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, totalfileslength, 8, 96);
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-				va(" %2u/%2u Files",loadcompletednum,fileneedednum));
-		}
-		else if (lastfilenum != -1)
-		{
-			INT32 dldlength;
-			static char tempname[28];
-			fileneeded_t *file;
-			char *filename;
-
-			if (snake)
-				Snake_Draw();
-
-			// Draw the bottom box.
-			CL_DrawConnectionStatusBox();
-
-			if (fileneeded)
-			{
-				file = &fileneeded[lastfilenum];
-				filename = file->filename;
-			}
-			else
-				return;
-
-			Net_GetNetStat();
-			dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256);
-			if (dldlength > 256)
-				dldlength = 256;
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
-
-			memset(tempname, 0, sizeof(tempname));
-			// offset filename to just the name only part
-			filename += strlen(filename) - nameonlylength(filename);
-
-			if (strlen(filename) > sizeof(tempname)-1) // too long to display fully
-			{
-				size_t endhalfpos = strlen(filename)-10;
-				// display as first 14 chars + ... + last 10 chars
-				// which should add up to 27 if our math(s) is correct
-				snprintf(tempname, sizeof(tempname), "%.14s...%.10s", filename, filename+endhalfpos);
-			}
-			else // we can copy the whole thing in safely
-			{
-				strncpy(tempname, filename, sizeof(tempname)-1);
-			}
-
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
-				va(M_GetText("Downloading \"%s\""), tempname));
-			V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-				va(" %4uK/%4uK",fileneeded[lastfilenum].currentsize>>10,file->totalsize>>10));
-			V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-				va("%3.1fK/s ", ((double)getbps)/1024));
-		}
-		else
-		{
-			if (snake)
-				Snake_Draw();
-
-			CL_DrawConnectionStatusBox();
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
-				M_GetText("Waiting to download files..."));
-		}
-	}
-}
-#endif
-
-static boolean CL_AskFileList(INT32 firstfile)
-{
-	netbuffer->packettype = PT_TELLFILESNEEDED;
-	netbuffer->u.filesneedednum = firstfile;
-
-	return HSendPacket(servernode, false, 0, sizeof (INT32));
-}
-
-/** Sends a special packet to declare how many players in local
-  * Used only in arbitratrenetstart()
-  * Sends a PT_CLIENTJOIN packet to the server
-  *
-  * \return True if the packet was successfully sent
-  * \todo Improve the description...
-  *
-  */
-static boolean CL_SendJoin(void)
-{
-	UINT8 localplayers = 1;
-	char const *player2name;
-	if (netgame)
-		CONS_Printf(M_GetText("Sending join request...\n"));
-	netbuffer->packettype = PT_CLIENTJOIN;
-
-	netbuffer->u.clientcfg.modversion = MODVERSION;
-	strncpy(netbuffer->u.clientcfg.application,
-			SRB2APPLICATION,
-			sizeof netbuffer->u.clientcfg.application);
-
-	if (splitscreen || botingame)
-		localplayers++;
-	netbuffer->u.clientcfg.localplayers = localplayers;
-
-	CleanupPlayerName(consoleplayer, cv_playername.zstring);
-	if (splitscreen)
-		CleanupPlayerName(1, cv_playername2.zstring);/* 1 is a HACK? oh no */
-	// Avoid empty string on bots to avoid softlocking in singleplayer
-	if (botingame)
-		player2name = strcmp(cv_playername.zstring, "Tails") == 0 ? "Tail" : "Tails";
-	else
-		player2name = cv_playername2.zstring;
-
-	strncpy(netbuffer->u.clientcfg.names[0], cv_playername.zstring, MAXPLAYERNAME);
-	strncpy(netbuffer->u.clientcfg.names[1], player2name, MAXPLAYERNAME);
-
-	return HSendPacket(servernode, true, 0, sizeof (clientconfig_pak));
-}
-
-static INT32 FindRejoinerNum(SINT8 node)
-{
-	char addressbuffer[64];
-	const char *nodeaddress;
-	const char *strippednodeaddress;
-	INT32 i;
-
-	// Make sure there is no dead dress before proceeding to the stripping
-	if (!I_GetNodeAddress)
-		return -1;
-	nodeaddress = I_GetNodeAddress(node);
-	if (!nodeaddress)
-		return -1;
-
-	// Strip the address of its port
-	strcpy(addressbuffer, nodeaddress);
-	strippednodeaddress = I_NetSplitAddress(addressbuffer, NULL);
-
-	// Check if any player matches the stripped address
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		if (playeringame[i] && playeraddress[i][0] && playernode[i] == UINT8_MAX
-		&& !strcmp(playeraddress[i], strippednodeaddress))
-			return i;
-	}
-
-	return -1;
-}
-
-static UINT8
-GetRefuseReason (INT32 node)
-{
-	if (!node || FindRejoinerNum(node) != -1)
-		return 0;
-	else if (bannednode && bannednode[node])
-		return REFUSE_BANNED;
-	else if (!cv_allownewplayer.value)
-		return REFUSE_JOINS_DISABLED;
-	else if (D_NumPlayers() >= cv_maxplayers.value)
-		return REFUSE_SLOTS_FULL;
-	else
-		return 0;
-}
-
-static void SV_SendServerInfo(INT32 node, tic_t servertime)
-{
-	UINT8 *p;
-
-	netbuffer->packettype = PT_SERVERINFO;
-	netbuffer->u.serverinfo._255 = 255;
-	netbuffer->u.serverinfo.packetversion = PACKETVERSION;
-	netbuffer->u.serverinfo.version = VERSION;
-	netbuffer->u.serverinfo.subversion = SUBVERSION;
-	strncpy(netbuffer->u.serverinfo.application, SRB2APPLICATION,
-			sizeof netbuffer->u.serverinfo.application);
-	// return back the time value so client can compute their ping
-	netbuffer->u.serverinfo.time = (tic_t)LONG(servertime);
-	netbuffer->u.serverinfo.leveltime = (tic_t)LONG(leveltime);
-
-	// Exclude bots from both counts
-	netbuffer->u.serverinfo.numberofplayer = (UINT8)(D_NumPlayers() - D_NumBots());
-	netbuffer->u.serverinfo.maxplayer = (UINT8)(cv_maxplayers.value - D_NumBots());
-
-	netbuffer->u.serverinfo.refusereason = GetRefuseReason(node);
-
-	strncpy(netbuffer->u.serverinfo.gametypename, Gametype_Names[gametype],
-			sizeof netbuffer->u.serverinfo.gametypename);
-	netbuffer->u.serverinfo.modifiedgame = (UINT8)modifiedgame;
-	netbuffer->u.serverinfo.cheatsenabled = CV_CheatsEnabled();
-	netbuffer->u.serverinfo.flags = (dedicated ? SV_DEDICATED : 0);
-	strncpy(netbuffer->u.serverinfo.servername, cv_servername.string,
-		MAXSERVERNAME);
-	strncpy(netbuffer->u.serverinfo.mapname, G_BuildMapName(gamemap), 7);
-
-	M_Memcpy(netbuffer->u.serverinfo.mapmd5, mapmd5, 16);
-
-	memset(netbuffer->u.serverinfo.maptitle, 0, sizeof netbuffer->u.serverinfo.maptitle);
-
-	if (mapheaderinfo[gamemap-1] && *mapheaderinfo[gamemap-1]->lvlttl)
-	{
-		char *read = mapheaderinfo[gamemap-1]->lvlttl, *writ = netbuffer->u.serverinfo.maptitle;
-		while (writ < (netbuffer->u.serverinfo.maptitle+32) && *read != '\0')
-		{
-			if (!(*read & 0x80))
-			{
-				*writ = toupper(*read);
-				writ++;
-			}
-			read++;
-		}
-		*writ = '\0';
-		//strncpy(netbuffer->u.serverinfo.maptitle, (char *)mapheaderinfo[gamemap-1]->lvlttl, 33);
-	}
-	else
-		strncpy(netbuffer->u.serverinfo.maptitle, "UNKNOWN", 32);
-
-	if (mapheaderinfo[gamemap-1] && !(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
-		netbuffer->u.serverinfo.iszone = 1;
-	else
-		netbuffer->u.serverinfo.iszone = 0;
-
-	if (mapheaderinfo[gamemap-1])
-		netbuffer->u.serverinfo.actnum = mapheaderinfo[gamemap-1]->actnum;
-
-	p = PutFileNeeded(0);
-
-	HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
-}
-
-static void SV_SendPlayerInfo(INT32 node)
-{
-	UINT8 i;
-	netbuffer->packettype = PT_PLAYERINFO;
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		if (!playeringame[i])
-		{
-			netbuffer->u.playerinfo[i].num = 255; // This slot is empty.
-			continue;
-		}
-
-		netbuffer->u.playerinfo[i].num = i;
-		strncpy(netbuffer->u.playerinfo[i].name, (const char *)&player_names[i], MAXPLAYERNAME+1);
-		netbuffer->u.playerinfo[i].name[MAXPLAYERNAME] = '\0';
-
-		//fetch IP address
-		//No, don't do that, you fuckface.
-		memset(netbuffer->u.playerinfo[i].address, 0, 4);
-
-		if (G_GametypeHasTeams())
-		{
-			if (!players[i].ctfteam)
-				netbuffer->u.playerinfo[i].team = 255;
-			else
-				netbuffer->u.playerinfo[i].team = (UINT8)players[i].ctfteam;
-		}
-		else
-		{
-			if (players[i].spectator)
-				netbuffer->u.playerinfo[i].team = 255;
-			else
-				netbuffer->u.playerinfo[i].team = 0;
-		}
-
-		netbuffer->u.playerinfo[i].score = LONG(players[i].score);
-		netbuffer->u.playerinfo[i].timeinserver = SHORT((UINT16)(players[i].jointime / TICRATE));
-		netbuffer->u.playerinfo[i].skin = (UINT8)(players[i].skin
-#ifdef DEVELOP // it's safe to do this only because PLAYERINFO isn't read by the game itself
-		% 3
-#endif
-		);
-
-		// Extra data
-		netbuffer->u.playerinfo[i].data = 0; //players[i].skincolor;
-
-		if (players[i].pflags & PF_TAGIT)
-			netbuffer->u.playerinfo[i].data |= 0x20;
-
-		if (players[i].gotflag)
-			netbuffer->u.playerinfo[i].data |= 0x40;
-
-		if (players[i].powers[pw_super])
-			netbuffer->u.playerinfo[i].data |= 0x80;
-	}
-
-	HSendPacket(node, false, 0, sizeof(plrinfo) * MAXPLAYERS);
-}
-
-/** Sends a PT_SERVERCFG packet
-  *
-  * \param node The destination
-  * \return True if the packet was successfully sent
-  *
-  */
-static boolean SV_SendServerConfig(INT32 node)
-{
-	boolean waspacketsent;
-
-	netbuffer->packettype = PT_SERVERCFG;
-
-	netbuffer->u.servercfg.serverplayer = (UINT8)serverplayer;
-	netbuffer->u.servercfg.totalslotnum = (UINT8)(doomcom->numslots);
-	netbuffer->u.servercfg.gametic = (tic_t)LONG(gametic);
-	netbuffer->u.servercfg.clientnode = (UINT8)node;
-	netbuffer->u.servercfg.gamestate = (UINT8)gamestate;
-	netbuffer->u.servercfg.gametype = (UINT8)gametype;
-	netbuffer->u.servercfg.modifiedgame = (UINT8)modifiedgame;
-	netbuffer->u.servercfg.usedCheats = (UINT8)usedCheats;
-
-	memcpy(netbuffer->u.servercfg.server_context, server_context, 8);
-
-	{
-		const size_t len = sizeof (serverconfig_pak);
-
-#ifdef DEBUGFILE
-		if (debugfile)
-		{
-			fprintf(debugfile, "ServerConfig Packet about to be sent, size of packet:%s to node:%d\n",
-				sizeu1(len), node);
-		}
-#endif
-
-		waspacketsent = HSendPacket(node, true, 0, len);
-	}
-
-#ifdef DEBUGFILE
-	if (debugfile)
-	{
-		if (waspacketsent)
-		{
-			fprintf(debugfile, "ServerConfig Packet was sent\n");
-		}
-		else
-		{
-			fprintf(debugfile, "ServerConfig Packet could not be sent right now\n");
-		}
-	}
-#endif
-
-	return waspacketsent;
-}
-
-#ifndef NONET
-#define SAVEGAMESIZE (768*1024)
-
-static boolean SV_ResendingSavegameToAnyone(void)
-{
-	INT32 i;
-
-	for (i = 0; i < MAXNETNODES; i++)
-		if (resendingsavegame[i])
-			return true;
-	return false;
-}
-
-static void SV_SendSaveGame(INT32 node, boolean resending)
-{
-	size_t length, compressedlen;
-	UINT8 *savebuffer;
-	UINT8 *compressedsave;
-	UINT8 *buffertosend;
-
-	// first save it in a malloced buffer
-	savebuffer = (UINT8 *)malloc(SAVEGAMESIZE);
-	if (!savebuffer)
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
-		return;
-	}
-
-	// Leave room for the uncompressed length.
-	save_p = savebuffer + sizeof(UINT32);
-
-	P_SaveNetGame(resending);
-
-	length = save_p - savebuffer;
-	if (length > SAVEGAMESIZE)
-	{
-		free(savebuffer);
-		save_p = NULL;
-		I_Error("Savegame buffer overrun");
-	}
-
-	// Allocate space for compressed save: one byte fewer than for the
-	// uncompressed data to ensure that the compression is worthwhile.
-	compressedsave = malloc(length - 1);
-	if (!compressedsave)
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
-		return;
-	}
-
-	// Attempt to compress it.
-	if((compressedlen = lzf_compress(savebuffer + sizeof(UINT32), length - sizeof(UINT32), compressedsave + sizeof(UINT32), length - sizeof(UINT32) - 1)))
-	{
-		// Compressing succeeded; send compressed data
-
-		free(savebuffer);
-
-		// State that we're compressed.
-		buffertosend = compressedsave;
-		WRITEUINT32(compressedsave, length - sizeof(UINT32));
-		length = compressedlen + sizeof(UINT32);
-	}
-	else
-	{
-		// Compression failed to make it smaller; send original
-
-		free(compressedsave);
-
-		// State that we're not compressed
-		buffertosend = savebuffer;
-		WRITEUINT32(savebuffer, 0);
-	}
-
-	AddRamToSendQueue(node, buffertosend, length, SF_RAM, 0);
-	save_p = NULL;
-
-	// Remember when we started sending the savegame so we can handle timeouts
-	sendingsavegame[node] = true;
-	freezetimeout[node] = I_GetTime() + jointimeout + length / 1024; // 1 extra tic for each kilobyte
-}
-
-#ifdef DUMPCONSISTENCY
-#define TMPSAVENAME "badmath.sav"
-static consvar_t cv_dumpconsistency = CVAR_INIT ("dumpconsistency", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
-
-static void SV_SavedGame(void)
-{
-	size_t length;
-	UINT8 *savebuffer;
-	char tmpsave[256];
-
-	if (!cv_dumpconsistency.value)
-		return;
-
-	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
-
-	// first save it in a malloced buffer
-	save_p = savebuffer = (UINT8 *)malloc(SAVEGAMESIZE);
-	if (!save_p)
-	{
-		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
-		return;
-	}
-
-	P_SaveNetGame(false);
-
-	length = save_p - savebuffer;
-	if (length > SAVEGAMESIZE)
-	{
-		free(savebuffer);
-		save_p = NULL;
-		I_Error("Savegame buffer overrun");
-	}
-
-	// then save it!
-	if (!FIL_WriteFile(tmpsave, savebuffer, length))
-		CONS_Printf(M_GetText("Didn't save %s for netgame"), tmpsave);
-
-	free(savebuffer);
-	save_p = NULL;
-}
-
-#undef  TMPSAVENAME
-#endif
-#define TMPSAVENAME "$$$.sav"
-
-
-static void CL_LoadReceivedSavegame(boolean reloading)
-{
-	UINT8 *savebuffer = NULL;
-	size_t length, decompressedlen;
-	char tmpsave[256];
-
-	FreeFileNeeded();
-
-	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
-
-	length = FIL_ReadFile(tmpsave, &savebuffer);
-
-	CONS_Printf(M_GetText("Loading savegame length %s\n"), sizeu1(length));
-	if (!length)
-	{
-		I_Error("Can't read savegame sent");
-		return;
-	}
-
-	save_p = savebuffer;
-
-	// Decompress saved game if necessary.
-	decompressedlen = READUINT32(save_p);
-	if(decompressedlen > 0)
-	{
-		UINT8 *decompressedbuffer = Z_Malloc(decompressedlen, PU_STATIC, NULL);
-		lzf_decompress(save_p, length - sizeof(UINT32), decompressedbuffer, decompressedlen);
-		Z_Free(savebuffer);
-		save_p = savebuffer = decompressedbuffer;
-	}
-
-	paused = false;
-	demoplayback = false;
-	titlemapinaction = TITLEMAP_OFF;
-	titledemo = false;
-	automapactive = false;
-
-	// load a base level
-	if (P_LoadNetGame(reloading))
-	{
-		const UINT8 actnum = mapheaderinfo[gamemap-1]->actnum;
-		CONS_Printf(M_GetText("Map is now \"%s"), G_BuildMapName(gamemap));
-		if (strcmp(mapheaderinfo[gamemap-1]->lvlttl, ""))
-		{
-			CONS_Printf(": %s", mapheaderinfo[gamemap-1]->lvlttl);
-			if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
-				CONS_Printf(M_GetText(" Zone"));
-			if (actnum > 0)
-				CONS_Printf(" %2d", actnum);
-		}
-		CONS_Printf("\"\n");
-	}
-
-	// done
-	Z_Free(savebuffer);
-	save_p = NULL;
-	if (unlink(tmpsave) == -1)
-		CONS_Alert(CONS_ERROR, M_GetText("Can't delete %s\n"), tmpsave);
-	consistancy[gametic%BACKUPTICS] = Consistancy();
-	CON_ToggleOff();
-
-	// Tell the server we have received and reloaded the gamestate
-	// so they know they can resume the game
-	netbuffer->packettype = PT_RECEIVEDGAMESTATE;
-	HSendPacket(servernode, true, 0, 0);
-}
-
-static void CL_ReloadReceivedSavegame(void)
-{
-	INT32 i;
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		LUA_InvalidatePlayer(&players[i]);
-		sprintf(player_names[i], "Player %d", i + 1);
-	}
-
-	CL_LoadReceivedSavegame(true);
-
-	if (neededtic < gametic)
-		neededtic = gametic;
-	maketic = neededtic;
-
-	ticcmd_oldangleturn[0] = players[consoleplayer].oldrelangleturn;
-	P_ForceLocalAngle(&players[consoleplayer], (angle_t)(players[consoleplayer].angleturn << 16));
-	if (splitscreen)
-	{
-		ticcmd_oldangleturn[1] = players[secondarydisplayplayer].oldrelangleturn;
-		P_ForceLocalAngle(&players[secondarydisplayplayer], (angle_t)(players[secondarydisplayplayer].angleturn << 16));
-	}
-
-	camera.subsector = R_PointInSubsector(camera.x, camera.y);
-	camera2.subsector = R_PointInSubsector(camera2.x, camera2.y);
-
-	cl_redownloadinggamestate = false;
-
-	CONS_Printf(M_GetText("Game state reloaded\n"));
-}
-#endif
-
-#ifndef NONET
-static void SendAskInfo(INT32 node)
-{
-	const tic_t asktime = I_GetTime();
-	netbuffer->packettype = PT_ASKINFO;
-	netbuffer->u.askinfo.version = VERSION;
-	netbuffer->u.askinfo.time = (tic_t)LONG(asktime);
-
-	// Even if this never arrives due to the host being firewalled, we've
-	// now allowed traffic from the host to us in, so once the MS relays
-	// our address to the host, it'll be able to speak to us.
-	HSendPacket(node, false, 0, sizeof (askinfo_pak));
-}
-
-serverelem_t serverlist[MAXSERVERLIST];
-UINT32 serverlistcount = 0;
-
-#define FORCECLOSE 0x8000
-
-static void SL_ClearServerList(INT32 connectedserver)
-{
-	UINT32 i;
-
-	for (i = 0; i < serverlistcount; i++)
-		if (connectedserver != serverlist[i].node)
-		{
-			Net_CloseConnection(serverlist[i].node|FORCECLOSE);
-			serverlist[i].node = 0;
-		}
-	serverlistcount = 0;
-}
-
-static UINT32 SL_SearchServer(INT32 node)
-{
-	UINT32 i;
-	for (i = 0; i < serverlistcount; i++)
-		if (serverlist[i].node == node)
-			return i;
-
-	return UINT32_MAX;
-}
-
-static void SL_InsertServer(serverinfo_pak* info, SINT8 node)
-{
-	UINT32 i;
-
-	// search if not already on it
-	i = SL_SearchServer(node);
-	if (i == UINT32_MAX)
-	{
-		// not found add it
-		if (serverlistcount >= MAXSERVERLIST)
-			return; // list full
-
-		/* check it later if connecting to this one */
-		if (node != servernode)
-		{
-			if (info->_255 != 255)
-				return;/* old packet format */
-
-			if (info->packetversion != PACKETVERSION)
-				return;/* old new packet format */
-
-			if (info->version != VERSION)
-				return; // Not same version.
-
-			if (info->subversion != SUBVERSION)
-				return; // Close, but no cigar.
-
-			if (strcmp(info->application, SRB2APPLICATION))
-				return;/* that's a different mod */
-		}
-
-		i = serverlistcount++;
-	}
-
-	serverlist[i].info = *info;
-	serverlist[i].node = node;
-
-	// resort server list
-	M_SortServerList();
-}
-
-#if defined (MASTERSERVER) && defined (HAVE_THREADS)
-struct Fetch_servers_ctx
-{
-	int room;
-	int id;
-};
-
-static void
-Fetch_servers_thread (struct Fetch_servers_ctx *ctx)
-{
-	msg_server_t *server_list;
-
-	server_list = GetShortServersList(ctx->room, ctx->id);
-
-	if (server_list)
-	{
-		I_lock_mutex(&ms_QueryId_mutex);
-		{
-			if (ctx->id != ms_QueryId)
-			{
-				free(server_list);
-				server_list = NULL;
-			}
-		}
-		I_unlock_mutex(ms_QueryId_mutex);
-
-		if (server_list)
-		{
-			I_lock_mutex(&m_menu_mutex);
-			{
-				if (m_waiting_mode == M_WAITING_SERVERS)
-					m_waiting_mode = M_NOT_WAITING;
-			}
-			I_unlock_mutex(m_menu_mutex);
-
-			I_lock_mutex(&ms_ServerList_mutex);
-			{
-				ms_ServerList = server_list;
-			}
-			I_unlock_mutex(ms_ServerList_mutex);
-		}
-	}
-
-	free(ctx);
-}
-#endif/*defined (MASTERSERVER) && defined (HAVE_THREADS)*/
-
-void CL_QueryServerList (msg_server_t *server_list)
-{
-	INT32 i;
-
-	for (i = 0; server_list[i].header.buffer[0]; i++)
-	{
-		// Make sure MS version matches our own, to
-		// thwart nefarious servers who lie to the MS.
-
-		/* lol bruh, that version COMES from the servers */
-		//if (strcmp(version, server_list[i].version) == 0)
-		{
-			INT32 node = I_NetMakeNodewPort(server_list[i].ip, server_list[i].port);
-			if (node == -1)
-				break; // no more node free
-			SendAskInfo(node);
-			// Force close the connection so that servers can't eat
-			// up nodes forever if we never get a reply back from them
-			// (usually when they've not forwarded their ports).
-			//
-			// Don't worry, we'll get in contact with the working
-			// servers again when they send SERVERINFO to us later!
-			//
-			// (Note: as a side effect this probably means every
-			// server in the list will probably be using the same node (e.g. node 1),
-			// not that it matters which nodes they use when
-			// the connections are closed afterwards anyway)
-			// -- Monster Iestyn 12/11/18
-			Net_CloseConnection(node|FORCECLOSE);
-		}
-	}
-}
-
-void CL_UpdateServerList(boolean internetsearch, INT32 room)
-{
-	(void)internetsearch;
-	(void)room;
-
-	SL_ClearServerList(0);
-
-	if (!netgame && I_NetOpenSocket)
-	{
-		if (I_NetOpenSocket())
-		{
-			netgame = true;
-			multiplayer = true;
-		}
-	}
-
-	// search for local servers
-	if (netgame)
-		SendAskInfo(BROADCASTADDR);
-
-#ifdef MASTERSERVER
-	if (internetsearch)
-	{
-#ifdef HAVE_THREADS
-		struct Fetch_servers_ctx *ctx;
-
-		ctx = malloc(sizeof *ctx);
-
-		/* This called from M_Refresh so I don't use a mutex */
-		m_waiting_mode = M_WAITING_SERVERS;
-
-		I_lock_mutex(&ms_QueryId_mutex);
-		{
-			ctx->id = ms_QueryId;
-		}
-		I_unlock_mutex(ms_QueryId_mutex);
-
-		ctx->room = room;
-
-		I_spawn_thread("fetch-servers", (I_thread_fn)Fetch_servers_thread, ctx);
-#else
-		msg_server_t *server_list;
-
-		server_list = GetShortServersList(room, 0);
-
-		if (server_list)
-		{
-			CL_QueryServerList(server_list);
-			free(server_list);
-		}
-#endif
-	}
-#endif/*MASTERSERVER*/
-}
-
-#endif // ifndef NONET
-
-static void M_ConfirmConnect(event_t *ev)
-{
-#ifndef NONET
-	if (ev->type == ev_keydown)
-	{
-		if (ev->key == ' ' || ev->key == 'y' || ev->key == KEY_ENTER || ev->key == KEY_JOY1)
-		{
-			if (totalfilesrequestednum > 0)
-			{
-				if (CL_SendFileRequest())
-				{
-					cl_mode = CL_DOWNLOADFILES;
-					Snake_Initialise();
-				}
-			}
-			else
-				cl_mode = CL_LOADFILES;
-
-			M_ClearMenus(true);
-		}
-		else if (ev->key == 'n' || ev->key == KEY_ESCAPE || ev->key == KEY_JOY1 + 3)
-		{
-			cl_mode = CL_ABORTED;
-			M_ClearMenus(true);
-		}
-	}
-#else
-	(void)ev;
-#endif
-}
-
-static boolean CL_FinishedFileList(void)
-{
-	INT32 i;
-	char *downloadsize = NULL;
-
-	//CONS_Printf(M_GetText("Checking files...\n"));
-	i = CL_CheckFiles();
-	if (i == 4) // still checking ...
-	{
-		return true;
-	}
-	else if (i == 3) // too many files
-	{
-		D_QuitNetGame();
-		CL_Reset();
-		D_StartTitle();
-		M_StartMessage(M_GetText(
-			"You have too many WAD files loaded\n"
-			"to add ones the server is using.\n"
-			"Please restart SRB2 before connecting.\n\n"
-			"Press ESC\n"
-		), NULL, MM_NOTHING);
-		return false;
-	}
-	else if (i == 2) // cannot join for some reason
-	{
-		D_QuitNetGame();
-		CL_Reset();
-		D_StartTitle();
-		M_StartMessage(M_GetText(
-			"You have the wrong addons loaded.\n\n"
-			"To play on this server, restart\n"
-			"the game and don't load any addons.\n"
-			"SRB2 will automatically add\n"
-			"everything you need when you join.\n\n"
-			"Press ESC\n"
-		), NULL, MM_NOTHING);
-		return false;
-	}
-	else if (i == 1)
-	{
-		if (serverisfull)
-		{
-			M_StartMessage(M_GetText(
-				"This server is full!\n"
-				"\n"
-				"You may load server addons (if any), and wait for a slot.\n"
-				"\n"
-				"Press ENTER to continue\nor ESC to cancel.\n\n"
-			), M_ConfirmConnect, MM_EVENTHANDLER);
-			cl_mode = CL_CONFIRMCONNECT;
-			curfadevalue = 0;
-		}
-		else
-			cl_mode = CL_LOADFILES;
-	}
-	else
-	{
-		// must download something
-		// can we, though?
-		if (!CL_CheckDownloadable()) // nope!
-		{
-			D_QuitNetGame();
-			CL_Reset();
-			D_StartTitle();
-			M_StartMessage(M_GetText(
-				"An error occured when trying to\n"
-				"download missing addons.\n"
-				"(This is almost always a problem\n"
-				"with the server, not your game.)\n\n"
-				"See the console or log file\n"
-				"for additional details.\n\n"
-				"Press ESC\n"
-			), NULL, MM_NOTHING);
-			return false;
-		}
-
-#ifndef NONET
-		downloadcompletednum = 0;
-		downloadcompletedsize = 0;
-		totalfilesrequestednum = 0;
-		totalfilesrequestedsize = 0;
-
-		if (fileneeded == NULL)
-			I_Error("CL_FinishedFileList: fileneeded == NULL");
-
-		for (i = 0; i < fileneedednum; i++)
-			if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
-			{
-				totalfilesrequestednum++;
-				totalfilesrequestedsize += fileneeded[i].totalsize;
-			}
-
-		if (totalfilesrequestedsize>>20 >= 100)
-			downloadsize = Z_StrDup(va("%uM",totalfilesrequestedsize>>20));
-		else
-			downloadsize = Z_StrDup(va("%uK",totalfilesrequestedsize>>10));
-#endif
-
-		if (serverisfull)
-			M_StartMessage(va(M_GetText(
-				"This server is full!\n"
-				"Download of %s additional content\nis required to join.\n"
-				"\n"
-				"You may download, load server addons,\nand wait for a slot.\n"
-				"\n"
-				"Press ENTER to continue\nor ESC to cancel.\n"
-			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
-		else
-			M_StartMessage(va(M_GetText(
-				"Download of %s additional content\nis required to join.\n"
-				"\n"
-				"Press ENTER to continue\nor ESC to cancel.\n"
-			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
-
-		Z_Free(downloadsize);
-		cl_mode = CL_CONFIRMCONNECT;
-		curfadevalue = 0;
-	}
-	return true;
-}
-
-#ifndef NONET
-static const char * InvalidServerReason (serverinfo_pak *info)
-{
-#define EOT "\nPress ESC\n"
-
-	/* magic number for new packet format */
-	if (info->_255 != 255)
-	{
-		return
-			"Outdated server (version unknown).\n" EOT;
-	}
-
-	if (strncmp(info->application, SRB2APPLICATION, sizeof
-				info->application))
-	{
-		return va(
-				"%s cannot connect\n"
-				"to %s servers.\n" EOT,
-				SRB2APPLICATION,
-				info->application);
-	}
-
-	if (
-			info->packetversion != PACKETVERSION ||
-			info->version != VERSION ||
-			info->subversion != SUBVERSION
-	){
-		return va(
-				"Incompatible %s versions.\n"
-				"(server version %d.%d.%d)\n" EOT,
-				SRB2APPLICATION,
-				info->version / 100,
-				info->version % 100,
-				info->subversion);
-	}
-
-	switch (info->refusereason)
-	{
-		case REFUSE_BANNED:
-			return
-				"You have been banned\n"
-				"from the server.\n" EOT;
-		case REFUSE_JOINS_DISABLED:
-			return
-				"The server is not accepting\n"
-				"joins for the moment.\n" EOT;
-		case REFUSE_SLOTS_FULL:
-			return va(
-					"Maximum players reached: %d\n" EOT,
-					info->maxplayer);
-		default:
-			if (info->refusereason)
-			{
-				return
-					"You can't join.\n"
-					"I don't know why,\n"
-					"but you can't join.\n" EOT;
-			}
-	}
-
-	return NULL;
-
-#undef EOT
-}
-#endif // ifndef NONET
-
-/** Called by CL_ServerConnectionTicker
-  *
-  * \param asksent The last time we asked the server to join. We re-ask every second in case our request got lost in transmit.
-  * \return False if the connection was aborted
-  * \sa CL_ServerConnectionTicker
-  * \sa CL_ConnectToServer
-  *
-  */
-static boolean CL_ServerConnectionSearchTicker(tic_t *asksent)
-{
-#ifndef NONET
-	INT32 i;
-
-	// serverlist is updated by GetPacket function
-	if (serverlistcount > 0)
-	{
-		// this can be a responce to our broadcast request
-		if (servernode == -1 || servernode >= MAXNETNODES)
-		{
-			i = 0;
-			servernode = serverlist[i].node;
-			CONS_Printf(M_GetText("Found, "));
-		}
-		else
-		{
-			i = SL_SearchServer(servernode);
-			if (i < 0)
-				return true;
-		}
-
-		if (client)
-		{
-			serverinfo_pak *info = &serverlist[i].info;
-
-			if (info->refusereason == REFUSE_SLOTS_FULL)
-				serverisfull = true;
-			else
-			{
-				const char *reason = InvalidServerReason(info);
-
-				// Quit here rather than downloading files
-				// and being refused later.
-				if (reason)
-				{
-					char *message = Z_StrDup(reason);
-					D_QuitNetGame();
-					CL_Reset();
-					D_StartTitle();
-					M_StartMessage(message, NULL, MM_NOTHING);
-					Z_Free(message);
-					return false;
-				}
-			}
-
-			D_ParseFileneeded(info->fileneedednum, info->fileneeded, 0);
-
-			if (info->flags & SV_LOTSOFADDONS)
-			{
-				cl_mode = CL_ASKFULLFILELIST;
-				cl_lastcheckedfilecount = 0;
-				return true;
-			}
-
-			cl_mode = CL_CHECKFILES;
-		}
-		else
-		{
-			cl_mode = CL_ASKJOIN; // files need not be checked for the server.
-			*asksent = 0;
-		}
-
-		return true;
-	}
-
-	// Ask the info to the server (askinfo packet)
-	if (*asksent + NEWTICRATE < I_GetTime())
-	{
-		SendAskInfo(servernode);
-		*asksent = I_GetTime();
-	}
-#else
-	(void)asksent;
-	// No netgames, so we skip this state.
-	cl_mode = CL_ASKJOIN;
-#endif // ifndef NONET/else
-
-	return true;
-}
-
-/** Called by CL_ConnectToServer
-  *
-  * \param tmpsave The name of the gamestate file???
-  * \param oldtic Used for knowing when to poll events and redraw
-  * \param asksent ???
-  * \return False if the connection was aborted
-  * \sa CL_ServerConnectionSearchTicker
-  * \sa CL_ConnectToServer
-  *
-  */
-static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic_t *asksent)
-{
-	boolean waitmore;
-	INT32 i;
-
-#ifdef NONET
-	(void)tmpsave;
-#endif
-
-	switch (cl_mode)
-	{
-		case CL_SEARCHING:
-			if (!CL_ServerConnectionSearchTicker(asksent))
-				return false;
-			break;
-
-		case CL_ASKFULLFILELIST:
-			if (cl_lastcheckedfilecount == UINT16_MAX) // All files retrieved
-				cl_mode = CL_CHECKFILES;
-			else if (fileneedednum != cl_lastcheckedfilecount || I_GetTime() >= *asksent)
-			{
-				if (CL_AskFileList(fileneedednum))
-				{
-					cl_lastcheckedfilecount = fileneedednum;
-					*asksent = I_GetTime() + NEWTICRATE;
-				}
-			}
-			break;
-		case CL_CHECKFILES:
-			if (!CL_FinishedFileList())
-				return false;
-			break;
-		case CL_DOWNLOADFILES:
-			waitmore = false;
-			for (i = 0; i < fileneedednum; i++)
-				if (fileneeded[i].status == FS_DOWNLOADING
-					|| fileneeded[i].status == FS_REQUESTED)
-				{
-					waitmore = true;
-					break;
-				}
-			if (waitmore)
-				break; // exit the case
-
-#ifndef NONET
-			if (snake)
-			{
-				free(snake);
-				snake = NULL;
-			}
-#endif
-
-			cl_mode = CL_LOADFILES;
-			break;
-		case CL_LOADFILES:
-			if (CL_LoadServerFiles())
-			{
-				FreeFileNeeded();
-				*asksent = 0; //This ensure the first join ask is right away
-				firstconnectattempttime = I_GetTime();
-				cl_mode = CL_ASKJOIN;
-			}
-			break;
-		case CL_ASKJOIN:
-			if (firstconnectattempttime + NEWTICRATE*300 < I_GetTime() && !server)
-			{
-				CONS_Printf(M_GetText("5 minute wait time exceeded.\n"));
-				CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
-				M_StartMessage(M_GetText(
-					"5 minute wait time exceeded.\n"
-					"You may retry connection.\n"
-					"\n"
-					"Press ESC\n"
-				), NULL, MM_NOTHING);
-				return false;
-			}
-#ifndef NONET
-			// prepare structures to save the file
-			// WARNING: this can be useless in case of server not in GS_LEVEL
-			// but since the network layer doesn't provide ordered packets...
-			CL_PrepareDownloadSaveGame(tmpsave);
-#endif
-			if (I_GetTime() >= *asksent && CL_SendJoin())
-			{
-				*asksent = I_GetTime() + NEWTICRATE*3;
-				cl_mode = CL_WAITJOINRESPONSE;
-			}
-			break;
-		case CL_WAITJOINRESPONSE:
-			if (I_GetTime() >= *asksent)
-			{
-				cl_mode = CL_ASKJOIN;
-			}
-			break;
-#ifndef NONET
-		case CL_DOWNLOADSAVEGAME:
-			// At this state, the first (and only) needed file is the gamestate
-			if (fileneeded[0].status == FS_FOUND)
-			{
-				// Gamestate is now handled within CL_LoadReceivedSavegame()
-				CL_LoadReceivedSavegame(false);
-				cl_mode = CL_CONNECTED;
-			} // don't break case continue to CL_CONNECTED
-			else
-				break;
-#endif
-
-		case CL_CONNECTED:
-		case CL_CONFIRMCONNECT: //logic is handled by M_ConfirmConnect
-		default:
-			break;
-
-		// Connection closed by cancel, timeout or refusal.
-		case CL_ABORTED:
-			cl_mode = CL_SEARCHING;
-			return false;
-	}
-
-	GetPackets();
-	Net_AckTicker();
-
-	// Call it only once by tic
-	if (*oldtic != I_GetTime())
-	{
-		I_OsPolling();
-
-		if (cl_mode == CL_CONFIRMCONNECT)
-			D_ProcessEvents(); //needed for menu system to receive inputs
-		else
-		{
-			// my hand has been forced and I am dearly sorry for this awful hack :vomit:
-			for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
-			{
-#ifndef NONET
-				if (!Snake_Joy_Grabber(&events[eventtail]))
-#endif
-					G_MapEventsToControls(&events[eventtail]);
-			}
-		}
-
-		if (gamekeydown[KEY_ESCAPE] || gamekeydown[KEY_JOY1+1] || cl_mode == CL_ABORTED)
-		{
-			CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
-			M_StartMessage(M_GetText("Network game synchronization aborted.\n\nPress ESC\n"), NULL, MM_NOTHING);
-
-#ifndef NONET
-			if (snake)
-			{
-				free(snake);
-				snake = NULL;
-			}
-#endif
-
-			D_QuitNetGame();
-			CL_Reset();
-			D_StartTitle();
-			memset(gamekeydown, 0, NUMKEYS);
-			return false;
-		}
-#ifndef NONET
-		else if (cl_mode == CL_DOWNLOADFILES && snake)
-			Snake_Handle();
-#endif
-
-		if (client && (cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADSAVEGAME))
-			FileReceiveTicker();
-
-		// why are these here? this is for servers, we're a client
-		//if (key == 's' && server)
-		//	doomcom->numnodes = (INT16)pnumnodes;
-		//FileSendTicker();
-		*oldtic = I_GetTime();
-
-#ifndef NONET
-		if (client && cl_mode != CL_CONNECTED && cl_mode != CL_ABORTED)
-		{
-			if (!snake)
-			{
-				F_MenuPresTicker(); // title sky
-				F_TitleScreenTicker(true);
-				F_TitleScreenDrawer();
-			}
-			CL_DrawConnectionStatus();
-#ifdef HAVE_THREADS
-			I_lock_mutex(&m_menu_mutex);
-#endif
-			M_Drawer(); //Needed for drawing messageboxes on the connection screen
-#ifdef HAVE_THREADS
-			I_unlock_mutex(m_menu_mutex);
-#endif
-			I_UpdateNoVsync(); // page flip or blit buffer
-			if (moviemode)
-				M_SaveFrame();
-			S_UpdateSounds();
-			S_UpdateClosedCaptions();
-		}
-#else
-		CON_Drawer();
-		I_UpdateNoVsync();
-#endif
-	}
-	else
-	{
-		I_Sleep(cv_sleep.value);
-		I_UpdateTime(cv_timescale.value);
-	}
-
-	return true;
-}
-
-/** Use adaptive send using net_bandwidth and stat.sendbytes
-  *
-  * \todo Better description...
-  *
-  */
-static void CL_ConnectToServer(void)
-{
-	INT32 pnumnodes, nodewaited = doomcom->numnodes, i;
-	tic_t oldtic;
-#ifndef NONET
-	tic_t asksent;
-	char tmpsave[256];
-
-	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
-
-	lastfilenum = -1;
-#endif
-
-	cl_mode = CL_SEARCHING;
-
-#ifndef NONET
-	// Don't get a corrupt savegame error because tmpsave already exists
-	if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1)
-		I_Error("Can't delete %s\n", tmpsave);
-#endif
-
-	if (netgame)
-	{
-		if (servernode < 0 || servernode >= MAXNETNODES)
-			CONS_Printf(M_GetText("Searching for a server...\n"));
-		else
-			CONS_Printf(M_GetText("Contacting the server...\n"));
-	}
-
-	if (gamestate == GS_INTERMISSION)
-		Y_EndIntermission(); // clean up intermission graphics etc
-
-	DEBFILE(va("waiting %d nodes\n", doomcom->numnodes));
-	G_SetGamestate(GS_WAITINGPLAYERS);
-	wipegamestate = GS_WAITINGPLAYERS;
-
-	ClearAdminPlayers();
-	pnumnodes = 1;
-	oldtic = I_GetTime() - 1;
-
-#ifndef NONET
-	asksent = (tic_t) - TICRATE;
-	firstconnectattempttime = I_GetTime();
-
-	i = SL_SearchServer(servernode);
-
-	if (i != -1)
-	{
-		char *gametypestr = serverlist[i].info.gametypename;
-		CONS_Printf(M_GetText("Connecting to: %s\n"), serverlist[i].info.servername);
-		gametypestr[sizeof serverlist[i].info.gametypename - 1] = '\0';
-		CONS_Printf(M_GetText("Gametype: %s\n"), gametypestr);
-		CONS_Printf(M_GetText("Version: %d.%d.%u\n"), serverlist[i].info.version/100,
-		 serverlist[i].info.version%100, serverlist[i].info.subversion);
-	}
-	SL_ClearServerList(servernode);
-#endif
-
-	do
-	{
-		// If the connection was aborted for some reason, leave
-#ifndef NONET
-		if (!CL_ServerConnectionTicker(tmpsave, &oldtic, &asksent))
-#else
-		if (!CL_ServerConnectionTicker((char*)NULL, &oldtic, (tic_t *)NULL))
-#endif
-			return;
-
-		if (server)
-		{
-			pnumnodes = 0;
-			for (i = 0; i < MAXNETNODES; i++)
-				if (nodeingame[i])
-					pnumnodes++;
-		}
-	}
-	while (!(cl_mode == CL_CONNECTED && (client || (server && nodewaited <= pnumnodes))));
-
-	if (netgame)
-		F_StartWaitingPlayers();
-	DEBFILE(va("Synchronisation Finished\n"));
-
-	displayplayer = consoleplayer;
-}
-
-#ifndef NONET
-typedef struct banreason_s
-{
-	char *reason;
-	struct banreason_s *prev; //-1
-	struct banreason_s *next; //+1
-} banreason_t;
-
-static banreason_t *reasontail = NULL; //last entry, use prev
-static banreason_t *reasonhead = NULL; //1st entry, use next
-
-static void Command_ShowBan(void) //Print out ban list
-{
-	size_t i;
-	const char *address, *mask;
-	banreason_t *reasonlist = reasonhead;
-
-	if (I_GetBanAddress)
-		CONS_Printf(M_GetText("Ban List:\n"));
-	else
-		return;
-
-	for (i = 0;(address = I_GetBanAddress(i)) != NULL;i++)
-	{
-		if (!I_GetBanMask || (mask = I_GetBanMask(i)) == NULL)
-			CONS_Printf("%s: %s ", sizeu1(i+1), address);
-		else
-			CONS_Printf("%s: %s/%s ", sizeu1(i+1), address, mask);
-
-		if (reasonlist && reasonlist->reason)
-			CONS_Printf("(%s)\n", reasonlist->reason);
-		else
-			CONS_Printf("\n");
-
-		if (reasonlist) reasonlist = reasonlist->next;
-	}
-
-	if (i == 0 && !address)
-		CONS_Printf(M_GetText("(empty)\n"));
-}
-
-void D_SaveBan(void)
-{
-	FILE *f;
-	size_t i;
-	banreason_t *reasonlist = reasonhead;
-	const char *address, *mask;
-	const char *path = va("%s"PATHSEP"%s", srb2home, "ban.txt");
-
-	if (!reasonhead)
-	{
-		remove(path);
-		return;
-	}
-
-	f = fopen(path, "w");
-
-	if (!f)
-	{
-		CONS_Alert(CONS_WARNING, M_GetText("Could not save ban list into ban.txt\n"));
-		return;
-	}
-
-	for (i = 0;(address = I_GetBanAddress(i)) != NULL;i++)
-	{
-		if (!I_GetBanMask || (mask = I_GetBanMask(i)) == NULL)
-			fprintf(f, "%s 0", address);
-		else
-			fprintf(f, "%s %s", address, mask);
-
-		if (reasonlist && reasonlist->reason)
-			fprintf(f, " %s\n", reasonlist->reason);
-		else
-			fprintf(f, " %s\n", "NA");
-
-		if (reasonlist) reasonlist = reasonlist->next;
-	}
-
-	fclose(f);
-}
-
-static void Ban_Add(const char *reason)
-{
-	banreason_t *reasonlist = malloc(sizeof(*reasonlist));
-
-	if (!reasonlist)
-		return;
-	if (!reason)
-		reason = "NA";
-
-	reasonlist->next = NULL;
-	reasonlist->reason = Z_StrDup(reason);
-	if ((reasonlist->prev = reasontail) == NULL)
-		reasonhead = reasonlist;
-	else
-		reasontail->next = reasonlist;
-	reasontail = reasonlist;
-}
-
-static void Ban_Clear(void)
-{
-	banreason_t *temp;
-
-	I_ClearBans();
-
-	reasontail = NULL;
-
-	while (reasonhead)
-	{
-		temp = reasonhead->next;
-		Z_Free(reasonhead->reason);
-		free(reasonhead);
-		reasonhead = temp;
-	}
-}
-
-static void Command_ClearBans(void)
-{
-	if (!I_ClearBans)
-		return;
-
-	Ban_Clear();
-	D_SaveBan();
-}
-
-static void Ban_Load_File(boolean warning)
-{
-	FILE *f;
-	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();
-
-	// Ensure synched when creating a new server
-	M_CopyGameData(serverGamedata, clientGamedata);
-
-	DEBFILE("\n-=-=-=-=-=-=-= Server Reset =-=-=-=-=-=-=-\n\n");
-}
-
-static inline void SV_GenContext(void)
-{
-	UINT8 i;
-	// generate server_context, as exactly 8 bytes of randomly mixed A-Z and a-z
-	// (hopefully M_Random is initialized!! if not this will be awfully silly!)
-	for (i = 0; i < 8; i++)
-	{
-		const char a = M_RandomKey(26*2);
-		if (a < 26) // uppercase
-			server_context[i] = 'A'+a;
-		else // lowercase
-			server_context[i] = 'a'+(a-26);
-	}
-}
-
-//
-// D_QuitNetGame
-// Called before quitting to leave a net game
-// without hanging the other players
-//
-void D_QuitNetGame(void)
-{
-	mousegrabbedbylua = true;
-	I_UpdateMouseGrab();
-
-	if (!netgame || !netbuffer)
-		return;
-
-	DEBFILE("===========================================================================\n"
-	        "                  Quitting Game, closing connection\n"
-	        "===========================================================================\n");
-
-	// abort send/receive of files
-	CloseNetFile();
-	RemoveAllLuaFileTransfers();
-	waitingforluafiletransfer = false;
-	waitingforluafilecommand = false;
-
-	if (server)
-	{
-		INT32 i;
-
-		netbuffer->packettype = PT_SERVERSHUTDOWN;
-		for (i = 0; i < MAXNETNODES; i++)
-			if (nodeingame[i])
-				HSendPacket(i, true, 0, 0);
-#ifdef MASTERSERVER
-		if (serverrunning && ms_RoomId > 0)
-			UnregisterServer();
-#endif
-	}
-	else if (servernode > 0 && servernode < MAXNETNODES && nodeingame[(UINT8)servernode])
-	{
-		netbuffer->packettype = PT_CLIENTQUIT;
-		HSendPacket(servernode, true, 0, 0);
-	}
-
-	D_CloseConnection();
-	ClearAdminPlayers();
-
-	DEBFILE("===========================================================================\n"
-	        "                         Log finish\n"
-	        "===========================================================================\n");
-#ifdef DEBUGFILE
-	if (debugfile)
-	{
-		fclose(debugfile);
-		debugfile = NULL;
-	}
-#endif
-}
-
-// Adds a node to the game (player will follow at map change or at savegame....)
-static inline void SV_AddNode(INT32 node)
-{
-	nettics[node] = gametic;
-	supposedtics[node] = gametic;
-	// little hack because the server connects to itself and puts
-	// nodeingame when connected not here
-	if (node)
-		nodeingame[node] = true;
-}
-
-// Xcmd XD_ADDPLAYER
-static void Got_AddPlayer(UINT8 **p, INT32 playernum)
-{
-	INT16 node, newplayernum;
-	boolean splitscreenplayer;
-	boolean rejoined;
-	player_t *newplayer;
-
-	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
-	{
-		// protect against hacked/buggy client
-		CONS_Alert(CONS_WARNING, M_GetText("Illegal add player command received from %s\n"), player_names[playernum]);
-		if (server)
-			SendKick(playernum, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
-		return;
-	}
-
-	node = READUINT8(*p);
-	newplayernum = READUINT8(*p);
-	splitscreenplayer = newplayernum & 0x80;
-	newplayernum &= ~0x80;
-
-	rejoined = playeringame[newplayernum];
-
-	if (!rejoined)
-	{
-		// Clear player before joining, lest some things get set incorrectly
-		// HACK: don't do this for splitscreen, it relies on preset values
-		if (!splitscreen && !botingame)
-			CL_ClearPlayer(newplayernum);
-		playeringame[newplayernum] = true;
-		G_AddPlayer(newplayernum);
-		if (newplayernum+1 > doomcom->numslots)
-			doomcom->numslots = (INT16)(newplayernum+1);
-
-		if (server && I_GetNodeAddress)
-		{
-			char addressbuffer[64];
-			const char *address = I_GetNodeAddress(node);
-			if (address) // MI: fix msvcrt.dll!_mbscat crash?
-			{
-				strcpy(addressbuffer, address);
-				strcpy(playeraddress[newplayernum],
-						I_NetSplitAddress(addressbuffer, NULL));
-			}
-		}
-	}
-
-	newplayer = &players[newplayernum];
-
-	newplayer->jointime = 0;
-	newplayer->quittime = 0;
-
-	READSTRINGN(*p, player_names[newplayernum], MAXPLAYERNAME);
-
-	// the server is creating my player
-	if (node == mynode)
-	{
-		playernode[newplayernum] = 0; // for information only
-		if (!splitscreenplayer)
-		{
-			consoleplayer = newplayernum;
-			displayplayer = newplayernum;
-			secondarydisplayplayer = newplayernum;
-			DEBFILE("spawning me\n");
-			ticcmd_oldangleturn[0] = newplayer->oldrelangleturn;
-		}
-		else
-		{
-			secondarydisplayplayer = newplayernum;
-			DEBFILE("spawning my brother\n");
-			if (botingame)
-				newplayer->bot = 1;
-			ticcmd_oldangleturn[1] = newplayer->oldrelangleturn;
-		}
-		P_ForceLocalAngle(newplayer, (angle_t)(newplayer->angleturn << 16));
-		D_SendPlayerConfig();
-		addedtogame = true;
-
-		if (rejoined)
-		{
-			if (newplayer->mo)
-			{
-				newplayer->viewheight = 41*newplayer->height/48;
-
-				if (newplayer->mo->eflags & MFE_VERTICALFLIP)
-					newplayer->viewz = newplayer->mo->z + newplayer->mo->height - newplayer->viewheight;
-				else
-					newplayer->viewz = newplayer->mo->z + newplayer->viewheight;
-			}
-
-			// wake up the status bar
-			ST_Start();
-			// wake up the heads up text
-			HU_Start();
-
-			if (camera.chase && !splitscreenplayer)
-				P_ResetCamera(newplayer, &camera);
-			if (camera2.chase && splitscreenplayer)
-				P_ResetCamera(newplayer, &camera2);
-		}
-	}
-
-	if (netgame)
-	{
-		char joinmsg[256];
-
-		if (rejoined)
-			strcpy(joinmsg, M_GetText("\x82*%s has rejoined the game (player %d)"));
-		else
-			strcpy(joinmsg, M_GetText("\x82*%s has joined the game (player %d)"));
-		strcpy(joinmsg, va(joinmsg, player_names[newplayernum], newplayernum));
-
-		// Merge join notification + IP to avoid clogging console/chat
-		if (server && cv_showjoinaddress.value && I_GetNodeAddress)
-		{
-			const char *address = I_GetNodeAddress(node);
-			if (address)
-				strcat(joinmsg, va(" (%s)", address));
-		}
-
-		HU_AddChatText(joinmsg, false);
-	}
-
-	if (server && multiplayer && motd[0] != '\0')
-		COM_BufAddText(va("sayto %d %s\n", newplayernum, motd));
-
-	if (!rejoined)
-		LUA_HookInt(newplayernum, HOOK(PlayerJoin));
-}
-
-static boolean SV_AddWaitingPlayers(const char *name, const char *name2)
-{
-	INT32 node, n, newplayer = false;
-	UINT8 buf[2 + MAXPLAYERNAME];
-	UINT8 *p;
-	INT32 newplayernum;
-
-	for (node = 0; node < MAXNETNODES; node++)
-	{
-		// splitscreen can allow 2 player in one node
-		for (; nodewaiting[node] > 0; nodewaiting[node]--)
-		{
-			newplayer = true;
-
-			newplayernum = FindRejoinerNum(node);
-			if (newplayernum == -1)
-			{
-				// search for a free playernum
-				// we can't use playeringame since it is not updated here
-				for (newplayernum = dedicated ? 1 : 0; newplayernum < MAXPLAYERS; newplayernum++)
-				{
-					if (playeringame[newplayernum])
-						continue;
-					for (n = 0; n < MAXNETNODES; n++)
-						if (nodetoplayer[n] == newplayernum || nodetoplayer2[n] == newplayernum)
-							break;
-					if (n == MAXNETNODES)
-						break;
-				}
-			}
-
-			// should never happen since we check the playernum
-			// before accepting the join
-			I_Assert(newplayernum < MAXPLAYERS);
-
-			playernode[newplayernum] = (UINT8)node;
-
-			p = buf + 2;
-			buf[0] = (UINT8)node;
-			buf[1] = newplayernum;
-			if (playerpernode[node] < 1)
-			{
-				nodetoplayer[node] = newplayernum;
-				WRITESTRINGN(p, name, MAXPLAYERNAME);
-			}
-			else
-			{
-				nodetoplayer2[node] = newplayernum;
-				buf[1] |= 0x80;
-				WRITESTRINGN(p, name2, MAXPLAYERNAME);
-			}
-			playerpernode[node]++;
-
-			SendNetXCmd(XD_ADDPLAYER, &buf, p - buf);
-
-			DEBFILE(va("Server added player %d node %d\n", newplayernum, node));
-		}
-	}
-
-	return newplayer;
-}
-
-void CL_AddSplitscreenPlayer(void)
-{
-	if (cl_mode == CL_CONNECTED)
-		CL_SendJoin();
-}
-
-void CL_RemoveSplitscreenPlayer(void)
-{
-	if (cl_mode != CL_CONNECTED)
-		return;
-
-	SendKick(secondarydisplayplayer, KICK_MSG_PLAYER_QUIT);
-}
-
-// is there a game running
-boolean Playing(void)
-{
-	return (server && serverrunning) || (client && cl_mode == CL_CONNECTED);
-}
-
-boolean SV_SpawnServer(void)
-{
-	if (demoplayback)
-		G_StopDemo(); // reset engine parameter
-	if (metalplayback)
-		G_StopMetalDemo();
-
-	if (!serverrunning)
-	{
-		CONS_Printf(M_GetText("Starting Server....\n"));
-		serverrunning = true;
-		SV_ResetServer();
-		SV_GenContext();
-		if (netgame && I_NetOpenSocket)
-		{
-			I_NetOpenSocket();
-#ifdef MASTERSERVER
-			if (ms_RoomId > 0)
-				RegisterServer();
-#endif
-		}
-
-		// non dedicated server just connect to itself
-		if (!dedicated)
-			CL_ConnectToServer();
-		else doomcom->numslots = 1;
-	}
-
-	return SV_AddWaitingPlayers(cv_playername.zstring, cv_playername2.zstring);
-}
-
-void SV_StopServer(void)
-{
-	tic_t i;
-
-	if (gamestate == GS_INTERMISSION)
-		Y_EndIntermission();
-	gamestate = wipegamestate = GS_NULL;
-
-	localtextcmd[0] = 0;
-	localtextcmd2[0] = 0;
-
-	for (i = firstticstosend; i < firstticstosend + BACKUPTICS; i++)
-		D_Clearticcmd(i);
-
-	consoleplayer = 0;
-	cl_mode = CL_SEARCHING;
-	maketic = gametic+1;
-	neededtic = maketic;
-	serverrunning = false;
-}
-
-// called at singleplayer start and stopdemo
-void SV_StartSinglePlayerServer(void)
-{
-	server = true;
-	netgame = false;
-	multiplayer = false;
-	G_SetGametype(GT_COOP);
-
-	// no more tic the game with this settings!
-	SV_StopServer();
-
-	if (splitscreen)
-		multiplayer = true;
-}
-
-static void SV_SendRefuse(INT32 node, const char *reason)
-{
-	strcpy(netbuffer->u.serverrefuse.reason, reason);
-
-	netbuffer->packettype = PT_SERVERREFUSE;
-	HSendPacket(node, true, 0, strlen(netbuffer->u.serverrefuse.reason) + 1);
-	Net_CloseConnection(node);
-}
-
-// used at txtcmds received to check packetsize bound
-static size_t TotalTextCmdPerTic(tic_t tic)
-{
-	INT32 i;
-	size_t total = 1; // num of textcmds in the tic (ntextcmd byte)
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		UINT8 *textcmd = D_GetExistingTextcmd(tic, i);
-		if ((!i || playeringame[i]) && textcmd)
-			total += 2 + textcmd[0]; // "+2" for size and playernum
-	}
-
-	return total;
-}
-
-static const char *
-ConnectionRefused (SINT8 node, INT32 rejoinernum)
-{
-	clientconfig_pak *cc = &netbuffer->u.clientcfg;
-
-	boolean rejoining = (rejoinernum != -1);
-
-	if (!node)/* server connecting to itself */
-		return NULL;
-
-	if (
-			cc->modversion != MODVERSION ||
-			strncmp(cc->application, SRB2APPLICATION,
-				sizeof cc->application)
-	){
-		return/* this is probably client's fault */
-			"Incompatible.";
-	}
-	else if (bannednode && bannednode[node])
-	{
-		return
-			"You have been banned\n"
-			"from the server.";
-	}
-	else if (cc->localplayers != 1)
-	{
-		return
-			"Wrong player count.";
-	}
-
-	if (!rejoining)
-	{
-		if (!cv_allownewplayer.value)
-		{
-			return
-				"The server is not accepting\n"
-				"joins for the moment.";
-		}
-		else if (D_NumPlayers() >= cv_maxplayers.value)
-		{
-			return va(
-					"Maximum players reached: %d",
-					cv_maxplayers.value - D_NumBots());
-		}
-	}
-
-	if (luafiletransfers)
-	{
-		return
-			"The serveris broadcasting a file\n"
-			"requested by a Lua script.\n"
-			"Please wait a bit and then\n"
-			"try rejoining.";
-	}
-
-	if (netgame)
-	{
-		const tic_t th = 2 * cv_joindelay.value * TICRATE;
-
-		if (joindelay > th)
-		{
-			return va(
-					"Too many people are connecting.\n"
-					"Please wait %d seconds and then\n"
-					"try rejoining.",
-					(joindelay - th) / TICRATE);
-		}
-	}
-
-	return NULL;
-}
-
-/** Called when a PT_CLIENTJOIN packet is received
-  *
-  * \param node The packet sender
-  *
-  */
-static void HandleConnect(SINT8 node)
-{
-	char names[MAXSPLITSCREENPLAYERS][MAXPLAYERNAME + 1];
-	INT32 rejoinernum;
-	INT32 i;
-	const char *refuse;
-
-	rejoinernum = FindRejoinerNum(node);
-
-	refuse = ConnectionRefused(node, rejoinernum);
-
-	if (refuse)
-		SV_SendRefuse(node, refuse);
-	else
-	{
-#ifndef NONET
-		boolean newnode = false;
-#endif
-
-		for (i = 0; i < netbuffer->u.clientcfg.localplayers - playerpernode[node]; i++)
-		{
-			strlcpy(names[i], netbuffer->u.clientcfg.names[i], MAXPLAYERNAME + 1);
-			if (!EnsurePlayerNameIsGood(names[i], rejoinernum))
-			{
-				SV_SendRefuse(node, "Bad player name");
-				return;
-			}
-		}
-
-		// client authorised to join
-		nodewaiting[node] = (UINT8)(netbuffer->u.clientcfg.localplayers - playerpernode[node]);
-		if (!nodeingame[node])
-		{
-			gamestate_t backupstate = gamestate;
-#ifndef NONET
-			newnode = true;
-#endif
-			SV_AddNode(node);
-
-			if (cv_joinnextround.value && gameaction == ga_nothing)
-				G_SetGamestate(GS_WAITINGPLAYERS);
-			if (!SV_SendServerConfig(node))
-			{
-				G_SetGamestate(backupstate);
-				/// \note Shouldn't SV_SendRefuse be called before ResetNode?
-				ResetNode(node);
-				SV_SendRefuse(node, M_GetText("Server couldn't send info, please try again"));
-				/// \todo fix this !!!
-				return; // restart the while
-			}
-			//if (gamestate != GS_LEVEL) // GS_INTERMISSION, etc?
-			//	SV_SendPlayerConfigs(node); // send bare minimum player info
-			G_SetGamestate(backupstate);
-			DEBFILE("new node joined\n");
-		}
-#ifndef NONET
-		if (nodewaiting[node])
-		{
-			if ((gamestate == GS_LEVEL || gamestate == GS_INTERMISSION) && newnode)
-			{
-				SV_SendSaveGame(node, false); // send a complete game state
-				DEBFILE("send savegame\n");
-			}
-			SV_AddWaitingPlayers(names[0], names[1]);
-			joindelay += cv_joindelay.value * TICRATE;
-			player_joining = true;
-		}
-#endif
-	}
-}
-
-/** Called when a PT_SERVERSHUTDOWN packet is received
-  *
-  * \param node The packet sender (should be the server)
-  *
-  */
-static void HandleShutdown(SINT8 node)
-{
-	(void)node;
-	LUA_HookBool(false, HOOK(GameQuit));
-	D_QuitNetGame();
-	CL_Reset();
-	D_StartTitle();
-	M_StartMessage(M_GetText("Server has shutdown\n\nPress Esc\n"), NULL, MM_NOTHING);
-}
-
-/** Called when a PT_NODETIMEOUT packet is received
-  *
-  * \param node The packet sender (should be the server)
-  *
-  */
-static void HandleTimeout(SINT8 node)
-{
-	(void)node;
-	LUA_HookBool(false, HOOK(GameQuit));
-	D_QuitNetGame();
-	CL_Reset();
-	D_StartTitle();
-	M_StartMessage(M_GetText("Server Timeout\n\nPress Esc\n"), NULL, MM_NOTHING);
-}
-
-#ifndef NONET
-/** Called when a PT_SERVERINFO packet is received
-  *
-  * \param node The packet sender
-  * \note What happens if the packet comes from a client or something like that?
-  *
-  */
-static void HandleServerInfo(SINT8 node)
-{
-	// compute ping in ms
-	const tic_t ticnow = I_GetTime();
-	const tic_t ticthen = (tic_t)LONG(netbuffer->u.serverinfo.time);
-	const tic_t ticdiff = (ticnow - ticthen)*1000/NEWTICRATE;
-	netbuffer->u.serverinfo.time = (tic_t)LONG(ticdiff);
-	netbuffer->u.serverinfo.servername[MAXSERVERNAME-1] = 0;
-	netbuffer->u.serverinfo.application
-		[sizeof netbuffer->u.serverinfo.application - 1] = '\0';
-	netbuffer->u.serverinfo.gametypename
-		[sizeof netbuffer->u.serverinfo.gametypename - 1] = '\0';
-
-	SL_InsertServer(&netbuffer->u.serverinfo, node);
-}
-#endif
-
-static void PT_WillResendGamestate(void)
-{
-#ifndef NONET
-	char tmpsave[256];
-
-	if (server || cl_redownloadinggamestate)
-		return;
-
-	// Send back a PT_CANRECEIVEGAMESTATE packet to the server
-	// so they know they can start sending the game state
-	netbuffer->packettype = PT_CANRECEIVEGAMESTATE;
-	if (!HSendPacket(servernode, true, 0, 0))
-		return;
-
-	CONS_Printf(M_GetText("Reloading game state...\n"));
-
-	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
-
-	// Don't get a corrupt savegame error because tmpsave already exists
-	if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1)
-		I_Error("Can't delete %s\n", tmpsave);
-
-	CL_PrepareDownloadSaveGame(tmpsave);
-
-	cl_redownloadinggamestate = true;
-#endif
-}
-
-static void PT_CanReceiveGamestate(SINT8 node)
-{
-#ifndef NONET
-	if (client || sendingsavegame[node])
-		return;
-
-	CONS_Printf(M_GetText("Resending game state to %s...\n"), player_names[nodetoplayer[node]]);
-
-	SV_SendSaveGame(node, true); // Resend a complete game state
-	resendingsavegame[node] = true;
-#else
-	(void)node;
-#endif
-}
-
-/** Handles a packet received from a node that isn't in game
-  *
-  * \param node The packet sender
-  * \todo Choose a better name, as the packet can also come from the server apparently?
-  * \sa HandlePacketFromPlayer
-  * \sa GetPackets
-  *
-  */
-static void HandlePacketFromAwayNode(SINT8 node)
-{
-	if (node != servernode)
-		DEBFILE(va("Received packet from unknown host %d\n", node));
-
-// macro for packets that should only be sent by the server
-// if it is NOT from the server, bail out and close the connection!
-#define SERVERONLY \
-			if (node != servernode) \
-			{ \
-				Net_CloseConnection(node); \
-				break; \
-			}
-	switch (netbuffer->packettype)
-	{
-		case PT_ASKINFOVIAMS:
-			Net_CloseConnection(node);
-			break;
-
-		case PT_TELLFILESNEEDED:
-			if (server && serverrunning)
-			{
-				UINT8 *p;
-				INT32 firstfile = netbuffer->u.filesneedednum;
-
-				netbuffer->packettype = PT_MOREFILESNEEDED;
-				netbuffer->u.filesneededcfg.first = firstfile;
-				netbuffer->u.filesneededcfg.more = 0;
-
-				p = PutFileNeeded(firstfile);
-
-				HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
-			}
-			else // Shouldn't get this if you aren't the server...?
-				Net_CloseConnection(node);
-			break;
-
-		case PT_MOREFILESNEEDED:
-			if (server && serverrunning)
-			{ // But wait I thought I'm the server?
-				Net_CloseConnection(node);
-				break;
-			}
-			SERVERONLY
-			if (cl_mode == CL_ASKFULLFILELIST && netbuffer->u.filesneededcfg.first == fileneedednum)
-			{
-				D_ParseFileneeded(netbuffer->u.filesneededcfg.num, netbuffer->u.filesneededcfg.files, netbuffer->u.filesneededcfg.first);
-				if (!netbuffer->u.filesneededcfg.more)
-					cl_lastcheckedfilecount = UINT16_MAX; // Got the whole file list
-			}
-			break;
-
-		case PT_ASKINFO:
-			if (server && serverrunning)
-			{
-				SV_SendServerInfo(node, (tic_t)LONG(netbuffer->u.askinfo.time));
-				SV_SendPlayerInfo(node); // Send extra info
-			}
-			Net_CloseConnection(node);
-			break;
-
-		case PT_SERVERREFUSE: // Negative response of client join request
-			if (server && serverrunning)
-			{ // But wait I thought I'm the server?
-				Net_CloseConnection(node);
-				break;
-			}
-			SERVERONLY
-			if (cl_mode == CL_WAITJOINRESPONSE)
-			{
-				// Save the reason so it can be displayed after quitting the netgame
-				char *reason = strdup(netbuffer->u.serverrefuse.reason);
-				if (!reason)
-					I_Error("Out of memory!\n");
-
-				if (strstr(reason, "Maximum players reached"))
-				{
-					serverisfull = true;
-					//Special timeout for when refusing due to player cap. The client will wait 3 seconds between join requests when waiting for a slot, so we need this to be much longer
-					//We set it back to the value of cv_nettimeout.value in CL_Reset
-					connectiontimeout = NEWTICRATE*7;
-					cl_mode = CL_ASKJOIN;
-					free(reason);
-					break;
-				}
-
-				M_StartMessage(va(M_GetText("Server refuses connection\n\nReason:\n%s"),
-					reason), NULL, MM_NOTHING);
-
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
-
-				free(reason);
-
-				// Will be reset by caller. Signals refusal.
-				cl_mode = CL_ABORTED;
-			}
-			break;
-
-		case PT_SERVERCFG: // Positive response of client join request
-		{
-			if (server && serverrunning && node != servernode)
-			{ // but wait I thought I'm the server?
-				Net_CloseConnection(node);
-				break;
-			}
-			SERVERONLY
-			/// \note how would this happen? and is it doing the right thing if it does?
-			if (cl_mode != CL_WAITJOINRESPONSE)
-				break;
-
-			if (client)
-			{
-				maketic = gametic = neededtic = (tic_t)LONG(netbuffer->u.servercfg.gametic);
-				G_SetGametype(netbuffer->u.servercfg.gametype);
-				modifiedgame = netbuffer->u.servercfg.modifiedgame;
-				if (netbuffer->u.servercfg.usedCheats)
-					G_SetUsedCheats(true);
-				memcpy(server_context, netbuffer->u.servercfg.server_context, 8);
-			}
-
-			nodeingame[(UINT8)servernode] = true;
-			serverplayer = netbuffer->u.servercfg.serverplayer;
-			doomcom->numslots = SHORT(netbuffer->u.servercfg.totalslotnum);
-			mynode = netbuffer->u.servercfg.clientnode;
-			if (serverplayer >= 0)
-				playernode[(UINT8)serverplayer] = servernode;
-
-			if (netgame)
-#ifndef NONET
-				CONS_Printf(M_GetText("Join accepted, waiting for complete game state...\n"));
-#else
-				CONS_Printf(M_GetText("Join accepted, waiting for next level change...\n"));
-#endif
-			DEBFILE(va("Server accept join gametic=%u mynode=%d\n", gametic, mynode));
-
-#ifndef NONET
-			/// \note Wait. What if a Lua script uses some global custom variables synched with the NetVars hook?
-			///       Shouldn't them be downloaded even at intermission time?
-			///       Also, according to HandleConnect, the server will send the savegame even during intermission...
-			if (netbuffer->u.servercfg.gamestate == GS_LEVEL/* ||
-				netbuffer->u.servercfg.gamestate == GS_INTERMISSION*/)
-				cl_mode = CL_DOWNLOADSAVEGAME;
-			else
-#endif
-				cl_mode = CL_CONNECTED;
-			break;
-		}
-
-		// Handled in d_netfil.c
-		case PT_FILEFRAGMENT:
-			if (server)
-			{ // But wait I thought I'm the server?
-				Net_CloseConnection(node);
-				break;
-			}
-			SERVERONLY
-			PT_FileFragment();
-			break;
-
-		case PT_FILEACK:
-			if (server)
-				PT_FileAck();
-			break;
-
-		case PT_FILERECEIVED:
-			if (server)
-				PT_FileReceived();
-			break;
-
-		case PT_REQUESTFILE:
-			if (server)
-			{
-				if (!cv_downloading.value || !PT_RequestFile(node))
-					Net_CloseConnection(node); // close connection if one of the requested files could not be sent, or you disabled downloading anyway
-			}
-			else
-				Net_CloseConnection(node); // nope
-			break;
-
-		case PT_NODETIMEOUT:
-		case PT_CLIENTQUIT:
-			if (server)
-				Net_CloseConnection(node);
-			break;
-
-		case PT_CLIENTCMD:
-			break; // This is not an "unknown packet"
-
-		case PT_SERVERTICS:
-			// Do not remove my own server (we have just get a out of order packet)
-			if (node == servernode)
-				break;
-			/* FALLTHRU */
-
-		default:
-			DEBFILE(va("unknown packet received (%d) from unknown host\n",netbuffer->packettype));
-			Net_CloseConnection(node);
-			break; // Ignore it
-
-	}
-#undef SERVERONLY
-}
-
-/** Handles a packet received from a node that is in game
-  *
-  * \param node The packet sender
-  * \todo Choose a better name
-  * \sa HandlePacketFromAwayNode
-  * \sa GetPackets
-  *
-  */
-static void HandlePacketFromPlayer(SINT8 node)
-{
-	INT32 netconsole;
-	tic_t realend, realstart;
-	UINT8 *pak, *txtpak, numtxtpak;
-#ifndef NOMD5
-	UINT8 finalmd5[16];/* Well, it's the cool thing to do? */
-#endif
-
-	txtpak = NULL;
-
-	if (dedicated && node == 0)
-		netconsole = 0;
-	else
-		netconsole = nodetoplayer[node];
-
-#ifdef PARANOIA
-	if (netconsole >= MAXPLAYERS)
-		I_Error("bad table nodetoplayer: node %d player %d", doomcom->remotenode, netconsole);
-#endif
-
-	switch (netbuffer->packettype)
-	{
-// -------------------------------------------- SERVER RECEIVE ----------
-		case PT_CLIENTCMD:
-		case PT_CLIENT2CMD:
-		case PT_CLIENTMIS:
-		case PT_CLIENT2MIS:
-		case PT_NODEKEEPALIVE:
-		case PT_NODEKEEPALIVEMIS:
-			if (client)
-				break;
-
-			// To save bytes, only the low byte of tic numbers are sent
-			// Use ExpandTics to figure out what the rest of the bytes are
-			realstart = ExpandTics(netbuffer->u.clientpak.client_tic, node);
-			realend = ExpandTics(netbuffer->u.clientpak.resendfrom, node);
-
-			if (netbuffer->packettype == PT_CLIENTMIS || netbuffer->packettype == PT_CLIENT2MIS
-				|| netbuffer->packettype == PT_NODEKEEPALIVEMIS
-				|| supposedtics[node] < realend)
-			{
-				supposedtics[node] = realend;
-			}
-			// Discard out of order packet
-			if (nettics[node] > realend)
-			{
-				DEBFILE(va("out of order ticcmd discarded nettics = %u\n", nettics[node]));
-				break;
-			}
-
-			// Update the nettics
-			nettics[node] = realend;
-
-			// This should probably still timeout though, as the node should always have a player 1 number
-			if (netconsole == -1)
-				break;
-
-			// As long as clients send valid ticcmds, the server can keep running, so reset the timeout
-			/// \todo Use a separate cvar for that kind of timeout?
-			freezetimeout[node] = I_GetTime() + connectiontimeout;
-
-			// Don't do anything for packets of type NODEKEEPALIVE?
-			// Sryder 2018/07/01: Update the freezetimeout still!
-			if (netbuffer->packettype == PT_NODEKEEPALIVE
-				|| netbuffer->packettype == PT_NODEKEEPALIVEMIS)
-				break;
-
-			// If we've alredy received a ticcmd for this tic, just submit it for the next one.
-			tic_t faketic = maketic;
-			if ((!!(netcmds[maketic % BACKUPTICS][netconsole].angleturn & TICCMD_RECEIVED))
-				&& (maketic - firstticstosend < BACKUPTICS - 1))
-				faketic++;
-
-			// Copy ticcmd
-			G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][netconsole], &netbuffer->u.clientpak.cmd, 1);
-
-			// Check ticcmd for "speed hacks"
-			if (netcmds[faketic%BACKUPTICS][netconsole].forwardmove > MAXPLMOVE || netcmds[faketic%BACKUPTICS][netconsole].forwardmove < -MAXPLMOVE
-				|| netcmds[faketic%BACKUPTICS][netconsole].sidemove > MAXPLMOVE || netcmds[faketic%BACKUPTICS][netconsole].sidemove < -MAXPLMOVE)
-			{
-				CONS_Alert(CONS_WARNING, M_GetText("Illegal movement value received from node %d\n"), netconsole);
-				//D_Clearticcmd(k);
-
-				SendKick(netconsole, KICK_MSG_CON_FAIL);
-				break;
-			}
-
-			// Splitscreen cmd
-			if ((netbuffer->packettype == PT_CLIENT2CMD || netbuffer->packettype == PT_CLIENT2MIS)
-				&& nodetoplayer2[node] >= 0)
-				G_MoveTiccmd(&netcmds[faketic%BACKUPTICS][(UINT8)nodetoplayer2[node]],
-					&netbuffer->u.client2pak.cmd2, 1);
-
-
-			// Check player consistancy during the level
-			if (realstart <= gametic && realstart + BACKUPTICS - 1 > gametic && gamestate == GS_LEVEL
-				&& consistancy[realstart%BACKUPTICS] != SHORT(netbuffer->u.clientpak.consistancy)
-#ifndef NONET
-				&& !SV_ResendingSavegameToAnyone()
-#endif
-				&& !resendingsavegame[node] && savegameresendcooldown[node] <= I_GetTime())
-			{
-				if (cv_resynchattempts.value)
-				{
-					// Tell the client we are about to resend them the gamestate
-					netbuffer->packettype = PT_WILLRESENDGAMESTATE;
-					HSendPacket(node, true, 0, 0);
-
-					resendingsavegame[node] = true;
-
-					if (cv_blamecfail.value)
-						CONS_Printf(M_GetText("Synch failure for player %d (%s); expected %hd, got %hd\n"),
-							netconsole+1, player_names[netconsole],
-							consistancy[realstart%BACKUPTICS],
-							SHORT(netbuffer->u.clientpak.consistancy));
-					DEBFILE(va("Restoring player %d (synch failure) [%update] %d!=%d\n",
-						netconsole, realstart, consistancy[realstart%BACKUPTICS],
-						SHORT(netbuffer->u.clientpak.consistancy)));
-					break;
-				}
-				else
-				{
-					SendKick(netconsole, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
-					DEBFILE(va("player %d kicked (synch failure) [%u] %d!=%d\n",
-						netconsole, realstart, consistancy[realstart%BACKUPTICS],
-						SHORT(netbuffer->u.clientpak.consistancy)));
-					break;
-				}
-			}
-			break;
-		case PT_BASICKEEPALIVE:
-			if (client)
-				break;
-
-			// This should probably still timeout though, as the node should always have a player 1 number
-			if (netconsole == -1)
-				break;
-
-			// If a client sends this it should mean they are done receiving the savegame
-			sendingsavegame[node] = false;
-
-			// As long as clients send keep alives, the server can keep running, so reset the timeout
-			/// \todo Use a separate cvar for that kind of timeout?
-			freezetimeout[node] = I_GetTime() + connectiontimeout;
-			break;
-		case PT_TEXTCMD2: // splitscreen special
-			netconsole = nodetoplayer2[node];
-			/* FALLTHRU */
-		case PT_TEXTCMD:
-			if (client)
-				break;
-
-			if (netconsole < 0 || netconsole >= MAXPLAYERS)
-				Net_UnAcknowledgePacket(node);
-			else
-			{
-				size_t j;
-				tic_t tic = maketic;
-				UINT8 *textcmd;
-
-				// ignore if the textcmd has a reported size of zero
-				// this shouldn't be sent at all
-				if (!netbuffer->u.textcmd[0])
-				{
-					DEBFILE(va("GetPacket: Textcmd with size 0 detected! (node %u, player %d)\n",
-						node, netconsole));
-					Net_UnAcknowledgePacket(node);
-					break;
-				}
-
-				// ignore if the textcmd size var is actually larger than it should be
-				// BASEPACKETSIZE + 1 (for size) + textcmd[0] should == datalength
-				if (netbuffer->u.textcmd[0] > (size_t)doomcom->datalength-BASEPACKETSIZE-1)
-				{
-					DEBFILE(va("GetPacket: Bad Textcmd packet size! (expected %d, actual %s, node %u, player %d)\n",
-					netbuffer->u.textcmd[0], sizeu1((size_t)doomcom->datalength-BASEPACKETSIZE-1),
-						node, netconsole));
-					Net_UnAcknowledgePacket(node);
-					break;
-				}
-
-				// check if tic that we are making isn't too large else we cannot send it :(
-				// doomcom->numslots+1 "+1" since doomcom->numslots can change within this time and sent time
-				j = software_MAXPACKETLENGTH
-					- (netbuffer->u.textcmd[0]+2+BASESERVERTICSSIZE
-					+ (doomcom->numslots+1)*sizeof(ticcmd_t));
-
-				// search a tic that have enougth space in the ticcmd
-				while ((textcmd = D_GetExistingTextcmd(tic, netconsole)),
-					(TotalTextCmdPerTic(tic) > j || netbuffer->u.textcmd[0] + (textcmd ? textcmd[0] : 0) > MAXTEXTCMD)
-					&& tic < firstticstosend + BACKUPTICS)
-					tic++;
-
-				if (tic >= firstticstosend + BACKUPTICS)
-				{
-					DEBFILE(va("GetPacket: Textcmd too long (max %s, used %s, mak %d, "
-						"tosend %u, node %u, player %d)\n", sizeu1(j), sizeu2(TotalTextCmdPerTic(maketic)),
-						maketic, firstticstosend, node, netconsole));
-					Net_UnAcknowledgePacket(node);
-					break;
-				}
-
-				// Make sure we have a buffer
-				if (!textcmd) textcmd = D_GetTextcmd(tic, netconsole);
-
-				DEBFILE(va("textcmd put in tic %u at position %d (player %d) ftts %u mk %u\n",
-					tic, textcmd[0]+1, netconsole, firstticstosend, maketic));
-
-				M_Memcpy(&textcmd[textcmd[0]+1], netbuffer->u.textcmd+1, netbuffer->u.textcmd[0]);
-				textcmd[0] += (UINT8)netbuffer->u.textcmd[0];
-			}
-			break;
-		case PT_LOGIN:
-			if (client)
-				break;
-
-#ifndef NOMD5
-			if (doomcom->datalength < 16)/* ignore partial sends */
-				break;
-
-			if (!adminpasswordset)
-			{
-				CONS_Printf(M_GetText("Password from %s failed (no password set).\n"), player_names[netconsole]);
-				break;
-			}
-
-			// Do the final pass to compare with the sent md5
-			D_MD5PasswordPass(adminpassmd5, 16, va("PNUM%02d", netconsole), &finalmd5);
-
-			if (!memcmp(netbuffer->u.md5sum, finalmd5, 16))
-			{
-				CONS_Printf(M_GetText("%s passed authentication.\n"), player_names[netconsole]);
-				COM_BufInsertText(va("promote %d\n", netconsole)); // do this immediately
-			}
-			else
-				CONS_Printf(M_GetText("Password from %s failed.\n"), player_names[netconsole]);
-#endif
-			break;
-		case PT_NODETIMEOUT:
-		case PT_CLIENTQUIT:
-			if (client)
-				break;
-
-			// nodeingame will be put false in the execution of kick command
-			// this allow to send some packets to the quitting client to have their ack back
-			nodewaiting[node] = 0;
-			if (netconsole != -1 && playeringame[netconsole])
-			{
-				UINT8 kickmsg;
-
-				if (netbuffer->packettype == PT_NODETIMEOUT)
-					kickmsg = KICK_MSG_TIMEOUT;
-				else
-					kickmsg = KICK_MSG_PLAYER_QUIT;
-				kickmsg |= KICK_MSG_KEEP_BODY;
-
-				SendKick(netconsole, kickmsg);
-				nodetoplayer[node] = -1;
-
-				if (nodetoplayer2[node] != -1 && nodetoplayer2[node] >= 0
-					&& playeringame[(UINT8)nodetoplayer2[node]])
-				{
-					SendKick(nodetoplayer2[node], kickmsg);
-					nodetoplayer2[node] = -1;
-				}
-			}
-			Net_CloseConnection(node);
-			nodeingame[node] = false;
-			break;
-		case PT_CANRECEIVEGAMESTATE:
-			PT_CanReceiveGamestate(node);
-			break;
-		case PT_ASKLUAFILE:
-			if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_ASKED)
-				AddLuaFileToSendQueue(node, luafiletransfers->realfilename);
-			break;
-		case PT_HASLUAFILE:
-			if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_SENDING)
-				SV_HandleLuaFileSent(node);
-			break;
-		case PT_RECEIVEDGAMESTATE:
-			sendingsavegame[node] = false;
-			resendingsavegame[node] = false;
-			savegameresendcooldown[node] = I_GetTime() + 5 * TICRATE;
-			break;
-// -------------------------------------------- CLIENT RECEIVE ----------
-		case PT_SERVERTICS:
-			// Only accept PT_SERVERTICS from the server.
-			if (node != servernode)
-			{
-				CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_SERVERTICS", node);
-				if (server)
-					SendKick(netconsole, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
-				break;
-			}
-
-			realstart = netbuffer->u.serverpak.starttic;
-			realend = realstart + netbuffer->u.serverpak.numtics;
-
-			if (!txtpak)
-				txtpak = (UINT8 *)&netbuffer->u.serverpak.cmds[netbuffer->u.serverpak.numslots
-					* netbuffer->u.serverpak.numtics];
-
-			if (realend > gametic + CLIENTBACKUPTICS)
-				realend = gametic + CLIENTBACKUPTICS;
-			cl_packetmissed = realstart > neededtic;
-
-			if (realstart <= neededtic && realend > neededtic)
-			{
-				tic_t i, j;
-				pak = (UINT8 *)&netbuffer->u.serverpak.cmds;
-
-				for (i = realstart; i < realend; i++)
-				{
-					// clear first
-					D_Clearticcmd(i);
-
-					// copy the tics
-					pak = G_ScpyTiccmd(netcmds[i%BACKUPTICS], pak,
-						netbuffer->u.serverpak.numslots*sizeof (ticcmd_t));
-
-					// copy the textcmds
-					numtxtpak = *txtpak++;
-					for (j = 0; j < numtxtpak; j++)
-					{
-						INT32 k = *txtpak++; // playernum
-						const size_t txtsize = txtpak[0]+1;
-
-						if (i >= gametic) // Don't copy old net commands
-							M_Memcpy(D_GetTextcmd(i, k), txtpak, txtsize);
-						txtpak += txtsize;
-					}
-				}
-
-				neededtic = realend;
-			}
-			else
-			{
-				DEBFILE(va("frame not in bound: %u\n", neededtic));
-				/*if (realend < neededtic - 2 * TICRATE || neededtic + 2 * TICRATE < realstart)
-					I_Error("Received an out of order PT_SERVERTICS packet!\n"
-							"Got tics %d-%d, needed tic %d\n\n"
-							"Please report this crash on the Master Board,\n"
-							"IRC or Discord so it can be fixed.\n", (INT32)realstart, (INT32)realend, (INT32)neededtic);*/
-			}
-			break;
-		case PT_PING:
-			// Only accept PT_PING from the server.
-			if (node != servernode)
-			{
-				CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_PING", node);
-				if (server)
-					SendKick(netconsole, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
-				break;
-			}
-
-			//Update client ping table from the server.
-			if (client)
-			{
-				UINT8 i;
-				for (i = 0; i < MAXPLAYERS; i++)
-					if (playeringame[i])
-						playerpingtable[i] = (tic_t)netbuffer->u.pingtable[i];
-
-				servermaxping = (tic_t)netbuffer->u.pingtable[MAXPLAYERS];
-			}
-
-			break;
-		case PT_SERVERCFG:
-			break;
-		case PT_FILEFRAGMENT:
-			// Only accept PT_FILEFRAGMENT from the server.
-			if (node != servernode)
-			{
-				CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_FILEFRAGMENT", node);
-				if (server)
-					SendKick(netconsole, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
-				break;
-			}
-			if (client)
-				PT_FileFragment();
-			break;
-		case PT_FILEACK:
-			if (server)
-				PT_FileAck();
-			break;
-		case PT_FILERECEIVED:
-			if (server)
-				PT_FileReceived();
-			break;
-		case PT_WILLRESENDGAMESTATE:
-			PT_WillResendGamestate();
-			break;
-		case PT_SENDINGLUAFILE:
-			if (client)
-				CL_PrepareDownloadLuaFile();
-			break;
-		default:
-			DEBFILE(va("UNKNOWN PACKET TYPE RECEIVED %d from host %d\n",
-				netbuffer->packettype, node));
-	} // end switch
-}
-
-/**	Handles all received packets, if any
-  *
-  * \todo Add details to this description (lol)
-  *
-  */
-static void GetPackets(void)
-{
-	SINT8 node; // The packet sender
-
-	player_joining = false;
-
-	while (HGetPacket())
-	{
-		node = (SINT8)doomcom->remotenode;
-
-		if (netbuffer->packettype == PT_CLIENTJOIN && server)
-		{
-			HandleConnect(node);
-			continue;
-		}
-		if (node == servernode && client && cl_mode != CL_SEARCHING)
-		{
-			if (netbuffer->packettype == PT_SERVERSHUTDOWN)
-			{
-				HandleShutdown(node);
-				continue;
-			}
-			if (netbuffer->packettype == PT_NODETIMEOUT)
-			{
-				HandleTimeout(node);
-				continue;
-			}
-		}
-
-#ifndef NONET
-		if (netbuffer->packettype == PT_SERVERINFO)
-		{
-			HandleServerInfo(node);
-			continue;
-		}
-#endif
-
-		if (netbuffer->packettype == PT_PLAYERINFO)
-			continue; // We do nothing with PLAYERINFO, that's for the MS browser.
-
-		// Packet received from someone already playing
-		if (nodeingame[node])
-			HandlePacketFromPlayer(node);
-		// Packet received from someone not playing
-		else
-			HandlePacketFromAwayNode(node);
-	}
-}
-
-//
-// NetUpdate
-// Builds ticcmds for console player,
-// sends out a packet
-//
-// no more use random generator, because at very first tic isn't yet synchronized
-// Note: It is called consistAncy on purpose.
-//
-static INT16 Consistancy(void)
-{
-	INT32 i;
-	UINT32 ret = 0;
-#ifdef MOBJCONSISTANCY
-	thinker_t *th;
-	mobj_t *mo;
-#endif
-
-	DEBFILE(va("TIC %u ", gametic));
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		if (!playeringame[i])
-			ret ^= 0xCCCC;
-		else if (!players[i].mo);
-		else
-		{
-			ret += players[i].mo->x;
-			ret -= players[i].mo->y;
-			ret += players[i].powers[pw_shield];
-			ret *= i+1;
-		}
-	}
-	// I give up
-	// Coop desynching enemies is painful
-	if (!G_PlatformGametype())
-		ret += P_GetRandSeed();
-
-#ifdef MOBJCONSISTANCY
-	if (gamestate == GS_LEVEL)
-	{
-		for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
-		{
-			if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
-				continue;
-
-			mo = (mobj_t *)th;
-
-			if (mo->flags & (MF_SPECIAL | MF_SOLID | MF_PUSHABLE | MF_BOSS | MF_MISSILE | MF_SPRING | MF_MONITOR | MF_FIRE | MF_ENEMY | MF_PAIN | MF_STICKY))
-			{
-				ret -= mo->type;
-				ret += mo->x;
-				ret -= mo->y;
-				ret += mo->z;
-				ret -= mo->momx;
-				ret += mo->momy;
-				ret -= mo->momz;
-				ret += mo->angle;
-				ret -= mo->flags;
-				ret += mo->flags2;
-				ret -= mo->eflags;
-				if (mo->target)
-				{
-					ret += mo->target->type;
-					ret -= mo->target->x;
-					ret += mo->target->y;
-					ret -= mo->target->z;
-					ret += mo->target->momx;
-					ret -= mo->target->momy;
-					ret += mo->target->momz;
-					ret -= mo->target->angle;
-					ret += mo->target->flags;
-					ret -= mo->target->flags2;
-					ret += mo->target->eflags;
-					ret -= mo->target->state - states;
-					ret += mo->target->tics;
-					ret -= mo->target->sprite;
-					ret += mo->target->frame;
-				}
-				else
-					ret ^= 0x3333;
-				if (mo->tracer && mo->tracer->type != MT_OVERLAY)
-				{
-					ret += mo->tracer->type;
-					ret -= mo->tracer->x;
-					ret += mo->tracer->y;
-					ret -= mo->tracer->z;
-					ret += mo->tracer->momx;
-					ret -= mo->tracer->momy;
-					ret += mo->tracer->momz;
-					ret -= mo->tracer->angle;
-					ret += mo->tracer->flags;
-					ret -= mo->tracer->flags2;
-					ret += mo->tracer->eflags;
-					ret -= mo->tracer->state - states;
-					ret += mo->tracer->tics;
-					ret -= mo->tracer->sprite;
-					ret += mo->tracer->frame;
-				}
-				else
-					ret ^= 0xAAAA;
-				ret -= mo->state - states;
-				ret += mo->tics;
-				ret -= mo->sprite;
-				ret += mo->frame;
-			}
-		}
-	}
-#endif
-
-	DEBFILE(va("Consistancy = %u\n", (ret & 0xFFFF)));
-
-	return (INT16)(ret & 0xFFFF);
-}
-
-// confusing, but this DOESN'T send PT_NODEKEEPALIVE, it sends PT_BASICKEEPALIVE
-// used during wipes to tell the server that a node is still connected
-static void CL_SendClientKeepAlive(void)
-{
-	netbuffer->packettype = PT_BASICKEEPALIVE;
-
-	HSendPacket(servernode, false, 0, 0);
-}
-
-static void SV_SendServerKeepAlive(void)
-{
-	INT32 n;
-
-	for (n = 1; n < MAXNETNODES; n++)
-	{
-		if (nodeingame[n])
-		{
-			netbuffer->packettype = PT_BASICKEEPALIVE;
-			HSendPacket(n, false, 0, 0);
-		}
-	}
-}
-
-// send the client packet to the server
-static void CL_SendClientCmd(void)
-{
-	size_t packetsize = 0;
-	boolean mis = false;
-
-	netbuffer->packettype = PT_CLIENTCMD;
-
-	if (cl_packetmissed)
-	{
-		netbuffer->packettype = PT_CLIENTMIS;
-		mis = true;
-	}
-
-	netbuffer->u.clientpak.resendfrom = (UINT8)(neededtic & UINT8_MAX);
-	netbuffer->u.clientpak.client_tic = (UINT8)(gametic & UINT8_MAX);
-
-	if (gamestate == GS_WAITINGPLAYERS)
-	{
-		// Send PT_NODEKEEPALIVE packet
-		netbuffer->packettype = (mis ? PT_NODEKEEPALIVEMIS : PT_NODEKEEPALIVE);
-		packetsize = sizeof (clientcmd_pak) - sizeof (ticcmd_t) - sizeof (INT16);
-		HSendPacket(servernode, false, 0, packetsize);
-	}
-	else if (gamestate != GS_NULL && (addedtogame || dedicated))
-	{
-		packetsize = sizeof (clientcmd_pak);
-		G_MoveTiccmd(&netbuffer->u.clientpak.cmd, &localcmds, 1);
-		netbuffer->u.clientpak.consistancy = SHORT(consistancy[gametic%BACKUPTICS]);
-
-		// Send a special packet with 2 cmd for splitscreen
-		if (splitscreen || botingame)
-		{
-			netbuffer->packettype = (mis ? PT_CLIENT2MIS : PT_CLIENT2CMD);
-			packetsize = sizeof (client2cmd_pak);
-			G_MoveTiccmd(&netbuffer->u.client2pak.cmd2, &localcmds2, 1);
-		}
-
-		HSendPacket(servernode, false, 0, packetsize);
-	}
-
-	if (cl_mode == CL_CONNECTED || dedicated)
-	{
-		// Send extra data if needed
-		if (localtextcmd[0])
-		{
-			netbuffer->packettype = PT_TEXTCMD;
-			M_Memcpy(netbuffer->u.textcmd, localtextcmd, localtextcmd[0]+1);
-			// All extra data have been sent
-			if (HSendPacket(servernode, true, 0, localtextcmd[0]+1)) // Send can fail...
-				localtextcmd[0] = 0;
-		}
-
-		// Send extra data if needed for player 2 (splitscreen)
-		if (localtextcmd2[0])
-		{
-			netbuffer->packettype = PT_TEXTCMD2;
-			M_Memcpy(netbuffer->u.textcmd, localtextcmd2, localtextcmd2[0]+1);
-			// All extra data have been sent
-			if (HSendPacket(servernode, true, 0, localtextcmd2[0]+1)) // Send can fail...
-				localtextcmd2[0] = 0;
-		}
-	}
-}
-
-// send the server packet
-// send tic from firstticstosend to maketic-1
-static void SV_SendTics(void)
-{
-	tic_t realfirsttic, lasttictosend, i;
-	UINT32 n;
-	INT32 j;
-	size_t packsize;
-	UINT8 *bufpos;
-	UINT8 *ntextcmd;
-
-	// send to all client but not to me
-	// for each node create a packet with x tics and send it
-	// x is computed using supposedtics[n], max packet size and maketic
-	for (n = 1; n < MAXNETNODES; n++)
-		if (nodeingame[n])
-		{
-			// assert supposedtics[n]>=nettics[n]
-			realfirsttic = supposedtics[n];
-			lasttictosend = min(maketic, nettics[n] + CLIENTBACKUPTICS);
-
-			if (realfirsttic >= lasttictosend)
-			{
-				// well we have sent all tics we will so use extrabandwidth
-				// to resent packet that are supposed lost (this is necessary since lost
-				// packet detection work when we have received packet with firsttic > neededtic
-				// (getpacket servertics case)
-				DEBFILE(va("Nothing to send node %u mak=%u sup=%u net=%u \n",
-					n, maketic, supposedtics[n], nettics[n]));
-				realfirsttic = nettics[n];
-				if (realfirsttic >= lasttictosend || (I_GetTime() + n)&3)
-					// all tic are ok
-					continue;
-				DEBFILE(va("Sent %d anyway\n", realfirsttic));
-			}
-			if (realfirsttic < firstticstosend)
-				realfirsttic = firstticstosend;
-
-			// compute the length of the packet and cut it if too large
-			packsize = BASESERVERTICSSIZE;
-			for (i = realfirsttic; i < lasttictosend; i++)
-			{
-				packsize += sizeof (ticcmd_t) * doomcom->numslots;
-				packsize += TotalTextCmdPerTic(i);
-
-				if (packsize > software_MAXPACKETLENGTH)
-				{
-					DEBFILE(va("packet too large (%s) at tic %d (should be from %d to %d)\n",
-						sizeu1(packsize), i, realfirsttic, lasttictosend));
-					lasttictosend = i;
-
-					// too bad: too much player have send extradata and there is too
-					//          much data in one tic.
-					// To avoid it put the data on the next tic. (see getpacket
-					// textcmd case) but when numplayer changes the computation can be different
-					if (lasttictosend == realfirsttic)
-					{
-						if (packsize > MAXPACKETLENGTH)
-							I_Error("Too many players: can't send %s data for %d players to node %d\n"
-							        "Well sorry nobody is perfect....\n",
-							        sizeu1(packsize), doomcom->numslots, n);
-						else
-						{
-							lasttictosend++; // send it anyway!
-							DEBFILE("sending it anyway\n");
-						}
-					}
-					break;
-				}
-			}
-
-			// Send the tics
-			netbuffer->packettype = PT_SERVERTICS;
-			netbuffer->u.serverpak.starttic = realfirsttic;
-			netbuffer->u.serverpak.numtics = (UINT8)(lasttictosend - realfirsttic);
-			netbuffer->u.serverpak.numslots = (UINT8)SHORT(doomcom->numslots);
-			bufpos = (UINT8 *)&netbuffer->u.serverpak.cmds;
-
-			for (i = realfirsttic; i < lasttictosend; i++)
-			{
-				bufpos = G_DcpyTiccmd(bufpos, netcmds[i%BACKUPTICS], doomcom->numslots * sizeof (ticcmd_t));
-			}
-
-			// add textcmds
-			for (i = realfirsttic; i < lasttictosend; i++)
-			{
-				ntextcmd = bufpos++;
-				*ntextcmd = 0;
-				for (j = 0; j < MAXPLAYERS; j++)
-				{
-					UINT8 *textcmd = D_GetExistingTextcmd(i, j);
-					INT32 size = textcmd ? textcmd[0] : 0;
-
-					if ((!j || playeringame[j]) && size)
-					{
-						(*ntextcmd)++;
-						WRITEUINT8(bufpos, j);
-						M_Memcpy(bufpos, textcmd, size + 1);
-						bufpos += size + 1;
-					}
-				}
-			}
-			packsize = bufpos - (UINT8 *)&(netbuffer->u);
-
-			HSendPacket(n, false, 0, packsize);
-			// when tic are too large, only one tic is sent so don't go backward!
-			if (lasttictosend-doomcom->extratics > realfirsttic)
-				supposedtics[n] = lasttictosend-doomcom->extratics;
-			else
-				supposedtics[n] = lasttictosend;
-			if (supposedtics[n] < nettics[n]) supposedtics[n] = nettics[n];
-		}
-	// node 0 is me!
-	supposedtics[0] = maketic;
-}
-
-//
-// TryRunTics
-//
-static void Local_Maketic(INT32 realtics)
-{
-	I_OsPolling(); // I_Getevent
-	D_ProcessEvents(); // menu responder, cons responder,
-	                   // game responder calls HU_Responder, AM_Responder,
-	                   // and G_MapEventsToControls
-	if (!dedicated) rendergametic = gametic;
-	// translate inputs (keyboard/mouse/joystick) into game controls
-	G_BuildTiccmd(&localcmds, realtics, 1);
-	if (splitscreen || botingame)
-		G_BuildTiccmd(&localcmds2, realtics, 2);
-
-	localcmds.angleturn |= TICCMD_RECEIVED;
-	localcmds2.angleturn |= TICCMD_RECEIVED;
-}
-
-// create missed tic
-static void SV_Maketic(void)
-{
-	INT32 i;
-
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		if (!playeringame[i])
-			continue;
-
-		// We didn't receive this tic
-		if ((netcmds[maketic % BACKUPTICS][i].angleturn & TICCMD_RECEIVED) == 0)
-		{
-			ticcmd_t *    ticcmd = &netcmds[(maketic    ) % BACKUPTICS][i];
-			ticcmd_t *prevticcmd = &netcmds[(maketic - 1) % BACKUPTICS][i];
-
-			if (players[i].quittime)
-			{
-				// Copy the angle/aiming from the previous tic
-				// and empty the other inputs
-				memset(ticcmd, 0, sizeof(netcmds[0][0]));
-				ticcmd->angleturn = prevticcmd->angleturn | TICCMD_RECEIVED;
-				ticcmd->aiming = prevticcmd->aiming;
-			}
-			else
-			{
-				DEBFILE(va("MISS tic%4d for player %d\n", maketic, i));
-				// Copy the input from the previous tic
-				*ticcmd = *prevticcmd;
-				ticcmd->angleturn &= ~TICCMD_RECEIVED;
-			}
-		}
-	}
-
-	// all tic are now proceed make the next
-	maketic++;
-}
-
-boolean TryRunTics(tic_t realtics)
-{
-	boolean ticking;
-
-	// the machine has lagged but it is not so bad
-	if (realtics > TICRATE/7) // FIXME: consistency failure!!
-	{
-		if (server)
-			realtics = 1;
-		else
-			realtics = TICRATE/7;
-	}
-
-	if (singletics)
-		realtics = 1;
-
-	if (realtics >= 1)
-	{
-		COM_BufTicker();
-		if (mapchangepending)
-			D_MapChange(-1, 0, ultimatemode, false, 2, false, fromlevelselect); // finish the map change
-	}
-
-	NetUpdate();
-
-	if (demoplayback)
-	{
-		neededtic = gametic + realtics;
-		// start a game after a demo
-		maketic += realtics;
-		firstticstosend = maketic;
-		tictoclear = firstticstosend;
-	}
-
-	GetPackets();
-
-#ifdef DEBUGFILE
-	if (debugfile && (realtics || neededtic > gametic))
-	{
-		//SoM: 3/30/2000: Need long INT32 in the format string for args 4 & 5.
-		//Shut up stupid warning!
-		fprintf(debugfile, "------------ Tryruntic: REAL:%d NEED:%d GAME:%d LOAD: %d\n",
-			realtics, neededtic, gametic, debugload);
-		debugload = 100000;
-	}
-#endif
-
-	ticking = neededtic > gametic;
-
-	if (ticking)
-	{
-		if (realtics)
-			hu_stopped = false;
-	}
-
-	if (player_joining)
-	{
-		if (realtics)
-			hu_stopped = true;
-		return false;
-	}
-
-	if (ticking)
-	{
-		if (advancedemo)
-		{
-			if (timedemo_quit)
-				COM_ImmedExecute("quit");
-			else
-				D_StartTitle();
-		}
-		else
-			// run the count * tics
-			while (neededtic > gametic)
-			{
-				boolean update_stats = !(paused || P_AutoPause());
-
-				DEBFILE(va("============ Running tic %d (local %d)\n", gametic, localgametic));
-
-				if (update_stats)
-					PS_START_TIMING(ps_tictime);
-
-				G_Ticker((gametic % NEWTICRATERATIO) == 0);
-				ExtraDataTicker();
-				gametic++;
-				consistancy[gametic%BACKUPTICS] = Consistancy();
-
-				if (update_stats)
-				{
-					PS_STOP_TIMING(ps_tictime);
-					PS_UpdateTickStats();
-				}
-
-				// Leave a certain amount of tics present in the net buffer as long as we've ran at least one tic this frame.
-				if (client && gamestate == GS_LEVEL && leveltime > 3 && neededtic <= gametic + cv_netticbuffer.value)
-					break;
-			}
-	}
-	else
-	{
-		if (realtics)
-			hu_stopped = true;
-	}
-
-	return ticking;
-}
-
-/*
-Ping Update except better:
-We call this once per second and check for people's pings. If their ping happens to be too high, we increment some timer and kick them out.
-If they're not lagging, decrement the timer by 1. Of course, reset all of this if they leave.
-*/
-
-static INT32 pingtimeout[MAXPLAYERS];
-
-static inline void PingUpdate(void)
-{
-	INT32 i;
-	boolean laggers[MAXPLAYERS];
-	UINT8 numlaggers = 0;
-	memset(laggers, 0, sizeof(boolean) * MAXPLAYERS);
-
-	netbuffer->packettype = PT_PING;
-
-	//check for ping limit breakage.
-	if (cv_maxping.value)
-	{
-		for (i = 1; i < MAXPLAYERS; i++)
-		{
-			if (playeringame[i] && !players[i].quittime
-			&& (realpingtable[i] / pingmeasurecount > (unsigned)cv_maxping.value))
-			{
-				if (players[i].jointime > 30 * TICRATE)
-					laggers[i] = true;
-				numlaggers++;
-			}
-			else
-				pingtimeout[i] = 0;
-		}
-
-		//kick lagging players... unless everyone but the server's ping sucks.
-		//in that case, it is probably the server's fault.
-		if (numlaggers < D_NumPlayers() - 1)
-		{
-			for (i = 1; i < MAXPLAYERS; i++)
-			{
-				if (playeringame[i] && laggers[i])
-				{
-					pingtimeout[i]++;
-					// ok your net has been bad for too long, you deserve to die.
-					if (pingtimeout[i] > cv_pingtimeout.value)
-					{
-						pingtimeout[i] = 0;
-						SendKick(i, KICK_MSG_PING_HIGH | KICK_MSG_KEEP_BODY);
-					}
-				}
-				/*
-					you aren't lagging,
-					but you aren't free yet.
-					In case you'll keep spiking,
-					we just make the timer go back down. (Very unstable net must still get kicked).
-				*/
-				else
-					pingtimeout[i] = (pingtimeout[i] == 0 ? 0 : pingtimeout[i]-1);
-			}
-		}
-	}
-
-	//make the ping packet and clear server data for next one
-	for (i = 0; i < MAXPLAYERS; i++)
-	{
-		netbuffer->u.pingtable[i] = realpingtable[i] / pingmeasurecount;
-		//server takes a snapshot of the real ping for display.
-		//otherwise, pings fluctuate a lot and would be odd to look at.
-		playerpingtable[i] = realpingtable[i] / pingmeasurecount;
-		realpingtable[i] = 0; //Reset each as we go.
-	}
-
-	// send the server's maxping as last element of our ping table. This is useful to let us know when we're about to get kicked.
-	netbuffer->u.pingtable[MAXPLAYERS] = cv_maxping.value;
-
-	//send out our ping packets
-	for (i = 0; i < MAXNETNODES; i++)
-		if (nodeingame[i])
-			HSendPacket(i, true, 0, sizeof(INT32) * (MAXPLAYERS+1));
-
-	pingmeasurecount = 1; //Reset count
-}
-
-static tic_t gametime = 0;
-
-static void UpdatePingTable(void)
-{
-	INT32 i;
-
-	if (server)
-	{
-		if (netgame && !(gametime % 35)) // update once per second.
-			PingUpdate();
-		// update node latency values so we can take an average later.
-		for (i = 0; i < MAXPLAYERS; i++)
-			if (playeringame[i] && playernode[i] != UINT8_MAX)
-				realpingtable[i] += G_TicsToMilliseconds(GetLag(playernode[i]));
-		pingmeasurecount++;
-	}
-}
-
-// Handle timeouts to prevent definitive freezes from happenning
-static void HandleNodeTimeouts(void)
-{
-	INT32 i;
-
-	if (server)
-	{
-		for (i = 1; i < MAXNETNODES; i++)
-			if (nodeingame[i] && freezetimeout[i] < I_GetTime())
-				Net_ConnectionTimeout(i);
-
-		// In case the cvar value was lowered
-		if (joindelay)
-			joindelay = min(joindelay - 1, 3 * (tic_t)cv_joindelay.value * TICRATE);
-	}
-}
-
-// Keep the network alive while not advancing tics!
-void NetKeepAlive(void)
-{
-	tic_t nowtime;
-	INT32 realtics;
-
-	nowtime = I_GetTime();
-	realtics = nowtime - gametime;
-
-	// return if there's no time passed since the last call
-	if (realtics <= 0) // nothing new to update
-		return;
-
-	UpdatePingTable();
-
-	GetPackets();
-
-#ifdef MASTERSERVER
-	MasterClient_Ticker();
-#endif
-
-	if (client)
-	{
-		// send keep alive
-		CL_SendClientKeepAlive();
-		// No need to check for resynch because we aren't running any tics
-	}
-	else
-	{
-		SV_SendServerKeepAlive();
-	}
-
-	// No else because no tics are being run and we can't resynch during this
-
-	Net_AckTicker();
-	HandleNodeTimeouts();
-	FileSendTicker();
-}
-
-void NetUpdate(void)
-{
-	static tic_t resptime = 0;
-	tic_t nowtime;
-	INT32 i;
-	INT32 realtics;
-
-	nowtime = I_GetTime();
-	realtics = nowtime - gametime;
-
-	if (realtics <= 0) // nothing new to update
-		return;
-
-	if (realtics > 5)
-	{
-		if (server)
-			realtics = 1;
-		else
-			realtics = 5;
-	}
-
-	if (server && dedicated && gamestate == GS_LEVEL)
-	{
-		const tic_t dedicatedidletime = cv_dedicatedidletime.value * TICRATE;
-		static tic_t dedicatedidletimeprev = 0;
-		static tic_t dedicatedidle = 0;
-
-		if (dedicatedidletime > 0)
-		{
-			for (i = 1; i < MAXNETNODES; ++i)
-				if (nodeingame[i])
-				{
-					if (dedicatedidle >= dedicatedidletime)
-					{
-						CONS_Printf("DEDICATED: Awakening from idle (Node %d detected...)\n", i);
-						dedicatedidle = 0;
-					}
-					break;
-				}
-
-			if (i == MAXNETNODES)
-			{
-				if (leveltime == 2)
-				{
-					// On next tick...
-					dedicatedidle = dedicatedidletime-1;
-				}
-				else if (dedicatedidle >= dedicatedidletime)
-				{
-					if (D_GetExistingTextcmd(gametic, 0) || D_GetExistingTextcmd(gametic+1, 0))
-					{
-						CONS_Printf("DEDICATED: Awakening from idle (Netxcmd detected...)\n");
-						dedicatedidle = 0;
-					}
-					else
-					{
-						realtics = 0;
-					}
-				}
-				else if ((dedicatedidle += realtics) >= dedicatedidletime)
-				{
-					const char *idlereason = "at round start";
-					if (leveltime > 3)
-						idlereason = va("for %d seconds", dedicatedidle/TICRATE);
-
-					CONS_Printf("DEDICATED: No nodes %s, idling...\n", idlereason);
-					realtics = 0;
-					dedicatedidle = dedicatedidletime;
-				}
-			}
-		}
-		else
-		{
-			if (dedicatedidletimeprev > 0 && dedicatedidle >= dedicatedidletimeprev)
-			{
-				CONS_Printf("DEDICATED: Awakening from idle (Idle disabled...)\n");
-			}
-			dedicatedidle = 0;
-		}
-
-		dedicatedidletimeprev = dedicatedidletime;
-	}
-
-	gametime = nowtime;
-
-	UpdatePingTable();
-
-	if (client)
-		maketic = neededtic;
-
-	Local_Maketic(realtics); // make local tic, and call menu?
-
-	if (server)
-		CL_SendClientCmd(); // send it
-
-	GetPackets(); // get packet from client or from server
-
-	// client send the command after a receive of the server
-	// the server send before because in single player is beter
-
-#ifdef MASTERSERVER
-	MasterClient_Ticker(); // Acking the Master Server
-#endif
-
-	if (client)
-	{
-#ifndef NONET
-		// If the client just finished redownloading the game state, load it
-		if (cl_redownloadinggamestate && fileneeded[0].status == FS_FOUND)
-			CL_ReloadReceivedSavegame();
-#endif
-
-		CL_SendClientCmd(); // Send tic cmd
-		hu_redownloadinggamestate = cl_redownloadinggamestate;
-	}
-	else
-	{
-		if (!demoplayback && realtics > 0)
-		{
-			INT32 counts;
-
-			hu_redownloadinggamestate = false;
-
-			// Don't erase tics not acknowledged
-			counts = realtics;
-
-			firstticstosend = gametic;
-			for (i = 0; i < MAXNETNODES; i++)
-			{
-				if (!nodeingame[i])
-					continue;
-				if (nettics[i] < firstticstosend)
-					firstticstosend = nettics[i];
-				if (maketic + counts >= nettics[i] + (BACKUPTICS - TICRATE))
-					Net_ConnectionTimeout(i);
-			}
-
-			if (maketic + counts >= firstticstosend + BACKUPTICS)
-				counts = firstticstosend+BACKUPTICS-maketic-1;
-
-			for (i = 0; i < counts; i++)
-				SV_Maketic(); // Create missed tics and increment maketic
-
-			for (; tictoclear < firstticstosend; tictoclear++) // Clear only when acknowledged
-				D_Clearticcmd(tictoclear);                    // Clear the maketic the new tic
-
-			SV_SendTics();
-
-			neededtic = maketic; // The server is a client too
-		}
-	}
-
-	Net_AckTicker();
-	HandleNodeTimeouts();
-
-	nowtime /= NEWTICRATERATIO;
-
-	if (nowtime > resptime)
-	{
-		resptime = nowtime;
-#ifdef HAVE_THREADS
-		I_lock_mutex(&m_menu_mutex);
-#endif
-		M_Ticker();
-#ifdef HAVE_THREADS
-		I_unlock_mutex(m_menu_mutex);
-#endif
-		CON_Ticker();
-	}
-
-	FileSendTicker();
-}
-
-/** Returns the number of players playing.
-  * \return Number of players. Can be zero if we're running a ::dedicated
-  *         server.
-  * \author Graue <graue@oceanbase.org>
-  */
-INT32 D_NumPlayers(void)
-{
-	INT32 num = 0, ix;
-	for (ix = 0; ix < MAXPLAYERS; ix++)
-		if (playeringame[ix])
-			num++;
-	return num;
-}
-
-/** Similar to the above, but counts only bots.
-  * Purpose is to remove bots from both the player count and the
-  * max player count on the server view
-*/
-INT32 D_NumBots(void)
-{
-	INT32 num = 0, ix;
-	for (ix = 0; ix < MAXPLAYERS; ix++)
-		if (playeringame[ix] && players[ix].bot)
-			num++;
-	return num;
-}
-
-tic_t GetLag(INT32 node)
-{
-	return gametic - nettics[node];
-}
-
-void D_MD5PasswordPass(const UINT8 *buffer, size_t len, const char *salt, void *dest)
-{
-#ifdef NOMD5
-	(void)buffer;
-	(void)len;
-	(void)salt;
-	memset(dest, 0, 16);
-#else
-	char tmpbuf[256];
-	const size_t sl = strlen(salt);
-
-	if (len > 256-sl)
-		len = 256-sl;
-
-	memcpy(tmpbuf, buffer, len);
-	memmove(&tmpbuf[len], salt, sl);
-	//strcpy(&tmpbuf[len], salt);
-	len += strlen(salt);
-	if (len < 256)
-		memset(&tmpbuf[len],0,256-len);
-
-	// Yes, we intentionally md5 the ENTIRE buffer regardless of size...
-	md5_buffer(tmpbuf, 256, dest);
-#endif
-}
diff --git a/src/d_main.c b/src/d_main.c
index 6e17af25876c950976adbf67549cc26544fa0f42..318616e809c9ade57d9fb23fd10175e168385b07 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
@@ -981,6 +981,7 @@ void D_StartTitle(void)
 	emeralds = 0;
 	memset(&luabanks, 0, sizeof(luabanks));
 	lastmaploaded = 0;
+	pickedchar = R_SkinAvailable(cv_defaultskin.string);
 
 	// In case someone exits out at the same time they start a time attack run,
 	// reset modeattacking
@@ -1626,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));
diff --git a/src/deh_soc.c b/src/deh_soc.c
index 2193cd875cd00898c035c0d47d030f189045572c..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"
diff --git a/src/deh_soc.h b/src/deh_soc.h
index 0cab545f680d6ec86d6e4707b53c2a16291439ba..029390133761c597c88cefb679cc847f8ce5e520 100644
--- a/src/deh_soc.h
+++ b/src/deh_soc.h
@@ -35,7 +35,7 @@
 #include "r_sky.h"
 #include "fastcmp.h"
 #include "lua_script.h" // Reluctantly included for LUA_EvalMath
-#include "d_clisrv.h"
+#include "netcode/d_clisrv.h"
 
 #ifdef HWRENDER
 #include "hardware/hw_light.h"
diff --git a/src/deh_tables.c b/src/deh_tables.c
index 0801cf935a184d5a30d0edd215fe90e2b68db737..7012ede466b2a1734fc9a47d9e63591ed900a05d 100644
--- a/src/deh_tables.c
+++ b/src/deh_tables.c
@@ -4883,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
diff --git a/src/doomdef.h b/src/doomdef.h
index 45d6645faa0bc0ff884e11cb5722e4967f679560..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
@@ -724,7 +723,7 @@ extern int
 /// Maintain compatibility with older 2.2 demos
 #define OLD22DEMOCOMPAT
 
-#if defined (HAVE_CURL) && ! defined (NONET)
+#ifdef HAVE_CURL
 #define MASTERSERVER
 #else
 #undef UPDATE_ALERT
diff --git a/src/doomstat.h b/src/doomstat.h
index fdd0d0b834ff58601cce67881e399709f9277d8b..6a2d6acf00f816804c1ec8830b69c9848deadcbe 100644
--- a/src/doomstat.h
+++ b/src/doomstat.h
@@ -634,7 +634,7 @@ extern boolean singletics;
 // Netgame stuff
 // =============
 
-#include "d_clisrv.h"
+#include "netcode/d_clisrv.h"
 
 extern consvar_t cv_timetic; // display high resolution timer
 extern consvar_t cv_powerupdisplay; // display powerups
diff --git a/src/dummy/i_net.c b/src/dummy/i_net.c
index f6e642022e4a54360d68004f69d61eaa5e3acb19..4c30dc767a04615b75e6badf68d3603bc475a575 100644
--- a/src/dummy/i_net.c
+++ b/src/dummy/i_net.c
@@ -1,4 +1,4 @@
-#include "../i_net.h"
+#include "../netcode/i_net.h"
 
 boolean I_InitNetwork(void)
 {
diff --git a/src/dummy/i_system.c b/src/dummy/i_system.c
index 125d2e8aec897f288995649aec2476989b4136d6..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;
 }
 
@@ -182,10 +187,12 @@ const char *I_ClipboardPaste(void)
 
 size_t I_GetRandomBytes(char *destination, size_t amount)
 {
+	(void)destination;
+	(void)amount;
 	return 0;
 }
 
-void I_RegisterSysCommands(void) {}
+void I_RegisterSysCommands(void){}
 
 void I_GetCursorPosition(INT32 *x, INT32 *y)
 {
diff --git a/src/f_finale.c b/src/f_finale.c
index c592c69bb3b691c48ece05ce8be47ee05f1f321e..59aa6dbb159ebef6c9210fd3a218ebe0c7c69fef 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"
@@ -2260,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;
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 4b9ff56e80f5711faabd18b53a9474f2c0a60c5e..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,7 +39,7 @@
 #include "v_video.h"
 #include "lua_hook.h"
 #include "md5.h" // demo checksums
-#include "d_netfil.h" // G_CheckDemoExtraFiles
+#include "netcode/d_netfil.h" // G_CheckDemoExtraFiles
 
 boolean timingdemo; // if true, exit with report on completion
 boolean nodrawers; // for comparative timing purposes
@@ -1500,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);
@@ -1887,16 +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 0x000f: // The previous demoversions also supported 
-	case 0x000e:
-	case 0x000d: // all that changed between then and now was longer color name
-	case 0x000c:
-		break;
-	// too old, cannot support.
-	default:
+		// too old (or new), cannot support
 		CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname);
 		Z_Free(buffer);
 		return UINT8_MAX;
@@ -1969,14 +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';
@@ -2035,23 +2029,13 @@ void G_DoPlayDemo(char *defdemoname)
 	subversion = READUINT8(demo_p);
 	demoversion = READUINT16(demo_p);
 	demo_forwardmove_rng = (demoversion < 0x0010);
-	switch(demoversion)
-	{
-	case 0x000f:
-	case 0x000d:
-	case 0x000e:
-	case DEMOVERSION: // latest always supported
-		cnamelen = MAXCOLORNAME;
-		break;
 #ifdef OLD22DEMOCOMPAT
-	// all that changed between then and now was longer color name
-	case 0x000c:
-		cnamelen = 16;
-		use_old_demo_vars = true;
-		break;
+	if (demoversion < 0x000c || demoversion > DEMOVERSION)
+#else
+	if (demoversion < 0x000d || demoversion > DEMOVERSION)
 #endif
-	// too old, cannot support.
-	default:
+	{
+		// too old (or new), cannot support
 		snprintf(msg, 1024, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemoname);
 		CONS_Alert(CONS_ERROR, "%s", msg);
 		M_StartMessage(msg, NULL, MM_NOTHING);
@@ -2178,8 +2162,8 @@ void G_DoPlayDemo(char *defdemoname)
 	demo_p += 16;
 
 	// Color
-	M_Memcpy(color,demo_p,cnamelen);
-	demo_p += cnamelen;
+	M_Memcpy(color, demo_p, (demoversion < 0x000d) ? 16 : MAXCOLORNAME);
+	demo_p += (demoversion < 0x000d) ? 16 : MAXCOLORNAME;
 
 	charability = READUINT8(demo_p);
 	charability2 = READUINT8(demo_p);
@@ -2215,7 +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
@@ -2263,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;
@@ -2349,19 +2332,13 @@ UINT8 G_CheckDemoForError(char *defdemoname)
 	demo_p++; // version
 	demo_p++; // subversion
 	our_demo_version = READUINT16(demo_p);
-	switch(our_demo_version)
-	{
-	case 0x000d:
-	case 0x000e:
-	case 0x000f:
-	case DEMOVERSION: // latest always supported
-		break;
 #ifdef OLD22DEMOCOMPAT
-	case 0x000c:
-		break;
+	if (our_demo_version < 0x000c || our_demo_version > DEMOVERSION)
+#else
+	if (our_demo_version < 0x000d || our_demo_version > DEMOVERSION)
 #endif
-	// too old, cannot support.
-	default:
+	{
+		// too old (or new), cannot support
 		return DFILE_ERROR_NOTDEMO;
 	}
 	demo_p += 16; // demo checksum
@@ -2383,7 +2360,6 @@ void G_AddGhost(char *defdemoname)
 	INT32 i;
 	lumpnum_t l;
 	char name[17],skin[17],color[MAXCOLORNAME+1],*n,*pdemoname,md5[16];
-	UINT8 cnamelen;
 	demoghost *gh;
 	UINT8 flags, subversion;
 	UINT8 *buffer,*p;
@@ -2435,20 +2411,9 @@ void G_AddGhost(char *defdemoname)
 	p++; // VERSION
 	subversion = READUINT8(p); // SUBVERSION
 	ghostversion = READUINT16(p);
-	switch(ghostversion)
+	if (ghostversion < 0x000c || ghostversion > DEMOVERSION)
 	{
-	case 0x000f:
-	case 0x000d:
-	case 0x000e:
-	case DEMOVERSION: // latest always supported
-		cnamelen = MAXCOLORNAME;
-		break;
-	// all that changed between then and now was longer color name
-	case 0x000c:
-		cnamelen = 16;
-		break;
-	// too old, cannot support.
-	default:
+		// too old (or new), cannot support
 		CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo version incompatible.\n"), pdemoname);
 		Z_Free(pdemoname);
 		Z_Free(buffer);
@@ -2511,8 +2476,8 @@ void G_AddGhost(char *defdemoname)
 	p += 16;
 
 	// Color
-	M_Memcpy(color, p,cnamelen);
-	p += cnamelen;
+	M_Memcpy(color, p, (ghostversion < 0x000d) ? 16 : MAXCOLORNAME);
+	p += (ghostversion < 0x000d) ? 16 : MAXCOLORNAME;
 
 	// Ghosts do not have a player structure to put this in.
 	p++; // charability
@@ -2695,16 +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 0x000f:
-	case 0x000e: // There are checks wheter the momentum is from older demo versions or not
-	case 0x000d: // all that changed between then and now was longer color name
-	case 0x000c:
-		break;
-	// too old, cannot support.
-	default:
+		// too old (or new), cannot support
 		CONS_Alert(CONS_WARNING, M_GetText("Failed to load bot recording for this map, format version incompatible.\n"));
 		Z_Free(metalbuffer);
 		return;
diff --git a/src/g_game.c b/src/g_game.c
index 9780bcd4d2c22cb56ec105501125b3c2799cf8cf..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;
@@ -316,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);
@@ -2080,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;
@@ -2173,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;
@@ -2749,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;
 
@@ -4803,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();
 
@@ -5001,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)
@@ -5024,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);
@@ -5107,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....
@@ -5134,6 +5118,9 @@ void G_InitNew(UINT8 pultmode, const char *mapname, boolean resetplayer, boolean
 	automapactive = false;
 	imcontinuing = false;
 
+	if (reset_skin)
+		D_SendPlayerConfig();
+
 	// fetch saved data if available
 	if (savedata.lives > 0)
 	{
diff --git a/src/g_game.h b/src/g_game.h
index a8c285f79f442b04a266f7e4ccbdd34997029833..4d86a0455e560e22afc9d75b24f5d97acaf4e95c 100644
--- a/src/g_game.h
+++ b/src/g_game.h
@@ -49,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;
@@ -174,8 +176,7 @@ void G_SpawnPlayer(INT32 playernum);
 
 // Can be called by the startup code or M_Responder.
 // A normal game starts at map 1, but a warp test can start elsewhere
-void G_DeferedInitNew(boolean pultmode, const char *mapname, INT32 pickedchar,
-	boolean SSSG, boolean FLS);
+void G_DeferedInitNew(boolean pultmode, const char *mapname, INT32 character, boolean SSSG, boolean FLS);
 void G_DoLoadLevel(boolean resetplayer);
 void G_StartTitleCard(void);
 void G_PreLevelTitleCard(void);
diff --git a/src/g_input.c b/src/g_input.c
index 826dcecbdf22b6ff833fce6e65bfb71ef051d05d..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/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_main.c b/src/hardware/hw_main.c
index bc66955fca4aab762076b4e322afe47ba548b2b2..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"
@@ -3595,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;
@@ -3606,9 +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);
-	if ((thing->scale != thing->old_scale) && (thing->scale >= FRACUNIT/1024)) // Interpolate shadows when scaling mobjs
-		scalemul = FixedMul(scalemul, FixedDiv(interp.scale, thing->scale));
+	scalemul = FixedMul(scalemul, (interp.radius*2) / gpatch->height);
 
 	fscale = FIXED_TO_FLOAT(scalemul);
 	fx = FIXED_TO_FLOAT(interp.x);
@@ -3720,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
 		{
@@ -4055,32 +4053,32 @@ static void HWR_DrawBoundingBox(gl_vissprite_t *vis)
 	// repeat this 4 times (overhead)
 	//
 	//
-	// 17    20  21    11
-	//    16 15  14 10
-	// 27 22  *--*  07 12
+	// 15    16  17    09
+	//    14 13  12 08
+	// 23 18  *--*  07 10
 	//        |  |
-	// 26 23  *--*  06 13
-	//    24 00  01 02
-	// 25    05  04    03
+	// 22 19  *--*  06 11
+	//    20 00  01 02
+	// 21    05  04    03
 	//
 
-	v[000].x = v[005].x = v[015].x = v[016].x = v[017].x = v[020].x =
-		v[022].x = v[023].x = v[024].x = v[025].x = v[026].x = v[027].x = vis->x1; // west
+	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[001].x = v[002].x = v[003].x = v[004].x = v[006].x = v[007].x =
-		v[010].x = v[011].x = v[012].x = v[013].x = v[014].x = v[021].x = vis->x2; // east
+	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[000].z = v[001].z = v[002].z = v[003].z = v[004].z = v[005].z =
-		v[006].z = v[013].z = v[023].z = v[024].z = v[025].z = v[026].z = vis->z1; // south
+	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[007].z = v[010].z = v[011].z = v[012].z = v[014].z = v[015].z =
-		v[016].z = v[017].z = v[020].z = v[021].z = v[022].z = v[027].z = vis->z2; // north
+	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[000].y = v[001].y = v[002].y = v[006].y = v[007].y = v[010].y =
-		v[014].y = v[015].y = v[016].y = v[022].y = v[023].y = v[024].y = vis->gz; // bottom
+	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[003].y = v[004].y = v[005].y = v[011].y = v[012].y = v[013].y =
-		v[017].y = v[020].y = v[021].y = v[025].y = v[026].y = v[027].y = vis->gzt; // top
+	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));
 	
@@ -4143,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;
@@ -5326,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));
@@ -5378,10 +5373,7 @@ static void HWR_ProjectSprite(mobj_t *thing)
 
 		if (vflip)
 		{
-			if (thing->scale != thing->old_scale) // Interpolate heights in reverse gravity when scaling mobjs
-				gz = FIXED_TO_FLOAT(interp.z + FixedMul(thing->height, FixedDiv(interp.scale, thing->scale))) - (FIXED_TO_FLOAT(spr_topoffset) * this_yscale);
-			else
-				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
@@ -5687,7 +5679,6 @@ static void HWR_ProjectBoundingBox(mobj_t *thing)
 	gl_vissprite_t *vis;
 	float tr_x, tr_y;
 	float tz;
-	float rad;
 
 	if (!thing)
 		return;
@@ -5722,15 +5713,13 @@ static void HWR_ProjectBoundingBox(mobj_t *thing)
 	tr_x += gl_viewx;
 	tr_y += gl_viewy;
 
-	rad = FIXED_TO_FLOAT(thing->radius);
-
 	vis = HWR_NewVisSprite();
-	vis->x1 = tr_x - rad;
-	vis->x2 = tr_x + rad;
-	vis->z1 = tr_y - rad;
-	vis->z2 = tr_y + rad;
+	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(thing->height);
+	vis->gzt = vis->gz + FIXED_TO_FLOAT(interp.height);
 	vis->mobj = thing;
 
 	vis->precip = false;
diff --git a/src/hardware/hw_md2.c b/src/hardware/hw_md2.c
index 87881be8d66da9777f86e1472750d9c05fb4094f..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)
@@ -1585,12 +1585,7 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 		p.y = FIXED_TO_FLOAT(interp.y)+md2->offset;
 
 		if (flip)
-		{
-			if (spr->mobj->scale != spr->mobj->old_scale) // Interpolate heights in reverse gravity when scaling mobjs
-				p.z = FIXED_TO_FLOAT(interp.z + FixedMul(spr->mobj->height, FixedDiv(interp.scale, spr->mobj->scale)));
-			else
-				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);
 
@@ -1626,8 +1621,8 @@ boolean HWR_DrawModel(gl_vissprite_t *spr)
 				p.roll = true;
 
 				// rotation pivot
-				p.centerx = FIXED_TO_FLOAT(spr->mobj->radius / 2);
-				p.centery = FIXED_TO_FLOAT(spr->mobj->height / 2);
+				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));
diff --git a/src/hardware/hw_model.c b/src/hardware/hw_model.c
index b69bce0e2da2dd5a453899720892187c5c71910c..4b6bce6f7b55834167d496a361c3936cd22cc8ac 100644
--- a/src/hardware/hw_model.c
+++ b/src/hardware/hw_model.c
@@ -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/hu_stuff.c b/src/hu_stuff.c
index 091e2b2fba50e46638aa6ee52bff2d7e6c81f9f0..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;
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/lua_baselib.c b/src/lua_baselib.c
index 4af5066aeb8df5a75b6091c205e932c72f6f2349..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"
@@ -3544,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;
 
@@ -3552,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);
diff --git a/src/lua_consolelib.c b/src/lua_consolelib.c
index 9e5d1e2ee03dfe94144c1eec79757fb2d3a2f064..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"
@@ -615,7 +616,7 @@ static int cvar_get(lua_State *L)
 		break;
 	default:
 		if (devparm)
-			return luaL_error(L, LUA_QL("consvar_t") " has no field named " LUA_QS, field);
+			return luaL_error(L, LUA_QL("consvar_t") " has no field named " LUA_QS ".", lua_tostring(L, 2));
 		else
 			return 0;
 	}
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 63a866606fe575a46e0af5168f57c072f4f8d87f..6eec91273352db4adc598a80b7e0138795589640 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -282,7 +282,6 @@ static int patch_get(lua_State *L)
 	patch_t *patch = *((patch_t **)luaL_checkudata(L, 1, META_PATCH));
 	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);
@@ -436,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;
 }
diff --git a/src/lua_maplib.c b/src/lua_maplib.c
index 3d95cdb751f8ae349ed026591ede5cd55438b189..e343979939056ec3c18b482a3bfd3423c446d70e 100644
--- a/src/lua_maplib.c
+++ b/src/lua_maplib.c
@@ -800,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;
@@ -1279,8 +1280,9 @@ 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;
@@ -2291,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;
@@ -2426,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);
 
diff --git a/src/lua_script.c b/src/lua_script.c
index 6a5982006379d3e32e9694009e73fe67111a7f40..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"
diff --git a/src/lua_skinlib.c b/src/lua_skinlib.c
index 13e0dd98715462fa9c63696aa87c307957162d4e..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,
@@ -63,6 +64,7 @@ static const char *const skin_opt[] = {
 	"flags",
 	"realname",
 	"hudname",
+	"supername",
 	"ability",
 	"ability2",
 	"thokitem",
@@ -126,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;
diff --git a/src/m_cheat.c b/src/m_cheat.c
index 7ad86353ac86916b167d3e0768f9a8ad6f23444e..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"
diff --git a/src/m_cond.c b/src/m_cond.c
index 6c87ebf6e5d8fb30789be6b5675f10daa574a22e..706a1b5106f487e4ed285880304a87335f7f716e 100644
--- a/src/m_cond.c
+++ b/src/m_cond.c
@@ -509,7 +509,7 @@ UINT8 M_CampaignWarpIsCheat(INT32 gt, INT32 mapnum, gamedata_t *data)
 		return true;
 	}
 
-	if (mapheaderinfo[mapnum-1]->menuflags & LF2_HIDEINMENU)
+	if (!mapheaderinfo[mapnum-1] || mapheaderinfo[mapnum-1]->menuflags & LF2_HIDEINMENU)
 	{
 		// You're never allowed to warp to this level.
 		return true;
diff --git a/src/m_menu.c b/src/m_menu.c
index c4944214158c16b64796701b37b3d8fc87bacff3..523430ed8e98145bd87dab952d75fa7fc8db376e 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?
 // ==========================================================================
@@ -296,7 +294,6 @@ static void M_SetupMultiPlayer2(INT32 choice);
 static void M_StartSplitServerMenu(INT32 choice);
 static void M_StartServer(INT32 choice);
 static void M_ServerOptions(INT32 choice);
-#ifndef NONET
 static void M_StartServerMenu(INT32 choice);
 static void M_ConnectMenu(INT32 choice);
 static void M_ConnectMenuModChecks(INT32 choice);
@@ -304,7 +301,6 @@ static void M_Refresh(INT32 choice);
 static void M_Connect(INT32 choice);
 static void M_ChooseRoom(INT32 choice);
 menu_t MP_MainDef;
-#endif
 
 // Options
 // Split into multiple parts due to size
@@ -382,11 +378,9 @@ static void M_DrawVideoMode(void);
 static void M_DrawColorMenu(void);
 static void M_DrawScreenshotMenu(void);
 static void M_DrawMonitorToggles(void);
-#ifndef NONET
 static void M_DrawConnectMenu(void);
 static void M_DrawMPMainMenu(void);
 static void M_DrawRoomMenu(void);
-#endif
 static void M_DrawJoystick(void);
 static void M_DrawSetupMultiPlayerMenu(void);
 static void M_DrawColorRamp(INT32 x, INT32 y, INT32 w, INT32 h, skincolor_t color);
@@ -401,10 +395,8 @@ static void M_HandleImageDef(INT32 choice);
 static void M_HandleLoadSave(INT32 choice);
 static void M_HandleLevelStats(INT32 choice);
 static void M_HandlePlaystyleMenu(INT32 choice);
-#ifndef NONET
 static boolean M_CancelConnect(void);
 static void M_HandleConnectIP(INT32 choice);
-#endif
 static void M_HandleSetupMultiPlayer(INT32 choice);
 static void M_HandleVideoMode(INT32 choice);
 
@@ -503,11 +495,7 @@ consvar_t cv_dummyloadless = CVAR_INIT ("dummyloadless", "In-game", CV_HIDEN, lo
 static menuitem_t MainMenu[] =
 {
 	{IT_STRING|IT_CALL,    NULL, "1  Player",   M_SinglePlayerMenu,      76},
-#ifndef NONET
 	{IT_STRING|IT_SUBMENU, NULL, "Multiplayer", &MP_MainDef,             84},
-#else
-	{IT_STRING|IT_CALL,    NULL, "Multiplayer", M_StartSplitServerMenu,  84},
-#endif
 	{IT_STRING|IT_CALL,    NULL, "Extras",      M_SecretsMenu,           92},
 	{IT_CALL   |IT_STRING, NULL, "Addons",      M_Addons,               100},
 	{IT_STRING|IT_CALL,    NULL, "Options",     M_Options,              108},
@@ -930,16 +918,10 @@ static menuitem_t SP_PlayerMenu[] =
 static menuitem_t MP_SplitServerMenu[] =
 {
 	{IT_STRING|IT_CALL,              NULL, "Select Gametype/Level...", M_MapChange,         100},
-#ifdef NONET // In order to keep player setup accessible.
-	{IT_STRING|IT_CALL,              NULL, "Player 1 setup...",        M_SetupMultiPlayer,  110},
-	{IT_STRING|IT_CALL,              NULL, "Player 2 setup...",        M_SetupMultiPlayer2, 120},
-#endif
 	{IT_STRING|IT_CALL,              NULL, "More Options...",          M_ServerOptions,     130},
 	{IT_WHITESTRING|IT_CALL,         NULL, "Start",                    M_StartServer,       140},
 };
 
-#ifndef NONET
-
 static menuitem_t MP_MainMenu[] =
 {
 	{IT_HEADER, NULL, "Join a game", NULL, 0},
@@ -1026,8 +1008,6 @@ menuitem_t MP_RoomMenu[] =
 	{IT_DISABLED,         NULL, "",               M_ChooseRoom, 162},
 };
 
-#endif
-
 static menuitem_t MP_PlayerSetupMenu[] =
 {
 	{IT_KEYHANDLER, NULL, "", M_HandleSetupMultiPlayer, 0}, // name
@@ -1586,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},
 
@@ -1632,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},
 
@@ -1640,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[] =
@@ -1954,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,
@@ -1966,8 +1938,6 @@ menu_t MP_SplitServerDef =
 	NULL
 };
 
-#ifndef NONET
-
 menu_t MP_MainDef =
 {
 	MN_MP_MAIN,
@@ -2019,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
@@ -2649,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;
@@ -2787,6 +2752,7 @@ void M_SetMenuCurBackground(const char *defaultname)
 {
 	char name[9];
 	strncpy(name, defaultname, 8);
+	name[8] = '\0';
 	M_IterateMenuTree(MIT_SetCurBackground, &name);
 }
 
@@ -2843,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;
@@ -3196,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)
@@ -3956,9 +3922,7 @@ void M_Init(void)
 		OP_JoystickSetMenu[i].itemaction = M_AssignJoystick;
 	}
 
-#ifndef NONET
 	CV_RegisterVar(&cv_serversort);
-#endif
 }
 
 void M_InitCharacterTables(void)
@@ -11108,7 +11072,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)
@@ -11380,11 +11343,9 @@ static int ServerListEntryComparator_modified(const void *entry1, const void *en
 	// Default to strcmp.
 	return strcmp(sa->info.servername, sb->info.servername);
 }
-#endif
 
 void M_SortServerList(void)
 {
-#ifndef NONET
 	switch(cv_serversort.value)
 	{
 	case 0:		// Ping.
@@ -11406,10 +11367,8 @@ void M_SortServerList(void)
 		qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_gametypename);
 		break;
 	}
-#endif
 }
 
-#ifndef NONET
 #ifdef UPDATE_ALERT
 static boolean M_CheckMODVersion(int id)
 {
@@ -11608,7 +11567,6 @@ static void M_ChooseRoom(INT32 choice)
 	if (currentMenu == &MP_ConnectDef)
 		M_Refresh(0);
 }
-#endif //NONET
 
 //===========================================================================
 // Start Server Menu
@@ -11656,7 +11614,6 @@ static void M_DrawServerMenu(void)
 {
 	M_DrawGenericMenu();
 
-#ifndef NONET
 	// Room name
 	if (currentMenu == &MP_ServerDef)
 	{
@@ -11668,15 +11625,10 @@ static void M_DrawServerMenu(void)
 			V_DrawRightAlignedString(BASEVIDWIDTH - currentMenu->x, currentMenu->y + MP_ServerMenu[mp_server_room].alphaKey,
 			                         V_YELLOWMAP, room_list[menuRoomIndex].name);
 	}
-#endif
 
 	if (cv_nextmap.value)
 	{
-#ifndef NONET
 #define imgheight MP_ServerMenu[mp_server_levelgt].alphaKey
-#else
-#define imgheight 100
-#endif
 		patch_t *PictureOfLevel;
 		lumpnum_t lumpnum;
 		char headerstr[40];
@@ -11728,7 +11680,6 @@ static void M_ServerOptions(INT32 choice)
 {
 	(void)choice;
 
-#ifndef NONET
 	if ((splitscreen && !netgame) || currentMenu == &MP_SplitServerDef)
 	{
 		OP_ServerOptionsMenu[ 1].status = IT_GRAYEDOUT; // Server name
@@ -11749,7 +11700,6 @@ static void M_ServerOptions(INT32 choice)
 		OP_ServerOptionsMenu[37].status = IT_STRING | IT_CVAR;
 		OP_ServerOptionsMenu[38].status = IT_STRING | IT_CVAR;
 	}
-#endif
 
 	/* Disable fading because of different menu head. */
 	if (currentMenu == &OP_MainDef)/* from Options menu */
@@ -11761,7 +11711,6 @@ static void M_ServerOptions(INT32 choice)
 	M_SetupNextMenu(&OP_ServerOptionsDef);
 }
 
-#ifndef NONET
 static void M_StartServerMenu(INT32 choice)
 {
 	(void)choice;
@@ -12028,7 +11977,6 @@ static void M_HandleConnectIP(INT32 choice)
 			M_ClearMenus(true);
 	}
 }
-#endif //!NONET
 
 // ========================
 // MULTIPLAYER PLAYER SETUP
@@ -12690,11 +12638,7 @@ static void M_SetupMultiPlayer(INT32 choice)
 	else
 		MP_PlayerSetupMenu[1].status = (IT_KEYHANDLER|IT_STRING);
 
-	// ditto with colour
-	if (Playing() && G_GametypeHasTeams())
-		MP_PlayerSetupMenu[2].status = (IT_GRAYEDOUT);
-	else
-		MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER|IT_STRING);
+	MP_PlayerSetupMenu[2].status = (IT_KEYHANDLER|IT_STRING);
 
 	multi_spr2 = P_GetSkinSprite2(&skins[setupm_fakeskin], SPR2_WALK, NULL);
 
@@ -12709,7 +12653,7 @@ static void M_SetupMultiPlayer2(INT32 choice)
 
 	multi_frame = 0;
 	multi_tics = 4*FRACUNIT;
-	
+
 	strcpy (setupm_name, cv_playername2.string);
 
 	// set for splitscreen secondary player
@@ -12735,11 +12679,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);
 
diff --git a/src/m_menu.h b/src/m_menu.h
index c925c7f49c775d4a6e0becd7e2c5eb03d60784e4..b8fe3b808928b81c86bf3c09731e557f4b113616 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -20,7 +20,7 @@
 #include "command.h"
 #include "f_finale.h" // for ttmode_enum
 #include "i_threads.h"
-#include "mserv.h"
+#include "netcode/mserv.h"
 #include "r_things.h" // for SKINNAMESIZE
 
 // Compatibility with old-style named NiGHTS replay files.
@@ -74,7 +74,7 @@ typedef enum
 	MN_MP_SERVER,
 	MN_MP_CONNECT,
 	MN_MP_ROOM,
-	MN_MP_PLAYERSETUP, // MP_PlayerSetupDef shared with SPLITSCREEN if #defined NONET
+	MN_MP_PLAYERSETUP,
 	MN_MP_SERVER_OPTIONS,
 
 	// Options
diff --git a/src/m_perfstats.c b/src/m_perfstats.c
index 17e026b3e181714acee6ff4ca7887a1b199f1b77..1511859324059a5ce8e5836ff99dbdbe841c02a1 100644
--- a/src/m_perfstats.c
+++ b/src/m_perfstats.c
@@ -12,7 +12,7 @@
 #include "m_perfstats.h"
 #include "v_video.h"
 #include "i_video.h"
-#include "d_netcmd.h"
+#include "netcode/d_netcmd.h"
 #include "r_main.h"
 #include "i_system.h"
 #include "z_zone.h"
diff --git a/src/netcode/CMakeLists.txt b/src/netcode/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..69b976d17e404e6e389bdf36079c72dac2480cdc
--- /dev/null
+++ b/src/netcode/CMakeLists.txt
@@ -0,0 +1,15 @@
+target_sources(SRB2SDL2 PRIVATE
+	d_clisrv.c
+	server_connection.c
+	client_connection.c
+	tic_command.c
+	net_command.c
+	gamestate.c
+	commands.c
+	d_net.c
+	d_netcmd.c
+	d_netfil.c
+	http-mserv.c
+	i_tcp.c
+	mserv.c
+)
diff --git a/src/netcode/Sourcefile b/src/netcode/Sourcefile
new file mode 100644
index 0000000000000000000000000000000000000000..7c0354714f9c3739c9b0920b0c7db6466dcef2c3
--- /dev/null
+++ b/src/netcode/Sourcefile
@@ -0,0 +1,13 @@
+d_clisrv.c
+server_connection.c
+client_connection.c
+tic_command.c
+net_command.c
+gamestate.c
+commands.c
+d_net.c
+d_netcmd.c
+d_netfil.c
+http-mserv.c
+i_tcp.c
+mserv.c
diff --git a/src/netcode/client_connection.c b/src/netcode/client_connection.c
new file mode 100644
index 0000000000000000000000000000000000000000..907021e7dafddfb3f31b2a2dd850f15d4890a16e
--- /dev/null
+++ b/src/netcode/client_connection.c
@@ -0,0 +1,1178 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  client_connection.h
+/// \brief Client connection handling
+
+#include "client_connection.h"
+#include "gamestate.h"
+#include "d_clisrv.h"
+#include "d_netfil.h"
+#include "../d_main.h"
+#include "../f_finale.h"
+#include "../g_game.h"
+#include "../g_input.h"
+#include "i_net.h"
+#include "../i_system.h"
+#include "../i_time.h"
+#include "../i_video.h"
+#include "../keys.h"
+#include "../m_menu.h"
+#include "../m_misc.h"
+#include "../snake.h"
+#include "../s_sound.h"
+#include "../v_video.h"
+#include "../y_inter.h"
+#include "../z_zone.h"
+#include "../doomtype.h"
+#include "../doomstat.h"
+#if defined (__GNUC__) || defined (__unix__)
+#include <unistd.h>
+#endif
+
+cl_mode_t cl_mode = CL_SEARCHING;
+static UINT16 cl_lastcheckedfilecount = 0;	// used for full file list
+boolean serverisfull = false; // lets us be aware if the server was full after we check files, but before downloading, so we can ask if the user still wants to download or not
+tic_t firstconnectattempttime = 0;
+UINT8 mynode;
+static void *snake = NULL;
+
+static void CL_DrawConnectionStatusBox(void)
+{
+	M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
+	if (cl_mode != CL_CONFIRMCONNECT)
+		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
+}
+
+//
+// CL_DrawConnectionStatus
+//
+// Keep the local client informed of our status.
+//
+static inline void CL_DrawConnectionStatus(void)
+{
+	INT32 ccstime = I_GetTime();
+
+	// Draw background fade
+	V_DrawFadeScreen(0xFF00, 16); // force default
+
+	if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_LOADFILES)
+	{
+		INT32 animtime = ((ccstime / 4) & 15) + 16;
+		UINT8 palstart;
+		const char *cltext;
+
+		// Draw the bottom box.
+		CL_DrawConnectionStatusBox();
+
+		if (cl_mode == CL_SEARCHING)
+			palstart = 32; // Red
+		else if (cl_mode == CL_CONFIRMCONNECT)
+			palstart = 48; // Orange
+		else
+			palstart = 96; // Green
+
+		if (!(cl_mode == CL_DOWNLOADSAVEGAME && lastfilenum != -1))
+			for (INT32 i = 0; i < 16; ++i) // 15 pal entries total.
+				V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-16, 16, 8, palstart + ((animtime - i) & 15));
+
+		switch (cl_mode)
+		{
+			case CL_DOWNLOADSAVEGAME:
+				if (fileneeded && lastfilenum != -1)
+				{
+					UINT32 currentsize = fileneeded[lastfilenum].currentsize;
+					UINT32 totalsize = fileneeded[lastfilenum].totalsize;
+					INT32 dldlength;
+
+					cltext = M_GetText("Downloading game state...");
+					Net_GetNetStat();
+
+					dldlength = (INT32)((currentsize/(double)totalsize) * 256);
+					if (dldlength > 256)
+						dldlength = 256;
+					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
+					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
+
+					V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
+						va(" %4uK/%4uK",currentsize>>10,totalsize>>10));
+
+					V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
+						va("%3.1fK/s ", ((double)getbps)/1024));
+				}
+				else
+					cltext = M_GetText("Waiting to download game state...");
+				break;
+			case CL_ASKFULLFILELIST:
+			case CL_CHECKFILES:
+				cltext = M_GetText("Checking server addon list...");
+				break;
+			case CL_CONFIRMCONNECT:
+				cltext = "";
+				break;
+			case CL_LOADFILES:
+				cltext = M_GetText("Loading server addons...");
+				break;
+			case CL_ASKJOIN:
+			case CL_WAITJOINRESPONSE:
+				if (serverisfull)
+					cltext = M_GetText("Server full, waiting for a slot...");
+				else
+					cltext = M_GetText("Requesting to join...");
+				break;
+			default:
+				cltext = M_GetText("Connecting to server...");
+				break;
+		}
+		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, cltext);
+	}
+	else
+	{
+		if (cl_mode == CL_LOADFILES)
+		{
+			INT32 totalfileslength;
+			INT32 loadcompletednum = 0;
+
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
+
+			// ima just count files here
+			if (fileneeded)
+			{
+				for (INT32 i = 0; i < fileneedednum; i++)
+					if (fileneeded[i].status == FS_OPEN)
+						loadcompletednum++;
+			}
+
+			// Loading progress
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, "Loading server addons...");
+			totalfileslength = (INT32)((loadcompletednum/(double)(fileneedednum)) * 256);
+			M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
+			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
+			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, totalfileslength, 8, 96);
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
+				va(" %2u/%2u Files",loadcompletednum,fileneedednum));
+		}
+		else if (lastfilenum != -1)
+		{
+			INT32 dldlength;
+			static char tempname[28];
+			fileneeded_t *file;
+			char *filename;
+
+			if (snake)
+				Snake_Draw(snake);
+
+			// Draw the bottom box.
+			CL_DrawConnectionStatusBox();
+
+			if (fileneeded)
+			{
+				file = &fileneeded[lastfilenum];
+				filename = file->filename;
+			}
+			else
+				return;
+
+			Net_GetNetStat();
+			dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256);
+			if (dldlength > 256)
+				dldlength = 256;
+			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
+			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
+
+			memset(tempname, 0, sizeof(tempname));
+			// offset filename to just the name only part
+			filename += strlen(filename) - nameonlylength(filename);
+
+			if (strlen(filename) > sizeof(tempname)-1) // too long to display fully
+			{
+				size_t endhalfpos = strlen(filename)-10;
+				// display as first 14 chars + ... + last 10 chars
+				// which should add up to 27 if our math(s) is correct
+				snprintf(tempname, sizeof(tempname), "%.14s...%.10s", filename, filename+endhalfpos);
+			}
+			else // we can copy the whole thing in safely
+			{
+				strncpy(tempname, filename, sizeof(tempname)-1);
+			}
+
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
+				va(M_GetText("Downloading \"%s\""), tempname));
+			V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
+				va(" %4uK/%4uK",fileneeded[lastfilenum].currentsize>>10,file->totalsize>>10));
+			V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
+				va("%3.1fK/s ", ((double)getbps)/1024));
+		}
+		else
+		{
+			if (snake)
+				Snake_Draw(snake);
+
+			CL_DrawConnectionStatusBox();
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
+				M_GetText("Waiting to download files..."));
+		}
+	}
+}
+
+static boolean CL_AskFileList(INT32 firstfile)
+{
+	netbuffer->packettype = PT_TELLFILESNEEDED;
+	netbuffer->u.filesneedednum = firstfile;
+
+	return HSendPacket(servernode, false, 0, sizeof (INT32));
+}
+
+/** Sends a PT_CLIENTJOIN packet to the server
+  *
+  * \return True if the packet was successfully sent
+  *
+  */
+boolean CL_SendJoin(void)
+{
+	UINT8 localplayers = 1;
+	char const *player2name;
+	if (netgame)
+		CONS_Printf(M_GetText("Sending join request...\n"));
+	netbuffer->packettype = PT_CLIENTJOIN;
+
+	netbuffer->u.clientcfg.modversion = MODVERSION;
+	strncpy(netbuffer->u.clientcfg.application,
+			SRB2APPLICATION,
+			sizeof netbuffer->u.clientcfg.application);
+
+	if (splitscreen || botingame)
+		localplayers++;
+	netbuffer->u.clientcfg.localplayers = localplayers;
+
+	CleanupPlayerName(consoleplayer, cv_playername.zstring);
+	if (splitscreen)
+		CleanupPlayerName(1, cv_playername2.zstring); // 1 is a HACK? oh no
+
+	// Avoid empty string on bots to avoid softlocking in singleplayer
+	if (botingame)
+		player2name = strcmp(cv_playername.zstring, "Tails") == 0 ? "Tail" : "Tails";
+	else
+		player2name = cv_playername2.zstring;
+
+	strncpy(netbuffer->u.clientcfg.names[0], cv_playername.zstring, MAXPLAYERNAME);
+	strncpy(netbuffer->u.clientcfg.names[1], player2name, MAXPLAYERNAME);
+
+	return HSendPacket(servernode, true, 0, sizeof (clientconfig_pak));
+}
+
+static void SendAskInfo(INT32 node)
+{
+	const tic_t asktime = I_GetTime();
+	netbuffer->packettype = PT_ASKINFO;
+	netbuffer->u.askinfo.version = VERSION;
+	netbuffer->u.askinfo.time = (tic_t)LONG(asktime);
+
+	// Even if this never arrives due to the host being firewalled, we've
+	// now allowed traffic from the host to us in, so once the MS relays
+	// our address to the host, it'll be able to speak to us.
+	HSendPacket(node, false, 0, sizeof (askinfo_pak));
+}
+
+serverelem_t serverlist[MAXSERVERLIST];
+UINT32 serverlistcount = 0;
+
+#define FORCECLOSE 0x8000
+
+static void SL_ClearServerList(INT32 connectedserver)
+{
+	for (UINT32 i = 0; i < serverlistcount; i++)
+		if (connectedserver != serverlist[i].node)
+		{
+			Net_CloseConnection(serverlist[i].node|FORCECLOSE);
+			serverlist[i].node = 0;
+		}
+	serverlistcount = 0;
+}
+
+static UINT32 SL_SearchServer(INT32 node)
+{
+	for (UINT32 i = 0; i < serverlistcount; i++)
+		if (serverlist[i].node == node)
+			return i;
+
+	return UINT32_MAX;
+}
+
+static void SL_InsertServer(serverinfo_pak* info, SINT8 node)
+{
+	UINT32 i;
+
+	// search if not already on it
+	i = SL_SearchServer(node);
+	if (i == UINT32_MAX)
+	{
+		// not found add it
+		if (serverlistcount >= MAXSERVERLIST)
+			return; // list full
+
+		// check it later if connecting to this one
+		if (node != servernode)
+		{
+			if (info->_255 != 255)
+				return; // Old packet format
+
+			if (info->packetversion != PACKETVERSION)
+				return; // Old new packet format
+
+			if (info->version != VERSION)
+				return; // Not same version.
+
+			if (info->subversion != SUBVERSION)
+				return; // Close, but no cigar.
+
+			if (strcmp(info->application, SRB2APPLICATION))
+				return; // That's a different mod
+		}
+
+		i = serverlistcount++;
+	}
+
+	serverlist[i].info = *info;
+	serverlist[i].node = node;
+
+	// resort server list
+	M_SortServerList();
+}
+
+#if defined (MASTERSERVER) && defined (HAVE_THREADS)
+struct Fetch_servers_ctx
+{
+	int room;
+	int id;
+};
+
+static void
+Fetch_servers_thread (struct Fetch_servers_ctx *ctx)
+{
+	msg_server_t *server_list;
+
+	server_list = GetShortServersList(ctx->room, ctx->id);
+
+	if (server_list)
+	{
+		I_lock_mutex(&ms_QueryId_mutex);
+		{
+			if (ctx->id != ms_QueryId)
+			{
+				free(server_list);
+				server_list = NULL;
+			}
+		}
+		I_unlock_mutex(ms_QueryId_mutex);
+
+		if (server_list)
+		{
+			I_lock_mutex(&m_menu_mutex);
+			{
+				if (m_waiting_mode == M_WAITING_SERVERS)
+					m_waiting_mode = M_NOT_WAITING;
+			}
+			I_unlock_mutex(m_menu_mutex);
+
+			I_lock_mutex(&ms_ServerList_mutex);
+			{
+				ms_ServerList = server_list;
+			}
+			I_unlock_mutex(ms_ServerList_mutex);
+		}
+	}
+
+	free(ctx);
+}
+#endif // defined (MASTERSERVER) && defined (HAVE_THREADS)
+
+void CL_QueryServerList (msg_server_t *server_list)
+{
+	for (INT32 i = 0; server_list[i].header.buffer[0]; i++)
+	{
+		// Make sure MS version matches our own, to
+		// thwart nefarious servers who lie to the MS.
+
+		// lol bruh, that version COMES from the servers
+		//if (strcmp(version, server_list[i].version) == 0)
+		{
+			INT32 node = I_NetMakeNodewPort(server_list[i].ip, server_list[i].port);
+			if (node == -1)
+				break; // no more node free
+			SendAskInfo(node);
+			// Force close the connection so that servers can't eat
+			// up nodes forever if we never get a reply back from them
+			// (usually when they've not forwarded their ports).
+			//
+			// Don't worry, we'll get in contact with the working
+			// servers again when they send SERVERINFO to us later!
+			//
+			// (Note: as a side effect this probably means every
+			// server in the list will probably be using the same node (e.g. node 1),
+			// not that it matters which nodes they use when
+			// the connections are closed afterwards anyway)
+			// -- Monster Iestyn 12/11/18
+			Net_CloseConnection(node|FORCECLOSE);
+		}
+	}
+}
+
+void CL_UpdateServerList(boolean internetsearch, INT32 room)
+{
+	(void)internetsearch;
+	(void)room;
+
+	SL_ClearServerList(0);
+
+	if (!netgame && I_NetOpenSocket)
+	{
+		if (I_NetOpenSocket())
+		{
+			netgame = true;
+			multiplayer = true;
+		}
+	}
+
+	// search for local servers
+	if (netgame)
+		SendAskInfo(BROADCASTADDR);
+
+#ifdef MASTERSERVER
+	if (internetsearch)
+	{
+#ifdef HAVE_THREADS
+		struct Fetch_servers_ctx *ctx;
+
+		ctx = malloc(sizeof *ctx);
+
+		// This called from M_Refresh so I don't use a mutex
+		m_waiting_mode = M_WAITING_SERVERS;
+
+		I_lock_mutex(&ms_QueryId_mutex);
+		{
+			ctx->id = ms_QueryId;
+		}
+		I_unlock_mutex(ms_QueryId_mutex);
+
+		ctx->room = room;
+
+		I_spawn_thread("fetch-servers", (I_thread_fn)Fetch_servers_thread, ctx);
+#else
+		msg_server_t *server_list;
+
+		server_list = GetShortServersList(room, 0);
+
+		if (server_list)
+		{
+			CL_QueryServerList(server_list);
+			free(server_list);
+		}
+#endif
+	}
+#endif // MASTERSERVER
+}
+
+static void M_ConfirmConnect(event_t *ev)
+{
+	if (ev->type == ev_keydown)
+	{
+		if (ev->key == ' ' || ev->key == 'y' || ev->key == KEY_ENTER || ev->key == KEY_JOY1)
+		{
+			if (totalfilesrequestednum > 0)
+			{
+				if (CL_SendFileRequest())
+				{
+					cl_mode = CL_DOWNLOADFILES;
+					Snake_Allocate(&snake);
+				}
+			}
+			else
+				cl_mode = CL_LOADFILES;
+
+			M_ClearMenus(true);
+		}
+		else if (ev->key == 'n' || ev->key == KEY_ESCAPE || ev->key == KEY_JOY1 + 3)
+		{
+			cl_mode = CL_ABORTED;
+			M_ClearMenus(true);
+		}
+	}
+}
+
+static boolean CL_FinishedFileList(void)
+{
+	INT32 i;
+	char *downloadsize = NULL;
+
+	//CONS_Printf(M_GetText("Checking files...\n"));
+	i = CL_CheckFiles();
+	if (i == 4) // still checking ...
+	{
+		return true;
+	}
+	else if (i == 3) // too many files
+	{
+		D_QuitNetGame();
+		CL_Reset();
+		D_StartTitle();
+		M_StartMessage(M_GetText(
+			"You have too many WAD files loaded\n"
+			"to add ones the server is using.\n"
+			"Please restart SRB2 before connecting.\n\n"
+			"Press ESC\n"
+		), NULL, MM_NOTHING);
+		return false;
+	}
+	else if (i == 2) // cannot join for some reason
+	{
+		D_QuitNetGame();
+		CL_Reset();
+		D_StartTitle();
+		M_StartMessage(M_GetText(
+			"You have the wrong addons loaded.\n\n"
+			"To play on this server, restart\n"
+			"the game and don't load any addons.\n"
+			"SRB2 will automatically add\n"
+			"everything you need when you join.\n\n"
+			"Press ESC\n"
+		), NULL, MM_NOTHING);
+		return false;
+	}
+	else if (i == 1)
+	{
+		if (serverisfull)
+		{
+			M_StartMessage(M_GetText(
+				"This server is full!\n"
+				"\n"
+				"You may load server addons (if any), and wait for a slot.\n"
+				"\n"
+				"Press ENTER to continue\nor ESC to cancel.\n\n"
+			), M_ConfirmConnect, MM_EVENTHANDLER);
+			cl_mode = CL_CONFIRMCONNECT;
+			curfadevalue = 0;
+		}
+		else
+			cl_mode = CL_LOADFILES;
+	}
+	else
+	{
+		// must download something
+		// can we, though?
+		if (!CL_CheckDownloadable()) // nope!
+		{
+			D_QuitNetGame();
+			CL_Reset();
+			D_StartTitle();
+			M_StartMessage(M_GetText(
+				"An error occurred when trying to\n"
+				"download missing addons.\n"
+				"(This is almost always a problem\n"
+				"with the server, not your game.)\n\n"
+				"See the console or log file\n"
+				"for additional details.\n\n"
+				"Press ESC\n"
+			), NULL, MM_NOTHING);
+			return false;
+		}
+
+		downloadcompletednum = 0;
+		downloadcompletedsize = 0;
+		totalfilesrequestednum = 0;
+		totalfilesrequestedsize = 0;
+
+		if (fileneeded == NULL)
+			I_Error("CL_FinishedFileList: fileneeded == NULL");
+
+		for (i = 0; i < fileneedednum; i++)
+			if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
+			{
+				totalfilesrequestednum++;
+				totalfilesrequestedsize += fileneeded[i].totalsize;
+			}
+
+		if (totalfilesrequestedsize>>20 >= 100)
+			downloadsize = Z_StrDup(va("%uM",totalfilesrequestedsize>>20));
+		else
+			downloadsize = Z_StrDup(va("%uK",totalfilesrequestedsize>>10));
+
+		if (serverisfull)
+			M_StartMessage(va(M_GetText(
+				"This server is full!\n"
+				"Download of %s additional content\nis required to join.\n"
+				"\n"
+				"You may download, load server addons,\nand wait for a slot.\n"
+				"\n"
+				"Press ENTER to continue\nor ESC to cancel.\n"
+			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
+		else
+			M_StartMessage(va(M_GetText(
+				"Download of %s additional content\nis required to join.\n"
+				"\n"
+				"Press ENTER to continue\nor ESC to cancel.\n"
+			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
+
+		Z_Free(downloadsize);
+		cl_mode = CL_CONFIRMCONNECT;
+		curfadevalue = 0;
+	}
+	return true;
+}
+
+static const char * InvalidServerReason (serverinfo_pak *info)
+{
+#define EOT "\nPress ESC\n"
+
+	// Magic number for new packet format
+	if (info->_255 != 255)
+	{
+		return
+			"Outdated server (version unknown).\n" EOT;
+	}
+
+	if (strncmp(info->application, SRB2APPLICATION, sizeof
+				info->application))
+	{
+		return va(
+				"%s cannot connect\n"
+				"to %s servers.\n" EOT,
+				SRB2APPLICATION,
+				info->application);
+	}
+
+	if (
+			info->packetversion != PACKETVERSION ||
+			info->version != VERSION ||
+			info->subversion != SUBVERSION
+	){
+		return va(
+				"Incompatible %s versions.\n"
+				"(server version %d.%d.%d)\n" EOT,
+				SRB2APPLICATION,
+				info->version / 100,
+				info->version % 100,
+				info->subversion);
+	}
+
+	switch (info->refusereason)
+	{
+		case REFUSE_BANNED:
+			return
+				"You have been banned\n"
+				"from the server.\n" EOT;
+		case REFUSE_JOINS_DISABLED:
+			return
+				"The server is not accepting\n"
+				"joins for the moment.\n" EOT;
+		case REFUSE_SLOTS_FULL:
+			return va(
+					"Maximum players reached: %d\n" EOT,
+					info->maxplayer - D_NumBots());
+		default:
+			if (info->refusereason)
+			{
+				return
+					"You can't join.\n"
+					"I don't know why,\n"
+					"but you can't join.\n" EOT;
+			}
+	}
+
+	return NULL;
+
+#undef EOT
+}
+
+/** Called by CL_ServerConnectionTicker
+  *
+  * \param asksent The last time we asked the server to join. We re-ask every second in case our request got lost in transmit.
+  * \return False if the connection was aborted
+  * \sa CL_ServerConnectionTicker
+  * \sa CL_ConnectToServer
+  *
+  */
+static boolean CL_ServerConnectionSearchTicker(tic_t *asksent)
+{
+	INT32 i;
+
+	// serverlist is updated by GetPackets
+	if (serverlistcount > 0)
+	{
+		// This can be a response to our broadcast request
+		if (servernode == -1 || servernode >= MAXNETNODES)
+		{
+			i = 0;
+			servernode = serverlist[i].node;
+			CONS_Printf(M_GetText("Found, "));
+		}
+		else
+		{
+			i = SL_SearchServer(servernode);
+			if (i < 0)
+				return true;
+		}
+
+		if (client)
+		{
+			serverinfo_pak *info = &serverlist[i].info;
+
+			if (info->refusereason == REFUSE_SLOTS_FULL)
+				serverisfull = true;
+			else
+			{
+				const char *reason = InvalidServerReason(info);
+
+				// Quit here rather than downloading files
+				// and being refused later.
+				if (reason)
+				{
+					char *message = Z_StrDup(reason);
+					D_QuitNetGame();
+					CL_Reset();
+					D_StartTitle();
+					M_StartMessage(message, NULL, MM_NOTHING);
+					Z_Free(message);
+					return false;
+				}
+			}
+
+			D_ParseFileneeded(info->fileneedednum, info->fileneeded, 0);
+
+			if (info->flags & SV_LOTSOFADDONS)
+			{
+				cl_mode = CL_ASKFULLFILELIST;
+				cl_lastcheckedfilecount = 0;
+				return true;
+			}
+
+			cl_mode = CL_CHECKFILES;
+		}
+		else
+		{
+			cl_mode = CL_ASKJOIN; // files need not be checked for the server.
+			*asksent = 0;
+		}
+
+		return true;
+	}
+
+	// Ask the info to the server (askinfo packet)
+	if (*asksent + NEWTICRATE < I_GetTime())
+	{
+		SendAskInfo(servernode);
+		*asksent = I_GetTime();
+	}
+
+	return true;
+}
+
+/** Called by CL_ConnectToServer
+  *
+  * \param tmpsave The name of the gamestate file???
+  * \param oldtic Used for knowing when to poll events and redraw
+  * \param asksent ???
+  * \return False if the connection was aborted
+  * \sa CL_ServerConnectionSearchTicker
+  * \sa CL_ConnectToServer
+  *
+  */
+static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic_t *asksent)
+{
+	boolean waitmore;
+
+	switch (cl_mode)
+	{
+		case CL_SEARCHING:
+			if (!CL_ServerConnectionSearchTicker(asksent))
+				return false;
+			break;
+
+		case CL_ASKFULLFILELIST:
+			if (cl_lastcheckedfilecount == UINT16_MAX) // All files retrieved
+				cl_mode = CL_CHECKFILES;
+			else if (fileneedednum != cl_lastcheckedfilecount || I_GetTime() >= *asksent)
+			{
+				if (CL_AskFileList(fileneedednum))
+				{
+					cl_lastcheckedfilecount = fileneedednum;
+					*asksent = I_GetTime() + NEWTICRATE;
+				}
+			}
+			break;
+		case CL_CHECKFILES:
+			if (!CL_FinishedFileList())
+				return false;
+			break;
+		case CL_DOWNLOADFILES:
+			waitmore = false;
+			for (INT32 i = 0; i < fileneedednum; i++)
+				if (fileneeded[i].status == FS_DOWNLOADING
+					|| fileneeded[i].status == FS_REQUESTED)
+				{
+					waitmore = true;
+					break;
+				}
+			if (waitmore)
+				break; // exit the case
+
+			Snake_Free(&snake);
+
+			cl_mode = CL_LOADFILES;
+			break;
+		case CL_LOADFILES:
+			if (CL_LoadServerFiles())
+			{
+				FreeFileNeeded();
+				*asksent = 0; // This ensures the first join request is right away
+				firstconnectattempttime = I_GetTime();
+				cl_mode = CL_ASKJOIN;
+			}
+			break;
+		case CL_ASKJOIN:
+			if (firstconnectattempttime + NEWTICRATE*300 < I_GetTime() && !server)
+			{
+				CONS_Printf(M_GetText("5 minute wait time exceeded.\n"));
+				CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
+				D_QuitNetGame();
+				CL_Reset();
+				D_StartTitle();
+				M_StartMessage(M_GetText(
+					"5 minute wait time exceeded.\n"
+					"You may retry connection.\n"
+					"\n"
+					"Press ESC\n"
+				), NULL, MM_NOTHING);
+				return false;
+			}
+
+			// Prepare structures to save the file
+			CL_PrepareDownloadSaveGame(tmpsave);
+
+			if (I_GetTime() >= *asksent && CL_SendJoin())
+			{
+				*asksent = I_GetTime() + NEWTICRATE*3;
+				cl_mode = CL_WAITJOINRESPONSE;
+			}
+			break;
+		case CL_WAITJOINRESPONSE:
+			if (I_GetTime() >= *asksent)
+			{
+				cl_mode = CL_ASKJOIN;
+			}
+			break;
+		case CL_DOWNLOADSAVEGAME:
+			// At this state, the first (and only) needed file is the gamestate
+			if (fileneeded[0].status == FS_FOUND)
+			{
+				// Gamestate is now handled within CL_LoadReceivedSavegame()
+				CL_LoadReceivedSavegame(false);
+				cl_mode = CL_CONNECTED;
+			} // don't break case continue to CL_CONNECTED
+			else
+				break;
+
+		case CL_CONNECTED:
+		case CL_CONFIRMCONNECT: //logic is handled by M_ConfirmConnect
+		default:
+			break;
+
+		// Connection closed by cancel, timeout or refusal.
+		case CL_ABORTED:
+			cl_mode = CL_SEARCHING;
+			return false;
+	}
+
+	GetPackets();
+	Net_AckTicker();
+
+	// Call it only once by tic
+	if (*oldtic != I_GetTime())
+	{
+		I_OsPolling();
+
+		if (cl_mode == CL_CONFIRMCONNECT)
+			D_ProcessEvents(); //needed for menu system to receive inputs
+		else
+		{
+			// my hand has been forced and I am dearly sorry for this awful hack :vomit:
+			for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
+			{
+				if (!Snake_JoyGrabber(snake, &events[eventtail]))
+					G_MapEventsToControls(&events[eventtail]);
+			}
+		}
+
+		if (gamekeydown[KEY_ESCAPE] || gamekeydown[KEY_JOY1+1] || cl_mode == CL_ABORTED)
+		{
+			CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
+			M_StartMessage(M_GetText("Network game synchronization aborted.\n\nPress ESC\n"), NULL, MM_NOTHING);
+
+			Snake_Free(&snake);
+
+			D_QuitNetGame();
+			CL_Reset();
+			D_StartTitle();
+			memset(gamekeydown, 0, NUMKEYS);
+			return false;
+		}
+		else if (cl_mode == CL_DOWNLOADFILES && snake)
+			Snake_Update(snake);
+
+		if (client && (cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADSAVEGAME))
+			FileReceiveTicker();
+
+		*oldtic = I_GetTime();
+
+		if (client && cl_mode != CL_CONNECTED && cl_mode != CL_ABORTED)
+		{
+			if (!snake)
+			{
+				F_MenuPresTicker(); // title sky
+				F_TitleScreenTicker(true);
+				F_TitleScreenDrawer();
+			}
+			CL_DrawConnectionStatus();
+#ifdef HAVE_THREADS
+			I_lock_mutex(&m_menu_mutex);
+#endif
+			M_Drawer(); //Needed for drawing messageboxes on the connection screen
+#ifdef HAVE_THREADS
+			I_unlock_mutex(m_menu_mutex);
+#endif
+			I_UpdateNoVsync(); // page flip or blit buffer
+			if (moviemode)
+				M_SaveFrame();
+			S_UpdateSounds();
+			S_UpdateClosedCaptions();
+		}
+	}
+	else
+	{
+		I_Sleep(cv_sleep.value);
+		I_UpdateTime(cv_timescale.value);
+	}
+
+	return true;
+}
+
+#define TMPSAVENAME "$$$.sav"
+
+void CL_ConnectToServer(void)
+{
+	INT32 pnumnodes, nodewaited = doomcom->numnodes, i;
+	tic_t oldtic;
+	tic_t asksent;
+	char tmpsave[256];
+
+	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
+
+	lastfilenum = -1;
+
+	cl_mode = CL_SEARCHING;
+
+	// Don't get a corrupt savegame error because tmpsave already exists
+	if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1)
+		I_Error("Can't delete %s\n", tmpsave);
+
+	if (netgame)
+	{
+		if (servernode < 0 || servernode >= MAXNETNODES)
+			CONS_Printf(M_GetText("Searching for a server...\n"));
+		else
+			CONS_Printf(M_GetText("Contacting the server...\n"));
+	}
+
+	if (gamestate == GS_INTERMISSION)
+		Y_EndIntermission(); // clean up intermission graphics etc
+
+	DEBFILE(va("waiting %d nodes\n", doomcom->numnodes));
+	G_SetGamestate(GS_WAITINGPLAYERS);
+	wipegamestate = GS_WAITINGPLAYERS;
+
+	ClearAdminPlayers();
+	pnumnodes = 1;
+	oldtic = I_GetTime() - 1;
+
+	asksent = (tic_t) - TICRATE;
+	firstconnectattempttime = I_GetTime();
+
+	i = SL_SearchServer(servernode);
+
+	if (i != -1)
+	{
+		char *gametypestr = serverlist[i].info.gametypename;
+		CONS_Printf(M_GetText("Connecting to: %s\n"), serverlist[i].info.servername);
+		gametypestr[sizeof serverlist[i].info.gametypename - 1] = '\0';
+		CONS_Printf(M_GetText("Gametype: %s\n"), gametypestr);
+		CONS_Printf(M_GetText("Version: %d.%d.%u\n"), serverlist[i].info.version/100,
+		 serverlist[i].info.version%100, serverlist[i].info.subversion);
+	}
+	SL_ClearServerList(servernode);
+
+	do
+	{
+		// If the connection was aborted for some reason, leave
+		if (!CL_ServerConnectionTicker(tmpsave, &oldtic, &asksent))
+			return;
+
+		if (server)
+		{
+			pnumnodes = 0;
+			for (i = 0; i < MAXNETNODES; i++)
+				if (netnodes[i].ingame)
+					pnumnodes++;
+		}
+	}
+	while (!(cl_mode == CL_CONNECTED && (client || (server && nodewaited <= pnumnodes))));
+
+	if (netgame)
+		F_StartWaitingPlayers();
+
+	DEBFILE(va("Synchronisation Finished\n"));
+
+	displayplayer = consoleplayer;
+}
+
+/** Called when a PT_SERVERINFO packet is received
+  *
+  * \param node The packet sender
+  * \note What happens if the packet comes from a client or something like that?
+  *
+  */
+void PT_ServerInfo(SINT8 node)
+{
+	// compute ping in ms
+	const tic_t ticnow = I_GetTime();
+	const tic_t ticthen = (tic_t)LONG(netbuffer->u.serverinfo.time);
+	const tic_t ticdiff = (ticnow - ticthen)*1000/NEWTICRATE;
+	netbuffer->u.serverinfo.time = (tic_t)LONG(ticdiff);
+	netbuffer->u.serverinfo.servername[MAXSERVERNAME-1] = 0;
+	netbuffer->u.serverinfo.application
+		[sizeof netbuffer->u.serverinfo.application - 1] = '\0';
+	netbuffer->u.serverinfo.gametypename
+		[sizeof netbuffer->u.serverinfo.gametypename - 1] = '\0';
+
+	SL_InsertServer(&netbuffer->u.serverinfo, node);
+}
+
+// Helper function for packets that should only be sent by the server
+// If it is NOT from the server, bail out and close the connection!
+static boolean ServerOnly(SINT8 node)
+{
+	if (node == servernode)
+		return false;
+
+	Net_CloseConnection(node);
+	return true;
+}
+
+void PT_MoreFilesNeeded(SINT8 node)
+{
+	if (server && serverrunning)
+	{ // But wait I thought I'm the server?
+		Net_CloseConnection(node);
+		return;
+	}
+	if (ServerOnly(node))
+		return;
+	if (cl_mode == CL_ASKFULLFILELIST && netbuffer->u.filesneededcfg.first == fileneedednum)
+	{
+		D_ParseFileneeded(netbuffer->u.filesneededcfg.num, netbuffer->u.filesneededcfg.files, netbuffer->u.filesneededcfg.first);
+		if (!netbuffer->u.filesneededcfg.more)
+			cl_lastcheckedfilecount = UINT16_MAX; // Got the whole file list
+	}
+}
+
+// Negative response of client join request
+void PT_ServerRefuse(SINT8 node)
+{
+	if (server && serverrunning)
+	{ // But wait I thought I'm the server?
+		Net_CloseConnection(node);
+		return;
+	}
+	if (ServerOnly(node))
+		return;
+	if (cl_mode == CL_WAITJOINRESPONSE)
+	{
+		// Save the reason so it can be displayed after quitting the netgame
+		char *reason = strdup(netbuffer->u.serverrefuse.reason);
+		if (!reason)
+			I_Error("Out of memory!\n");
+
+		if (strstr(reason, "Maximum players reached"))
+		{
+			serverisfull = true;
+			//Special timeout for when refusing due to player cap. The client will wait 3 seconds between join requests when waiting for a slot, so we need this to be much longer
+			//We set it back to the value of cv_nettimeout.value in CL_Reset
+			connectiontimeout = NEWTICRATE*7;
+			cl_mode = CL_ASKJOIN;
+			free(reason);
+			return;
+		}
+
+		M_StartMessage(va(M_GetText("Server refuses connection\n\nReason:\n%s"),
+			reason), NULL, MM_NOTHING);
+
+		D_QuitNetGame();
+		CL_Reset();
+		D_StartTitle();
+
+		free(reason);
+
+		// Will be reset by caller. Signals refusal.
+		cl_mode = CL_ABORTED;
+	}
+}
+
+// Positive response of client join request
+void PT_ServerCFG(SINT8 node)
+{
+	if (server && serverrunning && node != servernode)
+	{ // but wait I thought I'm the server?
+		Net_CloseConnection(node);
+		return;
+	}
+	if (ServerOnly(node))
+		return;
+	/// \note how would this happen? and is it doing the right thing if it does?
+	if (cl_mode != CL_WAITJOINRESPONSE)
+		return;
+
+	if (client)
+	{
+		maketic = gametic = neededtic = (tic_t)LONG(netbuffer->u.servercfg.gametic);
+		G_SetGametype(netbuffer->u.servercfg.gametype);
+		modifiedgame = netbuffer->u.servercfg.modifiedgame;
+		if (netbuffer->u.servercfg.usedCheats)
+			G_SetUsedCheats(true);
+		memcpy(server_context, netbuffer->u.servercfg.server_context, 8);
+	}
+
+	netnodes[(UINT8)servernode].ingame = true;
+	serverplayer = netbuffer->u.servercfg.serverplayer;
+	doomcom->numslots = SHORT(netbuffer->u.servercfg.totalslotnum);
+	mynode = netbuffer->u.servercfg.clientnode;
+	if (serverplayer >= 0)
+		playernode[(UINT8)serverplayer] = servernode;
+
+	if (netgame)
+		CONS_Printf(M_GetText("Join accepted, waiting for complete game state...\n"));
+	DEBFILE(va("Server accept join gametic=%u mynode=%d\n", gametic, mynode));
+
+	/// \note Wait. What if a Lua script uses some global custom variables synched with the NetVars hook?
+	///       Shouldn't they be downloaded even at intermission time?
+	///       Also, according to PT_ClientJoin, the server will send the savegame even during intermission...
+	if (netbuffer->u.servercfg.gamestate == GS_LEVEL/* ||
+		netbuffer->u.servercfg.gamestate == GS_INTERMISSION*/)
+		cl_mode = CL_DOWNLOADSAVEGAME;
+	else
+		cl_mode = CL_CONNECTED;
+}
diff --git a/src/netcode/client_connection.h b/src/netcode/client_connection.h
new file mode 100644
index 0000000000000000000000000000000000000000..4d75160d4a280061f08410b1f834df049a1b2a3e
--- /dev/null
+++ b/src/netcode/client_connection.h
@@ -0,0 +1,61 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  client_connection.h
+/// \brief Client connection handling
+
+#ifndef __D_CLIENT_CONNECTION__
+#define __D_CLIENT_CONNECTION__
+
+#include "../doomtype.h"
+#include "d_clisrv.h"
+
+#define MAXSERVERLIST (MAXNETNODES-1)
+
+typedef struct
+{
+	SINT8 node;
+	serverinfo_pak info;
+} serverelem_t;
+
+typedef enum
+{
+	CL_SEARCHING,
+	CL_CHECKFILES,
+	CL_DOWNLOADFILES,
+	CL_ASKJOIN,
+	CL_LOADFILES,
+	CL_WAITJOINRESPONSE,
+	CL_DOWNLOADSAVEGAME,
+	CL_CONNECTED,
+	CL_ABORTED,
+	CL_ASKFULLFILELIST,
+	CL_CONFIRMCONNECT
+} cl_mode_t;
+
+extern serverelem_t serverlist[MAXSERVERLIST];
+extern UINT32 serverlistcount;
+
+extern cl_mode_t cl_mode;
+extern boolean serverisfull; //lets us be aware if the server was full after we check files, but before downloading, so we can ask if the user still wants to download or not
+extern tic_t firstconnectattempttime;
+extern UINT8 mynode; // my address pointofview server
+
+void CL_QueryServerList(msg_server_t *list);
+void CL_UpdateServerList(boolean internetsearch, INT32 room);
+
+void CL_ConnectToServer(void);
+boolean CL_SendJoin(void);
+
+void PT_ServerInfo(SINT8 node);
+void PT_MoreFilesNeeded(SINT8 node);
+void PT_ServerRefuse(SINT8 node);
+void PT_ServerCFG(SINT8 node);
+
+#endif
diff --git a/src/netcode/commands.c b/src/netcode/commands.c
new file mode 100644
index 0000000000000000000000000000000000000000..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 93%
rename from src/d_net.c
rename to src/netcode/d_net.c
index 6d8c72942bbe8cd6714db4c6761a17e13f70bdef..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",
@@ -921,7 +872,6 @@ void Command_Drop(void)
 {
 	INT32 packetquantity;
 	const char *packetname;
-	size_t i;
 
 	if (COM_Argc() < 2)
 	{
@@ -951,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;
@@ -986,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
@@ -1028,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;
@@ -1093,8 +1036,6 @@ boolean HSendPacket(INT32 node, boolean reliable, UINT8 acknum, size_t packetlen
 	}
 #endif
 
-#endif // ndef NONET
-
 	return true;
 }
 
@@ -1112,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
@@ -1128,8 +1066,6 @@ boolean HGetPacket(void)
 	if (!netgame)
 		return false;
 
-#ifndef NONET
-
 	while(true)
 	{
 		//nodejustjoined = I_NetGet();
@@ -1189,7 +1125,6 @@ boolean HGetPacket(void)
 		}
 		break;
 	}
-#endif // ndef NONET
 
 	return true;
 }
@@ -1399,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;
@@ -1425,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,
@@ -1441,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 95%
rename from src/d_netcmd.c
rename to src/netcode/d_netcmd.c
index 33281e9924c8c73c7acf3638ac6c90968b2bd316..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 "u_list.h"
+#include "../z_zone.h"
+#include "../lua_script.h"
+#include "../lua_hook.h"
+#include "../m_cond.h"
+#include "../m_anigif.h"
+#include "../md5.h"
+#include "../m_perfstats.h"
+#include "../u_list.h"
 
 #ifdef NETGAME_DEVMODE
 #define CV_RESTRICT CV_NETVAR
@@ -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,13 +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);
 	CV_RegisterVar(&cv_dedicatedidletime);
-#endif
 
 	COM_AddCommand("ping", Command_Ping_f, COM_LUA);
 	CV_RegisterVar(&cv_nettimeout);
@@ -776,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);
@@ -1203,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
 //
@@ -1233,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)
 	{
@@ -1272,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);
+		SetColorLocal(consoleplayer, cv_playercolor.value);
 
-			SetPlayerSkin(consoleplayer, cv_skin.string);
-			CV_StealthSet(&cv_skin, skins[cv_skin.value].name);
-
-			/*if (notsame)
-			{
-				CV_StealthSetValue(&cv_playercolor, skins[cv_skin.value].prefcolor);
-
-				players[consoleplayer].skincolor = cv_playercolor.value % numskincolors;
-
-				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;
 	}
 
@@ -1332,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);
@@ -1367,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)
@@ -1398,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;
+		SetColorLocal(secondplaya, cv_playercolor2.value);
 
-			//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);
-
-				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;
 	}
 
@@ -1498,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]))
@@ -1507,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;
@@ -1556,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);
 }
@@ -1841,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;
 		}
@@ -2175,7 +2074,6 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
 	{
 		SetPlayerSkinByNum(0, cv_chooseskin.value-1);
 		players[0].skincolor = skins[players[0].skin].prefcolor;
-		CV_StealthSetValue(&cv_playercolor, players[0].skincolor);
 	}
 
 	mapnumber = M_MapNumber(mapname[3], mapname[4]);
@@ -2929,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();
@@ -4818,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);
 	}
 }
 
@@ -4836,7 +4728,6 @@ static void Name_OnChange(void)
 	}
 	else
 		SendNameAndColor();
-
 }
 
 static void Name2_OnChange(void)
@@ -4859,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]);
 	}
 }
 
@@ -4885,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]);
 	}
 }
 
@@ -4900,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 8bbc801d0ef700868482fd982346dff5296c5e14..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;
diff --git a/src/d_netfil.c b/src/netcode/d_netfil.c
similarity index 93%
rename from src/d_netfil.c
rename to src/netcode/d_netfil.c
index 3fef7568128f34bda6d8716cf5d227a19495b9de..c5ddef7b6563f22082c09e3b161acb3f1ff59f57 100644
--- a/src/d_netfil.c
+++ b/src/netcode/d_netfil.c
@@ -31,24 +31,25 @@
 #include <sys/utime.h>
 #endif
 
-#include "doomdef.h"
-#include "doomstat.h"
-#include "d_main.h"
-#include "g_game.h"
-#include "i_time.h"
+#include "../doomdef.h"
+#include "../doomstat.h"
+#include "../d_main.h"
+#include "../g_game.h"
+#include "../i_time.h"
 #include "i_net.h"
-#include "i_system.h"
-#include "m_argv.h"
+#include "../i_system.h"
+#include "../m_argv.h"
 #include "d_net.h"
-#include "w_wad.h"
+#include "../w_wad.h"
 #include "d_netfil.h"
-#include "z_zone.h"
-#include "byteptr.h"
-#include "p_setup.h"
-#include "m_misc.h"
-#include "m_menu.h"
-#include "md5.h"
-#include "filesrch.h"
+#include "net_command.h"
+#include "../z_zone.h"
+#include "../byteptr.h"
+#include "../p_setup.h"
+#include "../m_misc.h"
+#include "../m_menu.h"
+#include "../md5.h"
+#include "../filesrch.h"
 
 #include <errno.h>
 
@@ -103,26 +104,31 @@ typedef struct
 } pauseddownload_t;
 static pauseddownload_t *pauseddownload = NULL;
 
-#ifndef NONET
 // for cl loading screen
 INT32 lastfilenum = -1;
 INT32 downloadcompletednum = 0;
 UINT32 downloadcompletedsize = 0;
 INT32 totalfilesrequestednum = 0;
 UINT32 totalfilesrequestedsize = 0;
-#endif
 
 luafiletransfer_t *luafiletransfers = NULL;
 boolean waitingforluafiletransfer = false;
 boolean waitingforluafilecommand = false;
 char luafiledir[256 + 16] = "luafiles";
 
+// max file size to send to a player (in kilobytes)
+static CV_PossibleValue_t maxsend_cons_t[] = {{0, "MIN"}, {204800, "MAX"}, {0, NULL}};
+consvar_t cv_maxsend = CVAR_INIT ("maxsend", "4096", CV_SAVE|CV_NETVAR, maxsend_cons_t, NULL);
+
+consvar_t cv_noticedownload = CVAR_INIT ("noticedownload", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
+
+// Speed of file downloading (in packets per tic)
+static CV_PossibleValue_t downloadspeed_cons_t[] = {{1, "MIN"}, {300, "MAX"}, {0, NULL}};
+consvar_t cv_downloadspeed = CVAR_INIT ("downloadspeed", "16", CV_SAVE|CV_NETVAR, downloadspeed_cons_t, NULL);
 
 static UINT16 GetWadNumFromFileNeededId(UINT8 id)
 {
-	UINT16 wadnum;
-
-	for (wadnum = mainwads; wadnum < numwadfiles; wadnum++)
+	for (UINT16 wadnum = mainwads; wadnum < numwadfiles; wadnum++)
 	{
 		if (!wadfiles[wadnum]->important)
 			continue;
@@ -142,14 +148,13 @@ static UINT16 GetWadNumFromFileNeededId(UINT8 id)
   */
 UINT8 *PutFileNeeded(UINT16 firstfile)
 {
-	size_t i;
 	UINT8 count = 0;
 	UINT8 *p_start = netbuffer->packettype == PT_MOREFILESNEEDED ? netbuffer->u.filesneededcfg.files : netbuffer->u.serverinfo.fileneeded;
 	UINT8 *p = p_start;
 	char wadfilename[MAX_WADPATH] = "";
 	UINT8 filestatus, folder;
 
-	for (i = mainwads; i < numwadfiles; i++) //mainwads, otherwise we start on the first mainwad
+	for (size_t i = mainwads; i < numwadfiles; i++) //mainwads, otherwise we start on the first mainwad
 	{
 		// If it has only music/sound lumps, don't put it in the list
 		if (!wadfiles[i]->important)
@@ -224,7 +229,6 @@ void FreeFileNeeded(void)
   */
 void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 firstfile)
 {
-	INT32 i;
 	UINT8 *p;
 	UINT8 filestatus;
 
@@ -233,7 +237,7 @@ void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 fi
 
 	AllocFileNeeded(fileneedednum);
 
-	for (i = firstfile; i < fileneedednum; i++)
+	for (INT32 i = firstfile; i < fileneedednum; i++)
 	{
 		fileneeded[i].type = FILENEEDED_WAD;
 		fileneeded[i].status = FS_NOTCHECKED; // We haven't even started looking for the file yet
@@ -250,9 +254,7 @@ void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 fi
 
 void CL_PrepareDownloadSaveGame(const char *tmpsave)
 {
-#ifndef NONET
 	lastfilenum = -1;
-#endif
 
 	FreeFileNeeded();
 	AllocFileNeeded(1);
@@ -275,9 +277,9 @@ void CL_PrepareDownloadSaveGame(const char *tmpsave)
   */
 boolean CL_CheckDownloadable(void)
 {
-	UINT8 i,dlstatus = 0;
+	UINT8 dlstatus = 0;
 
-	for (i = 0; i < fileneedednum; i++)
+	for (UINT8 i = 0; i < fileneedednum; i++)
 		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN)
 		{
 			if (fileneeded[i].willsend == 1)
@@ -298,7 +300,7 @@ boolean CL_CheckDownloadable(void)
 
 	// not downloadable, put reason in console
 	CONS_Alert(CONS_NOTICE, M_GetText("You need additional files to connect to this server:\n"));
-	for (i = 0; i < fileneedednum; i++)
+	for (UINT8 i = 0; i < fileneedednum; i++)
 		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN)
 		{
 			CONS_Printf(" * \"%s\" (%dK)", fileneeded[i].filename, fileneeded[i].totalsize >> 10);
@@ -368,14 +370,13 @@ void CL_AbortDownloadResume(void)
 boolean CL_SendFileRequest(void)
 {
 	char *p;
-	INT32 i;
 	INT64 totalfreespaceneeded = 0, availablefreespace;
 
 #ifdef PARANOIA
 	if (M_CheckParm("-nodownload"))
 		I_Error("Attempted to download files in -nodownload mode");
 
-	for (i = 0; i < fileneedednum; i++)
+	for (INT32 i = 0; i < fileneedednum; i++)
 		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN
 			&& (fileneeded[i].willsend == 0 || fileneeded[i].willsend == 2))
 		{
@@ -385,7 +386,7 @@ boolean CL_SendFileRequest(void)
 
 	netbuffer->packettype = PT_REQUESTFILE;
 	p = (char *)netbuffer->u.textcmd;
-	for (i = 0; i < fileneedednum; i++)
+	for (INT32 i = 0; i < fileneedednum; i++)
 		if ((fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD))
 		{
 			totalfreespaceneeded += fileneeded[i].totalsize;
@@ -413,26 +414,31 @@ boolean CL_SendFileRequest(void)
 }
 
 // get request filepak and put it on the send queue
-// returns false if a requested file was not found or cannot be sent
-boolean PT_RequestFile(INT32 node)
+void PT_RequestFile(SINT8 node)
 {
 	UINT8 *p = netbuffer->u.textcmd;
-	UINT8 id;
+
+	if (client || !cv_downloading.value)
+	{
+		Net_CloseConnection(node); // close connection if you are not the server or disabled downloading
+		return;
+	}
 
 	while (p < netbuffer->u.textcmd + MAXTEXTCMD-1) // Don't allow hacked client to overflow
 	{
-		id = READUINT8(p);
+		UINT8 id = READUINT8(p);
 		if (id == 0xFF)
 			break;
 
 		if (!AddFileToSendQueue(node, id))
 		{
 			SV_AbortSendFiles(node);
-			return false; // don't read the rest of the files
+			Net_CloseConnection(node); // close connection if one of the requested files could not be sent
+			return; // don't read the rest of the files
 		}
 	}
 
-	return true; // no problems with any files
+	return; // no problems with any files
 }
 
 /** Checks if the files needed aren't already loaded or on the disk
@@ -531,9 +537,7 @@ INT32 CL_CheckFiles(void)
 // Load it now
 boolean CL_LoadServerFiles(void)
 {
-	INT32 i;
-
-	for (i = 0; i < fileneedednum; i++)
+	for (INT32 i = 0; i < fileneedednum; i++)
 	{
 		if (fileneeded[i].status == FS_OPEN)
 			continue; // Already loaded
@@ -629,11 +633,10 @@ void AddLuaFileTransfer(const char *filename, const char *mode)
 
 static void SV_PrepareSendLuaFileToNextNode(void)
 {
-	INT32 i;
 	UINT8 success = 1;
 
     // Find a client to send the file to
-	for (i = 1; i < MAXNETNODES; i++)
+	for (INT32 i = 1; i < MAXNETNODES; i++)
 		if (luafiletransfers->nodestatus[i] == LFTNS_WAITING) // Node waiting
 		{
 			// Tell the client we're about to send them the file
@@ -655,13 +658,12 @@ static void SV_PrepareSendLuaFileToNextNode(void)
 void SV_PrepareSendLuaFile(void)
 {
 	char *binfilename;
-	INT32 i;
 
 	luafiletransfers->ongoing = true;
 
 	// Set status to "waiting" for everyone
-	for (i = 0; i < MAXNETNODES; i++)
-		luafiletransfers->nodestatus[i] = (nodeingame[i] ? LFTNS_WAITING : LFTNS_NONE);
+	for (INT32 i = 0; i < MAXNETNODES; i++)
+		luafiletransfers->nodestatus[i] = (netnodes[i].ingame ? LFTNS_WAITING : LFTNS_NONE);
 
 	if (FIL_ReadFileOK(luafiletransfers->realfilename))
 	{
@@ -1137,12 +1139,13 @@ void FileSendTicker(void)
 	}
 }
 
-void PT_FileAck(void)
+void PT_FileAck(SINT8 node)
 {
 	fileack_pak *packet = &netbuffer->u.fileack;
-	INT32 node = doomcom->remotenode;
 	filetran_t *trans = &transfer[node];
-	INT32 i, j;
+
+	if (client)
+		return;
 
 	// Wrong file id? Ignore it, it's probably a late packet
 	if (!(trans->txlist && packet->fileid == trans->txlist->fileid))
@@ -1161,11 +1164,11 @@ void PT_FileAck(void)
 			trans->dontsenduntil = 0;
 	}
 
-	for (i = 0; i < packet->numsegments; i++)
+	for (INT32 i = 0; i < packet->numsegments; i++)
 	{
 		fileacksegment_t *segment = &packet->segments[i];
 
-		for (j = 0; j < 32; j++)
+		for (INT32 j = 0; j < 32; j++)
 			if (LONG(segment->acks) & (1 << j))
 			{
 				if (LONG(segment->start) * FILEFRAGMENTSIZE >= trans->txlist->size)
@@ -1190,24 +1193,23 @@ void PT_FileAck(void)
 	}
 }
 
-void PT_FileReceived(void)
+void PT_FileReceived(SINT8 node)
 {
-	filetx_t *trans = transfer[doomcom->remotenode].txlist;
+	filetx_t *trans = transfer[node].txlist;
 
-	if (trans && netbuffer->u.filereceived == trans->fileid)
-		SV_EndFileSend(doomcom->remotenode);
+	if (server && trans && netbuffer->u.filereceived == trans->fileid)
+		SV_EndFileSend(node);
 }
 
 static void SendAckPacket(fileack_pak *packet, UINT8 fileid)
 {
 	size_t packetsize;
-	INT32 i;
 
 	packetsize = sizeof(*packet) + packet->numsegments * sizeof(*packet->segments);
 
 	// Finalise the packet
 	packet->fileid = fileid;
-	for (i = 0; i < packet->numsegments; i++)
+	for (INT32 i = 0; i < packet->numsegments; i++)
 	{
 		packet->segments[i].start = LONG(packet->segments[i].start);
 		packet->segments[i].acks = LONG(packet->segments[i].acks);
@@ -1247,9 +1249,7 @@ static void AddFragmentToAckPacket(fileack_pak *packet, UINT8 iteration, UINT32
 
 void FileReceiveTicker(void)
 {
-	INT32 i;
-
-	for (i = 0; i < fileneedednum; i++)
+	for (INT32 i = 0; i < fileneedednum; i++)
 	{
 		fileneeded_t *file = &fileneeded[i];
 
@@ -1263,8 +1263,7 @@ void FileReceiveTicker(void)
 			if (file->ackresendposition != UINT32_MAX && file->status == FS_DOWNLOADING)
 			{
 				// Acknowledge ~70 MB/s, whichs means the client sends ~18 KB/s
-				INT32 j;
-				for (j = 0; j < 2048; j++)
+				for (INT32 j = 0; j < 2048; j++)
 				{
 					if (file->receivedfragments[file->ackresendposition])
 						AddFragmentToAckPacket(file->ackpacket, file->iteration, file->ackresendposition, i);
@@ -1281,8 +1280,27 @@ void FileReceiveTicker(void)
 	}
 }
 
-void PT_FileFragment(void)
+void PT_FileFragment(SINT8 node, INT32 netconsole)
 {
+	if (netnodes[node].ingame)
+	{
+		// Only accept PT_FILEFRAGMENT from the server.
+		if (node != servernode)
+		{
+			CONS_Alert(CONS_WARNING, M_GetText("%s received from non-host %d\n"), "PT_FILEFRAGMENT", node);
+			if (server)
+				SendKick(netconsole, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
+			return;
+		}
+		if (server)
+			return;
+	}
+	else if (server || node != servernode)
+	{
+		Net_CloseConnection(node);
+		return;
+	}
+
 	INT32 filenum = netbuffer->u.filetxpak.fileid;
 	fileneeded_t *file = &fileneeded[filenum];
 	UINT32 fragmentpos = LONG(netbuffer->u.filetxpak.position);
@@ -1439,9 +1457,7 @@ void PT_FileFragment(void)
 		I_Error("Received a file not requested (file id: %d, file status: %s)\n", filenum, s);
 	}
 
-#ifndef NONET
 	lastfilenum = filenum;
-#endif
 }
 
 /** \brief Checks if a node is downloading a file
@@ -1469,15 +1485,14 @@ void SV_AbortSendFiles(INT32 node)
 
 void CloseNetFile(void)
 {
-	INT32 i;
 	// Is sending?
-	for (i = 0; i < MAXNETNODES; i++)
+	for (INT32 i = 0; i < MAXNETNODES; i++)
 		SV_AbortSendFiles(i);
 
 	// Receiving a file?
 	if (fileneeded)
 	{
-		for (i = 0; i < fileneedednum; i++)
+		for (INT32 i = 0; i < fileneedednum; i++)
 			if (fileneeded[i].status == FS_DOWNLOADING && fileneeded[i].file)
 			{
 				fclose(fileneeded[i].file);
@@ -1510,9 +1525,7 @@ void CloseNetFile(void)
 
 void Command_Downloads_f(void)
 {
-	INT32 node;
-
-	for (node = 0; node < MAXNETNODES; node++)
+	for (INT32 node = 0; node < MAXNETNODES; node++)
 		if (transfer[node].txlist
 		&& transfer[node].txlist->ram == SF_FILE) // Node is downloading a file?
 		{
@@ -1546,14 +1559,11 @@ void Command_Downloads_f(void)
 
 void nameonly(char *s)
 {
-	size_t j, len;
-	void *ns;
-
-	for (j = strlen(s); j != (size_t)-1; j--)
+	for (size_t j = strlen(s); j != (size_t)-1; j--)
 		if ((s[j] == '\\') || (s[j] == ':') || (s[j] == '/'))
 		{
-			ns = &(s[j+1]);
-			len = strlen(ns);
+			void *ns = &(s[j+1]);
+			size_t len = strlen(ns);
 #if 0
 			M_Memcpy(s, ns, len+1);
 #else
@@ -1566,9 +1576,9 @@ void nameonly(char *s)
 // Returns the length in characters of the last element of a path.
 size_t nameonlylength(const char *s)
 {
-	size_t j, len = strlen(s);
+	size_t len = strlen(s);
 
-	for (j = len; j != (size_t)-1; j--)
+	for (size_t j = len; j != (size_t)-1; j--)
 		if ((s[j] == '\\') || (s[j] == ':') || (s[j] == '/'))
 			return len - j - 1;
 
diff --git a/src/d_netfil.h b/src/netcode/d_netfil.h
similarity index 94%
rename from src/d_netfil.h
rename to src/netcode/d_netfil.h
index ecec976be8dca97d8874aaa88d2279d7e4c7aaae..fdbec8c5396fa1619efccfa6f200fbd7a17903c6 100644
--- a/src/d_netfil.h
+++ b/src/netcode/d_netfil.h
@@ -15,7 +15,7 @@
 
 #include "d_net.h"
 #include "d_clisrv.h"
-#include "w_wad.h"
+#include "../w_wad.h"
 
 typedef enum
 {
@@ -70,13 +70,13 @@ extern INT32 fileneedednum;
 extern fileneeded_t *fileneeded;
 extern char downloaddir[512];
 
-#ifndef NONET
 extern INT32 lastfilenum;
 extern INT32 downloadcompletednum;
 extern UINT32 downloadcompletedsize;
 extern INT32 totalfilesrequestednum;
 extern UINT32 totalfilesrequestedsize;
-#endif
+
+extern consvar_t cv_maxsend, cv_noticedownload, cv_downloadspeed;
 
 void AllocFileNeeded(INT32 size);
 void FreeFileNeeded(void);
@@ -90,16 +90,16 @@ void AddRamToSendQueue(INT32 node, void *data, size_t size, freemethod_t freemet
 	UINT8 fileid);
 
 void FileSendTicker(void);
-void PT_FileAck(void);
-void PT_FileReceived(void);
+void PT_FileAck(SINT8 node);
+void PT_FileReceived(SINT8 node);
 boolean SendingFile(INT32 node);
 
 void FileReceiveTicker(void);
-void PT_FileFragment(void);
+void PT_FileFragment(SINT8 node, INT32 netconsole);
 
 boolean CL_CheckDownloadable(void);
 boolean CL_SendFileRequest(void);
-boolean PT_RequestFile(INT32 node);
+void PT_RequestFile(SINT8 node);
 
 typedef enum
 {
diff --git a/src/netcode/gamestate.c b/src/netcode/gamestate.c
new file mode 100644
index 0000000000000000000000000000000000000000..f36347c6d88e94a444fb186937d2672cfe3a0df9
--- /dev/null
+++ b/src/netcode/gamestate.c
@@ -0,0 +1,336 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  gamestate.c
+/// \brief Gamestate (re)sending
+
+#include "d_clisrv.h"
+#include "d_netfil.h"
+#include "gamestate.h"
+#include "i_net.h"
+#include "protocol.h"
+#include "server_connection.h"
+#include "../am_map.h"
+#include "../byteptr.h"
+#include "../console.h"
+#include "../d_main.h"
+#include "../doomstat.h"
+#include "../doomtype.h"
+#include "../f_finale.h"
+#include "../g_demo.h"
+#include "../g_game.h"
+#include "../i_time.h"
+#include "../lua_script.h"
+#include "../lzf.h"
+#include "../m_misc.h"
+#include "../p_local.h"
+#include "../p_saveg.h"
+#include "../r_main.h"
+#include "../tables.h"
+#include "../z_zone.h"
+#if defined (__GNUC__) || defined (__unix__)
+#include <unistd.h>
+#endif
+
+#define SAVEGAMESIZE (768*1024)
+
+UINT8 hu_redownloadinggamestate = 0;
+boolean cl_redownloadinggamestate = false;
+
+boolean SV_ResendingSavegameToAnyone(void)
+{
+	for (INT32 i = 0; i < MAXNETNODES; i++)
+		if (netnodes[i].resendingsavegame)
+			return true;
+	return false;
+}
+
+void SV_SendSaveGame(INT32 node, boolean resending)
+{
+	size_t length, compressedlen;
+	UINT8 *savebuffer;
+	UINT8 *compressedsave;
+	UINT8 *buffertosend;
+
+	// first save it in a malloced buffer
+	savebuffer = (UINT8 *)malloc(SAVEGAMESIZE);
+	if (!savebuffer)
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
+		return;
+	}
+
+	// Leave room for the uncompressed length.
+	save_p = savebuffer + sizeof(UINT32);
+
+	P_SaveNetGame(resending);
+
+	length = save_p - savebuffer;
+	if (length > SAVEGAMESIZE)
+	{
+		free(savebuffer);
+		save_p = NULL;
+		I_Error("Savegame buffer overrun");
+	}
+
+	// Allocate space for compressed save: one byte fewer than for the
+	// uncompressed data to ensure that the compression is worthwhile.
+	compressedsave = malloc(length - 1);
+	if (!compressedsave)
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
+		return;
+	}
+
+	// Attempt to compress it.
+	if((compressedlen = lzf_compress(savebuffer + sizeof(UINT32), length - sizeof(UINT32), compressedsave + sizeof(UINT32), length - sizeof(UINT32) - 1)))
+	{
+		// Compressing succeeded; send compressed data
+
+		free(savebuffer);
+
+		// State that we're compressed.
+		buffertosend = compressedsave;
+		WRITEUINT32(compressedsave, length - sizeof(UINT32));
+		length = compressedlen + sizeof(UINT32);
+	}
+	else
+	{
+		// Compression failed to make it smaller; send original
+
+		free(compressedsave);
+
+		// State that we're not compressed
+		buffertosend = savebuffer;
+		WRITEUINT32(savebuffer, 0);
+	}
+
+	AddRamToSendQueue(node, buffertosend, length, SF_RAM, 0);
+	save_p = NULL;
+
+	// Remember when we started sending the savegame so we can handle timeouts
+	netnodes[node].sendingsavegame = true;
+	netnodes[node].freezetimeout = I_GetTime() + jointimeout + length / 1024; // 1 extra tic for each kilobyte
+}
+
+#ifdef DUMPCONSISTENCY
+#define TMPSAVENAME "badmath.sav"
+static consvar_t cv_dumpconsistency = CVAR_INIT ("dumpconsistency", "Off", CV_SAVE|CV_NETVAR, CV_OnOff, NULL);
+
+void SV_SavedGame(void)
+{
+	size_t length;
+	UINT8 *savebuffer;
+	char tmpsave[256];
+
+	if (!cv_dumpconsistency.value)
+		return;
+
+	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
+
+	// first save it in a malloced buffer
+	save_p = savebuffer = (UINT8 *)malloc(SAVEGAMESIZE);
+	if (!save_p)
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
+		return;
+	}
+
+	P_SaveNetGame(false);
+
+	length = save_p - savebuffer;
+	if (length > SAVEGAMESIZE)
+	{
+		free(savebuffer);
+		save_p = NULL;
+		I_Error("Savegame buffer overrun");
+	}
+
+	// then save it!
+	if (!FIL_WriteFile(tmpsave, savebuffer, length))
+		CONS_Printf(M_GetText("Didn't save %s for netgame"), tmpsave);
+
+	free(savebuffer);
+	save_p = NULL;
+}
+
+#undef  TMPSAVENAME
+#endif
+#define TMPSAVENAME "$$$.sav"
+
+
+void CL_LoadReceivedSavegame(boolean reloading)
+{
+	UINT8 *savebuffer = NULL;
+	size_t length, decompressedlen;
+	char tmpsave[256];
+
+	FreeFileNeeded();
+
+	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
+
+	length = FIL_ReadFile(tmpsave, &savebuffer);
+
+	CONS_Printf(M_GetText("Loading savegame length %s\n"), sizeu1(length));
+	if (!length)
+	{
+		I_Error("Can't read savegame sent");
+		return;
+	}
+
+	save_p = savebuffer;
+
+	// Decompress saved game if necessary.
+	decompressedlen = READUINT32(save_p);
+	if(decompressedlen > 0)
+	{
+		UINT8 *decompressedbuffer = Z_Malloc(decompressedlen, PU_STATIC, NULL);
+		lzf_decompress(save_p, length - sizeof(UINT32), decompressedbuffer, decompressedlen);
+		Z_Free(savebuffer);
+		save_p = savebuffer = decompressedbuffer;
+	}
+
+	paused = false;
+	demoplayback = false;
+	titlemapinaction = TITLEMAP_OFF;
+	titledemo = false;
+	automapactive = false;
+
+	// load a base level
+	if (P_LoadNetGame(reloading))
+	{
+		const UINT8 actnum = mapheaderinfo[gamemap-1]->actnum;
+		CONS_Printf(M_GetText("Map is now \"%s"), G_BuildMapName(gamemap));
+		if (strcmp(mapheaderinfo[gamemap-1]->lvlttl, ""))
+		{
+			CONS_Printf(": %s", mapheaderinfo[gamemap-1]->lvlttl);
+			if (!(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
+				CONS_Printf(M_GetText(" Zone"));
+			if (actnum > 0)
+				CONS_Printf(" %2d", actnum);
+		}
+		CONS_Printf("\"\n");
+	}
+
+	// done
+	Z_Free(savebuffer);
+	save_p = NULL;
+	if (unlink(tmpsave) == -1)
+		CONS_Alert(CONS_ERROR, M_GetText("Can't delete %s\n"), tmpsave);
+	consistancy[gametic%BACKUPTICS] = Consistancy();
+	CON_ToggleOff();
+
+	// Tell the server we have received and reloaded the gamestate
+	// so they know they can resume the game
+	netbuffer->packettype = PT_RECEIVEDGAMESTATE;
+	HSendPacket(servernode, true, 0, 0);
+}
+
+void CL_ReloadReceivedSavegame(void)
+{
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+	{
+		LUA_InvalidatePlayer(&players[i]);
+		sprintf(player_names[i], "Player %d", i + 1);
+	}
+
+	CL_LoadReceivedSavegame(true);
+
+	neededtic = max(neededtic, gametic);
+	maketic = neededtic;
+
+	ticcmd_oldangleturn[0] = players[consoleplayer].oldrelangleturn;
+	P_ForceLocalAngle(&players[consoleplayer], (angle_t)(players[consoleplayer].angleturn << 16));
+	if (splitscreen)
+	{
+		ticcmd_oldangleturn[1] = players[secondarydisplayplayer].oldrelangleturn;
+		P_ForceLocalAngle(&players[secondarydisplayplayer], (angle_t)(players[secondarydisplayplayer].angleturn << 16));
+	}
+
+	camera.subsector = R_PointInSubsector(camera.x, camera.y);
+	camera2.subsector = R_PointInSubsector(camera2.x, camera2.y);
+
+	cl_redownloadinggamestate = false;
+
+	CONS_Printf(M_GetText("Game state reloaded\n"));
+}
+
+void Command_ResendGamestate(void)
+{
+	SINT8 playernum;
+
+	if (COM_Argc() == 1)
+	{
+		CONS_Printf(M_GetText("resendgamestate <playername/playernum>: resend the game state to a player\n"));
+		return;
+	}
+	else if (client)
+	{
+		CONS_Printf(M_GetText("Only the server can use this.\n"));
+		return;
+	}
+
+	playernum = nametonum(COM_Argv(1));
+	if (playernum == -1 || playernum == 0)
+		return;
+
+	// Send a PT_WILLRESENDGAMESTATE packet to the client so they know what's going on
+	netbuffer->packettype = PT_WILLRESENDGAMESTATE;
+	if (!HSendPacket(playernode[playernum], true, 0, 0))
+	{
+		CONS_Alert(CONS_ERROR, M_GetText("A problem occurred, please try again.\n"));
+		return;
+	}
+}
+
+void PT_CanReceiveGamestate(SINT8 node)
+{
+	if (client || netnodes[node].sendingsavegame)
+		return;
+
+	CONS_Printf(M_GetText("Resending game state to %s...\n"), player_names[netnodes[node].player]);
+
+	SV_SendSaveGame(node, true); // Resend a complete game state
+	netnodes[node].resendingsavegame = true;
+}
+
+void PT_ReceivedGamestate(SINT8 node)
+{
+	netnodes[node].sendingsavegame = false;
+	netnodes[node].resendingsavegame = false;
+	netnodes[node].savegameresendcooldown = I_GetTime() + 5 * TICRATE;
+}
+
+void PT_WillResendGamestate(SINT8 node)
+{
+	(void)node;
+
+	char tmpsave[256];
+
+	if (server || cl_redownloadinggamestate)
+		return;
+
+	// Send back a PT_CANRECEIVEGAMESTATE packet to the server
+	// so they know they can start sending the game state
+	netbuffer->packettype = PT_CANRECEIVEGAMESTATE;
+	if (!HSendPacket(servernode, true, 0, 0))
+		return;
+
+	CONS_Printf(M_GetText("Reloading game state...\n"));
+
+	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
+
+	// Don't get a corrupt savegame error because tmpsave already exists
+	if (FIL_FileExists(tmpsave) && unlink(tmpsave) == -1)
+		I_Error("Can't delete %s\n", tmpsave);
+
+	CL_PrepareDownloadSaveGame(tmpsave);
+
+	cl_redownloadinggamestate = true;
+}
diff --git a/src/netcode/gamestate.h b/src/netcode/gamestate.h
new file mode 100644
index 0000000000000000000000000000000000000000..a2fae1f14a75d13cd0479acf7950a09f08754493
--- /dev/null
+++ b/src/netcode/gamestate.h
@@ -0,0 +1,31 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2023 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  gamestate.h
+/// \brief Gamestate (re)sending
+
+#ifndef __GAMESTATE__
+#define __GAMESTATE__
+
+#include "../doomtype.h"
+
+extern UINT8 hu_redownloadinggamestate;
+extern boolean cl_redownloadinggamestate;
+
+boolean SV_ResendingSavegameToAnyone(void);
+void SV_SendSaveGame(INT32 node, boolean resending);
+void SV_SavedGame(void);
+void CL_LoadReceivedSavegame(boolean reloading);
+void CL_ReloadReceivedSavegame(void);
+void Command_ResendGamestate(void);
+void PT_CanReceiveGamestate(SINT8 node);
+void PT_ReceivedGamestate(SINT8 node);
+void PT_WillResendGamestate(SINT8 node);
+
+#endif
diff --git a/src/http-mserv.c b/src/netcode/http-mserv.c
similarity index 98%
rename from src/http-mserv.c
rename to src/netcode/http-mserv.c
index df9a71a5c5d6cfcb8f73ecd3eeff371170e76c8e..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);
 }
 
diff --git a/src/i_addrinfo.c b/src/netcode/i_addrinfo.c
similarity index 100%
rename from src/i_addrinfo.c
rename to src/netcode/i_addrinfo.c
diff --git a/src/i_addrinfo.h b/src/netcode/i_addrinfo.h
similarity index 100%
rename from src/i_addrinfo.h
rename to src/netcode/i_addrinfo.h
diff --git a/src/i_net.h b/src/netcode/i_net.h
similarity index 98%
rename from src/i_net.h
rename to src/netcode/i_net.h
index 12a07f183e515f7ca985afb67ac50cb07cf2c313..09b842296c313cdd43623cfecd1d8da64534397b 100644
--- a/src/i_net.h
+++ b/src/netcode/i_net.h
@@ -18,8 +18,8 @@
 #pragma interface
 #endif
 
-#include "doomdef.h"
-#include "command.h"
+#include "../doomdef.h"
+#include "../command.h"
 
 /// \brief program net id
 #define DOOMCOM_ID (INT32)0x12345678l
diff --git a/src/i_tcp.c b/src/netcode/i_tcp.c
similarity index 88%
rename from src/i_tcp.c
rename to src/netcode/i_tcp.c
index d95b381f4a65fa86030f9efd44c7164b487df61e..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
-	} mysockaddr_t;
+	#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
 
-	#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)
@@ -387,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;
@@ -455,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;
@@ -485,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;
@@ -497,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(" -       ");
@@ -531,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,
@@ -596,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
@@ -649,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);
@@ -675,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]);
@@ -694,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]);
@@ -714,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 :)
@@ -731,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)
@@ -1061,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
@@ -1121,7 +1076,7 @@ boolean I_InitTcpDriver(void)
 #endif
 		init_tcp_driver = true;
 	}
-#endif
+
 	if (!tcp_was_up && init_tcp_driver)
 	{
 		I_AddExitFunc(I_ShutdownTcpDriver);
@@ -1135,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))
@@ -1150,11 +1103,9 @@ static void SOCK_CloseSocket(void)
 		mysockets[i] = ERRSOCKET;
 	}
 }
-#endif
 
 void I_ShutdownTcpDriver(void)
 {
-#ifndef NONET
 	SOCK_CloseSocket();
 
 	CONS_Printf("I_ShutdownTcpDriver: ");
@@ -1164,10 +1115,8 @@ void I_ShutdownTcpDriver(void)
 #endif
 	CONS_Printf("shut down\n");
 	init_tcp_driver = false;
-#endif
 }
 
-#ifndef NONET
 static SINT8 SOCK_NetMakeNodewPort(const char *address, const char *port)
 {
 	SINT8 newnode = -1;
@@ -1223,17 +1172,13 @@ static SINT8 SOCK_NetMakeNodewPort(const char *address, const char *port)
 	I_freeaddrinfo(ai);
 	return newnode;
 }
-#endif
 
 static boolean SOCK_OpenSocket(void)
 {
-#ifndef NONET
-	size_t i;
-
 	memset(clientaddress, 0, sizeof (clientaddress));
 
 	nodeconnected[0] = true; // always connected to self
-	for (i = 1; i < MAXNETNODES; i++)
+	for (size_t i = 1; i < MAXNETNODES; i++)
 		nodeconnected[i] = false;
 	nodeconnected[BROADCASTADDR] = true;
 	I_NetSend = SOCK_Send;
@@ -1251,18 +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;
 
@@ -1281,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;
 
@@ -1335,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 99%
rename from src/mserv.h
rename to src/netcode/mserv.h
index 07253da8562906cb51c59e6a27f2ec289d62c9a8..0bc8c8e6b878a415887e2a9613188d3ceac30956 100644
--- a/src/mserv.h
+++ b/src/netcode/mserv.h
@@ -14,7 +14,7 @@
 #ifndef _MSERV_H_
 #define _MSERV_H_
 
-#include "i_threads.h"
+#include "../i_threads.h"
 
 // lowered from 32 due to menu changes
 #define NUM_LIST_ROOMS 16
diff --git a/src/netcode/net_command.c b/src/netcode/net_command.c
new file mode 100644
index 0000000000000000000000000000000000000000..2b3abfd0238a3246fedc086afc007a85c993b425
--- /dev/null
+++ b/src/netcode/net_command.c
@@ -0,0 +1,382 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2022 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  net_command.c
+/// \brief Net command handling
+
+#include "net_command.h"
+#include "tic_command.h"
+#include "gamestate.h"
+#include "server_connection.h"
+#include "d_clisrv.h"
+#include "i_net.h"
+#include "../byteptr.h"
+#include "../g_game.h"
+#include "../z_zone.h"
+#include "../doomtype.h"
+
+textcmdtic_t *textcmds[TEXTCMD_HASH_SIZE] = {NULL};
+UINT8 localtextcmd[MAXTEXTCMD];
+UINT8 localtextcmd2[MAXTEXTCMD]; // splitscreen
+static void (*listnetxcmd[MAXNETXCMD])(UINT8 **p, INT32 playernum);
+
+void RegisterNetXCmd(netxcmd_t id, void (*cmd_f)(UINT8 **p, INT32 playernum))
+{
+#ifdef PARANOIA
+	if (id >= MAXNETXCMD)
+		I_Error("Command id %d too big", id);
+	if (listnetxcmd[id] != 0)
+		I_Error("Command id %d already used", id);
+#endif
+	listnetxcmd[id] = cmd_f;
+}
+
+void SendNetXCmd(netxcmd_t id, const void *param, size_t nparam)
+{
+	if (localtextcmd[0]+2+nparam > MAXTEXTCMD)
+	{
+		// for future reference: if (cv_debug) != debug disabled.
+		CONS_Alert(CONS_ERROR, M_GetText("NetXCmd buffer full, cannot add netcmd %d! (size: %d, needed: %s)\n"), id, localtextcmd[0], sizeu1(nparam));
+		return;
+	}
+	localtextcmd[0]++;
+	localtextcmd[localtextcmd[0]] = (UINT8)id;
+	if (param && nparam)
+	{
+		M_Memcpy(&localtextcmd[localtextcmd[0]+1], param, nparam);
+		localtextcmd[0] = (UINT8)(localtextcmd[0] + (UINT8)nparam);
+	}
+}
+
+// splitscreen player
+void SendNetXCmd2(netxcmd_t id, const void *param, size_t nparam)
+{
+	if (localtextcmd2[0]+2+nparam > MAXTEXTCMD)
+	{
+		I_Error("No more place in the buffer for netcmd %d\n",id);
+		return;
+	}
+	localtextcmd2[0]++;
+	localtextcmd2[localtextcmd2[0]] = (UINT8)id;
+	if (param && nparam)
+	{
+		M_Memcpy(&localtextcmd2[localtextcmd2[0]+1], param, nparam);
+		localtextcmd2[0] = (UINT8)(localtextcmd2[0] + (UINT8)nparam);
+	}
+}
+
+UINT8 GetFreeXCmdSize(void)
+{
+	// -1 for the size and another -1 for the ID.
+	return (UINT8)(localtextcmd[0] - 2);
+}
+
+// Frees all textcmd memory for the specified tic
+void D_FreeTextcmd(tic_t tic)
+{
+	textcmdtic_t **tctprev = &textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
+	textcmdtic_t *textcmdtic = *tctprev;
+
+	while (textcmdtic && textcmdtic->tic != tic)
+	{
+		tctprev = &textcmdtic->next;
+		textcmdtic = textcmdtic->next;
+	}
+
+	if (textcmdtic)
+	{
+		// Remove this tic from the list.
+		*tctprev = textcmdtic->next;
+
+		// Free all players.
+		for (INT32 i = 0; i < TEXTCMD_HASH_SIZE; i++)
+		{
+			textcmdplayer_t *textcmdplayer = textcmdtic->playercmds[i];
+
+			while (textcmdplayer)
+			{
+				textcmdplayer_t *tcpnext = textcmdplayer->next;
+				Z_Free(textcmdplayer);
+				textcmdplayer = tcpnext;
+			}
+		}
+
+		// Free this tic's own memory.
+		Z_Free(textcmdtic);
+	}
+}
+
+// Gets the buffer for the specified ticcmd, or NULL if there isn't one
+UINT8* D_GetExistingTextcmd(tic_t tic, INT32 playernum)
+{
+	textcmdtic_t *textcmdtic = textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
+	while (textcmdtic && textcmdtic->tic != tic) textcmdtic = textcmdtic->next;
+
+	// Do we have an entry for the tic? If so, look for player.
+	if (textcmdtic)
+	{
+		textcmdplayer_t *textcmdplayer = textcmdtic->playercmds[playernum & (TEXTCMD_HASH_SIZE - 1)];
+		while (textcmdplayer && textcmdplayer->playernum != playernum) textcmdplayer = textcmdplayer->next;
+
+		if (textcmdplayer) return textcmdplayer->cmd;
+	}
+
+	return NULL;
+}
+
+// Gets the buffer for the specified ticcmd, creating one if necessary
+UINT8* D_GetTextcmd(tic_t tic, INT32 playernum)
+{
+	textcmdtic_t *textcmdtic = textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
+	textcmdtic_t **tctprev = &textcmds[tic & (TEXTCMD_HASH_SIZE - 1)];
+	textcmdplayer_t *textcmdplayer, **tcpprev;
+
+	// Look for the tic.
+	while (textcmdtic && textcmdtic->tic != tic)
+	{
+		tctprev = &textcmdtic->next;
+		textcmdtic = textcmdtic->next;
+	}
+
+	// If we don't have an entry for the tic, make it.
+	if (!textcmdtic)
+	{
+		textcmdtic = *tctprev = Z_Calloc(sizeof (textcmdtic_t), PU_STATIC, NULL);
+		textcmdtic->tic = tic;
+	}
+
+	tcpprev = &textcmdtic->playercmds[playernum & (TEXTCMD_HASH_SIZE - 1)];
+	textcmdplayer = *tcpprev;
+
+	// Look for the player.
+	while (textcmdplayer && textcmdplayer->playernum != playernum)
+	{
+		tcpprev = &textcmdplayer->next;
+		textcmdplayer = textcmdplayer->next;
+	}
+
+	// If we don't have an entry for the player, make it.
+	if (!textcmdplayer)
+	{
+		textcmdplayer = *tcpprev = Z_Calloc(sizeof (textcmdplayer_t), PU_STATIC, NULL);
+		textcmdplayer->playernum = playernum;
+	}
+
+	return textcmdplayer->cmd;
+}
+
+void ExtraDataTicker(void)
+{
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+		if (playeringame[i] || i == 0)
+		{
+			UINT8 *bufferstart = D_GetExistingTextcmd(gametic, i);
+
+			if (bufferstart)
+			{
+				UINT8 *curpos = bufferstart;
+				UINT8 *bufferend = &curpos[curpos[0]+1];
+
+				curpos++;
+				while (curpos < bufferend)
+				{
+					if (*curpos < MAXNETXCMD && listnetxcmd[*curpos])
+					{
+						const UINT8 id = *curpos;
+						curpos++;
+						DEBFILE(va("executing x_cmd %s ply %u ", netxcmdnames[id - 1], i));
+						(listnetxcmd[id])(&curpos, i);
+						DEBFILE("done\n");
+					}
+					else
+					{
+						if (server)
+						{
+							SendKick(i, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
+							DEBFILE(va("player %d kicked [gametic=%u] reason as follows:\n", i, gametic));
+						}
+						CONS_Alert(CONS_WARNING, M_GetText("Got unknown net command [%s]=%d (max %d)\n"), sizeu1(curpos - bufferstart), *curpos, bufferstart[0]);
+						break;
+					}
+				}
+			}
+		}
+
+	// If you are a client, you can safely forget the net commands for this tic
+	// If you are the server, you need to remember them until every client has been acknowledged,
+	// because if you need to resend a PT_SERVERTICS packet, you will need to put the commands in it
+	if (client)
+		D_FreeTextcmd(gametic);
+}
+
+// used at txtcmds received to check packetsize bound
+size_t TotalTextCmdPerTic(tic_t tic)
+{
+	size_t total = 1; // num of textcmds in the tic (ntextcmd byte)
+
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+	{
+		UINT8 *textcmd = D_GetExistingTextcmd(tic, i);
+		if ((!i || playeringame[i]) && textcmd)
+			total += 2 + textcmd[0]; // "+2" for size and playernum
+	}
+
+	return total;
+}
+
+void PT_TextCmd(SINT8 node, INT32 netconsole)
+{
+	if (client)
+		return;
+
+	// splitscreen special
+	if (netbuffer->packettype == PT_TEXTCMD2)
+		netconsole = netnodes[node].player2;
+
+	if (netconsole < 0 || netconsole >= MAXPLAYERS)
+		Net_UnAcknowledgePacket(node);
+	else
+	{
+		size_t j;
+		tic_t tic = maketic;
+		UINT8 *textcmd;
+
+		// ignore if the textcmd has a reported size of zero
+		// this shouldn't be sent at all
+		if (!netbuffer->u.textcmd[0])
+		{
+			DEBFILE(va("GetPacket: Textcmd with size 0 detected! (node %u, player %d)\n",
+				node, netconsole));
+			Net_UnAcknowledgePacket(node);
+			return;
+		}
+
+		// ignore if the textcmd size var is actually larger than it should be
+		// BASEPACKETSIZE + 1 (for size) + textcmd[0] should == datalength
+		if (netbuffer->u.textcmd[0] > (size_t)doomcom->datalength-BASEPACKETSIZE-1)
+		{
+			DEBFILE(va("GetPacket: Bad Textcmd packet size! (expected %d, actual %s, node %u, player %d)\n",
+			netbuffer->u.textcmd[0], sizeu1((size_t)doomcom->datalength-BASEPACKETSIZE-1),
+				node, netconsole));
+			Net_UnAcknowledgePacket(node);
+			return;
+		}
+
+		// check if tic that we are making isn't too large else we cannot send it :(
+		// doomcom->numslots+1 "+1" since doomcom->numslots can change within this time and sent time
+		j = software_MAXPACKETLENGTH
+			- (netbuffer->u.textcmd[0]+2+BASESERVERTICSSIZE
+			+ (doomcom->numslots+1)*sizeof(ticcmd_t));
+
+		// search a tic that have enougth space in the ticcmd
+		while ((textcmd = D_GetExistingTextcmd(tic, netconsole)),
+			(TotalTextCmdPerTic(tic) > j || netbuffer->u.textcmd[0] + (textcmd ? textcmd[0] : 0) > MAXTEXTCMD)
+			&& tic < firstticstosend + BACKUPTICS)
+			tic++;
+
+		if (tic >= firstticstosend + BACKUPTICS)
+		{
+			DEBFILE(va("GetPacket: Textcmd too long (max %s, used %s, mak %d, "
+				"tosend %u, node %u, player %d)\n", sizeu1(j), sizeu2(TotalTextCmdPerTic(maketic)),
+				maketic, firstticstosend, node, netconsole));
+			Net_UnAcknowledgePacket(node);
+			return;
+		}
+
+		// Make sure we have a buffer
+		if (!textcmd) textcmd = D_GetTextcmd(tic, netconsole);
+
+		DEBFILE(va("textcmd put in tic %u at position %d (player %d) ftts %u mk %u\n",
+			tic, textcmd[0]+1, netconsole, firstticstosend, maketic));
+
+		M_Memcpy(&textcmd[textcmd[0]+1], netbuffer->u.textcmd+1, netbuffer->u.textcmd[0]);
+		textcmd[0] += (UINT8)netbuffer->u.textcmd[0];
+	}
+}
+
+void SV_WriteNetCommandsForTic(tic_t tic, UINT8 **buf)
+{
+	UINT8 *numcmds;
+
+	numcmds = (*buf)++;
+	*numcmds = 0;
+	for (INT32 i = 0; i < MAXPLAYERS; i++)
+	{
+		UINT8 *cmd = D_GetExistingTextcmd(tic, i);
+		INT32 size = cmd ? cmd[0] : 0;
+
+		if ((!i || playeringame[i]) && size)
+		{
+			(*numcmds)++;
+			WRITEUINT8(*buf, i);
+			M_Memcpy(*buf, cmd, size + 1);
+			*buf += size + 1;
+		}
+	}
+}
+
+void CL_CopyNetCommandsFromServerPacket(tic_t tic, UINT8 **buf)
+{
+	UINT8 numcmds = *(*buf)++;
+
+	for (UINT32 i = 0; i < numcmds; i++)
+	{
+		INT32 playernum = *(*buf)++; // playernum
+		size_t size = (*buf)[0]+1;
+
+		if (tic >= gametic) // Don't copy old net commands
+			M_Memcpy(D_GetTextcmd(tic, playernum), *buf, size);
+		*buf += size;
+	}
+}
+
+void CL_SendNetCommands(void)
+{
+	// Send extra data if needed
+	if (localtextcmd[0])
+	{
+		netbuffer->packettype = PT_TEXTCMD;
+		M_Memcpy(netbuffer->u.textcmd,localtextcmd, localtextcmd[0]+1);
+		// All extra data have been sent
+		if (HSendPacket(servernode, true, 0, localtextcmd[0]+1)) // Send can fail...
+			localtextcmd[0] = 0;
+	}
+
+	// Send extra data if needed for player 2 (splitscreen)
+	if (localtextcmd2[0])
+	{
+		netbuffer->packettype = PT_TEXTCMD2;
+		M_Memcpy(netbuffer->u.textcmd, localtextcmd2, localtextcmd2[0]+1);
+		// All extra data have been sent
+		if (HSendPacket(servernode, true, 0, localtextcmd2[0]+1)) // Send can fail...
+			localtextcmd2[0] = 0;
+	}
+}
+
+void SendKick(UINT8 playernum, UINT8 msg)
+{
+	UINT8 buf[2];
+
+	if (!(server && cv_rejointimeout.value))
+		msg &= ~KICK_MSG_KEEP_BODY;
+
+	buf[0] = playernum;
+	buf[1] = msg;
+	SendNetXCmd(XD_KICK, &buf, 2);
+}
+
+void SendKicksForNode(SINT8 node, UINT8 msg)
+{
+	if (!netnodes[node].ingame)
+		return;
+
+	for (INT32 playernum = netnodes[node].player; playernum != -1; playernum = netnodes[node].player2)
+		if (playernum != -1 && playeringame[playernum])
+			SendKick(playernum, msg);
+}
diff --git a/src/netcode/net_command.h b/src/netcode/net_command.h
new file mode 100644
index 0000000000000000000000000000000000000000..a0c46f3a2ffb26156708bfe3fbb3244b5fbc48ec
--- /dev/null
+++ b/src/netcode/net_command.h
@@ -0,0 +1,66 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2022 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  net_command.h
+/// \brief Net command handling
+
+#ifndef __D_NET_COMMAND__
+#define __D_NET_COMMAND__
+
+#include "d_clisrv.h"
+#include "../doomtype.h"
+
+// Must be a power of two
+#define TEXTCMD_HASH_SIZE 4
+
+typedef struct textcmdplayer_s
+{
+	INT32 playernum;
+	UINT8 cmd[MAXTEXTCMD];
+	struct textcmdplayer_s *next;
+} textcmdplayer_t;
+
+typedef struct textcmdtic_s
+{
+	tic_t tic;
+	textcmdplayer_t *playercmds[TEXTCMD_HASH_SIZE];
+	struct textcmdtic_s *next;
+} textcmdtic_t;
+
+extern textcmdtic_t *textcmds[TEXTCMD_HASH_SIZE];
+
+extern UINT8 localtextcmd[MAXTEXTCMD];
+extern UINT8 localtextcmd2[MAXTEXTCMD]; // splitscreen
+
+void RegisterNetXCmd(netxcmd_t id, void (*cmd_f)(UINT8 **p, INT32 playernum));
+void SendNetXCmd(netxcmd_t id, const void *param, size_t nparam);
+void SendNetXCmd2(netxcmd_t id, const void *param, size_t nparam); // splitsreen player
+
+UINT8 GetFreeXCmdSize(void);
+void D_FreeTextcmd(tic_t tic);
+
+// Gets the buffer for the specified ticcmd, or NULL if there isn't one
+UINT8* D_GetExistingTextcmd(tic_t tic, INT32 playernum);
+
+// Gets the buffer for the specified ticcmd, creating one if necessary
+UINT8* D_GetTextcmd(tic_t tic, INT32 playernum);
+
+void ExtraDataTicker(void);
+
+// used at txtcmds received to check packetsize bound
+size_t TotalTextCmdPerTic(tic_t tic);
+
+void PT_TextCmd(SINT8 node, INT32 netconsole);
+void SV_WriteNetCommandsForTic(tic_t tic, UINT8 **buf);
+void CL_CopyNetCommandsFromServerPacket(tic_t tic, UINT8 **buf);
+void CL_SendNetCommands(void);
+void SendKick(UINT8 playernum, UINT8 msg);
+void SendKicksForNode(SINT8 node, UINT8 msg);
+
+#endif
diff --git a/src/d_clisrv.h b/src/netcode/protocol.h
similarity index 59%
rename from src/d_clisrv.h
rename to src/netcode/protocol.h
index 49fb5fc1db1507a75c1b379451c27141146d1036..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,7 +72,7 @@ typedef enum
 	PT_ASKLUAFILE,     // Client telling the server they don't have the file
 	PT_HASLUAFILE,     // Client telling the server they have the file
 
-	PT_BASICKEEPALIVE,// Keep the network alive during wipes, as tics aren't advanced and NetUpdate isn't called
+	PT_BASICKEEPALIVE, // Keep the network alive during wipes, as tics aren't advanced and NetUpdate isn't called
 
 	// Add non-PT_CANFAIL packet types here to avoid breaking MS compatibility.
 
@@ -92,7 +87,6 @@ typedef enum
 	PT_TEXTCMD,       // Extra text commands from the client.
 	PT_TEXTCMD2,      // Splitscreen text commands.
 	PT_CLIENTJOIN,    // Client wants to join; used in start game.
-	PT_NODETIMEOUT,   // Packet sent to self if the connection times out.
 
 	PT_LOGIN,         // Login attempt from the client.
 
@@ -103,14 +97,6 @@ typedef enum
 	NUMPACKETTYPE
 } packettype_t;
 
-#ifdef PACKETDROP
-void Command_Drop(void);
-void Command_Droprate(void);
-#endif
-#ifdef _DEBUG
-void Command_Numnodes(void);
-#endif
-
 #if defined(_MSC_VER)
 #pragma pack(1)
 #endif
@@ -139,13 +125,12 @@ typedef struct
 #endif
 
 // Server to client packet
-// this packet is too large
 typedef struct
 {
 	tic_t starttic;
 	UINT8 numtics;
 	UINT8 numslots; // "Slots filled": Highest player number in use plus one.
-	ticcmd_t cmds[45]; // Normally [BACKUPTIC][MAXPLAYERS] but too large
+	ticcmd_t cmds[45];
 } ATTRPACK servertics_pak;
 
 typedef struct
@@ -215,6 +200,7 @@ enum {
 
 #define MAXSERVERNAME 32
 #define MAXFILENEEDED 915
+
 // This packet is too large
 typedef struct
 {
@@ -275,7 +261,7 @@ typedef struct
 	UINT8 data; // Color is first four bits, hasflag, isit and issuper have one bit each, the last is unused.
 	UINT32 score;
 	UINT16 timeinserver; // In seconds.
-} ATTRPACK plrinfo;
+} ATTRPACK plrinfo_pak;
 
 // Shortest player information for join during intermission.
 typedef struct
@@ -286,7 +272,7 @@ typedef struct
 	UINT32 pflags;
 	UINT32 score;
 	UINT8 ctfteam;
-} ATTRPACK plrconfig;
+} ATTRPACK plrconfig_pak;
 
 typedef struct
 {
@@ -309,25 +295,25 @@ typedef struct
 	UINT8 reserved; // Padding
 	union
 	{
-		clientcmd_pak clientpak;            //         144 bytes
-		client2cmd_pak client2pak;          //         200 bytes
-		servertics_pak serverpak;           //      132495 bytes (more around 360, no?)
-		serverconfig_pak servercfg;         //         773 bytes
-		UINT8 textcmd[MAXTEXTCMD+1];        //       66049 bytes (wut??? 64k??? More like 257 bytes...)
-		filetx_pak filetxpak;               //         139 bytes
+		clientcmd_pak clientpak;
+		client2cmd_pak client2pak;
+		servertics_pak serverpak;
+		serverconfig_pak servercfg;
+		UINT8 textcmd[MAXTEXTCMD+1];
+		filetx_pak filetxpak;
 		fileack_pak fileack;
 		UINT8 filereceived;
-		clientconfig_pak clientcfg;         //         136 bytes
+		clientconfig_pak clientcfg;
 		UINT8 md5sum[16];
-		serverinfo_pak serverinfo;          //        1024 bytes
-		serverrefuse_pak serverrefuse;      //       65025 bytes (somehow I feel like those values are garbage...)
-		askinfo_pak askinfo;                //          61 bytes
-		msaskinfo_pak msaskinfo;            //          22 bytes
-		plrinfo playerinfo[MAXPLAYERS];     //         576 bytes(?)
-		plrconfig playerconfig[MAXPLAYERS]; // (up to) 528 bytes(?)
-		INT32 filesneedednum;               //           4 bytes
-		filesneededconfig_pak filesneededcfg; //       ??? bytes
-		UINT32 pingtable[MAXPLAYERS+1];     //          68 bytes
+		serverinfo_pak serverinfo;
+		serverrefuse_pak serverrefuse;
+		askinfo_pak askinfo;
+		msaskinfo_pak msaskinfo;
+		plrinfo_pak playerinfo[MAXPLAYERS];
+		plrconfig_pak playerconfig[MAXPLAYERS];
+		INT32 filesneedednum;
+		filesneededconfig_pak filesneededcfg;
+		UINT32 pingtable[MAXPLAYERS+1];
 	} u; // This is needed to pack diff packet types data together
 } ATTRPACK doomdata_t;
 
@@ -335,26 +321,7 @@ typedef struct
 #pragma pack()
 #endif
 
-#define MAXSERVERLIST (MAXNETNODES-1)
-typedef struct
-{
-	SINT8 node;
-	serverinfo_pak info;
-} serverelem_t;
-
-extern serverelem_t serverlist[MAXSERVERLIST];
-extern UINT32 serverlistcount;
-extern INT32 mapchangepending;
-
-// Points inside doomcom
-extern doomdata_t *netbuffer;
-
-extern consvar_t cv_showjoinaddress;
-extern consvar_t cv_playbackspeed;
-
-#define BASEPACKETSIZE      offsetof(doomdata_t, u)
 #define FILETXHEADER        offsetof(filetx_pak, data)
-#define BASESERVERTICSSIZE  offsetof(doomdata_t, u.serverpak.cmds[0])
 
 #define KICK_MSG_GO_AWAY     1
 #define KICK_MSG_CON_FAIL    2
@@ -366,107 +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;
-extern consvar_t cv_dedicatedidletime;
-
-// Used in d_net, the only dependence
-tic_t ExpandTics(INT32 low, INT32 node);
-void D_ClientServerInit(void);
-
-// Initialise the other field
-void RegisterNetXCmd(netxcmd_t id, void (*cmd_f)(UINT8 **p, INT32 playernum));
-void SendNetXCmd(netxcmd_t id, const void *param, size_t nparam);
-void SendNetXCmd2(netxcmd_t id, const void *param, size_t nparam); // splitsreen player
-void SendKick(UINT8 playernum, UINT8 msg);
-
-// Create any new ticcmds and broadcast to other players.
-void NetUpdate(void);
-
-// Maintain connections to nodes without timing them all out.
-void NetKeepAlive(void);
-
-void SV_StartSinglePlayerServer(void);
-boolean SV_SpawnServer(void);
-void SV_StopServer(void);
-void SV_ResetServer(void);
-void CL_AddSplitscreenPlayer(void);
-void CL_RemoveSplitscreenPlayer(void);
-void CL_Reset(void);
-void CL_ClearPlayer(INT32 playernum);
-void CL_QueryServerList(msg_server_t *list);
-void CL_UpdateServerList(boolean internetsearch, INT32 room);
-void CL_RemovePlayer(INT32 playernum, kickreason_t reason);
-// Is there a game running
-boolean Playing(void);
-
-// Broadcasts special packets to other players
-//  to notify of game exit
-void D_QuitNetGame(void);
-
-//? How many ticks to run?
-boolean TryRunTics(tic_t realtic);
-
-// extra data for lmps
-// these functions scare me. they contain magic.
-/*boolean AddLmpExtradata(UINT8 **demo_p, INT32 playernum);
-void ReadLmpExtraData(UINT8 **demo_pointer, INT32 playernum);*/
-
-#ifndef NONET
-// translate a playername in a player number return -1 if not found and
-// print a error message in the console
-SINT8 nametonum(const char *name);
-#endif
-
-extern char motd[254], server_context[8];
-extern UINT8 playernode[MAXPLAYERS];
-
-INT32 D_NumPlayers(void);
-INT32 D_NumBots(void);
-void D_ResetTiccmds(void);
-
-tic_t GetLag(INT32 node);
-UINT8 GetFreeXCmdSize(void);
-
-void D_MD5PasswordPass(const UINT8 *buffer, size_t len, const char *salt, void *dest);
-
-extern UINT8 hu_redownloadinggamestate;
-
-extern UINT8 adminpassmd5[16];
-extern boolean adminpasswordset;
-
-extern boolean hu_stopped;
-
 #endif
diff --git a/src/netcode/server_connection.c b/src/netcode/server_connection.c
new file mode 100644
index 0000000000000000000000000000000000000000..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/p_ceilng.c b/src/p_ceilng.c
index 98e9313625268fedd13a865038c4fc895692cdd6..61430d73f1599ff411639938f8d90f2b07fbae3e 100644
--- a/src/p_ceilng.c
+++ b/src/p_ceilng.c
@@ -17,7 +17,7 @@
 #include "r_main.h"
 #include "s_sound.h"
 #include "z_zone.h"
-#include "d_netcmd.h"
+#include "netcode/d_netcmd.h"
 
 // ==========================================================================
 //                              CEILINGS
diff --git a/src/p_enemy.c b/src/p_enemy.c
index eebb65f3cb146f229183b014acdc89e0042f8da2..93c828fbecd1394e1adbeac6aa3fd43cc43437b6 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -5248,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)
 			;
@@ -9059,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))
 	{
diff --git a/src/p_inter.c b/src/p_inter.c
index 271b6ebc45a5bf42d048b39ace8db61d212741c8..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") : ""
@@ -2630,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);
 
@@ -3323,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;
@@ -3440,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;
diff --git a/src/p_lights.c b/src/p_lights.c
index 4b6a3673b51b6a1720d237e4b85ae8f7c7839c4b..971165e88e9593634813da9fd89cb86359811cf1 100644
--- a/src/p_lights.c
+++ b/src/p_lights.c
@@ -17,7 +17,7 @@
 #include "r_state.h"
 #include "z_zone.h"
 #include "m_random.h"
-#include "d_netcmd.h"
+#include "netcode/d_netcmd.h"
 
 /** Removes any active lighting effects in a sector.
   *
diff --git a/src/p_local.h b/src/p_local.h
index 563e257d8f1e67e7e5d45d0a8f7122659c66b435..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);
diff --git a/src/p_map.c b/src/p_map.c
index 132a3cf8543baff5459fa6656f454058c89cc180..251837876caab9f979b92ef1ece83a1750f24768 100644
--- a/src/p_map.c
+++ b/src/p_map.c
@@ -2524,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;
@@ -2539,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;
@@ -2608,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;
 }
 
 //
@@ -3731,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)
 	{
diff --git a/src/p_mobj.c b/src/p_mobj.c
index de2c3a72c7136c484127c5a1c218963846ee70d4..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);
@@ -3688,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
 			{
@@ -3719,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
@@ -6882,7 +6883,6 @@ void P_RunOverlays(void)
 		mo->eflags = (mo->eflags & ~MFE_VERTICALFLIP) | (mo->target->eflags & MFE_VERTICALFLIP);
 		mo->scale = mo->destscale = mo->target->scale;
 		mo->angle = (mo->target->player ? mo->target->player->drawangle : mo->target->angle) + mo->movedir;
-		P_SetTarget(&mo->dontdrawforviewmobj, mo->target->dontdrawforviewmobj); // Hide the overlay from the view that its target is hidden from - But don't copy drawonlyforplayer!
 
 		if (!(mo->state->frame & FF_ANIMATE))
 			zoffs = FixedMul(((signed)mo->state->var2)*FRACUNIT, mo->scale);
@@ -11587,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;
@@ -11608,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))
@@ -11626,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),
diff --git a/src/p_setup.c b/src/p_setup.c
index e289b834699dd957aabc3ec554c25c720b6d5b2a..0390761b61a5e064b0b9b55daead7ebd1488ac49 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -88,6 +88,8 @@
 
 #include "taglist.h"
 
+#include "netcode/net_command.h"
+
 //
 // Map MD5, calculated on level load.
 // Sent to clients in PT_SERVERINFO.
@@ -7174,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;
 	}
 }
 
diff --git a/src/p_tick.c b/src/p_tick.c
index ec5d8a2da0a487a6e4743e39dedc04c2fb27069d..444b68d2fda3d5ca60ab958715754b9e164fb82c 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -26,6 +26,8 @@
 #include "r_main.h"
 #include "r_fps.h"
 #include "i_video.h" // rendermode
+#include "netcode/net_command.h"
+#include "netcode/server_connection.h"
 
 // Object place
 #include "m_cheat.h"
diff --git a/src/p_user.c b/src/p_user.c
index cb6f9d9b9349b6a7b02b9244dfe9ec7d48196e4e..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"
@@ -686,7 +687,7 @@ static void P_DeNightserizePlayer(player_t *player)
 
 	player->mo->skin = &skins[player->skin];
 	player->followitem = skins[player->skin].followitem;
-	player->mo->color = player->skincolor;
+	player->mo->color = P_GetPlayerColor(player);
 	G_GhostAddColor(GHC_RETURNSKIN);
 
 	// Restore aiming angle
@@ -1855,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);
 	}
@@ -1862,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)
@@ -2012,7 +2016,7 @@ mobj_t *P_SpawnGhostMobj(mobj_t *mobj)
 		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 object
+		P_SetTarget(&ghost2->dontdrawforviewmobj, mobj); // Hide the follow-ghost for the non-follow target
 		ghost2->flags2 |= (mobj->player->followmobj->flags2 & MF2_LINKDRAW);
 	}
 
@@ -3029,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);
 				}
 			}
@@ -4302,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);
 			}
 
@@ -4352,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);
 			}
 
@@ -11156,7 +11160,7 @@ static void P_MinecartThink(player_t *player)
 					detright->drawonlyforplayer = player;
 				}
 				else
-					P_RemoveMobj(detleft);
+					P_RemoveMobj(detright);
 			}
 		}
 		else
@@ -11867,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;
 
@@ -13116,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
index 4302d1b9ab2b007e92c729ea9bdbe0cd73f8d03c..cf417ec37639477b43a5a5e5035b059dc81490b4 100644
--- a/src/r_bbox.c
+++ b/src/r_bbox.c
@@ -34,7 +34,7 @@ static CV_PossibleValue_t renderhitbox_cons_t[] = {
 	{RENDERHITBOX_RINGS, "Rings"},
 	{0}};
 
-consvar_t cv_renderhitbox = CVAR_INIT ("renderhitbox", "Off", CV_CHEAT, renderhitbox_cons_t, NULL);
+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);
 
@@ -268,6 +268,9 @@ 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)
 	{
diff --git a/src/r_fps.c b/src/r_fps.c
index c6eb5948212972d87d1e0079c668483fb927de94..de450aaa7f465b8d47891baec585c757de361c60 100644
--- a/src/r_fps.c
+++ b/src/r_fps.c
@@ -292,6 +292,8 @@ 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;
@@ -307,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;
@@ -340,6 +354,8 @@ 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;
@@ -356,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);
diff --git a/src/r_fps.h b/src/r_fps.h
index 9a8bfa38aeccfcab3dcd0654cd1380ac49f39d4f..f43d29f300a8a6707a3e4c6f5fa24e1e3f0ea37f 100644
--- a/src/r_fps.h
+++ b/src/r_fps.h
@@ -63,6 +63,8 @@ typedef struct {
 	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 952171405cb7f06baa3d1a784314f83b9f24bed6..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
@@ -1490,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])
@@ -1603,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_segs.c b/src/r_segs.c
index 9af83f0c7ff8af10057f253a5419dca41c86638c..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"
diff --git a/src/r_skins.c b/src/r_skins.c
index 72598f38185acf3d5801365e315e54a920e38511..308cee8d6050496b361385c0595334b647927ee1 100644
--- a/src/r_skins.c
+++ b/src/r_skins.c
@@ -306,6 +306,21 @@ INT32 R_SkinAvailable(const char *name)
 	return -1;
 }
 
+INT32 R_GetForcedSkin(INT32 playernum)
+{
+	if (netgame && cv_forceskin.value >= 0 && R_SkinUsable(playernum, cv_forceskin.value))
+		return cv_forceskin.value;
+
+	if (mapheaderinfo[gamemap-1] && mapheaderinfo[gamemap-1]->forcecharacter[0] != '\0')
+	{
+		INT32 skinnum = R_SkinAvailable(mapheaderinfo[gamemap-1]->forcecharacter);
+		if (skinnum != -1 && R_SkinUsable(playernum, skinnum))
+			return skinnum;
+	}
+
+	return -1;
+}
+
 // Auxillary function that actually sets the skin
 static void SetSkin(player_t *player, INT32 skinnum)
 {
@@ -347,10 +362,6 @@ static void SetSkin(player_t *player, INT32 skinnum)
 
 	if (!(cv_debug || devparm) && !(netgame || multiplayer || demoplayback))
 	{
-		if (player == &players[consoleplayer])
-			CV_StealthSetValue(&cv_playercolor, skin->prefcolor);
-		else if (player == &players[secondarydisplayplayer])
-			CV_StealthSetValue(&cv_playercolor2, skin->prefcolor);
 		player->skincolor = newcolor = skin->prefcolor;
 		if (player->bot && botingame)
 		{
diff --git a/src/r_skins.h b/src/r_skins.h
index fab6fc12c0f659e8b35f5d111a497850edf127b0..bf2275a49a795a1a984343dc82d881b769a89592 100644
--- a/src/r_skins.h
+++ b/src/r_skins.h
@@ -96,6 +96,7 @@ void SetPlayerSkinByNum(INT32 playernum,INT32 skinnum); // Tails 03-16-2002
 boolean R_SkinUsable(INT32 playernum, INT32 skinnum);
 UINT32 R_GetSkinAvailabilities(void);
 INT32 R_SkinAvailable(const char *name);
+INT32 R_GetForcedSkin(INT32 playernum);
 void R_AddSkins(UINT16 wadnum, boolean mainfile);
 void R_PatchSkins(UINT16 wadnum, boolean mainfile);
 
diff --git a/src/r_splats.c b/src/r_splats.c
index 737b6d110a0a11e04137447a9b6fe901b76fef93..0b482d7988cc0e3a32f719a0f84a272fdf63d2f3 100644
--- a/src/r_splats.c
+++ b/src/r_splats.c
@@ -314,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; \
@@ -367,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;
@@ -392,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);
 
@@ -429,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;
 
@@ -500,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;
@@ -549,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_things.c b/src/r_things.c
index e69fe1b822313bbdeae871741e89924a4a678d36..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"
@@ -524,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};
 
@@ -598,7 +599,7 @@ void R_InitSprites(void)
 //
 void R_ClearSprites(void)
 {
-	visspritecount = clippedvissprites = 0;
+	visspritecount = numvisiblesprites = clippedvissprites = 0;
 }
 
 //
@@ -844,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);
@@ -887,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;
@@ -897,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;
@@ -1184,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);
 
@@ -1248,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;
@@ -1292,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;
@@ -1345,20 +1355,18 @@ 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;
 
 	scalemul = FixedMul(FRACUNIT - floordiff/640, scale);
-	if ((thing->scale != thing->old_scale) && (thing->scale >= FRACUNIT/1024)) // Interpolate shadows when scaling mobjs
-		scalemul = FixedMul(scalemul, FixedDiv(interp.scale, thing->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;
@@ -1484,8 +1492,8 @@ static void R_ProjectBoundingBox(mobj_t *thing, vissprite_t *vis)
 	// 0--2
 
 	// start in the (0) corner
-	gx = interp.x - thing->radius - viewx;
-	gy = interp.y - thing->radius - viewy;
+	gx = interp.x - interp.radius - viewx;
+	gy = interp.y - interp.radius - viewy;
 
 	tz = FixedMul(gx, viewcos) + FixedMul(gy, viewsin);
 
@@ -1507,14 +1515,14 @@ static void R_ProjectBoundingBox(mobj_t *thing, vissprite_t *vis)
 	box = R_NewVisSprite();
 	box->mobj = thing;
 	box->mobjflags = thing->flags;
-	box->thingheight = thing->height;
+	box->thingheight = interp.height;
 	box->cut = SC_BBOX;
 
 	box->gx = tx;
 	box->gy = tz;
 
-	box->scale = 2 * FixedMul(thing->radius, viewsin);
-	box->xscale = 2 * FixedMul(thing->radius, viewcos);
+	box->scale  = 2 * FixedMul(interp.radius, viewsin);
+	box->xscale = 2 * FixedMul(interp.radius, viewcos);
 
 	box->pz = interp.z;
 	box->pzt = box->pz + box->thingheight;
@@ -1563,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;
@@ -1638,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;
@@ -1758,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;
@@ -1810,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;
 
@@ -1977,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;
@@ -2068,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);
 			}
@@ -2083,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;
@@ -2099,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;
@@ -2114,11 +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!
-
-			if (oldthing->scale != oldthing->old_scale) // Interpolate heights in reverse gravity when scaling mobjs
-				gz = interp.z + FixedMul(oldthing->height, FixedDiv(interp.scale, oldthing->scale)) - FixedMul(spr_topoffset, FixedMul(spriteyscale, this_scale));
-			else
-				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
@@ -2197,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);
@@ -2637,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;
 
@@ -2663,21 +2685,27 @@ static void R_SortVisSprites(vissprite_t* vsprsortedhead, UINT32 start, UINT32 e
 				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)
@@ -2729,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;
+		}
 	}
 }
 
@@ -3165,6 +3196,44 @@ 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
 static void R_ClipVisSprite(vissprite_t *spr, INT32 x1, INT32 x2, portal_t* portal)
@@ -3308,8 +3377,7 @@ static void R_ClipVisSprite(vissprite_t *spr, INT32 x1, INT32 x2, portal_t* port
 			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)
@@ -3334,6 +3402,14 @@ static void R_ClipVisSprite(vissprite_t *spr, INT32 x1, INT32 x2, portal_t* port
 			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)
@@ -3402,8 +3478,19 @@ void R_ClipSprites(drawseg_t* dsstart, portal_t* portal)
 	{
 		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;
@@ -3425,6 +3512,9 @@ void R_ClipSprites(drawseg_t* dsstart, portal_t* portal)
 		}
 
 		R_ClipVisSprite(spr, x1, x2, portal);
+
+		if ((spr->cut & SC_NOTVISIBLE) == 0)
+			numvisiblesprites++;
 	}
 }
 
diff --git a/src/r_things.h b/src/r_things.h
index e11005363573789e81b7c2401038a50a09855404..318234886bbbfa2b603f1883d50bbfea4c67d370 100644
--- a/src/r_things.h
+++ b/src/r_things.h
@@ -123,21 +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_BBOX       = 1<<13,
+	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;
 
@@ -219,7 +220,7 @@ 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);
 
diff --git a/src/screen.c b/src/screen.c
index 417e793bde540c62a9edf2b6a0b8073250ee7f73..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
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index 0b95cd0b2e0732b3f562d85d5e7454c7ec351b64..9b51cfb8094a1d6897e0685e9531ccd21df0fbdb 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -262,7 +262,6 @@
     <ClInclude Include="..\u_list.h" />
     <ClInclude Include="..\hu_stuff.h" />
     <ClInclude Include="..\info.h" />
-    <ClInclude Include="..\i_addrinfo.h" />
     <ClInclude Include="..\i_joy.h" />
     <ClInclude Include="..\i_net.h" />
     <ClInclude Include="..\i_sound.h" />
@@ -297,6 +296,21 @@
     <ClInclude Include="..\m_queue.h" />
     <ClInclude Include="..\m_random.h" />
     <ClInclude Include="..\m_swap.h" />
+    <ClInclude Include="..\netcode\client_connection.h" />
+    <ClInclude Include="..\netcode\commands.h" />
+    <ClInclude Include="..\netcode\d_clisrv.h" />
+    <ClInclude Include="..\netcode\d_net.h" />
+    <ClInclude Include="..\netcode\d_netcmd.h" />
+    <ClInclude Include="..\netcode\d_netfil.h" />
+    <ClInclude Include="..\netcode\gamestate.h" />
+    <ClInclude Include="..\netcode\i_addrinfo.h" />
+    <ClInclude Include="..\netcode\i_net.h" />
+    <ClInclude Include="..\netcode\i_tcp.h" />
+    <ClInclude Include="..\netcode\mserv.h" />
+    <ClInclude Include="..\netcode\net_command.h" />
+    <ClInclude Include="..\netcode\protocol.h" />
+    <ClInclude Include="..\netcode\server_connection.h" />
+    <ClInclude Include="..\netcode\tic_command.h" />
     <ClInclude Include="..\p5prof.h" />
     <ClInclude Include="..\p_haptic.h" />
     <ClInclude Include="..\p_local.h" />
@@ -329,6 +343,7 @@
     <ClInclude Include="..\r_textures.h" />
     <ClInclude Include="..\r_things.h" />
     <ClInclude Include="..\screen.h" />
+    <ClInclude Include="..\snake.h" />
     <ClInclude Include="..\sounds.h" />
     <ClInclude Include="..\st_stuff.h" />
     <ClInclude Include="..\s_sound.h" />
@@ -400,11 +415,7 @@
     <ClCompile Include="..\deh_soc.c" />
     <ClCompile Include="..\deh_lua.c" />
     <ClCompile Include="..\deh_tables.c" />
-    <ClCompile Include="..\d_clisrv.c" />
     <ClCompile Include="..\d_main.c" />
-    <ClCompile Include="..\d_net.c" />
-    <ClCompile Include="..\d_netcmd.c" />
-    <ClCompile Include="..\d_netfil.c" />
     <ClCompile Include="..\filesrch.c" />
     <ClCompile Include="..\f_finale.c" />
     <ClCompile Include="..\f_wipe.c" />
@@ -427,10 +438,6 @@
     <ClCompile Include="..\u_list.c" />
     <ClCompile Include="..\hu_stuff.c" />
     <ClCompile Include="..\info.c" />
-    <ClCompile Include="..\i_addrinfo.c">
-      <ExcludedFromBuild>true</ExcludedFromBuild>
-    </ClCompile>
-    <ClCompile Include="..\i_tcp.c" />
     <ClCompile Include="..\i_time.c" />
     <ClCompile Include="..\lua_baselib.c" />
     <ClCompile Include="..\lua_blockmaplib.c" />
@@ -451,8 +458,6 @@
     <ClCompile Include="..\lua_thinkerlib.c" />
     <ClCompile Include="..\lzf.c" />
     <ClCompile Include="..\md5.c" />
-    <ClCompile Include="..\mserv.c" />
-    <ClCompile Include="..\http-mserv.c" />
     <ClCompile Include="..\m_aatree.c" />
     <ClCompile Include="..\m_anigif.c" />
     <ClCompile Include="..\m_argv.c" />
@@ -466,6 +471,22 @@
     <ClCompile Include="..\m_perfstats.c" />
     <ClCompile Include="..\m_queue.c" />
     <ClCompile Include="..\m_random.c" />
+    <ClCompile Include="..\netcode\client_connection.c" />
+    <ClCompile Include="..\netcode\commands.c" />
+    <ClCompile Include="..\netcode\d_clisrv.c" />
+    <ClCompile Include="..\netcode\d_net.c" />
+    <ClCompile Include="..\netcode\d_netcmd.c" />
+    <ClCompile Include="..\netcode\d_netfil.c" />
+    <ClCompile Include="..\netcode\gamestate.c" />
+    <ClCompile Include="..\netcode\http-mserv.c" />
+    <ClCompile Include="..\netcode\i_addrinfo.c">
+      <ExcludedFromBuild>true</ExcludedFromBuild>
+    </ClCompile>
+    <ClCompile Include="..\netcode\i_tcp.c" />
+    <ClCompile Include="..\netcode\mserv.c" />
+    <ClCompile Include="..\netcode\net_command.c" />
+    <ClCompile Include="..\netcode\server_connection.c" />
+    <ClCompile Include="..\netcode\tic_command.c" />
     <ClCompile Include="..\p_ceilng.c" />
     <ClCompile Include="..\p_enemy.c" />
     <ClCompile Include="..\p_floor.c" />
@@ -510,6 +531,7 @@
     <ClCompile Include="..\r_textures.c" />
     <ClCompile Include="..\r_things.c" />
     <ClCompile Include="..\screen.c" />
+    <ClCompile Include="..\snake.c" />
     <ClCompile Include="..\sounds.c" />
     <ClCompile Include="..\string.c" />
     <ClCompile Include="..\st_stuff.c" />
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj.filters b/src/sdl/Srb2SDL-vc10.vcxproj.filters
index eb37ce9da6815ad539789e972a18b963e020cb71..96501b2160e587937f5513fa864f3a2f93a6d6ac 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj.filters
+++ b/src/sdl/Srb2SDL-vc10.vcxproj.filters
@@ -156,24 +156,12 @@
     <ClInclude Include="..\doomtype.h">
       <Filter>D_Doom</Filter>
     </ClInclude>
-    <ClInclude Include="..\d_clisrv.h">
-      <Filter>D_Doom</Filter>
-    </ClInclude>
     <ClInclude Include="..\d_event.h">
       <Filter>D_Doom</Filter>
     </ClInclude>
     <ClInclude Include="..\d_main.h">
       <Filter>D_Doom</Filter>
     </ClInclude>
-    <ClInclude Include="..\d_net.h">
-      <Filter>D_Doom</Filter>
-    </ClInclude>
-    <ClInclude Include="..\d_netcmd.h">
-      <Filter>D_Doom</Filter>
-    </ClInclude>
-    <ClInclude Include="..\d_netfil.h">
-      <Filter>D_Doom</Filter>
-    </ClInclude>
     <ClInclude Include="..\d_player.h">
       <Filter>D_Doom</Filter>
     </ClInclude>
@@ -279,9 +267,6 @@
     <ClInclude Include="..\filesrch.h">
       <Filter>I_Interface</Filter>
     </ClInclude>
-    <ClInclude Include="..\i_addrinfo.h">
-      <Filter>I_Interface</Filter>
-    </ClInclude>
     <ClInclude Include="..\i_joy.h">
       <Filter>I_Interface</Filter>
     </ClInclude>
@@ -306,12 +291,6 @@
     <ClInclude Include="..\keys.h">
       <Filter>I_Interface</Filter>
     </ClInclude>
-    <ClInclude Include="..\mserv.h">
-      <Filter>I_Interface</Filter>
-    </ClInclude>
-    <ClInclude Include="..\http-mserv.h">
-      <Filter>I_Interface</Filter>
-    </ClInclude>
     <ClInclude Include="..\lua_hook.h">
       <Filter>LUA</Filter>
     </ClInclude>
@@ -372,6 +351,54 @@
     <ClInclude Include="..\m_swap.h">
       <Filter>M_Misc</Filter>
     </ClInclude>
+    <ClInclude Include="..\netcode\client_connection.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\commands.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\d_clisrv.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\d_net.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\d_netcmd.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\d_netfil.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\gamestate.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\http-mserv.h">
+      <Filter>I_Interface</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\i_addrinfo.h">
+      <Filter>I_Interface</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\i_net.h">
+      <Filter>I_Interface</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\i_tcp.h">
+      <Filter>I_Interface</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\mserv.h">
+      <Filter>I_Interface</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\net_command.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\protocol.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\server_connection.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
+    <ClInclude Include="..\netcode\tic_command.h">
+      <Filter>D_Doom</Filter>
+    </ClInclude>
     <ClInclude Include="..\comptime.h">
       <Filter>O_Other</Filter>
     </ClInclude>
@@ -465,6 +492,9 @@
     <ClInclude Include="..\v_video.h">
       <Filter>R_Rend</Filter>
     </ClInclude>
+    <ClInclude Include="..\snake.h">
+      <Filter>M_Misc</Filter>
+    </ClInclude>
     <ClInclude Include="..\sounds.h">
       <Filter>S_Sounds</Filter>
     </ClInclude>
@@ -639,21 +669,9 @@
     <ClCompile Include="..\deh_tables.c">
       <Filter>D_Doom</Filter>
     </ClCompile>
-    <ClCompile Include="..\d_clisrv.c">
-      <Filter>D_Doom</Filter>
-    </ClCompile>
     <ClCompile Include="..\d_main.c">
       <Filter>D_Doom</Filter>
     </ClCompile>
-    <ClCompile Include="..\d_net.c">
-      <Filter>D_Doom</Filter>
-    </ClCompile>
-    <ClCompile Include="..\d_netcmd.c">
-      <Filter>D_Doom</Filter>
-    </ClCompile>
-    <ClCompile Include="..\d_netfil.c">
-      <Filter>D_Doom</Filter>
-    </ClCompile>
     <ClCompile Include="..\z_zone.c">
       <Filter>D_Doom</Filter>
     </ClCompile>
@@ -732,18 +750,9 @@
     <ClCompile Include="..\filesrch.c">
       <Filter>I_Interface</Filter>
     </ClCompile>
-    <ClCompile Include="..\i_addrinfo.c">
-      <Filter>I_Interface</Filter>
-    </ClCompile>
     <ClCompile Include="..\i_tcp.c">
       <Filter>I_Interface</Filter>
     </ClCompile>
-    <ClCompile Include="..\mserv.c">
-      <Filter>I_Interface</Filter>
-    </ClCompile>
-    <ClCompile Include="..\http-mserv.c">
-      <Filter>I_Interface</Filter>
-    </ClCompile>
     <ClCompile Include="..\lua_baselib.c">
       <Filter>LUA</Filter>
     </ClCompile>
@@ -831,6 +840,48 @@
     <ClCompile Include="..\m_random.c">
       <Filter>M_Misc</Filter>
     </ClCompile>
+    <ClCompile Include="..\netcode\client_connection.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\commands.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\d_clisrv.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\d_net.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\d_netcmd.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\d_netfil.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\gamestate.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\http-mserv.c">
+      <Filter>I_Interface</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\i_addrinfo.c">
+      <Filter>I_Interface</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\i_tcp.c">
+      <Filter>I_Interface</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\mserv.c">
+      <Filter>I_Interface</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\net_command.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\server_connection.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
+    <ClCompile Include="..\netcode\tic_command.c">
+      <Filter>D_Doom</Filter>
+    </ClCompile>
     <ClCompile Include="..\string.c">
       <Filter>M_Misc</Filter>
     </ClCompile>
@@ -954,6 +1005,9 @@
     <ClCompile Include="..\v_video.c">
       <Filter>R_Rend</Filter>
     </ClCompile>
+    <ClCompile Include="..\snake.c">
+      <Filter>M_Misc</Filter>
+    </ClCompile>
     <ClCompile Include="..\sounds.c">
       <Filter>S_Sounds</Filter>
     </ClCompile>
diff --git a/src/sdl/i_net.c b/src/sdl/i_net.c
index ee4a34c13dd8d4d10950d53d972d0cf873068943..515a855684e1e3d13dce6d35a785a2c7d07c9e93 100644
--- a/src/sdl/i_net.c
+++ b/src/sdl/i_net.c
@@ -21,16 +21,16 @@
 
 #include "../i_system.h"
 #include "../d_event.h"
-#include "../d_net.h"
+#include "../netcode/d_net.h"
 #include "../m_argv.h"
 
 #include "../doomstat.h"
 
-#include "../i_net.h"
+#include "../netcode/i_net.h"
 
 #include "../z_zone.h"
 
-#include "../i_tcp.h"
+#include "../netcode/i_tcp.h"
 
 #ifdef HAVE_SDL
 
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index 902194f4f332592f69f8b0f872bdf04f0159ba90..b05f40ee375376bd7c585c3952daad2446580fca 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -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
@@ -405,8 +407,10 @@ static void I_ReportSignal(int num, int coredumped)
 
 	SDL_ShowMessageBox(&messageboxdata, &buttonid);
 
+#if SDL_VERSION_ATLEAST(2,0,14)
 	if (buttonid == 1)
 		SDL_OpenURL("https://www.srb2.org/discord");
+#endif
 }
 
 #ifndef NEWSIGNALHANDLER
@@ -2262,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];
 
@@ -2384,9 +2388,7 @@ void I_Quit(void)
 	SDLforceUngrabMouse();
 	quiting = SDL_FALSE;
 	M_SaveConfig(NULL); //save game config, cvars..
-#ifndef NONET
 	D_SaveBan(); // save the ban list
-#endif
 	G_SaveGameData(clientGamedata); // Tails 12-08-2002
 	//added:16-02-98: when recording a demo, should exit using 'q' key,
 	//        but sometimes we forget and use 'F10'.. so save here too.
@@ -2501,9 +2503,7 @@ void I_Error(const char *error, ...)
 	// ---
 
 	M_SaveConfig(NULL); // save game config, cvars..
-#ifndef NONET
 	D_SaveBan(); // save the ban list
-#endif
 	G_SaveGameData(clientGamedata); // Tails 12-08-2002
 
 	// Shutdown. Here might be other errors.
@@ -3035,11 +3035,11 @@ size_t I_GetFreeMem(size_t *total)
 #ifdef FREEBSD
 	u_int v_free_count, v_page_size, v_page_count;
 	size_t size = sizeof(v_free_count);
-	sysctlbyname("vm.stat.vm.v_free_count", &v_free_count, &size, NULL, 0);
-	size_t size = sizeof(v_page_size);
-	sysctlbyname("vm.stat.vm.v_page_size", &v_page_size, &size, NULL, 0);
-	size_t size = sizeof(v_page_count);
-	sysctlbyname("vm.stat.vm.v_page_count", &v_page_count, &size, NULL, 0);
+	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 = v_page_count * v_page_size;
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/snake.c b/src/snake.c
new file mode 100644
index 0000000000000000000000000000000000000000..2349d5fdbfb6d66af7d4bc55df03b553052153c8
--- /dev/null
+++ b/src/snake.c
@@ -0,0 +1,593 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2023-2023 by Louis-Antoine de Moulins de Rochefort.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  snake.c
+/// \brief Snake minigame for the download screen.
+
+#include "snake.h"
+#include "g_input.h"
+#include "g_game.h"
+#include "i_joy.h"
+#include "m_random.h"
+#include "s_sound.h"
+#include "screen.h"
+#include "v_video.h"
+#include "w_wad.h"
+#include "z_zone.h"
+
+#define SPEED 5
+
+#define NUM_BLOCKS_X 20
+#define NUM_BLOCKS_Y 10
+#define BLOCK_SIZE 12
+#define BORDER_SIZE 12
+
+#define MAP_WIDTH  (NUM_BLOCKS_X * BLOCK_SIZE)
+#define MAP_HEIGHT (NUM_BLOCKS_Y * BLOCK_SIZE)
+
+#define LEFT_X ((BASEVIDWIDTH - MAP_WIDTH) / 2 - BORDER_SIZE)
+#define RIGHT_X (LEFT_X + MAP_WIDTH + BORDER_SIZE * 2 - 1)
+#define BOTTOM_Y (BASEVIDHEIGHT - 48)
+#define TOP_Y (BOTTOM_Y - MAP_HEIGHT - BORDER_SIZE * 2 + 1)
+
+enum bonustype_s {
+	BONUS_NONE = 0,
+	BONUS_SLOW,
+	BONUS_FAST,
+	BONUS_GHOST,
+	BONUS_NUKE,
+	BONUS_SCISSORS,
+	BONUS_REVERSE,
+	BONUS_EGGMAN,
+	NUM_BONUSES,
+};
+
+typedef struct snake_s
+{
+	boolean paused;
+	boolean pausepressed;
+	tic_t time;
+	tic_t nextupdate;
+	boolean gameover;
+	UINT8 background;
+
+	UINT16 snakelength;
+	enum bonustype_s snakebonus;
+	tic_t snakebonustime;
+	UINT8 snakex[NUM_BLOCKS_X * NUM_BLOCKS_Y];
+	UINT8 snakey[NUM_BLOCKS_X * NUM_BLOCKS_Y];
+	UINT8 snakedir[NUM_BLOCKS_X * NUM_BLOCKS_Y];
+
+	UINT8 applex;
+	UINT8 appley;
+
+	enum bonustype_s bonustype;
+	UINT8 bonusx;
+	UINT8 bonusy;
+
+	event_t *joyevents[MAXEVENTS];
+	UINT16 joyeventcount;
+} snake_t;
+
+static const char *bonuspatches[] = {
+	NULL,
+	"DL_SLOW",
+	"TVSSC0",
+	"TVIVC0",
+	"TVARC0",
+	"DL_SCISSORS",
+	"TVRCC0",
+	"TVEGC0",
+};
+
+static const char *backgrounds[] = {
+	"RVPUMICF",
+	"FRSTRCKF",
+	"TAR",
+	"MMFLRB4",
+	"RVDARKF1",
+	"RVZWALF1",
+	"RVZWALF4",
+	"RVZWALF5",
+	"RVZGRS02",
+	"RVZGRS04",
+};
+
+static void Initialise(snake_t *snake)
+{
+	snake->paused = false;
+	snake->pausepressed = false;
+	snake->time = 0;
+	snake->nextupdate = SPEED;
+	snake->gameover = false;
+	snake->background = M_RandomKey(sizeof(backgrounds) / sizeof(*backgrounds));
+
+	snake->snakelength = 1;
+	snake->snakebonus = BONUS_NONE;
+	snake->snakex[0] = M_RandomKey(NUM_BLOCKS_X);
+	snake->snakey[0] = M_RandomKey(NUM_BLOCKS_Y);
+	snake->snakedir[0] = 0;
+	snake->snakedir[1] = 0;
+
+	snake->applex = M_RandomKey(NUM_BLOCKS_X);
+	snake->appley = M_RandomKey(NUM_BLOCKS_Y);
+
+	snake->bonustype = BONUS_NONE;
+
+	snake->joyeventcount = 0;
+}
+
+static UINT8 GetOppositeDir(UINT8 dir)
+{
+	if (dir == 1 || dir == 3)
+		return dir + 1;
+	else if (dir == 2 || dir == 4)
+		return dir - 1;
+	else
+		return 12 + 5 - dir;
+}
+
+static void FindFreeSlot(snake_t *snake, UINT8 *freex, UINT8 *freey, UINT8 headx, UINT8 heady)
+{
+	UINT8 x, y;
+	UINT16 i;
+
+	do
+	{
+		x = M_RandomKey(NUM_BLOCKS_X);
+		y = M_RandomKey(NUM_BLOCKS_Y);
+
+		for (i = 0; i < snake->snakelength; i++)
+			if (x == snake->snakex[i] && y == snake->snakey[i])
+				break;
+	} while (i < snake->snakelength || (x == headx && y == heady)
+		|| (x == snake->applex && y == snake->appley)
+		|| (snake->bonustype != BONUS_NONE && x == snake->bonusx && y == snake->bonusy));
+
+	*freex = x;
+	*freey = y;
+}
+
+void Snake_Allocate(void **opaque)
+{
+	if (*opaque)
+		Snake_Free(opaque);
+	*opaque = malloc(sizeof(snake_t));
+	Initialise(*opaque);
+}
+
+void Snake_Update(void *opaque)
+{
+	UINT8 x, y;
+	UINT8 oldx, oldy;
+	UINT16 i;
+	UINT16 joystate = 0;
+	static INT32 pjoyx = 0, pjoyy = 0;
+
+	snake_t *snake = opaque;
+
+	// Handle retry
+	if (snake->gameover && (PLAYER1INPUTDOWN(GC_JUMP) || gamekeydown[KEY_ENTER]))
+	{
+		Initialise(snake);
+		snake->pausepressed = true; // Avoid accidental pause on respawn
+	}
+
+	// Handle pause
+	if (PLAYER1INPUTDOWN(GC_PAUSE) || gamekeydown[KEY_ENTER])
+	{
+		if (!snake->pausepressed)
+			snake->paused = !snake->paused;
+		snake->pausepressed = true;
+	}
+	else
+		snake->pausepressed = false;
+
+	if (snake->paused)
+		return;
+
+	snake->time++;
+
+	x = snake->snakex[0];
+	y = snake->snakey[0];
+	oldx = snake->snakex[1];
+	oldy = snake->snakey[1];
+
+	// Process the input events in here dear lord
+	for (UINT16 j = 0; j < snake->joyeventcount; j++)
+	{
+		event_t *ev = snake->joyevents[j];
+		const INT32 jdeadzone = (JOYAXISRANGE * cv_digitaldeadzone.value) / FRACUNIT;
+		if (ev->y != INT32_MAX)
+		{
+			if (Joystick.bGamepadStyle || abs(ev->y) > jdeadzone)
+			{
+				if (ev->y < 0 && pjoyy >= 0)
+					joystate = 1;
+				else if (ev->y > 0 && pjoyy <= 0)
+					joystate = 2;
+				pjoyy = ev->y;
+			}
+			else
+				pjoyy = 0;
+		}
+
+		if (ev->x != INT32_MAX)
+		{
+			if (Joystick.bGamepadStyle || abs(ev->x) > jdeadzone)
+			{
+				if (ev->x < 0 && pjoyx >= 0)
+					joystate = 3;
+				else if (ev->x > 0 && pjoyx <= 0)
+					joystate = 4;
+				pjoyx = ev->x;
+			}
+			else
+				pjoyx = 0;
+		}
+	}
+	snake->joyeventcount = 0;
+
+	// Update direction
+	if (PLAYER1INPUTDOWN(GC_STRAFELEFT) || gamekeydown[KEY_LEFTARROW] || joystate == 3)
+	{
+		if (snake->snakelength < 2 || x <= oldx)
+			snake->snakedir[0] = 1;
+	}
+	else if (PLAYER1INPUTDOWN(GC_STRAFERIGHT) || gamekeydown[KEY_RIGHTARROW] || joystate == 4)
+	{
+		if (snake->snakelength < 2 || x >= oldx)
+			snake->snakedir[0] = 2;
+	}
+	else if (PLAYER1INPUTDOWN(GC_FORWARD) || gamekeydown[KEY_UPARROW] || joystate == 1)
+	{
+		if (snake->snakelength < 2 || y <= oldy)
+			snake->snakedir[0] = 3;
+	}
+	else if (PLAYER1INPUTDOWN(GC_BACKWARD) || gamekeydown[KEY_DOWNARROW] || joystate == 2)
+	{
+		if (snake->snakelength < 2 || y >= oldy)
+			snake->snakedir[0] = 4;
+	}
+
+	if (snake->snakebonustime)
+	{
+		snake->snakebonustime--;
+		if (!snake->snakebonustime)
+			snake->snakebonus = BONUS_NONE;
+	}
+
+	snake->nextupdate--;
+	if (snake->nextupdate)
+		return;
+	if (snake->snakebonus == BONUS_SLOW)
+		snake->nextupdate = SPEED * 2;
+	else if (snake->snakebonus == BONUS_FAST)
+		snake->nextupdate = SPEED * 2 / 3;
+	else
+		snake->nextupdate = SPEED;
+
+	if (snake->gameover)
+		return;
+
+	// Find new position
+	switch (snake->snakedir[0])
+	{
+		case 1:
+			if (x > 0)
+				x--;
+			else
+				snake->gameover = true;
+			break;
+		case 2:
+			if (x < NUM_BLOCKS_X - 1)
+				x++;
+			else
+				snake->gameover = true;
+			break;
+		case 3:
+			if (y > 0)
+				y--;
+			else
+				snake->gameover = true;
+			break;
+		case 4:
+			if (y < NUM_BLOCKS_Y - 1)
+				y++;
+			else
+				snake->gameover = true;
+			break;
+	}
+
+	// Check collision with snake
+	if (snake->snakebonus != BONUS_GHOST)
+		for (i = 1; i < snake->snakelength - 1; i++)
+			if (x == snake->snakex[i] && y == snake->snakey[i])
+			{
+				if (snake->snakebonus == BONUS_SCISSORS)
+				{
+					snake->snakebonus = BONUS_NONE;
+					snake->snakelength = i;
+					S_StartSound(NULL, sfx_adderr);
+				}
+				else
+					snake->gameover = true;
+			}
+
+	if (snake->gameover)
+	{
+		S_StartSound(NULL, sfx_lose);
+		return;
+	}
+
+	// Check collision with apple
+	if (x == snake->applex && y == snake->appley)
+	{
+		if (snake->snakelength + 3 < NUM_BLOCKS_X * NUM_BLOCKS_Y)
+		{
+			snake->snakelength++;
+			snake->snakex  [snake->snakelength - 1] = snake->snakex  [snake->snakelength - 2];
+			snake->snakey  [snake->snakelength - 1] = snake->snakey  [snake->snakelength - 2];
+			snake->snakedir[snake->snakelength - 1] = snake->snakedir[snake->snakelength - 2];
+		}
+
+		// Spawn new apple
+		FindFreeSlot(snake, &snake->applex, &snake->appley, x, y);
+
+		// Spawn new bonus
+		if (!(snake->snakelength % 5))
+		{
+			do
+			{
+				snake->bonustype = M_RandomKey(NUM_BONUSES - 1) + 1;
+			} while (snake->snakelength > NUM_BLOCKS_X * NUM_BLOCKS_Y * 3 / 4
+				&& (snake->bonustype == BONUS_EGGMAN || snake->bonustype == BONUS_FAST || snake->bonustype == BONUS_REVERSE));
+
+			FindFreeSlot(snake, &snake->bonusx, &snake->bonusy, x, y);
+		}
+
+		S_StartSound(NULL, sfx_s3k6b);
+	}
+
+	if (snake->snakelength > 1 && snake->snakedir[0])
+	{
+		UINT8 dir = snake->snakedir[0];
+
+		oldx = snake->snakex[1];
+		oldy = snake->snakey[1];
+
+		// Move
+		for (i = snake->snakelength - 1; i > 0; i--)
+		{
+			snake->snakex[i] = snake->snakex[i - 1];
+			snake->snakey[i] = snake->snakey[i - 1];
+			snake->snakedir[i] = snake->snakedir[i - 1];
+		}
+
+		// Handle corners
+		if      (x < oldx && dir == 3)
+			dir = 5;
+		else if (x > oldx && dir == 3)
+			dir = 6;
+		else if (x < oldx && dir == 4)
+			dir = 7;
+		else if (x > oldx && dir == 4)
+			dir = 8;
+		else if (y < oldy && dir == 1)
+			dir = 9;
+		else if (y < oldy && dir == 2)
+			dir = 10;
+		else if (y > oldy && dir == 1)
+			dir = 11;
+		else if (y > oldy && dir == 2)
+			dir = 12;
+		snake->snakedir[1] = dir;
+	}
+
+	snake->snakex[0] = x;
+	snake->snakey[0] = y;
+
+	// Check collision with bonus
+	if (snake->bonustype != BONUS_NONE && x == snake->bonusx && y == snake->bonusy)
+	{
+		S_StartSound(NULL, sfx_ncchip);
+
+		switch (snake->bonustype)
+		{
+		case BONUS_SLOW:
+			snake->snakebonus = BONUS_SLOW;
+			snake->snakebonustime = 20 * TICRATE;
+			break;
+		case BONUS_FAST:
+			snake->snakebonus = BONUS_FAST;
+			snake->snakebonustime = 20 * TICRATE;
+			break;
+		case BONUS_GHOST:
+			snake->snakebonus = BONUS_GHOST;
+			snake->snakebonustime = 10 * TICRATE;
+			break;
+		case BONUS_NUKE:
+			for (i = 0; i < snake->snakelength; i++)
+			{
+				snake->snakex  [i] = snake->snakex  [0];
+				snake->snakey  [i] = snake->snakey  [0];
+				snake->snakedir[i] = snake->snakedir[0];
+			}
+
+			S_StartSound(NULL, sfx_bkpoof);
+			break;
+		case BONUS_SCISSORS:
+			snake->snakebonus = BONUS_SCISSORS;
+			snake->snakebonustime = 60 * TICRATE;
+			break;
+		case BONUS_REVERSE:
+			for (i = 0; i < (snake->snakelength + 1) / 2; i++)
+			{
+				UINT16 i2 = snake->snakelength - 1 - i;
+				UINT8 tmpx   = snake->snakex  [i];
+				UINT8 tmpy   = snake->snakey  [i];
+				UINT8 tmpdir = snake->snakedir[i];
+
+				// Swap first segment with last segment
+				snake->snakex  [i] = snake->snakex  [i2];
+				snake->snakey  [i] = snake->snakey  [i2];
+				snake->snakedir[i] = GetOppositeDir(snake->snakedir[i2]);
+				snake->snakex  [i2] = tmpx;
+				snake->snakey  [i2] = tmpy;
+				snake->snakedir[i2] = GetOppositeDir(tmpdir);
+			}
+
+			snake->snakedir[0] = 0;
+
+			S_StartSound(NULL, sfx_gravch);
+			break;
+		default:
+			if (snake->snakebonus != BONUS_GHOST)
+			{
+				snake->gameover = true;
+				S_StartSound(NULL, sfx_lose);
+			}
+		}
+
+		snake->bonustype = BONUS_NONE;
+	}
+}
+
+void Snake_Draw(void *opaque)
+{
+	INT16 i;
+
+	snake_t *snake = opaque;
+
+	// Background
+	V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
+
+	V_DrawFlatFill(
+		LEFT_X + BORDER_SIZE,
+		TOP_Y  + BORDER_SIZE,
+		MAP_WIDTH,
+		MAP_HEIGHT,
+		W_GetNumForName(backgrounds[snake->background])
+	);
+
+	// Borders
+	V_DrawFill(LEFT_X, TOP_Y, BORDER_SIZE + MAP_WIDTH, BORDER_SIZE, 242); // Top
+	V_DrawFill(LEFT_X + BORDER_SIZE + MAP_WIDTH, TOP_Y, BORDER_SIZE, BORDER_SIZE + MAP_HEIGHT, 242); // Right
+	V_DrawFill(LEFT_X + BORDER_SIZE, TOP_Y + BORDER_SIZE + MAP_HEIGHT, BORDER_SIZE + MAP_WIDTH, BORDER_SIZE, 242); // Bottom
+	V_DrawFill(LEFT_X, TOP_Y + BORDER_SIZE, BORDER_SIZE, BORDER_SIZE + MAP_HEIGHT, 242); // Left
+
+	// Apple
+	V_DrawFixedPatch(
+		(LEFT_X + BORDER_SIZE + snake->applex * BLOCK_SIZE + BLOCK_SIZE / 2) * FRACUNIT,
+		(TOP_Y  + BORDER_SIZE + snake->appley * BLOCK_SIZE + BLOCK_SIZE / 2) * FRACUNIT,
+		FRACUNIT / 4,
+		0,
+		W_CachePatchLongName("DL_APPLE", PU_HUDGFX),
+		NULL
+	);
+
+	// Bonus
+	if (snake->bonustype != BONUS_NONE)
+		V_DrawFixedPatch(
+			(LEFT_X + BORDER_SIZE + snake->bonusx * BLOCK_SIZE + BLOCK_SIZE / 2    ) * FRACUNIT,
+			(TOP_Y  + BORDER_SIZE + snake->bonusy * BLOCK_SIZE + BLOCK_SIZE / 2 + 4) * FRACUNIT,
+			FRACUNIT / 2,
+			0,
+			W_CachePatchLongName(bonuspatches[snake->bonustype], PU_HUDGFX),
+			NULL
+		);
+
+	// Snake
+	if (!snake->gameover || snake->time % 8 < 8 / 2) // Blink if game over
+	{
+		for (i = snake->snakelength - 1; i >= 0; i--)
+		{
+			const char *patchname;
+			UINT8 dir = snake->snakedir[i];
+
+			if (i == 0) // Head
+			{
+				switch (dir)
+				{
+					case  1: patchname = "DL_SNAKEHEAD_L"; break;
+					case  2: patchname = "DL_SNAKEHEAD_R"; break;
+					case  3: patchname = "DL_SNAKEHEAD_T"; break;
+					case  4: patchname = "DL_SNAKEHEAD_B"; break;
+					default: patchname = "DL_SNAKEHEAD_M";
+				}
+			}
+			else // Body
+			{
+				switch (dir)
+				{
+					case  1: patchname = "DL_SNAKEBODY_L"; break;
+					case  2: patchname = "DL_SNAKEBODY_R"; break;
+					case  3: patchname = "DL_SNAKEBODY_T"; break;
+					case  4: patchname = "DL_SNAKEBODY_B"; break;
+					case  5: patchname = "DL_SNAKEBODY_LT"; break;
+					case  6: patchname = "DL_SNAKEBODY_RT"; break;
+					case  7: patchname = "DL_SNAKEBODY_LB"; break;
+					case  8: patchname = "DL_SNAKEBODY_RB"; break;
+					case  9: patchname = "DL_SNAKEBODY_TL"; break;
+					case 10: patchname = "DL_SNAKEBODY_TR"; break;
+					case 11: patchname = "DL_SNAKEBODY_BL"; break;
+					case 12: patchname = "DL_SNAKEBODY_BR"; break;
+					default: patchname = "DL_SNAKEBODY_B";
+				}
+			}
+
+			V_DrawFixedPatch(
+				(LEFT_X + BORDER_SIZE + snake->snakex[i] * BLOCK_SIZE + BLOCK_SIZE / 2) * FRACUNIT,
+				(TOP_Y  + BORDER_SIZE + snake->snakey[i] * BLOCK_SIZE + BLOCK_SIZE / 2) * FRACUNIT,
+				i == 0 && dir == 0 ? FRACUNIT / 5 : FRACUNIT / 2,
+				snake->snakebonus == BONUS_GHOST ? V_TRANSLUCENT : 0,
+				W_CachePatchLongName(patchname, PU_HUDGFX),
+				NULL
+			);
+		}
+	}
+
+	// Length
+	V_DrawString(RIGHT_X + 4, TOP_Y, V_MONOSPACE, va("%u", snake->snakelength));
+
+	// Bonus
+	if (snake->snakebonus != BONUS_NONE
+	&& (snake->snakebonustime >= 3 * TICRATE || snake->time % 4 < 4 / 2))
+		V_DrawFixedPatch(
+			(RIGHT_X + 10) * FRACUNIT,
+			(TOP_Y + 24) * FRACUNIT,
+			FRACUNIT / 2,
+			0,
+			W_CachePatchLongName(bonuspatches[snake->snakebonus], PU_HUDGFX),
+			NULL
+		);
+}
+
+void Snake_Free(void **opaque)
+{
+	if (*opaque)
+	{
+		free(*opaque);
+		*opaque = NULL;
+	}
+}
+
+// I'm screaming the hack is clean - ashi
+boolean Snake_JoyGrabber(void *opaque, event_t *ev)
+{
+	snake_t *snake = opaque;
+
+	if (ev->type == ev_joystick  && ev->key == 0)
+	{
+		snake->joyevents[snake->joyeventcount] = ev;
+		snake->joyeventcount++;
+		return true;
+	}
+	else
+		return false;
+}
diff --git a/src/snake.h b/src/snake.h
new file mode 100644
index 0000000000000000000000000000000000000000..6bca338e9643d91f567a1b2f701013bb7cfb29c6
--- /dev/null
+++ b/src/snake.h
@@ -0,0 +1,23 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 2023-2023 by Louis-Antoine de Moulins de Rochefort.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  snake.h
+/// \brief Snake minigame for the download screen.
+
+#ifndef __SNAKE__
+#define __SNAKE__
+
+#include "d_event.h"
+
+void Snake_Allocate(void **opaque);
+void Snake_Update(void *opaque);
+void Snake_Draw(void *opaque);
+void Snake_Free(void **opaque);
+boolean Snake_JoyGrabber(void *opaque, event_t *ev);
+
+#endif
diff --git a/src/st_stuff.c b/src/st_stuff.c
index b9f0c6bb93e1ab4d1b9e35a958670571883086e4..de19c468fcdcc4cc09ea3fb06eb34002b1fdf143 100644
--- a/src/st_stuff.c
+++ b/src/st_stuff.c
@@ -827,6 +827,8 @@ static void ST_drawLivesArea(void)
 	V_DrawSmallScaledPatch(hudinfo[HUD_LIVES].x, hudinfo[HUD_LIVES].y,
 		hudinfo[HUD_LIVES].f|V_PERPLAYER|V_HUDTRANS, livesback);
 
+	UINT16 facecolor = P_GetPlayerColor(stplyr);
+
 	// face
 	if (stplyr->spectator)
 	{
@@ -856,10 +858,10 @@ static void ST_drawLivesArea(void)
 			}
 		}
 	}
-	else if (stplyr->skincolor)
+	else if (facecolor)
 	{
 		// skincolor face
-		UINT8 *colormap = R_GetTranslationColormap(stplyr->skin, stplyr->skincolor, GTC_CACHE);
+		UINT8 *colormap = R_GetTranslationColormap(stplyr->skin, facecolor, GTC_CACHE);
 		V_DrawSmallMappedPatch(hudinfo[HUD_LIVES].x, hudinfo[HUD_LIVES].y,
 			hudinfo[HUD_LIVES].f|V_PERPLAYER|V_HUDTRANS, faceprefix[stplyr->skin], colormap);
 	}
@@ -1030,7 +1032,8 @@ static void ST_drawLivesArea(void)
 
 static void ST_drawInput(void)
 {
-	const INT32 accent = V_SNAPTOLEFT|V_SNAPTOBOTTOM|(stplyr->skincolor ? skincolors[stplyr->skincolor].ramp[4] : 0);
+	UINT16 color = P_GetPlayerColor(stplyr);
+	const INT32 accent = V_SNAPTOLEFT|V_SNAPTOBOTTOM|(color ? skincolors[color].ramp[4] : 0);
 	INT32 col;
 	UINT8 offs;
 
diff --git a/src/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/w_wad.c b/src/w_wad.c
index c8880f6934f0f04bbe892da599e90f487339b242..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"
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 8bec2b30f4e9314eb529defe4578daac5d0b697b..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"